Improve monitor

This commit is contained in:
Adrian Woźniak 2021-12-03 07:35:17 +01:00
parent 7dcb6e08cf
commit 5698ab15d8
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
46 changed files with 1108 additions and 341 deletions

View File

@ -14,28 +14,28 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-18.04, ubuntu-20.04] os: [ ubuntu-18.04, ubuntu-20.04 ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Add target - name: Add target
run: rustup target install x86_64-unknown-linux-musl run: rustup target install x86_64-unknown-linux-musl
- name: Run clippy - name: Run clippy
run: cargo clippy -- -D warnings run: cargo clippy -- -D warnings
- name: Run fmt check - name: Run fmt check
run: cargo fmt -- --check run: cargo fmt -- --check
- name: Run tests - name: Run tests
run: cargo test --verbose run: cargo test --verbose
- name: Install binary compressor - name: Install binary compressor
run: sudo apt-get update && sudo apt-get install upx-ucl run: sudo apt-get update && sudo apt-get install upx-ucl
- name: Build - name: Build
run: cargo build --release --verbose --target=x86_64-unknown-linux-musl && strip ./target/x86_64-unknown-linux-musl/release/amdfand && upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand run: cargo build --release --verbose --target=x86_64-unknown-linux-musl && strip ./target/x86_64-unknown-linux-musl/release/amdfand && upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.4 uses: actions/upload-artifact@v2.2.4
with: with:
# Artifact name # Artifact name
name: amdfand-${{ matrix.os }} name: amdfand-${{ matrix.os }}
# A file, directory or wildcard pattern that describes what to upload # A file, directory or wildcard pattern that describes what to upload
path: ./target/x86_64-unknown-linux-musl/release/amdfand path: ./target/x86_64-unknown-linux-musl/release/amdfand
# The desired behavior if no files are found using the provided path. # The desired behavior if no files are found using the provided path.

154
Cargo.lock generated
View File

@ -13,9 +13,10 @@ dependencies = [
[[package]] [[package]]
name = "amdfand" name = "amdfand"
version = "1.0.7" version = "1.0.8"
dependencies = [ dependencies = [
"amdgpu", "amdgpu",
"amdgpu-config",
"gumdrop", "gumdrop",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
@ -26,7 +27,7 @@ dependencies = [
[[package]] [[package]]
name = "amdgpu" name = "amdgpu"
version = "1.0.7" version = "1.0.8"
dependencies = [ dependencies = [
"gumdrop", "gumdrop",
"log", "log",
@ -37,10 +38,41 @@ dependencies = [
] ]
[[package]] [[package]]
name = "amdvold" name = "amdgpu-config"
version = "1.0.7" version = "1.0.8"
dependencies = [ dependencies = [
"amdgpu", "amdgpu",
"csv",
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"thiserror",
"toml",
]
[[package]]
name = "amdmond"
version = "1.0.8"
dependencies = [
"amdgpu",
"amdgpu-config",
"chrono",
"csv",
"gumdrop",
"log",
"pretty_env_logger",
"serde",
"thiserror",
"toml",
]
[[package]]
name = "amdvold"
version = "1.0.8"
dependencies = [
"amdgpu",
"amdgpu-config",
"gumdrop", "gumdrop",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
@ -60,12 +92,66 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"time",
"winapi",
]
[[package]]
name = "csv"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [
"bstr",
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.7.1" version = "0.7.1"
@ -117,6 +203,18 @@ dependencies = [
"quick-error", "quick-error",
] ]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.97" version = "0.2.97"
@ -138,6 +236,25 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "pretty_env_logger" name = "pretty_env_logger"
version = "0.4.0" version = "0.4.0"
@ -183,12 +300,24 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.25" version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "ryu"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.126" version = "1.0.126"
@ -249,6 +378,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.8" version = "0.5.8"
@ -264,6 +404,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -1,2 +1,2 @@
[workspace] [workspace]
members = ["amdfand", "amdgpu", "amdvold"] members = ["amdgpu", "amdgpu-config", "amdfand", "amdvold", "amdmond"]

View File

@ -2,20 +2,15 @@ MIT License
Copyright (c) 2021 Adrian Woźniak Copyright (c) 2021 Adrian Woźniak
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
of this software and associated documentation files (the "Software"), to deal documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
in the Software without restriction, including without limitation the rights rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell persons to whom the Software is furnished to do so, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
copies or substantial portions of the Software. Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,82 +1,24 @@
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud) ![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
# AMDGPU Fan control service # AMD GPU management tools
Available commands: This repository holds couple tools for AMD graphic cards
* `monitor` - Print current temp and fan speed * `amdfand` - fan speed daemon
* `service` - Set fan speed depends on GPU temperature * `amdvold` - voltage and overclocking tool
* `set-automatic` - Switch to GPU automatic fan speed control * `amdmond` - monitor daemon
* `set-manual` - Switch to GPU manual fan speed control
* `available` - Print available cards
#### amdfand set-automatic | set-manual [OPTIONS] For more information please check README each of them.
Optional arguments:
* -h, --help Help message
* -c, --card CARD GPU Card number
## Usage
```bash
cargo install argonfand
sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed
sudo argonfand service # check amdgpu temperature and adjust speed from config file
```
## Config file
```toml
# /etc/amdfand/config.toml
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
```
## Roadmap ## Roadmap
* [ ] Write metrics
* [ ] Add support for multiple cards * [ ] Add support for multiple cards
* Multiple services must recognize card even if there's multiple same version cards is installed * Multiple services must recognize card even if there's multiple same version cards is installed
* Support should be by using `--config` option * Support should be by using `--config` option
* [ ] CLI for fan config edit * [ ] CLI for fan config edit
* [ ] CLI for voltage edit * [ ] CLI for voltage edit
* [ ] GUI application using native Rust framework (ex. egui, druid) * [ ] GUI application using native Rust framework (ex. egui, druid)
## :bookmark: License ## :bookmark: License
This work is dual-licensed under Apache 2.0 and MIT. This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work.
You can choose between one of them if you use this work.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "amdfand" name = "amdfand"
version = "1.0.7" version = "1.0.8"
edition = "2018" edition = "2018"
description = "AMDGPU fan control service" description = "AMDGPU fan control service"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -9,7 +9,8 @@ categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud" repository = "https://github.com/Eraden/amdgpud"
[dependencies] [dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.7" } amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["fan"] }
serde = { version = "1.0.126", features = ["derive"] } serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" } toml = { version = "0.5.8" }
@ -21,3 +22,4 @@ pretty_env_logger = { version = "0.4.0" }
[dev-dependencies] [dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" } amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] }

66
amdfand/README.md Normal file
View File

@ -0,0 +1,66 @@
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
# AMDGPU Fan control service
Available commands:
* `service` - Set fan speed depends on GPU temperature
* `set-automatic` - Switch to GPU automatic fan speed control
* `set-manual` - Switch to GPU manual fan speed control
* `available` - Print available cards
#### amdfand set-automatic | set-manual [OPTIONS]
Optional arguments:
* -h, --help Help message
* -c, --card CARD GPU Card number
## Usage
```bash
cargo install argonfand
sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed
sudo argonfand service # check amdgpu temperature and adjust speed from config file
```
## Config file
```toml
# /etc/amdfand/config.toml
log_level = "Error"
temp_input = "temp1_input"
[[speed_matrix]]
temp = 4.0
speed = 4.0
[[speed_matrix]]
temp = 30.0
speed = 33.0
[[speed_matrix]]
temp = 45.0
speed = 50.0
[[speed_matrix]]
temp = 60.0
speed = 66.0
[[speed_matrix]]
temp = 65.0
speed = 69.0
[[speed_matrix]]
temp = 70.0
speed = 75.0
[[speed_matrix]]
temp = 75.0
speed = 89.0
[[speed_matrix]]
temp = 80.0
speed = 100.0
```

View File

@ -1,9 +1,10 @@
use crate::command::Fan;
use amdgpu::utils::hw_mons;
use gumdrop::Options; use gumdrop::Options;
use crate::config::Config; use amdgpu::utils::hw_mons;
use amdgpu_config::fan::Config;
use crate::{AmdFanError, FanMode}; use crate::{AmdFanError, FanMode};
use crate::command::Fan;
/// 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) -> crate::Result<()> { pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> crate::Result<()> {

View File

@ -1,23 +1,14 @@
use crate::{change_mode, monitor, service, Config};
use amdgpu::hw_mon::HwMon;
use amdgpu::utils::linear_map;
use amdgpu::TempInput;
use gumdrop::Options; use gumdrop::Options;
/// pulse width modulation fan control minimum level (0) use amdgpu::hw_mon::HwMon;
const PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; use amdgpu::utils::{linear_map, load_temp_inputs};
use amdgpu::{
TempInput, PULSE_WIDTH_MODULATION_AUTO, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN,
PULSE_WIDTH_MODULATION_MODE,
};
use amdgpu_config::fan::Config;
/// pulse width modulation fan control maximum level (255) use crate::{change_mode, service};
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)] #[derive(Debug, Options)]
pub struct AvailableCards { pub struct AvailableCards {
@ -27,8 +18,6 @@ pub struct AvailableCards {
#[derive(Debug, Options)] #[derive(Debug, Options)]
pub enum FanCommand { 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")] #[options(help = "Check AMD GPU temperature and change fan speed depends on configuration")]
Service(service::Service), Service(service::Service),
#[options(help = "Switch GPU to automatic fan speed control")] #[options(help = "Switch GPU to automatic fan speed control")]
@ -81,6 +70,8 @@ impl std::ops::DerefMut for Fan {
} }
} }
const MODULATION_ENABLED_FILE: &str = "pwm1_enable";
impl Fan { impl Fan {
pub fn wrap(hw_mon: HwMon, config: &Config) -> Self { pub fn wrap(hw_mon: HwMon, config: &Config) -> Self {
Self { Self {
@ -105,7 +96,7 @@ impl Fan {
} }
pub(crate) fn write_manual(&self) -> crate::Result<()> { pub(crate) fn write_manual(&self) -> crate::Result<()> {
self.hw_mon_write("pwm1_enable", 1) self.hw_mon_write(MODULATION_ENABLED_FILE, 1)
.map_err(FanError::ManualSpeedFailed)?; .map_err(FanError::ManualSpeedFailed)?;
Ok(()) Ok(())
} }
@ -124,27 +115,6 @@ impl Fan {
.map_err(|error| FanError::FailedToChangeSpeed { value, error })?; .map_err(|error| FanError::FailedToChangeSpeed { value, error })?;
Ok(()) 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 { pub fn is_fan_automatic(&self) -> bool {
self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE) self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE)
@ -170,19 +140,6 @@ impl Fan {
Ok(value) 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> { pub(crate) fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
let value = self let value = self
.hw_mon_read(name)? .hw_mon_read(name)?
@ -190,19 +147,18 @@ impl Fan {
.map_err(FanError::NonIntTemp)?; .map_err(FanError::NonIntTemp)?;
Ok(value) Ok(value)
} }
}
fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> { pub fn pwm_min(&mut self) -> u32 {
let dir = match std::fs::read_dir(hw_mon.mon_dir()) { if self.pwm_min.is_none() {
Ok(d) => d, self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
_ => return vec![], };
}; self.pwm_min.unwrap_or_default()
dir.filter_map(|f| f.ok()) }
.filter_map(|f| {
f.file_name() pub fn pwm_max(&mut self) -> u32 {
.to_str() if self.pwm_max.is_none() {
.filter(|s| s.starts_with("temp") && s.ends_with("_input")) self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
.map(String::from) };
}) self.pwm_max.unwrap_or(255)
.collect() }
} }

View File

@ -1,13 +1,12 @@
use crate::command::FanError;
use crate::config::ConfigError;
use amdgpu::AmdGpuError; use amdgpu::AmdGpuError;
use amdgpu_config::fan::ConfigError;
use crate::command::FanError;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AmdFanError { pub enum AmdFanError {
#[error("Vendor is not AMD")] #[error("Vendor is not AMD")]
NotAmdCard, 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")] #[error("No hwmod has been found in sysfs")]
NoHwMonFound, NoHwMonFound,
#[error("No AMD Card has been found in sysfs")] #[error("No AMD Card has been found in sysfs")]

View File

@ -1,23 +1,19 @@
extern crate log;
use gumdrop::Options;
use amdgpu::utils::{ensure_config_dir, hw_mons};
use amdgpu_config::fan::{load_config, Config, DEFAULT_FAN_CONFIG_PATH};
use crate::command::FanCommand; use crate::command::FanCommand;
use crate::error::AmdFanError; 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 change_mode;
mod command; mod command;
mod config;
mod error; mod error;
mod monitor;
mod panic_handler; mod panic_handler;
mod service; 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 type Result<T> = std::result::Result<T, AmdFanError>;
pub enum FanMode { pub enum FanMode {
@ -51,7 +47,6 @@ fn run(config: Config) -> Result<()> {
match opts.command { match opts.command {
None => service::run(config), None => service::run(config),
Some(FanCommand::Monitor(monitor)) => monitor::run(monitor, config),
Some(FanCommand::Service(_)) => service::run(config), Some(FanCommand::Service(_)) => service::run(config),
Some(FanCommand::SetAutomatic(switcher)) => { Some(FanCommand::SetAutomatic(switcher)) => {
change_mode::run(switcher, FanMode::Automatic, config) change_mode::run(switcher, FanMode::Automatic, config)
@ -78,13 +73,11 @@ fn setup() -> Result<(String, Config)> {
std::env::set_var("RUST_LOG", "DEBUG"); std::env::set_var("RUST_LOG", "DEBUG");
} }
pretty_env_logger::init(); pretty_env_logger::init();
if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { ensure_config_dir()?;
std::fs::create_dir_all(CONFIG_DIR)?;
}
let config_path = Opts::parse_args_default_or_exit() let config_path = Opts::parse_args_default_or_exit()
.config .config
.unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); .unwrap_or_else(|| DEFAULT_FAN_CONFIG_PATH.to_string());
let config = load_config(&config_path)?; let config = load_config(&config_path)?;
log::info!("{:?}", config); log::info!("{:?}", config);
log::set_max_level(config.log_level().as_str().parse().unwrap()); log::set_max_level(config.log_level().as_str().parse().unwrap());

View File

@ -1,6 +1,7 @@
use crate::command::Fan;
use amdgpu::utils::hw_mons; use amdgpu::utils::hw_mons;
use crate::command::Fan;
pub fn restore_automatic() { pub fn restore_automatic() {
for hw in hw_mons(true).unwrap_or_default() { for hw in hw_mons(true).unwrap_or_default() {
if let Err(error) = (Fan { if let Err(error) = (Fan {
@ -10,7 +11,7 @@ pub fn restore_automatic() {
pwm_min: None, pwm_min: None,
pwm_max: None, pwm_max: None,
}) })
.write_automatic() .write_automatic()
{ {
log::error!("{}", error); log::error!("{}", error);
} }

View File

@ -1,9 +1,10 @@
use crate::command::Fan;
use crate::AmdFanError;
use amdgpu::utils::hw_mons;
use gumdrop::Options; use gumdrop::Options;
use crate::config::Config; use amdgpu::utils::hw_mons;
use amdgpu_config::fan::Config;
use crate::AmdFanError;
use crate::command::Fan;
/// 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) -> crate::Result<()> { pub fn run(config: Config) -> crate::Result<()> {

29
amdgpu-config/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "amdgpu-config"
version = "1.0.8"
edition = "2021"
[lib]
name = "amdgpu_config"
path = "./src/lib.rs"
[features]
fan = []
voltage = []
monitor = []
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.8" }
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
csv = { version = "1.1.6" }
thiserror = "1.0.30"
gumdrop = { version = "0.8.0" }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }

5
amdgpu-config/README.md Normal file
View File

@ -0,0 +1,5 @@
# amdgpu-config
This crates holds config files for `amdfand`, `amdvold` and `amdmond`.
For more information please check those services.

View File

@ -1,6 +1,7 @@
use amdgpu::utils::linear_map; use amdgpu::utils::{ensure_config, linear_map};
use amdgpu::{LogLevel, TempInput}; use amdgpu::{LogLevel, TempInput};
use std::io::ErrorKind;
pub static DEFAULT_FAN_CONFIG_PATH: &str = "/etc/amdfand/config.toml";
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct MatrixPoint { pub struct MatrixPoint {
@ -27,6 +28,13 @@ impl Config {
self.cards.as_ref() self.cards.as_ref()
} }
pub fn speed_matrix_point(&self, temp: f64) -> Option<&MatrixPoint> {
match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
Some(idx) => self.speed_matrix.get(idx),
_ => None,
}
}
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,
@ -108,14 +116,14 @@ impl Default for Config {
} }
} }
#[derive(Debug, thiserror::Error, PartialEq)] #[derive(Debug, thiserror::Error)]
pub enum ConfigError { pub enum ConfigError {
#[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")] #[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")]
FanSpeedTooLow { value: f64, index: usize }, FanSpeedTooLow { value: f64, index: usize },
#[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")] #[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")]
FanSpeedTooHigh { value: f64, index: usize }, FanSpeedTooHigh { value: f64, index: usize },
#[error( #[error(
"Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted" "Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
)] )]
UnsortedFanSpeed { UnsortedFanSpeed {
current: f64, current: f64,
@ -123,28 +131,19 @@ pub enum ConfigError {
last: f64, last: f64,
}, },
#[error( #[error(
"Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted" "Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
)] )]
UnsortedFanTemp { UnsortedFanTemp {
current: f64, current: f64,
index: usize, index: usize,
last: f64, last: f64,
}, },
#[error("{0}")]
Io(#[from] std::io::Error),
} }
pub fn load_config(config_path: &str) -> crate::Result<Config> { pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
let config = match std::fs::read_to_string(config_path) { let config = ensure_config::<Config, ConfigError, _>(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!();
}
};
let mut last_point: Option<&MatrixPoint> = None; let mut last_point: Option<&MatrixPoint> = None;
@ -207,10 +206,10 @@ pub fn load_config(config_path: &str) -> crate::Result<Config> {
#[cfg(test)] #[cfg(test)]
mod parse_config { mod parse_config {
use crate::config::TempInput;
use amdgpu::{AmdGpuError, Card};
use serde::Deserialize; use serde::Deserialize;
use amdgpu::{AmdGpuError, Card, TempInput};
#[derive(Deserialize, PartialEq, Debug)] #[derive(Deserialize, PartialEq, Debug)]
pub struct Foo { pub struct Foo {
card: Card, card: Card,

9
amdgpu-config/src/lib.rs Normal file
View File

@ -0,0 +1,9 @@
#[cfg(feature = "fan")]
pub mod fan;
#[cfg(feature = "monitor")]
pub mod monitor;
#[cfg(feature = "voltage")]
pub mod voltage;
/// pulse width modulation fan level (0-255)
pub static PULSE_WIDTH_MODULATION: &str = "pwm1";

View File

@ -0,0 +1,53 @@
use serde::{Deserialize, Serialize};
use amdgpu::utils::ensure_config;
use amdgpu::LogLevel;
pub static DEFAULT_MONITOR_CONFIG_PATH: &str = "/etc/amdfand/monitor.toml";
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
/// Minimal log level
log_level: LogLevel,
/// Time in milliseconds
interval: u32,
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("{0}")]
Io(#[from] std::io::Error),
}
impl Default for Config {
fn default() -> Self {
Self {
log_level: LogLevel::Error,
interval: 5000,
}
}
}
impl Config {
pub fn log_level(&self) -> LogLevel {
self.log_level
}
pub fn interval(&self) -> u32 {
self.interval
}
}
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
let mut config: Config = ensure_config::<Config, ConfigError, _>(config_path)?;
if config.interval < 100 {
log::warn!(
"Minimal interval is 100ms, overriding {}ms",
config.interval
);
config.interval = 100;
}
Ok(config)
}

View File

@ -0,0 +1,31 @@
use amdgpu::utils::ensure_config;
use amdgpu::LogLevel;
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("{0}")]
Io(#[from] std::io::Error),
}
#[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) -> Result<Config, ConfigError> {
ensure_config::<Config, ConfigError, _>(config_path)
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "amdgpu" name = "amdgpu"
version = "1.0.7" version = "1.0.8"
edition = "2018" edition = "2018"
description = "Subcomponent of AMDGPU fan control service" description = "Subcomponent of AMDGPU fan control service"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

5
amdgpu/README.md Normal file
View File

@ -0,0 +1,5 @@
# amdgpu-config
This is shared data for `amdfand`, `amdvold` and `amdmond`.
For more information please check those services.

View File

@ -1,6 +1,7 @@
use crate::AmdGpuError;
use serde::Deserialize; use serde::Deserialize;
use crate::AmdGpuError;
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct Card(pub u32); pub struct Card(pub u32);
@ -29,8 +30,8 @@ impl std::str::FromStr for Card {
impl<'de> Deserialize<'de> for Card { impl<'de> Deserialize<'de> for Card {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
@ -44,8 +45,8 @@ impl<'de> Deserialize<'de> for Card {
} }
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where where
E: de::Error, E: de::Error,
{ {
match value.parse::<Card>() { match value.parse::<Card>() {
Ok(card) => Ok(*card), Ok(card) => Ok(*card),
@ -67,8 +68,8 @@ impl<'de> Deserialize<'de> for Card {
impl serde::Serialize for Card { impl serde::Serialize for Card {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.serialize_str(&self.to_string())
} }

View File

@ -1,19 +1,35 @@
use serde::{Deserialize, Serialize};
pub use card::*;
pub use error::*;
pub use temp_input::*;
mod card; mod card;
mod error; mod error;
pub mod hw_mon; pub mod hw_mon;
mod temp_input; mod temp_input;
pub mod utils; 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 CONFIG_DIR: &str = "/etc/amdfand";
pub static ROOT_DIR: &str = "/sys/class/drm"; pub static ROOT_DIR: &str = "/sys/class/drm";
pub static HW_MON_DIR: &str = "hwmon"; pub static HW_MON_DIR: &str = "hwmon";
/// pulse width modulation fan control minimum level (0)
pub static PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min";
/// pulse width modulation fan control maximum level (255)
pub static PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max";
/// pulse width modulation fan level (0-255)
pub static 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)
pub static PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable";
// static PULSE_WIDTH_MODULATION_DISABLED: &str = "0";
pub static PULSE_WIDTH_MODULATION_AUTO: &str = "2";
pub type Result<T> = std::result::Result<T, AmdGpuError>; pub type Result<T> = std::result::Result<T, AmdGpuError>;
#[derive(Serialize, Deserialize, Debug, Copy, Clone)] #[derive(Serialize, Deserialize, Debug, Copy, Clone)]

View File

@ -37,8 +37,8 @@ impl std::str::FromStr for TempInput {
impl<'de> serde::Deserialize<'de> for TempInput { impl<'de> serde::Deserialize<'de> for TempInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
@ -52,8 +52,8 @@ impl<'de> serde::Deserialize<'de> for TempInput {
} }
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where where
E: de::Error, E: de::Error,
{ {
match value.parse::<TempInput>() { match value.parse::<TempInput>() {
Ok(temp) => Ok(temp.0), Ok(temp) => Ok(temp.0),

View File

@ -1,5 +1,7 @@
use std::io::ErrorKind;
use crate::{Card, CONFIG_DIR, hw_mon, ROOT_DIR};
use crate::hw_mon::HwMon; use crate::hw_mon::HwMon;
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 {
@ -41,3 +43,45 @@ pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
}) })
.collect()) .collect())
} }
pub fn ensure_config<Config, Error, P>(config_path: P) -> std::result::Result<Config, Error>
where
Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized,
P: AsRef<std::path::Path>,
Error: From<std::io::Error>,
{
match std::fs::read_to_string(&config_path) {
Ok(s) => Ok(toml::from_str::<Config>(s.as_str()).unwrap()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let config = Config::default();
std::fs::write(config_path, toml::to_string(&config).unwrap())?;
Ok(config)
}
Err(e) => {
log::error!("{:?}", e);
panic!();
}
}
}
pub 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()
}
pub fn ensure_config_dir() -> std::io::Result<()> {
if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) {
std::fs::create_dir_all(CONFIG_DIR)?;
}
Ok(())
}

24
amdmond/Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "amdmond"
version = "1.0.8"
edition = "2021"
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["monitor", "fan"] }
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
csv = { version = "1.1.6" }
thiserror = "1.0.30"
gumdrop = { version = "0.8.0" }
chrono = { version = "0.4.19", features = ["serde"] }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] }

66
amdmond/README.md Normal file
View File

@ -0,0 +1,66 @@
# AMD Monitoring daemon
## Watch mode
Tool will check temperature and prints:
* Minimal modulation
* Maximal modulation
* Current modulation
* Current fan speed in percentage (PWD / PWD MAX * 100)
* Current value of each temperature sensor (typically temp1_input is which should be observed)
> `modulation` is a value between 0-255 which indicate how fast fan should be moving
```bash
/usr/bin/amdmond watch --format short
```
### Formats
There are 2 possible formats.
* `short` - very compact info
* `long` - more human-readable info
## Log File mode
This tool can be used to track GPU temperature and amdfand speed curve management to prevent GPU card from generating
unnecessary noise.
It will create csv log file with:
* time
* temperature
* card modulation
* matrix point temperature
* matrix point speed
```bash
/usr/bin/amdmond log_file -s /var/log/amdmon.csv
```
## Install
```bash
cargo install amdmond
```
## Usage
### minimal:
```
amdmond log_file -s /var/log/amdmon.csv
```
Required arguments:
* `-s`, `--stat-file STAT-FILE` Full path to statistics file
Optional arguments:
* `-h`, `--help` Help message
* `-v`, `--version` Print version
* `-c`, `--config CONFIG` Config location
* `-i`, `--interval INTERVAL` Time between each check. 1000 is 1s, by default 5s

114
amdmond/src/command.rs Normal file
View File

@ -0,0 +1,114 @@
use crate::{log_file, watch, AmdMonError};
use amdgpu::hw_mon::HwMon;
use amdgpu::utils::load_temp_inputs;
use amdgpu::{TempInput, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN};
use amdgpu_config::{fan, PULSE_WIDTH_MODULATION};
#[derive(gumdrop::Options)]
pub enum Command {
Watch(watch::Watch),
LogFile(log_file::LogFile),
}
impl Default for Command {
fn default() -> Self {
Self::Watch(watch::Watch::default())
}
}
pub struct AmdMon {
temp_input: Option<TempInput>,
inputs: Vec<String>,
hw_mon: HwMon,
/// 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 AmdMon {
type Target = HwMon;
fn deref(&self) -> &Self::Target {
&self.hw_mon
}
}
impl AmdMon {
pub(crate) fn wrap_all(mons: Vec<HwMon>, config: &fan::Config) -> Vec<Self> {
mons.into_iter()
.map(|hw_mon| Self::wrap(hw_mon, config))
.collect()
}
pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self {
Self {
temp_input: config.temp_input().cloned(),
inputs: load_temp_inputs(&hw_mon),
hw_mon,
pwm_min: None,
pwm_max: None,
}
}
pub fn gpu_temp(&self) -> Vec<(String, crate::Result<f64>)> {
self.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 fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
let value = self
.hw_mon_read(name)?
.parse::<u64>()
.map_err(AmdMonError::NonIntTemp)?;
Ok(value)
}
pub fn pwm(&self) -> crate::Result<u32> {
let value = self
.hw_mon_read(PULSE_WIDTH_MODULATION)?
.parse()
.map_err(AmdMonError::NonIntPwm)?;
Ok(value)
}
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 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.inputs.len());
for name in self.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(AmdMonError::EmptyTempSet)?;
Ok(value)
}
}

98
amdmond/src/log_file.rs Normal file
View File

@ -0,0 +1,98 @@
use crate::command::AmdMon;
use crate::AmdMonError;
use amdgpu::utils::hw_mons;
use amdgpu_config::fan;
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
use amdgpu_config::monitor::Config;
#[derive(gumdrop::Options)]
pub struct LogFile {
#[options(help = "Help message")]
help: bool,
#[options(help = "Full path to statistics file")]
stat_file: String,
#[options(help = "Time between each check. 1000 is 1s, by default 5s")]
interval: Option<u32>,
}
#[derive(serde::Serialize)]
struct Stat {
time: chrono::DateTime<chrono::Local>,
temperature: f64,
modulation: u32,
speed_setting: f64,
temperature_setting: f64,
}
pub fn run(command: LogFile, config: Config) -> crate::Result<()> {
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
let duration = std::time::Duration::new(
0,
command.interval.unwrap_or_else(|| config.interval()) * 1_000_000,
);
log::info!("Updating each: {:?}", duration);
let _ = std::fs::remove_file(command.stat_file.as_str());
let stat_file = std::fs::OpenOptions::new()
.create(true)
.append(false)
.write(true)
.open(command.stat_file.as_str())?;
let mut writer = csv::WriterBuilder::new()
.double_quote(true)
.has_headers(true)
.buffer_capacity(100)
.from_writer(stat_file);
let mon = {
let mons = hw_mons(true)?;
if mons.is_empty() {
return Err(AmdMonError::NoHwMon);
}
AmdMon::wrap(hw_mons(true)?.remove(0), &fan_config)
};
loop {
let time = chrono::Local::now();
let temperature = {
let mut temperatures = mon.gpu_temp();
if let Some(input) = fan_config.temp_input() {
let input_name = input.as_string();
temperatures
.into_iter()
.find(|(name, _value)| name == &input_name)
.and_then(|(_, v)| v.ok())
.unwrap_or_default()
} else if temperatures.len() > 1 {
temperatures.remove(1).1.unwrap_or_default()
} else {
temperatures
.get(0)
.and_then(|(_, v)| v.as_ref().ok().copied())
.unwrap_or_default()
}
};
let (speed_setting, temperature_setting) =
if let Some(speed_matrix_point) = fan_config.speed_matrix_point(temperature) {
(speed_matrix_point.speed, speed_matrix_point.temp)
} else {
Default::default()
};
let stat = Stat {
time,
temperature,
modulation: mon.pwm()?,
speed_setting,
temperature_setting,
};
writer.serialize(stat)?;
writer.flush()?;
std::thread::sleep(duration);
}
}

96
amdmond/src/main.rs Normal file
View File

@ -0,0 +1,96 @@
mod command;
mod log_file;
mod watch;
use gumdrop::Options;
use crate::command::Command;
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH};
use amdgpu_config::{fan, monitor};
#[derive(Debug, thiserror::Error)]
pub enum AmdMonError {
#[error("Mon AMD GPU card was found")]
NoHwMon,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
MonConfigError(#[from] monitor::ConfigError),
#[error("{0}")]
FanConfigError(#[from] fan::ConfigError),
#[error("{0}")]
Csv(#[from] csv::Error),
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
NonIntTemp(std::num::ParseIntError),
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
NonIntPwm(std::num::ParseIntError),
#[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")]
InvalidMonitorFormat,
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
EmptyTempSet,
}
pub type Result<T> = std::result::Result<T, AmdMonError>;
#[derive(gumdrop::Options)]
pub struct Opts {
#[options(help = "Help message")]
pub help: bool,
#[options(help = "Print version")]
pub version: bool,
#[options(help = "Config location")]
pub config: Option<String>,
#[options(command)]
pub command: Option<Command>,
}
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 {
Some(Command::Watch(w)) => watch::run(w, config),
Some(Command::LogFile(l)) => log_file::run(l, config),
_ => {
println!("{}", <Opts as gumdrop::Options>::usage());
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();
ensure_config_dir()?;
let config_path = Opts::parse_args_default_or_exit()
.config
.unwrap_or_else(|| DEFAULT_MONITOR_CONFIG_PATH.to_string());
let config = load_config(&config_path)?;
log::info!("{:?}", config);
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) => {
log::error!("{}", e);
std::process::exit(1);
}
}
}

View File

@ -1,9 +1,11 @@
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 amdgpu::utils::{hw_mons, linear_map};
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
use amdgpu_config::{fan, monitor};
use crate::command::AmdMon;
use crate::AmdMonError;
#[derive(Debug)] #[derive(Debug)]
pub enum MonitorFormat { pub enum MonitorFormat {
@ -18,35 +20,45 @@ impl Default for MonitorFormat {
} }
impl FromStr for MonitorFormat { impl FromStr for MonitorFormat {
type Err = AmdFanError; type Err = AmdMonError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"short" | "s" => Ok(MonitorFormat::Short), "short" | "s" => Ok(MonitorFormat::Short),
"verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose), "verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose),
_ => Err(AmdFanError::InvalidMonitorFormat), _ => Err(AmdMonError::InvalidMonitorFormat),
} }
} }
} }
#[derive(Debug, gumdrop::Options)] #[derive(Debug, gumdrop::Options)]
pub struct Monitor { pub struct Watch {
#[options(help = "Help message")] #[options(help = "Help message")]
help: bool, help: bool,
#[options(help = "Help message")] #[options(help = "Monitor format")]
format: MonitorFormat, format: MonitorFormat,
} }
/// Start print cards temperature and fan speed impl Default for Watch {
pub fn run(monitor: Monitor, config: Config) -> crate::Result<()> { fn default() -> Self {
match monitor.format { Self {
MonitorFormat::Short => short(config), help: false,
MonitorFormat::Verbose => verbose(config), format: MonitorFormat::Short,
}
} }
} }
pub fn verbose(config: Config) -> crate::Result<()> { /// Start print cards temperature and fan speed
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> {
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
match monitor.format {
MonitorFormat::Short => short(fan_config),
MonitorFormat::Verbose => verbose(fan_config),
}
}
pub fn verbose(config: fan::Config) -> crate::Result<()> {
let mut hw_mons = AmdMon::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);
@ -67,7 +79,7 @@ pub fn verbose(config: Config) -> crate::Result<()> {
min as f64, min as f64,
max as f64, max as f64,
0f64, 0f64,
100f64 100f64,
)) ))
.round(), .round(),
); );
@ -88,8 +100,8 @@ pub fn verbose(config: Config) -> crate::Result<()> {
} }
} }
pub fn short(config: Config) -> crate::Result<()> { pub fn short(config: fan::Config) -> crate::Result<()> {
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); let mut hw_mons = AmdMon::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 hw_mons.iter_mut() { for hw_mon in hw_mons.iter_mut() {
@ -110,7 +122,7 @@ pub fn short(config: Config) -> crate::Result<()> {
min as f64, min as f64,
max as f64, max as f64,
0f64, 0f64,
100f64 100f64,
) )
.round(), .round(),
); );

View File

@ -1,6 +1,6 @@
[package] [package]
name = "amdvold" name = "amdvold"
version = "1.0.7" version = "1.0.8"
edition = "2018" edition = "2018"
description = "AMDGPU fan control service" description = "AMDGPU fan control service"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -9,7 +9,8 @@ categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud" repository = "https://github.com/Eraden/amdgpud"
[dependencies] [dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.7" } amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["voltage"] }
serde = { version = "1.0.126", features = ["derive"] } serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" } toml = { version = "0.5.8" }
@ -21,3 +22,4 @@ pretty_env_logger = { version = "0.4.0" }
[dev-dependencies] [dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" } amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["voltage"] }

51
amdvold/README.md Normal file
View File

@ -0,0 +1,51 @@
# AMD graphic card voltage manager
This tool can be used to overclock you AMD graphic card on Linux
## Install
```bash
cargo install amdvold
```
## Usage
Available commands:
* `setup-info` - prints information how to enable voltage management on Linux (see Requirements)
* `print-states` - prints current card states
* `change-state` - change card voltage states
* `apply-changes` - apply changes
## Changing states
Positional arguments:
* `index` Profile number
* `module` Either memory or engine
* `frequency` New GPU module frequency
* `voltage` New GPU module voltage
Optional arguments:
* `-a`, `--apply-immediately` Apply changes immediately after change
Example:
```bash
amdvold 1 engine 1450MHz 772mV
```
## Requirements
To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following:
* In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example:
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1"
Easiest way is to modify "/etc/default/grub" and generate new grub config.
* If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example:
options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff
(only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff")

View File

@ -1,7 +1,8 @@
use crate::command::VoltageManipulator;
use crate::{Config, VoltageError};
use amdgpu::utils::hw_mons; use amdgpu::utils::hw_mons;
use crate::{Config, VoltageError};
use crate::command::VoltageManipulator;
#[derive(Debug, gumdrop::Options)] #[derive(Debug, gumdrop::Options)]
pub struct ApplyChanges { pub struct ApplyChanges {
help: bool, help: bool,

View File

@ -1,7 +1,8 @@
use amdgpu::utils::hw_mons;
use crate::clock_state::{Frequency, Voltage}; use crate::clock_state::{Frequency, Voltage};
use crate::command::{HardwareModule, VoltageManipulator}; use crate::command::{HardwareModule, VoltageManipulator};
use crate::{Config, VoltageError}; use crate::{Config, VoltageError};
use amdgpu::utils::hw_mons;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ChangeStateError { pub enum ChangeStateError {
@ -17,6 +18,8 @@ pub enum ChangeStateError {
#[derive(Debug, gumdrop::Options)] #[derive(Debug, gumdrop::Options)]
pub struct ChangeState { pub struct ChangeState {
#[options(help = "Help message")]
help: bool,
#[options(help = "Profile number", free)] #[options(help = "Profile number", free)]
index: u16, index: u16,
#[options(help = "Either memory or engine", free)] #[options(help = "Either memory or engine", free)]
@ -36,6 +39,7 @@ pub fn run(command: ChangeState, config: &Config) -> crate::Result<()> {
} }
let mon = mons.remove(0); let mon = mons.remove(0);
let ChangeState { let ChangeState {
help: _,
index, index,
module, module,
frequency, frequency,

View File

@ -385,47 +385,47 @@ VDDC_CURVE_VOLT[2]: 750mV 1200mV
CurvePoint { CurvePoint {
freq: Frequency { freq: Frequency {
value: 800, value: 800,
unit: "MHz".to_string() unit: "MHz".to_string(),
}, },
voltage: Voltage { voltage: Voltage {
value: 706, value: 706,
unit: "mV".to_string() unit: "mV".to_string(),
} },
}, },
CurvePoint { CurvePoint {
freq: Frequency { freq: Frequency {
value: 1450, value: 1450,
unit: "MHz".to_string() unit: "MHz".to_string(),
}, },
voltage: Voltage { voltage: Voltage {
value: 772, value: 772,
unit: "mV".to_string() unit: "mV".to_string(),
} },
}, },
CurvePoint { CurvePoint {
freq: Frequency { freq: Frequency {
value: 2100, value: 2100,
unit: "MHz".to_string() unit: "MHz".to_string(),
}, },
voltage: Voltage { voltage: Voltage {
value: 1143, value: 1143,
unit: "mV".to_string() unit: "mV".to_string(),
} },
} },
], ],
engine_label_lowest: Some(Frequency { engine_label_lowest: Some(Frequency {
value: 800, value: 800,
unit: "Mhz".to_string() unit: "Mhz".to_string(),
}), }),
engine_label_highest: Some(Frequency { engine_label_highest: Some(Frequency {
value: 2100, value: 2100,
unit: "Mhz".to_string() unit: "Mhz".to_string(),
}), }),
memory_label_lowest: Some(Frequency { memory_label_lowest: Some(Frequency {
value: 875, value: 875,
unit: "MHz".to_string() unit: "MHz".to_string(),
}), }),
memory_label_highest: None memory_label_highest: None,
}) })
); );
} }

View File

@ -1,10 +1,11 @@
use amdgpu::hw_mon::HwMon;
use crate::{Config, VoltageError};
use crate::apply_changes::ApplyChanges; use crate::apply_changes::ApplyChanges;
use crate::change_state::ChangeState; use crate::change_state::ChangeState;
use crate::clock_state::{ClockState, Frequency, Voltage}; use crate::clock_state::{ClockState, Frequency, Voltage};
use crate::print_states::PrintStates; use crate::print_states::PrintStates;
use crate::setup_info::SetupInfo; use crate::setup_info::SetupInfo;
use crate::{Config, VoltageError};
use amdgpu::hw_mon::HwMon;
#[derive(Debug)] #[derive(Debug)]
pub enum HardwareModule { pub enum HardwareModule {

View File

@ -1,40 +0,0 @@
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)
}

View File

@ -1,7 +1,8 @@
use amdgpu::AmdGpuError;
use amdgpu_config::voltage::ConfigError;
use crate::change_state::ChangeStateError; use crate::change_state::ChangeStateError;
use crate::clock_state::ClockStateError; use crate::clock_state::ClockStateError;
use crate::config::ConfigError;
use amdgpu::AmdGpuError;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum VoltageError { pub enum VoltageError {

View File

@ -1,15 +1,15 @@
use crate::command::VoltageCommand;
use crate::config::{load_config, Config};
use crate::error::VoltageError;
use amdgpu::CONFIG_DIR;
use gumdrop::Options; use gumdrop::Options;
use std::io::ErrorKind;
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::voltage::{Config, load_config};
use crate::command::VoltageCommand;
use crate::error::VoltageError;
mod apply_changes; mod apply_changes;
mod change_state; mod change_state;
mod clock_state; mod clock_state;
mod command; mod command;
mod config;
mod error; mod error;
mod print_states; mod print_states;
mod setup_info; mod setup_info;
@ -29,6 +29,7 @@ pub struct Opts {
#[options(command)] #[options(command)]
command: Option<command::VoltageCommand>, command: Option<command::VoltageCommand>,
} }
fn run(config: Config) -> Result<()> { fn run(config: Config) -> Result<()> {
let opts: Opts = Opts::parse_args_default_or_exit(); let opts: Opts = Opts::parse_args_default_or_exit();
@ -54,9 +55,7 @@ fn setup() -> Result<(String, Config)> {
std::env::set_var("RUST_LOG", "DEBUG"); std::env::set_var("RUST_LOG", "DEBUG");
} }
pretty_env_logger::init(); pretty_env_logger::init();
if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { ensure_config_dir()?;
std::fs::create_dir_all(CONFIG_DIR)?;
}
let config_path = Opts::parse_args_default_or_exit() let config_path = Opts::parse_args_default_or_exit()
.config .config

View File

@ -1,6 +1,7 @@
use amdgpu::utils::hw_mons;
use crate::command::VoltageManipulator; use crate::command::VoltageManipulator;
use crate::Config; use crate::Config;
use amdgpu::utils::hw_mons;
#[derive(Debug, gumdrop::Options)] #[derive(Debug, gumdrop::Options)]
pub struct PrintStates { pub struct PrintStates {

View File

@ -1,4 +1,4 @@
use crate::config::Config; use amdgpu_config::voltage::Config;
pub static ENABLE_VOLTAGE_INFO: &str = include_str!("../assets/enable_voltage_info.txt"); pub static ENABLE_VOLTAGE_INFO: &str = include_str!("../assets/enable_voltage_info.txt");

View File

@ -1,6 +1,11 @@
#!/usr/bin/env zsh #!/usr/bin/env zsh
cargo build --release cargo build --release
strip target/x86_64-unknown-linux-musl/release/amdfand
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
strip target/x86_64-unknown-linux-musl/release/amdfand
strip target/x86_64-unknown-linux-musl/release/amdvold
strip target/x86_64-unknown-linux-musl/release/amdmond
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond

18
publish.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env zsh
git_root() { echo "$(git rev-parse --show-toplevel)" }
cd $(git_root)/amdgpu
cargo publish
cd $(git_root)/amdgpu-config
cargo publish
cd $(git_root)/amdfand
cargo publish
cd $(git_root)/amdvold
cargo publish
cd $(git_root)/amdmond
cargo publish

11
services/amdmond Executable file
View File

@ -0,0 +1,11 @@
#!/sbin/openrc-run
description="AMD card monitoring tool."
pidfile="/run/${SVCNAME}.pid"
command="/usr/bin/amdfand log_file -s /var/log/amdmon.csv"
command_args="service"
command_user="root"
depend() {
need udev
}

9
services/amdmond.service Normal file
View File

@ -0,0 +1,9 @@
[Unit]
Description=AMD GPU monitoring tool
After=sysinit.target local-fs.target
[Service]
Restart=on-failure
RestartSec=4
ExecStart=/usr/bin/amdmond log_file -s /var/log/amdmon.csv
[Install]
WantedBy=multi-user.target