diff --git a/src/core.rs b/src/core.rs index 102069b..fa3f188 100644 --- a/src/core.rs +++ b/src/core.rs @@ -20,9 +20,10 @@ pub struct CpuGlobalInfo { pub current_governor: Option, pub available_governors: Vec, pub turbo_status: Option, // true for enabled, false for disabled - pub epp: Option, // Energy Performance Preference - pub epb: Option, // Energy Performance Bias + pub epp: Option, // Energy Performance Preference + pub epb: Option, // Energy Performance Bias pub platform_profile: Option, + pub average_temperature_celsius: Option, // 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 { diff --git a/src/main.rs b/src/main.rs index 7a4e1a0..3a668a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, }, /// Set ACPI platform profile - SetPlatformProfile { - profile: String, - }, + SetPlatformProfile { profile: String }, } fn main() { @@ -75,67 +73,97 @@ fn main() { }; let command_result = match cli.command { - 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!("Timestamp: {:?}", report.timestamp); + 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!("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!("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!("\n--- CPU Global Info ---"); + println!("Current Governor: {:?}", report.cpu_global.current_governor); + 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!("\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)) + ); + } + + 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!( - " 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)) + " 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--- 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), + + 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(()) } - } - Some(Commands::SetGovernor { governor, core_id }) => { - cpu::set_governor(&governor, core_id).map_err(|e| Box::new(e) as Box) - } + Err(e) => Err(Box::new(e) as Box), + }, + Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id) + .map_err(|e| Box::new(e) as Box), Some(Commands::SetTurbo { setting }) => { cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box) } @@ -146,14 +174,15 @@ fn main() { cpu::set_epb(&epb, core_id).map_err(|e| Box::new(e) as Box) } Some(Commands::SetMinFreq { freq_mhz, core_id }) => { - cpu::set_min_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box) + cpu::set_min_frequency(freq_mhz, core_id) + .map_err(|e| Box::new(e) as Box) } Some(Commands::SetMaxFreq { freq_mhz, core_id }) => { - cpu::set_max_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box) - } - Some(Commands::SetPlatformProfile { profile }) => { - cpu::set_platform_profile(&profile).map_err(|e| Box::new(e) as Box) + cpu::set_max_frequency(freq_mhz, core_id) + .map_err(|e| Box::new(e) as Box) } + Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile) + .map_err(|e| Box::new(e) as Box), 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::() { 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)." + ); } } diff --git a/src/monitor.rs b/src/monitor.rs index 572e0c9..96e3aa3 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -282,45 +282,93 @@ 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 = 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::(&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::(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 { + 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::(&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 { + 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::(&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::(&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 { + 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::(&input_path) { + return Some(temp_mc as f32 / 1000.0); + } + } + } + None +} + pub fn get_all_cpu_core_info() -> Result> { 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> { Ok(core_infos) } -pub fn get_cpu_global_info() -> Result { +pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result { // 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 { 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 { epp, epb, platform_profile, + average_temperature_celsius, }) } @@ -564,7 +705,7 @@ pub fn get_system_load() -> Result { pub fn collect_system_report(config: &AppConfig) -> Result { 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()?;