botboat.ai
Design Document · Page 1 of 4 · 2026-09-01

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.

Bow pod nRF52840 + BNO085 Midships pod nRF52840 + BNO085 Aft pod nRF52840 + BNO085 2.4 GHz ESB · 50 Hz · <1 ms · relative-µs stamped Dual-listener radio base (nRF52) · 10 Hz sync beacon LEVEL 2 · PREDICTIVE ENGINE — Raspberry Pi 5 time aligner · wave FFT/ARMA · hull model (RAO) · load observer regime classifier → Mixture-of-Experts (5) · MPC 10–15 s local Mosquitto broker · flight logger (Parquet/CBOR) may crash — the boat does not care LEVEL 1 · FAILOVER — stock pypilot core native reactive heading/wind PID · own compass fallback direct radio feed retained · calibration web UI 200 ms heartbeat loss → instant, bumpless PID takeover gain + feedforward injection (IPC) pypilot smart motor controller optocoupled H-bridge · current shunt (load cell) · rudder pot · end-stops SeaStar -3 reversible pump + balanced hydraulic ram (Tier 1) lockvalves hold the rudder de-energized · zero mechanical backlash Cloud broker + trainer (own server) 8883 TLS boat-ingest · 8083 WSS browsers botboat.ai · Three.js digital twin and the cockpit tablet on the boat's own AP
Full topology. The wireless pods feed both tiers independently; the logger and cloud sit entirely outside the steering path.

Design doctrines

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.

SubsystemComponentsRoleKit price
Sensor pod set ×3 (BB-POD-01)XIAO nRF52840 · BNO086 9-DOF · 1200 mAh LiPo · IP67 enclosure · Qwiic I²Cassembled & 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 buckphysics, logging, brokers, 10 Hz sync beacon — flashed and paired$194
Drive interface (BB-HELM-01)pypilot high-current controller, configuredH-bridge, current shunt, rudder feedback, end-stops$86
Actuator package (optional)SeaStar -3 reversible pump · balanced inboard ram · hose kit · reserve cylinder & fittingsthe 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.

Design Document · Page 2 of 4

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).
base · MasterTick µs beacon @10 Hz bow midships aft hardware latch at radio address-match (PPI → TIMER0) Offset = MasterTick − LatchedLocal · every sample: t_rel = Local + Offset no GPS · no NTP · no RTC · zero cumulative skew

Packet & link budget (per pod)

MQTT data contracts

TopicRatePayload
boat/v1/telemetry/attitude20–50 Hzts, node, q[4], accel_linear[3], rate[3]
boat/v1/telemetry/environment5–10 Hzsog, cog, awa, aws, current set/drift
boat/v1/telemetry/prediction10–20 Hzwave_primary/secondary {height, period, dir}, target/actual rudder, horizon_sec
boat/v1/config/tuneon changehot-reload deltas from the tuning sliders; persisted as named profiles
boat/v1/config/hullon save“My Boat” configurator output → physics priors (page 4)
boat/v1/config/updateon releasesigned 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

Design Document · Page 3 of 4

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 / ExpertTrigger fingerprintObjectivegain · pr · pd · piservo.speed · max A
0 · Harbor / dockingSOG < 2 kt, engine on, no wave powerfight prop walk; instant bite1.4 · .85 · .20 · .051.0 · 18 A
1 · Flat motoringSOG > 4 kt, wave energy ≈ 0whisper-quiet, minimal wear0.6 · .35 · .15 · .020.35 · 8 A
2 · Quartering swellroll > 8°, period > 8 s, ΔIwave spikesanti-broach: pre-empt stern lift1.6 · .40 · .90 · .000.95 · 20 A
3 · Head seas / chopa_z slamming, 2–4 s perioddon't chase peaks — notch the encounter freq, allow ±3° natural yaw0.8 · .30 · .40 · .010.50 · 12 A
4 · Heavy downwindheel > 15° sustained, surging SOG, high Itrimfull authority; asymmetric windward gains1.8 · .60 · .95 · .001.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

predictive MPCPi · experts · feedforward pypilot reactive PID200 ms watchdog takeover pypilot internal compassif the pod radio itself dies hydraulic lockvalverudder holds, hands take over each rung is dumber, older, and more reliable than the one before it
Design Document · Page 4 of 4

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)

REGIME: QUARTERING SEAS  |  PROFILE: AGGRESSIVE FEEDFORWARD  |  SLEW: 10°/s
ghost +10 s AWA ribbon amber crests = incoming energy vector (regime-themed shader)
◀◀▶ LIVE▶▶ 4×
14:22:07
RUDDER · target −3.8° / actual −3.5°
hydrodynamic load (motor current) — cyan → amber → red
DRIVE WORKLOAD
7.2 A peak · 41°/min rudder travel
short decisive bursts — regime 2 behavior
Waves: 1.8 m @ 8.5 s from 240°
+ 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)

DWL VCG CLR GM_T = 0.98 m ✓ stable hull turns warning-red if sliders reach GM ≤ 0
0%25%

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

#PhaseDeliverableSteering risk
1Bench podsESB link, packet v1, sync latch, power budgetnone
2Core networkAP + uplink, brokers + bridge, names, diag portalnone
3Hull loggingpassages logged; wave estimator benchmarked vs NOAA buoynone
4Web HUDdigital twin + scrubber on botboat.ai and helm.local; “My Boat” configuratornone
5Closed looppypilot injection; regimes; authority dial 0 → 1 across sea trialsgated: 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.