diff --git a/Cargo.lock b/Cargo.lock index d582048..1af78fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,12 +13,39 @@ dependencies = [ [[package]] name = "amdfand" -version = "1.0.5" +version = "1.0.6" +dependencies = [ + "amdgpu", + "gumdrop", + "log", + "pretty_env_logger", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "amdgpu" +version = "1.0.6" dependencies = [ "gumdrop", "log", "pretty_env_logger", "serde", + "thiserror", + "toml", +] + +[[package]] +name = "amdvold" +version = "1.0.6" +dependencies = [ + "amdgpu", + "gumdrop", + "log", + "pretty_env_logger", + "serde", + "thiserror", "toml", ] @@ -202,6 +229,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.5.8" diff --git a/Cargo.toml b/Cargo.toml index 060dc55..0ee33a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,2 @@ -[package] -name = "amdfand" -version = "1.0.5" -edition = "2018" -description = "AMDGPU fan control service" -license = "MIT OR Apache-2.0" -keywords = ["hardware", "amdgpu"] -categories = ["hardware-support"] -repository = "https://github.com/Eraden/amdgpud" - -[dependencies] -serde = { version = "1.0.126", features = ["derive"] } -toml = { version = "0.5.8" } - -gumdrop = { version = "0.8.0" } - -log = { version = "0.4.14" } -pretty_env_logger = { version = "0.4.0" } +[workspace] +members = ["amdfand", "amdgpu", "amdvold"] diff --git a/README.md b/README.md index 7ca61c1..932ba06 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ sudo argonfand service # check amdgpu temperature and adjust speed from config f ```toml # /etc/amdfand/config.toml log_level = "Error" -cards = ["card0"] +temp_input = "temp1_input" [[speed_matrix]] temp = 4.0 diff --git a/amdfand/Cargo.toml b/amdfand/Cargo.toml new file mode 100644 index 0000000..15b1b09 --- /dev/null +++ b/amdfand/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "amdfand" +version = "1.0.6" +edition = "2018" +description = "AMDGPU fan control service" +license = "MIT OR Apache-2.0" +keywords = ["hardware", "amdgpu"] +categories = ["hardware-support"] +repository = "https://github.com/Eraden/amdgpud" + +[dependencies] +amdgpu = { path = "../amdgpu" } + +serde = { version = "1.0.126", features = ["derive"] } +toml = { version = "0.5.8" } +thiserror = "1.0.30" +gumdrop = { version = "0.8.0" } + +log = { version = "0.4.14" } +pretty_env_logger = { version = "0.4.0" } diff --git a/src/fan/change_mode.rs b/amdfand/src/change_mode.rs similarity index 76% rename from src/fan/change_mode.rs rename to amdfand/src/change_mode.rs index bfc9a0c..1281c94 100644 --- a/src/fan/change_mode.rs +++ b/amdfand/src/change_mode.rs @@ -1,12 +1,13 @@ +use crate::command::Fan; +use amdgpu::utils::hw_mons; use gumdrop::Options; use crate::config::Config; -use crate::io_err::not_found; -use crate::FanMode; +use crate::{AmdFanError, FanMode}; /// Change card fan mode to either automatic or manual -pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result<()> { - let mut hw_mons = crate::utils::hw_mons(&config, true)?; +pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> crate::Result<()> { + let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); let cards = match switcher.card { Some(card_id) => match hw_mons.iter().position(|hw_mon| **hw_mon.card() == card_id) { @@ -16,7 +17,7 @@ pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result for hw_mon in hw_mons { eprintln!(" * {}", *hw_mon.card()); } - return Err(not_found()); + return Err(AmdFanError::NoAmdCardFound); } }, None => hw_mons, @@ -25,12 +26,12 @@ pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result for hw_mon in cards { match mode { FanMode::Automatic => { - if let Err(e) = hw_mon.set_automatic() { + if let Err(e) = hw_mon.write_automatic() { log::error!("{:?}", e); } } FanMode::Manual => { - if let Err(e) = hw_mon.set_manual() { + if let Err(e) = hw_mon.write_manual() { log::error!("{:?}", e); } } diff --git a/amdfand/src/command.rs b/amdfand/src/command.rs new file mode 100644 index 0000000..23a16fc --- /dev/null +++ b/amdfand/src/command.rs @@ -0,0 +1,208 @@ +use crate::{change_mode, monitor, service, Config}; +use amdgpu::hw_mon::HwMon; +use amdgpu::utils::linear_map; +use amdgpu::TempInput; +use gumdrop::Options; + +/// 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"; + +#[derive(Debug, Options)] +pub struct AvailableCards { + #[options(help = "Help message")] + help: bool, +} + +#[derive(Debug, Options)] +pub enum FanCommand { + #[options(help = "Print current temperature and fan speed")] + Monitor(monitor::Monitor), + #[options(help = "Check AMD GPU temperature and change fan speed depends on configuration")] + Service(service::Service), + #[options(help = "Switch GPU to automatic fan speed control")] + SetAutomatic(change_mode::Switcher), + #[options(help = "Switch GPU to manual fan speed control")] + SetManual(change_mode::Switcher), + #[options(help = "Print available cards")] + Available(AvailableCards), +} + +#[derive(Debug, thiserror::Error)] +pub enum FanError { + #[error("AMD GPU fan speed is malformed. It should be number. {0:?}")] + NonIntPwm(std::num::ParseIntError), + #[error("AMD GPU temperature is malformed. It should be number. {0:?}")] + NonIntTemp(std::num::ParseIntError), + #[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")] + EmptyTempSet, + #[error("Unable to change fan speed to manual mode. {0}")] + ManualSpeedFailed(std::io::Error), + #[error("Unable to change fan speed to automatic mode. {0}")] + AutomaticSpeedFailed(std::io::Error), + #[error("Unable to change AMD GPU modulation (a.k.a. speed) to {value}. {error}")] + FailedToChangeSpeed { value: u64, error: std::io::Error }, +} + +pub struct Fan { + pub hw_mon: HwMon, + /// List of available temperature inputs for current HW MOD + pub temp_inputs: Vec, + /// Preferred temperature input + pub temp_input: Option, + /// Minimal modulation (between 0-255) + pub pwm_min: Option, + /// Maximal modulation (between 0-255) + pub pwm_max: Option, +} + +impl std::ops::Deref for Fan { + type Target = HwMon; + + fn deref(&self) -> &Self::Target { + &self.hw_mon + } +} + +impl std::ops::DerefMut for Fan { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.hw_mon + } +} + +impl Fan { + pub fn wrap(hw_mon: HwMon, config: &Config) -> Self { + Self { + temp_input: config.temp_input().copied(), + temp_inputs: load_temp_inputs(&hw_mon), + hw_mon, + pwm_min: None, + pwm_max: None, + } + } + + pub fn wrap_all(v: Vec, config: &Config) -> Vec { + v.into_iter().map(|hw| Self::wrap(hw, config)).collect() + } + + pub(crate) fn set_speed(&mut self, speed: f64) -> crate::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 u64; + self.write_pwm(pwm)?; + Ok(()) + } + + pub(crate) fn write_manual(&self) -> crate::Result<()> { + self.hw_mon_write("pwm1_enable", 1) + .map_err(FanError::ManualSpeedFailed)?; + Ok(()) + } + + pub(crate) fn write_automatic(&self) -> crate::Result<()> { + self.hw_mon_write("pwm1_enable", 2) + .map_err(FanError::AutomaticSpeedFailed)?; + Ok(()) + } + + fn write_pwm(&self, value: u64) -> crate::Result<()> { + if self.is_fan_automatic() { + self.write_manual()?; + } + self.hw_mon_write("pwm1", value) + .map_err(|error| FanError::FailedToChangeSpeed { value, error })?; + Ok(()) + } + 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) -> crate::Result { + let value = self + .hw_mon_read(PULSE_WIDTH_MODULATION)? + .parse() + .map_err(FanError::NonIntPwm)?; + Ok(value) + } + + pub fn is_fan_automatic(&self) -> bool { + self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE) + .map(|s| s.as_str() == PULSE_WIDTH_MODULATION_AUTO) + .unwrap_or_default() + } + + pub fn max_gpu_temp(&self) -> crate::Result { + if let Some(input) = self.temp_input.as_ref() { + let value = self.read_gpu_temp(&input.as_string())?; + return Ok(value as f64 / 1000f64); + } + 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_unstable(); + let value = results + .last() + .copied() + .map(|temp| temp as f64 / 1000f64) + .ok_or(FanError::EmptyTempSet)?; + Ok(value) + } + + pub fn gpu_temp(&self) -> Vec<(String, crate::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() + } + + pub(crate) fn read_gpu_temp(&self, name: &str) -> crate::Result { + let value = self + .hw_mon_read(name)? + .parse::() + .map_err(FanError::NonIntTemp)?; + Ok(value) + } +} + +fn load_temp_inputs(hw_mon: &HwMon) -> Vec { + let dir = match std::fs::read_dir(hw_mon.mon_dir()) { + 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() +} diff --git a/src/config.rs b/amdfand/src/config.rs similarity index 55% rename from src/config.rs rename to amdfand/src/config.rs index 8120db4..cf26a4e 100644 --- a/src/config.rs +++ b/amdfand/src/config.rs @@ -1,140 +1,32 @@ -use crate::{AmdFanError, CONFIG_PATH}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::Formatter; +use amdgpu::utils::linear_map; +use amdgpu::{LogLevel, TempInput}; use std::io::ErrorKind; -use std::str::FromStr; -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Card(pub u32); - -impl std::fmt::Display for Card { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("card{}", self.0)) - } -} - -impl FromStr for Card { - type Err = AmdFanError; - - fn from_str(value: &str) -> Result { - if !value.starts_with("card") { - return Err(AmdFanError::InvalidPrefix); - } - if value.len() < 5 { - return Err(AmdFanError::InputTooShort); - } - value[4..] - .parse::() - .map_err(|e| AmdFanError::InvalidSuffix(format!("{:?}", e))) - .map(Card) - } -} - -impl<'de> Deserialize<'de> for Card { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - use serde::de::{self, Visitor}; - - struct CardVisitor; - - impl<'de> Visitor<'de> for CardVisitor { - type Value = u32; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("must have format cardX") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - match value.parse::() { - Ok(card) => Ok(*card), - Err(AmdFanError::InvalidPrefix) => { - Err(E::custom(format!("expect cardX but got {}", value))) - } - Err(AmdFanError::InvalidSuffix(s)) => Err(E::custom(s)), - Err(AmdFanError::InputTooShort) => Err(E::custom(format!( - "{:?} must have at least 5 characters", - value - ))), - Err(AmdFanError::NotAmdCard) => { - Err(E::custom(format!("{} is not an AMD GPU", value))) - } - Err(AmdFanError::FailedReadVendor) => Err(E::custom(format!( - "Failed to read vendor file for {}", - value - ))), - _ => unreachable!(), - } - } - } - deserializer.deserialize_str(CardVisitor).map(Card) - } -} - -impl Serialize for Card { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::ops::Deref for Card { - type Target = u32; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct MatrixPoint { pub temp: f64, pub speed: f64, } -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] -pub enum LogLevel { - /// A level lower than all log levels. - Off, - /// Corresponds to the `Error` log level. - Error, - /// Corresponds to the `Warn` log level. - Warn, - /// Corresponds to the `Info` log level. - Info, - /// Corresponds to the `Debug` log level. - Debug, - /// Corresponds to the `Trace` log level. - Trace, -} - -impl LogLevel { - pub fn as_str(&self) -> &str { - match self { - LogLevel::Off => "OFF", - LogLevel::Error => "ERROR", - LogLevel::Warn => "WARN", - LogLevel::Info => "INFO", - LogLevel::Debug => "DEBUG", - LogLevel::Trace => "TRACE", - } - } -} - -#[derive(Serialize, Deserialize, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Debug)] pub struct Config { + cards: Option>, log_level: LogLevel, speed_matrix: Vec, - temp_input: Option, + /// One of temperature inputs /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input + /// If nothing is provided higher reading will be taken (this is not good!) + temp_input: Option, } impl Config { + #[deprecated( + since = "1.0.6", + note = "Multi-card used is halted until we will have PC with multiple AMD GPU" + )] + pub fn cards(&self) -> Option<&Vec> { + self.cards.as_ref() + } + pub fn speed_for_temp(&self, temp: f64) -> f64 { let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) { Some(idx) => idx, @@ -145,7 +37,7 @@ impl Config { return self.max_speed(); } - crate::utils::linear_map( + linear_map( temp, self.speed_matrix[idx].temp, self.speed_matrix[idx + 1].temp, @@ -158,8 +50,8 @@ impl Config { self.log_level } - pub fn temp_input(&self) -> Option<&str> { - self.temp_input.as_deref() + pub fn temp_input(&self) -> Option<&TempInput> { + self.temp_input.as_ref() } fn min_speed(&self) -> f64 { @@ -174,6 +66,8 @@ impl Config { impl Default for Config { fn default() -> Self { Self { + #[allow(deprecated)] + cards: None, log_level: LogLevel::Error, speed_matrix: vec![ MatrixPoint { @@ -209,17 +103,41 @@ impl Default for Config { speed: 100f64, }, ], - temp_input: Some(String::from("temp1_input")), + temp_input: Some(TempInput(1)), } } } -pub fn load_config() -> std::io::Result { - let config = match std::fs::read_to_string(CONFIG_PATH) { +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ConfigError { + #[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")] + FanSpeedTooLow { value: f64, index: usize }, + #[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")] + FanSpeedTooHigh { value: f64, index: usize }, + #[error( + "Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted" + )] + UnsortedFanSpeed { + current: f64, + index: usize, + last: f64, + }, + #[error( + "Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted" + )] + UnsortedFanTemp { + current: f64, + index: usize, + last: f64, + }, +} + +pub fn load_config(config_path: &str) -> crate::Result { + let config = match std::fs::read_to_string(config_path) { Ok(s) => toml::from_str(&s).unwrap(), Err(e) if e.kind() == ErrorKind::NotFound => { let config = Config::default(); - std::fs::write(CONFIG_PATH, toml::to_string(&config).unwrap())?; + std::fs::write(config_path, toml::to_string(&config).unwrap())?; config } Err(e) => { @@ -230,17 +148,25 @@ pub fn load_config() -> std::io::Result { let mut last_point: Option<&MatrixPoint> = None; - for matrix_point in config.speed_matrix.iter() { + for (index, matrix_point) in config.speed_matrix.iter().enumerate() { if matrix_point.speed < 0f64 { log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed); - return Err(std::io::Error::from(ErrorKind::InvalidData)); + return Err(ConfigError::FanSpeedTooLow { + value: matrix_point.speed, + index, + } + .into()); } if matrix_point.speed > 100f64 { log::error!( "Fan speed can't be above 100.0 found {}", matrix_point.speed ); - return Err(std::io::Error::from(ErrorKind::InvalidData)); + return Err(ConfigError::FanSpeedTooHigh { + value: matrix_point.speed, + index, + } + .into()); } if let Some(last_point) = last_point { if matrix_point.speed < last_point.speed { @@ -249,7 +175,13 @@ pub fn load_config() -> std::io::Result { last_point.speed, matrix_point.speed ); - return Err(std::io::Error::from(ErrorKind::InvalidData)); + + return Err(ConfigError::UnsortedFanSpeed { + current: matrix_point.speed, + last: last_point.speed, + index, + } + .into()); } if matrix_point.temp < last_point.temp { log::error!( @@ -257,7 +189,13 @@ pub fn load_config() -> std::io::Result { last_point.temp, matrix_point.temp ); - return Err(std::io::Error::from(ErrorKind::InvalidData)); + + return Err(ConfigError::UnsortedFanTemp { + current: matrix_point.temp, + last: last_point.temp, + index, + } + .into()); } } @@ -269,7 +207,8 @@ pub fn load_config() -> std::io::Result { #[cfg(test)] mod parse_config { - use super::*; + use crate::config::TempInput; + use amdgpu::{AmdGpuError, Card}; use serde::Deserialize; #[derive(Deserialize, PartialEq, Debug)] @@ -291,6 +230,35 @@ mod parse_config { fn toml_card0() { assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) })) } + + #[test] + fn parse_invalid_temp_input() { + assert_eq!( + "".parse::(), + Err(AmdGpuError::InvalidTempInput("".to_string())) + ); + assert_eq!( + "12".parse::(), + Err(AmdGpuError::InvalidTempInput("12".to_string())) + ); + assert_eq!( + "temp12".parse::(), + Err(AmdGpuError::InvalidTempInput("temp12".to_string())) + ); + assert_eq!( + "12_input".parse::(), + Err(AmdGpuError::InvalidTempInput("12_input".to_string())) + ); + assert_eq!( + "temp_12_input".parse::(), + Err(AmdGpuError::InvalidTempInput("temp_12_input".to_string())) + ); + } + + #[test] + fn parse_valid_temp_input() { + assert_eq!("temp12_input".parse::(), Ok(TempInput(12))); + } } #[cfg(test)] diff --git a/amdfand/src/error.rs b/amdfand/src/error.rs new file mode 100644 index 0000000..5315cef --- /dev/null +++ b/amdfand/src/error.rs @@ -0,0 +1,23 @@ +use crate::command::FanError; +use crate::config::ConfigError; +use amdgpu::AmdGpuError; + +#[derive(Debug, thiserror::Error)] +pub enum AmdFanError { + #[error("Vendor is not AMD")] + NotAmdCard, + #[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")] + InvalidMonitorFormat, + #[error("No hwmod has been found in sysfs")] + NoHwMonFound, + #[error("No AMD Card has been found in sysfs")] + NoAmdCardFound, + #[error("{0}")] + AmdGpu(#[from] AmdGpuError), + #[error("{0}")] + Fan(#[from] FanError), + #[error("{0}")] + Config(#[from] ConfigError), + #[error("{0:}")] + Io(#[from] std::io::Error), +} diff --git a/amdfand/src/main.rs b/amdfand/src/main.rs new file mode 100644 index 0000000..b5f4098 --- /dev/null +++ b/amdfand/src/main.rs @@ -0,0 +1,109 @@ +use crate::command::FanCommand; +use crate::error::AmdFanError; +use amdgpu::utils::hw_mons; +use amdgpu::CONFIG_DIR; +use config::{load_config, Config}; +use gumdrop::Options; +use std::io::ErrorKind; + +mod change_mode; +mod command; +mod config; +mod error; +mod monitor; +mod panic_handler; +mod service; + +extern crate log; + +pub static DEFAULT_CONFIG_PATH: &str = "/etc/amdfand/config.toml"; + +pub type Result = std::result::Result; + +pub enum FanMode { + Manual, + Automatic, +} + +#[derive(Options)] +pub struct Opts { + #[options(help = "Help message")] + help: bool, + #[options(help = "Print version")] + version: bool, + #[options(help = "Config location")] + config: Option, + #[options(command)] + command: Option, +} + +fn run(config: Config) -> Result<()> { + let opts: Opts = Opts::parse_args_default_or_exit(); + + if opts.version { + println!("amdfand {}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } + #[allow(deprecated)] + if config.cards().is_some() { + log::warn!("cards config field is no longer supported"); + } + + match opts.command { + None => service::run(config), + Some(FanCommand::Monitor(monitor)) => monitor::run(monitor, config), + Some(FanCommand::Service(_)) => service::run(config), + Some(FanCommand::SetAutomatic(switcher)) => { + change_mode::run(switcher, FanMode::Automatic, config) + } + Some(FanCommand::SetManual(switcher)) => { + change_mode::run(switcher, FanMode::Manual, config) + } + Some(FanCommand::Available(_)) => { + println!("Available cards"); + hw_mons(false)?.into_iter().for_each(|hw_mon| { + println!( + " * {:6>} - {}", + hw_mon.card(), + hw_mon.name().unwrap_or_default() + ); + }); + Ok(()) + } + } +} + +fn setup() -> Result<(String, Config)> { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "DEBUG"); + } + pretty_env_logger::init(); + if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { + std::fs::create_dir_all(CONFIG_DIR)?; + } + + let config_path = Opts::parse_args_default_or_exit() + .config + .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); + let config = load_config(&config_path)?; + log::set_max_level(config.log_level().as_str().parse().unwrap()); + Ok((config_path, config)) +} + +fn main() -> Result<()> { + let (_config_path, config) = match setup() { + Ok(config) => config, + Err(e) => { + log::error!("{}", e); + std::process::exit(1); + } + }; + match run(config) { + Ok(()) => Ok(()), + Err(e) => { + panic_handler::restore_automatic(); + log::error!("{}", e); + std::process::exit(1); + } + } +} diff --git a/src/fan/monitor.rs b/amdfand/src/monitor.rs similarity index 85% rename from src/fan/monitor.rs rename to amdfand/src/monitor.rs index c976152..df438c7 100644 --- a/src/fan/monitor.rs +++ b/amdfand/src/monitor.rs @@ -1,8 +1,9 @@ +use crate::command::Fan; +use crate::AmdFanError; +use amdgpu::utils::{hw_mons, linear_map}; use std::str::FromStr; use crate::config::Config; -use crate::utils::hw_mons; -use crate::AmdFanError; #[derive(Debug)] pub enum MonitorFormat { @@ -37,18 +38,19 @@ pub struct Monitor { } /// Start print cards temperature and fan speed -pub fn run(monitor: Monitor, config: Config) -> std::io::Result<()> { +pub fn run(monitor: Monitor, config: Config) -> crate::Result<()> { match monitor.format { MonitorFormat::Short => short(config), MonitorFormat::Verbose => verbose(config), } } -pub fn verbose(config: Config) -> std::io::Result<()> { - let mut controllers = hw_mons(&config, true)?; +pub fn verbose(config: Config) -> crate::Result<()> { + let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); + loop { print!("{esc}[2J{esc}[1;1H", esc = 27 as char); - for hw_mon in controllers.iter_mut() { + for hw_mon in hw_mons.iter_mut() { println!("Card {:3}", hw_mon.card().to_string().replace("card", "")); println!(" MIN | MAX | PWM | %"); let min = hw_mon.pwm_min(); @@ -60,7 +62,7 @@ pub fn verbose(config: Config) -> std::io::Result<()> { hw_mon .pwm() .map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), - (crate::utils::linear_map( + (linear_map( hw_mon.pwm().unwrap_or_default() as f64, min as f64, max as f64, @@ -86,11 +88,11 @@ pub fn verbose(config: Config) -> std::io::Result<()> { } } -pub fn short(config: Config) -> std::io::Result<()> { - let mut controllers = hw_mons(&config, true)?; +pub fn short(config: Config) -> crate::Result<()> { + let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); loop { print!("{esc}[2J{esc}[1;1H", esc = 27 as char); - for hw_mon in controllers.iter_mut() { + for hw_mon in hw_mons.iter_mut() { println!( "Card {:3} | Temp | MIN | MAX | PWM | %", hw_mon.card().to_string().replace("card", "") @@ -103,7 +105,7 @@ pub fn short(config: Config) -> std::io::Result<()> { min, max, hw_mon.pwm().unwrap_or_default(), - crate::utils::linear_map( + linear_map( hw_mon.pwm().unwrap_or_default() as f64, min as f64, max as f64, diff --git a/amdfand/src/panic_handler.rs b/amdfand/src/panic_handler.rs new file mode 100644 index 0000000..ee900d0 --- /dev/null +++ b/amdfand/src/panic_handler.rs @@ -0,0 +1,18 @@ +use crate::command::Fan; +use amdgpu::utils::hw_mons; + +pub fn restore_automatic() { + for hw in hw_mons(true).unwrap_or_default() { + if let Err(error) = (Fan { + hw_mon: hw, + temp_inputs: vec![], + temp_input: None, + pwm_min: None, + pwm_max: None, + }) + .write_automatic() + { + log::error!("{}", error); + } + } +} diff --git a/src/fan/service.rs b/amdfand/src/service.rs similarity index 59% rename from src/fan/service.rs rename to amdfand/src/service.rs index 9664aa5..3eb23d8 100644 --- a/src/fan/service.rs +++ b/amdfand/src/service.rs @@ -1,18 +1,31 @@ +use crate::command::Fan; +use crate::AmdFanError; +use amdgpu::utils::hw_mons; use gumdrop::Options; use crate::config::Config; -use crate::io_err::not_found; /// Start service which will change fan speed according to config and GPU temperature -pub fn run(config: Config) -> std::io::Result<()> { - let mut controllers = crate::utils::hw_mons(&config, true)?; - if controllers.is_empty() { - return Err(not_found()); +pub fn run(config: Config) -> crate::Result<()> { + let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); + + if hw_mons.is_empty() { + return Err(AmdFanError::NoHwMonFound); } let mut cache = std::collections::HashMap::new(); loop { - for hw_mon in controllers.iter_mut() { - let gpu_temp = hw_mon.max_gpu_temp().unwrap_or_default(); + for hw_mon in hw_mons.iter_mut() { + let gpu_temp = config + .temp_input() + .and_then(|input| { + hw_mon + .read_gpu_temp(&input.as_string()) + .map(|temp| temp as f64 / 1000f64) + .ok() + }) + .or_else(|| hw_mon.max_gpu_temp().ok()) + .unwrap_or_default(); + log::debug!("Current {} temperature: {}", hw_mon.card(), gpu_temp); let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64); diff --git a/amdgpu/Cargo.toml b/amdgpu/Cargo.toml new file mode 100644 index 0000000..855d148 --- /dev/null +++ b/amdgpu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "amdgpu" +version = "1.0.6" +edition = "2018" +description = "AMDGPU fan control service" +license = "MIT OR Apache-2.0" +keywords = ["hardware", "amdgpu"] +categories = ["hardware-support"] +repository = "https://github.com/Eraden/amdgpud" + +[dependencies] +serde = { version = "1.0.126", features = ["derive"] } +toml = { version = "0.5.8" } +thiserror = "1.0.30" +gumdrop = { version = "0.8.0" } + +log = { version = "0.4.14" } +pretty_env_logger = { version = "0.4.0" } diff --git a/amdgpu/src/card.rs b/amdgpu/src/card.rs new file mode 100644 index 0000000..e6007f5 --- /dev/null +++ b/amdgpu/src/card.rs @@ -0,0 +1,83 @@ +use crate::AmdGpuError; +use serde::Deserialize; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Card(pub u32); + +impl std::fmt::Display for Card { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("card{}", self.0)) + } +} + +impl std::str::FromStr for Card { + type Err = AmdGpuError; + + fn from_str(value: &str) -> Result { + if !value.starts_with("card") { + return Err(AmdGpuError::CardInvalidPrefix); + } + if value.len() < 5 { + return Err(AmdGpuError::CardInputTooShort); + } + value[4..] + .parse::() + .map_err(|e| AmdGpuError::CardInvalidSuffix(format!("{:?}", e))) + .map(Card) + } +} + +impl<'de> Deserialize<'de> for Card { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, Visitor}; + + struct CardVisitor; + + impl<'de> Visitor<'de> for CardVisitor { + type Value = u32; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("must have format cardX") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value.parse::() { + Ok(card) => Ok(*card), + Err(AmdGpuError::CardInvalidPrefix) => { + Err(E::custom(format!("expect cardX but got {}", value))) + } + Err(AmdGpuError::CardInvalidSuffix(s)) => Err(E::custom(s)), + Err(AmdGpuError::CardInputTooShort) => Err(E::custom(format!( + "{:?} must have at least 5 characters", + value + ))), + _ => unreachable!(), + } + } + } + deserializer.deserialize_str(CardVisitor).map(Card) + } +} + +impl serde::Serialize for Card { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl std::ops::Deref for Card { + type Target = u32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/amdgpu/src/error.rs b/amdgpu/src/error.rs new file mode 100644 index 0000000..b985f41 --- /dev/null +++ b/amdgpu/src/error.rs @@ -0,0 +1,33 @@ +#[derive(Debug, thiserror::Error)] +pub enum AmdGpuError { + #[error("Card must starts with `card`.")] + CardInvalidPrefix, + #[error("Card must start with `card` and ends with a number. The given name is too short.")] + CardInputTooShort, + #[error("Value after `card` is invalid {0:}")] + CardInvalidSuffix(String), + #[error("Invalid temperature input")] + InvalidTempInput(String), + #[error("Unable to read GPU vendor")] + FailedReadVendor, + #[error("{0:?}")] + Io(#[from] std::io::Error), + #[error("Card does not have hwmon")] + NoAmdHwMon, +} + +impl PartialEq for AmdGpuError { + fn eq(&self, other: &Self) -> bool { + use AmdGpuError::*; + match (self, other) { + (CardInvalidPrefix, CardInvalidPrefix) => true, + (CardInputTooShort, CardInputTooShort) => true, + (CardInvalidSuffix(a), CardInvalidSuffix(b)) => a == b, + (InvalidTempInput(a), InvalidTempInput(b)) => a == b, + (FailedReadVendor, FailedReadVendor) => true, + (NoAmdHwMon, NoAmdHwMon) => true, + (Io(a), Io(b)) => a.kind() == b.kind(), + _ => false, + } + } +} diff --git a/amdgpu/src/hw_mon.rs b/amdgpu/src/hw_mon.rs new file mode 100644 index 0000000..b1fb5a7 --- /dev/null +++ b/amdgpu/src/hw_mon.rs @@ -0,0 +1,119 @@ +use crate::{AmdGpuError, Card, ROOT_DIR}; + +#[derive(Debug)] +pub struct HwMonName(pub String); + +impl std::ops::Deref for HwMonName { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub struct HwMon { + /// HW MOD cord (ex. card0) + pub card: Card, + /// MW MOD name (ex. hwmod0) + pub name: HwMonName, +} + +impl HwMon { + pub fn new(card: &Card, name: HwMonName) -> Self { + Self { card: *card, name } + } + + #[inline] + pub fn card(&self) -> &Card { + &self.card + } + + #[inline] + pub fn name(&self) -> std::io::Result { + self.hw_mon_read("name") + } + + #[inline] + pub fn is_amd(&self) -> bool { + self.device_read("vendor") + .map_err(|_| AmdGpuError::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() + } + + fn mon_file_path(&self, name: &str) -> std::path::PathBuf { + self.mon_dir().join(name) + } + + pub fn device_dir(&self) -> std::path::PathBuf { + std::path::PathBuf::new() + .join(ROOT_DIR) + .join(self.card.to_string()) + .join("device") + } + + pub fn mon_dir(&self) -> std::path::PathBuf { + self.device_dir().join(&*self.name) + } + + #[inline] + pub fn value_or(&self, name: &str, fallback: R) -> R { + self.hw_mon_read(name) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(fallback) + } + + pub fn hw_mon_read(&self, name: &str) -> std::io::Result { + std::fs::read_to_string(self.mon_file_path(name)).map(|s| String::from(s.trim())) + } + + pub fn device_read(&self, name: &str) -> std::io::Result { + std::fs::read_to_string(self.device_dir().join(name)).map(|s| String::from(s.trim())) + } + + pub fn hw_mon_write(&self, name: &str, value: u64) -> std::io::Result<()> { + std::fs::write(self.mon_file_path(name), format!("{}", value))?; + Ok(()) + } + + pub fn device_write>(&self, name: &str, value: C) -> std::io::Result<()> { + std::fs::write(self.device_dir().join(name), value)?; + Ok(()) + } +} + +#[inline] +fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf { + std::path::PathBuf::new() + .join(ROOT_DIR) + .join(card.to_string()) + .join("device") + .join("hwmon") +} + +pub fn open_hw_mon(card: Card) -> crate::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) + .map(HwMonName) + }) + .take(1) + .last() + .ok_or(AmdGpuError::NoAmdHwMon)?; + Ok(HwMon::new(&card, name)) +} diff --git a/amdgpu/src/lib.rs b/amdgpu/src/lib.rs new file mode 100644 index 0000000..6a02228 --- /dev/null +++ b/amdgpu/src/lib.rs @@ -0,0 +1,46 @@ +mod card; +mod error; +pub mod hw_mon; +mod temp_input; +pub mod utils; + +pub use card::*; +pub use error::*; +use serde::{Deserialize, Serialize}; +pub use temp_input::*; + +pub static CONFIG_DIR: &str = "/etc/amdfand"; + +pub static ROOT_DIR: &str = "/sys/class/drm"; +pub static HW_MON_DIR: &str = "hwmon"; + +pub type Result = std::result::Result; + +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub enum LogLevel { + /// A level lower than all log levels. + Off, + /// Corresponds to the `Error` log level. + Error, + /// Corresponds to the `Warn` log level. + Warn, + /// Corresponds to the `Info` log level. + Info, + /// Corresponds to the `Debug` log level. + Debug, + /// Corresponds to the `Trace` log level. + Trace, +} + +impl LogLevel { + pub fn as_str(&self) -> &str { + match self { + LogLevel::Off => "OFF", + LogLevel::Error => "ERROR", + LogLevel::Warn => "WARN", + LogLevel::Info => "INFO", + LogLevel::Debug => "DEBUG", + LogLevel::Trace => "TRACE", + } + } +} diff --git a/amdgpu/src/temp_input.rs b/amdgpu/src/temp_input.rs new file mode 100644 index 0000000..5e395d3 --- /dev/null +++ b/amdgpu/src/temp_input.rs @@ -0,0 +1,68 @@ +use crate::AmdGpuError; + +#[derive(PartialEq, Debug, Copy, Clone, serde::Serialize)] +pub struct TempInput(pub u16); + +impl TempInput { + pub fn as_string(&self) -> String { + format!("temp{}_input", self.0) + } +} + +impl std::str::FromStr for TempInput { + type Err = AmdGpuError; + + fn from_str(s: &str) -> Result { + if s.starts_with("temp") && s.ends_with("_input") { + let mut buffer = String::with_capacity(4); + for c in s[4..].chars() { + if c.is_numeric() { + buffer.push(c); + } else if buffer.is_empty() { + return Err(AmdGpuError::InvalidTempInput(s.to_string())); + } + } + buffer + .parse() + .map_err(|e| { + log::error!("Temp input error {:?}", e); + AmdGpuError::InvalidTempInput(s.to_string()) + }) + .map(Self) + } else { + Err(AmdGpuError::InvalidTempInput(s.to_string())) + } + } +} + +impl<'de> serde::Deserialize<'de> for TempInput { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, Visitor}; + + struct TempInputVisitor; + + impl<'de> Visitor<'de> for TempInputVisitor { + type Value = u16; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("must have format cardX") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value.parse::() { + Ok(temp) => Ok(temp.0), + _ => unreachable!(), + } + } + } + deserializer + .deserialize_str(TempInputVisitor) + .map(|v| TempInput(v as u16)) + } +} diff --git a/src/utils.rs b/amdgpu/src/utils.rs similarity index 80% rename from src/utils.rs rename to amdgpu/src/utils.rs index f87efed..03785ba 100644 --- a/src/utils.rs +++ b/amdgpu/src/utils.rs @@ -1,6 +1,5 @@ -use crate::config::{Card, Config}; use crate::hw_mon::HwMon; -use crate::{hw_mon, ROOT_DIR}; +use crate::{hw_mon, Card, ROOT_DIR}; /// linear mapping from the xrange to the yrange pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 { @@ -20,10 +19,10 @@ pub fn read_cards() -> std::io::Result> { /// Wrap cards in HW Mon manipulator and /// filter cards so only amd and listed in config cards are accessible -pub fn hw_mons(config: &Config, filter: bool) -> std::io::Result> { +pub fn hw_mons(filter: bool) -> std::io::Result> { Ok(read_cards()? .into_iter() - .filter_map(|card| hw_mon::open_hw_mon(card, config).ok()) + .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()) diff --git a/amdvold/Cargo.toml b/amdvold/Cargo.toml new file mode 100644 index 0000000..532ee40 --- /dev/null +++ b/amdvold/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "amdvold" +version = "1.0.6" +edition = "2018" +description = "AMDGPU fan control service" +license = "MIT OR Apache-2.0" +keywords = ["hardware", "amdgpu"] +categories = ["hardware-support"] +repository = "https://github.com/Eraden/amdgpud" + +[dependencies] +amdgpu = { path = "../amdgpu" } + +serde = { version = "1.0.126", features = ["derive"] } +toml = { version = "0.5.8" } +thiserror = "1.0.30" +gumdrop = { version = "0.8.0" } + +log = { version = "0.4.14" } +pretty_env_logger = { version = "0.4.0" } diff --git a/amdvold/assets/enable_voltage_info.txt b/amdvold/assets/enable_voltage_info.txt new file mode 100644 index 0000000..7bad773 --- /dev/null +++ b/amdvold/assets/enable_voltage_info.txt @@ -0,0 +1,13 @@ +To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following: + +* In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example: + + GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1" + + Easiest way is to modify "/etc/default/grub" and generate new grub config. + +* If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example: + + options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff + + (only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff") \ No newline at end of file diff --git a/amdvold/src/apply_changes.rs b/amdvold/src/apply_changes.rs new file mode 100644 index 0000000..175357a --- /dev/null +++ b/amdvold/src/apply_changes.rs @@ -0,0 +1,18 @@ +use crate::command::VoltageManipulator; +use crate::{Config, VoltageError}; +use amdgpu::utils::hw_mons; + +#[derive(Debug, gumdrop::Options)] +pub struct ApplyChanges { + help: bool, +} + +pub fn run(_command: ApplyChanges, config: &Config) -> crate::Result<()> { + let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config); + if mons.is_empty() { + return Err(VoltageError::NoAmdGpu); + } + let mon = mons.remove(0); + mon.write_apply()?; + Ok(()) +} diff --git a/amdvold/src/change_state.rs b/amdvold/src/change_state.rs new file mode 100644 index 0000000..19cea0c --- /dev/null +++ b/amdvold/src/change_state.rs @@ -0,0 +1,56 @@ +use crate::clock_state::{Frequency, Voltage}; +use crate::command::{HardwareModule, VoltageManipulator}; +use crate::{Config, VoltageError}; +use amdgpu::utils::hw_mons; + +#[derive(Debug, thiserror::Error)] +pub enum ChangeStateError { + #[error("No profile index was given")] + Index, + #[error("No frequency was given")] + Freq, + #[error("No voltage was given")] + Voltage, + #[error("No AMD GPU module was given (either memory or engine)")] + Module, +} + +#[derive(Debug, gumdrop::Options)] +pub struct ChangeState { + #[options(help = "Profile number", free)] + index: u16, + #[options(help = "Either memory or engine", free)] + module: Option, + #[options(help = "New GPU module frequency", free)] + frequency: Option, + #[options(help = "New GPU module voltage", free)] + voltage: Option, + #[options(help = "Apply changes immediately after change")] + apply_immediately: bool, +} + +pub fn run(command: ChangeState, config: &Config) -> crate::Result<()> { + let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config); + if mons.is_empty() { + return Err(VoltageError::NoAmdGpu); + } + let mon = mons.remove(0); + let ChangeState { + index, + module, + frequency, + voltage, + apply_immediately, + } = command; + mon.write_state( + index, + frequency.ok_or(ChangeStateError::Freq)?, + voltage.ok_or(ChangeStateError::Voltage)?, + module.ok_or(ChangeStateError::Module)?, + )?; + if apply_immediately { + mon.write_apply()?; + } + + Ok(()) +} diff --git a/amdvold/src/clock_state.rs b/amdvold/src/clock_state.rs new file mode 100644 index 0000000..b006d92 --- /dev/null +++ b/amdvold/src/clock_state.rs @@ -0,0 +1,432 @@ +use std::iter::Peekable; +use std::str::Chars; + +const ENGINE_CLOCK_LABEL: &str = "OD_SCLK:"; +const MEMORY_CLOCK_LABEL: &str = "OD_MCLK:"; +const CURVE_POINTS_LABEL: &str = "OD_VDDC_CURVE:"; + +#[derive(Debug, PartialEq)] +pub struct Frequency { + pub value: u32, + pub unit: String, +} + +impl ToString for Frequency { + fn to_string(&self) -> String { + format!("{}{}", self.value, self.unit) + } +} + +impl std::str::FromStr for Frequency { + type Err = ClockStateError; + + fn from_str(s: &str) -> Result { + let mut buffer = String::with_capacity(8); + let mut value = None; + for c in s.trim().chars() { + if c.is_numeric() && value.is_none() { + buffer.push(c); + } else if c.is_numeric() { + return Err(ClockStateError::NotFrequency(s.to_string())); + } else if value.is_none() { + if buffer.is_empty() { + return Err(ClockStateError::NotFrequency(s.to_string())); + } + value = Some(buffer.parse()?); + buffer.clear(); + buffer.push(c); + } else { + buffer.push(c); + } + } + let value = value.ok_or_else(|| ClockStateError::NotFrequency(s.to_string()))?; + if !buffer.ends_with("hz") && !buffer.ends_with("Hz") { + return Err(ClockStateError::NotFrequency(s.to_string())); + } + Ok(Self { + value, + unit: buffer, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct Voltage { + pub value: u32, + pub unit: String, +} + +impl ToString for Voltage { + fn to_string(&self) -> String { + format!("{}{}", self.value, self.unit) + } +} + +impl std::str::FromStr for Voltage { + type Err = ClockStateError; + + fn from_str(s: &str) -> Result { + let mut buffer = String::with_capacity(8); + let mut value = None; + for c in s.trim().chars() { + if c.is_numeric() && value.is_none() { + buffer.push(c); + } else if c.is_numeric() { + return Err(ClockStateError::NotVoltage(s.to_string())); + } else if value.is_none() { + if buffer.is_empty() { + return Err(ClockStateError::NotVoltage(s.to_string())); + } + value = Some(buffer.parse()?); + buffer.clear(); + buffer.push(c); + } else { + buffer.push(c); + } + } + let value = value.ok_or_else(|| ClockStateError::NotVoltage(s.to_string()))?; + if !buffer.ends_with('V') { + return Err(ClockStateError::NotVoltage(s.to_string())); + } + Ok(Self { + value, + unit: buffer, + }) + } +} + +#[derive(Debug, PartialEq)] +pub struct CurvePoint { + pub freq: Frequency, + pub voltage: Voltage, +} + +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ClockStateError { + #[error("Can't parse value. {0:?}")] + ParseValue(#[from] std::num::ParseIntError), + #[error("Value {0:?} is not a voltage")] + NotVoltage(String), + #[error("Value {0:?} is not a frequency")] + NotFrequency(String), + #[error("Voltage section for engine clock is not valid. Line {0:?} is malformed")] + InvalidEngineClockSection(String), +} + +#[derive(Debug, PartialEq)] +pub struct ClockState { + pub curve_labels: Vec, + pub engine_label_lowest: Option, + pub engine_label_highest: Option, + pub memory_label_lowest: Option, + pub memory_label_highest: Option, +} + +impl Default for ClockState { + fn default() -> Self { + Self { + curve_labels: Vec::with_capacity(3), + engine_label_lowest: None, + engine_label_highest: None, + memory_label_lowest: None, + memory_label_highest: None, + } + } +} + +impl std::str::FromStr for ClockState { + type Err = ClockStateError; + + fn from_str(s: &str) -> Result { + let mut clock_state = Self::default(); + enum State { + Unknown, + ParseEngineClock, + ParseMemoryClock, + ParseCurve, + } + let mut state = State::Unknown; + for line in s.lines() { + let start = match line.chars().position(|c| c != ' ' && c != '\0') { + Some(idx) => idx, + _ => continue, + }; + + let line = line[start..].trim(); + match state { + _ if line == "OD_RANGE:" => break, + _ if line == ENGINE_CLOCK_LABEL => { + state = State::ParseEngineClock; + } + _ if line == MEMORY_CLOCK_LABEL => { + state = State::ParseMemoryClock; + } + _ if line == CURVE_POINTS_LABEL => { + state = State::ParseCurve; + } + State::ParseEngineClock => { + if clock_state.engine_label_lowest.is_none() { + clock_state.engine_label_lowest = Some(parse_freq_line(line)?); + } else { + clock_state.engine_label_highest = Some(parse_freq_line(line)?); + } + } + State::ParseMemoryClock => { + if clock_state.memory_label_lowest.is_none() { + clock_state.memory_label_lowest = Some(parse_freq_line(line)?); + } else { + clock_state.memory_label_highest = Some(parse_freq_line(line)?); + } + } + State::ParseCurve => { + let (freq, volt) = parse_freq_voltage_line(line)?; + clock_state.curve_labels.push(CurvePoint { + freq, + voltage: volt, + }); + } + _ => {} + } + } + Ok(clock_state) + } +} + +fn consume_mode_number<'line>( + line: &'line str, + chars: &mut Peekable>, +) -> std::result::Result<(), ClockStateError> { + let mut buffer = String::with_capacity(4); + while chars.peek().filter(|c| c.is_numeric()).is_some() { + buffer.push(chars.next().unwrap()); + } + if buffer.is_empty() { + return Err(ClockStateError::InvalidEngineClockSection(line.to_string())); + } + chars + .next() + .filter(|c| *c == ':') + .ok_or_else(|| ClockStateError::InvalidEngineClockSection(line.to_string()))?; + Ok(()) +} + +fn consume_freq(chars: &mut Peekable) -> std::result::Result { + consume_white(chars); + chars + .take_while(|c| *c != ' ') + .collect::() + .parse::() +} + +fn consume_voltage(chars: &mut Peekable) -> std::result::Result { + consume_white(chars); + chars + .take_while(|c| *c != ' ') + .collect::() + .parse::() +} + +fn consume_white(chars: &mut Peekable) { + while chars.peek().filter(|c| **c == ' ').is_some() { + let _ = chars.next(); + } +} + +fn parse_freq_line(line: &str) -> std::result::Result { + let mut chars = line.chars().peekable(); + consume_mode_number(line, &mut chars)?; + consume_freq(&mut chars) +} + +fn parse_freq_voltage_line( + line: &str, +) -> std::result::Result<(Frequency, Voltage), ClockStateError> { + let mut chars = line.chars().peekable(); + consume_mode_number(line, &mut chars)?; + let freq = consume_freq(&mut chars)?; + consume_white(&mut chars); + Ok((freq, consume_voltage(&mut chars)?)) +} + +#[cfg(test)] +mod parse_frequency { + use crate::clock_state::{ClockStateError, Frequency}; + + #[test] + fn parse_empty_string() { + assert_eq!( + "".parse::(), + Err(ClockStateError::NotFrequency("".to_string())) + ); + } + + #[test] + fn parse_only_v_letter() { + assert_eq!( + "v".parse::(), + Err(ClockStateError::NotFrequency("v".to_string())) + ); + } + + #[test] + fn parse_only_hz() { + assert_eq!( + "hz".parse::(), + Err(ClockStateError::NotFrequency("hz".to_string())) + ); + } + + #[test] + fn parse_only_mhz() { + assert_eq!( + "Mhz".parse::(), + Err(ClockStateError::NotFrequency("Mhz".to_string())) + ); + } + + #[test] + fn parse_0mhz() { + assert_eq!( + "0Mhz".parse::(), + Ok(Frequency { + value: 0, + unit: "Mhz".to_string(), + }) + ); + } + + #[test] + fn parse_0khz() { + assert_eq!( + "0khz".parse::(), + Ok(Frequency { + value: 0, + unit: "khz".to_string(), + }) + ); + } + + #[test] + fn parse_0kz() { + assert_eq!( + "0hz".parse::(), + Ok(Frequency { + value: 0, + unit: "hz".to_string(), + }) + ); + } + + #[test] + fn parse_123mhz() { + assert_eq!( + "123Mhz".parse::(), + Ok(Frequency { + value: 123, + unit: "Mhz".to_string(), + }) + ); + } + + #[test] + fn parse_123khz() { + assert_eq!( + "123khz".parse::(), + Ok(Frequency { + value: 123, + unit: "khz".to_string(), + }) + ); + } + + #[test] + fn parse_123kz() { + assert_eq!( + "123hz".parse::(), + Ok(Frequency { + value: 123, + unit: "hz".to_string(), + }) + ); + } +} + +#[cfg(test)] +mod state_tests { + use crate::clock_state::{ClockState, CurvePoint, Frequency, Voltage}; + + #[test] + fn valid_string() { + let s = r#" +OD_SCLK: +0: 800Mhz +1: 2100Mhz +OD_MCLK: +1: 875MHz +OD_VDDC_CURVE: +0: 800MHz 706mV +1: 1450MHz 772mV +2: 2100MHz 1143mV +OD_RANGE: +SCLK: 800Mhz 2150Mhz +MCLK: 625Mhz 950Mhz +VDDC_CURVE_SCLK[0]: 800Mhz 2150Mhz +VDDC_CURVE_VOLT[0]: 750mV 1200mV +VDDC_CURVE_SCLK[1]: 800Mhz 2150Mhz +VDDC_CURVE_VOLT[1]: 750mV 1200mV +VDDC_CURVE_SCLK[2]: 800Mhz 2150Mhz +VDDC_CURVE_VOLT[2]: 750mV 1200mV + "#; + let res = s.trim().parse::(); + assert_eq!( + res, + Ok(ClockState { + curve_labels: vec![ + CurvePoint { + freq: Frequency { + value: 800, + unit: "MHz".to_string() + }, + voltage: Voltage { + value: 706, + unit: "mV".to_string() + } + }, + CurvePoint { + freq: Frequency { + value: 1450, + unit: "MHz".to_string() + }, + voltage: Voltage { + value: 772, + unit: "mV".to_string() + } + }, + CurvePoint { + freq: Frequency { + value: 2100, + unit: "MHz".to_string() + }, + voltage: Voltage { + value: 1143, + unit: "mV".to_string() + } + } + ], + engine_label_lowest: Some(Frequency { + value: 800, + unit: "Mhz".to_string() + }), + engine_label_highest: Some(Frequency { + value: 2100, + unit: "Mhz".to_string() + }), + memory_label_lowest: Some(Frequency { + value: 875, + unit: "MHz".to_string() + }), + memory_label_highest: None + }) + ); + } +} diff --git a/amdvold/src/command.rs b/amdvold/src/command.rs new file mode 100644 index 0000000..9cdbb47 --- /dev/null +++ b/amdvold/src/command.rs @@ -0,0 +1,96 @@ +use crate::apply_changes::ApplyChanges; +use crate::change_state::ChangeState; +use crate::clock_state::{ClockState, Frequency, Voltage}; +use crate::print_states::PrintStates; +use crate::setup_info::SetupInfo; +use crate::{Config, VoltageError}; +use amdgpu::hw_mon::HwMon; + +#[derive(Debug)] +pub enum HardwareModule { + Engine, + Memory, +} + +impl std::str::FromStr for HardwareModule { + type Err = VoltageError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "memory" => Ok(HardwareModule::Memory), + "engine" => Ok(HardwareModule::Engine), + _ => Err(VoltageError::UnknownHardwareModule(s.to_string())), + } + } +} + +#[derive(Debug, gumdrop::Options)] +pub enum VoltageCommand { + SetupInfo(SetupInfo), + PrintStates(PrintStates), + ChangeState(ChangeState), + ApplyChanges(ApplyChanges), +} + +pub struct VoltageManipulator { + hw_mon: HwMon, +} + +impl std::ops::Deref for VoltageManipulator { + type Target = HwMon; + + fn deref(&self) -> &Self::Target { + &self.hw_mon + } +} + +impl std::ops::DerefMut for VoltageManipulator { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.hw_mon + } +} + +impl VoltageManipulator { + pub fn wrap(hw_mon: HwMon, _config: &Config) -> Self { + Self { hw_mon } + } + + pub fn wrap_all(mons: Vec, config: &Config) -> Vec { + mons.into_iter() + .map(|mon| Self::wrap(mon, config)) + .collect() + } + + pub fn write_apply(&self) -> crate::Result<()> { + self.device_write("pp_od_clk_voltage", "c")?; + Ok(()) + } + + pub fn write_state( + &self, + state_index: u16, + freq: Frequency, + voltage: Voltage, + module: HardwareModule, + ) -> crate::Result<()> { + self.device_write( + "pp_od_clk_voltage", + format!( + "{module} {state_index} {freq} {voltage}", + state_index = state_index, + freq = freq.to_string(), + voltage = voltage.to_string(), + module = match module { + HardwareModule::Engine => "s", + HardwareModule::Memory => "m", + }, + ), + )?; + Ok(()) + } + + pub fn clock_states(&self) -> crate::Result { + let state = self.device_read("pp_od_clk_voltage")?.parse()?; + Ok(state) + } +} diff --git a/amdvold/src/config.rs b/amdvold/src/config.rs new file mode 100644 index 0000000..1cad871 --- /dev/null +++ b/amdvold/src/config.rs @@ -0,0 +1,40 @@ +use amdgpu::LogLevel; +use std::io::ErrorKind; + +#[derive(Debug, thiserror::Error)] +pub enum ConfigError {} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Config { + log_level: LogLevel, +} + +impl Default for Config { + fn default() -> Self { + Self { + log_level: LogLevel::Error, + } + } +} + +impl Config { + pub fn log_level(&self) -> LogLevel { + self.log_level + } +} + +pub fn load_config(config_path: &str) -> crate::Result { + let config = match std::fs::read_to_string(config_path) { + Ok(s) => toml::from_str(&s).unwrap(), + Err(e) if e.kind() == ErrorKind::NotFound => { + let config = Config::default(); + std::fs::write(config_path, toml::to_string(&config).unwrap())?; + config + } + Err(e) => { + log::error!("{:?}", e); + panic!(); + } + }; + Ok(config) +} diff --git a/amdvold/src/error.rs b/amdvold/src/error.rs new file mode 100644 index 0000000..dafcd1d --- /dev/null +++ b/amdvold/src/error.rs @@ -0,0 +1,22 @@ +use crate::change_state::ChangeStateError; +use crate::clock_state::ClockStateError; +use crate::config::ConfigError; +use amdgpu::AmdGpuError; + +#[derive(Debug, thiserror::Error)] +pub enum VoltageError { + #[error("No AMD GPU card was found")] + NoAmdGpu, + #[error("Unknown hardware module {0:?}")] + UnknownHardwareModule(String), + #[error("{0}")] + AmdGpu(AmdGpuError), + #[error("{0}")] + Config(#[from] ConfigError), + #[error("{0:}")] + Io(#[from] std::io::Error), + #[error("{0:}")] + ClockState(#[from] ClockStateError), + #[error("{0:}")] + ChangeStateError(#[from] ChangeStateError), +} diff --git a/amdvold/src/main.rs b/amdvold/src/main.rs new file mode 100644 index 0000000..d247f5c --- /dev/null +++ b/amdvold/src/main.rs @@ -0,0 +1,88 @@ +use crate::command::VoltageCommand; +use crate::config::{load_config, Config}; +use crate::error::VoltageError; +use amdgpu::CONFIG_DIR; +use gumdrop::Options; +use std::io::ErrorKind; + +mod apply_changes; +mod change_state; +mod clock_state; +mod command; +mod config; +mod error; +mod print_states; +mod setup_info; + +pub static DEFAULT_CONFIG_PATH: &str = "/etc/amdfand/voltage.toml"; + +pub type Result = std::result::Result; + +#[derive(gumdrop::Options)] +pub struct Opts { + #[options(help = "Help message")] + help: bool, + #[options(help = "Print version")] + version: bool, + #[options(help = "Config location")] + config: Option, + #[options(command)] + command: Option, +} +fn run(config: Config) -> Result<()> { + let opts: Opts = Opts::parse_args_default_or_exit(); + + if opts.version { + println!("amdfand {}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } + + match opts.command { + None => { + Opts::usage(); + Ok(()) + } + Some(VoltageCommand::PrintStates(command)) => print_states::run(command, config), + Some(VoltageCommand::SetupInfo(command)) => setup_info::run(command, &config), + Some(VoltageCommand::ChangeState(command)) => change_state::run(command, &config), + Some(VoltageCommand::ApplyChanges(command)) => apply_changes::run(command, &config), + } +} + +fn setup() -> Result<(String, Config)> { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "DEBUG"); + } + pretty_env_logger::init(); + if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { + std::fs::create_dir_all(CONFIG_DIR)?; + } + + let config_path = Opts::parse_args_default_or_exit() + .config + .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); + let config = load_config(&config_path)?; + log::set_max_level(config.log_level().as_str().parse().unwrap()); + Ok((config_path, config)) +} + +fn main() -> Result<()> { + let (config_path, config) = match setup() { + Ok(config) => config, + Err(e) => { + log::error!("{}", e); + std::process::exit(1); + } + }; + match run(config) { + Ok(()) => Ok(()), + Err(e) => { + let _config = load_config(&config_path).expect( + "Unable to restore automatic voltage control due to unreadable config file", + ); + // panic_handler::restore_automatic(config); + log::error!("{}", e); + std::process::exit(1); + } + } +} diff --git a/amdvold/src/print_states.rs b/amdvold/src/print_states.rs new file mode 100644 index 0000000..09cc29d --- /dev/null +++ b/amdvold/src/print_states.rs @@ -0,0 +1,41 @@ +use crate::command::VoltageManipulator; +use crate::Config; +use amdgpu::utils::hw_mons; + +#[derive(Debug, gumdrop::Options)] +pub struct PrintStates { + help: bool, +} + +pub fn run(_command: PrintStates, config: Config) -> crate::Result<()> { + let mons = VoltageManipulator::wrap_all(hw_mons(false)?, &config); + for mon in mons { + let states = mon.clock_states()?; + println!("Engine clock frequencies:"); + if let Some(freq) = states.engine_label_lowest { + println!(" LOWEST {}", freq.to_string()); + } + if let Some(freq) = states.engine_label_highest { + println!(" HIGHEST {}", freq.to_string()); + } + println!(); + println!("Memory clock frequencies:"); + if let Some(freq) = states.memory_label_lowest { + println!(" LOWEST {}", freq.to_string()); + } + if let Some(freq) = states.memory_label_highest { + println!(" HIGHEST {}", freq.to_string()); + } + println!(); + println!("Curves:"); + for curve in states.curve_labels.iter() { + println!( + " {:>10} {:>10}", + curve.freq.to_string(), + curve.voltage.to_string() + ); + } + println!(); + } + Ok(()) +} diff --git a/amdvold/src/setup_info.rs b/amdvold/src/setup_info.rs new file mode 100644 index 0000000..108ad65 --- /dev/null +++ b/amdvold/src/setup_info.rs @@ -0,0 +1,13 @@ +use crate::config::Config; + +pub static ENABLE_VOLTAGE_INFO: &str = include_str!("../assets/enable_voltage_info.txt"); + +#[derive(Debug, gumdrop::Options)] +pub struct SetupInfo { + help: bool, +} + +pub fn run(_command: SetupInfo, _config: &Config) -> crate::Result<()> { + println!("{}", ENABLE_VOLTAGE_INFO); + Ok(()) +} diff --git a/examples/cards_config.toml b/examples/cards_config.toml new file mode 100644 index 0000000..ff13d1f --- /dev/null +++ b/examples/cards_config.toml @@ -0,0 +1,34 @@ +log_level = "Info" +cards = ["card0"] + +[[speed_matrix]] +temp = 4.0 +speed = 4.0 + +[[speed_matrix]] +temp = 30.0 +speed = 33.0 + +[[speed_matrix]] +temp = 45.0 +speed = 50.0 + +[[speed_matrix]] +temp = 60.0 +speed = 66.0 + +[[speed_matrix]] +temp = 65.0 +speed = 69.0 + +[[speed_matrix]] +temp = 70.0 +speed = 75.0 + +[[speed_matrix]] +temp = 75.0 +speed = 89.0 + +[[speed_matrix]] +temp = 80.0 +speed = 100.0 \ No newline at end of file diff --git a/examples/default_config.toml b/examples/default_config.toml new file mode 100644 index 0000000..ec5c943 --- /dev/null +++ b/examples/default_config.toml @@ -0,0 +1,34 @@ +log_level = "Error" +temp_input = "temp1_input" + +[[speed_matrix]] +temp = 4.0 +speed = 4.0 + +[[speed_matrix]] +temp = 30.0 +speed = 33.0 + +[[speed_matrix]] +temp = 45.0 +speed = 50.0 + +[[speed_matrix]] +temp = 60.0 +speed = 66.0 + +[[speed_matrix]] +temp = 65.0 +speed = 69.0 + +[[speed_matrix]] +temp = 70.0 +speed = 75.0 + +[[speed_matrix]] +temp = 75.0 +speed = 89.0 + +[[speed_matrix]] +temp = 80.0 +speed = 100.0 \ No newline at end of file diff --git a/examples/unsorted_speed_config.toml b/examples/unsorted_speed_config.toml new file mode 100644 index 0000000..4674708 --- /dev/null +++ b/examples/unsorted_speed_config.toml @@ -0,0 +1,33 @@ +log_level = "Error" + +[[speed_matrix]] +temp = 4.0 +speed = 4.0 + +[[speed_matrix]] +temp = 30.0 +speed = 33.0 + +[[speed_matrix]] +temp = 45.0 +speed = 50.0 + +[[speed_matrix]] +temp = 60.0 +speed = 66.0 + +[[speed_matrix]] +temp = 65.0 +speed = 69.0 + +[[speed_matrix]] +temp = 70.0 +speed = 60.0 + +[[speed_matrix]] +temp = 75.0 +speed = 89.0 + +[[speed_matrix]] +temp = 80.0 +speed = 100.0 \ No newline at end of file diff --git a/examples/unsorted_temp_config.toml b/examples/unsorted_temp_config.toml new file mode 100644 index 0000000..0528ff1 --- /dev/null +++ b/examples/unsorted_temp_config.toml @@ -0,0 +1,33 @@ +log_level = "Error" + +[[speed_matrix]] +temp = 4.0 +speed = 4.0 + +[[speed_matrix]] +temp = 30.0 +speed = 33.0 + +[[speed_matrix]] +temp = 45.0 +speed = 50.0 + +[[speed_matrix]] +temp = 60.0 +speed = 66.0 + +[[speed_matrix]] +temp = 65.0 +speed = 69.0 + +[[speed_matrix]] +temp = 70.0 +speed = 75.0 + +[[speed_matrix]] +temp = 95.0 +speed = 89.0 + +[[speed_matrix]] +temp = 80.0 +speed = 100.0 \ No newline at end of file diff --git a/services/amdfand.service b/services/amdfand.service index 94edb56..50ade54 100644 --- a/services/amdfand.service +++ b/services/amdfand.service @@ -1,5 +1,5 @@ [Unit] -Description=amdfan controller +Description=AMD GPU fan daemon After=sysinit.target local-fs.target [Service] Restart=on-failure diff --git a/services/amdvold b/services/amdvold new file mode 100755 index 0000000..fb6dc1a --- /dev/null +++ b/services/amdvold @@ -0,0 +1,11 @@ +#!/sbin/openrc-run + +description="amdvol controller" +pidfile="/run/${SVCNAME}.pid" +command="/usr/bin/amdvold" +command_args="service" +command_user="root" + +depend() { + need udev +} diff --git a/services/amdvold.service b/services/amdvold.service new file mode 100644 index 0000000..aa6e6cc --- /dev/null +++ b/services/amdvold.service @@ -0,0 +1,9 @@ +[Unit] +Description=AMD GPU voltage daemon +After=sysinit.target local-fs.target +[Service] +Restart=on-failure +RestartSec=4 +ExecStart=/usr/bin/amdvold service +[Install] +WantedBy=multi-user.target diff --git a/src/fan/mod.rs b/src/fan/mod.rs deleted file mode 100644 index 462a473..0000000 --- a/src/fan/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use gumdrop::Options; - -pub mod change_mode; -pub mod monitor; -pub mod service; - -#[derive(Debug, Options)] -pub struct AvailableCards { - #[options(help = "Help message")] - help: bool, -} - -#[derive(Debug, Options)] -pub enum FanCommand { - #[options(help = "Print current temp and fan speed")] - Monitor(monitor::Monitor), - #[options(help = "Set fan speed depends on GPU temperature")] - Service(service::Service), - #[options(help = "Switch to GPU automatic fan speed control")] - SetAutomatic(change_mode::Switcher), - #[options(help = "Switch to GPU manual fan speed control")] - SetManual(change_mode::Switcher), - #[options(help = "Print available cards")] - Available(AvailableCards), -} diff --git a/src/hw_mon.rs b/src/hw_mon.rs deleted file mode 100644 index 34feaea..0000000 --- a/src/hw_mon.rs +++ /dev/null @@ -1,231 +0,0 @@ -use crate::config::{Card, Config}; -use crate::io_err::{invalid_input, not_found}; -use crate::utils::linear_map; -use crate::{AmdFanError, 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"; - -#[derive(Debug)] -pub struct HwMon { - /// HW MOD cord (ex. card0) - card: Card, - /// MW MOD name (ex. hwmod0) - name: String, - /// Minimal modulation (between 0-255) - pwm_min: Option, - /// Maximal modulation (between 0-255) - pwm_max: Option, - /// List of available temperature inputs for current HW MOD - temp_inputs: Vec, - /// Preferred temperature input - temp_input: Option, -} - -impl HwMon { - pub fn new(card: &Card, name: &str, config: &Config) -> Self { - Self { - card: *card, - temp_input: config.temp_input().map(String::from), - 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 { - if let Some(input) = self.temp_input.as_deref() { - return self - .read_gpu_temp(input) - .map(|temp| temp as f64 / 1000f64) - .map_err(|_| invalid_input()); - } - 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_unstable(); - 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 card(&self) -> &Card { - &self.card - } - - #[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_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: u64) -> std::io::Result<()> { - if self.is_fan_automatic() { - self.set_manual()?; - } - 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 u64; - 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, config: &Config) -> 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, config)) -} diff --git a/src/io_err.rs b/src/io_err.rs deleted file mode 100644 index eea3263..0000000 --- a/src/io_err.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::io::Error as IoErr; -use std::io::ErrorKind; - -#[inline(always)] -pub fn not_found() -> IoErr { - IoErr::from(ErrorKind::NotFound) -} - -#[inline(always)] -pub fn invalid_input() -> IoErr { - IoErr::from(ErrorKind::NotFound) -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index cbf5a96..0000000 --- a/src/main.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::fmt::Formatter; -use std::io::ErrorKind; - -use gumdrop::Options; - -use crate::config::load_config; - -mod config; -mod fan; -mod hw_mon; -mod io_err; -mod utils; -mod voltage; - -extern crate log; - -static CONFIG_DIR: &str = "/etc/amdfand"; -static CONFIG_PATH: &str = "/etc/amdfand/config.toml"; - -static ROOT_DIR: &str = "/sys/class/drm"; -static HW_MON_DIR: &str = "device/hwmon"; - -#[derive(Debug, PartialEq)] -pub enum AmdFanError { - InvalidPrefix, - InputTooShort, - 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 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)) - } - 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"), - } - } -} - -pub enum FanMode { - Manual, - Automatic, -} - -#[derive(Options)] -pub struct Opts { - #[options(help = "Help message")] - help: bool, - #[options(help = "Print version")] - version: bool, - #[options(command)] - command: Option, -} - -fn main() -> std::io::Result<()> { - if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "DEBUG"); - } - pretty_env_logger::init(); - 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().as_str().parse().unwrap()); - - let opts: Opts = Opts::parse_args_default_or_exit(); - - if opts.version { - println!("amdfand {}", env!("CARGO_PKG_VERSION")); - std::process::exit(0); - } - - match opts.command { - None => fan::service::run(config), - Some(fan::FanCommand::Monitor(monitor)) => fan::monitor::run(monitor, config), - Some(fan::FanCommand::Service(_)) => fan::service::run(config), - Some(fan::FanCommand::SetAutomatic(switcher)) => { - fan::change_mode::run(switcher, FanMode::Automatic, config) - } - Some(fan::FanCommand::SetManual(switcher)) => { - fan::change_mode::run(switcher, FanMode::Manual, config) - } - Some(fan::FanCommand::Available(_)) => { - println!("Available cards"); - utils::hw_mons(&config, false)? - .into_iter() - .for_each(|hw_mon| { - println!( - " * {:6>} - {}", - hw_mon.card(), - hw_mon.name().unwrap_or_default() - ); - }); - Ok(()) - } - } -} diff --git a/src/voltage/mod.rs b/src/voltage/mod.rs deleted file mode 100644 index 1f33363..0000000 --- a/src/voltage/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[derive(Debug, gumdrop::Options)] -pub enum VoltageCommand { - Placeholder(Placeholder), -} - -#[derive(Debug, gumdrop::Options)] -pub struct Placeholder { - help: bool, -}