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:
parent
2220c3084f
commit
75737de396
49
Cargo.lock
generated
49
Cargo.lock
generated
@ -13,12 +13,39 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "amdfand"
|
name = "amdfand"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
|
dependencies = [
|
||||||
|
"amdgpu",
|
||||||
|
"gumdrop",
|
||||||
|
"log",
|
||||||
|
"pretty_env_logger",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "amdgpu"
|
||||||
|
version = "1.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gumdrop",
|
"gumdrop",
|
||||||
"log",
|
"log",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "amdvold"
|
||||||
|
version = "1.0.6"
|
||||||
|
dependencies = [
|
||||||
|
"amdgpu",
|
||||||
|
"gumdrop",
|
||||||
|
"log",
|
||||||
|
"pretty_env_logger",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -202,6 +229,26 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
|
20
Cargo.toml
20
Cargo.toml
@ -1,18 +1,2 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "amdfand"
|
members = ["amdfand", "amdgpu", "amdvold"]
|
||||||
version = "1.0.5"
|
|
||||||
edition = "2018"
|
|
||||||
description = "AMDGPU fan control service"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1.0.126", features = ["derive"] }
|
|
||||||
toml = { version = "0.5.8" }
|
|
||||||
|
|
||||||
gumdrop = { version = "0.8.0" }
|
|
||||||
|
|
||||||
log = { version = "0.4.14" }
|
|
||||||
pretty_env_logger = { version = "0.4.0" }
|
|
||||||
|
@ -31,7 +31,7 @@ sudo argonfand service # check amdgpu temperature and adjust speed from config f
|
|||||||
```toml
|
```toml
|
||||||
# /etc/amdfand/config.toml
|
# /etc/amdfand/config.toml
|
||||||
log_level = "Error"
|
log_level = "Error"
|
||||||
cards = ["card0"]
|
temp_input = "temp1_input"
|
||||||
|
|
||||||
[[speed_matrix]]
|
[[speed_matrix]]
|
||||||
temp = 4.0
|
temp = 4.0
|
||||||
|
20
amdfand/Cargo.toml
Normal file
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 gumdrop::Options;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::io_err::not_found;
|
use crate::{AmdFanError, FanMode};
|
||||||
use crate::FanMode;
|
|
||||||
|
|
||||||
/// Change card fan mode to either automatic or manual
|
/// Change card fan mode to either automatic or manual
|
||||||
pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result<()> {
|
pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> crate::Result<()> {
|
||||||
let mut hw_mons = crate::utils::hw_mons(&config, true)?;
|
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
|
||||||
|
|
||||||
let cards = match switcher.card {
|
let cards = match switcher.card {
|
||||||
Some(card_id) => match hw_mons.iter().position(|hw_mon| **hw_mon.card() == card_id) {
|
Some(card_id) => match hw_mons.iter().position(|hw_mon| **hw_mon.card() == card_id) {
|
||||||
@ -16,7 +17,7 @@ pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result
|
|||||||
for hw_mon in hw_mons {
|
for hw_mon in hw_mons {
|
||||||
eprintln!(" * {}", *hw_mon.card());
|
eprintln!(" * {}", *hw_mon.card());
|
||||||
}
|
}
|
||||||
return Err(not_found());
|
return Err(AmdFanError::NoAmdCardFound);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => hw_mons,
|
None => hw_mons,
|
||||||
@ -25,12 +26,12 @@ pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> std::io::Result
|
|||||||
for hw_mon in cards {
|
for hw_mon in cards {
|
||||||
match mode {
|
match mode {
|
||||||
FanMode::Automatic => {
|
FanMode::Automatic => {
|
||||||
if let Err(e) = hw_mon.set_automatic() {
|
if let Err(e) = hw_mon.write_automatic() {
|
||||||
log::error!("{:?}", e);
|
log::error!("{:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FanMode::Manual => {
|
FanMode::Manual => {
|
||||||
if let Err(e) = hw_mon.set_manual() {
|
if let Err(e) = hw_mon.write_manual() {
|
||||||
log::error!("{:?}", e);
|
log::error!("{:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
208
amdfand/src/command.rs
Normal file
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 amdgpu::utils::linear_map;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use amdgpu::{LogLevel, TempInput};
|
||||||
use std::fmt::Formatter;
|
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct Card(pub u32);
|
|
||||||
|
|
||||||
impl std::fmt::Display for Card {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&format!("card{}", self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Card {
|
|
||||||
type Err = AmdFanError;
|
|
||||||
|
|
||||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
||||||
if !value.starts_with("card") {
|
|
||||||
return Err(AmdFanError::InvalidPrefix);
|
|
||||||
}
|
|
||||||
if value.len() < 5 {
|
|
||||||
return Err(AmdFanError::InputTooShort);
|
|
||||||
}
|
|
||||||
value[4..]
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|e| AmdFanError::InvalidSuffix(format!("{:?}", e)))
|
|
||||||
.map(Card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Card {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::{self, Visitor};
|
|
||||||
|
|
||||||
struct CardVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for CardVisitor {
|
|
||||||
type Value = u32;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("must have format cardX")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
match value.parse::<Card>() {
|
|
||||||
Ok(card) => Ok(*card),
|
|
||||||
Err(AmdFanError::InvalidPrefix) => {
|
|
||||||
Err(E::custom(format!("expect cardX but got {}", value)))
|
|
||||||
}
|
|
||||||
Err(AmdFanError::InvalidSuffix(s)) => Err(E::custom(s)),
|
|
||||||
Err(AmdFanError::InputTooShort) => Err(E::custom(format!(
|
|
||||||
"{:?} must have at least 5 characters",
|
|
||||||
value
|
|
||||||
))),
|
|
||||||
Err(AmdFanError::NotAmdCard) => {
|
|
||||||
Err(E::custom(format!("{} is not an AMD GPU", value)))
|
|
||||||
}
|
|
||||||
Err(AmdFanError::FailedReadVendor) => Err(E::custom(format!(
|
|
||||||
"Failed to read vendor file for {}",
|
|
||||||
value
|
|
||||||
))),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deserializer.deserialize_str(CardVisitor).map(Card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Card {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for Card {
|
|
||||||
type Target = u32;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct MatrixPoint {
|
pub struct MatrixPoint {
|
||||||
pub temp: f64,
|
pub temp: f64,
|
||||||
pub speed: f64,
|
pub speed: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
pub enum LogLevel {
|
|
||||||
/// A level lower than all log levels.
|
|
||||||
Off,
|
|
||||||
/// Corresponds to the `Error` log level.
|
|
||||||
Error,
|
|
||||||
/// Corresponds to the `Warn` log level.
|
|
||||||
Warn,
|
|
||||||
/// Corresponds to the `Info` log level.
|
|
||||||
Info,
|
|
||||||
/// Corresponds to the `Debug` log level.
|
|
||||||
Debug,
|
|
||||||
/// Corresponds to the `Trace` log level.
|
|
||||||
Trace,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogLevel {
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
LogLevel::Off => "OFF",
|
|
||||||
LogLevel::Error => "ERROR",
|
|
||||||
LogLevel::Warn => "WARN",
|
|
||||||
LogLevel::Info => "INFO",
|
|
||||||
LogLevel::Debug => "DEBUG",
|
|
||||||
LogLevel::Trace => "TRACE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
cards: Option<Vec<String>>,
|
||||||
log_level: LogLevel,
|
log_level: LogLevel,
|
||||||
speed_matrix: Vec<MatrixPoint>,
|
speed_matrix: Vec<MatrixPoint>,
|
||||||
temp_input: Option<String>,
|
/// One of temperature inputs /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input
|
||||||
|
/// If nothing is provided higher reading will be taken (this is not good!)
|
||||||
|
temp_input: Option<TempInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
#[deprecated(
|
||||||
|
since = "1.0.6",
|
||||||
|
note = "Multi-card used is halted until we will have PC with multiple AMD GPU"
|
||||||
|
)]
|
||||||
|
pub fn cards(&self) -> Option<&Vec<String>> {
|
||||||
|
self.cards.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn speed_for_temp(&self, temp: f64) -> f64 {
|
pub fn speed_for_temp(&self, temp: f64) -> f64 {
|
||||||
let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
|
let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
|
||||||
Some(idx) => idx,
|
Some(idx) => idx,
|
||||||
@ -145,7 +37,7 @@ impl Config {
|
|||||||
return self.max_speed();
|
return self.max_speed();
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::utils::linear_map(
|
linear_map(
|
||||||
temp,
|
temp,
|
||||||
self.speed_matrix[idx].temp,
|
self.speed_matrix[idx].temp,
|
||||||
self.speed_matrix[idx + 1].temp,
|
self.speed_matrix[idx + 1].temp,
|
||||||
@ -158,8 +50,8 @@ impl Config {
|
|||||||
self.log_level
|
self.log_level
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn temp_input(&self) -> Option<&str> {
|
pub fn temp_input(&self) -> Option<&TempInput> {
|
||||||
self.temp_input.as_deref()
|
self.temp_input.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn min_speed(&self) -> f64 {
|
fn min_speed(&self) -> f64 {
|
||||||
@ -174,6 +66,8 @@ impl Config {
|
|||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
cards: None,
|
||||||
log_level: LogLevel::Error,
|
log_level: LogLevel::Error,
|
||||||
speed_matrix: vec![
|
speed_matrix: vec![
|
||||||
MatrixPoint {
|
MatrixPoint {
|
||||||
@ -209,17 +103,41 @@ impl Default for Config {
|
|||||||
speed: 100f64,
|
speed: 100f64,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
temp_input: Some(String::from("temp1_input")),
|
temp_input: Some(TempInput(1)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config() -> std::io::Result<Config> {
|
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||||
let config = match std::fs::read_to_string(CONFIG_PATH) {
|
pub enum ConfigError {
|
||||||
|
#[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")]
|
||||||
|
FanSpeedTooLow { value: f64, index: usize },
|
||||||
|
#[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")]
|
||||||
|
FanSpeedTooHigh { value: f64, index: usize },
|
||||||
|
#[error(
|
||||||
|
"Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
|
||||||
|
)]
|
||||||
|
UnsortedFanSpeed {
|
||||||
|
current: f64,
|
||||||
|
index: usize,
|
||||||
|
last: f64,
|
||||||
|
},
|
||||||
|
#[error(
|
||||||
|
"Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
|
||||||
|
)]
|
||||||
|
UnsortedFanTemp {
|
||||||
|
current: f64,
|
||||||
|
index: usize,
|
||||||
|
last: f64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config(config_path: &str) -> crate::Result<Config> {
|
||||||
|
let config = match std::fs::read_to_string(config_path) {
|
||||||
Ok(s) => toml::from_str(&s).unwrap(),
|
Ok(s) => toml::from_str(&s).unwrap(),
|
||||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
std::fs::write(CONFIG_PATH, toml::to_string(&config).unwrap())?;
|
std::fs::write(config_path, toml::to_string(&config).unwrap())?;
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -230,17 +148,25 @@ pub fn load_config() -> std::io::Result<Config> {
|
|||||||
|
|
||||||
let mut last_point: Option<&MatrixPoint> = None;
|
let mut last_point: Option<&MatrixPoint> = None;
|
||||||
|
|
||||||
for matrix_point in config.speed_matrix.iter() {
|
for (index, matrix_point) in config.speed_matrix.iter().enumerate() {
|
||||||
if matrix_point.speed < 0f64 {
|
if matrix_point.speed < 0f64 {
|
||||||
log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed);
|
log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed);
|
||||||
return Err(std::io::Error::from(ErrorKind::InvalidData));
|
return Err(ConfigError::FanSpeedTooLow {
|
||||||
|
value: matrix_point.speed,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
if matrix_point.speed > 100f64 {
|
if matrix_point.speed > 100f64 {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Fan speed can't be above 100.0 found {}",
|
"Fan speed can't be above 100.0 found {}",
|
||||||
matrix_point.speed
|
matrix_point.speed
|
||||||
);
|
);
|
||||||
return Err(std::io::Error::from(ErrorKind::InvalidData));
|
return Err(ConfigError::FanSpeedTooHigh {
|
||||||
|
value: matrix_point.speed,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
if let Some(last_point) = last_point {
|
if let Some(last_point) = last_point {
|
||||||
if matrix_point.speed < last_point.speed {
|
if matrix_point.speed < last_point.speed {
|
||||||
@ -249,7 +175,13 @@ pub fn load_config() -> std::io::Result<Config> {
|
|||||||
last_point.speed,
|
last_point.speed,
|
||||||
matrix_point.speed
|
matrix_point.speed
|
||||||
);
|
);
|
||||||
return Err(std::io::Error::from(ErrorKind::InvalidData));
|
|
||||||
|
return Err(ConfigError::UnsortedFanSpeed {
|
||||||
|
current: matrix_point.speed,
|
||||||
|
last: last_point.speed,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
if matrix_point.temp < last_point.temp {
|
if matrix_point.temp < last_point.temp {
|
||||||
log::error!(
|
log::error!(
|
||||||
@ -257,7 +189,13 @@ pub fn load_config() -> std::io::Result<Config> {
|
|||||||
last_point.temp,
|
last_point.temp,
|
||||||
matrix_point.temp
|
matrix_point.temp
|
||||||
);
|
);
|
||||||
return Err(std::io::Error::from(ErrorKind::InvalidData));
|
|
||||||
|
return Err(ConfigError::UnsortedFanTemp {
|
||||||
|
current: matrix_point.temp,
|
||||||
|
last: last_point.temp,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +207,8 @@ pub fn load_config() -> std::io::Result<Config> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod parse_config {
|
mod parse_config {
|
||||||
use super::*;
|
use crate::config::TempInput;
|
||||||
|
use amdgpu::{AmdGpuError, Card};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize, PartialEq, Debug)]
|
#[derive(Deserialize, PartialEq, Debug)]
|
||||||
@ -291,6 +230,35 @@ mod parse_config {
|
|||||||
fn toml_card0() {
|
fn toml_card0() {
|
||||||
assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) }))
|
assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_invalid_temp_input() {
|
||||||
|
assert_eq!(
|
||||||
|
"".parse::<TempInput>(),
|
||||||
|
Err(AmdGpuError::InvalidTempInput("".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"12".parse::<TempInput>(),
|
||||||
|
Err(AmdGpuError::InvalidTempInput("12".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"temp12".parse::<TempInput>(),
|
||||||
|
Err(AmdGpuError::InvalidTempInput("temp12".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"12_input".parse::<TempInput>(),
|
||||||
|
Err(AmdGpuError::InvalidTempInput("12_input".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"temp_12_input".parse::<TempInput>(),
|
||||||
|
Err(AmdGpuError::InvalidTempInput("temp_12_input".to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_valid_temp_input() {
|
||||||
|
assert_eq!("temp12_input".parse::<TempInput>(), Ok(TempInput(12)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
23
amdfand/src/error.rs
Normal file
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 std::str::FromStr;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::utils::hw_mons;
|
|
||||||
use crate::AmdFanError;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum MonitorFormat {
|
pub enum MonitorFormat {
|
||||||
@ -37,18 +38,19 @@ pub struct Monitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start print cards temperature and fan speed
|
/// Start print cards temperature and fan speed
|
||||||
pub fn run(monitor: Monitor, config: Config) -> std::io::Result<()> {
|
pub fn run(monitor: Monitor, config: Config) -> crate::Result<()> {
|
||||||
match monitor.format {
|
match monitor.format {
|
||||||
MonitorFormat::Short => short(config),
|
MonitorFormat::Short => short(config),
|
||||||
MonitorFormat::Verbose => verbose(config),
|
MonitorFormat::Verbose => verbose(config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verbose(config: Config) -> std::io::Result<()> {
|
pub fn verbose(config: Config) -> crate::Result<()> {
|
||||||
let mut controllers = hw_mons(&config, true)?;
|
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
||||||
for hw_mon in controllers.iter_mut() {
|
for hw_mon in hw_mons.iter_mut() {
|
||||||
println!("Card {:3}", hw_mon.card().to_string().replace("card", ""));
|
println!("Card {:3}", hw_mon.card().to_string().replace("card", ""));
|
||||||
println!(" MIN | MAX | PWM | %");
|
println!(" MIN | MAX | PWM | %");
|
||||||
let min = hw_mon.pwm_min();
|
let min = hw_mon.pwm_min();
|
||||||
@ -60,7 +62,7 @@ pub fn verbose(config: Config) -> std::io::Result<()> {
|
|||||||
hw_mon
|
hw_mon
|
||||||
.pwm()
|
.pwm()
|
||||||
.map_or_else(|_e| String::from("FAILED"), |f| f.to_string()),
|
.map_or_else(|_e| String::from("FAILED"), |f| f.to_string()),
|
||||||
(crate::utils::linear_map(
|
(linear_map(
|
||||||
hw_mon.pwm().unwrap_or_default() as f64,
|
hw_mon.pwm().unwrap_or_default() as f64,
|
||||||
min as f64,
|
min as f64,
|
||||||
max as f64,
|
max as f64,
|
||||||
@ -86,11 +88,11 @@ pub fn verbose(config: Config) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn short(config: Config) -> std::io::Result<()> {
|
pub fn short(config: Config) -> crate::Result<()> {
|
||||||
let mut controllers = hw_mons(&config, true)?;
|
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
|
||||||
loop {
|
loop {
|
||||||
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
||||||
for hw_mon in controllers.iter_mut() {
|
for hw_mon in hw_mons.iter_mut() {
|
||||||
println!(
|
println!(
|
||||||
"Card {:3} | Temp | MIN | MAX | PWM | %",
|
"Card {:3} | Temp | MIN | MAX | PWM | %",
|
||||||
hw_mon.card().to_string().replace("card", "")
|
hw_mon.card().to_string().replace("card", "")
|
||||||
@ -103,7 +105,7 @@ pub fn short(config: Config) -> std::io::Result<()> {
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
hw_mon.pwm().unwrap_or_default(),
|
hw_mon.pwm().unwrap_or_default(),
|
||||||
crate::utils::linear_map(
|
linear_map(
|
||||||
hw_mon.pwm().unwrap_or_default() as f64,
|
hw_mon.pwm().unwrap_or_default() as f64,
|
||||||
min as f64,
|
min as f64,
|
||||||
max as f64,
|
max as f64,
|
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 gumdrop::Options;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::io_err::not_found;
|
|
||||||
|
|
||||||
/// Start service which will change fan speed according to config and GPU temperature
|
/// Start service which will change fan speed according to config and GPU temperature
|
||||||
pub fn run(config: Config) -> std::io::Result<()> {
|
pub fn run(config: Config) -> crate::Result<()> {
|
||||||
let mut controllers = crate::utils::hw_mons(&config, true)?;
|
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
|
||||||
if controllers.is_empty() {
|
|
||||||
return Err(not_found());
|
if hw_mons.is_empty() {
|
||||||
|
return Err(AmdFanError::NoHwMonFound);
|
||||||
}
|
}
|
||||||
let mut cache = std::collections::HashMap::new();
|
let mut cache = std::collections::HashMap::new();
|
||||||
loop {
|
loop {
|
||||||
for hw_mon in controllers.iter_mut() {
|
for hw_mon in hw_mons.iter_mut() {
|
||||||
let gpu_temp = hw_mon.max_gpu_temp().unwrap_or_default();
|
let gpu_temp = config
|
||||||
|
.temp_input()
|
||||||
|
.and_then(|input| {
|
||||||
|
hw_mon
|
||||||
|
.read_gpu_temp(&input.as_string())
|
||||||
|
.map(|temp| temp as f64 / 1000f64)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.or_else(|| hw_mon.max_gpu_temp().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
log::debug!("Current {} temperature: {}", hw_mon.card(), gpu_temp);
|
log::debug!("Current {} temperature: {}", hw_mon.card(), gpu_temp);
|
||||||
let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64);
|
let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64);
|
||||||
|
|
18
amdgpu/Cargo.toml
Normal file
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::HwMon;
|
||||||
use crate::{hw_mon, ROOT_DIR};
|
use crate::{hw_mon, Card, ROOT_DIR};
|
||||||
|
|
||||||
/// linear mapping from the xrange to the yrange
|
/// linear mapping from the xrange to the yrange
|
||||||
pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 {
|
pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 {
|
||||||
@ -20,10 +19,10 @@ pub fn read_cards() -> std::io::Result<Vec<Card>> {
|
|||||||
|
|
||||||
/// Wrap cards in HW Mon manipulator and
|
/// Wrap cards in HW Mon manipulator and
|
||||||
/// filter cards so only amd and listed in config cards are accessible
|
/// filter cards so only amd and listed in config cards are accessible
|
||||||
pub fn hw_mons(config: &Config, filter: bool) -> std::io::Result<Vec<HwMon>> {
|
pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
|
||||||
Ok(read_cards()?
|
Ok(read_cards()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|card| hw_mon::open_hw_mon(card, config).ok())
|
.filter_map(|card| hw_mon::open_hw_mon(card).ok())
|
||||||
.filter(|hw_mon| !filter || { hw_mon.is_amd() })
|
.filter(|hw_mon| !filter || { hw_mon.is_amd() })
|
||||||
.filter(|hw_mon| !filter || hw_mon.name_is_amd())
|
.filter(|hw_mon| !filter || hw_mon.name_is_amd())
|
||||||
.collect())
|
.collect())
|
20
amdvold/Cargo.toml
Normal file
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]
|
[Unit]
|
||||||
Description=amdfan controller
|
Description=AMD GPU fan daemon
|
||||||
After=sysinit.target local-fs.target
|
After=sysinit.target local-fs.target
|
||||||
[Service]
|
[Service]
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
11
services/amdvold
Executable file
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