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:
parent
587d6a070f
commit
36807f66c4
9 changed files with 458 additions and 288 deletions
256
src/battery.rs
Normal file
256
src/battery.rs
Normal 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")
|
||||
}
|
|
@ -5,7 +5,7 @@ use crate::monitor;
|
|||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Prints comprehensive debug information about the system
|
||||
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"));
|
||||
|
||||
// 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");
|
||||
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>> {
|
||||
let output = Command::new("systemctl")
|
||||
.arg("is-active")
|
||||
.arg(format!("{}.service", service_name))
|
||||
.arg(format!("{service_name}.service"))
|
||||
.stdout(Stdio::piped()) // capture stdout instead of letting it print
|
||||
.stderr(Stdio::null()) // redirect stderr to null
|
||||
.output()?;
|
||||
|
|
|
@ -68,9 +68,7 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result<AppConfig, C
|
|||
Ok(AppConfig {
|
||||
charger: ProfileConfig::from(default_toml_config.charger),
|
||||
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,
|
||||
poll_interval_sec: default_toml_config.poll_interval_sec,
|
||||
daemon: DaemonConfig::default(),
|
||||
})
|
||||
}
|
||||
|
@ -99,9 +97,7 @@ fn load_and_parse_config(path: &Path) -> Result<AppConfig, ConfigError> {
|
|||
Ok(AppConfig {
|
||||
charger: ProfileConfig::from(charger_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,
|
||||
poll_interval_sec: toml_app_config.poll_interval_sec,
|
||||
daemon: DaemonConfig {
|
||||
poll_interval_sec: toml_app_config.daemon.poll_interval_sec,
|
||||
adaptive_interval: toml_app_config.daemon.adaptive_interval,
|
||||
|
|
|
@ -38,19 +38,11 @@ pub struct AppConfig {
|
|||
pub charger: ProfileConfig,
|
||||
#[serde(default)]
|
||||
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>>,
|
||||
#[serde(default = "default_poll_interval_sec")]
|
||||
pub poll_interval_sec: u64,
|
||||
#[serde(default)]
|
||||
pub daemon: DaemonConfig,
|
||||
}
|
||||
|
||||
const fn default_poll_interval_sec() -> u64 {
|
||||
5
|
||||
}
|
||||
|
||||
// Error type for config loading
|
||||
#[derive(Debug)]
|
||||
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 {
|
||||
false
|
||||
}
|
||||
|
|
209
src/cpu.rs
209
src/cpu.rs
|
@ -1,12 +1,7 @@
|
|||
use crate::core::{GovernorOverrideMode, TurboSetting};
|
||||
use crate::util::error::ControlError;
|
||||
use core::str;
|
||||
use log::debug;
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
string::ToString,
|
||||
};
|
||||
use std::{fs, io, path::Path, string::ToString};
|
||||
|
||||
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<()> {
|
||||
// EPB is often an integer 0-15. Ensure `epb` string is valid if parsing.
|
||||
// For now, writing it directly as a string.
|
||||
// EPB is often an integer 0-15.
|
||||
let action = |id: u32| {
|
||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias");
|
||||
if Path::new(&path).exists() {
|
||||
|
@ -249,11 +243,13 @@ pub fn set_platform_profile(profile: &str) -> Result<()> {
|
|||
///
|
||||
/// # 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` 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>> {
|
||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
||||
|
@ -347,196 +343,3 @@ pub fn get_governor_override() -> Option<String> {
|
|||
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")
|
||||
}
|
||||
|
|
149
src/engine.rs
149
src/engine.rs
|
@ -1,8 +1,39 @@
|
|||
use crate::battery;
|
||||
use crate::config::{AppConfig, ProfileConfig};
|
||||
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
||||
use crate::cpu::{self};
|
||||
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,
|
||||
/// 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.",
|
||||
override_governor.trim()
|
||||
);
|
||||
|
||||
// Apply the override governor setting - validation is handled by set_governor
|
||||
cpu::set_governor(override_governor.trim(), None)?;
|
||||
}
|
||||
|
||||
|
@ -35,11 +68,15 @@ pub fn determine_and_apply_settings(
|
|||
}
|
||||
} else {
|
||||
// Determine AC/Battery status
|
||||
// If no batteries, assume AC power (desktop).
|
||||
// Otherwise, check the ac_connected status from the (first) battery.
|
||||
// XXX: This relies on the setting ac_connected in BatteryInfo being set correctly.
|
||||
let on_ac_power =
|
||||
report.batteries.is_empty() || report.batteries.first().is_some_and(|b| b.ac_connected);
|
||||
// For desktops (no batteries), we should always use the AC power profile
|
||||
// For laptops, we check if any battery is present and not connected to AC
|
||||
let on_ac_power = if report.batteries.is_empty() {
|
||||
// No batteries means desktop/server, always on AC
|
||||
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 {
|
||||
info!("On AC power, selecting Charger profile.");
|
||||
|
@ -53,7 +90,19 @@ pub fn determine_and_apply_settings(
|
|||
// Apply settings from selected_profile_config
|
||||
if let Some(governor) = &selected_profile_config.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 {
|
||||
|
@ -63,40 +112,56 @@ pub fn determine_and_apply_settings(
|
|||
debug!("Managing turbo in auto mode based on system conditions");
|
||||
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 {
|
||||
info!("Setting EPP to '{epp}'");
|
||||
cpu::set_epp(epp, None)?;
|
||||
try_apply_feature("EPP", epp, || cpu::set_epp(epp, None))?;
|
||||
}
|
||||
|
||||
if let Some(epb) = &selected_profile_config.epb {
|
||||
info!("Setting EPB to '{epb}'");
|
||||
cpu::set_epb(epb, None)?;
|
||||
try_apply_feature("EPB", epb, || cpu::set_epb(epb, None))?;
|
||||
}
|
||||
|
||||
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
||||
info!("Setting min frequency to '{min_freq} MHz'");
|
||||
cpu::set_min_frequency(min_freq, None)?;
|
||||
try_apply_feature("min frequency", &format!("{min_freq} MHz"), || {
|
||||
cpu::set_min_frequency(min_freq, None)
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
||||
info!("Setting max frequency to '{max_freq} MHz'");
|
||||
cpu::set_max_frequency(max_freq, None)?;
|
||||
try_apply_feature("max frequency", &format!("{max_freq} MHz"), || {
|
||||
cpu::set_max_frequency(max_freq, None)
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(profile) = &selected_profile_config.platform_profile {
|
||||
info!("Setting platform profile to '{profile}'");
|
||||
cpu::set_platform_profile(profile)?;
|
||||
try_apply_feature("platform profile", profile, || {
|
||||
cpu::set_platform_profile(profile)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Apply battery charge thresholds if configured
|
||||
apply_battery_charge_thresholds(
|
||||
selected_profile_config.battery_charge_thresholds,
|
||||
config.global_battery_charge_thresholds,
|
||||
)?;
|
||||
// Set battery charge thresholds if configured
|
||||
if let Some((start_threshold, stop_threshold)) =
|
||||
selected_profile_config.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.");
|
||||
|
||||
|
@ -192,41 +257,3 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mod battery;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod conflict;
|
||||
|
@ -370,10 +371,9 @@ fn main() {
|
|||
stop_threshold,
|
||||
}) => {
|
||||
info!(
|
||||
"Setting battery thresholds: start at {}%, stop at {}%",
|
||||
start_threshold, stop_threshold
|
||||
"Setting battery thresholds: start at {start_threshold}%, stop at {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>)
|
||||
}
|
||||
Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose),
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::config::AppConfig;
|
|||
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
|
||||
use crate::cpu::get_logical_core_count;
|
||||
use crate::util::error::SysMonitorError;
|
||||
use log::debug;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
|
@ -360,7 +361,7 @@ fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
|
|||
|
||||
pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
|
||||
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 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") {
|
||||
// fallback for type file missing
|
||||
// Fallback for type file missing
|
||||
if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) {
|
||||
if online == 1 {
|
||||
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)? {
|
||||
let entry = entry?;
|
||||
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 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 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)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
|
||||
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
|
||||
|
|
|
@ -49,7 +49,6 @@ pub enum SysMonitorError {
|
|||
ReadError(String),
|
||||
ParseError(String),
|
||||
ProcStatParseError(String),
|
||||
NotAvailable(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for SysMonitorError {
|
||||
|
@ -67,7 +66,6 @@ impl std::fmt::Display for SysMonitorError {
|
|||
Self::ProcStatParseError(s) => {
|
||||
write!(f, "Failed to parse /proc/stat: {s}")
|
||||
}
|
||||
Self::NotAvailable(s) => write!(f, "Information not available: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue