1 | // Take a look at the license at the top of the repository in the LICENSE file. |
2 | |
3 | // Information about values readable from `hwmon` sysfs. |
4 | // |
5 | // Values in /sys/class/hwmonN are `c_long` or `c_ulong` |
6 | // transposed to rust we only read `u32` or `i32` values. |
7 | use crate::ComponentExt; |
8 | |
9 | use std::collections::HashMap; |
10 | use std::fs::{read_dir, File}; |
11 | use std::io::Read; |
12 | use std::path::{Path, PathBuf}; |
13 | |
14 | #[doc = include_str!("../../md_doc/component.md" )] |
15 | #[derive (Default)] |
16 | pub struct Component { |
17 | /// Optional associated device of a `Component`. |
18 | device_model: Option<String>, |
19 | /// The chip name. |
20 | /// |
21 | /// Kernel documentation extract: |
22 | /// ```txt |
23 | /// This should be a short, lowercase string, not containing |
24 | /// whitespace, dashes, or the wildcard character '*'. |
25 | /// This attribute represents the chip name. It is the only |
26 | /// mandatory attribute. |
27 | /// I2C devices get this attribute created automatically. |
28 | /// ``` |
29 | name: String, |
30 | /// Temperature current value |
31 | /// - Read in: `temp[1-*]_input`. |
32 | /// - Unit: read as millidegree Celsius converted to Celsius. |
33 | temperature: Option<f32>, |
34 | /// Maximum value computed by `sysinfo`. |
35 | max: Option<f32>, |
36 | /// Max threshold provided by the chip/kernel |
37 | /// - Read in:`temp[1-*]_max` |
38 | /// - Unit: read as millidegree Celsius converted to Celsius. |
39 | threshold_max: Option<f32>, |
40 | /// Min threshold provided by the chip/kernel. |
41 | /// - Read in:`temp[1-*]_min` |
42 | /// - Unit: read as millidegree Celsius converted to Celsius. |
43 | threshold_min: Option<f32>, |
44 | /// Critical threshold provided by the chip/kernel previous user write. |
45 | /// Read in `temp[1-*]_crit`: |
46 | /// Typically greater than corresponding temp_max values. |
47 | /// - Unit: read as millidegree Celsius converted to Celsius. |
48 | threshold_critical: Option<f32>, |
49 | /// Sensor type, not common but can exist! |
50 | /// |
51 | /// Read in: `temp[1-*]_type` Sensor type selection. |
52 | /// Values integer: |
53 | /// - 1: CPU embedded diode |
54 | /// - 2: 3904 transistor |
55 | /// - 3: thermal diode |
56 | /// - 4: thermistor |
57 | /// - 5: AMD AMDSI |
58 | /// - 6: Intel PECI |
59 | /// Not all types are supported by all chips |
60 | sensor_type: Option<TermalSensorType>, |
61 | /// Component Label |
62 | /// |
63 | /// For formatting detail see `Component::label` function docstring. |
64 | /// |
65 | /// ## Linux implementation details |
66 | /// |
67 | /// read n: `temp[1-*]_label` Suggested temperature channel label. |
68 | /// Value: Text string |
69 | /// |
70 | /// Should only be created if the driver has hints about what |
71 | /// this temperature channel is being used for, and user-space |
72 | /// doesn't. In all other cases, the label is provided by user-space. |
73 | label: String, |
74 | // TODO: not used now. |
75 | // Historical minimum temperature |
76 | // - Read in:`temp[1-*]_lowest |
77 | // - Unit: millidegree Celsius |
78 | // |
79 | // Temperature critical min value, typically lower than |
80 | // corresponding temp_min values. |
81 | // - Read in:`temp[1-*]_lcrit` |
82 | // - Unit: millidegree Celsius |
83 | // |
84 | // Temperature emergency max value, for chips supporting more than |
85 | // two upper temperature limits. Must be equal or greater than |
86 | // corresponding temp_crit values. |
87 | // - temp[1-*]_emergency |
88 | // - Unit: millidegree Celsius |
89 | /// File to read current temperature shall be `temp[1-*]_input` |
90 | /// It may be absent but we don't continue if absent. |
91 | input_file: Option<PathBuf>, |
92 | /// `temp[1-*]_highest file` to read if available highest value. |
93 | highest_file: Option<PathBuf>, |
94 | } |
95 | |
96 | // Read arbitrary data from sysfs. |
97 | fn get_file_line(file: &Path, capacity: usize) -> Option<String> { |
98 | let mut reader: String = String::with_capacity(capacity); |
99 | let mut f: File = File::open(path:file).ok()?; |
100 | f.read_to_string(&mut reader).ok()?; |
101 | reader.truncate(new_len:reader.trim_end().len()); |
102 | Some(reader) |
103 | } |
104 | |
105 | /// Designed at first for reading an `i32` or `u32` aka `c_long` |
106 | /// from a `/sys/class/hwmon` sysfs file. |
107 | fn read_number_from_file<N>(file: &Path) -> Option<N> |
108 | where |
109 | N: std::str::FromStr, |
110 | { |
111 | let mut reader: [u8; 32] = [0u8; 32]; |
112 | let mut f: File = File::open(path:file).ok()?; |
113 | let n: usize = f.read(&mut reader).ok()?; |
114 | // parse and trim would complain about `\0`. |
115 | let number: &[u8] = &reader[..n]; |
116 | let number: &str = std::str::from_utf8(number).ok()?; |
117 | let number: &str = number.trim(); |
118 | // Assert that we cleaned a little bit that string. |
119 | if cfg!(feature = "debug" ) { |
120 | assert!(!number.contains(' \n' ) && !number.contains(' \0' )); |
121 | } |
122 | number.parse().ok() |
123 | } |
124 | |
125 | // Read a temperature from a `tempN_item` sensor form the sysfs. |
126 | // number returned will be in mili-celsius. |
127 | // |
128 | // Don't call it on `label`, `name` or `type` file. |
129 | #[inline ] |
130 | fn get_temperature_from_file(file: &Path) -> Option<f32> { |
131 | let temp: Option = read_number_from_file(file); |
132 | convert_temp_celsius(temp) |
133 | } |
134 | |
135 | /// Takes a raw temperature in mili-celsius and convert it to celsius. |
136 | #[inline ] |
137 | fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> { |
138 | temp.map(|n: i32| (n as f32) / 1000f32) |
139 | } |
140 | |
141 | /// Information about thermal sensor. It may be unavailable as it's |
142 | /// kernel module and chip dependent. |
143 | enum TermalSensorType { |
144 | /// 1: CPU embedded diode |
145 | CPUEmbeddedDiode, |
146 | /// 2: 3904 transistor |
147 | Transistor3904, |
148 | /// 3: thermal diode |
149 | ThermalDiode, |
150 | /// 4: thermistor |
151 | Thermistor, |
152 | /// 5: AMD AMDSI |
153 | AMDAMDSI, |
154 | /// 6: Intel PECI |
155 | IntelPECI, |
156 | /// Not all types are supported by all chips so we keep space for |
157 | /// unknown sensors. |
158 | Unknown(u8), |
159 | } |
160 | |
161 | impl From<u8> for TermalSensorType { |
162 | fn from(input: u8) -> Self { |
163 | match input { |
164 | 0 => Self::CPUEmbeddedDiode, |
165 | 1 => Self::Transistor3904, |
166 | 3 => Self::ThermalDiode, |
167 | 4 => Self::Thermistor, |
168 | 5 => Self::AMDAMDSI, |
169 | 6 => Self::IntelPECI, |
170 | n: u8 => Self::Unknown(n), |
171 | } |
172 | } |
173 | } |
174 | |
175 | /// Check given `item` dispatch to read the right `file` with the right parsing and store data in |
176 | /// given `component`. `id` is provided for `label` creation. |
177 | fn fill_component(component: &mut Component, item: &str, folder: &Path, file: &str) { |
178 | let hwmon_file = folder.join(file); |
179 | match item { |
180 | "type" => { |
181 | component.sensor_type = |
182 | read_number_from_file::<u8>(&hwmon_file).map(TermalSensorType::from) |
183 | } |
184 | "input" => { |
185 | let temperature = get_temperature_from_file(&hwmon_file); |
186 | component.input_file = Some(hwmon_file); |
187 | component.temperature = temperature; |
188 | // Maximum know try to get it from `highest` if not available |
189 | // use current temperature |
190 | if component.max.is_none() { |
191 | component.max = temperature; |
192 | } |
193 | } |
194 | "label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(), |
195 | "highest" => { |
196 | component.max = get_temperature_from_file(&hwmon_file).or(component.temperature); |
197 | component.highest_file = Some(hwmon_file); |
198 | } |
199 | "max" => component.threshold_max = get_temperature_from_file(&hwmon_file), |
200 | "min" => component.threshold_min = get_temperature_from_file(&hwmon_file), |
201 | "crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file), |
202 | _ => { |
203 | sysinfo_debug!( |
204 | "This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}" , |
205 | hwmon_file, |
206 | ); |
207 | } |
208 | } |
209 | } |
210 | |
211 | impl Component { |
212 | /// Read out `hwmon` info (hardware monitor) from `folder` |
213 | /// to get values' path to be used on refresh as well as files containing `max`, |
214 | /// `critical value` and `label`. Then we store everything into `components`. |
215 | /// |
216 | /// Note that a thermal [Component] must have a way to read its temperature. |
217 | /// If not, it will be ignored and not added into `components`. |
218 | /// |
219 | /// ## What is read: |
220 | /// |
221 | /// - Mandatory: `name` the name of the `hwmon`. |
222 | /// - Mandatory: `tempN_input` Drop [Component] if missing |
223 | /// - Optional: sensor `label`, in the general case content of `tempN_label` |
224 | /// see below for special cases |
225 | /// - Optional: `label` |
226 | /// - Optional: `/device/model` |
227 | /// - Optional: hightest historic value in `tempN_hightest`. |
228 | /// - Optional: max threshold value defined in `tempN_max` |
229 | /// - Optional: critical threshold value defined in `tempN_crit` |
230 | /// |
231 | /// Where `N` is a `u32` associated to a sensor like `temp1_max`, `temp1_input`. |
232 | /// |
233 | /// ## Doc to Linux kernel API. |
234 | /// |
235 | /// Kernel hwmon API: https://www.kernel.org/doc/html/latest/hwmon/hwmon-kernel-api.html |
236 | /// DriveTemp kernel API: https://docs.kernel.org/gpu/amdgpu/thermal.html#hwmon-interfaces |
237 | /// Amdgpu hwmon interface: https://www.kernel.org/doc/html/latest/hwmon/drivetemp.html |
238 | fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> { |
239 | let dir = read_dir(folder).ok()?; |
240 | let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10); |
241 | for entry in dir.flatten() { |
242 | let entry = entry.path(); |
243 | let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or("" ); |
244 | if entry.is_dir() || !filename.starts_with("temp" ) { |
245 | continue; |
246 | } |
247 | |
248 | let (id, item) = filename.split_once('_' )?; |
249 | let id = id.get(4..)?.parse::<u32>().ok()?; |
250 | |
251 | let component = matchings.entry(id).or_default(); |
252 | let name = get_file_line(&folder.join("name" ), 16); |
253 | component.name = name.unwrap_or_default(); |
254 | let device_model = get_file_line(&folder.join("device/model" ), 16); |
255 | component.device_model = device_model; |
256 | fill_component(component, item, folder, filename); |
257 | } |
258 | let compo = matchings |
259 | .into_iter() |
260 | .map(|(id, mut c)| { |
261 | // sysinfo expose a generic interface with a `label`. |
262 | // Problem: a lot of sensors don't have a label or a device model! ¯\_(ツ)_/¯ |
263 | // So let's pretend we have a unique label! |
264 | // See the table in `Component::label` documentation for the table detail. |
265 | c.label = c.format_label("temp" , id); |
266 | c |
267 | }) |
268 | // Remove components without `tempN_input` file termal. `Component` doesn't support this kind of sensors yet |
269 | .filter(|c| c.input_file.is_some()); |
270 | |
271 | components.extend(compo); |
272 | Some(()) |
273 | } |
274 | |
275 | /// Compute a label out of available information. |
276 | /// See the table in `Component::label`'s documentation. |
277 | fn format_label(&self, class: &str, id: u32) -> String { |
278 | let Component { |
279 | device_model, |
280 | name, |
281 | label, |
282 | .. |
283 | } = self; |
284 | let has_label = !label.is_empty(); |
285 | match (has_label, device_model) { |
286 | (true, Some(device_model)) => { |
287 | format!(" {name} {label} {device_model} {class}{id}" ) |
288 | } |
289 | (true, None) => format!(" {name} {label}" ), |
290 | (false, Some(device_model)) => format!(" {name} {device_model}" ), |
291 | (false, None) => format!(" {name} {class}{id}" ), |
292 | } |
293 | } |
294 | } |
295 | |
296 | impl ComponentExt for Component { |
297 | fn temperature(&self) -> f32 { |
298 | self.temperature.unwrap_or(f32::NAN) |
299 | } |
300 | |
301 | fn max(&self) -> f32 { |
302 | self.max.unwrap_or(f32::NAN) |
303 | } |
304 | |
305 | fn critical(&self) -> Option<f32> { |
306 | self.threshold_critical |
307 | } |
308 | |
309 | fn label(&self) -> &str { |
310 | &self.label |
311 | } |
312 | |
313 | fn refresh(&mut self) { |
314 | let current = self |
315 | .input_file |
316 | .as_ref() |
317 | .and_then(|file| get_temperature_from_file(file.as_path())); |
318 | // tries to read out kernel highest if not compute something from temperature. |
319 | let max = self |
320 | .highest_file |
321 | .as_ref() |
322 | .and_then(|file| get_temperature_from_file(file.as_path())) |
323 | .or_else(|| { |
324 | let last = self.temperature?; |
325 | let current = current?; |
326 | Some(last.max(current)) |
327 | }); |
328 | self.max = max; |
329 | self.temperature = current; |
330 | } |
331 | } |
332 | |
333 | pub(crate) fn get_components() -> Vec<Component> { |
334 | let mut components: Vec = Vec::with_capacity(10); |
335 | if let Ok(dir: ReadDir) = read_dir(Path::new("/sys/class/hwmon/" )) { |
336 | for entry: DirEntry in dir.flatten() { |
337 | let entry: PathBuf = entry.path(); |
338 | if !entry.is_dir() |
339 | || !entry&str |
340 | .file_name() |
341 | .and_then(|x| x.to_str()) |
342 | .unwrap_or(default:"" ) |
343 | .starts_with("hwmon" ) |
344 | { |
345 | continue; |
346 | } |
347 | Component::from_hwmon(&mut components, &entry); |
348 | } |
349 | components.sort_by(|c1: &Component, c2: &Component| c1.label.to_lowercase().cmp(&c2.label.to_lowercase())); |
350 | } |
351 | components |
352 | } |
353 | |