1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

system: cpu temperatures scanning

This commit is contained in:
RGBCube 2025-06-04 18:34:40 +03:00
parent 421d4aaacc
commit 303a84479c
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
3 changed files with 133 additions and 227 deletions

View file

@ -8,7 +8,6 @@ use crate::fs;
#[derive(Default, Debug, Clone, PartialEq)]
pub struct CpuRescanCache {
stat: OnceCell<HashMap<u32, CpuStat>>,
temperatures: OnceCell<HashMap<u32, f64>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -172,7 +171,6 @@ impl Cpu {
}
self.rescan_stat(cache)?;
self.rescan_temperature(cache)?;
Ok(())
}
@ -347,60 +345,6 @@ impl Cpu {
Ok(())
}
fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
// OnceCell::get_or_try_init is unstable. Cope:
let temperatures = match cache.temperatures.get() {
Some(temperature) => temperature,
None => {
const PATH: &str = "/sys/class/hwmon";
let temperatures = HashMap::new();
for entry in fs::read_dir(PATH)
.with_context(|| format!("failed to read hardware information from '{PATH}'"))?
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
{
let entry =
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_path = entry.path();
let Some(name) = fs::read(entry_path.join("name")).with_context(|| {
format!(
"failed to read name of hardware entry at '{path}'",
path = entry_path.display(),
)
})?
else {
continue;
};
match &*name {
// Intel CPU temperature driver
"coretemp" => todo!(),
// AMD CPU temperature driver
// TODO: 'zenergy' can also report those stats, I think?
"k10temp" | "zenpower" | "amdgpu" => todo!(),
// Other CPU temperature drivers
_ if name.contains("cpu") || name.contains("temp") => todo!(),
_ => {}
}
}
cache.temperatures.set(temperatures).unwrap();
cache.temperatures.get().unwrap()
}
};
self.temperature = temperatures.get(&self.number).copied();
Ok(())
}
pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> {
let Self {
number,

View file

@ -10,178 +10,30 @@ use std::{
time::SystemTime,
};
pub fn get_system_info() -> SystemInfo {
let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string());
// Try /sys/devices/platform paths for thermal zones as a last resort
// if temperature_celsius.is_none() {
// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") {
// for entry in thermal_zones.flatten() {
// let zone_path = entry.path();
// let name = entry.file_name().into_string().unwrap_or_default();
SystemInfo { cpu_model }
}
pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result<CpuCoreInfo> {
// Temperature detection.
// Should be generic enough to be able to support for multiple hardware sensors
// with the possibility of extending later down the road.
let mut temperature_celsius: Option<f32> = None;
// Search for temperature in hwmon devices
if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") {
for hw_entry in hwmon_dir.flatten() {
let hw_path = hw_entry.path();
// Check hwmon driver name
if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) {
// Intel CPU temperature driver
if name == "coretemp" {
if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") {
temperature_celsius = Some(temp);
break;
}
}
// AMD CPU temperature driver
// TODO: 'zenergy' can also report those stats, I think?
else if name == "k10temp" || name == "zenpower" || name == "amdgpu" {
// AMD's k10temp doesn't always label cores individually
// First try to find core-specific temps
if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Tdie") {
temperature_celsius = Some(temp);
break;
}
// Try Tctl temperature (CPU control temp)
if let Some(temp) = get_generic_sensor_temperature(&hw_path, "Tctl") {
temperature_celsius = Some(temp);
break;
}
// Try CPU temperature
if let Some(temp) = get_generic_sensor_temperature(&hw_path, "CPU") {
temperature_celsius = Some(temp);
break;
}
// Fall back to any available temperature input without a specific label
temperature_celsius = get_fallback_temperature(&hw_path);
if temperature_celsius.is_some() {
break;
}
}
// Other CPU temperature drivers
else if name.contains("cpu") || name.contains("temp") {
// Try to find a label that matches this core
if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") {
temperature_celsius = Some(temp);
break;
}
// Fall back to any temperature reading if specific core not found
temperature_celsius = get_fallback_temperature(&hw_path);
if temperature_celsius.is_some() {
break;
}
}
}
}
}
// Try /sys/devices/platform paths for thermal zones as a last resort
if temperature_celsius.is_none() {
if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") {
for entry in thermal_zones.flatten() {
let zone_path = entry.path();
let name = entry.file_name().into_string().unwrap_or_default();
if name.starts_with("thermal_zone") {
// Try to match by type
if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) {
if zone_type.contains("cpu")
|| zone_type.contains("x86")
|| zone_type.contains("core")
{
if let Ok(temp_mc) = read_sysfs_value::<i32>(zone_path.join("temp")) {
temperature_celsius = Some(temp_mc as f32 / 1000.0);
break;
}
}
}
}
}
}
}
Ok(CpuCoreInfo {
core_id,
temperature_celsius,
})
}
/// Finds core-specific temperature
fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option<f32> {
for i in 1..=32 {
// Increased range to handle systems with many sensors
let label_path = hw_path.join(format!("temp{i}_label"));
let input_path = hw_path.join(format!("temp{i}_input"));
if label_path.exists() && input_path.exists() {
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
// Match various common label formats:
// "Core X", "core X", "Core-X", "CPU Core X", etc.
let core_pattern = format!("{label_prefix} {core_id}");
let alt_pattern = format!("{label_prefix}-{core_id}");
if label.eq_ignore_ascii_case(&core_pattern)
|| label.eq_ignore_ascii_case(&alt_pattern)
|| label
.to_lowercase()
.contains(&format!("core {core_id}").to_lowercase())
{
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
return Some(temp_mc as f32 / 1000.0);
}
}
}
}
}
None
}
// Finds generic sensor temperatures by label
fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option<f32> {
for i in 1..=32 {
let label_path = hw_path.join(format!("temp{i}_label"));
let input_path = hw_path.join(format!("temp{i}_input"));
if label_path.exists() && input_path.exists() {
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
if label.eq_ignore_ascii_case(label_name)
|| label.to_lowercase().contains(&label_name.to_lowercase())
{
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
return Some(temp_mc as f32 / 1000.0);
}
}
}
} else if !label_path.exists() && input_path.exists() {
// Some sensors might not have labels but still have valid temp inputs
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
return Some(temp_mc as f32 / 1000.0);
}
}
}
None
}
// Fallback to any temperature reading from a sensor
fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
for i in 1..=32 {
let input_path = hw_path.join(format!("temp{i}_input"));
if input_path.exists() {
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
return Some(temp_mc as f32 / 1000.0);
}
}
}
None
}
// if name.starts_with("thermal_zone") {
// // Try to match by type
// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) {
// if zone_type.contains("cpu")
// || zone_type.contains("x86")
// || zone_type.contains("core")
// {
// if let Ok(temp_mc) = read_sysfs_value::<i32>(zone_path.join("temp")) {
// temperature_celsius = Some(temp_mc as f32 / 1000.0);
// break;
// }
// }
// }
// }
// }
// }
// }
pub fn get_cpu_model() -> anyhow::Result<String> {
let path = Path::new("/proc/cpuinfo");

View file

@ -1,3 +1,5 @@
use std::{collections::HashMap, path::Path};
use anyhow::{Context, bail};
use crate::{cpu, fs, power_supply};
@ -10,6 +12,8 @@ pub struct System {
pub load_average_15min: f64,
pub cpus: Vec<cpu::Cpu>,
pub cpu_temperatures: HashMap<u32, f64>,
pub power_supplies: Vec<power_supply::PowerSupply>,
}
@ -19,6 +23,8 @@ impl System {
is_ac: false,
cpus: Vec::new(),
cpu_temperatures: HashMap::new(),
power_supplies: Vec::new(),
load_average_1min: 0.0,
@ -48,6 +54,110 @@ impl System {
Ok(())
}
fn rescan_temperatures(&mut self) -> anyhow::Result<()> {
const PATH: &str = "/sys/class/hwmon";
let mut temperatures = HashMap::new();
for entry in fs::read_dir(PATH)
.with_context(|| format!("failed to read hardware information from '{PATH}'"))?
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
{
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_path = entry.path();
let Some(name) = fs::read(entry_path.join("name")).with_context(|| {
format!(
"failed to read name of hardware entry at '{path}'",
path = entry_path.display(),
)
})?
else {
continue;
};
match &*name {
// TODO: 'zenergy' can also report those stats, I think?
"coretemp" | "k10temp" | "zenpower" | "amdgpu" => {
Self::get_temperatures(&entry_path, &mut temperatures)?;
}
// Other CPU temperature drivers.
_ if name.contains("cpu") || name.contains("temp") => {
Self::get_temperatures(&entry_path, &mut temperatures)?;
}
_ => {}
}
}
self.cpu_temperatures = temperatures;
Ok(())
}
fn get_temperatures(
device_path: &Path,
temperatures: &mut HashMap<u32, f64>,
) -> anyhow::Result<()> {
// Increased range to handle systems with many sensors.
for i in 1..=96 {
let label_path = device_path.join(format!("temp{i}_label"));
let input_path = device_path.join(format!("temp{i}_input"));
if !label_path.exists() || !input_path.exists() {
continue;
}
let Some(label) = fs::read(&label_path).with_context(|| {
format!(
"failed to read hardware hardware device label from '{path}'",
path = label_path.display(),
)
})?
else {
continue;
};
// Match various common label formats:
// "Core X", "core X", "Core-X", "CPU Core X", etc.
let number = label
.trim_start_matches("cpu")
.trim_start_matches("CPU")
.trim_start()
.trim_start_matches("core")
.trim_start_matches("Core")
.trim_start()
.trim_start_matches("tdie")
.trim_start_matches("Tdie")
.trim_start()
.trim_start_matches("tctl")
.trim_start_matches("Tctl")
.trim_start()
.trim_start_matches("-")
.trim();
let Ok(number) = number.parse::<u32>() else {
continue;
};
let Some(temperature_mc) = fs::read_n::<i64>(&input_path).with_context(|| {
format!(
"failed to read CPU temperature from '{path}'",
path = input_path.display(),
)
})?
else {
continue;
};
temperatures.insert(number, temperature_mc as f64 / 1000.0);
}
Ok(())
}
fn is_desktop(&mut self) -> anyhow::Result<bool> {
if let Some(chassis_type) =
fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?