From f94ffbf27680275a7724fc49208a65fa34c3755f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Tue, 28 Jun 2022 15:58:26 +0200 Subject: [PATCH] Add fan config cli editor --- Cargo.lock | 89 +++++++++++++ Cargo.toml | 1 + amdfan/Cargo.toml | 26 ++++ amdfan/src/main.rs | 312 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 428 insertions(+) create mode 100644 amdfan/Cargo.toml create mode 100644 amdfan/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index d6e984a..f8e2d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "amdfan" +version = "0.1.0" +dependencies = [ + "amdgpu", + "amdgpu-config", + "crossbeam", + "crossterm", + "gumdrop", + "log", + "pretty_env_logger", + "ron 0.1.7", + "serde", + "thiserror", + "toml", + "tui", +] + [[package]] name = "amdfand" version = "1.0.13" @@ -313,6 +331,12 @@ dependencies = [ "nix 0.18.0", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.73" @@ -584,6 +608,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.8.3", + "parking_lot 0.12.1", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "csv" version = "1.1.6" @@ -1844,6 +1893,27 @@ dependencies = [ "libc", ] +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio 0.8.3", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2060,6 +2130,19 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d4b50cba812f0f04f0707bb6a0eaa5fae4ae05d90fc2a377998d2f21e77a1c" +[[package]] +name = "tui" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "unicode-ident" version = "1.0.1" @@ -2072,6 +2155,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 9bfa172..e9eed73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "amdgpu", "amdgpu-config", + "amdfan", "amdfand", "amdvold", "amdmond", diff --git a/amdfan/Cargo.toml b/amdfan/Cargo.toml new file mode 100644 index 0000000..aa0eda6 --- /dev/null +++ b/amdfan/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "amdfan" +version = "0.1.0" +edition = "2021" + +[dependencies] +amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] } +amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] } + +serde = { version = "1.0", features = ["derive"] } +toml = { version = "0.5" } +ron = { version = "0.1" } + +thiserror = { version = "1.0" } +gumdrop = { version = "0.8" } + +log = { version = "0.4" } +pretty_env_logger = { version = "0.4" } + +tui = { version = "0.18.0", features = [] } +crossbeam = { version = "0.8.1" } +crossterm = { version = "0.23.2" } + +[dev-dependencies] +amdgpu = { path = "../amdgpu", version = "1.0" } +amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] } diff --git a/amdfan/src/main.rs b/amdfan/src/main.rs new file mode 100644 index 0000000..efabe27 --- /dev/null +++ b/amdfan/src/main.rs @@ -0,0 +1,312 @@ +use amdgpu_config::fan::{MatrixPoint, DEFAULT_FAN_CONFIG_PATH}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use gumdrop::Options; +use std::path::PathBuf; +use std::time::Instant; +use std::{io, time::Duration}; +use tui::backend::Backend; +use tui::style::{Color, Modifier, Style}; +use tui::symbols::Marker; +use tui::{backend::CrosstermBackend, layout::*, widgets::*, Frame, Terminal}; + +#[derive(Options)] +struct Opts { + #[options(help = "Help message")] + help: bool, + #[options(help = "Config file location")] + config: Option, +} + +struct App<'a> { + config: amdgpu_config::fan::Config, + events: Vec<(&'a str, &'a str)>, + table_state: TableState, + selected_point: Option, +} + +impl<'a> App<'a> { + pub fn new(config: amdgpu_config::fan::Config) -> Self { + let mut table_state = TableState::default(); + table_state.select(Some(0)); + + Self { + config, + events: vec![], + table_state, + selected_point: None, + } + } + + /// Rotate through the event list. + /// This only exists to simulate some kind of "progress" + fn on_tick(&mut self) { + if self.events.is_empty() { + return; + } + let event = self.events.remove(0); + self.events.push(event); + } +} + +fn main() -> Result<(), io::Error> { + let opts: Opts = Options::parse_args_default_or_exit(); + let config_path = opts + .config + .as_deref() + .and_then(|p| p.to_str()) + .unwrap_or(DEFAULT_FAN_CONFIG_PATH); + let config = amdgpu_config::fan::load_config(config_path).unwrap(); + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let tick_rate = Duration::from_millis(250); + let app = App::new(config); + run_app(&mut terminal, app, tick_rate).unwrap(); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &mut app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if event::poll(timeout)? { + if let Status::Exit = handle_event(&mut app)? { + return Ok(()); + } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } + } +} + +enum Status { + Continue, + Exit, +} + +fn handle_event(app: &mut App) -> io::Result { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(Status::Exit), + KeyCode::Char('w') => { + let s = toml::to_string_pretty(&app.config).unwrap(); + std::fs::write(app.config.path(), &s)?; + } + KeyCode::Char(' ') => { + if let Some(index) = app.table_state.selected() { + app.selected_point = Some(index); + } + } + KeyCode::Down => match app.selected_point { + Some(index) => { + change_value(app, index, -1.0, y, y_mut); + } + None => match app.table_state.selected() { + Some(index) => { + if index + 1 < app.config.speed_matrix().len() { + app.table_state.select(Some(index + 1)); + } + } + None => { + app.table_state.select(Some(0)); + } + }, + }, + KeyCode::Up => match app.selected_point.clone() { + Some(index) => { + change_value(app, index, 1.0, y, y_mut); + } + None => { + if let Some(prev) = app + .table_state + .selected() + .and_then(|idx| idx.checked_sub(1)) + { + app.table_state.select(Some(prev)); + } + } + }, + KeyCode::Left => { + if let Some(index) = app.selected_point.clone() { + change_value(app, index, -1.0, x, x_mut); + } + } + KeyCode::Right => { + if let Some(index) = app.selected_point.clone() { + change_value(app, index, 1.0, x, x_mut); + } + } + _ => {} + } + } + Ok(Status::Continue) +} + +fn ui(f: &mut Frame, app: &mut App) { + let main_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(90), Constraint::Length(4)].as_ref()) + .split(f.size()); + + { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(30), Constraint::Min(100)].as_ref()) + .split(main_chunks[0]); + + { + let rows = app + .config + .speed_matrix() + .iter() + .enumerate() + .map(|(idx, point)| { + Row::new(vec![format!("{}", y(point)), format!("{}", x(point))]).style( + Style::default().fg(if Some(idx) == app.selected_point { + Color::Yellow + } else { + Color::White + }), + ) + }); + let table = Table::new(rows) + .block(Block::default()) + .header( + Row::new(vec!["Speed", "Temp"]) + .style(Style::default().fg(Color::Yellow)) + .bottom_margin(1), + ) + .widths(&[Constraint::Length(5), Constraint::Length(5)]) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("*") + .column_spacing(1); + f.render_stateful_widget(table, chunks[0], &mut app.table_state); + } + + { + let points = app + .config + .speed_matrix() + .iter() + .map(|point| (x(point), y(point))) + .collect::>(); + let dataset = vec![Dataset::default() + .data(&points) + .name("Temp/Speed") + .graph_type(GraphType::Scatter) + .marker(Marker::Dot) + .style(Style::default().fg(Color::White))]; + let chart = Chart::new(dataset) + .block( + Block::default() + .style(Style::default().fg(Color::White)) + .border_style(Style::default().fg(Color::White)) + .borders(Borders::all()), + ) + .style(Style::default()) + .x_axis(Axis::default().title("Speed").bounds([0.0, 100.0])) + .y_axis(Axis::default().title("Temp").bounds([0.0, 100.0])); + f.render_widget(chart, chunks[1]); + } + + { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ] + .as_ref(), + ) + .split(main_chunks[1]); + + f.render_widget(Paragraph::new("q to exit"), chunks[0]); + f.render_widget(Paragraph::new("Move up/down to choose setting"), chunks[1]); + f.render_widget( + Paragraph::new("Press SPACE to select and up/down/left/right to change values"), + chunks[2], + ); + f.render_widget(Paragraph::new("w to save"), chunks[3]); + } + } +} + +fn change_value(app: &mut App, index: usize, change: f64, read: Read, write: Write) +where + Read: FnOnce(&MatrixPoint) -> f64 + Copy, + Write: FnOnce(&mut MatrixPoint) -> &mut f64, +{ + let prev = index + .checked_sub(1) + .and_then(|i| app.config.speed_matrix().get(i)) + .map(|v| read(v)) + .unwrap_or(0.0); + let next = app + .config + .speed_matrix() + .get(index + 1) + .map(|v| read(v)) + .unwrap_or(100.0); + let current = app + .config + .speed_matrix() + .get(index) + .map(read) + .unwrap_or_default(); + + if (prev..=next).contains(&(current + change)) { + *app.config + .speed_matrix_mut() + .get_mut(index) + .map(write) + .unwrap() += change; + } +} + +fn y_mut(p: &mut MatrixPoint) -> &mut f64 { + &mut p.speed +} + +fn x_mut(p: &mut MatrixPoint) -> &mut f64 { + &mut p.temp +} + +fn y(p: &MatrixPoint) -> f64 { + p.speed +} + +fn x(p: &MatrixPoint) -> f64 { + p.temp +}