feat(v0.4): чистый ThermalPolicy (банды + гистерезис, A12/B10)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-25 15:27:29 +03:00
parent fb31a288c3
commit b9ae2f23d5
2 changed files with 108 additions and 0 deletions
+1
View File
@@ -3,5 +3,6 @@
pub mod fsm;
pub mod service;
pub mod thermal;
pub use service::PowerService;
+107
View File
@@ -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 (955)
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);
}
}