feat(power): Power1 стаб + dev-mock fake-ACC (feature dev-mocks)

PowerService (Power1, стаб running) + PowerMock (dev.PowerMock1, разделяет State через Arc<Mutex>).
dev-mocks — default-фича; прод (--no-default-features) mock не регистрирует.

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 12:46:22 +03:00
parent b8f084b1e1
commit 9aeff2aa7d
6 changed files with 229 additions and 0 deletions
Generated
+14
View File
@@ -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"
+1
View File
@@ -8,6 +8,7 @@ members = [
"crates/shturman-sdk",
"crates/core/shturman-firstboot",
"crates/core/shturman-settings",
"crates/core/shturman-power",
]
[workspace.package]
+22
View File
@@ -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" }
+6
View File
@@ -0,0 +1,6 @@
//! `ru.shturman.Power1` — стаб питания/жизненного цикла (домен B).
//! v0: статичное состояние `running`, мутируется только dev-mock (fake-ACC). Полная FSM/секвенсинг — v0.3.
pub mod service;
pub use service::PowerService;
+22
View File
@@ -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(())
}
+164
View File
@@ -0,0 +1,164 @@
//! Server-стаб `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
//! zbus 4: несколько интерфейсов на одном объекте — это РАЗНЫЕ типы на одном пути, разделяющие
//! состояние через `Arc<Mutex<State>>` (а не два `#[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<Mutex<State>>,
}
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<Mutex<State>>,
}
#[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::<IgnitionState>() {
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);
}
}