diff --git a/src/config.rs b/src/config.rs index 4de0ac3..b68485a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,7 @@ pub struct PowerDelta { impl PowerDelta { pub fn apply(&self) -> anyhow::Result<()> { - let mut power_supplies = match &self.for_ { + let power_supplies = match &self.for_ { Some(names) => { let mut power_supplies = Vec::with_capacity(names.len()); @@ -136,13 +136,13 @@ impl PowerDelta { .collect(), }; - for power_supply in &mut power_supplies { + for power_supply in power_supplies { if let Some(threshold_start) = self.charge_threshold_start { - power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?; + power_supply.set_charge_threshold_start(threshold_start)?; } if let Some(threshold_end) = self.charge_threshold_end { - power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?; + power_supply.set_charge_threshold_end(threshold_end)?; } } diff --git a/src/core.rs b/src/core.rs index 2e32854..38c3d0c 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,13 +6,16 @@ pub struct SystemInfo { pub struct CpuCoreInfo { // Per-core data pub core_id: u32, + pub usage_percent: Option, pub temperature_celsius: Option, } pub struct CpuGlobalInfo { // System-wide CPU settings - pub epp: Option, // Energy Performance Preference - pub epb: Option, // Energy Performance Bias + pub turbo_status: Option, // true for enabled, false for disabled + 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 } diff --git a/src/cpu.rs b/src/cpu.rs index 6712cdf..c41a0d0 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -49,10 +49,6 @@ impl Cpu { pub fn time_idle(&self) -> u64 { self.time_idle + self.time_iowait } - - pub fn usage(&self) -> f64 { - 1.0 - self.time_idle() as f64 / self.time_total() as f64 - } } impl fmt::Display for Cpu { @@ -160,8 +156,8 @@ impl Cpu { // TODO: Don't read this per CPU. Share the read or // find something in /sys/.../cpu{N} that does it. let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; + .context("/proc/stat does not exist")? + .context("failed to read CPU stat")?; let cpu_name = format!("cpu{number}", number = self.number); @@ -175,44 +171,44 @@ impl Cpu { self.time_user = stats .next() - .with_context(|| format!("failed to parse {self} user time"))? + .with_context(|| format!("failed to find {self} user time"))? .parse() - .with_context(|| format!("failed to find {self} user time"))?; + .with_context(|| format!("failed to parse {self} user time"))?; self.time_nice = stats .next() - .with_context(|| format!("failed to parse {self} nice time"))? + .with_context(|| format!("failed to find {self} nice time"))? .parse() - .with_context(|| format!("failed to find {self} nice time"))?; + .with_context(|| format!("failed to parse {self} nice time"))?; self.time_system = stats .next() - .with_context(|| format!("failed to parse {self} system time"))? + .with_context(|| format!("failed to find {self} system time"))? .parse() - .with_context(|| format!("failed to find {self} system time"))?; + .with_context(|| format!("failed to parse {self} system time"))?; self.time_idle = stats .next() - .with_context(|| format!("failed to parse {self} idle time"))? + .with_context(|| format!("failed to find {self} idle time"))? .parse() - .with_context(|| format!("failed to find {self} idle time"))?; + .with_context(|| format!("failed to parse {self} idle time"))?; self.time_iowait = stats .next() - .with_context(|| format!("failed to parse {self} iowait time"))? + .with_context(|| format!("failed to find {self} iowait time"))? .parse() - .with_context(|| format!("failed to find {self} iowait time"))?; + .with_context(|| format!("failed to parse {self} iowait time"))?; self.time_irq = stats .next() - .with_context(|| format!("failed to parse {self} irq time"))? + .with_context(|| format!("failed to find {self} irq time"))? .parse() - .with_context(|| format!("failed to find {self} irq time"))?; + .with_context(|| format!("failed to parse {self} irq time"))?; self.time_softirq = stats .next() - .with_context(|| format!("failed to parse {self} softirq time"))? + .with_context(|| format!("failed to find {self} softirq time"))? .parse() - .with_context(|| format!("failed to find {self} softirq time"))?; + .with_context(|| format!("failed to parse {self} softirq time"))?; self.time_steal = stats .next() - .with_context(|| format!("failed to parse {self} steal time"))? + .with_context(|| format!("failed to find {self} steal time"))? .parse() - .with_context(|| format!("failed to find {self} steal time"))?; + .with_context(|| format!("failed to parse {self} steal time"))?; Ok(()) } @@ -221,11 +217,9 @@ impl Cpu { let Self { number, .. } = *self; self.available_governors = 'available_governors: { - let Some(content) = fs::read(format!( + let Some(Ok(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" - )) - .with_context(|| format!("failed to read {self} available governors"))? - else { + )) else { break 'available_governors Vec::new(); }; @@ -239,8 +233,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" )) - .with_context(|| format!("failed to read {self} scaling governor"))? - .with_context(|| format!("failed to find {self} scaling governor"))?, + .with_context(|| format!("failed to find {self} scaling governor"))? + .with_context(|| format!("failed to read {self} scaling governor"))?, ); Ok(()) @@ -249,21 +243,21 @@ impl Cpu { fn rescan_frequency(&mut self) -> anyhow::Result<()> { let Self { number, .. } = *self; - let frequency_khz = fs::read_n::(format!( + let frequency_khz = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" )) - .with_context(|| format!("failed to parse {self} frequency"))? - .with_context(|| format!("failed to find {self} frequency"))?; - let frequency_khz_minimum = fs::read_n::(format!( + .with_context(|| format!("failed to find {self} frequency"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + let frequency_khz_minimum = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) - .with_context(|| format!("failed to parse {self} frequency minimum"))? - .with_context(|| format!("failed to find {self} frequency"))?; - let frequency_khz_maximum = fs::read_n::(format!( + .with_context(|| format!("failed to find {self} frequency minimum"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + let frequency_khz_maximum = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" )) - .with_context(|| format!("failed to parse {self} frequency maximum"))? - .with_context(|| format!("failed to find {self} frequency"))?; + .with_context(|| format!("failed to find {self} frequency maximum"))? + .with_context(|| format!("failed to parse {self} frequency"))?; self.frequency_mhz = Some(frequency_khz / 1000); self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); @@ -273,12 +267,12 @@ impl Cpu { } fn rescan_epp(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = *self; + let Self { number, .. } = self; self.available_epps = 'available_epps: { - let Some(content) = fs::read(format!( + let Some(Ok(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" - )).with_context(|| format!("failed to read {self} available EPPs"))? else { + )) else { break 'available_epps Vec::new(); }; @@ -292,8 +286,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" )) - .with_context(|| format!("failed to read {self} EPP"))? - .with_context(|| format!("failed to find {self} EPP"))?, + .with_context(|| format!("failed to find {self} EPP"))? + .with_context(|| format!("failed to read {self} EPP"))?, ); Ok(()) @@ -334,8 +328,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" )) - .with_context(|| format!("failed to read {self} EPB"))? - .with_context(|| format!("failed to find {self} EPB"))?, + .with_context(|| format!("failed to find {self} EPB"))? + .with_context(|| format!("failed to read {self} EPB"))?, ); Ok(()) @@ -393,14 +387,10 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing EPP") - })?; - - self.epp = Some(epp.to_owned()); - - Ok(()) + }) } - pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { + pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { let Self { number, available_epbs: ref epbs, @@ -420,11 +410,7 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing EPB") - })?; - - self.epb = Some(epb.to_owned()); - - Ok(()) + }) } pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { @@ -452,11 +438,9 @@ impl Cpu { fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Some(minimum_frequency_khz) = fs::read_n::(format!( + let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) - .with_context(|| format!("failed to read {self} minimum frequency"))? - else { + )) else { // Just let it pass if we can't find anything. return Ok(()); }; @@ -496,11 +480,9 @@ impl Cpu { fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Some(maximum_frequency_khz) = fs::read_n::(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" - )) - .with_context(|| format!("failed to read {self} maximum frequency"))? - else { + let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" + )) else { // Just let it pass if we can't find anything. return Ok(()); }; @@ -563,20 +545,4 @@ impl Cpu { bail!("no supported CPU boost control mechanism found"); } - - pub fn turbo() -> anyhow::Result> { - if let Some(content) = fs::read_n::("/sys/devices/system/cpu/intel_pstate/no_turbo") - .context("failed to read CPU turbo boost status")? - { - return Ok(Some(content == 0)); - } - - if let Some(content) = fs::read_n::("/sys/devices/system/cpu/cpufreq/boost") - .context("failed to read CPU turbo boost status")? - { - return Ok(Some(content == 1)); - } - - Ok(None) - } } diff --git a/src/fs.rs b/src/fs.rs index 526856d..4c11178 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,4 +1,4 @@ -use std::{error, fs, io, path::Path, str}; +use std::{fs, io, path::Path}; use anyhow::Context; @@ -15,35 +15,32 @@ pub fn read_dir(path: impl AsRef) -> anyhow::Result { .with_context(|| format!("failed to read directory '{path}'", path = path.display())) } -pub fn read(path: impl AsRef) -> anyhow::Result> { +pub fn read(path: impl AsRef) -> Option> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Ok(Some(string.trim().to_owned())), + Ok(string) => Some(Ok(string.trim().to_owned())), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) if error.kind() == io::ErrorKind::NotFound => None, - Err(error) => { - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) - } + Err(error) => Some( + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())), + ), } } -pub fn read_n(path: impl AsRef) -> anyhow::Result> -where - N::Err: error::Error + Send + Sync + 'static, -{ +pub fn read_u64(path: impl AsRef) -> Option> { let path = path.as_ref(); match read(path)? { - Some(content) => Ok(Some(content.trim().parse().with_context(|| { + Ok(content) => Some(content.trim().parse().with_context(|| { format!( "failed to parse contents of '{path}' as a unsigned number", path = path.display(), ) - })?)), + })), - None => Ok(None), + Err(error) => Some(Err(error)), } } diff --git a/src/monitor.rs b/src/monitor.rs index 88195c8..609213d 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -111,8 +111,28 @@ pub fn get_cpu_core_info( } } + let usage_percent: Option = { + let prev_idle = prev_times.idle_time(); + let current_idle = current_times.idle_time(); + + let prev_total = prev_times.total_time(); + let current_total = current_times.total_time(); + + let total_diff = current_total.saturating_sub(prev_total); + let idle_diff = current_idle.saturating_sub(prev_idle); + + // Avoid division by zero if no time has passed or counters haven't changed + if total_diff == 0 { + None + } else { + let usage = 100.0 * (1.0 - (idle_diff as f32 / total_diff as f32)); + Some(usage.clamp(0.0, 100.0)) // clamp between 0 and 100 + } + }; + Ok(CpuCoreInfo { core_id, + usage_percent, temperature_celsius, }) } @@ -218,6 +238,23 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { + let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); + let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); + + let turbo_status = if turbo_status_path.exists() { + // 0 means turbo enabled, 1 means disabled for intel_pstate + read_sysfs_value::(turbo_status_path) + .map(|val| val == 0) + .ok() + } else if boost_path.exists() { + // 1 means turbo enabled, 0 means disabled for generic cpufreq boost + read_sysfs_value::(boost_path).map(|val| val == 1).ok() + } else { + None + }; + + let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); + // Calculate average CPU temperature from the core temperatures let average_temperature_celsius = if cpu_cores.is_empty() { None @@ -242,10 +279,146 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { + turbo_status, + platform_profile, average_temperature_celsius, } } +pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> { + let mut batteries = Vec::new(); + let power_supply_path = Path::new("/sys/class/power_supply"); + + if !power_supply_path.exists() { + return Ok(batteries); // no power supply directory + } + + let ignored_supplies = config.ignored_power_supplies.clone().unwrap_or_default(); + + // Determine overall AC connection status + let mut overall_ac_connected = false; + for entry in fs::read_dir(power_supply_path)? { + let entry = entry?; + let ps_path = entry.path(); + let name = entry.file_name().into_string().unwrap_or_default(); + + // Check for AC adapter type (common names: AC, ACAD, ADP) + if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { + if ps_type == "Mains" + || ps_type == "USB_PD_DRP" + || ps_type == "USB_PD" + || ps_type == "USB_DCP" + || ps_type == "USB_CDP" + || ps_type == "USB_ACA" + { + // USB types can also provide power + if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { + if online == 1 { + overall_ac_connected = true; + break; + } + } + } + } else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") { + // Fallback for type file missing + if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { + if online == 1 { + overall_ac_connected = true; + break; + } + } + } + } + + // No AC adapter detected but we're on a desktop system + // Default to AC power for desktops + if !overall_ac_connected { + overall_ac_connected = is_likely_desktop_system(); + } + + for entry in fs::read_dir(power_supply_path)? { + let entry = entry?; + let ps_path = entry.path(); + let name = entry.file_name().into_string().unwrap_or_default(); + + if ignored_supplies.contains(&name) { + continue; + } + + if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { + if ps_type == "Battery" { + // Skip peripheral batteries that aren't real laptop batteries + if is_peripheral_battery(&ps_path, &name) { + log::debug!("Skipping peripheral battery: {name}"); + continue; + } + + let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok(); + let capacity_percent = read_sysfs_value::(ps_path.join("capacity")).ok(); + + let power_rate_watts = if ps_path.join("power_now").exists() { + read_sysfs_value::(ps_path.join("power_now")) // uW + .map(|uw| uw as f32 / 1_000_000.0) + .ok() + } else if ps_path.join("current_now").exists() + && ps_path.join("voltage_now").exists() + { + let current_ua = read_sysfs_value::(ps_path.join("current_now")).ok(); // uA + let voltage_uv = read_sysfs_value::(ps_path.join("voltage_now")).ok(); // uV + if let (Some(c), Some(v)) = (current_ua, voltage_uv) { + // Power (W) = (Voltage (V) * Current (A)) + // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W + Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32) + } else { + None + } + } else { + None + }; + + let charge_start_threshold = + read_sysfs_value::(ps_path.join("charge_control_start_threshold")).ok(); + let charge_stop_threshold = + read_sysfs_value::(ps_path.join("charge_control_end_threshold")).ok(); + + batteries.push(BatteryInfo { + name: name.clone(), + ac_connected: overall_ac_connected, + charging_state: status_str, + capacity_percent, + power_rate_watts, + charge_start_threshold, + charge_stop_threshold, + }); + } + } + } + + // If we found no batteries but have power supplies, we're likely on a desktop + if batteries.is_empty() && overall_ac_connected { + log::debug!("No laptop batteries found, likely a desktop system"); + } + + Ok(batteries) +} + +pub fn collect_system_report(config: &AppConfig) -> anyhow::Result { + let system_info = get_system_info(); + let cpu_cores = get_all_cpu_core_info()?; + let cpu_global = get_cpu_global_info(&cpu_cores); + let batteries = get_battery_info(config)?; + let system_load = get_system_load()?; + + Ok(SystemReport { + system_info, + cpu_cores, + cpu_global, + batteries, + system_load, + timestamp: SystemTime::now(), + }) +} + pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index f213e5b..5bbcebc 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -44,38 +44,16 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ ]; /// Represents a power supply that supports charge threshold control. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PowerSupply { pub name: String, pub path: PathBuf, - pub type_: String, pub is_from_peripheral: bool, - pub charge_state: Option, - pub charge_percent: Option, - - pub charge_threshold_start: f64, - pub charge_threshold_end: f64, - - pub drain_rate_watts: Option, - pub threshold_config: Option, } -impl PowerSupply { - pub fn is_ac(&self) -> bool { - !self.is_from_peripheral - && matches!( - &*self.type_, - "Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA" - ) - || self.type_.starts_with("AC") - || self.type_.contains("ACAD") - || self.type_.contains("ADP") - } -} - impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "power supply '{name}'", name = self.name.yellow())?; @@ -99,15 +77,6 @@ impl PowerSupply { let mut power_supply = Self { path: Path::new(POWER_SUPPLY_PATH).join(&name), name, - type_: String::new(), - - charge_state: None, - charge_percent: None, - - charge_threshold_start: 0.0, - charge_threshold_end: 1.0, - - drain_rate_watts: None, is_from_peripheral: false, @@ -130,15 +99,6 @@ impl PowerSupply { .to_string(), path, - type_: String::new(), - - charge_state: None, - charge_percent: None, - - charge_threshold_start: 0.0, - charge_threshold_end: 1.0, - - drain_rate_watts: None, is_from_peripheral: false, @@ -171,18 +131,37 @@ impl PowerSupply { Ok(power_supplies) } + fn get_type(&self) -> anyhow::Result { + let type_path = self.path.join("type"); + + let type_ = fs::read(&type_path) + .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? + .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; + + Ok(type_) + } + pub fn rescan(&mut self) -> anyhow::Result<()> { if !self.path.exists() { bail!("{self} does not exist"); } - self.type_ = { - let type_path = self.path.join("type"); + self.threshold_config = self + .get_type() + .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? + .eq("Battery") + .then(|| { + for config in POWER_SUPPLY_THRESHOLD_CONFIGS { + if self.path.join(config.path_start).exists() + && self.path.join(config.path_end).exists() + { + return Some(*config); + } + } - fs::read(&type_path) - .with_context(|| format!("failed to read '{path}'", path = type_path.display()))? - .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? - }; + None + }) + .flatten(); self.is_from_peripheral = 'is_from_peripheral: { let name_lower = self.name.to_lowercase(); @@ -200,9 +179,10 @@ impl PowerSupply { } // Small capacity batteries are likely not laptop batteries. - if let Some(energy_full) = fs::read_n::(self.path.join("energy_full")) - .with_context(|| format!("failed to read the max charge {self} can hold"))? - { + 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 { @@ -211,9 +191,10 @@ impl PowerSupply { } } // Check for model name that indicates a peripheral - if let Some(model) = fs::read(self.path.join("model_name")) - .with_context(|| format!("failed to read the model name of {self}"))? - { + 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; } @@ -222,53 +203,6 @@ impl PowerSupply { false }; - if self.type_ == "Battery" { - self.charge_state = fs::read(self.path.join("status")) - .with_context(|| format!("failed to read {self} charge status"))?; - - self.charge_percent = fs::read_n::(self.path.join("capacity")) - .with_context(|| format!("failed to read {self} charge percent"))? - .map(|percent| percent as f64 / 100.0); - - self.charge_threshold_start = - fs::read_n::(self.path.join("charge_control_start_threshold")) - .with_context(|| format!("failed to read {self} charge threshold start"))? - .map_or(0.0, |percent| percent as f64 / 100.0); - - self.charge_threshold_end = - fs::read_n::(self.path.join("charge_control_end_threshold")) - .with_context(|| format!("failed to read {self} charge threshold end"))? - .map_or(100.0, |percent| percent as f64 / 100.0); - - self.drain_rate_watts = match fs::read_n::(self.path.join("power_now")) - .with_context(|| format!("failed to read {self} power drain"))? - { - Some(drain) => Some(drain as f64), - - None => { - let current_ua = fs::read_n::(self.path.join("current_now")) - .with_context(|| format!("failed to read {self} current"))?; - - let voltage_uv = fs::read_n::(self.path.join("voltage_now")) - .with_context(|| format!("failed to read {self} voltage"))?; - - current_ua.zip(voltage_uv).map(|(current, voltage)| { - // Power (W) = Voltage (V) * Current (A) - // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W - current as f64 * voltage as f64 / 1e12 - }) - } - }; - - self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS - .iter() - .find(|config| { - self.path.join(config.path_start).exists() - && self.path.join(config.path_end).exists() - }) - .copied(); - } - Ok(()) } @@ -282,10 +216,7 @@ impl PowerSupply { .map(|config| self.path.join(config.path_end)) } - pub fn set_charge_threshold_start( - &mut self, - charge_threshold_start: f64, - ) -> anyhow::Result<()> { + pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { fs::write( &self.charge_threshold_path_start().ok_or_else(|| { anyhow!( @@ -293,18 +224,16 @@ impl PowerSupply { name = self.name, ) })?, - &((charge_threshold_start * 100.0) as u8).to_string(), + &charge_threshold_start.to_string(), ) .with_context(|| format!("failed to set charge threshold start for {self}"))?; - self.charge_threshold_start = charge_threshold_start; - log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); Ok(()) } - pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> { + pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { fs::write( &self.charge_threshold_path_end().ok_or_else(|| { anyhow!( @@ -312,30 +241,26 @@ impl PowerSupply { name = self.name, ) })?, - &((charge_threshold_end * 100.0) as u8).to_string(), + &charge_threshold_end.to_string(), ) .with_context(|| format!("failed to set charge threshold end for {self}"))?; - self.charge_threshold_end = charge_threshold_end; - log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); Ok(()) } - pub fn get_available_platform_profiles() -> anyhow::Result> { + pub fn get_available_platform_profiles() -> Vec { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Some(content) = - fs::read(path).context("failed to read available ACPI platform profiles")? - else { - return Ok(Vec::new()); + let Some(Ok(content)) = fs::read(path) else { + return Vec::new(); }; - Ok(content + content .split_whitespace() .map(ToString::to_string) - .collect()) + .collect() } /// Sets the platform profile. @@ -345,7 +270,7 @@ impl PowerSupply { /// /// [`The Kernel docs`]: pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { - let profiles = Self::get_available_platform_profiles()?; + let profiles = Self::get_available_platform_profiles(); if !profiles .iter() @@ -360,10 +285,4 @@ impl PowerSupply { fs::write("/sys/firmware/acpi/platform_profile", profile) .context("this probably means that your system does not support changing ACPI profiles") } - - pub fn platform_profile() -> anyhow::Result { - fs::read("/sys/firmware/acpi/platform_profile") - .context("failed to read platform profile")? - .context("failed to find platform profile") - } } diff --git a/src/system.rs b/src/system.rs index 17f8b73..f8820b1 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,25 +1,19 @@ use anyhow::{Context, bail}; -use crate::{cpu, fs, power_supply}; +use crate::fs; pub struct System { - pub is_ac: bool, + pub is_desktop: bool, pub load_average_1min: f64, pub load_average_5min: f64, pub load_average_15min: f64, - - pub cpus: Vec, - pub power_supplies: Vec, } impl System { pub fn new() -> anyhow::Result { let mut system = Self { - is_ac: false, - - cpus: Vec::new(), - power_supplies: Vec::new(), + is_desktop: false, load_average_1min: 0.0, load_average_5min: 0.0, @@ -32,37 +26,29 @@ impl System { } pub fn rescan(&mut self) -> anyhow::Result<()> { - self.cpus = cpu::Cpu::all().context("failed to scan CPUs")?; - - self.power_supplies = - power_supply::PowerSupply::all().context("failed to scan power supplies")?; - - self.is_ac = self - .power_supplies - .iter() - .any(|power_supply| power_supply.is_ac()) - || self.is_desktop()?; - + self.rescan_is_desktop()?; self.rescan_load_average()?; Ok(()) } - fn is_desktop(&mut self) -> anyhow::Result { - if let Some(chassis_type) = - fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? - { + fn rescan_is_desktop(&mut 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); + self.is_desktop = true; + return Ok(()); } // Laptop form factors. "9" | "10" | "14" => { - return Ok(false); + self.is_desktop = false; + return Ok(()); } // Unknown, continue with other checks @@ -75,7 +61,8 @@ impl System { || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); if !power_saving_exists { - return Ok(true); // Likely a desktop. + self.is_desktop = true; + return Ok(()); // Likely a desktop. } // Check battery-specific ACPI paths that laptops typically have @@ -87,18 +74,20 @@ impl System { for path in laptop_acpi_paths { if fs::exists(path) { - return Ok(false); // Likely a laptop. + self.is_desktop = false; // Likely a laptop. + return Ok(()); } } // Default to assuming desktop if we can't determine. - Ok(true) + self.is_desktop = true; + Ok(()) } fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("failed to read load average")? - .context("load average file doesn't exist, are you on linux?")?; + .context("load average file doesn't exist, are you on linux?")? + .context("failed to read load average")?; let mut parts = content.split_whitespace();