//! Read-only access to Linux hardware monitoring sysfs: hwmon chips
//! (voltages, temperatures, fans), per-cpu frequency, and RAPL energy
//! counters. This is the only module that touches the filesystem —
//! front-ends (dump, later the TUI) never read sysfs directly.
use std::fs;
use std::path::{Path, PathBuf};
/// A discovered hwmon chip, e.g. `/sys/class/hwmon/hwmon3` (nct6798).
pub struct Chip {
pub path: PathBuf,
pub name: String,
}
pub fn discover_chips() -> Vec<Chip> {
let mut chips = Vec::new();
if let Ok(entries) = fs::read_dir("/sys/class/hwmon") {
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = read_trimmed(&path.join("name")) {
chips.push(Chip { path, name });
}
}
}
chips.sort_by_key(|chip| hwmon_number(&chip.path));
chips
}
fn hwmon_number(path: &Path) -> u64 {
path.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.rsplit_once("hwmon"))
.and_then(|(_, digits)| digits.parse().ok())
.unwrap_or(u64::MAX)
}
fn read_trimmed(path: &Path) -> Option<String> {
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
}
fn read_int(path: &Path) -> Option<i64> {
read_trimmed(path)?.parse().ok()
}
/// All labeled numeric inputs matching `<prefix>N_input`, scaled by `scale`.
/// Prefers the kernel-provided `<prefix>N_label` when one exists, so e.g.
/// coretemp's "Core 0" / "Package id 0" names survive.
fn numbered_inputs(dir: &Path, prefix: &str, scale: f64) -> Vec<(String, f64)> {
let mut rows: Vec<(u64, String, f64)> = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let file = entry.file_name();
let file = match file.to_str() {
Some(f) => f,
None => continue,
};
let stem = match file.strip_suffix("_input") {
Some(s) => s,
None => continue,
};
let digits = match stem.strip_prefix(prefix) {
Some(d) => d,
None => continue,
};
let index = match digits.parse::<u64>() {
Ok(i) => i,
Err(_) => continue,
};
let raw = match read_int(&entry.path()) {
Some(v) => v,
None => continue,
};
let label = read_trimmed(&dir.join(format!("{stem}_label")))
.unwrap_or_else(|| stem.to_string());
rows.push((index, label, raw as f64 * scale));
}
}
rows.sort_by_key(|row| row.0);
rows.into_iter()
.map(|(_, label, value)| (label, value))
.collect()
}
/// Voltage inputs in volts (`in*_input` is millivolts).
pub fn voltages(chip: &Chip) -> Vec<(String, f64)> {
numbered_inputs(&chip.path, "in", 1.0 / 1000.0)
}
/// Temperature inputs in °C (`temp*_input` is millidegrees Celsius).
pub fn temperatures(chip: &Chip) -> Vec<(String, f64)> {
numbered_inputs(&chip.path, "temp", 1.0 / 1000.0)
}
/// Fan inputs in RPM.
pub fn fans(chip: &Chip) -> Vec<(String, f64)> {
numbered_inputs(&chip.path, "fan", 1.0)
}
/// Current per-cpu frequency in kHz from cpufreq sysfs (no root required).
pub fn cpu_frequencies() -> Vec<(usize, u64)> {
let mut out = Vec::new();
if let Ok(entries) = fs::read_dir("/sys/devices/system/cpu") {
for entry in entries.flatten() {
let name = entry.file_name();
let name = match name.to_str() {
Some(n) => n,
None => continue,
};
let cpu = match name
.strip_prefix("cpu")
.and_then(|d| d.parse::<usize>().ok())
{
Some(c) => c,
None => continue,
};
if let Some(khz) = read_int(&entry.path().join("cpufreq/scaling_cur_freq")) {
out.push((cpu, khz as u64));
}
}
}
out.sort();
out
}
/// A RAPL power domain with an `energy_uj` counter, e.g. package or core.
pub struct RaplDomain {
pub id: String, // sysfs dir name, e.g. "intel-rapl:0"
pub name: String, // kernel label, e.g. "package"
}
pub fn rapl_domains() -> Vec<RaplDomain> {
let mut out = Vec::new();
if let Ok(entries) = fs::read_dir("/sys/class/powercap") {
for entry in entries.flatten() {
let id = match entry.file_name().to_str() {
Some(i) => i.to_string(),
None => continue,
};
if !id.starts_with("intel-rapl") {
continue;
}
let path = entry.path();
// energy_uj exists on all domains but is 0400 root-only on many
// kernels — report the domain regardless and let reads fail loudly.
if !path.join("energy_uj").exists() {
continue;
}
let name = read_trimmed(&path.join("name")).unwrap_or_else(|| id.clone());
out.push(RaplDomain { id, name });
}
}
out.sort_by(|a, b| a.id.cmp(&b.id));
out
}
/// Cumulative energy in microjoules; deltas over time give watts.
pub fn rapl_energy_uj(domain: &str) -> Option<u64> {
read_int(
&Path::new("/sys/class/powercap")
.join(domain)
.join("energy_uj"),
)
.map(|e| e as u64)
}