//! Чистый 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, ThermalCleared, // тепло вернулось в норму до PONR → abort thermal-shutdown (гейт reason==Thermal) FailsafeCut, // MCU-авторитетный cut (зависший SoC / истёк hold-up) → off, необратимо } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { ShutdownImminent(ShutdownReason), ShutdownAborted, AccChanged(bool), StartGrace, Commit, Cut, // MCU снял питание (fail-safe) — сервис логирует + переходит в off } 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![] } // ACC-off: линия ACC сменилась (AccChanged) + старт shutdown. (Accessory | Running, E::AccOff) => { self.state = ShuttingDown { phase: Abortable, reason: ShutdownReason::AccOff, }; vec![ Action::AccChanged(false), Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace, ] } // under-voltage/thermal: ACC не менялся → без AccChanged. (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] } // тепло вернулось до PONR → abort (только thermal-shutdown; ACC-off/under-voltage — no-op) ( ShuttingDown { phase: Abortable, reason: ShutdownReason::Thermal, }, E::ThermalCleared, ) => { self.state = Running; vec![Action::ShutdownAborted] } // MCU fail-safe cut → off из любого не-off (необратимо, MCU-авторитет) (Off, E::FailsafeCut) => vec![], (_, E::FailsafeCut) => { self.state = Off; vec![Action::Cut] } // 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::AccChanged(false), 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 ); } #[test] fn thermal_cleared_aborts_only_thermal_abortable() { let mut f = PowerFsm::new(); // Running f.step(Event::ThermalTrip); // → ShuttingDown{Abortable, Thermal} assert_eq!(f.step(Event::ThermalCleared), vec![Action::ShutdownAborted]); assert_eq!(f.state(), State::Running); // из ACC-off-shutdown ThermalCleared — no-op let mut g = PowerFsm::new(); g.step(Event::AccOff); assert_eq!(g.step(Event::ThermalCleared), vec![]); assert_eq!(g.power_state(), PowerState::ShuttingDown); } #[test] fn failsafe_cut_forces_off_from_any_nonoff() { let mut f = PowerFsm::new(); // Running assert_eq!(f.step(Event::FailsafeCut), vec![Action::Cut]); assert_eq!(f.state(), State::Off); // из off — no-op assert_eq!(f.step(Event::FailsafeCut), vec![]); // даже из committed (необратимый shutdown) cut уводит в off let mut g = PowerFsm::new(); g.step(Event::AccOff); g.step(Event::GraceExpired); // committed assert_eq!(g.step(Event::FailsafeCut), vec![Action::Cut]); assert_eq!(g.state(), State::Off); } }