From dacf9f0fa5b1dd3292285234611d214477e54e05 Mon Sep 17 00:00:00 2001 From: linuswck Date: Mon, 17 Jun 2024 12:35:00 +0800 Subject: [PATCH] gui: Add pid autotune to ctrl panel --- pykirdy/kirdy_qt.py | 79 ++++++++++++++++++++++++++++++++++++++++- pykirdy/pid_autotune.py | 19 +++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/pykirdy/kirdy_qt.py b/pykirdy/kirdy_qt.py index 8fbfdf3..a2569c6 100644 --- a/pykirdy/kirdy_qt.py +++ b/pykirdy/kirdy_qt.py @@ -29,6 +29,7 @@ from ui.ui_kirdy_qt import Ui_MainWindow from dateutil import tz import math import socket +from pid_autotune import PIDAutotune, PIDAutotuneState COMMON_ERROR_MSG = "Connection Timeout. Disconnecting." @@ -396,7 +397,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): {'name': 'Minimum', 'type': 'float', 'step': 100, 'limits': (-1, 1), 'decimals': 6, 'suffix': 'A', 'lock': False, 'target': 'thermostat', 'action': 'set_pid_output_min'}, {'name': 'Maximum', 'type': 'float', 'step': 100, 'limits': (-1, 1), 'decimals': 6, 'suffix': 'A', 'lock': False, 'target': 'thermostat', 'action': 'set_pid_output_max'}, ]}, - # TODO PID AutoTune + {'name': 'PID Auto Tune', 'expanded': False, 'type': 'group', 'children': [ + {'name': 'Target Temperature', 'type': 'float', 'value': 20, 'step': 0.1, 'format': '{value:.4f} °C'}, + {'name': 'Test Current', 'type': 'float', 'value': 1000, 'decimals': 6, 'step': 100, 'limits': (-3000, 3000), 'suffix': 'mA'}, + {'name': 'Temperature Swing', 'type': 'float', 'value': 0.0, 'step': 0.1, 'prefix': '±', 'format': '{value:.4f} °C'}, + {'name': 'Lookback', 'type': 'float', 'value': 5.0, 'step': 0.1, 'format': '{value:.4f} s'}, + {'name': 'Run', 'type': 'action', 'tip': 'Run'}, + ]}, ]}, ] def __init__(self, args): @@ -404,6 +411,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.kirdy = Kirdy() self.setupUi(self) + self.info_box = QtWidgets.QMessageBox() + self.info_box.setIcon(QtWidgets.QMessageBox.Icon.Information) + # Load Global QT Style Sheet Settings qss=os.path.join(os.path.dirname(__file__), "ui/mainwindow.qss") with open(qss,"r") as fh: @@ -420,6 +430,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.max_samples = self.DEFAULT_MAX_SAMPLES + self.autotuner = PIDAutotune(25) + self.setup_menu_bar() self._set_up_ctrl_btns() self._set_up_plot_menu() @@ -627,6 +639,71 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.params[3].sigTreeStateChanged.connect(self.send_command) self.params[3].setValue = _setValue + @asyncSlot() + async def autotune(param): + print("button clicked") + match self.autotuner.state(): + case PIDAutotuneState.STATE_OFF: + settings = await self.kirdy.device.get_settings_summary() + self.autotuner.setParam( + param.parent().child('Target Temperature').value(), + param.parent().child('Test Current').value() / 1000, + param.parent().child('Temperature Swing').value(), + 1.0 / settings['thermostat']['temp_adc_settings']['rate'], + param.parent().child('Lookback').value()) + self.autotuner.setReady() + param.setOpts(title="Stop") + self.kirdy_data_watcher.set_report_mode(True) + await self.kirdy.thermostat.set_constant_current_control_mode() + self.kirdy_data_watcher.report_update_sig.connect(self.autotune_tick) + self.loading_spinner.show() + self.loading_spinner.start() + self.background_task_lbl.setText("Autotuning") + case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN: + self.autotuner.setOff() + param.setOpts(title="Run") + await self.kirdy.thermostat.set_tec_i_out(0.0) + self.kirdy_data_watcher.report_update_sig.disconnect(self.autotune_tick) + self.background_task_lbl.setText("Ready.") + self.loading_spinner.stop() + self.loading_spinner.hide() + self.params[3].child('PID Config', 'PID Auto Tune', 'Run').sigActivated.connect(autotune) + + @asyncSlot(dict) + async def autotune_tick(self, report): + match self.autotuner.state(): + case PIDAutotuneState.STATE_READY | PIDAutotuneState.STATE_RELAY_STEP_UP | PIDAutotuneState.STATE_RELAY_STEP_DOWN: + self.autotuner.run(report['thermostat']['temperature'], report['ts']/1000) + await self.kirdy.thermostat.set_tec_i_out(self.autotuner.output()) + case PIDAutotuneState.STATE_SUCCEEDED: + kp, ki, kd = self.autotuner.get_tec_pid() + self.autotuner.setOff() + self.params[3].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run") + await self.kirdy.thermostat.set_pid_kp(kp) + await self.kirdy.thermostat.set_pid_ki(ki) + await self.kirdy.thermostat.set_pid_kd(kd) + await self.kirdy.thermostat.set_pid_control_mode() + await self.kirdy.thermostat.set_temperature_setpoint(self.params[3].child('PID Config', 'PID Auto Tune', 'Target Temperature').value()) + self.kirdy_data_watcher.report_update_sig.disconnect(self.autotune_tick) + self.background_task_lbl.setText("Ready.") + self.loading_spinner.stop() + self.loading_spinner.hide() + self.info_box.setWindowTitle("PID AutoTune Success") + self.info_box.setText("PID Config has been loaded to Thermostat.\nRegulating temperature.") + self.info_box.show() + + case PIDAutotuneState.STATE_FAILED: + self.autotuner.setOff() + self.params[3].child('PID Config', 'PID Auto Tune', 'Run').setOpts(title="Run") + await self.kirdy.thermostat.set_tec_i_out(0.0) + self.kirdy_data_watcher.report_update_sig.disconnect(self.autotune_tick) + self.background_task_lbl.setText("Ready.") + self.loading_spinner.stop() + self.loading_spinner.hide() + self.info_box.setWindowTitle("PID Autotune Failed") + self.info_box.setText("PID Autotune is failed.") + self.info_box.show() + async def _on_connection_changed(self, result, hard_reset=False): def ctrl_panel_setEnable(result): self.ld_status.setEnabled(result) diff --git a/pykirdy/pid_autotune.py b/pykirdy/pid_autotune.py index b02af54..7ec3005 100644 --- a/pykirdy/pid_autotune.py +++ b/pykirdy/pid_autotune.py @@ -21,6 +21,7 @@ class PIDAutotuneState(Enum): STATE_RELAY_STEP_DOWN = 'relay step down' STATE_SUCCEEDED = 'succeeded' STATE_FAILED = 'failed' + STATE_READY = 'ready' class PIDAutotune: @@ -60,6 +61,21 @@ class PIDAutotune: self._Ku = 0 self._Pu = 0 + def setParam(self, target, step, noiseband, sampletime, lookback): + self._setpoint = target + self._outputstep = step + self._out_max = step + self._out_min = -step + self._noiseband = noiseband + self._inputs = deque(maxlen=round(lookback / sampletime)) + + def setReady(self): + self._state = PIDAutotuneState.STATE_READY + self._peak_count = 0 + + def setOff(self): + self._state = PIDAutotuneState.STATE_OFF + def state(self): """Get the current state.""" return self._state @@ -106,7 +122,8 @@ class PIDAutotune: if (self._state == PIDAutotuneState.STATE_OFF or self._state == PIDAutotuneState.STATE_SUCCEEDED - or self._state == PIDAutotuneState.STATE_FAILED): + or self._state == PIDAutotuneState.STATE_FAILED + or self._state == PIDAutotuneState.STATE_READY): self._state = PIDAutotuneState.STATE_RELAY_STEP_UP self._last_run_timestamp = now