feat(v0.4): TempSource/Throttler-абстракции + ThermalMonitor (A12/B10)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -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<AtomicI32>,
|
||||
}
|
||||
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::<i32>() {
|
||||
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<std::sync::Mutex<Option<ThermalLevel>>>,
|
||||
}
|
||||
impl NoopThrottler {
|
||||
pub fn last(&self) -> Option<ThermalLevel> {
|
||||
*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::*;
|
||||
|
||||
Reference in New Issue
Block a user