d51d870d7c6ff5656ca131dc1de08b117399a1a5 / aldermon/src/config.rs · 12967 bytes · raw
//! 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,
/// Peak-clock plot scale, kHz (P-core turbo ceiling, 4.5 GHz).
pub clock_bar_max: f64,
/// Temp graph scale, °C (shared by the graph scale and meter max).
pub temp_max: f64,
/// Per-graph scale floors (y-min; 0.0 = bottom of the box).
pub vcore_min: f64,
pub clock_min: f64,
pub power_min: f64,
pub temp_min: f64,
/// Core-column arrangement: auto (2 cols if wide), single, or dual.
pub layout: Layout,
/// Show nvtop-style line graphs in the package zone.
pub graphs: bool,
/// Tick / sample period, ms.
pub poll_ms: u64,
/// Graph window (seconds of history shown). History kept is capped by
/// the ring size (see plot::Ring::new).
pub graph_secs: u64,
/// Graph vertical scale: auto (window peak + margin) or fixed (the
/// bar_max limits below — no rescaling mid-benchmark).
pub graph_scale_fixed: bool,
/// Path the config was loaded from (save-back target; None = never
/// loaded, defaults in effect).
pub loaded_path: Option<std::path::PathBuf>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Layout {
Auto,
Single,
Dual,
}
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,
clock_bar_max: 4_500_000.0,
temp_max: 100.0,
vcore_min: 0.0,
clock_min: 0.0,
power_min: 0.0,
temp_min: 0.0,
layout: Layout::Auto,
graphs: true,
poll_ms: 250,
graph_secs: 300,
graph_scale_fixed: false,
loaded_path: None,
}
}
}
/// Default rail map: ASRock Z690M ITX/ax (nct6798). in0 CONFIRMED Vcore
/// (shown as the hero meter, not a rail row). v2 view: only CPU-relevant
/// rails visible by default (in4 VCCIO?, in9 1.05V PROC); the others stay
/// mapped but hidden — re-enable via `rail.inN.hide = false` in the conf.
/// Differential stress-ng probing 2026-08-29 + BIOS cross-check; "?" =
/// evidence-based guess.
pub fn default_rails() -> Vec<Rail> {
vec![
rail("in0", "CPU Vcore", false),
rail("in1", "Fixed 1.01", true),
rail("in2", "Fixed 3.42", true),
rail("in3", "+3.3V", true),
rail("in4", "VCCIO?", false),
rail("in5", "", true),
rail("in6", "", true),
rail("in7", "Fixed 3.42", true),
rail("in8", "3.3V aux", true),
rail("in9", "1.05V PROC", false),
rail("in10", "", true),
rail("in11", "", true),
rail("in12", "Fixed 1.14", true),
rail("in13", "", true),
rail("in14", "Fixed 1.5V", true),
]
}
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),
"clock_bar_max" => cfg.clock_bar_max = val.parse().unwrap_or(cfg.clock_bar_max),
"temp_max" => cfg.temp_max = val.parse().unwrap_or(cfg.temp_max),
"vcore_min" => cfg.vcore_min = val.parse().unwrap_or(cfg.vcore_min),
"clock_min" => cfg.clock_min = val.parse().unwrap_or(cfg.clock_min),
"power_min" => cfg.power_min = val.parse().unwrap_or(cfg.power_min),
"temp_min" => cfg.temp_min = val.parse().unwrap_or(cfg.temp_min),
"graphs" => cfg.graphs = val == "true",
"poll_ms" => cfg.poll_ms = val.parse().unwrap_or(cfg.poll_ms).max(50),
"graph_secs" => cfg.graph_secs = val.parse().unwrap_or(cfg.graph_secs).max(5),
"graph_scale_fixed" => cfg.graph_scale_fixed = val == "true",
"layout" => {
cfg.layout = match val {
"single" => Layout::Single,
"dual" => Layout::Dual,
_ => Layout::Auto,
}
}
"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);
}
cfg.loaded_path = Some(path);
break;
}
}
cfg
}
/// Serialize the settings editable in the settings pane. Written back to
/// the loaded conf path (F10); rail map and limits stay as-is on disk.
pub fn settings_text(cfg: &Config) -> String {
format!(
"poll_ms = {}\ngraph_secs = {}\ngraph_scale_fixed = {}\ngraphs = {}\nlayout = {}\nvcore_limit = {:.3}\ntemp_warn = {:.1}\ntemp_crit = {:.1}\nvcore_min = {:.3}\nvcore_bar_max = {:.3}\nclock_min = {:.0}\nclock_bar_max = {:.0}\npower_min = {:.1}\npower_bar_max = {:.1}\ntemp_min = {:.0}\ntemp_max = {:.0}\n",
cfg.poll_ms,
cfg.graph_secs,
cfg.graph_scale_fixed,
cfg.graphs,
match cfg.layout {
Layout::Auto => "auto",
Layout::Single => "single",
Layout::Dual => "dual",
},
cfg.vcore_limit,
cfg.temp_warn,
cfg.temp_crit,
cfg.vcore_min,
cfg.vcore_bar_max,
cfg.clock_min,
cfg.clock_bar_max,
cfg.power_min,
cfg.power_bar_max,
cfg.temp_min,
cfg.temp_max,
)
}
/// Replace the values of the settings-pane keys in conf text (append the
/// block if a key is missing). Returns the new file content.
pub fn update_conf_text(text: &str, cfg: &Config) -> String {
let wanted = settings_text(cfg);
let mut out_lines: Vec<String> = Vec::new();
let mut seen: Vec<String> = Vec::new();
for line in text.lines() {
let key = line.split('#').next().unwrap_or("").trim();
let key = key.split_once('=').map(|(k, _)| k.trim().to_string());
if let Some(k) = key {
if wanted
.lines()
.any(|w| w.split_once('=').map(|(wk, _)| wk.trim() == k).unwrap_or(false))
&& !seen.contains(&k)
{
// First occurrence of a managed key: emit the fresh block
// and mark every block key seen (the block covers them all).
for w in wanted.lines() {
out_lines.push(w.to_string());
if let Some((wk, _)) = w.split_once('=') {
seen.push(wk.trim().to_string());
}
}
continue;
}
if seen.contains(&k) {
continue; // subsequent dup of a managed key
}
}
out_lines.push(line.to_string());
}
let mut out = out_lines.join("\n");
for w in wanted.lines() {
let wk = w.split_once('=').unwrap().0.trim();
if !seen.iter().any(|s| s == wk) {
out.push('\n');
out.push_str(w);
}
}
out.push('\n');
out
}
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);
}
#[test]
fn parses_layout_and_graphs() {
let mut cfg = Config::default();
apply_line(&mut cfg, "layout = dual");
apply_line(&mut cfg, "graphs = false");
assert_eq!(cfg.layout, Layout::Dual);
assert!(!cfg.graphs);
apply_line(&mut cfg, "layout = nonsense");
assert_eq!(cfg.layout, Layout::Auto);
}
#[test]
fn non_cpu_rails_hidden_by_default() {
let cfg = Config::default();
let hidden: Vec<&str> = cfg.rails.iter().filter(|r| r.hide).map(|r| r.input.as_str()).collect();
assert_eq!(hidden, ["in1", "in2", "in3", "in5", "in6", "in7", "in8", "in10", "in11", "in12", "in13", "in14"]);
let shown: Vec<&str> = cfg.rails.iter().filter(|r| !r.hide && r.input != "in0").map(|r| r.input.as_str()).collect();
assert_eq!(shown, ["in4", "in9"]);
}
#[test]
fn parses_poll_and_graph_window() {
let mut cfg = Config::default();
apply_line(&mut cfg, "poll_ms = 500");
apply_line(&mut cfg, "graph_secs = 300");
assert_eq!(cfg.poll_ms, 500);
assert_eq!(cfg.graph_secs, 300);
apply_line(&mut cfg, "poll_ms = 1"); // floor 50
assert_eq!(cfg.poll_ms, 50);
}
#[test]
fn updates_conf_in_place() {
let text = "# comment\npoll_ms = 250\ngraph_secs = 120\nrail.in0.label = CPU Vcore\ngraphs = true\n";
let mut cfg = Config::default();
cfg.poll_ms = 500;
cfg.graph_secs = 300;
cfg.graphs = false;
let out = update_conf_text(text, &cfg);
assert!(out.contains("poll_ms = 500"));
assert!(out.contains("graph_secs = 300"));
assert!(out.contains("graphs = false"));
assert!(out.contains("rail.in0.label = CPU Vcore")); // untouched
assert!(out.starts_with("# comment")); // untouched
// Each managed key appears exactly once
assert_eq!(out.matches("poll_ms").count(), 1);
assert_eq!(out.matches("graphs").count(), 1);
}
#[test]
fn parses_scale_fixed() {
let mut cfg = Config::default();
apply_line(&mut cfg, "graph_scale_fixed = true");
assert!(cfg.graph_scale_fixed);
}
#[test]
fn settings_text_includes_scale() {
let cfg = Config::default();
assert!(settings_text(&cfg).contains("graph_scale_fixed = false"));
}
}