From 5698ab15d838b1bf927853f3caa2a07e70c0bfd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Fri, 3 Dec 2021 07:35:17 +0100 Subject: [PATCH] Improve monitor --- .github/workflows/rust.yml | 44 ++--- Cargo.lock | 154 +++++++++++++++++- Cargo.toml | 2 +- LICENSE.md | 25 ++- README.md | 76 +-------- amdfand/Cargo.toml | 6 +- amdfand/README.md | 66 ++++++++ amdfand/src/change_mode.rs | 7 +- amdfand/src/command.rs | 92 +++-------- amdfand/src/error.rs | 7 +- amdfand/src/main.rs | 25 +-- amdfand/src/panic_handler.rs | 5 +- amdfand/src/service.rs | 9 +- amdgpu-config/Cargo.toml | 29 ++++ amdgpu-config/README.md | 5 + .../src/config.rs => amdgpu-config/src/fan.rs | 39 +++-- amdgpu-config/src/lib.rs | 9 + amdgpu-config/src/monitor.rs | 53 ++++++ amdgpu-config/src/voltage.rs | 31 ++++ amdgpu/Cargo.toml | 2 +- amdgpu/README.md | 5 + amdgpu/src/card.rs | 15 +- amdgpu/src/lib.rs | 26 ++- amdgpu/src/temp_input.rs | 8 +- amdgpu/src/utils.rs | 46 +++++- amdmond/Cargo.toml | 24 +++ amdmond/README.md | 66 ++++++++ amdmond/src/command.rs | 114 +++++++++++++ amdmond/src/log_file.rs | 98 +++++++++++ amdmond/src/main.rs | 96 +++++++++++ .../src/monitor.rs => amdmond/src/watch.rs | 50 +++--- amdvold/Cargo.toml | 6 +- amdvold/README.md | 51 ++++++ amdvold/src/apply_changes.rs | 5 +- amdvold/src/change_state.rs | 6 +- amdvold/src/clock_state.rs | 28 ++-- amdvold/src/command.rs | 5 +- amdvold/src/config.rs | 40 ----- amdvold/src/error.rs | 5 +- amdvold/src/main.rs | 17 +- amdvold/src/print_states.rs | 3 +- amdvold/src/setup_info.rs | 2 +- build.sh | 9 +- publish.sh | 18 ++ services/amdmond | 11 ++ services/amdmond.service | 9 + 46 files changed, 1108 insertions(+), 341 deletions(-) create mode 100644 amdfand/README.md create mode 100644 amdgpu-config/Cargo.toml create mode 100644 amdgpu-config/README.md rename amdfand/src/config.rs => amdgpu-config/src/fan.rs (90%) create mode 100644 amdgpu-config/src/lib.rs create mode 100644 amdgpu-config/src/monitor.rs create mode 100644 amdgpu-config/src/voltage.rs create mode 100644 amdgpu/README.md create mode 100644 amdmond/Cargo.toml create mode 100644 amdmond/README.md create mode 100644 amdmond/src/command.rs create mode 100644 amdmond/src/log_file.rs create mode 100644 amdmond/src/main.rs rename amdfand/src/monitor.rs => amdmond/src/watch.rs (74%) create mode 100644 amdvold/README.md delete mode 100644 amdvold/src/config.rs create mode 100755 publish.sh create mode 100755 services/amdmond create mode 100644 services/amdmond.service diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7a329a8..45e8cbf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,28 +14,28 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-18.04, ubuntu-20.04] + os: [ ubuntu-18.04, ubuntu-20.04 ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - name: Add target - run: rustup target install x86_64-unknown-linux-musl - - name: Run clippy - run: cargo clippy -- -D warnings - - name: Run fmt check - run: cargo fmt -- --check - - name: Run tests - run: cargo test --verbose - - name: Install binary compressor - run: sudo apt-get update && sudo apt-get install upx-ucl - - 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 - - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.4 - with: - # Artifact name - name: amdfand-${{ matrix.os }} - # A file, directory or wildcard pattern that describes what to upload - path: ./target/x86_64-unknown-linux-musl/release/amdfand - # The desired behavior if no files are found using the provided path. + - uses: actions/checkout@v2 + - name: Add target + run: rustup target install x86_64-unknown-linux-musl + - name: Run clippy + run: cargo clippy -- -D warnings + - name: Run fmt check + run: cargo fmt -- --check + - name: Run tests + run: cargo test --verbose + - name: Install binary compressor + run: sudo apt-get update && sudo apt-get install upx-ucl + - 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 + - name: Upload a Build Artifact + uses: actions/upload-artifact@v2.2.4 + with: + # Artifact name + name: amdfand-${{ matrix.os }} + # A file, directory or wildcard pattern that describes what to upload + path: ./target/x86_64-unknown-linux-musl/release/amdfand + # The desired behavior if no files are found using the provided path. diff --git a/Cargo.lock b/Cargo.lock index 3876f7d..7d20d1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,10 @@ dependencies = [ [[package]] name = "amdfand" -version = "1.0.7" +version = "1.0.8" dependencies = [ "amdgpu", + "amdgpu-config", "gumdrop", "log", "pretty_env_logger", @@ -26,7 +27,7 @@ dependencies = [ [[package]] name = "amdgpu" -version = "1.0.7" +version = "1.0.8" dependencies = [ "gumdrop", "log", @@ -37,10 +38,41 @@ dependencies = [ ] [[package]] -name = "amdvold" -version = "1.0.7" +name = "amdgpu-config" +version = "1.0.8" dependencies = [ "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", "log", "pretty_env_logger", @@ -60,12 +92,66 @@ dependencies = [ "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]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "env_logger" version = "0.7.1" @@ -117,6 +203,18 @@ dependencies = [ "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]] name = "libc" version = "0.2.97" @@ -138,6 +236,25 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "pretty_env_logger" version = "0.4.0" @@ -183,12 +300,24 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "ryu" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" + [[package]] name = "serde" version = "1.0.126" @@ -249,6 +378,17 @@ dependencies = [ "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]] name = "toml" version = "0.5.8" @@ -264,6 +404,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 0ee33a5..8841d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["amdfand", "amdgpu", "amdvold"] +members = ["amdgpu", "amdgpu-config", "amdfand", "amdvold", "amdmond"] diff --git a/LICENSE.md b/LICENSE.md index e026ca0..26d9057 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,20 +2,15 @@ MIT License Copyright (c) 2021 Adrian Woźniak -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 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 -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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. diff --git a/README.md b/README.md index a9b48f2..de4356b 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,24 @@ ![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 -* `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` - fan speed daemon +* `amdvold` - voltage and overclocking tool +* `amdmond` - monitor daemon -#### 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 -``` +For more information please check README each of them. ## Roadmap -* [ ] Write metrics * [ ] Add support for multiple cards - * Multiple services must recognize card even if there's multiple same version cards is installed - * Support should be by using `--config` option + * Multiple services must recognize card even if there's multiple same version cards is installed + * Support should be by using `--config` option * [ ] CLI for fan config edit * [ ] CLI for voltage edit * [ ] GUI application using native Rust framework (ex. egui, druid) ## :bookmark: License -This work is dual-licensed under Apache 2.0 and MIT. -You can choose between one of them if you use this work. +This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work. diff --git a/amdfand/Cargo.toml b/amdfand/Cargo.toml index 8ad1431..e238a65 100644 --- a/amdfand/Cargo.toml +++ b/amdfand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdfand" -version = "1.0.7" +version = "1.0.8" edition = "2018" description = "AMDGPU fan control service" license = "MIT OR Apache-2.0" @@ -9,7 +9,8 @@ categories = ["hardware-support"] repository = "https://github.com/Eraden/amdgpud" [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"] } toml = { version = "0.5.8" } @@ -21,3 +22,4 @@ pretty_env_logger = { version = "0.4.0" } [dev-dependencies] amdgpu = { path = "../amdgpu", version = "1.0" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] } diff --git a/amdfand/README.md b/amdfand/README.md new file mode 100644 index 0000000..8168c39 --- /dev/null +++ b/amdfand/README.md @@ -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 +``` diff --git a/amdfand/src/change_mode.rs b/amdfand/src/change_mode.rs index 1281c94..ae59325 100644 --- a/amdfand/src/change_mode.rs +++ b/amdfand/src/change_mode.rs @@ -1,9 +1,10 @@ -use crate::command::Fan; -use amdgpu::utils::hw_mons; use gumdrop::Options; -use crate::config::Config; +use amdgpu::utils::hw_mons; +use amdgpu_config::fan::Config; + use crate::{AmdFanError, FanMode}; +use crate::command::Fan; /// Change card fan mode to either automatic or manual pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> crate::Result<()> { diff --git a/amdfand/src/command.rs b/amdfand/src/command.rs index 23a16fc..b9cbc70 100644 --- a/amdfand/src/command.rs +++ b/amdfand/src/command.rs @@ -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; -/// pulse width modulation fan control minimum level (0) -const PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; +use amdgpu::hw_mon::HwMon; +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) -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"; +use crate::{change_mode, service}; #[derive(Debug, Options)] pub struct AvailableCards { @@ -27,8 +18,6 @@ pub struct AvailableCards { #[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")] @@ -81,6 +70,8 @@ impl std::ops::DerefMut for Fan { } } +const MODULATION_ENABLED_FILE: &str = "pwm1_enable"; + impl Fan { pub fn wrap(hw_mon: HwMon, config: &Config) -> Self { Self { @@ -105,7 +96,7 @@ impl Fan { } 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)?; Ok(()) } @@ -124,27 +115,6 @@ impl Fan { .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 { - 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) @@ -170,19 +140,6 @@ impl Fan { Ok(value) } - pub fn gpu_temp(&self) -> Vec<(String, crate::Result)> { - 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 { let value = self .hw_mon_read(name)? @@ -190,19 +147,18 @@ impl Fan { .map_err(FanError::NonIntTemp)?; Ok(value) } -} -fn load_temp_inputs(hw_mon: &HwMon) -> Vec { - 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 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) + } } diff --git a/amdfand/src/error.rs b/amdfand/src/error.rs index 5315cef..e5a992c 100644 --- a/amdfand/src/error.rs +++ b/amdfand/src/error.rs @@ -1,13 +1,12 @@ -use crate::command::FanError; -use crate::config::ConfigError; use amdgpu::AmdGpuError; +use amdgpu_config::fan::ConfigError; + +use crate::command::FanError; #[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")] diff --git a/amdfand/src/main.rs b/amdfand/src/main.rs index 9cb5d73..86fb9d4 100644 --- a/amdfand/src/main.rs +++ b/amdfand/src/main.rs @@ -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::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 = std::result::Result; pub enum FanMode { @@ -51,7 +47,6 @@ fn run(config: Config) -> Result<()> { 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) @@ -78,13 +73,11 @@ fn setup() -> Result<(String, Config)> { 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)?; - } + ensure_config_dir()?; let config_path = Opts::parse_args_default_or_exit() .config - .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); + .unwrap_or_else(|| DEFAULT_FAN_CONFIG_PATH.to_string()); let config = load_config(&config_path)?; log::info!("{:?}", config); log::set_max_level(config.log_level().as_str().parse().unwrap()); diff --git a/amdfand/src/panic_handler.rs b/amdfand/src/panic_handler.rs index ee900d0..2f9b1cf 100644 --- a/amdfand/src/panic_handler.rs +++ b/amdfand/src/panic_handler.rs @@ -1,6 +1,7 @@ -use crate::command::Fan; use amdgpu::utils::hw_mons; +use crate::command::Fan; + pub fn restore_automatic() { for hw in hw_mons(true).unwrap_or_default() { if let Err(error) = (Fan { @@ -10,7 +11,7 @@ pub fn restore_automatic() { pwm_min: None, pwm_max: None, }) - .write_automatic() + .write_automatic() { log::error!("{}", error); } diff --git a/amdfand/src/service.rs b/amdfand/src/service.rs index 3eb23d8..d080349 100644 --- a/amdfand/src/service.rs +++ b/amdfand/src/service.rs @@ -1,9 +1,10 @@ -use crate::command::Fan; -use crate::AmdFanError; -use amdgpu::utils::hw_mons; 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 pub fn run(config: Config) -> crate::Result<()> { diff --git a/amdgpu-config/Cargo.toml b/amdgpu-config/Cargo.toml new file mode 100644 index 0000000..0fcf835 --- /dev/null +++ b/amdgpu-config/Cargo.toml @@ -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" } diff --git a/amdgpu-config/README.md b/amdgpu-config/README.md new file mode 100644 index 0000000..3de45ff --- /dev/null +++ b/amdgpu-config/README.md @@ -0,0 +1,5 @@ +# amdgpu-config + +This crates holds config files for `amdfand`, `amdvold` and `amdmond`. + +For more information please check those services. \ No newline at end of file diff --git a/amdfand/src/config.rs b/amdgpu-config/src/fan.rs similarity index 90% rename from amdfand/src/config.rs rename to amdgpu-config/src/fan.rs index cf26a4e..ca221f4 100644 --- a/amdfand/src/config.rs +++ b/amdgpu-config/src/fan.rs @@ -1,6 +1,7 @@ -use amdgpu::utils::linear_map; +use amdgpu::utils::{ensure_config, linear_map}; 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)] pub struct MatrixPoint { @@ -27,6 +28,13 @@ impl Config { 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 { let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) { Some(idx) => idx, @@ -108,14 +116,14 @@ impl Default for Config { } } -#[derive(Debug, thiserror::Error, PartialEq)] +#[derive(Debug, thiserror::Error)] 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" + "Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted" )] UnsortedFanSpeed { current: f64, @@ -123,28 +131,19 @@ pub enum ConfigError { last: f64, }, #[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 { current: f64, index: usize, last: f64, }, + #[error("{0}")] + Io(#[from] std::io::Error), } -pub fn load_config(config_path: &str) -> crate::Result { - 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!(); - } - }; +pub fn load_config(config_path: &str) -> Result { + let config = ensure_config::(config_path)?; let mut last_point: Option<&MatrixPoint> = None; @@ -207,10 +206,10 @@ pub fn load_config(config_path: &str) -> crate::Result { #[cfg(test)] mod parse_config { - use crate::config::TempInput; - use amdgpu::{AmdGpuError, Card}; use serde::Deserialize; + use amdgpu::{AmdGpuError, Card, TempInput}; + #[derive(Deserialize, PartialEq, Debug)] pub struct Foo { card: Card, diff --git a/amdgpu-config/src/lib.rs b/amdgpu-config/src/lib.rs new file mode 100644 index 0000000..9a127b7 --- /dev/null +++ b/amdgpu-config/src/lib.rs @@ -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"; diff --git a/amdgpu-config/src/monitor.rs b/amdgpu-config/src/monitor.rs new file mode 100644 index 0000000..73601b1 --- /dev/null +++ b/amdgpu-config/src/monitor.rs @@ -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 { + let mut config: Config = ensure_config::(config_path)?; + + if config.interval < 100 { + log::warn!( + "Minimal interval is 100ms, overriding {}ms", + config.interval + ); + config.interval = 100; + } + + Ok(config) +} diff --git a/amdgpu-config/src/voltage.rs b/amdgpu-config/src/voltage.rs new file mode 100644 index 0000000..fb381a4 --- /dev/null +++ b/amdgpu-config/src/voltage.rs @@ -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 { + ensure_config::(config_path) +} diff --git a/amdgpu/Cargo.toml b/amdgpu/Cargo.toml index 4028b93..a19fd57 100644 --- a/amdgpu/Cargo.toml +++ b/amdgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdgpu" -version = "1.0.7" +version = "1.0.8" edition = "2018" description = "Subcomponent of AMDGPU fan control service" license = "MIT OR Apache-2.0" diff --git a/amdgpu/README.md b/amdgpu/README.md new file mode 100644 index 0000000..7663529 --- /dev/null +++ b/amdgpu/README.md @@ -0,0 +1,5 @@ +# amdgpu-config + +This is shared data for `amdfand`, `amdvold` and `amdmond`. + +For more information please check those services. \ No newline at end of file diff --git a/amdgpu/src/card.rs b/amdgpu/src/card.rs index e6007f5..a5c01c6 100644 --- a/amdgpu/src/card.rs +++ b/amdgpu/src/card.rs @@ -1,6 +1,7 @@ -use crate::AmdGpuError; use serde::Deserialize; +use crate::AmdGpuError; + #[derive(Debug, Copy, Clone, PartialEq)] pub struct Card(pub u32); @@ -29,8 +30,8 @@ impl std::str::FromStr for Card { impl<'de> Deserialize<'de> for Card { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, + where + D: serde::Deserializer<'de>, { use serde::de::{self, Visitor}; @@ -44,8 +45,8 @@ impl<'de> Deserialize<'de> for Card { } fn visit_str(self, value: &str) -> Result - where - E: de::Error, + where + E: de::Error, { match value.parse::() { Ok(card) => Ok(*card), @@ -67,8 +68,8 @@ impl<'de> Deserialize<'de> for Card { impl serde::Serialize for Card { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } diff --git a/amdgpu/src/lib.rs b/amdgpu/src/lib.rs index 6a02228..9413a15 100644 --- a/amdgpu/src/lib.rs +++ b/amdgpu/src/lib.rs @@ -1,19 +1,35 @@ +use serde::{Deserialize, Serialize}; + +pub use card::*; +pub use error::*; +pub use temp_input::*; + 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"; +/// 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 = std::result::Result; #[derive(Serialize, Deserialize, Debug, Copy, Clone)] diff --git a/amdgpu/src/temp_input.rs b/amdgpu/src/temp_input.rs index 5e395d3..27984bb 100644 --- a/amdgpu/src/temp_input.rs +++ b/amdgpu/src/temp_input.rs @@ -37,8 +37,8 @@ impl std::str::FromStr for TempInput { impl<'de> serde::Deserialize<'de> for TempInput { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, + where + D: serde::Deserializer<'de>, { use serde::de::{self, Visitor}; @@ -52,8 +52,8 @@ impl<'de> serde::Deserialize<'de> for TempInput { } fn visit_str(self, value: &str) -> Result - where - E: de::Error, + where + E: de::Error, { match value.parse::() { Ok(temp) => Ok(temp.0), diff --git a/amdgpu/src/utils.rs b/amdgpu/src/utils.rs index 7398c91..60120e0 100644 --- a/amdgpu/src/utils.rs +++ b/amdgpu/src/utils.rs @@ -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, Card, ROOT_DIR}; /// linear mapping from the xrange to the yrange pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 { @@ -41,3 +43,45 @@ pub fn hw_mons(filter: bool) -> std::io::Result> { }) .collect()) } + +pub fn ensure_config(config_path: P) -> std::result::Result + where + Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized, + P: AsRef, + Error: From, +{ + match std::fs::read_to_string(&config_path) { + Ok(s) => Ok(toml::from_str::(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 { + 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(()) +} diff --git a/amdmond/Cargo.toml b/amdmond/Cargo.toml new file mode 100644 index 0000000..0229df4 --- /dev/null +++ b/amdmond/Cargo.toml @@ -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"] } diff --git a/amdmond/README.md b/amdmond/README.md new file mode 100644 index 0000000..40238bd --- /dev/null +++ b/amdmond/README.md @@ -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 diff --git a/amdmond/src/command.rs b/amdmond/src/command.rs new file mode 100644 index 0000000..7bbc03e --- /dev/null +++ b/amdmond/src/command.rs @@ -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, + inputs: Vec, + hw_mon: HwMon, + /// Minimal modulation (between 0-255) + pub pwm_min: Option, + /// Maximal modulation (between 0-255) + pub pwm_max: Option, +} + +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, config: &fan::Config) -> Vec { + 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)> { + 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 { + let value = self + .hw_mon_read(name)? + .parse::() + .map_err(AmdMonError::NonIntTemp)?; + Ok(value) + } + + pub fn pwm(&self) -> crate::Result { + 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 { + 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) + } +} diff --git a/amdmond/src/log_file.rs b/amdmond/src/log_file.rs new file mode 100644 index 0000000..b7715f9 --- /dev/null +++ b/amdmond/src/log_file.rs @@ -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, +} + +#[derive(serde::Serialize)] +struct Stat { + time: chrono::DateTime, + 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); + } +} diff --git a/amdmond/src/main.rs b/amdmond/src/main.rs new file mode 100644 index 0000000..0aa48b5 --- /dev/null +++ b/amdmond/src/main.rs @@ -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 = std::result::Result; + +#[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, + #[options(command)] + pub command: Option, +} + +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!("{}", ::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); + } + } +} diff --git a/amdfand/src/monitor.rs b/amdmond/src/watch.rs similarity index 74% rename from amdfand/src/monitor.rs rename to amdmond/src/watch.rs index df438c7..7e6994a 100644 --- a/amdfand/src/monitor.rs +++ b/amdmond/src/watch.rs @@ -1,9 +1,11 @@ -use crate::command::Fan; -use crate::AmdFanError; -use amdgpu::utils::{hw_mons, linear_map}; 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)] pub enum MonitorFormat { @@ -18,35 +20,45 @@ impl Default for MonitorFormat { } impl FromStr for MonitorFormat { - type Err = AmdFanError; + type Err = AmdMonError; fn from_str(s: &str) -> Result { match s { "short" | "s" => Ok(MonitorFormat::Short), "verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose), - _ => Err(AmdFanError::InvalidMonitorFormat), + _ => Err(AmdMonError::InvalidMonitorFormat), } } } #[derive(Debug, gumdrop::Options)] -pub struct Monitor { +pub struct Watch { #[options(help = "Help message")] help: bool, - #[options(help = "Help message")] + #[options(help = "Monitor format")] format: MonitorFormat, } -/// Start print cards temperature and fan speed -pub fn run(monitor: Monitor, config: Config) -> crate::Result<()> { - match monitor.format { - MonitorFormat::Short => short(config), - MonitorFormat::Verbose => verbose(config), +impl Default for Watch { + fn default() -> Self { + Self { + help: false, + format: MonitorFormat::Short, + } } } -pub fn verbose(config: Config) -> crate::Result<()> { - let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); +/// Start print cards temperature and fan speed +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 { print!("{esc}[2J{esc}[1;1H", esc = 27 as char); @@ -67,7 +79,7 @@ pub fn verbose(config: Config) -> crate::Result<()> { min as f64, max as f64, 0f64, - 100f64 + 100f64, )) .round(), ); @@ -88,8 +100,8 @@ pub fn verbose(config: Config) -> crate::Result<()> { } } -pub fn short(config: Config) -> crate::Result<()> { - let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config); +pub fn short(config: fan::Config) -> crate::Result<()> { + let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config); loop { print!("{esc}[2J{esc}[1;1H", esc = 27 as char); for hw_mon in hw_mons.iter_mut() { @@ -110,7 +122,7 @@ pub fn short(config: Config) -> crate::Result<()> { min as f64, max as f64, 0f64, - 100f64 + 100f64, ) .round(), ); diff --git a/amdvold/Cargo.toml b/amdvold/Cargo.toml index 1ae32e1..83ce529 100644 --- a/amdvold/Cargo.toml +++ b/amdvold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "amdvold" -version = "1.0.7" +version = "1.0.8" edition = "2018" description = "AMDGPU fan control service" license = "MIT OR Apache-2.0" @@ -9,7 +9,8 @@ categories = ["hardware-support"] repository = "https://github.com/Eraden/amdgpud" [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"] } toml = { version = "0.5.8" } @@ -21,3 +22,4 @@ pretty_env_logger = { version = "0.4.0" } [dev-dependencies] amdgpu = { path = "../amdgpu", version = "1.0" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["voltage"] } diff --git a/amdvold/README.md b/amdvold/README.md new file mode 100644 index 0000000..1ec556e --- /dev/null +++ b/amdvold/README.md @@ -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") \ No newline at end of file diff --git a/amdvold/src/apply_changes.rs b/amdvold/src/apply_changes.rs index 175357a..03165f4 100644 --- a/amdvold/src/apply_changes.rs +++ b/amdvold/src/apply_changes.rs @@ -1,7 +1,8 @@ -use crate::command::VoltageManipulator; -use crate::{Config, VoltageError}; use amdgpu::utils::hw_mons; +use crate::{Config, VoltageError}; +use crate::command::VoltageManipulator; + #[derive(Debug, gumdrop::Options)] pub struct ApplyChanges { help: bool, diff --git a/amdvold/src/change_state.rs b/amdvold/src/change_state.rs index 19cea0c..f1d9a68 100644 --- a/amdvold/src/change_state.rs +++ b/amdvold/src/change_state.rs @@ -1,7 +1,8 @@ +use amdgpu::utils::hw_mons; + 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 { @@ -17,6 +18,8 @@ pub enum ChangeStateError { #[derive(Debug, gumdrop::Options)] pub struct ChangeState { + #[options(help = "Help message")] + help: bool, #[options(help = "Profile number", free)] index: u16, #[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 ChangeState { + help: _, index, module, frequency, diff --git a/amdvold/src/clock_state.rs b/amdvold/src/clock_state.rs index b006d92..e6aa335 100644 --- a/amdvold/src/clock_state.rs +++ b/amdvold/src/clock_state.rs @@ -385,47 +385,47 @@ VDDC_CURVE_VOLT[2]: 750mV 1200mV CurvePoint { freq: Frequency { value: 800, - unit: "MHz".to_string() + unit: "MHz".to_string(), }, voltage: Voltage { value: 706, - unit: "mV".to_string() - } + unit: "mV".to_string(), + }, }, CurvePoint { freq: Frequency { value: 1450, - unit: "MHz".to_string() + unit: "MHz".to_string(), }, voltage: Voltage { value: 772, - unit: "mV".to_string() - } + unit: "mV".to_string(), + }, }, CurvePoint { freq: Frequency { value: 2100, - unit: "MHz".to_string() + unit: "MHz".to_string(), }, voltage: Voltage { value: 1143, - unit: "mV".to_string() - } - } + unit: "mV".to_string(), + }, + }, ], engine_label_lowest: Some(Frequency { value: 800, - unit: "Mhz".to_string() + unit: "Mhz".to_string(), }), engine_label_highest: Some(Frequency { value: 2100, - unit: "Mhz".to_string() + unit: "Mhz".to_string(), }), memory_label_lowest: Some(Frequency { value: 875, - unit: "MHz".to_string() + unit: "MHz".to_string(), }), - memory_label_highest: None + memory_label_highest: None, }) ); } diff --git a/amdvold/src/command.rs b/amdvold/src/command.rs index 9cdbb47..38db642 100644 --- a/amdvold/src/command.rs +++ b/amdvold/src/command.rs @@ -1,10 +1,11 @@ +use amdgpu::hw_mon::HwMon; + +use crate::{Config, VoltageError}; 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 { diff --git a/amdvold/src/config.rs b/amdvold/src/config.rs deleted file mode 100644 index 1cad871..0000000 --- a/amdvold/src/config.rs +++ /dev/null @@ -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 { - 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) -} diff --git a/amdvold/src/error.rs b/amdvold/src/error.rs index dafcd1d..2adc84d 100644 --- a/amdvold/src/error.rs +++ b/amdvold/src/error.rs @@ -1,7 +1,8 @@ +use amdgpu::AmdGpuError; +use amdgpu_config::voltage::ConfigError; + use crate::change_state::ChangeStateError; use crate::clock_state::ClockStateError; -use crate::config::ConfigError; -use amdgpu::AmdGpuError; #[derive(Debug, thiserror::Error)] pub enum VoltageError { diff --git a/amdvold/src/main.rs b/amdvold/src/main.rs index d247f5c..6d98577 100644 --- a/amdvold/src/main.rs +++ b/amdvold/src/main.rs @@ -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 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 change_state; mod clock_state; mod command; -mod config; mod error; mod print_states; mod setup_info; @@ -29,6 +29,7 @@ pub struct Opts { #[options(command)] command: Option, } + fn run(config: Config) -> Result<()> { 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"); } 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)?; - } + ensure_config_dir()?; let config_path = Opts::parse_args_default_or_exit() .config diff --git a/amdvold/src/print_states.rs b/amdvold/src/print_states.rs index 09cc29d..8684214 100644 --- a/amdvold/src/print_states.rs +++ b/amdvold/src/print_states.rs @@ -1,6 +1,7 @@ +use amdgpu::utils::hw_mons; + use crate::command::VoltageManipulator; use crate::Config; -use amdgpu::utils::hw_mons; #[derive(Debug, gumdrop::Options)] pub struct PrintStates { diff --git a/amdvold/src/setup_info.rs b/amdvold/src/setup_info.rs index 108ad65..165bcbd 100644 --- a/amdvold/src/setup_info.rs +++ b/amdvold/src/setup_info.rs @@ -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"); diff --git a/build.sh b/build.sh index 214dceb..298372e 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,11 @@ #!/usr/bin/env zsh 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 diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..8b04658 --- /dev/null +++ b/publish.sh @@ -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 diff --git a/services/amdmond b/services/amdmond new file mode 100755 index 0000000..0d35b79 --- /dev/null +++ b/services/amdmond @@ -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 +} diff --git a/services/amdmond.service b/services/amdmond.service new file mode 100644 index 0000000..be2b65f --- /dev/null +++ b/services/amdmond.service @@ -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