Skip to content

Commit

Permalink
WIP smooth fans
Browse files Browse the repository at this point in the history
  • Loading branch information
curiousercreative committed Nov 7, 2022
1 parent 4cceb52 commit 2f4f3d1
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 15 deletions.
21 changes: 16 additions & 5 deletions src/daemon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::{
Arc,
},
thread,
time::Duration,
time::{Duration, Instant},
};
use tokio::{
signal::unix::{signal, SignalKind},
Expand Down Expand Up @@ -106,7 +106,7 @@ impl PowerDaemon {
func(&mut self.profile_errors, self.initial_set);

let message =
Message::new_signal(DBUS_PATH, DBUS_NAME, "PowerProfileSwitch").unwrap().append1(name);
Message::new_signal(DBUS_PATH, DBUS_NAME, "PowerProfileSwitch").unwrap().append1(String::from(name));

if let Err(()) = self.dbus_connection.send(message) {
log::error!("failed to send power profile switch message");
Expand Down Expand Up @@ -222,6 +222,7 @@ pub async fn daemon() -> Result<(), String> {

let mut daemon = PowerDaemon::new(c.clone())?;
let nvidia_exists = !daemon.graphics.nvidia.is_empty();
let mut fan_daemon = FanDaemon::new(nvidia_exists, daemon.get_profile().unwrap());

log::info!("Disabling NMI Watchdog (for kernel debugging only)");
NmiWatchdog::default().set(b"0");
Expand Down Expand Up @@ -307,7 +308,16 @@ pub async fn daemon() -> Result<(), String> {
);
sync_get_method(b, "GetChargeProfiles", "profiles", PowerDaemon::get_charge_profiles);
b.signal::<(u64,), _>("HotPlugDetect", ("port",));
b.signal::<(&str,), _>("PowerProfileSwitch", ("profile",));
b.signal::<(String,), _>("PowerProfileSwitch", ("profile",));
b.method_with_cr(
"PowerProfileSwitch",
("profile",),
(),
move |ctx, _cr, (profile,): (String,)| {
// fan_daemon = FanDaemon::new(nvidia_exists, profile);
Ok(())
},
);
});
cr.insert(DBUS_PATH, &[iface_token], daemon);

Expand All @@ -323,8 +333,6 @@ pub async fn daemon() -> Result<(), String> {
// Spawn hid backlight daemon
let _hid_backlight = thread::spawn(hid_backlight::daemon);

let mut fan_daemon = FanDaemon::new(nvidia_exists);

let mut hpd_res = unsafe { HotPlugDetect::new(nvidia_device_id) };

let mux_res = unsafe { mux::DisplayPortMux::new() };
Expand All @@ -337,12 +345,15 @@ pub async fn daemon() -> Result<(), String> {
}
};

// let mut last_second = Instant::now();
let mut last = hpd();

log::info!("Handling dbus requests");

while CONTINUE.load(Ordering::SeqCst) {
sleep(Duration::from_millis(1000)).await;

// run fans on a 250ms loop and everything else on a 1000ms loop
fan_daemon.step();

let hpd = hpd();
Expand Down
136 changes: 126 additions & 10 deletions src/fan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,23 @@

use std::{
cell::Cell,
cmp, fs, io,
cmp,
collections::VecDeque,
fs,
io,
process::{Command, Stdio},
};
use sysfs_class::{HwMon, SysClass};

const COOLDOWN_SIZE: usize = from_seconds(2) as usize;
const HEATUP_SIZE: usize = from_seconds(1) as usize;

const fn from_seconds (seconds: u8) -> u8 {
const INTERVAL: usize = 1000;

return (1000 * (seconds as usize) / INTERVAL) as u8;
}

#[derive(Debug, thiserror::Error)]
pub enum FanDaemonError {
#[error("failed to collect hwmon devices: {}", _0)]
Expand All @@ -28,24 +40,31 @@ pub struct FanDaemon {
cpus: Vec<HwMon>,
nvidia_exists: bool,
displayed_warning: Cell<bool>,
fan_cooldown: VecDeque<u8>,
fan_heatup: VecDeque<u8>,
last_duty: u8,
}

impl FanDaemon {
pub fn new(nvidia_exists: bool) -> Self {
pub fn new(nvidia_exists: bool, profile: String) -> Self {
let model = fs::read_to_string("/sys/class/dmi/id/product_version").unwrap_or_default();
let mut daemon = FanDaemon {
curve: match model.trim() {
"thelio-major-r1" => FanCurve::threadripper2(),
"thelio-major-r2" | "thelio-major-r2.1" | "thelio-major-b1" | "thelio-major-b2"
| "thelio-major-b3" | "thelio-mega-r1" | "thelio-mega-r1.1" => FanCurve::hedt(),
"thelio-massive-b1" => FanCurve::xeon(),
"galp5" => FanCurve::galp5(profile),
_ => FanCurve::standard(),
},
amdgpus: Vec::new(),
platforms: Vec::new(),
cpus: Vec::new(),
nvidia_exists,
displayed_warning: Cell::new(false),
fan_cooldown: VecDeque::with_capacity(COOLDOWN_SIZE),
fan_heatup: VecDeque::with_capacity(HEATUP_SIZE),
last_duty: 0,
};

if let Err(err) = daemon.discover() {
Expand All @@ -67,7 +86,7 @@ impl FanDaemon {

match name.as_str() {
"amdgpu" => self.amdgpus.push(hwmon),
"system76" => (), // TODO: Support laptops
"system76_acpi" => self.platforms.push(hwmon),
"system76_io" => self.platforms.push(hwmon),
"coretemp" | "k10temp" => self.cpus.push(hwmon),
_ => (),
Expand Down Expand Up @@ -98,7 +117,7 @@ impl FanDaemon {
.fold(None, |mut temp_opt, input| {
// Assume temperatures are always above freezing
if temp_opt.map_or(true, |x| input as u32 > x) {
log::debug!("highest hwmon cpu/gpu temp: {}", input);
log::warn!("highest hwmon cpu/gpu temp: {}", input);
temp_opt = Some(input as u32);
}

Expand Down Expand Up @@ -139,11 +158,13 @@ impl FanDaemon {

/// Set the current duty cycle, from 0 to 255
/// 0 to 255 is the standard Linux hwmon pwm unit
pub fn set_duty(&self, duty_opt: Option<u8>) {
pub fn set_duty(&mut self, duty_opt: Option<u8>) {
if let Some(duty) = duty_opt {
self.last_duty = duty;
let duty_str = format!("{}", duty);
for platform in &self.platforms {
let _ = platform.write_file("pwm1_enable", "1");
let _ = platform.write_file("pwm1_enable", "2");
let _ = platform.write_file("pwm2_enable", "2");
let _ = platform.write_file("pwm1", &duty_str);
let _ = platform.write_file("pwm2", &duty_str);
}
Expand All @@ -154,10 +175,65 @@ impl FanDaemon {
}
}

fn smooth_duty(&mut self, duty_opt: Option<u8>) -> Option<u8> {
let SMOOTH_FANS = self.curve.SMOOTH_FANS.unwrap_or(0);
let SMOOTH_FANS_DOWN = self.curve.SMOOTH_FANS_DOWN.unwrap_or(SMOOTH_FANS);
let SMOOTH_FANS_UP = self.curve.SMOOTH_FANS_UP.unwrap_or(SMOOTH_FANS);
let SMOOTH_FANS_MIN = self.curve.SMOOTH_FANS_MIN;
let MAX_JUMP_DOWN = (255 / SMOOTH_FANS_DOWN) as u8;
let MAX_JUMP_UP = (255 / SMOOTH_FANS_UP) as u8;

if let Some(duty) = duty_opt {
let last_duty = self.last_duty;
let mut next_duty = duty;

self.fan_heatup.truncate(HEATUP_SIZE - 1);
self.fan_heatup.push_front(next_duty);
next_duty = *self.fan_heatup.iter().min().unwrap();

self.fan_cooldown.truncate(COOLDOWN_SIZE - 1);
self.fan_cooldown.push_front(next_duty);
next_duty = *self.fan_cooldown.iter().max().unwrap();

log::warn!("last_duty:{}, duty:{}, next_duty:{}", last_duty, duty, next_duty);

// ramping down
if next_duty < last_duty {
// out of bounds (lower) safeguard
let smoothed = last_duty.saturating_sub(MAX_JUMP_DOWN);

// use smoothed value if above min and if smoothed is closer than raw
if smoothed > SMOOTH_FANS_MIN {
next_duty = cmp::max(smoothed, next_duty);
}

log::warn!("ramping down, last_duty:{}, smoothed:{}, next_duty:{}", last_duty, smoothed, next_duty);
}

// ramping up
if next_duty > last_duty {
// out of bounds (higher) safeguard
let smoothed = last_duty.saturating_add(MAX_JUMP_UP);

// use smoothed value if above min and if smoothed is closer than raw
if smoothed > SMOOTH_FANS_MIN {
next_duty = cmp::min(smoothed, next_duty);
}

log::warn!("ramping up, last_duty:{}, smoothed:{}, next_duty:{}", last_duty, smoothed, next_duty);
}

return Some(next_duty);
}

Some(0)
}

/// Calculate the correct duty cycle and apply it to all fans
pub fn step(&mut self) {
if let Ok(()) = self.discover() {
self.set_duty(self.get_temp().and_then(|temp| self.get_duty(temp)));
let duty_opt: Option<u8> = self.smooth_duty(self.get_temp().and_then(|temp| self.get_duty(temp)));
self.set_duty(duty_opt);
}
}
}
Expand Down Expand Up @@ -192,7 +268,8 @@ impl FanPoint {

// If the temp is in between the previous and next points, interpolate the duty
if self.temp < temp && next.temp > temp {
return Some(self.interpolate_duties(next, temp));
return Some(self.duty);
// return Some(self.interpolate_duties(next, temp));
}

None
Expand All @@ -212,9 +289,25 @@ impl FanPoint {
}
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FanCurve {
points: Vec<FanPoint>,
points: Vec<FanPoint>,
SMOOTH_FANS: Option<u8>,
SMOOTH_FANS_DOWN: Option<u8>,
SMOOTH_FANS_MIN: u8,
SMOOTH_FANS_UP: Option<u8>,
}

impl Default for FanCurve {
fn default() -> FanCurve {
FanCurve {
points: Vec::default(),
SMOOTH_FANS: None,
SMOOTH_FANS_DOWN: Some(from_seconds(12)),
SMOOTH_FANS_MIN: 0,
SMOOTH_FANS_UP: Some(from_seconds(8)),
}
}
}

impl FanCurve {
Expand All @@ -240,6 +333,29 @@ impl FanCurve {
.append(88_00, 100_00)
}

/// test galp5 curve
pub fn galp5(profile: String) -> Self {
let mut curve = Self::default()
.append(69_00, 0_00)
.append(70_00, 25_00)
.append(79_99, 25_00)
.append(80_00, 40_00)
.append(87_99, 40_00)
.append(88_00, 100_00);

if profile == String::from("performance") {
curve = Self::default()
.append(69_00, 0_00)
.append(70_00, 25_00)
.append(79_99, 25_00)
.append(80_00, 100_00);

curve.SMOOTH_FANS_UP = Some(12);
}

return curve;
}

/// Fan curve for threadripper 2
pub fn threadripper2() -> Self {
Self::default()
Expand Down

0 comments on commit 2f4f3d1

Please sign in to comment.