Skip to content

Commit

Permalink
Merge branch 'g-mode-hotkey-support'
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexIII committed Sep 16, 2024
2 parents 55313b3 + 396b04b commit 6239bb3
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 34 deletions.
35 changes: 16 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -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*

Expand All @@ -8,30 +8,33 @@ Open-source alternative to AWCC*

<img src="./screen-2.png" />

<br/>

> 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

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

Expand All @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion installer-inno-config.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions make-release.bat
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
WMI>=1.5.1
PySide6>=6.2.2.1
windows-toasts>=1.3.0
75 changes: 63 additions & 12 deletions src/GUI/AppGUI.py
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -302,13 +309,15 @@ 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
if (self._failsafeTrippedPrevModeStr is not None and
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')

Expand All @@ -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:
Expand All @@ -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 = [
Expand Down Expand Up @@ -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([])
Expand Down
34 changes: 34 additions & 0 deletions src/GUI/HotKey.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions src/GUI/ThermalUnitWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 6239bb3

Please sign in to comment.