//! 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,
}
}
}
/// Default rail map: ASRock Z690M ITX/ax (nct6798). in0 CONFIRMED Vcore;
/// differential stress-ng probing 2026-08-29: in1/in12/in14 are dead-flat
/// fixed rails (NOT Vcore, NOT DDR5 VDD); in4/in9 respond to CPU compute;
/// in5/6/10/11/13 float (unwired). "?" = evidence-based guess.
pub fn default_rails() -> Vec<Rail> {
vec![
rail("in0", "CPU Vcore", false),
rail("in1", "Rail 1.01", false),
rail("in2", "+5V", false),
rail("in3", "+3.3V", false),
rail("in4", "VCCIO?", false),
rail("in5", "", true),
rail("in6", "", true),
rail("in7", "+5V", false),
rail("in8", "3.3V aux", false),
rail("in9", "CPU rail 2?", false),
rail("in10", "", true),
rail("in11", "", true),
rail("in12", "Fixed 1.14", false),
rail("in13", "", true),
rail("in14", "Fixed 1.5V", 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);
}
}