Heater and Stirrer can be real serial hardware (hendi, Pololu1376), but there was no connection concept at all - the constructors opened the port and crashed the whole server if the device was missing, with no way to see connection status or firmware version and no way to reconnect without a restart. Adds an observable Connectable mixin (components/connectable.py) shared by AHeater/AStirrer; real devices defer opening the serial port to an explicit connect(), auto-connect on server startup, and surface Connected/FirmwareVersion/Simulated plus manual Connect/Disconnect over the web GUI. Heating/stirring and Sud Start are all gated on connection state, and a disconnect mid-brew force-stops the run via the same path as a manual Stop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
import asyncio
|
|
from tasks import ATask
|
|
from ws.message import MsgIo
|
|
from components import AStirrer
|
|
from utils.value import ChangedInteger
|
|
|
|
|
|
class StirrerTask(ATask):
|
|
def __init__(self, stirrer_device: AStirrer, interval, msg_handler: MsgIo):
|
|
ATask.__init__(self, interval)
|
|
self.msg_handler = msg_handler
|
|
msg_handler.set_recv_handler(self.recv)
|
|
self.device = stirrer_device
|
|
self._on_connected_changed = None
|
|
stirrer_device.set_on_changed("speed", self.on_speed_changed)
|
|
stirrer_device.set_on_changed("dutyCycle", self.on_dutycycle_changed)
|
|
stirrer_device.set_on_changed("cycleTime", self.on_cycletime_changed)
|
|
stirrer_device.set_on_changed("connected", self.on_connected_changed)
|
|
stirrer_device.set_on_changed("firmware_version", self.on_firmware_version_changed)
|
|
|
|
def on_speed_changed(self, value):
|
|
asyncio.create_task(self.send({'Speed': value}))
|
|
|
|
def on_dutycycle_changed(self, value):
|
|
asyncio.create_task(self.send({'DutyCycle': value}))
|
|
|
|
def on_cycletime_changed(self, value):
|
|
asyncio.create_task(self.send({'CycleTime': value}))
|
|
|
|
def set_on_connected_changed(self, callback):
|
|
"""Registers a callback(connected: bool) invoked whenever the
|
|
device's connection state changes - lets server/brewpi.py wire this
|
|
into SudTask's start-gating/mid-brew auto-stop."""
|
|
self._on_connected_changed = callback
|
|
|
|
def on_connected_changed(self, value):
|
|
asyncio.create_task(self.send({'Connected': value}))
|
|
if self._on_connected_changed:
|
|
self._on_connected_changed(value)
|
|
|
|
def on_firmware_version_changed(self, value):
|
|
asyncio.create_task(self.send({'FirmwareVersion': value}))
|
|
|
|
async def recv(self, data):
|
|
for pair in data.items():
|
|
if pair[0] == 'Connect':
|
|
if self.device.connect():
|
|
self.device.activate(True)
|
|
elif pair[0] == 'Disconnect':
|
|
self.device.activate(False)
|
|
self.device.disconnect()
|
|
elif 'Speed' in pair[0]:
|
|
self.device.set_speed(pair[1])
|
|
|
|
async def send(self, data):
|
|
await self.msg_handler.send(data)
|
|
|
|
async def on_process(self):
|
|
await self.send({'Simulated': self.device.simulated})
|
|
|
|
self.device.set_speed(0.0)
|
|
self.device.set_cycle_time(10.0)
|
|
self.device.set_duty_cycle(1.0)
|
|
|
|
# Auto-connect on startup - see HeaterTask.on_process()'s own
|
|
# comment for why this replaces the old constructor-side connect.
|
|
self.device.connect()
|
|
# Explicit initial push - see HeaterTask.on_process()'s own comment
|
|
# for why a simulated device's connect() alone doesn't fire the
|
|
# observer.
|
|
await self.send({'Connected': self.device.connected})
|
|
await self.send({'FirmwareVersion': self.device.firmware_version})
|
|
|
|
with self.device.open():
|
|
while True:
|
|
try:
|
|
self.device.process()
|
|
except Exception as e:
|
|
print(f"StirrerTask: comm error, marking disconnected: {e}")
|
|
self.device.disconnect()
|
|
await asyncio.sleep(self.interval)
|