Add Asus ROG Ally fan support (without interpolation)

This commit is contained in:
NGnius (Graham) 2024-09-14 13:00:16 -04:00
parent dd0c3998f0
commit 364d9650f2
11 changed files with 191 additions and 117 deletions

2
backend-rs/Cargo.lock generated
View file

@ -1243,7 +1243,7 @@ dependencies = [
[[package]] [[package]]
name = "sysfuss" name = "sysfuss"
version = "0.3.0" version = "0.4.0"
[[package]] [[package]]
name = "tempfile" name = "tempfile"

View file

@ -19,7 +19,7 @@ nrpc = { version = "1.0", path = "../../nRPC/nrpc", features = ["async-trait"] }
prost = "0.11" prost = "0.11"
tokio = { version = "1", features = ["sync", "rt"] } tokio = { version = "1", features = ["sync", "rt"] }
sysfuss = { version = "0.3", features = ["derive"], path = "../../sysfs-nav" } sysfuss = { version = "0.4", features = ["derive"], path = "../../sysfs-nav" }
# logging # logging
log = "0.4" log = "0.4"

View file

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

View file

@ -0,0 +1,159 @@
use sysfuss::{BasicEntityPath, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, SysAttributeExt};
use sysfuss::{SysPath, SysEntityAttributesExt};
use crate::datastructs::{Settings, GraphPoint};
const ASUS_CUSTOM_FAN_CURVE_HWMON_NAME: &str = "asus_custom_fan_curve";
const ASUS_REGULAR_HWMON_NAME: &str = "asus";
pub struct RogAllyFan {
hwmon_curve: HwMonPath,
hwmon_asus: HwMonPath,
}
impl RogAllyFan {
pub fn maybe_find() -> Option<Self> {
let syspath = SysPath::path(crate::sys::SYSFS_ROOT);
match syspath.hwmon_by_name(ASUS_CUSTOM_FAN_CURVE_HWMON_NAME)
{
Err(e) => {
log::error!("sysfs hwmon iter error while finding Asus ROG Ally fan curve: {}", e);
None
},
Ok(hwmon_curve) => {
match syspath.hwmon_by_name(ASUS_REGULAR_HWMON_NAME) {
Err(e) => {
log::error!("sysfs hwmon iter error while finding Asus fan: {}", e);
None
},
Ok(hwmon_asus) => {
Some(Self {
hwmon_curve,
hwmon_asus,
})
}
}
}
}
}
// returns true when the point probably doesn't exist at all
// (instead of it just running out of pwm# entries)
fn set_gp(gp_i: u64, gp: &GraphPoint, basic_curve: &BasicEntityPath) -> bool {
for pwm_i in 1..u64::MAX {
let temp_attr = pwm_point_temp(pwm_i, gp_i);
let pwm_attr = pwm_point_pwm(pwm_i, gp_i);
if !(temp_attr.exists(basic_curve) && pwm_attr.exists(basic_curve)) {
return pwm_i == 1;
}
let pwm_val = (gp.y * 255.0).round() as u8;
if let Err(e) = basic_curve.set(&pwm_attr as &str, pwm_val) {
let attr_path = pwm_attr.path(basic_curve);
log::error!("failed to write to {}: {}", attr_path.display(), e);
}
let temp_val = (gp.x * 100.0).round() as u8;
if let Err(e) = basic_curve.set(&temp_attr as &str, temp_val) {
let attr_path = temp_attr.path(basic_curve);
log::error!("failed to write to {}: {}", attr_path.display(), e);
}
}
return false;
}
}
impl super::super::Adapter for RogAllyFan {
fn on_enable_toggled(&self, settings: &Settings) {
let value_to_set = settings.enable as u8;
for i in 1..u64::MAX {
let attr = pwm_enable_attr_name(i);
if self.hwmon_curve.exists(&attr) {
if let Err(e) = self.hwmon_curve.set(attr, value_to_set) {
log::error!("failed to write to {}: {}", self.hwmon_curve.path_to(attr).display(), e);
}
} else {
break;
}
}
}
fn control_fan(&self, settings: &Settings, _sensor: &crate::adapters::traits::SensorReading) {
let basic_curve = BasicEntityPath::new(self.hwmon_curve.as_ref());
if settings.curve.is_empty() {
for i in 1..u64::MAX {
let out_of_points = if i == 1 {
Self::set_gp(i as _, &GraphPoint { x: 0.0, y: 0.0 }, &basic_curve)
} else {
Self::set_gp(i as _, &GraphPoint { x: 1.0, y: 1.0 }, &basic_curve)
};
if out_of_points { break; }
}
}
for (i, gp) in settings.curve.iter().enumerate() {
Self::set_gp(i as _, gp, &basic_curve);
}
// set remaining pwm points to highest in curve
for i in settings.curve.len()..usize::MAX {
if Self::set_gp(i as _, &GraphPoint { x: 1.0, y: 1.0 }, &basic_curve) {
break;
}
}
}
fn sensor<'a: 'b, 'b>(&'a self) -> Option<&'b dyn crate::adapters::SensorAdapter> {
Some(self)
}
}
impl super::super::super::SensorAdapter for RogAllyFan {
fn read(&self) -> Result<crate::adapters::SensorReading, std::io::Error> {
let mut readings = Vec::new();
let mut last_error = None;
for i in 1..u64::MAX {
let attr = fan_reading_attr_name(i);
let reading = match self.hwmon_asus.attribute::<u64>(attr) {
Ok(reading) => reading,
Err(sysfuss::EitherErr2::First(e1)) => {
log::debug!("failed to read attribute {}: {} (ok; breaking loop...)", self.hwmon_asus.path_to(attr).display(), e1);
last_error = Some(e1);
break;
},
Err(sysfuss::EitherErr2::Second(e2)) => {
log::debug!("failed to read attribute {}: {} (ok; breaking loop...)", self.hwmon_asus.path_to(attr).display(), e2);
last_error = Some(std::io::Error::other(e2));
break;
},
};
readings.push(reading);
}
let mut reading_sum = 0;
for reading in readings.iter() {
reading_sum += *reading;
}
if readings.is_empty() {
Err(last_error.unwrap())
} else {
Ok(crate::adapters::SensorReading {
value: (reading_sum / (readings.len() as u64)) as f64,
meaning: crate::adapters::SensorType::Fan,
name: "ROG Ally Fan",
})
}
}
}
fn pwm_enable_attr_name(index: u64) -> HwMonAttribute {
HwMonAttribute::new(HwMonAttributeType::Pwm, index, HwMonAttributeItem::Enable)
}
fn fan_reading_attr_name(index: u64) -> HwMonAttribute {
HwMonAttribute::new(HwMonAttributeType::Fan, index, HwMonAttributeItem::Input)
}
fn pwm_point_temp(index: u64, point_index: u64) -> String {
format!("pwm{}_auto_point{}_temp", index, point_index)
}
fn pwm_point_pwm(index: u64, point_index: u64) -> String {
format!("pwm{}_auto_point{}_pwm", index, point_index)
}

View file

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

View file

@ -174,6 +174,10 @@ fn read_fan(hwmon: &HwMonPath) -> std::io::Result<u64> {
} }
} }
fn fan_range() -> std::ops::Range<f64> {
1.0..7000.0
}
fn do_fan_control(settings: &Settings, hwmon: &HwMonPath, thermal_zone: f64) { fn do_fan_control(settings: &Settings, hwmon: &HwMonPath, thermal_zone: f64) {
/* /*
curve = self.settings["curve"] curve = self.settings["curve"]
@ -193,8 +197,8 @@ fn do_fan_control(settings: &Settings, hwmon: &HwMonPath, thermal_zone: f64) {
fan_ratio = self.step_fan(self, index, temperature_ratio) fan_ratio = self.step_fan(self, index, temperature_ratio)
set_fan_target(int((fan_ratio * FAN_MAXIMUM) + FAN_MINIMUM)) set_fan_target(int((fan_ratio * FAN_MAXIMUM) + FAN_MINIMUM))
*/ */
let temperature_ratio = (((thermal_zone as f64)/1000.0) - settings.temperature_bounds.min) let fan_bounds = fan_range();
/ (settings.temperature_bounds.max - settings.temperature_bounds.min); let temperature_ratio = ((thermal_zone as f64)/1000.0) / 100.0 /* temperature range in C */;
let mut index = None; let mut index = None;
for i in (0..settings.curve.len()).rev() { for i in (0..settings.curve.len()).rev() {
if settings.curve[i].x < temperature_ratio { if settings.curve[i].x < temperature_ratio {
@ -207,7 +211,7 @@ fn do_fan_control(settings: &Settings, hwmon: &HwMonPath, thermal_zone: f64) {
} else { } else {
step_fan(settings, index, temperature_ratio) 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 _; let fan_speed: u64 = ((fan_ratio * (fan_bounds.end - fan_bounds.start)) + fan_bounds.start) as _;
if let Err(e) = write_fan_target(hwmon, fan_speed) { if let Err(e) = write_fan_target(hwmon, fan_speed) {
log::error!("Failed to write to Steam Deck fan target file: {}", e); log::error!("Failed to write to Steam Deck fan target file: {}", e);
} }

View file

@ -9,6 +9,14 @@ impl ThermalZoneSensor {
pub fn new<P: AsRef<std::path::Path>>(zone_path: P) -> Self { pub fn new<P: AsRef<std::path::Path>>(zone_path: P) -> Self {
Self { zone: BasicEntityPath::new(zone_path) } Self { zone: BasicEntityPath::new(zone_path) }
} }
pub fn new_if_exists<P: AsRef<std::path::Path>>(zone_path: P) -> Option<Self> {
if zone_path.as_ref().exists() {
Some(Self { zone: BasicEntityPath::new(zone_path) })
} else {
None
}
}
} }
impl super::Adapter for ThermalZoneSensor { impl super::Adapter for ThermalZoneSensor {

View file

@ -2,7 +2,7 @@ use std::default::Default;
use std::convert::{Into, From}; use std::convert::{Into, From};
use std::path::PathBuf; use std::path::PathBuf;
use super::json::{SettingsJson, GraphPointJson, BoundsJson}; use super::json::{SettingsJson, GraphPointJson};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Settings { pub struct Settings {
@ -10,8 +10,6 @@ pub struct Settings {
pub enable: bool, pub enable: bool,
pub interpolate: bool, pub interpolate: bool,
pub curve: Vec<GraphPoint>, pub curve: Vec<GraphPoint>,
pub fan_bounds: Bounds<f64>,
pub temperature_bounds: Bounds<f64>,
} }
impl Settings { impl Settings {
@ -28,42 +26,18 @@ impl From<SettingsJson> for Settings {
enable: other.enable, enable: other.enable,
interpolate: other.interpolate, interpolate: other.interpolate,
curve: other.curve.drain(..).map(|x| GraphPoint::from_json(x, other.version)).collect(), curve: other.curve.drain(..).map(|x| GraphPoint::from_json(x, other.version)).collect(),
fan_bounds: Bounds {
min: 1.0,
max: 7000.0,
},
temperature_bounds: Bounds {
min: 0.0,
max: 100.0,
},
}, },
1 => Self { 1 => Self {
version: 1, version: 1,
enable: other.enable, enable: other.enable,
interpolate: other.interpolate, interpolate: other.interpolate,
curve: other.curve.drain(..).map(|x| GraphPoint::from_json(x, other.version)).collect(), curve: other.curve.drain(..).map(|x| GraphPoint::from_json(x, other.version)).collect(),
fan_bounds: other.fan_bounds.map(|x| Bounds::<f64>::from_json(x, other.version)).unwrap_or(Bounds {
min: 1.0,
max: 7000.0,
}),
temperature_bounds: other.temperature_bounds.map(|x| Bounds::<f64>::from_json(x, other.version)).unwrap_or(Bounds {
min: 0.0,
max: 100.0,
}),
}, },
_ => Self { _ => Self {
version: 1, version: 1,
enable: other.enable, enable: other.enable,
interpolate: other.interpolate, interpolate: other.interpolate,
curve: other.curve.drain(..).map(|x| GraphPoint::from_json(x, other.version)).collect(), curve: other.curve.drain(..).map(|x| GraphPoint::from_json(x, other.version)).collect(),
fan_bounds: Bounds {
min: 1.0,
max: 7000.0,
},
temperature_bounds: Bounds {
min: 0.0,
max: 100.0,
},
} }
}; };
result.sort_curve(); result.sort_curve();
@ -79,8 +53,6 @@ impl Into<SettingsJson> for Settings {
enable: self.enable, enable: self.enable,
interpolate: self.interpolate, interpolate: self.interpolate,
curve: self.curve.drain(..).map(|x| x.into()).collect(), curve: self.curve.drain(..).map(|x| x.into()).collect(),
fan_bounds: Some(self.fan_bounds.into()),
temperature_bounds: Some(self.temperature_bounds.into()),
} }
} }
} }
@ -121,72 +93,6 @@ impl Into<GraphPointJson> for GraphPoint {
} }
} }
#[derive(Debug, Clone)]
pub struct Bounds<T: core::fmt::Debug + Clone> {
pub min: T,
pub max: T,
}
/*impl Bounds<usize> {
#[inline]
pub fn from_json(other: BoundsJson, version: u64) -> Self {
match version {
0 => Self {
min: other.min as _,
max: other.max as _,
},
1 => Self {
min: other.min as _,
max: other.max as _,
},
_ => Self {
min: other.min as _,
max: other.max as _,
}
}
}
}*/
impl Bounds<f64> {
#[inline]
pub fn from_json(other: BoundsJson, version: u64) -> Self {
match version {
0 => Self {
min: other.min,
max: other.max,
},
1 => Self {
min: other.min,
max: other.max,
},
_ => Self {
min: other.min,
max: other.max,
}
}
}
}
/*impl Into<BoundsJson> for Bounds<usize> {
#[inline]
fn into(self) -> BoundsJson {
BoundsJson {
min: self.min as _,
max: self.max as _,
}
}
}*/
impl Into<BoundsJson> for Bounds<f64> {
#[inline]
fn into(self) -> BoundsJson {
BoundsJson {
min: self.min,
max: self.max,
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct State { pub struct State {
pub home: PathBuf, pub home: PathBuf,

View file

@ -11,8 +11,6 @@ pub struct SettingsJson {
pub enable: bool, pub enable: bool,
pub interpolate: bool, pub interpolate: bool,
pub curve: Vec<GraphPointJson>, pub curve: Vec<GraphPointJson>,
pub fan_bounds: Option<BoundsJson>,
pub temperature_bounds: Option<BoundsJson>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -21,12 +19,6 @@ pub struct GraphPointJson {
pub y: f64, pub y: f64,
} }
#[derive(Serialize, Deserialize)]
pub struct BoundsJson {
pub min: f64,
pub max: f64,
}
impl Default for SettingsJson { impl Default for SettingsJson {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -34,14 +26,6 @@ impl Default for SettingsJson {
enable: false, enable: false,
interpolate: true, interpolate: true,
curve: Vec::new(), curve: Vec::new(),
fan_bounds: Some(BoundsJson {
min: 0.0,
max: 7000.0,
}),
temperature_bounds: Some(BoundsJson {
min: 0.0,
max: 100.0,
})
} }
} }
} }

View file

@ -32,9 +32,17 @@ fn main() -> Result<(), ()> {
api::FanService::new(control::ControlRuntime::new_boxed( api::FanService::new(control::ControlRuntime::new_boxed(
adapters::fans::SteamDeckFan::maybe_find() adapters::fans::SteamDeckFan::maybe_find()
.map(|f| Box::new(f) as Box<dyn adapters::FanAdapter>) .map(|f| Box::new(f) as Box<dyn adapters::FanAdapter>)
.or_else(
|| adapters::fans::RogAllyFan::maybe_find()
.map(|f| Box::new(f) as Box<dyn adapters::FanAdapter>)
)
.unwrap_or_else(|| Box::new(adapters::fans::DevModeFan)), .unwrap_or_else(|| Box::new(adapters::fans::DevModeFan)),
adapters::fans::SteamDeckFan::maybe_find_thermal_zone() adapters::fans::SteamDeckFan::maybe_find_thermal_zone()
.map(|t| Box::new(adapters::sensors::ThermalZoneSensor::new(t)) as Box<dyn adapters::SensorAdapter>) .map(|t| Box::new(adapters::sensors::ThermalZoneSensor::new(t)) as Box<dyn adapters::SensorAdapter>)
.or_else(
|| adapters::sensors::ThermalZoneSensor::new_if_exists("/sys/thermal/thermal_zone0")
.map(|t| Box::new(t) as Box<dyn adapters::SensorAdapter>)
)
.unwrap_or_else(|| Box::new(adapters::sensors::DevModeSensor)) .unwrap_or_else(|| Box::new(adapters::sensors::DevModeSensor))
)) ))
)) ))

View file

@ -1,6 +1,6 @@
{ {
"name": "Fantastic", "name": "Fantastic",
"version": "0.5.1-alpha1", "version": "0.6.0-alpha1",
"description": "A template to quickly create decky plugins from scratch, based on TypeScript and webpack", "description": "A template to quickly create decky plugins from scratch, based on TypeScript and webpack",
"scripts": { "scripts": {
"build": "shx rm -rf dist && rollup -c", "build": "shx rm -rf dist && rollup -c",