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
+
+ 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()