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

core: move batter logic out of cpu module; better hw detection

This commit is contained in:
NotAShelf 2025-05-15 20:06:24 +03:00
parent 587d6a070f
commit 36807f66c4
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
9 changed files with 458 additions and 288 deletions

256
src/battery.rs Normal file
View file

@ -0,0 +1,256 @@
use crate::util::error::ControlError;
use log::{debug, warn};
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,
}
/// Represents a battery that supports charge threshold control
pub struct SupportedBattery {
pub name: String,
pub pattern: 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(start_threshold, stop_threshold)?;
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(),
));
}
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, start_threshold, stop_threshold)
}
/// Validates that the threshold values are in acceptable ranges
fn validate_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> {
if start_threshold >= stop_threshold {
return Err(ControlError::InvalidValueError(format!(
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
)));
}
if stop_threshold > 100 {
return Err(ControlError::InvalidValueError(format!(
"Stop threshold ({stop_threshold}) cannot exceed 100%"
)));
}
Ok(())
}
/// Finds all batteries in the system that support threshold control
fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBattery>> {
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.flatten() {
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() {
warn!("No batteries with charge threshold support found");
} else {
debug!(
"Found {} batteries with threshold support",
supported_batteries.len()
);
for battery in &supported_batteries {
debug!(
"Battery '{}' supports {} threshold control",
battery.name, battery.pattern.description
);
}
}
Ok(supported_batteries)
}
/// Write a value to a sysfs file
fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<()> {
let p = path.as_ref();
fs::write(p, value).map_err(|e| {
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
if e.kind() == io::ErrorKind::PermissionDenied {
ControlError::PermissionDenied(error_msg)
} else {
ControlError::WriteError(error_msg)
}
})
}
/// Identifies if a battery supports threshold control and which pattern it uses
fn find_battery_with_threshold_support(ps_path: &Path) -> Option<SupportedBattery> {
let threshold_paths = vec![
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",
},
ThresholdPathPattern {
description: "Huawei",
start_path: "charge_start_threshold",
stop_path: "charge_stop_threshold",
},
// ThinkPad-specific, sometimes used in addition to standard paths
ThresholdPathPattern {
description: "ThinkPad",
start_path: "charge_start_threshold",
stop_path: "charge_stop_threshold",
},
// Framework laptop support
// FIXME: This needs actual testing. I inferred this behaviour from some
// Framework-specific code, but it may not be correct.
ThresholdPathPattern {
description: "Framework",
start_path: "charge_behaviour_start_threshold",
stop_path: "charge_behaviour_end_threshold",
},
];
for pattern in &threshold_paths {
let start_threshold_path = ps_path.join(pattern.start_path);
let stop_threshold_path = ps_path.join(pattern.stop_path);
if start_threshold_path.exists() && stop_threshold_path.exists() {
return Some(SupportedBattery {
name: ps_path.file_name()?.to_string_lossy().to_string(),
pattern: pattern.clone(),
path: ps_path.to_path_buf(),
});
}
}
None
}
/// 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);
match (
write_sysfs_value(&start_path, &start_threshold.to_string()),
write_sysfs_value(&stop_path, &stop_threshold.to_string()),
) {
(Ok(()), Ok(())) => {
debug!(
"Set {}-{}% charge thresholds for {} battery '{}'",
start_threshold, stop_threshold, battery.pattern.description, battery.name
);
success_count += 1;
}
(start_result, stop_result) => {
let mut error_msg = format!(
"Failed to set thresholds for {} battery '{}'",
battery.pattern.description, battery.name
);
if let Err(e) = start_result {
error_msg.push_str(&format!(": start threshold error: {e}"));
}
if let Err(e) = stop_result {
error_msg.push_str(&format!(": stop threshold error: {e}"));
}
errors.push(error_msg);
}
}
}
if success_count > 0 {
if !errors.is_empty() {
debug!(
"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 = fs::read_to_string(&type_path)
.map_err(|_| ControlError::ReadError(format!("Failed to read {}", type_path.display())))?
.trim()
.to_string();
Ok(ps_type == "Battery")
}

View file

@ -5,7 +5,7 @@ use crate::monitor;
use std::error::Error; use std::error::Error;
use std::fs; use std::fs;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime}; use std::time::Duration;
/// Prints comprehensive debug information about the system /// Prints comprehensive debug information about the system
pub fn run_debug(config: &AppConfig) -> Result<(), Box<dyn Error>> { pub fn run_debug(config: &AppConfig) -> Result<(), Box<dyn Error>> {
@ -13,7 +13,6 @@ pub fn run_debug(config: &AppConfig) -> Result<(), Box<dyn Error>> {
println!("Version: {}", env!("CARGO_PKG_VERSION")); println!("Version: {}", env!("CARGO_PKG_VERSION"));
// Current date and time // Current date and time
let _now = SystemTime::now(); // Prefix with underscore to indicate intentionally unused
let formatted_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); let formatted_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
println!("Timestamp: {formatted_time}"); println!("Timestamp: {formatted_time}");
@ -246,7 +245,7 @@ fn check_and_print_sysfs_path(path: &str, description: &str) {
fn is_systemd_service_active(service_name: &str) -> Result<bool, Box<dyn Error>> { fn is_systemd_service_active(service_name: &str) -> Result<bool, Box<dyn Error>> {
let output = Command::new("systemctl") let output = Command::new("systemctl")
.arg("is-active") .arg("is-active")
.arg(format!("{}.service", service_name)) .arg(format!("{service_name}.service"))
.stdout(Stdio::piped()) // capture stdout instead of letting it print .stdout(Stdio::piped()) // capture stdout instead of letting it print
.stderr(Stdio::null()) // redirect stderr to null .stderr(Stdio::null()) // redirect stderr to null
.output()?; .output()?;

View file

@ -68,9 +68,7 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result<AppConfig, C
Ok(AppConfig { Ok(AppConfig {
charger: ProfileConfig::from(default_toml_config.charger), charger: ProfileConfig::from(default_toml_config.charger),
battery: ProfileConfig::from(default_toml_config.battery), battery: ProfileConfig::from(default_toml_config.battery),
global_battery_charge_thresholds: default_toml_config.battery_charge_thresholds,
ignored_power_supplies: default_toml_config.ignored_power_supplies, ignored_power_supplies: default_toml_config.ignored_power_supplies,
poll_interval_sec: default_toml_config.poll_interval_sec,
daemon: DaemonConfig::default(), daemon: DaemonConfig::default(),
}) })
} }
@ -99,9 +97,7 @@ fn load_and_parse_config(path: &Path) -> Result<AppConfig, ConfigError> {
Ok(AppConfig { Ok(AppConfig {
charger: ProfileConfig::from(charger_profile), charger: ProfileConfig::from(charger_profile),
battery: ProfileConfig::from(battery_profile), battery: ProfileConfig::from(battery_profile),
global_battery_charge_thresholds: toml_app_config.battery_charge_thresholds,
ignored_power_supplies: toml_app_config.ignored_power_supplies, ignored_power_supplies: toml_app_config.ignored_power_supplies,
poll_interval_sec: toml_app_config.poll_interval_sec,
daemon: DaemonConfig { daemon: DaemonConfig {
poll_interval_sec: toml_app_config.daemon.poll_interval_sec, poll_interval_sec: toml_app_config.daemon.poll_interval_sec,
adaptive_interval: toml_app_config.daemon.adaptive_interval, adaptive_interval: toml_app_config.daemon.adaptive_interval,

View file

@ -38,19 +38,11 @@ pub struct AppConfig {
pub charger: ProfileConfig, pub charger: ProfileConfig,
#[serde(default)] #[serde(default)]
pub battery: ProfileConfig, pub battery: ProfileConfig,
#[serde(rename = "battery_charge_thresholds")]
pub global_battery_charge_thresholds: Option<(u8, u8)>, // (start_threshold, stop_threshold)
pub ignored_power_supplies: Option<Vec<String>>, pub ignored_power_supplies: Option<Vec<String>>,
#[serde(default = "default_poll_interval_sec")]
pub poll_interval_sec: u64,
#[serde(default)] #[serde(default)]
pub daemon: DaemonConfig, pub daemon: DaemonConfig,
} }
const fn default_poll_interval_sec() -> u64 {
5
}
// Error type for config loading // Error type for config loading
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
@ -225,6 +217,10 @@ impl Default for DaemonConfig {
} }
} }
const fn default_poll_interval_sec() -> u64 {
5
}
const fn default_adaptive_interval() -> bool { const fn default_adaptive_interval() -> bool {
false false
} }

View file

@ -1,12 +1,7 @@
use crate::core::{GovernorOverrideMode, TurboSetting}; use crate::core::{GovernorOverrideMode, TurboSetting};
use crate::util::error::ControlError; use crate::util::error::ControlError;
use core::str; use core::str;
use log::debug; use std::{fs, io, path::Path, string::ToString};
use std::{
fs, io,
path::{Path, PathBuf},
string::ToString,
};
pub type Result<T, E = ControlError> = std::result::Result<T, E>; pub type Result<T, E = ControlError> = std::result::Result<T, E>;
@ -170,8 +165,7 @@ pub fn set_epp(epp: &str, core_id: Option<u32>) -> Result<()> {
} }
pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> { pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> {
// EPB is often an integer 0-15. Ensure `epb` string is valid if parsing. // EPB is often an integer 0-15.
// For now, writing it directly as a string.
let action = |id: u32| { let action = |id: u32| {
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias"); let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias");
if Path::new(&path).exists() { if Path::new(&path).exists() {
@ -249,11 +243,13 @@ pub fn set_platform_profile(profile: &str) -> Result<()> {
/// ///
/// # Errors /// # Errors
/// ///
/// Returns [`ControlError::NotSupported`] if: /// # Returns
///
/// - [`ControlError::NotSupported`] if:
/// - The file `/sys/firmware/acpi/platform_profile_choices` does not exist. /// - The file `/sys/firmware/acpi/platform_profile_choices` does not exist.
/// - The file `/sys/firmware/acpi/platform_profile_choices` is empty. /// - The file `/sys/firmware/acpi/platform_profile_choices` is empty.
/// ///
/// Returns [`ControlError::PermissionDenied`] if the file `/sys/firmware/acpi/platform_profile_choices` cannot be read. /// - [`ControlError::PermissionDenied`] if the file `/sys/firmware/acpi/platform_profile_choices` cannot be read.
/// ///
pub fn get_platform_profiles() -> Result<Vec<String>> { pub fn get_platform_profiles() -> Result<Vec<String>> {
let path = "/sys/firmware/acpi/platform_profile_choices"; let path = "/sys/firmware/acpi/platform_profile_choices";
@ -347,196 +343,3 @@ pub fn get_governor_override() -> Option<String> {
None None
} }
} }
/// 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)
///
pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> {
// Validate threshold values
if start_threshold >= stop_threshold {
return Err(ControlError::InvalidValueError(format!(
"Start threshold ({}) must be less than stop threshold ({})",
start_threshold, stop_threshold
)));
}
if stop_threshold > 100 {
return Err(ControlError::InvalidValueError(format!(
"Stop threshold ({}) cannot exceed 100%",
stop_threshold
)));
}
// Known sysfs paths for battery threshold control by vendor
let threshold_paths = vec![
// Standard sysfs paths (used by Lenovo and some others)
ThresholdPathPattern {
description: "Standard",
start_path: "charge_control_start_threshold",
stop_path: "charge_control_end_threshold",
},
// ASUS-specific paths
ThresholdPathPattern {
description: "ASUS",
start_path: "charge_control_start_percentage",
stop_path: "charge_control_end_percentage",
},
// Huawei-specific paths
ThresholdPathPattern {
description: "Huawei",
start_path: "charge_start_threshold",
stop_path: "charge_stop_threshold",
},
];
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(),
));
}
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();
// Scan all power supplies for battery threshold support
for entry in entries.flatten() {
let ps_path = entry.path();
let name = entry.file_name().into_string().unwrap_or_default();
// Skip non-battery devices
if !is_battery(&ps_path)? {
continue;
}
// Try each threshold path pattern for this battery
for pattern in &threshold_paths {
let start_threshold_path = ps_path.join(pattern.start_path);
let stop_threshold_path = ps_path.join(pattern.stop_path);
if start_threshold_path.exists() && stop_threshold_path.exists() {
// Found a battery with threshold support
supported_batteries.push(SupportedBattery {
name: name.clone(),
pattern: pattern.clone(),
path: ps_path.clone(),
});
// Found a supported pattern, no need to check others for this battery
break;
}
}
}
if supported_batteries.is_empty() {
return Err(ControlError::NotSupported(
"No batteries with charge threshold control support found".to_string(),
));
}
// Apply thresholds to all supported batteries
let mut errors = Vec::new();
let mut success_count = 0;
for battery in supported_batteries {
let start_path = battery.path.join(battery.pattern.start_path);
let stop_path = battery.path.join(battery.pattern.stop_path);
// Attempt to set both thresholds
match (
write_sysfs_value(&start_path, &start_threshold.to_string()),
write_sysfs_value(&stop_path, &stop_threshold.to_string()),
) {
(Ok(_), Ok(_)) => {
debug!(
"Set {}-{}% charge thresholds for {} battery '{}'",
start_threshold, stop_threshold, battery.pattern.description, battery.name
);
success_count += 1;
}
(start_result, stop_result) => {
let mut error_msg = format!(
"Failed to set thresholds for {} battery '{}'",
battery.pattern.description, battery.name
);
if let Err(e) = start_result {
error_msg.push_str(&format!(": start threshold error: {}", e));
}
if let Err(e) = stop_result {
error_msg.push_str(&format!(": stop threshold error: {}", e));
}
errors.push(error_msg);
}
}
}
if success_count > 0 {
// As long as we successfully set thresholds on at least one battery, consider it a success
if !errors.is_empty() {
debug!(
"Partial success setting battery thresholds: {}",
errors.join("; ")
);
}
Ok(())
} else {
Err(ControlError::WriteError(format!(
"Failed to set charge thresholds on any battery: {}",
errors.join("; ")
)))
}
}
/// Helper struct for battery charge threshold path patterns
#[derive(Clone)]
struct ThresholdPathPattern {
description: &'static str,
start_path: &'static str,
stop_path: &'static str,
}
/// Helper struct for batteries with threshold support
struct SupportedBattery {
name: String,
pattern: ThresholdPathPattern,
path: PathBuf,
}
/// Check 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 = fs::read_to_string(&type_path)
.map_err(|_| ControlError::ReadError(format!("Failed to read {}", type_path.display())))?
.trim()
.to_string();
Ok(ps_type == "Battery")
}

View file

@ -1,8 +1,39 @@
use crate::battery;
use crate::config::{AppConfig, ProfileConfig}; use crate::config::{AppConfig, ProfileConfig};
use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::core::{OperationalMode, SystemReport, TurboSetting};
use crate::cpu::{self}; use crate::cpu::{self};
use crate::util::error::{ControlError, EngineError}; use crate::util::error::{ControlError, EngineError};
use log::{debug, info}; use log::{debug, info, warn};
/// Try applying a CPU feature and handle common error cases. Centralizes the where we
/// previously did:
/// 1. Try to apply a feature setting
/// 2. If not supported, log a warning and continue
/// 3. If other error, propagate the error
fn try_apply_feature<F, T>(
feature_name: &str,
value_description: &str,
apply_fn: F,
) -> Result<(), EngineError>
where
F: FnOnce() -> Result<T, ControlError>,
{
info!("Setting {feature_name} to '{value_description}'");
match apply_fn() {
Ok(_) => Ok(()),
Err(e) => {
if matches!(e, ControlError::NotSupported(_)) {
warn!(
"{feature_name} setting is not supported on this system. Skipping {feature_name} configuration."
);
Ok(())
} else {
Err(EngineError::ControlError(e))
}
}
}
}
/// Determines the appropriate CPU profile based on power status or forced mode, /// Determines the appropriate CPU profile based on power status or forced mode,
/// and applies the settings using functions from the `cpu` module. /// and applies the settings using functions from the `cpu` module.
@ -17,6 +48,8 @@ pub fn determine_and_apply_settings(
"Governor override is active: '{}'. Setting governor.", "Governor override is active: '{}'. Setting governor.",
override_governor.trim() override_governor.trim()
); );
// Apply the override governor setting - validation is handled by set_governor
cpu::set_governor(override_governor.trim(), None)?; cpu::set_governor(override_governor.trim(), None)?;
} }
@ -35,11 +68,15 @@ pub fn determine_and_apply_settings(
} }
} else { } else {
// Determine AC/Battery status // Determine AC/Battery status
// If no batteries, assume AC power (desktop). // For desktops (no batteries), we should always use the AC power profile
// Otherwise, check the ac_connected status from the (first) battery. // For laptops, we check if any battery is present and not connected to AC
// XXX: This relies on the setting ac_connected in BatteryInfo being set correctly. let on_ac_power = if report.batteries.is_empty() {
let on_ac_power = // No batteries means desktop/server, always on AC
report.batteries.is_empty() || report.batteries.first().is_some_and(|b| b.ac_connected); true
} else {
// At least one battery exists, check if it's on AC
report.batteries.first().is_some_and(|b| b.ac_connected)
};
if on_ac_power { if on_ac_power {
info!("On AC power, selecting Charger profile."); info!("On AC power, selecting Charger profile.");
@ -53,7 +90,19 @@ pub fn determine_and_apply_settings(
// Apply settings from selected_profile_config // Apply settings from selected_profile_config
if let Some(governor) = &selected_profile_config.governor { if let Some(governor) = &selected_profile_config.governor {
info!("Setting governor to '{governor}'"); info!("Setting governor to '{governor}'");
cpu::set_governor(governor, None)?; // Let set_governor handle the validation
if let Err(e) = cpu::set_governor(governor, None) {
// If the governor is not available, log a warning
if matches!(e, ControlError::InvalidValueError(_))
|| matches!(e, ControlError::NotSupported(_))
{
warn!(
"Configured governor '{governor}' is not available on this system. Skipping."
);
} else {
return Err(e.into());
}
}
} }
if let Some(turbo_setting) = selected_profile_config.turbo { if let Some(turbo_setting) = selected_profile_config.turbo {
@ -63,40 +112,56 @@ pub fn determine_and_apply_settings(
debug!("Managing turbo in auto mode based on system conditions"); debug!("Managing turbo in auto mode based on system conditions");
manage_auto_turbo(report, selected_profile_config)?; manage_auto_turbo(report, selected_profile_config)?;
} }
_ => cpu::set_turbo(turbo_setting)?, _ => {
try_apply_feature("Turbo boost", &format!("{turbo_setting:?}"), || {
cpu::set_turbo(turbo_setting)
})?;
}
} }
} }
if let Some(epp) = &selected_profile_config.epp { if let Some(epp) = &selected_profile_config.epp {
info!("Setting EPP to '{epp}'"); try_apply_feature("EPP", epp, || cpu::set_epp(epp, None))?;
cpu::set_epp(epp, None)?;
} }
if let Some(epb) = &selected_profile_config.epb { if let Some(epb) = &selected_profile_config.epb {
info!("Setting EPB to '{epb}'"); try_apply_feature("EPB", epb, || cpu::set_epb(epb, None))?;
cpu::set_epb(epb, None)?;
} }
if let Some(min_freq) = selected_profile_config.min_freq_mhz { if let Some(min_freq) = selected_profile_config.min_freq_mhz {
info!("Setting min frequency to '{min_freq} MHz'"); try_apply_feature("min frequency", &format!("{min_freq} MHz"), || {
cpu::set_min_frequency(min_freq, None)?; cpu::set_min_frequency(min_freq, None)
})?;
} }
if let Some(max_freq) = selected_profile_config.max_freq_mhz { if let Some(max_freq) = selected_profile_config.max_freq_mhz {
info!("Setting max frequency to '{max_freq} MHz'"); try_apply_feature("max frequency", &format!("{max_freq} MHz"), || {
cpu::set_max_frequency(max_freq, None)?; cpu::set_max_frequency(max_freq, None)
})?;
} }
if let Some(profile) = &selected_profile_config.platform_profile { if let Some(profile) = &selected_profile_config.platform_profile {
info!("Setting platform profile to '{profile}'"); try_apply_feature("platform profile", profile, || {
cpu::set_platform_profile(profile)?; cpu::set_platform_profile(profile)
})?;
} }
// Apply battery charge thresholds if configured // Set battery charge thresholds if configured
apply_battery_charge_thresholds( if let Some((start_threshold, stop_threshold)) =
selected_profile_config.battery_charge_thresholds, selected_profile_config.battery_charge_thresholds
config.global_battery_charge_thresholds, {
)?; if start_threshold < stop_threshold && stop_threshold <= 100 {
info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) {
Ok(()) => debug!("Battery charge thresholds set successfully"),
Err(e) => warn!("Failed to set battery charge thresholds: {e}"),
}
} else {
warn!(
"Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}"
);
}
}
debug!("Profile settings applied successfully."); debug!("Profile settings applied successfully.");
@ -192,41 +257,3 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
Err(e) => Err(EngineError::ControlError(e)), Err(e) => Err(EngineError::ControlError(e)),
} }
} }
/// Apply battery charge thresholds from configuration
fn apply_battery_charge_thresholds(
profile_thresholds: Option<(u8, u8)>,
global_thresholds: Option<(u8, u8)>,
) -> Result<(), EngineError> {
// Try profile-specific thresholds first, fall back to global thresholds
let thresholds = profile_thresholds.or(global_thresholds);
if let Some((start_threshold, stop_threshold)) = thresholds {
info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
match cpu::set_battery_charge_thresholds(start_threshold, stop_threshold) {
Ok(()) => {
debug!("Successfully set battery charge thresholds");
Ok(())
}
Err(e) => {
// If the battery doesn't support thresholds, log but don't fail
if matches!(e, ControlError::NotSupported(_)) {
debug!("Battery charge thresholds not supported: {e}");
Ok(())
} else {
// For permission errors, provide more helpful message
if matches!(e, ControlError::PermissionDenied(_)) {
debug!(
"Permission denied setting battery thresholds - requires root privileges"
);
}
Err(EngineError::ControlError(e))
}
}
}
} else {
// No thresholds configured, this is not an error
debug!("No battery charge thresholds configured");
Ok(())
}
}

View file

@ -1,3 +1,4 @@
mod battery;
mod cli; mod cli;
mod config; mod config;
mod conflict; mod conflict;
@ -370,10 +371,9 @@ fn main() {
stop_threshold, stop_threshold,
}) => { }) => {
info!( info!(
"Setting battery thresholds: start at {}%, stop at {}%", "Setting battery thresholds: start at {start_threshold}%, stop at {stop_threshold}%"
start_threshold, stop_threshold
); );
cpu::set_battery_charge_thresholds(start_threshold, stop_threshold) battery::set_battery_charge_thresholds(start_threshold, stop_threshold)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>) .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
} }
Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose),

View file

@ -2,6 +2,7 @@ use crate::config::AppConfig;
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport}; use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
use crate::cpu::get_logical_core_count; use crate::cpu::get_logical_core_count;
use crate::util::error::SysMonitorError; use crate::util::error::SysMonitorError;
use log::debug;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs, fs,
@ -360,7 +361,7 @@ fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> { pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
let initial_cpu_times = read_all_cpu_times()?; let initial_cpu_times = read_all_cpu_times()?;
thread::sleep(Duration::from_millis(250)); // Interval for CPU usage calculation thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
let final_cpu_times = read_all_cpu_times()?; let final_cpu_times = read_all_cpu_times()?;
let num_cores = get_logical_core_count() let num_cores = get_logical_core_count()
@ -503,7 +504,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
} }
} }
} else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") { } else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") {
// fallback for type file missing // Fallback for type file missing
if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) { if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) {
if online == 1 { if online == 1 {
overall_ac_connected = true; overall_ac_connected = true;
@ -513,6 +514,12 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
} }
} }
// 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)? { for entry in fs::read_dir(power_supply_path)? {
let entry = entry?; let entry = entry?;
let ps_path = entry.path(); let ps_path = entry.path();
@ -524,6 +531,12 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) {
if ps_type == "Battery" { if ps_type == "Battery" {
// Skip peripheral batteries that aren't real laptop batteries
if is_peripheral_battery(&ps_path, &name) {
debug!("Skipping peripheral battery: {name}");
continue;
}
let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok(); let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok();
let capacity_percent = read_sysfs_value::<u8>(ps_path.join("capacity")).ok(); let capacity_percent = read_sysfs_value::<u8>(ps_path.join("capacity")).ok();
@ -564,9 +577,91 @@ pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
} }
} }
} }
// If we found no batteries but have power supplies, we're likely on a desktop
if batteries.is_empty() && overall_ac_connected {
debug!("No laptop batteries found, likely a desktop system");
}
Ok(batteries) Ok(batteries)
} }
/// Check if a battery is likely a peripheral (mouse, keyboard, etc) not a laptop battery
fn is_peripheral_battery(ps_path: &Path, name: &str) -> bool {
// Common peripheral battery names
if name.contains("mouse")
|| name.contains("keyboard")
|| name.contains("trackpad")
|| name.contains("gamepad")
|| name.contains("controller")
|| name.contains("headset")
|| name.contains("headphone")
{
return true;
}
// Small capacity batteries are likely not laptop batteries
if let Ok(energy_full) = read_sysfs_value::<i32>(ps_path.join("energy_full")) {
// Most laptop batteries are at least 20,000,000 µWh (20 Wh)
// Peripheral batteries are typically much smaller
if energy_full < 10_000_000 {
// 10 Wh in µWh
return true;
}
}
// Check for model name that indicates a peripheral
if let Ok(model) = read_sysfs_file_trimmed(ps_path.join("model_name")) {
if model.contains("bluetooth") || model.contains("wireless") {
return true;
}
}
false
}
/// Determine if this is likely a desktop system rather than a laptop
fn is_likely_desktop_system() -> bool {
// Check for DMI system type information
if let Ok(chassis_type) = fs::read_to_string("/sys/class/dmi/id/chassis_type") {
let chassis_type = chassis_type.trim();
// Chassis types:
// 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 {
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return true, // desktop form factors
"9" | "10" | "14" => return false, // laptop form factors
_ => {} // Unknown, continue with other checks
}
}
// Check CPU power policies, desktops often don't have these
let power_saving_exists = Path::new("/sys/module/intel_pstate/parameters/no_hwp").exists()
|| Path::new("/sys/devices/system/cpu/cpufreq/conservative").exists();
if !power_saving_exists {
return true; // likely a desktop
}
// Check battery-specific ACPI paths that laptops typically have
let laptop_acpi_paths = [
"/sys/class/power_supply/BAT0",
"/sys/class/power_supply/BAT1",
"/proc/acpi/battery",
];
for path in &laptop_acpi_paths {
if Path::new(path).exists() {
return false; // Likely a laptop
}
}
// Default to assuming desktop if we can't determine
true
}
pub fn get_system_load() -> Result<SystemLoad> { pub fn get_system_load() -> Result<SystemLoad> {
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?; let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); let parts: Vec<&str> = loadavg_str.split_whitespace().collect();

View file

@ -49,7 +49,6 @@ pub enum SysMonitorError {
ReadError(String), ReadError(String),
ParseError(String), ParseError(String),
ProcStatParseError(String), ProcStatParseError(String),
NotAvailable(String),
} }
impl From<io::Error> for SysMonitorError { impl From<io::Error> for SysMonitorError {
@ -67,7 +66,6 @@ impl std::fmt::Display for SysMonitorError {
Self::ProcStatParseError(s) => { Self::ProcStatParseError(s) => {
write!(f, "Failed to parse /proc/stat: {s}") write!(f, "Failed to parse /proc/stat: {s}")
} }
Self::NotAvailable(s) => write!(f, "Information not available: {s}"),
} }
} }
} }