# 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

```
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 temperature rests ("Rasten")
                             with target temperature, heat rate, 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`,
  and `pyserial`/`spidev` if using real hardware backends.
- Client (`client/requirements.txt`): `PyQt5`.

Install with:

```bash
pip install -r brewpi/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 brewpi
   ./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
   ```

`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, stirrer speed/duty, and a list of temperature rests
("Rasten"), each with a target `temp`, `heatRate`, hold `time` (minutes), and
whether to `waitForUser` before continuing (e.g. to add malt or check
gravity).

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 rest and
`tasks/sud.py`'s `SudTask` drives the temperature controller's
`theta_soll`/`heatrate_soll` from it, advancing once `theta_ist` settles
close to the target and the rest's hold time has elapsed. It also switches
the stirrer between the schedule's `stirrSpeedHeat` (continuous, while
ramping) and `stirrSpeedRast`/`stirrDutyRast`/`stirrCycleTime` (intermittent,
while holding), stopping it entirely during a `waitForUser` pause. It
exposes progress (current rest, remaining hold time, state) on the `"Sud"`
WebSocket channel and accepts `{"Sud": {"Start": true}}` to begin the
schedule and `{"Sud": {"Confirm": true}}` to acknowledge a `waitForUser`
pause and move to the next rest. 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.
