Case study 04 · Embedded remote-control boat
KObait
A two-board embedded system for a GPS bait boat, with a handheld remote on a 3.5-inch TFT, a boat controller and a custom framed LoRa protocol, and safety gates in front of the motors.
01Overview
KObait is a remote-controlled bait boat for fishing. It carries bait out over the water, remembers Home, A and B waypoints and releases the bait through two hatches. It is also my hardware lab, the place where software habits such as single ownership, explicit contracts and tests meet wiring, radios and batteries.
Where it is now. The system was rebuilt from its first generation (an Arduino Nano remote with an nRF24L01 radio and an OLED) into two Mega 2560 boards with a LoRa link and a TFT remote. It is a bench prototype. The radio, displays, GPS and control logic work on the desk, and it has not yet been in the water.
Specification
- Boards
- 2 × Arduino Mega 2560: boat controller and handheld remote
- Radio
- UART LoRa modules with my own framing and CRC-16
- Protocol
- v3: 12-byte control, 34-byte telemetry; paired and sequenced
- Timing
- Control every 850 ms; failsafe stop after 3.4 s of silence
- Propulsion
- Two reverse-capable 60 A ESCs behind one propulsion module
- Navigation
- GPS and BNO055 heading; Home, A and B waypoints
- Remote UI
- 480×320 TFT, custom UI framework, 9 screens
- Firmware
- C++; about 4,300 lines on the boat, 8,600 on the remote
- Tests
- 196 host-side checks, 22 bench diagnostic sketches
- Only one module may write motor pulses, and only one may change the boat's mode. Tests enforce both.
- Every one-shot command is sequenced, retried until the boat echoes it, and executed exactly once.
- A lost radio link stops the boat. Autonomous return is deliberately not built until it is an explicit decision.
- The radio timing comes from a measured 808.75 ms round trip, not from a datasheet.
02What I built
Boat controller
- A six-mode state machine (boot-safe, waiting for neutral, manual, auto, failsafe stop, critical fault) with 13 stated stop reasons.
- A propulsion module working in a signed −255 to +255 domain, with per-ESC calibration checked at compile time, asymmetric ramps and a pause at neutral before reversing.
- A GPS and heading autopilot to saved Home, A and B waypoints, with calibration-health gating on the IMU.
- A latched leak alarm, a home water probe, battery thresholds for a 4S pack, a hardware watchdog and two bait-hatch servos.
Radio and protocol
- A framing layer for transparent UART LoRa radios (sync bytes, type, length, payload, CRC-16) parsed by a non-blocking state machine that resynchronises on noise.
- Protocol v3 with a pairing ID, wrap-safe sequence numbers, command IDs echoed back in telemetry, and deduplication.
- A half-duplex master and slave schedule. The remote transmits, and the boat answers in a jittered reply window or with a heartbeat every 5 s.
Handheld remote
- A 3.5-inch 480×320 TFT on the Mega, with every other peripheral moved to the analog header because the shield covers the digital pins.
- A custom UI framework with a priority screen router in which a water alarm outranks every screen except a diagnostic one opened on purpose, dirty-region repaint at 25 Hz and dark and light themes.
- Hall-effect joystick calibration stored in EEPROM with a version and CRC, an on-device calibration wizard and arcade mixing with reverse.
- A neutral-to-arm gate that holds output at zero until the stick has been healthy and centred for 300 ms.
Tooling
- 22 single-purpose bench sketches for ESCs, radio, GPS, IMU, displays, buttons and sensors.
- 196 host-side Python checks for architecture invariants, control logic and protocol behaviour.
03Architecture
Two boards and one radio link. The remote is the master and the boat only speaks when spoken to. On the boat, one state machine owns the mode and one module owns the motors.
Two boards, one link
Handheld remote
HardwareHall joystick
EEPROM calibration, 45° diagonal mapping
- connects to Remote firmware
HardwareButtons, D-pad
hold opens a hatch, tap closes it
- connects to Remote firmware
FirmwareRemote firmware
mixing, neutral-to-arm gate, sequenced commands
- connects to 3.5 in TFT · 25 Hz
- connects to LoRa radio
Client3.5 in TFT
custom UI framework, dirty-region repaint
HardwareLoRa radio
UART module, software serial
- connects to Remote firmware
- connects to Protocol v3
Radio link
StageProtocol v3
KB frame, CRC-16; paired, sequenced
- connects to LoRa radio
- connects to LoRa radio
Boat controller
HardwareLoRa radio
UART module, hardware serial
- connects to Protocol v3
- connects to BoatLink
FirmwareBoatLink
accept, dedup, build telemetry
- connects to LoRa radio
- connects to Boat state machine · accepted
- connects to Propulsion authority · manual
- connects to Bait hatches · hatch
FirmwareBoat state machine
sole mode owner: 6 modes, 13 stop reasons, failsafe stop after 3.4 s without control
- connects to Propulsion authority · gate
- connects to Autopilot · AUTO
FirmwarePropulsion authority
only PWM writer; ramp, dwell, per-ESC calib.
- connects to 2 × ESC + motors · PWM µs
Hardware2 × ESC + motors
reverse-capable, 60 A
FirmwareAutopilot
heading P-control to Home, A or B
HardwareGPS + BNO055
fix age, heading with calibration health
- connects to Autopilot
HardwareWater, leak, battery
latched leak alarm, 4S thresholds
- connects to Boat state machine · stop
HardwareBait hatches
two servos, closed before attach
04Hard problems
Radio collisions that looked like dead hardware
Problem
The first LoRa link looked like a dead uplink, even though both modules powered up and answered.
What I did
Separate beacon, ping and pong runs showed that every loss was a collision between two free-running transmitters on half-duplex radios. A master and slave schedule gave full delivery with zero CRC errors on the bench.
Timing from measured air time
Problem
A measured round trip of 808.75 ms made the original 100 ms control rate and 500 ms failsafe physically impossible.
What I did
I re-derived the schedule from the measurement. The remote sends every 850 ms, the boat stops after 3.4 s without control, and the remote shows signal lost after 4.25 s. The trade-off, an estimated 6 to 7 m of coasting at an assumed 2 m/s before the failsafe stop, is documented as an estimate, not a measurement.
A display shield that covers every digital pin
Problem
The TFT shield took the whole digital header, and its library would drive analog pins shared with the joystick and the radio.
What I did
All peripherals moved to A0 to A15, the software serial receive pin moved to a pin-change-interrupt pin, and build-time and runtime guards stop the display driver from touching anything else.
From forward-only to reverse-capable ESCs
Problem
A single legacy pulse value meant stop, neutral and minimum throttle at once. On bidirectional ESCs it could fall in the reverse band.
What I did
A signed control domain, per-ESC calibration with compile-time ordering checks, and a mandatory pause at neutral before any change of direction.
Stale commands after a reboot
Problem
A remote reboot restarted its sequence and command counters, so the boat rejected frames or echoed an old command back as a success.
What I did
The boat resets its sequence baseline once per outage and the remote ignores stale echoes. A session-aware v4 protocol is designed but not yet built.
05Decisions
- Sub-GHz LoRa instead of nRF24L01, with my own framing.
- Better range over water, and the display shield covers the SPI pins. A transparent UART radio has no packets, checksums or acknowledgements, so the firmware provides them.
- Stop, not return-to-home, when the link is lost.
- Autonomous movement after losing contact is a product decision that needs an explicit yes, not a default.
- A custom UI framework instead of LVGL.
- LVGL's recommended memory far exceeds the Mega's 8 KB of SRAM, and there is no framebuffer. The framework repaints only the regions that changed.
- The OLED and the IMU are non-critical.
- A display failure once left the boat in an infinite loop with the ESCs attached. Now the boat runs without a display, or without AUTO, instead of halting.
- The pairing ID is isolation, not authentication.
- It keeps two boats from answering each other's remote. It is not a security boundary, and the protocol documentation says so.
06Status
Works today
- Both boards run their firmware on the bench, and the LoRa link has been verified in both directions on a desk.
- GPS fix, displays, the remote UI, joystick calibration and sensor baselines are verified on the bench.
- AUTO is gated off in firmware because the IMU does not respond on the current bench wiring.
Not built yet
- ESC characterisation with measured endpoints, then staged tests with motors, props and restraint, and a tethered water test.
- Outdoor range testing and reading back the radio's configuration.
- Level shifting between the 5 V board and the radio's 3.3 V inputs.
- A session-aware protocol v4 and a decision on return-to-home.
No photos of the hardware are published yet. The plate at the top of this page is an illustration of the system, not a render of the boat.
07What I learned
- Measure before you tune. The radio's real round trip rewrote every timing constant.
- On a machine with propellers, safety is structure. One writer for the motors, one owner of the mode, and tests that keep it that way.
- Writing down what has not been tested yet (no water test, no range test) is part of the engineering, not an admission.
08Stack
- C++
- Arduino
- LoRa radio
- GPS navigation
- BNO055 IMU
- Python