Force the heater off and show a "Cool down" indicator during a passive cooldown
A ramp step whose target is below the current actual temperature can only be reached by ambient cooling - the heater can't actively cool. SudTask now detects this once per ramp phase and calls the controller's new set_cooling() override (forces y=0, bypassing the FSM) instead of waiting for its hysteresis-based IDLE transition. The GUI shows a blinking blue "Cool down" label in the status bar while this is active. Progression already only advances once theta_ist matches theta_soll regardless of direction, so no change was needed there.
This commit is contained in:
@@ -227,6 +227,18 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.status_label_env = QtWidgets.QLabel()
|
||||
self.statusBar().addPermanentWidget(self.status_label_env)
|
||||
self.update_status_env_label()
|
||||
|
||||
# Blinking indicator for a Sud ramp step that's cooling down
|
||||
# (target below the current temperature - the heater is forced off
|
||||
# server-side and we just wait for ambient loss to catch up).
|
||||
self.sud_cooling = False
|
||||
self.status_label_cooling = QtWidgets.QLabel("Cool down")
|
||||
self.status_label_cooling.setStyleSheet("color: blue; font-weight: bold;")
|
||||
self.status_label_cooling.setVisible(False)
|
||||
self.statusBar().addPermanentWidget(self.status_label_cooling)
|
||||
self.cooling_blink_timer = QtCore.QTimer(self)
|
||||
self.cooling_blink_timer.timeout.connect(self.on_cooling_blink)
|
||||
|
||||
self.update_sud_actions()
|
||||
self.btn_connect.clicked.connect(self.on_btn_connect_clicked)
|
||||
self.actionStart.triggered.connect(self.on_action_sud_start)
|
||||
@@ -336,6 +348,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.sud_start_time = None
|
||||
self.sud_paused_total = 0.0
|
||||
self.sud_pause_started_at = None
|
||||
self.set_sud_cooling(False)
|
||||
self.update_sud_actions()
|
||||
self.update_status_env_label()
|
||||
|
||||
@@ -361,6 +374,20 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
ambient = "{:.1f}°C".format(self.ambient_temp) if self.ambient_temp is not None else "-"
|
||||
self.status_label_env.setText("Ambient: {} Warp: {:.1f}x".format(ambient, self.warp_factor))
|
||||
|
||||
def set_sud_cooling(self, cooling):
|
||||
if cooling == self.sud_cooling:
|
||||
return
|
||||
self.sud_cooling = cooling
|
||||
if cooling:
|
||||
self.status_label_cooling.setVisible(True)
|
||||
self.cooling_blink_timer.start(500)
|
||||
else:
|
||||
self.cooling_blink_timer.stop()
|
||||
self.status_label_cooling.setVisible(False)
|
||||
|
||||
def on_cooling_blink(self):
|
||||
self.status_label_cooling.setVisible(not self.status_label_cooling.isVisible())
|
||||
|
||||
def on_slider_temp_soll_changed(self, value):
|
||||
print("on_slider_temp_soll_changed {}".format(value))
|
||||
self.msg_tempctrl.send({'Soll': {'Temp': value}})
|
||||
@@ -544,6 +571,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||
self.statusBar().showMessage("Step {}: {}".format(step.get('Index'), descr))
|
||||
else:
|
||||
self.statusBar().clearMessage()
|
||||
self.set_sud_cooling(step.get('Cooling', False))
|
||||
self.update_sud_actions()
|
||||
|
||||
def update_sud_forecast(self, doc):
|
||||
|
||||
@@ -37,6 +37,12 @@ class TempControllerBase(APid):
|
||||
self.pid_hold.set_params(params['Hold'])
|
||||
self.pid_rate.set_params(params['Heat'])
|
||||
self.is_startup = True
|
||||
# Explicit override for a ramp step whose target is below the
|
||||
# current temperature: the heater can't actively cool, so rather
|
||||
# than wait for the FSM's own (slower, hysteresis-based) IDLE
|
||||
# transition, force y to 0 as soon as the caller (SudTask) knows
|
||||
# the step needs to passively cool down.
|
||||
self.cooling = False
|
||||
|
||||
def on_state_entered(self, state):
|
||||
pass
|
||||
@@ -70,6 +76,9 @@ class TempControllerBase(APid):
|
||||
def set_heatrate_soll(self, value):
|
||||
self.heatrate_soll_set = value
|
||||
|
||||
def set_cooling(self, value):
|
||||
self.cooling = value
|
||||
|
||||
def get_heatrate_soll(self):
|
||||
return self.heatrate_soll
|
||||
|
||||
@@ -109,7 +118,7 @@ class TempControllerBase(APid):
|
||||
self.pid_hold.process(theta_err, -self.theta_ist, hold_scale)
|
||||
self.pid_rate.process(heatrate_err, -self.heatrate_ist)
|
||||
|
||||
if self.state == States.IDLE:
|
||||
if self.state == States.IDLE or self.cooling:
|
||||
self.y = 0
|
||||
else:
|
||||
self.y = self.pid_rate.get_y()
|
||||
|
||||
@@ -77,6 +77,15 @@ if __name__ == '__main__':
|
||||
if step is not None:
|
||||
ramping = sud.state == SudState.RAMPING
|
||||
phase = step['ramp'] if ramping and 'ramp' in step else step.get('hold', {})
|
||||
|
||||
# A ramp step whose target is below the current temperature can
|
||||
# only be reached by passive cooling - force the heater off,
|
||||
# mirroring tasks/sud.py's SudTask.
|
||||
cooling = ramping and 'ramp' in step and step['ramp']['temp'] < ctrl.get_theta_ist() - TEMP_REACHED_TOLERANCE
|
||||
ctrl.set_cooling(cooling)
|
||||
if cooling:
|
||||
print(" -> cooling down to {}".format(step['ramp']['temp']))
|
||||
|
||||
print(f"Step {step['number']}: {step['descr']}")
|
||||
apply_plant_params(step)
|
||||
if ramping and 'ramp' in step:
|
||||
|
||||
@@ -61,6 +61,14 @@ class SudTask(ATask):
|
||||
ramping = self.sud.state == SudState.RAMPING
|
||||
phase = ramp if ramping and ramp is not None else hold
|
||||
|
||||
# A ramp step whose target is below the current temperature can
|
||||
# only be reached by passive cooling - force the heater off for
|
||||
# its whole duration (decided once, here, rather than re-checked
|
||||
# every tick) instead of relying on the controller's own FSM to
|
||||
# notice and idle out.
|
||||
cooling = ramping and ramp is not None and ramp['temp'] < self.tc.get_theta_ist() - TEMP_REACHED_TOLERANCE
|
||||
self.tc.set_cooling(cooling)
|
||||
|
||||
if step is not None:
|
||||
self.apply_plant_params(step)
|
||||
if ramping and ramp is not None:
|
||||
@@ -76,6 +84,7 @@ class SudTask(ATask):
|
||||
'Rate': ramp.get('rate') if ramp else None,
|
||||
'Duration': hold.get('duration') if (hold is not None and not ramping) else None,
|
||||
'WaitForUser': step.get('user_wait_for_continue', False) if step else None,
|
||||
'Cooling': cooling,
|
||||
}}))
|
||||
|
||||
def on_state_changed(self, value):
|
||||
|
||||
Reference in New Issue
Block a user