From 137f801d2b4796018f797eb82b72b283e101efd0 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:41:53 +0300 Subject: [PATCH] cpu&power: add more attributes --- src/fs.rs | 19 ++++---- src/main.rs | 2 +- src/monitor.rs | 104 -------------------------------------------- src/power_supply.rs | 48 ++++++++++++++++++++ src/system.rs | 98 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 116 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index b1d1c71..9b150b3 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -29,18 +29,19 @@ pub fn read(path: impl AsRef) -> Option> { } } -pub fn read_u64(path: impl AsRef) -> anyhow::Result { +pub fn read_u64(path: impl AsRef) -> Option> { let path = path.as_ref(); - let content = fs::read_to_string(path) - .with_context(|| format!("failed to read '{path}'", path = path.display()))?; + match read(path)? { + Ok(content) => Some(content.trim().parse().with_context(|| { + format!( + "failed to parse contents of '{path}' as a unsigned number", + path = path.display(), + ) + })), - Ok(content.trim().parse().with_context(|| { - format!( - "failed to parse contents of '{path}' as a unsigned number", - path = path.display(), - ) - })?) + Err(error) => Some(Err(error)), + } } pub fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { diff --git a/src/main.rs b/src/main.rs index 825465d..cd6258f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ enum Command { /// Start the daemon. Start { /// The daemon config path. - #[arg(long, env = "SUPERFREQ_CONFIG")] + #[arg(long, env = "WATT_CONFIG")] config: PathBuf, }, diff --git a/src/monitor.rs b/src/monitor.rs index 79d2635..cda52dc 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -599,110 +599,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> Ok(batteries) } -/// Check if a battery is likely a peripheral (mouse, keyboard, etc) not a laptop battery -fn is_peripheral_battery(ps_path: &Path, name: &str) -> bool { - // Convert name to lowercase once for case-insensitive matching - let name_lower = name.to_lowercase(); - - // Common peripheral battery names - if name_lower.contains("mouse") - || name_lower.contains("keyboard") - || name_lower.contains("trackpad") - || name_lower.contains("gamepad") - || name_lower.contains("controller") - || name_lower.contains("headset") - || name_lower.contains("headphone") - { - return true; - } - - // Small capacity batteries are likely not laptop batteries - if let Ok(energy_full) = read_sysfs_value::(ps_path.join("energy_full")) { - // Most laptop batteries are at least 20,000,000 µWh (20 Wh) - // Peripheral batteries are typically much smaller - if energy_full < 10_000_000 { - // 10 Wh in µWh - return true; - } - } - - // Check for model name that indicates a peripheral - if let Ok(model) = read_sysfs_file_trimmed(ps_path.join("model_name")) { - if model.contains("bluetooth") || model.contains("wireless") { - return true; - } - } - - false -} - -/// Determine if this is likely a desktop system rather than a laptop -fn is_likely_desktop_system() -> bool { - // Check for DMI system type information - if let Ok(chassis_type) = fs::read_to_string("/sys/class/dmi/id/chassis_type") { - let chassis_type = chassis_type.trim(); - - // Chassis types: - // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower - // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One - // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis - match chassis_type { - "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return true, // desktop form factors - "9" | "10" | "14" => return false, // laptop form factors - _ => {} // Unknown, continue with other checks - } - } - - // Check CPU power policies, desktops often don't have these - let power_saving_exists = Path::new("/sys/module/intel_pstate/parameters/no_hwp").exists() - || Path::new("/sys/devices/system/cpu/cpufreq/conservative").exists(); - - if !power_saving_exists { - return true; // likely a desktop - } - - // Check battery-specific ACPI paths that laptops typically have - let laptop_acpi_paths = [ - "/sys/class/power_supply/BAT0", - "/sys/class/power_supply/BAT1", - "/proc/acpi/battery", - ]; - - for path in &laptop_acpi_paths { - if Path::new(path).exists() { - return false; // Likely a laptop - } - } - - // Default to assuming desktop if we can't determine - true -} - -pub fn get_system_load() -> anyhow::Result { - let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?; - let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); - if parts.len() < 3 { - return Err(SysMonitorError::ParseError( - "Could not parse /proc/loadavg: expected at least 3 parts".to_string(), - )); - } - let load_avg_1min = parts[0].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0])) - })?; - let load_avg_5min = parts[1].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1])) - })?; - let load_avg_15min = parts[2].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2])) - })?; - - Ok(SystemLoad { - load_avg_1min, - load_avg_5min, - load_avg_15min, - }) -} - pub fn collect_system_report(config: &AppConfig) -> anyhow::Result { let system_info = get_system_info(); let cpu_cores = get_all_cpu_core_info()?; diff --git a/src/power_supply.rs b/src/power_supply.rs index f1dcb41..025ac75 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -48,6 +48,9 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ pub struct PowerSupply { pub name: String, pub path: PathBuf, + + pub is_from_peripheral: bool, + pub threshold_config: Option, } @@ -74,6 +77,9 @@ impl PowerSupply { let mut power_supply = Self { path: Path::new(POWER_SUPPLY_PATH).join(&name), name, + + is_from_peripheral: false, + threshold_config: None, }; @@ -94,6 +100,8 @@ impl PowerSupply { path, + is_from_peripheral: false, + threshold_config: None, }; @@ -157,6 +165,46 @@ impl PowerSupply { self.threshold_config = threshold_config; + self.is_from_peripheral = 'is_from_peripheral: { + let name_lower = self.name.to_lowercase(); + + // Common peripheral battery names. + if name_lower.contains("mouse") + || name_lower.contains("keyboard") + || name_lower.contains("trackpad") + || name_lower.contains("gamepad") + || name_lower.contains("controller") + || name_lower.contains("headset") + || name_lower.contains("headphone") + { + break 'is_from_peripheral true; + } + + // Small capacity batteries are likely not laptop batteries. + if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) { + let energy_full = energy_full + .with_context(|| format!("failed to read the max charge '{self}' can hold"))?; + + // Most laptop batteries are at least 20,000,000 µWh (20 Wh). + // Peripheral batteries are typically much smaller. + if energy_full < 10_000_000 { + // 10 Wh in µWh. + break 'is_from_peripheral true; + } + } + // Check for model name that indicates a peripheral + if let Some(model) = fs::read(self.path.join("model_name")) { + let model = + model.with_context(|| format!("failed to read the model name of '{self}'"))?; + + if model.contains("bluetooth") || model.contains("wireless") { + break 'is_from_peripheral true; + } + } + + false + }; + Ok(()) } diff --git a/src/system.rs b/src/system.rs index 1d3e697..cc39776 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,14 +1,108 @@ +use anyhow::{Context, bail}; + +use crate::fs; + pub struct System { pub is_desktop: bool, + + pub load_average_1min: f64, + pub load_average_5min: f64, + pub load_average_15min: f64, } impl System { pub fn new() -> anyhow::Result { - let mut system = Self { is_desktop: false }; + let mut system = Self { + is_desktop: false, + + load_average_1min: 0.0, + load_average_5min: 0.0, + load_average_15min: 0.0, + }; + system.rescan()?; Ok(system) } - pub fn rescan(&mut self) -> anyhow::Result<()> {} + pub fn rescan(&mut self) -> anyhow::Result<()> { + self.is_desktop = self.is_desktop()?; + + let (load_average_1min, load_average_5min, load_average_15min) = self.load_average()?; + self.load_average_1min = load_average_1min; + self.load_average_5min = load_average_5min; + self.load_average_15min = load_average_15min; + + Ok(()) + } + + fn is_desktop(&self) -> anyhow::Result { + if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { + let chassis_type = chassis_type.context("failed to read chassis type")?; + + // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower + // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One + // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis + match chassis_type.trim() { + // Desktop form factors. + "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return Ok(true), + // Laptop form factors. + "9" | "10" | "14" => return Ok(false), + // Unknown, continue with other checks + _ => {} + } + } + + // Check CPU power policies, desktops often don't have these + let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") + || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); + + if !power_saving_exists { + return Ok(true); // Likely a desktop. + } + + // Check battery-specific ACPI paths that laptops typically have + let laptop_acpi_paths = [ + "/sys/class/power_supply/BAT0", + "/sys/class/power_supply/BAT1", + "/proc/acpi/battery", + ]; + + for path in laptop_acpi_paths { + if fs::exists(path) { + return Ok(false); // Likely a laptop. + } + } + + // Default to assuming desktop if we can't determine. + Ok(true) + } + + fn load_average(&self) -> anyhow::Result<(f64, f64, f64)> { + let content = fs::read("/proc/loadavg") + .context("load average file doesn't exist, are you on linux?")? + .context("failed to read load average")?; + + let mut parts = content.split_whitespace(); + + let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) = + (parts.next(), parts.next(), parts.next()) + else { + bail!( + "failed to parse first 3 load average entries due to there not being enough, content: {content}" + ); + }; + + Ok(( + load_average_1min + .parse() + .context("failed to parse load average")?, + load_average_5min + .parse() + .context("failed to parse load average")?, + load_average_15min + .parse() + .context("failed to parse load average")?, + )) + } }