From dd0c3998f0cdf25f8e6055b48c8a0b24ddfc4ef1 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 20 Aug 2024 21:25:43 -0400 Subject: [PATCH] Refactor fan and sensor into traits --- backend-rs/Cargo.lock | 2 +- backend-rs/Cargo.toml | 2 +- backend-rs/src/adapters/fans/dev_mode.rs | 11 + backend-rs/src/adapters/fans/mod.rs | 7 + .../src/adapters/fans/steam_deck/adapter.rs | 275 ++++++++++++++++++ .../src/adapters/fans/steam_deck/mod.rs | 2 + backend-rs/src/adapters/mod.rs | 5 + backend-rs/src/adapters/sensors/dev_mode.rs | 12 + backend-rs/src/adapters/sensors/mod.rs | 8 + .../src/adapters/sensors/thermal_zone.rs | 36 +++ backend-rs/src/adapters/traits.rs | 30 ++ backend-rs/src/api.rs | 62 ++-- backend-rs/src/control.rs | 213 ++------------ backend-rs/src/main.rs | 10 +- backend-rs/src/sys.rs | 88 +----- 15 files changed, 462 insertions(+), 301 deletions(-) create mode 100644 backend-rs/src/adapters/fans/dev_mode.rs create mode 100644 backend-rs/src/adapters/fans/mod.rs create mode 100644 backend-rs/src/adapters/fans/steam_deck/adapter.rs create mode 100644 backend-rs/src/adapters/fans/steam_deck/mod.rs create mode 100644 backend-rs/src/adapters/mod.rs create mode 100644 backend-rs/src/adapters/sensors/dev_mode.rs create mode 100644 backend-rs/src/adapters/sensors/mod.rs create mode 100644 backend-rs/src/adapters/sensors/thermal_zone.rs create mode 100644 backend-rs/src/adapters/traits.rs diff --git a/backend-rs/Cargo.lock b/backend-rs/Cargo.lock index 1a6e6e6..2632125 100644 --- a/backend-rs/Cargo.lock +++ b/backend-rs/Cargo.lock @@ -290,7 +290,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fantastic-rs" -version = "0.5.1-alpha1" +version = "0.6.0-alpha1" dependencies = [ "log", "nrpc", diff --git a/backend-rs/Cargo.toml b/backend-rs/Cargo.toml index 04c3820..39ffa29 100644 --- a/backend-rs/Cargo.toml +++ b/backend-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fantastic-rs" -version = "0.5.1-alpha1" +version = "0.6.0-alpha1" edition = "2021" authors = ["NGnius (Graham) "] description = "Backend (superuser) functionality for Fantastic" diff --git a/backend-rs/src/adapters/fans/dev_mode.rs b/backend-rs/src/adapters/fans/dev_mode.rs new file mode 100644 index 0000000..a9b3cac --- /dev/null +++ b/backend-rs/src/adapters/fans/dev_mode.rs @@ -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); + } +} diff --git a/backend-rs/src/adapters/fans/mod.rs b/backend-rs/src/adapters/fans/mod.rs new file mode 100644 index 0000000..42d01f5 --- /dev/null +++ b/backend-rs/src/adapters/fans/mod.rs @@ -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; diff --git a/backend-rs/src/adapters/fans/steam_deck/adapter.rs b/backend-rs/src/adapters/fans/steam_deck/adapter.rs new file mode 100644 index 0000000..398bac2 --- /dev/null +++ b/backend-rs/src/adapters/fans/steam_deck/adapter.rs @@ -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 { + find_hwmon(crate::sys::SYSFS_ROOT).map(|hwmon| Self { hwmon }) + } + + pub fn maybe_find_thermal_zone() -> Option { + 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 { + 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>(path: P) -> Option { + 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>(path: P) -> Option { + 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 { + 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, 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, _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 + } + } +} diff --git a/backend-rs/src/adapters/fans/steam_deck/mod.rs b/backend-rs/src/adapters/fans/steam_deck/mod.rs new file mode 100644 index 0000000..83ff1f8 --- /dev/null +++ b/backend-rs/src/adapters/fans/steam_deck/mod.rs @@ -0,0 +1,2 @@ +mod adapter; +pub use adapter::SteamDeckFan; diff --git a/backend-rs/src/adapters/mod.rs b/backend-rs/src/adapters/mod.rs new file mode 100644 index 0000000..04b139e --- /dev/null +++ b/backend-rs/src/adapters/mod.rs @@ -0,0 +1,5 @@ +mod traits; +pub use traits::{FanAdapter, SensorAdapter, SensorReading, SensorType}; + +pub mod fans; +pub mod sensors; diff --git a/backend-rs/src/adapters/sensors/dev_mode.rs b/backend-rs/src/adapters/sensors/dev_mode.rs new file mode 100644 index 0000000..4a93533 --- /dev/null +++ b/backend-rs/src/adapters/sensors/dev_mode.rs @@ -0,0 +1,12 @@ +pub struct DevModeSensor; + +impl super::Adapter for DevModeSensor { + fn read(&self) -> Result { + log::info!("read invoked"); + return Ok(super::Reading { + value: 42_000.0, + meaning: super::super::SensorType::Temperature, + name: "DevModeSensor", + }) + } +} diff --git a/backend-rs/src/adapters/sensors/mod.rs b/backend-rs/src/adapters/sensors/mod.rs new file mode 100644 index 0000000..349ad07 --- /dev/null +++ b/backend-rs/src/adapters/sensors/mod.rs @@ -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; diff --git a/backend-rs/src/adapters/sensors/thermal_zone.rs b/backend-rs/src/adapters/sensors/thermal_zone.rs new file mode 100644 index 0000000..1f7d919 --- /dev/null +++ b/backend-rs/src/adapters/sensors/thermal_zone.rs @@ -0,0 +1,36 @@ +use sysfuss::BasicEntityPath; +use sysfuss::SysEntityAttributesExt; + +pub struct ThermalZoneSensor { + zone: BasicEntityPath, +} + +impl ThermalZoneSensor { + pub fn new>(zone_path: P) -> Self { + Self { zone: BasicEntityPath::new(zone_path) } + } +} + +impl super::Adapter for ThermalZoneSensor { + fn read(&self) -> Result { + 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 { + 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)) + }, + } +} diff --git a/backend-rs/src/adapters/traits.rs b/backend-rs/src/adapters/traits.rs new file mode 100644 index 0000000..adf66bc --- /dev/null +++ b/backend-rs/src/adapters/traits.rs @@ -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; +} + +#[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, +} diff --git a/backend-rs/src/api.rs b/backend-rs/src/api.rs index 1412988..75d87ee 100644 --- a/backend-rs/src/api.rs +++ b/backend-rs/src/api.rs @@ -84,22 +84,33 @@ impl<'a> IFan<'a> for FanService { usdpl_back::nrpc::ServiceServerStream<'b, RpmMessage>, Box, > { - let hwmon = self.ctrl.hwmon_clone(); - let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| { - let hwmon = hwmon.clone(); - tokio::task::spawn_blocking( - /* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */ - move || if !is_first { std::thread::sleep(FAN_READ_PERIOD); }) - .map(move |_| { - if let Some(rpm) = crate::sys::read_fan(&hwmon) { - log::debug!("get_fan_rpm() success: {}", rpm); - Ok(RpmMessage { rpm: rpm as u32 }) - } else { - Err(usdpl_back::nrpc::ServiceError::Method(Box::::from("Failed to read fan speed"))) - } - }) - }); - Ok(Box::new(stream)) + 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 fan_clone2 = fan_clone.clone(); + tokio::task::spawn_blocking( + /* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */ + move || if !is_first { std::thread::sleep(FAN_READ_PERIOD); }) + .map(move |_| { + if let Some(fan_sensor) = fan_clone2.sensor() { + match fan_sensor.read() { + 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::::from(format!("Failed to read fan speed: {}", e)))) + } + } + } else { + Err(usdpl_back::nrpc::ServiceError::Method(Box::::from("Failed to get fan speed sensor"))) + } + }) + }); + Ok(Box::new(stream)) + } else { + Ok(Box::new(usdpl_back::nrpc::_helpers::futures::stream::empty())) + } } async fn get_temperature<'b: 'a>( @@ -109,19 +120,22 @@ impl<'a> IFan<'a> for FanService { usdpl_back::nrpc::ServiceServerStream<'b, TemperatureMessage>, Box, > { - 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 thermal_zone = thermal_zone.clone(); + let sensor_clone2 = sensor_clone.clone(); tokio::task::spawn_blocking( /* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */ move || if !is_first { std::thread::sleep(TEMPERATURE_READ_PERIOD); }) .map(move |_| { - if let Some(temperature) = crate::sys::read_thermal_zone(&thermal_zone) { - let real_temp = temperature as f64 / 1000.0; - log::debug!("get_temperature() success: {}", real_temp); - Ok(TemperatureMessage { temperature: real_temp }) - } else { - Err(usdpl_back::nrpc::ServiceError::Method(Box::::from("get_temperature failed to read thermal zone 0"))) + match sensor_clone2.read() { + Ok(reading) => { + let real_temp = reading.value as f64 / 1000.0; + log::debug!("get_temperature() success: {}", real_temp); + Ok(TemperatureMessage { temperature: real_temp }) + }, + Err(e) => { + Err(usdpl_back::nrpc::ServiceError::Method(Box::::from(format!("get_temperature failed to read sensor: {}", e)))) + } } }) }); diff --git a/backend-rs/src/control.rs b/backend-rs/src/control.rs index b1c11d5..53fbb12 100644 --- a/backend-rs/src/control.rs +++ b/backend-rs/src/control.rs @@ -1,35 +1,36 @@ //! Fan control use std::sync::Arc; +//use std::collections::HashMap; use tokio::sync::RwLock; use std::thread; use std::time::{Duration, Instant}; -use sysfuss::{HwMonPath, BasicEntityPath}; - -use super::datastructs::{Settings, State, GraphPoint}; +use super::datastructs::{Settings, State}; use super::json::SettingsJson; -const VALVE_FAN_SERVICE: &str = "jupiter-fan-control.service"; -const SYSFS_ROOT: &str = "/"; - pub struct ControlRuntime { settings: Arc>, state: Arc>, - hwmon: Arc, - thermal_zone: Arc, + fan_adapter: Arc>, + sensor_adapter: Arc>, } impl ControlRuntime { - pub fn new() -> Self { + #[allow(dead_code)] + pub fn new(fan: F, sensor: S) -> Self { + Self::new_boxed(Box::new(fan), Box::new(sensor)) + } + + pub(crate) fn new_boxed(fan: Box, sensor: Box) -> Self { let new_state = State::new(); let settings_p = settings_path(&new_state.home); Self { settings: Arc::new(RwLock::new(super::json::SettingsJson::open(settings_p).unwrap_or_default().into())), state: Arc::new(RwLock::new(new_state)), - hwmon: Arc::new(crate::sys::find_hwmon(SYSFS_ROOT)), - thermal_zone: Arc::new(crate::sys::find_thermal_zone(SYSFS_ROOT)) + fan_adapter: Arc::new(fan), + sensor_adapter: Arc::new(sensor), } } @@ -49,27 +50,19 @@ impl ControlRuntime { &self.state } - /*pub(crate) fn hwmon(&self) -> &'_ HwMonPath { - &self.hwmon - }*/ - - pub(crate) fn hwmon_clone(&self) -> Arc { - self.hwmon.clone() + pub(crate) fn sensor_clone(&self) -> Arc> { + self.sensor_adapter.clone() } - /*pub(crate) fn thermal_zone(&self) -> &'_ BasicEntityPath { - &self.thermal_zone - }*/ - - pub(crate) fn thermal_zone_clone(&self) -> Arc { - self.thermal_zone.clone() + pub(crate) fn fan_clone(&self) -> Arc> { + self.fan_adapter.clone() } pub fn run(&self) -> thread::JoinHandle<()> { let runtime_settings = self.settings_clone(); let runtime_state = self.state_clone(); - let runtime_hwmon = self.hwmon.clone(); - let runtime_thermal_zone = self.thermal_zone.clone(); + let runtime_fan = self.fan_adapter.clone(); + let runtime_sensor = self.sensor_adapter.clone(); thread::spawn(move || { let sleep_duration = Duration::from_millis(1000); let mut start_time = Instant::now(); @@ -78,10 +71,9 @@ impl ControlRuntime { // resumed from sleep; do fan re-init log::debug!("Detected resume from sleep, overriding fan again"); { - let state = runtime_state.blocking_read(); let settings = runtime_settings.blocking_read(); 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)) { 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); let mut state = runtime_state.blocking_write(); state.dirty = false; @@ -104,171 +96,18 @@ impl ControlRuntime { { // fan control let settings = runtime_settings.blocking_read(); if settings.enable { - Self::enforce_jupiter_status(true); - Self::do_fan_control(&settings, &runtime_hwmon, &runtime_thermal_zone); + match runtime_sensor.read() { + 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); } }) } - - 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, 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, _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>(home: P) -> std::path::PathBuf { diff --git a/backend-rs/src/main.rs b/backend-rs/src/main.rs index 3c7fb65..d88397f 100644 --- a/backend-rs/src/main.rs +++ b/backend-rs/src/main.rs @@ -1,4 +1,5 @@ mod api; +mod adapters; mod control; mod datastructs; mod json; @@ -28,7 +29,14 @@ fn main() -> Result<(), ()> { println!("Starting back-end ({} v{})", api::NAME, api::VERSION); usdpl_back::Server::new(PORT) .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) + .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) + .unwrap_or_else(|| Box::new(adapters::sensors::DevModeSensor)) + )) )) .run_blocking() .unwrap(); diff --git a/backend-rs/src/sys.rs b/backend-rs/src/sys.rs index 2ec4577..9f4b73b 100644 --- a/backend-rs/src/sys.rs +++ b/backend-rs/src/sys.rs @@ -1,87 +1 @@ -use sysfuss::{SysPath, capability::attributes, SysEntityAttributesExt}; -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 { - 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 { - 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>(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>(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 - } - } -} +pub const SYSFS_ROOT: &str = "/";