From 9ec9a0cf4018fb6c5b92d1edb2b2b44830face38 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 22 May 2022 17:47:33 -0400 Subject: [PATCH 01/11] Implement basic current game detection --- main.py | 41 ++++++++++++---- main_view.html | 43 ++++++++++++++++- server.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 server.py diff --git a/main.py b/main.py index 07f5598..e7819f5 100644 --- a/main.py +++ b/main.py @@ -2,11 +2,13 @@ import time import os import json import asyncio +import pathlib -VERSION = "0.5.0" -SETTINGS_LOCATION = "~/.config/powertools.json" +VERSION = "0.6.0" +HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve()) +DEFAULT_SETTINGS_LOCATION = HOME_DIR + "/.config/powertools/default_settings.json" LOG_LOCATION = "/tmp/powertools.log" -FANTASTIC_INSTALL_DIR = "~/homebrew/plugins/Fantastic" +FANTASTIC_INSTALL_DIR = HOME_DIR + "/homebrew/plugins/Fantastic" import logging @@ -17,8 +19,15 @@ logging.basicConfig( force = True) logger = logging.getLogger() -logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) logging.info(f"PowerTools v{VERSION} https://github.com/NGnius/PowerTools") +logging.info(f"CWD: {os.getcwd()} HOME:{HOME_DIR}") + +import sys +#import pathlib +sys.path.append(str(pathlib.Path(__file__).parent.resolve())) +import server as pt_server + startup_time = time.time() class CPU: @@ -120,6 +129,8 @@ class Plugin: auto_fan = True persistent = True modified_settings = False + current_game = None # None means main menu + last_recognised_game = None async def get_version(self) -> str: return VERSION @@ -245,12 +256,12 @@ class Plugin: # Asyncio-compatible long-running code, executed in a task when the plugin is loaded async def _main(self): # startup: load & apply settings - if os.path.exists(SETTINGS_LOCATION): - settings = read_json(SETTINGS_LOCATION) - logging.debug(f"Loaded settings from {SETTINGS_LOCATION}: {settings}") + if os.path.exists(DEFAULT_SETTINGS_LOCATION): + settings = read_json(DEFAULT_SETTINGS_LOCATION) + logging.debug(f"Loaded settings from {DEFAULT_SETTINGS_LOCATION}: {settings}") else: settings = None - logging.debug(f"Settings {SETTINGS_LOCATION} does not exist, skipped") + logging.debug(f"Settings {DEFAULT_SETTINGS_LOCATION} does not exist, skipped") if settings is None or settings["persistent"] == False: logging.debug("Ignoring settings from file") self.persistent = False @@ -288,12 +299,15 @@ class Plugin: write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", settings["fan"]["target"]) self.dirty = False logging.info("Handled saved settings, back-end startup complete") + # server setup + await pt_server.start(VERSION) # work loop while True: if self.modified_settings and self.persistent: self.save_settings(self) self.modified_settings = False await asyncio.sleep(1) + await pt_server.shutdown() # called from main_view::onViewReady async def on_ready(self): @@ -343,7 +357,16 @@ class Plugin: def save_settings(self): settings = self.current_settings(self) logging.info(f"Saving settings to file: {settings}") - write_json(SETTINGS_LOCATION, settings) + write_json(DEFAULT_SETTINGS_LOCATION, settings) + + # per-game profiles + + async def get_current_game(self) -> str: + current_game = pt_server.http_server.game() + if current_game is None: + return "Menu (default)" + else: + return f"{current_game.name()} ({current_game.appid()})" diff --git a/main_view.html b/main_view.html index 899076d..c9de4ba 100644 --- a/main_view.html +++ b/main_view.html @@ -81,12 +81,35 @@ function getPersistent() { return call_plugin_method("get_persistent", {}); } + + function getCurrentGame() { + return call_plugin_method("get_current_game", {}); + } // other logic async function onReady() { await onViewReady(); + // detect game starts and exits + console.log("Injecting game detection code into main window (SP)"); + await execute_in_tab("SP", false, + `SteamClient.Apps.RegisterForGameActionStart((actionType, data) => { + console.log("start game", appStore.GetAppOverviewByGameID(data)); + fetch("http://127.0.0.1:5030/on_game_start/" + data.toString(), {method: "POST", body: JSON.stringify(appStore.GetAppOverviewByGameID(data))}).then((_) => {}); + }); + // this seems to not run when I thought (runs right after ^^^, not when game exits) + /*SteamClient.Apps.RegisterForGameActionEnd((actionType, data) => { + if (data != null && data != undefined) { + console.log("stop game", appStore.GetAppOverviewByGameID(data)); + fetch("http://127.0.0.1:5030/on_game_exit/" + data.toString(), {method: "POST", body: JSON.stringify(appStore.GetAppOverviewByGameID(data))}).then((_) => {}); + } else { + console.log("stop game null"); + fetch("http://127.0.0.1:5030/on_game_exit_null", {method: "POST", body:data}).then((_) => {}); + } + });*/` + ); + let boostToggle = document.getElementById("boostToggle"); setToggleState(boostToggle, await getCPUBoost()); setToggleState(document.getElementById("smtToggle"), await getSMT()); @@ -106,7 +129,10 @@ setToggleState(document.getElementById("persistToggle"), await getPersistent()); // this is unimportant; always do it last await updateVersion(); - window.setInterval(function() {updateBatteryStats().then(_ => {})}, 5000); + window.setInterval(function() { + updateBatteryStats().then(_ => {}); + updateCurrentGame().then(_ => {}); + }, 5000); } async function setCPUNotch(index) { @@ -249,6 +275,12 @@ setToggleState(toggle, !isActive); } + async function updateCurrentGame() { + let gameNow = document.getElementById("gameNow"); + let gameNameNow = await getCurrentGame(); + gameNow.innerText = gameNameNow; + } + let versionCount = -1; async function updateVersion() { let version = await getVersion(); @@ -566,6 +598,15 @@
Restores settings after a reboot
+
+
+
Now Playing
+
+
Littlewood (894940)
+
+
+
+
diff --git a/server.py b/server.py new file mode 100644 index 0000000..28cfc7f --- /dev/null +++ b/server.py @@ -0,0 +1,124 @@ +import logging +import json +import os +import pathlib + +import asyncio +from aiohttp import web + +HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve()) +SETTINGS_DIR = HOME_DIR + "/.config/powertools" + +if not os.path.exists(SETTINGS_DIR): + os.mkdir(SETTINGS_DIR) + +http_runner = None +http_server = None + +class GameInfo: + def __init__(self, gameid: int, game_info: dict): + self.gameid = gameid + self.game_info = game_info + + def appid(self): + return self.game_info["appid"] + + def name(self): + return self.game_info["display_name"] + + def settings_path(self) -> str: + return SETTINGS_DIR + os.pathsep + str(self.appid()) + ".json" + + def load_settings(self) -> dict: + settings_path = self.settings_path() + if os.exists(settings_path): + with open(settings_path, mode="r") as f: + return json.load(f) + return None + + def has_settings(self) -> bool: + return os.exists(self.settings_path()) + + +class Server(web.Application): + + def __init__(self, version): + super().__init__() + self.version = version + self.current_game = None + self.last_recognised_game = None + self.add_routes([ + web.get("/", lambda req: self.index(req)), + web.post("/on_game_start/{game_id}", lambda req: self.on_game_start(req)), + web.post("/on_game_exit/{game_id}", lambda req: self.on_game_exit(req)), + web.post("/on_game_exit_null", lambda req: self.on_game_exit_null(req)) + ]) + logging.debug("Server init complete") + + def game(self) -> GameInfo: + return self.current_game + + def recognised_game(self) -> GameInfo: + return self.last_recognised_game + + async def index(self, request): + logging.debug("Debug index page accessed") + return web.json_response({ + "name": "PowerTools", + "version": self.version, + "latest_game_id": self.current_game, + "latest_recognised_game_id": self.last_recognised_game, + }, headers={"Access-Control-Allow-Origin": "*"}) + + async def on_game_start(self, request): + game_id = request.match_info["game_id"] + data = await request.text() + logging.debug(f"on_game_start {game_id} body:\n{data}") + try: + game_id = int(game_id) + data = json.loads(data) + except: + return web.Response(text="WTF", status=400) + self.current_game = GameInfo(game_id, data) + if True: # TODO check for game_id in existing profiles + self.last_recognised_game = self.current_game # only set this when profile exists + # TODO apply profile + return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"}) + + async def on_game_exit(self, request): + # ignored for now + game_id = request.match_info["game_id"] + data = await request.text() + logging.debug(f"on_game_exit {game_id}") + try: + game_id = int(game_id) + except ValueError: + return web.Response(text="WTF", status=400) + if self.current_game.gameid == game_id: + pass + #self.current_game = None + # TODO change settings to default + return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"}) + + async def on_game_exit_null(self, request): + # ignored for now + logging.info(f"on_game_exit_null") + #self.current_game = None + # TODO change settings to default + return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"}) + +async def start(version): + global http_runner, http_server + loop = asyncio.get_event_loop() + http_server = Server(version) + http_runner = web.AppRunner(http_server) + await http_runner.setup() + site = web.TCPSite(http_runner, '0.0.0.0', 5030) + await site.start() + +async def shutdown(): # never really called + global http_runner, http_server + if http_runner is not None: + await http_runner.cleanup() + http_runner = None + http_server = None From eac9f95ed706952fcd0842b676d45f9e31305377 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 22 May 2022 17:49:19 -0400 Subject: [PATCH 02/11] Add .## decimal to battery stats --- main_view.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main_view.html b/main_view.html index c9de4ba..04c6c2c 100644 --- a/main_view.html +++ b/main_view.html @@ -264,8 +264,8 @@ let chargeNow = await getChargeNow(); let chargeFull = await getChargeFull(); let chargeDesign = await getChargeDesign(); - batCapacityNow.innerText = (7.7 * chargeNow / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeNow / chargeFull).toFixed(0).toString() + "%)"; - batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(0).toString() + "%)"; + batCapacityNow.innerText = (7.7 * chargeNow / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeNow / chargeFull).toFixed(2).toString() + "%)"; + batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(2).toString() + "%)"; } async function togglePersist() { From d023f42ac552ff1a4c918594673b5d3ff2359eb6 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 22 May 2022 21:08:21 -0400 Subject: [PATCH 03/11] Improve slider input --- main_view.html | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/main_view.html b/main_view.html index 04c6c2c..5ed1539 100644 --- a/main_view.html +++ b/main_view.html @@ -141,6 +141,12 @@ selectNotch(ROOT_ID, await getCPUs() - 1, 8); } + async function onSlideCPUNotch(e) { + const ROOT_ID = "cpuThreadsNotch"; + let closest = closestNotch(e, ROOT_ID, 8); + await setCPUNotch(closest); + } + const TOGGLE_ON_CLASS = "gamepaddialog_On_3ld7T"; function setToggleState(toggle, state) { @@ -185,12 +191,24 @@ selectNotch(ROOT_ID, await getMaxBoost(), 3); } + async function onSlideBoostNotch(e) { + const ROOT_ID = "frequencyNotch"; + let closest = closestNotch(e, ROOT_ID, 8); + await setBoostNotch(closest); + } + async function onSetFanNotch(index) { const ROOT_ID = "fanNotch"; await setFanTick(index); selectNotch(ROOT_ID, index, 8); } + async function onSlideFanNotch(e) { + const ROOT_ID = "fanNotch"; + let closest = closestNotch(e, ROOT_ID, 8); + await onSetFanNotch(closest); + } + async function onReadyGPU() { let power1_cap = await getGPUPower(1); let power2_cap = await getGPUPower(2); @@ -228,6 +246,12 @@ selectNotch(ROOT_ID, index, 3); } + async function onSlideSlowPPTNotch(e) { + const ROOT_ID = "slowPPTNotch"; + let closest = closestNotch(e, ROOT_ID, 3); + onSetSlowPPTNotch(closest); + } + async function onSetFastPPTNotch(index) { const ROOT_ID = "fastPPTNotch"; document.getElementById("fastPPTAutoDefault").innerText = "Default"; @@ -241,6 +265,12 @@ selectNotch(ROOT_ID, index, 3); } + async function onSlideFastPPTNotch(e) { + const ROOT_ID = "fastPPTNotch"; + let closest = closestNotch(e, ROOT_ID, 3); + await onSetFastPPTNotch(closest); + } + function selectNotch(rootId, index, elements) { // WARNING: this yeets any style in div of slider const ENABLED_CLASS = "gamepadslider_TickActive_1gnUV"; @@ -257,6 +287,19 @@ } } + function closestNotch(e, rootId, elements) { + let root = document.getElementById(rootId); + let val = e.x / root.scrollWidth; + let closest_notch = Math.round(val * elements); + if (closest_notch > elements) { + closest_notch = elements; + } else if (closest_notch < 0) { + closest_notch = 0; + } + return closest_notch + //selectNotch(closest_notch); + } + async function updateBatteryStats() { //console.log("Updating battery stats"); let batCapacityNow = document.getElementById("batCapacityNow"); @@ -330,7 +373,7 @@
Threads
-
+
From 7158ac3596f9e8ec93e05b87bc853973540e695b Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 22 May 2022 22:16:06 -0400 Subject: [PATCH 04/11] Add per-game profile loading and fix stuff --- main.py | 111 ++++++++++++++++++++++++++++++++++--------------- main_view.html | 110 +++++++++++++++++++++++++++++++++++++----------- server.py | 48 +++++++++++++-------- 3 files changed, 193 insertions(+), 76 deletions(-) diff --git a/main.py b/main.py index e7819f5..1cdc206 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ logging.basicConfig( logger = logging.getLogger() logger.setLevel(logging.DEBUG) logging.info(f"PowerTools v{VERSION} https://github.com/NGnius/PowerTools") -logging.info(f"CWD: {os.getcwd()} HOME:{HOME_DIR}") +logging.debug(f"CWD: {os.getcwd()} HOME:{HOME_DIR}") import sys #import pathlib @@ -129,8 +129,7 @@ class Plugin: auto_fan = True persistent = True modified_settings = False - current_game = None # None means main menu - last_recognised_game = None + current_gameid = None async def get_version(self) -> str: return VERSION @@ -265,47 +264,44 @@ class Plugin: 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}") - + self.guess_settings(self) + self.modified_settings = True 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 + self.apply_settings(self, settings) + # self.modified_settings = False logging.info("Handled saved settings, back-end startup complete") # server setup await pt_server.start(VERSION) # work loop while True: + # persistence if self.modified_settings and self.persistent: self.save_settings(self) self.modified_settings = False + if self.persistent: + # per-game profiles + current_game = pt_server.http_server.game() + old_gameid = self.current_gameid + if current_game is not None and current_game.has_settings(): + self.current_gameid = current_game.gameid + if old_gameid != self.current_gameid: + logging.info(f"Applying custom settings for {current_game.name()} {current_game.appid()}") + # new game; apply settings + settings = current_game.load_settings() + if settings is not None: + self.apply_settings(self, settings) + else: + self.current_gameid = None + if old_gameid != self.current_gameid: + logging.info("Reapplying default settings; game without custom settings found") + # game without custom settings; apply defaults + settings = read_json(DEFAULT_SETTINGS_LOCATION) + self.apply_settings(self, settings) + logging.debug(f"gameid update: {old_gameid} -> {self.current_gameid}") + await asyncio.sleep(1) await pt_server.shutdown() @@ -356,8 +352,44 @@ class Plugin: def save_settings(self): settings = self.current_settings(self) - logging.info(f"Saving settings to file: {settings}") - write_json(DEFAULT_SETTINGS_LOCATION, settings) + logging.debug(f"Saving settings to file: {settings}") + current_game = pt_server.http_server.game() + if current_game is not None and self.current_gameid is not None: + save_location = current_game.settings_path() + else: + save_location = DEFAULT_SETTINGS_LOCATION + write_json(save_location, settings) + logging.info(f"Saved settings to {save_location}") + + def apply_settings(self, settings: dict): + # 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"]) + + def guess_settings(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() + 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}") # per-game profiles @@ -368,6 +400,17 @@ class Plugin: else: return f"{current_game.name()} ({current_game.appid()})" + async def set_per_game_profile(self, enabled: bool): + current_game = pt_server.http_server.game() + if enabled and self.persistent and current_game is not None: + self.current_gameid = current_game.gameid + self.modified_settings = True + else: + self.current_gameid = None + + async def get_per_game_profile(self) -> bool: + return self.current_gameid is not None + # 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 diff --git a/main_view.html b/main_view.html index 5ed1539..49fea6d 100644 --- a/main_view.html +++ b/main_view.html @@ -82,6 +82,14 @@ return call_plugin_method("get_persistent", {}); } + function setPerGameProfile(value) { + return call_plugin_method("set_per_game_profile", {"enabled": value}); + } + + function getPerGameProfile() { + return call_plugin_method("get_per_game_profile", {}); + } + function getCurrentGame() { return call_plugin_method("get_current_game", {}); } @@ -94,7 +102,8 @@ // detect game starts and exits console.log("Injecting game detection code into main window (SP)"); await execute_in_tab("SP", false, - `SteamClient.Apps.RegisterForGameActionStart((actionType, data) => { + `console.log("Hey PowerTools is over here now too!"); + SteamClient.Apps.RegisterForGameActionStart((actionType, data) => { console.log("start game", appStore.GetAppOverviewByGameID(data)); fetch("http://127.0.0.1:5030/on_game_start/" + data.toString(), {method: "POST", body: JSON.stringify(appStore.GetAppOverviewByGameID(data))}).then((_) => {}); }); @@ -110,6 +119,35 @@ });*/` ); + await updateCurrentGame(); + /*let boostToggle = document.getElementById("boostToggle"); + setToggleState(boostToggle, await getCPUBoost()); + setToggleState(document.getElementById("smtToggle"), await getSMT()); + selectNotch("cpuThreadsNotch", await getCPUs() - 1, 8); + selectNotch("frequencyNotch", await getMaxBoost(), 3); + await onReadyGPU(); + let isFantasticInstalled = await getFantastic(); + if (isFantasticInstalled) { + // Don't fight with Fantastic + let fanRoot = document.getElementById("fanRoot"); + fanRoot.style.visibility = "hidden"; + fanRoot.style.height = "0px"; + } else { + selectNotch("fanNotch", await getFanTick(), 8); + } + await updateBatteryStats(); + setToggleState(document.getElementById("persistToggle"), await getPersistent()); + setToggleState(document.getElementById("gameProfileToggle"), await getPerGameProfile()); + await updateCurrentGame();*/ + // this is unimportant; always do it last + await updateVersion(); + window.setInterval(function() { + updateBatteryStats().then(_ => {}); + updateCurrentGame().then(_ => {}); + }, 1000); + } + + async function reloadSettings() { let boostToggle = document.getElementById("boostToggle"); setToggleState(boostToggle, await getCPUBoost()); setToggleState(document.getElementById("smtToggle"), await getSMT()); @@ -127,12 +165,7 @@ } await updateBatteryStats(); setToggleState(document.getElementById("persistToggle"), await getPersistent()); - // this is unimportant; always do it last - await updateVersion(); - window.setInterval(function() { - updateBatteryStats().then(_ => {}); - updateCurrentGame().then(_ => {}); - }, 5000); + setToggleState(document.getElementById("gameProfileToggle"), await getPerGameProfile()); } async function setCPUNotch(index) { @@ -193,7 +226,7 @@ async function onSlideBoostNotch(e) { const ROOT_ID = "frequencyNotch"; - let closest = closestNotch(e, ROOT_ID, 8); + let closest = closestNotch(e, ROOT_ID, 3); await setBoostNotch(closest); } @@ -318,9 +351,23 @@ setToggleState(toggle, !isActive); } + async function toggleGameProfile() { + let toggle = document.getElementById("gameProfileToggle"); + let isActive = getToggleState(toggle); + await setPerGameProfile(!isActive); + setToggleState(toggle, await getPerGameProfile()); + } + + let lastGameName = ""; + async function updateCurrentGame() { let gameNow = document.getElementById("gameNow"); let gameNameNow = await getCurrentGame(); + if (lastGameName != gameNameNow) { + setToggleState(document.getElementById("gameProfileToggle"), await getPerGameProfile()); + await reloadSettings(); + } + lastGameName = gameNameNow gameNow.innerText = gameNameNow; } @@ -369,7 +416,7 @@ -
+
Threads
@@ -440,12 +487,12 @@ -
+
Max Frequency
-
+
@@ -485,7 +532,7 @@
GPU SlowPPT Power
-
+
@@ -511,12 +558,12 @@
-
+
GPU FastPPT Power
-
+
@@ -545,12 +592,12 @@ -
+
Fan RPM
-
+
@@ -605,7 +652,7 @@
-
+
Now (Charge)
@@ -615,7 +662,7 @@
-
+
Max (Design)
@@ -640,21 +687,34 @@
-
Restores settings after a reboot
-
-
-
Now Playing
-
-
Littlewood (894940)
+
Restores settings after an app or OS restart
+
+
+
+
+ Use per-game profile +
+
+
+
+
+
+
+
Now Playing
+
+
the bongos
+
+
+
-
+
PowerTools
diff --git a/server.py b/server.py index 28cfc7f..3ce6db6 100644 --- a/server.py +++ b/server.py @@ -3,8 +3,9 @@ import json import os import pathlib -import asyncio +# import asyncio from aiohttp import web +import aiohttp HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve()) SETTINGS_DIR = HOME_DIR + "/.config/powertools" @@ -14,6 +15,7 @@ if not os.path.exists(SETTINGS_DIR): http_runner = None http_server = None +http_site = None class GameInfo: def __init__(self, gameid: int, game_info: dict): @@ -27,17 +29,17 @@ class GameInfo: return self.game_info["display_name"] def settings_path(self) -> str: - return SETTINGS_DIR + os.pathsep + str(self.appid()) + ".json" + return SETTINGS_DIR + os.path.sep + str(self.appid()) + ".json" def load_settings(self) -> dict: settings_path = self.settings_path() - if os.exists(settings_path): + if os.path.exists(settings_path): with open(settings_path, mode="r") as f: return json.load(f) return None def has_settings(self) -> bool: - return os.exists(self.settings_path()) + return os.path.exists(self.settings_path()) class Server(web.Application): @@ -46,28 +48,27 @@ class Server(web.Application): super().__init__() self.version = version self.current_game = None - self.last_recognised_game = None self.add_routes([ web.get("/", lambda req: self.index(req)), web.post("/on_game_start/{game_id}", lambda req: self.on_game_start(req)), web.post("/on_game_exit/{game_id}", lambda req: self.on_game_exit(req)), - web.post("/on_game_exit_null", lambda req: self.on_game_exit_null(req)) + web.post("/on_game_exit_null", lambda req: self.on_game_exit_null(req)), + web.get("/self_destruct", lambda req: self.self_destruct(req)) ]) logging.debug("Server init complete") def game(self) -> GameInfo: return self.current_game - def recognised_game(self) -> GameInfo: - return self.last_recognised_game - async def index(self, request): logging.debug("Debug index page accessed") + current_game = None if self.current_game is None else self.current_game.gameid + game_info = None if self.current_game is None else self.current_game.game_info return web.json_response({ "name": "PowerTools", "version": self.version, - "latest_game_id": self.current_game, - "latest_recognised_game_id": self.last_recognised_game, + "latest_game_id": current_game, + "game_info": game_info }, headers={"Access-Control-Allow-Origin": "*"}) async def on_game_start(self, request): @@ -80,9 +81,8 @@ class Server(web.Application): except: return web.Response(text="WTF", status=400) self.current_game = GameInfo(game_id, data) - if True: # TODO check for game_id in existing profiles - self.last_recognised_game = self.current_game # only set this when profile exists - # TODO apply profile + if self.current_game.has_settings(): + self.last_recognised_game = self.current_game return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"}) async def on_game_exit(self, request): @@ -107,9 +107,21 @@ class Server(web.Application): # TODO change settings to default return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"}) + async def self_destruct(self, request): + logging.warning("Geodude self-destructed") + await shutdown() + # unreachable \/ \/ \/ + return web.Response(status=204, headers={"Access-Control-Allow-Origin": "*"}) + async def start(version): - global http_runner, http_server - loop = asyncio.get_event_loop() + global http_runner, http_server, http_site + # make sure old server has shutdown + try: + async with aiohttp.ClientSession() as session: + async with session.get('http://127.0.0.1:5030/self_destruct') as response: + await response.text() + except: + pass http_server = Server(version) http_runner = web.AppRunner(http_server) await http_runner.setup() @@ -117,8 +129,10 @@ async def start(version): await site.start() async def shutdown(): # never really called - global http_runner, http_server + global http_runner, http_server, http_site if http_runner is not None: await http_runner.cleanup() http_runner = None + http_site.stop() + http_site = None http_server = None From be0b8717602584b6aafa83f42ce25481e98094bf Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 23 May 2022 18:23:26 -0400 Subject: [PATCH 05/11] Update README, fix some per-game profile functionality --- README.md | 20 +++++++++++++++++++- main.py | 3 +++ server.py | 6 ++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee847bf..3d18495 100644 --- a/README.md +++ b/README.md @@ -14,7 +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`) +- Keep settings between restarts (stored in `~/.config/powertools/.json`) ## Cool, but that's too much work @@ -79,6 +79,24 @@ 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 + +The biggest limitation right now is it can't detect a game closing -- only opening -- and only after PowerTools is looked at at least once (per SteamOS restart). + +From a plugin, this can be accomplished by running some front-end Javascript. + +```javascript +await execute_in_tab("SP", false, + `SteamClient.Apps.RegisterForGameActionStart((actionType, data) => { + console.log("start game", appStore.GetAppOverviewByGameID(data)); + });` +); +``` + +In PowerTools, the callback (the part surrounded by `{` and `}`, containing `console.log(...)`) sends a message to a local HTTP server to notify the PowerTools back-end that a game has been launched. + +If you go to `http://127.0.0.1:5030` on your Steam Deck with PowerTools >=0.6.0, you can see some info about the last game you launched. + ## License This is licensed under GNU GPLv3. diff --git a/main.py b/main.py index 1cdc206..4cd2a64 100644 --- a/main.py +++ b/main.py @@ -406,6 +406,9 @@ class Plugin: self.current_gameid = current_game.gameid self.modified_settings = True else: + if not enabled and current_game is not None and current_game.has_settings(): + # delete settings; disable settings loading + os.remove(current_game.settings_path()) self.current_gameid = None async def get_per_game_profile(self) -> bool: diff --git a/server.py b/server.py index 3ce6db6..3e2e799 100644 --- a/server.py +++ b/server.py @@ -64,11 +64,13 @@ class Server(web.Application): logging.debug("Debug index page accessed") current_game = None if self.current_game is None else self.current_game.gameid game_info = None if self.current_game is None else self.current_game.game_info + settings_info = None if self.current_game is None else self.current_game.load_settings() return web.json_response({ "name": "PowerTools", "version": self.version, "latest_game_id": current_game, - "game_info": game_info + "game_info": game_info, + "settings": settings_info }, headers={"Access-Control-Allow-Origin": "*"}) async def on_game_start(self, request): @@ -125,7 +127,7 @@ async def start(version): http_server = Server(version) http_runner = web.AppRunner(http_server) await http_runner.setup() - site = web.TCPSite(http_runner, '0.0.0.0', 5030) + site = web.TCPSite(http_runner, '127.0.0.1', 5030) await site.start() async def shutdown(): # never really called From 6e2738d12c843804b5e65e570bf4c7d6bfa91f37 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 26 May 2022 20:02:46 -0400 Subject: [PATCH 06/11] Increase toggle clickable area to text area --- main_view.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main_view.html b/main_view.html index 49fea6d..3af1779 100644 --- a/main_view.html +++ b/main_view.html @@ -395,13 +395,13 @@
-
+
CPU SMT
-
+
@@ -469,13 +469,13 @@
-
+
CPU Boost
-
+
@@ -675,13 +675,13 @@
-
+
Persistent
-
+
@@ -689,13 +689,13 @@
Restores settings after an app or OS restart
-
+
Use per-game profile
-
+
From 1e0428c3c8307bdc07d491c74e7d375e0cae57f5 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 26 May 2022 20:03:59 -0400 Subject: [PATCH 07/11] Remove warning about MangoHUD, close #2 --- main_view.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/main_view.html b/main_view.html index 3af1779..8c85ffa 100644 --- a/main_view.html +++ b/main_view.html @@ -408,9 +408,6 @@
Enables odd-numbered CPUs
-
- WARNING: Disabling crashes the performance overlay. -
From 2433510db7923808896b424400aed69ae3a22aa0 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 26 May 2022 20:07:53 -0400 Subject: [PATCH 08/11] Add note about fan control --- main_view.html | 1 + 1 file changed, 1 insertion(+) diff --git a/main_view.html b/main_view.html index 8c85ffa..3d1ab0e 100644 --- a/main_view.html +++ b/main_view.html @@ -593,6 +593,7 @@
Fan RPM
+
Requires disabling updated fan control
From e745a849ee0b7bf3a5da878e2596cb329b1643e4 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 26 May 2022 20:33:32 -0400 Subject: [PATCH 09/11] Fix styling to remove horizontal overflow --- main_view.html | 380 ++++++++++++++++++++++++++----------------------- 1 file changed, 199 insertions(+), 181 deletions(-) diff --git a/main_view.html b/main_view.html index 3d1ab0e..726ddd9 100644 --- a/main_view.html +++ b/main_view.html @@ -388,12 +388,19 @@ + +
+
+ +
+
-
+
@@ -412,59 +419,61 @@
- -
-
-
Threads
-
-
-
-
-
-
+ +
+
+
+
Threads
-
-
-
-
-
1
-
-
-
-
2
-
-
-
-
3
-
-
-
-
4
-
-
-
-
5
-
-
-
-
6
-
-
-
-
7
-
-
-
-
8
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
2
+
+
+
+
3
+
+
+
+
4
+
+
+
+
5
+
+
+
+
6
+
+
+
+
7
+
+
+
+
8
+
+
-
-
+
@@ -483,168 +492,176 @@
- -
-
-
Max Frequency
-
-
-
-
-
-
-
+ +
+
+
+
Max Frequency
-
-
-
-
-
1.7GHz
+
+
+
+
+
+
+
+
+
+
+
+
1.7GHz
+
+
+
+
2.4GHz
+
+
+
+
2.8GHz
+
+
-
-
-
2.4GHz
-
-
-
2.8GHz
+
+ WARNING: This will change the CPU governor.
-
-
- WARNING: This will change the CPU governor. -
-
-
-
+
- -
-
-
GPU SlowPPT Power
-
-
-
-
-
-
-
+ +
+
+
+
GPU SlowPPT Power
-
-
-
-
-
0
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
Auto
+
+
+
+
Max
+
+
-
-
-
Auto
-
-
-
-
Max
-
-
- -
-
-
GPU FastPPT Power
-
-
-
-
-
-
-
+ +
+
+
+
GPU FastPPT Power
-
-
-
-
-
0
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
Auto
+
+
+
+
Max
+
+
-
-
-
Auto
-
-
-
-
Max
-
-
-
- -
-
-
Fan RPM
-
-
Requires disabling updated fan control
-
-
-
-
-
-
+
+
+ +
+
+
Fan RPM
+
+
Requires disabling updated fan control
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
1K
+
+
+
+
2K
+
+
+
+
3K
+
+
+
+
4K
+
+
+
+
5K
+
+
+
+
6K
+
+
+
+
Auto
+
+
+
+
+
+ WARNING: This can cause component overheating.
-
-
-
-
0
-
-
-
-
1K
-
-
-
-
2K
-
-
-
-
3K
-
-
-
-
4K
-
-
-
-
5K
-
-
-
-
6K
-
-
-
-
Auto
-
-
-
-
-
- WARNING: This can cause component overheating.
-
+
Battery
@@ -671,7 +688,7 @@
-
+
@@ -687,6 +704,8 @@
Restores settings after an app or OS restart
+
+
@@ -700,6 +719,8 @@
+
+
Now Playing
@@ -709,8 +730,6 @@
-
-
@@ -723,6 +742,5 @@
-
From c8ee099fcc9a7ee485e54395cc4c8766029098ee Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Thu, 26 May 2022 20:42:27 -0400 Subject: [PATCH 10/11] Reduce battery precision by on decimal place for everything so that it fits on one line --- main_view.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main_view.html b/main_view.html index 726ddd9..583d9a9 100644 --- a/main_view.html +++ b/main_view.html @@ -340,8 +340,8 @@ let chargeNow = await getChargeNow(); let chargeFull = await getChargeFull(); let chargeDesign = await getChargeDesign(); - batCapacityNow.innerText = (7.7 * chargeNow / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeNow / chargeFull).toFixed(2).toString() + "%)"; - batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(2).toString() + "%)"; + batCapacityNow.innerText = (7.7 * chargeNow / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeNow / chargeFull).toFixed(1).toString() + "%)"; + batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(1).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(1).toString() + "%)"; } async function togglePersist() { From 249e86e8df18f01942e1e14e809b2ddcf0fd050f Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sat, 28 May 2022 11:15:19 -0400 Subject: [PATCH 11/11] Automatically disable jupiter-fan-control when appropriate; hide warning about it --- main.py | 31 +++++++++++++++++++++++++++++-- main_view.html | 2 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 4cd2a64..f35b04f 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ import os import json import asyncio import pathlib +import subprocess VERSION = "0.6.0" HOME_DIR = str(pathlib.Path(os.getcwd()).parent.parent.resolve()) @@ -208,16 +209,16 @@ class Plugin: self.modified_settings = True if tick >= len(self.FAN_SPEEDS): # automatic mode + self.enable_jupiter_fan_control(self) 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.Popen("systemctl start jupiter-fan-control.service", stdout=subprocess.PIPE, shell=True).wait() else: # manual voltage + self.disable_jupiter_fan_control(self) 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.Popen("systemctl stop jupiter-fan-control.service", stdout=subprocess.PIPE, shell=True).wait() async def get_fan_tick(self) -> int: fan_target = read_fan_target() @@ -241,6 +242,27 @@ class Plugin: async def fantastic_installed(self) -> bool: return os.path.exists(FANTASTIC_INSTALL_DIR) + def disable_jupiter_fan_control(self): + active = subprocess.Popen(["systemctl", "is-active", "jupiter-fan-control.service"]).wait() == 0 + if active: + logging.info("Stopping jupiter-fan-control.service so it doesn't interfere") + # only disable if currently active + self.jupiter_fan_control_was_disabled = True + stop_p = subprocess.Popen(["systemctl", "stop", "jupiter-fan-control.service"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stop_p.wait() + logging.debug("systemctl stop jupiter-fan-control.service stdout:\n" + stop_p.stdout.read().decode()) + logging.debug("systemctl stop jupiter-fan-control.service stderr:\n" + stop_p.stderr.read().decode()) + + def enable_jupiter_fan_control(self): + if self.jupiter_fan_control_was_disabled: + logging.info("Starting jupiter-fan-control.service so it doesn't interfere") + # only re-enable if I disabled it + self.jupiter_fan_control_was_disabled = False + start_p = subprocess.Popen(["systemctl", "start", "jupiter-fan-control.service"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + start_p.wait() + logging.debug("systemctl start jupiter-fan-control.service stdout:\n" + start_p.stdout.read().decode()) + logging.debug("systemctl start jupiter-fan-control.service stderr:\n" + start_p.stderr.read().decode()) + # Battery stuff async def get_charge_now(self) -> int: @@ -255,6 +277,7 @@ class Plugin: # Asyncio-compatible long-running code, executed in a task when the plugin is loaded async def _main(self): # startup: load & apply settings + self.jupiter_fan_control_was_disabled = False if os.path.exists(DEFAULT_SETTINGS_LOCATION): settings = read_json(DEFAULT_SETTINGS_LOCATION) logging.debug(f"Loaded settings from {DEFAULT_SETTINGS_LOCATION}: {settings}") @@ -374,8 +397,12 @@ class Plugin: write_gpu_ppt(2, settings["gpu"]["fastppt"]) # Fan if not (os.path.exists(FANTASTIC_INSTALL_DIR) or settings["fan"]["auto"]): + self.disable_jupiter_fan_control(self) write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1) write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", settings["fan"]["target"]) + elif settings["fan"]["auto"] and not os.path.exists(FANTASTIC_INSTALL_DIR): + self.enable_jupiter_fan_control(self) + def guess_settings(self): self.cpus = [] diff --git a/main_view.html b/main_view.html index 583d9a9..9b23608 100644 --- a/main_view.html +++ b/main_view.html @@ -608,7 +608,7 @@
Fan RPM
-
Requires disabling updated fan control
+