From fdf4c4f0d66f3f6036aa66df63f75d92fe1d7b21 Mon Sep 17 00:00:00 2001 From: atse Date: Thu, 6 Jul 2023 16:06:33 +0800 Subject: [PATCH] Plot temperature and current graphs - Have units - Samples are limited - pglive is used for better live graphs -- Also fixes bug with constantly updating normal pyqtgraphs where it will bug out if right-clicked on and context menu is brought up --Since pglive requires pyqtgraph == 0.13.3, upgrade pyqtgraph to that too. --- flake.nix | 27 ++++++++++++++++++-- pytec/tec_qt.py | 62 +++++++++++++++++++++++++++++++++++++++++++++- pytec/tec_qt.ui | 20 +++++++-------- pytec/ui_tec_qt.py | 12 ++++----- 4 files changed, 102 insertions(+), 19 deletions(-) diff --git a/flake.nix b/flake.nix index ccc5ad8..5c80712 100644 --- a/flake.nix +++ b/flake.nix @@ -68,13 +68,36 @@ propagatedBuildInputs = [ pkgs.python3Packages.pyqt6 ]; }; + pyqtgraph = pkgs.python3Packages.buildPythonPackage rec { + pname = "pyqtgraph"; + version = "0.13.3"; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + sha256 = "sha256-WBCNhBHHBU4IQdi3ke6F4QH8KWubNZwOAd3jipj/Ks4="; + }; + propagatedBuildInputs = with pkgs.python3Packages; [ numpy pyqt6 ]; + }; + + pglive = pkgs.python3Packages.buildPythonPackage rec { + pname = "pglive"; + version = "0.7.2"; + format = "pyproject"; + src = pkgs.fetchPypi { + inherit pname version; + sha256 = "sha256-jqj8X6H1N5mJQ4OrY5ANqRB0YJByqg/bNneEALWmH1A="; + }; + buildInputs = [ pkgs.python3Packages.poetry-core ]; + propagatedBuildInputs = [ pyqtgraph pkgs.python3Packages.numpy ]; + }; + thermostat_gui = pkgs.python3Packages.buildPythonPackage { pname = "thermostat_gui"; version = "0.0.0"; src = "${self}/pytec"; nativeBuildInputs = [ pkgs.qt6.wrapQtAppsHook ]; - propagatedBuildInputs = [ pkgs.qt6.qtbase ] ++ (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync ]); + propagatedBuildInputs = [ pkgs.qt6.qtbase ] ++ (with pkgs.python3Packages; [ pyqtgraph pyqt6 qasync pglive ]); dontWrapQtApps = true; postFixup = '' @@ -100,7 +123,7 @@ buildInputs = with pkgs; [ rust openocd dfu-util ] ++ (with python3Packages; [ - numpy matplotlib pyqtgraph setuptools pyqt6 qasync + numpy matplotlib pyqtgraph setuptools pyqt6 qasync pglive ]); }; defaultPackage.x86_64-linux = thermostat; diff --git a/pytec/tec_qt.py b/pytec/tec_qt.py index ec3be67..bfa362b 100644 --- a/pytec/tec_qt.py +++ b/pytec/tec_qt.py @@ -1,8 +1,12 @@ from PyQt6 import QtWidgets, QtGui from PyQt6.QtCore import pyqtSignal, QObject, QSignalBlocker, pyqtSlot -from pyqtgraph import PlotWidget from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType import pyqtgraph as pg +from pglive.sources.data_connector import DataConnector +from pglive.kwargs import Axis +from pglive.sources.live_plot import LiveLinePlot +from pglive.sources.live_plot_widget import LivePlotWidget +from pglive.sources.live_axis import LiveAxis import sys import argparse import logging @@ -126,11 +130,17 @@ class ClientWatcher(QObject): class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): + + """The maximum number of sample points to store.""" + DEFAULT_MAX_SAMPLES = 1000 + def __init__(self, args): super().__init__() self.setupUi(self) + self.max_samples = self.DEFAULT_MAX_SAMPLES + self._set_up_context_menu() self.fan_power_slider.valueChanged.connect(self.fan_set) @@ -138,11 +148,24 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self._set_param_tree() + self.ch0_t_plot = LiveLinePlot() + self.ch0_i_plot = LiveLinePlot() + self.ch1_t_plot = LiveLinePlot() + self.ch1_i_plot = LiveLinePlot() + + self._set_up_graphs() + + self.ch0_t_connector = DataConnector(self.ch0_t_plot, max_points=self.DEFAULT_MAX_SAMPLES) + self.ch0_i_connector = DataConnector(self.ch0_i_plot, max_points=self.DEFAULT_MAX_SAMPLES) + self.ch1_t_connector = DataConnector(self.ch1_t_plot, max_points=self.DEFAULT_MAX_SAMPLES) + self.ch1_i_connector = DataConnector(self.ch1_i_plot, max_points=self.DEFAULT_MAX_SAMPLES) + self.fan_pwm_recommended = False self.tec_client = Client() self.client_watcher = ClientWatcher(self, self.tec_client, self.report_refresh_spin.value()) self.client_watcher.fan_update.connect(self.fan_update) + self.client_watcher.report_update.connect(self.plot) self.client_watcher.report_update.connect(self.update_report) self.client_watcher.pid_update.connect(self.update_pid) self.client_watcher.pwm_update.connect(self.update_pwm) @@ -175,6 +198,31 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.thermostat_settings.setMenu(self.menu) + def _set_up_graphs(self): + for graph in self.ch0_t_graph, self.ch0_i_graph, self.ch1_t_graph, self.ch1_i_graph: + time_axis = LiveAxis('bottom', text="Time since Thermostat reset", **{Axis.TICK_FORMAT: Axis.DURATION}) + time_axis.showLabel() + graph.setAxisItems({'bottom': time_axis}) + + for graph in self.ch0_t_graph, self.ch1_t_graph: + temperature_axis = LiveAxis('left', text="Temperature", units="°C") + temperature_axis.showLabel() + graph.setAxisItems({'left': temperature_axis}) + + for graph in self.ch0_i_graph, self.ch1_i_graph: + current_axis = LiveAxis('left', text="Current", units="A") + current_axis.showLabel() + graph.setAxisItems({'left': current_axis}) + + self.ch0_t_graph.addItem(self.ch0_t_plot) + self.ch0_i_graph.addItem(self.ch0_i_plot) + self.ch1_t_graph.addItem(self.ch1_t_plot) + self.ch1_i_graph.addItem(self.ch1_i_plot) + + def clear_graphs(self): + for connector in self.ch0_t_connector, self.ch0_i_connector, self.ch1_t_connector, self.ch1_i_connector: + connector.clear() + async def _on_connection_changed(self, result): self.graph_group.setEnabled(result) self.fan_group.setEnabled(result) @@ -191,6 +239,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.status_lbl.setText("Disconnected") self.fan_pwm_warning.setPixmap(QtGui.QPixmap()) self.fan_pwm_warning.setToolTip("") + self.clear_graphs() self.client_watcher.stop_watching() def _set_fan_pwm_warning(self): @@ -270,6 +319,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): await self._on_connection_changed(False) await self.tec_client.disconnect() + @pyqtSlot(list) + def plot(self, report): + for channel in range(2): + temperature = report[channel]['temperature'] + current = report[channel]['tec_i'] + time = report[channel]['time'] + + if temperature is not None and current is not None: + getattr(self, f'ch{channel}_t_connector').cb_append_data_point(temperature, time) + getattr(self, f'ch{channel}_i_connector').cb_append_data_point(current, time) + @asyncSlot(object, object) async def send_command(self, param, changes): for param, change, data in changes: diff --git a/pytec/tec_qt.ui b/pytec/tec_qt.ui index 6cda65f..73533f6 100644 --- a/pytec/tec_qt.ui +++ b/pytec/tec_qt.ui @@ -104,28 +104,28 @@ - + Channel 1 Temperature - + Channel 0 Temperature - + Channel 0 Current - + Channel 1 Current @@ -656,18 +656,18 @@ - - PlotWidget - QWidget -
pyqtgraph
- 1 -
ParameterTree QWidget
pyqtgraph.parametertree
1
+ + LivePlotWidget + QWidget +
pglive.sources.live_plot_widget
+ 1 +
diff --git a/pytec/ui_tec_qt.py b/pytec/ui_tec_qt.py index 96cee43..40f54e6 100644 --- a/pytec/ui_tec_qt.py +++ b/pytec/ui_tec_qt.py @@ -1,6 +1,6 @@ # Form implementation generated from reading ui file 'tec_qt.ui' # -# Created by: PyQt6 UI code generator 6.4.2 +# Created by: PyQt6 UI code generator 6.5.0 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. @@ -52,16 +52,16 @@ class Ui_MainWindow(object): self.ch0_tree.setHeaderHidden(True) self.ch0_tree.setObjectName("ch0_tree") self.graphs_layout.addWidget(self.ch0_tree, 0, 0, 1, 1) - self.ch1_t_graph = PlotWidget(parent=self.graph_group) + self.ch1_t_graph = LivePlotWidget(parent=self.graph_group) self.ch1_t_graph.setObjectName("ch1_t_graph") self.graphs_layout.addWidget(self.ch1_t_graph, 1, 1, 1, 1) - self.ch0_t_graph = PlotWidget(parent=self.graph_group) + self.ch0_t_graph = LivePlotWidget(parent=self.graph_group) self.ch0_t_graph.setObjectName("ch0_t_graph") self.graphs_layout.addWidget(self.ch0_t_graph, 0, 1, 1, 1) - self.ch0_i_graph = PlotWidget(parent=self.graph_group) + self.ch0_i_graph = LivePlotWidget(parent=self.graph_group) self.ch0_i_graph.setObjectName("ch0_i_graph") self.graphs_layout.addWidget(self.ch0_i_graph, 0, 2, 1, 1) - self.ch1_i_graph = PlotWidget(parent=self.graph_group) + self.ch1_i_graph = LivePlotWidget(parent=self.graph_group) self.ch1_i_graph.setObjectName("ch1_i_graph") self.graphs_layout.addWidget(self.ch1_i_graph, 1, 2, 1, 1) self.graphs_layout.setColumnMinimumWidth(0, 100) @@ -306,7 +306,7 @@ class Ui_MainWindow(object): self.report_refresh_spin.setSuffix(_translate("MainWindow", " s")) self.report_box.setText(_translate("MainWindow", "Report")) self.report_apply_btn.setText(_translate("MainWindow", "Apply")) -from pyqtgraph import PlotWidget +from pglive.sources.live_plot_widget import LivePlotWidget from pyqtgraph.parametertree import ParameterTree