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:
commit
4c0cea5f33
49
Cargo.lock
generated
49
Cargo.lock
generated
@ -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"
|
||||
|
20
Cargo.toml
20
Cargo.toml
@ -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"]
|
||||
|
@ -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
20
amdfand/Cargo.toml
Normal 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" }
|
@ -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
208
amdfand/src/command.rs
Normal 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()
|
||||
}
|
@ -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
23
amdfand/src/error.rs
Normal 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
109
amdfand/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
18
amdfand/src/panic_handler.rs
Normal file
18
amdfand/src/panic_handler.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
18
amdgpu/Cargo.toml
Normal 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
83
amdgpu/src/card.rs
Normal 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
33
amdgpu/src/error.rs
Normal 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
119
amdgpu/src/hw_mon.rs
Normal 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
46
amdgpu/src/lib.rs
Normal 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
68
amdgpu/src/temp_input.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -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
20
amdvold/Cargo.toml
Normal 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" }
|
13
amdvold/assets/enable_voltage_info.txt
Normal file
13
amdvold/assets/enable_voltage_info.txt
Normal 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")
|
18
amdvold/src/apply_changes.rs
Normal file
18
amdvold/src/apply_changes.rs
Normal 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(())
|
||||
}
|
56
amdvold/src/change_state.rs
Normal file
56
amdvold/src/change_state.rs
Normal 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
432
amdvold/src/clock_state.rs
Normal 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
96
amdvold/src/command.rs
Normal 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
40
amdvold/src/config.rs
Normal 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
22
amdvold/src/error.rs
Normal 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
88
amdvold/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
41
amdvold/src/print_states.rs
Normal file
41
amdvold/src/print_states.rs
Normal 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
13
amdvold/src/setup_info.rs
Normal 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(())
|
||||
}
|
34
examples/cards_config.toml
Normal file
34
examples/cards_config.toml
Normal 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
|
34
examples/default_config.toml
Normal file
34
examples/default_config.toml
Normal 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
|
33
examples/unsorted_speed_config.toml
Normal file
33
examples/unsorted_speed_config.toml
Normal 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
|
33
examples/unsorted_temp_config.toml
Normal file
33
examples/unsorted_temp_config.toml
Normal 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
|
@ -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
11
services/amdvold
Executable 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
9
services/amdvold.service
Normal 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
|
@ -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),
|
||||
}
|
231
src/hw_mon.rs
231
src/hw_mon.rs
@ -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))
|
||||
}
|
@ -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)
|
||||
}
|
108
src/main.rs
108
src/main.rs
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
#[derive(Debug, gumdrop::Options)]
|
||||
pub enum VoltageCommand {
|
||||
Placeholder(Placeholder),
|
||||
}
|
||||
|
||||
#[derive(Debug, gumdrop::Options)]
|
||||
pub struct Placeholder {
|
||||
help: bool,
|
||||
}
|
Loading…
Reference in New Issue
Block a user