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::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()?;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
209
src/cpu.rs
209
src/cpu.rs
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
149
src/engine.rs
149
src/engine.rs
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue