diff --git a/crates/core/shturman-power/src/fsm.rs b/crates/core/shturman-power/src/fsm.rs new file mode 100644 index 0000000..afa6b16 --- /dev/null +++ b/crates/core/shturman-power/src/fsm.rs @@ -0,0 +1,199 @@ +//! Чистый 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); + } +} diff --git a/crates/core/shturman-power/src/lib.rs b/crates/core/shturman-power/src/lib.rs index 7e41428..3fb0d82 100644 --- a/crates/core/shturman-power/src/lib.rs +++ b/crates/core/shturman-power/src/lib.rs @@ -1,6 +1,7 @@ -//! `ru.shturman.Power1` — стаб питания/жизненного цикла (домен B). -//! v0: статичное состояние `running`, мутируется только dev-mock (fake-ACC). Полная FSM/секвенсинг — v0.3. +//! `ru.shturman.Power1` — питание/жизненный цикл (домен B). v0.3: реальный lifecycle-FSM +//! (`fsm`), сервис оборачивает его (D-Bus state/signals из FSM; dev-mock кормит входы). +pub mod fsm; pub mod service; pub use service::PowerService;