Compare commits

...

18 commits
v0.4.0 ... main

Author SHA1 Message Date
924f1aaa51 Use correct fan max range for Steam Deck OLED; fixes #19 2024-09-29 17:16:22 -04:00
364d9650f2 Add Asus ROG Ally fan support (without interpolation) 2024-09-14 13:00:16 -04:00
dd0c3998f0 Refactor fan and sensor into traits 2024-08-20 21:25:43 -04:00
a0f565895b Fix compile with USDPL & nRPC v1.0 2024-04-02 20:53:55 -04:00
32d09219da Update DFL, move back to dependencies (not devDeps) 2024-03-10 22:01:53 -04:00
b637f4f5f9 Fix canvas clicking for #20 2024-03-10 21:48:30 -04:00
8c9a7b7c68 Switch to global DFL 2024-01-28 11:24:34 -05:00
f620dc1854 Add extra debug information for system, add root flag 2023-12-02 13:33:55 -05:00
6c8b335417 Make periodic reads protobuf streams to reuse open websockets 2023-10-09 18:21:31 -04:00
dd6672f9ba Don't init twice 2023-09-03 18:21:23 -04:00
f4a3f06aae Remove unused functions, improve startup checks 2023-09-03 17:38:20 -04:00
255958f96b Add extra version info in pre-release versions 2023-09-03 11:41:59 -04:00
2bec0087a0 Use sysfuss for kernel interactions, usability fixes for alpha1 2023-09-03 11:23:57 -04:00
9972ac7854 Make all code compile with latest indev crates 2023-08-29 19:51:33 -04:00
438d91f639 Get experimental USDPL working 2023-06-28 22:22:48 -04:00
1c6a889006
Create FUNDING.yml 2023-06-04 20:46:37 +00:00
3d744f31aa Upgrade frontend to experimental USDPL next 2023-06-04 16:31:29 -04:00
c88402e580 Upgrade backend & wasm to USDPL next 2023-06-04 14:50:58 -04:00
43 changed files with 3668 additions and 2272 deletions

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: NGnius
liberapay: NGnius

1
.gitignore vendored
View file

@ -44,6 +44,7 @@ yalc.lock
/backend-rs/target
backend
/bin/backend
/src/rust/target
# packaged teasers
*.zip

1595
backend-rs/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,34 @@
[package]
name = "fantastic-rs"
version = "0.4.0"
version = "0.6.0-alpha1"
edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Backend (superuser) functionality for Fantastic"
license = "GPL-3.0-only"
repository = "https://git.ngni.us/NG-SD-Plugins/Fantastic"
keywords = ["utility", "fan-control", "root", "decky"]
readme = "../README.md"
[dependencies]
usdpl-back = { version = "0.10", features = ["blocking"]}
usdpl-back = { version = "1.0", features = ["blocking"], path = "../../usdpl-rs/usdpl-back"}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
nrpc = { version = "1.0", path = "../../nRPC/nrpc", features = ["async-trait"] }
prost = "0.11"
tokio = { version = "1", features = ["sync", "rt"] }
sysfuss = { version = "0.4", features = ["derive"], path = "../../sysfs-nav" }
sysinfo = "0.31"
# logging
log = "0.4"
simplelog = "0.12"
[build-dependencies]
usdpl-build = { version = "1.0", path = "../../usdpl-rs/usdpl-build" }
[profile.release]
debug = false
strip = true

9
backend-rs/build.rs Normal file
View file

@ -0,0 +1,9 @@
fn main() {
//println!("CWD: {}", std::env::current_dir().unwrap().display());
usdpl_build::back::build([
format!("{}/protos/fantastic.proto", std::env::current_dir().unwrap().display())
].into_iter(),
[
format!("{}/protos/", std::env::current_dir().unwrap().display())
].into_iter())
}

View file

@ -1,11 +1,13 @@
#!/bin/bash
#cargo build --release --target x86_64-unknown-linux-musl
cargo build --target x86_64-unknown-linux-musl
#cargo build --target x86_64-unknown-linux-musl
#cross build --release
cargo build
mkdir -p ../bin
#cp ./target/x86_64-unknown-linux-musl/release/fantastic-rs ../bin/backend
cp ./target/x86_64-unknown-linux-musl/debug/fantastic-rs ../bin/backend
#cp ./target/release/fantastic-rs ../bin/backend
#cp --preserve=mode ./target/x86_64-unknown-linux-musl/release/fantastic-rs ../bin/backend
#cp --preserve=mode ./target/x86_64-unknown-linux-musl/debug/fantastic-rs ../bin/backend
#cp --preserve=mode ./target/release/fantastic-rs ../bin/backend
cp --preserve=mode ./target/debug/fantastic-rs ../bin/backend

View file

@ -0,0 +1,136 @@
syntax = "proto3";
package fantastic;
// The most amazing fan service
service Fan {
// Send back the exact same message as received
rpc echo (EchoMessage) returns (EchoMessage);
// Hello world
rpc hello (NameMessage) returns (HelloResponse);
// Fantastic version info
rpc version (Empty) returns (VersionMessage);
// Fantastic version number string
rpc version_str (Empty) returns (VersionDisplayMessage);
// Rust name (fantastic)
rpc name (Empty) returns (NameMessage);
// Get fan speed
rpc get_fan_rpm (Empty) returns (stream RpmMessage);
// Get system temperature
rpc get_temperature (Empty) returns (stream TemperatureMessage);
// Set custom fan control enabled
rpc set_enable (EnablementMessage) returns (EnablementMessage);
// Get custon fan control status
rpc get_enable (Empty) returns (EnablementMessage);
// Set fan control interpolation
rpc set_interpolate (EnablementMessage) returns (EnablementMessage);
// Get fan control interpolation
rpc get_interpolate (Empty) returns (EnablementMessage);
// Get fan control curve
rpc get_curve_x (Empty) returns (CurveMessageX);
// Get fan control curve
rpc get_curve_y (Empty) returns (CurveMessageY);
// Add a new point to the fan curve
rpc add_curve_point (GraphPoint) returns (Empty);
// Remove a point from the fan curve
rpc remove_curve_point (IndexMessage) returns (Empty);
/*
.register("echo", api::echo)
.register("hello", api::hello)
.register("version", api::version)
.register("name", api::name)
.register("get_fan_rpm", api::get_fan_rpm)
.register("get_temperature", api::get_temperature)
.register("set_enable", api::set_enable_gen(&runtime))
.register("get_enable", api::get_enable_gen(&runtime))
.register("set_interpolate", api::set_interpolate_gen(&runtime))
.register("get_interpolate", api::get_interpolate_gen(&runtime))
.register("get_curve", api::get_curve_gen(&runtime))
.register("add_curve_point", api::add_curve_point_gen(&runtime))
.register("remove_curve_point", api::remove_curve_point_gen(&runtime))
*/
}
// The request and response message for Echo
message EchoMessage {
string msg = 1;
}
message NameMessage {
string name = 1;
}
message HelloResponse {
string phrase = 1;
}
message Empty {
bool ok = 1;
}
message VersionMessage {
int32 major = 1;
int32 minor = 2;
int32 patch = 3;
//string display = 4;
}
message VersionDisplayMessage {
string display = 1;
}
message VersionStr {
string version_str = 1;
}
message RpmMessage {
uint32 rpm = 1;
}
message TemperatureMessage {
double temperature = 1;
}
message EnablementMessage {
bool is_enabled = 1;
}
message GraphPoint {
double x = 1;
double y = 2;
}
/*message CurveMessage {
//repeated GraphPoint points = 1;
repeated double x = 1;
repeated double y = 2;
}*/
message CurveMessageX {
//repeated GraphPoint points = 1;
repeated double x = 1;
}
message CurveMessageY {
//repeated GraphPoint points = 1;
repeated double y = 2;
}
message IndexMessage {
uint32 index = 1;
}

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

@ -1,259 +1,238 @@
use usdpl_back::core::serdes::Primitive;
use crate::services::fantastic::*;
use usdpl_back::nrpc::_helpers::futures::{StreamExt, FutureExt};
use super::control::ControlRuntime;
use super::json::GraphPointJson;
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub const NAME: &'static str = env!("CARGO_PKG_NAME");
pub fn hello(params: Vec<Primitive>) -> Vec<Primitive> {
if let Some(Primitive::String(name)) = params.get(0) {
vec![Primitive::String(format!("Hello {}", name))]
} else {
vec![]
}
const FAN_READ_PERIOD: std::time::Duration = std::time::Duration::from_millis(1000);
const TEMPERATURE_READ_PERIOD: std::time::Duration = std::time::Duration::from_millis(2000);
pub struct FanService {
ctrl: ControlRuntime,
}
pub fn echo(params: Vec<Primitive>) -> Vec<Primitive> {
params
}
pub fn version(_: Vec<Primitive>) -> Vec<Primitive> {
vec![VERSION.into()]
}
pub fn name(_: Vec<Primitive>) -> Vec<Primitive> {
vec![NAME.into()]
}
pub fn get_fan_rpm(_: Vec<Primitive>) -> Vec<Primitive> {
if let Some(rpm) = crate::sys::read_fan() {
log::debug!("get_fan_rpm() success: {}", rpm);
vec![rpm.into()]
} else {
log::error!("get_fan_rpm failed to read fan speed");
Vec::new()
}
}
pub fn get_temperature(_: Vec<Primitive>) -> Vec<Primitive> {
if let Some(temperature) = crate::sys::read_thermal_zone(0) {
let real_temp = temperature as f64 / 1000.0;
log::debug!("get_temperature() success: {}", real_temp);
vec![real_temp.into()]
} else {
log::error!("get_fan_rpm failed to read fan speed");
Vec::new()
}
}
pub fn set_enable_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
let runtime_state = runtime.state_clone();
move |params| {
if let Some(Primitive::Bool(enabled)) = params.get(0) {
let mut settings = match runtime_settings.write() {
Ok(x) => x,
Err(e) => {
log::error!("set_enable failed to acquire settings write lock: {}", e);
return vec![];
}
};
if settings.enable != *enabled {
settings.enable = *enabled;
let mut state = match runtime_state.write() {
Ok(x) => x,
Err(e) => {
log::error!("set_enable failed to acquire state write lock: {}", e);
return vec![];
}
};
state.dirty = true;
log::debug!("set_enable({}) success", enabled);
}
vec![(*enabled).into()]
} else {
Vec::new()
impl FanService {
pub fn new(runtime: ControlRuntime) -> Self {
runtime.run();
Self {
ctrl: runtime,
}
}
}
pub fn get_enable_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
move |_| {
let lock = match runtime_settings.read() {
Ok(x) => x,
Err(e) => {
log::error!("get_enable failed to acquire settings read lock: {}", e);
return vec![];
}
};
log::debug!("get_enable() success");
vec![lock.enable.into()]
}
fn once_true() -> impl std::iter::Iterator<Item = bool> {
// iters over [true, false, false, ...]
std::iter::once(true).chain(std::iter::repeat(false))
}
pub fn set_interpolate_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
let runtime_state = runtime.state_clone();
move |params| {
if let Some(Primitive::Bool(enabled)) = params.get(0) {
let mut settings = match runtime_settings.write() {
Ok(x) => x,
Err(e) => {
log::error!("set_enable failed to acquire settings write lock: {}", e);
return vec![];
}
};
if settings.interpolate != *enabled {
settings.interpolate = *enabled;
let mut state = match runtime_state.write() {
Ok(x) => x,
Err(e) => {
log::error!("set_interpolate failed to acquire state write lock: {}", e);
return vec![];
}
};
state.dirty = true;
log::debug!("set_interpolate({}) success", enabled);
}
vec![(*enabled).into()]
} else {
Vec::new()
#[::nrpc::_helpers::async_trait::async_trait]
impl<'a> IFan<'a> for FanService {
async fn echo(
&mut self,
input: EchoMessage,
) -> Result<EchoMessage, Box<dyn std::error::Error + Send>> {
Ok(input)
}
}
}
pub fn get_interpolate_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
move |_| {
let lock = match runtime_settings.read() {
Ok(x) => x,
Err(e) => {
log::error!("get_interpolate failed to acquire settings read lock: {}", e);
return vec![];
}
};
log::debug!("get_interpolate() success");
vec![lock.interpolate.into()]
}
}
fn curve_to_json(curve: &Vec<super::datastructs::GraphPoint>) -> serde_json::Result<String> {
let mut curve_points = Vec::<GraphPointJson>::with_capacity(curve.len());
for point in curve.iter() {
curve_points.push(point.clone().into());
}
serde_json::to_string(&curve_points)
}
pub fn get_curve_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
move |_| {
let lock = match runtime_settings.read() {
Ok(x) => x,
Err(e) => {
log::error!("get_curve failed to acquire settings read lock: {}", e);
return vec![];
}
};
let json_str = match curve_to_json(&lock.curve) {
Ok(x) => x,
Err(e) => {
log::error!("get_curve failed to serialize points: {}", e);
return vec![];
}
};
log::debug!("get_curve() success");
vec![Primitive::Json(json_str)]
}
}
pub fn add_curve_point_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
let runtime_state = runtime.state_clone();
move |params| {
if let Some(Primitive::Json(json_str)) = params.get(0) {
let mut settings = match runtime_settings.write() {
Ok(x) => x,
Err(e) => {
log::error!("add_curve_point failed to acquire settings write lock: {}", e);
return vec![];
}
};
let new_point: GraphPointJson = match serde_json::from_str(&json_str) {
Ok(x) => x,
Err(e) => {
log::error!("add_curve_point failed deserialize point json: {}", e);
return vec![];
}
};
let version = settings.version;
settings.curve.push(super::datastructs::GraphPoint::from_json(new_point, version));
settings.sort_curve();
let mut state = match runtime_state.write() {
Ok(x) => x,
Err(e) => {
log::error!("add_curve_point failed to acquire state write lock: {}", e);
return vec![];
}
};
state.dirty = true;
let json_str = match curve_to_json(&settings.curve) {
Ok(x) => x,
Err(e) => {
log::error!("add_curve_point failed to serialize points: {}", e);
return vec![];
}
};
log::debug!("add_curve_point({}) success", json_str);
vec![Primitive::Json(json_str)]
} else {
Vec::new()
async fn hello(
&mut self,
input: NameMessage,
) -> Result<HelloResponse, Box<dyn std::error::Error + Send>> {
Ok(HelloResponse {
phrase: format!("Hello {}", input.name)
})
}
}
}
pub fn remove_curve_point_gen(runtime: &ControlRuntime) -> impl Fn(Vec<Primitive>) -> Vec<Primitive> {
let runtime_settings = runtime.settings_clone();
let runtime_state = runtime.state_clone();
move |params| {
if let Some(Primitive::F64(index)) = params.get(0) {
let mut settings = match runtime_settings.write() {
Ok(x) => x,
Err(e) => {
log::error!("remove_curve_point failed to acquire settings write lock: {}", e);
return vec![];
async fn version(
&mut self,
_input: Empty,
) -> Result<VersionMessage, Box<dyn std::error::Error + Send>> {
Ok(
VersionMessage {
major: 0,
minor: 0,
patch: 0,
//display: VERSION.to_string(),
}
};
let rounded = index.round();
if rounded >= 0.0 && rounded < settings.curve.len() as _ {
let index = rounded as usize;
settings.curve.swap_remove(index);
settings.sort_curve();
let mut state = match runtime_state.write() {
Ok(x) => x,
Err(e) => {
log::error!("remove_curve_point failed to acquire state write lock: {}", e);
return vec![];
}
};
state.dirty = true;
let json_str = match curve_to_json(&settings.curve) {
Ok(x) => x,
Err(e) => {
log::error!("remove_curve_point failed to serialize points: {}", e);
return vec![];
}
};
log::debug!("remove_curve_point({}) success", json_str);
vec![Primitive::Json(json_str)]
)
}
async fn version_str(
&mut self,
_input: Empty,
) -> Result<VersionDisplayMessage, Box<dyn std::error::Error + Send>> {
Ok(
VersionDisplayMessage {
display: VERSION.to_owned(),
}
)
}
async fn name(
&mut self,
_input: Empty,
) -> Result<NameMessage, Box<dyn std::error::Error + Send>> {
Ok(
NameMessage {
name: NAME.to_string(),
}
)
}
async fn get_fan_rpm<'b: 'a>(
&mut self,
_input: Empty,
) -> Result<
usdpl_back::nrpc::ServiceServerStream<'b, RpmMessage>,
Box<dyn std::error::Error + Send>,
> {
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::<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 {
log::error!("remove_curve_point received index out of bounds: {} indexing array of length {}", index, settings.curve.len());
return vec![];
Ok(Box::new(usdpl_back::nrpc::_helpers::futures::stream::empty()))
}
}
async fn get_temperature<'b: 'a>(
&mut self,
_input: Empty,
) -> Result<
usdpl_back::nrpc::ServiceServerStream<'b, TemperatureMessage>,
Box<dyn std::error::Error + Send>,
> {
let sensor_clone = self.ctrl.sensor_clone();
let stream = usdpl_back::nrpc::_helpers::futures::stream::iter(once_true()).then(move |is_first| {
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 |_| {
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::<dyn std::error::Error + Send + Sync>::from(format!("get_temperature failed to read sensor: {}", e))))
}
}
})
});
Ok(Box::new(stream))
}
async fn set_enable(
&mut self,
input: EnablementMessage,
) -> Result<EnablementMessage, Box<dyn std::error::Error + Send>>{
let mut settings = self.ctrl.settings().write().await;
if settings.enable != input.is_enabled {
let mut state = self.ctrl.state().write().await;
settings.enable = input.is_enabled;
state.dirty = true;
}
log::debug!("set_enable({}) success", input.is_enabled);
Ok(input)
}
async fn get_enable(
&mut self,
_input: Empty,
) -> Result<EnablementMessage, Box<dyn std::error::Error + Send>>{
let is_enabled = self.ctrl.settings().read().await.enable;
log::debug!("get_enable() success");
Ok(EnablementMessage { is_enabled })
}
async fn set_interpolate(
&mut self,
input: EnablementMessage,
) -> Result<EnablementMessage, Box<dyn std::error::Error + Send>>{
let mut settings = self.ctrl.settings().write().await;
if settings.interpolate != input.is_enabled {
let mut state = self.ctrl.state().write().await;
settings.interpolate = input.is_enabled;
state.dirty = true;
}
log::debug!("set_interpolate({}) success", input.is_enabled);
Ok(input)
}
async fn get_interpolate(
&mut self,
_input: Empty,
) -> Result<EnablementMessage, Box<dyn std::error::Error + Send>>{
let is_enabled = self.ctrl.settings().read().await.interpolate;
log::debug!("get_interpolate() success");
Ok(EnablementMessage { is_enabled })
}
async fn get_curve_x(
&mut self,
_input: Empty,
) -> Result<CurveMessageX, Box<dyn std::error::Error + Send>>{
let settings = self.ctrl.settings().read().await;
let x = settings.curve.iter().map(|p| p.x).collect();
log::debug!("get_curve_x() success");
Ok(CurveMessageX { x })
}
async fn get_curve_y(
&mut self,
_input: Empty,
) -> Result<CurveMessageY, Box<dyn std::error::Error + Send>>{
let settings = self.ctrl.settings().read().await;
let y = settings.curve.iter().map(|p| p.y).collect();
log::debug!("get_curve_x() success");
Ok(CurveMessageY { y })
}
async fn add_curve_point(
&mut self,
point: GraphPoint,
) -> Result<Empty, Box<dyn std::error::Error + Send>>{
let mut settings = self.ctrl.settings().write().await;
settings.curve.push(super::datastructs::GraphPoint {
x: point.x,
y: point.y
});
settings.sort_curve();
let mut state = self.ctrl.state().write().await;
state.dirty = true;
log::debug!("add_curve_point(point: {:?}) success", point);
Ok(Empty { ok: true })
}
async fn remove_curve_point(
&mut self,
input: IndexMessage,
) -> Result<Empty, Box<dyn std::error::Error + Send>>{
let mut settings = self.ctrl.settings().write().await;
let i = input.index as usize;
if settings.curve.len() > i {
settings.curve.swap_remove(i);
settings.sort_curve();
let mut state = self.ctrl.state().write().await;
state.dirty = true;
log::debug!("remove_curve_point(point: {}) success", input.index);
Ok(Empty { ok: true })
} else {
log::debug!("remove_curve_point(index: {}) failed", input.index);
Ok(Empty { ok: false })
}
} else {
Vec::new()
}
}
}

View file

@ -1,27 +1,36 @@
//! Fan control
use std::sync::{RwLock, Arc};
use std::sync::Arc;
//use std::collections::HashMap;
use tokio::sync::RwLock;
use std::thread;
use std::time::{Duration, Instant};
use super::datastructs::{Settings, State, GraphPoint};
use super::datastructs::{Settings, State};
use super::json::SettingsJson;
const VALVE_FAN_SERVICE: &str = "jupiter-fan-control.service";
pub struct ControlRuntime {
settings: Arc<RwLock<Settings>>,
state: Arc<RwLock<State>>,
fan_adapter: Arc<Box<dyn crate::adapters::FanAdapter + 'static>>,
sensor_adapter: Arc<Box<dyn crate::adapters::SensorAdapter + 'static>>,
}
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 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)),
fan_adapter: Arc::new(fan),
sensor_adapter: Arc::new(sensor),
}
}
@ -33,9 +42,27 @@ impl ControlRuntime {
self.state.clone()
}
pub(crate) fn settings(&self) -> &'_ RwLock<Settings> {
&self.settings
}
pub(crate) fn state(&self) -> &'_ RwLock<State> {
&self.state
}
pub(crate) fn sensor_clone(&self) -> Arc<Box<dyn crate::adapters::SensorAdapter>> {
self.sensor_adapter.clone()
}
pub(crate) fn fan_clone(&self) -> Arc<Box<dyn crate::adapters::FanAdapter>> {
self.fan_adapter.clone()
}
pub fn run(&self) -> thread::JoinHandle<()> {
let runtime_settings = self.settings_clone();
let runtime_state = self.state_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();
@ -44,233 +71,43 @@ impl ControlRuntime {
// resumed from sleep; do fan re-init
log::debug!("Detected resume from sleep, overriding fan again");
{
let state = match runtime_state.read() {
Ok(x) => x,
Err(e) => {
log::error!("runtime failed to acquire state read lock: {}", e);
continue;
}
};
let settings = match runtime_settings.read() {
Ok(x) => x,
Err(e) => {
log::error!("runtime failed to acquire settings read lock: {}", e);
continue;
}
};
let settings = runtime_settings.blocking_read();
if settings.enable {
Self::on_set_enable(&settings, &state);
runtime_fan.on_enable_toggled(&settings);
}
}
}
start_time = Instant::now();
{ // save to file
let state = match runtime_state.read() {
Ok(x) => x,
Err(e) => {
log::error!("runtime failed to acquire state read lock: {}", e);
continue;
}
};
let state = runtime_state.blocking_read();
if state.dirty {
// save settings to file
let settings = match runtime_settings.read() {
Ok(x) => x,
Err(e) => {
log::error!("runtime failed to acquire settings read lock: {}", e);
continue;
}
};
let settings = runtime_settings.blocking_read();
let settings_json: SettingsJson = settings.clone().into();
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_fan.on_enable_toggled(&settings);
drop(state);
let mut state = match runtime_state.write() {
Ok(x) => x,
Err(e) => {
log::error!("runtime failed to acquire state write lock: {}", e);
continue;
}
};
let mut state = runtime_state.blocking_write();
state.dirty = false;
}
}
{ // fan control
let settings = match runtime_settings.read() {
Ok(x) => x,
Err(e) => {
log::error!("runtime failed to acquire settings read lock: {}", e);
continue;
}
};
let settings = runtime_settings.blocking_read();
if settings.enable {
Self::enforce_jupiter_status(true);
Self::do_fan_control(&settings);
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) {
// 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(settings.enable) {
log::error!("runtime failed to write to fan recalculate file: {}", e);
}
}
fn do_fan_control(settings: &Settings) {
/*
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(0) {
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(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 {

View file

@ -2,7 +2,7 @@ use std::default::Default;
use std::convert::{Into, From};
use std::path::PathBuf;
use super::json::{SettingsJson, GraphPointJson, BoundsJson};
use super::json::{SettingsJson, GraphPointJson};
#[derive(Debug, Clone)]
pub struct Settings {
@ -10,8 +10,6 @@ pub struct Settings {
pub enable: bool,
pub interpolate: bool,
pub curve: Vec<GraphPoint>,
pub fan_bounds: Bounds<f64>,
pub temperature_bounds: Bounds<f64>,
}
impl Settings {
@ -28,42 +26,18 @@ impl From<SettingsJson> for Settings {
enable: other.enable,
interpolate: other.interpolate,
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 {
version: 1,
enable: other.enable,
interpolate: other.interpolate,
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 {
version: 1,
enable: other.enable,
interpolate: other.interpolate,
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();
@ -79,8 +53,6 @@ impl Into<SettingsJson> for Settings {
enable: self.enable,
interpolate: self.interpolate,
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)]
pub struct State {
pub home: PathBuf,

View file

@ -11,8 +11,6 @@ pub struct SettingsJson {
pub enable: bool,
pub interpolate: bool,
pub curve: Vec<GraphPointJson>,
pub fan_bounds: Option<BoundsJson>,
pub temperature_bounds: Option<BoundsJson>,
}
#[derive(Serialize, Deserialize)]
@ -21,12 +19,6 @@ pub struct GraphPointJson {
pub y: f64,
}
#[derive(Serialize, Deserialize)]
pub struct BoundsJson {
pub min: f64,
pub max: f64,
}
impl Default for SettingsJson {
fn default() -> Self {
Self {
@ -34,14 +26,6 @@ impl Default for SettingsJson {
enable: false,
interpolate: true,
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 adapters;
mod control;
mod datastructs;
mod json;
@ -6,7 +7,13 @@ mod sys;
use simplelog::{WriteLogger, LevelFilter};
use usdpl_back::Instance;
#[allow(missing_docs)]
#[allow(dead_code)]
pub mod services {
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
}
use services::fantastic::FanServer;
const PORT: u16 = 44444;
@ -20,9 +27,29 @@ fn main() -> Result<(), ()> {
log::info!("Starting back-end ({} v{})", api::NAME, api::VERSION);
println!("Starting back-end ({} v{})", api::NAME, api::VERSION);
let runtime = control::ControlRuntime::new();
runtime.run();
Instance::new(PORT)
usdpl_back::Server::new(PORT)
.register(FanServer::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()
.unwrap();
Ok(())
/*Instance::new(PORT)
.register("echo", api::echo)
.register("hello", api::hello)
.register("version", api::version)
@ -36,7 +63,7 @@ fn main() -> Result<(), ()> {
.register("get_curve", api::get_curve_gen(&runtime))
.register("add_curve_point", api::add_curve_point_gen(&runtime))
.register("remove_curve_point", api::remove_curve_point_gen(&runtime))
.run_blocking()
.run_blocking()*/
//Ok(())
//println!("Hello, world!");
}

View file

@ -1,19 +1 @@
use usdpl_back::api::files::*;
const HWMON_INDEX: usize = 5;
pub fn read_fan() -> Option<u64> {
read_single(format!("/sys/class/hwmon/hwmon{}/fan1_input", HWMON_INDEX)).ok()
}
pub fn read_thermal_zone(index: u8) -> Option<u64> {
read_single(format!("/sys/class/thermal/thermal_zone{}/temp", index)).ok()
}
pub fn write_fan_recalc(enabled: bool) -> Result<(), std::io::Error> {
write_single(format!("/sys/class/hwmon/hwmon{}/recalculate", HWMON_INDEX), enabled as u8)
}
pub fn write_fan_target(rpm: u64) -> Result<(), std::io::Error> {
write_single(format!("/sys/class/hwmon/hwmon{}/fan1_target", HWMON_INDEX), rpm)
}
pub const SYSFS_ROOT: &str = "/";

View file

@ -1,6 +1,6 @@
{
"name": "Fantastic",
"version": "0.4.0",
"version": "0.6.0-alpha1",
"description": "A template to quickly create decky plugins from scratch, based on TypeScript and webpack",
"scripts": {
"build": "shx rm -rf dist && rollup -c",
@ -32,17 +32,17 @@
"@rollup/plugin-replace": "^4.0.0",
"@rollup/plugin-typescript": "^8.5.0",
"@types/react": "16.14.0",
"@types/webpack": "^5.28.0",
"@types/webpack": "^5.28.5",
"rollup": "^2.79.1",
"rollup-plugin-import-assets": "^1.1.1",
"shx": "^0.3.4",
"tslib": "^2.5.0",
"tslib": "^2.6.2",
"typescript": "^4.9.5"
},
"dependencies": {
"decky-frontend-lib": "~3.19.1",
"react-icons": "^4.7.1",
"usdpl-front": "file:src/usdpl"
"decky-frontend-lib": "^3.25.0",
"fantastic-wasm": "file:src/rust/pkg",
"react-icons": "^4.12.0"
},
"pnpm": {
"peerDependencyRules": {

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,7 +1,15 @@
import {init_usdpl, target_usdpl, init_embedded, call_backend} from "usdpl-front";
//import {init_usdpl, target_usdpl, init_embedded, call_backend} from "usdpl-front";
import { init_embedded, target_usdpl } from "fantastic-wasm";
import { Fan } from "fantastic-wasm";
//@ts-ignore
//const Fan = {};
const USDPL_PORT: number = 44444;
var FAN_CLIENT: Fan | undefined = undefined;
// Utility
export function resolve(promise: Promise<any>, setter: any) {
@ -26,7 +34,7 @@ export function execute(promise: Promise<any[]>) {
export async function initBackend() {
// init usdpl
await init_embedded();
init_usdpl(USDPL_PORT);
FAN_CLIENT = new Fan(USDPL_PORT);
console.log("FANTASTIC: USDPL started for framework: " + target_usdpl());
//setReady(true);
}
@ -34,45 +42,62 @@ export async function initBackend() {
// Back-end functions
export async function setEnabled(value: boolean): Promise<boolean> {
return (await call_backend("set_enable", [value]))[0];
return (await FAN_CLIENT!.set_enable(value))?? value;
//return (await call_backend("set_enable", [value]))[0];
}
export async function getEnabled(): Promise<boolean> {
return (await call_backend("get_enable", []))[0];
return (await FAN_CLIENT!.get_enable(true)) ?? false;
}
export async function setInterpolate(value: boolean): Promise<boolean> {
return (await call_backend("set_interpolate", [value]))[0];
return (await FAN_CLIENT!.set_interpolate(value)) ?? value;
//return (await call_backend("set_interpolate", [value]))[0];
}
export async function getInterpolate(): Promise<boolean> {
return (await call_backend("get_interpolate", []))[0];
return (await FAN_CLIENT!.get_interpolate(true)) ?? false;
//return (await call_backend("get_interpolate", []))[0];
}
export async function getVersion(): Promise<string> {
return (await call_backend("version", []))[0];
return (await FAN_CLIENT!.version_str(true)) ?? "version";
//return (await call_backend("version", []))[0];
}
export async function getName(): Promise<string> {
return (await call_backend("name", []))[0];
return (await FAN_CLIENT!.name(true))?? "broken";
//return (await call_backend("name", []))[0];
}
export async function getCurve(): Promise<{"x": number, "y": number}[]> {
return (await call_backend("get_curve", []))[0];
var x_s = (await FAN_CLIENT!.get_curve_x(true))?? [];
var y_s = (await FAN_CLIENT!.get_curve_y(true))?? [];
let result: {"x": number, "y": number}[] = [];
for (let i = 0; i < x_s.length && i < y_s.length; i++) {
result.push({
x: x_s[i],
y: y_s[i],
});
}
return result;
}
export async function addCurvePoint(point: {"x": number, "y": number}): Promise<{"x": number, "y": number}[]> {
return (await call_backend("add_curve_point", [point]))[0];
await FAN_CLIENT!.add_curve_point(point.x, point.y);
return getCurve();
}
export async function removeCurvePoint(index: number): Promise<{"x": number, "y": number}[]> {
return (await call_backend("remove_curve_point", [index]))[0];
await FAN_CLIENT!.remove_curve_point(index);
return getCurve();
//return (await call_backend("remove_curve_point", [index]))[0];
}
export async function getFanRpm(): Promise<number> {
return (await call_backend("get_fan_rpm", []))[0];
export async function getFanRpm(callback: (rpm: number) => void): Promise<void> {
return (await FAN_CLIENT!.get_fan_rpm(true, callback));
}
export async function getTemperature(): Promise<number> {
return (await call_backend("get_temperature", []))[0];
export async function getTemperature(callback: (temp: number) => void): Promise<void> {
return (await FAN_CLIENT!.get_temperature(true, callback));
}

View file

@ -1,17 +1,19 @@
import {
definePlugin,
DialogButton,
PanelSection,
PanelSectionRow,
Field,
ServerAPI,
ToggleField,
staticClasses,
Navigation,
} from "decky-frontend-lib";
import { VFC, useState } from "react";
import { FaFan } from "react-icons/fa";
import { SiOnlyfans } from "react-icons/si";
import { version_usdpl } from "fantastic-wasm";
import * as backend from "./backend";
import {Canvas} from "./canvas";
@ -22,10 +24,15 @@ var usdplReady: boolean = false;
var name: string = "";
var version: string = "";
var egg = 0;
var curve_backup: {x: number, y: number}[] = [];
var tempCache: number = -1337;
var setTemperature_display = (_: number) => {};
var fanRpmCache: number = -273;
var setFanRpm_display = (_: number) => {};
const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
// const [result, setResult] = useState<number | undefined>();
@ -53,21 +60,30 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
curve_backup = value;
}
const [temperatureGlobal, setTemperature] = useState<number>(-273.15);
const [fanRpmGlobal, setFanRpm] = useState<number>(-1337);
const [temperatureGlobal, setTemperature] = useState<number>(tempCache);
const [fanRpmGlobal, setFanRpm] = useState<number>(fanRpmCache);
setTemperature_display = (x) => {
setTemperature(x);
tempCache = x;
};
setFanRpm_display = (x) => {
setFanRpm(x);
fanRpmCache = x;
};
function setEnable(enable: boolean) {
setEnableInternal(enable);
}
function onClickCanvas(e: any) {
//console.log("canvas click", e);
//console.log("[FANTASTIC] canvas click", e);
const realEvent: any = e.nativeEvent;
//console.log("Canvas click @ (" + realEvent.layerX.toString() + ", " + realEvent.layerY.toString() + ")");
const target: any = e.currentTarget;
//console.log("Target dimensions " + target.width.toString() + "x" + target.height.toString());
const clickX = realEvent.layerX;
const clickY = realEvent.layerY;
//console.log("[FANTASTIC] Target dimensions " + target.width.toString() + "x" + target.height.toString());
var clickX = realEvent.offsetX;
var clickY = realEvent.offsetY;
//console.debug("[FANTASTIC] curve click:", clickX, clickY);
for (let i = 0; i < curveGlobal.length; i++) {
const curvePoint = curveGlobal[i];
const pointX = curvePoint.x * target.width;
@ -84,7 +100,8 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
}
}
//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 {
@ -205,23 +222,27 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
backend.resolve(backend.getEnabled(), setEnable);
backend.resolve(backend.getInterpolate(), setInterpol);
backend.resolve(backend.getCurve(), setCurve);
backend.resolve(backend.getTemperature(), setTemperature);
backend.resolve(backend.getFanRpm(), setFanRpm);
backend.resolve(backend.getTemperature(setTemperature_display), (_: any) => {});
backend.resolve(backend.getFanRpm(setFanRpm_display), (_: any) => {});
if (periodicHook != null) {
clearInterval(periodicHook);
}
periodicHook = setInterval(function() {
/*periodicHook = setInterval(function() {
backend.resolve(backend.getTemperature(), setTemperature);
backend.resolve(backend.getFanRpm(), setFanRpm);
}, 1000);
}, 1000);*/
}
if (!usdplReady) {
return (
<PanelSection>
</PanelSection>
<PanelSectionRow>
<Field
label="Loading...">
If you can read this, something probably went wrong :(
</Field>
</PanelSectionRow>
);
}
@ -285,37 +306,32 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
<PanelSectionRow>
<Field
label={name}
onClick={()=> { egg++; }}>
{egg % 10 == 9 ? "by NGnius" : "v" + version}
onClick={()=> { Navigation.NavigateToExternalWeb("https://git.ngni.us/NG-SD-Plugins/Fantastic/releases"); }}>
{"v" + version}
</Field>
</PanelSectionRow>
{ (version?.includes("alpha") || version?.includes("beta")) && <PanelSectionRow>
<Field
label="USDPL"
onClick={()=> { Navigation.NavigateToExternalWeb("https://git.ngni.us/NG-SD-Plugins/usdpl-rs"); }}>
v{version_usdpl()}
</Field>
</PanelSectionRow>}
</PanelSection>
);
};
const DeckyPluginRouterTest: VFC = () => {
return (
<div style={{ marginTop: "50px", color: "white" }}>
Hello World!
<DialogButton onClick={() => {}}>
Go to Store
</DialogButton>
</div>
);
};
(async function(){
if (!usdplReady) {
await backend.initBackend();
usdplReady = true;
backend.getEnabled();
name = await backend.getName();
version = await backend.getVersion();
}
})();
export default definePlugin((serverApi: ServerAPI) => {
serverApi.routerHook.addRoute("/decky-plugin-test", DeckyPluginRouterTest, {
exact: true,
});
(async function(){
await backend.initBackend();
usdplReady = true;
backend.getEnabled();
name = await backend.getName();
version = await backend.getVersion();
})();
let ico = <FaFan />;
let now = new Date();
@ -329,7 +345,6 @@ export default definePlugin((serverApi: ServerAPI) => {
icon: ico,
onDismount() {
clearInterval(periodicHook!);
serverApi.routerHook.removeRoute("/decky-plugin-test");
},
};
});

981
src/rust/Cargo.lock generated Normal file
View file

@ -0,0 +1,981 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "bumpalo"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fantastic-wasm"
version = "0.5.0"
dependencies = [
"nrpc",
"prost",
"usdpl-build",
"usdpl-front",
]
[[package]]
name = "fastrand"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
[[package]]
name = "futures-task"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
[[package]]
name = "futures-util"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gloo-net"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"http",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "http"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "linux-raw-sys"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "logos"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-codegen"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68"
dependencies = [
"beef",
"fnv",
"proc-macro2",
"quote",
"regex-syntax 0.6.29",
"syn 2.0.29",
]
[[package]]
name = "logos-derive"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e"
dependencies = [
"logos-codegen",
]
[[package]]
name = "memchr"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
[[package]]
name = "miette"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
dependencies = [
"miette-derive",
"once_cell",
"thiserror",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "multimap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "nrpc"
version = "1.0.0"
dependencies = [
"bytes",
"futures",
"prost",
]
[[package]]
name = "nrpc-build"
version = "1.0.0"
dependencies = [
"nrpc",
"prettyplease 0.2.12",
"proc-macro2",
"prost-build",
"prost-types",
"protox",
"quote",
"syn 2.0.29",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "petgraph"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "pin-project"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "prettyplease"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
dependencies = [
"proc-macro2",
"syn 1.0.109",
]
[[package]]
name = "prettyplease"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62"
dependencies = [
"proc-macro2",
"syn 2.0.29",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
dependencies = [
"bytes",
"heck",
"itertools",
"lazy_static",
"log",
"multimap",
"petgraph",
"prettyplease 0.1.25",
"prost",
"prost-types",
"regex",
"syn 1.0.109",
"tempfile",
"which",
]
[[package]]
name = "prost-derive"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "prost-reflect"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b823de344848e011658ac981009100818b322421676740546f8b52ed5249428"
dependencies = [
"logos",
"miette",
"once_cell",
"prost",
"prost-types",
]
[[package]]
name = "prost-types"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
dependencies = [
"prost",
]
[[package]]
name = "protox"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24022a7eb88547eaba87a1db7954c9c4cb4a143565c4e8f2b7f3e76eed63db2d"
dependencies = [
"bytes",
"miette",
"prost",
"prost-reflect",
"prost-types",
"protox-parse",
"thiserror",
]
[[package]]
name = "protox-parse"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712a2a651fa4466e67df6c967df5d7fc6cbffac89afc7b834f97ec49846c9c11"
dependencies = [
"logos",
"miette",
"prost-types",
"thiserror",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax 0.7.5",
]
[[package]]
name = "regex-automata"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "rustix"
version = "0.38.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453"
dependencies = [
"bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "serde_json"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "usdpl-build"
version = "1.0.0"
dependencies = [
"nrpc-build",
"prettyplease 0.2.12",
"proc-macro2",
"prost-build",
"prost-types",
"quote",
"syn 2.0.29",
]
[[package]]
name = "usdpl-core"
version = "1.0.0"
dependencies = [
"base64",
]
[[package]]
name = "usdpl-front"
version = "1.0.0"
dependencies = [
"console_error_panic_hook",
"futures",
"futures-channel",
"gloo-net",
"js-sys",
"log",
"nrpc",
"prost",
"usdpl-core",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.29",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "web-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "which"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
dependencies = [
"either",
"libc",
"once_cell",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

33
src/rust/Cargo.toml Normal file
View file

@ -0,0 +1,33 @@
[package]
name = "fantastic-wasm"
version = "0.5.0"
edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Frontend bindings for fan control functionality"
license = "GPL-3.0-only"
repository = "https://git.ngni.us/NG-SD-Plugins/Fantastic"
keywords = ["utility", "fan-control", "root", "decky"]
readme = "../../README.md"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
usdpl-front = { version = "1.0", path = "../../../usdpl-rs/usdpl-front" }
nrpc = { version = "1.0", path = "../../../nRPC/nrpc", default-features = false }
prost = "0.11"
[build-dependencies]
usdpl-build = { version = "1.0", path = "../../../usdpl-rs/usdpl-build" }
[features]
debug = ["usdpl-front/debug"]
decky = ["usdpl-front/decky"]
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
debug = false
strip = true
lto = true
codegen-units = 4

15
src/rust/build.rs Normal file
View file

@ -0,0 +1,15 @@
fn main() {
println!("CWD: {}", std::env::current_dir().unwrap().display());
usdpl_build::front::build(
[format!(
"{}/../../backend-rs/protos/fantastic.proto",
std::env::current_dir().unwrap().display()
)]
.into_iter(),
[format!(
"{}/../../backend-rs/protos/",
std::env::current_dir().unwrap().display()
)]
.into_iter(),
)
}

19
src/rust/build.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
if [ -n "$1" ]; then
if [ "$1" == "--help" ]; then
echo "Usage:
$0 [decky|crankshaft|<nothing>]"
exit 0
elif [ "$1" == "decky" ]; then
echo "Building WASM module for decky framework"
RUSTFLAGS="--cfg aes_compact" wasm-pack build --target web --features decky,$2
else
echo "Unsupported plugin framework \`$1\`"
exit 1
fi
else
echo "WARNING: Building for any plugin framework, which may not work for every framework"
RUSTFLAGS="--cfg aes_compact" wasm-pack build --target web --features debug,$2
fi
python3 ./scripts/generate_embedded_wasm.py

View file

@ -0,0 +1,45 @@
import base64
if __name__ == "__main__":
print("Embedding WASM into js")
# assumption: current working directory (relative to this script) is ../
# assumption: release wasm binary at ./pkg/usdpl_bg.wasm
with open("./pkg/fantastic_wasm_bg.wasm", mode="rb") as infile:
with open("./pkg/fantastic_wasm.js", mode="ab") as outfile:
outfile.write("\n\n// USDPL customization\nconst encoded = \"".encode())
encoded = base64.b64encode(infile.read())
outfile.write(encoded)
outfile.write("\";\n\n".encode())
outfile.write(
"""function asciiToBinary(str) {
if (typeof atob === 'function') {
return atob(str)
} else {
return new Buffer(str, 'base64').toString('binary');
}
}
function decode() {
var binaryString = asciiToBinary(encoded);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return (async function() {
return new Response(bytes.buffer, {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'application/wasm'
}
});
})();
}
export function init_embedded() {
return __wbg_init(decode())
}
""".encode())
with open("./pkg/fantastic_wasm.d.ts", "a") as outfile:
outfile.write("\n\n// USDPL customization\nexport function init_embedded();\n")
print("Done: Embedded WASM into js")

7
src/rust/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
#[allow(missing_docs)]
#[allow(dead_code)]
pub mod services {
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
}
pub use usdpl_front;

View file

@ -1,9 +0,0 @@
[![Crates.io](https://img.shields.io/crates/v/usdpl-front?style=flat-square)](https://crates.io/crates/usdpl-front)
# usdpl-front-front
Front-end library to be called from Javascript.
Targets WASM.
In true Javascript tradition, this part of the library does not support error handling.

View file

@ -1,21 +0,0 @@
{
"name": "usdpl-front",
"collaborators": [
"NGnius (Graham) <ngniusness@gmail.com>"
],
"description": "Universal Steam Deck Plugin Library front-end designed for WASM",
"version": "0.10.0",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "https://github.com/NGnius/usdpl-rs"
},
"files": [
"usdpl_front_bg.wasm",
"usdpl_front.js",
"usdpl_front.d.ts"
],
"module": "usdpl_front.js",
"types": "usdpl_front.d.ts",
"sideEffects": false
}

View file

@ -1,13 +0,0 @@
#!/bin/bash
git clone https://github.com/NGnius/usdpl-rs usdpl-rs
cd usdpl-rs/usdpl-front/
./build.sh $1 $2
cd ../..
cp -f ./usdpl-rs/usdpl-front/pkg/* ./
#rm ./.gitignore
rm -rf ./usdpl-rs

View file

@ -1,105 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Initialize the front-end library
* @param {number} port
*/
export function init_usdpl(port: number): void;
/**
* Get the targeted plugin framework, or "any" if unknown
* @returns {string}
*/
export function target_usdpl(): string;
/**
* Get the UDSPL front-end version
* @returns {string}
*/
export function version_usdpl(): string;
/**
* Get the targeted plugin framework, or "any" if unknown
* @param {string} key
* @param {any} value
* @returns {any}
*/
export function set_value(key: string, value: any): any;
/**
* Get the targeted plugin framework, or "any" if unknown
* @param {string} key
* @returns {any}
*/
export function get_value(key: string): any;
/**
* Call a function on the back-end.
* Returns null (None) if this fails for any reason.
* @param {string} name
* @param {any[]} parameters
* @returns {Promise<any>}
*/
export function call_backend(name: string, parameters: any[]): Promise<any>;
/**
* Initialize translation strings for the front-end
* @param {string} locale
* @returns {Promise<void>}
*/
export function init_tr(locale: string): Promise<void>;
/**
* Translate a phrase, equivalent to tr_n(msg_id, 0)
* @param {string} msg_id
* @returns {string}
*/
export function tr(msg_id: string): string;
/**
* Translate a phrase, retrieving the plural form for `n` items
* @param {string} msg_id
* @param {number} n
* @returns {string}
*/
export function tr_n(msg_id: string, n: number): string;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly init_usdpl: (a: number) => void;
readonly target_usdpl: (a: number) => void;
readonly version_usdpl: (a: number) => void;
readonly set_value: (a: number, b: number, c: number) => number;
readonly get_value: (a: number, b: number) => number;
readonly call_backend: (a: number, b: number, c: number, d: number) => number;
readonly init_tr: (a: number, b: number) => number;
readonly tr: (a: number, b: number, c: number) => void;
readonly tr_n: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_export_0: (a: number) => number;
readonly __wbindgen_export_1: (a: number, b: number, c: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_export_3: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_export_4: (a: number, b: number) => void;
readonly __wbindgen_export_5: (a: number) => void;
readonly __wbindgen_export_6: (a: number, b: number, c: number, d: number) => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {SyncInitInput} module
*
* @returns {InitOutput}
*/
export function initSync(module: SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
// USDPL customization
export function init_embedded();

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -1,20 +0,0 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export function init_usdpl(a: number): void;
export function target_usdpl(a: number): void;
export function version_usdpl(a: number): void;
export function set_value(a: number, b: number, c: number): number;
export function get_value(a: number, b: number): number;
export function call_backend(a: number, b: number, c: number, d: number): number;
export function init_tr(a: number, b: number): number;
export function tr(a: number, b: number, c: number): void;
export function tr_n(a: number, b: number, c: number, d: number): void;
export function __wbindgen_export_0(a: number): number;
export function __wbindgen_export_1(a: number, b: number, c: number): number;
export const __wbindgen_export_2: WebAssembly.Table;
export function __wbindgen_export_3(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_export_4(a: number, b: number): void;
export function __wbindgen_export_5(a: number): void;
export function __wbindgen_export_6(a: number, b: number, c: number, d: number): void;