Step 6: Software
Goal: the Pico 2 runs the Solo Starter control firmware, the ESP32-S3 runs the hub, and Home Assistant shows the hub’s entities.
Time: 1–2 h.
⚠️ The firmware has not run on real hardware yet (
firmware/README.md). Treat the first boot as a test, with the 24 V brick unplugged.
6.1 Control firmware (Pico 2)
The firmware is built per kit. The kit sets the TEC current limit: Solo Starter = 3 TECs × 5.0 A = 15 A zone
current, and zone 1 inert. Flash only the solo-starter image; another kit’s image can exceed your parts’ rating.
Option A: download the UF2 (no toolchain)
- Open the latest release at https://github.com/moinsen-dev/open-bed-climate/releases and download
opod-pico2-solo-starter.uf2andSHA256SUMS. - Check the file:
sha256sum -c SHA256SUMS --ignore-missing(Linux) orshasum -a 256 opod-pico2-solo-starter.uf2(macOS) and compare with the line inSHA256SUMS. - Unplug the 24 V brick. Hold the Pico’s BOOTSEL button, plug in USB, release. A USB drive appears (named
RP2350on a Pico 2). - Drag
opod-pico2-solo-starter.uf2onto the drive. The Pico reboots into the firmware and the drive disappears.
Prebuilt images use the generic NTC values (10 kΩ, β 3435) and a flow factor of 98 Hz per L/min, which fits YF-S401-class turbine sensors. Use option B if your NTC calibration in step 2 was more than 1 °C off the generic model, or if your flow sensor needs a different factor. You check the factor in step 7, section 2.2 before any TEC gets power. The shopping list’s YF-S401 matches the prebuilt factor; other flow sensors almost certainly need option B.
Option B: build from source
Needed for your own NTC calibration, your flow factor, or a temporary low current limit for bench tests.
rustup target add thumbv8m.main-none-eabihf
cd firmware
cargo test # core logic vs. golden vectors, no hardware needed
cd opod-pico2
# edit src/board.rs: NTC_CAL (your r25/beta), FLOW_HZ_PER_LPM (your sensor)
cargo build --release --no-default-features --features kit-solo-starter
Flash it:
- With a debug probe (a second Pico running
debugprobe, wired to the SWD pins):cargo run --release --no-default-features --features kit-solo-starter. This also streams thedefmtlog. - Without a probe: hold BOOTSEL, plug in, then
picotool load -u -v -x -t elf target/thumbv8m.main-none-eabihf/release/opod-pico2.
What you may change in board.rs: calibration (NTC_CAL, FLOW_HZ_PER_LPM), the register map after verifying it
(DPS_MAP), and, for a bench test only, MAX_TEC_CURRENT_A in the kit-solo-starter block (down only; the core
clamps it to 6 A). Revert that one afterwards and never commit it: it must match hardware/kits/solo-starter.toml, and
the kit tests check that.
What you must not change: the safety limits in opod-core::control (43 °C, 45 °C, 75 °C, flow interlock, heartbeat,
dead time). See SAFETY.md.
✅ Checkpoint: first boot (24 V brick unplugged)
- With a probe: the log shows a start line with
solo-starter, 1 active zone, 5 A × 3 TECs. - Without a probe: there is no visible sign yet (no status LED). Check that every output gate (GP10–GP13) still reads < 0.3 V and nothing clicks. The hub link in 6.3 is your real checkpoint.
6.2 Hub (ESP32-S3)
Option A: browser installer
- Open https://openbedclimate.moinsen.dev/install/ in Chrome or Edge (Web Serial is needed).
- Connect the ESP32-S3 by USB (disconnect its 5 V pin from the control board’s rail while USB is plugged in, unless your board protects against back-feed). Click install, choose the port, wait.
- Set Wi-Fi right after flashing in the browser (Improv over USB). Alternatives: Improv over Bluetooth, or the
open-bed-climate-xxxxxxaccess point and its captive portal.
The installer flashes the same image as opod-hub-factory.bin in the release, so you can also flash it with
esptool.py write_flash 0x0 opod-hub-factory.bin.
Option B: ESPHome CLI
cd hub/esphome
cp secrets.example.yaml secrets.yaml # Wi-Fi, api_key (openssl rand -base64 32), ota_password
esphome run opod-hub.yaml # or: uvx --from esphome esphome run opod-hub.yaml
The device (pins, entities, night programs) is defined in opod-hub-common.yaml. For a Solo build you can delete the
right / zone 1 entries there; with the Solo firmware they are harmless either way.
Room sensor (strongly recommended before stage B)
Add an SHT45 (role temp_sensors) on the hub’s I2C bus and set room_temperature / room_humidity in the opod:
block. That enables the dew-point guard (cooling setpoints below 16 °C are raised to at least dew point + 1 K) and
gives you the room temperature for the measurements in step 7. See hub/esphome/README.md.
6.3 Adopt in Home Assistant
- Home Assistant → Settings → Devices & services. The hub appears as a discovered ESPHome device. Click
Configure. (Factory image: Home Assistant sets the API encryption key itself when it adopts the device. CLI
build: enter the
api_keyfrom yoursecrets.yaml.) - Open the device page. You get, per side: a climate entity (off / heat_cool, 13–43 °C), water and heatsink temperature, TEC current, flow and fault code sensors, a problem binary sensor and a clear faults button; plus Control board link and Prime water loop.
- Hide or disable the Right entities: on a Solo build they always show no temperature and reject any target.
Sensor notes:
- TEC current is per TEC. Home Assistant shows 5.0 A when the DPS shows 15 A (3 × 5 A).
- The example config averages the diagnostic sensors over 30 s (
throttle_average). That is fine for the measurements in step 7; shorten it inopod-hub-common.yamlif you need faster curves.
Fault code
The fault code sensor is a bit field (04-firmware-protocol.md). Add up the values:
| Value | Fault | Latching (needs clear faults) |
|---|---|---|
| 1 | water over-temperature (> 45 °C) | yes |
| 2 | water under-temperature (< 8 °C) | no |
| 4 | hot side over-temperature (> 75 °C) | yes |
| 8 | no flow (< 0.15 L/min for 20 s while enabled) | no |
| 16 | leak | yes |
| 32 | sensor invalid (NTC open/short) | yes |
| 64 | host timeout (no heartbeat for 10 s while enabled) | no |
| 128 | low water | no |
| 256 | TEC driver communication (3 failed Modbus transactions) | firmware-only |
Example: 160 = 128 + 32 = low water and an invalid sensor.
✅ Checkpoint (hub and control board connected, 24 V brick still unplugged, loop filled)
- Control board link: on within a few seconds of power-up.
- Left water temperature ≈ your bath reference thermometer (± 1 °C, more if uncalibrated). Left heatsink temperature ≈ room temperature.
- Left flow: 0 (pump off, climate off).
- Left fault code: 256 (TEC driver communication). That is correct here: the DPS gets its power from the unplugged 24 V brick, so the firmware’s Modbus writes fail. Any other bit set means a sensor, leak or level problem to fix first. In step 7, with the DPS powered and talking, it drops to 0.
- Unplug the hub’s UART wires: within 5 s “Control board link” goes off and “Left problem” goes on. Plug back in: link on again.
- Press Prime water loop: the pump runs for 60 s, flow shows ≥ 0.15 L/min, then the pump stops.
If it fails
- No USB drive in BOOTSEL mode: charge-only USB cable, or BOOTSEL released too early.
- Link stays off: TX/RX not crossed (ESP32 GPIO17 → Pico GP1, GPIO18 ← GP0), missing GND between the boards, or the Pico not running (re-flash).
- Water temperature unavailable / fault 32: NTC divider or ADS1115 wiring (check the node voltage, I2C address 0x48, SDA/SCL swapped).
- Fault 16 right away: leak input HIGH: sensor module unpowered, wrong contact, or a real leak.
- Fault 128 right away: level input HIGH: reservoir below the sensor, sensor polarity, or a broken wire.
- Prime does nothing: prime only runs while there is no leak and the level is OK; check the fault code.
Rendered from docs/build/06-software.md. View or edit the source. Nobody has built this yet: if something is unclear or wrong, that is exactly what we need to know.