Compare commits

...

16 Commits

7 changed files with 108 additions and 88 deletions

View File

@ -30,6 +30,19 @@ class PIDAutoTuner(QObject):
def get_state(self, ch): def get_state(self, ch):
return self.autotuners[ch].state() return self.autotuners[ch].state()
@asyncSlot()
async def pid_auto_tune_request(self, ch):
match self.get_state(ch):
case PIDAutotuneState.STATE_OFF | PIDAutotuneState.STATE_FAILED:
self.load_params_and_set_ready(ch)
case (
PIDAutotuneState.STATE_READY
| PIDAutotuneState.STATE_RELAY_STEP_UP
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
):
await self.stop_pid_from_running(ch)
def load_params_and_set_ready(self, ch): def load_params_and_set_ready(self, ch):
self.autotuners[ch].setParam( self.autotuners[ch].setParam(
self.target_temp[ch], self.target_temp[ch],

View File

@ -39,7 +39,6 @@ class Thermostat(QObject, metaclass=PropertyMeta):
async def start_session(self, host, port): async def start_session(self, host, port):
self.connection_state_changed.emit(ThermostatConnectionState.CONNECTING) self.connection_state_changed.emit(ThermostatConnectionState.CONNECTING)
await self._client.connect(host, port) await self._client.connect(host, port)
await self.get_hw_rev()
self.connection_state_changed.emit(ThermostatConnectionState.CONNECTED) self.connection_state_changed.emit(ThermostatConnectionState.CONNECTED)
self.start_watching() self.start_watching()
@ -55,14 +54,11 @@ class Thermostat(QObject, metaclass=PropertyMeta):
exc_info=True, exc_info=True,
) )
self.connection_error.emit() self.connection_error.emit()
self.connection_errored = True
return return
self._update_params_task = asyncio.create_task(self.update_params()) self._update_params_task = asyncio.create_task(self.update_params())
await asyncio.sleep(self._update_s) await asyncio.sleep(self._update_s)
@pyqtSlot()
def timed_out(self):
self.connection_errored = True
async def get_hw_rev(self): async def get_hw_rev(self):
self.hw_rev = await self._client.hw_rev() self.hw_rev = await self._client.hw_rev()
return self.hw_rev return self.hw_rev

View File

@ -1,9 +1,17 @@
from PyQt6 import QtWidgets, QtCore from PyQt6 import QtWidgets, QtCore
from PyQt6.QtCore import pyqtSlot
from pytec.gui.model.thermostat import ThermostatConnectionState
class ConnMenu(QtWidgets.QMenu): class ConnMenu(QtWidgets.QMenu):
def __init__(self): def __init__(self, thermostat, connect_btn):
super().__init__() super().__init__()
self._thermostat = thermostat
self._connect_btn = connect_btn
self._thermostat.connection_state_changed.connect(
self.thermostat_state_change_handler
)
self.setTitle("Connection Settings") self.setTitle("Connection Settings")
self.host_set_line = QtWidgets.QLineEdit() self.host_set_line = QtWidgets.QLineEdit()
@ -13,7 +21,7 @@ class ConnMenu(QtWidgets.QMenu):
self.host_set_line.setClearButtonEnabled(True) self.host_set_line.setClearButtonEnabled(True)
def connect_on_enter_press(): def connect_on_enter_press():
self.connect_btn.click() self._connect_btn.click()
self.hide() self.hide()
self.host_set_line.returnPressed.connect(connect_on_enter_press) self.host_set_line.returnPressed.connect(connect_on_enter_press)
@ -54,3 +62,12 @@ class ConnMenu(QtWidgets.QMenu):
exit_action.setDefaultWidget(self.exit_button) exit_action.setDefaultWidget(self.exit_button)
self.addAction(exit_action) self.addAction(exit_action)
self.exit_action = exit_action self.exit_action = exit_action
@pyqtSlot(ThermostatConnectionState)
def thermostat_state_change_handler(self, state):
self.host_set_line.setEnabled(
state == ThermostatConnectionState.DISCONNECTED
)
self.port_set_spin.setEnabled(
state == ThermostatConnectionState.DISCONNECTED
)

View File

@ -94,13 +94,14 @@ class CtrlPanel(QObject):
) )
self.params[i].child( self.params[i].child(
"PID Config", "PID Auto Tune", "Run" "PID Config", "PID Auto Tune", "Run"
).sigActivated.connect(partial(self.pid_auto_tune_request, i)) ).sigActivated.connect(partial(self.autotuners.pid_auto_tune_request, i))
self.thermostat.pid_update.connect(self.update_pid) self.thermostat.pid_update.connect(self.update_pid)
self.thermostat.report_update.connect(self.update_report) self.thermostat.report_update.connect(self.update_report)
self.thermostat.thermistor_update.connect(self.update_thermistor) self.thermostat.thermistor_update.connect(self.update_thermistor)
self.thermostat.pwm_update.connect(self.update_pwm) self.thermostat.pwm_update.connect(self.update_pwm)
self.thermostat.postfilter_update.connect(self.update_postfilter) self.thermostat.postfilter_update.connect(self.update_postfilter)
self.autotuners.autotune_state_changed.connect(self.update_pid_autotune)
def _setValue(self, value, blockSignal=None): def _setValue(self, value, blockSignal=None):
""" """
@ -259,6 +260,31 @@ class CtrlPanel(QObject):
"Thermistor Config", "Postfilter Rate" "Thermistor Config", "Postfilter Rate"
).setValue(postfilter_params["rate"]) ).setValue(postfilter_params["rate"])
def update_pid_autotune(self, ch, state):
match state:
case PIDAutotuneState.STATE_OFF:
self.change_params_title(
ch, ("PID Config", "PID Auto Tune", "Run"), "Run"
)
case (
PIDAutotuneState.STATE_READY
| PIDAutotuneState.STATE_RELAY_STEP_UP
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
):
self.change_params_title(
ch, ("PID Config", "PID Auto Tune", "Run"), "Stop"
)
case PIDAutotuneState.STATE_SUCCEEDED:
self.info_box.display_info_box(
"PID Autotune Success",
f"Channel {ch} PID Config has been loaded to Thermostat. Regulating temperature.",
)
case PIDAutotuneState.STATE_FAILED:
self.info_box.display_info_box(
"PID Autotune Failed",
f"Channel {ch} PID Autotune has failed.",
)
@asyncSlot(int) @asyncSlot(int)
async def load_settings(self, ch): async def load_settings(self, ch):
await self.thermostat.load_cfg(ch) await self.thermostat.load_cfg(ch)
@ -277,17 +303,3 @@ class CtrlPanel(QObject):
f"Channel {ch} settings has been saved to flash.\n" f"Channel {ch} settings has been saved to flash.\n"
"It will be loaded on Thermostat reset, or when settings are explicitly loaded.", "It will be loaded on Thermostat reset, or when settings are explicitly loaded.",
) )
@asyncSlot()
async def pid_auto_tune_request(self, ch=0):
match self.autotuners.get_state(ch):
case PIDAutotuneState.STATE_OFF | PIDAutotuneState.STATE_FAILED:
self.autotuners.load_params_and_set_ready(ch)
case (
PIDAutotuneState.STATE_READY
| PIDAutotuneState.STATE_RELAY_STEP_UP
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
):
await self.autotuners.stop_pid_from_running(ch)

View File

@ -5,6 +5,7 @@ from pglive.sources.live_plot import LiveLinePlot
from pglive.sources.live_axis import LiveAxis from pglive.sources.live_axis import LiveAxis
from collections import deque from collections import deque
import pyqtgraph as pg import pyqtgraph as pg
from pytec.gui.model.thermostat import ThermostatConnectionState
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
@ -16,6 +17,9 @@ class LiveDataPlotter(QObject):
self._thermostat.report_update.connect(self.update_report) self._thermostat.report_update.connect(self.update_report)
self._thermostat.pid_update.connect(self.update_pid) self._thermostat.pid_update.connect(self.update_pid)
self._thermostat.connection_state_changed.connect(
self.thermostat_state_change_handler
)
self.NUM_CHANNELS = len(live_plots) self.NUM_CHANNELS = len(live_plots)
self.graphs = [] self.graphs = []
@ -25,6 +29,11 @@ class LiveDataPlotter(QObject):
live_plot[1].setTitle(f"Channel {i} Current") live_plot[1].setTitle(f"Channel {i} Current")
self.graphs.append(_TecGraphs(live_plot[0], live_plot[1])) self.graphs.append(_TecGraphs(live_plot[0], live_plot[1]))
@pyqtSlot(ThermostatConnectionState)
def thermostat_state_change_handler(self, state):
if state == ThermostatConnectionState.DISCONNECTED:
self.clear_graphs()
def _config_connector_max_pts(self, connector, samples): def _config_connector_max_pts(self, connector, samples):
connector.max_points = samples connector.max_points = samples
connector.x = deque(maxlen=int(connector.max_points)) connector.x = deque(maxlen=int(connector.max_points))

View File

@ -3,6 +3,7 @@ from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QSignalBlocker from PyQt6.QtCore import pyqtSignal, pyqtSlot, QSignalBlocker
from qasync import asyncSlot from qasync import asyncSlot
from pytec.gui.view.net_settings_input_diag import NetSettingsInputDiag from pytec.gui.view.net_settings_input_diag import NetSettingsInputDiag
from pytec.gui.model.thermostat import ThermostatConnectionState
class ThermostatCtrlMenu(QtWidgets.QMenu): class ThermostatCtrlMenu(QtWidgets.QMenu):
@ -15,6 +16,9 @@ class ThermostatCtrlMenu(QtWidgets.QMenu):
self.hw_rev_data = dict() self.hw_rev_data = dict()
self._thermostat.hw_rev_update.connect(self.hw_rev) self._thermostat.hw_rev_update.connect(self.hw_rev)
self._thermostat.connection_state_changed.connect(
self.thermostat_state_change_handler
)
self.fan_group = QtWidgets.QWidget() self.fan_group = QtWidgets.QWidget()
self.fan_group.setEnabled(False) self.fan_group.setEnabled(False)
@ -144,6 +148,12 @@ class ThermostatCtrlMenu(QtWidgets.QMenu):
self.fan_pwm_warning.setPixmap(QtGui.QPixmap()) self.fan_pwm_warning.setPixmap(QtGui.QPixmap())
self.fan_pwm_warning.setToolTip("") self.fan_pwm_warning.setToolTip("")
@pyqtSlot(ThermostatConnectionState)
def thermostat_state_change_handler(self, state):
if state == ThermostatConnectionState.DISCONNECTED:
self.fan_pwm_warning.setPixmap(QtGui.QPixmap())
self.fan_pwm_warning.setToolTip("")
@pyqtSlot("QVariantMap") @pyqtSlot("QVariantMap")
def hw_rev(self, hw_rev): def hw_rev(self, hw_rev):
self.hw_rev_data = hw_rev self.hw_rev_data = hw_rev

View File

@ -69,12 +69,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.info_box.display_info_box( self.info_box.display_info_box(
"Connection Error", "Thermostat connection lost. Is it unplugged?" "Connection Error", "Thermostat connection lost. Is it unplugged?"
) )
self.thermostat.end_session()
self.thermostat.connection_error.connect(handle_connection_error) self.thermostat.connection_error.connect(handle_connection_error)
self.thermostat.connection_error.connect(self.thermostat.timed_out)
self.thermostat.connection_error.connect(self.thermostat.end_session)
self.thermostat.connection_state_changed.connect(self._on_connection_changed) self.thermostat.connection_state_changed.connect(self._on_connection_changed)
self.autotuners = PIDAutoTuner(self, self.thermostat, 2) self.autotuners = PIDAutoTuner(self, self.thermostat, 2)
@ -99,7 +97,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.zero_limits_warning.set_limits_warning self.zero_limits_warning.set_limits_warning
) )
self.thermostat.hw_rev_update.connect(self._status)
self.report_apply_btn.clicked.connect( self.report_apply_btn.clicked.connect(
lambda: self.thermostat.set_update_s(self.report_refresh_spin.value()) lambda: self.thermostat.set_update_s(self.report_refresh_spin.value())
) )
@ -115,7 +112,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.plot_options_menu = PlotOptionsMenu(self.channel_graphs) self.plot_options_menu = PlotOptionsMenu(self.channel_graphs)
self.plot_settings.setMenu(self.plot_options_menu) self.plot_settings.setMenu(self.plot_options_menu)
self.conn_menu = ConnMenu() self.conn_menu = ConnMenu(self.thermostat, self.connect_btn)
self.connect_btn.setMenu(self.conn_menu) self.connect_btn.setMenu(self.conn_menu)
self.thermostat_ctrl_menu = ThermostatCtrlMenu( self.thermostat_ctrl_menu = ThermostatCtrlMenu(
@ -133,53 +130,34 @@ class MainWindow(QtWidgets.QMainWindow):
self.connect_btn.click() self.connect_btn.click()
@asyncSlot(ThermostatConnectionState) @asyncSlot(ThermostatConnectionState)
async def _on_connection_changed(self, result): async def _on_connection_changed(self, state):
match result: self.graph_group.setEnabled(state == ThermostatConnectionState.CONNECTED)
self.report_group.setEnabled(state == ThermostatConnectionState.CONNECTED)
self.thermostat_settings.setEnabled(
state == ThermostatConnectionState.CONNECTED
)
match state:
case ThermostatConnectionState.CONNECTED: case ThermostatConnectionState.CONNECTED:
self.graph_group.setEnabled(True)
self.report_group.setEnabled(True)
self.thermostat_settings.setEnabled(True)
self.conn_menu.host_set_line.setEnabled(False)
self.conn_menu.port_set_spin.setEnabled(False)
self.connect_btn.setText("Disconnect") self.connect_btn.setText("Disconnect")
hw_rev_d = await self.thermostat.get_hw_rev()
case ThermostatConnectionState.CONNECTING:
self.status_lbl.setText("Connecting...")
self.connect_btn.setText("Stop")
self.conn_menu.host_set_line.setEnabled(False)
self.conn_menu.port_set_spin.setEnabled(False)
case ThermostatConnectionState.DISCONNECTED:
self.graph_group.setEnabled(False)
self.report_group.setEnabled(False)
self.thermostat_settings.setEnabled(False)
self.conn_menu.host_set_line.setEnabled(True)
self.conn_menu.port_set_spin.setEnabled(True)
self.connect_btn.setText("Connect")
self.status_lbl.setText("Disconnected")
self.background_task_lbl.setText("Ready.")
self.loading_spinner.hide()
self.loading_spinner.stop()
self.thermostat_ctrl_menu.fan_pwm_warning.setPixmap(QtGui.QPixmap())
self.thermostat_ctrl_menu.fan_pwm_warning.setToolTip("")
self.channel_graphs.clear_graphs()
self.report_box.setChecked(False)
for ch in range(self.NUM_CHANNELS):
if self.autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
if self.thermostat.connection_errored:
# Don't send any commands, just reset local state
self.autotuners.autotuners[ch].setOff()
else:
await self.autotuners.stop_pid_from_running(ch)
def _status(self, hw_rev_d: dict):
self.status_lbl.setText( self.status_lbl.setText(
f"Connected to Thermostat v{hw_rev_d['rev']['major']}.{hw_rev_d['rev']['minor']}" f"Connected to Thermostat v{hw_rev_d['rev']['major']}.{hw_rev_d['rev']['minor']}"
) )
case ThermostatConnectionState.CONNECTING:
self.connect_btn.setText("Stop")
self.status_lbl.setText("Connecting...")
case ThermostatConnectionState.DISCONNECTED:
self.connect_btn.setText("Connect")
self.status_lbl.setText("Disconnected")
self.background_task_lbl.setText("Ready.")
self.loading_spinner.hide()
self.loading_spinner.stop()
self.report_box.setChecked(False)
@asyncSlot(int) @asyncSlot(int)
async def on_report_box_stateChanged(self, enabled): async def on_report_box_stateChanged(self, enabled):
await self.thermostat.set_report_mode(enabled) await self.thermostat.set_report_mode(enabled)
@ -213,41 +191,26 @@ class MainWindow(QtWidgets.QMainWindow):
elif self._connecting_task is not None: elif self._connecting_task is not None:
self._connecting_task.cancel() self._connecting_task.cancel()
else: else:
for ch in range(self.NUM_CHANNELS):
if self.autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
if self.thermostat.connection_errored:
# Don't send any commands, just reset local state
self.autotuners.autotuners[ch].setOff()
else:
await self.autotuners.stop_pid_from_running(ch)
await self.thermostat.end_session() await self.thermostat.end_session()
@asyncSlot(int, PIDAutotuneState) @asyncSlot(int, PIDAutotuneState)
async def pid_autotune_handler(self, _ch, _state): async def pid_autotune_handler(self, _ch, _state):
ch_tuning = [] ch_tuning = []
for ch in range(self.NUM_CHANNELS): for ch in range(self.NUM_CHANNELS):
match self.autotuners.get_state(ch): if self.autotuners.get_state(ch) in {
case PIDAutotuneState.STATE_OFF: PIDAutotuneState.STATE_READY,
self.ctrl_panel_view.change_params_title( PIDAutotuneState.STATE_RELAY_STEP_UP,
ch, ("PID Config", "PID Auto Tune", "Run"), "Run" PIDAutotuneState.STATE_RELAY_STEP_DOWN,
) }:
case (
PIDAutotuneState.STATE_READY
| PIDAutotuneState.STATE_RELAY_STEP_UP
| PIDAutotuneState.STATE_RELAY_STEP_DOWN
):
self.ctrl_panel_view.change_params_title(
ch, ("PID Config", "PID Auto Tune", "Run"), "Stop"
)
ch_tuning.append(ch) ch_tuning.append(ch)
case PIDAutotuneState.STATE_SUCCEEDED:
self.info_box.display_info_box(
"PID Autotune Success",
f"Channel {ch} PID Config has been loaded to Thermostat. Regulating temperature.",
)
self.info_box.show()
case PIDAutotuneState.STATE_FAILED:
self.info_box.display_info_box(
"PID Autotune Failed",
f"Channel {ch} PID Autotune has failed.",
)
self.info_box.show()
if len(ch_tuning) == 0: if len(ch_tuning) == 0:
self.background_task_lbl.setText("Ready.") self.background_task_lbl.setText("Ready.")
self.loading_spinner.hide() self.loading_spinner.hide()