mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
feat: fan control hysteresis (#292)
* feat: implement a fan spindown delay setting * chore: change request/response logging to the trace level * feat: add fan speed change threshold setting * feat: implement GUI for hysteresis settings * fix: always set hysteresis settings
This commit is contained in:
parent
16c50970f6
commit
0ad46e2d08
@ -10,8 +10,8 @@ use anyhow::{anyhow, Context};
|
||||
use nix::unistd::getuid;
|
||||
use schema::{
|
||||
request::{ConfirmCommand, SetClocksCommand},
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanCurveMap, PmfwOptions,
|
||||
PowerStates, Request, Response, SystemInfo,
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, Request,
|
||||
Response, SystemInfo,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@ -104,24 +104,8 @@ impl DaemonClient {
|
||||
self.make_request(Request::ListDevices)
|
||||
}
|
||||
|
||||
pub fn set_fan_control(
|
||||
&self,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
mode: Option<FanControlMode>,
|
||||
static_speed: Option<f64>,
|
||||
curve: Option<FanCurveMap>,
|
||||
pmfw: PmfwOptions,
|
||||
) -> anyhow::Result<u64> {
|
||||
self.make_request(Request::SetFanControl {
|
||||
id,
|
||||
enabled,
|
||||
mode,
|
||||
static_speed,
|
||||
curve,
|
||||
pmfw,
|
||||
})?
|
||||
.inner()
|
||||
pub fn set_fan_control(&self, cmd: FanOptions) -> anyhow::Result<u64> {
|
||||
self.make_request(Request::SetFanControl(cmd))?.inner()
|
||||
}
|
||||
|
||||
pub fn set_power_cap(&self, id: &str, cap: Option<f64>) -> anyhow::Result<u64> {
|
||||
|
@ -109,6 +109,8 @@ pub struct FanControlSettings {
|
||||
pub temperature_key: String,
|
||||
pub interval_ms: u64,
|
||||
pub curve: FanCurve,
|
||||
pub spindown_delay_ms: Option<u64>,
|
||||
pub change_threshold: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for FanControlSettings {
|
||||
@ -119,6 +121,8 @@ impl Default for FanControlSettings {
|
||||
temperature_key: "edge".to_owned(),
|
||||
interval_ms: 500,
|
||||
curve: FanCurve(default_fan_curve()),
|
||||
spindown_delay_ms: None,
|
||||
change_threshold: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,11 +232,10 @@ fn default_apply_settings_timer() -> u64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lact_schema::{FanControlMode, PmfwOptions};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{ClocksConfiguration, Config, Daemon, FanControlSettings, Gpu};
|
||||
use crate::server::gpu_controller::fan_control::FanCurve;
|
||||
use lact_schema::{FanControlMode, PmfwOptions};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn serde_de_full() {
|
||||
@ -248,6 +251,8 @@ mod tests {
|
||||
interval_ms: 500,
|
||||
mode: FanControlMode::Curve,
|
||||
static_speed: 0.5,
|
||||
spindown_delay_ms: Some(5000),
|
||||
change_threshold: Some(3),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ pub mod fan_control;
|
||||
|
||||
use self::fan_control::FanCurve;
|
||||
use super::vulkan::get_vulkan_info;
|
||||
use crate::config::{self, ClocksConfiguration};
|
||||
use crate::config::{self, ClocksConfiguration, FanControlSettings};
|
||||
use amdgpu_sysfs::{
|
||||
error::Error,
|
||||
gpu_handle::{
|
||||
@ -19,7 +19,6 @@ use lact_schema::{
|
||||
PciInfo, PmfwInfo, PowerState, PowerStates, PowerStats, VoltageStats, VramStats,
|
||||
};
|
||||
use pciid_parser::Database;
|
||||
use std::collections::BTreeMap;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
@ -29,6 +28,7 @@ use std::{
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{collections::BTreeMap, time::Instant};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::Notify,
|
||||
@ -250,20 +250,17 @@ impl GpuController {
|
||||
}
|
||||
|
||||
pub fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> DeviceStats {
|
||||
let fan_settings = gpu_config.and_then(|config| config.fan_control_settings.as_ref());
|
||||
DeviceStats {
|
||||
fan: FanStats {
|
||||
control_enabled: gpu_config
|
||||
.map(|config| config.fan_control_enabled)
|
||||
.unwrap_or_default(),
|
||||
control_mode: gpu_config
|
||||
.and_then(|config| config.fan_control_settings.as_ref())
|
||||
.map(|settings| settings.mode),
|
||||
static_speed: gpu_config
|
||||
.and_then(|config| config.fan_control_settings.as_ref())
|
||||
.map(|settings| settings.static_speed),
|
||||
curve: gpu_config
|
||||
.and_then(|config| config.fan_control_settings.as_ref())
|
||||
.map(|settings| settings.curve.0.clone()),
|
||||
control_mode: fan_settings.map(|settings| settings.mode),
|
||||
static_speed: fan_settings.map(|settings| settings.static_speed),
|
||||
curve: fan_settings.map(|settings| settings.curve.0.clone()),
|
||||
spindown_delay_ms: fan_settings.and_then(|settings| settings.spindown_delay_ms),
|
||||
change_threshold: fan_settings.and_then(|settings| settings.change_threshold),
|
||||
speed_current: self.hw_mon_and_then(HwMon::get_fan_current),
|
||||
speed_max: self.hw_mon_and_then(HwMon::get_fan_max),
|
||||
speed_min: self.hw_mon_and_then(HwMon::get_fan_min),
|
||||
@ -423,8 +420,7 @@ impl GpuController {
|
||||
async fn start_curve_fan_control(
|
||||
&self,
|
||||
curve: FanCurve,
|
||||
temp_key: String,
|
||||
interval: Duration,
|
||||
settings: FanControlSettings,
|
||||
) -> anyhow::Result<()> {
|
||||
// Use the PMFW curve functionality when it is available
|
||||
// Otherwise, fall back to manual fan control via a task
|
||||
@ -441,18 +437,14 @@ impl GpuController {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
self.start_curve_fan_control_task(curve, temp_key, interval)
|
||||
.await
|
||||
}
|
||||
Err(_) => self.start_curve_fan_control_task(curve, settings).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_curve_fan_control_task(
|
||||
&self,
|
||||
curve: FanCurve,
|
||||
temp_key: String,
|
||||
interval: Duration,
|
||||
settings: FanControlSettings,
|
||||
) -> anyhow::Result<()> {
|
||||
// Stop existing task to re-apply new curve
|
||||
self.stop_fan_control(false).await?;
|
||||
@ -475,7 +467,17 @@ impl GpuController {
|
||||
let notify = Rc::new(Notify::new());
|
||||
let task_notify = notify.clone();
|
||||
|
||||
debug!("spawning new fan control task");
|
||||
let handle = tokio::task::spawn_local(async move {
|
||||
let mut last_pwm = (None, Instant::now());
|
||||
let mut last_temp = 0.0;
|
||||
|
||||
let temp_key = settings.temperature_key.clone();
|
||||
let interval = Duration::from_millis(settings.interval_ms);
|
||||
let spindown_delay = Duration::from_millis(settings.spindown_delay_ms.unwrap_or(0));
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let change_threshold = settings.change_threshold.unwrap_or(0) as f32;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
() = sleep(interval) => (),
|
||||
@ -486,7 +488,31 @@ impl GpuController {
|
||||
let temp = temps
|
||||
.remove(&temp_key)
|
||||
.expect("Could not get temperature by given key");
|
||||
|
||||
let current_temp = temp.current.expect("Missing temp");
|
||||
|
||||
if (last_temp - current_temp).abs() < change_threshold {
|
||||
trace!("temperature changed from {last_temp}°C to {current_temp}°C, which is less than the {change_threshold}°C threshold, skipping speed adjustment");
|
||||
continue;
|
||||
}
|
||||
|
||||
let target_pwm = curve.pwm_at_temp(temp);
|
||||
let now = Instant::now();
|
||||
|
||||
if let (Some(previous_pwm), previous_timestamp) = last_pwm {
|
||||
let diff = now - previous_timestamp;
|
||||
if target_pwm < previous_pwm && diff < spindown_delay {
|
||||
trace!(
|
||||
"delaying fan spindown ({}ms left)",
|
||||
(spindown_delay - diff).as_millis()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
last_pwm = (Some(target_pwm), now);
|
||||
last_temp = current_temp;
|
||||
|
||||
trace!("fan control tick: setting pwm to {target_pwm}");
|
||||
|
||||
if let Err(err) = hw_mon.set_fan_pwm(target_pwm) {
|
||||
@ -501,7 +527,7 @@ impl GpuController {
|
||||
|
||||
debug!(
|
||||
"started fan control with interval {}ms",
|
||||
interval.as_millis()
|
||||
settings.interval_ms
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@ -742,14 +768,9 @@ impl GpuController {
|
||||
return Err(anyhow!("Cannot use empty fan curve"));
|
||||
}
|
||||
|
||||
let interval = Duration::from_millis(settings.interval_ms);
|
||||
self.start_curve_fan_control(
|
||||
settings.curve.clone(),
|
||||
settings.temperature_key.clone(),
|
||||
interval,
|
||||
)
|
||||
.await
|
||||
.context("Failed to set curve fan control")?;
|
||||
self.start_curve_fan_control(settings.curve.clone(), settings.clone())
|
||||
.await
|
||||
.context("Failed to set curve fan control")?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -11,7 +11,7 @@ use anyhow::{anyhow, Context};
|
||||
use lact_schema::{
|
||||
default_fan_curve,
|
||||
request::{ConfirmCommand, SetClocksCommand},
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanCurveMap, PmfwOptions,
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions,
|
||||
PowerStates,
|
||||
};
|
||||
use libflate::gzip;
|
||||
@ -270,40 +270,35 @@ impl<'a> Handler {
|
||||
self.controller_by_id(id)?.get_clocks_info()
|
||||
}
|
||||
|
||||
pub async fn set_fan_control(
|
||||
&'a self,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
mode: Option<FanControlMode>,
|
||||
static_speed: Option<f64>,
|
||||
curve: Option<FanCurveMap>,
|
||||
pmfw: PmfwOptions,
|
||||
) -> anyhow::Result<u64> {
|
||||
pub async fn set_fan_control(&'a self, opts: FanOptions<'_>) -> anyhow::Result<u64> {
|
||||
let settings = {
|
||||
let mut config_guard = self
|
||||
.config
|
||||
.try_borrow_mut()
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
let gpu_config = config_guard.gpus.entry(id.to_owned()).or_default();
|
||||
let gpu_config = config_guard.gpus.entry(opts.id.to_owned()).or_default();
|
||||
|
||||
match mode {
|
||||
match opts.mode {
|
||||
Some(mode) => match mode {
|
||||
FanControlMode::Static => {
|
||||
if matches!(static_speed, Some(speed) if !(0.0..=1.0).contains(&speed)) {
|
||||
if matches!(opts.static_speed, Some(speed) if !(0.0..=1.0).contains(&speed))
|
||||
{
|
||||
return Err(anyhow!("static speed value out of range"));
|
||||
}
|
||||
|
||||
if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone()
|
||||
{
|
||||
existing_settings.mode = mode;
|
||||
if let Some(static_speed) = static_speed {
|
||||
if let Some(static_speed) = opts.static_speed {
|
||||
existing_settings.static_speed = static_speed;
|
||||
}
|
||||
Some(existing_settings)
|
||||
} else {
|
||||
Some(FanControlSettings {
|
||||
mode,
|
||||
static_speed: static_speed.unwrap_or_else(default_fan_static_speed),
|
||||
static_speed: opts
|
||||
.static_speed
|
||||
.unwrap_or_else(default_fan_static_speed),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@ -312,18 +307,27 @@ impl<'a> Handler {
|
||||
if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone()
|
||||
{
|
||||
existing_settings.mode = mode;
|
||||
if let Some(raw_curve) = curve {
|
||||
if let Some(change_threshold) = opts.change_threshold {
|
||||
existing_settings.change_threshold = Some(change_threshold);
|
||||
}
|
||||
if let Some(spindown_delay) = opts.spindown_delay_ms {
|
||||
existing_settings.spindown_delay_ms = Some(spindown_delay);
|
||||
}
|
||||
|
||||
if let Some(raw_curve) = opts.curve {
|
||||
let curve = FanCurve(raw_curve);
|
||||
curve.validate()?;
|
||||
existing_settings.curve = curve;
|
||||
}
|
||||
Some(existing_settings)
|
||||
} else {
|
||||
let curve = FanCurve(curve.unwrap_or_else(default_fan_curve));
|
||||
let curve = FanCurve(opts.curve.unwrap_or_else(default_fan_curve));
|
||||
curve.validate()?;
|
||||
Some(FanControlSettings {
|
||||
mode,
|
||||
curve,
|
||||
change_threshold: opts.change_threshold,
|
||||
spindown_delay_ms: opts.spindown_delay_ms,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@ -333,12 +337,12 @@ impl<'a> Handler {
|
||||
}
|
||||
};
|
||||
|
||||
self.edit_gpu_config(id.to_owned(), |config| {
|
||||
config.fan_control_enabled = enabled;
|
||||
self.edit_gpu_config(opts.id.to_owned(), |config| {
|
||||
config.fan_control_enabled = opts.enabled;
|
||||
if let Some(settings) = settings {
|
||||
config.fan_control_settings = Some(settings);
|
||||
}
|
||||
config.pmfw_options = pmfw;
|
||||
config.pmfw_options = opts.pmfw;
|
||||
})
|
||||
.await
|
||||
.context("Failed to edit GPU config")
|
||||
|
@ -12,7 +12,7 @@ use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::{UnixListener, UnixStream},
|
||||
};
|
||||
use tracing::{debug, error, instrument};
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
pub struct Server {
|
||||
pub handler: Handler,
|
||||
@ -52,7 +52,7 @@ pub async fn handle_stream(stream: UnixStream, handler: Handler) -> anyhow::Resu
|
||||
|
||||
let mut buf = String::new();
|
||||
while stream.read_line(&mut buf).await? != 0 {
|
||||
debug!("handling request: {}", buf.trim_end());
|
||||
trace!("handling request: {}", buf.trim_end());
|
||||
|
||||
let maybe_request = serde_json::from_str(&buf);
|
||||
let response = match maybe_request {
|
||||
@ -86,18 +86,7 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho
|
||||
Request::DevicePowerProfileModes { id } => {
|
||||
ok_response(handler.get_power_profile_modes(id)?)
|
||||
}
|
||||
Request::SetFanControl {
|
||||
id,
|
||||
enabled,
|
||||
mode,
|
||||
static_speed,
|
||||
curve,
|
||||
pmfw,
|
||||
} => ok_response(
|
||||
handler
|
||||
.set_fan_control(id, enabled, mode, static_speed, curve, pmfw)
|
||||
.await?,
|
||||
),
|
||||
Request::SetFanControl(opts) => ok_response(handler.set_fan_control(opts).await?),
|
||||
Request::ResetPmfw { id } => ok_response(handler.reset_pmfw(id).await?),
|
||||
Request::SetPowerCap { id, cap } => ok_response(handler.set_power_cap(id, cap).await?),
|
||||
Request::SetPerformanceLevel {
|
||||
@ -127,7 +116,7 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho
|
||||
}
|
||||
|
||||
fn ok_response<T: Serialize + Debug>(data: T) -> anyhow::Result<Vec<u8>> {
|
||||
debug!("responding with {data:?}");
|
||||
trace!("responding with {data:?}");
|
||||
Ok(serde_json::to_vec(&Response::Ok(data))?)
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ use gtk::glib::{timeout_future, ControlFlow};
|
||||
use gtk::{gio::ApplicationFlags, prelude::*, *};
|
||||
use header::Header;
|
||||
use lact_client::schema::request::{ConfirmCommand, SetClocksCommand};
|
||||
use lact_client::schema::GIT_COMMIT;
|
||||
use lact_client::schema::{FanOptions, GIT_COMMIT};
|
||||
use lact_client::DaemonClient;
|
||||
use lact_daemon::MODULE_CONF_PATH;
|
||||
use root_stack::RootStack;
|
||||
@ -408,16 +408,19 @@ impl App {
|
||||
|
||||
if let Some(thermals_settings) = self.root_stack.thermals_page.get_thermals_settings() {
|
||||
debug!("applying thermal settings: {thermals_settings:?}");
|
||||
let opts = FanOptions {
|
||||
id: &gpu_id,
|
||||
enabled: thermals_settings.manual_fan_control,
|
||||
mode: thermals_settings.mode,
|
||||
static_speed: thermals_settings.static_speed,
|
||||
curve: thermals_settings.curve,
|
||||
pmfw: thermals_settings.pmfw,
|
||||
spindown_delay_ms: thermals_settings.spindown_delay_ms,
|
||||
change_threshold: thermals_settings.change_threshold,
|
||||
};
|
||||
|
||||
self.daemon_client
|
||||
.set_fan_control(
|
||||
&gpu_id,
|
||||
thermals_settings.manual_fan_control,
|
||||
thermals_settings.mode,
|
||||
thermals_settings.static_speed,
|
||||
thermals_settings.curve,
|
||||
thermals_settings.pmfw,
|
||||
)
|
||||
.set_fan_control(opts)
|
||||
.context("Could not set fan control")?;
|
||||
self.daemon_client
|
||||
.confirm_pending_config(ConfirmCommand::Confirm)
|
||||
|
@ -1,6 +1,7 @@
|
||||
mod point_adjustment;
|
||||
|
||||
use self::point_adjustment::PointAdjustment;
|
||||
use crate::app::root_stack::oc_adjustment::OcAdjustment;
|
||||
use glib::clone;
|
||||
use gtk::graphene::Point;
|
||||
use gtk::gsk::Transform;
|
||||
@ -11,11 +12,17 @@ use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
const DEFAULT_CHANGE_THRESHOLD: u64 = 2;
|
||||
const DEFAULT_SPINDOWN_DELAY_MS: u64 = 5000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FanCurveFrame {
|
||||
pub container: Box,
|
||||
curve_container: Frame,
|
||||
points: Rc<RefCell<Vec<PointAdjustment>>>,
|
||||
spindown_delay_adj: OcAdjustment,
|
||||
change_threshold_adj: OcAdjustment,
|
||||
hysteresis_grid: Grid,
|
||||
}
|
||||
|
||||
impl FanCurveFrame {
|
||||
@ -68,15 +75,55 @@ impl FanCurveFrame {
|
||||
|
||||
let points = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let hysteresis_grid = Grid::new();
|
||||
hysteresis_grid.set_margin_top(10);
|
||||
|
||||
let spindown_delay_adj = oc_adjustment_row(
|
||||
&hysteresis_grid,
|
||||
0,
|
||||
"Spindown delay",
|
||||
"How long the GPU needs to remain at a lower temperature point for before ramping down the fan",
|
||||
" ms",
|
||||
OcAdjustmentOptions {
|
||||
default: DEFAULT_SPINDOWN_DELAY_MS as f64,
|
||||
min: 0.0,
|
||||
max: 30_000.0,
|
||||
step: 10.0,
|
||||
digits: 0,
|
||||
},
|
||||
);
|
||||
|
||||
let change_threshold_adj = oc_adjustment_row(
|
||||
&hysteresis_grid,
|
||||
1,
|
||||
"Speed change threshold",
|
||||
"Hysteresis",
|
||||
"°C",
|
||||
OcAdjustmentOptions {
|
||||
default: DEFAULT_CHANGE_THRESHOLD as f64,
|
||||
min: 0.0,
|
||||
max: 10.0,
|
||||
step: 1.0,
|
||||
digits: 0,
|
||||
},
|
||||
);
|
||||
|
||||
root_box.append(&hysteresis_grid);
|
||||
|
||||
let curve_frame = Self {
|
||||
container: root_box,
|
||||
curve_container,
|
||||
points,
|
||||
spindown_delay_adj: spindown_delay_adj.clone(),
|
||||
change_threshold_adj: change_threshold_adj.clone(),
|
||||
hysteresis_grid,
|
||||
};
|
||||
|
||||
default_button.connect_clicked(clone!(@strong curve_frame => move |_| {
|
||||
let curve = default_fan_curve();
|
||||
curve_frame.set_curve(&curve);
|
||||
spindown_delay_adj.set_value(DEFAULT_SPINDOWN_DELAY_MS as f64);
|
||||
change_threshold_adj.set_value(DEFAULT_CHANGE_THRESHOLD as f64);
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(@strong curve_frame => move |_| {
|
||||
@ -148,6 +195,15 @@ impl FanCurveFrame {
|
||||
}
|
||||
|
||||
pub fn connect_adjusted<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
self.change_threshold_adj
|
||||
.connect_value_changed(clone!(@strong f => move |_| {
|
||||
f();
|
||||
}));
|
||||
self.spindown_delay_adj
|
||||
.connect_value_changed(clone!(@strong f => move |_| {
|
||||
f();
|
||||
}));
|
||||
|
||||
let closure = clone!(@strong f => move |_: &Adjustment| {
|
||||
f();
|
||||
});
|
||||
@ -157,6 +213,97 @@ impl FanCurveFrame {
|
||||
point.temperature.connect_value_changed(closure.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_change_threshold(&self, value: Option<u64>) {
|
||||
self.change_threshold_adj
|
||||
.set_initial_value(value.unwrap_or(0) as f64);
|
||||
}
|
||||
|
||||
pub fn set_spindown_delay_ms(&self, value: Option<u64>) {
|
||||
self.spindown_delay_adj
|
||||
.set_initial_value(value.unwrap_or(0) as f64);
|
||||
}
|
||||
|
||||
pub fn get_change_threshold(&self) -> u64 {
|
||||
self.change_threshold_adj.value() as u64
|
||||
}
|
||||
|
||||
pub fn get_spindown_delay_ms(&self) -> u64 {
|
||||
self.spindown_delay_adj.value() as u64
|
||||
}
|
||||
|
||||
pub fn set_hysteresis_settings_visibile(&self, visible: bool) {
|
||||
self.hysteresis_grid.set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
struct OcAdjustmentOptions {
|
||||
default: f64,
|
||||
min: f64,
|
||||
max: f64,
|
||||
step: f64,
|
||||
digits: i32,
|
||||
}
|
||||
|
||||
fn oc_adjustment_row(
|
||||
grid: &Grid,
|
||||
row: i32,
|
||||
label: &str,
|
||||
tooltip: &str,
|
||||
unit: &'static str,
|
||||
opts: OcAdjustmentOptions,
|
||||
) -> OcAdjustment {
|
||||
let label = Label::builder()
|
||||
.label(label)
|
||||
.halign(Align::Start)
|
||||
.tooltip_text(tooltip)
|
||||
.build();
|
||||
let adjustment = OcAdjustment::new(
|
||||
opts.default,
|
||||
opts.min,
|
||||
opts.max,
|
||||
opts.step,
|
||||
opts.step,
|
||||
opts.step,
|
||||
);
|
||||
|
||||
let scale = Scale::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.adjustment(&adjustment)
|
||||
.hexpand(true)
|
||||
.round_digits(opts.digits)
|
||||
.digits(opts.digits)
|
||||
.value_pos(PositionType::Right)
|
||||
.margin_start(5)
|
||||
.margin_end(5)
|
||||
.build();
|
||||
|
||||
let value_selector = SpinButton::new(Some(&adjustment), opts.step, opts.digits as u32);
|
||||
|
||||
let value_label = Label::new(Some(&format!("{}{unit}", opts.default)));
|
||||
|
||||
let popover = Popover::builder().child(&value_selector).build();
|
||||
let value_button = MenuButton::builder()
|
||||
.popover(&popover)
|
||||
.child(&value_label)
|
||||
.build();
|
||||
|
||||
adjustment.connect_value_changed(clone!(@strong value_label => move |adjustment| {
|
||||
let value = match opts.digits {
|
||||
0 => adjustment.value().round(),
|
||||
_ => {
|
||||
let rounding = opts.digits as f64 * 10.0;
|
||||
(adjustment.value() * rounding).round() / rounding
|
||||
}
|
||||
};
|
||||
value_label.set_text(&format!("{value}{unit}"));
|
||||
}));
|
||||
|
||||
grid.attach(&label, 0, row, 1, 1);
|
||||
grid.attach(&scale, 1, row, 4, 1);
|
||||
grid.attach(&value_button, 6, row, 4, 1);
|
||||
|
||||
adjustment
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "gtk-tests"))]
|
||||
|
@ -5,7 +5,7 @@ use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{
|
||||
default_fan_curve, DeviceInfo, DeviceStats, FanControlMode, FanCurveMap, PmfwOptions,
|
||||
default_fan_curve, DeviceInfo, DeviceStats, FanControlMode, FanCurveMap, PmfwInfo, PmfwOptions,
|
||||
SystemInfo,
|
||||
};
|
||||
use lact_daemon::AMDGPU_FAMILY_GC_11_0_0;
|
||||
@ -25,6 +25,8 @@ pub struct ThermalsSettings {
|
||||
pub static_speed: Option<f64>,
|
||||
pub curve: Option<FanCurveMap>,
|
||||
pub pmfw: PmfwOptions,
|
||||
pub spindown_delay_ms: Option<u64>,
|
||||
pub change_threshold: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -195,6 +197,15 @@ impl ThermalsPage {
|
||||
self.fan_curve_frame.set_curve(curve);
|
||||
}
|
||||
|
||||
self.fan_curve_frame
|
||||
.set_spindown_delay_ms(stats.fan.spindown_delay_ms);
|
||||
self.fan_curve_frame
|
||||
.set_change_threshold(stats.fan.change_threshold);
|
||||
|
||||
// Only show hysteresis settings when PMFW is not used
|
||||
self.fan_curve_frame
|
||||
.set_hysteresis_settings_visibile(stats.fan.pmfw_info == PmfwInfo::default());
|
||||
|
||||
if !stats.fan.control_enabled && self.fan_curve_frame.get_curve().is_empty() {
|
||||
self.fan_curve_frame.set_curve(&default_fan_curve());
|
||||
}
|
||||
@ -246,6 +257,8 @@ impl ThermalsPage {
|
||||
static_speed,
|
||||
curve,
|
||||
pmfw,
|
||||
change_threshold: Some(self.fan_curve_frame.get_change_threshold()),
|
||||
spindown_delay_ms: Some(self.fan_curve_frame.get_spindown_delay_ms()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
@ -197,6 +197,8 @@ pub struct FanStats {
|
||||
pub speed_current: Option<u32>,
|
||||
pub speed_max: Option<u32>,
|
||||
pub speed_min: Option<u32>,
|
||||
pub spindown_delay_ms: Option<u64>,
|
||||
pub change_threshold: Option<u64>,
|
||||
// RDNA3+ params
|
||||
#[serde(default)]
|
||||
pub pmfw_info: PmfwInfo,
|
||||
@ -273,3 +275,17 @@ pub struct PmfwOptions {
|
||||
pub minimum_pwm: Option<u32>,
|
||||
pub target_temperature: Option<u32>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
pub struct FanOptions<'a> {
|
||||
pub id: &'a str,
|
||||
pub enabled: bool,
|
||||
pub mode: Option<FanControlMode>,
|
||||
pub static_speed: Option<f64>,
|
||||
pub curve: Option<FanCurveMap>,
|
||||
#[serde(default)]
|
||||
pub pmfw: PmfwOptions,
|
||||
pub spindown_delay_ms: Option<u64>,
|
||||
pub change_threshold: Option<u64>,
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{FanControlMode, FanCurveMap, PmfwOptions};
|
||||
use crate::FanOptions;
|
||||
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -20,15 +20,7 @@ pub enum Request<'a> {
|
||||
DevicePowerProfileModes {
|
||||
id: &'a str,
|
||||
},
|
||||
SetFanControl {
|
||||
id: &'a str,
|
||||
enabled: bool,
|
||||
mode: Option<FanControlMode>,
|
||||
static_speed: Option<f64>,
|
||||
curve: Option<FanCurveMap>,
|
||||
#[serde(default)]
|
||||
pmfw: PmfwOptions,
|
||||
},
|
||||
SetFanControl(FanOptions<'a>),
|
||||
ResetPmfw {
|
||||
id: &'a str,
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{Pong, Request, Response};
|
||||
use crate::{FanControlMode, FanOptions, PmfwOptions, Pong, Request, Response};
|
||||
use anyhow::anyhow;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn ping_requset() {
|
||||
@ -57,3 +58,31 @@ fn error_response() {
|
||||
|
||||
assert_eq!(serde_json::to_value(response).unwrap(), expected_response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_fan_clocks() {
|
||||
let value = r#"{
|
||||
"command": "set_fan_control",
|
||||
"args": {
|
||||
"id": "123",
|
||||
"enabled": true,
|
||||
"mode": "curve",
|
||||
"curve": {
|
||||
"30": 30.0,
|
||||
"50": 50.0
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let request: Request = serde_json::from_str(value).unwrap();
|
||||
let expected_request = Request::SetFanControl(FanOptions {
|
||||
id: "123",
|
||||
enabled: true,
|
||||
mode: Some(FanControlMode::Curve),
|
||||
static_speed: None,
|
||||
curve: Some(BTreeMap::from([(30, 30.0), (50, 50.0)])),
|
||||
pmfw: PmfwOptions::default(),
|
||||
spindown_delay_ms: None,
|
||||
change_threshold: None,
|
||||
});
|
||||
assert_eq!(expected_request, request);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user