From d606fe3c12ae221f949ae637a7cc884de7505302 Mon Sep 17 00:00:00 2001 From: aprdec <1263868407@qq.com> Date: Sun, 15 Sep 2024 19:24:31 +0400 Subject: [PATCH 1/7] Add G-Mode keyboard hot key handling Co-authored-by: Alexander Chernoskutov --- src/GUI/AppGUI.py | 34 +++++++++++++++++++++++++++++++--- src/GUI/HotKey.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/GUI/HotKey.py diff --git a/src/GUI/AppGUI.py b/src/GUI/AppGUI.py index 7f98c57..f5742c8 100644 --- a/src/GUI/AppGUI.py +++ b/src/GUI/AppGUI.py @@ -7,6 +7,7 @@ from GUI.AppColors import Colors from GUI.ThermalUnitWidget import ThermalUnitWidget from GUI.QGaugeTrayIcon import QGaugeTrayIcon +from GUI import HotKey GUI_ICON = 'icons/gaugeIcon.png' @@ -112,6 +113,9 @@ class TCC_GUI(QtWidgets.QWidget): _failsafeOn = True _prevSavedSettingsValues: list = [] + _gModeKeySignal = QtCore.Signal() + _gModeKeyPrevModeStr: Optional[str] = None + def __init__(self, awcc: AWCCThermal): super().__init__() self._awcc = awcc @@ -260,7 +264,7 @@ def onModeChange(val: str): res = self._awcc.setMode(self._awcc.Mode[val]) print(f'Set mode {val}: ' + ('ok' if res else 'fail')) if not res: - errorExit(f"Failed to set mode {val}", "Program is terminated") + self._errorExit(f"Failed to set mode {val}", "Program is terminated") updateFanSpeed() if val != ThermalMode.G_Mode.value: self._failsafeTrippedPrevModeStr = None # In case the mode was switched manually @@ -325,6 +329,10 @@ def updateAppState(): updateAppState() self._updateGaugesTask.start() + self.gModeHotKey = HotKey.HotKey(HotKey.G_MODE_KEY, self._gModeKeySignal) + self._gModeKeySignal.connect(self._onGModeHotKeyPressed) + self.gModeHotKey.start() + def closeEvent(self, event): minimizeOnClose = self.settings.value(SettingsKey.MinimizeOnCloseFlag.value) if minimizeOnClose is None: @@ -345,12 +353,30 @@ def closeEvent(self, event): def onExit(self): print("exit") # Set mode to Balanced before exit - self._updateGaugesTask.stop() prevMode = self._modeSwitch.getChecked() self._modeSwitch.setChecked(ThermalMode.Balanced.value) if prevMode != ThermalMode.Balanced.value: alert("Mode changed", "Thermal mode has been reset to Balanced") - sys.exit(1) + self._destroy() + sys.exit(0) + + def _errorExit(self, message: str, message2: Optional[str] = None) -> None: + self._destroy() + errorExit(message, message2) + + def _destroy(self): + self.gModeHotKey.stop() + self.gModeHotKey.wait() + self._updateGaugesTask.stop() + print('Cleanup: done') + + def _onGModeHotKeyPressed(self): + current = self._modeSwitch.getChecked() + if current == ThermalMode.G_Mode.value: + self._modeSwitch.setChecked(self._gModeKeyPrevModeStr or ThermalMode.Balanced.value) + else: + self._gModeKeyPrevModeStr = current + self._modeSwitch.setChecked(ThermalMode.G_Mode.value) def _saveAppSettings(self): curValues = [ @@ -392,6 +418,8 @@ def clearAppSettings(self): self.settings.clear() self._loadAppSettings() + def G_Mode_key_Pressed(self, val): + print("G_Mode_key " + str(val)) def runApp(startMinimized = False) -> int: app = QtWidgets.QApplication([]) diff --git a/src/GUI/HotKey.py b/src/GUI/HotKey.py new file mode 100644 index 0000000..a648257 --- /dev/null +++ b/src/GUI/HotKey.py @@ -0,0 +1,34 @@ +import threading +import win32con +from ctypes import * +from ctypes.wintypes import * +from PySide6.QtCore import * + +G_MODE_KEY = 0x80 + +_STOP_SIGNAL_CODE = 0x9AB10000 + +class HotKey(QThread): + def __init__(self, key: int, keyPressedSignal: Signal): + super(HotKey, self).__init__() + self.keyPressedSignal = keyPressedSignal + self.key = key + + def run(self): + self.nativeThreadId = threading.get_native_id() + user32 = windll.user32 + + if not user32.RegisterHotKey(None, 1, 0, self.key): + print(f"Could not register hot key {self.key}") + return + msg = MSG() + + while user32.GetMessageA(byref(msg), None, 0, 0): + if msg.message == win32con.WM_USER and msg.wParam == _STOP_SIGNAL_CODE: + break + if msg.message == win32con.WM_HOTKEY and msg.wParam == 1: + self.keyPressedSignal.emit() + user32.UnregisterHotKey(None, 1) + + def stop(self): + windll.user32.PostThreadMessageW(self.nativeThreadId, win32con.WM_USER, _STOP_SIGNAL_CODE, 0) From 786fbc45b5b20b0b491ba8b6beec4d70510b23ab Mon Sep 17 00:00:00 2001 From: aprdec <1263868407@qq.com> Date: Sun, 15 Sep 2024 19:53:49 +0400 Subject: [PATCH 2/7] Add toaster message for mode change with hotkey Co-authored-by: Alexander Chernoskutov --- requirements.txt | 1 + src/GUI/AppGUI.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3fefd8a..c931655 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ WMI>=1.5.1 PySide6>=6.2.2.1 +windows-toasts>=1.3.0 \ No newline at end of file diff --git a/src/GUI/AppGUI.py b/src/GUI/AppGUI.py index f5742c8..3665b9c 100644 --- a/src/GUI/AppGUI.py +++ b/src/GUI/AppGUI.py @@ -1,7 +1,8 @@ import sys, os, time from enum import Enum -from typing import Callable, Literal, Optional, Tuple +from typing import Callable, Literal, Optional, Tuple, List from PySide6 import QtCore, QtGui, QtWidgets +from windows_toasts import WindowsToaster, Toast, ToastDuration, ToastDisplayImage from Backend.AWCCThermal import AWCCThermal, NoAWCCWMIClass, CannotInstAWCCWMI from GUI.QRadioButtonSet import QRadioButtonSet from GUI.AppColors import Colors @@ -37,6 +38,13 @@ def autorunTask(action: Literal['add', 'remove']) -> int: else: return os.system(removeCmd) +def toasterMessage(title: str, message: List[str | None]) -> None: + toaster = WindowsToaster(title) + toast = Toast(duration=ToastDuration.Short) + toast.text_fields = message + toast.AddImage(ToastDisplayImage.fromPath(resourcePath(GUI_ICON))) + toaster.show_toast(toast) + def alert(title: str, message: str, type: QtWidgets.QMessageBox.Icon = QtWidgets.QMessageBox.Icon.Information, *, message2: Optional[str] = None) -> None: msg = QtWidgets.QMessageBox(type, title, message) msg.setWindowIcon(QtGui.QIcon(resourcePath(GUI_ICON))) @@ -377,6 +385,7 @@ def _onGModeHotKeyPressed(self): else: self._gModeKeyPrevModeStr = current self._modeSwitch.setChecked(ThermalMode.G_Mode.value) + toasterMessage("Thermal mode changed", [self._modeSwitch.getChecked().replace('_', '-')]) def _saveAppSettings(self): curValues = [ From 6cce39c1f79f6da87c54c08369cb81b46ca9810c Mon Sep 17 00:00:00 2001 From: Alexander Chernoskutov Date: Mon, 16 Sep 2024 11:03:05 +0400 Subject: [PATCH 3/7] Update readme --- README.md | 12 +++++++----- src/GUI/HotKey.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e414f01..3e967ca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Thermal Control Center for Dell G15 5511 / 5515 / 5520 / 5525 / 5530 +# Thermal Control Center for Dell G15 (and some others) Open-source alternative to AWCC* @@ -15,16 +15,17 @@ Open-source alternative to AWCC* OS: Windows 10/11. Supported models -- Dell G15: 5511, 5515, 5520, 5525, 5530. +- Dell G15: 5511, 5515, 5520, 5525, 5530, 5535. - Dell Alienware m16 R1 May also work on other Dell G15 / Alienware laptops. Please report if it worked / didn't work for you. Your feedback is highly appreciated. -_Big thanks to @T7imal, @cemkaya-mpi, @THSLP13 for testing and debugging._ - -_Thanks to @Dtwpurple, @WinterholdPrime for the compatibility reports._ +_Big thanks to the amazing people who contributed to the project_ +- _@AprDeci for code / new features contribution_ +- _@T7imal, @cemkaya-mpi, @THSLP13 for testing and debugging_ +- _@Dtwpurple, @WinterholdPrime, @Dhia-zorai for compatibility reports._ ## What it can do @@ -32,6 +33,7 @@ _Thanks to @Dtwpurple, @WinterholdPrime for the compatibility reports._ - ✔️ Shows GPU/CPU temperature and fan speed - ✔️ Semi-manual fan speed control - ✔️ An option to automatically enable G-mode when GPU/CPU temperature reaches critical +- ✔️ Support for keyboard G-mode hotkey ## Limitations diff --git a/src/GUI/HotKey.py b/src/GUI/HotKey.py index a648257..e873b82 100644 --- a/src/GUI/HotKey.py +++ b/src/GUI/HotKey.py @@ -6,7 +6,7 @@ G_MODE_KEY = 0x80 -_STOP_SIGNAL_CODE = 0x9AB10000 +_STOP_SIGNAL_CODE = 0x9AB10000 # This number was picked randomly class HotKey(QThread): def __init__(self, key: int, keyPressedSignal: Signal): From fc3cc652d3a1ffc4d918e826911eebd65dadfbc8 Mon Sep 17 00:00:00 2001 From: Alexander Chernoskutov Date: Mon, 16 Sep 2024 11:07:16 +0400 Subject: [PATCH 4/7] Bump app version --- installer-inno-config.iss | 2 +- src/GUI/AppGUI.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer-inno-config.iss b/installer-inno-config.iss index e83839c..23cbf1e 100644 --- a/installer-inno-config.iss +++ b/installer-inno-config.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Thermal Control Center" -#define MyAppVersion "1.5.4" +#define MyAppVersion "1.6.0" #define MyAppPublisher "AlexIII" #define MyAppURL "https://github.com/AlexIII/tcc-g15" #define MyAppExeName "tcc-g15.exe" diff --git a/src/GUI/AppGUI.py b/src/GUI/AppGUI.py index 3665b9c..b5aa5e8 100644 --- a/src/GUI/AppGUI.py +++ b/src/GUI/AppGUI.py @@ -106,7 +106,7 @@ class TCC_GUI(QtWidgets.QWidget): FAILSAFE_TRIGGER_DELAY_SEC = 8 FAILSAFE_RESET_AFTER_TEMP_IS_OK_FOR_SEC = 60 APP_NAME = "Thermal Control Center for Dell G15" - APP_VERSION = "1.5.4" + APP_VERSION = "1.6.0" APP_DESCRIPTION = "This app is an open-source replacement for Alienware Control Center " APP_URL = "github.com/AlexIII/tcc-g15" From c33e4ee448940f4cca4dc953cc302ed69589a8ae Mon Sep 17 00:00:00 2001 From: Alexander Chernoskutov Date: Mon, 16 Sep 2024 11:17:38 +0400 Subject: [PATCH 5/7] Update readme --- README.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3e967ca..96e2185 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ Open-source alternative to AWCC* +
+ +> Liked the app? Glad you did! 😸 Spread the word! 🚀 Leave a star! ⭐ + **AWCC - "Alienware Control Center" is an app for thermal control that Dell ships with their G-series notebooks.* ## Target platform @@ -57,19 +61,6 @@ If this alternative works out for you, you can safely remove from your PC: - Alieanware Command Center Suite - Alieanware OC Controls -## TO-DO - -I'll implement these things if the project receives sufficient number of stars* - -- ✔️ Minimize to tray (10x ⭐) -- ✔️ Save settings between restarts (20x ⭐) -- ✔️ Autorun on system startup option (30x ⭐) -- ✔️ Adjustable fail-safe threshold temperature, drop out of fail-safe mode after the temperature returns to normal (40x ⭐) -- ✔️ Patch for G15 5511 / 5525 -- ✔️ Proper Windows installer (50x ⭐) - -*or maybe I'll do it regardless, who knows. - ## How it works It is a PyQt based GUI for WMI Dell thermal control interface. From ad6ca62b0c99c6511523e5d4804b59b86b80c8ef Mon Sep 17 00:00:00 2001 From: Alexander Chernoskutov Date: Mon, 16 Sep 2024 13:17:07 +0400 Subject: [PATCH 6/7] Fix toaster problems (build, layout) --- make-release.bat | 4 ++-- src/GUI/AppGUI.py | 44 ++++++++++++++++++++++++------------ src/GUI/ThermalUnitWidget.py | 3 +++ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/make-release.bat b/make-release.bat index f35d600..e5ad4b5 100644 --- a/make-release.bat +++ b/make-release.bat @@ -1,10 +1,10 @@ @echo off echo Compiling Portable version... -pyinstaller --icon=icons/gaugeIcon.ico --add-data icons/gaugeIcon.png;icons --add-data tcc_g15_task.xml;. --hidden-import PySide6 -w -F -y src/tcc-g15.py || goto :error +pyinstaller --icon=icons/gaugeIcon.ico --add-data icons/gaugeIcon.png;icons --add-data tcc_g15_task.xml;. --hidden-import PySide6 --collect-all winrt -w -F -y src/tcc-g15.py || goto :error echo Compiling version for installer... -pyinstaller --icon=icons/gaugeIcon.ico --add-data icons/gaugeIcon.png;icons --add-data tcc_g15_task.xml;. --hidden-import PySide6 -w -y src/tcc-g15.py || goto :error +pyinstaller --icon=icons/gaugeIcon.ico --add-data icons/gaugeIcon.png;icons --add-data tcc_g15_task.xml;. --hidden-import PySide6 --collect-all winrt -w -y src/tcc-g15.py || goto :error echo Compiling installer... "C:\Program Files (x86)\Inno Setup 6\iscc.exe" installer-inno-config.iss || goto :error diff --git a/src/GUI/AppGUI.py b/src/GUI/AppGUI.py index b5aa5e8..933a5ed 100644 --- a/src/GUI/AppGUI.py +++ b/src/GUI/AppGUI.py @@ -1,4 +1,4 @@ -import sys, os, time +import sys, os, time, datetime from enum import Enum from typing import Callable, Literal, Optional, Tuple, List from PySide6 import QtCore, QtGui, QtWidgets @@ -38,19 +38,12 @@ def autorunTask(action: Literal['add', 'remove']) -> int: else: return os.system(removeCmd) -def toasterMessage(title: str, message: List[str | None]) -> None: - toaster = WindowsToaster(title) - toast = Toast(duration=ToastDuration.Short) - toast.text_fields = message - toast.AddImage(ToastDisplayImage.fromPath(resourcePath(GUI_ICON))) - toaster.show_toast(toast) - def alert(title: str, message: str, type: QtWidgets.QMessageBox.Icon = QtWidgets.QMessageBox.Icon.Information, *, message2: Optional[str] = None) -> None: - msg = QtWidgets.QMessageBox(type, title, message) - msg.setWindowIcon(QtGui.QIcon(resourcePath(GUI_ICON))) - if message2: msg.setInformativeText(message2) - msg.setStandardButtons(QtWidgets.QMessageBox.Ok) - msg.exec() + msg = QtWidgets.QMessageBox(type, title, message) + msg.setWindowIcon(QtGui.QIcon(resourcePath(GUI_ICON))) + if message2: msg.setInformativeText(message2) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + msg.exec() def confirm(title: str, message: str, options: Optional[Tuple[str, str]] = None, dontAskAgain: bool = False) -> Tuple[bool, Optional[bool]]: msg = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) @@ -124,6 +117,8 @@ class TCC_GUI(QtWidgets.QWidget): _gModeKeySignal = QtCore.Signal() _gModeKeyPrevModeStr: Optional[str] = None + _toaster = WindowsToaster(APP_NAME) + def __init__(self, awcc: AWCCThermal): super().__init__() self._awcc = awcc @@ -314,6 +309,7 @@ def updateAppState(): ): self._failsafeTrippedPrevModeStr = self._modeSwitch.getChecked() self._modeSwitch.setChecked(ThermalMode.G_Mode.value) + self._toasterMessageCurrentMode(source='failsafe') print(f'Fail-safe tripped at GPU={gpuTemp} CPU={cpuTemp}') # Auto-reset failsafe @@ -321,6 +317,7 @@ def updateAppState(): time.time() - self._failsafeTempIsHighTs > self.FAILSAFE_RESET_AFTER_TEMP_IS_OK_FOR_SEC ): self._modeSwitch.setChecked(self._failsafeTrippedPrevModeStr) + self._toasterMessageCurrentMode(source='failsafe') self._failsafeTrippedPrevModeStr = None print('Fail-safe reset') @@ -364,7 +361,7 @@ def onExit(self): prevMode = self._modeSwitch.getChecked() self._modeSwitch.setChecked(ThermalMode.Balanced.value) if prevMode != ThermalMode.Balanced.value: - alert("Mode changed", "Thermal mode has been reset to Balanced") + self._toasterMessageCurrentMode() self._destroy() sys.exit(0) @@ -385,7 +382,24 @@ def _onGModeHotKeyPressed(self): else: self._gModeKeyPrevModeStr = current self._modeSwitch.setChecked(ThermalMode.G_Mode.value) - toasterMessage("Thermal mode changed", [self._modeSwitch.getChecked().replace('_', '-')]) + self._toasterMessageCurrentMode() + + def _toasterMessageCurrentMode(self, source: Optional[Literal['failsafe']] = None) -> None: + sourceStr = f" [Fail-safe]" if source == 'failsafe' else "" + self.toasterMessage( + [ + self._modeSwitch.getChecked().replace('_', '-'), + f"GPU: {self._thermalGPU.getTemp()}°C, CPU: {self._thermalCPU.getTemp()}°C", + "Thermal mode changed" + sourceStr + ], + source != 'failsafe' + ) + + def toasterMessage(self, message: List[str | None], expire = True) -> None: + toast = Toast(duration=ToastDuration.Short, expiration_time= (datetime.datetime.now() + datetime.timedelta(seconds=5)) if expire else None) + toast.text_fields = message + toast.AddImage(ToastDisplayImage.fromPath(resourcePath(GUI_ICON))) + self._toaster.show_toast(toast) def _saveAppSettings(self): curValues = [ diff --git a/src/GUI/ThermalUnitWidget.py b/src/GUI/ThermalUnitWidget.py index 7332344..07f7ea7 100644 --- a/src/GUI/ThermalUnitWidget.py +++ b/src/GUI/ThermalUnitWidget.py @@ -50,6 +50,9 @@ def _makeGaugeWithLabel(self, minMax: Tuple[int,int], units: str, colorLimits: O def setTemp(self, temp: int) -> None: self._tempBar.setValue(temp) + def getTemp(self) -> int: + return self._tempBar.value() + def setFanRPM(self, rpm: int) -> None: self._fanBar.setValue(rpm) From 396b04bae9c992ccdd0d873dc32919649e7d2385 Mon Sep 17 00:00:00 2001 From: Alexander Chernoskutov Date: Mon, 16 Sep 2024 13:46:04 +0400 Subject: [PATCH 7/7] Update readme --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 96e2185..156f9de 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ Open-source alternative to AWCC*
-> Liked the app? Glad you did! 😸 Spread the word! 🚀 Leave a star! ⭐ +> Liked the app? Glad you did! 😸 Help by spreading the word 🚀 and leaving the project a star ⭐ + +> Didn't work out for you? Please report the problem by creating an [issue](https://github.com/AlexIII/tcc-g15/issues). Feedback is always welcome! **AWCC - "Alienware Control Center" is an app for thermal control that Dell ships with their G-series notebooks.* @@ -26,11 +28,6 @@ May also work on other Dell G15 / Alienware laptops. Please report if it worked / didn't work for you. Your feedback is highly appreciated. -_Big thanks to the amazing people who contributed to the project_ -- _@AprDeci for code / new features contribution_ -- _@T7imal, @cemkaya-mpi, @THSLP13 for testing and debugging_ -- _@Dtwpurple, @WinterholdPrime, @Dhia-zorai for compatibility reports._ - ## What it can do - ✔️ Switch thermal mode between G-mode, Balanced and Custom @@ -86,6 +83,13 @@ https://tm-sdk.platinumai.net https://qa-external-tm.plawebsvc01.net ``` +## Credits + +Big thanks to the amazing people who has contributed to the project +- @AprDeci for code / new features +- @T7imal, @cemkaya-mpi, @THSLP13 for testing and debugging +- @Dtwpurple, @WinterholdPrime, @Dhia-zorai for compatibility reports + ## License © github.com/AlexIII