use cr1140_hal::sys::{read_temp_c, SOC_THERMAL_ZONE};
use std::fs;
pub struct CpuSampler {
prev_idle: u64,
prev_total: u64,
primed: bool,
}
impl CpuSampler {
pub fn new() -> Self {
Self {
prev_idle: 0,
prev_total: 0,
primed: false,
}
}
pub fn sample(&mut self) -> Option<f32> {
let s = fs::read_to_string("/proc/stat").ok()?;
let (idle, total) = parse_stat(&s)?;
let pct = if self.primed && total > self.prev_total {
let di = idle.saturating_sub(self.prev_idle);
let dt = total - self.prev_total;
(1.0 - di as f32 / dt as f32) * 100.0
} else {
0.0
};
self.prev_idle = idle;
self.prev_total = total;
self.primed = true;
Some(pct.clamp(0.0, 100.0))
}
}
impl Default for CpuSampler {
fn default() -> Self {
Self::new()
}
}
pub fn parse_stat(content: &str) -> Option<(u64, u64)> {
let line = content.lines().next()?;
let mut it = line.split_whitespace();
if it.next()? != "cpu" {
return None;
}
let vals: Vec<u64> = it.filter_map(|t| t.parse().ok()).collect();
if vals.len() < 4 {
return None;
}
let idle = vals[3] + vals.get(4).copied().unwrap_or(0); let total: u64 = vals.iter().sum();
Some((idle, total))
}
pub fn parse_meminfo(content: &str) -> Option<(u64, u64)> {
let mut total = None;
let mut avail = None;
for l in content.lines() {
if let Some(v) = l.strip_prefix("MemTotal:") {
total = v.split_whitespace().next().and_then(|n| n.parse().ok());
} else if let Some(v) = l.strip_prefix("MemAvailable:") {
avail = v.split_whitespace().next().and_then(|n| n.parse().ok());
}
}
Some((total?, avail?))
}
pub fn read_meminfo() -> Option<(u64, u64)> {
parse_meminfo(&fs::read_to_string("/proc/meminfo").ok()?)
}
pub fn mem_used_percent(total: u64, avail: u64) -> f32 {
if total == 0 {
0.0
} else {
((1.0 - avail as f32 / total as f32) * 100.0).clamp(0.0, 100.0)
}
}
pub fn parse_uptime(content: &str) -> Option<f64> {
content.split_whitespace().next()?.parse().ok()
}
pub fn read_uptime() -> Option<f64> {
parse_uptime(&fs::read_to_string("/proc/uptime").ok()?)
}
pub fn format_uptime(secs: f64) -> String {
let s = secs as u64;
let (h, m, sec) = (s / 3600, (s % 3600) / 60, s % 60);
if h > 0 {
format!("{h}h {m:02}m {sec:02}s")
} else {
format!("{m}m {sec:02}s")
}
}
pub fn parse_loadavg(content: &str) -> Option<f32> {
content.split_whitespace().next()?.parse().ok()
}
pub fn read_loadavg() -> Option<f32> {
parse_loadavg(&fs::read_to_string("/proc/loadavg").ok()?)
}
#[derive(Clone, Copy, Debug)]
pub struct MemInfo {
pub total_kb: u64,
pub avail_kb: u64,
}
impl MemInfo {
pub fn used_percent(&self) -> f32 {
mem_used_percent(self.total_kb, self.avail_kb)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Snapshot {
pub cpu_percent: Option<f32>,
pub mem: Option<MemInfo>,
pub soc_temp_c: Option<f32>,
pub board_temp_c: Option<f32>,
pub uptime_secs: Option<f64>,
pub load1: Option<f32>,
}
pub struct Telemetry {
cpu: CpuSampler,
soc_zone: u32,
}
impl Telemetry {
pub fn new() -> Self {
Self {
cpu: CpuSampler::new(),
soc_zone: SOC_THERMAL_ZONE,
}
}
pub fn with_soc_zone(zone: u32) -> Self {
Self {
cpu: CpuSampler::new(),
soc_zone: zone,
}
}
pub fn sample(&mut self) -> Snapshot {
Snapshot {
cpu_percent: self.cpu.sample(),
mem: read_meminfo().map(|(total_kb, avail_kb)| MemInfo { total_kb, avail_kb }),
soc_temp_c: read_temp_c(self.soc_zone).ok(),
board_temp_c: crate::device::read_board_temp_c(),
uptime_secs: read_uptime(),
load1: read_loadavg(),
}
}
}
impl Default for Telemetry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_stat_sums_and_idles() {
let s = "cpu 100 10 40 800 50 0 0\ncpu0 1 2 3 4\n";
let (idle, total) = parse_stat(s).unwrap();
assert_eq!(idle, 800 + 50);
assert_eq!(total, 100 + 10 + 40 + 800 + 50);
}
#[test]
fn parse_stat_rejects_non_cpu() {
assert_eq!(parse_stat("intr 1 2 3\n"), None);
}
#[test]
fn parse_meminfo_extracts_total_and_avail() {
let s = "MemTotal: 1019600 kB\nMemFree: 200000 kB\nMemAvailable: 509800 kB\n";
assert_eq!(parse_meminfo(s), Some((1019600, 509800)));
}
#[test]
fn mem_used_percent_is_complement_of_available() {
assert!((mem_used_percent(1000, 250) - 75.0).abs() < 0.001);
assert_eq!(mem_used_percent(0, 0), 0.0);
}
#[test]
fn parse_uptime_reads_first_field() {
assert_eq!(parse_uptime("12345.67 98765.43\n"), Some(12345.67));
}
#[test]
fn format_uptime_switches_on_hours() {
assert_eq!(format_uptime(65.0), "1m 05s");
assert_eq!(format_uptime(3725.0), "1h 02m 05s");
}
#[test]
fn parse_loadavg_reads_first_field() {
assert_eq!(parse_loadavg("0.17 0.08 0.08 1/99 12009"), Some(0.17));
assert_eq!(parse_loadavg(""), None);
}
#[test]
fn read_meminfo_shape_is_total_ge_avail_when_present() {
if let Some((total, avail)) = read_meminfo() {
assert!(total >= avail, "total {total} < avail {avail}");
assert!(total > 0);
}
}
#[test]
fn read_uptime_is_nonnegative_when_present() {
if let Some(secs) = read_uptime() {
assert!(secs >= 0.0);
}
}
#[test]
fn meminfo_used_percent_matches_helper() {
let m = MemInfo {
total_kb: 1000,
avail_kb: 250,
};
assert!((m.used_percent() - 75.0).abs() < 0.001);
let zero = MemInfo {
total_kb: 0,
avail_kb: 0,
};
assert_eq!(zero.used_percent(), 0.0);
}
#[test]
fn telemetry_sample_first_cpu_is_zero_then_populates() {
let mut t = Telemetry::new();
let first = t.sample();
if let Some(p) = first.cpu_percent {
assert_eq!(p, 0.0);
}
}
}