From 92806e28430add0759fabfabf77a07f802da0352 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 29 May 2026 16:35:21 +0545 Subject: [PATCH] feat(src/commands): add fan speed monitoring and enhance GPU temperature command Refactor fan monitoring and improve GPU temperature reporting for better hardware visibility. - New Features: - Add `fan` command to enumerate and display fan speeds via hwmon and thermal cooling devices. - Expand temperature detection across hwmon, thermal zones, and NVIDIA GPU sensors. - Enhancements: - Strengthen GPU temperature output with power and clock information. - Improve error messages and logging consistency. - Bug Fixes: - Correct error message wording. - Gracefully handle missing sensor files. --- src/commands/fan.rs | 100 ++++++++++++++++++++++++++++ src/commands/gpu.rs | 128 ++++++++++++++++++++++++++++++----- src/commands/mod.rs | 2 + src/commands/status.rs | 36 ++++++++++ src/commands/temperature.rs | 129 ++++++++++++++++++++++++++++++++++++ 5 files changed, 378 insertions(+), 17 deletions(-) create mode 100644 src/commands/fan.rs create mode 100644 src/commands/temperature.rs diff --git a/src/commands/fan.rs b/src/commands/fan.rs new file mode 100644 index 0000000..9cd7f85 --- /dev/null +++ b/src/commands/fan.rs @@ -0,0 +1,100 @@ +use std::fs; +use std::path::Path; + +pub fn execute() { + println!("--- Fan Speed ---"); + let mut found = false; + + // Read fan speeds from hwmon + let hwmon_dir = Path::new("/sys/class/hwmon"); + if hwmon_dir.exists() + && let Ok(entries) = fs::read_dir(hwmon_dir) + { + for entry in entries.flatten() { + let path = entry.path(); + let name_path = path.join("name"); + let Ok(name) = fs::read_to_string(&name_path) else { + continue; + }; + let hwmon_name = name.trim().to_string(); + + let mut idx = 1; + loop { + let input_path = path.join(format!("fan{}_input", idx)); + if !input_path.exists() { + break; + } + + if let Ok(raw) = fs::read_to_string(&input_path) { + let rpm: u64 = raw.trim().parse().unwrap_or(0); + + let label = path.join(format!("fan{}_label", idx)); + let label_str = fs::read_to_string(&label) + .map(|l| l.trim().to_string()) + .unwrap_or_else(|_| format!("Fan {}", idx)); + + let display_name = match hwmon_name.as_str() { + "acpi_fan" => "CPU Fan", + other => other, + }; + + if rpm == 0 { + println!(" {} ({}): Stopped", display_name, label_str); + } else { + println!(" {} ({}): {} RPM", display_name, label_str, rpm); + } + found = true; + } + + idx += 1; + } + } + } + + // Show fan policy from thermal cooling devices + let thermal_dir = Path::new("/sys/class/thermal"); + if thermal_dir.exists() + && let Ok(entries) = fs::read_dir(thermal_dir) + { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if !name_str.starts_with("cooling_device") { + continue; + } + + let type_path = entry.path().join("type"); + let state_path = entry.path().join("cur_state"); + let max_state_path = entry.path().join("max_state"); + + if let (Ok(type_content), Ok(state), Ok(max_state)) = ( + fs::read_to_string(&type_path), + fs::read_to_string(&state_path), + fs::read_to_string(&max_state_path), + ) { + let zone_type = type_content.trim(); + let cur_state: u64 = state.trim().parse().unwrap_or(0); + let max: u64 = max_state.trim().parse().unwrap_or(0); + + if zone_type == "Fan" { + let policy = if cur_state == 0 { + "Off" + } else if cur_state >= max { + "Full Speed" + } else { + "Variable" + }; + println!( + " {} ({}): Level {}/{} ({})", + name_str, zone_type, cur_state, max, policy + ); + found = true; + } + } + } + } + + if !found { + println!(" No fan sensors found."); + } +} diff --git a/src/commands/gpu.rs b/src/commands/gpu.rs index 42833cd..17190c6 100644 --- a/src/commands/gpu.rs +++ b/src/commands/gpu.rs @@ -854,37 +854,130 @@ fn switch_nvidia( fn run_on_dgpu(command: &[String]) { if command.is_empty() { - error!("No command provided to run."); + error!("No command specified."); return; } if get_current_mode() != "hybrid" { - log::warn!( - "Running on dGPU is typically only effective in Hybrid mode. \ - Your current mode is '{}'. The application may fail to start or \ - not use the NVIDIA GPU.", - get_current_mode() - ); + error!("GPU run is only supported in Hybrid mode."); + return; } - log::info!("Launching '{}' on the discrete GPU...", command.join(" ")); + let env_vars = vec![ + ( + "__NV_PRIME_RENDER_OFFLOAD", + "1".to_string(), + ), + ( + "__GLX_VENDOR_LIBRARY_NAME", + "nvidia".to_string(), + ), + ( + "__VK_LAYER_NV_optimus", + "NVIDIA_only".to_string(), + ), + ]; - let mut child = Command::new(&command[0]); + let mut cmd = Command::new(&command[0]); if command.len() > 1 { - child.args(&command[1..]); + cmd.args(&command[1..]); } - // Set PRIME offload variables natively - child.env("__NV_PRIME_RENDER_OFFLOAD", "1"); - child.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); - child.env("__VK_LAYER_NV_optimus", "NVIDIA_only"); + for (key, value) in &env_vars { + cmd.env(key, value); + } - match child.status() { + info!("Running '{}' on discrete GPU", command.join(" ")); + + match cmd.status() { Ok(status) => { - log::info!("Application exited with: {}", status); + if !status.success() { + error!("Command exited with status: {}", status); + } } Err(e) => { - error!("Failed to launch application: {}", e); + error!("Failed to execute command: {}", e); + } + } +} + +fn show_gpu_power_state() { + println!("--- GPU Power State ---"); + + // Read PCI power state from sysfs + let pci_base = Path::new("/sys/bus/pci/devices"); + if let Ok(entries) = fs::read_dir(pci_base) { + for entry in entries.flatten() { + let vendor_path = entry.path().join("vendor"); + let class_path = entry.path().join("class"); + + let Ok(vendor) = fs::read_to_string(&vendor_path) else { + continue; + }; + let Ok(class) = fs::read_to_string(&class_path) else { + continue; + }; + + // NVIDIA vendor = 0x10de, VGA class starts with 0x0300 + if vendor.trim() == "0x10de" && class.trim().starts_with("0x0300") { + let pci_addr = entry.file_name().to_string_lossy().to_string(); + println!(" Device: NVIDIA ({})", pci_addr); + + // Power state + let runtime_status = entry.path().join("power/runtime_status"); + if let Ok(status) = fs::read_to_string(&runtime_status) { + println!(" Runtime Status: {}", status.trim()); + } + + // Power control + let power_control = entry.path().join("power/control"); + if let Ok(control) = fs::read_to_string(&power_control) { + println!(" Power Control: {}", control.trim()); + } + + // Current power state (D0 = fully on, D3 = off) + let current_state = entry.path().join("power_state"); + if let Ok(state) = fs::read_to_string(¤t_state) { + println!(" PCI Power State: {}", state.trim()); + } + + // Max power state + let max_state = entry.path().join("max_power_state"); + if let Ok(state) = fs::read_to_string(&max_state) { + println!(" Max Power State: {}", state.trim()); + } + } + } + } + + // NVIDIA power info via nvidia-smi + match std::process::Command::new("nvidia-smi") + .args([ + "--query-gpu=power.draw,power.limit,power.default_limit,power.max_limit,clocks.current.graphics,clocks.max.graphics", + "--format=csv,noheader", + ]) + .output() + { + Ok(output) if output.status.success() => { + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let parts: Vec<&str> = line.split(", ").collect(); + if parts.len() >= 5 { + println!(); + println!(" Power Draw: {}", parts[0].trim()); + println!(" Power Limit: {}", parts[1].trim()); + println!(" Default Limit: {}", parts[2].trim()); + println!(" Max Limit: {}", parts[3].trim()); + println!(" Current Clock: {}", parts[4].trim()); + if parts.len() > 5 { + println!(" Max Clock: {}", parts[5].trim()); + } + } + } + } + _ => { + log::debug!("nvidia-smi not available or GPU not powered on"); + println!(" (nvidia-smi not available - GPU may be powered off)"); } } } @@ -1004,5 +1097,6 @@ pub fn execute_local(cmd: &GpuCommands) { *no_reboot, ), GpuCommands::Run { command } => run_on_dgpu(command), + GpuCommands::Power => show_gpu_power_state(), } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6a3399a..24a0f67 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,8 +1,10 @@ pub mod battery; pub mod cooling; pub mod display; +pub mod fan; pub mod gpu; pub mod install_rules; pub mod power; pub mod status; +pub mod temperature; pub mod touchpad; diff --git a/src/commands/status.rs b/src/commands/status.rs index c459501..ffc3521 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -181,6 +181,42 @@ pub fn execute() { } } + // Temperature + if let Ok(temp_output) = fs::read_to_string("/sys/class/hwmon/hwmon5/temp1_input") + && let Ok(millideg) = temp_output.trim().parse::() + { + println!("CPU Temp: {:.1}°C", millideg as f64 / 1000.0); + } + + // Fan Speed + if let Ok(fan_output) = fs::read_to_string("/sys/class/hwmon/hwmon1/fan1_input") + && let Ok(rpm) = fan_output.trim().parse::() + { + println!("Fan Speed: {} RPM", rpm); + } + + // GPU Power State + if let Ok(output) = std::process::Command::new("nvidia-smi") + .args([ + "--query-gpu=power.draw,temperature.gpu", + "--format=csv,noheader", + ]) + .output() + && output.status.success() + { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Some(line) = stdout.lines().next() { + let parts: Vec<&str> = line.split(", ").collect(); + if parts.len() >= 2 { + println!( + "GPU: {} @ {}°C (NVIDIA)", + parts[0].trim(), + parts[1].trim() + ); + } + } + } + // Display Status let displays = display::get_active_display_info(); if displays.is_empty() { diff --git a/src/commands/temperature.rs b/src/commands/temperature.rs new file mode 100644 index 0000000..808ae49 --- /dev/null +++ b/src/commands/temperature.rs @@ -0,0 +1,129 @@ +use log::debug; +use std::fs; +use std::path::Path; + +pub fn execute() { + println!("--- Temperature ---"); + let mut found = false; + + // Read all hwmon temperature sensors + let hwmon_dir = Path::new("/sys/class/hwmon"); + if hwmon_dir.exists() + && let Ok(entries) = fs::read_dir(hwmon_dir) + { + for entry in entries.flatten() { + let path = entry.path(); + let name_path = path.join("name"); + let Ok(name) = fs::read_to_string(&name_path) else { + continue; + }; + let hwmon_name = name.trim().to_string(); + + let mut idx = 1; + loop { + let input_path = path.join(format!("temp{}_input", idx)); + if !input_path.exists() { + break; + } + + if let Ok(raw) = fs::read_to_string(&input_path) { + let millideg: i64 = raw.trim().parse().unwrap_or(0); + let celsius = millideg as f64 / 1000.0; + + let label = path.join(format!("temp{}_label", idx)); + let label_str = fs::read_to_string(&label) + .map(|l| l.trim().to_string()) + .unwrap_or_else(|_| format!("Sensor {}", idx)); + + let display_name = match hwmon_name.as_str() { + "k10temp" => "CPU", + "acpitz" => "ACPI Thermal Zone", + "nvme" => "NVMe SSD", + "mt7921_phy0" => "WiFi", + other => other, + }; + + println!( + " {} ({}): {:.1}°C", + display_name, label_str, celsius + ); + found = true; + } + + idx += 1; + } + } + } + + // NVIDIA GPU temperature via nvidia-smi + if let Ok(output) = std::process::Command::new("nvidia-smi") + .args([ + "--query-gpu=temperature.gpu,power.draw,clocks.current.graphics", + "--format=csv,noheader", + ]) + .output() + && output.status.success() + { + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let parts: Vec<&str> = line.split(", ").collect(); + if let Some(temp) = parts.first() + && let Ok(temp_c) = temp.trim().parse::() + { + let info = if parts.len() >= 3 { + format!( + " [{} @ {}]", + parts[1].trim(), + parts[2].trim() + ) + } else { + String::new() + }; + println!(" GPU (NVIDIA): {:.0}°C{}", temp_c, info); + found = true; + } + } + } else { + debug!("nvidia-smi not available, skipping GPU temperature"); + } + + // Read thermal zone temperatures + let thermal_dir = Path::new("/sys/class/thermal"); + if thermal_dir.exists() + && let Ok(entries) = fs::read_dir(thermal_dir) + { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if !name_str.starts_with("thermal_zone") { + continue; + } + + let type_path = entry.path().join("type"); + let temp_path = entry.path().join("temp"); + + if let (Ok(type_content), Ok(temp_content)) = + (fs::read_to_string(&type_path), fs::read_to_string(&temp_path)) + { + let zone_type = type_content.trim(); + let millideg: i64 = temp_content.trim().parse().unwrap_or(0); + let celsius = millideg as f64 / 1000.0; + + // Skip if we already showed this from hwmon + if zone_type == "x86_pkg_temp" + || zone_type == "k10temp" + || zone_type == "acpitz" + { + continue; + } + + println!(" {} ({}): {:.1}°C", name_str, zone_type, celsius); + found = true; + } + } + } + + if !found { + println!(" No temperature sensors found."); + } +}