feat(v0.4): проводка thermal+coprocessor циклов + D-Bus ThermalState/ThermalChanged + dev-mock
spawn_loops (thermal-монитор + coprocessor heartbeat/wait/safe-to-cut/B09) в main после регистрации интерфейса; PowerService += thermal_state + хендлы fsm/thermal_state + mock(temp,copro); dev-mock += SetTemp/HangSoc/McuLinkLoss; proxy.rs += thermal_state/thermal_changed. Existing integration-тесты подогнаны под mock(temp,copro). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -10,13 +10,44 @@ async fn main() -> anyhow::Result<()> {
|
||||
init_tracing("shturman-power");
|
||||
let conn = connect().await?;
|
||||
let svc = PowerService::new();
|
||||
let fsm = svc.fsm_handle();
|
||||
let thermal_state = svc.thermal_state_handle();
|
||||
|
||||
// источники: dev = mock (управляется dev-D-Bus), prod = реальные (sysfs/UART)
|
||||
#[cfg(feature = "dev-mocks")]
|
||||
let mock = svc.mock();
|
||||
let temp = std::sync::Arc::new(shturman_power::thermal::MockTempSource::new(20));
|
||||
#[cfg(not(feature = "dev-mocks"))]
|
||||
let temp = std::sync::Arc::new(shturman_power::thermal::SysfsTempSource::new());
|
||||
let throttler = std::sync::Arc::new(shturman_power::thermal::NoopThrottler::default());
|
||||
#[cfg(feature = "dev-mocks")]
|
||||
let copro = std::sync::Arc::new(shturman_power::coprocessor::MockCoprocessor::new());
|
||||
#[cfg(not(feature = "dev-mocks"))]
|
||||
let copro = std::sync::Arc::new(shturman_power::coprocessor::SerialCoprocessor::new());
|
||||
|
||||
#[cfg(feature = "dev-mocks")]
|
||||
let mock = svc.mock(temp.clone(), copro.clone());
|
||||
|
||||
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 на шине");
|
||||
|
||||
// контекст сигналов для фоновых циклов (после регистрации интерфейса)
|
||||
let iface = conn
|
||||
.object_server()
|
||||
.interface::<_, PowerService>(names::power::PATH)
|
||||
.await?;
|
||||
let ctx = iface.signal_context().to_owned();
|
||||
shturman_power::service::spawn_loops(
|
||||
fsm,
|
||||
thermal_state,
|
||||
temp as std::sync::Arc<dyn shturman_power::thermal::TempSource>,
|
||||
throttler as std::sync::Arc<dyn shturman_power::thermal::Throttler>,
|
||||
copro as std::sync::Arc<dyn shturman_power::coprocessor::Coprocessor>,
|
||||
ctx,
|
||||
);
|
||||
|
||||
tracing::info!("ru.shturman.Power1 на шине (FSM + thermal + coprocessor)");
|
||||
std::future::pending::<()>().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! 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 crate::coprocessor::{Coprocessor, CoprocessorClient, BROWNOUT_MV};
|
||||
use crate::fsm::{Action, Event, Phase, PowerFsm, State};
|
||||
use crate::protocol::McuToSoc;
|
||||
use crate::thermal::{TempSource, ThermalLevel, ThermalMonitor, Throttler};
|
||||
use shturman_common::monotonic_secs;
|
||||
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState};
|
||||
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState, ShutdownReason};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use zbus::interface;
|
||||
@@ -14,12 +17,14 @@ const GRACE_SECS: u32 = 2;
|
||||
|
||||
pub struct PowerService {
|
||||
fsm: Arc<Mutex<PowerFsm>>,
|
||||
thermal_state: Arc<Mutex<ThermalLevel>>,
|
||||
}
|
||||
|
||||
impl Default for PowerService {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fsm: Arc::new(Mutex::new(PowerFsm::new())),
|
||||
thermal_state: Arc::new(Mutex::new(ThermalLevel::Normal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,11 +42,23 @@ impl PowerService {
|
||||
pub fn source(&self) -> PowerSource {
|
||||
self.fsm.lock().unwrap().source()
|
||||
}
|
||||
pub fn fsm_handle(&self) -> Arc<Mutex<PowerFsm>> {
|
||||
Arc::clone(&self.fsm)
|
||||
}
|
||||
pub fn thermal_state_handle(&self) -> Arc<Mutex<ThermalLevel>> {
|
||||
Arc::clone(&self.thermal_state)
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-mocks")]
|
||||
pub fn mock(&self) -> PowerMock {
|
||||
pub fn mock(
|
||||
&self,
|
||||
temp: Arc<crate::thermal::MockTempSource>,
|
||||
copro: Arc<crate::coprocessor::MockCoprocessor>,
|
||||
) -> PowerMock {
|
||||
PowerMock {
|
||||
fsm: Arc::clone(&self.fsm),
|
||||
temp,
|
||||
copro,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +106,99 @@ async fn apply_event(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Фоновые циклы v0.4 — thermal-монитор + coprocessor (heartbeat/wait/safe-to-cut/B09). Монотоника.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_loops(
|
||||
fsm: Arc<Mutex<PowerFsm>>,
|
||||
thermal_state: Arc<Mutex<ThermalLevel>>,
|
||||
temp: Arc<dyn TempSource>,
|
||||
throttler: Arc<dyn Throttler>,
|
||||
copro: Arc<dyn Coprocessor>,
|
||||
ctx: SignalContext<'static>,
|
||||
) {
|
||||
// thermal-цикл
|
||||
{
|
||||
let (fsm, ctx) = (Arc::clone(&fsm), ctx.clone());
|
||||
tokio::spawn(async move {
|
||||
let mut mon = ThermalMonitor::new();
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(crate::thermal::POLL_SECS)).await;
|
||||
let t = temp.read_celsius();
|
||||
let obs = mon.observe(t);
|
||||
if !obs.changed {
|
||||
continue;
|
||||
}
|
||||
throttler.apply(obs.level);
|
||||
*thermal_state.lock().unwrap() = obs.level;
|
||||
let _ = PowerService::thermal_changed(&ctx, obs.level.as_str(), t).await;
|
||||
if obs.entered_critical {
|
||||
let _ = apply_event(&fsm, Event::ThermalTrip, &ctx).await;
|
||||
}
|
||||
if obs.left_critical {
|
||||
let in_thermal_abortable = matches!(
|
||||
fsm.lock().unwrap().state(),
|
||||
State::ShuttingDown {
|
||||
phase: Phase::Abortable,
|
||||
reason: ShutdownReason::Thermal
|
||||
}
|
||||
);
|
||||
if in_thermal_abortable {
|
||||
let _ = apply_event(&fsm, Event::ThermalCleared, &ctx).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// coprocessor-цикл (heartbeat / wait-for-completion / safe-to-cut / B09 failsafe)
|
||||
{
|
||||
tokio::spawn(async move {
|
||||
let mut client = CoprocessorClient::new(Arc::clone(&copro));
|
||||
let mut last_committed = false;
|
||||
let mut last_shutting = false;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(crate::coprocessor::HEARTBEAT_SECS)).await;
|
||||
copro.set_now(monotonic_secs());
|
||||
// входящие MCU→SoC → FSM
|
||||
for msg in client.poll() {
|
||||
match msg {
|
||||
McuToSoc::Acc { on } => {
|
||||
let ev = if on { Event::AccOn } else { Event::AccOff };
|
||||
let _ = apply_event(&fsm, ev, &ctx).await;
|
||||
}
|
||||
McuToSoc::Voltage { mv } if mv < BROWNOUT_MV => {
|
||||
let _ = apply_event(&fsm, Event::UnderVoltage, &ctx).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// рёбра состояния FSM → протокол
|
||||
let st = fsm.lock().unwrap().state();
|
||||
let shutting = matches!(st, State::ShuttingDown { .. });
|
||||
let committed = matches!(
|
||||
st,
|
||||
State::ShuttingDown {
|
||||
phase: Phase::Committed,
|
||||
..
|
||||
}
|
||||
);
|
||||
if shutting && !last_shutting {
|
||||
client.shutdown_imminent(crate::coprocessor::HOLDUP_BUDGET_SECS as u8);
|
||||
} else if committed && !last_committed {
|
||||
client.safe_to_cut(); // PONR → MCU режет немедленно
|
||||
} else if !shutting {
|
||||
client.heartbeat(); // running/accessory — keepalive
|
||||
}
|
||||
last_shutting = shutting;
|
||||
last_committed = committed;
|
||||
// B09: независимый fail-safe-таймер (зависший SoC / истёк бюджет)
|
||||
if copro.failsafe_due() {
|
||||
let _ = apply_event(&fsm, Event::FailsafeCut, &ctx).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "ru.shturman.Power1")]
|
||||
impl PowerService {
|
||||
async fn get_power_state(&self) -> String {
|
||||
@@ -110,10 +220,20 @@ impl PowerService {
|
||||
async fn power_source(&self) -> String {
|
||||
self.source().as_str().to_string()
|
||||
}
|
||||
#[zbus(property)]
|
||||
async fn thermal_state(&self) -> String {
|
||||
self.thermal_state.lock().unwrap().as_str().to_string()
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
async fn acc_changed(ctx: &SignalContext<'_>, on: bool) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
async fn thermal_changed(
|
||||
ctx: &SignalContext<'_>,
|
||||
state: &str,
|
||||
celsius: i32,
|
||||
) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
async fn shutdown_imminent(
|
||||
ctx: &SignalContext<'_>,
|
||||
seconds: u32,
|
||||
@@ -131,6 +251,8 @@ impl PowerService {
|
||||
#[cfg(feature = "dev-mocks")]
|
||||
pub struct PowerMock {
|
||||
fsm: Arc<Mutex<PowerFsm>>,
|
||||
temp: Arc<crate::thermal::MockTempSource>,
|
||||
copro: Arc<crate::coprocessor::MockCoprocessor>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-mocks")]
|
||||
@@ -168,6 +290,21 @@ impl PowerMock {
|
||||
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
||||
let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await;
|
||||
}
|
||||
|
||||
/// Задать температуру (°C) → thermal-монитор подхватит на следующем poll.
|
||||
async fn set_temp(&self, celsius: i32) {
|
||||
self.temp.set(celsius);
|
||||
}
|
||||
|
||||
/// «Завис SoC»: heartbeat перестаёт освежать B09-таймер → MCU срежет питание.
|
||||
async fn hang_soc(&self) {
|
||||
self.copro.hang();
|
||||
}
|
||||
|
||||
/// Тишина линка: SoC-сторона деградирует (лог, не self-cut — red-line). MCU-политика cut-vs-hold — B §12/HW.
|
||||
async fn mcu_link_loss(&self) {
|
||||
tracing::warn!("coprocessor: MCU link loss — SoC деградирует (cut-vs-hold политика — HW/§12)");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use shturman_ipc::{names, types::PowerState};
|
||||
use shturman_power::coprocessor::MockCoprocessor;
|
||||
use shturman_power::thermal::MockTempSource;
|
||||
use shturman_power::PowerService;
|
||||
use shturman_sdk::PowerClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "нужна session-шина: just test-integration"]
|
||||
async fn power_state_and_fake_acc() {
|
||||
let svc = PowerService::new();
|
||||
let mock = svc.mock();
|
||||
let mock = svc.mock(Arc::new(MockTempSource::new(20)), Arc::new(MockCoprocessor::new()));
|
||||
|
||||
// сервер: Power1 + dev.PowerMock1 на одном пути (владеет ru.shturman.Power)
|
||||
let server = zbus::Connection::session().await.unwrap();
|
||||
@@ -53,7 +56,7 @@ async fn power_state_and_fake_acc() {
|
||||
#[ignore = "нужна session-шина: just test-integration"]
|
||||
async fn shutdown_imminent_then_abort() {
|
||||
let svc = PowerService::new();
|
||||
let mock = svc.mock();
|
||||
let mock = svc.mock(Arc::new(MockTempSource::new(20)), Arc::new(MockCoprocessor::new()));
|
||||
let server = zbus::Connection::session().await.unwrap();
|
||||
server
|
||||
.object_server()
|
||||
|
||||
@@ -22,10 +22,14 @@ pub trait Power1 {
|
||||
fn uptime(&self) -> zbus::Result<u64>;
|
||||
#[zbus(property)]
|
||||
fn power_source(&self) -> zbus::Result<String>;
|
||||
#[zbus(property)]
|
||||
fn thermal_state(&self) -> zbus::Result<String>;
|
||||
|
||||
#[zbus(signal)]
|
||||
fn acc_changed(&self, on: bool) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
fn thermal_changed(&self, state: String, celsius: i32) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
fn shutdown_imminent(&self, seconds: u32, reason: String) -> zbus::Result<()>;
|
||||
#[zbus(signal)]
|
||||
fn shutdown_aborted(&self) -> zbus::Result<()>;
|
||||
|
||||
Reference in New Issue
Block a user