Onboard companion computer for Project Caribou. Runs on a Raspberry Pi CM5 in the airframe, ingests telemetry from the Pixhawk 6X flight controller (MAVLink/UDP), per-arm ESCs (DroneCAN on can0) and per-arm BMSes (DroneCAN on can1), and serves it to Caribou Hub operators via an inbound WebSocket server (HubLink) over Tailscale on 4G.
Status: V1 deployed and HIL-validated on the bench drone. Telemetry plane is live end-to-end; per-arm CAN listeners are deployed and waiting for production ESC/BMS hardware on the buses. Camera plane is scaffolded and ready to install on drones that have a camera. Tracking issue: Arrow-air/project-caribou#12 Architecture: Docs/Architecture.md HubLink wire spec: Docs/HubLink_Implementation_Spec.md
Originated as a fork of the Feather Companion Computer (FCPC); FCPC heritage modules have been removed (see git history).
- Telemetry ingest — MAVLink over UDP from the FC (eth0 to Pixhawk 6X); DroneCAN on
can0for 6× Hobbywing XRotor X15 ESCs; DroneCAN oncan1for 6× per-arm BMSes. - Hub service — WebSocket server
caribou.stream.v1on:8765. Hubs dial in over Tailscale, present a per-drone bearer key, receive 10 Hz telemetry frames + capability manifest. Single-writer control lease arbitrates commands across multiple connected operators. - Run-as-a-service — runs under systemd (
csu.service) withRestart=always; CAN interfaces brought up at boot bycaribou-can.service. Survives reboots, transient outages, andkill -9on the main process. - (Optional) Camera plane — an opt-in, independent stack (
Installation/camera/) bridges any RTSP camera to the Hub via go2rtc + Tailscale Funnel.
[Pixhawk 6X] ─MAVLink/UDP─▶ MAVLink.py ─┐
[6× XRotor X15] ─DroneCAN can0▶ Hobbywing.py ─┤
[6× per-arm BMS] ─DroneCAN can1▶ TattuBMS.py ─┼─▶ Data.py
┘ (tlock-guarded)
│
▼
HubLink.py (WS :8765)
▲
│ Hubs dial IN
Tailscale + 4G
│
Hub #1 … Hub #N
See Docs/Architecture.md for the full diagram, threading model, security layers, Hub-side compatibility notes, and open questions.
| Component | Choice |
|---|---|
| Compute | Raspberry Pi CM5 |
| Carrier | Waveshare CM5-DUAL-ETH-4G-5G-BASE |
| CAN | Waveshare 2-CH CAN HAT — MCP2515 × 2 on SPI0, 12 MHz crystals, IRQs GPIO 25 / GPIO 23 |
| Flight controller | Pixhawk 6X (Ethernet to CM5 eth0 on 192.168.144.0/24) |
| WAN | SIM7600X-H M.2 4G LTE (AT-driven; reference in Docs/AT_Command_Reference.docx) |
| ESCs | 6 × Hobbywing XRotor X15 (set to DroneCAN mode via Hobbywing DataLink) |
| BMS | 6 × per-arm packs (DroneCAN BatteryInfo target; Tattu 18S confirmation pending) |
| Optional camera | any RTSP source (gimbal / IP camera / FPV cam) |
| Enclosure | 3D-printed (Hardware/Enclosure/) |
| Module | Role |
|---|---|
| src/CSU.py | Process entry — spawns four daemon threads, signals + shutdown |
| src/MAVLink.py | MAVSDK UDP listener; 8 parallel collectors → Data.MAVLinkPacket |
| src/Hobbywing.py | DroneCAN esc.Status on can0; per-arm → Data.ESCArms (default node IDs 20–25) |
| src/TattuBMS.py | DroneCAN BatteryInfo on can1; per-arm → Data.BMSArms (default node IDs 10–15) |
| src/HubLink.py | Inbound WebSocket server (caribou.stream.v1), 10 Hz telemetry, control lease, capability manifest, bearer auth |
| src/Data.py | Shared state — tlock + the three structured dicts above |
All configuration is via environment variables. See each module's docstring (or Docs/Architecture.md) for the full list.
# 1. clone
git clone https://github.com/Pan-Robotics/Caribou-System-Unit.git ~/Caribou-System-Unit
cd ~/Caribou-System-Unit
# 2. one-time: enable SPI + load MCP2515 overlays, then reboot
sudo ./Installation/enable_uart_spi.sh # idempotent, reboots at end
# 3. bootstrap everything else (interactive or env-var driven)
./Installation/bootstrap_drone.shbootstrap_drone.sh provisions:
- System packages (
curl,python3-venv,ca-certificates) - Tailscale install + tagged join (non-ephemeral, single-use auth key)
- Python venv at
.venv/withmavsdk,websockets,dronecan ~/caribou-csu.env(API_KEY+DRONE_ID, mode0600)/etc/systemd/system/caribou-can.service(can0500 kbit,can11 Mbit, brought up once kernel MCP2515 devices appear)/etc/systemd/system/csu.service(enabled, started,Restart=always, orderedAfter=caribou-can.service)- Connection-summary card with the drone's MagicDNS name + tailnet IP + stream port to type into the Hub UI
Inputs can be interactive (defaults shown in prompts) or env-var supplied for unattended fleet provisioning:
DRONE_ID=caribou_007 \
API_KEY=<per-drone-bearer> \
TS_AUTHKEY=tskey-auth-... \
./Installation/bootstrap_drone.shThe Tailscale auth key should be non-ephemeral, single-use, and tagged (tag:drone, tag:fleet-caribou by default). Non-ephemeral so drones persist on the tailnet across reboots and outages; single-use so each drone has its own minted-and-spent key.
If the drone has an RTSP camera and you want browser-based WebRTC playback via the Hub, install the camera stack from Installation/camera/ after bootstrap_drone.sh has joined the tailnet. This adds three systemd units that run parallel to and independent of csu.service — telemetry continues unaffected if the camera is removed or broken.
sudo ./Installation/camera/install_camera_services.shWhat gets installed:
go2rtc.service— go2rtc transcodes RTSP → WebRTC; serves the WHEP signaling API on:1984tailscale-funnel.service— exposes:1984on a public HTTPS URL via Tailscale Funnel (signaling only; WebRTC media flows peer-to-peer over UDP)camera-stream.service— Python wrapper that health-checks go2rtc and POSTs the WHEP URL to the Caribou Hub's/api/rest/camera/stream-registerevery 5 minutes
The installer inherits DRONE_ID/API_KEY from ~/caribou-csu.env and writes camera-specific values (RTSP_URL, HUB_URL, GO2RTC_*_PORT) to ~/caribou-camera.env. Two env files = two independently configurable service planes.
Data-plane separation:
Telemetry plane: FC + CAN ─▶ CSU ─▶ HubLink (drone-as-server, Hub dials IN over Tailscale)
Camera plane: RTSP cam ─▶ go2rtc ─▶ Tailscale Funnel (public HTTPS) ─▶ Hub WHEP proxy
Same Tailscale identity, distinct ports + services. A broken camera doesn't affect telemetry; removing CSU doesn't affect the camera.
For remote post-flight .BIN log download and over-the-air firmware updates from the Hub UI, install the Logs & OTA stack from Installation/logs-ota/. Like the camera plane, this is independent of and parallel to csu.service — telemetry continues if Logs & OTA is removed or broken.
sudo ./Installation/logs-ota/install_logs_ota.shWhat gets installed:
logs-ota.service— Python service that polls the Hub job queue forscan_fc_logs,download_fc_log, andflash_firmwarejobs. Reports system diagnostics (CPU, mem, disk, temp, services) every 10 s. Streamsjournalctloutput over Socket.IO when the browser asks for live log tails.firmware_puller.lua+net_webserver_put.lua— Lua scripts that go on the FC's SD card underAPM/scripts/. The puller polls the Pi on:8070for available firmware; the web server (optional Tier 2 fallback) supports HTTP PUT to/APM/.
OTA flash path (Tier 1):
Hub UI ─▶ Pi downloads .abin ─▶ Pi serves on :8070 ─▶ FC (firmware_puller.lua) pulls
─▶ FC writes /APM/ardupilot.abin
─▶ Pi MAVLink-reboots the FC
─▶ FC bootloader flashes new firmware
─▶ Pi polls FC webserver to confirm reboot
Two things to know before install:
-
MAVLink endpoint contention. CSU already listens on UDP
:14540. Logs & OTA needs its own MAVSDK link for MAVFTP / arm-state / reboot, defaulting to UDP:14550. You'll need to configure the Pixhawk'sNET_P2_*parameters to push a second MAVLink stream to the Pi on:14550(alternative: runmavlink-routeron the Pi). See Installation/logs-ota/OTA_Setup_Guide.md. -
FC SD-card payload (remote, no SD removal).
install_fc_scripts.pypushesfirmware_puller.luaandnet_webserver_put.luato the FC's/APM/scripts/over MAVFTP —install_logs_ota.shprompts to run it as its final step. Then setSCR_ENABLE=1,FWPULL_ENABLE=1,FWPULL_PI_IPx, and reboot the FC via Mission Planner. The only situation that still requires touching the SD card / USB-attaching the FC is the very first install on a fresh Pixhawk where noNET_*parameters have been set yet.
The installer inherits DRONE_ID + API_KEY from ~/caribou-csu.env and writes service-specific values (HUB_URL, FC_CONNECTION, FC_WEBSERVER_URL) to ~/caribou-logs-ota.env.
The Hub's Telemetry app (Caribou Hub/Hub Interface/client/src/components/apps/TelemetryApp.tsx) consumes our wire format via a small normalisation function toArmData() that maps HubLink's nested per-arm shape {arm_id, bms:{}, esc:{}} → the flat shape HexStructuralView renders. All fields HubLink emits today are wired up Hub-side, with two minor notes:
heading_degis emitted by HubLink but not currently passed to<CockpitHUD heading={…}>inTelemetryApp.tsx— compass falls back toattitude.yaw_deg. Hub-side fix.motor_temp_c(motor winding temperature, distinct from ESC board temp) is now passed through by HubLink as of June 2026.
See Docs/Architecture.md for the full field-by-field table.
Caribou System Unit/
├── src/ 6 .py files: CSU, Data, MAVLink, Hobbywing, TattuBMS, HubLink
├── Docs/ Architecture, HubLink wire spec, modem AT-command reference
├── Hardware/ 2-CH CAN HAT reference + 3D-printed enclosure files
├── Installation/ bootstrap + per-step scripts + systemd units + env template
│ ├── camera/ optional RTSP camera streaming stack (go2rtc + Tailscale Funnel)
│ └── logs-ota/ optional Logs + OTA stack (FC HTTP pull, diagnostics, journalctl streaming)
├── Logs/ post-flight CSV log parsing tools
└── Test/ HIL fixtures (Arduino BMS/ESC dummies, CAN HAT demo)
# Service status / logs
sudo systemctl status csu.service caribou-can.service
sudo journalctl -u csu.service -f
sudo journalctl -u csu.service --since "5 min ago" | grep -E "(Workers|ready|connected|Hub)"
# Restart core
sudo systemctl restart csu.service
# CAN sanity
ip -br link show type can # both UP?
candump can0 # ESC frames flowing?
candump can1 # BMS frames flowing?
# Tailscale + Hub reach
tailscale status
tailscale ip # your tailnet IP
ss -tlnp | grep 8765 # HubLink listening?- Architecture proposal — full design + current state
- HubLink Implementation Spec — wire protocol, auth, lease, manifest
- Caribou tracking issue
- Caribou Hub (sibling repo) — the production Hub the System Unit serves
- Feather Companion Computer (heritage upstream)