Compare commits

..

8 commits
v0.5.0 ... main

26 changed files with 1362 additions and 934 deletions

919
backend-rs/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "fantastic-rs" name = "fantastic-rs"
version = "0.5.0-alpha3" 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"
@ -11,22 +11,23 @@ readme = "../README.md"
[dependencies] [dependencies]
usdpl-back = { version = "0.11", features = ["blocking"], path = "../../usdpl-rs/usdpl-back"} usdpl-back = { version = "1.0", features = ["blocking"], path = "../../usdpl-rs/usdpl-back"}
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
nrpc = { version = "0.10", path = "../../nRPC/nrpc" } 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" }
sysinfo = "0.31"
# logging # logging
log = "0.4" log = "0.4"
simplelog = "0.12" simplelog = "0.12"
[build-dependencies] [build-dependencies]
usdpl-build = { version = "0.11", path = "../../usdpl-rs/usdpl-build" } usdpl-build = { version = "1.0", path = "../../usdpl-rs/usdpl-build" }
[profile.release] [profile.release]
debug = false debug = false

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,10 @@
mod dev_mode;
pub use dev_mode::DevModeFan;
mod steam_deck;
pub use steam_deck::SteamDeckFan;
mod rog_ally;
pub use rog_ally::RogAllyFan;
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

@ -0,0 +1,297 @@
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 fan_range() -> std::ops::Range<f64> {
let sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::new().with_cpu(sysinfo::CpuRefreshKind::everything()),
);
if sys.cpus().is_empty() {
1.0..7123.0
} else {
let cpu_name = sys.cpus()[0].brand();
if cpu_name.contains("AMD Custom APU 0405") {
// LCD
1.0..7000.0
} else if cpu_name.contains("AMD Custom APU 0932") {
// OLED
1.0..9000.0
} else {
// Hopefully never reached
1.0..7001.0
}
}
}
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 fan_bounds = fan_range();
let temperature_ratio = ((thermal_zone as f64)/1000.0) / 100.0 /* temperature range in C */;
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 * (fan_bounds.end - fan_bounds.start)) + fan_bounds.start) 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,44 @@
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) }
}
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 {
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

@ -28,7 +28,7 @@ fn once_true() -> impl std::iter::Iterator<Item = bool> {
std::iter::once(true).chain(std::iter::repeat(false)) std::iter::once(true).chain(std::iter::repeat(false))
} }
#[usdpl_back::nrpc::_helpers::async_trait::async_trait] #[::nrpc::_helpers::async_trait::async_trait]
impl<'a> IFan<'a> for FanService { impl<'a> IFan<'a> for FanService {
async fn echo( async fn echo(
&mut self, &mut self,
@ -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();
let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| { if fan_clone.sensor().is_some() {
let hwmon = hwmon.clone(); let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| {
tokio::task::spawn_blocking( let fan_clone2 = fan_clone.clone();
/* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */ tokio::task::spawn_blocking(
move || if !is_first { std::thread::sleep(FAN_READ_PERIOD); }) /* tokio::time::sleep(..) is not Unpin (but this is)... *grumble grumble* */
.map(move |_| { move || if !is_first { std::thread::sleep(FAN_READ_PERIOD); })
if let Some(rpm) = crate::sys::read_fan(&hwmon) { .map(move |_| {
log::debug!("get_fan_rpm() success: {}", rpm); if let Some(fan_sensor) = fan_clone2.sensor() {
Ok(RpmMessage { rpm: rpm as u32 }) match fan_sensor.read() {
} else { Ok(reading) => {
Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from("Failed to read fan speed"))) log::debug!("get_fan_rpm() success: {}", reading.value);
} Ok(RpmMessage { rpm: reading.value as u32 })
}) }
}); Err(e) => {
Ok(Box::new(stream)) Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::from(format!("Failed to read fan speed: {}", e))))
}
}
} else {
Err(usdpl_back::nrpc::ServiceError::Method(Box::<dyn std::error::Error + Send + Sync>::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>( 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) => {
log::debug!("get_temperature() success: {}", real_temp); let real_temp = reading.value as f64 / 1000.0;
Ok(TemperatureMessage { temperature: real_temp }) log::debug!("get_temperature() success: {}", real_temp);
} else { Ok(TemperatureMessage { temperature: real_temp })
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

@ -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

@ -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,22 @@ 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>)
.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)),
adapters::fans::SteamDeckFan::maybe_find_thermal_zone()
.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))
))
)) ))
.run_blocking() .run_blocking()
.unwrap(); .unwrap();

View file

@ -1,83 +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) => {
iter.next()
.unwrap_or_else(|| {
log::error!("sysfs hwmon iter empty: [no capable results]");
syspath.hwmon_by_index(HWMON_INDEX)
})
}
}
}
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) => {
iter.next()
.unwrap_or_else(|| {
log::error!("sysfs thermal class iter empty: [no capable results]");
BasicEntityPath::new("/sys/class/thermal/thermal_zone0")
})
}
}
}

View file

@ -1,6 +1,6 @@
{ {
"name": "Fantastic", "name": "Fantastic",
"version": "0.5.0-alpha3", "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",
@ -32,7 +32,7 @@
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@rollup/plugin-typescript": "^8.5.0", "@rollup/plugin-typescript": "^8.5.0",
"@types/react": "16.14.0", "@types/react": "16.14.0",
"@types/webpack": "^5.28.2", "@types/webpack": "^5.28.5",
"rollup": "^2.79.1", "rollup": "^2.79.1",
"rollup-plugin-import-assets": "^1.1.1", "rollup-plugin-import-assets": "^1.1.1",
"shx": "^0.3.4", "shx": "^0.3.4",
@ -40,9 +40,9 @@
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
"dependencies": { "dependencies": {
"decky-frontend-lib": "~3.22.0", "decky-frontend-lib": "^3.25.0",
"fantastic-wasm": "file:src/rust/pkg", "fantastic-wasm": "file:src/rust/pkg",
"react-icons": "^4.10.1" "react-icons": "^4.12.0"
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {

View file

@ -1,11 +1,11 @@
{ {
"name": "Fantastic", "name": "Fantastic",
"author": "NGnius", "author": "NGnius",
"flags": ["root", "debug"], "flags": ["root", "_debug", "global-dfl"],
"publish": { "publish": {
"discord_id": "106537989684887552", "discord_id": "106537989684887552",
"description": "Fan controls", "description": "Fan controls",
"tags": [ "utility", "fan-control" ], "tags": [ "utility", "fan-control", "root" ],
"image": "https://raw.githubusercontent.com/NGnius/Fantastic/main/assets/thumbnail.png" "image": "https://raw.githubusercontent.com/NGnius/Fantastic/main/assets/thumbnail.png"
} }
} }

View file

@ -6,14 +6,14 @@ settings:
dependencies: dependencies:
decky-frontend-lib: decky-frontend-lib:
specifier: ~3.22.0 specifier: ^3.25.0
version: 3.22.0 version: 3.25.0
fantastic-wasm: fantastic-wasm:
specifier: file:src/rust/pkg specifier: file:src/rust/pkg
version: file:src/rust/pkg version: file:src/rust/pkg
react-icons: react-icons:
specifier: ^4.10.1 specifier: ^4.12.0
version: 4.10.1 version: 4.12.0
devDependencies: devDependencies:
'@rollup/plugin-commonjs': '@rollup/plugin-commonjs':
@ -35,8 +35,8 @@ devDependencies:
specifier: 16.14.0 specifier: 16.14.0
version: 16.14.0 version: 16.14.0
'@types/webpack': '@types/webpack':
specifier: ^5.28.2 specifier: ^5.28.5
version: 5.28.2 version: 5.28.5
rollup: rollup:
specifier: ^2.79.1 specifier: ^2.79.1
version: 2.79.1 version: 2.79.1
@ -55,40 +55,40 @@ devDependencies:
packages: packages:
/@jridgewell/gen-mapping@0.3.3: /@jridgewell/gen-mapping@0.3.5:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dependencies: dependencies:
'@jridgewell/set-array': 1.1.2 '@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
'@jridgewell/trace-mapping': 0.3.19 '@jridgewell/trace-mapping': 0.3.25
dev: true dev: true
/@jridgewell/resolve-uri@3.1.1: /@jridgewell/resolve-uri@3.1.2:
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dev: true dev: true
/@jridgewell/set-array@1.1.2: /@jridgewell/set-array@1.2.1:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dev: true dev: true
/@jridgewell/source-map@0.3.5: /@jridgewell/source-map@0.3.5:
resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.3 '@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.19 '@jridgewell/trace-mapping': 0.3.25
dev: true dev: true
/@jridgewell/sourcemap-codec@1.4.15: /@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true dev: true
/@jridgewell/trace-mapping@0.3.19: /@jridgewell/trace-mapping@0.3.25:
resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.1 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true dev: true
@ -104,7 +104,7 @@ packages:
glob: 7.2.3 glob: 7.2.3
is-reference: 1.2.1 is-reference: 1.2.1
magic-string: 0.25.9 magic-string: 0.25.9
resolve: 1.22.4 resolve: 1.22.8
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
@ -128,7 +128,7 @@ packages:
deepmerge: 4.3.1 deepmerge: 4.3.1
is-builtin-module: 3.2.1 is-builtin-module: 3.2.1
is-module: 1.0.0 is-module: 1.0.0
resolve: 1.22.4 resolve: 1.22.8
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
@ -154,7 +154,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 3.1.0(rollup@2.79.1) '@rollup/pluginutils': 3.1.0(rollup@2.79.1)
resolve: 1.22.4 resolve: 1.22.8
rollup: 2.79.1 rollup: 2.79.1
tslib: 2.6.2 tslib: 2.6.2
typescript: 4.9.5 typescript: 4.9.5
@ -172,59 +172,61 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/@types/eslint-scope@3.7.4: /@types/eslint-scope@3.7.7:
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
dependencies: dependencies:
'@types/eslint': 8.44.2 '@types/eslint': 8.56.5
'@types/estree': 1.0.1 '@types/estree': 1.0.5
dev: true dev: true
/@types/eslint@8.44.2: /@types/eslint@8.56.5:
resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} resolution: {integrity: sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==}
dependencies: dependencies:
'@types/estree': 1.0.1 '@types/estree': 1.0.5
'@types/json-schema': 7.0.12 '@types/json-schema': 7.0.15
dev: true dev: true
/@types/estree@0.0.39: /@types/estree@0.0.39:
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
dev: true dev: true
/@types/estree@1.0.1: /@types/estree@1.0.5:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true dev: true
/@types/json-schema@7.0.12: /@types/json-schema@7.0.15:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true dev: true
/@types/node@20.5.9: /@types/node@20.11.25:
resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==} resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==}
dependencies:
undici-types: 5.26.5
dev: true dev: true
/@types/prop-types@15.7.5: /@types/prop-types@15.7.11:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
dev: true dev: true
/@types/react@16.14.0: /@types/react@16.14.0:
resolution: {integrity: sha512-jJjHo1uOe+NENRIBvF46tJimUvPnmbQ41Ax0pEm7pRvhPg+wuj8VMOHHiMvaGmZRzRrCtm7KnL5OOE/6kHPK8w==} resolution: {integrity: sha512-jJjHo1uOe+NENRIBvF46tJimUvPnmbQ41Ax0pEm7pRvhPg+wuj8VMOHHiMvaGmZRzRrCtm7KnL5OOE/6kHPK8w==}
dependencies: dependencies:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.11
csstype: 3.1.2 csstype: 3.1.3
dev: true dev: true
/@types/resolve@1.17.1: /@types/resolve@1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies: dependencies:
'@types/node': 20.5.9 '@types/node': 20.11.25
dev: true dev: true
/@types/webpack@5.28.2: /@types/webpack@5.28.5:
resolution: {integrity: sha512-7tcxyrIOd7WGimZIcWU6pDsNh2edGGnwYExOvd3l/nMvuxqwVPrFXnnTbYCnplqV9BJoU7Mo2mfFtiH8CNFvYw==} resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==}
dependencies: dependencies:
'@types/node': 20.5.9 '@types/node': 20.11.25
tapable: 2.2.1 tapable: 2.2.1
webpack: 5.88.2 webpack: 5.90.3
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/core' - '@swc/core'
- esbuild - esbuild
@ -346,16 +348,16 @@ packages:
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
dev: true dev: true
/acorn-import-assertions@1.9.0(acorn@8.10.0): /acorn-import-assertions@1.9.0(acorn@8.11.3):
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
peerDependencies: peerDependencies:
acorn: ^8 acorn: ^8
dependencies: dependencies:
acorn: 8.10.0 acorn: 8.11.3
dev: true dev: true
/acorn@8.10.0: /acorn@8.11.3:
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
dev: true dev: true
@ -388,15 +390,15 @@ packages:
concat-map: 0.0.1 concat-map: 0.0.1
dev: true dev: true
/browserslist@4.21.10: /browserslist@4.23.0:
resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001525 caniuse-lite: 1.0.30001596
electron-to-chromium: 1.4.508 electron-to-chromium: 1.4.699
node-releases: 2.0.13 node-releases: 2.0.14
update-browserslist-db: 1.0.11(browserslist@4.21.10) update-browserslist-db: 1.0.13(browserslist@4.23.0)
dev: true dev: true
/buffer-from@1.1.2: /buffer-from@1.1.2:
@ -408,8 +410,8 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/caniuse-lite@1.0.30001525: /caniuse-lite@1.0.30001596:
resolution: {integrity: sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==} resolution: {integrity: sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==}
dev: true dev: true
/chrome-trace-event@1.0.3: /chrome-trace-event@1.0.3:
@ -429,12 +431,12 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
/csstype@3.1.2: /csstype@3.1.3:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dev: true dev: true
/decky-frontend-lib@3.22.0: /decky-frontend-lib@3.25.0:
resolution: {integrity: sha512-MJ0y0bhNMHJyMVxHht3O0L0GxdT9sckUmh35HG7/ERqyZQsfKpDqOeW6pC1R07SnuWwgbl4fY3tzjlrb7qUeoA==} resolution: {integrity: sha512-2lBoHS2AIRmuluq/bGdHBz+uyToQE7k3K/vDq1MQbDZ4eC+8CGDuh2T8yZOj3D0yjGP2MdikNNAWPA9Z5l2qDg==}
dev: false dev: false
/deepmerge@4.3.1: /deepmerge@4.3.1:
@ -442,24 +444,24 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/electron-to-chromium@1.4.508: /electron-to-chromium@1.4.699:
resolution: {integrity: sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==} resolution: {integrity: sha512-I7q3BbQi6e4tJJN5CRcyvxhK0iJb34TV8eJQcgh+fR2fQ8miMgZcEInckCo1U9exDHbfz7DLDnFn8oqH/VcRKw==}
dev: true dev: true
/enhanced-resolve@5.15.0: /enhanced-resolve@5.15.1:
resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} resolution: {integrity: sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.2.1 tapable: 2.2.1
dev: true dev: true
/es-module-lexer@1.3.0: /es-module-lexer@1.4.1:
resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==}
dev: true dev: true
/escalade@3.1.1: /escalade@3.1.2:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
@ -525,8 +527,8 @@ packages:
dev: true dev: true
optional: true optional: true
/function-bind@1.1.1: /function-bind@1.1.2:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: true dev: true
/glob-to-regexp@0.4.1: /glob-to-regexp@0.4.1:
@ -553,11 +555,11 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/has@1.0.3: /hasown@2.0.2:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4'}
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.2
dev: true dev: true
/inflight@1.0.6: /inflight@1.0.6:
@ -583,10 +585,10 @@ packages:
builtin-modules: 3.3.0 builtin-modules: 3.3.0
dev: true dev: true
/is-core-module@2.13.0: /is-core-module@2.13.1:
resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
dependencies: dependencies:
has: 1.0.3 hasown: 2.0.2
dev: true dev: true
/is-module@1.0.0: /is-module@1.0.0:
@ -596,14 +598,14 @@ packages:
/is-reference@1.2.1: /is-reference@1.2.1:
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
dependencies: dependencies:
'@types/estree': 1.0.1 '@types/estree': 1.0.5
dev: true dev: true
/jest-worker@27.5.1: /jest-worker@27.5.1:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
dependencies: dependencies:
'@types/node': 20.5.9 '@types/node': 20.11.25
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 8.1.1 supports-color: 8.1.1
dev: true dev: true
@ -657,8 +659,8 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true dev: true
/node-releases@2.0.13: /node-releases@2.0.14:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: true dev: true
/once@1.4.0: /once@1.4.0:
@ -685,8 +687,8 @@ packages:
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
dev: true dev: true
/punycode@2.3.0: /punycode@2.3.1:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
@ -696,8 +698,8 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: true dev: true
/react-icons@4.10.1: /react-icons@4.12.0:
resolution: {integrity: sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==} resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==}
peerDependencies: peerDependencies:
react: '*' react: '*'
peerDependenciesMeta: peerDependenciesMeta:
@ -709,14 +711,14 @@ packages:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
dependencies: dependencies:
resolve: 1.22.4 resolve: 1.22.8
dev: true dev: true
/resolve@1.22.4: /resolve@1.22.8:
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true hasBin: true
dependencies: dependencies:
is-core-module: 2.13.0 is-core-module: 2.13.1
path-parse: 1.0.7 path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0 supports-preserve-symlinks-flag: 1.0.0
dev: true dev: true
@ -753,13 +755,13 @@ packages:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
dependencies: dependencies:
'@types/json-schema': 7.0.12 '@types/json-schema': 7.0.15
ajv: 6.12.6 ajv: 6.12.6
ajv-keywords: 3.5.2(ajv@6.12.6) ajv-keywords: 3.5.2(ajv@6.12.6)
dev: true dev: true
/serialize-javascript@6.0.1: /serialize-javascript@6.0.2:
resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
dependencies: dependencies:
randombytes: 2.1.0 randombytes: 2.1.0
dev: true dev: true
@ -817,8 +819,8 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/terser-webpack-plugin@5.3.9(webpack@5.88.2): /terser-webpack-plugin@5.3.10(webpack@5.90.3):
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
peerDependencies: peerDependencies:
'@swc/core': '*' '@swc/core': '*'
@ -833,21 +835,21 @@ packages:
uglify-js: uglify-js:
optional: true optional: true
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.19 '@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1 jest-worker: 27.5.1
schema-utils: 3.3.0 schema-utils: 3.3.0
serialize-javascript: 6.0.1 serialize-javascript: 6.0.2
terser: 5.19.3 terser: 5.29.1
webpack: 5.88.2 webpack: 5.90.3
dev: true dev: true
/terser@5.19.3: /terser@5.29.1:
resolution: {integrity: sha512-pQzJ9UJzM0IgmT4FAtYI6+VqFf0lj/to58AV0Xfgg0Up37RyPG7Al+1cepC6/BVuAxR9oNb41/DL4DEoHJvTdg==} resolution: {integrity: sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
dependencies: dependencies:
'@jridgewell/source-map': 0.3.5 '@jridgewell/source-map': 0.3.5
acorn: 8.10.0 acorn: 8.11.3
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
dev: true dev: true
@ -862,21 +864,25 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/update-browserslist-db@1.0.11(browserslist@4.21.10): /undici-types@5.26.5:
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
/update-browserslist-db@1.0.13(browserslist@4.23.0):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
dependencies: dependencies:
browserslist: 4.21.10 browserslist: 4.23.0
escalade: 3.1.1 escalade: 3.1.2
picocolors: 1.0.0 picocolors: 1.0.0
dev: true dev: true
/uri-js@4.4.1: /uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies: dependencies:
punycode: 2.3.0 punycode: 2.3.1
dev: true dev: true
/url-join@4.0.1: /url-join@4.0.1:
@ -896,8 +902,8 @@ packages:
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
dev: true dev: true
/webpack@5.88.2: /webpack@5.90.3:
resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} resolution: {integrity: sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -906,17 +912,17 @@ packages:
webpack-cli: webpack-cli:
optional: true optional: true
dependencies: dependencies:
'@types/eslint-scope': 3.7.4 '@types/eslint-scope': 3.7.7
'@types/estree': 1.0.1 '@types/estree': 1.0.5
'@webassemblyjs/ast': 1.11.6 '@webassemblyjs/ast': 1.11.6
'@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6
'@webassemblyjs/wasm-parser': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.10.0 acorn: 8.11.3
acorn-import-assertions: 1.9.0(acorn@8.10.0) acorn-import-assertions: 1.9.0(acorn@8.11.3)
browserslist: 4.21.10 browserslist: 4.23.0
chrome-trace-event: 1.0.3 chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0 enhanced-resolve: 5.15.1
es-module-lexer: 1.3.0 es-module-lexer: 1.4.1
eslint-scope: 5.1.1 eslint-scope: 5.1.1
events: 3.3.0 events: 3.3.0
glob-to-regexp: 0.4.1 glob-to-regexp: 0.4.1
@ -927,7 +933,7 @@ packages:
neo-async: 2.6.2 neo-async: 2.6.2
schema-utils: 3.3.0 schema-utils: 3.3.0
tapable: 2.2.1 tapable: 2.2.1
terser-webpack-plugin: 5.3.9(webpack@5.88.2) terser-webpack-plugin: 5.3.10(webpack@5.90.3)
watchpack: 2.4.0 watchpack: 2.4.0
webpack-sources: 3.2.3 webpack-sources: 3.2.3
transitivePeerDependencies: transitivePeerDependencies:

View file

@ -24,12 +24,13 @@ export default defineConfig({
}) })
], ],
context: 'window', context: 'window',
external: ['react', 'react-dom'], external: ['react', 'react-dom', 'decky-frontend-lib'],
output: { output: {
file: 'dist/index.js', file: 'dist/index.js',
globals: { globals: {
react: 'SP_REACT', react: 'SP_REACT',
'react-dom': 'SP_REACTDOM', 'react-dom': 'SP_REACTDOM',
'decky-frontend-lib': 'DFL',
}, },
format: 'iife', format: 'iife',
exports: 'default', exports: 'default',

View file

@ -76,13 +76,14 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
} }
function onClickCanvas(e: any) { function onClickCanvas(e: any) {
//console.log("canvas click", e); //console.log("[FANTASTIC] canvas click", e);
const realEvent: any = e.nativeEvent; const realEvent: any = e.nativeEvent;
//console.log("Canvas click @ (" + realEvent.layerX.toString() + ", " + realEvent.layerY.toString() + ")"); //console.log("Canvas click @ (" + realEvent.layerX.toString() + ", " + realEvent.layerY.toString() + ")");
const target: any = e.currentTarget; const target: any = e.currentTarget;
//console.log("Target dimensions " + target.width.toString() + "x" + target.height.toString()); //console.log("[FANTASTIC] Target dimensions " + target.width.toString() + "x" + target.height.toString());
const clickX = realEvent.layerX; var clickX = realEvent.offsetX;
const clickY = realEvent.layerY; var clickY = realEvent.offsetY;
//console.debug("[FANTASTIC] curve click:", clickX, clickY);
for (let i = 0; i < curveGlobal.length; i++) { for (let i = 0; i < curveGlobal.length; i++) {
const curvePoint = curveGlobal[i]; const curvePoint = curveGlobal[i];
const pointX = curvePoint.x * target.width; const pointX = curvePoint.x * target.width;
@ -99,7 +100,8 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
} }
} }
//console.log("Adding new point"); //console.log("Adding new point");
backend.resolve(backend.addCurvePoint({x: clickX / target.width, y: 1 - (clickY / target.height)}), setCurve); const curvePoint = {x: clickX / target.width, y: 1 - (clickY / target.height)};
backend.resolve(backend.addCurvePoint(curvePoint), setCurve);
} }
function drawCanvas(ctx: any, frameCount: number): void { function drawCanvas(ctx: any, frameCount: number): void {

22
src/rust/Cargo.lock generated
View file

@ -17,17 +17,6 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "async-trait"
version = "0.1.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -429,9 +418,8 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]] [[package]]
name = "nrpc" name = "nrpc"
version = "0.10.0" version = "1.0.0"
dependencies = [ dependencies = [
"async-trait",
"bytes", "bytes",
"futures", "futures",
"prost", "prost",
@ -439,7 +427,7 @@ dependencies = [
[[package]] [[package]]
name = "nrpc-build" name = "nrpc-build"
version = "0.10.0" version = "1.0.0"
dependencies = [ dependencies = [
"nrpc", "nrpc",
"prettyplease 0.2.12", "prettyplease 0.2.12",
@ -803,7 +791,7 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]] [[package]]
name = "usdpl-build" name = "usdpl-build"
version = "0.11.0" version = "1.0.0"
dependencies = [ dependencies = [
"nrpc-build", "nrpc-build",
"prettyplease 0.2.12", "prettyplease 0.2.12",
@ -816,14 +804,14 @@ dependencies = [
[[package]] [[package]]
name = "usdpl-core" name = "usdpl-core"
version = "0.11.0" version = "1.0.0"
dependencies = [ dependencies = [
"base64", "base64",
] ]
[[package]] [[package]]
name = "usdpl-front" name = "usdpl-front"
version = "0.11.0" version = "1.0.0"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"futures", "futures",

View file

@ -13,12 +13,12 @@ readme = "../../README.md"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
usdpl-front = { version = "0.11", path = "../../../usdpl-rs/usdpl-front" } usdpl-front = { version = "1.0", path = "../../../usdpl-rs/usdpl-front" }
nrpc = { version = "0.10", path = "../../../nRPC/nrpc", default-features = false } nrpc = { version = "1.0", path = "../../../nRPC/nrpc", default-features = false }
prost = "0.11" prost = "0.11"
[build-dependencies] [build-dependencies]
usdpl-build = { version = "0.11", path = "../../../usdpl-rs/usdpl-build" } usdpl-build = { version = "1.0", path = "../../../usdpl-rs/usdpl-build" }
[features] [features]
debug = ["usdpl-front/debug"] debug = ["usdpl-front/debug"]