GSoC 2026: AP_SwarmMesh - Resilient MAVLink Ad-HOC Swarm Networking for ArduPilot

GSoC 2026: AP_SwarmMesh: Resilient MAVLink Ad-HOC Swarm Networking for ArduPilot

A framework for onboard swarm coordination within ArduPilot

Mentors: Nate Mailhot & Asif Khan | Timeframe: May → Sep 2026 | Project: GSoC

1. Introduction

Hi ArduPilot community! My name is Kwaku and I am currently pursuing a Masters degree in Mechanical Engineering at the University of Ottawa. Over the past year, I have been working on a project which provides an open-source swarming platform for aerial drones using off-the-shelf micro-drone hardware called ArduSwarm. ArduSwarm mates Bitcraze Crazyflie hardware with a custom version of ArduPilot to enable self-contained decentralized indoor swarming, with the end goal of using the platform to support my current research into decentralized indoor swarming algorithms.

ArduSwarm drone hardware. Experimental validation of critical ArduSwarm functionality

Figure 1 - From left to right: ArduSwarm drone hardware, Experimental validation of critical ArduSwarm functionality.

In this time, I have become very familiar with the topic of swarming and peer-to-peer (P2P) networks in particular, and as such I’m excited to be working on this with the community!

2. Problem Statement

Demand is rising for full-fledged swarming support within open-source flight control software like ArduPilot. Currently, most swarming coordination is centralized, meaning communication is routed through a central Ground Control Station (GCS) (see swarming).

This kind of swarm backend is:

  • Vulnerable to connection loss (single point of failure).
  • Relies on external computation for coordination and/or advanced autonomy.
  • Not easily scalable (2-10 vehicles).

To enable a robust, decentralized swarm backend, I propose adding a new library to ArduPilot, dubbed AP_SwarmMesh, which enables distributed peer state updates and peer state forwarding while also maintaining an onboard peer state table which is accessible and adaptable for future swarm implementations within ArduPilot.

3. Project Proposal

This project builds a scalable communication backend within ArduPilot for decentralized peer-to-peer networks for swarm coordination

Figure 2 - High-level overview of the AP_SwarmMesh software and hardware structure. Each vehicle connects to onboard radios through two UART ports (one for standard telemetry, one for P2P mesh). AP_SwarmMesh serializes a stream of peer data into MAVLink packets and then encapsules them in a peer routing header. These peer packets are broadcast to neighbors and are distributed throughout the swarm. Upon receiving a peer packet, a vehicle parses the packet and stores a limited state of each peer.

The project introduces:

  • Logging integration: any peer messages which pass the initial screening are parsed and are logged using the existing logging backend.
  • Peer state table: the user sets a swarm size which initializes a peer state table in memory. As peer messages come in, the corresponding state entry in the peer table is updated.
  • Persistent snapshots: a new directory under external storage, APM/PEERS/, which stores snapshots of the peer state table at a set frequency. Intended to protect against reboots.
  • Peer stream: user-configurable stream of MAVLink messages intended to disseminate state data to peers. Rate-limited depending on hardware backend.
  • Peer frame header: peer header prepended to MAVLink messages which includes important routing information used for forwarding and duplicate/timeout rejection.

This project builds infrastructure to enable full decentralized swarming within ArduPilot. Rather than managing multiple vehicles through a GCS with multiple vehicle connections, this project gives each drone the ability to manage swarm coordination locally, while also enabling MAVLink forwarding over an ad-hoc peer network in environments where network connections are unreliable.

4. Wire Format

The following byte map details the peer header, which is prepended to each outgoing peer packet. The peer header is fixed in length (23 bytes) and thus can be read quickly first by a state machine which routes the packet and filters duplicate / old packets. Only after passing through the state machine will a packet’s contents be parsed by passing the payload to a MAVLink parser. This minimizes the resources used in the receiving path.

Table 1 - Proposed Peer Routing Packet Structure

Name Data Type Purpose
magic/sync uint16_t Marks the start of a peer packet.
version uint8_t Lets us change the header later without breaking compatibility. If we later add fields, change flag meanings, etc. old implementations can reject unsupported versions cleanly.
flags uint8_t Bitmask for special behaviors. It tells the receiver how to interpret the packet beyond the basic fields.
type uint8_t Payload contents (ie. MAVLink, ack, etc.). Allows us to route packets without parsing the payload.
ttl uint8_t Time-to-live, essentially a hop count limit. Every relay decrements it by 1. When it reaches 0, the packet is dropped. Prevents: loops, endless forwarding, stale packets.
origin_id uint8_t This identifies the node that originally created the packet. Adds: duplicates handling, stats, route/debug logging, source ID without parsing MAVLink sysid.
prev_id uint8_t Tracks the most recent forwarding node.
dest_id uint8_t Allows broadcast or targeted delivery.
seq uint16_t Relay sequence number generated by the origin node. Used to identify each packet from each origin. Adds: duplicate suppression, packet tracking, ACK/NACK logic, loss stats.
origin_time_us uint64_t Timestamp when the origin node created the relay packet (unix time). Available for GPS-equipped drones for synchronization.
deadline_ms uint16_t This is a relative freshness budget. Tells the network how long the packet remains useful. (ex. a RC_CHANNELS_OVERRIDE packet is not very useful after 100 ms, whereas a STATUSTEXT is valid way after).
payload_len uint8_t This is the length of the MAVLink frame. It tells the receiver where the payload ends.
crc uint8_t One byte CRC over header contents (stx through payload_len) to catch errors in header. Payload has its own CRC.
MAVLink Frame variable Carries the full serialized MAVLink packet unchanged.

5. TX Path - Outbound

AP_Scheduler passes AP_SwarmMesh::update() at a reduced frequency of around 100 Hz to reduce compute overhead. If a serial port is bound to AP_SwarmMesh, it passes AP_SwarmMesh_Serial::update(), which iterates through the MAVLink streams (POSITION, ATTITUDE, STATUS, etc.), which are enabled by bitmask parameter. When a stream’s interval elapses, it checks for any priority deferred messages (HEARTBEAT / PARAM_SET) first, then falls through to stream messages serialised with the existing MAVLink macros.

Before sending the packet out, txspace() is checked for adequate space (packet is discarded if full). The P2P header is then prepended, and the packet is written as two succussive buffers to uart, which is the bound serial port which is connected to the P2P radio (either a companion computer in a ‘Full’ peer stream, or a generic radio in a ‘Lite’ peer stream). Also, forwarded packets are written to uart if there is space in txspace().

Any dropped packets are counted, and the loop yields if the scheduler’s 5ms budget is exceeded.

proposed_transmission_path.pdf (31.3 KB)

Figure 3 - Proposed transmission path.

6. RX Path - Inbound

Bytes are fed from the serial port (from either a companion computer in a ‘Full’ peer stream, or a generic radio in a ‘Lite’ peer stream) into a fixed-size peer header state machine. Bytes are fed into the state machine until the magic bytes are seen. The state machine then validates the header via a one-byte CRC and then reads the payload. Once the entire packet has been written to buffer, the state machine returns true.

A valid header then hits a version check which checks which header version is being sent. We then check if the packet is a duplicate against a per-peer ring buffer (any duplicates are discarded). Next, the packet passes a flag check (which checks for any special behavior such as if the origin drone had GPS-based RTC for synchronization). The packet is also checked for staleness, if the time since creation is greater than the deadline, the packet is dropped. Then, packets decrement TTL (zero-TTL packets are dropped).

If ttl > 1 and the destination id is not the same as the current sysid, a forwarded copy is written out (with prev_id rewritten to sysid and TTL decremented).

Finally, the packet hits a type check which routes the packet to the correct parser. If the packet is MAVLink, the MAVLink frame is fed to handle_mavlink() [5], and on a clean parse AP_Logger is written [6], and the periodic peer state snapshot is updated (at a set frequency).

proposed_receiving_path.pdf (43.3 KB)

Figure 4 - Proposed receiving path.

7. Peer State Table

The peer state table encodes the minimum data required for swarm coordination and management. The idea is that each drone within the swarm maintains its own table for each connected peer in memory so that it can be used in future swarming algorithms easily. The below struct is a basic v1 version which is subject to change given different swarming requirements. Each peer state table is around 160 bytes in memory; thus the maximum swarm size is set to 16 currently (~2.5kB in memory).

Peer states are updated as messages come in and are logged using the logging backend, similar to ADS-B. Table snapshots are saved to external storage periodically to protect against reboots.

struct PeerState {
    // Peer identity
    uint8_t  sysid;         // unique ID of original peer
    uint8_t  vehicle_type;  // 0: copter, 1: plane, 2: sub, 3: blimp, 4: rover
    uint8_t  prev_id;       // ID of peer which forwarded message

    // Liveness / Link quality
    uint64_t last_heard;    // system time of last update from this peer for staleness detection (unix)
    uint16_t last_seq;      // for dedup ring buffer
    uint32_t seq_seen_mask; // bitmask of the 32 seq numbers behind last_seq
    uint8_t  rssi;          // signal strength
    uint16_t rx_count;      // received message count
    uint16_t drop_count;    // dropped message count
    bool     freshness;     // true: FRESH, false: STALE

    // Kinematic state
    Vector3f local_pos_NED; // offset from origin [x, y, z] in meters
    Vector3f global_pos;    // GPS [lat (degE7), lon (degE7). alt (mm)]
    float    pos_covariance[9];
    Vector3f attitude;      // [pitch, roll, yaw] in rads
    float    att_covariance[9];

    // Vehicle state
    uint8_t  mode;
    bool     armed_state;   // true: armed, false: disarmed
    bool     landed_state;  // true: landed, false: not landed
    uint8_t  failsafe_flags;
    uint16_t battery_voltage;
    uint8_t  health_flags;

    // Coordination state
    uint8_t  role;
    uint8_t  task_id;
    uint8_t  formation_slot;
    Vector3f target_pos;    // [lat (degE7), lon (degE7). alt (mm)]
    uint8_t  priority;
};

8. SITL and Hardware Demos

I am planning on completing an initial prototype for AP_SwarmMesh within the next couple of weeks. For the midterm evaluation, I would like to have a working demo of AP_SwarmMesh on SITL.

The SITL backend (AP_SwarmMesh_SITL) will replace the physical serial connection with a UDP socket. Each simulated vehicle binds to a known port and sends/receives the same p2p_header_t framed packets in a loop. This means:

  • TX: instead of uart->write(), serialize the header + MAVLink payload into a buffer and sendto() each peer’s UDP port (or a multicast address).
  • RX: recvfrom() in update() feeds bytes into the same parse_byte() state machine. No changes needed to the parsing or forwarding logic.

TTL and forwarding still work as-is, so we can test multi-hop routing by running 3+ SITL instances and blocking direct links by not sending to certain ports.

The key advantage is that the entire framing/routing stack (parse_byte, process_packet, forward_mavlink) is shared and tested in SITL before testing the same code on real hardware.

After SITL testing, I would like to try implementing a hardware demo showcasing a leader-follower formation flight using AP_SwarmMesh as the inter-vehicle communication layer.

A small fleet of ArduCopter drones would be powered on and each vehicle would initialize its peer state table by exchanging MAVLink heartbeat and position messages over the mesh network until every node has a complete view of the swarm. Once the tables are populated, an onboard Lua script reads the local peer state and computes each drone’s target position as a fixed NED offset from the designated leader.

The leader drone would hover in GUIDED mode and would be flown manually via RC_CHANNELS_OVERRIDE. As it moves, AP_SwarmMesh propagates its updated GLOBAL_POSITION_INT to all followers, where the Lua script continuously recomputes the offset target and issues SET_POSITION_TARGET_GLOBAL_INT commands to maintain formation.

The demo would showcase the full library stack: serial framing, header CRC, deduplication, TTL-based multi-hop forwarding, peer state updates, and the onboard logger.

9. Request for Feedback

It would be great to get some feedback from the community! Specifically, I would appreciate any feedback regarding:

  • Architectural issues: state machine logic, peer state table contents, external logging structure, etc.
  • P2P Mesh use-cases: what specific use-case do you have in mind for a P2P network? Are there specific MAVLink messages that you would need? Specific routing / filtering algorithms you would want?

10. GitHub

My working repository is below. Feel free to observe my progress as I go!

Repository: https://github.com/kwakurichter/ardupilot_gsoc/tree/feature/AP_SwarmMesh

I am primarily using the AP_Beacon library as a reference for my library structure:

AP_Beacon: https://github.com/ArduPilot/ardupilot/tree/master/libraries/AP_Beacon

11. References

[1] - https://summerofcode.withgoogle.com/programs/2026/projects/kTHHO1JP

[2] - https://github.com/kwakurichter/ArduSwarm

[3] - https://ardupilot.org/planner/docs/swarming.html

[4] - https://ardupilot.org/mavproxy/docs/modules/swarm.html

[5] - https://github.com/ArduPilot/ardupilot/blob/744d5e1378974a46df7ceffe824f0aa711e8ca91/libraries/AP_Proximity/AP_Proximity_MAV.cpp#L4

[6] - https://github.com/ArduPilot/ardupilot/blob/744d5e1378974a46df7ceffe824f0aa711e8ca91/libraries/AP_GPS/LogStructure.h#L4

[7] - https://ardupilot.org/copter/docs/common-ads-b-receiver.html

4 Likes
  1. I see no hardware specified here, what system will be used and how does it handles collisions and what it is throughput
  2. 2 serial ports to FC shouldn’t be necessary.
  3. Duplicating sysids and other MAVLink message fields wastes airtime and IMHO is pointless
  4. storing both local and global coordinates in peer state is wasteful
  5. IMHO velocity vector is much more useful than attitude for coordination, maybe velocity and acceleration, additionally having both actual target and actual velocity and acceleration would allow feed forward in position control.
  6. storing other node vehicle internal state seems wasteful,
  7. Coordination state requires explaination of its purpose and seems application specific.
  8. Required message rates fall off with distance (which can be approximated by hop count) so how will seq_seen_mask work if vehicle reports its position at 10Hz with ttl=0 and at 1Hz with ttl=5 (example)
  9. If the system needs common time and (deadline_ms) suggests that it does there must be provision for automatic clock synchronization.
  10. IMHO mesh management should be handled by the mesh radio and it probably should be done by a separate component comparable to ESP32 based DroneBridge, AFAIK ELRS team found incorporating radio code into FC and managing versions to be problematic wrt user experience.
  11. How will assymetric reception be handled by this system ie. node A hears node B but node B can’t hear node A due to eg. background noise.
  12. Limiting swarms to a dozen or so vehicles is constraining even compared to paltry 255 vehicles supported by MAVLink.
  13. IMHO routing data and neighbour data should be handled separately as you should be able to route to any peer but only close neighbours are relevant for navigation (assuming boid type swarming)
  14. Logging a lot of information about neighbours seems pointless. IMHO it would be much better to develop multi log capable log viewer eg as an option in the Ardupilot log viewer. (plot.ardupilot.org)

Congratulations on taking a very ambitious project and good luck.

Hi, thanks for the detailed feedback!

  1. I am considering two distinct reference hardware backends. The first being the “Full” backend which would use an RPi with a Wio-WM6108 pcie board which would provide full 802.11s support and would provide upwards of ~15mbps bandwidth. The second being the “Lite” backend which would attempt to target “generic” hardware (ie. ESP32) at a raw 1mbps. This would require some heavy work into writing embedded firmware for the ESP and would need to implement its own routing algorithm (unlike the Full backend). For CA, I eventually envision implementing something like using voronoi cells to bound trajectory for drones within their local neighbourhood, but this is very much a stretch goal.
  2. Yes I should clarify, AP_SwarmMesh will only use one serial port. The other is used by the standard telemetry link.
  3. This is still a question for me as well. The idea here is to eliminate the dependance on any MAVLink fields so the new protocol can be easily extended for other message types (ie. ACK, SYNC, etc.) in the future. Also, both the external radio and the FC don’t need to parse the wrapped MAVLink packet for routing this way. I am still torn as to what is the best compromise here.
  4. This is a good point the idea was that local coords would only be stored if global were not available. However maybe I can reuse the same field for both to save 12 bytes per peer?
  5. Yes this makes sense. Perhaps I do pos, vel, accel 3 DoF, reusing the same fields for global and local depending on if the peer has a global stream or not. Do you mean having target vel and accel as well as target pos is useful?
  6. My thinking was some vehicle state data could be useful for some safe coordination functions and health monitoring of local neighbours. Do you have any thoughts on a more succinct way of monitoring health/safety? Also worth noting that these streams would not be enabled by default and would be up to the user to determine what data they deem necessary by setting a bitmask.
  7. Yes I completely agree, I am not yet sure what the best way to generalize this data is. As you mention, it is application specific. Do you have any ideas? Or maybe I can omit the coord state entirely and leave that up to a downstream coordination algorithm to handle?
  8. The idea with seq_seen_mask is to add some basic duplicate rejection in the edge case that two of the same packet arrive from different peers at around the same time but last_seq != seq due to another packet arriving in-between. seq_seen_mask is a sliding window bit mask which allows you to check up to last_seq - 32 for these duplicates. Duplicates should in theory be caught by the routing protocol on the external radio, but I thought I’d add this check on the FC as well.
  9. Yes absolutely the staleness check will only apply if the drone has a time sync source. If not deadline_ms is ignored and staleness check falls back to TTL only.
  10. I completely agree, my plan was to leave all routing and radio management to the external radio (either the “Full” or “Lite” backend). The idea is that the peer header is visible to the FC, but its fields are primarily intended to be used by the external radio for its routing algorithm.
  11. Currently the first implementation is not “synchronized” and does not handle asymmetric reception, however this is where the modularity of the peer header (as mentioned in #3) would come into play. A stretch goal could be to implement ACKs on certain critical data without needing a new header and infrastructure.
  12. Yes I agree, I have been debating how to balance sufficient data for coordination with sufficient room for scalability. Currently my code initializes a state table in memory based on a set max peers which limits total ram usage to around 2.5 kb. Perhaps a better approach would be a dynamic max peer allocation depending on which FC you are using (ie. lower end FCs would be limited in scale, while FCs such as an H7 could support much larger swarms)? I would also think that in many decentralized swarming applications a massive peer table in memory wouldn’t be strictly necessary. In many use cases you probably only need to maintain memory of a relatively small amount of local neighbours. However, I think you are right in that this determination should be up to the user and their specific hardware config.
  13. Yes I agree, my intention is that routing will be handled by the external radio and peer state data will likely be limited to local neighbours as mentioned above.
  14. Yes I think the majority of the data shared between peers here would not be logged. My intention was to provide the capability to log most peer data if the user should choose to. A logging bitmask would be provided similar to standard telemetry streams.

Personally I would keep mesh and swarming systems separate. IMHO mesh radio should only care about managing routing and should do that independently of the FC. In fact I remember seeing somebody post a work in progress mesh radio project based on ESP NOW a few months ago.

I think emergencies should be handled by managing “pathfinding priority” so drone in an emergency state would have other drones move out of the way though that requires the malfunctioning drone to know where it is.

1 Like

Hi @LupusTheCanine, thanks for taking the time to review and leave your thoughts.

The intent of this project, and really the main challenge, is to demonstrate a decentralized coordination capability that is independent of the specific radio hardware being used. The only hard assumption is that there is a board running ArduPilot and some transport capable of moving peer packets between vehicles. I agree that purpose-built mesh radios or something like DroneBridge is the better answer for many use cases… but if the project depends on a specific mesh radio stack from the beginning, then we have mostly demonstrated that ArduPilot can do swarm coordination with that stack. What I am trying to define here through this GSoC project that Kwaku has taken on is a minimal, native ArduPilot abstraction that for things like peer identity, peer state, forwarding rules, duplicate rejection, etc, that smarter transports could later implement more efficiently.

The motivation here is that ArduPilot has a mature onboard stack for single-vehicle autonomy, but comparatively little native infrastructure for onboard multi-vehicle coordination. Our coordination is either centralized through a GCS, so is usually brittle, or some mission specific hard coded vehicle-to-vehicle communication scheme. I’d like to get things to a point where the vehicles can exchange low frequency information and keep some reference of each other while in operation so that developers can build new coordinated autonomous systems with such a capability in mind.

1 Like

Swarming and MANET (Mobile adhoc network) are two independent problems, except for driving some requirements in the other system. I think we should keep them separate so the next person wanting to do swarm or MANET in another way doesn’t have to carry the burden of having to deal with intermingled systems.

Unless you want to deal with more than 254 drones (+1 GCS) hearing each other (which IMHO is quite a bit harder as it would require working on a lot of components) I would consider defining your OTA as “MAVLink with alternate header” and translating all or selected incoming packets to regular MAVLink in the mesh driver, packing entire mavlink messages makes no sense, and if you want to do more than 255 entities in the meh you will need address translation anyways and still probably should just replace the header and reconstruct it in the driver (with translated sysids).

This way swarm driver can grab relevant information from the MAVLink stream independently of the network subsystem. Analogously it can send messages to other vehicles. You will probably need to define custom MAVLink dialect that later (with some operational experience) may become basis for “official” mesh and swarming frames.

Few more remarks for the swarm node status:

  1. global position should use int32_t, floats are lossy for storing lat/lon, using int32_t with 1mm resolution for altitude would be fine too
    2.covariances could probably be stred in more compact form if we assume drone must have reasonably good position estimate to work in the swarm
  2. velocity and acceleration I proposed storing can be stored in int16_t with quite resonable accuracy of 1cm/s(^2) with range of ±327m/s(^2) absolutely fine for acceleration and I do not expect high subsonic swarm :sweat_smile:
1 Like

Midterm Update: AP_SwarmMesh C++ Library Running SITL Experiments at Scale

Hi ArduPilot community! As the GSoC midterm passes, I would like to share an update on AP_SwarmMesh and the SITL experiments I have been using to validate it.

A working prototype of the library is pushed. Several SITL scripts have been tested, including a 254 drone leader-follower experiment demonstrating the library at scale.

1. AP_SwarmMesh C++ Library

As promised in the initial blog post, I have completed a working prototype of AP_SwarmMesh. The current version demonstrates the full send/receive path at scale through a leader-follower experiment running in SITL. AP_SwarmMesh provides a scalable communication backend inside ArduPilot for decentralized peer-to-peer swarm coordination.

2. Changes and Improvements

Since the initial proposal, there have been several important changes driven by community feedback and bugs found during testing.

2.1 Velocity and Acceleration Feed Forward

In the original concept, the peer state stored kinetic state (global or local position and attitude), but no dynamic state for better formation control. Community feedback pointed out that future swarm coordination would benefit from velocity and acceleration information. At the same time, memory use needed to stay minimal because many flight controllers are constrained by RAM.

I updated the peer state to add actual and target velocity/acceleration while removing the covariance arrays and replacing them with normalized EKF variance values. That saves 36 bytes per peer, or up to around ~9 kB at the full SITL size. In the leader-follower SITL experiments, velocity feed forward significantly improved formation responsiveness.

struct PeerState {

        ...
        // Kinematic state
        Vector3f local_pos_NED;
        Vector3l global_pos;         // GPS \[lat (degE7), lon (degE7), alt (mm)\]
        int16_t  velocity\[3\];      // actual velocity \[x, y, z\] NED, cm/s. From LOCAL_POSITION_NED or GLOBAL_POSITION_INT; global overrides local when both arrive in the same TX cycle
        int16_t  accel\[3\];         // actual acceleration \[x, y, z\] body frame, mG, EKF bias removed. From SCALED_IMU
        float    pos_horiz_variance; // EKF horizontal position variance, from EKF_STATUS_REPORT
        float    pos_vert_variance;  // EKF vertical position variance, from EKF_STATUS_REPORT
        float    vel_variance;       // EKF velocity variance, from EKF_STATUS_REPORT
        Vector3f attitude;
        ...


        // Coordination state
        uint8_t  role;
        uint8_t  task_id;
        uint8_t  formation_slot;
        Vector3l target_pos;           // \[lat (degE7), lon (degE7), alt (mm)\]
        int16_t  target_velocity\[3\]; // commanded velocity \[x, y, z\] NED, cm/s, from POSITION_TARGET_GLOBAL_INT
        int16_t  target_accel\[3\];    // commanded acceleration \[x, y, z\] NED, cm/s/s, from POSITION_TARGET_GLOBAL_INT
        uint8_t  priority;
};

2.2 Dynamic System Management

Another community feedback item was the need for better built-in scaling. The initial concept assumed each drone would maintain only a small peer state table, generally only containing nearby neighbours. This kept memory use low and made peer context easier to manage. Each peer table entry uses around 160 bytes, and the first design capped the table at 16 entries (around ~2.5 kB total), which is reasonable even for small flight controllers such as STM32F4 boards.

The current design instead scales the maximum peer table size based on available system RAM. For example, SITL builds can use the full 254 peer range, while low memory builds use smaller limits.

It is also important to manage receive path workload as the swarm grows. The inbound parser can only consume a bounded number of bytes per update cycle, and real hardware radios will also impose their own bandwidth limits. Instead of using one universal parser budget, the receive budget now scales by board class. This protects the flight controller from unbounded receive load. Note that that users will still need to tune stream rates for their hardware and specific application to avoid dropped messages.

2.3 Bugs and Lessons Uncovered at Scale

Pushing the library to a full 254 node swarm in SITL revealed some bugs that were invisible at small scale but would affect real hardware.

The first was the freshness budget (deadline_ms). During the initial scale runs, followers split into two groups: roughly half tracked the leader correctly while the other half remained still. Packets were being rejected as stale whenever the receiver’s clock read slightly ahead of the sender’s origin_time_us, combined with a default deadline_ms of zero. Since each vehicle boots with a slightly different clock, a node whose clock lagged a peer’s clock could silently discard everything from that peer. The fix was simply to set deadline_ms == 0 as “no freshness budget” and skip the staleness check entirely.

The second was a decoupling between a peer’s freshness flag and its position value. freshness was originally updated whenever a drone received any packet from a peer, while the stored position only updates on GLOBAL_POSITION_INT. Under heavy mesh load, the heartbeat can keep a peer flagged as fresh while its actual position value is stale by tens of seconds. In the formation experiment this showed up as followers freezing in place while the leader flew away, then snapping to catch up.

This was because followers were each broadcasting their own position at 2 Hz even though follower positions weren’t necessary for this test. Silencing the follower position stream (they still emit heartbeats, so tables still populate) immediately restored tracking.

Fixes/lessons from this:

  1. Freshness should be tracked per message class, so a consumer can distinguish a fresh flag from a fresh state.
  2. A node must only send the streams the swarm actually needs.

3. SITL Experiments

3.1 Experiment Design and Scripts

To show the full pipeline, I built a leader-follower formation experiment that runs entirely in SITL, with all inter-vehicle coordination hapenning over the AP_SwarmMesh mesh rather than through a GCS.

First, I added a Lua scripting binding (swarm:) so onboard scripts can read the peer state table directly: swarm:count(), swarm:get_peer_location(sysid), and swarm:get_peer_velocity_NED(sysid).

The binding works with swarm_follower.lua, the onboard follower logic. Each follower reads the designated leader’s position from its own local peer table, applies a fixed North/East offset (its formation slot), and commands the result as a GUIDED target using the leader’s velocity as feed forward. Because the target is derived purely from mesh peer state, velocity feed forward eliminates most of the lag. In early 4 drone runs, steady-state lag against a moving leader dropped from about 1.4 m with position-only control to about 0.17 m with velocity feed forward added.

swarm_formation_scale_test.py launches up to 254 ArduCopter SITL instances (staggered to avoid a boot time CPU spike), brings them up concurrently, arranges the followers in a sunflower pattern around the leader for even spacing, and then flies the leader through a box trajectory in GUIDED. As the leader moves, its GLOBAL_POSITION_INT propagates over the mesh and every follower recomputes and commands its slot offset.

3.2 Results

The main result is that a swarm at the full sysid range holds sub-meter formation (ran on a 48 GB M4 Pro MacBook Pro):

Table 2 - Leader-follower formation accuracy in SITL (leader flying a box trajectory)

Followers Armed & airborne Tracking moving leader Median formation error
40 100% 100% 0.36 m
253 253 / 254 236 / 252 (94%) 0.87 m (p90 5.3 m)

Getting almost the full 254 node run to hold this tightly came down to the freshness/position lesson from section 2.3. Before silencing the follower position streams, the same 254 node run tracked the leader at a median error of about 19 m. Afterwards it dropped to 0.87 m simply by not flooding the mesh with unused data.

It is also worth noting what does not scale well. AP_SwarmMesh’s peer table and routing layer scale cleanly to the full 253 peers, which I verified by injecting synthetic peers over UDP. That test shows the same process_packet() path without needing to generate hundreds of vehicles. The practical ceiling in the flying experiment is thus most likely the simulation hardware as expected, not the mesh itself.

To view interactive replay of the experiments, download the following and open the html files on any web browser: swarm_formation_runs.zip (493.5 KB)

Figure 5 - Replay of the 40 follower leader-follower run. The leader is shown in amber, followers are shown as the surrounding swarm, and the right panel shows formation error over time.

Figure 6 - Replay of the full 253 follower leader-follower run. The leader is shown in amber, followers are shown as the surrounding swarm, and the right panel shows formation error over time. Notice some followers lose tracking (around ~6%) due to SITL hardware limitations.

3.3 Running the Experiment

To run the scale experiment:

# Full 254-node run (~35-40 min). --sr-pos 0 is the default and is essential so followers stay silent so the leader's position stays fresh over the mesh.
python3 libraries/AP_SwarmMesh/tools/swarm_formation_scale_test.py \
    --followers 253 --spacing 4 --alt 15 --speedup 1 \
    --leader-sr 8 --leader-path box --leader-speed 2 \
    --move-time 120 --converge-wait 120 \
    --work-dir ~/swarm_run --csv ~/swarm_run/track.csv

# Then render the animated replay:
python3 libraries/AP_SwarmMesh/tools/swarm_formation_viz.py \
    ~/swarm_run/track.csv -o ~/swarm_run/formation.html

Note a lighter test run is --followers 40 --move-time 60 (about 8 minutes). Full details are in README.md.

4. Planned Changes and Improvements

Along with continuing to fix issues found in simulation and eventually on hardware, there are a few planned improvements that have not yet been implemented.

4.1 Ack/Nack Synchronization

Currently, there is no mechanism for checking whether critical messages sent by one drone are received and saved by their target. I plan to add ACK/NACK packets for this. If a message type is flagged as critical, the receiver can send an acknowledgement packet back to the sender so lost or dropped packets can be monitored by the swarm.

4.2 AP_LocationDB Integration

Community feedback has expressed interest in integrating AP_SwarmMesh with AP_LocationDB, which is intended to serve as a central repository for kinematic and dynamic state information. AP_LocationDB can be used downstream for tasks such as following vehicles, tracking vehicles, avoidance, and formation control. The idea is to use the peer state context infrastructure built in AP_SwarmMesh to populate AP_LocationDB with peer locations.

5. Next Steps

  • Implement the planned improvements from section 4.
  • Continue hardware testing and fixing bugs.
  • Add SITL test cases and automated checks.
  • Write setup and usage documentation.
  • Prepare the final demo video and PR to ArduPilot.

Thanks to all who provided feedback previously, please feel free to provide additional feedback as I continue my work!

1 Like