//! 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); `--log` = append CSV rows //! to ./aldermon-vid.log for off-line analysis. mod config; mod log; mod plot; mod sensors; mod ui; use std::io::IsTerminal; use std::path::Path; 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 spike (needs MSR access, see below) --log append CSV samples to ./aldermon-vid.log --help show this help --version print version and exit The TUI always shows the delivered vCore, the requested VID and the delta. VID needs MSR access: run as root, or install the setcap'd aldermon-msr helper (doas make install + aldermon-install.sh); otherwise it shows 'VID n/a (no msr access)'. --log appends one CSV row per poll; with --tui it logs live, with --vid it logs continuously until interrupted (otherwise --vid runs a 5-sample spike). Short flags group: -lvt == --log --vid --tui. -V == --version."; #[derive(Default, Clone, Copy)] struct Opts { tui: bool, vid: bool, log: bool, } pub const LOG_PATH: &str = "aldermon-vid.log"; fn main() { let args: Vec = std::env::args().collect(); let mut opts = Opts::default(); for a in &args[1..] { if let Some(long) = a.strip_prefix("--") { match long { "tui" => opts.tui = true, "vid" => opts.vid = true, "log" => opts.log = true, "version" => { println!("aldermon {}", env!("CARGO_PKG_VERSION")); return; } "help" => { println!("{USAGE}"); return; } _ => bad_flag(a), } } else if let Some(short) = a.strip_prefix('-') { for c in short.chars() { match c { 't' => opts.tui = true, 'v' => opts.vid = true, 'l' => opts.log = true, 'V' => { println!("aldermon {}", env!("CARGO_PKG_VERSION")); return; } 'h' => { println!("{USAGE}"); return; } _ => bad_flag(a), } } } } if opts.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(opts.log); return; } if opts.vid { vid_spike(opts.log); return; } if opts.log { eprintln!("aldermon: --log needs --tui or --vid (nothing to sample)\n{USAGE}"); std::process::exit(2); } dump(); } fn bad_flag(a: &str) -> ! { eprintln!("aldermon: unknown option '{a}'\n{USAGE}"); std::process::exit(2); } fn dump() { 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: compare SIO in0 (delivered rail) with the CPU's requested /// SVID setpoint (IA32_PERF_STATUS) per sample. Needs MSR access (root or /// the setcap'd aldermon-msr helper). /// Without `log` this is a bounded 5-sample spike; with `log` it runs until /// interrupted, appending CSV rows to ./aldermon-vid.log. fn vid_spike(log: bool) { let cpus = sensors::cpu_count(); let access = sensors::probe_msr(); let mut logger = if log { match log::VidLogger::open(Path::new(LOG_PATH), cpus) { Ok(l) => { println!("logging to ./{LOG_PATH} ({cpus} cpus) — Ctrl-C to stop"); Some(l) } Err(e) => { eprintln!("aldermon: cannot open {LOG_PATH}: {e}"); std::process::exit(1); } } } else { None }; if logger.is_none() { println!("== VID cross-check (MSR access: root, or setcap'd aldermon-msr) =="); println!("(hint: --log appends to ./{LOG_PATH} continuously)"); if !access.usable() { println!("(no MSR access — run under root, or install + setup the helper)"); } } // Package-energy counter for the logged watts (None until the second // sample; the first delta has no baseline). let pkg = rapl_domains() .into_iter() .find(|d| d.name.starts_with("package")); let mut prev_energy: Option<(u64, Instant)> = None; let rounds: u64 = if logger.is_some() { u64::MAX } else { 5 }; for round in 0..rounds { let vcore = sensors::sio_vcore(); let msrs = sensors::vid_msrs(access, cpus); let vmax = sensors::vid_max(&msrs); let freqs = cpu_frequencies(); // Watts over this inter-sample interval (same math as the TUI). let watts = pkg.as_ref().and_then(|d| { let e = sensors::rapl_energy_uj(&d.id)?; let w = prev_energy.and_then(|(prev, t0)| { let dt = t0.elapsed().as_secs_f64(); (dt > 0.0 && e >= prev).then(|| (e - prev) as f64 / 1_000_000.0 / dt) }); prev_energy = Some((e, Instant::now())); w }); if let Some(l) = logger.as_mut() { let sample = log::Sample { sio_vcore: vcore, vid_max: vmax, pkg_watts: watts, vid_msr: msrs.clone(), freq_khz: freq_by_cpu(&freqs, cpus), }; if let Err(e) = l.log(&sample) { eprintln!("aldermon: log write failed: {e}"); } match (vcore, vmax) { (Some(got), Some(ask)) => { println!("in0 {got:.3} V VID {ask:.3} V Δ{:+.3}", got - ask) } (Some(got), None) => println!("in0 {got:.3} V VID unreadable"), (None, Some(ask)) => println!("in0 unreadable VID {ask:.3} V"), (None, None) => println!("in0 unreadable VID unreadable"), } } else { println!("-- sample {} --", round); match vcore { Some(v) => println!(" SIO in0 (vCore): {v:.3} V"), None => println!(" SIO in0 (vCore): UNAVAILABLE"), } for (cpu, msr) in msrs.iter().enumerate() { match msr { Some(m) => println!( " cpu{cpu:>2} VID: {:.3} V (0x{m:016x})", sensors::vid_from_msr(*m) ), None => println!(" cpu{cpu:>2} VID: unreadable"), } } } sleep(Duration::from_millis(400)); } } /// Per-logical-cpu frequency vector (kHz), empty where a cpu has no reading. fn freq_by_cpu(freqs: &[(usize, u64)], cpus: usize) -> Vec> { let mut out = vec![None; cpus]; for (cpu, khz) in freqs { if let Some(slot) = out.get_mut(*cpu) { *slot = Some(*khz); } } out }