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

monitor: CPU average temp calculation

This commit is contained in:
NotAShelf 2025-05-13 20:55:59 +03:00
parent c88b3c00af
commit e6e46e18a8
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
3 changed files with 272 additions and 99 deletions

View file

@ -23,6 +23,7 @@ pub struct CpuGlobalInfo {
pub epp: Option<String>, // Energy Performance Preference
pub epb: Option<String>, // Energy Performance Bias
pub platform_profile: Option<String>,
pub average_temperature_celsius: Option<f32>, // Average temperature across all cores
}
pub struct BatteryInfo {
@ -59,8 +60,8 @@ pub enum OperationalMode {
Performance,
}
use serde::Deserialize;
use clap::ValueEnum;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
pub enum TurboSetting {

View file

@ -1,12 +1,12 @@
mod core;
mod config;
mod monitor;
mod core;
mod cpu;
mod engine;
mod monitor;
use clap::Parser;
use crate::config::AppConfig;
use crate::core::TurboSetting;
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
@ -55,9 +55,7 @@ enum Commands {
core_id: Option<u32>,
},
/// Set ACPI platform profile
SetPlatformProfile {
profile: String,
},
SetPlatformProfile { profile: String },
}
fn main() {
@ -75,33 +73,55 @@ fn main() {
};
let command_result = match cli.command {
Some(Commands::Info) => {
match monitor::collect_system_report(&config) {
Some(Commands::Info) => match monitor::collect_system_report(&config) {
Ok(report) => {
println!("--- System Information ---");
println!("CPU Model: {}", report.system_info.cpu_model);
println!("Architecture: {}", report.system_info.architecture);
println!("Linux Distribution: {}", report.system_info.linux_distribution);
println!(
"Linux Distribution: {}",
report.system_info.linux_distribution
);
println!("Timestamp: {:?}", report.timestamp);
println!("\n--- CPU Global Info ---");
println!("Current Governor: {:?}", report.cpu_global.current_governor);
println!("Available Governors: {:?}", report.cpu_global.available_governors.join(", "));
println!(
"Available Governors: {:?}",
report.cpu_global.available_governors.join(", ")
);
println!("Turbo Status: {:?}", report.cpu_global.turbo_status);
println!("EPP: {:?}", report.cpu_global.epp);
println!("EPB: {:?}", report.cpu_global.epb);
println!("Platform Profile: {:?}", report.cpu_global.platform_profile);
println!(
"Average CPU Temperature: {}",
report.cpu_global.average_temperature_celsius.map_or_else(
|| "N/A (CPU temperature sensor not detected)".to_string(),
|t| format!("{:.1}°C", t)
)
);
println!("\n--- CPU Core Info ---");
for core_info in report.cpu_cores {
println!(
" Core {}: Current Freq: {:?} MHz, Min Freq: {:?} MHz, Max Freq: {:?} MHz, Usage: {:?}%, Temp: {:?}°C",
core_info.core_id,
core_info.current_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info.min_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info.max_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info.usage_percent.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)),
core_info.temperature_celsius.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f))
core_info
.current_frequency_mhz
.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info
.min_frequency_mhz
.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info
.max_frequency_mhz
.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
core_info
.usage_percent
.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)),
core_info
.temperature_celsius
.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f))
);
}
@ -115,27 +135,35 @@ fn main() {
battery_info.name,
battery_info.ac_connected,
battery_info.charging_state.as_deref().unwrap_or("N/A"),
battery_info.capacity_percent.map_or_else(|| "N/A".to_string(), |c| c.to_string()),
battery_info.power_rate_watts.map_or_else(|| "N/A".to_string(), |p| format!("{:.2}", p)),
battery_info.charge_start_threshold.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
battery_info.charge_stop_threshold.map_or_else(|| "N/A".to_string(), |t| t.to_string())
battery_info
.capacity_percent
.map_or_else(|| "N/A".to_string(), |c| c.to_string()),
battery_info
.power_rate_watts
.map_or_else(|| "N/A".to_string(), |p| format!("{:.2}", p)),
battery_info
.charge_start_threshold
.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
battery_info
.charge_stop_threshold
.map_or_else(|| "N/A".to_string(), |t| t.to_string())
);
}
}
println!("\n--- System Load ---");
println!("Load Average (1m, 5m, 15m): {:.2}, {:.2}, {:.2}",
println!(
"Load Average (1m, 5m, 15m): {:.2}, {:.2}, {:.2}",
report.system_load.load_avg_1min,
report.system_load.load_avg_5min,
report.system_load.load_avg_15min);
report.system_load.load_avg_15min
);
Ok(())
}
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
}
}
Some(Commands::SetGovernor { governor, core_id }) => {
cpu::set_governor(&governor, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
},
Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
Some(Commands::SetTurbo { setting }) => {
cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
@ -146,14 +174,15 @@ fn main() {
cpu::set_epb(&epb, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
Some(Commands::SetMinFreq { freq_mhz, core_id }) => {
cpu::set_min_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
cpu::set_min_frequency(freq_mhz, core_id)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
Some(Commands::SetMaxFreq { freq_mhz, core_id }) => {
cpu::set_max_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
Some(Commands::SetPlatformProfile { profile }) => {
cpu::set_platform_profile(&profile).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
cpu::set_max_frequency(freq_mhz, core_id)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
None => {
println!("Welcome to superfreq! Use --help for commands.");
println!("Current effective configuration: {:?}", config);
@ -172,7 +201,9 @@ fn main() {
// We'll revisit this in the future once CPU logic is more stable.
if let Some(control_error) = e.downcast_ref::<cpu::ControlError>() {
if matches!(control_error, cpu::ControlError::PermissionDenied(_)) {
eprintln!("Hint: This operation may require administrator privileges (e.g., run with sudo).");
eprintln!(
"Hint: This operation may require administrator privileges (e.g., run with sudo)."
);
}
}

View file

@ -282,46 +282,94 @@ pub fn get_cpu_core_info(
.map(|khz| khz / 1000)
.ok();
// Temperature: Iterate through hwmon to find core-specific temperatures
// This is a common but not universal approach.
// 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();
// Try to find a label that indicates it's for this core or package
// e.g. /sys/class/hwmon/hwmonX/name might be "coretemp" or similar
// and /sys/class/hwmon/hwmonX/tempY_label might be "Core Z" or "Physical id 0"
// This is highly system-dependent, and not all systems will have this. For now,
// we'll try a common pattern for "coretemp" driver because it works:tm: on my system.
// Check hwmon driver name
if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) {
// Intel CPU temperature driver
if name == "coretemp" {
// Common driver for Intel core temperatures
for i in 1..=16 {
// Check a reasonable number of temp inputs
let label_path = hw_path.join(format!("temp{}_label", i));
let input_path = hw_path.join(format!("temp{}_input", i));
if label_path.exists() && input_path.exists() {
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
// Example: "Core 0", "Core 1", etc. or "Physical id 0" for package
if label.eq_ignore_ascii_case(&format!("Core {}", core_id))
|| label
.eq_ignore_ascii_case(&format!("Package id {}", core_id))
{
//core_id might map to package for some sensors
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
temperature_celsius = Some(temp_mc as f32 / 1000.0);
break; // found temp for this core
}
}
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;
}
}
}
}
}
}
}
let usage_percent: Option<f32> = {
@ -353,6 +401,76 @@ pub fn get_cpu_core_info(
})
}
/// 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{}_label", i));
let input_path = hw_path.join(format!("temp{}_input", i));
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{}_label", i));
let input_path = hw_path.join(format!("temp{}_input", i));
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{}_input", i));
if input_path.exists() {
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
return Some(temp_mc as f32 / 1000.0);
}
}
}
None
}
pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
let initial_cpu_times = read_all_cpu_times()?;
thread::sleep(Duration::from_millis(250)); // Interval for CPU usage calculation
@ -381,7 +499,7 @@ pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
Ok(core_infos)
}
pub fn get_cpu_global_info() -> Result<CpuGlobalInfo> {
pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result<CpuGlobalInfo> {
// FIXME: Assume global settings can be read from cpu0 or are consistent.
// This might not work properly for heterogeneous systems (e.g. big.LITTLE)
let cpufreq_base = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/");
@ -423,6 +541,28 @@ pub fn get_cpu_global_info() -> Result<CpuGlobalInfo> {
let _platform_profile_choices =
read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok();
// Calculate average CPU temperature from the core temperatures
let average_temperature_celsius = if !cpu_cores.is_empty() {
// Filter cores with temperature readings, then calculate average
let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores
.iter()
.filter(|core| core.temperature_celsius.is_some())
.collect();
if !cores_with_temp.is_empty() {
// Sum up all temperatures and divide by count
let sum: f32 = cores_with_temp
.iter()
.map(|core| core.temperature_celsius.unwrap())
.sum();
Some(sum / cores_with_temp.len() as f32)
} else {
None
}
} else {
None
};
Ok(CpuGlobalInfo {
current_governor,
available_governors,
@ -430,6 +570,7 @@ pub fn get_cpu_global_info() -> Result<CpuGlobalInfo> {
epp,
epb,
platform_profile,
average_temperature_celsius,
})
}
@ -564,7 +705,7 @@ pub fn get_system_load() -> Result<SystemLoad> {
pub fn collect_system_report(config: &AppConfig) -> Result<SystemReport> {
let system_info = get_system_info()?;
let cpu_cores = get_all_cpu_core_info()?;
let cpu_global = get_cpu_global_info()?;
let cpu_global = get_cpu_global_info(&cpu_cores)?;
let batteries = get_battery_info(config)?;
let system_load = get_system_load()?;