Status: public. Racing quads + GPS Rescue. Native Betaflight stick feel during the race. Not a weapons stack.
Clone without a radio or flight controller:
cd companion
pip install -e ".[dev]"
python -m unittest discover -s tests -t . -vCI on main runs those tests plus Lua syntax. Hardware work starts at Phase 0 (GPS Rescue). Do not fly until the safety runbook and props-off MSP loopback pass. MIT, not for safety-of-life use.
Build guide entry point:
docs/00_START_HERE.mdCompanion software:companion/(Python, runs on Pi Zero 2W) Betaflight CLI configs:bf_config/
Project_Beta_Ardu/
├── README.md # this file — architectural plan + rationale
├── MEMORY.md # project handoff/context — read if picking up cold
├── ROADMAP.md # what's shipped, what's deferred + triggers
├── HARDWARE_BOM.md # parts list with prices and links
├── .github/workflows/tests.yml # CI: pytest + Lua syntax check on push
├── scripts/check_lua_syntax.py # CI helper, runnable locally
├── docs/ # phase-by-phase build instructions
│ ├── 00_START_HERE.md # read first
│ ├── 01_phase0_gps_rescue.md # GPS Rescue per drone (safety floor)
│ ├── 02_phase05_hardware_audit.md
│ ├── 03_phase1_companion_wiring.md
│ ├── 04_phase2_controller.md
│ ├── 05_phase3_field_drills.md
│ ├── 06_phase4_race_day.md
│ ├── safety_runbook.md # cross-cutting safety doctrine
│ ├── mavlink_setup.md # QGroundControl on phone + UART config
│ ├── drill_log.md # per-drill PASS/FAIL log template
│ ├── incident_log.md # post-mortem template + rules
│ └── sign_offs.md # per-drone drill sign-offs
├── companion/ # Python companion software
│ ├── racer_companion/ # main package
│ │ ├── msp.py # MSP v1 wire protocol
│ │ ├── nav.py # haversine + P controllers
│ │ ├── state.py # IDLE→CLIMB→TRANSIT→HOLD→RELEASED
│ │ ├── safety.py # bounds + geofence + watchdog
│ │ ├── mavlink.py # MAVLink v2 publisher (UDP / serial)
│ │ ├── config.py # JSON config loader
│ │ └── main.py # 50Hz loop
│ ├── tests/ # 48 unit tests
│ ├── tools/
│ │ ├── msp_loopback_test.py # bench test against real BF FC
│ │ └── replay_synth.py # offline controller replay
│ ├── systemd/racer-companion.service
│ ├── config/start_line.example.json
│ └── README.md # Pi setup from scratch
├── bf_config/
│ ├── phase0_gps_rescue.diff # BF CLI snippet for Phase 0
│ ├── phase1_msp_companion.diff # BF CLI snippet for Phase 1
│ ├── per_drone/ # versioned per-drone `diff all` dumps
│ └── README.md
├── sitl/ # Betaflight SITL test rig (Docker)
│ ├── Dockerfile # builds BF 4.5.1 SITL target
│ ├── docker-compose.yml # one-line up: docker compose up
│ ├── defaults.txt # auto-applied Phase 0+1 BF config
│ ├── start.sh # boots SITL + applies defaults
│ └── README.md
└── edgetx_scripts/ # Radio-side EdgeTX Lua scripts
├── SCRIPTS/MIXES/racestrt.lua # Race-start sequencer (one-button auto-launch)
├── SCRIPTS/MIXES/safelock.lua # Forces ACRO+AUX-HIGH = unreachable
├── SCRIPTS/TELEMETRY/racehud.lua # Race-state HUD on radio screen
├── lua_for_beginners.md # Lua + EdgeTX tutorial for new coders
├── model_setup_walkthrough.md # Step-by-step radio config (Pocket-class)
└── README.md
git clone https://github.com/gasantiago16/Project_Beta_Ardu.git
cd Project_Beta_Ardu
$EDITOR docs/00_START_HERE.md # read the build guideFor the impatient: if you just want the safety win on every drone today, skip to Phase 0. It's an evening per drone and gives you reliable lost-link RTL with zero new hardware beyond a $25 GPS module.
Radio-side scripts (EdgeTX): if you have a Radiomaster / Jumper / FrSky
radio running EdgeTX, the edgetx_scripts/ directory
adds a one-button race-start sequencer (no more fumbling the dual-flip
between AUX-companion-LOW and ACRO at "GO"), a software safety lock that
makes the dangerous ACRO + AUX-HIGH combo physically unreachable, and a
race HUD. Includes a Lua tutorial for new coders.
Picking the project up cold? Read MEMORY.md — captures
the architectural decisions, sharp edges, and "what to do next" so you
don't have to re-derive them from the code.
A group of expert Betaflight racers want to add two autonomous capabilities to their racing quads without sacrificing race-day stick feel:
- Autonomous flight to a start-line waypoint, then manual takeover for the race
- Reliable Return-To-Launch on lost RC link that recovers, stabilizes, and flies home to a pre-set waypoint
The hard constraint is that the race itself must feel like Betaflight — full rate/acro, no leveling assist, no nav-stack mush. Deep research found that no single firmware delivers both ends cleanly: Betaflight has no flight-proven waypoint nav, INAV/ArduPilot give ~90% of Betaflight's race feel (a competitive racer notices within a lap), and a true dual-FC + ESC-mux design has never shipped credibly because DShot is bidirectional and cold-gyro handover is a crash window.
The shipped pattern that preserves 100% of Betaflight feel and adds real autonomy is Betaflight + a small companion computer that writes virtual stick inputs over MSP, with the pilot's transmitter switch handing control back instantly via msp_override_channels_mask. Native Betaflight GPS Rescue continues to own the lost-link path independently — even if the companion dies.
This plan proposes a 5-phase rollout starting with the lowest-risk safety win (GPS Rescue alone on the existing fleet) and ending with full autonomous-launch + race + failsafe-RTH.
Confirmed scope: 100% native Betaflight stick feel during the race is non-negotiable → companion-computer architecture is correct. Fleet hardware is mixed → Phase 0.5 audit is required before committing per-drone work. This is a group project → workstreams are explicitly parallelizable and called out at the end.
Single Betaflight FC (H7, 4.5+) + Pi Zero 2W or ESP32-S3 companion + dedicated GPS/baro module. No second flight controller, no ESC mux, no firmware swap. Companion talks to BF over UART using MSP. Pilot's existing radio is unchanged.
+----------------+ UART/MSP +-----------------+
| Pi Zero 2W / | <--------------------> | Betaflight FC | -- DShot --> ESCs
| ESP32-S3 | MSP_SET_RAW_RC | (H7, 4.5+) |
| (companion) | RC_OVERRIDE mask | |
| | | GPS Rescue OWNS |
| - GPS read | | the failsafe |
| - waypoint | | path natively |
| logic | +--------+--------+
| - virtual | |
| sticks | | SBUS/CRSF
+----------------+ |
+--------+--------+
| ELRS / Crossfire RX |
+-----------------------+
Why this design:
- Race feel: 100% native Betaflight — pilot's race lap is identical to today's setup.
- Failsafe is independent: BF GPS Rescue triggers on RC link loss regardless of companion state.
- Handoff: pilot flips an AUX switch → companion sees the AUX state and clears the MSP override mask → real RC takes the sticks within one MSP frame (~20 ms).
- Reversible: pull the companion, you have a normal Betaflight quad.
- Hardware additions are cheap (~$30–50 per drone: Pi Zero 2W or ESP32-S3 + M10/M9N GPS + baro if FC lacks one).
Before any autonomy work, get every drone in the fleet onto Betaflight 4.5+ with GPS Rescue properly configured. This independently solves the lost-link requirement and validates the GPS/baro hardware that Phase 1 depends on.
Per drone:
- Flash Betaflight 4.5 (or 4.6 if H7).
- Add GPS module (Matek M10Q-5883 or BN-880 — note: prefer no mag or disable mag per Betaflight wiki; mag is the #1 cause of GPS Rescue flyaways).
- Add barometer if FC doesn't have one (DPS310 / BMP388).
- Configure GPS Rescue:
failsafe_procedure = GPS_RESCUEgps_rescue_min_sats = 8gps_rescue_alt_mode = MAX_ALT(climb to max of pilot-set altitude or current)gps_rescue_initial_climb = 10mgps_rescue_descent_dist = 20mgps_rescue_landing_alt = 4mgps_rescue_throttle_hover= tuned per quad (typically 1275–1325)- Disable magnetometer (
set mag_hardware = NONE) until proven on the bench
- Bench validation drill: Arm in safe location, fly out 50 m, manually flip TX off → quad must climb, rotate, return, descend, land. Repeat 5×.
- Document
gps_rescue_throttle_hoverper quad in a shared spreadsheet.
Critical files referenced (Betaflight master):
src/main/flight/gps_rescue_multirotor.c— state machine:IDLE → INITIALIZE → ATTAIN_ALT → ROTATE → FLY_HOME → DESCENT → LANDINGsrc/main/flight/failsafe.c—FAILSAFE_PROCEDURE_GPS_RESCUEchain (lines ~280–340)src/main/pg/gps_rescue.h— all tunables
Exit criterion: Every drone passes 5 consecutive bench failsafe drills without flyaway, drift, or hard landing.
Required because the fleet is mixed. Do this before anyone orders parts.
For each drone, log:
- FC model + MCU (F4 / F7 / H7) — read from BF Configurator → Setup tab
- Free UARTs (need at least one not already used by RX, ESC telemetry, VTX, OSD, RX SmartAudio)
- Onboard baro? (DPS310, BMP388, none)
- Existing GPS? Mag? (most race quads: no)
- ESC protocol (DShot300/600, BLHeli32 vs AM32) — informational only, no change needed
- Available 5V load capacity for adding companion + GPS
Decision matrix per drone:
- H7 + free UART + baro present → ready for full plan (Phases 1–4)
- H7 + free UART + no baro → add baro module before Phase 1
- F7 + free UART → Phase 0 GPS Rescue works fine; Phase 1+ companion may run but tight on CPU/UART. Test on one F7 drone before committing fleet.
- F4 → Phase 0 (GPS Rescue) only. Recommend FC upgrade to H7 before companion work.
- No free UART → FC upgrade required. Don't try to share a UART with VTX or RX.
Output: shared spreadsheet, one row per drone, with a "Phase target" column (0 / 0+1 / full). Drives BOM ordering and per-pilot work plan.
Wire the companion computer to a spare drone (don't touch the fleet yet).
BOM per drone:
- Pi Zero 2W (
$15) or ESP32-S3 DevKit ($8). Pi gives you a real OS for logging/telemetry; ESP32 is lighter and lower power. Recommendation: Pi Zero 2W for the first build (easier debug), migrate to ESP32 once code is stable. - 5V BEC if not already on FC
- 4-pin JST to FC UART (TX/RX/GND/5V)
- Vibration-isolated mount (3D-printed TPU is fine)
Wiring:
- Companion UART → free UART on FC (commonly UART6 or UART4 on H7 boards)
- Set in BF:
feature RX_MSPis not what we want. Instead, keepserialrxon the real RX UART, and on the companion UART setset msp_override_channels_mask = 15(binary 1111 = roll/pitch/yaw/throttle overrideable). - Verify
msp_override_channels_maskis non-zero only when an AUX switch is high (companion-active mode).
Companion software stub (Pi):
- Python with
pyMultiWiioryaMSPor raw pyserial. - Library/protocol reference: Betaflight
src/main/msp/msp_protocol.h—MSP_SET_RAW_RC = 200, payload is 8×uint16(1000–2000 µs per channel). - Loop at 50 Hz writing center sticks (1500/1500/1500/1000) to prove pipe works.
- Read
MSP_RAW_GPSandMSP_ATTITUDEfor closed-loop later.
Bench validation:
- Props OFF. Arm BF in ACRO. Pull TX sticks to extremes; confirm BF logs the real RC values.
- Flip AUX switch HIGH (companion-active). Companion sends 1500 center sticks.
- Verify: BF rate-output is now zero regardless of TX stick position. AUX-switch LOW → real RC restored within ~50 ms.
Critical files referenced:
- Betaflight
src/main/msp/msp_protocol.h— MSP command IDs - Betaflight discussion #12615 (Offboard Control with Companion Computer) — canonical pattern
- Betaflight issue #13374 — known caveat: companion-active state must NOT trigger RX failsafe; verify behavior on chosen 4.x branch
Implement a minimal "fly-to-waypoint" controller on the Pi.
Controller design (intentionally simple — this is not a research autopilot):
- Inputs: GPS lat/lon/alt (from companion's own GPS module — do not rely on FC's GPS for the autonomous controller; keep them isolated so a sensor fault on one doesn't sink both), target lat/lon/alt, current heading from MSP_ATTITUDE.
- Bearing-to-target: standard great-circle math.
- Distance-to-target: haversine.
- Yaw command: P-controller on (target_bearing − current_heading), clamp to ±200°/s rate.
- Pitch command: P-controller on distance, capped at ~15° equivalent stick (mapped to ~1650 µs forward).
- Roll command: 1500 µs (no lateral, just yaw-to-bearing then pitch-forward).
- Throttle: hold-altitude PID using MSP_ALTITUDE, hover throttle from Phase 0 spreadsheet.
- Arrival: distance < 3 m for 2 s → switch to "hold" state (zero pitch, throttle = hover, yaw locked).
Target waypoint loading:
- Simple JSON file on Pi:
start_line.json = {"lat": ..., "lon": ..., "alt_m": 5} - Loaded at boot. For race day, can be set from a phone over Pi's WiFi AP.
Bench/SITL validation:
- Run controller against simulated GPS data (replay a CSV of GPS coords moving toward target). Confirm computed stick outputs are sane.
- Tethered hover test: tie quad to fence post via 3 m line, set waypoint 5 m to one side, watch it pull toward the line.
This is where it has to actually fly. Stage it:
- Hover test: Arm in ACRO, manual takeoff to 3 m, flip companion AUX HIGH. Quad should hold position. Flip AUX LOW, manual landing.
- Short hop: Set waypoint 20 m away at 5 m altitude. Hand-launch into companion mode. Quad flies to waypoint, holds. Pilot flips AUX LOW → manual flyback.
- Full launch sequence: Place quad at takeoff pad. Companion mode armed. Companion: throttle ramp to hover → hold 3 s → fly to start-line waypoint → hold. Pilot flips AUX LOW, flies the race lap manually.
- Failsafe drill mid-companion-flight: During step 3, mid-transit, kill the TX. GPS Rescue must trigger natively (not via companion) and bring the quad home. This is the load-bearing safety test — verify the companion's MSP traffic does NOT mask the RX failsafe trigger. Reference: BF issue #13374.
- Companion-death drill: During companion-controlled flight, pull the companion's power. BF must detect stale MSP, drop the override (real RX takes over), and pilot recovers manually OR — if also TX failsafe — GPS Rescue fires. Bench-validate stale-MSP behavior on the chosen BF version before this test in air.
Exit criteria:
- 10 consecutive successful auto-launch → handoff → manual race laps
- 3 consecutive successful TX-kill drills with native GPS Rescue recovery
- 3 consecutive successful companion-power-cut drills with manual recovery
- Set start-line waypoint from a phone connected to Pi WiFi AP, walk to start grid, place quad, arm.
- Race director countdown: pilots flip AUX HIGH → all quads auto-launch and hold at start gate.
- "GO" → pilots flip AUX LOW → race begins with native Betaflight feel.
- Crash / lost link → GPS Rescue brings the quad to pre-set home waypoint.
Betaflight side (CLI config only — no firmware fork needed):
- Per-drone
bf_dump.txt(output ofdiffCLI command), versioned in git, including:- GPS Rescue parameters (Phase 0)
msp_override_channels_maskand AUX-conditioned activation (Phase 1)- UART assignment for companion link
Companion side (new repo, e.g., racer_autonav/):
companion/main.py— main loop at 50 Hzcompanion/msp.py— thin MSP_SET_RAW_RC / MSP_RAW_GPS / MSP_ATTITUDE / MSP_ALTITUDE wrapper (usepyMultiWiiif it works on current BF, otherwise raw pyserial againstmsp_protocol.h)companion/nav.py— bearing/distance math + P controllerscompanion/state.py— state machine: IDLE → CLIMB → TRANSIT → HOLD → RELEASEDcompanion/config/start_line.json— waypointcompanion/systemd/racer.service— auto-start on Pi boot
Documentation deliverable:
racer_autonav/README.md— wiring diagram, BF CLI dump template, calibration procedure, drill checklists
- No dual-FC + ESC mux. Research confirmed nobody credible has shipped this. DShot is bidirectional half-duplex with RPM telemetry; an analog switch glitches it. Cold-gyro handover at race speeds is a 50–200 ms crash window. Skip it.
- No INAV migration. Single-firmware INAV is the alternative path. It works (waypoints + RTH + decent ACRO), but a competitive Betaflight racer will feel the difference within one lap. Race feel is non-negotiable for this group.
- No ArduPilot Copter on the race quad. Even with 4.7 +
FSTRATE_ENABLE(4 kHz rate loop on H7), it's ~90% of Betaflight feel and zero racing community uses it. Wrong tool. - No Betaflight master/alpha waypoint code. The
pg/flight_plan.c+AUTOPILOT_MODEwork in BF master is real but SITL-only and unproven on hardware. Not flyable today. - No MAVLink command ingest. Betaflight only sends MAVLink as telemetry; it does not ingest MAVLink commands. MSP is the only command channel.
These three streams can run in parallel once the audit is done. Assign one owner per stream.
- Set up
racer_autonav/repo - Write/port MSP wrapper (
pyMultiWiior raw pyserial againstmsp_protocol.h) - Implement
nav.py(bearing/distance + P controllers) +state.py(state machine) - SITL/replay test harness with synthetic GPS data
- systemd service for Pi auto-start
- Deliverable: working companion image (Pi SD card or ESP32 firmware) that any team member can flash and use
- Write canonical
bf_dump.txttemplate (GPS Rescue params + companion UART +msp_override_channels_mask+ AUX-switch mapping) - Per-drone Phase 0 GPS Rescue tuning (especially
gps_rescue_throttle_hover) - Document per-drone CLI diffs in shared repo
- Bench-validate stale-MSP failsafe behavior on the chosen BF version (load-bearing safety check — see Phase 3 step 5)
- Deliverable: versioned per-drone config + a "flash + dump" runbook
- Build the Phase 3 drill checklist into a printable card
- Identify safe field locations with GPS lock + open emergency-land area
- Run drills on the reference rig first, then each drone
- Maintain a drill log (date / drone / drill / pass-fail / notes)
- Deliverable: signed-off go-live gate per drone before race day
- After Phase 0.5 audit → share spreadsheet, finalize BOM
- After Phase 1 bench tests → all owners review companion+BF wiring on reference rig
- After Phase 2 SITL → group walks through controller behavior in sim
- Before Phase 3 field tests → group safety briefing, drill order locked
- Before Phase 4 race day → 100% drill pass rate per drone, signed off by Stream C owner
If Phase 0.5 reveals the fleet is mostly F4 / no-free-UART, FC upgrades may be cost-prohibitive. Fallback path: keep Phase 0 (GPS Rescue) on the existing fleet for the safety win, and migrate only the upgrade-willing pilots to the full companion architecture. Do not fall back to INAV (single firmware, ~90% feel) — that violates the "100% BF feel non-negotiable" requirement the team agreed on. INAV stays off the table for this group.
Bench (props off):
- BF arms in ACRO, real RC controls all axes
- AUX HIGH + companion sending: real RC inputs are masked, companion stick values appear at output
- AUX LOW: real RC restored within 50 ms
- Companion power cut while AUX HIGH: BF reverts to real RC (or failsafe per
msp_overridestale behavior — version-specific, must verify) - TX power off: BF triggers GPS Rescue regardless of AUX state
Tethered hover (3 m fence-post tether):
- AUX HIGH: holds position over launch point
- Waypoint 5 m N: drifts north until tether stops it
- Throttle hold maintains altitude within ±0.5 m
Open field, low altitude (≤ 10 m):
- Auto-launch to 5 m hover
- Auto-transit to 50 m waypoint
- Hold for 5 s
- Pilot AUX LOW handoff → manual control feels identical to baseline Betaflight
- TX-kill mid-transit → GPS Rescue brings home to within 5 m of takeoff
- Companion-power-cut mid-transit → pilot recovers manually
Race-rep dress rehearsal:
- 10 consecutive auto-launch → handoff → manual lap → land cycles, zero anomalies
- 3 consecutive intentional TX failsafes during race lap, GPS Rescue recovery within 30 s
Go-live gate: all bench + tethered + field tests pass; one full dress rehearsal with all participating pilots' drones; lost-link recovery success rate 100% over ≥10 trials per drone.
- Race feel: 100% native Betaflight is non-negotiable → companion-computer architecture, not INAV migration.
- Fleet hardware: mixed/unknown → Phase 0.5 audit gates Phase 1 BOM ordering and per-drone work.
- Build mode: group project → three parallel workstreams (companion firmware / BF config / field testing) with named owners and explicit sync points.
- Safety floor: every drone gets Phase 0 (GPS Rescue) regardless of whether it goes on to Phase 1+. No drone leaves the bench without a passing failsafe drill.
Research that backed this plan:
Betaflight
- Betaflight 4.5 Release Notes
- Betaflight GPS Rescue Wiki
- Betaflight Magnetometer docs
- Issue #12692 — 4.4.1 GPS lag flyaway
- Issue #14191 — Magnetometer 4.6
- Discussion #12615 — Offboard Control with Companion Computer
- Issue #12790 — MSP_SET_RAW_RC with serial RX
- Issue #13374 — MSP override + failsafe
msp_protocol.h
ArduPilot (evaluated, not selected)
- Acro Mode — Copter docs
- Aggressive Rate Loop Tuning — Copter docs
- Unlocking Faster Attitude Rates — ArduPilot Blog
mode_acro.cpp
INAV (evaluated, not selected)
Companion-computer pattern