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>
This commit is contained in:
2026-06-24 23:17:13 +03:00
parent d8465c91e4
commit aaae0508b9
6 changed files with 151 additions and 81 deletions
+15 -2
View File
@@ -102,7 +102,16 @@ impl PowerFsm {
self.state = Accessory;
vec![]
}
(Accessory | Running, E::AccOff) => self.begin_shutdown(ShutdownReason::AccOff),
// 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) => {
@@ -149,7 +158,11 @@ mod tests {
let mut f = PowerFsm::new(); // Running
assert_eq!(
f.step(Event::AccOff),
vec![Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace]
vec![
Action::AccChanged(false),
Action::ShutdownImminent(ShutdownReason::AccOff),
Action::StartGrace
]
);
assert_eq!(f.power_state(), PowerState::ShuttingDown);
assert_eq!(f.source(), PowerSource::HoldupCap);
+72 -59
View File
@@ -1,39 +1,24 @@
//! Server-стаб `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
//! zbus 4: несколько интерфейсов на одном объекте — это РАЗНЫЕ типы на одном пути, разделяющие
//! состояние через `Arc<Mutex<State>>` (а не два `#[interface]` на одном типе).
//! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
//! v0.3: оборачивает чистый `PowerFsm` (спека §5–§7). dev-mock кормит входы FSM (не флипает состояние).
use crate::fsm::{Action, Event, PowerFsm};
use shturman_common::monotonic_secs;
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use zbus::interface;
use zbus::object_server::SignalContext;
struct State {
power: PowerState,
ignition: IgnitionState,
source: PowerSource,
}
/// Grace-окно (сек): и поле сигнала `ShutdownImminent`, и длительность таймера. v0.3 — фикс. (конфиг — позже).
const GRACE_SECS: u32 = 2;
impl Default for State {
fn default() -> Self {
Self {
power: PowerState::Running,
ignition: IgnitionState::Running,
source: PowerSource::Vehicle12v,
}
}
}
/// Стаб питания (`Power1`). В v0 стартует в `running`; запись/actuator отсутствуют (#2).
pub struct PowerService {
state: Arc<Mutex<State>>,
fsm: Arc<Mutex<PowerFsm>>,
}
impl Default for PowerService {
fn default() -> Self {
Self {
state: Arc::new(Mutex::new(State::default())),
}
Self { fsm: Arc::new(Mutex::new(PowerFsm::new())) }
}
}
@@ -41,25 +26,60 @@ impl PowerService {
pub fn new() -> Self {
Self::default()
}
// Inherent-аксессоры (тесты + источник для interface-методов).
pub fn power_state(&self) -> PowerState {
self.state.lock().unwrap().power
self.fsm.lock().unwrap().power_state()
}
pub fn ignition(&self) -> IgnitionState {
self.state.lock().unwrap().ignition
self.fsm.lock().unwrap().ignition()
}
pub fn source(&self) -> PowerSource {
self.state.lock().unwrap().source
self.fsm.lock().unwrap().source()
}
/// dev-mock «fake-ACC», разделяющий состояние (только в dev-сборке).
#[cfg(feature = "dev-mocks")]
pub fn mock(&self) -> PowerMock {
PowerMock {
state: Arc::clone(&self.state),
PowerMock { fsm: Arc::clone(&self.fsm) }
}
}
/// Durable-write barrier (#5): сбросить грязные страницы `/data` ДО PONR (Settings уже синхронен).
fn durable_barrier() {
let _ = std::process::Command::new("sync").status();
tracing::info!(
"power: commit (PONR) — durable barrier sync; load-shed: amp/backlight/modem (нет реальных нагрузок в v0)"
);
}
/// Шагнуть FSM и исполнить действия (эмит сигналов, grace-таймер, durable-barrier).
async fn apply_event(
fsm: &Arc<Mutex<PowerFsm>>,
ev: Event,
ctx: &SignalContext<'_>,
) -> zbus::Result<()> {
let actions = fsm.lock().unwrap().step(ev);
for a in actions {
match a {
Action::ShutdownImminent(r) => {
PowerService::shutdown_imminent(ctx, GRACE_SECS, r.as_str()).await?
}
Action::ShutdownAborted => PowerService::shutdown_aborted(ctx).await?,
Action::AccChanged(on) => PowerService::acc_changed(ctx, on).await?,
Action::StartGrace => {
// Фоновый grace-таймер (монотоника tokio). По истечении — GraceExpired:
// commit (durable-barrier), если FSM ещё в abortable; если был re-power (abort) — no-op.
let fsm = Arc::clone(fsm);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(GRACE_SECS as u64)).await;
let acts = fsm.lock().unwrap().step(Event::GraceExpired);
if acts.contains(&Action::Commit) {
durable_barrier();
}
});
}
Action::Commit => durable_barrier(),
}
}
Ok(())
}
#[interface(name = "ru.shturman.Power1")]
@@ -68,19 +88,17 @@ impl PowerService {
self.power_state().as_str().to_string()
}
/// Внутренний; в v0-стабе — no-op (полная sleep/wake — v1/v2, B §7).
/// Внутренний; sleep/wake — v1/v2 (B §7). В v0.3 — no-op.
async fn request_sleep(&self) {}
#[zbus(property)]
async fn ignition_state(&self) -> String {
self.ignition().as_str().to_string()
}
#[zbus(property)]
async fn uptime(&self) -> u64 {
monotonic_secs()
}
#[zbus(property)]
async fn power_source(&self) -> String {
self.source().as_str().to_string()
@@ -102,51 +120,46 @@ impl PowerService {
async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>;
}
/// dev-mock «fake-ACC» — отдельный тип на том же пути. Прод (`--no-default-features`) его НЕ регистрирует.
/// Методы возвращают `()` (ошибку эмита сигнала игнорируем — мок не отвечает D-Bus-ошибкой).
/// dev-mock «fake-ACC/voltage/thermal» — кормит входы FSM. Прод (`--no-default-features`) не регистрирует.
#[cfg(feature = "dev-mocks")]
pub struct PowerMock {
state: Arc<Mutex<State>>,
fsm: Arc<Mutex<PowerFsm>>,
}
#[cfg(feature = "dev-mocks")]
#[interface(name = "ru.shturman.dev.PowerMock1")]
impl PowerMock {
async fn set_acc(&self, on: bool, #[zbus(signal_context)] ctx: SignalContext<'_>) {
{
let mut st = self.state.lock().unwrap();
st.ignition = if on {
IgnitionState::Running
} else {
IgnitionState::Off
};
st.power = if on {
PowerState::Running
} else {
PowerState::Off
};
}
// Эмитим Power1-сигнал (тот же путь; имя интерфейса добавляет сама acc_changed).
let _ = PowerService::acc_changed(&ctx, on).await;
let ev = if on { Event::AccOn } else { Event::AccOff };
let _ = apply_event(&self.fsm, ev, &ctx).await;
}
async fn set_ignition(&self, state: String) {
if let Ok(ig) = state.parse::<IgnitionState>() {
self.state.lock().unwrap().ignition = ig;
}
async fn set_ignition(&self, state: String, #[zbus(signal_context)] ctx: SignalContext<'_>) {
// accessory↔running — через EngineOn/Off; off — AccOff.
let ev = match state.as_str() {
"running" => Event::EngineOn,
"accessory" => Event::EngineOff,
_ => Event::AccOff,
};
let _ = apply_event(&self.fsm, ev, &ctx).await;
}
async fn trigger_shutdown(
&self,
seconds: u32,
_seconds: u32,
reason: String,
#[zbus(signal_context)] ctx: SignalContext<'_>,
) {
let _ = PowerService::shutdown_imminent(&ctx, seconds, &reason).await;
let ev = match reason.as_str() {
"thermal" => Event::ThermalTrip,
"under_voltage" => Event::UnderVoltage,
_ => Event::AccOff,
};
let _ = apply_event(&self.fsm, ev, &ctx).await;
}
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
let _ = PowerService::shutdown_aborted(&ctx).await;
let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await;
}
}