This commit is contained in:
Adrian Woźniak 2021-07-02 13:36:36 +02:00
commit 8ce6bf86a1
No known key found for this signature in database
GPG Key ID: DE43476F72AD3F6C
7 changed files with 1054 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
amdfand.toml

249
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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(())
}