Merge pull request #25 from Eraden/readable-errors-and-voltage
Better error messages, better error handling, improve temp config, move forward with voltage
This commit is contained in:
commit
4c0cea5f33
49
Cargo.lock
generated
49
Cargo.lock
generated
@ -13,12 +13,39 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[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