From 1459479342e138b8eb93e4ca89049852497aac8f Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 13 May 2022 14:01:48 -0400 Subject: [PATCH 1/2] Make settings (optionally) persistent --- main.py | 180 ++++++++++++++++++++++++++++++++++++++++++++----- main_view.html | 34 ++++++++++ 2 files changed, 197 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index e71c195..4cf8fb6 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,39 @@ import time import os +import json +import asyncio -VERSION = "0.4.2" +VERSION = "0.5.0" +SETTINGS_LOCATION = "/home/deck/.config/powertools.json" +LOG_LOCATION = "/home/deck/.powertools.log" +FANTASTIC_INSTALL_DIR = "/home/deck/homebrew/plugins/Fantastic" + +import logging + +logging.basicConfig( + filename = LOG_LOCATION, + format = '%(asctime)s %(levelname)s %(message)s', + filemode = 'w', + force = True) + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +logging.info(f"PowerTools v{VERSION}") class CPU: SCALING_FREQUENCIES = [1700000, 2400000, 2800000] - def __init__(self, number): + def __init__(self, number, settings=None): self.number = number + if settings is not None: + self.set_max_boost(settings["max_boost"]) + if settings["online"]: + self.enable() + else: + self.disable() + # TODO governor + if(self.status()): self.max_boost = self._get_max_boost() else: @@ -46,6 +71,16 @@ class CPU: filepath = cpu_online_path(self.number) return read_from_sys(filepath) == "1" + def governor(self) -> str: + return self._read_scaling_governor() + + def settings(self) -> dict: + return { + "online": self.status(), + "max_boost": self.max_boost, + "governor": self.governor(), + } + def _read_scaling_governor(self) -> str: filepath = cpu_governor_scaling_path(self.number) return read_from_sys(filepath, amount=-1).strip() @@ -82,6 +117,8 @@ class Plugin: FAN_SPEEDS = [0, 1000, 2000, 3000, 4000, 5000, 6000] auto_fan = True + persistent = True + modified_settings = False async def get_version(self) -> str: return VERSION @@ -90,6 +127,7 @@ class Plugin: # call from main_view.html with setCPUs(count, smt) async def set_cpus(self, count, smt=True): + self.modified_settings = True cpu_count = len(self.cpus) self.smt = smt # print("Setting CPUs") @@ -121,13 +159,15 @@ class Plugin: return self.smt async def set_boost(self, enabled: bool) -> bool: - write_to_sys("/sys/devices/system/cpu/cpufreq/boost", int(enabled)) + self.modified_settings = True + write_cpu_boost(enabled) return True async def get_boost(self) -> bool: - return read_from_sys("/sys/devices/system/cpu/cpufreq/boost") == "1" + return read_cpu_boost() async def set_max_boost(self, index): + self.modified_settings = True if index < 0 or index >= len(CPU.SCALING_FREQUENCIES): return 0 @@ -144,30 +184,32 @@ class Plugin: # GPU stuff async def set_gpu_power(self, value: int, power_number: int) -> bool: - write_to_sys(gpu_power_path(power_number), value) + self.modified_settings = True + write_gpu_ppt(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()) + return read_gpu_ppt(power_number) # Fan stuff async def set_fan_tick(self, tick: int): + self.modified_settings = True 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"]) + #subprocess.Popen("systemctl start jupiter-fan-control.service", stdout=subprocess.PIPE, shell=True).wait() 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"]) + #subprocess.Popen("systemctl stop jupiter-fan-control.service", stdout=subprocess.PIPE, shell=True).wait() async def get_fan_tick(self) -> int: - fan_target = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_target", amount=-1).strip()) + fan_target = read_fan_target() 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 @@ -176,17 +218,17 @@ class Plugin: 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) + # when fan_target was changed recently (hence set RPM caching) return len(self.FAN_SPEEDS) else: - # quantize voltage to nearest tick (price is right rules; closest without going over) + # quantize RPM 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 async def fantastic_installed(self) -> bool: - return os.path.exists("/home/deck/homebrew/plugins/Fantastic") + return os.path.exists(FANTASTIC_INSTALL_DIR) # Battery stuff @@ -201,14 +243,46 @@ class Plugin: # Asyncio-compatible long-running code, executed in a task when the plugin is loaded async def _main(self): - pass + # startup: load & apply settings + if os.path.exists(SETTINGS_LOCATION): + settings = read_json(SETTINGS_LOCATION) + logging.debug(f"Loaded settings from file: {settings}") + else: + settings = None + if settings is None or settings["persistent"] == False: + self.persistent = False + self.cpus = [] + + for cpu_number in range(0, Plugin.CPU_COUNT): + self.cpus.append(CPU(cpu_number)) + else: + # apply settings + logging.debug("Restoring settings from file") + self.persistent = True + # CPU + self.cpus = [] + + for cpu_number in range(0, Plugin.CPU_COUNT): + self.cpus.append(CPU(cpu_number, settings=settings["cpu"]["threads"][cpu_number])) + self.smt = settings["cpu"]["smt"] + write_cpu_boost(settings["cpu"]["boost"]) + # GPU + write_gpu_ppt(1, settings["gpu"]["slowppt"]) + write_gpu_ppt(2, settings["gpu"]["fastppt"]) + # Fan + if not (os.path.exists(FANTASTIC_INSTALL_DIR) or settings["fan"]["auto"]): + write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1) + write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", settings["fan"]["target"]) + self.dirty = False + # work loop + while True: + if self.modified_settings and self.persistent: + self.save_settings(self) + self.modified_settings = False + await asyncio.sleep(1) # called from main_view::onViewReady async def on_ready(self): - self.cpus = [] - - for cpu_number in range(0, Plugin.CPU_COUNT): - self.cpus.append(CPU(cpu_number)) # If any core has two threads, smt is True self.smt = self.cpus[1].status() @@ -218,6 +292,52 @@ class Plugin: self.smt = True break + # persistence + + async def get_persistent(self) -> bool: + return self.persistent + + async def set_persistent(self, enabled: bool): + logging.debug(f"Persistence is now: {enabled}") + self.persistent = enabled + self.save_settings(self) + + def current_settings(self) -> dict: + settings = dict() + settings["cpu"] = self.current_cpu_settings(self) + settings["gpu"] = self.current_gpu_settings(self) + settings["fan"] = self.current_fan_settings(self) + settings["persistent"] = self.persistent + return settings + + def current_cpu_settings(self) -> dict: + settings = dict() + cpu_settings = [] + for cpu in self.cpus: + cpu_settings.append(cpu.settings()) + settings["threads"] = cpu_settings + settings["smt"] = self.smt + settings["boost"] = read_cpu_boost() + return settings + + def current_gpu_settings(self) -> dict: + settings = dict() + settings["slowppt"] = read_gpu_ppt(1) + settings["fastppt"] = read_gpu_ppt(2) + return settings + + def current_fan_settings(self) -> dict: + settings = dict() + settings["target"] = read_fan_target() + settings["auto"] = self.auto_fan + return settings + + def save_settings(self): + settings = self.current_settings(self) + logging.debug(f"Saving settings to file: {settings}") + write_json(SETTINGS_LOCATION, settings) + + # these are stateless (well, the state is not saved internally) functions, so there's no need for these to be called like a class method @@ -232,6 +352,21 @@ def cpu_governor_scaling_path(cpu_number: int) -> str: def gpu_power_path(power_number: int) -> str: return f"/sys/class/hwmon/hwmon4/power{power_number}_cap" + +def read_cpu_boost() -> bool: + return read_from_sys("/sys/devices/system/cpu/cpufreq/boost") == "1" + +def write_cpu_boost(enable: bool): + write_to_sys("/sys/devices/system/cpu/cpufreq/boost", int(enable)) + +def read_gpu_ppt(power_number: int) -> int: + return read_sys_int(gpu_power_path(power_number)) + +def write_gpu_ppt(power_number:int, value: int): + write_to_sys(gpu_power_path(power_number), value) + +def read_fan_target() -> int: + return read_sys_int("/sys/class/hwmon/hwmon5/fan1_target") def write_to_sys(path, value: int): with open(path, mode="w") as f: @@ -240,3 +375,14 @@ def write_to_sys(path, value: int): def read_from_sys(path, amount=1): with open(path, mode="r") as f: return f.read(amount) + +def read_sys_int(path) -> int: + return int(read_from_sys(path, amount=-1).strip()) + +def write_json(path, data): + with open(path, mode="w") as f: + json.dump(data, f) # I always guess which is which param and I hate it + +def read_json(path): + with open(path, mode="r") as f: + return json.load(f) diff --git a/main_view.html b/main_view.html index 8ad7816..899076d 100644 --- a/main_view.html +++ b/main_view.html @@ -73,6 +73,14 @@ function getChargeDesign() { return call_plugin_method("get_charge_design", {}); } + + function setPersistent(value) { + return call_plugin_method("set_persistent", {"enabled": value}); + } + + function getPersistent() { + return call_plugin_method("get_persistent", {}); + } // other logic @@ -95,6 +103,7 @@ selectNotch("fanNotch", await getFanTick(), 8); } await updateBatteryStats(); + setToggleState(document.getElementById("persistToggle"), await getPersistent()); // this is unimportant; always do it last await updateVersion(); window.setInterval(function() {updateBatteryStats().then(_ => {})}, 5000); @@ -233,6 +242,13 @@ batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(0).toString() + "%)"; } + async function togglePersist() { + let toggle = document.getElementById("persistToggle"); + let isActive = getToggleState(toggle); + await setPersistent(!isActive); + setToggleState(toggle, !isActive); + } + let versionCount = -1; async function updateVersion() { let version = await getVersion(); @@ -535,6 +551,23 @@ +
+
+
+
+
+ Persistent +
+
+
+
+
+
+
+
+
Restores settings after a reboot
+
+
@@ -548,5 +581,6 @@
+
From 2cbf3ec2c668f895f9868eee07d72ffa9e7628f7 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 15 May 2022 11:52:57 -0400 Subject: [PATCH 2/2] Improve logging and docs --- README.md | 1 + main.py | 46 ++++++++++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5418747..ee847bf 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ You will need that installed for this plugin to work. - Set some GPU power parameters (fastPPT & slowPPT) - Set the fan RPM (unsupported on SteamOS beta) - Display supplementary battery info +- Keep settings between restarts (stored in `~/.config/powertools.json`) ## Cool, but that's too much work diff --git a/main.py b/main.py index 4cf8fb6..07f5598 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,9 @@ import json import asyncio VERSION = "0.5.0" -SETTINGS_LOCATION = "/home/deck/.config/powertools.json" -LOG_LOCATION = "/home/deck/.powertools.log" -FANTASTIC_INSTALL_DIR = "/home/deck/homebrew/plugins/Fantastic" +SETTINGS_LOCATION = "~/.config/powertools.json" +LOG_LOCATION = "/tmp/powertools.log" +FANTASTIC_INSTALL_DIR = "~/homebrew/plugins/Fantastic" import logging @@ -17,8 +17,9 @@ logging.basicConfig( force = True) logger = logging.getLogger() -logger.setLevel(logging.DEBUG) -logging.info(f"PowerTools v{VERSION}") +logger.setLevel(logging.INFO) +logging.info(f"PowerTools v{VERSION} https://github.com/NGnius/PowerTools") +startup_time = time.time() class CPU: SCALING_FREQUENCIES = [1700000, 2400000, 2800000] @@ -246,15 +247,27 @@ class Plugin: # startup: load & apply settings if os.path.exists(SETTINGS_LOCATION): settings = read_json(SETTINGS_LOCATION) - logging.debug(f"Loaded settings from file: {settings}") + logging.debug(f"Loaded settings from {SETTINGS_LOCATION}: {settings}") else: settings = None + logging.debug(f"Settings {SETTINGS_LOCATION} does not exist, skipped") if settings is None or settings["persistent"] == False: + logging.debug("Ignoring settings from file") self.persistent = False self.cpus = [] for cpu_number in range(0, Plugin.CPU_COUNT): self.cpus.append(CPU(cpu_number)) + + # If any core has two threads, smt is True + self.smt = self.cpus[1].status() + if(not self.smt): + for cpu_number in range(2, len(self.cpus), 2): + if(self.cpus[cpu_number].status()): + self.smt = True + break + logging.info(f"SMT state is guessed to be {self.smt}") + else: # apply settings logging.debug("Restoring settings from file") @@ -274,6 +287,7 @@ class Plugin: write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1) write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", settings["fan"]["target"]) self.dirty = False + logging.info("Handled saved settings, back-end startup complete") # work loop while True: if self.modified_settings and self.persistent: @@ -283,14 +297,8 @@ class Plugin: # called from main_view::onViewReady async def on_ready(self): - - # If any core has two threads, smt is True - self.smt = self.cpus[1].status() - if(not self.smt): - for cpu_number in range(2, len(self.cpus), 2): - if(self.cpus[cpu_number].status()): - self.smt = True - break + delta = time.time() - startup_time + logging.info(f"Front-end initialised {delta}s after startup") # persistence @@ -334,7 +342,7 @@ class Plugin: def save_settings(self): settings = self.current_settings(self) - logging.debug(f"Saving settings to file: {settings}") + logging.info(f"Saving settings to file: {settings}") write_json(SETTINGS_LOCATION, settings) @@ -371,10 +379,13 @@ def read_fan_target() -> int: def write_to_sys(path, value: int): with open(path, mode="w") as f: f.write(str(value)) + logging.debug(f"Wrote `{value}` to {path}") def read_from_sys(path, amount=1): with open(path, mode="r") as f: - return f.read(amount) + value = f.read(amount) + logging.debug(f"Read `{value}` from {path}") + return value def read_sys_int(path) -> int: return int(read_from_sys(path, amount=-1).strip()) @@ -386,3 +397,6 @@ def write_json(path, data): def read_json(path): with open(path, mode="r") as f: return json.load(f) + +os_release = read_from_sys("/etc/os-release", amount=-1).strip() +logging.info(f"/etc/os-release\n{os_release}")