Skip to content

MAVLink 2 Interface

Lyrebird presents each aircraft as a MAVLink 2 vehicle, so a stock ground station — QGroundControl, MAVSDK, pymavlink — connects, sees telemetry and video, and flies it with no plugin and no configuration file.

This is a full control surface, not a telemetry feed. Every command the ROS ground station uses has a MAVLink form, and so does every /send/set* setting. The HTTP surface on port 8080 still exists and is unchanged; the two are kept in step deliberately, and the dashboard’s MAVLink tab reads both at once so a disagreement between them is visible rather than inferred.

Disabled by default. Following PX4’s pattern of switching MAVLink instances on by parameter rather than by build:

PreferenceTypeDefaultMeaning
lb_mav_0_enabledboolfalseStart the endpoint at all
lb_mav_0_hoststring(empty)Ground-station address. Empty means broadcast on the subnet
lb_mav_0_portint14550Port to send to and listen on
lb_mav_0_modestringnormalStream profile: normal or minimal
lb_mav_0_sysidint1MAVLink system id — one per aircraft
lb_mav_0_allow_flightboolfalseAllow commands that move the aircraft
lb_mav_0_signing_keystring(empty)64 hex characters shared with the Safety Computer
lb_mission_execstringdji_nativeWho flies an uploaded plan: onboard or dji_native — see Missions

Flight commands are gated twice: lb_mav_0_allow_flight must be on, and the command must pass the same ControlAuthority check the HTTP surface uses. Anything that would fly the aircraft somewhere new is refused while the pilot holds the sticks; land, return and abort stay available, because those are the recovery actions.

MicroserviceWhat works
TelemetryHeartbeat, attitude, position, GPS, battery, VFR HUD, extended system state, home position, current mode, mission progress, gimbal attitude, rangefinder distance
CommandTake-off (with altitude), land, return, reposition, yaw, altitude, stick input, arm/disarm, camera, gimbal, payload release
MissionUpload and download handshake, MISSION_START, progress and arrival reports. Per-waypoint hold time, acceptance radius, pass-through, heading, speed, camera/gimbal actions and region of interest are all honoured by one of two executors — see Missions
ParameterPARAM_SET for numeric settings; PARAM_EXT_SET for string settings such as the drone name, video source and MediaMTX address
Camera / GimbalCamera information, settings, capture status, video stream information; gimbal attitude and pitch/yaw control
File transferRead-only MAVLink FTP for listing and reading the SD card
SigningMAVLink 2 packet signing identifies the Safety Computer, mirroring X-Safety-Token on the HTTP surface

Lyrebird’s heartbeat claims MAV_AUTOPILOT_PX4. It does not run PX4 firmware — DJI’s own flight controller does everything — but MAVLink identity is what a ground station uses to decide which UI to show, and getting that identity right is what makes QGroundControl’s Fly View and Plan view (including mission upload and start) work with zero configuration, rather than a telemetry-only read-out with no way to fly the aircraft.

Why PX4 specifically. QGroundControl only enables its Fly View action buttons (Takeoff, Land, RTL) and its Plan view’s mission controls for firmware plugins that declare guided-mode capability. Its Generic plugin — what MAV_AUTOPILOT_INVALID/_GENERIC gets — declares none. Between QGC’s two vendor plugins, PX4’s mode list is the closer match to what a DJI aircraft can actually do: Takeoff, Land and Return exist as named PX4 modes, where ArduCopter’s mode list has no equivalents.

Modes are PX4’s packed numbers, not names. Claiming PX4 has a cost: QGC’s PX4 plugin renders HEARTBEAT.custom_mode through PX4’s own mode enum — a packed (main_mode << 16) | (sub_mode << 24) integer, not a string. MavlinkFlightMode maps each of Lyrebird’s own modes onto the PX4 mode number whose meaning matches:

Lyrebird modeReported as PX4 mode
Position holdPOSCTL
Altitude holdALTCTL
Offboard (virtual stick)OFFBOARD
MissionMISSION
Take-offTAKEOFF
LandLAND
ReturnRTL
OrbitORBIT
ManualMANUAL
DJI intelligent-flight modesFOLLOW_TARGET

This works in both directions: when an operator presses Land or RTL in QGC’s Fly View, QGC sends SET_MODE with PX4’s number for that mode, and the endpoint maps it straight back to the Lyrebird action it named. The same modes are also advertised through the newer, portable MAV_STANDARD_MODE protocol (AVAILABLE_MODES/CURRENT_MODE) wherever a standard identity exists, so a ground station that understands standard modes doesn’t need any PX4-specific knowledge at all.

A version number QGC insists on. QGC’s initial-connect handshake explicitly requests AUTOPILOT_VERSION by name and retries until it gets one — streaming it on a timer alone was not enough (found by running QGC against the endpoint and reading RequestAutopilotVersion: Max retries exhausted in its log). The reply reports PX4 firmware 1.15.0: leaving the version at zero makes QGC show two dialogs on every single connection — a compatibility notice and a “not running latest stable firmware” warning — for no reason other than an absent version number.

Capabilities are only claimed once they exist. The same message reports a capability bitmask; today that is only MAVLINK2 and FTP, matching the read-only MAVLink FTP server Lyrebird actually serves. Claiming MISSION_INT or any other capability before the code behind it exists would be the same class of mistake as claiming the wrong autopilot — QGC would assume behaviour that isn’t there.

Where honesty resumes. The identity claim stops at the wire-protocol level. Any telemetry value DJI genuinely does not provide is reported using MAVLink’s own “unknown” conventions (NaN, INT32_MAX, and similar), not a plausible-looking zero that would read as real data.

Heading, per waypoint. NAV_WAYPOINT.param4 is NaN for “use the vehicle’s own heading mode” and a value for “hold this heading”. That is exactly the difference between Lyrebird’s two waypoint controllers, so one plan can mix them, and DO_REPOSITION reads it the same way. No custom mission item was needed.

Arrival, per waypoint. param1 (hold time), param2 (acceptance radius) and param3 (pass through) come from the plan, because only the plan knows which leg is the last one. A leg marked pass-through is flown through; the final one settles.

A UDP datagram goes to exactly one socket, so each ground station needs its own listen port; the aircraft fans telemetry out to every station it has heard from.

ListensSends to aircraft
QGroundControl1455014550
ROS drone nodes (LB_MAVLINK_PORT)1455114550
Dashboard MAVLink tab (LB_WEBAPP_MAVLINK_PORT)1455214550

A fleet needs one port per aircraft for the same reason.

Terminal window
pip install pymavlink
lyrebird-mavlink-listen --summary 5

It prints the first instance of each message with decoded values, then a rate summary, and reports malformed frames loudly as BAD_DATA. It never transmits.

Connect QGroundControl: it listens on UDP 14550 by default and adds a link on receiving traffic. If the RC and the ground station share a LAN, enabling lb_mav_0_enabled is all that is required — see above for why QGC’s action buttons and mission controls light up with no plugin.

The Field Test page is the procedure for verifying all of this against a real aircraft. The ground half runs itself:

Terminal window
cd GroundStation/Python
python test_scripts/field_check.py <PHONE_IP> # listens only
python test_scripts/field_check.py <PHONE_IP> --phase ground # parameter writes
python test_scripts/field_check.py <PHONE_IP> --phase payload --move
python test_scripts/field_check.py <PHONE_IP> --phase flight --fly

Checks are grouped by what they can move, and the script will not cross a group boundary without being told to: it sends nothing at all by default, needs --move before the gimbal or camera responds, and only prints the flight list under --fly.

Lyrebird’s supported public video path is WebRTC publishing through WHIP to MediaMTX, with browser playback through WHEP. The app publishes DJI camera frames to a WHIP URL selected by the ground station, normally:

http://<ground-station-ip>:8889/<drone_name>/whip

The GroundStation video dashboard then watches the matching WHEP URL:

http://<ground-station-ip>:8889/<drone_name>/whep

This replaces the older direct RC-hosted WebSocket-signaling viewer. That sample viewer/server path is no longer part of the public app or ground-station tooling.