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

battery: clean up, rename to power_supply

This commit is contained in:
RGBCube 2025-05-18 23:12:18 +03:00
parent 87085f913b
commit d0932ae78c
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
5 changed files with 190 additions and 295 deletions

View file

@ -1,267 +0,0 @@
use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs};
use std::{
fs, io,
path::{Path, PathBuf},
};
pub type Result<T, E = ControlError> = std::result::Result<T, E>;
/// Represents a pattern of path suffixes used to control battery charge thresholds
/// for different device vendors.
#[derive(Clone)]
pub struct ThresholdPathPattern {
pub description: &'static str,
pub start_path: &'static str,
pub stop_path: &'static str,
}
// Threshold patterns
const THRESHOLD_PATTERNS: &[ThresholdPathPattern] = &[
ThresholdPathPattern {
description: "Standard",
start_path: "charge_control_start_threshold",
stop_path: "charge_control_end_threshold",
},
ThresholdPathPattern {
description: "ASUS",
start_path: "charge_control_start_percentage",
stop_path: "charge_control_end_percentage",
},
// Combine Huawei and ThinkPad since they use identical paths
ThresholdPathPattern {
description: "ThinkPad/Huawei",
start_path: "charge_start_threshold",
stop_path: "charge_stop_threshold",
},
// Framework laptop support
ThresholdPathPattern {
description: "Framework",
start_path: "charge_behaviour_start_threshold",
stop_path: "charge_behaviour_end_threshold",
},
];
/// Represents a battery that supports charge threshold control
pub struct SupportedBattery<'a> {
pub name: String,
pub pattern: &'a ThresholdPathPattern,
pub path: PathBuf,
}
/// Set battery charge thresholds to protect battery health
///
/// This sets the start and stop charging thresholds for batteries that support this feature.
/// Different laptop vendors implement battery thresholds in different ways, so this function
/// attempts to handle multiple implementations (Lenovo, ASUS, etc.).
///
/// The thresholds determine at what percentage the battery starts charging (when below `start_threshold`)
/// and at what percentage it stops (when it reaches `stop_threshold`).
///
/// # Arguments
///
/// * `start_threshold` - The battery percentage at which charging should start (typically 0-99)
/// * `stop_threshold` - The battery percentage at which charging should stop (typically 1-100)
///
/// # Errors
///
/// Returns an error if:
/// - The thresholds are invalid (start >= stop or stop > 100)
/// - No power supply path is found
/// - No batteries with threshold support are found
/// - Failed to set thresholds on any battery
pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> {
// Validate thresholds using `BatteryChargeThresholds`
let thresholds =
BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e {
crate::config::types::ConfigError::Validation(msg) => {
ControlError::InvalidValueError(msg)
}
_ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")),
})?;
let power_supply_path = Path::new("/sys/class/power_supply");
if !power_supply_path.exists() {
return Err(ControlError::NotSupported(
"Power supply path not found, battery threshold control not supported".to_string(),
));
}
// XXX: Skip checking directory writability since /sys is a virtual filesystem
// Individual file writability will be checked by find_battery_with_threshold_support
let supported_batteries = find_supported_batteries(power_supply_path)?;
if supported_batteries.is_empty() {
return Err(ControlError::NotSupported(
"No batteries with charge threshold control support found".to_string(),
));
}
apply_thresholds_to_batteries(&supported_batteries, thresholds.start, thresholds.stop)
}
/// Finds all batteries in the system that support threshold control
fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBattery<'static>>> {
let entries = fs::read_dir(power_supply_path).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied {
ControlError::PermissionDenied(format!(
"Permission denied accessing power supply directory: {}",
power_supply_path.display()
))
} else {
ControlError::Io(e)
}
})?;
let mut supported_batteries = Vec::new();
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
log::warn!("Failed to read power-supply entry: {e}");
continue;
}
};
let ps_path = entry.path();
if is_battery(&ps_path)? {
if let Some(battery) = find_battery_with_threshold_support(&ps_path) {
supported_batteries.push(battery);
}
}
}
if supported_batteries.is_empty() {
log::warn!("No batteries with charge threshold support found");
} else {
log::debug!(
"Found {} batteries with threshold support",
supported_batteries.len()
);
for battery in &supported_batteries {
log::debug!(
"Battery '{}' supports {} threshold control",
battery.name,
battery.pattern.description
);
}
}
Ok(supported_batteries)
}
/// Applies the threshold settings to all supported batteries
fn apply_thresholds_to_batteries(
batteries: &[SupportedBattery<'_>],
start_threshold: u8,
stop_threshold: u8,
) -> Result<()> {
let mut errors = Vec::new();
let mut success_count = 0;
for battery in batteries {
let start_path = battery.path.join(battery.pattern.start_path);
let stop_path = battery.path.join(battery.pattern.stop_path);
// Read current thresholds in case we need to restore them
let current_stop = sysfs::read_sysfs_value(&stop_path).ok();
// Write stop threshold first (must be >= start threshold)
let stop_result = sysfs::write_sysfs_value(&stop_path, &stop_threshold.to_string());
// Only proceed to set start threshold if stop threshold was set successfully
if matches!(stop_result, Ok(())) {
let start_result = sysfs::write_sysfs_value(&start_path, &start_threshold.to_string());
match start_result {
Ok(()) => {
log::debug!(
"Set {}-{}% charge thresholds for {} battery '{}'",
start_threshold,
stop_threshold,
battery.pattern.description,
battery.name
);
success_count += 1;
}
Err(e) => {
// Start threshold failed, try to restore the previous stop threshold
if let Some(prev_stop) = &current_stop {
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
if let Err(re) = restore_result {
log::warn!(
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
battery.name,
re
);
} else {
log::debug!(
"Restored previous stop threshold ({}) for battery '{}'",
prev_stop,
battery.name
);
}
}
errors.push(format!(
"Failed to set start threshold for {} battery '{}': {}",
battery.pattern.description, battery.name, e
));
}
}
} else if let Err(e) = stop_result {
errors.push(format!(
"Failed to set stop threshold for {} battery '{}': {}",
battery.pattern.description, battery.name, e
));
}
}
if success_count > 0 {
if !errors.is_empty() {
log::warn!(
"Partial success setting battery thresholds: {}",
errors.join("; ")
);
}
Ok(())
} else {
Err(ControlError::WriteError(format!(
"Failed to set charge thresholds on any battery: {}",
errors.join("; ")
)))
}
}
/// Determines if a power supply entry is a battery
fn is_battery(path: &Path) -> Result<bool> {
let type_path = path.join("type");
if !type_path.exists() {
return Ok(false);
}
let ps_type = sysfs::read_sysfs_value(&type_path).map_err(|e| {
ControlError::ReadError(format!("Failed to read {}: {}", type_path.display(), e))
})?;
Ok(ps_type == "Battery")
}
/// Identifies if a battery supports threshold control and which pattern it uses
fn find_battery_with_threshold_support(ps_path: &Path) -> Option<SupportedBattery<'static>> {
for pattern in THRESHOLD_PATTERNS {
let start_threshold_path = ps_path.join(pattern.start_path);
let stop_threshold_path = ps_path.join(pattern.stop_path);
// Ensure both paths exist and are writable before considering this battery supported
if sysfs::path_exists_and_writable(&start_threshold_path)
&& sysfs::path_exists_and_writable(&stop_threshold_path)
{
return Some(SupportedBattery {
name: ps_path.file_name()?.to_string_lossy().to_string(),
pattern,
path: ps_path.to_path_buf(),
});
}
}
None
}

View file

@ -15,12 +15,12 @@ macro_rules! default_const {
} }
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct BatteryChargeThresholds { pub struct PowerSupplyChargeThresholds {
pub start: u8, pub start: u8,
pub stop: u8, pub stop: u8,
} }
impl BatteryChargeThresholds { impl PowerSupplyChargeThresholds {
pub fn new(start: u8, stop: u8) -> Result<Self, ConfigError> { pub fn new(start: u8, stop: u8) -> Result<Self, ConfigError> {
if stop == 0 { if stop == 0 {
return Err(ConfigError::Validation( return Err(ConfigError::Validation(
@ -42,7 +42,7 @@ impl BatteryChargeThresholds {
} }
} }
impl TryFrom<(u8, u8)> for BatteryChargeThresholds { impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
type Error = ConfigError; type Error = ConfigError;
fn try_from(values: (u8, u8)) -> Result<Self, Self::Error> { fn try_from(values: (u8, u8)) -> Result<Self, Self::Error> {
@ -66,7 +66,7 @@ pub struct ProfileConfig {
#[serde(default)] #[serde(default)]
pub enable_auto_turbo: bool, pub enable_auto_turbo: bool,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<BatteryChargeThresholds>, pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
} }
impl Default for ProfileConfig { impl Default for ProfileConfig {
@ -124,7 +124,7 @@ pub struct ProfileConfigToml {
#[serde(default = "default_enable_auto_turbo")] #[serde(default = "default_enable_auto_turbo")]
pub enable_auto_turbo: bool, pub enable_auto_turbo: bool,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<BatteryChargeThresholds>, pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
} }
#[derive(Deserialize, Serialize, Debug, Clone, Default)] #[derive(Deserialize, Serialize, Debug, Clone, Default)]
@ -134,7 +134,7 @@ pub struct AppConfigToml {
#[serde(default)] #[serde(default)]
pub battery: ProfileConfigToml, pub battery: ProfileConfigToml,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<BatteryChargeThresholds>, pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
pub ignored_power_supplies: Option<Vec<String>>, pub ignored_power_supplies: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub daemon: DaemonConfigToml, pub daemon: DaemonConfigToml,

View file

@ -1,7 +1,7 @@
use crate::battery;
use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::core::{OperationalMode, SystemReport, TurboSetting};
use crate::cpu::{self}; use crate::cpu::{self};
use crate::power_supply;
use crate::util::error::{ControlError, EngineError}; use crate::util::error::{ControlError, EngineError};
use std::sync::OnceLock; use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -277,7 +277,7 @@ pub fn determine_and_apply_settings(
if start_threshold < stop_threshold && stop_threshold <= 100 { if start_threshold < stop_threshold && stop_threshold <= 100 {
log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) { match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) {
Ok(()) => log::debug!("Battery charge thresholds set successfully"), Ok(()) => log::debug!("Battery charge thresholds set successfully"),
Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"), Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"),
} }

View file

@ -1,4 +1,3 @@
mod battery;
mod cli; mod cli;
mod config; mod config;
mod core; mod core;
@ -6,6 +5,7 @@ mod cpu;
mod daemon; mod daemon;
mod engine; mod engine;
mod monitor; mod monitor;
mod power_supply;
mod util; mod util;
use anyhow::{Context, anyhow, bail}; use anyhow::{Context, anyhow, bail};
@ -148,27 +148,14 @@ fn real_main() -> anyhow::Result<()> {
cpu::set_platform_profile(platform_profile)?; cpu::set_platform_profile(platform_profile)?;
} }
// TODO: This is like this because [`cpu`] doesn't expose for power_supply in power_supply::get_power_supplies()? {
// a way of setting them individually. Will clean this up if let Some(threshold_start) = charge_threshold_start {
// after that is cleaned. power_supply::set_charge_threshold_start(&power_supply, threshold_start)?;
if charge_threshold_start.is_some() || charge_threshold_end.is_some() {
let charge_threshold_start = charge_threshold_start.ok_or_else(|| {
anyhow!("both charge thresholds should be given at the same time")
})?;
let charge_threshold_end = charge_threshold_end.ok_or_else(|| {
anyhow!("both charge thresholds should be given at the same time")
})?;
if charge_threshold_start >= charge_threshold_end {
bail!(
"charge start threshold (given as {charge_threshold_start}) must be less than stop threshold (given as {charge_threshold_end})"
);
} }
battery::set_battery_charge_thresholds( if let Some(threshold_end) = charge_threshold_end {
charge_threshold_start, power_supply::set_charge_threshold_end(&power_supply, threshold_end)?;
charge_threshold_end, }
)?;
} }
Ok(()) Ok(())

175
src/power_supply.rs Normal file
View file

@ -0,0 +1,175 @@
use anyhow::Context;
use std::{
fmt, fs,
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 manufacturer: &'static str,
pub path_start: &'static str,
pub path_end: &'static str,
}
/// Charge threshold configs.
const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[
PowerSupplyConfig {
manufacturer: "Standard",
path_start: "charge_control_start_threshold",
path_end: "charge_control_end_threshold",
},
PowerSupplyConfig {
manufacturer: "ASUS",
path_start: "charge_control_start_percentage",
path_end: "charge_control_end_percentage",
},
// Combine Huawei and ThinkPad since they use identical paths.
PowerSupplyConfig {
manufacturer: "ThinkPad/Huawei",
path_start: "charge_start_threshold",
path_end: "charge_stop_threshold",
},
// Framework laptop support.
PowerSupplyConfig {
manufacturer: "Framework",
path_start: "charge_behaviour_start_threshold",
path_end: "charge_behaviour_end_threshold",
},
];
/// Represents a power supply that supports charge threshold control.
pub struct PowerSupply {
pub name: String,
pub path: PathBuf,
pub config: PowerSupplyConfig,
}
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,
)
}
}
impl PowerSupply {
pub fn charge_threshold_path_start(&self) -> PathBuf {
self.path.join(self.config.path_start)
}
pub fn charge_threshold_path_end(&self) -> PathBuf {
self.path.join(self.config.path_end)
}
}
// TODO: Migrate to central utils file. Same exists in cpu.rs.
fn write(path: impl AsRef<Path>, 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(),
)
})
}
fn is_power_supply(path: &Path) -> anyhow::Result<bool> {
let type_path = 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")
}
/// 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";
let mut power_supplies = Vec::new();
'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{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();
if !is_power_supply(&entry_path).with_context(|| {
format!(
"failed to determine whether if '{path}' is a power supply",
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;
}
}
}
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(),
&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(),
&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(())
}