1use std::fs;
8use std::path::Path;
9
10use super::{SysRoots, read_i64, read_trimmed, read_u64, sorted_entries};
11use crate::snapshot::{Fan, SensorChip, Temperature};
12
13const CPU_CHIPS: &[&str] = &["k10temp", "zenpower", "coretemp", "cpu_thermal"];
15
16#[derive(Debug, Clone, Default)]
18pub struct HwmonChip {
19 pub chip: SensorChip,
21 pub power_w: Vec<(String, f32)>,
23}
24
25pub fn read_chips(roots: &SysRoots) -> Vec<HwmonChip> {
27 let class_dir = roots.sys.join("class/hwmon");
28 sorted_entries(&class_dir)
29 .into_iter()
30 .filter_map(|entry| read_chip(&class_dir.join(&entry), &entry))
31 .filter(|c| {
34 !c.chip.temperatures.is_empty() || !c.chip.fans.is_empty() || !c.power_w.is_empty()
35 })
36 .collect()
37}
38
39pub fn is_cpu_chip(chip: &SensorChip) -> bool {
41 CPU_CHIPS.contains(&chip.name.as_str())
42}
43
44fn read_chip(dir: &Path, entry: &str) -> Option<HwmonChip> {
46 let name = read_trimmed(&dir.join("name"))?;
47 let device = fs::read_link(dir.join("device"))
51 .ok()
52 .and_then(|target| target.file_name().map(|n| n.to_string_lossy().into_owned()))
53 .unwrap_or_else(|| entry.to_owned());
54 let key = format!("{name}:{device}");
55
56 let files = sorted_entries(dir);
57 let temperatures = channel_numbers(&files, "temp")
58 .into_iter()
59 .filter_map(|n| read_temperature(dir, &key, n))
60 .collect();
61 let fans = channel_numbers(&files, "fan")
62 .into_iter()
63 .filter_map(|n| {
64 let rpm = read_u64(&dir.join(format!("fan{n}_input")))?;
65 let label = read_trimmed(&dir.join(format!("fan{n}_label")))
66 .unwrap_or_else(|| format!("fan{n}"));
67 Some(Fan {
68 key: format!("{key}/fan{n}"),
69 label,
70 rpm: rpm.min(u32::MAX as u64) as u32,
71 })
72 })
73 .collect();
74 let power_w = channel_numbers(&files, "power")
75 .into_iter()
76 .filter_map(|n| {
77 let micro = read_u64(&dir.join(format!("power{n}_input")))
80 .or_else(|| read_u64(&dir.join(format!("power{n}_average"))))?;
81 let label = read_trimmed(&dir.join(format!("power{n}_label")))
82 .unwrap_or_else(|| format!("power{n}"));
83 Some((label, micro as f32 / 1_000_000.0))
84 })
85 .collect();
86
87 Some(HwmonChip {
88 chip: SensorChip {
89 key,
90 name,
91 temperatures,
92 fans,
93 },
94 power_w,
95 })
96}
97
98fn read_temperature(dir: &Path, chip_key: &str, n: u32) -> Option<Temperature> {
100 let milli =
101 |suffix: &str| read_i64(&dir.join(format!("temp{n}_{suffix}"))).map(|v| v as f32 / 1000.0);
102 let celsius = milli("input")?;
103 let label =
104 read_trimmed(&dir.join(format!("temp{n}_label"))).unwrap_or_else(|| format!("temp{n}"));
105 Some(Temperature {
106 key: format!("{chip_key}/temp{n}"),
107 label,
108 celsius,
109 max_celsius: milli("max"),
110 critical_celsius: milli("crit"),
111 })
112}
113
114fn channel_numbers(files: &[String], prefix: &str) -> Vec<u32> {
117 let mut numbers: Vec<u32> = files
118 .iter()
119 .filter_map(|f| {
120 let rest = f.strip_prefix(prefix)?;
121 let (num, suffix) = rest.split_once('_')?;
122 if suffix == "input" || (prefix == "power" && suffix == "average") {
123 num.parse().ok()
124 } else {
125 None
126 }
127 })
128 .collect();
129 numbers.sort_unstable();
130 numbers.dedup();
131 numbers
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::linux::testutil::{symlink, write};
138 use std::path::PathBuf;
139
140 fn roots(dir: &Path) -> SysRoots {
141 SysRoots {
142 sys: dir.join("sys"),
143 proc: dir.join("proc"),
144 }
145 }
146
147 #[test]
148 fn reads_temperatures_fans_and_power() {
149 let tmp = tempfile::tempdir().unwrap();
150 let root = tmp.path();
151 write(root, "sys/class/hwmon/hwmon5/name", "k10temp\n");
153 write(root, "sys/class/hwmon/hwmon5/temp1_input", "45250\n");
154 write(root, "sys/class/hwmon/hwmon5/temp1_label", "Tctl\n");
155 write(root, "sys/class/hwmon/hwmon5/temp3_input", "40000\n");
156 write(root, "sys/class/hwmon/hwmon5/temp3_label", "Tccd1\n");
157 symlink(
158 root,
159 "sys/class/hwmon/hwmon5/device",
160 &PathBuf::from("../../../0000:00:18.3"),
161 );
162 write(root, "sys/class/hwmon/hwmon10/name", "nct6686\n");
164 write(root, "sys/class/hwmon/hwmon10/fan1_input", "886\n");
165 write(root, "sys/class/hwmon/hwmon10/fan2_input", "0\n");
166 symlink(
167 root,
168 "sys/class/hwmon/hwmon10/device",
169 &PathBuf::from("../../../nct6683.2592"),
170 );
171 write(root, "sys/class/hwmon/hwmon2/name", "amdgpu\n");
173 write(root, "sys/class/hwmon/hwmon2/power1_input", "12500000\n");
174 write(root, "sys/class/hwmon/hwmon2/power1_label", "PPT\n");
175
176 write(root, "sys/class/hwmon/hwmon3/name", "hid_battery\n");
178
179 let chips = read_chips(&roots(root));
180 let keys: Vec<&str> = chips.iter().map(|c| c.chip.key.as_str()).collect();
182 assert_eq!(
183 keys,
184 vec![
185 "amdgpu:hwmon2",
186 "k10temp:0000:00:18.3",
187 "nct6686:nct6683.2592"
188 ]
189 );
190
191 let amdgpu = &chips[0];
192 assert_eq!(amdgpu.power_w, vec![("PPT".to_owned(), 12.5)]);
193
194 let k10 = &chips[1].chip;
195 assert!(is_cpu_chip(k10));
196 assert_eq!(k10.temperatures.len(), 2);
197 assert_eq!(k10.temperatures[0].label, "Tctl");
198 assert_eq!(k10.temperatures[0].key, "k10temp:0000:00:18.3/temp1");
199 assert!((k10.temperatures[0].celsius - 45.25).abs() < 1e-6);
200 assert_eq!(k10.temperatures[1].label, "Tccd1");
201 assert_eq!(k10.temperatures[1].max_celsius, None);
202
203 let nct = &chips[2].chip;
204 assert_eq!(nct.fans.len(), 2);
205 assert_eq!(nct.fans[0].key, "nct6686:nct6683.2592/fan1");
206 assert_eq!(nct.fans[0].label, "fan1");
207 assert_eq!(nct.fans[0].rpm, 886);
208 assert_eq!(nct.fans[1].rpm, 0);
209 }
210
211 #[test]
212 fn missing_class_directory_yields_no_chips() {
213 let tmp = tempfile::tempdir().unwrap();
214 assert!(read_chips(&roots(tmp.path())).is_empty());
215 }
216}