Files
shturman/crates/core/shturman-power/src/fsm.rs
T
kk0t9 aaae0508b9 feat(v0.3): Power-сервис на FSM — dev-mock кормит события, grace+durable-barrier
P7.2: service.rs оборачивает PowerFsm — D-Bus state/signals из FSM; apply_event
исполняет действия (эмит сигналов, фоновый grace-таймер, durable-barrier sync).
dev-mock SetAcc/SetIgnition/TriggerShutdown/AbortShutdown кормят входы FSM.
FSM: AccOff → AccChanged(false)+ShutdownImminent (сохранён walking-skeleton-регресс).
Integration: ShutdownImminent + abort. zbus → tokio-executor (default-features=false,
features=["tokio"]) — иначе tokio::spawn в хендлере паникует (async-io). test-integration
--test-threads=1 (тесты владеют одним именем на шине).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
2026-06-24 23:17:13 +03:00

213 lines
7.0 KiB
Rust

//! Чистый 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<Action> {
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]
}
// committed/off/sleep/battery_cutoff + всё прочее — no-op (committed не abort-ится)
_ => vec![],
}
}
fn begin_shutdown(&mut self, reason: ShutdownReason) -> Vec<Action> {
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);
}
}