//! aldermon — Alder Lake hardware monitor (i5-12600KF / Z690M ITX/ax). //! //! Modes: no args = one-shot sensor dump; `--tui` = live TUI (vCore hero, //! per-core bars, braille graphs, F2 settings); `--vid` = root-only VID vs //! in0 cross-check (debug spike, kept as a tool). mod config; mod plot; mod sensors; mod ui; use std::io::IsTerminal; use std::thread::sleep; use std::time::{Duration, Instant}; use sensors::{cpu_frequencies, discover_chips, rapl_domains, rapl_energy_uj}; const USAGE: &str = "\ usage: aldermon [OPTION] (no option) one-shot sensor dump to stdout --tui live monitoring TUI (q/Esc quit, F2 settings) --vid VID vs SIO in0 cross-check (needs root + msr module) --help show this help"; fn main() { let args: Vec = std::env::args().collect(); for a in &args[1..] { if a.starts_with('-') && !matches!(a.as_str(), "--tui" | "--vid" | "--help") { eprintln!("aldermon: unknown option '{a}'\n{USAGE}"); std::process::exit(2); } } if args.iter().any(|a| a == "--help") { println!("{USAGE}"); return; } if args.iter().any(|a| a == "--vid") { vid_spike(); return; } if args.iter().any(|a| a == "--tui") { if !std::io::stdout().is_terminal() { eprintln!("aldermon: --tui needs a real terminal (stdout is not a tty)"); std::process::exit(1); } ui::run_tui(); return; } println!("== hwmon =="); for chip in discover_chips() { println!("[{}] {}", chip.name, chip.path.display()); for (label, volts) in sensors::voltages(&chip) { println!(" {:>18}: {:8.3} V", label, volts); } for (label, temp) in sensors::temperatures(&chip) { println!(" {:>18}: {:7.1} °C", label, temp); } for (label, rpm) in sensors::fans(&chip) { println!(" {:>18}: {:7.0} RPM", label, rpm); } } println!("\n== cpu frequency (MHz) =="); for (cpu, khz) in cpu_frequencies() { println!(" cpu{:>2}: {:8.1} MHz", cpu, khz as f64 / 1000.0); } println!("\n== RAPL power (sampled over 500 ms) =="); let domains = rapl_domains(); let first: Vec> = domains.iter().map(|d| rapl_energy_uj(&d.id)).collect(); let start = Instant::now(); sleep(Duration::from_millis(500)); let elapsed = start.elapsed().as_secs_f64(); for (domain, first) in domains.iter().zip(first) { match (first, rapl_energy_uj(&domain.id)) { (Some(a), Some(b)) if b >= a => { let watts = (b - a) as f64 / 1_000_000.0 / elapsed; println!(" {:>18} ({}): {:6.2} W", domain.name, domain.id, watts); } _ => println!(" {:>18} ({}): unreadable", domain.name, domain.id), } } } /// VID cross-check spike: compare SIO in0 (delivered rail) with the CPU's /// reported VID (IA32_PERF_STATUS) per sample. Requires root + msr module. fn vid_spike() { println!("== VID cross-check (needs root: /dev/cpu/*/msr) =="); for round in 0..5 { let vcore = sensors::sio_vcore(); let vids: Vec> = (0..16).map(sensors::vid_volts).collect(); println!("-- sample {} --", round); match vcore { Some(v) => println!(" SIO in0 (vCore): {:?}", v), None => println!(" SIO in0 (vCore): UNAVAILABLE"), } for (cpu, vid) in vids.iter().enumerate() { match vid { Some(v) => println!(" cpu{:>2} VID: {:?}", cpu, v), None => println!(" cpu{:>2} VID: unreadable", cpu), } } sleep(Duration::from_millis(400)); } }