mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-28 01:17:45 +00:00
Merge pull request #20 from NotAShelf/better-battery-mgnmnt
core: implement better battery management
This commit is contained in:
commit
1eeb6d2d90
15 changed files with 992 additions and 161 deletions
29
README.md
29
README.md
|
@ -111,6 +111,26 @@ sudo superfreq set-min-freq 1200 --core-id 0
|
|||
sudo superfreq set-max-freq 2800 --core-id 1
|
||||
```
|
||||
|
||||
### Battery Management
|
||||
|
||||
```bash
|
||||
# Set battery charging thresholds to extend battery lifespan
|
||||
sudo superfreq set-battery-thresholds 40 80 # Start charging at 40%, stop at 80%
|
||||
```
|
||||
|
||||
Battery charging thresholds help extend battery longevity by preventing constant
|
||||
charging to 100%. Different laptop vendors implement this feature differently,
|
||||
but Superfreq attempts to support multiple vendor implementations including:
|
||||
|
||||
- Lenovo ThinkPad/IdeaPad (Standard implementation)
|
||||
- ASUS laptops
|
||||
- Huawei laptops
|
||||
- Other devices using the standard Linux power_supply API
|
||||
|
||||
Note that battery management is sensitive, and that your mileage may vary.
|
||||
Please open an issue if your vendor is not supported, but patches would help
|
||||
more than issue reports, as supporting hardware _needs_ hardware.
|
||||
|
||||
## Configuration
|
||||
|
||||
Superfreq uses TOML configuration files. Default locations:
|
||||
|
@ -139,6 +159,8 @@ platform_profile = "performance"
|
|||
# Min/max frequency in MHz (optional)
|
||||
min_freq_mhz = 800
|
||||
max_freq_mhz = 3500
|
||||
# Optional: Profile-specific battery charge thresholds (overrides global setting)
|
||||
# battery_charge_thresholds = [40, 80] # Start at 40%, stop at 80%
|
||||
|
||||
# Settings for when on battery power
|
||||
[battery]
|
||||
|
@ -149,6 +171,13 @@ epb = "balance_power"
|
|||
platform_profile = "low-power"
|
||||
min_freq_mhz = 800
|
||||
max_freq_mhz = 2500
|
||||
# Optional: Profile-specific battery charge thresholds (overrides global setting)
|
||||
# battery_charge_thresholds = [60, 80] # Start at 60%, stop at 80% (more conservative)
|
||||
|
||||
# Global battery charging thresholds (applied to both profiles unless overridden)
|
||||
# Start charging at 40%, stop at 80% - extends battery lifespan
|
||||
# NOTE: Profile-specific thresholds (in [charger] or [battery] sections) take precedence over this global setting
|
||||
battery_charge_thresholds = [40, 80]
|
||||
|
||||
# Daemon configuration
|
||||
[daemon]
|
||||
|
|
262
src/battery.rs
Normal file
262
src/battery.rs
Normal file
|
@ -0,0 +1,262 @@
|
|||
use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs};
|
||||
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,
|
||||
}
|
||||
|
||||
// Threshold patterns
|
||||
const THRESHOLD_PATTERNS: &[ThresholdPathPattern] = &[
|
||||
ThresholdPathPattern {
|
||||
description: "Standard",
|
||||
start_path: "charge_control_start_threshold",
|
||||
stop_path: "charge_control_end_threshold",
|
||||
},
|
||||
ThresholdPathPattern {
|
||||
description: "ASUS",
|
||||
start_path: "charge_control_start_percentage",
|
||||
stop_path: "charge_control_end_percentage",
|
||||
},
|
||||
// Combine Huawei and ThinkPad since they use identical paths
|
||||
ThresholdPathPattern {
|
||||
description: "ThinkPad/Huawei",
|
||||
start_path: "charge_start_threshold",
|
||||
stop_path: "charge_stop_threshold",
|
||||
},
|
||||
// Framework laptop support
|
||||
ThresholdPathPattern {
|
||||
description: "Framework",
|
||||
start_path: "charge_behaviour_start_threshold",
|
||||
stop_path: "charge_behaviour_end_threshold",
|
||||
},
|
||||
];
|
||||
|
||||
/// Represents a battery that supports charge threshold control
|
||||
pub struct SupportedBattery<'a> {
|
||||
pub name: String,
|
||||
pub pattern: &'a ThresholdPathPattern,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Set battery charge thresholds to protect battery health
|
||||
///
|
||||
/// This sets the start and stop charging thresholds for batteries that support this feature.
|
||||
/// Different laptop vendors implement battery thresholds in different ways, so this function
|
||||
/// attempts to handle multiple implementations (Lenovo, ASUS, etc.).
|
||||
///
|
||||
/// The thresholds determine at what percentage the battery starts charging (when below `start_threshold`)
|
||||
/// and at what percentage it stops (when it reaches `stop_threshold`).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `start_threshold` - The battery percentage at which charging should start (typically 0-99)
|
||||
/// * `stop_threshold` - The battery percentage at which charging should stop (typically 1-100)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The thresholds are invalid (start >= stop or stop > 100)
|
||||
/// - No power supply path is found
|
||||
/// - No batteries with threshold support are found
|
||||
/// - Failed to set thresholds on any battery
|
||||
pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> {
|
||||
// Validate thresholds using `BatteryChargeThresholds`
|
||||
let thresholds =
|
||||
BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e {
|
||||
crate::config::types::ConfigError::ValidationError(msg) => {
|
||||
ControlError::InvalidValueError(msg)
|
||||
}
|
||||
_ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")),
|
||||
})?;
|
||||
|
||||
let power_supply_path = Path::new("/sys/class/power_supply");
|
||||
if !power_supply_path.exists() {
|
||||
return Err(ControlError::NotSupported(
|
||||
"Power supply path not found, battery threshold control not supported".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// XXX: Skip checking directory writability since /sys is a virtual filesystem
|
||||
// Individual file writability will be checked by find_battery_with_threshold_support
|
||||
|
||||
let supported_batteries = find_supported_batteries(power_supply_path)?;
|
||||
if supported_batteries.is_empty() {
|
||||
return Err(ControlError::NotSupported(
|
||||
"No batteries with charge threshold control support found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
apply_thresholds_to_batteries(&supported_batteries, thresholds.start, thresholds.stop)
|
||||
}
|
||||
|
||||
/// Finds all batteries in the system that support threshold control
|
||||
fn find_supported_batteries(power_supply_path: &Path) -> Result<Vec<SupportedBattery<'static>>> {
|
||||
let entries = fs::read_dir(power_supply_path).map_err(|e| {
|
||||
if e.kind() == io::ErrorKind::PermissionDenied {
|
||||
ControlError::PermissionDenied(format!(
|
||||
"Permission denied accessing power supply directory: {}",
|
||||
power_supply_path.display()
|
||||
))
|
||||
} else {
|
||||
ControlError::Io(e)
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut supported_batteries = Vec::new();
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
warn!("Failed to read power-supply entry: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let ps_path = entry.path();
|
||||
if is_battery(&ps_path)? {
|
||||
if let Some(battery) = find_battery_with_threshold_support(&ps_path) {
|
||||
supported_batteries.push(battery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if supported_batteries.is_empty() {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Applies the threshold settings to all supported batteries
|
||||
fn apply_thresholds_to_batteries(
|
||||
batteries: &[SupportedBattery<'_>],
|
||||
start_threshold: u8,
|
||||
stop_threshold: u8,
|
||||
) -> Result<()> {
|
||||
let mut errors = Vec::new();
|
||||
let mut success_count = 0;
|
||||
|
||||
for battery in batteries {
|
||||
let start_path = battery.path.join(battery.pattern.start_path);
|
||||
let stop_path = battery.path.join(battery.pattern.stop_path);
|
||||
|
||||
// Read current thresholds in case we need to restore them
|
||||
let current_stop = sysfs::read_sysfs_value(&stop_path).ok();
|
||||
|
||||
// Write stop threshold first (must be >= start threshold)
|
||||
let stop_result = sysfs::write_sysfs_value(&stop_path, &stop_threshold.to_string());
|
||||
|
||||
// Only proceed to set start threshold if stop threshold was set successfully
|
||||
if matches!(stop_result, Ok(())) {
|
||||
let start_result = sysfs::write_sysfs_value(&start_path, &start_threshold.to_string());
|
||||
|
||||
match start_result {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"Set {}-{}% charge thresholds for {} battery '{}'",
|
||||
start_threshold, stop_threshold, battery.pattern.description, battery.name
|
||||
);
|
||||
success_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
// Start threshold failed, try to restore the previous stop threshold
|
||||
if let Some(prev_stop) = ¤t_stop {
|
||||
let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop);
|
||||
if let Err(re) = restore_result {
|
||||
warn!(
|
||||
"Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.",
|
||||
battery.name, re
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Restored previous stop threshold ({}) for battery '{}'",
|
||||
prev_stop, battery.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
errors.push(format!(
|
||||
"Failed to set start threshold for {} battery '{}': {}",
|
||||
battery.pattern.description, battery.name, e
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if let Err(e) = stop_result {
|
||||
errors.push(format!(
|
||||
"Failed to set stop threshold for {} battery '{}': {}",
|
||||
battery.pattern.description, battery.name, e
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if success_count > 0 {
|
||||
if !errors.is_empty() {
|
||||
warn!(
|
||||
"Partial success setting battery thresholds: {}",
|
||||
errors.join("; ")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ControlError::WriteError(format!(
|
||||
"Failed to set charge thresholds on any battery: {}",
|
||||
errors.join("; ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if a power supply entry is a battery
|
||||
fn is_battery(path: &Path) -> Result<bool> {
|
||||
let type_path = path.join("type");
|
||||
|
||||
if !type_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let ps_type = sysfs::read_sysfs_value(&type_path).map_err(|e| {
|
||||
ControlError::ReadError(format!("Failed to read {}: {}", type_path.display(), e))
|
||||
})?;
|
||||
|
||||
Ok(ps_type == "Battery")
|
||||
}
|
||||
|
||||
/// Identifies if a battery supports threshold control and which pattern it uses
|
||||
fn find_battery_with_threshold_support(ps_path: &Path) -> Option<SupportedBattery<'static>> {
|
||||
for pattern in THRESHOLD_PATTERNS {
|
||||
let start_threshold_path = ps_path.join(pattern.start_path);
|
||||
let stop_threshold_path = ps_path.join(pattern.stop_path);
|
||||
|
||||
// Ensure both paths exist and are writable before considering this battery supported
|
||||
if sysfs::path_exists_and_writable(&start_threshold_path)
|
||||
&& sysfs::path_exists_and_writable(&stop_threshold_path)
|
||||
{
|
||||
return Some(SupportedBattery {
|
||||
name: ps_path.file_name()?.to_string_lossy().to_string(),
|
||||
pattern,
|
||||
path: ps_path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
|
@ -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();
|
||||
let formatted_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
println!("Timestamp: {formatted_time}");
|
||||
|
||||
|
|
|
@ -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),
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
@ -82,13 +80,28 @@ fn load_and_parse_config(path: &Path) -> Result<AppConfig, ConfigError> {
|
|||
let toml_app_config =
|
||||
toml::from_str::<AppConfigToml>(&contents).map_err(ConfigError::TomlError)?;
|
||||
|
||||
// Handle inheritance of values from global to profile configs
|
||||
let mut charger_profile = toml_app_config.charger.clone();
|
||||
let mut battery_profile = toml_app_config.battery.clone();
|
||||
|
||||
// Clone global battery_charge_thresholds once if it exists
|
||||
if let Some(global_thresholds) = toml_app_config.battery_charge_thresholds {
|
||||
// Apply to charger profile if not already set
|
||||
if charger_profile.battery_charge_thresholds.is_none() {
|
||||
charger_profile.battery_charge_thresholds = Some(global_thresholds.clone());
|
||||
}
|
||||
|
||||
// Apply to battery profile if not already set
|
||||
if battery_profile.battery_charge_thresholds.is_none() {
|
||||
battery_profile.battery_charge_thresholds = Some(global_thresholds);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert AppConfigToml to AppConfig
|
||||
Ok(AppConfig {
|
||||
charger: ProfileConfig::from(toml_app_config.charger),
|
||||
battery: ProfileConfig::from(toml_app_config.battery),
|
||||
battery_charge_thresholds: toml_app_config.battery_charge_thresholds,
|
||||
charger: ProfileConfig::from(charger_profile),
|
||||
battery: ProfileConfig::from(battery_profile),
|
||||
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,
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
pub mod load;
|
||||
pub mod types;
|
||||
pub mod watcher;
|
||||
|
||||
// Re-export all configuration types and functions
|
||||
pub use self::load::*;
|
||||
pub use self::types::*;
|
||||
|
||||
// Internal organization of config submodules
|
||||
mod load;
|
||||
mod types;
|
||||
pub use load::*;
|
||||
pub use types::*;
|
||||
|
|
|
@ -1,9 +1,47 @@
|
|||
// Configuration types and structures for superfreq
|
||||
use crate::core::TurboSetting;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BatteryChargeThresholds {
|
||||
pub start: u8,
|
||||
pub stop: u8,
|
||||
}
|
||||
|
||||
impl BatteryChargeThresholds {
|
||||
pub fn new(start: u8, stop: u8) -> Result<Self, ConfigError> {
|
||||
if stop == 0 {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"Stop threshold must be greater than 0%".to_string(),
|
||||
));
|
||||
}
|
||||
if start >= stop {
|
||||
return Err(ConfigError::ValidationError(format!(
|
||||
"Start threshold ({start}) must be less than stop threshold ({stop})"
|
||||
)));
|
||||
}
|
||||
if stop > 100 {
|
||||
return Err(ConfigError::ValidationError(format!(
|
||||
"Stop threshold ({stop}) cannot exceed 100%"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self { start, stop })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(u8, u8)> for BatteryChargeThresholds {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(values: (u8, u8)) -> Result<Self, Self::Error> {
|
||||
let (start, stop) = values;
|
||||
Self::new(start, stop)
|
||||
}
|
||||
}
|
||||
|
||||
// Structs for configuration using serde::Deserialize
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ProfileConfig {
|
||||
pub governor: Option<String>,
|
||||
pub turbo: Option<TurboSetting>,
|
||||
|
@ -13,6 +51,8 @@ pub struct ProfileConfig {
|
|||
pub max_freq_mhz: Option<u32>,
|
||||
pub platform_profile: Option<String>,
|
||||
pub turbo_auto_settings: Option<TurboAutoSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_charge_thresholds: Option<BatteryChargeThresholds>,
|
||||
}
|
||||
|
||||
impl Default for ProfileConfig {
|
||||
|
@ -26,28 +66,22 @@ impl Default for ProfileConfig {
|
|||
max_freq_mhz: None, // no override
|
||||
platform_profile: None, // no override
|
||||
turbo_auto_settings: Some(TurboAutoSettings::default()),
|
||||
battery_charge_thresholds: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
|
||||
pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
pub charger: ProfileConfig,
|
||||
#[serde(default)]
|
||||
pub battery: ProfileConfig,
|
||||
pub 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 {
|
||||
|
@ -55,6 +89,7 @@ pub enum ConfigError {
|
|||
TomlError(toml::de::Error),
|
||||
NoValidConfigFound,
|
||||
HomeDirNotFound,
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ConfigError {
|
||||
|
@ -76,6 +111,7 @@ impl std::fmt::Display for ConfigError {
|
|||
Self::TomlError(e) => write!(f, "TOML parsing error: {e}"),
|
||||
Self::NoValidConfigFound => write!(f, "No valid configuration file found."),
|
||||
Self::HomeDirNotFound => write!(f, "Could not determine user home directory."),
|
||||
Self::ValidationError(s) => write!(f, "Configuration validation error: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +119,7 @@ impl std::fmt::Display for ConfigError {
|
|||
impl std::error::Error for ConfigError {}
|
||||
|
||||
// Intermediate structs for TOML parsing
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ProfileConfigToml {
|
||||
pub governor: Option<String>,
|
||||
pub turbo: Option<String>, // "always", "auto", "never"
|
||||
|
@ -92,18 +128,19 @@ pub struct ProfileConfigToml {
|
|||
pub min_freq_mhz: Option<u32>,
|
||||
pub max_freq_mhz: Option<u32>,
|
||||
pub platform_profile: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_charge_thresholds: Option<BatteryChargeThresholds>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
|
||||
pub struct AppConfigToml {
|
||||
#[serde(default)]
|
||||
pub charger: ProfileConfigToml,
|
||||
#[serde(default)]
|
||||
pub battery: ProfileConfigToml,
|
||||
pub battery_charge_thresholds: Option<(u8, u8)>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_charge_thresholds: Option<BatteryChargeThresholds>,
|
||||
pub ignored_power_supplies: Option<Vec<String>>,
|
||||
#[serde(default = "default_poll_interval_sec")]
|
||||
pub poll_interval_sec: u64,
|
||||
#[serde(default)]
|
||||
pub daemon: DaemonConfigToml,
|
||||
}
|
||||
|
@ -118,11 +155,12 @@ impl Default for ProfileConfigToml {
|
|||
min_freq_mhz: None,
|
||||
max_freq_mhz: None,
|
||||
platform_profile: None,
|
||||
battery_charge_thresholds: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct TurboAutoSettings {
|
||||
#[serde(default = "default_load_threshold_high")]
|
||||
pub load_threshold_high: f32,
|
||||
|
@ -175,11 +213,12 @@ impl From<ProfileConfigToml> for ProfileConfig {
|
|||
max_freq_mhz: toml_config.max_freq_mhz,
|
||||
platform_profile: toml_config.platform_profile,
|
||||
turbo_auto_settings: Some(TurboAutoSettings::default()),
|
||||
battery_charge_thresholds: toml_config.battery_charge_thresholds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct DaemonConfig {
|
||||
#[serde(default = "default_poll_interval_sec")]
|
||||
pub poll_interval_sec: u64,
|
||||
|
@ -197,7 +236,7 @@ pub struct DaemonConfig {
|
|||
pub stats_file_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warning,
|
||||
|
@ -219,6 +258,10 @@ impl Default for DaemonConfig {
|
|||
}
|
||||
}
|
||||
|
||||
const fn default_poll_interval_sec() -> u64 {
|
||||
5
|
||||
}
|
||||
|
||||
const fn default_adaptive_interval() -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -243,7 +286,7 @@ const fn default_stats_file_path() -> Option<String> {
|
|||
None
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct DaemonConfigToml {
|
||||
#[serde(default = "default_poll_interval_sec")]
|
||||
pub poll_interval_sec: u64,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
|
||||
pub enum TurboSetting {
|
||||
Always, // turbo is forced on (if possible)
|
||||
Auto, // system or driver controls turbo
|
||||
|
|
312
src/cpu.rs
312
src/cpu.rs
|
@ -1,11 +1,31 @@
|
|||
use crate::core::{GovernorOverrideMode, TurboSetting};
|
||||
use crate::util::error::ControlError;
|
||||
use core::str;
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, io, path::Path, string::ToString};
|
||||
|
||||
pub type Result<T, E = ControlError> = std::result::Result<T, E>;
|
||||
|
||||
// Valid EPB string values
|
||||
const VALID_EPB_STRINGS: &[&str] = &[
|
||||
"performance",
|
||||
"balance-performance",
|
||||
"balance_performance", // alternative form
|
||||
"balance-power",
|
||||
"balance_power", // alternative form
|
||||
"power",
|
||||
];
|
||||
|
||||
// EPP (Energy Performance Preference) string values
|
||||
const EPP_FALLBACK_VALUES: &[&str] = &[
|
||||
"default",
|
||||
"performance",
|
||||
"balance-performance",
|
||||
"balance_performance", // alternative form with underscore
|
||||
"balance-power",
|
||||
"balance_power", // alternative form with underscore
|
||||
"power",
|
||||
];
|
||||
|
||||
// Write a value to a sysfs file
|
||||
fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<()> {
|
||||
let p = path.as_ref();
|
||||
|
@ -83,15 +103,13 @@ where
|
|||
}
|
||||
|
||||
pub fn set_governor(governor: &str, core_id: Option<u32>) -> Result<()> {
|
||||
// First, check if the requested governor is available on the system
|
||||
let available_governors = get_available_governors()?;
|
||||
// Validate the governor is available on this system
|
||||
// This returns both the validation result and the list of available governors
|
||||
let (is_valid, available_governors) = is_governor_valid(governor)?;
|
||||
|
||||
if !available_governors
|
||||
.iter()
|
||||
.any(|g| g.eq_ignore_ascii_case(governor))
|
||||
{
|
||||
return Err(ControlError::InvalidGovernor(format!(
|
||||
"Governor '{}' is not available. Available governors: {}",
|
||||
if !is_valid {
|
||||
return Err(ControlError::InvalidValueError(format!(
|
||||
"Governor '{}' is not available on this system. Valid governors: {}",
|
||||
governor,
|
||||
available_governors.join(", ")
|
||||
)));
|
||||
|
@ -111,53 +129,87 @@ pub fn set_governor(governor: &str, core_id: Option<u32>) -> Result<()> {
|
|||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
||||
}
|
||||
|
||||
/// Retrieves the list of available CPU governors on the system
|
||||
pub fn get_available_governors() -> Result<Vec<String>> {
|
||||
// Prefer cpu0, fall back to first cpu with cpufreq
|
||||
let mut governor_path =
|
||||
PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors");
|
||||
if !governor_path.exists() {
|
||||
let core_count = get_logical_core_count()?;
|
||||
let candidate = (0..core_count)
|
||||
.map(|i| format!("/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_available_governors"))
|
||||
.find(|path| Path::new(path).exists());
|
||||
if let Some(path) = candidate {
|
||||
governor_path = path.into();
|
||||
}
|
||||
}
|
||||
if !governor_path.exists() {
|
||||
return Err(ControlError::NotSupported(
|
||||
"Could not determine available governors".to_string(),
|
||||
));
|
||||
}
|
||||
/// Check if the provided governor is available in the system
|
||||
/// Returns a tuple of (`is_valid`, `available_governors`) to avoid redundant file reads
|
||||
fn is_governor_valid(governor: &str) -> Result<(bool, Vec<String>)> {
|
||||
let governors = get_available_governors()?;
|
||||
|
||||
let content = fs::read_to_string(&governor_path).map_err(|e| {
|
||||
if e.kind() == io::ErrorKind::PermissionDenied {
|
||||
ControlError::PermissionDenied(format!(
|
||||
"Permission denied reading from {}",
|
||||
governor_path.display()
|
||||
))
|
||||
} else {
|
||||
ControlError::ReadError(format!(
|
||||
"Failed to read from {}: {e}",
|
||||
governor_path.display()
|
||||
))
|
||||
}
|
||||
// Convert input governor to lowercase for case-insensitive comparison
|
||||
let governor_lower = governor.to_lowercase();
|
||||
|
||||
// Convert all available governors to lowercase for comparison
|
||||
let governors_lower: Vec<String> = governors.iter().map(|g| g.to_lowercase()).collect();
|
||||
|
||||
// Check if the lowercase governor is in the lowercase list
|
||||
Ok((governors_lower.contains(&governor_lower), governors))
|
||||
}
|
||||
|
||||
/// Get available CPU governors from the system
|
||||
fn get_available_governors() -> Result<Vec<String>> {
|
||||
let cpu_base_path = Path::new("/sys/devices/system/cpu");
|
||||
|
||||
// First try the traditional path with cpu0. This is the most common case
|
||||
// and will usually catch early, but we should try to keep the code to handle
|
||||
// "edge" cases lightweight, for the (albeit smaller) number of users that
|
||||
// run Superfreq on unusual systems.
|
||||
let cpu0_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors";
|
||||
if Path::new(cpu0_path).exists() {
|
||||
let content = fs::read_to_string(cpu0_path).map_err(|e| {
|
||||
ControlError::ReadError(format!("Failed to read available governors from cpu0: {e}"))
|
||||
})?;
|
||||
|
||||
// Parse the space-separated list of governors
|
||||
let governors = content
|
||||
let governors: Vec<String> = content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>();
|
||||
.collect();
|
||||
|
||||
if governors.is_empty() {
|
||||
return Err(ControlError::ParseError(
|
||||
"No available governors found".to_string(),
|
||||
));
|
||||
if !governors.is_empty() {
|
||||
return Ok(governors);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(governors)
|
||||
// If cpu0 doesn't have the file or it's empty, scan all CPUs
|
||||
// This handles heterogeneous systems where cpu0 might not have cpufreq
|
||||
if let Ok(entries) = fs::read_dir(cpu_base_path) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let name = match file_name.to_str() {
|
||||
Some(name) => name,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Skip non-CPU directories
|
||||
if !name.starts_with("cpu")
|
||||
|| name.len() <= 3
|
||||
|| !name[3..].chars().all(char::is_numeric)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let governor_path = path.join("cpufreq/scaling_available_governors");
|
||||
if governor_path.exists() {
|
||||
match fs::read_to_string(&governor_path) {
|
||||
Ok(content) => {
|
||||
let governors: Vec<String> = content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
|
||||
if !governors.is_empty() {
|
||||
return Ok(governors);
|
||||
}
|
||||
}
|
||||
Err(_) => continue, // try next CPU if this one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we couldn't find any valid governors list
|
||||
Err(ControlError::NotSupported(
|
||||
"Could not determine available governors on any CPU".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_turbo(setting: TurboSetting) -> Result<()> {
|
||||
|
@ -220,6 +272,16 @@ fn try_set_per_core_boost(value: &str) -> Result<bool> {
|
|||
}
|
||||
|
||||
pub fn set_epp(epp: &str, core_id: Option<u32>) -> Result<()> {
|
||||
// Validate the EPP value against available options
|
||||
let available_epp = get_available_epp_values()?;
|
||||
if !available_epp.iter().any(|v| v.eq_ignore_ascii_case(epp)) {
|
||||
return Err(ControlError::InvalidValueError(format!(
|
||||
"Invalid EPP value: '{}'. Available values: {}",
|
||||
epp,
|
||||
available_epp.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
let action = |id: u32| {
|
||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_preference");
|
||||
if Path::new(&path).exists() {
|
||||
|
@ -231,9 +293,31 @@ pub fn set_epp(epp: &str, core_id: Option<u32>) -> Result<()> {
|
|||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
||||
}
|
||||
|
||||
/// Get available EPP values from the system
|
||||
fn get_available_epp_values() -> Result<Vec<String>> {
|
||||
let path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences";
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
// If the file doesn't exist, fall back to a default set of common values
|
||||
// This is safer than failing outright, as some systems may allow these values │
|
||||
// even without explicitly listing them
|
||||
return Ok(EPP_FALLBACK_VALUES.iter().map(|&s| s.to_string()).collect());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(path).map_err(|e| {
|
||||
ControlError::ReadError(format!("Failed to read available EPP values: {e}"))
|
||||
})?;
|
||||
|
||||
Ok(content
|
||||
.split_whitespace()
|
||||
.map(ToString::to_string)
|
||||
.collect())
|
||||
}
|
||||
|
||||
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.
|
||||
// Validate EPB value - should be a number 0-15 or a recognized string value
|
||||
validate_epb_value(epb)?;
|
||||
|
||||
let action = |id: u32| {
|
||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias");
|
||||
if Path::new(&path).exists() {
|
||||
|
@ -245,8 +329,50 @@ pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> {
|
|||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
||||
}
|
||||
|
||||
fn validate_epb_value(epb: &str) -> Result<()> {
|
||||
// EPB can be a number from 0-15 or a recognized string
|
||||
// Try parsing as a number first
|
||||
if let Ok(value) = epb.parse::<u8>() {
|
||||
if value <= 15 {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(ControlError::InvalidValueError(format!(
|
||||
"EPB numeric value must be between 0 and 15, got {value}"
|
||||
)));
|
||||
}
|
||||
|
||||
// If not a number, check if it's a recognized string value.
|
||||
// This is using case-insensitive comparison
|
||||
if VALID_EPB_STRINGS
|
||||
.iter()
|
||||
.any(|valid| valid.eq_ignore_ascii_case(epb))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ControlError::InvalidValueError(format!(
|
||||
"Invalid EPB value: '{}'. Must be a number 0-15 or one of: {}",
|
||||
epb,
|
||||
VALID_EPB_STRINGS.join(", ")
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_min_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
||||
let freq_khz_str = (freq_mhz * 1000).to_string();
|
||||
// Check if the new minimum frequency would be greater than current maximum
|
||||
if let Some(id) = core_id {
|
||||
validate_min_frequency(id, freq_mhz)?;
|
||||
} else {
|
||||
// Check for all cores
|
||||
let num_cores = get_logical_core_count()?;
|
||||
for id in 0..num_cores {
|
||||
validate_min_frequency(id, freq_mhz)?;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: We use u64 for the intermediate calculation to prevent overflow
|
||||
let freq_khz = u64::from(freq_mhz) * 1000;
|
||||
let freq_khz_str = freq_khz.to_string();
|
||||
|
||||
let action = |id: u32| {
|
||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_min_freq");
|
||||
if Path::new(&path).exists() {
|
||||
|
@ -259,7 +385,21 @@ pub fn set_min_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
|||
}
|
||||
|
||||
pub fn set_max_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
||||
let freq_khz_str = (freq_mhz * 1000).to_string();
|
||||
// Check if the new maximum frequency would be less than current minimum
|
||||
if let Some(id) = core_id {
|
||||
validate_max_frequency(id, freq_mhz)?;
|
||||
} else {
|
||||
// Check for all cores
|
||||
let num_cores = get_logical_core_count()?;
|
||||
for id in 0..num_cores {
|
||||
validate_max_frequency(id, freq_mhz)?;
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: Use a u64 here as well.
|
||||
let freq_khz = u64::from(freq_mhz) * 1000;
|
||||
let freq_khz_str = freq_khz.to_string();
|
||||
|
||||
let action = |id: u32| {
|
||||
let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_max_freq");
|
||||
if Path::new(&path).exists() {
|
||||
|
@ -271,6 +411,66 @@ pub fn set_max_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
|||
core_id.map_or_else(|| for_each_cpu_core(action), action)
|
||||
}
|
||||
|
||||
fn read_sysfs_value_as_u32(path: &str) -> Result<u32> {
|
||||
if !Path::new(path).exists() {
|
||||
return Err(ControlError::NotSupported(format!(
|
||||
"File does not exist: {path}"
|
||||
)));
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(path)
|
||||
.map_err(|e| ControlError::ReadError(format!("Failed to read {path}: {e}")))?;
|
||||
|
||||
content
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.map_err(|e| ControlError::ReadError(format!("Failed to parse value from {path}: {e}")))
|
||||
}
|
||||
|
||||
fn validate_min_frequency(core_id: u32, new_min_freq_mhz: u32) -> Result<()> {
|
||||
let max_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_max_freq");
|
||||
|
||||
if !Path::new(&max_freq_path).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let max_freq_khz = read_sysfs_value_as_u32(&max_freq_path)?;
|
||||
let new_min_freq_khz = new_min_freq_mhz * 1000;
|
||||
|
||||
if new_min_freq_khz > max_freq_khz {
|
||||
return Err(ControlError::InvalidValueError(format!(
|
||||
"Minimum frequency ({} MHz) cannot be higher than maximum frequency ({} MHz) for core {}",
|
||||
new_min_freq_mhz,
|
||||
max_freq_khz / 1000,
|
||||
core_id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> {
|
||||
let min_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_min_freq");
|
||||
|
||||
if !Path::new(&min_freq_path).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let min_freq_khz = read_sysfs_value_as_u32(&min_freq_path)?;
|
||||
let new_max_freq_khz = new_max_freq_mhz * 1000;
|
||||
|
||||
if new_max_freq_khz < min_freq_khz {
|
||||
return Err(ControlError::InvalidValueError(format!(
|
||||
"Maximum frequency ({} MHz) cannot be lower than minimum frequency ({} MHz) for core {}",
|
||||
new_max_freq_mhz,
|
||||
min_freq_khz / 1000,
|
||||
core_id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the platform profile.
|
||||
/// This changes the system performance, temperature, fan, and other hardware replated characteristics.
|
||||
///
|
||||
|
@ -311,11 +511,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";
|
||||
|
@ -336,7 +538,7 @@ pub fn get_platform_profiles() -> Result<Vec<String>> {
|
|||
}
|
||||
|
||||
/// Path for storing the governor override state
|
||||
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/superfreq/governor_override";
|
||||
const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override";
|
||||
|
||||
/// Force a specific CPU governor or reset to automatic mode
|
||||
pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> {
|
||||
|
|
|
@ -202,16 +202,6 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the logger with the appropriate level
|
||||
const fn get_log_level_filter(log_level: LogLevel) -> LevelFilter {
|
||||
match log_level {
|
||||
LogLevel::Error => LevelFilter::Error,
|
||||
LogLevel::Warning => LevelFilter::Warn,
|
||||
LogLevel::Info => LevelFilter::Info,
|
||||
LogLevel::Debug => LevelFilter::Debug,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write current system stats to a file for --stats to read
|
||||
fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> {
|
||||
let mut file = File::create(path)?;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::config::{AppConfig, ProfileConfig};
|
||||
use crate::battery;
|
||||
use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
|
||||
use crate::core::{OperationalMode, SystemReport, TurboSetting};
|
||||
use crate::cpu::{self};
|
||||
use crate::util::error::{ControlError, EngineError};
|
||||
|
@ -22,14 +23,13 @@ where
|
|||
match apply_fn() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
if matches!(e, ControlError::NotSupported(_))
|
||||
|| matches!(e, ControlError::PathMissing(_))
|
||||
{
|
||||
if matches!(e, ControlError::NotSupported(_)) {
|
||||
warn!(
|
||||
"{feature_name} setting is not supported on this system. Skipping {feature_name} configuration."
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
// Propagate all other errors, including InvalidValueError
|
||||
Err(EngineError::ControlError(e))
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +50,10 @@ pub fn determine_and_apply_settings(
|
|||
override_governor.trim()
|
||||
);
|
||||
|
||||
// Apply the override governor setting - validation is handled by set_governor
|
||||
cpu::set_governor(override_governor.trim(), None)?;
|
||||
// Apply the override governor setting
|
||||
try_apply_feature("override governor", override_governor.trim(), || {
|
||||
cpu::set_governor(override_governor.trim(), None)
|
||||
})?;
|
||||
}
|
||||
|
||||
let selected_profile_config: &ProfileConfig;
|
||||
|
@ -69,11 +71,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 {
|
||||
// Check if any battery reports AC connected
|
||||
report.batteries.iter().any(|b| b.ac_connected)
|
||||
};
|
||||
|
||||
if on_ac_power {
|
||||
info!("On AC power, selecting Charger profile.");
|
||||
|
@ -90,7 +96,7 @@ pub fn determine_and_apply_settings(
|
|||
// 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::InvalidGovernor(_))
|
||||
if matches!(e, ControlError::InvalidValueError(_))
|
||||
|| matches!(e, ControlError::NotSupported(_))
|
||||
{
|
||||
warn!(
|
||||
|
@ -143,6 +149,24 @@ pub fn determine_and_apply_settings(
|
|||
})?;
|
||||
}
|
||||
|
||||
// Set battery charge thresholds if configured
|
||||
if let Some(thresholds) = &selected_profile_config.battery_charge_thresholds {
|
||||
let start_threshold = thresholds.start;
|
||||
let stop_threshold = thresholds.stop;
|
||||
|
||||
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.");
|
||||
|
||||
Ok(())
|
||||
|
@ -152,6 +176,9 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
|
|||
// Get the auto turbo settings from the config, or use defaults
|
||||
let turbo_settings = config.turbo_auto_settings.clone().unwrap_or_default();
|
||||
|
||||
// Validate the complete configuration to ensure it's usable
|
||||
validate_turbo_auto_settings(&turbo_settings)?;
|
||||
|
||||
// Get average CPU temperature and CPU load
|
||||
let cpu_temp = report.cpu_global.average_temperature_celsius;
|
||||
|
||||
|
@ -177,14 +204,6 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
|
|||
}
|
||||
};
|
||||
|
||||
// Validate the configuration to ensure it's usable
|
||||
if turbo_settings.load_threshold_high <= turbo_settings.load_threshold_low {
|
||||
return Err(EngineError::ConfigurationError(
|
||||
"Invalid turbo auto settings: high threshold must be greater than low threshold"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Decision logic for enabling/disabling turbo
|
||||
let enable_turbo = match (cpu_temp, avg_cpu_usage) {
|
||||
// If temperature is too high, disable turbo regardless of load
|
||||
|
@ -237,3 +256,30 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
|
|||
Err(e) => Err(EngineError::ControlError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_turbo_auto_settings(settings: &TurboAutoSettings) -> Result<(), EngineError> {
|
||||
// Validate load thresholds
|
||||
if settings.load_threshold_high <= settings.load_threshold_low {
|
||||
return Err(EngineError::ConfigurationError(
|
||||
"Invalid turbo auto settings: high threshold must be greater than low threshold"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate range of load thresholds (should be 0-100%)
|
||||
if settings.load_threshold_high > 100.0 || settings.load_threshold_low < 0.0 {
|
||||
return Err(EngineError::ConfigurationError(
|
||||
"Invalid turbo auto settings: load thresholds must be between 0% and 100%".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Validate temperature threshold (realistic range for CPU temps in Celsius)
|
||||
if settings.temp_threshold_high <= 0.0 || settings.temp_threshold_high > 110.0 {
|
||||
return Err(EngineError::ConfigurationError(
|
||||
"Invalid turbo auto settings: temperature threshold must be between 0°C and 110°C"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
93
src/main.rs
93
src/main.rs
|
@ -1,3 +1,4 @@
|
|||
mod battery;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod conflict;
|
||||
|
@ -11,7 +12,7 @@ mod util;
|
|||
use crate::config::AppConfig;
|
||||
use crate::core::{GovernorOverrideMode, TurboSetting};
|
||||
use crate::util::error::ControlError;
|
||||
use clap::Parser;
|
||||
use clap::{Parser, value_parser};
|
||||
use env_logger::Builder;
|
||||
use log::{debug, error, info};
|
||||
use std::sync::Once;
|
||||
|
@ -77,6 +78,15 @@ enum Commands {
|
|||
},
|
||||
/// Set ACPI platform profile
|
||||
SetPlatformProfile { profile: String },
|
||||
/// Set battery charge thresholds to extend battery lifespan
|
||||
SetBatteryThresholds {
|
||||
/// Percentage at which charging starts (when below this value)
|
||||
#[clap(value_parser = value_parser!(u8).range(0..=99))]
|
||||
start_threshold: u8,
|
||||
/// Percentage at which charging stops (when it reaches this value)
|
||||
#[clap(value_parser = value_parser!(u8).range(1..=100))]
|
||||
stop_threshold: u8,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -349,15 +359,74 @@ fn main() {
|
|||
cpu::set_epb(&epb, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
}
|
||||
Some(Commands::SetMinFreq { freq_mhz, core_id }) => {
|
||||
// Basic validation for reasonable CPU frequency values
|
||||
if let Err(e) = validate_freq(freq_mhz, "Minimum") {
|
||||
error!("{e}");
|
||||
Err(e)
|
||||
} else {
|
||||
cpu::set_min_frequency(freq_mhz, core_id)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
}
|
||||
}
|
||||
Some(Commands::SetMaxFreq { freq_mhz, core_id }) => {
|
||||
// Basic validation for reasonable CPU frequency values
|
||||
if let Err(e) = validate_freq(freq_mhz, "Maximum") {
|
||||
error!("{e}");
|
||||
Err(e)
|
||||
} else {
|
||||
cpu::set_max_frequency(freq_mhz, core_id)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
}
|
||||
Some(Commands::SetPlatformProfile { profile }) => cpu::set_platform_profile(&profile)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>),
|
||||
}
|
||||
Some(Commands::SetPlatformProfile { profile }) => {
|
||||
// Get available platform profiles and validate early if possible
|
||||
match cpu::get_platform_profiles() {
|
||||
Ok(available_profiles) => {
|
||||
if available_profiles.contains(&profile) {
|
||||
info!("Setting platform profile to '{profile}'");
|
||||
cpu::set_platform_profile(&profile)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
} else {
|
||||
error!(
|
||||
"Invalid platform profile: '{}'. Available profiles: {}",
|
||||
profile,
|
||||
available_profiles.join(", ")
|
||||
);
|
||||
Err(Box::new(ControlError::InvalidProfile(format!(
|
||||
"Invalid platform profile: '{}'. Available profiles: {}",
|
||||
profile,
|
||||
available_profiles.join(", ")
|
||||
))) as Box<dyn std::error::Error>)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// If we can't get profiles (e.g., feature not supported), pass through to the function
|
||||
// which will provide appropriate error
|
||||
cpu::set_platform_profile(&profile)
|
||||
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::SetBatteryThresholds {
|
||||
start_threshold,
|
||||
stop_threshold,
|
||||
}) => {
|
||||
// We only need to check if start < stop since the range validation is handled by Clap
|
||||
if start_threshold >= stop_threshold {
|
||||
error!(
|
||||
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
|
||||
);
|
||||
Err(Box::new(ControlError::InvalidValueError(format!(
|
||||
"Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})"
|
||||
))) as Box<dyn std::error::Error>)
|
||||
} else {
|
||||
info!(
|
||||
"Setting battery thresholds: start at {start_threshold}%, stop at {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),
|
||||
Some(Commands::Debug) => cli::debug::run_debug(&config),
|
||||
None => {
|
||||
|
@ -404,3 +473,21 @@ fn init_logger() {
|
|||
debug!("Logger initialized with RUST_LOG={env_log}");
|
||||
});
|
||||
}
|
||||
|
||||
/// Validate CPU frequency input values
|
||||
fn validate_freq(freq_mhz: u32, label: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if freq_mhz == 0 {
|
||||
error!("{label} frequency cannot be zero");
|
||||
Err(Box::new(ControlError::InvalidValueError(format!(
|
||||
"{label} frequency cannot be zero"
|
||||
))) as Box<dyn std::error::Error>)
|
||||
} else if freq_mhz > 10000 {
|
||||
// Extremely high value unlikely to be valid
|
||||
error!("{label} frequency ({freq_mhz} MHz) is unreasonably high");
|
||||
Err(Box::new(ControlError::InvalidValueError(format!(
|
||||
"{label} frequency ({freq_mhz} MHz) is unreasonably high"
|
||||
))) as Box<dyn std::error::Error>)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
132
src/monitor.rs
132
src/monitor.rs
|
@ -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,
|
||||
|
@ -48,7 +49,7 @@ pub fn get_system_info() -> SystemInfo {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct CpuTimes {
|
||||
pub struct CpuTimes {
|
||||
user: u64,
|
||||
nice: u64,
|
||||
system: u64,
|
||||
|
@ -57,8 +58,6 @@ struct CpuTimes {
|
|||
irq: u64,
|
||||
softirq: u64,
|
||||
steal: u64,
|
||||
guest: u64,
|
||||
guest_nice: u64,
|
||||
}
|
||||
|
||||
impl CpuTimes {
|
||||
|
@ -147,18 +146,6 @@ fn read_all_cpu_times() -> Result<HashMap<u32, CpuTimes>> {
|
|||
parts[8]
|
||||
))
|
||||
})?,
|
||||
guest: parts[9].parse().map_err(|_| {
|
||||
SysMonitorError::ProcStatParseError(format!(
|
||||
"Failed to parse guest time: {}",
|
||||
parts[9]
|
||||
))
|
||||
})?,
|
||||
guest_nice: parts[10].parse().map_err(|_| {
|
||||
SysMonitorError::ProcStatParseError(format!(
|
||||
"Failed to parse guest_nice time: {}",
|
||||
parts[10]
|
||||
))
|
||||
})?,
|
||||
};
|
||||
cpu_times_map.insert(core_id, times);
|
||||
}
|
||||
|
@ -288,7 +275,7 @@ pub fn get_cpu_core_info(
|
|||
None
|
||||
} else {
|
||||
let usage = 100.0 * (1.0 - (idle_diff as f32 / total_diff as f32));
|
||||
Some(usage.max(0.0).min(100.0)) // clamp between 0 and 100
|
||||
Some(usage.clamp(0.0, 100.0)) // clamp between 0 and 100
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -374,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()
|
||||
|
@ -412,11 +399,13 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
|
|||
eprintln!("Warning: {e}");
|
||||
0
|
||||
});
|
||||
let path = (0..core_count)
|
||||
.map(|i| PathBuf::from(format!("/sys/devices/system/cpu/cpu{i}/cpufreq/")))
|
||||
.find(|path| path.exists());
|
||||
if let Some(test_path_buf) = path {
|
||||
cpufreq_base_path_buf = test_path_buf;
|
||||
|
||||
for i in 0..core_count {
|
||||
let test_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{i}/cpufreq/"));
|
||||
if test_path.exists() {
|
||||
cpufreq_base_path_buf = test_path;
|
||||
break; // Exit the loop as soon as we find a valid path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -533,7 +522,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;
|
||||
|
@ -543,6 +532,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();
|
||||
|
@ -554,6 +549,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();
|
||||
|
||||
|
@ -594,9 +595,94 @@ 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 {
|
||||
// Convert name to lowercase once for case-insensitive matching
|
||||
let name_lower = name.to_lowercase();
|
||||
|
||||
// Common peripheral battery names
|
||||
if name_lower.contains("mouse")
|
||||
|| name_lower.contains("keyboard")
|
||||
|| name_lower.contains("trackpad")
|
||||
|| name_lower.contains("gamepad")
|
||||
|| name_lower.contains("controller")
|
||||
|| name_lower.contains("headset")
|
||||
|| name_lower.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();
|
||||
|
|
|
@ -4,13 +4,13 @@ use std::io;
|
|||
pub enum ControlError {
|
||||
Io(io::Error),
|
||||
WriteError(String),
|
||||
ReadError(String),
|
||||
InvalidValueError(String),
|
||||
NotSupported(String),
|
||||
PermissionDenied(String),
|
||||
InvalidProfile(String),
|
||||
InvalidGovernor(String),
|
||||
ParseError(String),
|
||||
ReadError(String),
|
||||
PathMissing(String),
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ impl std::fmt::Display for ControlError {
|
|||
match self {
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Self::WriteError(s) => write!(f, "Failed to write to sysfs path: {s}"),
|
||||
Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"),
|
||||
Self::InvalidValueError(s) => write!(f, "Invalid value for setting: {s}"),
|
||||
Self::NotSupported(s) => write!(f, "Control action not supported: {s}"),
|
||||
Self::PermissionDenied(s) => {
|
||||
|
@ -45,9 +46,6 @@ impl std::fmt::Display for ControlError {
|
|||
Self::ParseError(s) => {
|
||||
write!(f, "Failed to parse value: {s}")
|
||||
}
|
||||
Self::ReadError(s) => {
|
||||
write!(f, "Failed to read sysfs path: {s}")
|
||||
}
|
||||
Self::PathMissing(s) => {
|
||||
write!(f, "Path missing: {s}")
|
||||
}
|
||||
|
@ -63,7 +61,6 @@ pub enum SysMonitorError {
|
|||
ReadError(String),
|
||||
ParseError(String),
|
||||
ProcStatParseError(String),
|
||||
NotAvailable(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for SysMonitorError {
|
||||
|
@ -81,7 +78,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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod error;
|
||||
pub mod sysfs;
|
||||
|
|
80
src/util/sysfs.rs
Normal file
80
src/util/sysfs.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use crate::util::error::ControlError;
|
||||
use std::{fs, io, path::Path};
|
||||
|
||||
/// Write a value to a sysfs file with consistent error handling
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The file path to write to
|
||||
/// * `value` - The string value to write
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a `ControlError` variant based on the specific error:
|
||||
/// - `ControlError::PermissionDenied` if permission is denied
|
||||
/// - `ControlError::PathMissing` if the path doesn't exist
|
||||
/// - `ControlError::WriteError` for other I/O errors
|
||||
pub fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<(), ControlError> {
|
||||
let p = path.as_ref();
|
||||
|
||||
fs::write(p, value).map_err(|e| {
|
||||
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
|
||||
match e.kind() {
|
||||
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg),
|
||||
io::ErrorKind::NotFound => {
|
||||
ControlError::PathMissing(format!("Path '{}' does not exist", p.display()))
|
||||
}
|
||||
_ => ControlError::WriteError(error_msg),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a value from a sysfs file with consistent error handling
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The file path to read from
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns the trimmed contents of the file as a String
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a `ControlError` variant based on the specific error:
|
||||
/// - `ControlError::PermissionDenied` if permission is denied
|
||||
/// - `ControlError::PathMissing` if the path doesn't exist
|
||||
/// - `ControlError::ReadError` for other I/O errors
|
||||
pub fn read_sysfs_value(path: impl AsRef<Path>) -> Result<String, ControlError> {
|
||||
let p = path.as_ref();
|
||||
fs::read_to_string(p)
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("Path: {:?}, Error: {}", p.display(), e);
|
||||
match e.kind() {
|
||||
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg),
|
||||
io::ErrorKind::NotFound => {
|
||||
ControlError::PathMissing(format!("Path '{}' does not exist", p.display()))
|
||||
}
|
||||
_ => ControlError::ReadError(error_msg),
|
||||
}
|
||||
})
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
/// Safely check if a path exists and is writable
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The file path to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns true if the path exists and is writable, false otherwise
|
||||
pub fn path_exists_and_writable(path: &Path) -> bool {
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to open the file with write access to verify write permission
|
||||
fs::OpenOptions::new().write(true).open(path).is_ok()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue