System Architecture
A predictive feedforward helm built as three cooperating layers — wireless sensing, a compute tier that may fail, and a steering tier that may not. Every layer below is simpler and more reliable than the one above it.
Design doctrines
- Local-first, cloud-optional — at every layer. Steering survives the Pi dying; the cockpit HUD survives Starlink dying; the local broker bridges to the cloud when a link exists and shrugs when it doesn't.
- Inject, never fork. The predictive engine writes gains and feedforward offsets into stock pypilot over IPC. Upstream stays upstream; failover is the absence of injection, enforced by a 200 ms watchdog.
- Relative time only. Wave math needs Δt between sensors, not wall-clock. One shared microsecond tick, latched in radio silicon — no GPS, no NTP, no drift, nothing to misconfigure.
- Simulation first. Recorded passages replay through the models ashore before any code commands a live rudder; feedforward authority starts at 0.0 and earns its way up.
- Render only what was computed. The HUD's sea surface is displaced by the estimated spectrum — never generic rolling water.
Reference kit & pricing
Reference kit: ≈ $650. Pricing includes assembled and potted pods, flashed and paired firmware, the calibrated Tier-1 hydrodynamic profiles, and support — not a bag of parts.
| Subsystem | Components | Role | Kit price |
|---|---|---|---|
| Sensor pod set ×3 (BB-POD-01) | XIAO nRF52840 · BNO086 9-DOF · 1200 mAh LiPo · IP67 enclosure · Qwiic I²C | assembled & potted pods: 2 Mbps ESB, on-chip fusion quaternions, multi-week battery, Dual-Lock stringer mount | $370 |
| Base station (BB-CORE-01) | Raspberry Pi 5 + cooler · nRF52840 receiver · 12 V buck | physics, logging, brokers, 10 Hz sync beacon — flashed and paired | $194 |
| Drive interface (BB-HELM-01) | pypilot high-current controller, configured | H-bridge, current shunt, rudder feedback, end-stops | $86 |
| Actuator package (optional) | SeaStar -3 reversible pump · balanced inboard ram · hose kit · reserve cylinder & fittings | the complete Tier-1 hydraulic stack, supplied and configured — or owner-supplied (often already fitted) | $2,995 |
| Reference kit total (cabling & mounting hardware included; actuator package optional) | ≈ $650 | ||
One connector, product-wide. Every board exposes I²C on a Qwiic (JST-SH 1.0 mm) socket — the same pinout as SparkFun Qwiic and Adafruit STEMMA QT — so servicing the kit needs one cable and one spare, not an adapter drawer. Those sockets live inside the sealed enclosures: pods talk to each other over the 2 Mbps radio, never over a cable run.
Sensing, Time & Data
One IMU at the center of motion is mathematically sufficient for attitude. The other two exist because lever-arm-amplified acceleration at the bow and stern is signal — it's how a hull becomes its own wave buoy.
The relative microsecond clock
- The base radio broadcasts its boot-microsecond counter every 100 ms. That counter is the network's time.
- Each pod wires the radio's address-match event through PPI to a 16 MHz hardware timer — the latch happens in silicon, with no interrupt and no software jitter: ~1–5 µs alignment.
- Every IMU sample is stamped t_rel = LocalTimer + Offset — a 4-byte integer in the packet. The beacon continuously re-locks crystal drift.
- Why it matters: bow and aft are ~30 ft apart; a 15 kt swell transits in ~1.2 s. 100 ms of clock error is a 10% phase error in the wave solution. 50 µs is zero.
- Ruled out on physics: NTP (5–50 ms jitter), PTP (wired + power), per-pod GPS (no sky in a bilge), mesh radio (100–1500 ms), LoRa (airtime can't do 50 Hz), ESP-NOW (TX current kills the battery pod).
Packet & link budget (per pod)
- int16 q[4] 8 B + int16 accel[3] 6 B + t_rel/seq/status ≈ 24 B → ~13 kbps at 50 Hz; ESB airtime ≈ 200 µs/packet at 2 Mbps.
- +8 dBm TX from firmware day one; pods mount high on stringers (bilge water and tanks eat 2.4 GHz; dry fiberglass doesn't); U.FL + adhesive dipole escape hatch for shielded lockers.
- The helm's receiver and the Pi's logger listen to the same broadcast — the logger is a passive sniffer that can crash without consequence.
MQTT data contracts
| Topic | Rate | Payload |
|---|---|---|
| boat/v1/telemetry/attitude | 20–50 Hz | ts, node, q[4], accel_linear[3], rate[3] |
| boat/v1/telemetry/environment | 5–10 Hz | sog, cog, awa, aws, current set/drift |
| boat/v1/telemetry/prediction | 10–20 Hz | wave_primary/secondary {height, period, dir}, target/actual rudder, horizon_sec |
| boat/v1/config/tune | on change | hot-reload deltas from the tuning sliders; persisted as named profiles |
| boat/v1/config/hull | on save | “My Boat” configurator output → physics priors (page 4) |
| boat/v1/config/update | on release | signed per-regime model artifacts — applied on confirmation, never mid-passage |
MQTT over raw WebSockets, deliberately: retained messages give a fresh visitor instant state, LWT flags the boat offline the moment the uplink drops, QoS survives Starlink beam handoffs, and topic filtering lets a phone skip the 50 Hz firehose.
Tiered ingestion & the bake period
- Live stream: 5–10 Hz, ~120 B/msg (~50 MB/day) — the HUD interpolates to 60 FPS client-side.
- Training data: raw 50 Hz logs stay aboard as Parquet/CBOR chunks; uploaded opportunistically at the dock (a 4-hour rough passage ≈ 30–45 MB).
- Bake: hull-archetype prior (0–2 h, conservative) → system identification (10–20 underway h incl. automated zig-zag sweeps → this vessel's RAOs and amps-per-degree) → baked digital twin at 30–50 h, ~95% converged. Re-bake only on big trim/tankage/sail changes.
The Control Stack
A supervisory classifier decides what kind of sea this is; a regime-specialized expert decides what the rudder should do about it; stock pypilot and a hydraulic lockvalve make sure a software mistake can't become a steering casualty.
Mixture of Experts — five regimes, five models
One monolithic network fails here: rudder-lift math that's true at 6 kt is fiction at 1.5 kt, and gradient descent averaging contradictory physics yields a controller that's mediocre everywhere. Instead the classifier (rolling 30–90 s window; wave-energy variance σ(a_z), spectral peak, AWA, SOG, motor load; 15–30 s hysteresis) routes control to one of five specialized experts — and cloud updates ship per-regime signed artifacts, so a swell-model update can never destabilize proven docking behavior.
| Regime / Expert | Trigger fingerprint | Objective | gain · pr · pd · pi | servo.speed · max A |
|---|---|---|---|---|
| 0 · Harbor / docking | SOG < 2 kt, engine on, no wave power | fight prop walk; instant bite | 1.4 · .85 · .20 · .05 | 1.0 · 18 A |
| 1 · Flat motoring | SOG > 4 kt, wave energy ≈ 0 | whisper-quiet, minimal wear | 0.6 · .35 · .15 · .02 | 0.35 · 8 A |
| 2 · Quartering swell | roll > 8°, period > 8 s, ΔIwave spikes | anti-broach: pre-empt stern lift | 1.6 · .40 · .90 · .00 | 0.95 · 20 A |
| 3 · Head seas / chop | a_z slamming, 2–4 s period | don't chase peaks — notch the encounter freq, allow ±3° natural yaw | 0.8 · .30 · .40 · .01 | 0.50 · 12 A |
| 4 · Heavy downwind | heel > 15° sustained, surging SOG, high Itrim | full authority; asymmetric windward gains | 1.8 · .60 · .95 · .00 | 1.0 · 22 A |
v1 preset matrix — the cold-start seed the bake refines per vessel. Note pi clamped to zero in any swell: integral windup from rolling waves is how autopilots broach politely.
How the engine touches the boat — pypilot injection
- Two feedforward paths: ap.pilot.heading_offset biases course into oncoming crests; servo.command drives the rudder profile directly. Chosen per maneuver class, tuned in replay.
- Regime dispatch: complete parameter blocks (ap.pilot.gain/pr/pd/pi, servo.speed, servo.max_current) over local socket IPC on every state change.
- Virtual IMU: ap.source points at the fused, time-aligned pod stream — pypilot consumes three radios as one clean compass.
- Auto-trim: a slow observer nudges rudder.offset until sustained weather helm reads zero.
- The rudder as a load cell: shunt current on the pump decodes into ΔIwave (impacts verified at the blade before the hull yaws — a leading input) and Itrim (weather-helm baseline). servo.max_current doubles as the jam/stall detector.
- Stall barrier: the MPC never commands rudder past the configured foil stall angle (~12–15° spade, ~18° skeg) — a stalled rudder is drag that accelerates the broach it meant to stop.
- Authority dial: feedforward_authority_ratio runs 0.0 → 1.0. Sea trials start fully reactive and earn prediction in increments.
Failure ladder
Experience: HUD, “My Boat” & Rollout
The Pi never renders a pixel. Three.js runs in whatever browser is looking — cockpit tablet on the boat's own Wi-Fi, or a visitor on botboat.ai — fed by the same MQTT topics through one relative URL.
The cockpit HUD (helm.local)
short decisive bursts — regime 2 behavior
+ 0.6 m chop @ 4.2 s from 290°
Example UI — the mode banner, regime-themed wave shader, Ghost Horizon, force-colored rudder arc, and the live/playback scrubber.
“My Boat” — the parametric hull configurator (boat.local/hull)
Blade area forward of the rudder stock.
- Physics: counterbalances water pressure against the turning blade.
- AI model: calibrates expected baseline hydraulic line pressure.
- Actuator: prevents false stall alarms on unbalanced blades; sets pump-draw expectations.
Example UI — sliders morph the mesh and recompute hydrostatics live; every control carries the three-point help flyout (Physics / AI model / Actuator). Saving publishes boat/v1/config/hull and seeds the day-one Kalman matrices — and the HUD twin becomes this hull, never a stock sailboat.
Onboard naming
helm.local the HUD · settings.local pypilot calibration · diag.local pipe rates, pod batteries, log downloads · boat.local → HUD. Resolved twice on purpose (Avahi aliases + dnsmasq hosts — some Android builds ignore mDNS); routed by a small onboard nginx so no URL ever carries a port number. The local AP and every one of these pages survive Starlink being switched off at anchor.
Build phases & open decisions
| # | Phase | Deliverable | Steering risk |
|---|---|---|---|
| 1 | Bench pods | ESB link, packet v1, sync latch, power budget | none |
| 2 | Core network | AP + uplink, brokers + bridge, names, diag portal | none |
| 3 | Hull logging | passages logged; wave estimator benchmarked vs NOAA buoy | none |
| 4 | Web HUD | digital twin + scrubber on botboat.ai and helm.local; “My Boat” configurator | none |
| 5 | Closed loop | pypilot injection; regimes; authority dial 0 → 1 across sea trials | gated: watchdog ×10, buoy benchmark, stall barrier, manual override proven |
Open decisions (tracked in the repo, none blocking phases 1–2): regime-state topic schema · regime-vs-slider profile layering · public topic ACLs (position privacy at anchor) · pod battery reporting mechanism · OTA artifact signing & keys · confirm-to-apply UX · pod store form (kit / assembled / published BOM) · slider-vs-baked-model precedence display.