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

@ -20,9 +20,10 @@ pub struct CpuGlobalInfo {
pub current_governor: Option<String>, pub current_governor: Option<String>,
pub available_governors: Vec<String>, pub available_governors: Vec<String>,
pub turbo_status: Option<bool>, // true for enabled, false for disabled pub turbo_status: Option<bool>, // true for enabled, false for disabled
pub epp: Option<String>, // Energy Performance Preference pub epp: Option<String>, // Energy Performance Preference
pub epb: Option<String>, // Energy Performance Bias pub epb: Option<String>, // Energy Performance Bias
pub platform_profile: Option<String>, pub platform_profile: Option<String>,
pub average_temperature_celsius: Option<f32>, // Average temperature across all cores
} }
pub struct BatteryInfo { pub struct BatteryInfo {
@ -59,8 +60,8 @@ pub enum OperationalMode {
Performance, Performance,
} }
use serde::Deserialize;
use clap::ValueEnum; use clap::ValueEnum;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
pub enum TurboSetting { pub enum TurboSetting {

View file

@ -1,12 +1,12 @@
mod core;
mod config; mod config;
mod monitor; mod core;
mod cpu; mod cpu;
mod engine; mod engine;
mod monitor;
use clap::Parser;
use crate::config::AppConfig; use crate::config::AppConfig;
use crate::core::TurboSetting; use crate::core::TurboSetting;
use clap::Parser;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
@ -55,9 +55,7 @@ enum Commands {
core_id: Option<u32>, core_id: Option<u32>,
}, },
/// Set ACPI platform profile /// Set ACPI platform profile
SetPlatformProfile { SetPlatformProfile { profile: String },
profile: String,
},
} }
fn main() { fn main() {
@ -75,67 +73,97 @@ fn main() {
}; };
let command_result = match cli.command { let command_result = match cli.command {
Some(Commands::Info) => { Some(Commands::Info) => match monitor::collect_system_report(&config) {
match monitor::collect_system_report(&config) { Ok(report) => {
Ok(report) => { println!("--- System Information ---");
println!("--- System Information ---"); println!("CPU Model: {}", report.system_info.cpu_model);
println!("CPU Model: {}", report.system_info.cpu_model); println!("Architecture: {}", report.system_info.architecture);
println!("Architecture: {}", report.system_info.architecture); println!(
println!("Linux Distribution: {}", report.system_info.linux_distribution); "Linux Distribution: {}",
println!("Timestamp: {:?}", report.timestamp); report.system_info.linux_distribution
);
println!("Timestamp: {:?}", report.timestamp);
println!("\n--- CPU Global Info ---"); println!("\n--- CPU Global Info ---");
println!("Current Governor: {:?}", report.cpu_global.current_governor); println!("Current Governor: {:?}", report.cpu_global.current_governor);
println!("Available Governors: {:?}", report.cpu_global.available_governors.join(", ")); println!(
println!("Turbo Status: {:?}", report.cpu_global.turbo_status); "Available Governors: {:?}",
println!("EPP: {:?}", report.cpu_global.epp); report.cpu_global.available_governors.join(", ")
println!("EPB: {:?}", report.cpu_global.epb); );
println!("Platform Profile: {:?}", report.cpu_global.platform_profile); 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 ---"); println!("\n--- CPU Core Info ---");
for core_info in report.cpu_cores { 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))
);
}
println!("\n--- Battery Info ---");
if report.batteries.is_empty() {
println!(" No batteries found or all are ignored.");
} else {
for battery_info in report.batteries {
println!( println!(
" Core {}: Current Freq: {:?} MHz, Min Freq: {:?} MHz, Max Freq: {:?} MHz, Usage: {:?}%, Temp: {:?}°C", " Battery {}: AC Connected: {}, State: {:?}, Capacity: {:?}%, Power Rate: {:?} W, Charge Thresholds: {:?}-{:?}",
core_info.core_id, battery_info.name,
core_info.current_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()), battery_info.ac_connected,
core_info.min_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()), battery_info.charging_state.as_deref().unwrap_or("N/A"),
core_info.max_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()), battery_info
core_info.usage_percent.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)), .capacity_percent
core_info.temperature_celsius.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)) .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--- Battery Info ---");
if report.batteries.is_empty() {
println!(" No batteries found or all are ignored.");
} else {
for battery_info in report.batteries {
println!(
" Battery {}: AC Connected: {}, State: {:?}, Capacity: {:?}%, Power Rate: {:?} W, Charge Thresholds: {:?}-{:?}",
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())
);
}
}
println!("\n--- System Load ---");
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);
Ok(())
} }
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
println!("\n--- System Load ---");
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
);
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 }) => { Some(Commands::SetTurbo { setting }) => {
cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>) 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>) 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 }) => { 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 }) => { 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>) 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>)
} }
Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
None => { None => {
println!("Welcome to superfreq! Use --help for commands."); println!("Welcome to superfreq! Use --help for commands.");
println!("Current effective configuration: {:?}", config); println!("Current effective configuration: {:?}", config);
@ -172,7 +201,9 @@ fn main() {
// We'll revisit this in the future once CPU logic is more stable. // We'll revisit this in the future once CPU logic is more stable.
if let Some(control_error) = e.downcast_ref::<cpu::ControlError>() { if let Some(control_error) = e.downcast_ref::<cpu::ControlError>() {
if matches!(control_error, cpu::ControlError::PermissionDenied(_)) { 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,45 +282,93 @@ pub fn get_cpu_core_info(
.map(|khz| khz / 1000) .map(|khz| khz / 1000)
.ok(); .ok();
// Temperature: Iterate through hwmon to find core-specific temperatures // Temperature detection.
// This is a common but not universal approach. // 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; let mut temperature_celsius: Option<f32> = None;
// Search for temperature in hwmon devices
if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") { if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") {
for hw_entry in hwmon_dir.flatten() { for hw_entry in hwmon_dir.flatten() {
let hw_path = hw_entry.path(); 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 // Check hwmon driver name
// 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.
if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) { if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) {
// Intel CPU temperature driver
if name == "coretemp" { if name == "coretemp" {
// Common driver for Intel core temperatures if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") {
for i in 1..=16 { temperature_celsius = Some(temp);
// Check a reasonable number of temp inputs break;
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() { // AMD CPU temperature driver
if let Ok(label) = read_sysfs_file_trimmed(&label_path) { // TODO: 'zenergy' can also report those stats, I think?
// Example: "Core 0", "Core 1", etc. or "Physical id 0" for package else if name == "k10temp" || name == "zenpower" || name == "amdgpu" {
if label.eq_ignore_ascii_case(&format!("Core {}", core_id)) // AMD's k10temp doesn't always label cores individually
|| label // First try to find core-specific temps
.eq_ignore_ascii_case(&format!("Package id {}", core_id)) if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Tdie") {
{ temperature_celsius = Some(temp);
//core_id might map to package for some sensors break;
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 // 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;
} }
} }
} }
} }
} }
if temperature_celsius.is_some() {
break;
}
} }
} }
@ -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>> { pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
let initial_cpu_times = read_all_cpu_times()?; let initial_cpu_times = read_all_cpu_times()?;
thread::sleep(Duration::from_millis(250)); // Interval for CPU usage calculation 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) 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. // FIXME: Assume global settings can be read from cpu0 or are consistent.
// This might not work properly for heterogeneous systems (e.g. big.LITTLE) // This might not work properly for heterogeneous systems (e.g. big.LITTLE)
let cpufreq_base = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/"); 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 = let _platform_profile_choices =
read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok(); 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 { Ok(CpuGlobalInfo {
current_governor, current_governor,
available_governors, available_governors,
@ -430,6 +570,7 @@ pub fn get_cpu_global_info() -> Result<CpuGlobalInfo> {
epp, epp,
epb, epb,
platform_profile, 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> { pub fn collect_system_report(config: &AppConfig) -> Result<SystemReport> {
let system_info = get_system_info()?; let system_info = get_system_info()?;
let cpu_cores = get_all_cpu_core_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 batteries = get_battery_info(config)?;
let system_load = get_system_load()?; let system_load = get_system_load()?;