Merge pull request #25 from Eraden/readable-errors-and-voltage

Better error messages, better error handling, improve temp config, move forward with voltage
This commit is contained in:
Adrian Woźniak 2021-11-24 12:16:38 +01:00 committed by GitHub
commit 4c0cea5f33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1939 additions and 572 deletions

49
Cargo.lock generated
View File

@ -13,12 +13,39 @@ dependencies = [
[[package]] [[package]]
name = "amdfand" 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 = [ dependencies = [
"gumdrop", "gumdrop",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
"serde", "serde",
"thiserror",
"toml",
]
[[package]]
name = "amdvold"
version = "1.0.6"
dependencies = [
"amdgpu",
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"thiserror",
"toml", "toml",
] ]
@ -202,6 +229,26 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "toml" name = "toml"
version = "0.5.8" version = "0.5.8"

View File

@ -1,18 +1,2 @@
[package] [workspace]
name = "amdfand" members = ["amdfand", "amdgpu", "amdvold"]
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" }

View File

@ -31,7 +31,7 @@ sudo argonfand service # check amdgpu temperature and adjust speed from config f
```toml ```toml
# /etc/amdfand/config.toml # /etc/amdfand/config.toml
log_level = "Error" log_level = "Error"
cards = ["card0"] temp_input = "temp1_input"
[[speed_matrix]] [[speed_matrix]]
temp = 4.0 temp = 4.0

20
amdfand/Cargo.toml Normal file
View File

@ -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" }

View File

@ -1,12 +1,13 @@
use crate::command::Fan;
use amdgpu::utils::hw_mons;
use gumdrop::Options; use gumdrop::Options;
use crate::config::Config; use crate::config::Config;
use crate::io_err::not_found; use crate::{AmdFanError, FanMode};
use crate::FanMode;
/// Change card fan mode to either automatic or manual /// Change card fan mode to either automatic or manual
pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result<()> { pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> crate::Result<()> {
let mut hw_mons = crate::utils::hw_mons(&config, true)?; let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
let cards = match switcher.card { let cards = match switcher.card {
Some(card_id) => match hw_mons.iter().position(|hw_mon| **hw_mon.card() == card_id) { 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 { for hw_mon in hw_mons {
eprintln!(" * {}", *hw_mon.card()); eprintln!(" * {}", *hw_mon.card());
} }
return Err(not_found()); return Err(AmdFanError::NoAmdCardFound);
} }
}, },
None => hw_mons, None => hw_mons,
@ -25,12 +26,12 @@ pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result
for hw_mon in cards { for hw_mon in cards {
match mode { match mode {
FanMode::Automatic => { FanMode::Automatic => {
if let Err(e) = hw_mon.set_automatic() { if let Err(e) = hw_mon.write_automatic() {
log::error!("{:?}", e); log::error!("{:?}", e);
} }
} }
FanMode::Manual => { FanMode::Manual => {
if let Err(e) = hw_mon.set_manual() { if let Err(e) = hw_mon.write_manual() {
log::error!("{:?}", e); log::error!("{:?}", e);
} }
} }

208
amdfand/src/command.rs Normal file
View File

@ -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<String>,
/// Preferred temperature input
pub temp_input: Option<TempInput>,
/// Minimal modulation (between 0-255)
pub pwm_min: Option<u32>,
/// Maximal modulation (between 0-255)
pub pwm_max: Option<u32>,
}
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<HwMon>, config: &Config) -> Vec<Fan> {
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<u32> {
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<f64> {
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<f64>)> {
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<u64> {
let value = self
.hw_mon_read(name)?
.parse::<u64>()
.map_err(FanError::NonIntTemp)?;
Ok(value)
}
}
fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> {
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()
}

View File

@ -1,140 +1,32 @@
use crate::{AmdFanError, CONFIG_PATH}; use amdgpu::utils::linear_map;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use amdgpu::{LogLevel, TempInput};
use std::fmt::Formatter;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::str::FromStr;
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(serde::Serialize, serde::Deserialize, Debug, 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(Card)
}
}
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),
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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)]
pub struct MatrixPoint { pub struct MatrixPoint {
pub temp: f64, pub temp: f64,
pub speed: f64, pub speed: f64,
} }
#[derive(Serialize, Deserialize, Debug, Copy, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug)]
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)]
pub struct Config { pub struct Config {
cards: Option<Vec<String>>,
log_level: LogLevel, log_level: LogLevel,
speed_matrix: Vec<MatrixPoint>, speed_matrix: Vec<MatrixPoint>,
temp_input: Option<String>, /// 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<TempInput>,
} }
impl Config { 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<String>> {
self.cards.as_ref()
}
pub fn speed_for_temp(&self, temp: f64) -> f64 { pub fn speed_for_temp(&self, temp: f64) -> f64 {
let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) { let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
Some(idx) => idx, Some(idx) => idx,
@ -145,7 +37,7 @@ impl Config {
return self.max_speed(); return self.max_speed();
} }
crate::utils::linear_map( linear_map(
temp, temp,
self.speed_matrix[idx].temp, self.speed_matrix[idx].temp,
self.speed_matrix[idx + 1].temp, self.speed_matrix[idx + 1].temp,
@ -158,8 +50,8 @@ impl Config {
self.log_level self.log_level
} }
pub fn temp_input(&self) -> Option<&str> { pub fn temp_input(&self) -> Option<&TempInput> {
self.temp_input.as_deref() self.temp_input.as_ref()
} }
fn min_speed(&self) -> f64 { fn min_speed(&self) -> f64 {
@ -174,6 +66,8 @@ impl Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
#[allow(deprecated)]
cards: None,
log_level: LogLevel::Error, log_level: LogLevel::Error,
speed_matrix: vec![ speed_matrix: vec![
MatrixPoint { MatrixPoint {
@ -209,17 +103,41 @@ impl Default for Config {
speed: 100f64, speed: 100f64,
}, },
], ],
temp_input: Some(String::from("temp1_input")), temp_input: Some(TempInput(1)),
} }
} }
} }
pub fn load_config() -> std::io::Result<Config> { #[derive(Debug, thiserror::Error, PartialEq)]
let config = match std::fs::read_to_string(CONFIG_PATH) { 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<Config> {
let config = match std::fs::read_to_string(config_path) {
Ok(s) => toml::from_str(&s).unwrap(), Ok(s) => toml::from_str(&s).unwrap(),
Err(e) if e.kind() == ErrorKind::NotFound => { Err(e) if e.kind() == ErrorKind::NotFound => {
let config = Config::default(); 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 config
} }
Err(e) => { Err(e) => {
@ -230,17 +148,25 @@ pub fn load_config() -> std::io::Result<Config> {
let mut last_point: Option<&MatrixPoint> = None; 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 { if matrix_point.speed < 0f64 {
log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed); 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 { if matrix_point.speed > 100f64 {
log::error!( log::error!(
"Fan speed can't be above 100.0 found {}", "Fan speed can't be above 100.0 found {}",
matrix_point.speed 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 let Some(last_point) = last_point {
if matrix_point.speed < last_point.speed { if matrix_point.speed < last_point.speed {
@ -249,7 +175,13 @@ pub fn load_config() -> std::io::Result<Config> {
last_point.speed, last_point.speed,
matrix_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 { if matrix_point.temp < last_point.temp {
log::error!( log::error!(
@ -257,7 +189,13 @@ pub fn load_config() -> std::io::Result<Config> {
last_point.temp, last_point.temp,
matrix_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<Config> {
#[cfg(test)] #[cfg(test)]
mod parse_config { mod parse_config {
use super::*; use crate::config::TempInput;
use amdgpu::{AmdGpuError, Card};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize, PartialEq, Debug)] #[derive(Deserialize, PartialEq, Debug)]
@ -291,6 +230,35 @@ mod parse_config {
fn toml_card0() { fn toml_card0() {
assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) })) assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) }))
} }
#[test]
fn parse_invalid_temp_input() {
assert_eq!(
"".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("".to_string()))
);
assert_eq!(
"12".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("12".to_string()))
);
assert_eq!(
"temp12".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("temp12".to_string()))
);
assert_eq!(
"12_input".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("12_input".to_string()))
);
assert_eq!(
"temp_12_input".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("temp_12_input".to_string()))
);
}
#[test]
fn parse_valid_temp_input() {
assert_eq!("temp12_input".parse::<TempInput>(), Ok(TempInput(12)));
}
} }
#[cfg(test)] #[cfg(test)]

23
amdfand/src/error.rs Normal file
View File

@ -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),
}

109
amdfand/src/main.rs Normal file
View File

@ -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<T> = std::result::Result<T, AmdFanError>;
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<String>,
#[options(command)]
command: Option<command::FanCommand>,
}
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);
}
}
}

View File

@ -1,8 +1,9 @@
use crate::command::Fan;
use crate::AmdFanError;
use amdgpu::utils::{hw_mons, linear_map};
use std::str::FromStr; use std::str::FromStr;
use crate::config::Config; use crate::config::Config;
use crate::utils::hw_mons;
use crate::AmdFanError;
#[derive(Debug)] #[derive(Debug)]
pub enum MonitorFormat { pub enum MonitorFormat {
@ -37,18 +38,19 @@ pub struct Monitor {
} }
/// Start print cards temperature and fan speed /// 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 { match monitor.format {
MonitorFormat::Short => short(config), MonitorFormat::Short => short(config),
MonitorFormat::Verbose => verbose(config), MonitorFormat::Verbose => verbose(config),
} }
} }
pub fn verbose(config: Config) -> std::io::Result<()> { pub fn verbose(config: Config) -> crate::Result<()> {
let mut controllers = hw_mons(&config, true)?; let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
loop { loop {
print!("{esc}[2J{esc}[1;1H", esc = 27 as char); 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!("Card {:3}", hw_mon.card().to_string().replace("card", ""));
println!(" MIN | MAX | PWM | %"); println!(" MIN | MAX | PWM | %");
let min = hw_mon.pwm_min(); let min = hw_mon.pwm_min();
@ -60,7 +62,7 @@ pub fn verbose(config: Config) -> std::io::Result<()> {
hw_mon hw_mon
.pwm() .pwm()
.map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), .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, hw_mon.pwm().unwrap_or_default() as f64,
min as f64, min as f64,
max 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<()> { pub fn short(config: Config) -> crate::Result<()> {
let mut controllers = hw_mons(&config, true)?; let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
loop { loop {
print!("{esc}[2J{esc}[1;1H", esc = 27 as char); 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!( println!(
"Card {:3} | Temp | MIN | MAX | PWM | %", "Card {:3} | Temp | MIN | MAX | PWM | %",
hw_mon.card().to_string().replace("card", "") hw_mon.card().to_string().replace("card", "")
@ -103,7 +105,7 @@ pub fn short(config: Config) -> std::io::Result<()> {
min, min,
max, max,
hw_mon.pwm().unwrap_or_default(), hw_mon.pwm().unwrap_or_default(),
crate::utils::linear_map( linear_map(
hw_mon.pwm().unwrap_or_default() as f64, hw_mon.pwm().unwrap_or_default() as f64,
min as f64, min as f64,
max as f64, max as f64,

View File

@ -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);
}
}
}

View File

@ -1,18 +1,31 @@
use crate::command::Fan;
use crate::AmdFanError;
use amdgpu::utils::hw_mons;
use gumdrop::Options; use gumdrop::Options;
use crate::config::Config; use crate::config::Config;
use crate::io_err::not_found;
/// Start service which will change fan speed according to config and GPU temperature /// Start service which will change fan speed according to config and GPU temperature
pub fn run(config: Config) -> std::io::Result<()> { pub fn run(config: Config) -> crate::Result<()> {
let mut controllers = crate::utils::hw_mons(&config, true)?; let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
if controllers.is_empty() {
return Err(not_found()); if hw_mons.is_empty() {
return Err(AmdFanError::NoHwMonFound);
} }
let mut cache = std::collections::HashMap::new(); let mut cache = std::collections::HashMap::new();
loop { loop {
for hw_mon in controllers.iter_mut() { for hw_mon in hw_mons.iter_mut() {
let gpu_temp = hw_mon.max_gpu_temp().unwrap_or_default(); 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); log::debug!("Current {} temperature: {}", hw_mon.card(), gpu_temp);
let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64); let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64);

18
amdgpu/Cargo.toml Normal file
View File

@ -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" }

83
amdgpu/src/card.rs Normal file
View File

@ -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<Self, Self::Err> {
if !value.starts_with("card") {
return Err(AmdGpuError::CardInvalidPrefix);
}
if value.len() < 5 {
return Err(AmdGpuError::CardInputTooShort);
}
value[4..]
.parse::<u32>()
.map_err(|e| AmdGpuError::CardInvalidSuffix(format!("{:?}", e)))
.map(Card)
}
}
impl<'de> Deserialize<'de> for Card {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value.parse::<Card>() {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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
}
}

33
amdgpu/src/error.rs Normal file
View File

@ -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,
}
}
}

119
amdgpu/src/hw_mon.rs Normal file
View File

@ -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<String> {
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<R: std::str::FromStr>(&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<String> {
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<String> {
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<C: AsRef<[u8]>>(&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<HwMon> {
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))
}

46
amdgpu/src/lib.rs Normal file
View File

@ -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<T> = std::result::Result<T, AmdGpuError>;
#[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",
}
}
}

68
amdgpu/src/temp_input.rs Normal file
View File

@ -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<Self, Self::Err> {
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value.parse::<TempInput>() {
Ok(temp) => Ok(temp.0),
_ => unreachable!(),
}
}
}
deserializer
.deserialize_str(TempInputVisitor)
.map(|v| TempInput(v as u16))
}
}

View File

@ -1,6 +1,5 @@
use crate::config::{Card, Config};
use crate::hw_mon::HwMon; 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 /// linear mapping from the xrange to the yrange
pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 { 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<Vec<Card>> {
/// Wrap cards in HW Mon manipulator and /// Wrap cards in HW Mon manipulator and
/// filter cards so only amd and listed in config cards are accessible /// filter cards so only amd and listed in config cards are accessible
pub fn hw_mons(config: &Config, filter: bool) -> std::io::Result<Vec<HwMon>> { pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
Ok(read_cards()? Ok(read_cards()?
.into_iter() .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.is_amd() })
.filter(|hw_mon| !filter || hw_mon.name_is_amd()) .filter(|hw_mon| !filter || hw_mon.name_is_amd())
.collect()) .collect())

20
amdvold/Cargo.toml Normal file
View File

@ -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" }

View File

@ -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")

View File

@ -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(())
}

View File

@ -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<HardwareModule>,
#[options(help = "New GPU module frequency", free)]
frequency: Option<Frequency>,
#[options(help = "New GPU module voltage", free)]
voltage: Option<Voltage>,
#[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(())
}

432
amdvold/src/clock_state.rs Normal file
View File

@ -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<Self, Self::Err> {
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<Self, Self::Err> {
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<CurvePoint>,
pub engine_label_lowest: Option<Frequency>,
pub engine_label_highest: Option<Frequency>,
pub memory_label_lowest: Option<Frequency>,
pub memory_label_highest: Option<Frequency>,
}
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<Self, Self::Err> {
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<Chars<'line>>,
) -> 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<Chars>) -> std::result::Result<Frequency, ClockStateError> {
consume_white(chars);
chars
.take_while(|c| *c != ' ')
.collect::<String>()
.parse::<Frequency>()
}
fn consume_voltage(chars: &mut Peekable<Chars>) -> std::result::Result<Voltage, ClockStateError> {
consume_white(chars);
chars
.take_while(|c| *c != ' ')
.collect::<String>()
.parse::<Voltage>()
}
fn consume_white(chars: &mut Peekable<Chars>) {
while chars.peek().filter(|c| **c == ' ').is_some() {
let _ = chars.next();
}
}
fn parse_freq_line(line: &str) -> std::result::Result<Frequency, ClockStateError> {
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::<Frequency>(),
Err(ClockStateError::NotFrequency("".to_string()))
);
}
#[test]
fn parse_only_v_letter() {
assert_eq!(
"v".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("v".to_string()))
);
}
#[test]
fn parse_only_hz() {
assert_eq!(
"hz".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("hz".to_string()))
);
}
#[test]
fn parse_only_mhz() {
assert_eq!(
"Mhz".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("Mhz".to_string()))
);
}
#[test]
fn parse_0mhz() {
assert_eq!(
"0Mhz".parse::<Frequency>(),
Ok(Frequency {
value: 0,
unit: "Mhz".to_string(),
})
);
}
#[test]
fn parse_0khz() {
assert_eq!(
"0khz".parse::<Frequency>(),
Ok(Frequency {
value: 0,
unit: "khz".to_string(),
})
);
}
#[test]
fn parse_0kz() {
assert_eq!(
"0hz".parse::<Frequency>(),
Ok(Frequency {
value: 0,
unit: "hz".to_string(),
})
);
}
#[test]
fn parse_123mhz() {
assert_eq!(
"123Mhz".parse::<Frequency>(),
Ok(Frequency {
value: 123,
unit: "Mhz".to_string(),
})
);
}
#[test]
fn parse_123khz() {
assert_eq!(
"123khz".parse::<Frequency>(),
Ok(Frequency {
value: 123,
unit: "khz".to_string(),
})
);
}
#[test]
fn parse_123kz() {
assert_eq!(
"123hz".parse::<Frequency>(),
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::<ClockState>();
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
})
);
}
}

96
amdvold/src/command.rs Normal file
View File

@ -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<Self, Self::Err> {
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<HwMon>, config: &Config) -> Vec<Self> {
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<ClockState> {
let state = self.device_read("pp_od_clk_voltage")?.parse()?;
Ok(state)
}
}

40
amdvold/src/config.rs Normal file
View File

@ -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<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!();
}
};
Ok(config)
}

22
amdvold/src/error.rs Normal file
View File

@ -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),
}

88
amdvold/src/main.rs Normal file
View File

@ -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<T> = std::result::Result<T, VoltageError>;
#[derive(gumdrop::Options)]
pub struct Opts {
#[options(help = "Help message")]
help: bool,
#[options(help = "Print version")]
version: bool,
#[options(help = "Config location")]
config: Option<String>,
#[options(command)]
command: Option<command::VoltageCommand>,
}
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);
}
}
}

View File

@ -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(())
}

13
amdvold/src/setup_info.rs Normal file
View File

@ -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(())
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=amdfan controller Description=AMD GPU fan daemon
After=sysinit.target local-fs.target After=sysinit.target local-fs.target
[Service] [Service]
Restart=on-failure Restart=on-failure

11
services/amdvold Executable file
View File

@ -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
}

9
services/amdvold.service Normal file
View File

@ -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

View File

@ -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),
}

View File

@ -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<u32>,
/// Maximal modulation (between 0-255)
pwm_max: Option<u32>,
/// List of available temperature inputs for current HW MOD
temp_inputs: Vec<String>,
/// Preferred temperature input
temp_input: Option<String>,
}
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<f64> {
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<f64>)> {
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<u64> {
self.read(name)?.parse::<u64>().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<String> {
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<u32> {
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<String> {
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<R: std::str::FromStr>(&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<String> {
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<HwMon> {
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))
}

View File

@ -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)
}

View File

@ -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<fan::FanCommand>,
}
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(())
}
}
}

View File

@ -1,9 +0,0 @@
#[derive(Debug, gumdrop::Options)]
pub enum VoltageCommand {
Placeholder(Placeholder),
}
#[derive(Debug, gumdrop::Options)]
pub struct Placeholder {
help: bool,
}