From 2e6144c54f15068771390a0589a2690216bd7575 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 25 Jun 2026 15:32:53 +0300 Subject: [PATCH] =?UTF-8?q?feat(v0.4):=20TempSource/Throttler-=D0=B0=D0=B1?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BA=D1=86=D0=B8=D0=B8=20+=20ThermalM?= =?UTF-8?q?onitor=20(A12/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/thermal.rs | 150 ++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/crates/core/shturman-power/src/thermal.rs b/crates/core/shturman-power/src/thermal.rs index 66e17b3..c5e4de4 100644 --- a/crates/core/shturman-power/src/thermal.rs +++ b/crates/core/shturman-power/src/thermal.rs @@ -34,6 +34,7 @@ pub const WARN_C: i32 = 75; pub const THROTTLE_C: i32 = 85; pub const CRITICAL_C: i32 = 95; pub const HYST_C: i32 = 5; +pub const POLL_SECS: u64 = 1; // период опроса температуры (монотоника) /// Чистая политика: `(предыдущий уровень, температура) → уровень` с гистерезисом (Schmitt по бандам). pub struct ThermalPolicy; @@ -74,6 +75,155 @@ impl ThermalPolicy { } } +use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::Arc; + +/// Источник температуры (°C). real = sysfs; VM = mock. +pub trait TempSource: Send + Sync { + fn read_celsius(&self) -> i32; +} + +/// Mock-источник (dev): температуру задаёт `SetTemp` через dev-D-Bus. +#[derive(Clone)] +pub struct MockTempSource { + temp: Arc, +} +impl MockTempSource { + pub fn new(init_c: i32) -> Self { + Self { + temp: Arc::new(AtomicI32::new(init_c)), + } + } + pub fn set(&self, c: i32) { + self.temp.store(c, Ordering::Relaxed); + } +} +impl TempSource for MockTempSource { + fn read_celsius(&self) -> i32 { + self.temp.load(Ordering::Relaxed) + } +} + +/// Прод: max по `/sys/class/thermal/thermal_zone*/temp` (миллиградусы). В Lima зоны статичны → числа на RK3588. +#[derive(Default)] +pub struct SysfsTempSource; +impl SysfsTempSource { + pub fn new() -> Self { + Self + } +} +impl TempSource for SysfsTempSource { + fn read_celsius(&self) -> i32 { + let mut max = i32::MIN; + if let Ok(rd) = std::fs::read_dir("/sys/class/thermal") { + for e in rd.flatten() { + let p = e.path().join("temp"); + if let Ok(s) = std::fs::read_to_string(&p) { + if let Ok(milli) = s.trim().parse::() { + max = max.max(milli / 1000); + } + } + } + } + if max == i32::MIN { + 0 + } else { + max + } + } +} + +/// Применение throttle. real = cpufreq (HW); VM = запись уровня (no-op-эффект). +pub trait Throttler: Send + Sync { + fn apply(&self, level: ThermalLevel); +} + +/// VM/прод-каркас: логирует + запоминает последний уровень (реальный cpufreq — HW). +#[derive(Default, Clone)] +pub struct NoopThrottler { + last: Arc>>, +} +impl NoopThrottler { + pub fn last(&self) -> Option { + *self.last.lock().unwrap() + } +} +impl Throttler for NoopThrottler { + fn apply(&self, level: ThermalLevel) { + *self.last.lock().unwrap() = Some(level); + tracing::info!( + "thermal: throttle уровень {} (эффект cpufreq — HW)", + level.as_str() + ); + } +} + +/// Результат шага монитора: уровень + рёбра входа/выхода Critical (для FSM ThermalTrip/ThermalCleared). +#[derive(Debug, PartialEq, Eq)] +pub struct ThermalObservation { + pub level: ThermalLevel, + pub changed: bool, + pub entered_critical: bool, + pub left_critical: bool, +} + +/// Монитор: хранит предыдущий уровень, применяет политику, размечает рёбра Critical. +pub struct ThermalMonitor { + prev: ThermalLevel, +} +impl Default for ThermalMonitor { + fn default() -> Self { + Self { + prev: ThermalLevel::Normal, + } + } +} +impl ThermalMonitor { + pub fn new() -> Self { + Self::default() + } + pub fn observe(&mut self, temp_c: i32) -> ThermalObservation { + let level = ThermalPolicy::next(self.prev, temp_c); + let changed = level != self.prev; + let entered_critical = changed && level == ThermalLevel::Critical; + let left_critical = changed && self.prev == ThermalLevel::Critical; + self.prev = level; + ThermalObservation { + level, + changed, + entered_critical, + left_critical, + } + } +} + +#[cfg(test)] +mod monitor_tests { + use super::*; + + #[test] + fn marks_critical_edges() { + let mut m = ThermalMonitor::new(); + let o = m.observe(96); + assert!(o.entered_critical && o.changed && o.level == ThermalLevel::Critical); + let o = m.observe(96); // держится — рёбер нет + assert!(!o.changed && !o.entered_critical); + let o = m.observe(80); // < 90 → выход из critical + assert!(o.left_critical && o.level == ThermalLevel::Throttle(1)); + } + + #[test] + fn mock_source_and_noop_throttler() { + let src = MockTempSource::new(20); + assert_eq!(src.read_celsius(), 20); + src.set(88); + assert_eq!(src.read_celsius(), 88); + let th = NoopThrottler::default(); + th.apply(ThermalLevel::Throttle(1)); + assert_eq!(th.last(), Some(ThermalLevel::Throttle(1))); + } +} + #[cfg(test)] mod policy_tests { use super::*;