diff --git a/pytec/setup.py b/pytec/setup.py index 210b175..4ac8f58 100644 --- a/pytec/setup.py +++ b/pytec/setup.py @@ -14,5 +14,5 @@ setup( "tec_qt = tec_qt:main", ] }, - py_modules=['tec_qt', 'ui_tec_qt', 'autotune'], + py_modules=['tec_qt', 'ui_tec_qt', 'autotune', 'waitingspinnerwidget'], ) diff --git a/pytec/tec_qt.py b/pytec/tec_qt.py index 43f2928..1a12909 100644 --- a/pytec/tec_qt.py +++ b/pytec/tec_qt.py @@ -258,9 +258,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): {'name': 'Target Temperature', 'type': 'float', 'value': 20, 'step': 0.1, 'siPrefix': True, 'suffix': '°C'}, {'name': 'Test Current', 'type': 'float', 'value': 1, 'step': 0.1, 'siPrefix': True, 'suffix': 'A'}, {'name': 'Temperature Swing', 'type': 'float', 'value': 1.5, 'step': 0.1, 'siPrefix': True, 'prefix': '±', 'suffix': '°C'}, - {'name': 'Run', 'type': 'action', 'tip': 'Run', 'children': [ - {'name': 'Autotuning...', 'type': 'progress', 'value': 0, 'readonly': True, 'visible': False}, - ]}, + {'name': 'Run', 'type': 'action', 'tip': 'Run'}, ]}, ]}, {'name': 'Save to flash', 'type': 'action', 'tip': 'Save config to thermostat, applies on reset'}, @@ -307,6 +305,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): for _ in range(2) ] + self.loading_spinner.hide() + self.hw_rev_data = None if args.connect: @@ -675,16 +675,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.report_refresh_spin.value(), 3) self.autotuners[ch].setReady() - param.child('Autotuning...').show(True) param.setOpts(title="Stop") self.client_watcher.report_update.connect(self.autotune_tick) + self.loading_spinner.show() + self.loading_spinner.start() + if self.autotuners[1 - ch].state() == PIDAutotuneState.STATE_OFF: + self.background_task_lbl.setText("Autotuning channel {ch}...".format(ch=ch)) + else: + self.background_task_lbl.setText("Autotuning channel 0 and 1...") case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN: self.autotuners[ch].setOff() - param.child('Autotuning...').hide() param.setOpts(title="Run") await self.client.set_param('pwm', ch, 'i_set', 0) self.client_watcher.report_update.disconnect(self.autotune_tick) - param.child('Autotuning...').setValue(0) + if self.autotuners[1 - ch].state() == PIDAutotuneState.STATE_OFF: + self.background_task_lbl.setText("Ready.") + self.loading_spinner.stop() + self.loading_spinner.hide() + else: + self.background_task_lbl.setText("Autotuning channel {ch}...".format(ch=1-ch)) self.params[i].child('PID Config', 'PID Auto Tune', 'Run').sigActivated.connect(autotune) @@ -695,29 +704,34 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): match self.autotuners[channel].state(): case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN: self.autotuners[channel].run(channel_report['temperature'], channel_report['time']) - progress_bar = self.params[channel].child('PID Config', 'PID Auto Tune', 'Run', 'Autotuning...') - if progress_bar.value() < 99: - progress_bar.setValue(progress_bar.value() + 1) # TODO: Measure the progress better await self.client.set_param('pwm', channel, 'i_set', self.autotuners[channel].output()) case PIDAutotuneState.STATE_SUCCEEDED: kp, ki, kd = self.autotuners[channel].get_tec_pid() self.autotuners[channel].setOff() self.params[channel].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run") - self.params[channel].child('PID Config', 'PID Auto Tune', 'Run', 'Autotuning...').hide() await self.client.set_param('pid', channel, 'kp', kp) await self.client.set_param('pid', channel, 'ki', ki) await self.client.set_param('pid', channel, 'kd', kd) await self.client.set_param('pwm', channel, 'pid') await self.client.set_param('pid', channel, 'target', self.params[channel].child("PID Config", "PID Auto Tune", "Target Temperature").value()) self.client_watcher.report_update.disconnect(self.autotune_tick) - self.params[channel].child('PID Config', 'PID Auto Tune', 'Run', 'Autotuning...').setValue(0) + if self.autotuners[1 - channel].state() == PIDAutotuneState.STATE_OFF: + self.background_task_lbl.setText("Ready.") + self.loading_spinner.stop() + self.loading_spinner.hide() + else: + self.background_task_lbl.setText("Autotuning channel {ch}...".format(ch=1-ch)) case PIDAutotuneState.STATE_FAILED: self.autotuners[channel].setOff() - self.params[channel].child('PID Config', 'PID Auto Tune', 'Run', 'Autotuning...').hide() self.params[channel].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run") await self.client.set_param('pwm', channel, 'i_set', 0) self.client_watcher.report_update.disconnect(self.autotune_tick) - self.params[channel].child('PID Config', 'PID Auto Tune', 'Run', 'Autotuning...').setValue(0) + if self.autotuners[1 - channel].state() == PIDAutotuneState.STATE_OFF: + self.background_task_lbl.setText("Ready.") + self.loading_spinner.stop() + self.loading_spinner.hide() + else: + self.background_task_lbl.setText("Autotuning channel {ch}...".format(ch=1-ch)) @pyqtSlot(list) def update_pid(self, pid_settings): diff --git a/pytec/tec_qt.ui b/pytec/tec_qt.ui index c5790d8..fb95c2f 100644 --- a/pytec/tec_qt.ui +++ b/pytec/tec_qt.ui @@ -248,6 +248,16 @@ + + + + Ready. + + + + + + @@ -511,6 +521,12 @@
pglive.sources.live_plot_widget
1 + + QtWaitingSpinner + QWidget +
waitingspinnerwidget
+ 1 +
diff --git a/pytec/ui_tec_qt.py b/pytec/ui_tec_qt.py index ca9f1cc..e879fbc 100644 --- a/pytec/ui_tec_qt.py +++ b/pytec/ui_tec_qt.py @@ -124,6 +124,12 @@ class Ui_MainWindow(object): self.plot_settings.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup) self.plot_settings.setObjectName("plot_settings") self.settings_layout.addWidget(self.plot_settings) + self.background_task_lbl = QtWidgets.QLabel(parent=self.bottom_settings_group) + self.background_task_lbl.setObjectName("background_task_lbl") + self.settings_layout.addWidget(self.background_task_lbl) + self.loading_spinner = QtWaitingSpinner(parent=self.bottom_settings_group) + self.loading_spinner.setObjectName("loading_spinner") + self.settings_layout.addWidget(self.loading_spinner) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.settings_layout.addItem(spacerItem) self.report_group = QtWidgets.QWidget(parent=self.bottom_settings_group) @@ -223,6 +229,7 @@ class Ui_MainWindow(object): self.status_lbl.setText(_translate("MainWindow", "Disconnected")) self.plot_settings.setToolTip(_translate("MainWindow", "Plot Settings")) self.plot_settings.setText(_translate("MainWindow", "📉")) + self.background_task_lbl.setText(_translate("MainWindow", "Ready.")) self.report_lbl.setText(_translate("MainWindow", "Poll every: ")) self.report_refresh_spin.setSuffix(_translate("MainWindow", " s")) self.report_box.setText(_translate("MainWindow", "Report")) @@ -241,6 +248,7 @@ class Ui_MainWindow(object): self.actionSave_all_configs.setToolTip(_translate("MainWindow", "Save configuration for all channels to flash")) from pglive.sources.live_plot_widget import LivePlotWidget from pyqtgraph.parametertree import ParameterTree +from waitingspinnerwidget import QtWaitingSpinner if __name__ == "__main__": diff --git a/pytec/waitingspinnerwidget.py b/pytec/waitingspinnerwidget.py new file mode 100644 index 0000000..2c6e647 --- /dev/null +++ b/pytec/waitingspinnerwidget.py @@ -0,0 +1,194 @@ +""" +The MIT License (MIT) + +Copyright (c) 2012-2014 Alexander Turkin +Copyright (c) 2014 William Hallatt +Copyright (c) 2015 Jacob Dawid +Copyright (c) 2016 Luca Weiss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import math + +from PyQt6.QtCore import * +from PyQt6.QtGui import * +from PyQt6.QtWidgets import * + + +class QtWaitingSpinner(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + # WAS IN initialize() + self._color = QColor(Qt.GlobalColor.black) + self._roundness = 100.0 + self._minimumTrailOpacity = 3.14159265358979323846 + self._trailFadePercentage = 80.0 + self._revolutionsPerSecond = 1.57079632679489661923 + self._numberOfLines = 20 + self._lineLength = 5 + self._lineWidth = 2 + self._innerRadius = 5 + self._currentCounter = 0 + + self._timer = QTimer(self) + self._timer.timeout.connect(self.rotate) + self.updateSize() + self.updateTimer() + # END initialize() + + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + def paintEvent(self, QPaintEvent): + painter = QPainter(self) + painter.fillRect(self.rect(), Qt.GlobalColor.transparent) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) + + if self._currentCounter >= self._numberOfLines: + self._currentCounter = 0 + + painter.setPen(Qt.PenStyle.NoPen) + for i in range(0, self._numberOfLines): + painter.save() + painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength) + rotateAngle = float(360 * i) / float(self._numberOfLines) + painter.rotate(rotateAngle) + painter.translate(self._innerRadius, 0) + distance = self.lineCountDistanceFromPrimary(i, self._currentCounter, self._numberOfLines) + color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage, + self._minimumTrailOpacity, self._color) + painter.setBrush(color) + painter.drawRoundedRect(QRect(0, int(-self._lineWidth / 2), self._lineLength, self._lineWidth), self._roundness, + self._roundness, Qt.SizeMode.RelativeSize) + painter.restore() + + def start(self): + if not self._timer.isActive(): + self._timer.start() + self._currentCounter = 0 + + def stop(self): + if self._timer.isActive(): + self._timer.stop() + self._currentCounter = 0 + + def setNumberOfLines(self, lines): + self._numberOfLines = lines + self._currentCounter = 0 + self.updateTimer() + + def setLineLength(self, length): + self._lineLength = length + self.updateSize() + + def setLineWidth(self, width): + self._lineWidth = width + self.updateSize() + + def setInnerRadius(self, radius): + self._innerRadius = radius + self.updateSize() + + def color(self): + return self._color + + def roundness(self): + return self._roundness + + def minimumTrailOpacity(self): + return self._minimumTrailOpacity + + def trailFadePercentage(self): + return self._trailFadePercentage + + def revolutionsPersSecond(self): + return self._revolutionsPerSecond + + def numberOfLines(self): + return self._numberOfLines + + def lineLength(self): + return self._lineLength + + def lineWidth(self): + return self._lineWidth + + def innerRadius(self): + return self._innerRadius + + def setRoundness(self, roundness): + self._roundness = max(0.0, min(100.0, roundness)) + + def setColor(self, color=Qt.GlobalColor.black): + self._color = QColor(color) + + def setRevolutionsPerSecond(self, revolutionsPerSecond): + self._revolutionsPerSecond = revolutionsPerSecond + self.updateTimer() + + def setTrailFadePercentage(self, trail): + self._trailFadePercentage = trail + + def setMinimumTrailOpacity(self, minimumTrailOpacity): + self._minimumTrailOpacity = minimumTrailOpacity + + def rotate(self): + self._currentCounter += 1 + if self._currentCounter >= self._numberOfLines: + self._currentCounter = 0 + self.update() + + def updateSize(self): + self.size = (self._innerRadius + self._lineLength) * 2 + self.setFixedSize(self.size, self.size) + + def updateTimer(self): + self._timer.setInterval(int(1000 / (self._numberOfLines * self._revolutionsPerSecond))) + + def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines): + distance = primary - current + if distance < 0: + distance += totalNrOfLines + return distance + + def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput): + color = QColor(colorinput) + if countDistance == 0: + return color + minAlphaF = minOpacity / 100.0 + distanceThreshold = int(math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)) + if countDistance > distanceThreshold: + color.setAlphaF(minAlphaF) + else: + alphaDiff = color.alphaF() - minAlphaF + gradient = alphaDiff / float(distanceThreshold + 1) + resultAlpha = color.alphaF() - gradient * countDistance + # If alpha is out of bounds, clip it. + resultAlpha = min(1.0, max(0.0, resultAlpha)) + color.setAlphaF(resultAlpha) + return color + + +if __name__ == '__main__': + app = QApplication([]) + waiting_spinner = QtWaitingSpinner() + waiting_spinner.show() + waiting_spinner.start() + app.exec()