Add Closed-loop/Open-loop heater control mode
Closed-loop (default, checkbox checked): TC has exclusive control over the heater; temp-soll and heatrate-soll sliders are active; direct heater power entry is disabled. TC now stays enabled at server startup and whenever closed-loop mode is active — SudTask no longer disables it on IDLE/DONE, so adjusting temp-soll manually with no sud running immediately drives the heater via the TC. Open-loop (checkbox unchecked, only selectable when paused/stopped): TC is disconnected from the heater; only the heater-power slider is active. Switching back to closed-loop resets power_soll to 0 and re-enables the TC. On play, the mode is automatically reset to closed-loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
This commit is contained in:
+18
-3
@@ -15,10 +15,15 @@ class HeaterTask(ATask):
|
||||
msg_handler.set_recv_handler(self.recv)
|
||||
self.power_soll = 0
|
||||
self.power_actor = 0
|
||||
self.closed_loop = True
|
||||
self._on_closed_loop_changed = None
|
||||
self.pulse_counter = 0
|
||||
device.set_on_changed('power_eff', ChangedInteger(self.on_changed_power).set)
|
||||
self.power_set_changed = ChangedInteger(self.on_changed_power_set).set
|
||||
|
||||
def set_on_closed_loop_changed(self, callback):
|
||||
self._on_closed_loop_changed = callback
|
||||
|
||||
def actor(self, y):
|
||||
self.power_actor = max(0, self.device.get_power_max() * y)
|
||||
|
||||
@@ -30,7 +35,14 @@ class HeaterTask(ATask):
|
||||
|
||||
async def recv(self, data):
|
||||
for pair in data.items():
|
||||
if 'Power' in pair[0]:
|
||||
if pair[0] == 'ClosedLoop':
|
||||
self.closed_loop = pair[1]
|
||||
if self.closed_loop:
|
||||
self.power_soll = 0
|
||||
if self._on_closed_loop_changed:
|
||||
self._on_closed_loop_changed(self.closed_loop)
|
||||
await self.send({'ClosedLoop': self.closed_loop})
|
||||
elif 'Power' in pair[0]:
|
||||
self.power_soll = pair[1]
|
||||
|
||||
async def send(self, data):
|
||||
@@ -52,14 +64,17 @@ class HeaterTask(ATask):
|
||||
|
||||
async def on_process(self):
|
||||
await self.send({'Capabilities': {'Power': {'Min': 0, 'Max': self.device.get_power_max()}}})
|
||||
await self.send({'ClosedLoop': self.closed_loop})
|
||||
if self._on_closed_loop_changed:
|
||||
self._on_closed_loop_changed(self.closed_loop)
|
||||
|
||||
pulse_period_s = 10
|
||||
pulse_period_count = pulse_period_s/self.interval
|
||||
|
||||
with self.device.open():
|
||||
while True:
|
||||
# Combine power set from different sources
|
||||
power_soll = max(self.power_soll, self.power_actor)
|
||||
# Closed-loop: TC has full control. Open-loop: direct manual power only.
|
||||
power_soll = self.power_actor if self.closed_loop else self.power_soll
|
||||
self.power_set_changed(power_soll)
|
||||
|
||||
# Calculate duty cycle
|
||||
|
||||
@@ -220,14 +220,6 @@ class SudTask(ATask):
|
||||
if value in (SudState.DONE, SudState.IDLE):
|
||||
self.stirrer.set_duty_cycle(1.0)
|
||||
self.stirrer.set_speed(0)
|
||||
# A finished/stopped run no longer owns the controller - hand
|
||||
# control back to manual mode (off by default there too).
|
||||
self.tc.set_enabled(False)
|
||||
else:
|
||||
# Any other state (RAMPING/HOLDING/WAIT_USER/PAUSED) means a
|
||||
# run is in progress and needs the controller actively driving
|
||||
# the heater.
|
||||
self.tc.set_enabled(True)
|
||||
|
||||
def on_user_message_changed(self, value):
|
||||
asyncio.create_task(self.send({'UserMessage': value}))
|
||||
|
||||
Reference in New Issue
Block a user