From 8ce6bf86a15f603d354f0fb4992c16f748f92b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Fri, 2 Jul 2021 13:36:36 +0200 Subject: [PATCH] Initial --- .gitignore | 2 + Cargo.lock | 249 +++++++++++++++++++++++++++++++ Cargo.toml | 14 ++ README.md | 65 +++++++++ amdfand.service | 7 + src/config.rs | 337 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 380 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1054 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 amdfand.service create mode 100644 src/config.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80a3edd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +amdfand.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d02bad0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,249 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "amdfand" +version = "1.0.0" +dependencies = [ + "gumdrop", + "log", + "pretty_env_logger", + "serde", + "toml", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "gumdrop" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46571f5d540478cf70d2a42dd0d6d8e9f4b9cc7531544b93311e657b86568a0b" +dependencies = [ + "gumdrop_derive", +] + +[[package]] +name = "gumdrop_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915ef07c710d84733522461de2a734d4d62a3fd39a4d4f404c2f385ef8618d05" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ef748f6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "amdfand" +version = "1.0.0" +edition = "2018" +description = "AMDGPU fan control service" + +[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" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..96f05dc --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# AMDGPU Fan control service + +Available commands: + +* `monitor` - Print current temp and fan speed +* `service` - Set fan speed depends on GPU temperature +* `set-automatic` - Switch to GPU automatic fan speed control +* `set-manual` - Switch to GPU manual fan speed control +* `available` - Print available cards + +#### amdfand set-automatic | set-manual [OPTIONS] + +Optional arguments: + +* -h, --help Help message +* -c, --card CARD GPU Card number + +## Usage + +```bash +cargo install argonfand + +sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed +sudo argonfand service # check amdgpu temperature and adjust speed from config file +``` + +## Config file + +```toml +# /etc/amdfand/config.toml +log_level = "Error" +cards = ["card0"] + +[[speed_matrix]] +temp = 4.0 +speed = 4 + +[[speed_matrix]] +temp = 30.0 +speed = 33 + +[[speed_matrix]] +temp = 45.0 +speed = 50 + +[[speed_matrix]] +temp = 60.0 +speed = 66 + +[[speed_matrix]] +temp = 65.0 +speed = 69 + +[[speed_matrix]] +temp = 70.0 +speed = 75 + +[[speed_matrix]] +temp = 75.0 +speed = 89 + +[[speed_matrix]] +temp = 80.0 +speed = 100 +``` diff --git a/amdfand.service b/amdfand.service new file mode 100644 index 0000000..166a108 --- /dev/null +++ b/amdfand.service @@ -0,0 +1,7 @@ +[Unit] +Description=amdfan controller +[Service] +ExecStart=/usr/bin/amdfand service +Restart=always +[Install] +WantedBy=multi-user.target diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..12ea1b8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,337 @@ +use crate::{AmdFanError, CONFIG_PATH}; +use log::LevelFilter; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::Formatter; +use std::io::ErrorKind; +use std::str::FromStr; + +#[derive(Debug, Copy, Clone)] +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(|n| Card(n)) + } +} + +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.0), + 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 + ))), + } + } + } + deserializer.deserialize_str(CardVisitor).map(|v| Card(v)) + } +} + +impl Serialize for Card { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MatrixPoint { + pub temp: f64, + pub speed: u32, +} + +#[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 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, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + log_level: LogLevel, + cards: Vec, + speed_matrix: Vec, +} + +impl Config { + pub fn cards(&self) -> &[Card] { + &self.cards + } + + pub fn speed_for_temp(&self, temp: f64) -> u32 { + let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) { + Some(idx) => idx, + _ => return 4, + }; + + match (idx, self.speed_matrix.len() - 1) { + (0, _) => self.min_speed(), + (current, max) if current == max => self.max_speed(), + _ => { + if self.is_exact_point(idx, temp) { + return self.speed_matrix.get(idx).map(|p| p.speed).unwrap_or(4); + } + let max = match self.speed_matrix.get(idx + 1) { + Some(p) => p, + _ => return 4, + }; + let min = match self.speed_matrix.get(idx) { + Some(p) => p, + _ => return 4, + }; + let speed_diff = max.speed as f64 - min.speed as f64; + let temp_diff = max.temp as f64 - min.temp as f64; + let increase_by = + (((temp as f64 - min.temp as f64) / temp_diff) * speed_diff).round(); + min.speed + increase_by as u32 + } + } + } + + pub fn log_level(&self) -> LogLevel { + self.log_level + } + + fn min_speed(&self) -> u32 { + self.speed_matrix.first().map(|p| p.speed).unwrap_or(4) + } + + fn max_speed(&self) -> u32 { + self.speed_matrix.last().map(|p| p.speed).unwrap_or(100) + } + + fn is_exact_point(&self, idx: usize, temp: f64) -> bool { + static DELTA: f64 = 0.001f64; + self.speed_matrix + .get(idx) + .map(|p| p.temp - DELTA < temp && p.temp + DELTA > temp) + .unwrap_or(false) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + log_level: LogLevel::Error, + cards: vec![Card(0)], + speed_matrix: vec![ + MatrixPoint { + temp: 4f64, + speed: 4, + }, + MatrixPoint { + temp: 30f64, + speed: 33, + }, + MatrixPoint { + temp: 45f64, + speed: 50, + }, + MatrixPoint { + temp: 60f64, + speed: 66, + }, + MatrixPoint { + temp: 65f64, + speed: 69, + }, + MatrixPoint { + temp: 70f64, + speed: 75, + }, + MatrixPoint { + temp: 75f64, + speed: 89, + }, + MatrixPoint { + temp: 80f64, + speed: 100, + }, + ], + } + } +} + +pub fn load_config() -> std::io::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!(); + } + }; + + if config.speed_matrix.iter().fold( + 1000, + |n, point| if point.speed < n { point.speed } else { n }, + ) < 4 + { + log::error!("Due to driver bug lowest fan speed must be greater or equal 4"); + return Err(std::io::Error::from(ErrorKind::InvalidData)); + } + + let mut last_point: Option<&MatrixPoint> = None; + + for matrix_point in config.speed_matrix.iter() { + if matrix_point.speed <= 0 { + log::error!("Fan speed can's be below 0 found {}", matrix_point.speed); + return Err(std::io::Error::from(ErrorKind::InvalidData)); + } + if matrix_point.speed > 100 { + log::error!("Fan speed can's be above 100 found {}", matrix_point.speed); + return Err(std::io::Error::from(ErrorKind::InvalidData)); + } + if let Some(last_point) = last_point { + if matrix_point.speed < last_point.speed { + log::error!( + "Curve fan speeds should be monotonically increasing, found {} then {}", + last_point.speed, + matrix_point.speed + ); + return Err(std::io::Error::from(ErrorKind::InvalidData)); + } + if matrix_point.temp < last_point.temp { + log::error!( + "Curve fan temps should be monotonically increasing, found {} then {}", + last_point.temp, + matrix_point.temp + ); + return Err(std::io::Error::from(ErrorKind::InvalidData)); + } + } + + last_point = Some(matrix_point) + } + + Ok(config) +} + +#[cfg(test)] +mod speed_for_temp { + use super::*; + + #[test] + fn below_minimal() { + let config = Config::default(); + assert_eq!(config.speed_for_temp(1f64), 4); + } + + #[test] + fn minimal() { + let config = Config::default(); + assert_eq!(config.speed_for_temp(4f64), 4); + } + + #[test] + fn between_3_and_4_temp_46() { + let config = Config::default(); + // 45 -> 50 + // 60 -> 66 + assert_eq!(config.speed_for_temp(46f64), 51); + } + + #[test] + fn between_3_and_4_temp_58() { + let config = Config::default(); + // 45 -> 50 + // 60 -> 66 + assert_eq!(config.speed_for_temp(58f64), 64); + } + + #[test] + fn between_3_and_4_temp_59() { + let config = Config::default(); + // 45 -> 50 + // 60 -> 66 + assert_eq!(config.speed_for_temp(59f64), 65); + } + + #[test] + fn average() { + let config = Config::default(); + assert_eq!(config.speed_for_temp(60f64), 66); + } + + #[test] + fn max() { + let config = Config::default(); + assert_eq!(config.speed_for_temp(80f64), 100); + } + + #[test] + fn above_max() { + let config = Config::default(); + assert_eq!(config.speed_for_temp(160f64), 100); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..573aff4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,380 @@ +mod config; + +extern crate log; + +use std::io::ErrorKind; + +use crate::config::{load_config, Card, Config}; +use gumdrop::Options; + +static CONFIG_PATH: &str = "/etc/amdfand/config.toml"; + +static ROOT_DIR: &str = "/sys/class/drm"; +static HW_MON_DIR: &str = "device/hwmon"; + +pub enum AmdFanError { + InvalidPrefix, + InputTooShort, + InvalidSuffix(String), +} + +#[derive(Debug)] +pub struct HwMon { + card: Card, + name: String, + fan_min: Option, + fan_max: Option, +} + +impl HwMon { + pub fn new(card: &Card, name: &str) -> Self { + Self { + card: card.clone(), + name: String::from(name), + fan_min: None, + fan_max: None, + } + } + + 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 fan_min(&mut self) -> u32 { + if self.fan_min.is_none() { + self.fan_min = Some( + self.read("fan1_min") + .unwrap_or_default() + .parse() + .unwrap_or(0), + ) + }; + self.fan_min.unwrap_or(0) + } + + pub fn fan_max(&mut self) -> u32 { + if self.fan_max.is_none() { + self.fan_max = Some( + self.read("fan1_max") + .unwrap_or_default() + .parse() + .unwrap_or(255), + ) + }; + self.fan_max.unwrap_or(255) + } + + pub fn fan_speed(&self) -> std::io::Result { + self.read("fan1_input")?.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 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_speed(&self, speed: u64) -> std::io::Result<()> { + if self.is_fan_automatic() { + self.set_manual()?; + } + self.write("pwm1", speed) + } + + 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()) + .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); + let entries = std::fs::read_dir(read_path)?; + entries + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + entry + .file_name() + .as_os_str() + .to_str() + .map(|s| s.to_string()) + }) + .find(|name| name.starts_with("hwmon")) + .ok_or_else(|| std::io::Error::from(ErrorKind::NotFound)) + } +} + +pub enum FanMode { + Manual, + Automatic, +} + +#[derive(Debug, Options)] +pub struct Monitor { + #[options(help = "Help message")] + help: bool, +} + +#[derive(Debug, Options)] +pub struct Service { + #[options(help = "Help message")] + help: bool, +} + +#[derive(Debug, Options)] +pub struct Switcher { + #[options(help = "Help message")] + help: bool, + #[options(help = "GPU Card number")] + card: Option, +} + +#[derive(Debug, Options)] +pub struct AvailableCards { + #[options(help = "Help message")] + help: bool, +} + +#[derive(Debug, Options)] +pub enum Command { + #[options(help = "Print current temp and fan speed")] + Monitor(Monitor), + #[options(help = "Set fan speed depends on GPU temperature")] + Service(Service), + #[options(help = "Switch to GPU automatic fan speed control")] + SetAutomatic(Switcher), + #[options(help = "Switch to GPU manual fan speed control")] + SetManual(Switcher), + #[options(help = "Print available cards")] + Available(AvailableCards), +} + +#[derive(Options)] +pub struct Opts { + #[options(help = "Help message")] + help: bool, + #[options(command)] + command: Option, +} + +fn read_cards() -> std::io::Result> { + let mut cards = vec![]; + let entries = std::fs::read_dir(ROOT_DIR)?; + for entry in entries { + match entry + .and_then(|entry| { + entry + .file_name() + .as_os_str() + .to_str() + .map(|s| s.to_string()) + .ok_or_else(|| std::io::Error::from(ErrorKind::InvalidData)) + }) + .and_then(|file_name| { + file_name + .parse::() + .map_err(|_| std::io::Error::from(ErrorKind::InvalidData)) + }) { + Ok(card) => { + cards.push(card); + } + _ => continue, + }; + } + Ok(cards) +} + +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(|reader| { + !filter + || reader + .hw_mon + .name() + .ok() + .filter(|s| s.as_str() == "amdgpu") + .is_some() + }) + .collect()) +} + +fn service(config: Config) -> std::io::Result<()> { + let mut controllers = controllers(&config, true)?; + loop { + for controller in controllers.iter_mut() { + let gpu_temp = controller.hw_mon.gpu_temp().unwrap_or_default(); + + let speed = config.speed_for_temp(gpu_temp); + if controller.hw_mon.fan_min() > speed || controller.hw_mon.fan_max() < speed { + continue; + } + + if let Err(e) = controller.hw_mon.set_speed(speed as u64) { + log::error!("Failed to change speed to {}. {:?}", speed, e); + } + controller.last_temp = gpu_temp; + } + std::thread::sleep(std::time::Duration::from_secs(4)); + } +} + +fn change_mode(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result<()> { + 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) => vec![controllers.remove(card)], + None => { + eprintln!("Card does not exists. Available cards: "); + for card in controllers { + eprintln!(" * {}", card.hw_mon.card.0); + } + return Err(std::io::Error::from(ErrorKind::NotFound)); + } + }, + None => controllers, + }; + + for card in cards { + match mode { + FanMode::Automatic => { + if let Err(e) = card.hw_mon.set_automatic() { + log::error!("{:?}", e); + } + } + FanMode::Manual => { + if let Err(e) = card.hw_mon.set_manual() { + log::error!("{:?}", e); + } + } + } + } + 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 | fan speed | MAX | MIN ", + card.hw_mon.card.0 + ); + println!( + " | {:>5.2} | {:>9} | {:>4} | {:>4}", + card.hw_mon.gpu_temp().unwrap_or_default(), + card.hw_mon.fan_speed().unwrap_or_default(), + card.hw_mon.fan_min(), + card.hw_mon.fan_max(), + ); + } + 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")?; + } + + let config = load_config()?; + + log::set_max_level(config.log_level().into()); + pretty_env_logger::init(); + + let opts: Opts = Opts::parse_args_default_or_exit(); + + match opts.command { + None => return Ok(()), + Some(Command::Monitor(_)) => { + monitor_cards(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| { + println!( + " * {:6>} - {}", + card.hw_mon.card.to_string(), + card.hw_mon.name().unwrap_or_default() + ); + }); + } + } + + Ok(()) +}