From 6ef4da91131c2259cd33108426e55aa2797e3588 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 16:17:54 +0300 Subject: [PATCH] power_supply&cpu: use objects --- src/cpu.rs | 645 +++++++++++++++++++------------------------- src/main.rs | 36 ++- src/power_supply.rs | 317 ++++++++++++---------- 3 files changed, 473 insertions(+), 525 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 0f5f304..ff79ccd 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,19 +1,6 @@ use anyhow::{Context, bail}; -use derive_more::Display; -use serde::{Deserialize, Serialize}; -use std::{fs, io, path::Path, string::ToString}; - -// // Valid EPP (Energy Performance Preference) string values. -// const EPP_FALLBACK_VALUES: &[&str] = &[ -// "default", -// "performance", -// "balance-performance", -// "balance_performance", // Alternative form with underscore. -// "balance-power", -// "balance_power", // Alternative form with underscore. -// "power", -// ]; +use std::{fs, path::Path, string::ToString}; fn exists(path: impl AsRef) -> bool { let path = path.as_ref(); @@ -41,394 +28,326 @@ fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { }) } -/// Get real, tunable CPUs. -pub fn get_real_cpus() -> anyhow::Result> { - const PATH: &str = "/sys/devices/system/cpu"; +pub struct Cpu { + pub number: u32, + pub has_cpufreq: bool, +} - let mut cpus = vec![]; - - for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read contents of '{PATH}'"))? - .flatten() - { - let entry_file_name = entry.file_name(); - - let Some(name) = entry_file_name.to_str() else { - continue; +impl Cpu { + pub fn new(number: u32) -> anyhow::Result { + let mut cpu = Self { + number, + has_cpufreq: false, }; + cpu.rescan()?; - let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { - continue; - }; + Ok(cpu) + } - // Has to match "cpu{N}". - let Ok(cpu) = cpu_prefix_removed.parse::() else { - continue; - }; + /// Get all CPUs. + pub fn all() -> anyhow::Result> { + const PATH: &str = "/sys/devices/system/cpu"; - // Has to match "cpu{N}/cpufreq". - if !entry.path().join("cpufreq").exists() { - continue; + let mut cpus = vec![]; + + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read contents of '{PATH}'"))? + .flatten() + { + let entry_file_name = entry.file_name(); + + let Some(name) = entry_file_name.to_str() else { + continue; + }; + + let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { + continue; + }; + + // Has to match "cpu{N}". + let Ok(number) = cpu_prefix_removed.parse::() else { + continue; + }; + + cpus.push(Self::new(number)?); } - cpus.push(cpu); + // Fall back if sysfs iteration above fails to find any cpufreq CPUs. + if cpus.is_empty() { + for number in 0..num_cpus::get() as u32 { + cpus.push(Self::new(number)?); + } + } + + Ok(cpus) } - // Fall back if sysfs iteration above fails to find any cpufreq CPUs. - if cpus.is_empty() { - cpus = (0..num_cpus::get() as u32).collect(); + /// Rescan CPU, tuning local copy of settings. + pub fn rescan(&mut self) -> anyhow::Result<()> { + let has_cpufreq = exists(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq", + number = self.number, + )); + + self.has_cpufreq = has_cpufreq; + + Ok(()) } - Ok(cpus) -} + pub fn get_available_governors(&self) -> Vec { + let Self { number, .. } = self; -/// Set the governor for a CPU. -pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> { - let governors = get_available_governors_for(cpu); + let Ok(content) = fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" + )) else { + return Vec::new(); + }; - if !governors - .iter() - .any(|avail_governor| avail_governor == governor) - { - bail!( - "governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}", - governors = governors.join(", "), - ); + content + .split_whitespace() + .map(ToString::to_string) + .collect() } - write( - format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor"), - governor, - ) - .with_context(|| { - format!( - "this probably means that CPU {cpu} doesn't exist or doesn't support changing governors" - ) - }) -} + pub fn set_governor(&self, governor: &str) -> anyhow::Result<()> { + let Self { number, .. } = self; -/// Get available CPU governors for a CPU. -fn get_available_governors_for(cpu: u32) -> Vec { - let Ok(content) = fs::read_to_string(format!( - "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors" - )) else { - return Vec::new(); - }; + let governors = self.get_available_governors(); - content - .split_whitespace() - .map(ToString::to_string) - .collect() -} + if !governors + .iter() + .any(|avail_governor| avail_governor == governor) + { + bail!( + "governor '{governor}' is not available for CPU {number}. available governors: {governors}", + governors = governors.join(", "), + ); + } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] -pub enum Turbo { - Always, - Never, -} - -pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> { - let value_boost = match setting { - Turbo::Always => "1", // boost = 1 means turbo is enabled. - Turbo::Never => "0", // boost = 0 means turbo is disabled. - }; - - let value_boost_negated = match setting { - Turbo::Always => "0", // no_turbo = 0 means turbo is enabled. - Turbo::Never => "1", // no_turbo = 1 means turbo is disabled. - }; - - // AMD specific paths - let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; - let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; - - // Path priority (from most to least specific) - let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo"; - let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost"; - - // Try each boost control path in order of specificity - if write(intel_boost_path_negated, value_boost_negated).is_ok() { - return Ok(()); - } - if write(amd_boost_path, value_boost).is_ok() { - return Ok(()); - } - if write(msr_boost_path, value_boost).is_ok() { - return Ok(()); - } - if write(generic_boost_path, value_boost).is_ok() { - return Ok(()); - } - - // Also try per-core cpufreq boost for some AMD systems. - if get_real_cpus()?.iter().any(|cpu| { write( - &format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/boost"), - value_boost, + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"), + governor, ) - .is_ok() - }) { - return Ok(()); + .with_context(|| { + format!( + "this probably means that CPU {number} doesn't exist or doesn't support changing governors" + ) + }) } - bail!("no supported CPU boost control mechanism found"); -} + pub fn get_available_epps(&self) -> Vec { + let Self { number, .. } = self; -pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> { - // Validate the EPP value against available options - let epps = get_available_epps(cpu); + let Ok(content) = fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" + )) else { + return Vec::new(); + }; - if !epps.iter().any(|avail_epp| avail_epp == epp) { - bail!( - "epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}", - epps = epps.join(", "), - ); + content + .split_whitespace() + .map(ToString::to_string) + .collect() } - write( - format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"), - epp, - ) - .with_context(|| { - format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP") - }) -} + pub fn set_epp(&self, epp: &str) -> anyhow::Result<()> { + let Self { number, .. } = self; -/// Get available EPP values for a CPU. -fn get_available_epps(cpu: u32) -> Vec { - let Ok(content) = fs::read_to_string(format!( - "/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_available_preferences" - )) else { - return Vec::new(); - }; + let epps = self.get_available_epps(); - content - .split_whitespace() - .map(ToString::to_string) - .collect() -} + if !epps.iter().any(|avail_epp| avail_epp == epp) { + bail!( + "EPP value '{epp}' is not availabile for CPU {number}. available EPP values: {epps}", + epps = epps.join(", "), + ); + } -pub fn set_epb(epb: &str, cpu: u32) -> anyhow::Result<()> { - // Validate EPB value - should be a number 0-15 or a recognized string value. - validate_epb_value(epb)?; + write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"), + epp, + ) + .with_context(|| { + format!( + "this probably means that CPU {number} doesn't exist or doesn't support changing EPP" + ) + }) + } - write( - format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"), - epb, - ) - .with_context(|| { - format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB") - }) -} + pub fn get_available_epbs(&self) -> &'static [&'static str] { + if !self.has_cpufreq { + return &[]; + } -fn validate_epb_value(epb: &str) -> anyhow::Result<()> { - // EPB can be a number from 0-15 or a recognized string. + &[ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "performance", + "balance-performance", + "balance_performance", // Alternative form with underscore. + "balance-power", + "balance_power", // Alternative form with underscore. + "power", + ] + } - const VALID_EPB_STRINGS: &[&str] = &[ - "performance", - "balance-performance", - "balance_performance", // Alternative form with underscore. - "balance-power", - "balance_power", // Alternative form with underscore. - "power", - ]; + pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { + let Self { number, .. } = self; - // Try parsing as a number first. - if let Ok(value) = epb.parse::() { - if value <= 15 { + let epbs = self.get_available_epbs(); + + if !epbs.contains(&epb) { + bail!( + "EPB value '{epb}' is not available for CPU {number}. available EPB values: {valid}", + valid = epbs.join(", "), + ); + } + + write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"), + epb, + ) + .with_context(|| { + format!( + "this probably means that CPU {number} doesn't exist or doesn't support changing EPB" + ) + }) + } + + pub fn set_frequency_minimum(&self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.validate_frequency_minimum(frequency_mhz)?; + + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = u64::from(frequency_mhz) * 1000; + let frequency_khz = frequency_khz.to_string(); + + write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that CPU {number} doesn't exist or doesn't support changing minimum frequency") + }) + } + + fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + let Ok(minimum_frequency_khz) = 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(()); + }; + + if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz { + bail!( + "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {number}", + minimum_frequency_khz / 1000, + ); + } + + Ok(()) + } + + pub fn set_frequency_maximum(&self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.validate_frequency_maximum(frequency_mhz)?; + + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = u64::from(frequency_mhz) * 1000; + let frequency_khz = frequency_khz.to_string(); + + write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that CPU {number} doesn't exist or doesn't support changing maximum frequency") + }) + } + + fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + let Ok(maximum_frequency_khz) = 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(()); + }; + + if new_frequency_mhz * 1000 > maximum_frequency_khz { + bail!( + "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {number}", + maximum_frequency_khz / 1000, + ); + } + + Ok(()) + } + + pub fn set_turbo(on: bool) -> anyhow::Result<()> { + let value_boost = match on { + true => "1", // boost = 1 means turbo is enabled. + false => "0", // boost = 0 means turbo is disabled. + }; + + let value_boost_negated = match on { + true => "0", // no_turbo = 0 means turbo is enabled. + false => "1", // no_turbo = 1 means turbo is disabled. + }; + + // AMD specific paths + let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; + let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; + + // Path priority (from most to least specific) + let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo"; + let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost"; + + // Try each boost control path in order of specificity + if write(intel_boost_path_negated, value_boost_negated).is_ok() { + return Ok(()); + } + if write(amd_boost_path, value_boost).is_ok() { + return Ok(()); + } + if write(msr_boost_path, value_boost).is_ok() { + return Ok(()); + } + if write(generic_boost_path, value_boost).is_ok() { return Ok(()); } - bail!("EPB numeric value must be between 0 and 15, got {value}"); - } + // Also try per-core cpufreq boost for some AMD systems. + if Self::all()?.iter().any(|cpu| { + let Cpu { number, .. } = cpu; - // If not a number, check if it's a recognized string value. - if VALID_EPB_STRINGS.contains(&epb) { - return Ok(()); - } - - bail!( - "invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}", - valid = VALID_EPB_STRINGS.join(", "), - ); -} - -pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { - validate_frequency_minimum(frequency_mhz, cpu)?; - - // We use u64 for the intermediate calculation to prevent overflow - let frequency_khz = u64::from(frequency_mhz) * 1000; - let frequency_khz = frequency_khz.to_string(); - - write( - format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"), - &frequency_khz, - ) - .with_context(|| { - format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing minimum frequency") - }) -} - -pub fn set_frequency_maximum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { - validate_max_frequency(frequency_mhz, cpu)?; - - // We use u64 for the intermediate calculation to prevent overflow - let frequency_khz = u64::from(frequency_mhz) * 1000; - let frequency_khz = frequency_khz.to_string(); - - write( - format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_max_freq"), - &frequency_khz, - ) - .with_context(|| { - format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing maximum frequency") - }) -} - -fn validate_frequency_minimum(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { - let Ok(minimum_frequency_khz) = read_u64(format!( - "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq" - )) else { - // Just let it pass if we can't find anything. - return Ok(()); - }; - - if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz { - bail!( - "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {cpu}", - minimum_frequency_khz / 1000, - ); - } - - Ok(()) -} - -fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { - let Ok(maximum_frequency_khz) = read_u64(format!( - "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq" - )) else { - // Just let it pass if we can't find anything. - return Ok(()); - }; - - if new_frequency_mhz * 1000 > maximum_frequency_khz { - bail!( - "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}", - maximum_frequency_khz / 1000, - ); - } - - Ok(()) -} - -/// Sets the platform profile. -/// This changes the system performance, temperature, fan, and other hardware replated characteristics. -/// -/// Also see [`The Kernel docs`] for this. -/// -/// [`The Kernel docs`]: -pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { - let profiles = get_platform_profiles(); - - if !profiles - .iter() - .any(|avail_profile| avail_profile == profile) - { - bail!( - "profile '{profile}' is not available for system. valid profiles: {profiles}", - profiles = profiles.join(", "), - ); - } - - write("/sys/firmware/acpi/platform_profile", profile) - .context("this probably means that your system does not support changing ACPI profiles") -} - -/// Get the list of available platform profiles. -pub fn get_platform_profiles() -> Vec { - let path = "/sys/firmware/acpi/platform_profile_choices"; - - let Ok(content) = fs::read_to_string(path) else { - return Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() -} - -/// Path for storing the governor override state. -const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override"; - -#[derive(Display, Debug, Clone, Copy, clap::ValueEnum)] -pub enum GovernorOverride { - #[display("performance")] - Performance, - #[display("powersave")] - Powersave, - #[display("reset")] - Reset, -} - -pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> { - let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap(); - if !parent.exists() { - fs::create_dir_all(parent).with_context(|| { - format!( - "failed to create directory '{path}'", - path = parent.display(), + write( + &format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), + value_boost, ) - })?; - } - - match mode { - GovernorOverride::Reset => { - // Remove the override file if it exists - let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH); - - if let Err(error) = result { - if error.kind() != io::ErrorKind::NotFound { - return Err(error).with_context(|| { - format!( - "failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'" - ) - }); - } - } - - log::info!( - "governor override has been deleted. normal profile-based settings will be used" - ); + .is_ok() + }) { + return Ok(()); } - GovernorOverride::Performance | GovernorOverride::Powersave => { - let governor = mode.to_string(); - - write(GOVERNOR_OVERRIDE_PATH, &governor) - .context("failed to write governor override")?; - - // TODO: Apply the setting too. - - log::info!( - "governor override set to '{governor}'. this setting will persist across reboots" - ); - log::info!("to reset, run: superfreq set --governor-persist reset"); - } - } - - Ok(()) -} - -/// Get the current governor override if set. -pub fn get_governor_override() -> anyhow::Result> { - match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) { - Ok(governor_override) => Ok(Some(governor_override)), - - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - - Err(error) => Err(error).with_context(|| { - format!("failed to read governor override at '{GOVERNOR_OVERRIDE_PATH}'") - }), + bail!("no supported CPU boost control mechanism found"); } } diff --git a/src/main.rs b/src/main.rs index bfca56f..f4a7120 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ enum Command { /// Set turbo boost behaviour. Has to be for all CPUs. #[arg(short = 't', long, conflicts_with = "for_")] - turbo: Option, + turbo: Option, }, /// Modify power supply attributes. @@ -113,34 +113,42 @@ fn real_main() -> anyhow::Result<()> { turbo, } => { let cpus = match for_ { - Some(cpus) => cpus, - None => cpu::get_real_cpus()?, + Some(numbers) => { + let mut cpus = Vec::with_capacity(numbers.len()); + + for number in numbers { + cpus.push(cpu::Cpu::new(number)?); + } + + cpus + } + None => cpu::Cpu::all()?, }; for cpu in cpus { if let Some(governor) = governor.as_ref() { - cpu::set_governor(governor, cpu)?; + cpu.set_governor(governor)?; } if let Some(epp) = energy_performance_preference.as_ref() { - cpu::set_epp(epp, cpu)?; + cpu.set_epp(epp)?; } if let Some(epb) = energy_performance_bias.as_ref() { - cpu::set_epb(epb, cpu)?; + cpu.set_epb(epb)?; } if let Some(mhz_minimum) = frequency_mhz_minimum { - cpu::set_frequency_minimum(mhz_minimum, cpu)?; + cpu.set_frequency_minimum(mhz_minimum)?; } if let Some(mhz_maximum) = frequency_mhz_maximum { - cpu::set_frequency_maximum(mhz_maximum, cpu)?; + cpu.set_frequency_maximum(mhz_maximum)?; } } if let Some(turbo) = turbo { - cpu::set_turbo(turbo)?; + cpu::Cpu::set_turbo(turbo)?; } Ok(()) @@ -157,13 +165,13 @@ fn real_main() -> anyhow::Result<()> { let power_supplies = Vec::with_capacity(names.len()); for name in names { - power_supplies.push(power_supply::get_power_supply(&name)?); + power_supplies.push(power_supply::PowerSupply::from_name(name)?); } power_supplies } - None => power_supply::get_power_supplies()? + None => power_supply::PowerSupply::all()? .into_iter() .filter(|power_supply| power_supply.threshold_config.is_some()) .collect(), @@ -171,16 +179,16 @@ fn real_main() -> anyhow::Result<()> { for power_supply in power_supplies { if let Some(threshold_start) = charge_threshold_start { - power_supply::set_charge_threshold_start(&power_supply, threshold_start)?; + power_supply.set_charge_threshold_start(threshold_start)?; } if let Some(threshold_end) = charge_threshold_end { - power_supply::set_charge_threshold_end(&power_supply, threshold_end)?; + power_supply.set_charge_threshold_end(threshold_end)?; } } if let Some(platform_profile) = platform_profile.as_ref() { - cpu::set_platform_profile(platform_profile)?; + power_supply::PowerSupply::set_platform_profile(platform_profile); } Ok(()) diff --git a/src/power_supply.rs b/src/power_supply.rs index bd1cd74..92147da 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,11 +1,22 @@ -use anyhow::Context; +use anyhow::{Context, bail}; use std::{ fmt, fs, - os::macos::fs::MetadataExt, path::{Path, PathBuf}, }; +// TODO: Migrate to central utils file. Same exists in cpu.rs. +fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { + let path = path.as_ref(); + + fs::write(path, value).with_context(|| { + format!( + "failed to write '{value}' to '{path}'", + path = path.display(), + ) + }) +} + /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -50,7 +61,12 @@ pub struct PowerSupply { impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "power supply '{name}'", name = &self.name)?; + write!( + f, + "power supply '{name}' at '{path}'", + name = &self.name, + path = self.path.display(), + )?; if let Some(config) = self.threshold_config.as_ref() { write!( @@ -64,7 +80,93 @@ impl fmt::Display for PowerSupply { } } +const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply"; + impl PowerSupply { + pub fn from_name(name: String) -> anyhow::Result { + let mut power_supply = Self { + path: Path::new(POWER_SUPPLY_PATH).join(&name), + name, + threshold_config: None, + }; + + power_supply.rescan()?; + + Ok(power_supply) + } + + pub fn from_path(path: PathBuf) -> anyhow::Result { + let mut power_supply = PowerSupply { + name: path + .file_name() + .with_context(|| { + format!("failed to get file name of '{path}'", path = path.display(),) + })? + .to_string_lossy() + .to_string(), + + path, + + threshold_config: None, + }; + + power_supply.rescan()?; + + Ok(power_supply) + } + + pub fn all() -> anyhow::Result> { + let mut power_supplies = Vec::new(); + + 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, + + Err(error) => { + log::warn!("failed to read power supply entry: {error}"); + continue; + } + }; + + power_supplies.push(PowerSupply::from_path(entry.path())?); + } + + Ok(power_supplies) + } + + fn is_battery(&self) -> anyhow::Result { + let type_path = self.path.join("type"); + + let type_ = fs::read_to_string(&type_path) + .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; + + Ok(type_ == "Battery") + } + + pub fn rescan(&mut self) -> anyhow::Result<()> { + let threshold_config = self + .is_battery() + .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? + .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); + } + } + + None + }) + .flatten(); + + self.threshold_config = threshold_config; + + Ok(()) + } + pub fn charge_threshold_path_start(&self) -> Option { self.threshold_config .map(|config| self.path.join(config.path_start)) @@ -74,155 +176,74 @@ impl PowerSupply { self.threshold_config .map(|config| self.path.join(config.path_end)) } -} -// TODO: Migrate to central utils file. Same exists in cpu.rs. -fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { - let path = path.as_ref(); - - fs::write(path, value).with_context(|| { - format!( - "failed to write '{value}' to '{path}'", - path = path.display(), + pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { + write( + &self.charge_threshold_path_start().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = self.name, + ) + })?, + &charge_threshold_start.to_string(), ) - }) -} + .with_context(|| format!("failed to set charge threshold start for {self}"))?; -fn is_battery(path: &Path) -> anyhow::Result { - let type_path = path.join("type"); + log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); - let type_ = fs::read_to_string(&type_path) - .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - - Ok(type_ == "Battery") -} - -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(); - - 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, - - Err(error) => { - log::warn!("failed to read power supply entry: {error}"); - continue; - } - }; - - let entry_path = entry.path(); - - let mut power_supply_config = None; - - if is_battery(&entry_path).with_context(|| { - format!( - "failed to determine what type of power supply '{path}' is", - path = entry_path.display(), - ) - })? { - 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(()) } - Ok(power_supplies) -} - -pub fn set_charge_threshold_start( - power_supply: &PowerSupply, - charge_threshold_start: u8, -) -> anyhow::Result<()> { - write( - &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}"))?; - - log::info!("set battery threshold start for {power_supply} to {charge_threshold_start}%"); - - Ok(()) -} - -pub fn set_charge_threshold_end( - power_supply: &PowerSupply, - charge_threshold_end: u8, -) -> anyhow::Result<()> { - write( - &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}"))?; - - log::info!("set battery threshold end for {power_supply} to {charge_threshold_end}%"); - - Ok(()) + pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { + write( + &self.charge_threshold_path_end().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = self.name, + ) + })?, + &charge_threshold_end.to_string(), + ) + .with_context(|| format!("failed to set charge threshold end for {self}"))?; + + log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); + + Ok(()) + } + + pub fn get_available_platform_profiles() -> Vec { + let path = "/sys/firmware/acpi/platform_profile_choices"; + + let Ok(content) = fs::read_to_string(path) else { + return Vec::new(); + }; + + content + .split_whitespace() + .map(ToString::to_string) + .collect() + } + + /// Sets the platform profile. + /// This changes the system performance, temperature, fan, and other hardware replated characteristics. + /// + /// Also see [`The Kernel docs`] for this. + /// + /// [`The Kernel docs`]: + pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { + let profiles = Self::get_available_platform_profiles(); + + if !profiles + .iter() + .any(|avail_profile| avail_profile == profile) + { + bail!( + "profile '{profile}' is not available for system. valid profiles: {profiles}", + profiles = profiles.join(", "), + ); + } + + write("/sys/firmware/acpi/platform_profile", profile) + .context("this probably means that your system does not support changing ACPI profiles") + } }