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.
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.
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.
| 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).
| 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
Firmware, PCB design files, and test data are available on request. Email hello@yaritech.in with subject line swarm-fw-request.