Logs load/start/pause/continue/stop/entering-step/wait-user/finished
on Sud (components/sud.py) and connected/disconnected on
StirrerTask/HeaterTask, using the self.log added earlier.
SudForecastEstimator.estimate() (components/sud_forecast.py) drives its
own throwaway Sud through an entire schedule in a tight loop (no real
waiting) to predict its duration, and re-runs that on every real step
transition too - since it's still a Sud, it logged through the exact
same "Sud" logger as the real, server-driven one, making an instant
internal forecast run indistinguishable in the log from an actual
brew. Gave it its own "SudForecastEstimator" logger, silenced to
WARNING by default, so only the real Sud's transitions show up.
AttributeChange (the common base of every ATask and component ABC) now
sets self.log = logging.getLogger(type(self).__name__), so components
no longer need to hand-type their own name into each message. Wired
logging.basicConfig() in server/brewpi.py with a bare "%(name)s:
%(message)s" formatter - Tee (see prior commit) still supplies the
"<date>T<time>:" prefix, so lines read "<date>T<time>:<component>:
<message>" without double-stamping, and third-party loggers
(websockets, asyncio) now get the same formatting for free.
Converted the print() call sites that were standing in for this in
tasks/ and components/ (leaving __main__ demo blocks and explicit
debug-dump helpers alone).
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EU3NTwLygh8jkrnqPoHNXF
A manual Disconnect command left the last commanded power/speed in
place server-side, so a reconnect (or the UI's PowerSet/Speed echo)
could show a stale nonzero setpoint for a device that's no longer
attached.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N5WEDCc4v473DKqSaQhTQt
Root cause (from the brewpi Pi's log): HendiCtrl.disconnect() only
caught HendiException (a protocol-level NAK), but a genuine unplug
fails deep inside pyserial itself - flushInput()/readline() raise
termios.error/OSError on the now-dead fd. That escaped disconnect()
uncaught (skipping self.ser.close() too), then kept propagating up
through every caller that invoked disconnect() from its own except
handler (HeaterHendi.process()/activate(), AHeater.open()'s
context-manager exit), all the way out of HeaterTask.on_process()'s
coroutine entirely. Nothing restarts a dead ATask, so the heater's
process()/duty-cycle loop was gone for the rest of the process's
life - reconnecting afterward changed `connected` back to True but
nothing was left running to ever call process() again.
Fixes:
- HendiCtrl.disconnect() now catches broadly and always closes the
port, matching Pololu1376.disconnect()'s already-correct pattern.
- HeaterTask/StirrerTask.on_process() gain an outer retry loop as a
safety net: even an unanticipated exception now logs, marks
disconnected, and re-enters rather than permanently killing the
task.
Verified by reproducing the exact failure (comm error raising a raw
OSError, not HendiException) against a fake device - the task now
survives and resumes process()ing once reconnected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
Firmware version is already printed to the log at connect() time
(HendiCtrl/Pololu1376) - remove the live GUI/websocket firmware
reporting entirely (Connectable, HeaterHendi/StirrerPololu1376,
Heater/StirrerTask, web GUI) as redundant.
Also move the Heater panel's Closed-loop checkbox into the Controller
panel and rename it "Enable" - the Controller panel now shows a
live Enabled (green) / Disabled (gray) status badge next to its
heading, matching the Connected/Disconnected badges on the Heater/
Stirrer panels.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
The previous hotfix's disconnect()-on-comm-error path can now fire
during server/brewpi.py's own final best-effort hardware-release
cleanup (finally: block's heater.close()/stirrer.activate(False)),
which runs after the event loop has already stopped - previously
nothing about hardware teardown touched an AttributeChange-tracked
field, so this path was dormant. asyncio.create_task() there raises
RuntimeError: no running event loop, crashing the shutdown sequence.
Adds fire_and_forget() (tasks/task.py) - asyncio.create_task() that
silently drops the message instead of raising when there's no loop to
schedule on - and uses it for the Connected/FirmwareVersion broadcasts.
SudTask.check_connections() also now no-ops entirely without a running
loop, since self.sud.stop() cascades into further unguarded
create_task() calls (on_state_changed, heater_task.shutdown(), ...)
that a stopped loop can't help with anyway.
Reproduced and verified fixed directly against HeaterTask/HeaterHendi
with no event loop running (mirrors the exact server/brewpi.py
shutdown-time call path that crashed on the brewpi Pi).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
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
- Stirrer starts at speed 0 (was 50) so blades don't spin on connect
- Grain particles only levitate when stirrer is running
- Fire size driven by PowerSet (soll) instead of Power (ist)
- Split animation-play-state out of stirrer shorthand for reliable pausing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLR2133M2PyjiYq584i2G7
Callbacks were registered inside the async on_process() coroutine,
meaning they weren't active until the event loop started ticking.
Moving them to __init__ ensures they're wired up at construction time.
Also removes remaining debug prints from the affected task files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tqxrk8uj4M3w3d3eXm3xK8