sude/sud_0010.json moved from a list of combined ramp+hold+wait
"Rasten" with global stirrer constants to a flat "schedule" list of
explicit {"type": "heat"|"hold", ...} steps, each carrying its own
stirrer_speed/stirrer_time_on/stirrer_time_off and an optional
user_wait_for_continue (+ user_message).
- components/sud.py: Sud now tracks the current `step` instead of
`rast`; "heat" steps enter RAMPING (advanced externally via
temp_reached()), "hold" steps enter HOLDING and count down
`duration` minutes (0 if omitted). Either step type can pause in
WAIT_USER via user_wait_for_continue, surfaced through the new
user_message attribute.
- tasks/sud.py: SudTask only pushes theta_soll/heatrate_soll on
"heat" steps, converts each step's stirrer_time_on/off into a
duty_cycle/cycle_time on every step change (replacing the old
state-based RAMPING/HOLDING stirrer switching), and broadcasts
user_message changes. Stirring is now fully schedule-driven instead
of implicitly stopped on WAIT_USER/DONE (DONE still stops it, since
there's no step left to read settings from).
- scripts/demos/sud/demo_sud.py: mirrors the same step-driven wiring.
- README's Mash schedules section rewritten for the new schema.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.6 KiB
BrewPi
A Python-based controller for automating the mash/brewing process of beer: it holds a pot of liquid at target temperatures (or ramps it at a target heating rate) according to a configurable mash schedule, drives a heater and stirrer, and exposes live control/telemetry over a WebSocket so a desktop GUI (or any other client) can monitor and steer the brew.
The project began as a simulation/control-theory playground (Smith-predictor
temperature control, pot transport-delay model, see
docs/NonLinMPC.pdf) and has grown real-hardware
backends for an induction hob and an RTD temperature probe.
Architecture
brewpi/brewpi.py Server entry point: wires sensor, pot/plant,
heater, temperature controller and stirrer
together, runs them as asyncio tasks, and
serves state/commands over a WebSocket.
client/brewpi_gui.py PyQt5 desktop client (brewpi.ui) that connects
to the server's WebSocket, displays live
temperature/power/state, and lets the user set
target temperature, heat rate, stirrer speed,
and switch the heater/stirrer on or off.
components/ Pluggable building blocks behind factories:
pid/ temperature controllers: plain PID
("Normal") or PID + Smith predictor
("Smith", runs two internal pot models —
one with the plant's transport delay, one
without — to compensate for the dead time)
plant/ pot thermal model (transport-delay line)
used in simulation
sensor/ temperature sensors: simulated, or a real
MAX31865 RTD amplifier over SPI
actor/ heater and stirrer drivers: simulated, a
Hendi induction hob (serial protocol), or a
Pololu 1376 stirrer motor controller
sud.py optional mash-schedule sequencer that steps
through a sude/*.json schedule and drives
the temperature controller from it
tasks/ Async tasks (one per component) that poll
hardware/sim state at a fixed interval and
publish changes through the message dispatcher.
ws/ Minimal WebSocket pub/sub layer: server
(single- and multi-user), client, and a
keyed message dispatcher (e.g. "Sensor",
"Heater", "TempCtrl", "Stirrer", "Pot", "Sud").
tracer.py Logs traced variables to .mat files (for
offline analysis/tuning in MATLAB/Octave,
see results.m / results_tc.m).
scripts/demos/ Standalone, eyeballed-plot demos (matplotlib)
exercising components/* in isolation — pid/,
plant/, and a full mash-schedule run in sud/.
Not used at runtime; run directly, e.g.
`python -m scripts.demos.sud.demo_sud`.
sude/ Mash schedules ("Sud" = brew/wort), each a
JSON list of "heat"/"hold" steps with target
temperature/heat rate or hold duration,
per-step stirrer timing, and optional pause
for user confirmation.
config.json.templ Configuration template (real hardware).
config.json.sim Configuration template for simulation mode.
Data flow
The server (brewpi/brewpi.py) loads config.json, builds the configured
sensor/heater/stirrer/controller via factories (*Factory.create(name, ...))
based on the Controller section (sensor_name, heater_name,
stirrer_name, pid_type, or "sim" for any of them), and connects their
outputs to each other's inputs via set_on_changed callbacks, e.g.:
- sensor temperature → temperature controller's
theta_ist - temperature controller output
y→ heater power - heater effective power → pot model power (simulation only)
Each component runs inside its own ATask at a configurable interval
(Controller.dt, scaled by sim_warp_factor) and pushes state changes onto a
keyed WebSocket channel that any connected client can subscribe to and send
commands back on (e.g. {"TempCtrl": {"Soll": {"Temp": 65}}}).
Requirements
- Python 3.8+
- Server (
brewpi/requirements.txt):numpy,scipy,websockets,dpath, andpyserial/spidevif using real hardware backends. - Client (
client/requirements.txt):PyQt5.
Install with:
pip install -r brewpi/requirements.txt
pip install -r client/requirements.txt
Running
-
Copy a config template to
config.jsonnext tobrewpi.pyand adjust it. Useconfig.json.simto run entirely in simulation (no hardware needed), orconfig.json.templas a starting point for real hardware (set the heater/stirrer serial ports and sensor type). -
Start the server:
cd brewpi ./brewpi.pyThis serves the WebSocket on
ws://0.0.0.0:8765. -
Start the GUI client and connect to the server's URI:
cd client ./brewpi_gui.py
brewpi/brewpi.sh shows how this is wired up to run under a virtualenvwrapper
environment ($WORKON_HOME/$BREWPI_HOME) on a Raspberry Pi-style deployment.
Mash schedules
Files under sude/ describe a brew's mash schedule ("Sud"): pot weight,
malt/water weights, and a flat schedule list of steps, each either:
"type": "heat"— ramp totempatrate(°C/min), or"type": "hold"— hold the current target fordurationminutes (omit for an immediate, zero-duration step).
Every step also carries its own stirrer timing (stirrer_speed,
stirrer_time_on/stirrer_time_off in seconds — time_off: 0 means
continuous), and may set user_wait_for_continue: true (with an optional
user_message) to pause for confirmation once the step completes, e.g. to
add malt or check gravity, instead of advancing immediately.
Set the top-level sud config key to a path under sude/ (see
config.json.sim/config.json.templ) to have the server step through it
automatically: components/sud.py's Sud tracks the current step and
tasks/sud.py's SudTask drives the temperature controller's
theta_soll/heatrate_soll from "heat" steps (advancing once theta_ist
settles close to the target) and counts down "hold" steps' duration,
applying each step's stirrer timing as it goes (converting
stirrer_time_on/stirrer_time_off into a duty cycle/cycle time). It
exposes progress (current step, remaining hold time, state, any
user_message) on the "Sud" WebSocket channel and accepts
{"Sud": {"Start": true}} to begin the schedule and {"Sud": {"Confirm": true}} to acknowledge a pause and move to the next step. Omit sud from
the config to run without schedule automation (manual
theta_soll/heatrate_soll control via the GUI, as before).
Logging & analysis
tracer.py (and tasks/tracer.py) periodically dump traced signals
(temperatures, heat rates, heater power) to logs/*.mat files, which can be
loaded in MATLAB/Octave — see results.m and results_tc.m — to evaluate
and tune the controller.