thermostat/pytec/tec_qt.py

255 lines
8.8 KiB
Python
Raw Normal View History

2024-09-09 15:09:04 +08:00
"""GUI for the Sinara 8451 Thermostat"""
2024-09-09 15:08:17 +08:00
import json
import asyncio
import logging
import argparse
import importlib.resources
import qasync
from qasync import asyncSlot, asyncClose
from autotune import PIDAutotuneState
from PyQt6 import QtWidgets, QtGui, uic
from PyQt6.QtCore import pyqtSlot
from pytec.gui.model.thermostat import Thermostat, ThermostatConnectionState
from pytec.gui.model.pid_autotuner import PIDAutoTuner
2024-09-09 15:06:22 +08:00
from pytec.gui.view.zero_limits_warning_view import ZeroLimitsWarningView
from pytec.gui.view.thermostat_settings_menu import ThermostatSettingsMenu
from pytec.gui.view.connection_details_menu import ConnectionDetailsMenu
from pytec.gui.view.plot_options_menu import PlotOptionsMenu
from pytec.gui.view.live_plot_view import LiveDataPlotter
from pytec.gui.view.ctrl_panel import CtrlPanel
from pytec.gui.view.info_box import InfoBox
def get_argparser():
parser = argparse.ArgumentParser(description="Thermostat Control Panel")
parser.add_argument(
"--connect",
default=None,
action="store_true",
2024-08-23 15:43:02 +08:00
help="Automatically connect to the specified Thermostat in host:port format",
)
2024-08-23 15:43:02 +08:00
parser.add_argument("HOST", metavar="host", default=None, nargs="?")
parser.add_argument("PORT", metavar="port", default=None, nargs="?")
parser.add_argument(
"-l",
"--log",
dest="logLevel",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set the logging level",
)
parser.add_argument(
"-p",
"--param_tree",
default=importlib.resources.files("pytec.gui.view").joinpath("param_tree.json"),
help="Param Tree Description JSON File",
)
return parser
class MainWindow(QtWidgets.QMainWindow):
NUM_CHANNELS = 2
2023-08-16 17:35:13 +08:00
def __init__(self, args):
2024-08-28 17:22:59 +08:00
super().__init__()
2023-08-16 17:35:13 +08:00
ui_file_path = importlib.resources.files("pytec.gui.view").joinpath("tec_qt.ui")
uic.loadUi(ui_file_path, self)
2023-08-16 17:35:13 +08:00
2024-09-09 12:01:42 +08:00
self._info_box = InfoBox()
2023-08-16 17:35:13 +08:00
2024-08-28 17:20:14 +08:00
# Models
2024-09-09 12:01:42 +08:00
self._thermostat = Thermostat(self, self.report_refresh_spin.value())
self._connecting_task = None
2024-09-09 12:03:19 +08:00
self._thermostat.connection_state_update.connect(
self._on_connection_state_changed
)
2024-09-09 12:01:42 +08:00
self._autotuners = PIDAutoTuner(self, self._thermostat, 2)
2024-09-09 12:03:19 +08:00
self._autotuners.autotune_state_changed.connect(
self._on_pid_autotune_state_changed
)
2024-08-28 17:20:14 +08:00
# Handlers for disconnections
async def autotune_disconnect():
for ch in range(self.NUM_CHANNELS):
2024-09-09 12:01:42 +08:00
if self._autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
await self._autotuners.stop_pid_from_running(ch)
self._thermostat.disconnect_cb = autotune_disconnect
2024-08-29 12:11:49 +08:00
@pyqtSlot()
def handle_connection_error():
2024-09-09 12:01:42 +08:00
self._info_box.display_info_box(
"Connection Error", "Thermostat connection lost. Is it unplugged?"
)
2024-09-09 12:01:42 +08:00
self._thermostat.connection_error.connect(handle_connection_error)
2023-08-16 17:35:13 +08:00
2024-08-28 17:20:14 +08:00
# Control Panel
def get_ctrl_panel_config(args):
2024-08-14 11:54:15 +08:00
with open(args.param_tree, "r", encoding="utf-8") as f:
return json.load(f)["ctrl_panel"]
2024-09-09 12:01:42 +08:00
self._ctrl_panel_view = CtrlPanel(
self._thermostat,
self._autotuners,
self._info_box,
[self.ch0_tree, self.ch1_tree],
get_ctrl_panel_config(args),
)
2024-06-28 10:02:43 +08:00
2024-08-28 17:20:14 +08:00
# Graphs
2024-09-09 12:01:42 +08:00
self._channel_graphs = LiveDataPlotter(
self._thermostat,
[
[getattr(self, f"ch{ch}_t_graph"), getattr(self, f"ch{ch}_i_graph")]
for ch in range(self.NUM_CHANNELS)
]
)
2024-08-28 17:20:14 +08:00
# Bottom bar menus
2024-09-09 15:06:22 +08:00
self.connection_details_menu = ConnectionDetailsMenu(
self._thermostat, self.connect_btn
)
self.connect_btn.setMenu(self.connection_details_menu)
2023-08-16 17:35:13 +08:00
2024-09-09 15:06:22 +08:00
self._thermostat_settings_menu = ThermostatSettingsMenu(
2024-09-09 12:01:42 +08:00
self._thermostat, self._info_box, self.style()
)
2024-09-09 15:06:22 +08:00
self.thermostat_settings.setMenu(self._thermostat_settings_menu)
2023-08-16 17:35:13 +08:00
2024-09-09 12:01:42 +08:00
self._plot_options_menu = PlotOptionsMenu(self._channel_graphs)
self.plot_settings.setMenu(self._plot_options_menu)
2024-08-28 17:20:14 +08:00
# Status line
2024-09-09 15:06:22 +08:00
self._zero_limits_warning_view = ZeroLimitsWarningView(
2024-09-09 12:01:42 +08:00
self._thermostat, self.style(), self.limits_warning
2024-08-28 17:20:14 +08:00
)
2023-08-16 17:35:13 +08:00
self.loading_spinner.hide()
2024-08-28 17:20:14 +08:00
self.report_apply_btn.clicked.connect(
2024-09-09 12:01:42 +08:00
lambda: self._thermostat.set_update_s(self.report_refresh_spin.value())
2024-08-28 17:20:14 +08:00
)
2024-08-29 11:26:11 +08:00
@asyncClose
async def closeEvent(self, _event):
try:
2024-09-09 12:01:42 +08:00
await self._thermostat.end_session()
self._thermostat.connection_state = ThermostatConnectionState.DISCONNECTED
2024-08-29 11:26:11 +08:00
except:
pass
2024-08-29 12:51:18 +08:00
@pyqtSlot(ThermostatConnectionState)
2024-09-09 12:03:19 +08:00
def _on_connection_state_changed(self, state):
2024-08-28 10:24:37 +08:00
self.graph_group.setEnabled(state == ThermostatConnectionState.CONNECTED)
2024-08-28 10:24:25 +08:00
self.thermostat_settings.setEnabled(
2024-08-28 10:24:37 +08:00
state == ThermostatConnectionState.CONNECTED
2024-08-28 10:24:25 +08:00
)
2024-08-29 11:47:39 +08:00
self.report_group.setEnabled(state == ThermostatConnectionState.CONNECTED)
2024-08-28 10:24:25 +08:00
2024-08-28 10:24:37 +08:00
match state:
case ThermostatConnectionState.CONNECTED:
self.connect_btn.setText("Disconnect")
2024-08-27 18:01:27 +08:00
self.status_lbl.setText(
2024-08-28 16:32:31 +08:00
"Connected to Thermostat v"
2024-09-09 12:01:42 +08:00
f"{self._thermostat.hw_rev['rev']['major']}."
f"{self._thermostat.hw_rev['rev']['minor']}"
2024-08-27 18:01:27 +08:00
)
case ThermostatConnectionState.CONNECTING:
self.connect_btn.setText("Stop")
2024-08-28 10:50:48 +08:00
self.status_lbl.setText("Connecting...")
case ThermostatConnectionState.DISCONNECTED:
self.connect_btn.setText("Connect")
self.status_lbl.setText("Disconnected")
2024-08-29 12:51:18 +08:00
@pyqtSlot(int, PIDAutotuneState)
2024-09-09 12:03:19 +08:00
def _on_pid_autotune_state_changed(self, _ch, _state):
2024-09-09 15:10:16 +08:00
autotuning_channels = []
2024-08-29 11:26:11 +08:00
for ch in range(self.NUM_CHANNELS):
2024-09-09 12:01:42 +08:00
if self._autotuners.get_state(ch) in {
2024-08-29 11:26:11 +08:00
PIDAutotuneState.STATE_READY,
PIDAutotuneState.STATE_RELAY_STEP_UP,
PIDAutotuneState.STATE_RELAY_STEP_DOWN,
}:
2024-09-09 15:10:16 +08:00
autotuning_channels.append(ch)
2024-09-09 15:10:16 +08:00
if len(autotuning_channels) == 0:
2024-08-29 11:26:11 +08:00
self.background_task_lbl.setText("Ready.")
self.loading_spinner.hide()
self.loading_spinner.stop()
else:
self.background_task_lbl.setText(
2024-09-09 15:10:16 +08:00
f"Autotuning channel {autotuning_channels}..."
2024-08-29 11:26:11 +08:00
)
self.loading_spinner.start()
self.loading_spinner.show()
@asyncSlot()
async def on_connect_btn_clicked(self):
2024-09-09 12:01:42 +08:00
match self._thermostat.connection_state:
case ThermostatConnectionState.DISCONNECTED:
self._connecting_task = asyncio.create_task(
2024-09-09 12:01:42 +08:00
self._thermostat.start_session(
2024-09-09 15:06:22 +08:00
host=self.connection_details_menu.host_set_line.text(),
port=self.connection_details_menu.port_set_spin.value(),
)
)
2024-09-09 12:01:42 +08:00
self._thermostat.connection_state = ThermostatConnectionState.CONNECTING
await self._connecting_task
self._connecting_task = None
2024-09-09 12:01:42 +08:00
self._thermostat.connection_state = ThermostatConnectionState.CONNECTED
self._thermostat.start_watching()
case ThermostatConnectionState.CONNECTING:
self._connecting_task.cancel()
2024-09-09 14:52:33 +08:00
self._connecting_task = None
await self._thermostat.end_session()
self._thermostat.connection_state = (
ThermostatConnectionState.DISCONNECTED
)
case ThermostatConnectionState.CONNECTED:
2024-09-09 12:01:42 +08:00
await self._thermostat.end_session()
self._thermostat.connection_state = (
ThermostatConnectionState.DISCONNECTED
)
async def coro_main():
args = get_argparser().parse_args()
if args.logLevel:
logging.basicConfig(level=getattr(logging, args.logLevel))
app_quit_event = asyncio.Event()
app = QtWidgets.QApplication.instance()
app.aboutToQuit.connect(app_quit_event.set)
app.setWindowIcon(
2024-07-03 14:40:13 +08:00
QtGui.QIcon(
str(importlib.resources.files("pytec.gui.resources").joinpath("artiq.ico"))
)
)
main_window = MainWindow(args)
main_window.show()
if args.connect:
if args.HOST:
2024-09-09 15:06:22 +08:00
main_window.connection_details_menu.host_set_line.setText(args.HOST)
if args.PORT:
2024-09-09 15:06:22 +08:00
main_window.connection_details_menu.port_set_spin.setValue(int(args.PORT))
main_window.connect_btn.click()
await app_quit_event.wait()
def main():
qasync.run(coro_main())
if __name__ == "__main__":
main()