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");
|
init_tracing("shturman-power");
|
||||||
let conn = connect().await?;
|
let conn = connect().await?;
|
||||||
let svc = PowerService::new();
|
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")]
|
#[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?;
|
conn.object_server().at(names::power::PATH, svc).await?;
|
||||||
#[cfg(feature = "dev-mocks")]
|
#[cfg(feature = "dev-mocks")]
|
||||||
conn.object_server().at(names::power::PATH, mock).await?;
|
conn.object_server().at(names::power::PATH, mock).await?;
|
||||||
conn.request_name(names::power::NAME).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;
|
std::future::pending::<()>().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
//! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
|
//! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
|
||||||
//! v0.3: оборачивает чистый `PowerFsm` (спека §5–§7). dev-mock кормит входы FSM (не флипает состояние).
|
//! 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_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::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use zbus::interface;
|
use zbus::interface;
|
||||||
@@ -14,12 +17,14 @@ const GRACE_SECS: u32 = 2;
|
|||||||
|
|
||||||
pub struct PowerService {
|
pub struct PowerService {
|
||||||
fsm: Arc<Mutex<PowerFsm>>,
|
fsm: Arc<Mutex<PowerFsm>>,
|
||||||
|
thermal_state: Arc<Mutex<ThermalLevel>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PowerService {
|
impl Default for PowerService {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
fsm: Arc::new(Mutex::new(PowerFsm::new())),
|
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 {
|
pub fn source(&self) -> PowerSource {
|
||||||
self.fsm.lock().unwrap().source()
|
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")]
|
#[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 {
|
PowerMock {
|
||||||
fsm: Arc::clone(&self.fsm),
|
fsm: Arc::clone(&self.fsm),
|
||||||
|
temp,
|
||||||
|
copro,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,6 +106,99 @@ async fn apply_event(
|
|||||||
Ok(())
|
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")]
|
#[interface(name = "ru.shturman.Power1")]
|
||||||
impl PowerService {
|
impl PowerService {
|
||||||
async fn get_power_state(&self) -> String {
|
async fn get_power_state(&self) -> String {
|
||||||
@@ -110,10 +220,20 @@ impl PowerService {
|
|||||||
async fn power_source(&self) -> String {
|
async fn power_source(&self) -> String {
|
||||||
self.source().as_str().to_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)]
|
#[zbus(signal)]
|
||||||
async fn acc_changed(ctx: &SignalContext<'_>, on: bool) -> zbus::Result<()>;
|
async fn acc_changed(ctx: &SignalContext<'_>, on: bool) -> zbus::Result<()>;
|
||||||
#[zbus(signal)]
|
#[zbus(signal)]
|
||||||
|
async fn thermal_changed(
|
||||||
|
ctx: &SignalContext<'_>,
|
||||||
|
state: &str,
|
||||||
|
celsius: i32,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
#[zbus(signal)]
|
||||||
async fn shutdown_imminent(
|
async fn shutdown_imminent(
|
||||||
ctx: &SignalContext<'_>,
|
ctx: &SignalContext<'_>,
|
||||||
seconds: u32,
|
seconds: u32,
|
||||||
@@ -131,6 +251,8 @@ impl PowerService {
|
|||||||
#[cfg(feature = "dev-mocks")]
|
#[cfg(feature = "dev-mocks")]
|
||||||
pub struct PowerMock {
|
pub struct PowerMock {
|
||||||
fsm: Arc<Mutex<PowerFsm>>,
|
fsm: Arc<Mutex<PowerFsm>>,
|
||||||
|
temp: Arc<crate::thermal::MockTempSource>,
|
||||||
|
copro: Arc<crate::coprocessor::MockCoprocessor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dev-mocks")]
|
#[cfg(feature = "dev-mocks")]
|
||||||
@@ -168,6 +290,21 @@ impl PowerMock {
|
|||||||
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
|
||||||
let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await;
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
|
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use shturman_ipc::{names, types::PowerState};
|
use shturman_ipc::{names, types::PowerState};
|
||||||
|
use shturman_power::coprocessor::MockCoprocessor;
|
||||||
|
use shturman_power::thermal::MockTempSource;
|
||||||
use shturman_power::PowerService;
|
use shturman_power::PowerService;
|
||||||
use shturman_sdk::PowerClient;
|
use shturman_sdk::PowerClient;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore = "нужна session-шина: just test-integration"]
|
#[ignore = "нужна session-шина: just test-integration"]
|
||||||
async fn power_state_and_fake_acc() {
|
async fn power_state_and_fake_acc() {
|
||||||
let svc = PowerService::new();
|
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)
|
// сервер: Power1 + dev.PowerMock1 на одном пути (владеет ru.shturman.Power)
|
||||||
let server = zbus::Connection::session().await.unwrap();
|
let server = zbus::Connection::session().await.unwrap();
|
||||||
@@ -53,7 +56,7 @@ async fn power_state_and_fake_acc() {
|
|||||||
#[ignore = "нужна session-шина: just test-integration"]
|
#[ignore = "нужна session-шина: just test-integration"]
|
||||||
async fn shutdown_imminent_then_abort() {
|
async fn shutdown_imminent_then_abort() {
|
||||||
let svc = PowerService::new();
|
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();
|
let server = zbus::Connection::session().await.unwrap();
|
||||||
server
|
server
|
||||||
.object_server()
|
.object_server()
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ pub trait Power1 {
|
|||||||
fn uptime(&self) -> zbus::Result<u64>;
|
fn uptime(&self) -> zbus::Result<u64>;
|
||||||
#[zbus(property)]
|
#[zbus(property)]
|
||||||
fn power_source(&self) -> zbus::Result<String>;
|
fn power_source(&self) -> zbus::Result<String>;
|
||||||
|
#[zbus(property)]
|
||||||
|
fn thermal_state(&self) -> zbus::Result<String>;
|
||||||
|
|
||||||
#[zbus(signal)]
|
#[zbus(signal)]
|
||||||
fn acc_changed(&self, on: bool) -> zbus::Result<()>;
|
fn acc_changed(&self, on: bool) -> zbus::Result<()>;
|
||||||
#[zbus(signal)]
|
#[zbus(signal)]
|
||||||
|
fn thermal_changed(&self, state: String, celsius: i32) -> zbus::Result<()>;
|
||||||
|
#[zbus(signal)]
|
||||||
fn shutdown_imminent(&self, seconds: u32, reason: String) -> zbus::Result<()>;
|
fn shutdown_imminent(&self, seconds: u32, reason: String) -> zbus::Result<()>;
|
||||||
#[zbus(signal)]
|
#[zbus(signal)]
|
||||||
fn shutdown_aborted(&self) -> zbus::Result<()>;
|
fn shutdown_aborted(&self) -> zbus::Result<()>;
|
||||||
|
|||||||
Reference in New Issue
Block a user