Initial
This commit is contained in:
commit
8ce6bf86a1
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
amdfand.toml
|
249
Cargo.lock
generated
Normal file
249
Cargo.lock
generated
Normal file
@ -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"
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -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" }
|
65
README.md
Normal file
65
README.md
Normal file
@ -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
|
||||
```
|
7
amdfand.service
Normal file
7
amdfand.service
Normal file
@ -0,0 +1,7 @@
|
||||
[Unit]
|
||||
Description=amdfan controller
|
||||
[Service]
|
||||
ExecStart=/usr/bin/amdfand service
|
||||
Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
337
src/config.rs
Normal file
337
src/config.rs
Normal file
@ -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<Self, Self::Err> {
|
||||
if !value.starts_with("card") {
|
||||
return Err(AmdFanError::InvalidPrefix);
|
||||
}
|
||||
if value.len() < 5 {
|
||||
return Err(AmdFanError::InputTooShort);
|
||||
}
|
||||
value[4..]
|
||||
.parse::<u32>()
|
||||
.map_err(|e| AmdFanError::InvalidSuffix(format!("{:?}", e)))
|
||||
.map(|n| Card(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Card {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match value.parse::<Card>() {
|
||||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<LogLevel> 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<Card>,
|
||||
speed_matrix: Vec<MatrixPoint>,
|
||||
}
|
||||
|
||||
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<Config> {
|
||||
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);
|
||||
}
|
||||
}
|
380
src/main.rs
Normal file
380
src/main.rs
Normal file
@ -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<u32>,
|
||||
fan_max: Option<u32>,
|
||||
}
|
||||
|
||||
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<f64> {
|
||||
let value = self.read("temp1_input")?.parse::<u64>().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<String> {
|
||||
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<u64> {
|
||||
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<String> {
|
||||
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<Self> {
|
||||
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<String> {
|
||||
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<u32>,
|
||||
}
|
||||
|
||||
#[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<Command>,
|
||||
}
|
||||
|
||||
fn read_cards() -> std::io::Result<Vec<Card>> {
|
||||
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::<Card>()
|
||||
.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<Vec<CardController>> {
|
||||
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(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user