gui: Add pid autotune to ctrl panel

This commit is contained in:
linuswck 2024-06-17 12:35:00 +08:00
parent bfd8c0e43a
commit dacf9f0fa5
2 changed files with 96 additions and 2 deletions

View File

@ -29,6 +29,7 @@ from ui.ui_kirdy_qt import Ui_MainWindow
from dateutil import tz from dateutil import tz
import math import math
import socket import socket
from pid_autotune import PIDAutotune, PIDAutotuneState
COMMON_ERROR_MSG = "Connection Timeout. Disconnecting." 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': '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'}, {'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): def __init__(self, args):
@ -404,6 +411,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.kirdy = Kirdy() self.kirdy = Kirdy()
self.setupUi(self) self.setupUi(self)
self.info_box = QtWidgets.QMessageBox()
self.info_box.setIcon(QtWidgets.QMessageBox.Icon.Information)
# Load Global QT Style Sheet Settings # Load Global QT Style Sheet Settings
qss=os.path.join(os.path.dirname(__file__), "ui/mainwindow.qss") qss=os.path.join(os.path.dirname(__file__), "ui/mainwindow.qss")
with open(qss,"r") as fh: with open(qss,"r") as fh:
@ -420,6 +430,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.max_samples = self.DEFAULT_MAX_SAMPLES self.max_samples = self.DEFAULT_MAX_SAMPLES
self.autotuner = PIDAutotune(25)
self.setup_menu_bar() self.setup_menu_bar()
self._set_up_ctrl_btns() self._set_up_ctrl_btns()
self._set_up_plot_menu() 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].sigTreeStateChanged.connect(self.send_command)
self.params[3].setValue = _setValue 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): async def _on_connection_changed(self, result, hard_reset=False):
def ctrl_panel_setEnable(result): def ctrl_panel_setEnable(result):
self.ld_status.setEnabled(result) self.ld_status.setEnabled(result)

View File

@ -21,6 +21,7 @@ class PIDAutotuneState(Enum):
STATE_RELAY_STEP_DOWN = 'relay step down' STATE_RELAY_STEP_DOWN = 'relay step down'
STATE_SUCCEEDED = 'succeeded' STATE_SUCCEEDED = 'succeeded'
STATE_FAILED = 'failed' STATE_FAILED = 'failed'
STATE_READY = 'ready'
class PIDAutotune: class PIDAutotune:
@ -60,6 +61,21 @@ class PIDAutotune:
self._Ku = 0 self._Ku = 0
self._Pu = 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): def state(self):
"""Get the current state.""" """Get the current state."""
return self._state return self._state
@ -106,7 +122,8 @@ class PIDAutotune:
if (self._state == PIDAutotuneState.STATE_OFF if (self._state == PIDAutotuneState.STATE_OFF
or self._state == PIDAutotuneState.STATE_SUCCEEDED 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._state = PIDAutotuneState.STATE_RELAY_STEP_UP
self._last_run_timestamp = now self._last_run_timestamp = now