linux_fan_utility/
hwmon.rs1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14
15const HWMON_ROOT: &str = "/sys/class/hwmon";
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Fan {
24 pub id: String,
26 pub label: Option<String>,
28 pub pwm_path: PathBuf,
30 pub pwm_enable_path: PathBuf,
32 pub rpm_path: Option<PathBuf>,
34 pub hwmon_name: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct TempSensor {
41 pub id: String,
43 pub label: Option<String>,
45 pub input_path: PathBuf,
47 pub hwmon_name: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct FanStatus {
54 pub id: String,
55 pub label: Option<String>,
56 pub hwmon_name: String,
57 pub pwm: Option<u8>,
59 pub pwm_enable: Option<u8>,
61 pub rpm: Option<u32>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TempStatus {
68 pub id: String,
69 pub label: Option<String>,
70 pub hwmon_name: String,
71 pub temp_c: Option<f64>,
73}
74
75pub fn discover_fans() -> io::Result<Vec<Fan>> {
81 let mut fans = Vec::new();
82
83 for entry in fs::read_dir(HWMON_ROOT)? {
84 let entry = entry?;
85 let hwmon_dir = entry.path();
86 let hwmon_name = read_trimmed(&hwmon_dir.join("name")).unwrap_or_default();
87 let hwmon_basename = entry.file_name().to_string_lossy().to_string();
88
89 for n in 1..=16 {
91 let pwm_path = hwmon_dir.join(format!("pwm{n}"));
92 let pwm_enable_path = hwmon_dir.join(format!("pwm{n}_enable"));
93
94 if !pwm_path.exists() {
95 break;
96 }
97
98 let id = format!("{hwmon_basename}/pwm{n}");
99 let label = read_trimmed(&hwmon_dir.join(format!("fan{n}_label")));
100 let rpm_path = {
101 let p = hwmon_dir.join(format!("fan{n}_input"));
102 if p.exists() { Some(p) } else { None }
103 };
104
105 fans.push(Fan {
106 id,
107 label,
108 pwm_path,
109 pwm_enable_path,
110 rpm_path,
111 hwmon_name: hwmon_name.clone(),
112 });
113 }
114 }
115
116 fans.sort_by(|a, b| a.id.cmp(&b.id));
117 Ok(fans)
118}
119
120pub fn discover_temp_sensors() -> io::Result<Vec<TempSensor>> {
122 let mut sensors = Vec::new();
123
124 for entry in fs::read_dir(HWMON_ROOT)? {
125 let entry = entry?;
126 let hwmon_dir = entry.path();
127 let hwmon_name = read_trimmed(&hwmon_dir.join("name")).unwrap_or_default();
128 let hwmon_basename = entry.file_name().to_string_lossy().to_string();
129
130 for n in 1..=32 {
131 let input_path = hwmon_dir.join(format!("temp{n}_input"));
132
133 if !input_path.exists() {
134 break;
135 }
136
137 let id = format!("{hwmon_basename}/temp{n}");
138 let label = read_trimmed(&hwmon_dir.join(format!("temp{n}_label")));
139
140 sensors.push(TempSensor {
141 id,
142 label,
143 input_path,
144 hwmon_name: hwmon_name.clone(),
145 });
146 }
147 }
148
149 sensors.sort_by(|a, b| a.id.cmp(&b.id));
150 Ok(sensors)
151}
152
153pub fn read_fan_status(fan: &Fan) -> FanStatus {
159 let pwm = read_trimmed(&fan.pwm_path)
160 .and_then(|s| s.parse::<u8>().ok());
161 let pwm_enable = read_trimmed(&fan.pwm_enable_path)
162 .and_then(|s| s.parse::<u8>().ok());
163 let rpm = fan.rpm_path.as_ref().and_then(|p| {
164 read_trimmed(p).and_then(|s| s.parse::<u32>().ok())
165 });
166
167 FanStatus {
168 id: fan.id.clone(),
169 label: fan.label.clone(),
170 hwmon_name: fan.hwmon_name.clone(),
171 pwm,
172 pwm_enable,
173 rpm,
174 }
175}
176
177pub fn read_temp_status(sensor: &TempSensor) -> TempStatus {
179 let temp_c = read_trimmed(&sensor.input_path)
180 .and_then(|s| s.parse::<i64>().ok())
181 .map(|millic| millic as f64 / 1000.0);
182
183 TempStatus {
184 id: sensor.id.clone(),
185 label: sensor.label.clone(),
186 hwmon_name: sensor.hwmon_name.clone(),
187 temp_c,
188 }
189}
190
191pub fn read_all_fan_statuses(fans: &[Fan]) -> Vec<FanStatus> {
193 fans.iter().map(read_fan_status).collect()
194}
195
196pub fn read_all_temp_statuses(sensors: &[TempSensor]) -> Vec<TempStatus> {
198 sensors.iter().map(read_temp_status).collect()
199}
200
201pub fn read_temp_map(sensors: &[TempSensor]) -> HashMap<String, f64> {
203 let mut map = HashMap::new();
204 for s in sensors {
205 if let Some(t) = read_temp_status(s).temp_c {
206 map.insert(s.id.clone(), t);
207 }
208 }
209 map
210}
211
212pub fn set_pwm_enable(fan: &Fan, mode: u8) -> io::Result<()> {
221 fs::write(&fan.pwm_enable_path, format!("{mode}"))
222}
223
224pub fn set_pwm(fan: &Fan, value: u8) -> io::Result<()> {
227 fs::write(&fan.pwm_path, format!("{value}"))
228}
229
230pub fn set_manual_pwm(fan: &Fan, value: u8) -> io::Result<()> {
232 set_pwm_enable(fan, 1)?;
233 set_pwm(fan, value)
234}
235
236pub fn restore_automatic(fan: &Fan) -> io::Result<()> {
238 set_pwm_enable(fan, 2)
239}
240
241pub fn restore_all_automatic(fans: &[Fan]) {
243 for fan in fans {
244 if let Err(e) = restore_automatic(fan) {
245 log::warn!("Failed to restore automatic control for {}: {e}", fan.id);
246 }
247 }
248}
249
250fn read_trimmed(path: &Path) -> Option<String> {
255 fs::read_to_string(path).ok().map(|s| s.trim().to_string())
256}