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.
This commit is contained in:
5 files changed
+378
-17
No files matched your search
@@ -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.");
|
||||
}
|
||||
}
|
||||
+111
-17
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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::<i64>()
|
||||
{
|
||||
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::<u64>()
|
||||
{
|
||||
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() {
|
||||
|
||||
@@ -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::<f64>()
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user