PX4 MAVLink Module Deep Dive: Architecture, Flow & Multi-Interface Mechanisms
Analysis target:
src/modules/mavlink/
1. Overview
The MAVLink module is PX4’s core communication module linking the autopilot to external ground control stations (GCS), companion computers, RC transmitters, and other MAVLink devices. It implements the complete MAVLink protocol (v1/v2) send/receive chain, supports serial, UDP, TCP and other physical links, and includes a built-in multi-instance mechanism capable of running multiple independent channels simultaneously.
Core Challenges
The MAVLink module must solve multiplexing at three levels simultaneously:
| Level | Problem | Solution |
|---|---|---|
| Physical Link | Handle serial, UDP, TCP links simultaneously | Unified abstraction via read(fd) / write(fd); differences isolated in the driver layer |
| Multi-Interface Concurrency | Multiple interfaces running at once with different configs | Multi-Instance mode; each instance runs independently |
| Multi-Protocol Services | Handle Mission / Parameter / FTP / Timesync sub-protocols concurrently | Protocol services as independent classes, plugged in via unified handle_message() + send() interfaces |
Data Flow Diagram

2. File Structure
src/modules/mavlink/
├── mavlink_main.cpp # Module entry + main loop (send thread)
├── mavlink_main.h # Mavlink class definition (3515+721 lines)
├── mavlink_receiver.cpp # Receive thread + message dispatch (3571 lines)
├── mavlink_receiver.h # MavlinkReceiver class definition
├── mavlink_messages.cpp # Stream registry + factory functions
├── mavlink_messages.h # StreamListItem template
├── mavlink_stream.cpp # MavlinkStream::update() timing logic
├── mavlink_stream.h # MavlinkStream base class
├── mavlink_rate_limiter.cpp/h # Rate limiter
├── mavlink_command_sender.cpp/h # COMMAND_ACK sending
├── mavlink_parameters.cpp/h # Parameter protocol (MAVLink ↔ PX4 params)
├── mavlink_mission.cpp/h # Mission protocol (waypoint upload/download)
├── mavlink_ftp.cpp/h # File transfer protocol
├── mavlink_timesync.cpp/h # Time sync protocol
├── mavlink_events.cpp/h # Event protocol
├── mavlink_ulog.cpp/h # ULog log streaming
├── mavlink_shell.cpp/h # NuttX Shell remote access
├── mavlink_log_handler.cpp/h # Log message handling
├── tune_publisher.cpp/h # Music playback (buzzer tunes)
├── mavlink_simple_analyzer.cpp/h # Simple signal analyzer
├── open_drone_id_translations.cpp/h # Open Drone ID protocol
├── mavlink.c # Global mavlink_system definition (sysid=1, compid=1)
├── mavlink_params.c # Parameter definitions
├── module.yaml # Auto-start config and parameter definitions
├── streams/ # 100+ MAVLink message stream implementations
│ ├── HEARTBEAT.hpp
│ ├── ATTITUDE.hpp
│ ├── BATTERY_STATUS.hpp
│ ├── GPS_RAW_INT.hpp
│ ├── SYS_STATUS.hpp
│ ├── GLOBAL_POSITION_INT.hpp
│ ├── RC_CHANNELS.hpp
│ ├── SERVO_OUTPUT_RAW.hpp
│ └── ... (100+ files)
└── mavlink_tests/ # Unit tests
├── mavlink_ftp_test.cpp
└── mavlink_tests.cpp
3. Startup Flow
3.1 Complete Startup Call Chain

3.2 Startup Parameters
| Parameter | Meaning | Default |
|---|---|---|
-b |
Baud rate | 57600 |
-r |
Data rate (bytes/s) | baud/20 |
-d |
Serial device name | /dev/ttyS1 |
-u |
UDP local port | — |
-o |
UDP remote port | 14550 |
-t |
Target IP (UDP) | 127.0.0.1 |
-m |
Mode | normal |
-f |
Enable forwarding | — |
-n |
Network interface name | — |
-p |
Enable broadcast | — |
-c |
Multicast address | — |
-x |
Enable FTP | — |
-w |
Wait to send | — |
-z |
Hardware flow control | — |
-Z |
Force no flow control | — |
-s |
Software throttling | — |
-F |
Iridium frequency | — |
4. Thread Model
Each MAVLink instance contains two threads:
Inter-Thread Synchronization
| Shared Resource | Protection Mechanism | Description |
|---|---|---|
_buf[] send buffer |
_send_mutex |
Protects concurrent multi-thread sends |
| Forwarded message buffer | _message_buffer_mutex |
Protects VariableLengthRingbuffer |
| Radio status | _radio_status_mutex |
Protects _rstatus, _radio_status_mult |
| Shell object | _mavlink_shell_mutex |
Protects remote shell access |
| Instance array | mavlink_module_mutex (global) |
Protects mavlink_module_instances[] |
| Event buffer | mavlink_event_buffer_mutex (global) |
Protects shared event buffer |
5. First Layer of Multiplexing: Multi-Instance
The MAVLink module uses the multi-instance mechanism to solve concurrent multi-physical-interface problems.
5.1 Instance Array
static Mavlink *mavlink_module_instances[MAVLINK_COMM_NUM_BUFFERS] {};
MAVLINK_COMM_NUM_BUFFERSis usually 4, maximum 7- Each instance occupies one slot, mapped to
MAVLINK_COMM_0 ~ MAVLINK_COMM_6
5.2 Instance Registration & Channel Mapping
void Mavlink::set_instance_id() {
for (int i = 0; i < MAVLINK_COMM_NUM_BUFFERS; i++) {
if (mavlink_module_instances[i] == nullptr) {
mavlink_module_instances[i] = this;
_instance_id = i;
mavlink_instance_count++;
break;
}
}
}
The instance ID maps directly to the MAVLink library channel (MAVLINK_COMM_0 ~ MAVLINK_COMM_6).
5.3 Typical Multi-Instance Scenario
Each instance is fully independent — its own _streams linked list, its own poll() file descriptors, its own rate configuration.
6. Second Layer of Multiplexing: Physical Link Abstraction
6.1 Protocol Abstraction Enum
enum class Protocol {
SERIAL = 0,
UDP, // Enabled only when MAVLINK_UDP is compiled
};
6.2 Receive Path: Unified poll, Differentiated Read
struct pollfd fds[1] = {}; // Select different fd based on protocol type
if (_mavlink.get_protocol() == Protocol::SERIAL) {
fds[0].fd = _mavlink.get_uart_fd();
} else if (_mavlink.get_protocol() == Protocol::UDP) {
fds[0].fd = _mavlink.get_socket_fd();
}
fds[0].events = POLLIN;
while (!_should_exit.load()) {
int ret = poll(&fds[0], 1, timeout);
if (ret > 0) {
// Select different read method based on protocol type
if (_mavlink.get_protocol() == Protocol::SERIAL) {
nread = ::read(fds[0].fd, buf, sizeof(buf));
} else if (_mavlink.get_protocol() == Protocol::UDP) {
nread = recvfrom(..., buf, sizeof(buf), ...);
}
// Unified parsing and dispatch
for (ssize_t i = 0; i < nread; i++) {
if (mavlink_parse_char(channel, buf[i], &msg, &status)) {
handle_message(&msg);
}
}
}
}
Key point: the read and poll methods differ, but parsing and dispatch are completely unified. Physical link differences are isolated at the read / recvfrom call sites.
6.3 Send Path: Three-Layer Callbacks
Regardless of the physical link, sending goes through the MAVLink library's three-layer callbacks:

7. Send Thread — Main Loop Deep Dive
7.1 Main Loop Flow
7.2 Stream Send Mechanism
Each MAVLink message stream inherits from the MavlinkStream base class:
class MavlinkStream : public ListNode
public:
int update(const hrt_abstime &t); // Called by main loop
virtual bool send() = 0; // Implemented by subclasses
virtual void update_data() { } // High-frequency data update
virtual bool const_rate() { return false; }
virtual unsigned get_size() = 0;
};
update() logic:
7.3 Rate Adaptation
update_rate_mult() dynamically adjusts _rate_mult (0.05 ~ 1.0) to keep total bandwidth below _datarate:

8. Receive Thread — Message Reception & Dispatch
8.1 Receive Loop

8.2 Message Dispatch — handle_message()
handle_message() uses a large switch for message routing, then dispatches messages to each protocol service via the back-chain:

8.3 Message Handling → uORB Publication
Each handle_message_xxx() function typically: decodes the MAVLink message → builds a uORB message → orb_publish().
Typical mapping relationships:
| MAVLink Message | Published to uORB Topic |
|---|---|
COMMAND_LONG |
vehicle_command |
MANUAL_CONTROL |
manual_control_setpoint |
RC_CHANNELS_OVERRIDE |
rc_channels |
SET_POSITION_TARGET_LOCAL_NED |
vehicle_command / position_setpoint_triplet |
SET_ATTITUDE_TARGET |
vehicle_command |
VISION_POSITION_ESTIMATE |
vehicle_visual_odometry |
ODOMETRY |
vehicle_visual_odometry |
DISTANCE_SENSOR |
distance_sensor |
GPS_RTCM_DATA |
gps_rtcm_data |
HEARTBEAT |
No publish → updates internal timeout state |
PING |
No publish → replies PING directly |
9. Third Layer of Multiplexing: Protocol Services
9.1 Protocol Service Members
In the MavlinkReceiver class, multiple protocol services exist as member objects:
class MavlinkReceiver {
MavlinkFTP _mavlink_ftp;
MavlinkLogHandler _mavlink_log_handler;
MavlinkMissionManager _mission_manager;
MavlinkParametersManager _parameters_manager;
MavlinkTimesync _mavlink_timesync;
MavlinkStatustextHandler _mavlink_statustext_handler;
TunePublisher *_tune_publisher;
};
9.2 Chain of Responsibility: handle_message() Back-Chain
Each protocol service connects to message dispatch via the chain of responsibility pattern — each handler only processes messages it cares about and skips the rest:

void MavlinkReceiver::handle_message(mavlink_message_t *msg)
{
switch (msg->msgid) {
// ... 30+ message types handled directly
}
// Back-chain: each protocol service checks and handles messages it cares about
_mission_manager.handle_message(msg); // MISSION_xxx
_parameters_manager.handle_message(msg); // PARAM_xxx
if (_mavlink.ftp_enabled()) {
_mavlink_ftp.handle_message(msg); // FILE_TRANSFER_PROTOCOL
}
_mavlink_log_handler.handle_message(msg); // LOG_xxx
_mavlink_timesync.handle_message(msg); // TIMESYNC
_mavlink.handle_message(msg); // Forward to other instances
}
Each protocol service only processes msgids it cares about and ignores the rest.
9.3 Receive Thread Periodic Polling (Send Direction)
In the receive thread's main loop, each protocol service's send() is polled every 10ms:

const hrt_abstime t = hrt_absolute_time();
if (t - last_send_update > timeout * 1000) { // timeout = 10ms
_mission_manager.check_active_mission();
_mission_manager.send();
if (_mavlink.get_mode() != MAVLINK_MODE_IRIDIUM) {
_parameters_manager.send();
_mavlink.set_sending_parameters(_parameters_manager.send_active());
}
if (_mavlink.ftp_enabled()) {
_mavlink_ftp.send();
}
_mavlink_log_handler.send();
last_send_update = t;
}
if (_tune_publisher != nullptr) {
_tune_publisher->publish_next_tune(t);
}
9.4 Protocol Service Dispatch Matrix
| Protocol Service | Receive Direction | Send Direction | Running Thread |
|---|---|---|---|
| Stream Messages (Stream) | N/A | stream->update(t) iteration |
Send thread |
| Command (COMMAND) | handle_message_command_long/int() |
handleAndGetCurrentCommandAck() |
Send thread |
| Mission | _mission_manager.handle_message(msg) |
_mission_manager.send() |
Receive thread (10ms) |
| Parameter | _parameters_manager.handle_message(msg) |
_parameters_manager.send() |
Receive thread (10ms) |
| FTP | _mavlink_ftp.handle_message(msg) |
_mavlink_ftp.send() |
Receive thread (10ms) |
| Log | _mavlink_log_handler.handle_message(msg) |
_mavlink_log_handler.send() |
Receive thread (10ms) |
| Timesync | _mavlink_timesync.handle_message(msg) |
N/A | Receive thread |
| Events | handle_message_request_event() |
_events.update(t) |
Send thread |
| ULog stream | handle_message_logging_ack() |
_mavlink_ulog->handle_update() |
Send thread |
| Shell | handle_message_serial_control() |
handleMavlinkShellOutput() |
Send thread |
| Tune | handle_message_play_tune() |
_tune_publisher->publish_next_tune() |
Receive thread |
| Forward | _mavlink.handle_message(msg) |
Main loop pops buffer | Send thread |
10. Cross-Instance Forwarding Mechanism
When messages need to be shared between different physical interfaces, forward_message() handles it.
10.1 Forwarding Flow
The forwarding mechanism lets a message received by one instance be passed to other instances and finally transmitted through their physical links. The core flow:

10.2 Forwarding Filter Conditions
void Mavlink::forward_message(const mavlink_message_t *msg, Mavlink *self)
{
// 1. No other instances → no forwarding
if (mavlink_instance_count.load() <= 1) return;
// 2. Target is myself → no forwarding
if (target_system_id == self->get_system_id()
&& target_component_id == self->get_component_id()) return;
// 3. Heartbeat not forwarded by default (unless forward_heartbeats_enabled)
if (msg->msgid == MAVLINK_MSG_ID_HEARTBEAT
&& !self->forward_heartbeats_enabled()) return;
// 4. Low bandwidth mode blocks specific messages
if (self->get_mode() == MAVLINK_MODE_LOW_BANDWIDTH
&& msg->msgid == MAVLINK_MSG_ID_ONBOARD_COMPUTER_STATUS) return;
// 5. Only forward to instances where the target component has appeared
for (Mavlink *inst : mavlink_module_instances) {
if (inst && inst != self && inst->get_forwarding_on()) {
if (inst->_receiver.component_was_seen(target_system_id, target_component_id)) {
inst->pass_message(msg);
}
}
}
}
11. Modes & Stream Configuration
11.1 Supported Modes
| Mode | Purpose | Data Rate |
|---|---|---|
NORMAL |
Full GCS telemetry (default) | Medium |
CUSTOM |
No default streams | Custom |
ONBOARD |
Companion computer high rate | High |
OSD |
OSD overlay display | Low |
CONFIG |
USB debug configuration | High |
IRIDIUM |
Iridium satellite link | Very low |
MINIMAL |
Extremely low bandwidth link | Very low |
EXTVISION |
External vision (VIO) | High |
EXTVISIONMIN |
External vision minimal | Medium |
GIMBAL |
Gimbal communication | Low |
ONBOARD_LOW_BANDWIDTH |
Onboard low bandwidth | Medium |
UAVIONIX |
ADSB transponder | Low |
LOW_BANDWIDTH |
Severely constrained link | Very low |
DISTANCE_SENSOR |
Distance sensor only | Very low |
11.2 Stream Registry
In mavlink_messages.cpp, the stream list is built at compile time via template metaprogramming:
static const StreamListItem streams_list[] = {
create_stream_list_item
create_stream_list_item
create_stream_list_item
create_stream_list_item
// ... 100+ streams
};
Each stream is guarded by #if defined(XXX_HPP), allowing trimming on resource-constrained targets.
11.3 Stream Lifecycle
12. Protocol Services in Detail
12.1 FTP Protocol (MavlinkFTP)
Implements remote filesystem access via MAVLink FILE_TRANSFER_PROTOCOL messages.
Supported operations: List Directory, Open File, Read/Write File, Create/Remove Directory, Rename, Calc CRC32, Burst Read.
How it works:
- Receive:
handle_message()parses payload →_process_request()→ switch(opcode) dispatch - Send: receive thread calls
send()every 10ms to handle burst transfers - Security: all paths are prefixed with
_root_dirto prevent path escape
12.2 Mission Protocol (MavlinkMissionManager)
Implements the MAVLink waypoint/mission protocol (upload, download, clear, set current target).
State machine: IDLE → SENDLIST / GETLIST → IDLE
Key design decisions:
- Uses
DatamanClientfor persistent mission item storage - Supports three mission types:
MISSION,FENCE,RALLY - Uses
MavlinkRateLimiterto limit speed _transfer_in_progressglobal flag prevents concurrent transfers
12.3 Parameter Protocol (MavlinkParametersManager)
Implements the MAVLink parameter protocol (read, write, list sync).
send_all(): iterates all PX4 parameters, sends in batchessend_one(): responds to single parameter read requestssend_untransmitted(): sends changed-but-unsent parameters
Key design decision: parameter sending blocks until boot_complete, ensuring parameters only sync after full system initialization.
12.4 Time Sync (MavlinkTimesync)
Implements the TIMESYNC protocol to synchronize PX4 and GCS clocks. The principle is round-trip time (RTT) measurement, computing the clock offset.
12.5 Other Protocol Services
| Protocol Service | Function |
|---|---|
| Log protocol (MavlinkLogHandler) | Handles STATUSTEXT, LOGGING_ACK |
| Event protocol (MavlinkEvents) | Streams PX4 internal events to GCS via MAVLink (handled in send thread) |
| ULog stream (MavlinkULog) | Streams ULog binary logs to GCS in real time (handled in send thread) |
| Remote Shell (MavlinkShell) | NuttX Shell remote access |
| Tune playback (TunePublisher) | Buzzer tune MAVLink control |
13. Full Data Flow Overview

14. Design Pattern Analysis
14.1 Chain of Responsibility
The large switch in handle_message() plus the back-chain protocol service calls form a chain of responsibility:
MAVLink message → COMMAND handler → MISSION → PARAM → FTP → ...
Each handler only processes messages it cares about and skips the rest.
14.2 Strategy Pattern
Physical links implement the strategy pattern via the Protocol enum + conditional branches; each mode defines a different stream set and rate in configure_streams_to_default() — another strategy pattern application.
14.3 Observer Pattern — Pull Model
MAVLink streams use the pull model — the main loop periodically iterates all streams, actively pulling data from uORB. This differs from uORB's push model (publisher pushes).
Main loop: for (stream : _streams) stream->update(t)
→ sub->update() pulls latest data from uORB
→ send() → pack → write(fd)
14.4 Factory Method (Stream Creation)
create_stream_list_item is a template factory function that generates stream creation info at compile time. configure_stream() matches by name string at runtime and calls the factory to create instances.
14.5 Command Pattern (COMMAND Handling)
handle_message_command_long() converts MAVLink COMMAND messages into vehicle_command uORB messages, executed asynchronously by the Commander module — decoupling the command sender from the executor.
14.6 Adapter Pattern (Protocol Services)
Each protocol service (MavlinkFTP, MavlinkParametersManager, etc.) acts as an adapter between the MAVLink protocol and PX4 internals, converting MAVLink protocol messages into internal operations and encoding internal state back into MAVLink messages.
15. Decoupling Analysis
✅ Well-Decoupled Areas
| Dimension | Implementation | Rating |
|---|---|---|
| Physical link abstraction | Unifies serial and UDP via read(fd) / write(fd) |
★★★★★ Differences isolated in 2 conditional branches |
| Message streams vs business logic | 100+ independent stream files, each encapsulating one message | ★★★★★ Adding a message = creating a .hpp file |
| Inter-instance isolation | Each instance has independent _streams list and poll() fd |
★★★★★ Fully independent; bridged via forwarding |
| Pluggable protocol services | Each service as an independent class via handle_message() + send() interfaces |
★★★★ Adding a protocol = new class + declaration |
| Send thread vs receive thread | Two independent threads communicating via uORB and message queues | ★★★★ Receive never blocks send |
| MAVLink library | Standard C library mavlink_parse_char() / mavlink_msg_xxx_pack() |
★★★★★ No dependency on specific MAVLink library internals |
| Streaming message plugin-ization | 100+ stream files, one message per file | ★★★★★ Adding a message = new .hpp |
⚠️ Areas for Improvement
| Dimension | Current State | Issue |
|---|---|---|
| handle_message() file size | 3571 lines, one file with 30+ handlers | File too large, violates single responsibility principle |
| Main loop responsibilities | Single loop handles 10+ tasks | High coupling, hard to modify and maintain |
| Hardcoded protocol services | 6 protocol services are fixed members of MavlinkReceiver |
Cannot dynamically load/unload at runtime |
| Protocol service call frequency | All services polled at fixed 10ms interval | Wastes CPU under low load |
| Receive thread overload | Does both message parse/dispatch and periodic protocol service sends | Could split send duties to send thread |
| Rate adaptation | Coupled with send logic in task_main() |
Could extract to independent strategy class |
| Global instance array | Static array + global mutex | Limits maximum instance count |
| Stream configuration | Hardcoded in configure_streams_to_default() |
Adding a new mode requires modifying this function |
16. Summary
Core Architecture
The MAVLink module solves multi-interface, multi-protocol concurrency through three-layer multiplexing:
Overall Assessment
| Dimension | Assessment |
|---|---|
| Architecture | Dual-thread (send + receive) + multi-instance; each instance runs independently |
| Message reception | poll() → parse_char() → switch dispatch → handler → uORB |
| Message sending | Stream-based: main loop iterates streams → update() checks interval → send() → write(fd) |
| Rate control | Dynamic _rate_mult adaptation considering link congestion |
| Protocol services | 6+ sub-protocol services (mission/param/ftp/timesync/events/ulog) |
| Extensibility | Adding a message = new stream file + register in streams_list[] |
| Overall | ★★★★☆ Complete functionality, clear architecture. The oversized receive dispatch switch is the main coupling point, but the plug-in stream design is a highlight |
At Aomway, we build and fly PX4-based FPV platforms daily, and the MAVLink module is the invisible backbone that connects every Aomway airframe to its ground station — carrying attitude, GPS, battery, and command data over the exact mechanisms dissected above. Understanding this architecture matters far beyond academia: when you're configuring an Aomway Commander ground station, tuning stream rates for a low-bandwidth long-range link, or debugging why a companion computer sees telemetry but your RC failsafe doesn't respond, you're working with the three-layer multiplexing, rate adaptation, and protocol services this article explains. The MAVLink module's plug-in stream design is precisely why PX4 remains the most flexible open autopilot for custom FPV and commercial UAV builds. Questions about MAVLink configuration, PX4 tuning, or integrating Aomway flight systems with your ground control software? Contact us at [email protected].
Frequently Asked Questions
1. What exactly is "multi-instance" in the PX4 MAVLink module and why does it matter?
Multi-instance means the MAVLink module can run several independent instances simultaneously — each bound to a different physical link (e.g., one on a serial telemetry radio to the RC controller, another over USB to a ground station, another over UDP to a companion computer). Each instance gets its own slot in mavlink_module_instances[] (usually up to 4, max 7), its own stream list, file descriptors, and rate configuration. Instances are bridged by the forwarding mechanism, so a command received on one link can be relayed to another. This is why PX4 can talk to a GCS and a companion computer at the same time without conflict.
2. How does the three-layer multiplexing work in practice?
Layer 1 is multi-instance, solving concurrent physical interfaces (each instance owns a link). Layer 2 is the physical link abstraction — serial vs UDP are unified behind read(fd)/write(fd), so parsing and dispatch code is identical regardless of link type. Layer 3 is the protocol services: Mission, Parameter, FTP, Timesync, Events, ULog, Shell, and Tune each exist as independent classes plugged into a unified handle_message() + send() interface, implementing the chain of responsibility pattern. Together they let one module handle any combination of links, protocols, and message types without code duplication.
3. What is the difference between the send thread and the receive thread?
The send thread (in mavlink_main.cpp) runs the main loop that periodically iterates all registered streams, calls update() to check each stream's send interval, invokes send() to pack data (pulled from uORB), and writes to the file descriptor. The receive thread (in mavlink_receiver.cpp) polls the fd, feeds bytes into mavlink_parse_char(), dispatches complete messages via handle_message(), and publishes to uORB topics. The receive thread also polls protocol service send() methods every 10ms for mission/param/FTP/log responses. This split ensures incoming messages never block outgoing telemetry.
4. How does rate adaptation (_rate_mult) work?
update_rate_mult() dynamically adjusts a multiplier _rate_mult between 0.05 and 1.0 to keep total stream bandwidth under the configured _datarate (default baud/20 bytes per second). When the module detects that the link is congested — typically by monitoring radio status, link throughput, or buffer pressure — it scales down stream rates proportionally, prioritizing critical streams. This is why on a low-bandwidth link (like Iridium satellite), the module can still deliver heartbeat and critical status while throttling high-rate streams like raw IMU data. It's a practical congestion control mechanism that keeps telemetry alive on marginal links.
5. How do I add a custom MAVLink message or stream to PX4?
To add a new stream: (1) create a new .hpp file in src/modules/mavlink/streams/ that inherits from MavlinkStream, implementing send() and get_size(), and optionally update_data(); (2) register it in streams_list[] in mavlink_messages.cpp via create_stream_list_item; (3) ensure the new message ID is defined in the MAVLink library headers (or generate custom XML definitions). For incoming messages, add a case to handle_message() in mavlink_receiver.cpp (or create a protocol service class following the existing patterns). The plugin-style stream design means most additions are isolated to a single file.