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:
parent
421d4aaacc
commit
303a84479c
3 changed files with 133 additions and 227 deletions
56
src/cpu.rs
56
src/cpu.rs
|
@ -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,
|
||||
|
|
194
src/monitor.rs
194
src/monitor.rs
|
@ -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");
|
||||
|
|
110
src/system.rs
110
src/system.rs
|
@ -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")?
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue