Added some tests, fixed mistakes tests found

This commit is contained in:
2024-09-01 08:53:50 -04:00
parent aa1d4a9371
commit 84a105639c
6 changed files with 526 additions and 49 deletions

View File

@ -1,3 +1,11 @@
[profile.release]
opt-level = "s"
[profile.dev]
debug = true # Symbols are nice and they don't increase the size on Flash
opt-level = "z"
[package]
name = "gem-remotes-lib"
version = "0.1.0"
@ -6,6 +14,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0.86"
async-channel = "2.3.1"
async-io = "2.3.4"
log = "0.4.22"
strum = "0.26.3"
strum_macros = "0.26.4"

View File

@ -15,9 +15,9 @@ pub enum Commands {
PicRecvUp {data: Button},
PicRecvDown {data: Button},
PicRecvStop {data: Button},
PicRecvLimitUp {data: Button}, // 0 for not hit, 1 for hit
PicRecvLimitDown {data: Button}, // 0 for not hit, 1 for hit
PicRecvAutoMode {data: Button}, // 0 for disallowed, 1 for allowed
PicRecvLimitUp {data: Button},
PicRecvLimitDown {data: Button},
PicRecvAutoMode {data: Toggle}, // 0 for disallowed, 1 for allowed
// TODO: real hardware buttons - consider re-sending occasionally when pressed, so that transitions like holding up -> stopping -> holding down -> stopped -> (should go down but gets no new notice) work.
@ -44,6 +44,8 @@ pub enum Commands {
NotifyMotorStop {data: Button},
EraseBleBonds,
TestingExit, // Used only in unit/integration tests. Do not subscribe for.
}
#[derive(Copy, Clone, Debug)]
@ -52,6 +54,13 @@ pub enum Button {
Pressed =1
}
// Distinguish toggles(like auto) which is on/off from buttons (which are pressed/released)
#[derive(Copy, Clone, Debug)]
pub enum Toggle {
Inactive = 0,
Active =1
}
pub type CmdType = std::mem::Discriminant<Commands>;
/// Consider commands equal if they have the same command type, but different values

View File

@ -27,19 +27,16 @@ pub use dispatch::{
// Test Code for whole module
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
// // Test Code for whole module // //////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use super::*;
//use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
fn empty_test() {
assert_eq!(1, 1);
}
}
// TODO: Check whole module for panics (unwrap, expect, panic) and ensure that it is appropriate

View File

@ -2,9 +2,9 @@
/// command messages.
use log::*; //{trace, debug, info, warn, error}
use anyhow::Result;
use anyhow::{anyhow, Result};
use async_channel::{Receiver, Sender};
use crate::commands::{Commands, Button};
use crate::commands::{Commands, Button, Toggle};
use crate::dispatch::{Dispatch, DispatchRecvQ, DispatchSendQ};
// The main internal state of the controller, representing the current control method of the motors.
@ -18,8 +18,7 @@ enum ControllerStates {
AutoDown,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MotorCommands {
StartUp,
StartDown,
@ -76,7 +75,7 @@ impl Controller {
Commands::BluetoothStop{data: Button::Released},
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvLimitDown{data: Button::Released},
Commands::PicRecvAutoMode{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Inactive},
Commands::StopTimerExpired,
Commands::ButtonTimerExpired,
];
@ -155,34 +154,35 @@ impl Controller {
}
/// Determines the state the controller should be in based on the command received.
async fn handle_cmd(&mut self, cmd: &Commands) -> ControllerStates {
async fn handle_cmd(&mut self, cmd: &Commands) -> Result<ControllerStates> {
let mut rc = self.state.clone(); // Don't transition by default.
match cmd {
Commands::PicRecvUp { data } | Commands::BluetoothUp { data }=> {
match self.state {
ControllerStates::Stopped => {return self.change_state_if_pressed(data, self.remote_up_or_auto_up())}
ControllerStates::Stopped => {rc = self.change_state_if_pressed(data, self.remote_up_or_auto_up())}
ControllerStates::Stopping => {}
ControllerStates::GoingUp => {
self.send.send(Commands::ButtonTimerRestart).await.expect("Failed to necessary timer");
return self.change_state_if_released(data, ControllerStates::Stopping)
rc = self.change_state_if_released(data, ControllerStates::Stopping)
}
ControllerStates::AutoUp => {} // Don't stop auto on button release
ControllerStates::GoingDown |
ControllerStates::AutoDown => {
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc= self.change_state_if_pressed(data, ControllerStates::Stopping)
}
}
}
Commands::PicRecvDown { data } | Commands::BluetoothDown { data } => {
match self.state {
ControllerStates::Stopped => {return self.change_state_if_pressed(data, self.remote_down_or_auto_down())}
ControllerStates::Stopped => {rc = self.change_state_if_pressed(data, self.remote_down_or_auto_down())}
ControllerStates::Stopping => {}
ControllerStates::GoingUp |
ControllerStates::AutoUp => {
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc = self.change_state_if_pressed(data, ControllerStates::Stopping)
}
ControllerStates::GoingDown => {
self.send.send(Commands::ButtonTimerRestart).await.expect("Failed to necessary timer");
return self.change_state_if_released(data, ControllerStates::Stopping)
rc = self.change_state_if_released(data, ControllerStates::Stopping)
}
ControllerStates::AutoDown => {}
}
@ -195,7 +195,7 @@ impl Controller {
ControllerStates::AutoUp |
ControllerStates::GoingDown |
ControllerStates::AutoDown => {
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc = self.change_state_if_pressed(data, ControllerStates::Stopping)
}
}
}
@ -207,13 +207,13 @@ impl Controller {
ControllerStates::GoingUp |
ControllerStates::AutoUp=> {
released_warning(data, "Limit switches may be installed incorrectly!");
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc = self.change_state_if_pressed(data, ControllerStates::Stopping)
}
ControllerStates::GoingDown |
ControllerStates::AutoDown=> {
pressed_warning(data, "Limit switches may be installed incorrectly!");
// Stop out of an abundance of caution. We should not get a limit press, even if it's the wrong one.
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc = self.change_state_if_pressed(data, ControllerStates::Stopping)
}
}
}
@ -226,20 +226,30 @@ impl Controller {
ControllerStates::AutoUp => {
pressed_warning(data, "Limit switches may be installed incorrectly!");
// Stop out of an abundance of caution. We should not get a limit press, even if it's the wrong one.
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc = self.change_state_if_pressed(data, ControllerStates::Stopping)
}
ControllerStates::GoingDown |
ControllerStates::AutoDown => {
released_warning(data, "Limit switches may be installed incorrectly!");
return self.change_state_if_pressed(data, ControllerStates::Stopping)
rc = self.change_state_if_pressed(data, ControllerStates::Stopping)
}
}
}
Commands::PicRecvAutoMode { data } => {self.set_auto(data);}
Commands::PicRecvAutoMode { data } => {
self.set_auto(data);
match self.state {
ControllerStates::Stopped => {}
ControllerStates::Stopping => {}
ControllerStates::GoingUp => {}
ControllerStates::AutoUp => {rc = ControllerStates::GoingUp}
ControllerStates::GoingDown => {}
ControllerStates::AutoDown => {rc = ControllerStates::GoingDown}
}
}
Commands::StopTimerExpired => {
match self.state {
ControllerStates::Stopped => {}
ControllerStates::Stopping => {return ControllerStates::Stopped}
ControllerStates::Stopping => {rc = ControllerStates::Stopped}
ControllerStates::GoingUp |
ControllerStates::AutoUp |
ControllerStates::GoingDown |
@ -252,9 +262,9 @@ impl Controller {
match self.state {
ControllerStates::Stopped => {}
ControllerStates::Stopping => {}
ControllerStates::GoingUp => {return ControllerStates::Stopping}
ControllerStates::GoingUp => {rc = ControllerStates::Stopping}
ControllerStates::AutoUp => {}
ControllerStates::GoingDown => {return ControllerStates::Stopping}
ControllerStates::GoingDown => {rc = ControllerStates::Stopping}
ControllerStates::AutoDown => {}
}
}
@ -277,8 +287,12 @@ impl Controller {
Commands::BluetoothName { data } => {
warn!("Unexpected command received by motor controller {:?}", data) // TODO: internal "us" error.
}
Commands::TestingExit => {
return Err(anyhow!("Exiting due to testing"))
}
}
self.state.clone() // Don't transition by default
//self.state.clone() // Don't transition by default
Ok(rc)
}
async fn transition_state(&mut self, old_s: &ControllerStates, new_s: &ControllerStates) -> Result<()> {
@ -298,10 +312,17 @@ impl Controller {
loop {
let cmd = self.recv.recv().await.expect("Motor controller command queue unexpectedly failed");
trace!("Got command {:?}",cmd);
let new_s = self.handle_cmd(&cmd).await;
trace!("State current {:?} new {:?}", self.state, new_s);
self.transition_state(&self.state.clone(), &new_s).await.expect("Unexpected state change failure in motor controller");
match self.handle_cmd(&cmd).await {
Ok(new_s) => {
trace!("State current {:?} new {:?}", self.state, new_s);
self.transition_state(&self.state.clone(), &new_s)
.await
.expect("Unexpected state change failure in motor controller");
}
Err(_) => {break;}
}
}
Err(anyhow!("Unexpectedly exited loop"))
}
fn remote_up_or_auto_up(&self) -> ControllerStates {
@ -356,12 +377,12 @@ impl Controller {
}
}
fn set_auto(&mut self, data: &Button) {
fn set_auto(&mut self, data: &Toggle) {
match data {
Button::Released => {
Toggle::Inactive => {
self.auto_mode = AutoMode::Disallowed;
}
Button::Pressed => {
Toggle::Active => {
if self.limit_state == LimitState::BothHit {
warn!("Limit switches not detected. Aborting auto mode.");
} else {
@ -371,6 +392,7 @@ impl Controller {
}
}
// Adjusts the current limit state, based on its present state and an incoming button press or release.
fn adjust_limit(&mut self, limit: LimitState, pressed: &Button) {
match pressed {
Button::Released => {
@ -430,16 +452,315 @@ impl Controller {
}
// Give a user warning if the given button state is pressed at the time
fn pressed_warning(data: &Button, warn: &str) {
match data {
Button::Pressed => {warn!("{}", warn);} // TODO: user warning, not intenral
Button::Pressed => {warn!("{}", warn);} // TODO: user warning, not internal
Button::Released => {}
}
}
// Give a user warning if the given button state is released at the time
fn released_warning(data: &Button, warn: &str) {
match data {
Button::Released => {warn!("{}", warn);} // TODO: user warning, not intenral
Button::Released => {warn!("{}", warn);} // TODO: user warning, not internal
Button::Pressed => {}
}
}
}
//// Test ////////////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use super::*;
use async_channel::{unbounded, TryRecvError};
use async_io::block_on;
// Creates a new controller for use in tests. Has async-channen send, recv, and motor
// endpoints to use. Starts in AutoMode::Disallowed and LimitState::BothHit
fn create_controller() -> (Controller, DispatchSendQ, DispatchRecvQ, MotorRecvQ) {
let (ch1s, ch1r) = unbounded();
let (ch2s, ch2r) = unbounded();
let (chms, chmr) = unbounded();
let con = Controller::new(ch1r, ch2s, chms);
(con, ch1s, ch2r, chmr)
}
// Send a series of messages to the controller, then exit.
fn run_cmd_queue(con: &mut Controller, cs: DispatchSendQ, q: &mut Vec<Commands>) -> Result<()>{
block_on (
async {
q.push(Commands::TestingExit);
for c in q {
cs.send(c.clone()).await?
}
let _ = con.run().await; // If this doesn't error out of the loop, we'll hang here
Ok(())
}
)
}
/* States we want to test:
Controller states:
Stopped,
Stopping,
GoingUp,
AutoUp,
GoingDown,
AutoDown,
Auto states:
Disallowed
Allowed
Limit states:
NoLimitsHit,
UpperHit,
LowerHit,
BothHit,
*/
#[test]
fn motor_stops_on_initialization() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopping);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_stop_state_after_timeout() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopped);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_refuses_auto_when_bothhit() -> Result<()>{
let (mut con, cs, _cr, _mr) = create_controller();
let mut q = vec![
Commands::PicRecvAutoMode{data: Toggle::Active},
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.auto_mode, AutoMode::Disallowed);
Ok(())
}
#[test]
fn motor_succeeds_auto_when_not_bothhit() -> Result<()>{
let (mut con, cs, _cr, _mr) = create_controller();
let mut q = vec![
Commands::PicRecvLimitDown { data: Button::Released },
Commands::PicRecvAutoMode{data: Toggle::Active},
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.auto_mode, AutoMode::Allowed);
Ok(())
}
#[test]
fn motor_enters_timed_up() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvUp { data: Button::Pressed }
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::GoingUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_stops_after_no_input_no_auto() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::BluetoothDown { data: Button::Pressed },
Commands::ButtonTimerExpired,
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopping);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartDown);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_stops_on_release_timed_up() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvUp { data: Button::Released },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopping);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_stops_on_limit_timed_up() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvLimitUp { data: Button::Pressed },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopping);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_enters_auto_up() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::PicRecvUp { data: Button::Pressed },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::AutoUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_does_not_stop_after_no_input_in_auto() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::BluetoothDown { data: Button::Pressed },
Commands::ButtonTimerExpired,
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::AutoDown);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartDown);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_does_not_stop_auto_release() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvUp { data: Button::Released },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::AutoUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_switches_to_timed_when_auto_toggled_off() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvAutoMode{data: Toggle::Inactive},
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::GoingUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp); // Second up commanded should be okay
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_continues_with_additional_presses() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvUp { data: Button::Released },
Commands::BluetoothUp { data: Button::Pressed },
Commands::BluetoothUp { data: Button::Released },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::AutoUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_stops_on_opposite_button() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvUp { data: Button::Released },
Commands::BluetoothDown { data: Button::Pressed },
Commands::BluetoothDown { data: Button::Released },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopping);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
#[test]
fn motor_stops_if_limits_installed_backwards() -> Result<()>{
let (mut con, cs, _cr, mr) = create_controller();
let mut q = vec![
Commands::StopTimerExpired,
Commands::PicRecvLimitUp{data: Button::Released},
Commands::PicRecvLimitDown{data: Button::Released},
Commands::PicRecvAutoMode{data: Toggle::Active},
Commands::PicRecvUp { data: Button::Pressed },
Commands::PicRecvUp { data: Button::Released },
Commands::PicRecvLimitDown { data: Button::Pressed },
];
run_cmd_queue(&mut con, cs, &mut q)?;
assert_eq!(con.state, ControllerStates::Stopping);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::StartUp);
assert_eq!(mr.try_recv().unwrap(), MotorCommands::Stop);
assert_eq!(mr.try_recv(), Err(TryRecvError::Empty)); // Make sure queue is empty
Ok(())
}
}