blob: 3286829f454e61ea386f27f78c7462163a3c03ce [file] [log] [blame]
// Take a look at the license at the top of the repository in the LICENSE file.
// Information about values readable from `hwmon` sysfs.
//
// Values in /sys/class/hwmonN are `c_long` or `c_ulong`
// transposed to rust we only read `u32` or `i32` values.
use crate::ComponentExt;
use std::collections::HashMap;
use std::fs::{read_dir, File};
use std::io::Read;
use std::path::{Path, PathBuf};
#[doc = include_str!("../../md_doc/component.md")]
#[derive(Default)]
pub struct Component {
/// Optional associated device of a `Component`.
device_model: Option<String>,
/// The chip name.
///
/// Kernel documentation extract:
/// ```txt
/// This should be a short, lowercase string, not containing
/// whitespace, dashes, or the wildcard character '*'.
/// This attribute represents the chip name. It is the only
/// mandatory attribute.
/// I2C devices get this attribute created automatically.
/// ```
name: String,
/// Temperature current value
/// - Read in: `temp[1-*]_input`.
/// - Unit: read as millidegree Celsius converted to Celsius.
temperature: Option<f32>,
/// Maximum value computed by `sysinfo`.
max: Option<f32>,
/// Max threshold provided by the chip/kernel
/// - Read in:`temp[1-*]_max`
/// - Unit: read as millidegree Celsius converted to Celsius.
threshold_max: Option<f32>,
/// Min threshold provided by the chip/kernel.
/// - Read in:`temp[1-*]_min`
/// - Unit: read as millidegree Celsius converted to Celsius.
threshold_min: Option<f32>,
/// Critical threshold provided by the chip/kernel previous user write.
/// Read in `temp[1-*]_crit`:
/// Typically greater than corresponding temp_max values.
/// - Unit: read as millidegree Celsius converted to Celsius.
threshold_critical: Option<f32>,
/// Sensor type, not common but can exist!
///
/// Read in: `temp[1-*]_type` Sensor type selection.
/// Values integer:
/// - 1: CPU embedded diode
/// - 2: 3904 transistor
/// - 3: thermal diode
/// - 4: thermistor
/// - 5: AMD AMDSI
/// - 6: Intel PECI
/// Not all types are supported by all chips
sensor_type: Option<TermalSensorType>,
/// Component Label
///
/// For formatting detail see `Component::label` function docstring.
///
/// ## Linux implementation details
///
/// read n: `temp[1-*]_label` Suggested temperature channel label.
/// Value: Text string
///
/// Should only be created if the driver has hints about what
/// this temperature channel is being used for, and user-space
/// doesn't. In all other cases, the label is provided by user-space.
label: String,
// TODO: not used now.
// Historical minimum temperature
// - Read in:`temp[1-*]_lowest
// - Unit: millidegree Celsius
//
// Temperature critical min value, typically lower than
// corresponding temp_min values.
// - Read in:`temp[1-*]_lcrit`
// - Unit: millidegree Celsius
//
// Temperature emergency max value, for chips supporting more than
// two upper temperature limits. Must be equal or greater than
// corresponding temp_crit values.
// - temp[1-*]_emergency
// - Unit: millidegree Celsius
/// File to read current temperature shall be `temp[1-*]_input`
/// It may be absent but we don't continue if absent.
input_file: Option<PathBuf>,
/// `temp[1-*]_highest file` to read if available highest value.
highest_file: Option<PathBuf>,
}
// Read arbitrary data from sysfs.
fn get_file_line(file: &Path, capacity: usize) -> Option<String> {
let mut reader = String::with_capacity(capacity);
let mut f = File::open(file).ok()?;
f.read_to_string(&mut reader).ok()?;
reader.truncate(reader.trim_end().len());
Some(reader)
}
/// Designed at first for reading an `i32` or `u32` aka `c_long`
/// from a `/sys/class/hwmon` sysfs file.
fn read_number_from_file<N>(file: &Path) -> Option<N>
where
N: std::str::FromStr,
{
let mut reader = [0u8; 32];
let mut f = File::open(file).ok()?;
let n = f.read(&mut reader).ok()?;
// parse and trim would complain about `\0`.
let number = &reader[..n];
let number = std::str::from_utf8(number).ok()?;
let number = number.trim();
// Assert that we cleaned a little bit that string.
if cfg!(feature = "debug") {
assert!(!number.contains('\n') && !number.contains('\0'));
}
number.parse().ok()
}
// Read a temperature from a `tempN_item` sensor form the sysfs.
// number returned will be in mili-celsius.
//
// Don't call it on `label`, `name` or `type` file.
#[inline]
fn get_temperature_from_file(file: &Path) -> Option<f32> {
let temp = read_number_from_file(file);
convert_temp_celsius(temp)
}
/// Takes a raw temperature in mili-celsius and convert it to celsius.
#[inline]
fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> {
temp.map(|n| (n as f32) / 1000f32)
}
/// Information about thermal sensor. It may be unavailable as it's
/// kernel module and chip dependent.
enum TermalSensorType {
/// 1: CPU embedded diode
CPUEmbeddedDiode,
/// 2: 3904 transistor
Transistor3904,
/// 3: thermal diode
ThermalDiode,
/// 4: thermistor
Thermistor,
/// 5: AMD AMDSI
AMDAMDSI,
/// 6: Intel PECI
IntelPECI,
/// Not all types are supported by all chips so we keep space for
/// unknown sensors.
Unknown(u8),
}
impl From<u8> for TermalSensorType {
fn from(input: u8) -> Self {
match input {
0 => Self::CPUEmbeddedDiode,
1 => Self::Transistor3904,
3 => Self::ThermalDiode,
4 => Self::Thermistor,
5 => Self::AMDAMDSI,
6 => Self::IntelPECI,
n => Self::Unknown(n),
}
}
}
/// Check given `item` dispatch to read the right `file` with the right parsing and store data in
/// given `component`. `id` is provided for `label` creation.
fn fill_component(component: &mut Component, item: &str, folder: &Path, file: &str) {
let hwmon_file = folder.join(file);
match item {
"type" => {
component.sensor_type =
read_number_from_file::<u8>(&hwmon_file).map(TermalSensorType::from)
}
"input" => {
let temperature = get_temperature_from_file(&hwmon_file);
component.input_file = Some(hwmon_file);
component.temperature = temperature;
// Maximum know try to get it from `highest` if not available
// use current temperature
if component.max.is_none() {
component.max = temperature;
}
}
"label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(),
"highest" => {
component.max = get_temperature_from_file(&hwmon_file).or(component.temperature);
component.highest_file = Some(hwmon_file);
}
"max" => component.threshold_max = get_temperature_from_file(&hwmon_file),
"min" => component.threshold_min = get_temperature_from_file(&hwmon_file),
"crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file),
_ => {
sysinfo_debug!(
"This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}",
hwmon_file,
);
}
}
}
impl Component {
/// Read out `hwmon` info (hardware monitor) from `folder`
/// to get values' path to be used on refresh as well as files containing `max`,
/// `critical value` and `label`. Then we store everything into `components`.
///
/// Note that a thermal [Component] must have a way to read its temperature.
/// If not, it will be ignored and not added into `components`.
///
/// ## What is read:
///
/// - Mandatory: `name` the name of the `hwmon`.
/// - Mandatory: `tempN_input` Drop [Component] if missing
/// - Optional: sensor `label`, in the general case content of `tempN_label`
/// see below for special cases
/// - Optional: `label`
/// - Optional: `/device/model`
/// - Optional: hightest historic value in `tempN_hightest`.
/// - Optional: max threshold value defined in `tempN_max`
/// - Optional: critical threshold value defined in `tempN_crit`
///
/// Where `N` is a `u32` associated to a sensor like `temp1_max`, `temp1_input`.
///
/// ## Doc to Linux kernel API.
///
/// Kernel hwmon API: https://www.kernel.org/doc/html/latest/hwmon/hwmon-kernel-api.html
/// DriveTemp kernel API: https://docs.kernel.org/gpu/amdgpu/thermal.html#hwmon-interfaces
/// Amdgpu hwmon interface: https://www.kernel.org/doc/html/latest/hwmon/drivetemp.html
fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> {
let dir = read_dir(folder).ok()?;
let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10);
for entry in dir.flatten() {
let entry = entry.path();
let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or("");
if entry.is_dir() || !filename.starts_with("temp") {
continue;
}
let (id, item) = filename.split_once('_')?;
let id = id.get(4..)?.parse::<u32>().ok()?;
let component = matchings.entry(id).or_insert_with(Component::default);
let name = get_file_line(&folder.join("name"), 16);
component.name = name.unwrap_or_default();
let device_model = get_file_line(&folder.join("device/model"), 16);
component.device_model = device_model;
fill_component(component, item, folder, filename);
}
let compo = matchings
.into_iter()
.map(|(id, mut c)| {
// sysinfo expose a generic interface with a `label`.
// Problem: a lot of sensors don't have a label or a device model! ¯\_(ツ)_/¯
// So let's pretend we have a unique label!
// See the table in `Component::label` documentation for the table detail.
c.label = c.format_label("temp", id);
c
})
// Remove components without `tempN_input` file termal. `Component` doesn't support this kind of sensors yet
.filter(|c| c.input_file.is_some());
components.extend(compo);
Some(())
}
/// Compute a label out of available information.
/// See the table in `Component::label`'s documentation.
fn format_label(&self, class: &str, id: u32) -> String {
let Component {
device_model,
name,
label,
..
} = self;
let has_label = !label.is_empty();
match (has_label, device_model) {
(true, Some(device_model)) => {
format!("{name} {label} {device_model} {class}{id}")
}
(true, None) => format!("{name} {label}"),
(false, Some(device_model)) => format!("{name} {device_model}"),
(false, None) => format!("{name} {class}{id}"),
}
}
}
impl ComponentExt for Component {
fn temperature(&self) -> f32 {
self.temperature.unwrap_or(f32::NAN)
}
fn max(&self) -> f32 {
self.max.unwrap_or(f32::NAN)
}
fn critical(&self) -> Option<f32> {
self.threshold_critical
}
fn label(&self) -> &str {
&self.label
}
fn refresh(&mut self) {
let current = self
.input_file
.as_ref()
.and_then(|file| get_temperature_from_file(file.as_path()));
// tries to read out kernel highest if not compute something from temperature.
let max = self
.highest_file
.as_ref()
.and_then(|file| get_temperature_from_file(file.as_path()))
.or_else(|| {
let last = self.temperature?;
let current = current?;
Some(last.max(current))
});
self.max = max;
self.temperature = current;
}
}
pub(crate) fn get_components() -> Vec<Component> {
let mut components = Vec::with_capacity(10);
if let Ok(dir) = read_dir(Path::new("/sys/class/hwmon/")) {
for entry in dir.flatten() {
let entry = entry.path();
if !entry.is_dir()
|| !entry
.file_name()
.and_then(|x| x.to_str())
.unwrap_or("")
.starts_with("hwmon")
{
continue;
}
Component::from_hwmon(&mut components, &entry);
}
components.sort_by(|c1, c2| c1.label.to_lowercase().cmp(&c2.label.to_lowercase()));
}
components
}