Update UI to use new back-end, complete #22 #23 #27, paves the way for #21

This commit is contained in:
NGnius (Graham) 2022-08-31 20:18:15 -04:00
parent 9590a90722
commit 1538f9a862
25 changed files with 726 additions and 482 deletions

View file

@ -15,95 +15,6 @@ You will need that installed for this plugin to work.
- Display supplementary battery info - Display supplementary battery info
- Keep settings between restarts (stored in `~/.config/powertools/<appid>.json`) - Keep settings between restarts (stored in `~/.config/powertools/<appid>.json`)
## Cool, but that's too much work
Fair enough.
In case you still want some of the functionality, without the nice GUI, here's some equivalent commands.
These should all be run as superuser, i.e. run `sudo su` and then run these commands in that.
### Enable & Disable CPU threads
Enable: `echo 1 > /sys/devices/system/cpu/cpu{cpu_number}/online` where `{cpu_number}` is a number from 1 to 7 (inclusive).
Disable: `echo 0 > /sys/devices/system/cpu/cpu{cpu_number}/online` where `{cpu_number}` is a number from 1 to 7 (inclusive).
NOTE: You cannot enable or disable cpu0, hence why there are only 7 in the range for 8 cpu threads.
### Enable & Disable CPU boost
Enable: `echo 1 > /sys/devices/system/cpu/cpufreq/boost` enables boost across all threads.
Disable: `echo 0 > /sys/devices/system/cpu/cpufreq/boost` disables boost across all threads.
### Set CPU frequency
Use `cpupower` (usage: `cpupower --help`).
This isn't strictly how PowerTools does it, but it's a multi-step process which can involve changing the CPU governor.
All that can be done automatically by `cpupower frequency-set --freq {frequency}` where `{frequency}` is `1.7G`, `2.4G` or `2.8G`.
### Set GPU Power
Set Slow Powerplay Table (PPT):`echo {microwatts} > /sys/class/hwmon/hwmon4/power1_cap` where `{microwatts}` is a wattage in millionths of a Watt. This doesn't seem to do a lot.
Set Fast Powerplay Table (PPT): `echo {microwatts} > /sys/class/hwmon/hwmon4/power2_cap` where `{microwatts}` is a wattage in millionths of a Watt.
Get the entry limits for those two commands with `cat /sys/class/hwmon/hwmon4/power{number}_cap_max` where `{number}` is `1` (slowPPT) or `2` (fastPPT).
### Set Fan speed
NOTE: PowerTools no longer supports this, since [Fantastic](https://github.com/NGnius/Fantastic) does it much better.
Enable automatic control: `echo 0 > /sys/class/hwmon/hwmon5/recalculate` enables automatic fan control.
Disable automatic control: `echo 1 > /sys/class/hwmon/hwmon5/recalculate` disables automatic (temperature-based) fan control and starts using the set fan target instead.
Set the fan speed: `echo {rpm} > /sys/class/hwmon/hwmon5/fan1_target` where `{rpm}` is the RPM.
Read the actual fan RPM: `cat /sys/class/hwmon/hwmon5/fan1_input` gives the fan speed.
NOTE: There's a bug in the fan controller; if you enable automatic fan control it will forget any previously-set target despite it appearing to be set correctly (i.e. `cat /sys/class/hwmon/hwmon5/fan1_target` will display the correct value).
When you disable automatic fan control, you will need to set the fan RPM again.
### Battery stats
Get the battery charge right now: `cat /sys/class/hwmon/hwmon2/device/charge_now` gives charge in uAh (uAh * 7.7/1000000 = charge in Wh).
Get the maximum battery capacity: `cat /sys/class/hwmon/hwmon2/device/charge_full` gives charge in uAh.
Get the design battery capacity: `cat /sys/class/hwmon/hwmon2/device/charge_full_design` gives charge in uAh.
Get whether the deck is plugged in: `cat /sys/class/hwmon/hwmon5/curr1_input` gives the charger current in mA.
NOTE: 7.7 is the voltage of the battery -- it's not just a magic number.
### Steam Deck kernel patches
This is how I figured out how the fan stuff works.
I've only scratched the surface of what this code allows, I'm sure it has more useful information.
https://lkml.org/lkml/2022/2/5/391
### Game launch detection
```typescript
//@ts-ignore
let lifetimeHook = SteamClient.GameSessions.RegisterForAppLifetimeNotifications((update) => {
if (update.bRunning) {
console.log("AppID " + update.unAppID.toString() + " is now running");
} else {
console.log("AppID " + update.unAppID.toString() + " is no longer running");
// game exit code here
// NOTE: custom games always have 0 as AppID, so AppID is bad to use as ID
}
});
//@ts-ignore
let startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => {
//@ts-ignore
let gameInfo: any = appStore.GetAppOverviewByGameID(id);
// game start code here
// NOTE: GameID (variable: id) is always unique, even for custom games, so it's better to use than AppID
});
```
## License ## License
This is licensed under GNU GPLv3. This is licensed under GNU GPLv3.

View file

@ -13,4 +13,4 @@ class Plugin:
# startup # startup
self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"]) self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"])
while True: while True:
asyncio.sleep(1) await asyncio.sleep(1)

View file

@ -1,6 +1,6 @@
{ {
"name": "PowerTools", "name": "PowerTools",
"version": "0.7.0", "version": "1.0.0-alpha",
"description": "Power tweaks for power users", "description": "Power tweaks for power users",
"scripts": { "scripts": {
"build": "shx rm -rf dist && rollup -c", "build": "shx rm -rf dist && rollup -c",

View file

@ -1,7 +1,7 @@
{ {
"name": "PowerTools", "name": "PowerTools",
"author": "NGnius", "author": "NGnius",
"flags": ["root", "_debug"], "flags": ["root", "debug"],
"publish": { "publish": {
"discord_id": "106537989684887552", "discord_id": "106537989684887552",
"description": "Power tweaks for power users", "description": "Power tweaks for power users",

View file

@ -1054,8 +1054,6 @@ dependencies = [
[[package]] [[package]]
name = "usdpl-back" name = "usdpl-back"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbbc0781e83ba990f8239142e33173a2d2548701775f3db66702d1af4fd0319a"
dependencies = [ dependencies = [
"bytes", "bytes",
"hex", "hex",
@ -1068,8 +1066,6 @@ dependencies = [
[[package]] [[package]]
name = "usdpl-core" name = "usdpl-core"
version = "0.6.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862153581fac266458521f49e5906a71c1eee1665cb4c7d71e9586bd34b45394"
dependencies = [ dependencies = [
"aes-gcm-siv", "aes-gcm-siv",
"base64", "base64",

View file

@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
usdpl-back = { version = "0.6.0", features = ["blocking"]} usdpl-back = { version = "0.6.0", features = ["blocking"], path = "../../usdpl-rs/usdpl-back"}
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View file

@ -1,5 +1,10 @@
#!/bin/bash #!/bin/bash
cargo build --release --target x86_64-unknown-linux-musl #cargo build --release --target x86_64-unknown-linux-musl
mkdir ../bin &> /dev/null cargo build --target x86_64-unknown-linux-musl
cp ./target/release/powertools-rs ../bin/backend #cross build --release
mkdir -p ../bin
#cp ./target/x86_64-unknown-linux-musl/release/powertools-rs ../bin/backend
cp ./target/x86_64-unknown-linux-musl/debug/powertools-rs ../bin/backend
#cp ./target/release/powertools-rs ../bin/backend

View file

@ -18,10 +18,12 @@ pub fn set_persistent(
unwrap_lock(saver.lock(), "save channel").send(()), unwrap_lock(saver.lock(), "save channel").send(()),
"Failed to send on save channel", "Failed to send on save channel",
); );
super::utility::map_empty_result( let result = super::utility::map_empty_result(
settings_lock.on_set(), settings_lock.on_set(),
settings_lock.persistent, settings_lock.persistent,
) );
log::debug!("Persistent is now {}", settings_lock.persistent);
result
} else { } else {
vec!["set_persistent missing parameter".into()] vec!["set_persistent missing parameter".into()]
} }
@ -46,9 +48,14 @@ pub fn load_settings(
move |params_in: super::ApiParameterType| { move |params_in: super::ApiParameterType| {
if let Some(Primitive::String(path)) = params_in.get(0) { if let Some(Primitive::String(path)) = params_in.get(0) {
if let Some(Primitive::String(name)) = params_in.get(1) { if let Some(Primitive::String(name)) = params_in.get(1) {
super::utility::map_result( match settings.load_file(path.into(), name.to_owned()) {
settings.load_file(path.into(), name.to_owned()) Err(e) => vec![e.msg.into()],
) Ok(success) =>
super::utility::map_empty_result(
settings.clone().on_set(),
success
)
}
} else { } else {
vec!["load_settings missing name parameter".into()] vec!["load_settings missing name parameter".into()]
} }
@ -59,6 +66,24 @@ pub fn load_settings(
} }
} }
/// Generate load default settings from file web method
pub fn load_default_settings(
settings: Settings,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
move |_: super::ApiParameterType| {
match settings.load_file(
crate::consts::DEFAULT_SETTINGS_FILE.into(),
crate::consts::DEFAULT_SETTINGS_NAME.to_owned()
) {
Err(e) => vec![e.msg.into()],
Ok(success) => super::utility::map_empty_result(
settings.clone().on_set(),
success
)
}
}
}
/// Generate get current settings name /// Generate get current settings name
pub fn get_name( pub fn get_name(
settings: Arc<Mutex<General>>, settings: Arc<Mutex<General>>,
@ -71,3 +96,16 @@ pub fn get_name(
.into()] .into()]
} }
} }
/// Generate get current settings name
pub fn lock_unlock_all(
settings: Settings,
) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType {
move |_: super::ApiParameterType| {
let _lock = unwrap_lock(settings.general.lock(), "general");
let _lock = unwrap_lock(settings.cpus.lock(), "cpus");
let _lock = unwrap_lock(settings.gpu.lock(), "gpu");
let _lock = unwrap_lock(settings.battery.lock(), "battery");
vec![true.into()]
}
}

View file

@ -3,19 +3,27 @@ use usdpl_back::core::serdes::Primitive;
use crate::settings::SettingError; use crate::settings::SettingError;
#[inline]
pub fn map_result<T: Into<Primitive>>(result: Result<T, SettingError>) -> super::ApiParameterType { pub fn map_result<T: Into<Primitive>>(result: Result<T, SettingError>) -> super::ApiParameterType {
match result { match result {
Ok(val) => vec![val.into()], Ok(val) => vec![val.into()],
Err(e) => vec![e.msg.into()], Err(e) => {
log::debug!("Mapping error to primitive: {}", e);
vec![e.msg.into()]
},
} }
} }
#[inline]
pub fn map_empty_result<T: Into<Primitive>>( pub fn map_empty_result<T: Into<Primitive>>(
result: Result<(), SettingError>, result: Result<(), SettingError>,
success: T, success: T,
) -> super::ApiParameterType { ) -> super::ApiParameterType {
match result { match result {
Ok(_) => vec![success.into()], Ok(_) => vec![success.into()],
Err(e) => vec![e.msg.into()], Err(e) => {
log::debug!("Mapping error to primitive: {}", e);
vec![e.msg.into()]
},
} }
} }

View file

@ -4,3 +4,4 @@ pub const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
pub const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_SETTINGS_FILE: &str = "default_settings.json"; pub const DEFAULT_SETTINGS_FILE: &str = "default_settings.json";
pub const DEFAULT_SETTINGS_NAME: &str = "Default";

View file

@ -9,6 +9,8 @@ mod resume_worker;
mod save_worker; mod save_worker;
mod utility; mod utility;
use settings::OnSet;
use simplelog::{LevelFilter, WriteLogger}; use simplelog::{LevelFilter, WriteLogger};
use usdpl_back::core::serdes::Primitive; use usdpl_back::core::serdes::Primitive;
@ -32,118 +34,124 @@ fn main() -> Result<(), ()> {
log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION);
println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION);
let default_settings = persist::SettingsJson::open(settings_dir().join(DEFAULT_SETTINGS_FILE)) let mut loaded_settings = persist::SettingsJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE))
.map(|settings| settings::Settings::from_json(settings, DEFAULT_SETTINGS_FILE.into())) .map(|settings| settings::Settings::from_json(settings, DEFAULT_SETTINGS_FILE.into()))
.unwrap_or_else(|_| settings::Settings::system_default(DEFAULT_SETTINGS_FILE.into())); .unwrap_or_else(|_| settings::Settings::system_default(DEFAULT_SETTINGS_FILE.into()));
log::debug!("Settings: {:?}", default_settings); log::debug!("Settings: {:?}", loaded_settings);
let (_save_handle, save_sender) = save_worker::spawn(default_settings.clone()); let (_save_handle, save_sender) = save_worker::spawn(loaded_settings.clone());
let _resume_handle = resume_worker::spawn(default_settings.clone()); let _resume_handle = resume_worker::spawn(loaded_settings.clone());
if let Err(e) = loaded_settings.on_set() {
log::error!("Startup Settings.on_set() error: {}", e);
}
Instance::new(PORT) Instance::new(PORT)
.register("hello", |_: Vec<Primitive>| { .register("V_INFO", |_: Vec<Primitive>| {
vec![format!("Hello {}", PACKAGE_NAME).into()] vec![format!("{} v{}", PACKAGE_NAME, PACKAGE_VERSION).into()]
}) })
// battery API functions // battery API functions
.register("BATTERY_current_now", api::battery::current_now) .register("BATTERY_current_now", api::battery::current_now)
.register( .register(
"BATTERY_set_charge_rate", "BATTERY_set_charge_rate",
api::battery::set_charge_rate(default_settings.battery.clone(), save_sender.clone()), api::battery::set_charge_rate(loaded_settings.battery.clone(), save_sender.clone()),
) )
.register( .register(
"BATTERY_get_charge_rate", "BATTERY_get_charge_rate",
api::battery::get_charge_rate(default_settings.battery.clone()), api::battery::get_charge_rate(loaded_settings.battery.clone()),
) )
.register( .register(
"BATTERY_unset_charge_rate", "BATTERY_unset_charge_rate",
api::battery::unset_charge_rate(default_settings.battery.clone(), save_sender.clone()), api::battery::unset_charge_rate(loaded_settings.battery.clone(), save_sender.clone()),
) )
// cpu API functions // cpu API functions
.register("CPU_count", api::cpu::max_cpus) .register("CPU_count", api::cpu::max_cpus)
.register( .register(
"CPU_set_online", "CPU_set_online",
api::cpu::set_cpu_online(default_settings.cpus.clone(), save_sender.clone()) api::cpu::set_cpu_online(loaded_settings.cpus.clone(), save_sender.clone())
) )
.register( .register(
"CPU_get_onlines", "CPU_get_onlines",
api::cpu::get_cpus_online(default_settings.cpus.clone()) api::cpu::get_cpus_online(loaded_settings.cpus.clone())
) )
.register( .register(
"CPU_set_clock_limits", "CPU_set_clock_limits",
api::cpu::set_clock_limits(default_settings.cpus.clone(), save_sender.clone()) api::cpu::set_clock_limits(loaded_settings.cpus.clone(), save_sender.clone())
) )
.register( .register(
"CPU_get_clock_limits", "CPU_get_clock_limits",
api::cpu::get_clock_limits(default_settings.cpus.clone()) api::cpu::get_clock_limits(loaded_settings.cpus.clone())
) )
.register( .register(
"CPU_unset_clock_limits", "CPU_unset_clock_limits",
api::cpu::unset_clock_limits(default_settings.cpus.clone(), save_sender.clone()) api::cpu::unset_clock_limits(loaded_settings.cpus.clone(), save_sender.clone())
) )
.register( .register(
"CPU_set_governor", "CPU_set_governor",
api::cpu::set_cpu_governor(default_settings.cpus.clone(), save_sender.clone()) api::cpu::set_cpu_governor(loaded_settings.cpus.clone(), save_sender.clone())
) )
.register( .register(
"CPU_get_governors", "CPU_get_governors",
api::cpu::get_cpu_governors(default_settings.cpus.clone()) api::cpu::get_cpu_governors(loaded_settings.cpus.clone())
) )
// gpu API functions // gpu API functions
.register( .register(
"GPU_set_ppt", "GPU_set_ppt",
api::gpu::set_ppt(default_settings.gpu.clone(), save_sender.clone()) api::gpu::set_ppt(loaded_settings.gpu.clone(), save_sender.clone())
) )
.register( .register(
"GPU_get_ppt", "GPU_get_ppt",
api::gpu::get_ppt(default_settings.gpu.clone()) api::gpu::get_ppt(loaded_settings.gpu.clone())
) )
.register( .register(
"GPU_unset_ppt", "GPU_unset_ppt",
api::gpu::unset_ppt(default_settings.gpu.clone(), save_sender.clone()) api::gpu::unset_ppt(loaded_settings.gpu.clone(), save_sender.clone())
) )
.register( .register(
"GPU_set_clock_limits", "GPU_set_clock_limits",
api::gpu::set_clock_limits(default_settings.gpu.clone(), save_sender.clone()) api::gpu::set_clock_limits(loaded_settings.gpu.clone(), save_sender.clone())
) )
.register( .register(
"GPU_get_clock_limits", "GPU_get_clock_limits",
api::gpu::get_clock_limits(default_settings.gpu.clone()) api::gpu::get_clock_limits(loaded_settings.gpu.clone())
) )
.register( .register(
"GPU_unset_clock_limits", "GPU_unset_clock_limits",
api::gpu::unset_clock_limits(default_settings.gpu.clone(), save_sender.clone()) api::gpu::unset_clock_limits(loaded_settings.gpu.clone(), save_sender.clone())
) )
.register( .register(
"GPU_set_slow_memory", "GPU_set_slow_memory",
api::gpu::set_slow_memory(default_settings.gpu.clone(), save_sender.clone()) api::gpu::set_slow_memory(loaded_settings.gpu.clone(), save_sender.clone())
) )
.register( .register(
"GPU_get_slow_memory", "GPU_get_slow_memory",
api::gpu::get_slow_memory(default_settings.gpu.clone()) api::gpu::get_slow_memory(loaded_settings.gpu.clone())
) )
// general API functions // general API functions
.register( .register(
"GENERAL_set_persistent", "GENERAL_set_persistent",
api::general::set_persistent(default_settings.general.clone(), save_sender.clone()) api::general::set_persistent(loaded_settings.general.clone(), save_sender.clone())
) )
.register( .register(
"GENERAL_get_persistent", "GENERAL_get_persistent",
api::general::get_persistent(default_settings.general.clone()) api::general::get_persistent(loaded_settings.general.clone())
) )
.register( .register(
"GENERAL_load_settings", "GENERAL_load_settings",
api::general::load_settings(default_settings.clone()) api::general::load_settings(loaded_settings.clone())
)
.register(
"GENERAL_load_default_settings",
api::general::load_default_settings(loaded_settings.clone())
) )
.register( .register(
"GENERAL_get_name", "GENERAL_get_name",
api::general::get_name(default_settings.general.clone()) api::general::get_name(loaded_settings.general.clone())
)
.register(
"GENERAL_wait_for_unlocks",
api::general::lock_unlock_all(loaded_settings.clone())
) )
.run_blocking() .run_blocking()
} }
fn settings_dir() -> std::path::PathBuf {
usdpl_back::api::dirs::home()
.unwrap_or_else(|| "/home/deck".into())
.join(".config/powertools/")
}

View file

@ -19,7 +19,7 @@ impl Default for SettingsJson {
fn default() -> Self { fn default() -> Self {
Self { Self {
version: 0, version: 0,
name: "default".to_owned(), name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(),
persistent: false, persistent: false,
cpus: Vec::with_capacity(8), cpus: Vec::with_capacity(8),
gpu: GpuJson::default(), gpu: GpuJson::default(),

View file

@ -8,6 +8,7 @@ const ALLOWED_ERROR: f64 = 0.001;
pub fn spawn(settings: Settings) -> JoinHandle<()> { pub fn spawn(settings: Settings) -> JoinHandle<()> {
thread::spawn(move || { thread::spawn(move || {
log::info!("resume_worker starting...");
let duration = Duration::from_millis(5000); let duration = Duration::from_millis(5000);
let mut start = Instant::now(); let mut start = Instant::now();
loop { loop {
@ -17,11 +18,12 @@ pub fn spawn(settings: Settings) -> JoinHandle<()> {
if old_start.as_secs_f64() > duration.as_secs_f64() * (1.0 + ALLOWED_ERROR) { if old_start.as_secs_f64() > duration.as_secs_f64() * (1.0 + ALLOWED_ERROR) {
// has just resumed from sleep // has just resumed from sleep
unwrap_maybe_fatal(settings.on_resume(), "On resume failure"); unwrap_maybe_fatal(settings.on_resume(), "On resume failure");
log::info!( log::debug!(
"OnResume completed after sleeping for {}s", "OnResume completed after sleeping for {}s",
old_start.as_secs_f32() old_start.as_secs_f32()
); );
} }
} }
//log::warn!("resume_worker completed!");
}) })
} }

View file

@ -8,12 +8,17 @@ use crate::utility::{unwrap_lock, unwrap_maybe_fatal};
pub fn spawn(settings: Settings) -> (JoinHandle<()>, Sender<()>) { pub fn spawn(settings: Settings) -> (JoinHandle<()>, Sender<()>) {
let (sender, receiver): (Sender<()>, Receiver<()>) = mpsc::channel(); let (sender, receiver): (Sender<()>, Receiver<()>) = mpsc::channel();
let worker = thread::spawn(move || { let worker = thread::spawn(move || {
log::info!("save_worker starting...");
for _ in receiver.iter() { for _ in receiver.iter() {
let save_path = unwrap_lock(settings.general.lock(), "general").path.clone(); log::debug!("save_worker is saving...");
let save_json: SettingsJson = settings.clone().into(); let save_path = crate::utility::settings_dir()
.join(unwrap_lock(settings.general.lock(), "general").path.clone());
let settings_clone = settings.clone();
let save_json: SettingsJson = settings_clone.into();
unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings"); unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings");
log::debug!("Saved settings to {}", save_path.display()); log::debug!("Saved settings to {}", save_path.display());
} }
log::warn!("save_worker completed!");
}); });
(worker, sender) (worker, sender)
} }

View file

@ -101,16 +101,18 @@ impl Cpu {
})?; })?;
} }
// set governor // set governor
let governor_path = cpu_governor_path(self.index); if self.index == 0 || self.online {
usdpl_back::api::files::write_single(&governor_path, &self.governor).map_err(|e| { let governor_path = cpu_governor_path(self.index);
SettingError { usdpl_back::api::files::write_single(&governor_path, &self.governor).map_err(|e| {
msg: format!( SettingError {
"Failed to write `{}` to `{}`: {}", msg: format!(
&self.governor, &governor_path, e "Failed to write `{}` to `{}`: {}",
), &self.governor, &governor_path, e
setting: super::SettingVariant::Cpu, ),
} setting: super::SettingVariant::Cpu,
})?; }
})?;
}
Ok(()) Ok(())
} }
@ -141,7 +143,7 @@ impl Cpu {
if let Some(dash_index) = data.find('-') { if let Some(dash_index) = data.find('-') {
let data = data.split_off(dash_index + 1); let data = data.split_off(dash_index + 1);
if let Ok(max_cpu) = data.parse::<usize>() { if let Ok(max_cpu) = data.parse::<usize>() {
return Some(max_cpu); return Some(max_cpu + 1);
} }
} }
log::warn!("Failed to parse CPU info from kernel, is Tux evil?"); log::warn!("Failed to parse CPU info from kernel, is Tux evil?");

View file

@ -40,7 +40,7 @@ impl OnSet for General {
// remove settings file when persistence is turned off, to prevent it from be loaded next time. // remove settings file when persistence is turned off, to prevent it from be loaded next time.
if !self.persistent && self.path.exists() { if !self.persistent && self.path.exists() {
std::fs::remove_file(&self.path).map_err(|e| SettingError { std::fs::remove_file(&self.path).map_err(|e| SettingError {
msg: e.to_string(), msg: format!("Failed to delete `{}`: {}", self.path.display(), e),
setting: SettingVariant::General, setting: SettingVariant::General,
})?; })?;
} }
@ -67,16 +67,7 @@ impl OnSet for Settings {
} }
} }
unwrap_lock(self.gpu.lock(), "gpu").on_set()?; unwrap_lock(self.gpu.lock(), "gpu").on_set()?;
{ unwrap_lock(self.general.lock(), "general").on_set()?;
// general lock scope
let gen_lock = unwrap_lock(self.general.lock(), "general");
if !gen_lock.persistent && gen_lock.path.exists() {
std::fs::remove_file(&gen_lock.path).map_err(|e| SettingError {
msg: format!("Failed to delete `{}`: {}", gen_lock.path.display(), e),
setting: SettingVariant::General,
})?;
}
}
Ok(()) Ok(())
} }
} }
@ -121,7 +112,7 @@ impl Settings {
general: Arc::new(Mutex::new(General { general: Arc::new(Mutex::new(General {
persistent: false, persistent: false,
path: json_path, path: json_path,
name: "".to_owned(), name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(),
})), })),
cpus: Arc::new(Mutex::new(Cpu::system_default())), cpus: Arc::new(Mutex::new(Cpu::system_default())),
gpu: Arc::new(Mutex::new(Gpu::system_default())), gpu: Arc::new(Mutex::new(Gpu::system_default())),
@ -179,17 +170,26 @@ impl OnResume for Settings {
impl Into<SettingsJson> for Settings { impl Into<SettingsJson> for Settings {
#[inline] #[inline]
fn into(self) -> SettingsJson { fn into(self) -> SettingsJson {
log::debug!("Locking settings to convert into json");
let gen_lock = unwrap_lock(self.general.lock(), "general");
log::debug!("Got general lock");
let cpu_lock = unwrap_lock(self.cpus.lock(), "cpu");
log::debug!("Got cpus lock");
let gpu_lock = unwrap_lock(self.gpu.lock(), "gpu");
log::debug!("Got gpu lock");
let batt_lock = unwrap_lock(self.battery.lock(), "battery");
log::debug!("Got battery lock");
SettingsJson { SettingsJson {
version: LATEST_VERSION, version: LATEST_VERSION,
name: unwrap_lock(self.general.lock(), "general").name.clone(), name: gen_lock.name.clone(),
persistent: unwrap_lock(self.general.lock(), "general").persistent, persistent: gen_lock.persistent,
cpus: unwrap_lock(self.cpus.lock(), "cpu") cpus: cpu_lock
.clone() .clone()
.drain(..) .drain(..)
.map(|cpu| cpu.into()) .map(|cpu| cpu.into())
.collect(), .collect(),
gpu: unwrap_lock(self.gpu.lock(), "gpu").clone().into(), gpu: gpu_lock.clone().into(),
battery: unwrap_lock(self.battery.lock(), "battery").clone().into(), battery: batt_lock.clone().into(),
} }
} }
} }

View file

@ -23,3 +23,9 @@ pub fn unwrap_lock<'a, T: Sized>(
} }
} }
} }
pub fn settings_dir() -> std::path::PathBuf {
usdpl_back::api::dirs::home()
.unwrap_or_else(|| "/home/deck".into())
.join(".config/powertools/")
}

View file

@ -1,4 +1,4 @@
import {init_usdpl, target, init_embedded, call_backend} from "usdpl-front"; import {init_usdpl, target_usdpl, init_embedded, call_backend} from "usdpl-front";
const USDPL_PORT: number = 44443; const USDPL_PORT: number = 44443;
@ -20,12 +20,16 @@ export async function initBackend() {
// init usdpl // init usdpl
await init_embedded(); await init_embedded();
init_usdpl(USDPL_PORT); init_usdpl(USDPL_PORT);
console.log("USDPL started for framework: " + target()); console.log("USDPL started for framework: " + target_usdpl());
//setReady(true); //setReady(true);
} }
// API // API
export async function getInfo(): Promise<string> {
return (await call_backend("V_INFO", []))[0];
}
// Battery // Battery
export async function getBatteryCurrent(): Promise<number> { export async function getBatteryCurrent(): Promise<number> {
@ -126,6 +130,14 @@ export async function loadGeneralSettings(path: string, name: string): Promise<b
return (await call_backend("GENERAL_load_settings", [path, name]))[0]; return (await call_backend("GENERAL_load_settings", [path, name]))[0];
} }
export async function getGeneralPersistent(): Promise<boolean> { export async function loadGeneralDefaultSettings(): Promise<boolean> {
return (await call_backend("GENERAL_load_default_settings", []))[0];
}
export async function getGeneralSettingsName(): Promise<boolean> {
return (await call_backend("GENERAL_get_name", []))[0]; return (await call_backend("GENERAL_get_name", []))[0];
} }
export async function waitForComplete(): Promise<boolean> {
return (await call_backend("GENERAL_wait_for_unlocks", []))[0];
}

View file

@ -1,7 +1,7 @@
import { import {
//ButtonItem, ButtonItem,
definePlugin, definePlugin,
DialogButton, //DialogButton,
//Menu, //Menu,
//MenuItem, //MenuItem,
PanelSection, PanelSection,
@ -19,161 +19,131 @@ import {
import { VFC, useState } from "react"; import { VFC, useState } from "react";
import { GiDrill } from "react-icons/gi"; import { GiDrill } from "react-icons/gi";
import * as python from "./python"; //import * as python from "./python";
import * as backend from "./backend";
import {set_value, get_value, target_usdpl, version_usdpl} from "usdpl-front";
//import logo from "../assets/logo.png";
// interface AddMethodArgs {
// left: number;
// right: number;
// }
var firstTime: boolean = true;
var versionGlobalHolder: string = "0.0.0-jank";
var periodicHook: NodeJS.Timer | null = null; var periodicHook: NodeJS.Timer | null = null;
var lastGame: string = "";
var lifetimeHook: any = null; var lifetimeHook: any = null;
var startHook: any = null; var startHook: any = null;
var usdplReady = false;
var smt_backup: boolean = true; var smtAllowed = true;
var cpus_backup: number = 8; var smtGlobal = smtAllowed;
var boost_backup: boolean = true;
var freq_backup: number = 8;
var slowPPT_backup: number = 1;
var fastPPT_backup: number = 1;
var chargeNow_backup: number = 5200000;
var chargeFull_backup: number = 5200000;
var chargeDesign_backup: number = 5200000;
var persistent_backup: boolean = false;
var perGameProfile_backup: boolean = false;
var reload = function(){}; // usdpl persistent store keys
const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => { const BACKEND_INFO = "VINFO";
// const [result, setResult] = useState<number | undefined>();
// const onClick = async () => { const CURRENT_BATT = "BATTERY_current_now";
// const result = await serverAPI.callPluginMethod<AddMethodArgs, number>( const CHARGE_RATE_BATT = "BATTERY_charge_rate";
// "add",
// {
// left: 2,
// right: 2,
// }
// );
// if (result.success) {
// setResult(result.result);
// }
// };
python.setServer(serverAPI); const TOTAL_CPUS = "CPUs_total";
const ONLINE_CPUS = "CPUs_online";
const CLOCK_MIN_CPU = "CPUs_min_clock";
const CLOCK_MAX_CPU = "CPUs_max_clock";
const GOVERNOR_CPU = "CPUs_governor";
const [smtGlobal, setSMT_internal] = useState<boolean>(smt_backup); const FAST_PPT_GPU = "GPU_fastPPT";
const setSMT = (value: boolean) => { const SLOW_PPT_GPU = "GPU_slowPPT";
smt_backup = value; const CLOCK_MIN_GPU = "GPU_min_clock";
setSMT_internal(value); const CLOCK_MAX_GPU = "GPU_max_clock";
}; const SLOW_MEMORY_GPU = "GPU_slow_memory";
const [cpusGlobal, setCPUs_internal] = useState<number>(cpus_backup); const PERSISTENT_GEN = "GENERAL_persistent";
const setCPUs = (value: number) => { const NAME_GEN = "GENERAL_name";
cpus_backup = value;
setCPUs_internal(value);
};
const [boostGlobal, setBoost_internal] = useState<boolean>(boost_backup); const reload = function() {
const setBoost = (value: boolean) => { if (!usdplReady) {return;}
boost_backup = value;
setBoost_internal(value);
};
const [freqGlobal, setFreq_internal] = useState<number>(freq_backup); backend.resolve(backend.getBatteryCurrent(), (rate: number) => { set_value(CURRENT_BATT, rate) });
const setFreq = (value: number) => { backend.resolve(backend.getBatteryChargeRate(), (rate: number) => { set_value(CHARGE_RATE_BATT, rate) });
freq_backup = value;
setFreq_internal(value);
};
const [slowPPTGlobal, setSlowPPT_internal] = useState<number>(slowPPT_backup); backend.resolve(backend.getCpuCount(), (count: number) => { set_value(TOTAL_CPUS, count)});
const setSlowPPT = (value: number) => { backend.resolve(backend.getCpusOnline(), (statii: boolean[]) => {
slowPPT_backup = value; // TODO: allow for per-core control of online status
setSlowPPT_internal(value); let count = 0;
}; for (let i = 0; i < statii.length; i++) {
if (statii[i]) {
count += 1;
}
}
set_value(ONLINE_CPUS, count);
smtGlobal = statii.length > 3 && statii[0] == statii[1] && statii[2] == statii[3] && smtAllowed;
});
// TODO: allow for per-core control of clock limits
backend.resolve(backend.getCpuClockLimits(0), (limits: number[]) => {
set_value(CLOCK_MIN_CPU, limits[0]);
set_value(CLOCK_MAX_CPU, limits[1]);
});
// TODO: allow for control of governor
backend.resolve(backend.getCpusGovernor(), (governors: string[]) => { set_value(GOVERNOR_CPU, governors[0]) });
const [fastPPTGlobal, setFastPPT_internal] = useState<number>(fastPPT_backup); backend.resolve(backend.getGpuPpt(), (ppts: number[]) => {
const setFastPPT = (value: number) => { set_value(FAST_PPT_GPU, ppts[0]);
fastPPT_backup = value; set_value(SLOW_PPT_GPU, ppts[1]);
setFastPPT_internal(value); });
}; backend.resolve(backend.getGpuClockLimits(), (limits: number[]) => {
set_value(CLOCK_MIN_GPU, limits[0]);
set_value(CLOCK_MAX_GPU, limits[1]);
});
backend.resolve(backend.getGpuSlowMemory(), (status: boolean) => { set_value(SLOW_MEMORY_GPU, status) });
const [chargeNowGlobal, setChargeNow_internal] = useState<number>(chargeNow_backup); backend.resolve(backend.getGeneralPersistent(), (value: boolean) => { set_value(PERSISTENT_GEN, value) });
const setChargeNow = (value: number) => { backend.resolve(backend.getGeneralSettingsName(), (name: string) => { set_value(NAME_GEN, name) });
chargeNow_backup = value;
setChargeNow_internal(value);
};
const [chargeFullGlobal, setChargeFull_internal] = useState<number>(chargeFull_backup); backend.resolve(backend.getInfo(), (info: string) => { set_value(BACKEND_INFO, info) });
const setChargeFull = (value: number) => { };
chargeFull_backup = value;
setChargeFull_internal(value);
};
const [chargeDesignGlobal, setChargeDesign_internal] = useState<number>(chargeDesign_backup); // init USDPL WASM and connection to back-end
const setChargeDesign = (value: number) => { (async function(){
chargeDesign_backup = value; await backend.initBackend();
setChargeDesign_internal(value); usdplReady = true;
}; set_value(NAME_GEN, "Default");
reload(); // technically this is only a load
const [persistGlobal, setPersist_internal] = useState<boolean>(persistent_backup); // register Steam callbacks
const setPersist = (value: boolean) => { //@ts-ignore
persistent_backup = value; lifetimeHook = SteamClient.GameSessions.RegisterForAppLifetimeNotifications((update) => {
setPersist_internal(value); if (update.bRunning) {
}; //console.debug("AppID " + update.unAppID.toString() + " is now running");
} else {
//console.debug("AppID " + update.unAppID.toString() + " is no longer running");
backend.resolve(
backend.loadGeneralDefaultSettings(),
(ok: boolean) => {console.debug("Loading default settings ok? " + ok)}
);
}
});
//@ts-ignore
startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => {
//@ts-ignore
let gameInfo: any = appStore.GetAppOverviewByGameID(id);
backend.resolve(
backend.loadGeneralSettings(gameInfo.appid.toString() + ".json", gameInfo.display_name),
(ok: boolean) => {console.debug("Loading settings ok? " + ok)}
);
});
const [perGameProfileGlobal, setPerGameProfile_internal] = useState<boolean>(perGameProfile_backup); console.debug("Registered PowerTools callbacks, hello!");
const setPerGameProfile = (value: boolean) => { })();
perGameProfile_backup = value;
setPerGameProfile_internal(value);
};
const [gameGlobal, setGame_internal] = useState<string>(lastGame); const periodicals = function() {
const setGame = (value: string) => { backend.resolve(backend.getBatteryCurrent(), (rate: number) => { set_value(CURRENT_BATT, rate) });
lastGame = value;
setGame_internal(value);
};
const [versionGlobal, setVersion_internal] = useState<string>(versionGlobalHolder); backend.resolve(backend.getGeneralPersistent(), (value: boolean) => { set_value(PERSISTENT_GEN, value) });
const setVersion = (value: string) => { backend.resolve(backend.getGeneralSettingsName(), (name: string) => {
versionGlobalHolder = value; const oldValue = get_value(NAME_GEN);
setVersion_internal(value); set_value(NAME_GEN, name);
}; if (name != oldValue) {
reload();
}
});
};
reload = function () { const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
python.execute(python.onViewReady());
python.resolve(python.getSMT(), setSMT); const [_idc, reloadGUI] = useState<any>("/shrug");
python.resolve(python.getCPUs(), setCPUs);
python.resolve(python.getCPUBoost(), setBoost);
python.resolve(python.getMaxBoost(), setFreq);
python.resolve(python.getGPUPowerI(1), setSlowPPT);
python.resolve(python.getGPUPowerI(2), setFastPPT);
python.resolve(python.getPersistent(), setPersist);
python.resolve(python.getPerGameProfile(), setPerGameProfile);
};
if (firstTime) {
firstTime = false;
reload(); // technically it's just load, not reload ;)
python.resolve(python.getChargeNow(), setChargeNow);
python.resolve(python.getChargeFull(), setChargeFull);
python.resolve(python.getChargeDesign(), setChargeDesign);
python.resolve(python.getCurrentGame(), setGame);
python.resolve(python.getVersion(), setVersion);
}
if (periodicHook != null) { if (periodicHook != null) {
clearInterval(periodicHook); clearInterval(periodicHook);
@ -181,189 +151,387 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
} }
periodicHook = setInterval(function() { periodicHook = setInterval(function() {
python.resolve(python.getChargeFull(), setChargeFull); periodicals();
python.resolve(python.getChargeNow(), setChargeNow); reloadGUI("periodic" + (new Date()).getTime().toString());
python.resolve(python.getCurrentGame(), (game: string) => {
if (lastGame != game) {
setGame(game);
lastGame = game;
reload();
}
});
}, 1000); }, 1000);
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard); const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
const total_cpus = get_value(TOTAL_CPUS);
return ( return (
<PanelSection> <PanelSection>
{/* CPU */} {/* CPU */ /* TODO: set per-core stuff*/}
<div className={staticClasses.PanelSectionTitle}> <div className={staticClasses.PanelSectionTitle}>
CPU CPU
</div> </div>
<PanelSectionRow> {smtAllowed && <PanelSectionRow>
<ToggleField <ToggleField
checked={smtGlobal} checked={smtGlobal}
label="SMT" label="SMT"
description="Enables odd-numbered CPUs" description="Enables odd-numbered CPUs"
onChange={(smt: boolean) => { onChange={(smt: boolean) => {
console.log("SMT is now " + smt.toString()); console.debug("SMT is now " + smt.toString());
python.execute(python.setCPUs(cpusGlobal, smt)); const cpus = get_value(ONLINE_CPUS);
python.resolve(python.getCPUs(), setCPUs); set_value(ONLINE_CPUS, 0);
python.resolve(python.getSMT(), setSMT); smtGlobal = smt && smtAllowed;
for (let i = 0; i < total_cpus; i++) {
const online = (smtGlobal? i < cpus : (i % 2 == 0) && (i < cpus * 2))
|| (!smtGlobal && cpus == 4);
backend.resolve(backend.setCpuOnline(i, online), (value: boolean) => {
if (value) {set_value(ONLINE_CPUS, get_value(ONLINE_CPUS) + 1)}
})
}
backend.resolve(backend.waitForComplete(), (_: boolean[]) => {
reloadGUI("SMT");
});
}} }}
/> />
</PanelSectionRow> </PanelSectionRow>}
<PanelSectionRow> <PanelSectionRow>
<SliderField <SliderField
label="Threads" label="Threads"
value={cpusGlobal} value={get_value(ONLINE_CPUS)}
step={1} step={1}
max={smtGlobal? 8 : 4} max={smtGlobal? total_cpus : total_cpus/2}
min={1} min={1}
showValue={true} showValue={true}
onChange={(cpus: number) => { onChange={(cpus: number) => {
console.log("CPU slider is now " + cpus.toString()); console.debug("CPU slider is now " + cpus.toString());
if (cpus != cpusGlobal) { const onlines = get_value(ONLINE_CPUS);
python.execute(python.setCPUs(cpus, smtGlobal)); if (cpus != onlines) {
python.resolve(python.getCPUs(), setCPUs); set_value(ONLINE_CPUS, 0);
for (let i = 0; i < total_cpus; i++) {
const online = smtGlobal? i < cpus : (i % 2 == 0) && (i < cpus * 2);
backend.resolve(backend.setCpuOnline(i, online), (value: boolean) => {
if (value) {set_value(ONLINE_CPUS, get_value(ONLINE_CPUS) + 1)}
})
}
backend.resolve(backend.waitForComplete(), (_: boolean[]) => {
reloadGUI("CPUs");
});
} }
}} }}
/> />
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
<ToggleField <ToggleField
checked={boostGlobal} checked={get_value(CLOCK_MIN_CPU) != null && get_value(CLOCK_MAX_CPU) != null}
label="Boost" label="Frequency Limits"
description="Allows the CPU to go above max frequency" onChange={(value: boolean) => {
onChange={(boost: boolean) => { if (value) {
console.log("Boost is now " + boost.toString()); set_value(CLOCK_MIN_CPU, 1400);
python.execute(python.setCPUBoost(boost)); set_value(CLOCK_MAX_CPU, 3500);
python.resolve(python.getCPUBoost(), setBoost); reloadGUI("CPUFreqToggle");
}} } else {
/> set_value(CLOCK_MIN_CPU, null);
</PanelSectionRow> set_value(CLOCK_MAX_CPU, null);
<PanelSectionRow> for (let i = 0; i < total_cpus; i++) {
<SliderField backend.resolve(backend.unsetCpuClockLimits(i), (_idc: any[]) => {});
label="Max Frequency" }
value={freqGlobal} backend.resolve(backend.waitForComplete(), (_: boolean[]) => {
max={2} reloadGUI("CPUUnsetFreq");
min={0} });
notchCount={3}
notchLabels={[
{notchIndex: 0, label: "1.7GHz"},
{notchIndex: 1, label: "2.4GHz"},
{notchIndex: 2, label: "2.8GHz"},
]}
notchTicksVisible={true}
onChange={(freq: number) => {
console.log("CPU slider is now " + freq.toString());
if (freq != freqGlobal) {
python.execute(python.setMaxBoost(freq));
python.resolve(python.getMaxBoost(), setFreq);
} }
}} }}
/> />
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow>
{get_value(CLOCK_MIN_CPU) != null && <SliderField
label="Minimum (MHz)"
value={get_value(CLOCK_MIN_CPU)}
max={3500}
min={1400}
step={100}
showValue={true}
disabled={get_value(CLOCK_MIN_CPU) == null}
onChange={(freq: number) => {
console.debug("Min freq slider is now " + freq.toString());
const freqNow = get_value(CLOCK_MIN_CPU);
if (freq != freqNow) {
for (let i = 0; i < total_cpus; i++) {
backend.resolve(backend.setCpuClockLimits(i, freq, get_value(CLOCK_MAX_CPU)),
(limits: number[]) => {
set_value(CLOCK_MIN_CPU, limits[0]);
set_value(CLOCK_MAX_CPU, limits[1]);
});
}
backend.resolve(backend.waitForComplete(), (_: boolean[]) => {
reloadGUI("CPUMinFreq");
});
}
}}
/>}
</PanelSectionRow>
<PanelSectionRow>
{get_value(CLOCK_MAX_CPU) != null && <SliderField
label="Maximum (MHz)"
value={get_value(CLOCK_MAX_CPU)}
max={3500}
min={500}
step={100}
showValue={true}
disabled={get_value(CLOCK_MAX_CPU) == null}
onChange={(freq: number) => {
console.debug("Max freq slider is now " + freq.toString());
const freqNow = get_value(CLOCK_MAX_CPU);
if (freq != freqNow) {
for (let i = 0; i < total_cpus; i++) {
backend.resolve(backend.setCpuClockLimits(i, get_value(CLOCK_MIN_CPU), freq),
(limits: number[]) => {
set_value(CLOCK_MIN_CPU, limits[0]);
set_value(CLOCK_MAX_CPU, limits[1]);
});
}
backend.resolve(backend.waitForComplete(), (_: boolean[]) => {
reloadGUI("CPUMaxFreq");
});
}
}}
/>}
</PanelSectionRow>
{/* TODO: CPU governor */}
{/* GPU */} {/* GPU */}
<div className={staticClasses.PanelSectionTitle}> <div className={staticClasses.PanelSectionTitle}>
GPU GPU
</div> </div>
<PanelSectionRow> <PanelSectionRow>
{/* index: 1 */} <ToggleField
<SliderField checked={get_value(SLOW_PPT_GPU) != null && get_value(FAST_PPT_GPU) != null}
label="SlowPPT Power" label="PowerPlay Limits"
value={slowPPTGlobal} description="Override APU TDP settings"
max={2} onChange={(value: boolean) => {
min={0} if (value) {
notchCount={3} set_value(SLOW_PPT_GPU, 15000000);
notchLabels={[ set_value(FAST_PPT_GPU, 15000000);
{notchIndex: 0, label: "Min"}, reloadGUI("GPUPPTToggle");
{notchIndex: 1, label: "Auto"}, } else {
{notchIndex: 2, label: "Max"}, set_value(SLOW_PPT_GPU, null);
]} set_value(FAST_PPT_GPU, null);
notchTicksVisible={true} backend.resolve(backend.unsetGpuPpt(), (_: any[]) => {
onChange={(ppt: number) => { reloadGUI("GPUUnsetPPT");
console.log("SlowPPT is now " + ppt.toString()); });
if (ppt != slowPPTGlobal) {
python.execute(python.setGPUPowerI(ppt, 1));
python.resolve(python.getGPUPowerI(1), setSlowPPT);
} }
}} }}
/> />
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow> <PanelSectionRow>
{/* index: 2 */} { get_value(SLOW_PPT_GPU) != null && <SliderField
<SliderField label="SlowPPT (uW)"
label="FastPPT Power" value={get_value(SLOW_PPT_GPU)}
value={fastPPTGlobal} max={29000000}
max={2} min={1000000}
min={0} step={1000000}
notchCount={3} showValue={true}
notchLabels={[ disabled={get_value(SLOW_PPT_GPU) == null}
{notchIndex: 0, label: "Min"},
{notchIndex: 1, label: "Auto"},
{notchIndex: 2, label: "Max"},
]}
notchTicksVisible={true}
onChange={(ppt: number) => { onChange={(ppt: number) => {
console.log("FastPPT is now " + ppt.toString()); console.debug("SlowPPT is now " + ppt.toString());
if (ppt != fastPPTGlobal) { const pptNow = get_value(SLOW_PPT_GPU);
python.execute(python.setGPUPowerI(ppt, 2)); if (ppt != pptNow) {
python.resolve(python.getGPUPowerI(2), setFastPPT); backend.resolve(backend.setGpuPpt(get_value(FAST_PPT_GPU), ppt),
(limits: number[]) => {
set_value(FAST_PPT_GPU, limits[0]);
set_value(SLOW_PPT_GPU, limits[1]);
reloadGUI("GPUSlowPPT");
});
} }
}} }}
/>}
</PanelSectionRow>
<PanelSectionRow>
{get_value(FAST_PPT_GPU) != null && <SliderField
label="FastPPT (uW)"
value={get_value(FAST_PPT_GPU)}
max={29000000}
min={1000000}
step={1000000}
showValue={true}
disabled={get_value(FAST_PPT_GPU) == null}
onChange={(ppt: number) => {
console.debug("FastPPT is now " + ppt.toString());
const pptNow = get_value(FAST_PPT_GPU);
if (ppt != pptNow) {
backend.resolve(backend.setGpuPpt(get_value(SLOW_PPT_GPU), ppt),
(limits: number[]) => {
set_value(FAST_PPT_GPU, limits[0]);
set_value(SLOW_PPT_GPU, limits[1]);
reloadGUI("GPUFastPPT");
});
}
}}
/>}
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
checked={get_value(CLOCK_MIN_GPU) != null && get_value(CLOCK_MAX_GPU) != null}
label="Frequency Limits"
onChange={(value: boolean) => {
if (value) {
set_value(CLOCK_MIN_GPU, 200);
set_value(CLOCK_MAX_GPU, 1600);
reloadGUI("GPUFreqToggle");
} else {
set_value(CLOCK_MIN_GPU, null);
set_value(CLOCK_MIN_GPU, null);
backend.resolve(backend.unsetGpuClockLimits(), (_: any[]) => {
reloadGUI("GPUUnsetFreq");
});
}
}}
/>
</PanelSectionRow>
<PanelSectionRow>
{ get_value(CLOCK_MIN_GPU) != null && <SliderField
label="Minimum (MHz)"
value={get_value(CLOCK_MIN_GPU)}
max={1600}
min={200}
step={100}
showValue={true}
disabled={get_value(CLOCK_MIN_GPU) == null}
onChange={(val: number) => {
console.debug("GPU Clock Min is now " + val.toString());
const valNow = get_value(CLOCK_MIN_GPU);
if (val != valNow) {
backend.resolve(backend.setGpuClockLimits(val, get_value(CLOCK_MAX_GPU)),
(limits: number[]) => {
set_value(CLOCK_MIN_GPU, limits[0]);
set_value(CLOCK_MAX_GPU, limits[1]);
reloadGUI("GPUMinClock");
});
}
}}
/>}
</PanelSectionRow>
<PanelSectionRow>
{get_value(CLOCK_MAX_GPU) != null && <SliderField
label="Maximum (MHz)"
value={get_value(CLOCK_MAX_GPU)}
max={1600}
min={200}
step={100}
showValue={true}
disabled={get_value(CLOCK_MAX_GPU) == null}
onChange={(val: number) => {
console.debug("GPU Clock Max is now " + val.toString());
const valNow = get_value(CLOCK_MAX_GPU);
if (val != valNow) {
backend.resolve(backend.setGpuClockLimits(get_value(CLOCK_MIN_GPU), val),
(limits: number[]) => {
set_value(CLOCK_MIN_GPU, limits[0]);
set_value(CLOCK_MAX_GPU, limits[1]);
reloadGUI("GPUMaxClock");
});
}
}}
/>}
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
checked={get_value(SLOW_MEMORY_GPU)}
label="Downclock Memory"
description="Force RAM into low-power mode"
onChange={(value: boolean) => {
backend.resolve(backend.setGpuSlowMemory(value), (val: boolean) => {
set_value(SLOW_MEMORY_GPU, val);
reloadGUI("GPUSlowMemory");
})
}}
/> />
</PanelSectionRow> </PanelSectionRow>
{/* Battery */} {/* Battery */}
<div className={staticClasses.PanelSectionTitle}> <div className={staticClasses.PanelSectionTitle}>
Battery Battery
</div> </div>
<PanelSectionRow> { false && <PanelSectionRow>
<div className={FieldWithSeparator}> <div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}> <div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}> <div className={gamepadDialogClasses.FieldLabel}>
Now (Charge) Now (Charge)
</div> </div>
<div className={gamepadDialogClasses.FieldChildren}> <div className={gamepadDialogClasses.FieldChildren}>
{(7.7 * chargeNowGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeNowGlobal / chargeFullGlobal).toFixed(1).toString() + "%)"} {/* TODO: (7.7 * chargeNowGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeNowGlobal / chargeFullGlobal).toFixed(1).toString() + "%)"*/}
</div> </div>
</div> </div>
</div> </div>
</PanelSectionRow> </PanelSectionRow>}
<PanelSectionRow> { false && <PanelSectionRow>
<div className={FieldWithSeparator}> <div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}> <div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}> <div className={gamepadDialogClasses.FieldLabel}>
Max (Design) Max (Design)
</div> </div>
<div className={gamepadDialogClasses.FieldChildren}> <div className={gamepadDialogClasses.FieldChildren}>
{(7.7 * chargeFullGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeFullGlobal / chargeDesignGlobal).toFixed(1).toString() + "%)"} {/* TODO: (7.7 * chargeFullGlobal / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeFullGlobal / chargeDesignGlobal).toFixed(1).toString() + "%)"*/}
</div>
</div>
</div>
</PanelSectionRow>}
<PanelSectionRow>
<ToggleField
checked={get_value(CHARGE_RATE_BATT) != null}
label="Charge Current Limits"
description="Control battery charge rate"
onChange={(value: boolean) => {
if (value) {
set_value(CHARGE_RATE_BATT, 2500);
reloadGUI("BATTChargeRateToggle");
} else {
set_value(CHARGE_RATE_BATT, null);
backend.resolve(backend.unsetBatteryChargeRate(), (_: any[]) => {
reloadGUI("BATTUnsetChargeRate");
});
}
}}
/>
{ get_value(CHARGE_RATE_BATT) != null && <SliderField
label="Maximum (mA)"
value={get_value(CHARGE_RATE_BATT)}
max={2500}
min={250}
step={50}
showValue={true}
disabled={get_value(CHARGE_RATE_BATT) == null}
onChange={(val: number) => {
console.debug("Charge rate is now " + val.toString());
const rateNow = get_value(CHARGE_RATE_BATT);
if (val != rateNow) {
backend.resolve(backend.setBatteryChargeRate(val),
(rate: number) => {
set_value(CHARGE_RATE_BATT, rate);
reloadGUI("BATTChargeRate");
});
}
}}
/>}
</PanelSectionRow>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
Current
</div>
<div className={gamepadDialogClasses.FieldChildren}>
{get_value(CURRENT_BATT)} mA
</div> </div>
</div> </div>
</div> </div>
</PanelSectionRow> </PanelSectionRow>
{/* Persistence */} {/* Persistence */}
<div className={staticClasses.PanelSectionTitle}>
Miscellaneous
</div>
<PanelSectionRow> <PanelSectionRow>
<ToggleField <ToggleField
checked={persistGlobal} checked={get_value(PERSISTENT_GEN)}
label="Persistent" label="Persistent"
description="Restores settings after an app or OS restart" description="Restores settings after an app or OS restart"
onChange={(persist: boolean) => { onChange={(persist: boolean) => {
console.log("Persist is now " + persist.toString()); console.debug("Persist is now " + persist.toString());
python.execute(python.setPersistent(persist)); backend.resolve(
python.resolve(python.getPersistent(), setPersist); backend.setGeneralPersistent(persist),
}} (val: boolean) => {set_value(PERSISTENT_GEN, val)}
/> );
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
checked={perGameProfileGlobal}
label="Use per-game profile"
onChange={(p: boolean) => {
console.log("Per game profile is now " + p.toString());
python.execute(python.setPerGameProfile(p));
python.resolve(python.getPerGameProfile(), setPerGameProfile);
reload();
}} }}
/> />
</PanelSectionRow> </PanelSectionRow>
@ -374,12 +542,12 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
Now Playing Now Playing
</div> </div>
<div className={gamepadDialogClasses.FieldChildren}> <div className={gamepadDialogClasses.FieldChildren}>
{gameGlobal} {get_value(NAME_GEN)}
</div> </div>
</div> </div>
</div> </div>
</PanelSectionRow> </PanelSectionRow>
{/* Version */} {/* Version Info */}
<div className={staticClasses.PanelSectionTitle}> <div className={staticClasses.PanelSectionTitle}>
Debug Debug
</div> </div>
@ -387,68 +555,75 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({serverAPI}) => {
<div className={FieldWithSeparator}> <div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}> <div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}> <div className={gamepadDialogClasses.FieldLabel}>
PowerTools Native
</div> </div>
<div className={gamepadDialogClasses.FieldChildren}> <div className={gamepadDialogClasses.FieldChildren}>
v{versionGlobal} {get_value(BACKEND_INFO)}
</div> </div>
</div> </div>
</div> </div>
</PanelSectionRow> </PanelSectionRow>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
Framework
</div>
<div className={gamepadDialogClasses.FieldChildren}>
{target_usdpl()}
</div>
</div>
</div>
</PanelSectionRow>
<PanelSectionRow>
<div className={FieldWithSeparator}>
<div className={gamepadDialogClasses.FieldLabelRow}>
<div className={gamepadDialogClasses.FieldLabel}>
USDPL
</div>
<div className={gamepadDialogClasses.FieldChildren}>
v{version_usdpl()}
</div>
</div>
</div>
</PanelSectionRow>
<PanelSectionRow>
<ButtonItem
layout="below"
onClick={(_: MouseEvent) => {
console.debug("Loading default PowerTools settings");
backend.resolve(
backend.setGeneralPersistent(false),
(val: boolean) => {
set_value(PERSISTENT_GEN, val);
backend.resolve(backend.loadGeneralDefaultSettings(), (_: any[]) => {
reload();
backend.resolve(backend.waitForComplete(), (_: any[]) => {reloadGUI("LoadDefaults")});
});
}
);
}}
>
Defaults
</ButtonItem>
</PanelSectionRow>
</PanelSection> </PanelSection>
); );
}; };
const DeckyPluginRouterTest: VFC = () => {
return (
<div style={{ marginTop: "50px", color: "white" }}>
Hello World!
<DialogButton onClick={() => {}}>
Go to Store
</DialogButton>
</div>
);
};
export default definePlugin((serverApi: ServerAPI) => { export default definePlugin((serverApi: ServerAPI) => {
serverApi.routerHook.addRoute("/decky-plugin-test", DeckyPluginRouterTest, {
exact: true,
});
python.setServer(serverApi);
//@ts-ignore
lifetimeHook = SteamClient.GameSessions.RegisterForAppLifetimeNotifications((update) => {
if (update.bRunning) {
console.log("AppID " + update.unAppID.toString() + " is now running");
} else {
console.log("AppID " + update.unAppID.toString() + " is no longer running");
python.execute(python.onGameStop(null));
}
});
//@ts-ignore
startHook = SteamClient.Apps.RegisterForGameActionStart((actionType, id) => {
//@ts-ignore
let gameInfo: any = appStore.GetAppOverviewByGameID(id);
python.execute(python.onGameStart(id, gameInfo));
});
console.log("Registered PowerTools callbacks, hello!");
return { return {
title: <div className={staticClasses.Title}>PowerTools</div>, title: <div className={staticClasses.Title}>PowerTools</div>,
content: <Content serverAPI={serverApi} />, content: <Content serverAPI={serverApi} />,
icon: <GiDrill />, icon: <GiDrill />,
onDismount() { onDismount() {
console.log("PowerTools shutting down"); console.debug("PowerTools shutting down");
clearInterval(periodicHook!); clearInterval(periodicHook!);
periodicHook = null; periodicHook = null;
lifetimeHook!.unregister(); lifetimeHook!.unregister();
startHook!.unregister(); startHook!.unregister();
serverApi.routerHook.removeRoute("/decky-plugin-test"); serverApi.routerHook.removeRoute("/decky-plugin-test");
firstTime = true; console.debug("Unregistered PowerTools callbacks, goodbye.");
lastGame = "";
console.log("Unregistered PowerTools callbacks, goodbye.");
}, },
}; };
}); });

View file

@ -0,0 +1,9 @@
[![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

@ -4,7 +4,7 @@
"NGnius (Graham) <ngniusness@gmail.com>" "NGnius (Graham) <ngniusness@gmail.com>"
], ],
"description": "Universal Steam Deck Plugin Library front-end designed for WASM", "description": "Universal Steam Deck Plugin Library front-end designed for WASM",
"version": "0.6.0", "version": "0.6.2",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -9,7 +9,25 @@ export function init_usdpl(port: number): void;
* Get the targeted plugin framework, or "any" if unknown * Get the targeted plugin framework, or "any" if unknown
* @returns {string} * @returns {string}
*/ */
export function target(): 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. * Call a function on the back-end.
* Returns null (None) if this fails for any reason. * Returns null (None) if this fails for any reason.
@ -24,7 +42,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
export interface InitOutput { export interface InitOutput {
readonly memory: WebAssembly.Memory; readonly memory: WebAssembly.Memory;
readonly init_usdpl: (a: number) => void; readonly init_usdpl: (a: number) => void;
readonly target: (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 call_backend: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_0: (a: number) => number; readonly __wbindgen_export_0: (a: number) => number;
readonly __wbindgen_export_1: (a: number, b: number, c: number) => number; readonly __wbindgen_export_1: (a: number, b: number, c: number) => number;

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -2,7 +2,10 @@
/* eslint-disable */ /* eslint-disable */
export const memory: WebAssembly.Memory; export const memory: WebAssembly.Memory;
export function init_usdpl(a: number): void; export function init_usdpl(a: number): void;
export function target(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 call_backend(a: number, b: number, c: number, d: number): number;
export function __wbindgen_export_0(a: number): number; export function __wbindgen_export_0(a: number): number;
export function __wbindgen_export_1(a: number, b: number, c: number): number; export function __wbindgen_export_1(a: number, b: number, c: number): number;