diff --git a/README.md b/README.md index 4d52803..860460e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # PowerTools -![plugin_demo](https://raw.githubusercontent.com/NGnius/PowerTools/master/extras/ui.png) +![plugin_demo](./extras/ui.png) Steam Deck power tweaks for power users. This is generated from the template plugin for the [SteamOS Plugin Loader](https://github.com/SteamDeckHomebrew/PluginLoader). +You will need that installed for this plugin to work. -## Cool, whatever +## Cool, but that's too much work -Yeah, that's fair. +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. @@ -32,6 +33,43 @@ 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 + +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. + +### 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 + ## License This is licensed under GNU GPLv3. diff --git a/extras/ui.png b/extras/ui.png index a282a17..42dc575 100644 Binary files a/extras/ui.png and b/extras/ui.png differ diff --git a/main.py b/main.py index bf2d8d7..3c84787 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,19 @@ import time +#import subprocess + +VERSION = "0.3.0" class Plugin: CPU_COUNT = 8 SCALING_FREQUENCIES = [1700000, 2400000, 2800000] + FAN_SPEEDS = [0, 1000, 2000, 3000, 4000, 5000, 6000] + + auto_fan = True + + async def get_version(self) -> str: + return VERSION + + # CPU stuff # call from main_view.html with setCPUs(count, smt) async def set_cpus(self, count, smt=True) -> int: @@ -32,7 +43,10 @@ class Plugin: return online_count async def get_smt(self) -> bool: - return status_cpu(1) == status_cpu(2) and status_cpu(3) == status_cpu(4) + for cpu in range(1, self.CPU_COUNT, 2): + if (not status_cpu(cpu)) and status_cpu(cpu+1): + return False + return True async def set_boost(self, enabled: bool) -> bool: write_to_sys("/sys/devices/system/cpu/cpufreq/boost", int(enabled)) @@ -66,6 +80,61 @@ class Plugin: freq = int(freq_maybe) return self.SCALING_FREQUENCIES.index(freq) + # GPU stuff + + async def set_gpu_power(self, value: int, power_number: int) -> bool: + write_to_sys(gpu_power_path(power_number), value) + return True + + async def get_gpu_power(self, power_number: int) -> int: + return int(read_from_sys(gpu_power_path(power_number), amount=-1).strip()) + + # Fan stuff + + async def set_fan_tick(self, tick: int): + if tick >= len(self.FAN_SPEEDS): + # automatic mode + self.auto_fan = True + write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 0) + write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", 4099) # 4099 is default + #subprocess.run(["systemctl", "start", "jupiter-fan-control.service"]) + else: + # manual voltage + self.auto_fan = False + write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1) + write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", self.FAN_SPEEDS[tick]) + #subprocess.run(["systemctl", "stop", "jupiter-fan-control.service"]) + + async def get_fan_tick(self) -> int: + fan_target = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_target", amount=-1).strip()) + fan_input = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_input", amount=-1).strip()) + fan_target_v = float(fan_target) / 1000 + fan_input_v = float(fan_input) / 1000 + if self.auto_fan: + return len(self.FAN_SPEEDS) + elif fan_target == 4099 or (int(round(fan_target_v)) != int(round(fan_input_v)) and fan_target not in self.FAN_SPEEDS): + # cannot read /sys/class/hwmon/hwmon5/recalculate, so guess based on available fan info + # NOTE: the fan takes time to ramp up, so fan_target will never approximately equal fan_input + # when fan_target was changed recently (hence set voltage caching) + return len(self.FAN_SPEEDS) + else: + # quantize voltage to nearest tick (price is right rules; closest without going over) + for i in range(len(self.FAN_SPEEDS)-1): + if fan_target <= self.FAN_SPEEDS[i]: + return i + return len(self.FAN_SPEEDS)-1 # any higher value is considered as highest manual setting + + # Battery stuff + + async def get_charge_now(self) -> int: + return int(read_from_sys("/sys/class/hwmon/hwmon2/device/charge_now", amount=-1).strip()) + + async def get_charge_full(self) -> int: + return int(read_from_sys("/sys/class/hwmon/hwmon2/device/charge_full", amount=-1).strip()) + + async def get_charge_design(self) -> int: + return int(read_from_sys("/sys/class/hwmon/hwmon2/device/charge_full_design", amount=-1).strip()) + # Asyncio-compatible long-running code, executed in a task when the plugin is loaded async def _main(self): pass @@ -81,6 +150,9 @@ def cpu_freq_scaling_path(cpu_number: int) -> str: def cpu_governor_scaling_path(cpu_number: int) -> str: return f"/sys/devices/system/cpu/cpu{cpu_number}/cpufreq/scaling_governor" + +def gpu_power_path(power_number: int) -> str: + return f"/sys/class/hwmon/hwmon4/power{power_number}_cap" def write_to_sys(path, value: int): with open(path, mode="w") as f: diff --git a/main_view.html b/main_view.html index 294f330..380efb1 100644 --- a/main_view.html +++ b/main_view.html @@ -6,6 +6,10 @@ - + + + +
Disables odd-numbered CPUs
+
@@ -199,10 +320,12 @@
+
Allows the CPU to go above max frequency
+
@@ -236,5 +359,160 @@ WARNING: This will change the CPU governor.
+ + + + +
+
+
GPU SlowPPT Power
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
Auto
+
+
+
+
Max
+
+
+
+
+
+ + +
+
+
GPU FastPPT Power
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
Auto
+
+
+
+
Max
+
+
+
+
+
+ + + +
+
+
Fan RPM
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
1K
+
+
+
+
2K
+
+
+
+
3K
+
+
+
+
4K
+
+
+
+
5K
+
+
+
+
6K
+
+
+
+
Auto
+
+
+
+
+
+ + +
+
+
Battery
+
+
+
+
+
+
Now (Charge%)
+
+
:'( (|-_-|)
+
+
+
+
+
+
+
+
Max (Health%)
+
+
9000+ (420%)
+
+
+
+
+
+
+
+
+
+
PowerTools
+
+
v0.42.0
+
+
+
+