1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

power_supply: don't ignore non-batteries

This commit is contained in:
RGBCube 2025-05-18 23:38:44 +03:00
parent baef8af981
commit 6377480312
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
2 changed files with 153 additions and 70 deletions

View file

@ -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<Vec<u32>>,
/// Set the CPU governor.
#[arg(long)]
#[arg(short = 'g', long)]
governor: Option<String>, // 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<String>,
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(long, alias = "epb")]
#[arg(short = 'b', long, alias = "epb")]
energy_performance_bias: Option<String>,
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
@ -60,20 +60,27 @@ enum Command {
frequency_mhz_maximum: Option<u64>,
/// Set turbo boost behaviour. Has to be for all CPUs.
#[arg(long, conflicts_with = "for_")]
#[arg(short = 't', long, conflicts_with = "for_")]
turbo: Option<cpu::Turbo>,
},
/// Set ACPI platform profile. Has to be for all CPUs.
#[arg(long, alias = "profile", conflicts_with = "for_")]
platform_profile: Option<String>,
/// 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<Vec<String>>,
/// 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<u8>,
/// 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<u8>,
/// Set ACPI platform profile. Has to be for all power supplies.
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
platform_profile: Option<String>,
},
}
@ -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(())
}
}

View file

@ -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<PowerSupplyThresholdConfig>,
}
impl fmt::Display for PowerSupply {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "power supply '{name}'", name = &self.name)?;
if let Some(config) = self.threshold_config.as_ref() {
write!(
f,
"power suppply '{name}' from manufacturer '{manufacturer}'",
name = &self.name,
manufacturer = &self.config.manufacturer,
)
" 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<PathBuf> {
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<PathBuf> {
self.threshold_config
.map(|config| self.path.join(config.path_end))
}
}
@ -80,7 +88,7 @@ fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
})
}
fn is_power_supply(path: &Path) -> anyhow::Result<bool> {
fn is_battery(path: &Path) -> anyhow::Result<bool> {
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<bool> {
Ok(type_ == "Battery")
}
/// Get all batteries in the system that support threshold control.
pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
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<PowerSupply> {
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<Vec<PowerSupply>> {
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,19 +148,24 @@ pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
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 {
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()
@ -134,11 +180,8 @@ pub fn get_power_supplies() -> anyhow::Result<Vec<PowerSupply>> {
path: entry_path,
config: *config,
threshold_config: power_supply_config,
});
continue 'entries;
}
}
}
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}"))?;