Commit Graph
22 Commits
Author SHA1 Message Date
jensandClaude Sonnet 5 671e0c2474 fix: heater task permanently died after a mid-brew unplug, never recovered
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
2026-07-03 21:21:44 +02:00
jensandClaude Sonnet 5 7b162cf1f5 refactor: drop firmware-version display, move Enable toggle to Controller
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
2026-07-03 20:23:55 +02:00
jensandClaude Sonnet 5 069fc352bc fix: guard connection-state callbacks against no running event loop
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
2026-07-03 19:47:17 +02:00
jensandClaude Sonnet 5 f20e617d81 feat: add connect/disconnect status for real heater/stirrer hardware
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
2026-07-03 19:18:59 +02:00
jensandClaude Sonnet 4.6 9f221c5d19 fix: re-enable closed-loop on Sud start regardless of client
HeaterTask.shutdown() switched the heater to open-loop when a brew
ended, but nothing switched it back when a new Sud was started.  The
browser happened to send {ClosedLoop: true} as part of its UI setup,
so its connect-and-start path worked; PyQt (and any other client that
doesn't send that message) left the heater in open-loop and the TC
never drove it.

Add SudTask._on_start / set_on_start() hook, HeaterTask.start_closed_loop()
(mirror of shutdown()), and wire them in brewpi.py so the server
switches to closed-loop and broadcasts the new state to all clients the
moment any client starts a brew.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 00:43:55 +02:00
jensandClaude Sonnet 4.6 6d481d7764 Shut heater off on brew end (DONE) and stop (IDLE)
Adds HeaterTask.shutdown() which switches to open-loop at power 0,
disables TC, and broadcasts ClosedLoop=false to clients so the browser
checkbox unchecks immediately. SudTask fires it via a new set_on_end()
callback whenever state reaches DONE or IDLE, giving both a completed
schedule and a manual Stop the same safe behaviour: stirrer and heater
both off, no heater left running unattended.

Re-enabling after shutdown: check the closed-loop checkbox (sends
ClosedLoop=true → TC re-enabled) or adjust the power slider directly;
the next brew's Play press already sends ClosedLoop=true automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
2026-06-28 12:07:21 +02:00
jensandClaude Sonnet 4.6 f55d085ae4 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
2026-06-28 10:29:30 +02:00
jensandClaude Sonnet 4.6 0316cfd33b Remove server-side Activate/Enable residues from heater and tempctrl tasks
HeaterTask.on_changed_active() was never registered as a callback so
never fired; TcTask.recv() no longer needs to handle {Enable: ...}
since no client sends it after the override checkbox removal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DF16nV2ispNw1C9SbvqbRQ
2026-06-27 21:13:49 +02:00
jensandClaude Sonnet 4.6 47fb09aa06 Move set_on_changed registrations from on_process() to __init__()
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
2026-06-25 20:39:06 +02:00
jens 93b5667a64 Add a real-time Plot tab to brewpi_gui, mirroring demo_sud figure 1
Embeds a dark-themed matplotlib canvas (3 stacked strip charts: theta
ist/soll, heatrate ist/soll, heater power_set/power_eff) in a new Plot
tab, sampled once per second from the latest websocket-received values.

HeaterTask now also emits a PowerSet message (the pre-discretization
target power) alongside the existing Power (effective) message, since
the GUI had no way to see the set-point side of figure 1's bottom plot
otherwise.
2026-06-20 22:31:07 +02:00
jens 9b997b8348 - generalized heaters 2021-10-18 14:32:56 +02:00
jens 6dc5e90956 - permit heater to override power 2021-10-12 11:37:24 +01:00
jens 33fd6fa5ab - fixed heater 2021-10-09 12:45:01 +02:00
jens ac6139cae7 - fixed heater 2021-10-09 11:40:29 +01:00
jens fb71f9554c - fixed heater 2021-10-09 12:35:39 +02:00
jens c50f02510c - fixed heater 2021-10-09 12:33:21 +02:00
jens 1e80b02216 - redefine pulse period 2020-12-18 17:33:26 +01:00
jens c8660ca60e - fixed hendi power 2020-12-18 17:03:17 +01:00
jens 4e508c86c5 - redefine activate method
- for task introduced open() conext method
2020-12-18 15:58:02 +01:00
jens adf664ca08 - create bool expression 2020-12-17 18:25:34 +01:00
jens 87cb265faf - reduce debug log
- wired stirrer to emit messages
2020-12-17 16:58:03 +01:00
jens fb37750ec7 - refactored tasks 2020-12-17 14:45:43 +01:00