From 71b1010b27c113320d5376b79548891c89f06b74 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Tue, 7 Jul 2026 19:46:22 +0200 Subject: [PATCH] heater/stirrer: zero the setpoint on every disconnect, not just explicit ones A comm-error/timeout-triggered disconnect left power_soll/power_actor (heater) or speed (stirrer) untouched, so a later reconnect - or the closed-loop TC, which keeps computing y regardless of connection state - could immediately resume driving the device at whatever it was doing before the outage. HeaterTask.disconnect()/StirrerTask.disconnect() now centralize disconnect + zero-setpoint and are used for the explicit Disconnect message, the per-tick comm-error path, and the outer unexpected-error recovery path alike. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EU3NTwLygh8jkrnqPoHNXF --- tasks/heater.py | 22 +++++++++++++++++----- tasks/stirrer.py | 18 ++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/tasks/heater.py b/tasks/heater.py index 0b69058..f186793 100755 --- a/tasks/heater.py +++ b/tasks/heater.py @@ -55,6 +55,20 @@ class HeaterTask(ATask): self._on_closed_loop_changed(True) asyncio.create_task(self.send({'ClosedLoop': True})) + def disconnect(self): + """Disconnects the device and zeroes its setpoint, so a later + reconnect (whether from an explicit Connect or an auto-reconnect) + never resumes at a stale power. Used for every disconnect - a + user-requested Disconnect, a comm-error timeout, and the + unexpected-exception recovery path alike - not just the explicit + one, since power_actor otherwise keeps whatever the TC last + computed (which can be stale/wound-up from before the outage) and + would be re-applied on the very next tick after reconnecting.""" + self.device.disconnect() + self.power_soll = 0 + self.power_actor = 0 + self.power_set_changed(0) + def actor(self, y): self.power_actor = max(0, self.device.get_power_max() * y) @@ -78,9 +92,7 @@ class HeaterTask(ATask): self.device.activate(True) elif pair[0] == 'Disconnect': self.device.activate(False) - self.device.disconnect() - self.power_soll = 0 - self.power_set_changed(0) + self.disconnect() elif 'Power' in pair[0]: self.power_soll = pair[1] @@ -167,12 +179,12 @@ class HeaterTask(ATask): self.device.process() except Exception as e: print(f"HeaterTask: comm error, marking disconnected: {e}") - self.device.disconnect() + self.disconnect() await asyncio.sleep(self.interval) except Exception as e: print(f"HeaterTask: unexpected error, recovering: {e}") try: - self.device.disconnect() + self.disconnect() except Exception: pass await asyncio.sleep(self.interval) diff --git a/tasks/stirrer.py b/tasks/stirrer.py index 3a3e61e..24bc8c0 100644 --- a/tasks/stirrer.py +++ b/tasks/stirrer.py @@ -37,6 +37,17 @@ class StirrerTask(ATask): if self._on_connected_changed: self._on_connected_changed(value) + def disconnect(self): + """Disconnects the device and zeroes its speed setpoint, so a later + reconnect never resumes spinning at whatever speed was last set. + Used for every disconnect - a user-requested Disconnect, a + comm-error timeout, and the unexpected-exception recovery path + alike - not just the explicit one, since self.device.speed + otherwise still holds the old value and the duty-cycle pulsing in + AStirrer.process() re-sends it on the next isOn transition.""" + self.device.disconnect() + self.device.set_speed(0.0) + async def recv(self, data): for pair in data.items(): if pair[0] == 'Connect': @@ -44,8 +55,7 @@ class StirrerTask(ATask): self.device.activate(True) elif pair[0] == 'Disconnect': self.device.activate(False) - self.device.disconnect() - self.device.set_speed(0.0) + self.disconnect() elif 'Speed' in pair[0]: self.device.set_speed(pair[1]) @@ -79,12 +89,12 @@ class StirrerTask(ATask): self.device.process() except Exception as e: print(f"StirrerTask: comm error, marking disconnected: {e}") - self.device.disconnect() + self.disconnect() await asyncio.sleep(self.interval) except Exception as e: print(f"StirrerTask: unexpected error, recovering: {e}") try: - self.device.disconnect() + self.disconnect() except Exception: pass await asyncio.sleep(self.interval)