diff --git a/adlermon.conf b/adlermon.conf
new file mode 100644
index 0000000..173849a
--- /dev/null
+++ b/adlermon.conf
@@ -0,0 +1,16 @@
+# adlermon board config — ASRock Z690M ITX/ax (nct6798)
+#
+# Labels marked ? are best guesses from voltage patterns; verify against the
+# BIOS HW Monitor and fix the label. Unwired inputs read floating garbage:
+# set rail.inN.hide = true to hide one, or give it a label to show it.
+# For other boards: delete/override rows freely. Full key list in
+# src/config.rs (vcore_limit, temp_warn, temp_crit, vcore_bar_max,
+# power_bar_max, rail.inN.label, rail.inN.hide).
+
+# vcore_limit = 1.403 # Intel stock spec for 12600KF
+# temp_warn = 80.0
+# temp_crit = 95.0
+# vcore_bar_max = 1.50 # bar scale floors; auto-scale rises above these
+# power_bar_max = 200.0
+
+# rail.in12.label = VCCSA # example: relabel an input
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..91a573f
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,167 @@
+//! Static board config: rail labels + display limits. Zero deps: simple
+//! `KEY = VALUE` lines, `#` comments, blank lines ignored. Unknown keys are
+//! ignored (forward compatibility). Loaded once at startup.
+
+/// Which hwmon input of the SIO chip maps to what. `(key, label)` pairs;
+/// the key is the sysfs stem, e.g. "in0" → `in0_input`.
+pub struct Rail {
+ pub input: String,
+ pub label: String,
+ /// Inputs not connected on this board report garbage (floating) values;
+ /// hide them unless the user maps them.
+ pub hide: bool,
+}
+
+pub struct Config {
+ pub rails: Vec<Rail>,
+ pub vcore_limit: f64,
+ pub temp_warn: f64,
+ pub temp_crit: f64,
+ pub vcore_bar_max: f64,
+ pub power_bar_max: f64,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Config {
+ rails: default_rails(),
+ vcore_limit: 1.403,
+ temp_warn: 80.0,
+ temp_crit: 95.0,
+ vcore_bar_max: 1.50,
+ power_bar_max: 200.0,
+ }
+ }
+}
+
+/// Best-guess rail map for ASRock Z690M ITX/ax (nct6798). Voltage evidence
+/// from 2026-08-29 probe; labels are GUESSES except in0/12 (load-tested) —
+/// verify against the BIOS HW monitor and fix in the config file.
+pub fn default_rails() -> Vec<Rail> {
+ vec![
+ rail("in0", "CPU Vcore", false),
+ rail("in1", "VCCSA?", false),
+ rail("in2", "+5V?", false),
+ rail("in3", "+3.3V", false),
+ rail("in4", "VCCIN?", false),
+ rail("in5", "", true),
+ rail("in6", "", true),
+ rail("in7", "+5V?", false),
+ rail("in8", "3VSB", false),
+ rail("in9", "VCCIO?", false),
+ rail("in10", "", true),
+ rail("in11", "", true),
+ rail("in12", "VCCSA?", false),
+ rail("in13", "", true),
+ rail("in14", "DRAM VDD", false),
+ ]
+}
+
+fn rail(input: &str, label: &str, hide: bool) -> Rail {
+ Rail {
+ input: input.to_string(),
+ label: label.to_string(),
+ hide,
+ }
+}
+
+/// Parse config text over a default Config. Lines:
+/// rail.in12.label = VCCSA
+/// rail.in5.hide = true
+/// vcore_limit = 1.35
+fn apply_line(cfg: &mut Config, line: &str) {
+ let line = line.split('#').next().unwrap_or("").trim();
+ if line.is_empty() {
+ return;
+ }
+ let Some((key, val)) = line.split_once('=') else {
+ return;
+ };
+ let key = key.trim();
+ let val = val.trim();
+ match key {
+ "vcore_limit" => cfg.vcore_limit = val.parse().unwrap_or(cfg.vcore_limit),
+ "temp_warn" => cfg.temp_warn = val.parse().unwrap_or(cfg.temp_warn),
+ "temp_crit" => cfg.temp_crit = val.parse().unwrap_or(cfg.temp_crit),
+ "vcore_bar_max" => cfg.vcore_bar_max = val.parse().unwrap_or(cfg.vcore_bar_max),
+ "power_bar_max" => cfg.power_bar_max = val.parse().unwrap_or(cfg.power_bar_max),
+ "rail.hide" => return, // needs input suffix; handled below
+ _ => {}
+ }
+ if let Some(rest) = key.strip_prefix("rail.") {
+ match rest.split_once('.') {
+ Some((input, "label")) => set_rail(cfg, input, val, None),
+ Some((input, "hide")) => set_rail(cfg, input, "", Some(val == "true")),
+ _ => {}
+ }
+ }
+}
+
+fn set_rail(cfg: &mut Config, input: &str, label: &str, hide: Option<bool>) {
+ if let Some(r) = cfg.rails.iter_mut().find(|r| r.input == input) {
+ if !label.is_empty() {
+ r.label = label.to_string();
+ }
+ if let Some(h) = hide {
+ r.hide = h;
+ }
+ } else {
+ cfg.rails.push(Rail {
+ input: input.to_string(),
+ label: label.to_string(),
+ hide: hide.unwrap_or(false),
+ });
+ }
+}
+
+pub fn load() -> Config {
+ let mut cfg = Config::default();
+ // Search order: ./adlermon.conf then ~/.config/adlermon/adlermon.conf
+ let candidates = [
+ Some(std::path::PathBuf::from("adlermon.conf")),
+ dirs_home().map(|h| h.join(".config/adlermon/adlermon.conf")),
+ ];
+ for path in candidates.into_iter().flatten() {
+ if let Ok(text) = std::fs::read_to_string(&path) {
+ for line in text.lines() {
+ apply_line(&mut cfg, line);
+ }
+ break;
+ }
+ }
+ cfg
+}
+
+fn dirs_home() -> Option<std::path::PathBuf> {
+ std::env::var_os("HOME").map(std::path::PathBuf::from)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_rail_label() {
+ let mut cfg = Config::default();
+ apply_line(&mut cfg, "rail.in12.label = CPU Vccio # note");
+ assert_eq!(cfg.rails.iter().find(|r| r.input == "in12").unwrap().label, "CPU Vccio");
+ }
+
+ #[test]
+ fn parses_hide_and_limits() {
+ let mut cfg = Config::default();
+ apply_line(&mut cfg, "rail.in5.hide = true");
+ apply_line(&mut cfg, "vcore_limit = 1.35");
+ assert!(cfg.rails.iter().find(|r| r.input == "in5").unwrap().hide);
+ assert_eq!(cfg.vcore_limit, 1.35);
+ }
+
+ #[test]
+ fn tolerates_junk() {
+ let mut cfg = Config::default();
+ apply_line(&mut cfg, "not a config line");
+ apply_line(&mut cfg, "unknown_key = 42");
+ apply_line(&mut cfg, "vcore_limit = notanumber");
+ assert_eq!(cfg.vcore_limit, 1.403);
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 40f06ac..f9eedb3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@
//! Currently a dump tool: prints a one-shot sensor snapshot to stdout. The
//! TUI consumes the same `sensors` layer once the vCore source spike is done.
+mod config;
mod sensors;
mod ui;
diff --git a/src/sensors.rs b/src/sensors.rs
index d243d48..29ad604 100644
--- a/src/sensors.rs
+++ b/src/sensors.rs
@@ -208,9 +208,14 @@ pub fn vid_volts(cpu: usize) -> Option<f64> {
/// Sampled vCore from the nct6798 SIO (hwmon in0). Find the chip by NAME,
/// never by index — hwmon numbering shifts between boots.
pub fn sio_vcore() -> Option<f64> {
- let chip = discover_chips().into_iter().find(|c| c.name == "nct6798")?;
+ sio_input("in0")
+}
+
+/// Any SIO voltage input by sysfs stem ("in0", "in12", …).
+pub fn sio_input(input: &str) -> Option<f64> {
+ let chip = discover_chips().into_iter().find(|c| c.name.starts_with("nct"))?;
voltages(&chip)
.into_iter()
- .find(|(label, _)| label == "in0")
+ .find(|(label, _)| label == input)
.map(|(_, v)| v)
}
diff --git a/src/ui.rs b/src/ui.rs
index aab61be..7596606 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -11,26 +11,20 @@ use ratatui::{
text::{Line, Span},
};
+use crate::config;
use crate::sensors;
const TICK_MS: u64 = 250;
const PEAK_WINDOW: Duration = Duration::from_secs(5 * 60);
/// vCore shown Yellow when within this margin of the limit, Red over it.
const VCORE_MARGIN: f64 = 0.05;
-/// Safety limit, volts. User-tunable config value. Intel max Vcore
-/// spec default for the 12600KF.
-const VCORE_LIMIT: f64 = 1.403;
-const VCORE_BAR_MAX: f64 = 1.50; // default; auto-scaled above by peak
-const TEMP_WARN: f64 = 80.0;
-const TEMP_CRIT: f64 = 95.0;
-/// Package power bar scale, watts (default; auto-scaled above by peak).
-const POWER_BAR_MAX: f64 = 200.0;
/// Auto-scale headroom above the session peak, as a fraction.
const SCALE_MARGIN: f64 = 0.05;
/// Temp bar scale, °C.
const TEMP_BAR_MAX: f64 = 100.0;
pub struct App {
+ cfg: config::Config,
vcore: Option<f64>,
vcore_peak: Option<f64>,
/// (timestamp, volts) per-tick maxima inside the 5-min peak window
@@ -65,7 +59,9 @@ struct CoreRow {
impl App {
pub fn new() -> App {
+ let cfg = config::load();
App {
+ cfg,
vcore: None,
vcore_peak: None,
peak_ring: Vec::new(),
@@ -193,20 +189,20 @@ fn core_to_cpus(core_id: u64) -> Vec<usize> {
}
}
-fn temp_color(t: f64) -> Color {
- if t >= TEMP_CRIT {
+fn temp_color(t: f64, warn: f64, crit: f64) -> Color {
+ if t >= crit {
Color::Red
- } else if t >= TEMP_WARN {
+ } else if t >= warn {
Color::Yellow
} else {
Color::Green
}
}
-fn vcore_color(v: f64) -> Color {
- if v >= VCORE_LIMIT {
+fn vcore_color(v: f64, limit: f64) -> Color {
+ if v >= limit {
Color::Red
- } else if v >= VCORE_LIMIT - VCORE_MARGIN {
+ } else if v >= limit - VCORE_MARGIN {
Color::Yellow
} else {
Color::Green
@@ -292,7 +288,7 @@ pub fn draw(f: &mut Frame, app: &App) {
// ---- vCore hero ----
let vc_color = match app.vcore {
- Some(v) => vcore_color(v),
+ Some(v) => vcore_color(v, app.cfg.vcore_limit),
None => Color::DarkGray,
};
let hero_text = match (app.vcore, app.vcore_peak) {
@@ -306,8 +302,16 @@ pub fn draw(f: &mut Frame, app: &App) {
)));
// Bar maxima auto-scale: session all-time peak + margin, floored at the
// static defaults. OC'ing past a default compresses the bar to fit.
- let vc_max = dynamic_max(app.vcore_all_time_peak, VCORE_BAR_MAX, SCALE_MARGIN);
- let watt_max = dynamic_max(app.watts_all_time_peak, POWER_BAR_MAX, SCALE_MARGIN);
+ let vc_max = dynamic_max(
+ app.vcore_all_time_peak,
+ app.cfg.vcore_bar_max,
+ SCALE_MARGIN,
+ );
+ let watt_max = dynamic_max(
+ app.watts_all_time_peak,
+ app.cfg.power_bar_max,
+ SCALE_MARGIN,
+ );
lines.push(meter_line(
"",
@@ -315,10 +319,38 @@ pub fn draw(f: &mut Frame, app: &App) {
app.vcore.map(|v| v / vc_max),
vc_color,
w,
- Some(VCORE_LIMIT / vc_max),
+ Some(app.cfg.vcore_limit / vc_max),
));
lines.push(Line::from(""));
+ // ---- rails (config-mapped SIO inputs) ----
+ let rail_rows: Vec<(String, Option<f64>)> = app
+ .cfg
+ .rails
+ .iter()
+ .filter(|r| !r.hide && r.input != "in0") // in0 shown as the hero
+ .map(|r| (r.label.clone(), sensors::sio_input(&r.input)))
+ .collect();
+ if rail_rows.iter().any(|(_, v)| v.is_some()) {
+ lines.push(Line::from(Span::styled(
+ "Rails",
+ Style::default().fg(Color::Cyan),
+ )));
+ for (label, v) in &rail_rows {
+ let label = if label.is_empty() {
+ "??".to_string()
+ } else {
+ label.clone()
+ };
+ let text = match v {
+ Some(v) => format!("{:.3} V", v),
+ None => "n/a".to_string(),
+ };
+ lines.push(meter_line(&label, &text, None, Color::Magenta, w, None));
+ }
+ lines.push(Line::from(""));
+ }
+
// ---- package ----
lines.push(meter_line(
"Pkg power",
@@ -332,7 +364,9 @@ pub fn draw(f: &mut Frame, app: &App) {
"Pkg temp",
&fmt_opt_temp(app.package_temp),
app.package_temp.map(|t| t / TEMP_BAR_MAX),
- app.package_temp.map_or(Color::DarkGray, temp_color),
+ app.package_temp.map_or(Color::DarkGray, |t| {
+ temp_color(t, app.cfg.temp_warn, app.cfg.temp_crit)
+ }),
w,
None,
));
@@ -366,7 +400,7 @@ pub fn draw(f: &mut Frame, app: &App) {
_ => None,
};
let color = match core.temp {
- Some(t) => temp_color(t),
+ Some(t) => temp_color(t, app.cfg.temp_warn, app.cfg.temp_crit),
None => Color::DarkGray,
};
lines.push(meter_line(&core.label, &text, ratio, color, w, None));
Config file: board rail map + display limits; Rails section in TUI
16cdf793c3a122e1c728c16a2565a8246a824551
josie <administrator@josie-c.com> · 2026-08-29T12:41 ·
browse files at this commit
parents:
0f53d38