diff --git a/Cargo.lock b/Cargo.lock index 3ab4f4f..f6edfdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -800,6 +800,20 @@ dependencies = [ "zbus", ] +[[package]] +name = "shturman-power" +version = "0.0.0" +dependencies = [ + "anyhow", + "shturman-common", + "shturman-ipc", + "shturman-sdk", + "tempfile", + "tokio", + "tracing", + "zbus", +] + [[package]] name = "shturman-sdk" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index deb06e2..597fd1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/shturman-sdk", "crates/core/shturman-firstboot", "crates/core/shturman-settings", + "crates/core/shturman-power", ] [workspace.package] diff --git a/crates/core/shturman-power/Cargo.toml b/crates/core/shturman-power/Cargo.toml new file mode 100644 index 0000000..cb4e3a2 --- /dev/null +++ b/crates/core/shturman-power/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "shturman-power" +version = "0.0.0" +edition.workspace = true +license.workspace = true + +[features] +# dev-mocks — вкл. в dev (fake-ACC для тестов/v0.3); прод выключает `--no-default-features`. +default = ["dev-mocks"] +dev-mocks = [] + +[dependencies] +shturman-ipc = { path = "../../shturman-ipc" } +shturman-common = { path = "../../shturman-common" } +zbus.workspace = true +tokio.workspace = true +anyhow.workspace = true +tracing.workspace = true + +[dev-dependencies] +tempfile.workspace = true +shturman-sdk = { path = "../../shturman-sdk" } diff --git a/crates/core/shturman-power/src/lib.rs b/crates/core/shturman-power/src/lib.rs new file mode 100644 index 0000000..7e41428 --- /dev/null +++ b/crates/core/shturman-power/src/lib.rs @@ -0,0 +1,6 @@ +//! `ru.shturman.Power1` — стаб питания/жизненного цикла (домен B). +//! v0: статичное состояние `running`, мутируется только dev-mock (fake-ACC). Полная FSM/секвенсинг — v0.3. + +pub mod service; + +pub use service::PowerService; diff --git a/crates/core/shturman-power/src/main.rs b/crates/core/shturman-power/src/main.rs new file mode 100644 index 0000000..20d2d54 --- /dev/null +++ b/crates/core/shturman-power/src/main.rs @@ -0,0 +1,22 @@ +//! `ru.shturman.Power1` — сервис. На шину выводит systemd (План 5). Полная FSM/секвенсинг — v0.3. +//! В dev-сборке дополнительно регистрирует `ru.shturman.dev.PowerMock1` (fake-ACC) на том же пути. + +use shturman_common::init_tracing; +use shturman_ipc::{connect, names}; +use shturman_power::PowerService; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + init_tracing("shturman-power"); + let conn = connect().await?; + let svc = PowerService::new(); + #[cfg(feature = "dev-mocks")] + let mock = svc.mock(); + conn.object_server().at(names::power::PATH, svc).await?; + #[cfg(feature = "dev-mocks")] + conn.object_server().at(names::power::PATH, mock).await?; + conn.request_name(names::power::NAME).await?; + tracing::info!("ru.shturman.Power1 на шине"); + std::future::pending::<()>().await; + Ok(()) +} diff --git a/crates/core/shturman-power/src/service.rs b/crates/core/shturman-power/src/service.rs new file mode 100644 index 0000000..15a639e --- /dev/null +++ b/crates/core/shturman-power/src/service.rs @@ -0,0 +1,164 @@ +//! Server-стаб `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC). +//! zbus 4: несколько интерфейсов на одном объекте — это РАЗНЫЕ типы на одном пути, разделяющие +//! состояние через `Arc>` (а не два `#[interface]` на одном типе). + +use shturman_common::monotonic_secs; +use shturman_ipc::types::{IgnitionState, PowerSource, PowerState}; +use std::sync::{Arc, Mutex}; +use zbus::interface; +use zbus::object_server::SignalContext; + +struct State { + power: PowerState, + ignition: IgnitionState, + source: PowerSource, +} + +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>, +} + +impl Default for PowerService { + fn default() -> Self { + Self { + state: Arc::new(Mutex::new(State::default())), + } + } +} + +impl PowerService { + pub fn new() -> Self { + Self::default() + } + + // Inherent-аксессоры (тесты + источник для interface-методов). + pub fn power_state(&self) -> PowerState { + self.state.lock().unwrap().power + } + pub fn ignition(&self) -> IgnitionState { + self.state.lock().unwrap().ignition + } + pub fn source(&self) -> PowerSource { + self.state.lock().unwrap().source + } + + /// dev-mock «fake-ACC», разделяющий состояние (только в dev-сборке). + #[cfg(feature = "dev-mocks")] + pub fn mock(&self) -> PowerMock { + PowerMock { + state: Arc::clone(&self.state), + } + } +} + +#[interface(name = "ru.shturman.Power1")] +impl PowerService { + async fn get_power_state(&self) -> String { + self.power_state().as_str().to_string() + } + + /// Внутренний; в v0-стабе — no-op (полная sleep/wake — v1/v2, B §7). + 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() + } + + #[zbus(signal)] + async fn acc_changed(ctx: &SignalContext<'_>, on: bool) -> zbus::Result<()>; + #[zbus(signal)] + async fn shutdown_imminent( + ctx: &SignalContext<'_>, + seconds: u32, + reason: &str, + ) -> zbus::Result<()>; + #[zbus(signal)] + async fn shutdown_aborted(ctx: &SignalContext<'_>) -> zbus::Result<()>; + #[zbus(signal)] + async fn sleep(ctx: &SignalContext<'_>) -> zbus::Result<()>; + #[zbus(signal)] + async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>; +} + +/// dev-mock «fake-ACC» — отдельный тип на том же пути. Прод (`--no-default-features`) его НЕ регистрирует. +/// Методы возвращают `()` (ошибку эмита сигнала игнорируем — мок не отвечает D-Bus-ошибкой). +#[cfg(feature = "dev-mocks")] +pub struct PowerMock { + state: Arc>, +} + +#[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; + } + + async fn set_ignition(&self, state: String) { + if let Ok(ig) = state.parse::() { + self.state.lock().unwrap().ignition = ig; + } + } + + async fn trigger_shutdown( + &self, + seconds: u32, + reason: String, + #[zbus(signal_context)] ctx: SignalContext<'_>, + ) { + let _ = PowerService::shutdown_imminent(&ctx, seconds, &reason).await; + } + + async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) { + let _ = PowerService::shutdown_aborted(&ctx).await; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_running() { + let svc = PowerService::new(); + assert_eq!(svc.power_state(), PowerState::Running); + assert_eq!(svc.ignition(), IgnitionState::Running); + assert_eq!(svc.source(), PowerSource::Vehicle12v); + } +}