use std::time::{Duration, Instant}; use types::pcdu::SwitchId; use crate::eps::PowerSwitchHelper; /// This is a helper trait required to make [SwitchAndModeHelper] generic. /// /// It allows distinguish a powered-off state from one or more powered-on states, so /// [`SwitchAndModeHelper`] knows which way to drive the switch for a given target mode. pub trait PowerSwitchedMode: Copy + PartialEq { const OFF: Self; fn requires_power(&self) -> bool; } impl PowerSwitchedMode for types::DeviceMode { const OFF: Self = types::DeviceMode::Off; fn requires_power(&self) -> bool { *self != types::DeviceMode::Off } } #[derive(Default, Debug, PartialEq, Eq)] enum SwitchTransitionState { #[default] Idle, PowerSwitching, Done, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PowerCycleState { Idle, SwitchingOff { restore_mode: Mode }, WaitingOff { restore_mode: Mode, since: Instant }, SwitchingOn { restore_mode: Mode }, } /// Outcome of a pending mode transition, once [`SwitchAndModeHelper::handle_mode_transition`] /// has driven it to completion. Carries back whichever TC commanded the transition, if any, so /// the caller can reply to it -- what that reply looks like is handler-specific, so this stays /// out of the helper. pub enum ModeTransitionEvent { Reached(Option), Failed(Option), /// The power cycle completed and the mode before the power cycle was restored. PowerCycleDone, /// Power switching failed during the power cycle. The power cycle is not hidden anymore, /// so [SwitchAndModeHelper::reported_mode] returns the actual mode again. PowerCycleFailed, } /// Drives the on/off power-switch commanding state machine (Idle -> PowerSwitching -> Done) /// shared by every device handler that owns a single power switch of its own. /// /// Handler-specific reactions (sending telemetry, invalidating cached sensor data, reporting to /// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a /// transition finishes (or fails) and leaves what to do about it to the caller. pub struct SwitchAndModeHelper { mode_helper: satrs_example::ModeHelper, switch_helper: PowerSwitchHelper, switch_id: SwitchId, power_cycle: PowerCycleState, power_cycle_off_duration: Duration, } impl SwitchAndModeHelper { pub fn new( init_mode: Mode, timeout: Duration, switch_helper: PowerSwitchHelper, switch_id: SwitchId, ) -> Self { Self { mode_helper: satrs_example::ModeHelper::new(init_mode, timeout), switch_helper, switch_id, power_cycle: PowerCycleState::Idle, power_cycle_off_duration: Duration::ZERO, } } #[inline] pub fn mode(&self) -> Mode { self.mode_helper.current } #[inline] pub fn target(&self) -> Option { self.mode_helper.target } /// Mode which should be reported to other components. A power cycle is hidden from them, /// so this is the mode which is restored after the power cycle while one is active. pub fn reported_mode(&self) -> Mode { match self.power_cycle { PowerCycleState::SwitchingOff { restore_mode } | PowerCycleState::WaitingOff { restore_mode, .. } | PowerCycleState::SwitchingOn { restore_mode } => restore_mode, PowerCycleState::Idle => self.mode(), } } #[inline] pub fn power_cycle_active(&self) -> bool { self.power_cycle != PowerCycleState::Idle } /// Starts a new transition, aborting a running power cycle. pub fn start_transition( &mut self, target_mode: Mode, tc_commander: Option, ) { self.power_cycle = PowerCycleState::Idle; self.start_transition_internal(target_mode, tc_commander); } /// Switches the device off, keeps it off for `off_duration` and then restores the current /// mode. Reaching the intermediate off mode does not generate an event. pub fn start_power_cycle(&mut self, off_duration: Duration) { self.power_cycle = PowerCycleState::SwitchingOff { restore_mode: self.mode(), }; self.power_cycle_off_duration = off_duration; self.start_transition_internal(Mode::OFF, None); } fn start_transition_internal( &mut self, target_mode: Mode, tc_commander: Option, ) { self.mode_helper.tc_commander = tc_commander; self.mode_helper.start(target_mode); } pub fn handle_mode_transition(&mut self) -> Option { if let PowerCycleState::WaitingOff { restore_mode, since, } = self.power_cycle && since.elapsed() >= self.power_cycle_off_duration { self.power_cycle = PowerCycleState::SwitchingOn { restore_mode }; self.start_transition_internal(restore_mode, None); } let event = self.handle_switch_transition()?; if self.power_cycle == PowerCycleState::Idle { return Some(event); } match event { ModeTransitionEvent::Reached(_) => match self.power_cycle { PowerCycleState::SwitchingOff { restore_mode } => { self.power_cycle = PowerCycleState::WaitingOff { restore_mode, since: Instant::now(), }; None } _ => { self.power_cycle = PowerCycleState::Idle; Some(ModeTransitionEvent::PowerCycleDone) } }, ModeTransitionEvent::Failed(_) => { self.power_cycle = PowerCycleState::Idle; Some(ModeTransitionEvent::PowerCycleFailed) } event => Some(event), } } fn handle_switch_transition(&mut self) -> Option { let target_mode = self.mode_helper.target?; let switch_target_on = target_mode.requires_power(); if self.mode_helper.transition_state == SwitchTransitionState::Idle { let result = if switch_target_on { self.switch_helper.send_switch_on_cmd(self.switch_id) } else { self.switch_helper.send_switch_off_cmd(self.switch_id) }; if result.is_err() { // Could not send switch command.. still continue with transition. log::error!( "failed to send switch {} command", if switch_target_on { "on" } else { "off" } ); } self.mode_helper.transition_state = SwitchTransitionState::PowerSwitching; } if self.mode_helper.transition_state == SwitchTransitionState::PowerSwitching { if self.switch_helper.is_switch_on(self.switch_id) == switch_target_on { log::info!("switch is {}", if switch_target_on { "on" } else { "off" }); self.mode_helper.transition_state = SwitchTransitionState::Done; } else if self.mode_helper.timed_out() { return Some(ModeTransitionEvent::Failed(self.mode_helper.finish(false))); } } if self.mode_helper.transition_state == SwitchTransitionState::Done { return Some(ModeTransitionEvent::Reached(self.mode_helper.finish(true))); } None } } #[cfg(test)] mod tests { use std::sync::{Arc, Mutex, mpsc}; use arbitrary_int::u11; use satrs::spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader}; use types::{ DeviceMode, pcdu::{SwitchRequest, SwitchState, SwitchStateBinary}, }; use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet}; use super::*; const TIMEOUT: Duration = Duration::from_millis(50); struct Testbench { helper: SwitchAndModeHelper, switch_rx: mpsc::Receiver, shared_switch_set: SharedSwitchSet, } impl Testbench { fn new() -> Self { let (switch_tx, switch_rx) = mpsc::sync_channel(10); let mut switch_map = SwitchMap::new(); switch_map.insert(SwitchId::Mgm0, SwitchState::Off); let shared_switch_set: SharedSwitchSet = Arc::new(Mutex::new(SwitchSet::new(switch_map))); Self { helper: SwitchAndModeHelper::new( DeviceMode::Off, TIMEOUT, PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()), SwitchId::Mgm0, ), switch_rx, shared_switch_set, } } fn set_switch_state(&self, state: SwitchState) { self.shared_switch_set .lock() .unwrap() .set_switch_state(SwitchId::Mgm0, state); } fn switch_requests(&self) -> Vec { self.switch_rx .try_iter() .map(|req| req.target_state) .collect() } /// Drives a transition to `Normal` to completion. fn switch_to_normal(&mut self) { self.helper.start_transition(DeviceMode::Normal, None); self.set_switch_state(SwitchState::On); assert!(matches!( self.helper.handle_mode_transition(), Some(ModeTransitionEvent::Reached(None)) )); self.switch_requests(); } /// Starts a power cycle from `Normal` and drives it until the device is off. fn power_cycle_until_off(&mut self, off_duration: Duration) { self.switch_to_normal(); self.helper.start_power_cycle(off_duration); assert!(self.helper.handle_mode_transition().is_none()); assert_eq!(self.switch_requests(), [SwitchStateBinary::Off]); self.set_switch_state(SwitchState::Off); assert!(self.helper.handle_mode_transition().is_none()); assert_eq!(self.helper.mode(), DeviceMode::Off); } } fn tc_id() -> CcsdsPacketIdAndPsc { CcsdsPacketIdAndPsc::new_from_ccsds_packet(&SpacePacketHeader::new_from_apid(u11::new(1))) } #[test] fn test_no_transition() { let mut tb = Testbench::new(); assert_eq!(tb.helper.mode(), DeviceMode::Off); assert_eq!(tb.helper.target(), None); assert!(tb.helper.handle_mode_transition().is_none()); assert!(tb.switch_requests().is_empty()); assert!(!tb.helper.power_cycle_active()); assert_eq!(tb.helper.reported_mode(), DeviceMode::Off); } #[test] fn test_switch_on() { let mut tb = Testbench::new(); tb.helper .start_transition(DeviceMode::Normal, Some(tc_id())); assert!(tb.helper.handle_mode_transition().is_none()); assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]); assert_eq!(tb.helper.mode(), DeviceMode::Off); assert_eq!(tb.helper.target(), Some(DeviceMode::Normal)); tb.set_switch_state(SwitchState::On); match tb.helper.handle_mode_transition() { Some(ModeTransitionEvent::Reached(Some(id))) => assert_eq!(id, tc_id()), _ => panic!("expected mode reached event with TC commander"), } assert_eq!(tb.helper.mode(), DeviceMode::Normal); assert_eq!(tb.helper.target(), None); assert!(tb.switch_requests().is_empty()); } #[test] fn test_switch_off() { let mut tb = Testbench::new(); tb.switch_to_normal(); tb.helper.start_transition(DeviceMode::Off, None); assert!(tb.helper.handle_mode_transition().is_none()); assert_eq!(tb.switch_requests(), [SwitchStateBinary::Off]); tb.set_switch_state(SwitchState::Off); assert!(matches!( tb.helper.handle_mode_transition(), Some(ModeTransitionEvent::Reached(None)) )); assert_eq!(tb.helper.mode(), DeviceMode::Off); } #[test] fn test_switch_already_in_target_state() { let mut tb = Testbench::new(); tb.set_switch_state(SwitchState::On); tb.helper.start_transition(DeviceMode::On, None); assert!(matches!( tb.helper.handle_mode_transition(), Some(ModeTransitionEvent::Reached(None)) )); // The switch command is still sent. assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]); assert_eq!(tb.helper.mode(), DeviceMode::On); } #[test] fn test_switch_timeout() { let mut tb = Testbench::new(); tb.helper .start_transition(DeviceMode::Normal, Some(tc_id())); assert!(tb.helper.handle_mode_transition().is_none()); std::thread::sleep(TIMEOUT); match tb.helper.handle_mode_transition() { Some(ModeTransitionEvent::Failed(Some(id))) => assert_eq!(id, tc_id()), _ => panic!("expected mode failed event with TC commander"), } assert_eq!(tb.helper.mode(), DeviceMode::Off); assert_eq!(tb.helper.target(), None); } #[test] fn test_power_cycle() { let mut tb = Testbench::new(); tb.power_cycle_until_off(Duration::ZERO); assert!(tb.helper.power_cycle_active()); // The off duration elapsed, so switching on starts right away. assert!(tb.helper.handle_mode_transition().is_none()); assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]); assert_eq!(tb.helper.target(), Some(DeviceMode::Normal)); tb.set_switch_state(SwitchState::On); assert!(matches!( tb.helper.handle_mode_transition(), Some(ModeTransitionEvent::PowerCycleDone) )); assert_eq!(tb.helper.mode(), DeviceMode::Normal); assert!(!tb.helper.power_cycle_active()); } #[test] fn test_power_cycle_reports_restored_mode() { let mut tb = Testbench::new(); tb.switch_to_normal(); tb.helper.start_power_cycle(Duration::from_secs(60)); assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal); tb.helper.handle_mode_transition(); tb.set_switch_state(SwitchState::Off); tb.helper.handle_mode_transition(); assert_eq!(tb.helper.mode(), DeviceMode::Off); assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal); } #[test] fn test_power_cycle_reports_restored_mode_while_switching_on() { let mut tb = Testbench::new(); tb.power_cycle_until_off(Duration::ZERO); assert!(tb.helper.handle_mode_transition().is_none()); assert_eq!(tb.helper.target(), Some(DeviceMode::Normal)); assert_eq!(tb.helper.mode(), DeviceMode::Off); assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal); } #[test] fn test_power_cycle_waits_off_duration() { let mut tb = Testbench::new(); tb.power_cycle_until_off(Duration::from_secs(60)); for _ in 0..3 { assert!(tb.helper.handle_mode_transition().is_none()); } assert!(tb.switch_requests().is_empty()); assert_eq!(tb.helper.target(), None); assert!(tb.helper.power_cycle_active()); } #[test] fn test_power_cycle_switch_off_timeout() { let mut tb = Testbench::new(); tb.switch_to_normal(); tb.helper.start_power_cycle(Duration::ZERO); assert!(tb.helper.handle_mode_transition().is_none()); std::thread::sleep(TIMEOUT); assert!(matches!( tb.helper.handle_mode_transition(), Some(ModeTransitionEvent::PowerCycleFailed) )); assert_eq!(tb.helper.mode(), DeviceMode::Normal); assert!(!tb.helper.power_cycle_active()); } #[test] fn test_power_cycle_switch_on_timeout() { let mut tb = Testbench::new(); tb.power_cycle_until_off(Duration::ZERO); assert!(tb.helper.handle_mode_transition().is_none()); std::thread::sleep(TIMEOUT); assert!(matches!( tb.helper.handle_mode_transition(), Some(ModeTransitionEvent::PowerCycleFailed) )); assert_eq!(tb.helper.mode(), DeviceMode::Off); assert!(!tb.helper.power_cycle_active()); // The failed power cycle is not hidden anymore. assert_eq!(tb.helper.reported_mode(), DeviceMode::Off); } #[test] fn test_transition_aborts_power_cycle() { let mut tb = Testbench::new(); tb.power_cycle_until_off(Duration::from_secs(60)); tb.helper.start_transition(DeviceMode::On, Some(tc_id())); assert!(!tb.helper.power_cycle_active()); assert_eq!(tb.helper.reported_mode(), DeviceMode::Off); tb.set_switch_state(SwitchState::On); // A regular transition event instead of a power cycle event. assert!(matches!( tb.helper.handle_mode_transition(), Some(ModeTransitionEvent::Reached(Some(_))) )); assert_eq!(tb.helper.mode(), DeviceMode::On); } }