Skip to the content
4 layers194 × 40 mmRobobouoy V4.0, the printed circuit board in 3D

Robobouoy V4.0

pnn69/NicE-Buoy

This device sails autonome to a given location.

Size
194 × 40 mm
Copper layers
4
Thickness
1.6 mm
Parts
258
Nets
177
Pads
775
Tracks, vias
1,709, 403
Pours
5
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
1 May 2023
Updated
27 Sep 2026

Overview

Robobouoy V4.0 is an open-source printed circuit board by pnn69, from the pnn69/NicE-Buoy repository on GitHub. It is a 4-layer board of 194 × 40 mm with 258 parts on 177 nets, routed with 1,709 tracks and 403 vias.

It is built around the SN74LV1T34DBV, LMR16020PDDA and INA181A1IDBVR(XBLW), with the LM1117S-3.3, ICM-20948 and LP5907MFX-1.8 among its other parts. Its parts include 80 resistors, 67 capacitors, 23 diodes, 18 transistors and 15 connectors. It belongs with the radio and wireless boards and the ESP32 boards of this library.

Main components

12

README

README.md

NicE-Buoy Autonomous Marine System Firmware

Welcome to the master repository for the NicE-Buoy Autonomous Marine System. This codebase powers a highly reliable, dual-processor autonomous marine buoy designed for GPS lock-to-position, remote configuration, automated sailing, and live telemetry tracking over LoRa and WiFi (UDP).


📌 Project Overview

NicE-Buoy is an industrial-grade marine robotic system consisting of multiple hardware and software components. Each buoy uses a dual-ESP32 architecture split into a Top Unit (RoboTop) and a Sub Unit (RoboSub), connected by a single-wire half-duplex serial link. This physical decoupling isolates high-frequency navigation, radio communication, and web server tasks (Top) from raw thruster driving, compass polling, and the PID motor loops (Sub).

Alongside the buoys sit two shore-side controllers: RoboCYD, a touchscreen unit that doubles as the field WiFi access point, and RoboLora, a USB/WiFi LoRa gateway.


🏗️ System Architecture

text
        ┌───────────────────────────┐        ┌───────────────────────────────┐
        │  RoboPythonDisplay (PC)   │        │  RoboCYD (touchscreen)        │
        │  Tkinter dashboard        │        │  - IS the field AP Robo_WiFi  │
        └────────────┬──────────────┘        │  - LoRa RX task + web UI      │
                     │ UDP / LoRa            └───────────────┬───────────────┘
                     │                                       │
        ┌────────────▼──────────────┐                        │
        │  RoboLora (gateway)       │   UDP 1001 (protocol)  │
        │  USB + WiFi <-> LoRa      │   UDP 1002 (debug log) │
        └────────────┬──────────────┘                        │
                     └───────────────┬───────────────────────┘
                                     │  LoRa 868 MHz  +  WiFi/UDP
┌────────────────────────────────────▼────────────────────────────────────────┐
│                                  RoboTop                                    │
│  - GPS polling (TinyGPS++, 0,0 placeholder rejected)                        │
│  - LoraTask: RF telemetry, ACK retry table (10 slots)                       │
│  - WiFiTask: web dashboard, UDP broadcast, OTA, captive portal, mDNS        │
│  - SercomTask: bridge to the Sub          - Button, buzzer, RGB LEDs        │
└────────────────────────────────────┬────────────────────────────────────────┘
                                     │  single-wire half-duplex UART
                                     │  Top TX 22 / RX 21  <->  Sub RX 5 / TX 18
┌────────────────────────────────────▼────────────────────────────────────────┐
│                                  RoboSub                                    │
│  - Dual ESC thrusters (BB & SB)     - ICM-20948 9-DoF compass + fusion      │

The codebase is structured into the following decoupled sub-projects:

  1. RoboTop (Top Unit) — ESP32, FreeRTOS. Acquires GPS, hosts the HTML5 control dashboard, broadcasts telemetry over UDP and LoRa, and delegates motor execution to the Sub. Tasks: GpsTask, LoraTask, WiFiTask, SerialTask (SercomTask), buttonTask, LedTask, buzzTask.
  2. RoboSub (Sub Unit) — ESP32, FreeRTOS. Drives the thrusters, reads the compass, runs the PID loops, and is the authoritative owner of all tuning parameters (see below). Tasks: CompassTask, EscTask, SerialTask, WiFiTask, LedTask, buzzTask. The rudder and speed PIDs live in pidrudspeed.cpp.
  3. RoboCYD (Touchscreen Controller) — ESP32 + 320x240 TFT with resistive touch. Displays the fleet, sends commands, and raises the field access point Robo_WiFi when no home network is present. Has a dedicated LoRa receive task and serves both a dashboard and a standalone fullscreen map.
  4. RoboDependency (RoboCompute + RoboTone libraries) — shared code linked by Top and Sub. Navigation maths (RouteToPoint), the unified RoboStruct, formatFloat, and the telemetry encoder/decoder (rfCode / rfDeCode).
  5. RoboLora (LoRa Gateway) — USB-and-WiFi to LoRa gateway with its own web UI. Relays operator commands to the buoys; stamps its outgoing frames with sender ID 0x99.
  6. RoboPythonDisplay (Python GUI Monitor) — Tkinter desktop dashboard (RoboControl.py) for up to 3 buoys: windroses, thruster indicators, battery voltages, PID menus.
  7. RoboChargingStation — solar monitoring and charging dock firmware.

Who owns what

Worth stating plainly, because it drives a lot of the design:

  • The Sub owns the tuning. PID gains, compass offset, holdRad, thruster inversion and swap all live in the Sub's NVS. The Top holds a RAM copy only, fetched via SETUPDATA when the serial link comes up, and shown on the dashboards until the Sub reports again.
  • compassOffset, revBB, revSB and swap_BB_SB are hull-specific — they describe the physical boat, not the electronics. Move a Sub to another hull and these travel with it and will be wrong until re-entered.
  • The Sub has no LoRa. Its only path to the outside world is the serial line to its Top.

🌐 Network Layout

Every node follows the same priority list:

text
NicE_WiFi  ->  Robo_WiFi  ->  its own AP
  • At home everything joins NicE_WiFi.
  • In the field, when no NicE_WiFi is in reach, the CYD becomes Robo_WiFi (password geenanker, 192.168.1.1/24, 8 client slots) and the Tops and RoboLora join it.
  • A node that finds neither raises its own AP — TOP_<id>, SUB_<id>, LORA_<id>, password geenanker, on 192.168.4.1 — and keeps hunting underneath it. Joining that AP opens the dashboard automatically via a captive portal.
  • The Sub deliberately skips Robo_WiFi, to keep the CYD's client slots for the Tops and a phone. It costs nothing: the Sub never transmits on UDP anyway, and all of its telemetry reaches you folded into the Top's TOPDATA.

mDNS gives every node a stable name that works in both modes — top-<id>.local, sub-<id>.local, robocyd.local, lora-<id>.local. ArduinoOTA registers the responder; nothing else may call MDNS.begin(), as a second responder has crashed this hardware before.

UDP ports: 1001 carries the protocol; 1002 carries a plain-text debug log (see the RoboTop README).


📡 Communication Protocol & Frame Format

The system communicates using a lightweight, proprietary, ASCII-based comma-separated protocol designed to optimize RF bandwidth while remaining human-readable.

Frame Anatomy

text
$TargetID,SenderID,AckType,CommandType,Status,Param1,Param2,...,ParamN*CRC\n
  • $: Starting sentinel character.
  • TargetID: Address of the recipient. All IDs are hexadecimal. A buoy's ID is the low four bytes of its MAC (e.g. b7a5b578). 1 (BUOYIDALL) is the broadcast address; 0 is treated as a legacy broadcast.
  • SenderID: Address of the transmitting station. 99 (i.e. 0x99) is the web UI / RoboLora and 98 is the CYD display. These two are the only senders whose broadcasts a buoy will act on — a broadcast command from a peer buoy is ignored, which is what stops one buoy commanding another.
  • AckType: Packet type classification:
    • 1 (GET): Request parameter values.
    • 2 (SET): Modify parameter values.
    • 3 (GETACK): Request command execution with receipt verification.
    • 4 (ACK): Receipt confirmation.
    • 6 (INF): Telemetry status update, or an answer to a question.
  • CommandType: Integer mapping to system command (e.g., 51 for TOPDATA, 19 for BUOYPOS, 83 for SETUPDATA, 47 for DIRDIST, 21 for LOCKPOS, 23 for DOCKPOS).
  • Status: Current state machine status of the buoy (7 IDLE, 13 LOCKED, 16 DOCKED, …).
  • Params: Dynamic floating-point or integer parameters depending on the command.
  • *: End-of-payload delimiter.
  • CRC: 2-character hexadecimal CRC calculated by XOR'ing all characters between $ and *.

Broadcasts that expect an ACK are a trap. An ACK for IDr = BUOYIDALL can never match in removeAckMsg(), so a SET or GETACK broadcast sits in the LoRa retry table for all five retransmits and blocks a slot. Broadcast beacons must use INF.


🚀 Advanced Telemetry Optimizations

1. Bandwidth Compression

To maximize LoRa airtime efficiency and range, telemetry strings undergo two compression steps in RoboCompute.cpp:

  • Float Trailing Stripping: formatFloat() trims redundant trailing zeros and decimal points (e.g. 12.5000 becomes 12.5).
  • Zero Compression: RoboCode() compresses consecutive zero fields to empty strings (e.g. ,0,0,0,0, becomes ,,,,,).

2. The Top-to-Sub Link, and Why One-Way Failures Matter

The Top and Sub share one wire, half duplex, with hardware separating the two directions (opto-isolated; see Datasheets/). The firmware configures two GPIOs per board — Top TX 22 / RX 21, Sub RX 5 / TX 18 — but that is not two conductors.

  • The diagnostic consequence: if the connector or the wire fails, traffic stops in both directions. A failure in one direction only therefore cannot be the plug — it is in the direction-separation circuitry, which is per board. Swapping a Sub between hulls localises such a fault to a specific board.
  • SercomTask implements echo filtering, discarding frames whose sender ID is its own, because a transmitter hears itself on a shared wire.
  • Implicit ACKs: any valid INF response from the Sub signals that it processed the command, clearing the retry queue without extra traffic.
  • The Sub sleeps. After reconnection it sits in power-down and only wakes on an incoming command — send one, wait, then it communicates. Do not read a quiet Sub as a fault until it has been nudged.

3. Hardware Self-Healing Watchdogs

  • LoRa transceiver watchdog: if transmission fails for 500 ms the SPI/state machine is assumed locked and InitLora() forces a full re-init.
  • Compass watchdog: static I2C readings under thruster power trigger COMPASS STUCK - REINIT.
  • Serial watchdog: the Top clears isSerConnected after 2 s of silence and re-requests SETUPDATA when the Sub returns.
  • Stack headroom is observable. The Top's /data reports StkLoop, StkLora, StkWifi, StkSer and heap figures. A LoRa task running on 100 bytes of stack was the cause of a hard-to-find panic; check these after any change that adds work to a task.

🧭 In-Field Navigation Calibration State Machines

  1. In-Field Compass Spin Calibration — self-guided rotation while measuring magnetic min/max to derive hard-iron offsets.

  2. In-Field Offset Alignment — sails a straight leg on compass heading, compares the GPS track, and writes the difference to the stored compass offset.

  3. Guided 8-Point Compass Calibration — the hull is turned to the compass's own zero and then indexed round a mechanical fixture 45° at a time, one capture per mark. Produces the 8-point correction table, which is held on the Sub and reported through SETUPDATA. The table describes the hull's magnetic deviation only; which way the sensor is bolted in is a separate setting (Set as North), applied after the table.

    A GPS-based version of this used to exist, sailing eight 100 m legs and inferring the table from the track. It was removed when the mounting offset moved out of the table's input — its residual arithmetic assumed the old ordering.


🛠️ Developer Guide

Prerequisites

Compilation & Flash Commands

bash
pio run                      # build
pio run -t upload            # upload (OTA by default for Top and Sub)
pio device monitor           # serial monitor

Over-The-Air (OTA) Updates

RoboTop and RoboSub default to upload_protocol = espota, so a plain upload goes over the network:

bash
pio run -t upload --upload-port 192.168.1.71        # by address
pio run -t upload --upload-port top-b7a5099c.local  # or by mDNS name

RoboCYD and RoboLora have no espota configuration in their platformio.ini (their upload_port names a COM port that may not exist), so flash those directly:

bash
python ~/.platformio/packages/framework-arduinoespressif32/tools/espota.py \
       -i <ip> -p 3232 -f .pio/build/<env>/firmware.bin -r

A RoboTop change that touches data/ also needs pio run -t uploadfs — the web page lives in SPIFFS.

Renaming an SSID or changing the WiFi policy must be rolled out to the whole fleet at home on NicE_WiFi. A device left on the old scheme cannot be reached in the field.

Running the Python GUI Dashboard

bash
pip install pyserial
python RoboPythonDisplay/RoboControl.py

📜 License & Contributions

This project is proprietary and confidential. For contributions, pull requests, or feature requests, contact Peter de Nijs (pnn69pnn@gmail.com).

Parts

258
QtyValueFootprintReferences
32100nA_Device:C_0603C201, C202, C204, C207, C208, C210, C211, C212, C304, C305, C307, C309, C310, C313, C315, C316 +16
1610KA_Device:R_0603R201, R208, R209, R307, R321, R322, R335, R344, R345, R346, R501, R502, R503, R601, R610, R620
141KA_Device:R_0603R308, R310, R313, R314, R327, R340, R342, R602, R605, R609, R613, R617, R619, R621
11100A_Device:R_0603R203, R207, R301, R302, R331, R504, R505, R506, R507, R508, R612
11100KA_Device:R_0603R311, R312, R315, R316, R317, R318, R336, R603, R606, R607, R614
10100u/6.3VCapacitor_SMD:C_1206_3216MetricC203, C209, C312, C502, C503, C511, C512, C607, C612, C615
101nA_Device:C_0603C206, C303, C311, C319, C510, C514, C515, C516, C602, C614
10HoleA_Mechanical:Hole_3.1mmH101, H102, H103, H104, H105, H106, H107, H108, H109, H110
9AO3400APackage_TO_SOT_SMD:SOT-23Q201, Q304, Q305, Q310, Q401, Q402, Q501, Q502, Q603
81N4148WSA_Device:D_SOD-323_1.8x1.4x1.15D205, D306, D307, D311, D501, D601, D603, D606
610kA_Device:R_0402R401, R402, R403, R404, R405, R406
62.2KA_Device:R_0603R202, R205, R206, R306, R309, R604
647KA_Device:R_0603R320, R326, R328, R608, R611, R615
6WS2812BLED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mmD202, D203, D204, D502, D503, D504
510u/25VA_Device:C_0805C405, C406, C603, C610, C611
5GREENLED_SMD:LED_0603_1608MetricD201, D304, D305, D309, D605
410nA_Device:C_0603C205, C213, C308, C606
4Mousebite_2_Sides_BridgedA_Misc:MousebitConnectorDoubleSidedJP102, JP104, JP201, JP205
3AO3401APackage_TO_SOT_SMD:SOT-23Q302, Q311, Q604
20.75VA_Pads_Pins:Testpad_1TP306, TP604
2100nA_Device:C_0402C403, C404
210uHA_Device:L_6.6x7.0mmL301, L601
218kA_Device:R_0603R323, R324
222uF/35VCapacitor_SMD:C_1206_3216MetricC301, C601
24.7kA_Device:R_0603R304, R305
25mOhmA_Device:R_1206R319, R325
26.2kA_Device:R_0603R341, R618
28.2KA_Device:R_0603R329, R616
2AON6407-MSPackage_DFN_QFN:AO_DFN-8-1EP_5.55x5.2mm_P1.27mm_EP4.12x4.6mmQ307, Q308
2COMA_Pads_Pins:Testpad_1TP311, TP601
2COMPASSConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ202, J401
2CY-12085G-3016-P4EasyEDA:BUZ-TH_BD12.0-P6.50-D0.6-FDBUZZER201, BUZZER501
2ESC_PWMA_Pads_Pins:Testpad_1TP301, TP302
2ESP32-WROOM-32E_(visible_peripherals)A_Module:ESP32-WROOM-32U202, U501
2GNDA_Pads_Pins:Testpoint_0.9x1.5TP313, TP606
2LM1117S-3.3A_IC_SOT:SOT223U201, U601
2LMR16020PDDAPackage_SO:SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.81mm_ThermalViasU303, U602
2MMBT3904PCM_JLCPCB:Q_SOT-23Q301, Q605
2MMBT5401PCM_JLCPCB:Q_SOT-23Q303, Q602
2On/OffA_Connector:Header_1x03_P2.54_MaleSW301, SW601
2PWR_ENA_Pads_Pins:Testpad_1TP305, TP602
2ProgA_Connector:IDC_2x3_1.27mmJ201, J501
2SN74LV1T34DBVPackage_TO_SOT_SMD:SOT-23-5U301, U302
2SS54Diode_SMD:D_SMAD308, D604
2VbattA_Pads_Pins:Testpad_1TP307, TP603
110nA_Device:C_0402C409
11nA_Device:C_0402C407
15VA_Pads_Pins:Testpad_1TP312
15V_ESC_BBA_Pads_Pins:Testpad_1TP303
15V_ESC_SBA_Pads_Pins:Testpad_1TP304
1Ai-Thinker-Ra-02A_Module:Ai-Thinker-Ra-02-LoRaU502
1CHRG/DATAConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ601
1CHRG/DATAA_Connector:MR60PB-MJ301
1COM_TOPA_Device:LED_0603D602
1Conn_01x08_PinConnector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical_SMD_Pin1LeftJ506
1ESC_BBConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ302
1ESC_PWR_BBA_Connector:AMASS_XT60PB-F_1x02_P7.20mm_VerticalJ304
1ESC_PWR_SBA_Connector:AMASS_XT60PB-F_1x02_P7.20mm_VerticalJ305
1ESC_SBConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ303
1EXT LEDA_Pads_Pins:Testpoint_0.9x1.5TP505
1GPSConnector_JST:JST_XH_B4B-XH-A_1x04_P2.50mm_VerticalJ503
1ICM-20948EasyEDA:QFN-24_L3.0-W3.0-P0.40-BL-EPU402
1INA181A1IDBVR(XBLW)Package_TO_SOT_SMD:SOT-23-6U305
1LEDSTRIPConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ504
1LIGHT/BUTTONConnector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_VerticalJ502
1LP5907MFX-1.8Package_TO_SOT_SMD:SOT-23-5U401
1LiPo S6A_Connector:AMASS_XT60PB-M_1x02_P7.20mm_VerticalJ306
1MODEA_Connector:Header_1x02_P2.54_JumperSW201
1REDLED_SMD:LED_0603_1608MetricD302
1RX_SUBA_Pads_Pins:Testpad_1TP309
1TX_SUBA_Pads_Pins:Testpad_1TP310
1VSAMPA_Pads_Pins:Testpad_1TP308
1VSampA_Pads_Pins:Testpad_1TP605
1~PCM_JLCPCB:Part_Num_JLCPCBPN101
1~{BOOT}A_Pads_Pins:Testpad_1TP201

Similar boards

  • 4 layers103 × 36 mmRobobuoy Sub v3 0, the printed circuit board in 3D

    Robobuoy Sub v3 0

    pnn69/Kicad_PCB

    Schematics and PCB's designed with Kicad

    160 parts
  • 4 layers155 × 103 mmMEMLNaut 0 3, the printed circuit board in 3D

    MEMLNaut 0 3

    MusicallyEmbodiedML/MEMLNaut

    138 parts6
  • 4 layers65 × 56 mmPI DMX hat, the printed circuit board in 3D

    PI DMX hat

    Skrooter/PI-DMX-hat

    A DMX hat for Raspberry pi which can also be used to power a Pi from 12V

    53 parts
  • 4 layers140 × 100 mmDc usv, the printed circuit board in 3D

    Dc usv

    TobleMiner/DC-UPS

    A small DC UPS, designed to keep small appliances like consumer network equipment running during power outages

    351 parts223CERN-OHL-P-2.0

Questions

How big is the Robobouoy V4.0 PCB?

It measures 194 × 40 mm and has 4 copper layers, 1.6 mm thick.

What parts does the Robobouoy V4.0 use?

258 parts, built around the SN74LV1T34DBV, LMR16020PDDA, INA181A1IDBVR(XBLW) and LM1117S-3.3. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Robobouoy V4.0 KiCad files?

From the pnn69/NicE-Buoy repository on GitHub, at the commit this page was made from: the board is PCBA_Robobouoy-V4.0/Robobouoy-V4.0.kicad_pcb and its schematic PCBA_Robobouoy-V4.0/Robobouoy-V4.0.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Robobouoy V4.0 design in my own project?

Its repository states no licence, so its author keeps all rights: ask pnn69 before reusing the design.