Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP smooth fans #367

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 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,9 +345,11 @@ 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;

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(from_seconds(4));
}

return curve;
}

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