From cfa9867e3d734ad8404aa90b2069524fb2c20a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Fri, 30 Jul 2021 21:34:31 +0200 Subject: [PATCH 01/10] Refactor, fix broken resolve conflicts, add verbose monitor, skip update speed if temp didn't changed. --- src/config.rs | 43 ++-- src/main.rs | 566 +++++++++++++++++++++++++++++++------------------- 2 files changed, 386 insertions(+), 223 deletions(-) diff --git a/src/config.rs b/src/config.rs index c9bec04..219ed8e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ use crate::{AmdFanError, CONFIG_PATH}; -use log::LevelFilter; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::Formatter; use std::io::ErrorKind; @@ -52,7 +51,7 @@ impl<'de> Deserialize<'de> for Card { E: de::Error, { match value.parse::() { - Ok(card) => Ok(card.0), + Ok(card) => Ok(*card), Err(AmdFanError::InvalidPrefix) => { Err(E::custom(format!("expect cardX but got {}", value))) } @@ -68,6 +67,7 @@ impl<'de> Deserialize<'de> for Card { "Failed to read vendor file for {}", value ))), + _ => unreachable!(), } } } @@ -84,6 +84,14 @@ impl Serialize for Card { } } +impl std::ops::Deref for Card { + type Target = u32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MatrixPoint { pub temp: f64, @@ -106,15 +114,15 @@ pub enum LogLevel { Trace, } -impl From for LevelFilter { - fn from(level: LogLevel) -> Self { - match level { - LogLevel::Off => LevelFilter::Off, - LogLevel::Error => LevelFilter::Error, - LogLevel::Warn => LevelFilter::Warn, - LogLevel::Info => LevelFilter::Info, - LogLevel::Debug => LevelFilter::Debug, - LogLevel::Trace => LevelFilter::Trace, +impl LogLevel { + pub fn to_str(&self) -> &str { + match self { + LogLevel::Off => "OFF", + LogLevel::Error => "ERROR", + LogLevel::Warn => "WARN", + LogLevel::Info => "INFO", + LogLevel::Debug => "DEBUG", + LogLevel::Trace => "TRACE", } } } @@ -141,7 +149,13 @@ impl Config { return self.max_speed(); } - crate::linear_map(temp, self.speed_matrix[idx].temp, self.speed_matrix[idx+1].temp, self.speed_matrix[idx].speed, self.speed_matrix[idx+1].speed) + crate::linear_map( + temp, + self.speed_matrix[idx].temp, + self.speed_matrix[idx + 1].temp, + self.speed_matrix[idx].speed, + self.speed_matrix[idx + 1].speed, + ) } pub fn log_level(&self) -> LogLevel { @@ -222,7 +236,10 @@ pub fn load_config() -> std::io::Result { return Err(std::io::Error::from(ErrorKind::InvalidData)); } if matrix_point.speed > 100f64 { - log::error!("Fan speed can't be above 100.0 found {}", matrix_point.speed); + log::error!( + "Fan speed can't be above 100.0 found {}", + matrix_point.speed + ); return Err(std::io::Error::from(ErrorKind::InvalidData)); } if let Some(last_point) = last_point { diff --git a/src/main.rs b/src/main.rs index 2aa19b9..31503f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,14 @@ mod config; extern crate log; +use std::io::Error as IoErr; use std::io::ErrorKind; use crate::config::{load_config, Card, Config}; use gumdrop::Options; +use std::fmt::Formatter; +static CONFIG_DIR: &str = "/etc/amdfand"; static CONFIG_PATH: &str = "/etc/amdfand/config.toml"; static ROOT_DIR: &str = "/sys/class/drm"; @@ -19,6 +22,24 @@ pub enum AmdFanError { InvalidSuffix(String), NotAmdCard, FailedReadVendor, + InvalidMonitorFormat, +} + +impl std::fmt::Display for AmdFanError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AmdFanError::InvalidPrefix => f.write_str("Card must starts with `card`."), + AmdFanError::InputTooShort => f.write_str( + "Card must starts with `card` and ends with number. Given name is too short.", + ), + AmdFanError::InvalidSuffix(s) => { + f.write_fmt(format_args!("Value after `card` is invalid {}", s)) + } + AmdFanError::NotAmdCard => f.write_str("Vendor is not AMD"), + AmdFanError::FailedReadVendor => f.write_str("Unable to read GPU vendor"), + AmdFanError::InvalidMonitorFormat => f.write_str("Monitor format is not valid. Available values are: short, s, long l, verbose and v"), + } + } } // linear mapping from the xrange to the yrange @@ -33,159 +54,220 @@ pub struct HwMon { name: String, pwm_min: Option, pwm_max: Option, + temp_inputs: Vec, } -impl HwMon { - pub fn new(card: &Card, name: &str) -> Self { - Self { - card: card.clone(), - name: String::from(name), - pwm_min: None, - pwm_max: None, +/// pulse width modulation fan control minimum level (0) +static PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; + +/// pulse width modulation fan control maximum level (255) +static PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max"; + +/// pulse width modulation fan level (0-255) +static PULSE_WIDTH_MODULATION: &str = "pwm1"; + +/// pulse width modulation fan control method (0: no fan speed control, 1: manual fan speed control using pwm interface, 2: automatic fan speed control) +static PULSE_WIDTH_MODULATION_ENABLED: &str = "pwm1_enable"; + +mod hw_mon { + use std::io::Error as IoErr; + + use crate::config::Card; + use crate::{ + linear_map, AmdFanError, HwMon, HW_MON_DIR, PULSE_WIDTH_MODULATION, + PULSE_WIDTH_MODULATION_ENABLED, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN, + ROOT_DIR, + }; + use std::io::ErrorKind; + + impl HwMon { + pub fn new(card: &Card, name: &str) -> Self { + Self { + card: card.clone(), + name: String::from(name), + pwm_min: None, + pwm_max: None, + temp_inputs: load_temp_inputs(&card, name), + } + } + + pub fn max_gpu_temp(&self) -> std::io::Result { + let mut results = Vec::with_capacity(self.temp_inputs.len()); + for name in self.temp_inputs.iter() { + results.push(self.read_gpu_temp(name).unwrap_or(0)); + } + results.sort(); + results + .last() + .copied() + .map(|temp| temp as f64 / 1000f64) + .ok_or_else(|| IoErr::from(ErrorKind::InvalidInput)) + } + + pub fn gpu_temp(&self) -> Vec<(String, std::io::Result)> { + self.temp_inputs + .clone() + .into_iter() + .map(|name| { + let temp = self + .read_gpu_temp(name.as_str()) + .map(|temp| temp as f64 / 1000f64); + (name, temp) + }) + .collect() + } + + fn read_gpu_temp(&self, name: &str) -> std::io::Result { + self.read(name)?.parse::().map_err(|e| { + log::warn!("Read from gpu monitor failed. Invalid temperature. {}", e); + IoErr::from(ErrorKind::InvalidInput) + }) + } + + #[inline] + pub(crate) fn name(&self) -> std::io::Result { + self.read("name") + } + + pub fn pwm_min(&mut self) -> u32 { + if self.pwm_min.is_none() { + self.pwm_min = Some(self.value_or_fallback(PULSE_WIDTH_MODULATION_MIN, 0)); + }; + self.pwm_min.unwrap_or_default() + } + + pub fn pwm_max(&mut self) -> u32 { + if self.pwm_max.is_none() { + self.pwm_max = Some(self.value_or_fallback(PULSE_WIDTH_MODULATION_MAX, 255)); + }; + self.pwm_max.unwrap_or(255) + } + + pub fn pwm(&self) -> std::io::Result { + self.read(PULSE_WIDTH_MODULATION)?.parse().map_err(|_e| { + log::warn!("Read from gpu monitor failed. Invalid pwm value"); + IoErr::from(ErrorKind::InvalidInput) + }) + } + + pub fn is_fan_manual(&self) -> bool { + self.read(PULSE_WIDTH_MODULATION_ENABLED) + .map(|s| s.as_str() == "1") + .unwrap_or_default() + } + + pub fn is_fan_automatic(&self) -> bool { + self.read(PULSE_WIDTH_MODULATION_ENABLED) + .map(|s| s.as_str() == "2") + .unwrap_or_default() + } + + #[inline] + pub fn is_amd(&self) -> bool { + std::fs::read_to_string(format!("{}/{}/device/vendor", ROOT_DIR, self.card)) + .map_err(|_| AmdFanError::FailedReadVendor) + .map(|vendor| vendor.trim() == "0x1002") + .unwrap_or_default() + } + + #[inline] + pub fn name_is_amd(&self) -> bool { + self.name().ok().filter(|s| s.trim() == "amdgpu").is_some() + } + + pub fn set_manual(&self) -> std::io::Result<()> { + self.write("pwm1_enable", 1) + } + + pub fn set_automatic(&self) -> std::io::Result<()> { + self.write("pwm1_enable", 2) + } + + pub fn set_pwm(&self, value: u32) -> std::io::Result<()> { + if self.is_fan_automatic() { + self.set_manual()?; + } + self.write("pwm1", value as u64) + } + + pub fn set_speed(&mut self, speed: f64) -> std::io::Result<()> { + let min = self.pwm_min() as f64; + let max = self.pwm_max() as f64; + let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u32; + self.set_pwm(pwm) + } + + fn read(&self, name: &str) -> std::io::Result { + std::fs::read_to_string(self.path(name)).map(|s| String::from(s.trim())) + } + + fn write(&self, name: &str, value: u64) -> std::io::Result<()> { + std::fs::write(self.path(name), format!("{}", value))?; + Ok(()) + } + + fn path(&self, name: &str) -> std::path::PathBuf { + self.mon_dir().join(name) + } + + fn mon_dir(&self) -> std::path::PathBuf { + hw_mon_dir_path(&self.card, &self.name) + } + + #[inline] + fn value_or_fallback(&self, name: &str, fallback: R) -> R { + self.read(name) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(fallback) } } - pub fn gpu_temp(&self) -> std::io::Result { - let value = self.read("temp1_input")?.parse::().map_err(|_| { - log::warn!("Read from gpu monitor failed. Invalid temperature"); - std::io::Error::from(ErrorKind::InvalidInput) - })?; - Ok(value as f64 / 1000f64) - } - - fn name(&self) -> std::io::Result { - self.read("name") - } - - pub fn pwm_min(&mut self) -> u32 { - if self.pwm_min.is_none() { - self.pwm_min = Some( - self.read("pwm1_min") - .unwrap_or_default() - .parse() - .unwrap_or(0), - ) + fn load_temp_inputs(card: &Card, name: &str) -> Vec { + let dir = match std::fs::read_dir(hw_mon_dir_path(card, name)) { + Ok(d) => d, + _ => return vec![], }; - self.pwm_min.unwrap_or_default() + dir.filter_map(|f| f.ok()) + .filter_map(|f| { + f.file_name() + .to_str() + .filter(|s| s.starts_with("temp") && s.ends_with("_input")) + .map(String::from) + }) + .collect() } - pub fn pwm_max(&mut self) -> u32 { - if self.pwm_max.is_none() { - self.pwm_max = Some( - self.read("pwm1_max") - .unwrap_or_default() - .parse() - .unwrap_or(255), - ) - }; - self.pwm_max.unwrap_or(255) - } - - pub fn pwm(&self) -> std::io::Result { - self.read("pwm1")?.parse().map_err(|_e| { - log::warn!("Read from gpu monitor failed. Invalid pwm value"); - std::io::Error::from(ErrorKind::InvalidInput) - }) - } - - pub fn pwm_speed(&self) -> std::io::Result { - self.read("pwm1")?.parse().map_err(|_e| { - log::warn!("Read from gpu monitor failed. Invalid fan speed"); - std::io::Error::from(ErrorKind::InvalidInput) - }) - } - - pub fn is_fan_manual(&self) -> bool { - self.read("pwm1_enable") - .map(|s| s.as_str() == "1") - .unwrap_or_default() - } - - pub fn is_fan_automatic(&self) -> bool { - self.read("pwm1_enable") - .map(|s| s.as_str() == "2") - .unwrap_or_default() - } - - pub fn is_amd(&self) -> bool { - std::fs::read_to_string(format!("{}/{}/device/vendor", ROOT_DIR, self.card)) - .map_err(|_| AmdFanError::FailedReadVendor) - .map(|vendor| vendor.trim() == "0x1002") - .unwrap_or_default() - } - - pub fn set_manual(&self) -> std::io::Result<()> { - self.write("pwm1_enable", 1) - } - - pub fn set_automatic(&self) -> std::io::Result<()> { - self.write("pwm1_enable", 2) - } - - pub fn set_pwm(&self, value: u32) -> std::io::Result<()> { - if self.is_fan_automatic() { - self.set_manual()?; - } - self.write("pwm1", value as u64) - } - - pub fn set_speed(&mut self, speed: f64) -> std::io::Result<()> { - let min = self.pwm_min() as f64; - let max = self.pwm_max() as f64; - let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u32; - self.set_pwm(pwm) - } - - fn read(&self, name: &str) -> std::io::Result { - let read_path = self.path(name); - std::fs::read_to_string(read_path).map(|s| s.trim().to_string()) - } - - fn write(&self, name: &str, value: u64) -> std::io::Result<()> { - std::fs::write(self.path(name), format!("{}", value))?; - Ok(()) - } - - fn path(&self, name: &str) -> std::path::PathBuf { - self.mon_dir().join(name) - } - - fn mon_dir(&self) -> std::path::PathBuf { - std::path::Path::new(ROOT_DIR) - .join(self.card.to_string()) + #[inline] + fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf { + std::path::PathBuf::new() + .join(ROOT_DIR) + .join(card.to_string()) .join(HW_MON_DIR) - .join(&self.name) - } -} - -pub struct CardController { - pub hw_mon: HwMon, - pub last_temp: f64, -} - -impl CardController { - pub fn new(card: Card) -> std::io::Result { - let name = Self::find_hw_mon(&card)?; - Ok(Self { - hw_mon: HwMon::new(&card, &name), - last_temp: 1_000f64, - }) } - fn find_hw_mon(card: &Card) -> std::io::Result { - let read_path = format!("{}/{}/{}", ROOT_DIR, card.to_string(), HW_MON_DIR); + #[inline] + fn hw_mon_dir_path(card: &Card, name: &str) -> std::path::PathBuf { + hw_mon_dirs_path(&card).join(name) + } + + pub(crate) fn open_hw_mon(card: Card) -> std::io::Result { + let read_path = hw_mon_dirs_path(&card); let entries = std::fs::read_dir(read_path)?; - entries + let name = entries .filter_map(|entry| entry.ok()) .filter_map(|entry| { entry .file_name() .as_os_str() .to_str() - .map(|s| s.to_string()) + .filter(|name| name.starts_with("hwmon")) + .map(String::from) }) - .find(|name| name.starts_with("hwmon")) - .ok_or_else(|| std::io::Error::from(ErrorKind::NotFound)) + .take(1) + .last() + .ok_or_else(|| IoErr::from(ErrorKind::NotFound))?; + Ok(HwMon::new(&card, &name)) } } @@ -194,12 +276,6 @@ pub enum FanMode { Automatic, } -#[derive(Debug, Options)] -pub struct Monitor { - #[options(help = "Help message")] - help: bool, -} - #[derive(Debug, Options)] pub struct Service { #[options(help = "Help message")] @@ -223,7 +299,7 @@ pub struct AvailableCards { #[derive(Debug, Options)] pub enum Command { #[options(help = "Print current temp and fan speed")] - Monitor(Monitor), + Monitor(monitor::Monitor), #[options(help = "Set fan speed depends on GPU temperature")] Service(Service), #[options(help = "Switch to GPU automatic fan speed control")] @@ -244,6 +320,10 @@ pub struct Opts { command: Option, } +fn invalid_data() -> IoErr { + IoErr::from(ErrorKind::InvalidData) +} + fn read_cards() -> std::io::Result> { let mut cards = vec![]; let entries = std::fs::read_dir(ROOT_DIR)?; @@ -254,14 +334,11 @@ fn read_cards() -> std::io::Result> { .file_name() .as_os_str() .to_str() - .map(|s| s.to_string()) - .ok_or_else(|| std::io::Error::from(ErrorKind::InvalidData)) + .map(String::from) + .ok_or_else(invalid_data) }) - .and_then(|file_name| { - file_name - .parse::() - .map_err(|_| std::io::Error::from(ErrorKind::InvalidData)) - }) { + .and_then(|file_name| file_name.parse::().map_err(|_| invalid_data())) + { Ok(card) => { cards.push(card); } @@ -271,47 +348,36 @@ fn read_cards() -> std::io::Result> { Ok(cards) } -fn controllers(config: &Config, filter: bool) -> std::io::Result> { +fn controllers(config: &Config, filter: bool) -> std::io::Result> { Ok(read_cards()? .into_iter() - .filter(|card| { - !filter - || config - .cards() - .iter() - .find(|name| name.0 == card.0) - .is_some() - }) - .map(|card| CardController::new(card).unwrap()) - .filter(|controller| !filter || controller.hw_mon.is_amd()) - .filter(|reader| { - !filter - || reader - .hw_mon - .name() - .ok() - .filter(|s| s.as_str() == "amdgpu") - .is_some() - }) + .filter(|card| !filter || config.cards().iter().find(|name| **name == *card).is_some()) + .filter_map(|card| hw_mon::open_hw_mon(card).ok()) + .filter(|hw_mon| !filter || { hw_mon.is_amd() }) + .filter(|hw_mon| !filter || hw_mon.name_is_amd()) .collect()) } fn service(config: Config) -> std::io::Result<()> { let mut controllers = controllers(&config, true)?; + let mut cache = std::collections::HashMap::new(); loop { - for controller in controllers.iter_mut() { - let gpu_temp = controller.hw_mon.gpu_temp().unwrap_or_default(); + for hw_mon in controllers.iter_mut() { + let gpu_temp = hw_mon.max_gpu_temp().unwrap_or_default(); + log::info!("Current {} temperature: {}", hw_mon.card, gpu_temp); + let last = *cache.entry(*hw_mon.card).or_insert(1_000f64); - let speed = config.speed_for_temp(gpu_temp); - let target_pwm = (speed as f32 * 2.55).round() as u32; - if controller.hw_mon.pwm_min() > target_pwm || controller.hw_mon.pwm_max() < target_pwm { + if ((last - 0.001f64)..(last + 0.001f64)).contains(&gpu_temp) { + log::info!("temperature didn't changed"); continue; - } + }; + let speed = config.speed_for_temp(gpu_temp); + log::info!("Resolved speed {:.2}", speed); - if let Err(e) = controller.hw_mon.set_speed(target_pwm as f64) { - log::error!("Failed to change speed to {}. {:?}", target_pwm, e); + if let Err(e) = hw_mon.set_speed(speed) { + log::error!("Failed to change speed to {}. {:?}", speed, e); } - controller.last_temp = gpu_temp; + cache.insert(*hw_mon.card, gpu_temp); } std::thread::sleep(std::time::Duration::from_secs(4)); } @@ -321,28 +387,31 @@ fn change_mode(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Re let mut controllers = controllers(&config, true)?; let cards = match switcher.card { - Some(card_id) => match controllers.iter().position(|c| c.hw_mon.card.0 == card_id) { + Some(card_id) => match controllers + .iter() + .position(|hw_mon| *hw_mon.card == card_id) + { Some(card) => vec![controllers.remove(card)], None => { eprintln!("Card does not exists. Available cards: "); - for card in controllers { - eprintln!(" * {}", card.hw_mon.card.0); + for hw_mon in controllers { + eprintln!(" * {}", *hw_mon.card); } - return Err(std::io::Error::from(ErrorKind::NotFound)); + return Err(IoErr::from(ErrorKind::NotFound)); } }, None => controllers, }; - for card in cards { + for hw_mon in cards { match mode { FanMode::Automatic => { - if let Err(e) = card.hw_mon.set_automatic() { + if let Err(e) = hw_mon.set_automatic() { log::error!("{:?}", e); } } FanMode::Manual => { - if let Err(e) = card.hw_mon.set_manual() { + if let Err(e) = hw_mon.set_manual() { log::error!("{:?}", e); } } @@ -351,37 +420,114 @@ fn change_mode(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Re Ok(()) } -fn monitor_cards(config: Config) -> std::io::Result<()> { - let mut controllers = controllers(&config, true)?; - loop { - print!("{esc}[2J{esc}[1;1H", esc = 27 as char); - for card in controllers.iter_mut() { - println!( - "Card {:3} | Temp | RPM | MIN | MAX | PWM | %", - card.hw_mon.card.to_string().replace("card", "") - ); - println!( - " | {:>5.2} | {:>4} | {:>4} | {:>4} | {:>4} | {:>3}", - card.hw_mon.gpu_temp().unwrap_or_default(), - card.hw_mon.pwm_speed().unwrap_or_default(), - card.hw_mon.pwm_min(), - card.hw_mon.pwm_max(), - card.hw_mon.pwm_speed().unwrap_or_default(), - (card.hw_mon.pwm_speed().unwrap_or_default() as f32 / 2.55).round(), - ); +mod monitor { + use crate::config::Config; + use crate::{controllers, AmdFanError}; + use std::str::FromStr; + + #[derive(Debug)] + pub enum MonitorFormat { + Short, + Verbose, + } + + impl Default for MonitorFormat { + fn default() -> Self { + MonitorFormat::Short + } + } + + impl FromStr for MonitorFormat { + type Err = AmdFanError; + + fn from_str(s: &str) -> Result { + match s { + "short" | "s" => Ok(MonitorFormat::Short), + "verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose), + _ => Err(AmdFanError::InvalidMonitorFormat), + } + } + } + + #[derive(Debug, gumdrop::Options)] + pub struct Monitor { + #[options(help = "Help message")] + help: bool, + #[options(help = "Help message")] + format: MonitorFormat, + } + + pub fn run(monitor: Monitor, config: Config) -> std::io::Result<()> { + match monitor.format { + MonitorFormat::Short => short(config), + MonitorFormat::Verbose => verbose(config), + } + } + + pub fn verbose(config: Config) -> std::io::Result<()> { + let mut controllers = controllers(&config, true)?; + loop { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + for hw_mon in controllers.iter_mut() { + println!("Card {:3}", hw_mon.card.to_string().replace("card", "")); + println!(" MIN | MAX | PWM | %"); + println!( + " {:>4} | {:>4} | {:>6} | {:>3}", + hw_mon.pwm_min(), + hw_mon.pwm_max(), + hw_mon + .pwm() + .map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), + (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), + ); + + println!(); + println!(" Current temperature"); + hw_mon.gpu_temp().into_iter().for_each(|(name, temp)| { + println!( + " {:6} | {:>9.2}", + name.replace("_input", ""), + temp.unwrap_or_default(), + ); + }); + } + println!(); + println!("> PWM may be 0 even if RPM is higher"); + std::thread::sleep(std::time::Duration::from_secs(4)); + } + } + + pub fn short(config: Config) -> std::io::Result<()> { + let mut controllers = controllers(&config, true)?; + loop { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + for hw_mon in controllers.iter_mut() { + println!( + "Card {:3} | Temp | MIN | MAX | PWM | %", + hw_mon.card.to_string().replace("card", "") + ); + println!( + " | {:>5.2} | {:>4} | {:>4} | {:>4} | {:>3}", + hw_mon.max_gpu_temp().unwrap_or_default(), + hw_mon.pwm_min(), + hw_mon.pwm_max(), + hw_mon.pwm().unwrap_or_default(), + (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), + ); + } + std::thread::sleep(std::time::Duration::from_secs(4)); } - std::thread::sleep(std::time::Duration::from_secs(4)); } } fn main() -> std::io::Result<()> { - if std::fs::read("/etc/amdfand").map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { - std::fs::create_dir_all("/etc/amdfand")?; + if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { + std::fs::create_dir_all(CONFIG_DIR)?; } let config = load_config()?; - log::set_max_level(config.log_level().into()); + std::env::set_var("RUST_LOG", config.log_level().to_str()); pretty_env_logger::init(); let opts: Opts = Opts::parse_args_default_or_exit(); @@ -393,17 +539,17 @@ fn main() -> std::io::Result<()> { match opts.command { None => service(config), - Some(Command::Monitor(_)) => monitor_cards(config), + Some(Command::Monitor(monitor)) => monitor::run(monitor, config), Some(Command::Service(_)) => service(config), Some(Command::SetAutomatic(switcher)) => change_mode(switcher, FanMode::Automatic, config), Some(Command::SetManual(switcher)) => change_mode(switcher, FanMode::Manual, config), Some(Command::Available(_)) => { println!("Available cards"); - controllers(&config, false)?.into_iter().for_each(|card| { + controllers(&config, false)?.into_iter().for_each(|hw_mon| { println!( " * {:6>} - {}", - card.hw_mon.card.to_string(), - card.hw_mon.name().unwrap_or_default() + hw_mon.card, + hw_mon.name().unwrap_or_default() ); }); Ok(()) From 73369978ca8852c1ec7d3705d282117d8d57deac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Sat, 31 Jul 2021 20:33:45 +0200 Subject: [PATCH 02/10] Update src/main.rs Co-authored-by: Richard Kuhnt --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 31503f4..e9596e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ impl std::fmt::Display for AmdFanError { match self { AmdFanError::InvalidPrefix => f.write_str("Card must starts with `card`."), AmdFanError::InputTooShort => f.write_str( - "Card must starts with `card` and ends with number. Given name is too short.", + "Card must start with `card` and ends with a number. The given name is too short.", ), AmdFanError::InvalidSuffix(s) => { f.write_fmt(format_args!("Value after `card` is invalid {}", s)) From 6eeeb3a4877e27a7071319553daa5daa317fc1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Sat, 31 Jul 2021 20:34:41 +0200 Subject: [PATCH 03/10] Update src/main.rs Co-authored-by: Richard Kuhnt --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e9596e8..442514c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -368,7 +368,7 @@ fn service(config: Config) -> std::io::Result<()> { let last = *cache.entry(*hw_mon.card).or_insert(1_000f64); if ((last - 0.001f64)..(last + 0.001f64)).contains(&gpu_temp) { - log::info!("temperature didn't changed"); + log::info!("Temperature didn't change"); continue; }; let speed = config.speed_for_temp(gpu_temp); From 8c5fa618c3bc7eb29374f9a241011e83d07038aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Sat, 31 Jul 2021 22:33:32 +0200 Subject: [PATCH 04/10] Refactor after review --- src/main.rs | 54 ++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index 442514c..48bea10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,29 +57,29 @@ pub struct HwMon { temp_inputs: Vec, } -/// pulse width modulation fan control minimum level (0) -static PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; - -/// pulse width modulation fan control maximum level (255) -static PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max"; - -/// pulse width modulation fan level (0-255) -static PULSE_WIDTH_MODULATION: &str = "pwm1"; - -/// pulse width modulation fan control method (0: no fan speed control, 1: manual fan speed control using pwm interface, 2: automatic fan speed control) -static PULSE_WIDTH_MODULATION_ENABLED: &str = "pwm1_enable"; - mod hw_mon { use std::io::Error as IoErr; use crate::config::Card; - use crate::{ - linear_map, AmdFanError, HwMon, HW_MON_DIR, PULSE_WIDTH_MODULATION, - PULSE_WIDTH_MODULATION_ENABLED, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN, - ROOT_DIR, - }; + use crate::{linear_map, AmdFanError, HwMon, HW_MON_DIR, ROOT_DIR}; use std::io::ErrorKind; + /// pulse width modulation fan control minimum level (0) + const PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; + + /// pulse width modulation fan control maximum level (255) + const PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max"; + + /// pulse width modulation fan level (0-255) + const PULSE_WIDTH_MODULATION: &str = "pwm1"; + + /// pulse width modulation fan control method (0: no fan speed control, 1: manual fan speed control using pwm interface, 2: automatic fan speed control) + const PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable"; + + // static PULSE_WIDTH_MODULATION_DISABLED: &str = "0"; + const PULSE_WIDTH_MODULATION_AUTO: &str = "2"; + const PULSE_WIDTH_MODULATION_MANUAL: &str = "1"; + impl HwMon { pub fn new(card: &Card, name: &str) -> Self { Self { @@ -131,14 +131,14 @@ mod hw_mon { pub fn pwm_min(&mut self) -> u32 { if self.pwm_min.is_none() { - self.pwm_min = Some(self.value_or_fallback(PULSE_WIDTH_MODULATION_MIN, 0)); + self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); }; self.pwm_min.unwrap_or_default() } pub fn pwm_max(&mut self) -> u32 { if self.pwm_max.is_none() { - self.pwm_max = Some(self.value_or_fallback(PULSE_WIDTH_MODULATION_MAX, 255)); + self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); }; self.pwm_max.unwrap_or(255) } @@ -151,14 +151,14 @@ mod hw_mon { } pub fn is_fan_manual(&self) -> bool { - self.read(PULSE_WIDTH_MODULATION_ENABLED) - .map(|s| s.as_str() == "1") + self.read(PULSE_WIDTH_MODULATION_MODE) + .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_MANUAL) .unwrap_or_default() } pub fn is_fan_automatic(&self) -> bool { - self.read(PULSE_WIDTH_MODULATION_ENABLED) - .map(|s| s.as_str() == "2") + self.read(PULSE_WIDTH_MODULATION_MODE) + .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_AUTO) .unwrap_or_default() } @@ -215,7 +215,7 @@ mod hw_mon { } #[inline] - fn value_or_fallback(&self, name: &str, fallback: R) -> R { + fn value_or(&self, name: &str, fallback: R) -> R { self.read(name) .ok() .and_then(|s| s.parse().ok()) @@ -364,15 +364,15 @@ fn service(config: Config) -> std::io::Result<()> { loop { for hw_mon in controllers.iter_mut() { let gpu_temp = hw_mon.max_gpu_temp().unwrap_or_default(); - log::info!("Current {} temperature: {}", hw_mon.card, gpu_temp); + log::debug!("Current {} temperature: {}", hw_mon.card, gpu_temp); let last = *cache.entry(*hw_mon.card).or_insert(1_000f64); if ((last - 0.001f64)..(last + 0.001f64)).contains(&gpu_temp) { - log::info!("Temperature didn't change"); + log::debug!("Temperature didn't change"); continue; }; let speed = config.speed_for_temp(gpu_temp); - log::info!("Resolved speed {:.2}", speed); + log::debug!("Resolved speed {:.2}", speed); if let Err(e) = hw_mon.set_speed(speed) { log::error!("Failed to change speed to {}. {:?}", speed, e); From 365c58ff377433852d0a6410f9adf2b3805bffb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Sat, 31 Jul 2021 22:40:57 +0200 Subject: [PATCH 05/10] Move errors to own file. Move monitor to own file. --- src/io_err.rs | 14 ++++++ src/main.rs | 122 ++++--------------------------------------------- src/monitor.rs | 97 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 113 deletions(-) create mode 100644 src/io_err.rs create mode 100644 src/monitor.rs diff --git a/src/io_err.rs b/src/io_err.rs new file mode 100644 index 0000000..2962c52 --- /dev/null +++ b/src/io_err.rs @@ -0,0 +1,14 @@ +use std::io::Error as IoErr; +use std::io::ErrorKind; + +pub fn invalid_data() -> IoErr { + IoErr::from(ErrorKind::InvalidData) +} + +pub fn not_found() -> IoErr { + IoErr::from(ErrorKind::NotFound) +} + +pub fn invalid_input() -> IoErr { + IoErr::from(ErrorKind::NotFound) +} diff --git a/src/main.rs b/src/main.rs index 48bea10..510b647 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ mod config; +mod io_err; +mod monitor; extern crate log; -use std::io::Error as IoErr; use std::io::ErrorKind; use crate::config::{load_config, Card, Config}; +use crate::io_err::{invalid_data, not_found}; use gumdrop::Options; use std::fmt::Formatter; @@ -58,11 +60,9 @@ pub struct HwMon { } mod hw_mon { - use std::io::Error as IoErr; - use crate::config::Card; + use crate::io_err::{invalid_input, not_found}; use crate::{linear_map, AmdFanError, HwMon, HW_MON_DIR, ROOT_DIR}; - use std::io::ErrorKind; /// pulse width modulation fan control minimum level (0) const PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; @@ -101,7 +101,7 @@ mod hw_mon { .last() .copied() .map(|temp| temp as f64 / 1000f64) - .ok_or_else(|| IoErr::from(ErrorKind::InvalidInput)) + .ok_or_else(invalid_input) } pub fn gpu_temp(&self) -> Vec<(String, std::io::Result)> { @@ -120,7 +120,7 @@ mod hw_mon { fn read_gpu_temp(&self, name: &str) -> std::io::Result { self.read(name)?.parse::().map_err(|e| { log::warn!("Read from gpu monitor failed. Invalid temperature. {}", e); - IoErr::from(ErrorKind::InvalidInput) + invalid_input() }) } @@ -146,7 +146,7 @@ mod hw_mon { pub fn pwm(&self) -> std::io::Result { self.read(PULSE_WIDTH_MODULATION)?.parse().map_err(|_e| { log::warn!("Read from gpu monitor failed. Invalid pwm value"); - IoErr::from(ErrorKind::InvalidInput) + invalid_input() }) } @@ -266,7 +266,7 @@ mod hw_mon { }) .take(1) .last() - .ok_or_else(|| IoErr::from(ErrorKind::NotFound))?; + .ok_or_else(not_found)?; Ok(HwMon::new(&card, &name)) } } @@ -320,10 +320,6 @@ pub struct Opts { command: Option, } -fn invalid_data() -> IoErr { - IoErr::from(ErrorKind::InvalidData) -} - fn read_cards() -> std::io::Result> { let mut cards = vec![]; let entries = std::fs::read_dir(ROOT_DIR)?; @@ -397,7 +393,7 @@ fn change_mode(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Re for hw_mon in controllers { eprintln!(" * {}", *hw_mon.card); } - return Err(IoErr::from(ErrorKind::NotFound)); + return Err(not_found()); } }, None => controllers, @@ -420,106 +416,6 @@ fn change_mode(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Re Ok(()) } -mod monitor { - use crate::config::Config; - use crate::{controllers, AmdFanError}; - use std::str::FromStr; - - #[derive(Debug)] - pub enum MonitorFormat { - Short, - Verbose, - } - - impl Default for MonitorFormat { - fn default() -> Self { - MonitorFormat::Short - } - } - - impl FromStr for MonitorFormat { - type Err = AmdFanError; - - fn from_str(s: &str) -> Result { - match s { - "short" | "s" => Ok(MonitorFormat::Short), - "verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose), - _ => Err(AmdFanError::InvalidMonitorFormat), - } - } - } - - #[derive(Debug, gumdrop::Options)] - pub struct Monitor { - #[options(help = "Help message")] - help: bool, - #[options(help = "Help message")] - format: MonitorFormat, - } - - pub fn run(monitor: Monitor, config: Config) -> std::io::Result<()> { - match monitor.format { - MonitorFormat::Short => short(config), - MonitorFormat::Verbose => verbose(config), - } - } - - pub fn verbose(config: Config) -> std::io::Result<()> { - let mut controllers = controllers(&config, true)?; - loop { - print!("{esc}[2J{esc}[1;1H", esc = 27 as char); - for hw_mon in controllers.iter_mut() { - println!("Card {:3}", hw_mon.card.to_string().replace("card", "")); - println!(" MIN | MAX | PWM | %"); - println!( - " {:>4} | {:>4} | {:>6} | {:>3}", - hw_mon.pwm_min(), - hw_mon.pwm_max(), - hw_mon - .pwm() - .map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), - (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), - ); - - println!(); - println!(" Current temperature"); - hw_mon.gpu_temp().into_iter().for_each(|(name, temp)| { - println!( - " {:6} | {:>9.2}", - name.replace("_input", ""), - temp.unwrap_or_default(), - ); - }); - } - println!(); - println!("> PWM may be 0 even if RPM is higher"); - std::thread::sleep(std::time::Duration::from_secs(4)); - } - } - - pub fn short(config: Config) -> std::io::Result<()> { - let mut controllers = controllers(&config, true)?; - loop { - print!("{esc}[2J{esc}[1;1H", esc = 27 as char); - for hw_mon in controllers.iter_mut() { - println!( - "Card {:3} | Temp | MIN | MAX | PWM | %", - hw_mon.card.to_string().replace("card", "") - ); - println!( - " | {:>5.2} | {:>4} | {:>4} | {:>4} | {:>3}", - hw_mon.max_gpu_temp().unwrap_or_default(), - hw_mon.pwm_min(), - hw_mon.pwm_max(), - hw_mon.pwm().unwrap_or_default(), - (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), - ); - } - std::thread::sleep(std::time::Duration::from_secs(4)); - } - } -} - fn main() -> std::io::Result<()> { if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { std::fs::create_dir_all(CONFIG_DIR)?; diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..c5a40c3 --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,97 @@ +use crate::config::Config; +use crate::{controllers, AmdFanError}; +use std::str::FromStr; + +#[derive(Debug)] +pub enum MonitorFormat { + Short, + Verbose, +} + +impl Default for MonitorFormat { + fn default() -> Self { + MonitorFormat::Short + } +} + +impl FromStr for MonitorFormat { + type Err = AmdFanError; + + fn from_str(s: &str) -> Result { + match s { + "short" | "s" => Ok(MonitorFormat::Short), + "verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose), + _ => Err(AmdFanError::InvalidMonitorFormat), + } + } +} + +#[derive(Debug, gumdrop::Options)] +pub struct Monitor { + #[options(help = "Help message")] + help: bool, + #[options(help = "Help message")] + format: MonitorFormat, +} + +pub fn run(monitor: Monitor, config: Config) -> std::io::Result<()> { + match monitor.format { + MonitorFormat::Short => short(config), + MonitorFormat::Verbose => verbose(config), + } +} + +pub fn verbose(config: Config) -> std::io::Result<()> { + let mut controllers = controllers(&config, true)?; + loop { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + for hw_mon in controllers.iter_mut() { + println!("Card {:3}", hw_mon.card.to_string().replace("card", "")); + println!(" MIN | MAX | PWM | %"); + println!( + " {:>4} | {:>4} | {:>6} | {:>3}", + hw_mon.pwm_min(), + hw_mon.pwm_max(), + hw_mon + .pwm() + .map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), + (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), + ); + + println!(); + println!(" Current temperature"); + hw_mon.gpu_temp().into_iter().for_each(|(name, temp)| { + println!( + " {:6} | {:>9.2}", + name.replace("_input", ""), + temp.unwrap_or_default(), + ); + }); + } + println!(); + println!("> PWM may be 0 even if RPM is higher"); + std::thread::sleep(std::time::Duration::from_secs(4)); + } +} + +pub fn short(config: Config) -> std::io::Result<()> { + let mut controllers = controllers(&config, true)?; + loop { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + for hw_mon in controllers.iter_mut() { + println!( + "Card {:3} | Temp | MIN | MAX | PWM | %", + hw_mon.card.to_string().replace("card", "") + ); + println!( + " | {:>5.2} | {:>4} | {:>4} | {:>4} | {:>3}", + hw_mon.max_gpu_temp().unwrap_or_default(), + hw_mon.pwm_min(), + hw_mon.pwm_max(), + hw_mon.pwm().unwrap_or_default(), + (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), + ); + } + std::thread::sleep(std::time::Duration::from_secs(4)); + } +} From c5f968fcf93639ad81a89627c537cc94bab7be81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Sat, 31 Jul 2021 22:42:58 +0200 Subject: [PATCH 06/10] Move hw_mod to own file. --- src/hw_mon.rs | 209 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 213 +------------------------------------------------- 2 files changed, 210 insertions(+), 212 deletions(-) create mode 100644 src/hw_mon.rs diff --git a/src/hw_mon.rs b/src/hw_mon.rs new file mode 100644 index 0000000..39cfab5 --- /dev/null +++ b/src/hw_mon.rs @@ -0,0 +1,209 @@ +use crate::config::Card; +use crate::io_err::{invalid_input, not_found}; +use crate::{linear_map, AmdFanError, HwMon, HW_MON_DIR, ROOT_DIR}; + +/// pulse width modulation fan control minimum level (0) +const PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; + +/// pulse width modulation fan control maximum level (255) +const PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max"; + +/// pulse width modulation fan level (0-255) +const PULSE_WIDTH_MODULATION: &str = "pwm1"; + +/// pulse width modulation fan control method (0: no fan speed control, 1: manual fan speed control using pwm interface, 2: automatic fan speed control) +const PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable"; + +// static PULSE_WIDTH_MODULATION_DISABLED: &str = "0"; +const PULSE_WIDTH_MODULATION_AUTO: &str = "2"; +const PULSE_WIDTH_MODULATION_MANUAL: &str = "1"; + +impl HwMon { + pub fn new(card: &Card, name: &str) -> Self { + Self { + card: card.clone(), + name: String::from(name), + pwm_min: None, + pwm_max: None, + temp_inputs: load_temp_inputs(&card, name), + } + } + + pub fn max_gpu_temp(&self) -> std::io::Result { + let mut results = Vec::with_capacity(self.temp_inputs.len()); + for name in self.temp_inputs.iter() { + results.push(self.read_gpu_temp(name).unwrap_or(0)); + } + results.sort(); + results + .last() + .copied() + .map(|temp| temp as f64 / 1000f64) + .ok_or_else(invalid_input) + } + + pub fn gpu_temp(&self) -> Vec<(String, std::io::Result)> { + self.temp_inputs + .clone() + .into_iter() + .map(|name| { + let temp = self + .read_gpu_temp(name.as_str()) + .map(|temp| temp as f64 / 1000f64); + (name, temp) + }) + .collect() + } + + fn read_gpu_temp(&self, name: &str) -> std::io::Result { + self.read(name)?.parse::().map_err(|e| { + log::warn!("Read from gpu monitor failed. Invalid temperature. {}", e); + invalid_input() + }) + } + + #[inline] + pub(crate) fn name(&self) -> std::io::Result { + self.read("name") + } + + pub fn pwm_min(&mut self) -> u32 { + if self.pwm_min.is_none() { + self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); + }; + self.pwm_min.unwrap_or_default() + } + + pub fn pwm_max(&mut self) -> u32 { + if self.pwm_max.is_none() { + self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); + }; + self.pwm_max.unwrap_or(255) + } + + pub fn pwm(&self) -> std::io::Result { + self.read(PULSE_WIDTH_MODULATION)?.parse().map_err(|_e| { + log::warn!("Read from gpu monitor failed. Invalid pwm value"); + invalid_input() + }) + } + + pub fn is_fan_manual(&self) -> bool { + self.read(PULSE_WIDTH_MODULATION_MODE) + .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_MANUAL) + .unwrap_or_default() + } + + pub fn is_fan_automatic(&self) -> bool { + self.read(PULSE_WIDTH_MODULATION_MODE) + .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_AUTO) + .unwrap_or_default() + } + + #[inline] + pub fn is_amd(&self) -> bool { + std::fs::read_to_string(format!("{}/{}/device/vendor", ROOT_DIR, self.card)) + .map_err(|_| AmdFanError::FailedReadVendor) + .map(|vendor| vendor.trim() == "0x1002") + .unwrap_or_default() + } + + #[inline] + pub fn name_is_amd(&self) -> bool { + self.name().ok().filter(|s| s.trim() == "amdgpu").is_some() + } + + pub fn set_manual(&self) -> std::io::Result<()> { + self.write("pwm1_enable", 1) + } + + pub fn set_automatic(&self) -> std::io::Result<()> { + self.write("pwm1_enable", 2) + } + + pub fn set_pwm(&self, value: u32) -> std::io::Result<()> { + if self.is_fan_automatic() { + self.set_manual()?; + } + self.write("pwm1", value as u64) + } + + pub fn set_speed(&mut self, speed: f64) -> std::io::Result<()> { + let min = self.pwm_min() as f64; + let max = self.pwm_max() as f64; + let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u32; + self.set_pwm(pwm) + } + + fn read(&self, name: &str) -> std::io::Result { + std::fs::read_to_string(self.path(name)).map(|s| String::from(s.trim())) + } + + fn write(&self, name: &str, value: u64) -> std::io::Result<()> { + std::fs::write(self.path(name), format!("{}", value))?; + Ok(()) + } + + fn path(&self, name: &str) -> std::path::PathBuf { + self.mon_dir().join(name) + } + + fn mon_dir(&self) -> std::path::PathBuf { + hw_mon_dir_path(&self.card, &self.name) + } + + #[inline] + fn value_or(&self, name: &str, fallback: R) -> R { + self.read(name) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(fallback) + } +} + +fn load_temp_inputs(card: &Card, name: &str) -> Vec { + let dir = match std::fs::read_dir(hw_mon_dir_path(card, name)) { + Ok(d) => d, + _ => return vec![], + }; + dir.filter_map(|f| f.ok()) + .filter_map(|f| { + f.file_name() + .to_str() + .filter(|s| s.starts_with("temp") && s.ends_with("_input")) + .map(String::from) + }) + .collect() +} + +#[inline] +fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf { + std::path::PathBuf::new() + .join(ROOT_DIR) + .join(card.to_string()) + .join(HW_MON_DIR) +} + +#[inline] +fn hw_mon_dir_path(card: &Card, name: &str) -> std::path::PathBuf { + hw_mon_dirs_path(&card).join(name) +} + +pub(crate) fn open_hw_mon(card: Card) -> std::io::Result { + let read_path = hw_mon_dirs_path(&card); + let entries = std::fs::read_dir(read_path)?; + let name = entries + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + entry + .file_name() + .as_os_str() + .to_str() + .filter(|name| name.starts_with("hwmon")) + .map(String::from) + }) + .take(1) + .last() + .ok_or_else(not_found)?; + Ok(HwMon::new(&card, &name)) +} diff --git a/src/main.rs b/src/main.rs index 510b647..8a6fcff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod hw_mon; mod io_err; mod monitor; @@ -59,218 +60,6 @@ pub struct HwMon { temp_inputs: Vec, } -mod hw_mon { - use crate::config::Card; - use crate::io_err::{invalid_input, not_found}; - use crate::{linear_map, AmdFanError, HwMon, HW_MON_DIR, ROOT_DIR}; - - /// pulse width modulation fan control minimum level (0) - const PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; - - /// pulse width modulation fan control maximum level (255) - const PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max"; - - /// pulse width modulation fan level (0-255) - const PULSE_WIDTH_MODULATION: &str = "pwm1"; - - /// pulse width modulation fan control method (0: no fan speed control, 1: manual fan speed control using pwm interface, 2: automatic fan speed control) - const PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable"; - - // static PULSE_WIDTH_MODULATION_DISABLED: &str = "0"; - const PULSE_WIDTH_MODULATION_AUTO: &str = "2"; - const PULSE_WIDTH_MODULATION_MANUAL: &str = "1"; - - impl HwMon { - pub fn new(card: &Card, name: &str) -> Self { - Self { - card: card.clone(), - name: String::from(name), - pwm_min: None, - pwm_max: None, - temp_inputs: load_temp_inputs(&card, name), - } - } - - pub fn max_gpu_temp(&self) -> std::io::Result { - let mut results = Vec::with_capacity(self.temp_inputs.len()); - for name in self.temp_inputs.iter() { - results.push(self.read_gpu_temp(name).unwrap_or(0)); - } - results.sort(); - results - .last() - .copied() - .map(|temp| temp as f64 / 1000f64) - .ok_or_else(invalid_input) - } - - pub fn gpu_temp(&self) -> Vec<(String, std::io::Result)> { - self.temp_inputs - .clone() - .into_iter() - .map(|name| { - let temp = self - .read_gpu_temp(name.as_str()) - .map(|temp| temp as f64 / 1000f64); - (name, temp) - }) - .collect() - } - - fn read_gpu_temp(&self, name: &str) -> std::io::Result { - self.read(name)?.parse::().map_err(|e| { - log::warn!("Read from gpu monitor failed. Invalid temperature. {}", e); - invalid_input() - }) - } - - #[inline] - pub(crate) fn name(&self) -> std::io::Result { - self.read("name") - } - - pub fn pwm_min(&mut self) -> u32 { - if self.pwm_min.is_none() { - self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); - }; - self.pwm_min.unwrap_or_default() - } - - pub fn pwm_max(&mut self) -> u32 { - if self.pwm_max.is_none() { - self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); - }; - self.pwm_max.unwrap_or(255) - } - - pub fn pwm(&self) -> std::io::Result { - self.read(PULSE_WIDTH_MODULATION)?.parse().map_err(|_e| { - log::warn!("Read from gpu monitor failed. Invalid pwm value"); - invalid_input() - }) - } - - pub fn is_fan_manual(&self) -> bool { - self.read(PULSE_WIDTH_MODULATION_MODE) - .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_MANUAL) - .unwrap_or_default() - } - - pub fn is_fan_automatic(&self) -> bool { - self.read(PULSE_WIDTH_MODULATION_MODE) - .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_AUTO) - .unwrap_or_default() - } - - #[inline] - pub fn is_amd(&self) -> bool { - std::fs::read_to_string(format!("{}/{}/device/vendor", ROOT_DIR, self.card)) - .map_err(|_| AmdFanError::FailedReadVendor) - .map(|vendor| vendor.trim() == "0x1002") - .unwrap_or_default() - } - - #[inline] - pub fn name_is_amd(&self) -> bool { - self.name().ok().filter(|s| s.trim() == "amdgpu").is_some() - } - - pub fn set_manual(&self) -> std::io::Result<()> { - self.write("pwm1_enable", 1) - } - - pub fn set_automatic(&self) -> std::io::Result<()> { - self.write("pwm1_enable", 2) - } - - pub fn set_pwm(&self, value: u32) -> std::io::Result<()> { - if self.is_fan_automatic() { - self.set_manual()?; - } - self.write("pwm1", value as u64) - } - - pub fn set_speed(&mut self, speed: f64) -> std::io::Result<()> { - let min = self.pwm_min() as f64; - let max = self.pwm_max() as f64; - let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u32; - self.set_pwm(pwm) - } - - fn read(&self, name: &str) -> std::io::Result { - std::fs::read_to_string(self.path(name)).map(|s| String::from(s.trim())) - } - - fn write(&self, name: &str, value: u64) -> std::io::Result<()> { - std::fs::write(self.path(name), format!("{}", value))?; - Ok(()) - } - - fn path(&self, name: &str) -> std::path::PathBuf { - self.mon_dir().join(name) - } - - fn mon_dir(&self) -> std::path::PathBuf { - hw_mon_dir_path(&self.card, &self.name) - } - - #[inline] - fn value_or(&self, name: &str, fallback: R) -> R { - self.read(name) - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(fallback) - } - } - - fn load_temp_inputs(card: &Card, name: &str) -> Vec { - let dir = match std::fs::read_dir(hw_mon_dir_path(card, name)) { - Ok(d) => d, - _ => return vec![], - }; - dir.filter_map(|f| f.ok()) - .filter_map(|f| { - f.file_name() - .to_str() - .filter(|s| s.starts_with("temp") && s.ends_with("_input")) - .map(String::from) - }) - .collect() - } - - #[inline] - fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf { - std::path::PathBuf::new() - .join(ROOT_DIR) - .join(card.to_string()) - .join(HW_MON_DIR) - } - - #[inline] - fn hw_mon_dir_path(card: &Card, name: &str) -> std::path::PathBuf { - hw_mon_dirs_path(&card).join(name) - } - - pub(crate) fn open_hw_mon(card: Card) -> std::io::Result { - let read_path = hw_mon_dirs_path(&card); - let entries = std::fs::read_dir(read_path)?; - let name = entries - .filter_map(|entry| entry.ok()) - .filter_map(|entry| { - entry - .file_name() - .as_os_str() - .to_str() - .filter(|name| name.starts_with("hwmon")) - .map(String::from) - }) - .take(1) - .last() - .ok_or_else(not_found)?; - Ok(HwMon::new(&card, &name)) - } -} - pub enum FanMode { Manual, Automatic, From 35532b1eb7a17c951b63aad75d6780dd7dc88bf9 Mon Sep 17 00:00:00 2001 From: Stefan Gehr Date: Mon, 2 Aug 2021 00:02:03 +0200 Subject: [PATCH 07/10] Simplifications and fixes for edge cases monitor.rs: Removed hardcoded pwm-range of 0 to 255 and instead correctly interpolate between pwm_min() and pwm_max() main.rs: Simpler comparison to check whether temperature changed since last time hw_mon.rs: Removed unnecessary conversions between u32 and u64 --- src/hw_mon.rs | 6 +++--- src/main.rs | 2 +- src/monitor.rs | 16 ++++++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/hw_mon.rs b/src/hw_mon.rs index 39cfab5..932c5ea 100644 --- a/src/hw_mon.rs +++ b/src/hw_mon.rs @@ -121,17 +121,17 @@ impl HwMon { self.write("pwm1_enable", 2) } - pub fn set_pwm(&self, value: u32) -> std::io::Result<()> { + pub fn set_pwm(&self, value: u64) -> std::io::Result<()> { if self.is_fan_automatic() { self.set_manual()?; } - self.write("pwm1", value as u64) + self.write("pwm1", value) } pub fn set_speed(&mut self, speed: f64) -> std::io::Result<()> { let min = self.pwm_min() as f64; let max = self.pwm_max() as f64; - let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u32; + let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u64; self.set_pwm(pwm) } diff --git a/src/main.rs b/src/main.rs index 8a6fcff..a5d2479 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,7 +152,7 @@ fn service(config: Config) -> std::io::Result<()> { log::debug!("Current {} temperature: {}", hw_mon.card, gpu_temp); let last = *cache.entry(*hw_mon.card).or_insert(1_000f64); - if ((last - 0.001f64)..(last + 0.001f64)).contains(&gpu_temp) { + if (last - gpu_temp).abs() < 0.001f64 { log::debug!("Temperature didn't change"); continue; }; diff --git a/src/monitor.rs b/src/monitor.rs index c5a40c3..24703fb 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -48,14 +48,16 @@ pub fn verbose(config: Config) -> std::io::Result<()> { for hw_mon in controllers.iter_mut() { println!("Card {:3}", hw_mon.card.to_string().replace("card", "")); println!(" MIN | MAX | PWM | %"); + let min = hw_mon.pwm_min(); + let max = hw_mon.pwm_max(); println!( " {:>4} | {:>4} | {:>6} | {:>3}", - hw_mon.pwm_min(), - hw_mon.pwm_max(), + min, + max, hw_mon .pwm() .map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), - (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), + (crate::linear_map(hw_mon.pwm().unwrap_or_default() as f64, min as f64, max as f64, 0f64, 100f64)).round(), ); println!(); @@ -83,13 +85,15 @@ pub fn short(config: Config) -> std::io::Result<()> { "Card {:3} | Temp | MIN | MAX | PWM | %", hw_mon.card.to_string().replace("card", "") ); + let min = hw_mon.pwm_min(); + let max = hw_mon.pwm_max(); println!( " | {:>5.2} | {:>4} | {:>4} | {:>4} | {:>3}", hw_mon.max_gpu_temp().unwrap_or_default(), - hw_mon.pwm_min(), - hw_mon.pwm_max(), + min, + max, hw_mon.pwm().unwrap_or_default(), - (hw_mon.pwm().unwrap_or_default() as f32 / 2.55).round(), + crate::linear_map(hw_mon.pwm().unwrap_or_default() as f64, min as f64, max as f64, 0f64, 100f64).round(), ); } std::thread::sleep(std::time::Duration::from_secs(4)); From e5cac912c1fb8acd667f8fa5de947b8da392f505 Mon Sep 17 00:00:00 2001 From: Stefan Gehr Date: Mon, 2 Aug 2021 00:05:21 +0200 Subject: [PATCH 08/10] Correct ExecStart-Path for systemd-service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also added correct dependency local-fs.target But often the hwmon paths are still not resolved when the service starts ¯\_(ツ)_/¯ --- services/amdfand.service | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/amdfand.service b/services/amdfand.service index 2899887..94edb56 100644 --- a/services/amdfand.service +++ b/services/amdfand.service @@ -1,8 +1,9 @@ [Unit] Description=amdfan controller -Requires=graphical.target +After=sysinit.target local-fs.target [Service] -ExecStart=/usr/local/bin/amdfand service -Restart=always +Restart=on-failure +RestartSec=4 +ExecStart=/usr/bin/amdfand service [Install] WantedBy=multi-user.target From 0539f0b515b8acd916c27c2013d4aa8ff1fb6e99 Mon Sep 17 00:00:00 2001 From: Stefan Gehr Date: Mon, 2 Aug 2021 00:08:04 +0200 Subject: [PATCH 09/10] Workaround for service starting before hwmon paths Let the service fail when there are no hwmon paths yet. --- src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.rs b/src/main.rs index a5d2479..6c1f2e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,6 +145,9 @@ fn controllers(config: &Config, filter: bool) -> std::io::Result> { fn service(config: Config) -> std::io::Result<()> { let mut controllers = controllers(&config, true)?; + if controllers.len() < 1 { + return Err(not_found()); + } let mut cache = std::collections::HashMap::new(); loop { for hw_mon in controllers.iter_mut() { From a527f7396e37d22bff4eb2469c0670c2e2823bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Mon, 2 Aug 2021 10:21:11 +0200 Subject: [PATCH 10/10] Replace len() < 1 with is_empty() --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 6c1f2e3..f882c0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,7 +145,7 @@ fn controllers(config: &Config, filter: bool) -> std::io::Result> { fn service(config: Config) -> std::io::Result<()> { let mut controllers = controllers(&config, true)?; - if controllers.len() < 1 { + if controllers.is_empty() { return Err(not_found()); } let mut cache = std::collections::HashMap::new();