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 e71c195..07f5598 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,40 @@ import time import os +import json +import asyncio -VERSION = "0.4.2" +VERSION = "0.5.0" +SETTINGS_LOCATION = "~/.config/powertools.json" +LOG_LOCATION = "/tmp/powertools.log" +FANTASTIC_INSTALL_DIR = "~/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.INFO) +logging.info(f"PowerTools v{VERSION} https://github.com/NGnius/PowerTools") +startup_time = time.time() 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 +72,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 +118,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 +128,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 +160,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 +185,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 +219,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,22 +244,107 @@ 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 {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") + 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 + logging.info("Handled saved settings, back-end startup complete") + # 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 = [] + delta = time.time() - startup_time + logging.info(f"Front-end initialised {delta}s after startup") - for cpu_number in range(0, Plugin.CPU_COUNT): - self.cpus.append(CPU(cpu_number)) + # 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.info(f"Saving settings to file: {settings}") + write_json(SETTINGS_LOCATION, settings) - # 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 # 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,11 +360,43 @@ 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: 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()) + +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) + +os_release = read_from_sys("/etc/os-release", amount=-1).strip() +logging.info(f"/etc/os-release\n{os_release}") 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 @@ +