YariTech Icon yaritech labs
home / blog / paper
featured // paper // 2026.09.24 // 12 min read

Decentralized flocking algorithms for ESP32 mesh nodes

Analysis of emergent swarm behavior over a low-power 868MHz peer-to-peer radio topology — building Boids-inspired coordination without any central orchestrator.

YT

YariTech Engineering

hello@yaritech.in

esp32 mesh networking swarm robotics 868MHz embedded C
n.01 n.02 n.03 n.04 n.05 n.06 n.07 n.08 n.09 n.10 active node idle node

fig.01 — 868MHz mesh topology: active flocking nodes shown in cyan-blue gradient, idle nodes in grey

Abstract

Emergent collective behavior in multi-agent systems has long been studied in simulation; translating those properties onto real constrained hardware is a different problem entirely. This paper documents our implementation of a Boids-derived flocking algorithm running on a ten-node ESP32 mesh, communicating over 868MHz LoRa radios with no central coordinator — and no cloud infrastructure of any kind.

Each node makes autonomous steering decisions using only locally received neighbor state packets. The result is stable cohesion, separation, and alignment across the full swarm at a round-trip latency well below 80ms, consuming under 18mA average per node in active flight mode.

key result

Ten-node mesh with full Boids dynamics: stable swarm cohesion achieved at median inter-node latency of 61ms, drawing 17.4mA average per node on a 3.7V LiPo.

61ms
median mesh RTT
17.4mA
avg current / node
10 nodes
swarm size tested

Background — Boids in brief

Craig Reynolds' 1987 Boids model distills flocking into three steering rules applied per agent using only local neighbor information:

  • Separation — steer away from neighbors that are too close
  • Alignment — steer toward the average heading of nearby neighbors
  • Cohesion — steer toward the average position of nearby neighbors

Each rule produces a force vector; a weighted sum gives the final steering output. In simulation this runs trivially — every agent has instantaneous access to all neighbor positions. On a real radio mesh, "instantaneous" is fiction. Neighbor state is stale by the time it arrives, link quality varies, and nodes drop packets. Our primary challenge was quantifying how much latency and packet loss the algorithm tolerates before the swarm stops cohering.

Hardware platform

Compute

ESP32-S3 (dual Xtensa LX7, 240MHz) running bare-metal FreeRTOS. No operating system beyond the RTOS task scheduler. The Boids compute task runs at 20Hz on core 1; the radio driver runs on core 0 to avoid contention.

Radio

EBYTE E22-900M22S LoRa module (SX1262 chipset) at 868MHz, SF7, BW 250kHz, CR 4/5 — chosen for the sub-50ms air-time on our 32-byte state packets while staying within the 1% duty-cycle limit mandated under ETSI EN 300 220 for licence-exempt operation in India.

State packet format

typedef struct __attribute__((packed)) {
  uint8_t  node_id;      // 1 byte  — sender identity
  int16_t  pos_x;        // 2 bytes — position X  (cm, fixed-point)
  int16_t  pos_y;        // 2 bytes — position Y
  int16_t  pos_z;        // 2 bytes — altitude Z
  int16_t  vel_x;        // 2 bytes — velocity X  (cm/s)
  int16_t  vel_y;        // 2 bytes — velocity Y
  int16_t  vel_z;        // 2 bytes — velocity Z
  uint8_t  seq;          // 1 byte  — rolling sequence number
  uint8_t  rssi_last;    // 1 byte  — RSSI of last received packet (dBm + 200)
  uint16_t crc;          // 2 bytes — CRC-16/CCITT
} NodeState_t;            // total: 18 bytes

Each node broadcasts its own NodeState_t at 10Hz. Receivers maintain a neighbor table keyed by node_id, expiring entries after 500ms of silence. The Boids kernel reads this table directly — no copying, no locks (the table entries are 32-bit aligned and writes are atomic on LX7).

The flocking kernel

Below is the core steering function. All arithmetic is integer fixed-point to avoid FPU context switches in the RTOS scheduler:

/* weights — tuned empirically over 40 test flights */
#define W_SEP  180   /* separation weight  × 1000 */
#define W_ALG  80    /* alignment weight   × 1000 */
#define W_COH  60    /* cohesion weight    × 1000 */
#define R_SEP  120   /* separation radius, cm */
#define R_NEIGH 400  /* neighborhood radius, cm */

Vec3i boids_steer(NodeState_t *self, NeighborTable_t *tbl) {
    Vec3i sep = {0,0,0}, alg = {0,0,0}, coh = {0,0,0};
    int n_sep = 0, n_neigh = 0;

    for (int i = 0; i < NEIGHBOR_MAX; i++) {
        if (!tbl->valid[i]) continue;
        NodeState_t *nb = &tbl->entry[i];

        int dx = nb->pos_x - self->pos_x;
        int dy = nb->pos_y - self->pos_y;
        int dz = nb->pos_z - self->pos_z;
        int d2 = dx*dx + dy*dy + dz*dz;   /* squared dist, cm² */

        if (d2 < R_SEP * R_SEP) {
            sep.x -= dx; sep.y -= dy; sep.z -= dz;
            n_sep++;
        }
        if (d2 < R_NEIGH * R_NEIGH) {
            alg.x += nb->vel_x; alg.y += nb->vel_y; alg.z += nb->vel_z;
            coh.x += nb->pos_x; coh.y += nb->pos_y; coh.z += nb->pos_z;
            n_neigh++;
        }
    }

    if (n_neigh > 0) {
        coh.x = coh.x / n_neigh - self->pos_x;
        coh.y = coh.y / n_neigh - self->pos_y;
        coh.z = coh.z / n_neigh - self->pos_z;
        alg.x /= n_neigh; alg.y /= n_neigh; alg.z /= n_neigh;
    }

    return (Vec3i){
        (sep.x*W_SEP + alg.x*W_ALG + coh.x*W_COH) / 1000,
        (sep.y*W_SEP + alg.y*W_ALG + coh.y*W_COH) / 1000,
        (sep.z*W_SEP + alg.z*W_ALG + coh.z*W_COH) / 1000,
    };
}

Latency tolerance analysis

We deliberately injected artificial packet delay and loss to find the degradation thresholds. The swarm was judged "coherent" if the mean inter-agent distance remained within 2× the cohesion radius for over 60 consecutive seconds.

table 01 — latency vs. cohesion stability
injected delay packet loss cohesion notes
0ms 0% stable baseline
50ms 0% stable no observable degradation
120ms 0% stable minor oscillation in Z axis
200ms 0% marginal visible clustering drift
80ms 15% stable seq-number gap handling effective
80ms 30% marginal swarm splits into 2 sub-clusters
80ms 50% fails neighbor tables expire, swarm dissolves

"The 200ms latency threshold was surprising — we expected failure closer to 100ms. The separation rule is the stabilizing force; it reacts to stale position data far more gracefully than alignment or cohesion."

Power budget

Running on a 500mAh single-cell LiPo, the swarm node lasts approximately 28 hours in standby mesh mode or 8.4 hours in active flight mode (factoring in the flight controller and motors drawing ~180mA additional).

table 02 — per-node power breakdown
subsystem mode current (mA)
ESP32-S3 dual-core active ~80
SX1262 radio TX (22dBm) ~120
SX1262 radio RX continuous ~4.2
IMU + baro active ~3.5
misc regulators quiescent ~1.2
total (avg, 10% TX duty) mesh active ~17.4

Conclusions & next steps

A fully decentralized Boids swarm running on commodity ESP32 hardware over 868MHz LoRa is feasible today — and surprisingly robust. The algorithm tolerates up to 120ms of network latency and 15% packet loss without visible degradation, which comfortably covers real-world indoor RF environments.

Open research questions we plan to pursue:

  • Obstacle avoidance integration using onboard optical flow — no centralized map
  • Swarm size scaling beyond 10 nodes: does the neighbor table O(n) approach hold at 30 nodes?
  • Sub-GHz channel congestion under dense deployments (>5 swarms in proximity)
  • Energy-harvesting sleep cycles — waking only on detected neighbor radio activity
open-source

Firmware, PCB design files, and test data are available on request. Email hello@yaritech.in with subject line swarm-fw-request.