From 63774803123fe29c89b0c20fa7a33f0b7d216d78 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:38:44 +0300 Subject: [PATCH] power_supply: don't ignore non-batteries --- src/main.rs | 68 +++++++++++++------ src/power_supply.rs | 155 +++++++++++++++++++++++++++++--------------- 2 files changed, 153 insertions(+), 70 deletions(-) diff --git a/src/main.rs b/src/main.rs index b86b414..bfca56f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,22 +33,22 @@ enum Command { /// Start the daemon. Start, - /// Modify attributes. - Set { + /// Modify CPU attributes. + CpuSet { /// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs. #[arg(short = 'c', long = "for")] for_: Option>, /// Set the CPU governor. - #[arg(long)] + #[arg(short = 'g', long)] governor: Option, // TODO: Validate with clap for available governors. /// Set CPU Energy Performance Preference (EPP). Short form: --epp. - #[arg(long, alias = "epp")] + #[arg(short = 'p', long, alias = "epp")] energy_performance_preference: Option, /// Set CPU Energy Performance Bias (EPB). Short form: --epb. - #[arg(long, alias = "epb")] + #[arg(short = 'b', long, alias = "epb")] energy_performance_bias: Option, /// Set minimum CPU frequency in MHz. Short form: --freq-min. @@ -60,20 +60,27 @@ enum Command { frequency_mhz_maximum: Option, /// Set turbo boost behaviour. Has to be for all CPUs. - #[arg(long, conflicts_with = "for_")] + #[arg(short = 't', long, conflicts_with = "for_")] turbo: Option, + }, - /// Set ACPI platform profile. Has to be for all CPUs. - #[arg(long, alias = "profile", conflicts_with = "for_")] - platform_profile: Option, + /// Modify power supply attributes. + PowerSet { + /// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies. + #[arg(short = 'p', long = "for")] + for_: Option>, /// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start. - #[arg(short = 'p', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")] + #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] charge_threshold_start: Option, /// Set the percentage where charging will stop. Short form: --charge-end. - #[arg(short = 'P', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")] + #[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))] charge_threshold_end: Option, + + /// Set ACPI platform profile. Has to be for all power supplies. + #[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")] + platform_profile: Option, }, } @@ -96,7 +103,7 @@ fn real_main() -> anyhow::Result<()> { Ok(()) } - Command::Set { + Command::CpuSet { for_, governor, energy_performance_preference, @@ -104,9 +111,6 @@ fn real_main() -> anyhow::Result<()> { frequency_mhz_minimum, frequency_mhz_maximum, turbo, - platform_profile, - charge_threshold_start, - charge_threshold_end, } => { let cpus = match for_ { Some(cpus) => cpus, @@ -139,11 +143,33 @@ fn real_main() -> anyhow::Result<()> { cpu::set_turbo(turbo)?; } - if let Some(platform_profile) = platform_profile.as_ref() { - cpu::set_platform_profile(platform_profile)?; - } + Ok(()) + } - for power_supply in power_supply::get_power_supplies()? { + Command::PowerSet { + for_, + charge_threshold_start, + charge_threshold_end, + platform_profile, + } => { + let power_supplies = match for_ { + Some(names) => { + let power_supplies = Vec::with_capacity(names.len()); + + for name in names { + power_supplies.push(power_supply::get_power_supply(&name)?); + } + + power_supplies + } + + None => power_supply::get_power_supplies()? + .into_iter() + .filter(|power_supply| power_supply.threshold_config.is_some()) + .collect(), + }; + + for power_supply in power_supplies { if let Some(threshold_start) = charge_threshold_start { power_supply::set_charge_threshold_start(&power_supply, threshold_start)?; } @@ -153,6 +179,10 @@ fn real_main() -> anyhow::Result<()> { } } + if let Some(platform_profile) = platform_profile.as_ref() { + cpu::set_platform_profile(platform_profile)?; + } + Ok(()) } } diff --git a/src/power_supply.rs b/src/power_supply.rs index 7320f18..bd1cd74 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -2,38 +2,39 @@ use anyhow::Context; use std::{ fmt, fs, + os::macos::fs::MetadataExt, path::{Path, PathBuf}, }; /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PowerSupplyConfig { +pub struct PowerSupplyThresholdConfig { pub manufacturer: &'static str, pub path_start: &'static str, pub path_end: &'static str, } -/// Charge threshold configs. -const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[ - PowerSupplyConfig { +/// Power supply threshold configs. +const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ + PowerSupplyThresholdConfig { manufacturer: "Standard", path_start: "charge_control_start_threshold", path_end: "charge_control_end_threshold", }, - PowerSupplyConfig { + PowerSupplyThresholdConfig { manufacturer: "ASUS", path_start: "charge_control_start_percentage", path_end: "charge_control_end_percentage", }, // Combine Huawei and ThinkPad since they use identical paths. - PowerSupplyConfig { + PowerSupplyThresholdConfig { manufacturer: "ThinkPad/Huawei", path_start: "charge_start_threshold", path_end: "charge_stop_threshold", }, // Framework laptop support. - PowerSupplyConfig { + PowerSupplyThresholdConfig { manufacturer: "Framework", path_start: "charge_behaviour_start_threshold", path_end: "charge_behaviour_end_threshold", @@ -44,27 +45,34 @@ const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[ pub struct PowerSupply { pub name: String, pub path: PathBuf, - pub config: PowerSupplyConfig, + pub threshold_config: Option, } impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "power suppply '{name}' from manufacturer '{manufacturer}'", - name = &self.name, - manufacturer = &self.config.manufacturer, - ) + write!(f, "power supply '{name}'", name = &self.name)?; + + if let Some(config) = self.threshold_config.as_ref() { + write!( + f, + " from manufacturer '{manufacturer}'", + manufacturer = config.manufacturer, + )?; + } + + Ok(()) } } impl PowerSupply { - pub fn charge_threshold_path_start(&self) -> PathBuf { - self.path.join(self.config.path_start) + pub fn charge_threshold_path_start(&self) -> Option { + self.threshold_config + .map(|config| self.path.join(config.path_start)) } - pub fn charge_threshold_path_end(&self) -> PathBuf { - self.path.join(self.config.path_end) + pub fn charge_threshold_path_end(&self) -> Option { + self.threshold_config + .map(|config| self.path.join(config.path_end)) } } @@ -80,7 +88,7 @@ fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { }) } -fn is_power_supply(path: &Path) -> anyhow::Result { +fn is_battery(path: &Path) -> anyhow::Result { let type_path = path.join("type"); let type_ = fs::read_to_string(&type_path) @@ -89,13 +97,46 @@ fn is_power_supply(path: &Path) -> anyhow::Result { Ok(type_ == "Battery") } -/// Get all batteries in the system that support threshold control. -pub fn get_power_supplies() -> anyhow::Result> { - const PATH: &str = "/sys/class/power_supply"; +const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply"; +/// Get power supply. +pub fn get_power_supply(name: &str) -> anyhow::Result { + let entry_path = Path::new(POWER_SUPPLY_PATH).join(name); + + let threshold_config = is_battery(&entry_path) + .with_context(|| { + format!( + "failed to determine what type of power supply '{path}' is", + path = entry_path.display(), + ) + })? + .then(|| { + for config in POWER_SUPPLY_THRESHOLD_CONFIGS { + if entry_path.join(config.path_start).exists() + && entry_path.join(config.path_end).exists() + { + return Some(*config); + } + } + + None + }) + .flatten(); + + Ok(PowerSupply { + name: name.to_owned(), + path: entry_path, + threshold_config, + }) +} + +/// Get all power supplies. +pub fn get_power_supplies() -> anyhow::Result> { let mut power_supplies = Vec::new(); - 'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{PATH}'"))? { + for entry in fs::read_dir(POWER_SUPPLY_PATH) + .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + { let entry = match entry { Ok(entry) => entry, @@ -107,38 +148,40 @@ pub fn get_power_supplies() -> anyhow::Result> { let entry_path = entry.path(); - if !is_power_supply(&entry_path).with_context(|| { + let mut power_supply_config = None; + + if is_battery(&entry_path).with_context(|| { format!( - "failed to determine whether if '{path}' is a power supply", + "failed to determine what type of power supply '{path}' is", path = entry_path.display(), ) })? { - continue; - } - - for config in POWER_SUPPLY_CONFIGS { - if entry_path.join(config.path_start).exists() - && entry_path.join(config.path_end).exists() - { - power_supplies.push(PowerSupply { - name: entry_path - .file_name() - .with_context(|| { - format!( - "failed to get file name of '{path}'", - path = entry_path.display(), - ) - })? - .to_string_lossy() - .to_string(), - - path: entry_path, - - config: *config, - }); - continue 'entries; + for config in POWER_SUPPLY_THRESHOLD_CONFIGS { + if entry_path.join(config.path_start).exists() + && entry_path.join(config.path_end).exists() + { + power_supply_config = Some(*config); + break; + } } } + + power_supplies.push(PowerSupply { + name: entry_path + .file_name() + .with_context(|| { + format!( + "failed to get file name of '{path}'", + path = entry_path.display(), + ) + })? + .to_string_lossy() + .to_string(), + + path: entry_path, + + threshold_config: power_supply_config, + }); } Ok(power_supplies) @@ -149,7 +192,12 @@ pub fn set_charge_threshold_start( charge_threshold_start: u8, ) -> anyhow::Result<()> { write( - &power_supply.charge_threshold_path_start(), + &power_supply.charge_threshold_path_start().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = power_supply.name, + ) + })?, &charge_threshold_start.to_string(), ) .with_context(|| format!("failed to set charge threshold start for {power_supply}"))?; @@ -164,7 +212,12 @@ pub fn set_charge_threshold_end( charge_threshold_end: u8, ) -> anyhow::Result<()> { write( - &power_supply.charge_threshold_path_end(), + &power_supply.charge_threshold_path_end().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = power_supply.name, + ) + })?, &charge_threshold_end.to_string(), ) .with_context(|| format!("failed to set charge threshold end for {power_supply}"))?;