From b9ae2f23d5cefcd7aaf975c5f67da29722caac3a Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 25 Jun 2026 15:27:29 +0300 Subject: [PATCH] =?UTF-8?q?feat(v0.4):=20=D1=87=D0=B8=D1=81=D1=82=D1=8B?= =?UTF-8?q?=D0=B9=20ThermalPolicy=20(=D0=B1=D0=B0=D0=BD=D0=B4=D1=8B=20+=20?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D0=B5=D1=80=D0=B5=D0=B7=D0=B8=D1=81,?= =?UTF-8?q?=20A12/B10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alexander --- crates/core/shturman-power/src/lib.rs | 1 + crates/core/shturman-power/src/thermal.rs | 107 ++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 crates/core/shturman-power/src/thermal.rs diff --git a/crates/core/shturman-power/src/lib.rs b/crates/core/shturman-power/src/lib.rs index 3fb0d82..d21f3af 100644 --- a/crates/core/shturman-power/src/lib.rs +++ b/crates/core/shturman-power/src/lib.rs @@ -3,5 +3,6 @@ pub mod fsm; pub mod service; +pub mod thermal; pub use service::PowerService; diff --git a/crates/core/shturman-power/src/thermal.rs b/crates/core/shturman-power/src/thermal.rs new file mode 100644 index 0000000..66e17b3 --- /dev/null +++ b/crates/core/shturman-power/src/thermal.rs @@ -0,0 +1,107 @@ +//! Тепловая подсистема (A12/B10, спека v0.4 §5). `ThermalPolicy` — чистая (без I/O), с гистерезисом. +//! Источники/throttler/монитор — P8.5 (этот же файл). + +/// Уровень теплового состояния. `Throttle(u8)` — банд (v0.4 использует уровень 1; мульти-банд — RK3588). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ThermalLevel { + Normal, + Warn, + Throttle(u8), + Critical, +} + +impl ThermalLevel { + pub fn as_str(&self) -> &'static str { + match self { + ThermalLevel::Normal => "normal", + ThermalLevel::Warn => "warn", + ThermalLevel::Throttle(_) => "throttle", + ThermalLevel::Critical => "critical", + } + } + fn rank(&self) -> u8 { + match self { + ThermalLevel::Normal => 0, + ThermalLevel::Warn => 1, + ThermalLevel::Throttle(_) => 2, + ThermalLevel::Critical => 3, + } + } +} + +// Пороги — placeholder-константы (°C). Тюнинг на RK3588 (hardware §1a; Tjmax ~100 °C). +pub const WARN_C: i32 = 75; +pub const THROTTLE_C: i32 = 85; +pub const CRITICAL_C: i32 = 95; +pub const HYST_C: i32 = 5; + +/// Чистая политика: `(предыдущий уровень, температура) → уровень` с гистерезисом (Schmitt по бандам). +pub struct ThermalPolicy; + +impl ThermalPolicy { + fn band_by_entry(t: i32) -> ThermalLevel { + if t >= CRITICAL_C { + ThermalLevel::Critical + } else if t >= THROTTLE_C { + ThermalLevel::Throttle(1) + } else if t >= WARN_C { + ThermalLevel::Warn + } else { + ThermalLevel::Normal + } + } + fn band_by_exit(t: i32) -> ThermalLevel { + // нижние (гистерезисные) пороги = entry − HYST + if t >= CRITICAL_C - HYST_C { + ThermalLevel::Critical + } else if t >= THROTTLE_C - HYST_C { + ThermalLevel::Throttle(1) + } else if t >= WARN_C - HYST_C { + ThermalLevel::Warn + } else { + ThermalLevel::Normal + } + } + + /// Подъём — по entry-порогам; спуск — по exit-порогам (entry − HYST) → нет осцилляции на границе. + pub fn next(prev: ThermalLevel, temp_c: i32) -> ThermalLevel { + let up = Self::band_by_entry(temp_c); + if up.rank() >= prev.rank() { + up + } else { + Self::band_by_exit(temp_c) + } + } +} + +#[cfg(test)] +mod policy_tests { + use super::*; + + #[test] + fn rises_by_entry_thresholds() { + assert_eq!(ThermalPolicy::next(ThermalLevel::Normal, 70), ThermalLevel::Normal); + assert_eq!(ThermalPolicy::next(ThermalLevel::Normal, 75), ThermalLevel::Warn); + assert_eq!(ThermalPolicy::next(ThermalLevel::Warn, 85), ThermalLevel::Throttle(1)); + assert_eq!(ThermalPolicy::next(ThermalLevel::Throttle(1), 95), ThermalLevel::Critical); + // прыжок вверх через банды + assert_eq!(ThermalPolicy::next(ThermalLevel::Normal, 99), ThermalLevel::Critical); + } + + #[test] + fn hysteresis_holds_until_below_exit() { + // critical держится до < 90 (95−5) + assert_eq!(ThermalPolicy::next(ThermalLevel::Critical, 92), ThermalLevel::Critical); + assert_eq!(ThermalPolicy::next(ThermalLevel::Critical, 89), ThermalLevel::Throttle(1)); + // warn держится до < 70 + assert_eq!(ThermalPolicy::next(ThermalLevel::Warn, 73), ThermalLevel::Warn); + assert_eq!(ThermalPolicy::next(ThermalLevel::Warn, 69), ThermalLevel::Normal); + } + + #[test] + fn no_oscillation_at_boundary() { + // на 84 (чуть ниже entry throttle=85): зависит от prev (Schmitt), не дёргается + assert_eq!(ThermalPolicy::next(ThermalLevel::Throttle(1), 84), ThermalLevel::Throttle(1)); + assert_eq!(ThermalPolicy::next(ThermalLevel::Warn, 84), ThermalLevel::Warn); + } +}