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.
7use crate::ComponentExt;
8
9use std::collections::HashMap;
10use std::fs::{read_dir, File};
11use std::io::Read;
12use std::path::{Path, PathBuf};
13
14#[doc = include_str!("../../md_doc/component.md")]
15#[derive(Default)]
16pub 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.
97fn 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.
107fn read_number_from_file<N>(file: &Path) -> Option<N>
108where
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]
130fn 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]
137fn 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.
143enum 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
161impl 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.
177fn 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
211impl 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
296impl 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
333pub(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