feat(v0.3): чистый PowerFsm (состояния/переходы B03)
P7.1: State/Event/Action + step (чистый, без I/O) + проекции в PowerState/
IgnitionState/PowerSource. Переходы off↔accessory↔running→shutting_down{abortable→
committed}→off; abort до PONR; sleep/battery_cutoff — каркас. 8 unit-тестов (каждый переход).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -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<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![]
|
||||
}
|
||||
(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<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::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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user