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 = false to 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):
    1. POST /heating {enabled: false}
    2. Wait for a 2xx response (short timeout, e.g. 2s; one retry)
    3. Show "Heating stopped" on screen briefly
    4. Run sudo shutdown -h now
    5. 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 /target with a fixed step (e.g. ±1°F), update the displayed target optimistically, then reconcile on the next /status poll.

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)

  1. Flash Raspberry Pi OS Lite (headless) to the SD card; enable SSH + Wi-Fi via raspi-config imaging tool.
  2. Boot the Zero, confirm SSH access.
  3. sudo raspi-config nonint do_spi 0 (enable SPI), reboot.
  4. Attach the Display HAT Mini, pip3 install displayhatmini.
  5. 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)

  1. Stand up the minimal /status, /heating, /target endpoints on the Pi 3 control daemon (or a temporary mock Flask app for early testing, same pattern as temp-sensor/app.py's mock mode).
  2. Write console.py: background poll thread → in-memory state, render loop drawing state to the HAT.
  3. Confirm the console shows live numbers from the (mocked) Pi 3.

Phase 3: Wire up real button behavior

  1. Map Power/Up/Down to the three endpoints.
  2. Implement the hold-to-power-off sequence exactly as above (stop signal → confirm → shutdown).
  3. Add the staleness banner and RGB LED status cue.

Phase 4: Enclosure + field install

  1. Mount Zero + HAT in the splash-resistant box; route power through a cable gland.
  2. Run power to the tub location.
  3. 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.
  4. Set console.py to run as a systemd service 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 /heating endpoint 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.