Better error messages, better error handling, improve temp config, move forward with voltage

Reorganize code, add basic voltage manipulation, fix errors

Reorganize code, add basic voltage manipulation, fix errors
This commit is contained in:
Adrian Woźniak 2021-11-17 17:28:09 +01:00 committed by Adrian Woźniak
parent 2220c3084f
commit 75737de396
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
42 changed files with 1939 additions and 572 deletions

49
Cargo.lock generated
View File

@ -13,12 +13,39 @@ dependencies = [
[[package]]
name = "amdfand"
version = "1.0.5"
version = "1.0.6"
dependencies = [
"amdgpu",
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"thiserror",
"toml",
]
[[package]]
name = "amdgpu"
version = "1.0.6"
dependencies = [
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"thiserror",
"toml",
]
[[package]]
name = "amdvold"
version = "1.0.6"
dependencies = [
"amdgpu",
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"thiserror",
"toml",
]
@ -202,6 +229,26 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.5.8"

View File

@ -1,18 +1,2 @@
[package]
name = "amdfand"
version = "1.0.5"
edition = "2018"
description = "AMDGPU fan control service"
license = "MIT OR Apache-2.0"
keywords = ["hardware", "amdgpu"]
categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[dependencies]
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
gumdrop = { version = "0.8.0" }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
[workspace]
members = ["amdfand", "amdgpu", "amdvold"]

View File

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

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

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 serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::Formatter;
use amdgpu::utils::linear_map;
use amdgpu::{LogLevel, TempInput};
use std::io::ErrorKind;
use std::str::FromStr;
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Card(pub u32);
impl std::fmt::Display for Card {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("card{}", self.0))
}
}
impl FromStr for Card {
type Err = AmdFanError;
fn from_str(value: &str) -> Result<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)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct MatrixPoint {
pub temp: f64,
pub speed: f64,
}
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
pub enum LogLevel {
/// A level lower than all log levels.
Off,
/// Corresponds to the `Error` log level.
Error,
/// Corresponds to the `Warn` log level.
Warn,
/// Corresponds to the `Info` log level.
Info,
/// Corresponds to the `Debug` log level.
Debug,
/// Corresponds to the `Trace` log level.
Trace,
}
impl LogLevel {
pub fn as_str(&self) -> &str {
match self {
LogLevel::Off => "OFF",
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
LogLevel::Debug => "DEBUG",
LogLevel::Trace => "TRACE",
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Config {
cards: Option<Vec<String>>,
log_level: LogLevel,
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 {
#[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 {
let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
Some(idx) => idx,
@ -145,7 +37,7 @@ impl Config {
return self.max_speed();
}
crate::utils::linear_map(
linear_map(
temp,
self.speed_matrix[idx].temp,
self.speed_matrix[idx + 1].temp,
@ -158,8 +50,8 @@ impl Config {
self.log_level
}
pub fn temp_input(&self) -> Option<&str> {
self.temp_input.as_deref()
pub fn temp_input(&self) -> Option<&TempInput> {
self.temp_input.as_ref()
}
fn min_speed(&self) -> f64 {
@ -174,6 +66,8 @@ impl Config {
impl Default for Config {
fn default() -> Self {
Self {
#[allow(deprecated)]
cards: None,
log_level: LogLevel::Error,
speed_matrix: vec![
MatrixPoint {
@ -209,17 +103,41 @@ impl Default for Config {
speed: 100f64,
},
],
temp_input: Some(String::from("temp1_input")),
temp_input: Some(TempInput(1)),
}
}
}
pub fn load_config() -> std::io::Result<Config> {
let config = match std::fs::read_to_string(CONFIG_PATH) {
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum ConfigError {
#[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")]
FanSpeedTooLow { value: f64, index: usize },
#[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")]
FanSpeedTooHigh { value: f64, index: usize },
#[error(
"Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
)]
UnsortedFanSpeed {
current: f64,
index: usize,
last: f64,
},
#[error(
"Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
)]
UnsortedFanTemp {
current: f64,
index: usize,
last: f64,
},
}
pub fn load_config(config_path: &str) -> crate::Result<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())?;
std::fs::write(config_path, toml::to_string(&config).unwrap())?;
config
}
Err(e) => {
@ -230,17 +148,25 @@ pub fn load_config() -> std::io::Result<Config> {
let mut last_point: Option<&MatrixPoint> = None;
for matrix_point in config.speed_matrix.iter() {
for (index, matrix_point) in config.speed_matrix.iter().enumerate() {
if matrix_point.speed < 0f64 {
log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed);
return Err(std::io::Error::from(ErrorKind::InvalidData));
return Err(ConfigError::FanSpeedTooLow {
value: matrix_point.speed,
index,
}
.into());
}
if matrix_point.speed > 100f64 {
log::error!(
"Fan speed can't be above 100.0 found {}",
matrix_point.speed
);
return Err(std::io::Error::from(ErrorKind::InvalidData));
return Err(ConfigError::FanSpeedTooHigh {
value: matrix_point.speed,
index,
}
.into());
}
if let Some(last_point) = last_point {
if matrix_point.speed < last_point.speed {
@ -249,7 +175,13 @@ pub fn load_config() -> std::io::Result<Config> {
last_point.speed,
matrix_point.speed
);
return Err(std::io::Error::from(ErrorKind::InvalidData));
return Err(ConfigError::UnsortedFanSpeed {
current: matrix_point.speed,
last: last_point.speed,
index,
}
.into());
}
if matrix_point.temp < last_point.temp {
log::error!(
@ -257,7 +189,13 @@ pub fn load_config() -> std::io::Result<Config> {
last_point.temp,
matrix_point.temp
);
return Err(std::io::Error::from(ErrorKind::InvalidData));
return Err(ConfigError::UnsortedFanTemp {
current: matrix_point.temp,
last: last_point.temp,
index,
}
.into());
}
}
@ -269,7 +207,8 @@ pub fn load_config() -> std::io::Result<Config> {
#[cfg(test)]
mod parse_config {
use super::*;
use crate::config::TempInput;
use amdgpu::{AmdGpuError, Card};
use serde::Deserialize;
#[derive(Deserialize, PartialEq, Debug)]
@ -291,6 +230,35 @@ mod parse_config {
fn toml_card0() {
assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) }))
}
#[test]
fn parse_invalid_temp_input() {
assert_eq!(
"".parse::<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)]

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

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

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

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]
Description=amdfan controller
Description=AMD GPU fan daemon
After=sysinit.target local-fs.target
[Service]
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,
}