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:
Generated
+14
-17
@@ -165,17 +165,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
@@ -3885,6 +3874,16 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "softbuffer"
|
||||
version = "0.4.8"
|
||||
@@ -4209,11 +4208,14 @@ version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -5441,15 +5443,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
@@ -5463,6 +5459,7 @@ dependencies = [
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.52.0",
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ rust-version = "1.96"
|
||||
|
||||
[workspace.dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] }
|
||||
zbus = "4"
|
||||
# tokio-executor у zbus (а не async-io) — сервисы на #[tokio::main]; нужно для tokio::spawn в хендлерах (grace-таймер).
|
||||
zbus = { version = "4", default-features = false, features = ["tokio"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,3 +48,48 @@ async fn power_state_and_fake_acc() {
|
||||
let sig = acc.next().await.unwrap();
|
||||
assert!(!sig.args().unwrap().on());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "нужна session-шина: just test-integration"]
|
||||
async fn shutdown_imminent_then_abort() {
|
||||
let svc = PowerService::new();
|
||||
let mock = svc.mock();
|
||||
let server = zbus::Connection::session().await.unwrap();
|
||||
server.object_server().at(names::power::PATH, svc).await.unwrap();
|
||||
server.object_server().at(names::power::PATH, mock).await.unwrap();
|
||||
server.request_name(names::power::NAME).await.unwrap();
|
||||
|
||||
let client = zbus::Connection::session().await.unwrap();
|
||||
let power = PowerClient::new(&client).await.unwrap();
|
||||
let mut imminent = power.proxy().receive_shutdown_imminent().await.unwrap();
|
||||
let mut aborted = power.proxy().receive_shutdown_aborted().await.unwrap();
|
||||
|
||||
// ACC-off → ShutdownImminent(acc_off), состояние shutting_down
|
||||
client
|
||||
.call_method(
|
||||
Some(names::power::NAME),
|
||||
names::power::PATH,
|
||||
Some(names::power::MOCK_IFACE),
|
||||
"SetAcc",
|
||||
&(false,),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let sig = imminent.next().await.unwrap();
|
||||
assert_eq!(sig.args().unwrap().reason(), "acc_off");
|
||||
assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown);
|
||||
|
||||
// re-power до grace → ShutdownAborted + running
|
||||
client
|
||||
.call_method(
|
||||
Some(names::power::NAME),
|
||||
names::power::PATH,
|
||||
Some(names::power::MOCK_IFACE),
|
||||
"SetAcc",
|
||||
&(true,),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
aborted.next().await.unwrap();
|
||||
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ lint:
|
||||
deny:
|
||||
cargo deny check
|
||||
|
||||
# интеграционные тесты на session-шине (нужен dbus: brew install dbus / в Lima)
|
||||
# интеграционные тесты на session-шине (нужен dbus: brew install dbus / в Lima).
|
||||
# --test-threads=1: тесты владеют одними well-known именами на общей шине → серийно (иначе кросс-talk/вис).
|
||||
test-integration:
|
||||
dbus-run-session -- cargo test --workspace -- --ignored
|
||||
dbus-run-session -- cargo test --workspace -- --ignored --test-threads=1
|
||||
|
||||
# полный локальный гейт
|
||||
ci: lint test deny
|
||||
|
||||
Reference in New Issue
Block a user