""" gridcontrol.py -------------- This is the main module of Grid Control. Implements the UI and business logic. """ import sys import threading import grid import helper import openhwmon import polling import serial import settings from PyQt5 import QtCore, QtWidgets, QtGui from ui.mainwindow import Ui_MainWindow # Define status icons (available in the resource file built with "pyrcc5" ICON_RED_LED = ":/icons/led-red-on.png" ICON_GREEN_LED = ":/icons/green-led-on.png" class GridControl(QtWidgets.QMainWindow): """Create the UI, based on PyQt5. The UI elements are defined in "mainwindow.py" and resource file "resources_rc.py", created in QT Designer. To update "mainwindow.py": Run "pyuic5.exe --from-imports mainwindow.ui -o mainwindow.py" To update "resources_rc.py": Run "pyrcc5.exe resources.qrc -o resource_rc.py" Note: Never modify "mainwindow.py" or "resource_rc.py" manually. """ def __init__(self): super().__init__() # Create the main window self.ui = Ui_MainWindow() # Set upp the UI self.ui.setupUi(self) # Object for locking the serial port while sending/receiving data self.lock = threading.Lock() # Serial communication object self.ser = serial.Serial() # Initialize WMI communication with OpenHardwareMonitor # "initialize_hwmon()" returns a WMI object self.hwmon = openhwmon.initialize_hwmon() # QSettings object for storing the UI configuration in the OS native repository (Registry for Windows, ini-file for Linux) # In Windows, parameters will be stored at HKEY_CURRENT_USER/SOFTWARE/GridControl/App self.config = QtCore.QSettings('GridControl', 'App') # Get a list of available serial ports (e.g. "COM1" in Windows) self.serial_ports = grid.get_serial_ports() # Populate the "COM port" combo box with available serial ports self.ui.comboBoxComPorts.addItems(self.serial_ports) # Read saved UI configuration settings.read_settings(self.config, self.ui, self.hwmon) # Populates the tree widget on tab "Sensor Config" with values from OpenHardwareMonitor openhwmon.populate_tree(self.hwmon, self.ui.treeWidgetHWMonData, self.ui.checkBoxStartSilently.isChecked()) # System tray icon self.trayIcon = SystemTrayIcon(QtGui.QIcon(QtGui.QPixmap(":/icons/grid.png")), self) self.trayIcon.show() # Create a QThread object that will poll the Grid for fan rpm and voltage and HWMon for temperatures # The lock is needed in all operations with the serial port self.thread = polling.PollingThread(polling_interval=int(self.ui.comboBoxPolling.currentText()), ser=self.ser, lock=self.lock, cpu_sensor_ids=self.get_cpu_sensor_ids(), gpu_sensor_ids=self.get_gpu_sensor_ids(), cpu_calc="Max" if self.ui.radioButtonCPUMax.isChecked() else "Avg", gpu_calc="Max" if self.ui.radioButtonGPUMax.isChecked() else "Avg") # Connect signals and slots self.setup_ui_logic() # Setup UI parameters that cannot be defined in QT Designer self.setup_ui_design() # Store current horizontal slider values # Used for restoring values after automatic mode has been used self.manual_value_fan1 = self.ui.horizontalSliderFan1.value() self.manual_value_fan2 = self.ui.horizontalSliderFan2.value() self.manual_value_fan3 = self.ui.horizontalSliderFan3.value() self.manual_value_fan4 = self.ui.horizontalSliderFan4.value() self.manual_value_fan5 = self.ui.horizontalSliderFan5.value() self.manual_value_fan6 = self.ui.horizontalSliderFan6.value() # Minimize to tray if enabled if self.ui.checkBoxStartMinimized.isChecked(): self.setWindowState(QtCore.Qt.WindowMinimized) else: self.show() # Initialize communication self.init_communication() def setup_ui_logic(self): """Define QT signal and slot connections and initializes UI values.""" # Update "Fan percentage" LCD values from horizontal sliders initial value self.ui.lcdNumberFan1.display(self.ui.horizontalSliderFan1.value()) self.ui.lcdNumberFan2.display(self.ui.horizontalSliderFan2.value()) self.ui.lcdNumberFan3.display(self.ui.horizontalSliderFan3.value()) self.ui.lcdNumberFan4.display(self.ui.horizontalSliderFan4.value()) self.ui.lcdNumberFan5.display(self.ui.horizontalSliderFan5.value()) self.ui.lcdNumberFan6.display(self.ui.horizontalSliderFan6.value()) # Update "fan labels" from "Fan Config" tab self.ui.groupBoxFan1.setTitle(self.ui.lineEditFan1.text()) self.ui.groupBoxFan2.setTitle(self.ui.lineEditFan2.text()) self.ui.groupBoxFan3.setTitle(self.ui.lineEditFan3.text()) self.ui.groupBoxFan4.setTitle(self.ui.lineEditFan4.text()) self.ui.groupBoxFan5.setTitle(self.ui.lineEditFan5.text()) self.ui.groupBoxFan6.setTitle(self.ui.lineEditFan6.text()) self.ui.groupBoxCurrentFan1.setTitle(self.ui.lineEditFan1.text()) self.ui.groupBoxCurrentFan2.setTitle(self.ui.lineEditFan2.text()) self.ui.groupBoxCurrentFan3.setTitle(self.ui.lineEditFan3.text()) self.ui.groupBoxCurrentFan4.setTitle(self.ui.lineEditFan4.text()) self.ui.groupBoxCurrentFan5.setTitle(self.ui.lineEditFan5.text()) self.ui.groupBoxCurrentFan6.setTitle(self.ui.lineEditFan6.text()) self.ui.groupBoxConfigFan1.setTitle(self.ui.lineEditFan1.text()) self.ui.groupBoxConfigFan2.setTitle(self.ui.lineEditFan2.text()) self.ui.groupBoxConfigFan3.setTitle(self.ui.lineEditFan3.text()) self.ui.groupBoxConfigFan4.setTitle(self.ui.lineEditFan4.text()) self.ui.groupBoxConfigFan5.setTitle(self.ui.lineEditFan5.text()) self.ui.groupBoxConfigFan6.setTitle(self.ui.lineEditFan6.text()) # Connect events from sliders to update "Fan percentage" LCD value self.ui.horizontalSliderFan1.valueChanged.connect(self.ui.lcdNumberFan1.display) self.ui.horizontalSliderFan2.valueChanged.connect(self.ui.lcdNumberFan2.display) self.ui.horizontalSliderFan3.valueChanged.connect(self.ui.lcdNumberFan3.display) self.ui.horizontalSliderFan4.valueChanged.connect(self.ui.lcdNumberFan4.display) self.ui.horizontalSliderFan5.valueChanged.connect(self.ui.lcdNumberFan5.display) self.ui.horizontalSliderFan6.valueChanged.connect(self.ui.lcdNumberFan6.display) # Connect "Manual/Automatic" fan control radio button self.ui.radioButtonManual.toggled.connect(self.disable_enable_sliders) # Connect "Simulated temperatures" checkbox self.ui.checkBoxSimulateTemp.stateChanged.connect(self.simulate_temperatures) # Connect "Restart Communication" button self.ui.pushButtonRestart.clicked.connect(self.restart) # Connect "Add CPU sensors" button self.ui.pushButtonAddCPUSensor.clicked.connect(self.add_cpu_sensors) # Connect "Add GPU sensors" button self.ui.pushButtonAddGPUSensor.clicked.connect(self.add_gpu_sensors) # Connect "Remove CPU sensors" button self.ui.pushButtonRemoveCPUSensor.clicked.connect(self.remove_cpu_sensors) # Connect "Remove GPU sensors" button self.ui.pushButtonRemoveGPUSensor.clicked.connect(self.remove_gpu_sensors) # Connect event from changed serial port combo box self.ui.comboBoxComPorts.currentIndexChanged.connect(self.init_communication) # Connect event from changed polling interval combo box self.ui.comboBoxPolling.currentIndexChanged.connect(self.init_communication) # Update fan voltage (speed) based on changes to the horizontal sliders # # "grid.calculate_voltage" converts the percent value to valid voltages supported by the Grid # "lambda" is needed to send four arguments (serial object, fan id, fan voltage and lock object) self.ui.horizontalSliderFan1.valueChanged.connect( lambda: grid.set_fan(ser=self.ser, fan=1, voltage=grid.calculate_voltage(self.ui.lcdNumberFan1.value()) ,lock=self.lock)) self.ui.horizontalSliderFan2.valueChanged.connect( lambda: grid.set_fan(ser=self.ser, fan=2, voltage=grid.calculate_voltage(self.ui.lcdNumberFan2.value()), lock=self.lock)) self.ui.horizontalSliderFan3.valueChanged.connect( lambda: grid.set_fan(ser=self.ser, fan=3, voltage=grid.calculate_voltage(self.ui.lcdNumberFan3.value()), lock=self.lock)) self.ui.horizontalSliderFan4.valueChanged.connect( lambda: grid.set_fan(ser=self.ser, fan=4, voltage=grid.calculate_voltage(self.ui.lcdNumberFan4.value()), lock=self.lock)) self.ui.horizontalSliderFan5.valueChanged.connect( lambda: grid.set_fan(ser=self.ser, fan=5, voltage=grid.calculate_voltage(self.ui.lcdNumberFan5.value()), lock=self.lock)) self.ui.horizontalSliderFan6.valueChanged.connect( lambda: grid.set_fan(ser=self.ser, fan=6, voltage=grid.calculate_voltage(self.ui.lcdNumberFan6.value()), lock=self.lock)) # Connect "Change value" events from "Fan config" tab (all "spin boxes") to verify that the values are valid for fan in range(1, 7): getattr(self.ui, "spinBoxMinSpeedFan" + str(fan)).valueChanged.connect(self.validate_fan_config) getattr(self.ui, "spinBoxStartIncreaseSpeedFan" + str(fan)).valueChanged.connect(self.validate_fan_config) getattr(self.ui, "spinBoxIntermediateSpeedFan" + str(fan)).valueChanged.connect(self.validate_fan_config) getattr(self.ui, "spinBoxMaxSpeedFan" + str(fan)).valueChanged.connect(self.validate_fan_config) getattr(self.ui, "spinBoxIntermediateTempFan" + str(fan)).valueChanged.connect(self.validate_fan_config) getattr(self.ui, "spinBoxMaxTempFan" + str(fan)).valueChanged.connect(self.validate_fan_config) # Connect fan rpm signal (from polling thread) to fan rpm label self.thread.rpm_signal_fan1.connect(self.ui.labelRPMFan1.setText) self.thread.rpm_signal_fan2.connect(self.ui.labelRPMFan2.setText) self.thread.rpm_signal_fan3.connect(self.ui.labelRPMFan3.setText) self.thread.rpm_signal_fan4.connect(self.ui.labelRPMFan4.setText) self.thread.rpm_signal_fan5.connect(self.ui.labelRPMFan5.setText) self.thread.rpm_signal_fan6.connect(self.ui.labelRPMFan6.setText) # Connect fan voltage signal (from polling thread) to fan voltage value self.thread.voltage_signal_fan1.connect(self.ui.labelVFan1.setText) self.thread.voltage_signal_fan2.connect(self.ui.labelVFan2.setText) self.thread.voltage_signal_fan3.connect(self.ui.labelVFan3.setText) self.thread.voltage_signal_fan4.connect(self.ui.labelVFan4.setText) self.thread.voltage_signal_fan5.connect(self.ui.labelVFan5.setText) self.thread.voltage_signal_fan6.connect(self.ui.labelVFan6.setText) # Connect pixmap signal (from polling thread) for updating the fan status icon # "lambda" is needed to transmit two arguments, "icon resource name" from the signal (x) and fan id self.thread.pixmap_signal_fan1.connect(lambda x: self.change_fan_icon(x, 1)) self.thread.pixmap_signal_fan2.connect(lambda x: self.change_fan_icon(x, 2)) self.thread.pixmap_signal_fan3.connect(lambda x: self.change_fan_icon(x, 3)) self.thread.pixmap_signal_fan4.connect(lambda x: self.change_fan_icon(x, 4)) self.thread.pixmap_signal_fan5.connect(lambda x: self.change_fan_icon(x, 5)) self.thread.pixmap_signal_fan6.connect(lambda x: self.change_fan_icon(x, 6)) # Connect CPU and GPU temperature signals (from polling thread) to GPU and CPU LCD values self.thread.cpu_temp_signal.connect(self.ui.lcdNumberCurrentCPU.display) self.thread.gpu_temp_signal.connect(self.ui.lcdNumberCurrentGPU.display) # Connect update signal to fan update function self.thread.update_signal.connect(self.update_fan_speed) # Connect CPU and GPU temperature signals (from polling thread) to function for updating HWMon status self.thread.hwmon_status_signal.connect(self.ui.labelHWMonStatus.setText) # Connect exception signal to show exception message from running thread # This is needed as it's not possible to show a message box widget from the QThread directly self.thread.exception_signal.connect(self.thread_exception_handling) def validate_fan_config(self): """Validate fan configuration values, prevent incorrect/invalid values.""" # Name of widget (spin box) calling function sender = self.sender().objectName() # Fan id is the last character in the name fan = sender[-1:] # Get current values from spin boxes min_speed_fan = getattr(self.ui, "spinBoxMinSpeedFan" + str(fan)).value() start_increase_speed_fan = getattr(self.ui, "spinBoxStartIncreaseSpeedFan" + str(fan)).value() intermediate_speed_fan = getattr(self.ui, "spinBoxIntermediateSpeedFan" + str(fan)).value() max_speed_fan = getattr(self.ui, "spinBoxMaxSpeedFan" + str(fan)).value() intermediate_temp_fan = getattr(self.ui, "spinBoxIntermediateTempFan" + str(fan)).value() max_temp_fan = getattr(self.ui, "spinBoxMaxTempFan" + str(fan)).value() # Logic for preventing incorrect/invalid values if sender.startswith("spinBoxMinSpeedFan"): if min_speed_fan >= intermediate_speed_fan: getattr(self.ui, sender).setValue(intermediate_speed_fan - 1) elif sender.startswith("spinBoxStartIncreaseSpeedFan"): if start_increase_speed_fan >= intermediate_temp_fan: getattr(self.ui, sender).setValue(intermediate_temp_fan - 1) elif sender.startswith("spinBoxIntermediateSpeedFan"): if intermediate_speed_fan >= max_speed_fan: getattr(self.ui, sender).setValue(max_speed_fan - 1) if intermediate_speed_fan <= min_speed_fan: getattr(self.ui, sender).setValue(min_speed_fan + 1) elif sender.startswith("spinBoxMaxSpeedFan"): if max_speed_fan <= intermediate_speed_fan: getattr(self.ui, sender).setValue(intermediate_speed_fan + 1) elif sender.startswith("spinBoxIntermediateTempFan"): if intermediate_temp_fan >= max_temp_fan: getattr(self.ui, sender).setValue(max_temp_fan - 1) if intermediate_temp_fan <= start_increase_speed_fan: getattr(self.ui, sender).setValue(start_increase_speed_fan + 1) elif sender.startswith("spinBoxMaxTempFan"): if max_temp_fan <= intermediate_temp_fan: getattr(self.ui, sender).setValue(intermediate_temp_fan + 1) def setup_ui_design(self): """Define UI parameters that cannot be configured in QT Creator directly.""" # "OpenHardwareMonitor tree widget" configuration self.ui.treeWidgetHWMonData.setHeaderLabels(["Node", "ID", "Temp (at init)"]) self.ui.treeWidgetHWMonData.expandAll() self.ui.treeWidgetHWMonData.setSortingEnabled(False) self.ui.treeWidgetHWMonData.sortByColumn(0, 0) self.ui.treeWidgetHWMonData.setColumnWidth(0, 200) self.ui.treeWidgetHWMonData.setColumnWidth(1, 100) self.ui.treeWidgetHWMonData.setColumnWidth(2, 50) # treeWidget.setColumnHidden(1, True) self.ui.treeWidgetHWMonData.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) # "Selected CPU sensors" tree widget configuration self.ui.treeWidgetSelectedCPUSensors.setHeaderLabels(["Node", "ID"]) self.ui.treeWidgetSelectedCPUSensors.setColumnWidth(0, 150) self.ui.treeWidgetSelectedCPUSensors.setColumnWidth(1, 50) self.ui.treeWidgetSelectedCPUSensors.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) # "Selected GPU sensors" tree widget configuration self.ui.treeWidgetSelectedGPUSensors.setHeaderLabels(["Node", "ID"]) self.ui.treeWidgetSelectedGPUSensors.setColumnWidth(0, 150) self.ui.treeWidgetSelectedGPUSensors.setColumnWidth(1, 50) self.ui.treeWidgetSelectedGPUSensors.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) # "Simulate temperatures" group box settings self.ui.checkBoxSimulateTemp.setChecked(False) self.ui.horizontalSliderCPUTemp.setEnabled(False) self.ui.horizontalSliderGPUTemp.setEnabled(False) # If manual mode is enabled, disable "Simulate temperatures" if self.ui.radioButtonManual.isChecked(): self.ui.groupBoxSimulateTemperatures.setEnabled(False) # If automatic mode is enabled, disable the horizontal sliders if self.ui.radioButtonAutomatic.isChecked(): self.ui.horizontalSliderFan1.setEnabled(False) self.ui.horizontalSliderFan2.setEnabled(False) self.ui.horizontalSliderFan3.setEnabled(False) self.ui.horizontalSliderFan4.setEnabled(False) self.ui.horizontalSliderFan5.setEnabled(False) self.ui.horizontalSliderFan6.setEnabled(False) def init_communication(self): """Configure the serial device, serial port and polling interval before starting the polling thread. Called at: - Start of application - When the "Serial port" or "Polling interval" combo box is changed - When "Restart Communication" button is clicked """ # If the polling thread is running, stop it to be able to update port/polling interval and reset fans if self.thread.isRunning(): self.thread.stop() # Reset fan and temperature data (set rpm and voltage to "---" and temp to "0") self.reset_data() # If the serial port is open, close it with self.lock: if self.ser.isOpen(): self.ser.close() # Check if a serial port is selected if self.ui.comboBoxComPorts.currentText() != "<Select port>": # Setup serial device using selected serial port grid.setup_serial(self.ser, self.ui.comboBoxComPorts.currentText(), self.lock) # Open serial device grid.open_serial(self.ser, self.lock) # If manual mode is selected, enable horizontal sliders (they are disabled if no serial port is selected) if self.ui.radioButtonManual.isChecked(): self.ui.horizontalSliderFan1.setEnabled(True) self.ui.horizontalSliderFan2.setEnabled(True) self.ui.horizontalSliderFan3.setEnabled(True) self.ui.horizontalSliderFan4.setEnabled(True) self.ui.horizontalSliderFan5.setEnabled(True) self.ui.horizontalSliderFan6.setEnabled(True) # Enable other UI elements self.ui.radioButtonManual.setEnabled(True) self.ui.radioButtonAutomatic.setEnabled(True) self.ui.checkBoxSimulateTemp.setEnabled(True) if self.ui.checkBoxSimulateTemp.isChecked(): self.ui.horizontalSliderCPUTemp.setEnabled(True) self.ui.horizontalSliderGPUTemp.setEnabled(True) # Initialize the Grid+ V2 device if grid.initialize_grid(self.ser, self.lock): # Set the initial fan speeds based on UI values self.initialize_fans() # Update the polling interval (ms) based on UI value self.thread.update_polling_interval(new_polling_interval=int(self.ui.comboBoxPolling.currentText())) # Update temperature calculation (Maximum or Average) based on UI settings on "Sensor Config" tab self.thread.set_temp_calc(cpu_calc="Max" if self.ui.radioButtonCPUMax.isChecked() else "Avg", gpu_calc="Max" if self.ui.radioButtonGPUMax.isChecked() else "Avg") # Start the polling thread self.thread.start() # Update status in UI self.ui.labelPollingStatus.setText('<b><font color="green">Running</font></b>') # Handle unsuccessful initialization else: # As there is a communication problem, reset the "serial port" combo box index = self.ui.comboBoxComPorts.findText("<Select port>") self.ui.comboBoxComPorts.setCurrentIndex(index) # Update status in UI self.ui.labelPollingStatus.setText('<b><font color="red">Stopped</font></b>') # If no serial port is selected, disable UI elements else: self.ui.horizontalSliderFan1.setEnabled(False) self.ui.horizontalSliderFan2.setEnabled(False) self.ui.horizontalSliderFan3.setEnabled(False) self.ui.horizontalSliderFan4.setEnabled(False) self.ui.horizontalSliderFan5.setEnabled(False) self.ui.horizontalSliderFan6.setEnabled(False) self.ui.radioButtonManual.setEnabled(False) self.ui.radioButtonAutomatic.setEnabled(False) self.ui.checkBoxSimulateTemp.setEnabled(False) self.ui.horizontalSliderCPUTemp.setEnabled(False) self.ui.horizontalSliderGPUTemp.setEnabled(False) self.ui.horizontalSliderCPUTemp.setValue(0) self.ui.horizontalSliderGPUTemp.setValue(0) def reset_data(self): """Reset fan rpm and voltage to "---" and activate the red status icon. Reset CPU and GPU temperature to "0".""" # Reset fan rpm self.ui.labelRPMFan1.setText('<b><font color="red">---</font></b>') self.ui.labelRPMFan2.setText('<b><font color="red">---</font></b>') self.ui.labelRPMFan3.setText('<b><font color="red">---</font></b>') self.ui.labelRPMFan4.setText('<b><font color="red">---</font></b>') self.ui.labelRPMFan5.setText('<b><font color="red">---</font></b>') self.ui.labelRPMFan6.setText('<b><font color="red">---</font></b>') # Reset fan voltage self.ui.labelVFan1.setText('<b><font color="red">---</font></b>') self.ui.labelVFan2.setText('<b><font color="red">---</font></b>') self.ui.labelVFan3.setText('<b><font color="red">---</font></b>') self.ui.labelVFan4.setText('<b><font color="red">---</font></b>') self.ui.labelVFan5.setText('<b><font color="red">---</font></b>') self.ui.labelVFan6.setText('<b><font color="red">---</font></b>') # Activate the red led icon self.ui.labelStatusFan1.setPixmap(QtGui.QPixmap(ICON_RED_LED)) self.ui.labelStatusFan2.setPixmap(QtGui.QPixmap(ICON_RED_LED)) self.ui.labelStatusFan3.setPixmap(QtGui.QPixmap(ICON_RED_LED)) self.ui.labelStatusFan4.setPixmap(QtGui.QPixmap(ICON_RED_LED)) self.ui.labelStatusFan5.setPixmap(QtGui.QPixmap(ICON_RED_LED)) self.ui.labelStatusFan6.setPixmap(QtGui.QPixmap(ICON_RED_LED)) # Reset temperatures self.ui.lcdNumberCurrentCPU.display(0) self.ui.lcdNumberCurrentGPU.display(0) # Update status in UI self.ui.labelPollingStatus.setText('<b><font color="red">Stopped</font></b>') self.ui.labelHWMonStatus.setText('<b><font color="red">---</font></b>') def initialize_fans(self): """Initialize fans to the initial slider values.""" grid.set_fan(ser=self.ser, fan=1, voltage=grid.calculate_voltage(self.ui.lcdNumberFan1.value()), lock=self.lock) grid.set_fan(ser=self.ser, fan=2, voltage=grid.calculate_voltage(self.ui.lcdNumberFan2.value()), lock=self.lock) grid.set_fan(ser=self.ser, fan=3, voltage=grid.calculate_voltage(self.ui.lcdNumberFan3.value()), lock=self.lock) grid.set_fan(ser=self.ser, fan=4, voltage=grid.calculate_voltage(self.ui.lcdNumberFan4.value()), lock=self.lock) grid.set_fan(ser=self.ser, fan=5, voltage=grid.calculate_voltage(self.ui.lcdNumberFan5.value()), lock=self.lock) grid.set_fan(ser=self.ser, fan=6, voltage=grid.calculate_voltage(self.ui.lcdNumberFan6.value()), lock=self.lock) def disable_enable_sliders(self): """Disables the horizontal sliders if "Automatic" mode is selected. When changing from automatic to manual mode, restore manual values.""" # If "Automatic" radio button was clicked (i.e. it's "Checked") if self.ui.radioButtonAutomatic.isChecked(): # Save current manual values self.manual_value_fan1 = self.ui.horizontalSliderFan1.value() self.manual_value_fan2 = self.ui.horizontalSliderFan2.value() self.manual_value_fan3 = self.ui.horizontalSliderFan3.value() self.manual_value_fan4 = self.ui.horizontalSliderFan4.value() self.manual_value_fan5 = self.ui.horizontalSliderFan5.value() self.manual_value_fan6 = self.ui.horizontalSliderFan6.value() # Disable sliders self.ui.horizontalSliderFan1.setEnabled(False) self.ui.horizontalSliderFan2.setEnabled(False) self.ui.horizontalSliderFan3.setEnabled(False) self.ui.horizontalSliderFan4.setEnabled(False) self.ui.horizontalSliderFan5.setEnabled(False) self.ui.horizontalSliderFan6.setEnabled(False) # Enable simulate temperatures self.ui.groupBoxSimulateTemperatures.setEnabled(True) # If "Manual" radio button was clicked else: # Restore saved manual values self.ui.horizontalSliderFan1.setValue(self.manual_value_fan1) self.ui.horizontalSliderFan2.setValue(self.manual_value_fan2) self.ui.horizontalSliderFan3.setValue(self.manual_value_fan3) self.ui.horizontalSliderFan4.setValue(self.manual_value_fan4) self.ui.horizontalSliderFan5.setValue(self.manual_value_fan5) self.ui.horizontalSliderFan6.setValue(self.manual_value_fan6) # Enable sliders self.ui.horizontalSliderFan1.setEnabled(True) self.ui.horizontalSliderFan2.setEnabled(True) self.ui.horizontalSliderFan3.setEnabled(True) self.ui.horizontalSliderFan4.setEnabled(True) self.ui.horizontalSliderFan5.setEnabled(True) self.ui.horizontalSliderFan6.setEnabled(True) # Disable simulate temperatures self.ui.groupBoxSimulateTemperatures.setEnabled(False) self.ui.checkBoxSimulateTemp.setChecked(False) def update_fan_speed(self): """Update fan speed based on CPU and GPU temperatures.""" # If automatic mode is selected if self.ui.radioButtonAutomatic.isChecked(): # For each fan (1 ... 6) for fan in range(1, 7): # Linear equation calculation # y = k*x + m # k = (y2 - y1) / (x2 - x1) # First equation (a): # From "Start increase speed at" to "Intermediate fan speed at" (temperature on x-axis) # Temperatures (x-axis) x1_a = int(getattr(self.ui, "spinBoxStartIncreaseSpeedFan" + str(fan)).value()) x2_a = int(getattr(self.ui, "spinBoxIntermediateTempFan" + str(fan)).value()) # Speed in percent (y-axis) y1_a = int(getattr(self.ui, "spinBoxMinSpeedFan" + str(fan)).value()) y2_a = int(getattr(self.ui, "spinBoxIntermediateSpeedFan" + str(fan)).value()) # Calculate "k" and "m" k_a = (y2_a - y1_a) / (x2_a - x1_a) m_a = y1_a - k_a * x1_a # Second equation (b) # From "Intermediate fan speed at" to "Maximum fan speed at" (temperature on x-axis) # Temperatures (x-axis) x1_b = int(getattr(self.ui, "spinBoxIntermediateTempFan" + str(fan)).value()) x2_b = int(getattr(self.ui, "spinBoxMaxTempFan" + str(fan)).value()) # Speed in percent (y-axis) y1_b = int(getattr(self.ui, "spinBoxIntermediateSpeedFan" + str(fan)).value()) y2_b = int(getattr(self.ui, "spinBoxMaxSpeedFan" + str(fan)).value()) # Calculate "k" and "m" k_b = (y2_b - y1_b) / (x2_b - x1_b) m_b = y1_b - k_b * x1_b min_temperature = int(getattr(self.ui, "spinBoxStartIncreaseSpeedFan" + str(fan)).value()) intermediate_temperature = int(getattr(self.ui, "spinBoxIntermediateTempFan" + str(fan)).value()) max_temperature = int(getattr(self.ui, "spinBoxMaxTempFan" + str(fan)).value()) # If "Use CPU temperature" is selected, use current CPU temperature (from LCD widget in UI) if getattr(self.ui, "radioButtonCPUFan" + str(fan)).isChecked(): current_temperature = self.ui.lcdNumberCurrentCPU.value() # Else, use current GPU temperature (from LCD widget i UI) else: current_temperature = self.ui.lcdNumberCurrentGPU.value() if current_temperature <= min_temperature: # Set fan to minimum fan speed (constant value) fan_speed = int(getattr(self.ui, "spinBoxMinSpeedFan" + str(fan)).value()) elif current_temperature <= intermediate_temperature: # Calculate temperature according to first linear equation fan_speed = k_a * current_temperature + m_a elif current_temperature <= max_temperature: # Calculate temperature according to second linear equation fan_speed = k_b * current_temperature + m_b else: # Set fan to maximum fan speed (constant value) fan_speed = int(getattr(self.ui, "spinBoxMaxSpeedFan" + str(fan)).value()) # Update horizontal slider value getattr(self.ui, "horizontalSliderFan" + str(fan)).setValue(round(fan_speed)) def simulate_temperatures(self): """Simulate CPU and GPU temperatures, used for verifying the functionality of the fan control system.""" # If "Simulate temperatures" checkbox is enabled if self.ui.checkBoxSimulateTemp.isChecked(): # Enable sliders self.ui.horizontalSliderCPUTemp.setEnabled(True) self.ui.horizontalSliderGPUTemp.setEnabled(True) # Update CPU and GPU values from current horizontal slider values self.ui.lcdNumberCurrentCPU.display(self.ui.horizontalSliderCPUTemp.value()) self.ui.lcdNumberCurrentGPU.display(self.ui.horizontalSliderGPUTemp.value()) # Disconnect temperature signals from polling thread self.thread.cpu_temp_signal.disconnect(self.ui.lcdNumberCurrentCPU.display) self.thread.gpu_temp_signal.disconnect(self.ui.lcdNumberCurrentGPU.display) # Connect the horizontal sliders to the "CPU" and "GPU" LCD widget self.ui.horizontalSliderCPUTemp.valueChanged.connect(self.ui.lcdNumberCurrentCPU.display) self.ui.horizontalSliderGPUTemp.valueChanged.connect(self.ui.lcdNumberCurrentGPU.display) # Update group box headers to indicate simulation mode self.ui.groupBoxCurrentCPUTemp.setTitle("Sim. CPU temp") self.ui.groupBoxCurrentGPUTemp.setTitle("Sim. GPU temp") # If "Simulate temperatures" checkbox is disabled, reset settings else: # Disable horizontal sliders self.ui.horizontalSliderCPUTemp.setEnabled(False) self.ui.horizontalSliderGPUTemp.setEnabled(False) # Reconnect signals from polling thread self.thread.cpu_temp_signal.connect(self.ui.lcdNumberCurrentCPU.display) self.thread.gpu_temp_signal.connect(self.ui.lcdNumberCurrentGPU.display) # Reset headers in UI self.ui.groupBoxCurrentCPUTemp.setTitle("Current CPU temp") self.ui.groupBoxCurrentGPUTemp.setTitle("Current GPU temp") def restart(self): """Update 'Selected CPU and GPU sensors' and restart application""" # TODO: Add apply button self.thread.update_sensors(self.get_cpu_sensor_ids(), self.get_gpu_sensor_ids()) self.init_communication() def thread_exception_handling(self, msg): """Display an error message with details about the exception and reset the "serial port value" to <Select port>. Called when an exception occurs in the polling thread.""" # Show error message helper.show_error(msg) # Reset the "serial port" combo box index = self.ui.comboBoxComPorts.findText("<Select port>") self.ui.comboBoxComPorts.setCurrentIndex(index) def add_cpu_sensors(self): """Add selected temperature sensor(s) to the "Selected CPU sensor(s)" three widget.""" items = [item for item in self.ui.treeWidgetHWMonData.selectedItems()] # The new items should have the tree widget itself as parent parent = self.ui.treeWidgetSelectedCPUSensors for item in items: sensor_item = QtWidgets.QTreeWidgetItem(parent) sensor_item.setText(0, item.text(0)) sensor_item.setText(1, item.text(1)) sensor_item.setForeground(0, QtGui.QBrush(QtCore.Qt.blue)) # Text color blue # Deselect all items in the HWMon tree widget after they have been added self.ui.treeWidgetHWMonData.clearSelection() def add_gpu_sensors(self): """Add selected temperature sensor(s) to the "Selected GPU sensor(s)" three widget.""" items = [item for item in self.ui.treeWidgetHWMonData.selectedItems()] # The new items should have the tree widget itself as parent parent = self.ui.treeWidgetSelectedGPUSensors for item in items: sensor_item = QtWidgets.QTreeWidgetItem(parent) sensor_item.setText(0, item.text(0)) sensor_item.setText(1, item.text(1)) sensor_item.setForeground(0, QtGui.QBrush(QtCore.Qt.blue)) # Text color blue # Deselect all items in the HWMon tree widget after they have been added self.ui.treeWidgetHWMonData.clearSelection() def remove_cpu_sensors(self): """Remove selected CPU sensors.""" root = self.ui.treeWidgetSelectedCPUSensors.invisibleRootItem() for item in self.ui.treeWidgetSelectedCPUSensors.selectedItems(): root.removeChild(item) def remove_gpu_sensors(self): """Remove selected GPU sensors.""" root = self.ui.treeWidgetSelectedGPUSensors.invisibleRootItem() for item in self.ui.treeWidgetSelectedGPUSensors.selectedItems(): root.removeChild(item) def get_cpu_sensor_ids(self): """Get id's for each sensor in the "Selected CPU sensors" tree.""" root = self.ui.treeWidgetSelectedCPUSensors.invisibleRootItem() child_count = root.childCount() cpu_sensor_ids = [] for i in range(child_count): item = root.child(i) cpu_sensor_ids.append(item.text(1)) # Second column is the id return cpu_sensor_ids def get_gpu_sensor_ids(self): """Get id's for each sensor in the "Selected GPU sensors" tree.""" root = self.ui.treeWidgetSelectedGPUSensors.invisibleRootItem() child_count = root.childCount() gpu_sensor_ids = [] for i in range(child_count): item = root.child(i) gpu_sensor_ids.append(item.text(1)) # Second column is the id return gpu_sensor_ids def change_fan_icon(self, icon, fan): """Update the fan status icon.""" if fan == 1: self.ui.labelStatusFan1.setPixmap(QtGui.QPixmap(icon)) if fan == 2: self.ui.labelStatusFan2.setPixmap(QtGui.QPixmap(icon)) if fan == 3: self.ui.labelStatusFan3.setPixmap(QtGui.QPixmap(icon)) if fan == 4: self.ui.labelStatusFan4.setPixmap(QtGui.QPixmap(icon)) if fan == 5: self.ui.labelStatusFan5.setPixmap(QtGui.QPixmap(icon)) if fan == 6: self.ui.labelStatusFan6.setPixmap(QtGui.QPixmap(icon)) def closeEvent(self, event): """Save UI settings and stops the running thread gracefully, then exit the application. Called when closing the application window. """ # Stop the running thread if self.thread.isRunning(): self.thread.stop() print("Thread stopped") # Save UI settings settings.save_settings(self.config, self.ui) print("Settings saved") # Hide tray icon self.trayIcon.hide() # Accept the closing event and close application event.accept() def changeEvent(self, event): if event.type() == QtCore.QEvent.WindowStateChange: if self.windowState() & QtCore.Qt.WindowMinimized: if self.ui.checkBoxMinimizeToTray.isChecked(): event.ignore() self.minimize_to_tray() else: self.show() event.accept() def toggle_visibility(self): if self.isVisible(): self.minimize_to_tray() else: self.restore_from_tray() def minimize_to_tray(self): self.hide() # self.trayIcon.show() def restore_from_tray(self): self.setWindowState(self.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) self.activateWindow() self.show() # self.trayIcon.hide() class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def __init__(self, icon, parent=None): super(SystemTrayIcon, self).__init__(icon, parent) self.parent = parent self.setToolTip("Grid Control") self.activated.connect(self.on_systemTrayIcon_activated) menu = QtWidgets.QMenu() showAction = menu.addAction("Hide/Show") showAction.triggered.connect(parent.toggle_visibility) menu.addSeparator() exitAction = menu.addAction("Exit") exitAction.triggered.connect(parent.close) self.setContextMenu(menu) def on_systemTrayIcon_activated(self, reason): if reason == QtWidgets.QSystemTrayIcon.DoubleClick: self.parent.toggle_visibility() if __name__ == "__main__": # Use a rewritten excepthook for displaying unhandled exceptions as a QMessageBox sys.excepthook = helper.excepthook # Create the QT application app = QtWidgets.QApplication(sys.argv) # Create the main window win = GridControl() # Set program version win.setWindowTitle("Grid Control 1.0.9") # Show window #win.show() # Disable window resizing win.setFixedSize(win.size()) # Start QT application sys.exit(app.exec_())