//! Чистый FSM питания (B03, спека v0.3 §5). Без D-Bus/async/I/O — сервис исполняет `Action`. use shturman_ipc::types::{IgnitionState, PowerSource, PowerState, ShutdownReason}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Phase { Abortable, Committed, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum State { Off, Accessory, Running, ShuttingDown { phase: Phase, reason: ShutdownReason }, Sleep, // зарезервировано (полные sleep/wake — v1/v2) BatteryCutoff, // зарезервировано (long-park — v1/v2) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Event { AccOn, AccOff, EngineOn, EngineOff, UnderVoltage, ThermalTrip, GraceExpired, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { ShutdownImminent(ShutdownReason), ShutdownAborted, AccChanged(bool), StartGrace, Commit, } pub struct PowerFsm { state: State, } impl Default for PowerFsm { fn default() -> Self { Self { state: State::Running } } } impl PowerFsm { pub fn new() -> Self { Self::default() } pub fn state(&self) -> State { self.state } /// D-Bus-проекция состояния (`PowerState`). pub fn power_state(&self) -> PowerState { match self.state { State::Off => PowerState::Off, State::Accessory => PowerState::Accessory, State::Running => PowerState::Running, State::ShuttingDown { .. } => PowerState::ShuttingDown, State::Sleep => PowerState::Sleep, State::BatteryCutoff => PowerState::BatteryCutoff, } } /// Ось зажигания (канон, B §1). pub fn ignition(&self) -> IgnitionState { match self.state { State::Running => IgnitionState::Running, State::Accessory => IgnitionState::Accessory, _ => IgnitionState::Off, } } /// Источник питания — сигнал потребителям «времени мало» при shutdown. pub fn source(&self) -> PowerSource { match self.state { State::ShuttingDown { reason: ShutdownReason::UnderVoltage, .. } => PowerSource::LowBattery, State::ShuttingDown { .. } => PowerSource::HoldupCap, _ => PowerSource::Vehicle12v, } } /// Шаг FSM. Возвращает действия для исполнения сервисом (спека §5). pub fn step(&mut self, ev: Event) -> Vec { use Event as E; use Phase::*; use State::*; match (self.state, ev) { (Off, E::AccOn) => { self.state = Accessory; vec![Action::AccChanged(true)] } (Accessory, E::EngineOn) => { self.state = Running; vec![] } (Running, E::EngineOff) => { self.state = Accessory; vec![] } (Accessory | Running, E::AccOff) => self.begin_shutdown(ShutdownReason::AccOff), (Accessory | Running, E::UnderVoltage) => self.begin_shutdown(ShutdownReason::UnderVoltage), (Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal), (ShuttingDown { phase: Abortable, .. }, E::AccOn) => { self.state = Running; vec![Action::ShutdownAborted, Action::AccChanged(true)] } (ShuttingDown { phase: Abortable, reason }, E::GraceExpired) => { self.state = ShuttingDown { phase: Committed, reason }; vec![Action::Commit] } // committed/off/sleep/battery_cutoff + всё прочее — no-op (committed не abort-ится) _ => vec![], } } fn begin_shutdown(&mut self, reason: ShutdownReason) -> Vec { self.state = State::ShuttingDown { phase: Phase::Abortable, reason }; vec![Action::ShutdownImminent(reason), Action::StartGrace] } } #[cfg(test)] mod tests { use super::*; #[test] fn off_acc_on_to_accessory() { let mut f = PowerFsm { state: State::Off }; assert_eq!(f.step(Event::AccOn), vec![Action::AccChanged(true)]); assert_eq!(f.state(), State::Accessory); } #[test] fn accessory_engine_on_to_running_and_back() { let mut f = PowerFsm { state: State::Accessory }; assert_eq!(f.step(Event::EngineOn), vec![]); assert_eq!(f.state(), State::Running); assert_eq!(f.step(Event::EngineOff), vec![]); assert_eq!(f.state(), State::Accessory); } #[test] fn acc_off_begins_abortable_shutdown() { let mut f = PowerFsm::new(); // Running assert_eq!( f.step(Event::AccOff), vec![Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace] ); assert_eq!(f.power_state(), PowerState::ShuttingDown); assert_eq!(f.source(), PowerSource::HoldupCap); } #[test] fn under_voltage_reason_and_source() { let mut f = PowerFsm::new(); let a = f.step(Event::UnderVoltage); assert_eq!(a[0], Action::ShutdownImminent(ShutdownReason::UnderVoltage)); assert_eq!(f.source(), PowerSource::LowBattery); } #[test] fn abort_before_ponr() { let mut f = PowerFsm::new(); f.step(Event::AccOff); assert_eq!( f.step(Event::AccOn), vec![Action::ShutdownAborted, Action::AccChanged(true)] ); assert_eq!(f.state(), State::Running); } #[test] fn grace_expired_commits_and_is_irreversible() { let mut f = PowerFsm::new(); f.step(Event::AccOff); assert_eq!(f.step(Event::GraceExpired), vec![Action::Commit]); assert_eq!(f.step(Event::AccOn), vec![]); // committed: abort игнорируется assert!(matches!(f.state(), State::ShuttingDown { phase: Phase::Committed, .. })); } #[test] fn reserved_states_noop() { let mut f = PowerFsm { state: State::Sleep }; assert_eq!(f.step(Event::AccOn), vec![]); assert_eq!(f.state(), State::Sleep); } #[test] fn ignition_projection() { assert_eq!(PowerFsm { state: State::Running }.ignition(), IgnitionState::Running); assert_eq!(PowerFsm { state: State::Accessory }.ignition(), IgnitionState::Accessory); assert_eq!(PowerFsm { state: State::Off }.ignition(), IgnitionState::Off); } }