Refactor fan and sensor into traits

This commit is contained in:
NGnius (Graham) 2024-08-20 21:25:43 -04:00
parent a0f565895b
commit dd0c3998f0
15 changed files with 462 additions and 301 deletions

2
backend-rs/Cargo.lock generated
View file

@ -290,7 +290,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]] [[package]]
name = "fantastic-rs" name = "fantastic-rs"
version = "0.5.1-alpha1" version = "0.6.0-alpha1"
dependencies = [ dependencies = [
"log", "log",
"nrpc", "nrpc",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "fantastic-rs" name = "fantastic-rs"
version = "0.5.1-alpha1" version = "0.6.0-alpha1"
edition = "2021" edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Backend (superuser) functionality for Fantastic" description = "Backend (superuser) functionality for Fantastic"

View file

@ -0,0 +1,11 @@
pub struct DevModeFan;
impl super::Adapter for DevModeFan {
fn on_enable_toggled(&self, settings: &crate::datastructs::Settings) {
log::info!("on_enable_toggled invoked with settings {:?}", settings);
}
fn control_fan(&self, settings: &crate::datastructs::Settings, sensor: &crate::adapters::traits::SensorReading) {
log::info!("control_fan invoked with settings {:?} and sensor {:?}", settings, sensor);
}
}

View file

@ -0,0 +1,7 @@
mod dev_mode;
pub use dev_mode::DevModeFan;
mod steam_deck;
pub use steam_deck::SteamDeckFan;
pub(self) use super::FanAdapter as Adapter;

View file

@ -0,0 +1,275 @@
use sysfuss::{HwMonPath, HwMonAttribute, HwMonAttributeType, HwMonAttributeItem, BasicEntityPath};
use sysfuss::{SysPath, capability::attributes, SysEntityAttributesExt};
use crate::datastructs::{Settings, GraphPoint};
const VALVE_FAN_SERVICE: &str = "jupiter-fan-control.service";
pub struct SteamDeckFan {
hwmon: HwMonPath,
}
impl SteamDeckFan {
pub fn maybe_find() -> Option<Self> {
find_hwmon(crate::sys::SYSFS_ROOT).map(|hwmon| Self { hwmon })
}
pub fn maybe_find_thermal_zone() -> Option<BasicEntityPath> {
find_thermal_zone(crate::sys::SYSFS_ROOT)
}
}
impl super::super::Adapter for SteamDeckFan {
fn on_enable_toggled(&self, settings: &Settings) {
on_set_enable(settings, &self.hwmon)
}
fn control_fan(&self, settings: &Settings, sensor: &crate::adapters::traits::SensorReading) {
enforce_jupiter_status(true);
do_fan_control(settings, &self.hwmon, sensor.value)
}
fn sensor<'a: 'b, 'b>(&'a self) -> Option<&'b dyn crate::adapters::SensorAdapter> {
Some(self)
}
}
impl super::super::super::SensorAdapter for SteamDeckFan {
fn read(&self) -> Result<crate::adapters::SensorReading, std::io::Error> {
Ok(crate::adapters::SensorReading {
value: read_fan(&self.hwmon)? as f64,
meaning: crate::adapters::SensorType::Fan,
name: "Steam Deck Fan",
})
}
}
const RECALCULATE_ATTR: HwMonAttribute = HwMonAttribute::custom("recalculate");
const FAN1_INPUT_ATTR: HwMonAttribute = HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Input);
const FAN1_LABEL_ATTR: HwMonAttribute = HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Label);
const FAN1_TARGET_ATTR: HwMonAttribute = HwMonAttribute::custom("fan1_target");
const HWMON_NEEDS: [HwMonAttribute; 3] = [
//RECALCULATE_ATTR,
FAN1_INPUT_ATTR,
FAN1_TARGET_ATTR,
FAN1_LABEL_ATTR,
];
fn find_hwmon<P: AsRef<std::path::Path>>(path: P) -> Option<HwMonPath> {
let syspath = SysPath::path(path);
match syspath.hwmon(attributes(HWMON_NEEDS.into_iter()))
{
Err(e) => {
log::error!("sysfs hwmon iter error while finding Steam Deck fan: {}", e);
None
},
Ok(mut iter) => {
if let Some(entity) = iter.next() {
log::info!("Found Steam Deck fan hwmon {}", entity.as_ref().display());
Some(entity)
} else {
log::error!("sysfs hwmon iter empty while finding Steam Deck fan: [no capable results]");
None
}
}
}
}
fn find_thermal_zone<P: AsRef<std::path::Path>>(path: P) -> Option<BasicEntityPath> {
let syspath = SysPath::path(path);
match syspath.class("thermal",
|ent: &BasicEntityPath| ent.exists(&"temp".to_owned()) && ent.exists(&"type".to_owned()) && ent.attribute("type".to_owned()).map(|val: String| val.to_lowercase() != "processor").unwrap_or(false))
{
Err(e) => {
log::error!("sysfs thermal class iter error while finding Steam Deck thermal zone: {}", e);
None
},
Ok(mut iter) => {
if let Some(entity) = iter.next() {
log::info!("Found thermal zone {}", entity.as_ref().display());
Some(entity)
} else {
log::error!("sysfs thermal class iter empty while finding Steam Deck thermal zone: [no capable results]");
None
}
}
}
}
fn on_set_enable(settings: &Settings, hwmon: &HwMonPath) {
// stop/start jupiter fan control (since the client-side way of doing this was removed :( )
enforce_jupiter_status(settings.enable);
if let Err(e) = write_fan_recalc(hwmon, settings.enable) {
log::error!("runtime failed to write to fan recalculate file: {}", e);
}
}
fn enforce_jupiter_status(enabled: bool) {
// enabled refers to whether this plugin's functionality is enabled,
// not the jupiter fan control service
let service_status = detect_jupiter_fan_service();
log::debug!("fan control service is enabled? {}", service_status);
if enabled == service_status {
// do not run Valve's fan service along with Fantastic, since they fight
if enabled {
stop_fan_service();
} else {
start_fan_service();
}
}
}
fn detect_jupiter_fan_service() -> bool {
match std::process::Command::new("systemctl")
.args(["is-active", VALVE_FAN_SERVICE])
.output() {
Ok(cmd) => String::from_utf8_lossy(&cmd.stdout).trim() == "active",
Err(e) => {
log::error!("`systemctl is-active {}` err: {}", VALVE_FAN_SERVICE, e);
false
}
}
}
fn start_fan_service() {
match std::process::Command::new("systemctl")
.args(["start", VALVE_FAN_SERVICE])
.output() {
Err(e) => log::error!("`systemctl start {}` err: {}", VALVE_FAN_SERVICE, e),
Ok(out) => log::debug!("started `{}`:\nstdout:{}\nstderr:{}", VALVE_FAN_SERVICE, String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)),
}
}
fn stop_fan_service() {
match std::process::Command::new("systemctl")
.args(["stop", VALVE_FAN_SERVICE])
.output() {
Err(e) => log::error!("`systemctl stop {}` err: {}", VALVE_FAN_SERVICE, e),
Ok(out) => log::debug!("stopped `{}`:\nstdout:{}\nstderr:{}", VALVE_FAN_SERVICE, String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)),
}
}
fn write_fan_recalc(hwmon: &HwMonPath, enabled: bool) -> Result<(), std::io::Error> {
hwmon.set(RECALCULATE_ATTR, enabled as u8)
//write_single(format!("/sys/class/hwmon/hwmon{}/recalculate", HWMON_INDEX), enabled as u8)
}
fn write_fan_target(hwmon: &HwMonPath, rpm: u64) -> Result<(), std::io::Error> {
hwmon.set(FAN1_TARGET_ATTR, rpm)
//write_single(format!("/sys/class/hwmon/hwmon{}/fan1_target", HWMON_INDEX), rpm)
}
fn read_fan(hwmon: &HwMonPath) -> std::io::Result<u64> {
match hwmon.attribute(FAN1_INPUT_ATTR){
Ok(x) => Ok(x),
Err(sysfuss::EitherErr2::First(e)) => {
log::error!("Failed Steam Deck read_fan(): {}", e);
Err(e)
},
Err(sysfuss::EitherErr2::Second(e)) => {
log::error!("Failed Steam Deck read_fan(): {}", e);
Err(std::io::Error::other(e))
},
}
}
fn do_fan_control(settings: &Settings, hwmon: &HwMonPath, thermal_zone: f64) {
/*
curve = self.settings["curve"]
fan_ratio = 0 # unnecessary in Python, but stupid without
if len(curve) == 0:
fan_ratio = 1
else:
index = -1
temperature_ratio = (thermal_zone(0) - TEMPERATURE_MINIMUM) / (TEMPERATURE_MAXIMUM - TEMPERATURE_MINIMUM)
for i in range(len(curve)-1, -1, -1):
if curve[i]["x"] < temperature_ratio:
index = i
break
if self.settings["interpolate"]:
fan_ratio = self.interpolate_fan(self, index, temperature_ratio)
else:
fan_ratio = self.step_fan(self, index, temperature_ratio)
set_fan_target(int((fan_ratio * FAN_MAXIMUM) + FAN_MINIMUM))
*/
let temperature_ratio = (((thermal_zone as f64)/1000.0) - settings.temperature_bounds.min)
/ (settings.temperature_bounds.max - settings.temperature_bounds.min);
let mut index = None;
for i in (0..settings.curve.len()).rev() {
if settings.curve[i].x < temperature_ratio {
index = Some(i);
break;
}
}
let fan_ratio = if settings.interpolate {
interpolate_fan(settings, index, temperature_ratio)
} else {
step_fan(settings, index, temperature_ratio)
};
let fan_speed: u64 = ((fan_ratio * (settings.fan_bounds.max - settings.fan_bounds.min)) + settings.fan_bounds.min) as _;
if let Err(e) = write_fan_target(hwmon, fan_speed) {
log::error!("Failed to write to Steam Deck fan target file: {}", e);
}
}
fn interpolate_fan(settings: &Settings, index: Option<usize>, t_ratio: f64) -> f64 {
/*
curve = self.settings["curve"]
upper_point = {"x": 1.0, "y": 0.0}
lower_point = {"x": 0.0, "y": 1.0}
if index != -1: # guaranteed to not be empty
lower_point = curve[index]
if index != len(curve) - 1:
upper_point = curve[index+1]
#logging.debug(f"lower_point: {lower_point}, upper_point: {upper_point}")
upper_y = 1-upper_point["y"]
lower_y = 1-lower_point["y"]
slope_m = (upper_y - lower_y) / (upper_point["x"] - lower_point["x"])
y_intercept_b = lower_y - (slope_m * lower_point["x"])
logging.debug(f"interpolation: y = {slope_m}x + {y_intercept_b}")
return (slope_m * temperature_ratio) + y_intercept_b
*/
let (upper, lower) = if let Some(i) = index {
(if i != settings.curve.len() - 1 {
settings.curve[i+1].clone()
} else {
GraphPoint{x: 1.0, y: 1.0}
},
settings.curve[i].clone())
} else {
(if settings.curve.is_empty() {
GraphPoint{x: 1.0, y: 1.0}
} else {
settings.curve[0].clone()
},
GraphPoint{x: 0.0, y: 0.0})
};
let slope_m = (upper.y - lower.y) / (upper.x - lower.x);
let y_intercept_b = lower.y - (slope_m * lower.x);
log::debug!("interpolation: y = {}x + {} (between {:?} and {:?})", slope_m, y_intercept_b, upper, lower);
(slope_m * t_ratio) + y_intercept_b
}
fn step_fan(settings: &Settings, index: Option<usize>, _t_ratio: f64) -> f64 {
/*
curve = self.settings["curve"]
if index != -1:
return 1 - curve[index]["y"]
else:
if len(curve) == 0:
return 1
else:
return 0.5
*/
// step fan, what are you doing?
if let Some(index) = index {
settings.curve[index].y
} else {
if settings.curve.is_empty() {
1.0
} else {
0.5
}
}
}

View file

@ -0,0 +1,2 @@
mod adapter;
pub use adapter::SteamDeckFan;

View file

@ -0,0 +1,5 @@
mod traits;
pub use traits::{FanAdapter, SensorAdapter, SensorReading, SensorType};
pub mod fans;
pub mod sensors;

View file

@ -0,0 +1,12 @@
pub struct DevModeSensor;
impl super::Adapter for DevModeSensor {
fn read(&self) -> Result<super::Reading, std::io::Error> {
log::info!("read invoked");
return Ok(super::Reading {
value: 42_000.0,
meaning: super::super::SensorType::Temperature,
name: "DevModeSensor",
})
}
}

View file

@ -0,0 +1,8 @@
mod dev_mode;
pub use dev_mode::DevModeSensor;
mod thermal_zone;
pub use thermal_zone::ThermalZoneSensor;
pub(self) use super::SensorAdapter as Adapter;
pub(self) use super::SensorReading as Reading;

View file

@ -0,0 +1,36 @@
use sysfuss::BasicEntityPath;
use sysfuss::SysEntityAttributesExt;
pub struct ThermalZoneSensor {
zone: BasicEntityPath,
}
impl ThermalZoneSensor {
pub fn new<P: AsRef<std::path::Path>>(zone_path: P) -> Self {
Self { zone: BasicEntityPath::new(zone_path) }
}
}
impl super::Adapter for ThermalZoneSensor {
fn read(&self) -> Result<super::Reading, std::io::Error> {
Ok(super::Reading {
value: read_thermal_zone(&self.zone)? as f64,
meaning: super::super::SensorType::Temperature,
name: "thermal_zone",
})
}
}
fn read_thermal_zone(entity: &BasicEntityPath) -> std::io::Result<u64> {
match entity.attribute("temp".to_owned()) {
Ok(x) => Ok(x),
Err(sysfuss::EitherErr2::First(e)) => {
log::error!("Failed read_thermal_zone(): {}", e);
Err(e)
},
Err(sysfuss::EitherErr2::Second(e)) => {
log::error!("Failed read_thermal_zone(): {}", e);
Err(std::io::Error::other(e))
},
}
}

View file

@ -0,0 +1,30 @@
use crate::datastructs::Settings;
pub trait FanAdapter: Send + Sync {
/// Handle fan enable UI toggle
fn on_enable_toggled(&self, settings: &Settings);
/// Apply fan settings to fan (probably through udev/sysfs)
fn control_fan(&self, settings: &Settings, sensor: &SensorReading);
/// Get fan speed sensor
fn sensor<'a: 'b, 'b>(&'a self) -> Option<&'b dyn SensorAdapter> { None }
}
pub trait SensorAdapter: Send + Sync {
/// Read sensor value
fn read(&self) -> Result<SensorReading, std::io::Error>;
}
#[derive(Debug, Clone, Copy)]
pub struct SensorReading {
pub value: f64,
pub meaning: SensorType,
pub name: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub enum SensorType {
Temperature, // milli-degrees Celcius
Fan, // revolutions per minute (RPM)
#[allow(dead_code)]
Unknown,
}

View file

@ -84,22 +84,33 @@ impl<'a> IFan<'a> for FanService {
usdpl_back::nrpc::ServiceServerStream<'b, RpmMessage>, usdpl_back::nrpc::ServiceServerStream<'b, RpmMessage>,
Box<dyn std::error::Error + Send>, Box<dyn std::error::Error + Send>,
> { > {
let hwmon = self.ctrl.hwmon_clone(); let fan_clone = self.ctrl.fan_clone();
if fan_clone.sensor().is_some() {
let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| { let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| {
let hwmon = hwmon.clone(); let fan_clone2 = fan_clone.clone();
tokio::task::spawn_blocking( tokio::task::spawn_blocking(
/* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */ /* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */
move || if !is_first { std::thread::sleep(FAN_READ_PERIOD); }) move || if !is_first { std::thread::sleep(FAN_READ_PERIOD); })
.map(move |_| { .map(move |_| {
if let Some(rpm) = crate::sys::read_fan(&hwmon) { if let Some(fan_sensor) = fan_clone2.sensor() {
log::debug!("get_fan_rpm() success: {}", rpm); match fan_sensor.read() {
Ok(RpmMessage { rpm: rpm as u32 }) Ok(reading) => {
log::debug!("get_fan_rpm() success: {}", reading.value);
Ok(RpmMessage { rpm: reading.value as u32 })
}
Err(e) => {
Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from(format!("Failed to read fan speed: {}", e))))
}
}
} else { } else {
Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from("Failed to read fan speed"))) Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from("Failed to get fan speed sensor")))
} }
}) })
}); });
Ok(Box::new(stream)) Ok(Box::new(stream))
} else {
Ok(Box::new(usdpl_back::nrpc::_helpers::futures::stream::empty()))
}
} }
async fn get_temperature<'b: 'a>( async fn get_temperature<'b: 'a>(
@ -109,19 +120,22 @@ impl<'a> IFan<'a> for FanService {
usdpl_back::nrpc::ServiceServerStream<'b, TemperatureMessage>, usdpl_back::nrpc::ServiceServerStream<'b, TemperatureMessage>,
Box<dyn std::error::Error + Send>, Box<dyn std::error::Error + Send>,
> { > {
let thermal_zone = self.ctrl.thermal_zone_clone(); let sensor_clone = self.ctrl.sensor_clone();
let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| { let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| {
let thermal_zone = thermal_zone.clone(); let sensor_clone2 = sensor_clone.clone();
tokio::task::spawn_blocking( tokio::task::spawn_blocking(
/* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */ /* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */
move || if !is_first { std::thread::sleep(TEMPERATURE_READ_PERIOD); }) move || if !is_first { std::thread::sleep(TEMPERATURE_READ_PERIOD); })
.map(move |_| { .map(move |_| {
if let Some(temperature) = crate::sys::read_thermal_zone(&thermal_zone) { match sensor_clone2.read() {
let real_temp = temperature as f64 / 1000.0; Ok(reading) => {
let real_temp = reading.value as f64 / 1000.0;
log::debug!("get_temperature() success: {}", real_temp); log::debug!("get_temperature() success: {}", real_temp);
Ok(TemperatureMessage { temperature: real_temp }) Ok(TemperatureMessage { temperature: real_temp })
} else { },
Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from("get_temperature failed to read thermal zone 0"))) Err(e) => {
Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from(format!("get_temperature failed to read sensor: {}", e))))
}
} }
}) })
}); });

View file

@ -1,35 +1,36 @@
//! Fan control //! Fan control
use std::sync::Arc; use std::sync::Arc;
//use std::collections::HashMap;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use sysfuss::{HwMonPath, BasicEntityPath}; use super::datastructs::{Settings, State};
use super::datastructs::{Settings, State, GraphPoint};
use super::json::SettingsJson; use super::json::SettingsJson;
const VALVE_FAN_SERVICE: &str = "jupiter-fan-control.service";
const SYSFS_ROOT: &str = "/";
pub struct ControlRuntime { pub struct ControlRuntime {
settings: Arc<RwLock<Settings>>, settings: Arc<RwLock<Settings>>,
state: Arc<RwLock<State>>, state: Arc<RwLock<State>>,
hwmon: Arc<HwMonPath>, fan_adapter: Arc<Box<dyn crate::adapters::FanAdapter + 'static>>,
thermal_zone: Arc<BasicEntityPath>, sensor_adapter: Arc<Box<dyn crate::adapters::SensorAdapter + 'static>>,
} }
impl ControlRuntime { impl ControlRuntime {
pub fn new() -> Self { #[allow(dead_code)]
pub fn new<F: crate::adapters::FanAdapter + 'static, S: crate::adapters::SensorAdapter + 'static>(fan: F, sensor: S) -> Self {
Self::new_boxed(Box::new(fan), Box::new(sensor))
}
pub(crate) fn new_boxed(fan: Box<dyn crate::adapters::FanAdapter + 'static>, sensor: Box<dyn crate::adapters::SensorAdapter + 'static>) -> Self {
let new_state = State::new(); let new_state = State::new();
let settings_p = settings_path(&new_state.home); let settings_p = settings_path(&new_state.home);
Self { Self {
settings: Arc::new(RwLock::new(super::json::SettingsJson::open(settings_p).unwrap_or_default().into())), settings: Arc::new(RwLock::new(super::json::SettingsJson::open(settings_p).unwrap_or_default().into())),
state: Arc::new(RwLock::new(new_state)), state: Arc::new(RwLock::new(new_state)),
hwmon: Arc::new(crate::sys::find_hwmon(SYSFS_ROOT)), fan_adapter: Arc::new(fan),
thermal_zone: Arc::new(crate::sys::find_thermal_zone(SYSFS_ROOT)) sensor_adapter: Arc::new(sensor),
} }
} }
@ -49,27 +50,19 @@ impl ControlRuntime {
&self.state &self.state
} }
/*pub(crate) fn hwmon(&self) -> &'_ HwMonPath { pub(crate) fn sensor_clone(&self) -> Arc<Box<dyn crate::adapters::SensorAdapter>> {
&self.hwmon self.sensor_adapter.clone()
}*/
pub(crate) fn hwmon_clone(&self) -> Arc<HwMonPath> {
self.hwmon.clone()
} }
/*pub(crate) fn thermal_zone(&self) -> &'_ BasicEntityPath { pub(crate) fn fan_clone(&self) -> Arc<Box<dyn crate::adapters::FanAdapter>> {
&self.thermal_zone self.fan_adapter.clone()
}*/
pub(crate) fn thermal_zone_clone(&self) -> Arc<BasicEntityPath> {
self.thermal_zone.clone()
} }
pub fn run(&self) -> thread::JoinHandle<()> { pub fn run(&self) -> thread::JoinHandle<()> {
let runtime_settings = self.settings_clone(); let runtime_settings = self.settings_clone();
let runtime_state = self.state_clone(); let runtime_state = self.state_clone();
let runtime_hwmon = self.hwmon.clone(); let runtime_fan = self.fan_adapter.clone();
let runtime_thermal_zone = self.thermal_zone.clone(); let runtime_sensor = self.sensor_adapter.clone();
thread::spawn(move || { thread::spawn(move || {
let sleep_duration = Duration::from_millis(1000); let sleep_duration = Duration::from_millis(1000);
let mut start_time = Instant::now(); let mut start_time = Instant::now();
@ -78,10 +71,9 @@ impl ControlRuntime {
// resumed from sleep; do fan re-init // resumed from sleep; do fan re-init
log::debug!("Detected resume from sleep, overriding fan again"); log::debug!("Detected resume from sleep, overriding fan again");
{ {
let state = runtime_state.blocking_read();
let settings = runtime_settings.blocking_read(); let settings = runtime_settings.blocking_read();
if settings.enable { if settings.enable {
Self::on_set_enable(&settings, &state, &runtime_hwmon); runtime_fan.on_enable_toggled(&settings);
} }
} }
} }
@ -95,7 +87,7 @@ impl ControlRuntime {
if let Err(e) = settings_json.save(settings_path(&state.home)) { if let Err(e) = settings_json.save(settings_path(&state.home)) {
log::error!("SettingsJson.save({}) error: {}", settings_path(&state.home).display(), e); log::error!("SettingsJson.save({}) error: {}", settings_path(&state.home).display(), e);
} }
Self::on_set_enable(&settings, &state, &runtime_hwmon); runtime_fan.on_enable_toggled(&settings);
drop(state); drop(state);
let mut state = runtime_state.blocking_write(); let mut state = runtime_state.blocking_write();
state.dirty = false; state.dirty = false;
@ -104,171 +96,18 @@ impl ControlRuntime {
{ // fan control { // fan control
let settings = runtime_settings.blocking_read(); let settings = runtime_settings.blocking_read();
if settings.enable { if settings.enable {
Self::enforce_jupiter_status(true); match runtime_sensor.read() {
Self::do_fan_control(&settings, &runtime_hwmon, &runtime_thermal_zone); Err(e) => log::error!("Failed to read sensor for control_fan input: {}", e),
Ok(reading) => {
runtime_fan.control_fan(&settings, &reading);
}
}
} }
} }
thread::sleep(sleep_duration); thread::sleep(sleep_duration);
} }
}) })
} }
fn on_set_enable(settings: &Settings, _state: &State, hwmon: &HwMonPath) {
// stop/start jupiter fan control (since the client-side way of doing this was removed :( )
Self::enforce_jupiter_status(settings.enable);
if let Err(e) = crate::sys::write_fan_recalc(hwmon, settings.enable) {
log::error!("runtime failed to write to fan recalculate file: {}", e);
}
}
fn do_fan_control(settings: &Settings, hwmon: &HwMonPath, thermal_zone: &BasicEntityPath) {
/*
curve = self.settings["curve"]
fan_ratio = 0 # unnecessary in Python, but stupid without
if len(curve) == 0:
fan_ratio = 1
else:
index = -1
temperature_ratio = (thermal_zone(0) - TEMPERATURE_MINIMUM) / (TEMPERATURE_MAXIMUM - TEMPERATURE_MINIMUM)
for i in range(len(curve)-1, -1, -1):
if curve[i]["x"] < temperature_ratio:
index = i
break
if self.settings["interpolate"]:
fan_ratio = self.interpolate_fan(self, index, temperature_ratio)
else:
fan_ratio = self.step_fan(self, index, temperature_ratio)
set_fan_target(int((fan_ratio * FAN_MAXIMUM) + FAN_MINIMUM))
*/
let fan_ratio: f64 = if let Some(thermal_zone) = crate::sys::read_thermal_zone(thermal_zone) {
let temperature_ratio = (((thermal_zone as f64)/1000.0) - settings.temperature_bounds.min)
/ (settings.temperature_bounds.max - settings.temperature_bounds.min);
let mut index = None;
for i in (0..settings.curve.len()).rev() {
if settings.curve[i].x < temperature_ratio {
index = Some(i);
break;
}
}
if settings.interpolate {
Self::interpolate_fan(settings, index, temperature_ratio)
} else {
Self::step_fan(settings, index, temperature_ratio)
}
} else {
1.0
};
let fan_speed: u64 = ((fan_ratio * (settings.fan_bounds.max - settings.fan_bounds.min)) + settings.fan_bounds.min) as _;
if let Err(e) = crate::sys::write_fan_target(hwmon, fan_speed) {
log::error!("runtime failed to write to fan target file: {}", e);
}
}
fn interpolate_fan(settings: &Settings, index: Option<usize>, t_ratio: f64) -> f64 {
/*
curve = self.settings["curve"]
upper_point = {"x": 1.0, "y": 0.0}
lower_point = {"x": 0.0, "y": 1.0}
if index != -1: # guaranteed to not be empty
lower_point = curve[index]
if index != len(curve) - 1:
upper_point = curve[index+1]
#logging.debug(f"lower_point: {lower_point}, upper_point: {upper_point}")
upper_y = 1-upper_point["y"]
lower_y = 1-lower_point["y"]
slope_m = (upper_y - lower_y) / (upper_point["x"] - lower_point["x"])
y_intercept_b = lower_y - (slope_m * lower_point["x"])
logging.debug(f"interpolation: y = {slope_m}x + {y_intercept_b}")
return (slope_m * temperature_ratio) + y_intercept_b
*/
let (upper, lower) = if let Some(i) = index {
(if i != settings.curve.len() - 1 {
settings.curve[i+1].clone()
} else {
GraphPoint{x: 1.0, y: 1.0}
},
settings.curve[i].clone())
} else {
(if settings.curve.is_empty() {
GraphPoint{x: 1.0, y: 1.0}
} else {
settings.curve[0].clone()
},
GraphPoint{x: 0.0, y: 0.0})
};
let slope_m = (upper.y - lower.y) / (upper.x - lower.x);
let y_intercept_b = lower.y - (slope_m * lower.x);
log::debug!("interpolation: y = {}x + {} (between {:?} and {:?})", slope_m, y_intercept_b, upper, lower);
(slope_m * t_ratio) + y_intercept_b
}
fn step_fan(settings: &Settings, index: Option<usize>, _t_ratio: f64) -> f64 {
/*
curve = self.settings["curve"]
if index != -1:
return 1 - curve[index]["y"]
else:
if len(curve) == 0:
return 1
else:
return 0.5
*/
// step fan, what are you doing?
if let Some(index) = index {
settings.curve[index].y
} else {
if settings.curve.is_empty() {
1.0
} else {
0.5
}
}
}
fn enforce_jupiter_status(enabled: bool) {
// enabled refers to whether this plugin's functionality is enabled,
// not the jupiter fan control service
let service_status = Self::detect_jupiter_fan_service();
log::debug!("fan control service is enabled? {}", service_status);
if enabled == service_status {
// do not run Valve's fan service along with Fantastic, since they fight
if enabled {
Self::stop_fan_service();
} else {
Self::start_fan_service();
}
}
}
fn detect_jupiter_fan_service() -> bool {
match std::process::Command::new("systemctl")
.args(["is-active", VALVE_FAN_SERVICE])
.output() {
Ok(cmd) => String::from_utf8_lossy(&cmd.stdout).trim() == "active",
Err(e) => {
log::error!("`systemctl is-active {}` err: {}", VALVE_FAN_SERVICE, e);
false
}
}
}
fn start_fan_service() {
match std::process::Command::new("systemctl")
.args(["start", VALVE_FAN_SERVICE])
.output() {
Err(e) => log::error!("`systemctl start {}` err: {}", VALVE_FAN_SERVICE, e),
Ok(out) => log::debug!("started `{}`:\nstdout:{}\nstderr:{}", VALVE_FAN_SERVICE, String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)),
}
}
fn stop_fan_service() {
match std::process::Command::new("systemctl")
.args(["stop", VALVE_FAN_SERVICE])
.output() {
Err(e) => log::error!("`systemctl stop {}` err: {}", VALVE_FAN_SERVICE, e),
Ok(out) => log::debug!("stopped `{}`:\nstdout:{}\nstderr:{}", VALVE_FAN_SERVICE, String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)),
}
}
} }
fn settings_path<P: AsRef<std::path::Path>>(home: P) -> std::path::PathBuf { fn settings_path<P: AsRef<std::path::Path>>(home: P) -> std::path::PathBuf {

View file

@ -1,4 +1,5 @@
mod api; mod api;
mod adapters;
mod control; mod control;
mod datastructs; mod datastructs;
mod json; mod json;
@ -28,7 +29,14 @@ fn main() -> Result<(), ()> {
println!("Starting back-end ({} v{})", api::NAME, api::VERSION); println!("Starting back-end ({} v{})", api::NAME, api::VERSION);
usdpl_back::Server::new(PORT) usdpl_back::Server::new(PORT)
.register(FanServer::new( .register(FanServer::new(
api::FanService::new(control::ControlRuntime::new()) api::FanService::new(control::ControlRuntime::new_boxed(
adapters::fans::SteamDeckFan::maybe_find()
.map(|f| Box::new(f) as Box<dyn adapters::FanAdapter>)
.unwrap_or_else(|| Box::new(adapters::fans::DevModeFan)),
adapters::fans::SteamDeckFan::maybe_find_thermal_zone()
.map(|t| Box::new(adapters::sensors::ThermalZoneSensor::new(t)) as Box<dyn adapters::SensorAdapter>)
.unwrap_or_else(|| Box::new(adapters::sensors::DevModeSensor))
))
)) ))
.run_blocking() .run_blocking()
.unwrap(); .unwrap();

View file

@ -1,87 +1 @@
use sysfuss::{SysPath, capability::attributes, SysEntityAttributesExt}; pub const SYSFS_ROOT: &str = "/";
use sysfuss::{BasicEntityPath, HwMonPath, HwMonAttribute, HwMonAttributeType, HwMonAttributeItem};
const HWMON_INDEX: u64 = 5;
pub const RECALCULATE_ATTR: HwMonAttribute = HwMonAttribute::custom("recalculate");
pub const FAN1_INPUT_ATTR: HwMonAttribute = HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Input);
pub const FAN1_LABEL_ATTR: HwMonAttribute = HwMonAttribute::new(HwMonAttributeType::Fan, 1, HwMonAttributeItem::Label);
pub const FAN1_TARGET_ATTR: HwMonAttribute = HwMonAttribute::custom("fan1_target");
const HWMON_NEEDS: [HwMonAttribute; 3] = [
//RECALCULATE_ATTR,
FAN1_INPUT_ATTR,
FAN1_TARGET_ATTR,
FAN1_LABEL_ATTR,
];
pub fn read_fan(hwmon: &HwMonPath) -> Option<u64> {
match hwmon.attribute(FAN1_INPUT_ATTR){
Ok(x) => Some(x),
Err(e) => {
log::error!("Failed read_fan(): {}", e);
None
},
}
}
pub fn read_thermal_zone(entity: &BasicEntityPath) -> Option<u64> {
match entity.attribute("temp".to_owned()) {
Ok(x) => Some(x),
Err(e) => {
log::error!("Failed read_thermal_zone(): {}", e);
None
},
}
}
pub fn write_fan_recalc(hwmon: &HwMonPath, enabled: bool) -> Result<(), std::io::Error> {
hwmon.set(RECALCULATE_ATTR, enabled as u8)
//write_single(format!("/sys/class/hwmon/hwmon{}/recalculate", HWMON_INDEX), enabled as u8)
}
pub fn write_fan_target(hwmon: &HwMonPath, rpm: u64) -> Result<(), std::io::Error> {
hwmon.set(FAN1_TARGET_ATTR, rpm)
//write_single(format!("/sys/class/hwmon/hwmon{}/fan1_target", HWMON_INDEX), rpm)
}
pub fn find_hwmon<P: AsRef<std::path::Path>>(path: P) -> HwMonPath {
let syspath = SysPath::path(path);
match syspath.hwmon(attributes(HWMON_NEEDS.into_iter()))
{
Err(e) => {
log::error!("sysfs hwmon iter error: {}", e);
syspath.hwmon_by_index(HWMON_INDEX)
},
Ok(mut iter) => {
let entity = iter.next()
.unwrap_or_else(|| {
log::error!("sysfs hwmon iter empty: [no capable results]");
syspath.hwmon_by_index(HWMON_INDEX)
});
log::info!("Found fan hwmon {}", entity.as_ref().display());
entity
}
}
}
pub fn find_thermal_zone<P: AsRef<std::path::Path>>(path: P) -> BasicEntityPath {
let syspath = SysPath::path(path);
match syspath.class("thermal",
|ent: &BasicEntityPath| ent.exists(&"temp".to_owned()) && ent.exists(&"type".to_owned()) && ent.attribute("type".to_owned()).map(|val: String| val.to_lowercase() != "processor").unwrap_or(false))
{
Err(e) => {
log::error!("sysfs thermal class iter error: {}", e);
BasicEntityPath::new("/sys/class/thermal/thermal_zone0")
},
Ok(mut iter) => {
let entity = iter.next()
.unwrap_or_else(|| {
log::error!("sysfs thermal class iter empty: [no capable results]");
BasicEntityPath::new("/sys/class/thermal/thermal_zone0")
});
log::info!("Found thermal zone {}", entity.as_ref().display());
entity
}
}
}