Pi Zero Tub-Side Control Panel — Build-Out Plan
This document covers the second Raspberry Pi Zero W: a small physical console mounted at the tub. It displays live status and lets the user adjust target temperature and enable/disable heating without a phone.
See Hardware Setup and Overview for the rest of the system. This device is a client of the Pi 3 controller — it has no sensors and does not drive any pumps directly.
Role Recap
- Pi 3: source of truth, owns sensors/relays, runs the control daemon and web/API.
- Pi Zero #1: optional wireless temp node (if used) — not this document.
- Pi Zero #2 (this document): at-tub display + 3 buttons, talks to the Pi 3 over the local network (or a dedicated Pi-3 hotspot if Wi-Fi coverage at the tub is unreliable).
Interaction contract:
-
Power (short press): enable heating (
heating_enabled = true) on the Pi 3. -
Power (hold ~3s): send
heating_enabled = falseto the Pi 3 first, confirm/acknowledge, then shut the Zero down. - + / −: adjust target temperature on the Pi 3 by a fixed step (e.g. 1°F per press).
The console never writes to GPIO relays and never assumes offline = stop; only an explicit Power-hold sends the stop signal.
Recommended Hardware
Primary pick: Pimoroni Display HAT Mini
A single HAT gives you the screen and all the buttons you need with no extra wiring or soldering.
- 2.0" 320×240 IPS LCD (ST7789V2), SPI
- 4 tactile buttons (A/B/X/Y) — map 3 of them to Power / Up / Down, leave one spare (e.g. backlight toggle or a future "filter" shortcut)
- RGB status LED (handy for "heating" / "fault" color cues)
- Plugs directly onto the Zero's 40-pin header, no soldering if your Zero already has header pins
- Python library:
displayhatmini(PyPI), actively maintained
GPIO use (BCM numbering), for reference — all handled by the library, no manual wiring:
| Function | BCM pin |
|---|---|
| Button A | 5 |
| Button B | 6 |
| Button X | 16 |
| Button Y | 24 |
| RGB LED R/G/B | 17 / 27 / 22 |
| Backlight | 13 |
| SPI (display) | 7, 9, 10, 11 |
Buy:
- 1 Raspberry Pi Zero W (you already have two) with 40-pin header soldered, or a header strip + a few minutes of soldering if yours is headerless
- 1 Pimoroni Display HAT Mini
- 1 microSD card (8 GB+ is plenty)
- 1 small enclosure or 3D-printed stand-off case sized for Zero + HAT (Pimoroni ships 2 standoffs; add more for a tub-side mount)
- 1 USB power source rated for outdoor/damp-adjacent use (see enclosure note below)
Budget / fallback alternative: Adafruit Mini PiTFT 1.3"
If the Display HAT Mini isn't available:
- 240×240 IPS TFT, SPI, snaps onto the Zero
- Only 2 onboard buttons (GPIO23/24) — you'll need 1 extra momentary push-button wired to a free GPIO + GND for the third control
- Slightly smaller footprint than the Display HAT Mini
Extra parts if you go this route:
- 1 momentary push-button (panel-mount, waterproof-ish if console lives outdoors)
- Short jumper wires
Either way: enclosure note
This console sits "at the tub," which likely means outdoor/splash exposure. Follow the same philosophy as the main hardware doc:
- Small IP-rated box or 3D-printed splash shield
- Cable glands for the power cord
- Keep it out of direct spray range even if "splash-resistant"
Bill of Materials Summary
| Item | Qty | Notes |
|---|---|---|
| Raspberry Pi Zero W (have) | 1 | needs 40-pin header |
| Pimoroni Display HAT Mini | 1 | screen + 4 buttons + LED |
| microSD card | 1 | 8 GB+ |
| USB power supply / cable | 1 | match run length to tub location |
| Small splash-resistant enclosure | 1 | box or printed shell |
| Cable gland | 1 | for power cable entry |
Software Architecture
One small Python service on the console Zero:
┌───────────────────────────────┐
│ console.py (one process) │
│ │
│ poll loop ──► GET /status ───┼──► Pi 3 API
│ │ (every 1-2s) │
│ ▼ │
│ render(display, state) │
│ │
│ button callbacks: │
│ - Power short → POST enable │
│ - Power hold → POST disable │
│ → then shutdown
│ - Up/Down → POST target │
└───────────────────────────────┘
Pi 3 API this console needs (add to the control daemon)
Minimal surface:
-
GET /status→{ average_f, hot_input_f, target_f, heating_enabled, heat_pump_on, filter_pump_on, failed_sensors, ts } -
POST /heating→{ enabled: bool } -
POST /target→{ target_f: number }(or{ delta_f: number }if you prefer relative steps)
Keep these idempotent and fast; the console polls frequently and must stay responsive.
Console display layout (320×240, Display HAT Mini)
┌──────────────────────────────┐
│ Tub: 101.8°F → 104°F │ current vs target, large text
│ Hot-in: 118°F │
│ │
│ Heat: [ON] Filter: [OFF]│ status chips, color via RGB LED too
│ │
│ (stale data / fault banner) │ only shown if age_ms too high
└──────────────────────────────┘
A = Power X = − Y = + B = (spare)
Use the RGB LED for an at-a-glance cue (e.g. green = heating normally, amber = heating disabled, red = stale/fault data) so the user doesn't need to read text from across the yard.
Button behavior details
- Debounce every button in software (the library exposes press callbacks; ignore repeats under ~150 ms).
-
Power short press:
POST /heating {enabled: true}. -
Power hold (~3s):
POST /heating {enabled: false}- Wait for a 2xx response (short timeout, e.g. 2s; one retry)
- Show "Heating stopped" on screen briefly
- Run
sudo shutdown -h now - If the request fails after retry, show an error and do not shut down automatically — require a second deliberate hold to force power-off, so "stop heating" isn't silently skipped.
-
Up / Down:
POST /targetwith a fixed step (e.g. ±1°F), update the displayed target optimistically, then reconcile on the next/statuspoll.
Staleness handling
If /status hasn't succeeded in some threshold (e.g. 10–15s), show a "no connection to controller" banner instead of a stale number. Never show a confident-looking number that might be minutes old.
Power-off service shape
Don't use the kernel gpio-shutdown overlay for the Power button here — that overlay shuts the board down directly on a GPIO edge with no chance to call the Pi 3 first. Instead, handle the button entirely in console.py (via the HAT's button callbacks) so the stop-heating request always fires before shutdown -h now.
Build Phases
Phase 1: Bench bring-up (no tub involved)
- Flash Raspberry Pi OS Lite (headless) to the SD card; enable SSH + Wi-Fi via
raspi-configimaging tool. - Boot the Zero, confirm SSH access.
-
sudo raspi-config nonint do_spi 0(enable SPI), reboot. - Attach the Display HAT Mini,
pip3 install displayhatmini. - Run the library's example script, confirm screen + all 4 buttons register presses over SSH logs.
Phase 2: Talk to the Pi 3 (can mock the Pi 3 side first)
- Stand up the minimal
/status,/heating,/targetendpoints on the Pi 3 control daemon (or a temporary mock Flask app for early testing, same pattern astemp-sensor/app.py's mock mode). - Write
console.py: background poll thread → in-memorystate, render loop drawingstateto the HAT. - Confirm the console shows live numbers from the (mocked) Pi 3.
Phase 3: Wire up real button behavior
- Map Power/Up/Down to the three endpoints.
- Implement the hold-to-power-off sequence exactly as above (stop signal → confirm → shutdown).
- Add the staleness banner and RGB LED status cue.
Phase 4: Enclosure + field install
- Mount Zero + HAT in the splash-resistant box; route power through a cable gland.
- Run power to the tub location.
- Confirm Wi-Fi signal is solid at the install spot; if not, consider the Pi 3 running its own access point, or a Wi-Fi repeater near the tub.
- Set
console.pyto run as asystemdservice on boot (Restart=on-failure), same pattern you'll use for the Pi 3 daemons.
Testing Checklist
- Screen shows correct average, hot-input, target, and pump states.
- Up/Down change target on the Pi 3 and reflect back within one poll cycle.
- Power short-press enables heating; confirm on the Pi 3 / web UI.
- Power hold: watch the Pi 3 log the disable call, then watch the Zero actually power down.
- Kill Wi-Fi mid-session: console shows a stale/fault banner instead of frozen "good-looking" numbers.
- Force the Pi 3
/heatingendpoint to fail (e.g. stop the daemon) and confirm the hold sequence refuses to silently power off without feedback. - Re-power the Zero after a hold-off, confirm it can re-enable heating (or confirm via phone instead).
Open Decisions
- Exact target-temp step size (suggest 1°F per press, repeat-on-hold if you want faster adjustment later).
- Whether the spare 4th button (B, in the primary HAT pick) should do something now (e.g. force filter toggle) or stay reserved.
- Whether console offline for an extended period should trigger any Pi-3-side alert (separate from the explicit stop signal) — worth deciding once the console has run for a while in practice.