Command Center

TL;DR

The spa's BWA Wi-Fi module periodically drops off the network for 30–60 seconds at a time, then rejoins. While it's connected the link is pristine, but during each dropout the Homebridge plugin had nothing to read, pushed null to HomeKit, and Apple Home flashed "No Response." Aria built a small TCP proxy that drops the edge-garbage and replays the last-good status through the blackouts, so HomeKit stays steady. Plugin-version-agnostic. ~200 LOC. Shipped as a Docker sidecar on the NAS.

Before / after (per-hour HomeBridge log)

Pre-cutover
171
bad-checksum / bad-termination / short-message frames per hour
Pre-cutover
62
null Current Temperature pushes to HomeKit per hour (= "No Response" flashes)
Pre-cutover
3
successful state updates per hour (vs ~3,600 expected at 1 Hz)
First minute post-cutover
168 / 0
good frames forwarded / corrupt frames dropped at the proxy
First 5 min post-cutover
0
null pushes (vs 62/hr before)
First 5 min post-cutover
0
bad-frame log lines (vs 171/hr before)

The first 5 min was a clean-radio window — the proxy's value really shows up the next time the link wobbles, when the replay layer keeps HomeKit steady where before it would have flashed null.

Architecture

The proxy is a thin layer in front of the Balboa BWA Wi-Fi module. The Homebridge balboa-spa plugin connects to the proxy on 127.0.0.1:4257; the proxy owns the single real connection upstream to 192.168.4.254:4257.

Apple HomeKit (steady readings) Homebridge balboa-spa plugin v2.2.6 spa-resilience-proxy 127.0.0.1:4257 (Docker sidecar) BWA Wi-Fi module 192.168.4.254:4257 (outdoor) Balboa BP100G2 validated commands commands raw protocol (corrupt frames + gaps) healthy / what HomeKit sees unreliable surface (Wi-Fi) proxy / endpoint Plugin sees a clean ~1 Hz status stream — proxy fills any silent gaps for up to 120s with the last-good frame.

What the proxy does

  1. Holds the single TCP slot upstream. BWA modules accept one connection at a time on port 4257.
  2. Parses Balboa frames [0x7E][len][type+data][cksum][0x7E] with CRC-8 SAE-J1850 validation (seed 0x02, xor-out 0x02 — matches the plugin's compute_checksum).
  3. Drops corrupt frames silently before the plugin's parser sees them. No more bad-checksum logs, no more null pushes.
  4. Replays the last good status frame at ~1 Hz during upstream gaps so HomeKit always has a current reading.
  5. Stops replaying after 120 s of upstream silence — HomeKit eventually shows truthful "No Response" instead of a permanent lie.
  6. Pass-through for downstream control writes (set temp, jets, lights) — plugin sends commands, proxy forwards unchanged.
  7. Exponential backoff reconnect upstream: 1s → 2s → 4s → 8s → 16s → 30s → 60s steady.
  8. Per-minute stats: good frames, corrupt drops, replays, reconnects, client count, last-status-age.

Module identity (now decoded)

Spa controller: Balboa BP100G2
Software ID: M100_230 V52.0
Wi-Fi module MAC: 00:15:27:56:07:df
LAN IP: 192.168.4.254 (DHCP-reserved in eero)
Protocol port: tcp/4257
TCP slots: 1 (BWA constraint)

Root cause (corrected after bake analysis)

First read was "2.4 GHz signal corruption." That was wrong. The module sits on the office eero pod (~50 ft, one clean wall) and when it's connected the link is pristine — 200 frames/min, sub-80 ms freshness, zero corruption for minutes at a stretch. Not a weak-signal situation.

The real failure is the module dropping off the network entirely for 30–60 s at a time (EHOSTUNREACH — it's unreachable at the link layer), then rejoining and being instantly perfect. Episodic, not continuous. The handful of garbled frames only appear at the edges of these dropouts. The spa's own filter pump is ruled out — connectivity went perfect exactly as the pump cycled on.

Two suspects remain, and neither is fixed by a closer pod: (A) the BWA 50350 module's own radio/firmware self-disconnecting (a documented quirk), or (B) eero's invisible band-steering kicking the 2.4-only client off the pod. A longer bake settles it — regular cadence points to eero, random bursts point to the module. Either way the proxy is the software resilience layer that neutralizes both: it drops the edge-garbage and replays the last-good reading through the blackouts, so HomeKit never sees the gap.

Status & next steps

Plan C — replace the BWA module with an ESP32 bridge

This is the durable fix. The root cause is the BWA Wi-Fi module's own radio (it self-disconnects) or eero steering that 2.4-only client off the pod — both live inside a Balboa radio you don't control. Plan C deletes that radio entirely: a tiny ESP32 wired straight to the spa's RS485 bus runs firmware that re-presents the exact same TCP server on port 4257. Our resilience proxy (or the Homebridge balboa-spa plugin) just points at the ESP32's IP instead of 192.168.4.254 — one IP change, nothing else. Leave the proxy in front for belt-and-suspenders.

The two Amazon parts

Likely-already-have extras: a few female-female Dupont jumper wires, and ideally a Balboa RS485 Y-splitter (spa-supply part) so you tap the bus without cutting the existing harness.

Cleaner two-piece alternative (solder-free): an M5Stack Atom Lite + Atomic RS485 Base. They stack together with zero wiring between them, run on USB-C, and the same firmware supports them. Only 3 wires to the spa. ~$25 total, the tidiest build. Bonus: a PoE base later puts the whole thing on Ethernet.

⚠️ The one thing that can fry a board — power

The spa's connector carries +12 to 15 VDC on pin 1. That will instantly destroy a 3.3V ESP32. Two safe ways to power it:

  1. Easiest — don't touch pin 1. Power the ESP32 from a USB phone charger / outlet near the spa. Only the two data wires + ground go to the spa bus.
  2. Fully internal — add a small LM2596 buck converter to step pin-1's 12-15V down to 5V/3.3V. This is the only reason you'd need a third part.

Wiring to the spa bus

The BWA module plugs into a 4-pin connector on the Balboa main board (Molex 43025-0400 / TE 794617-4). Tap it via an open receptacle on the board or a Y-splitter. Verified pinout (Balboa physical-layer spec, 115200 baud 8-N-1):

Spa pin Signal Connect to
1 +12-15 VDC supply Leave disconnected (USB power) — or buck-converter input
2 RS-485 B (Data+) RS485 module B / A+
3 RS-485 A (Data-) RS485 module A / B-
4 Ground / return RS485 module GND + ESP32 GND

A/B polarity is harmless to get wrong: if pins 2 & 3 are swapped, nothing is damaged — the topside panel just shows NO COMM. Swap the two data wires and you're done. The only way to cause damage is wiring the 12-15V pin into the logic board.

ESP32 ↔ RS485 module (skip this if using the M5Stack)

Firmware

  1. Install PlatformIO (VS Code extension or CLI).
  2. Clone NorthernMan54/esp32_balboa_spa ↗.
  3. Enable BRIDGE mode (the build flag that turns on the local TCP server) and set your Wi-Fi SSID/password — pin it to your solid 2.4 GHz SSID, which is also where you sidestep eero band-steering.
  4. Flash over USB. It boots, joins Wi-Fi, reads the RS485 bus, and serves the BWA protocol on tcp/4257.

Cutover (≈5 min, fully reversible)

  1. Reserve a DHCP IP for the ESP32 in eero.
  2. Point the resilience proxy's upstream (or the plugin's host) at <esp32-ip>:4257 instead of 192.168.4.254:4257.
  3. Unplug the old BWA Wi-Fi module — the flaky radio is now physically out of the system.
  4. Watch the proxy's per-minute stats: corrupt drops + reconnects should fall to ~0 and stay there.

Bottom line on effort: ~$15-25 in parts, one evening. No soldering if you use a Y-splitter pigtail + a screw-terminal RS485 module (or the M5Stack). The actual work: stack 2 boards (or 4-6 jumpers), 3 wires to the spa, flash from your laptop, change one IP. The only "don't screw this up" step is not wiring 12V to the ESP32. Plan A (IoT SSID) and Plan B (newer BWA module) both still lean on a Balboa radio you can't fully control — Plan C removes that variable for good. It's the "just works, stop tinkering" endgame.

Files & tracking