diff --git a/README.md b/README.md
index e414f01..156f9de 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*
@@ -8,6 +8,12 @@ Open-source alternative to AWCC*
+
+
+> 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.*
## Target platform
@@ -15,23 +21,20 @@ 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._
-
## What it can do
- ✔️ Switch thermal mode between G-mode, Balanced and Custom
- ✔️ 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
@@ -55,19 +58,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.
@@ -93,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
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/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/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 7f98c57..933a5ed 100644
--- a/src/GUI/AppGUI.py
+++ b/src/GUI/AppGUI.py
@@ -1,12 +1,14 @@
-import sys, os, time
+import sys, os, time, datetime
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
from GUI.ThermalUnitWidget import ThermalUnitWidget
from GUI.QGaugeTrayIcon import QGaugeTrayIcon
+from GUI import HotKey
GUI_ICON = 'icons/gaugeIcon.png'
@@ -37,11 +39,11 @@ def autorunTask(action: Literal['add', 'remove']) -> int:
return os.system(removeCmd)
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)
@@ -97,7 +99,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"
@@ -112,6 +114,11 @@ class TCC_GUI(QtWidgets.QWidget):
_failsafeOn = True
_prevSavedSettingsValues: list = []
+ _gModeKeySignal = QtCore.Signal()
+ _gModeKeyPrevModeStr: Optional[str] = None
+
+ _toaster = WindowsToaster(APP_NAME)
+
def __init__(self, awcc: AWCCThermal):
super().__init__()
self._awcc = awcc
@@ -260,7 +267,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
@@ -302,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
@@ -309,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')
@@ -325,6 +334,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 +358,48 @@ 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._toasterMessageCurrentMode()
+ 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)
+ 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 = [
@@ -392,6 +441,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..e873b82
--- /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 # This number was picked randomly
+
+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)
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)