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

cpu&power: add more attributes

This commit is contained in:
RGBCube 2025-05-20 19:41:53 +03:00
parent 543e5a052e
commit 137f801d2b
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
5 changed files with 155 additions and 116 deletions

View file

@ -29,18 +29,19 @@ pub fn read(path: impl AsRef<Path>) -> Option<anyhow::Result<String>> {
} }
} }
pub fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> { pub fn read_u64(path: impl AsRef<Path>) -> Option<anyhow::Result<u64>> {
let path = path.as_ref(); let path = path.as_ref();
let content = fs::read_to_string(path) match read(path)? {
.with_context(|| format!("failed to read '{path}'", path = path.display()))?; Ok(content) => Some(content.trim().parse().with_context(|| {
format!(
"failed to parse contents of '{path}' as a unsigned number",
path = path.display(),
)
})),
Ok(content.trim().parse().with_context(|| { Err(error) => Some(Err(error)),
format!( }
"failed to parse contents of '{path}' as a unsigned number",
path = path.display(),
)
})?)
} }
pub fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> { pub fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {

View file

@ -36,7 +36,7 @@ enum Command {
/// Start the daemon. /// Start the daemon.
Start { Start {
/// The daemon config path. /// The daemon config path.
#[arg(long, env = "SUPERFREQ_CONFIG")] #[arg(long, env = "WATT_CONFIG")]
config: PathBuf, config: PathBuf,
}, },

View file

@ -599,110 +599,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result<Vec<BatteryInfo>>
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 {
// 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() -> anyhow::Result<SystemLoad> {
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
if parts.len() < 3 {
return Err(SysMonitorError::ParseError(
"Could not parse /proc/loadavg: expected at least 3 parts".to_string(),
));
}
let load_avg_1min = parts[0].parse().map_err(|_| {
SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0]))
})?;
let load_avg_5min = parts[1].parse().map_err(|_| {
SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1]))
})?;
let load_avg_15min = parts[2].parse().map_err(|_| {
SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2]))
})?;
Ok(SystemLoad {
load_avg_1min,
load_avg_5min,
load_avg_15min,
})
}
pub fn collect_system_report(config: &AppConfig) -> anyhow::Result<SystemReport> { pub fn collect_system_report(config: &AppConfig) -> anyhow::Result<SystemReport> {
let system_info = get_system_info(); let system_info = get_system_info();
let cpu_cores = get_all_cpu_core_info()?; let cpu_cores = get_all_cpu_core_info()?;

View file

@ -48,6 +48,9 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
pub struct PowerSupply { pub struct PowerSupply {
pub name: String, pub name: String,
pub path: PathBuf, pub path: PathBuf,
pub is_from_peripheral: bool,
pub threshold_config: Option<PowerSupplyThresholdConfig>, pub threshold_config: Option<PowerSupplyThresholdConfig>,
} }
@ -74,6 +77,9 @@ impl PowerSupply {
let mut power_supply = Self { let mut power_supply = Self {
path: Path::new(POWER_SUPPLY_PATH).join(&name), path: Path::new(POWER_SUPPLY_PATH).join(&name),
name, name,
is_from_peripheral: false,
threshold_config: None, threshold_config: None,
}; };
@ -94,6 +100,8 @@ impl PowerSupply {
path, path,
is_from_peripheral: false,
threshold_config: None, threshold_config: None,
}; };
@ -157,6 +165,46 @@ impl PowerSupply {
self.threshold_config = threshold_config; self.threshold_config = threshold_config;
self.is_from_peripheral = 'is_from_peripheral: {
let name_lower = self.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")
{
break 'is_from_peripheral true;
}
// Small capacity batteries are likely not laptop batteries.
if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) {
let energy_full = energy_full
.with_context(|| format!("failed to read the max charge '{self}' can hold"))?;
// 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.
break 'is_from_peripheral true;
}
}
// Check for model name that indicates a peripheral
if let Some(model) = fs::read(self.path.join("model_name")) {
let model =
model.with_context(|| format!("failed to read the model name of '{self}'"))?;
if model.contains("bluetooth") || model.contains("wireless") {
break 'is_from_peripheral true;
}
}
false
};
Ok(()) Ok(())
} }

View file

@ -1,14 +1,108 @@
use anyhow::{Context, bail};
use crate::fs;
pub struct System { pub struct System {
pub is_desktop: bool, pub is_desktop: bool,
pub load_average_1min: f64,
pub load_average_5min: f64,
pub load_average_15min: f64,
} }
impl System { impl System {
pub fn new() -> anyhow::Result<Self> { pub fn new() -> anyhow::Result<Self> {
let mut system = Self { is_desktop: false }; let mut system = Self {
is_desktop: false,
load_average_1min: 0.0,
load_average_5min: 0.0,
load_average_15min: 0.0,
};
system.rescan()?; system.rescan()?;
Ok(system) Ok(system)
} }
pub fn rescan(&mut self) -> anyhow::Result<()> {} pub fn rescan(&mut self) -> anyhow::Result<()> {
self.is_desktop = self.is_desktop()?;
let (load_average_1min, load_average_5min, load_average_15min) = self.load_average()?;
self.load_average_1min = load_average_1min;
self.load_average_5min = load_average_5min;
self.load_average_15min = load_average_15min;
Ok(())
}
fn is_desktop(&self) -> anyhow::Result<bool> {
if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") {
let chassis_type = chassis_type.context("failed to read chassis type")?;
// 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.trim() {
// Desktop form factors.
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return Ok(true),
// Laptop form factors.
"9" | "10" | "14" => return Ok(false),
// Unknown, continue with other checks
_ => {}
}
}
// Check CPU power policies, desktops often don't have these
let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp")
|| fs::exists("/sys/devices/system/cpu/cpufreq/conservative");
if !power_saving_exists {
return Ok(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 fs::exists(path) {
return Ok(false); // Likely a laptop.
}
}
// Default to assuming desktop if we can't determine.
Ok(true)
}
fn load_average(&self) -> anyhow::Result<(f64, f64, f64)> {
let content = fs::read("/proc/loadavg")
.context("load average file doesn't exist, are you on linux?")?
.context("failed to read load average")?;
let mut parts = content.split_whitespace();
let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) =
(parts.next(), parts.next(), parts.next())
else {
bail!(
"failed to parse first 3 load average entries due to there not being enough, content: {content}"
);
};
Ok((
load_average_1min
.parse()
.context("failed to parse load average")?,
load_average_5min
.parse()
.context("failed to parse load average")?,
load_average_15min
.parse()
.context("failed to parse load average")?,
))
}
} }