# 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`](docs/NonLinMPC.pdf)) and has grown real-hardware backends for an induction hob and an RTD temperature probe. ## Architecture ``` server/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 mash-schedule sequencer; starts out empty, a client loads one of several sude/*.json schedules onto it and it 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 (`server/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 (`server/requirements.txt`): `numpy`, `scipy`, `websockets`, `dpath`, and `pyserial`/`spidev` if using real hardware backends. - Client (`client/requirements.txt`): `PyQt5`. Install with: ```bash pip install -r server/requirements.txt pip install -r client/requirements.txt ``` ## Running 1. Copy a config template to `config.json` next to `brewpi.py` and adjust it. Use `config.json.sim` to run entirely in simulation (no hardware needed), or `config.json.templ` as a starting point for real hardware (set the heater/stirrer serial ports and sensor type). 2. Start the server: ```bash cd server ./brewpi.py ``` This serves the WebSocket on `ws://0.0.0.0:8765`. 3. Start the GUI client and connect to the server's URI: ```bash cd client ./brewpi_gui.py ``` `server/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_mass`, `pot_material` (fixed for the whole brew), and a `steps` list, each a ramp, a hold, or both, plus a step-level `temperature` (the target a ramp step ramps to, and what a hold step holds at): - a ramp — `"ramp": {"rate": ...}` — ramp to `temperature` at `rate` (°C/min), and/or - a hold — `"hold": {"duration": ...}` — hold the current target for `duration` minutes (omit/`0` for an immediate step). A step with both ramps to `temperature` and then holds there for `duration` - useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing two separate schedule entries. `user_wait_for_continue`/`user_message` (below) apply once the whole step is done, i.e. after the hold phase if there is one. Both `ramp` and `hold` carry a `stirrer` block (`speed`, `interval_time`, `on_ratio`): `interval_time: 0` runs the stirrer continuously at `speed`; `interval_time > 0` pulses it on a period of `interval_time` seconds, on for `on_ratio * interval_time` of it. A step may also set `user_wait_for_continue: true` (with an optional `user_message` to prompt the user with) to pause for confirmation once the step completes, e.g. to add malt or check gravity, instead of advancing immediately. Each step also has its own `grain_mass`/`water_mass` (defaulted like everything else from `default.step`), since both change over the course of a brew — e.g. malt going in partway through, or water boiling off. Pass a step's `grain_mass`/`water_mass` to `Sud.derive_plant_params()` (together with the brew-wide `pot_mass`/`pot_material`) to get that step's lumped `Pot` `M`/`C`; `scripts/demos/sud/demo_sud.py` recomputes and re-applies these (via `Pot.set_thermal_params()`/`TempController.set_model_params()`, on both the real plant and the controller's Smith-predictor model) every time the current step changes, instead of deriving them once at startup. The top-level `default.step` object gives every field above (including the nested `ramp`/`hold`/`stirrer` blocks) a default value; a step in `steps` only needs to specify the fields it overrides — anything it omits is filled in from `default.step`, recursively. A step is a ramp or a hold depending on which of those two keys it specifies; the other is not defaulted in. The server always runs a `Sud` (`components/sud.py`), starting out empty (no schedule) - `sude/` can hold several schedule files, and a client loads one of them onto the running `Sud` (`{"Sud": {"Load": }}`), kept purely in memory until replaced by another Load or the server restarts; nothing is ever read from or written to `sude/*.json` by the server itself, that's on the client (e.g. the GUI's File menu). `Sud` resolves each raw step against `default.step` and tracks the current (resolved) step, while `tasks/sud.py`'s `SudTask` drives the temperature controller's `theta_soll`/`heatrate_soll` from ramp steps (advancing once `theta_ist` settles close to the target), counts down hold steps' `duration`, and applies each step's `stirrer` block (`interval_time`/ `on_ratio` map directly onto the stirrer's cycle time/duty cycle). 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. With no schedule loaded (or all its steps run), it just sits idle - manual `theta_soll`/`heatrate_soll` control via the GUI works the same as ever. ## 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.