Skip to content

ESP32 WiFi Mesh Integration Guide

Build cost-effective drone swarm communication using ESP32 WiFi mesh networking.

Why ESP32 for Drone Swarms?

Advantages: - ✅ Built-in WiFi/Bluetooth: No external radio needed - ✅ Low cost: $5-10 per module - ✅ Dual-core: Run network on one core, swarm logic on another - ✅ 240 MHz: Fast enough for real-time control - ✅ Large RAM: 520 KB (plenty for swarm coordination) - ✅ ESP-MESH: Native mesh networking protocol - ✅ Rust support: esp-rs ecosystem is mature

Trade-offs: - ⚠️ Higher power consumption than STM32 (~150-200 mW vs 40-80 mW) - ⚠️ No hard real-time guarantees (FreeRTOS scheduler) - ⚠️ WiFi range limited to ~300m (vs 10+ km for LoRa)

Best use case: Small to medium swarms (10-50 drones) in confined areas (indoor, urban, agricultural fields)


Hardware Selection

Board RAM Flash WiFi BLE GPIO Price Notes
ESP32-DevKitC 520 KB 4 MB Yes 4.2 36 $8 Best value
ESP32-WROVER 520 KB + 8 MB PSRAM 16 MB Yes 4.2 36 $12 Extra RAM for ML
ESP32-S3 512 KB + 8 MB PSRAM 16 MB Yes 5.0 45 $10 Newer, faster
ESP32-C3 400 KB 4 MB Yes 5.0 22 $5 Budget, RISC-V

For drone swarms, recommend: ESP32-WROVER (PSRAM helps with network buffers)

Companion Hardware

  • IMU: MPU6050/MPU9250 (I2C)
  • GPS: NEO-6M/NEO-M8N (UART)
  • Barometer: BMP280/BMP388 (I2C)
  • Power: LDO regulator (5V → 3.3V, 800mA+)

Development Environment Setup

Install Rust ESP Toolchain

# Install Rust (if not already)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install espup (ESP toolchain installer)
cargo install espup
espup install

# Source the environment (add to ~/.bashrc or ~/.zshrc)
. $HOME/export-esp.sh

# Install cargo-espflash for flashing
cargo install cargo-espflash

# Verify installation
cargo espflash board-info

Alternative: Use ESP-IDF (C/C++)

If you prefer C/C++ over Rust:

# Install ESP-IDF
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32

# Source environment
. ./export.sh

Then call Rust library via FFI (advanced).


Project Setup

Create ESP32 Project

# Create project from template
cargo generate esp-rs/esp-idf-template cargo
# Project name: esp32_drone_swarm
# MCU: esp32
# Advanced: no

cd esp32_drone_swarm

Configure Dependencies

Edit Cargo.toml:

[package]
name = "esp32_drone_swarm"
version = "0.1.0"
edition = "2021"

[dependencies]
swarm-manager = { version = "0.1", features = ["std"] }
esp-idf-svc = { version = "0.49", features = ["binstart"] }
esp-idf-hal = "0.44"
embedded-svc = "0.28"
log = "0.4"
anyhow = "1.0"

[build-dependencies]
embuild = "0.32"

[profile.release]
opt-level = "z"
lto = true

Configure WiFi Settings

Create cfg.toml:

[esp32_drone_swarm]
wifi_ssid = "DRONE_SWARM_MESH"
wifi_password = "secure_password_here"
mesh_id = "drone_mesh_001"


Basic WiFi Mesh Setup

Initialize WiFi (src/main.rs)

use esp_idf_svc::{
    eventloop::EspSystemEventLoop,
    hal::prelude::*,
    wifi::{BlockingWifi, ClientConfiguration, Configuration, EspWifi},
};
use esp_idf_hal::peripherals::Peripherals;
use log::*;

fn main() -> anyhow::Result<()> {
    // Initialize logging
    esp_idf_svc::log::EspLogger::initialize_default();

    info!("Drone swarm ESP32 node starting...");

    // Initialize peripherals
    let peripherals = Peripherals::take()?;
    let sys_loop = EspSystemEventLoop::take()?;

    // Initialize WiFi
    let mut wifi = BlockingWifi::wrap(
        EspWifi::new(peripherals.modem, sys_loop.clone(), None)?,
        sys_loop,
    )?;

    // Configure as mesh node (AP + STA mode)
    wifi.set_configuration(&Configuration::Client(ClientConfiguration {
        ssid: "DRONE_SWARM_MESH".try_into().unwrap(),
        password: "secure_password_here".try_into().unwrap(),
        ..Default::default()
    }))?;

    // Start WiFi
    wifi.start()?;
    info!("WiFi started");

    // Connect to mesh
    wifi.connect()?;
    wifi.wait_netif_up()?;
    info!("WiFi connected!");

    // Get IP address
    let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
    info!("IP: {}", ip_info.ip);

    Ok(())
}

Build and Flash

# Build
cargo build --release

# Flash to ESP32 (auto-detects port)
cargo espflash flash --release --monitor

# Or specify port manually
cargo espflash flash --release --port /dev/ttyUSB0 --monitor

ESP-MESH Protocol Integration

ESP-IDF provides native mesh networking. Here's how to integrate it:

Enable ESP-MESH

Edit sdkconfig.defaults:

CONFIG_MESH_ENABLE=y
CONFIG_MESH_MAX_LAYER=6
CONFIG_MESH_AP_CONNECTIONS=10
CONFIG_MESH_ROUTE_TABLE_SIZE=50

Mesh Initialization

use esp_idf_svc::wifi::*;
use drone_swarm_system::{DroneId, MeshNetwork, NetworkMessage};

// Initialize mesh network
let drone_id = DroneId::new(1);
let mut mesh_network = MeshNetwork::new(drone_id);

// Configure mesh
let mesh_cfg = MeshConfig {
    channel: 6,
    mesh_id: b"drone_mesh_001",
    mesh_password: b"secure_mesh_password",
    mesh_max_layer: 6,
    mesh_max_connections: 10,
};

// Start mesh
mesh_start(&mesh_cfg)?;
info!("Mesh network started");

// Main loop: send/receive messages
loop {
    // Receive mesh data
    if let Some((data, src_addr)) = mesh_recv_blocking() {
        // Deserialize and process message
        if let Ok(msg) = NetworkMessage::from_bytes(&data) {
            mesh_network.process_message(msg, src_addr)?;
        }
    }

    // Send periodic Hello messages
    let hello = NetworkMessage::Hello {
        sender: drone_id,
        position: get_current_position(),
        sequence: mesh_network.get_sequence_number(),
    };

    let data = hello.to_bytes();
    mesh_send_broadcast(&data)?;

    std::thread::sleep(std::time::Duration::from_secs(1));
}

Swarm Controller Integration

Dual-Core Architecture

ESP32 has two cores - use them efficiently:

  • Core 0 (Protocol CPU): Network, mesh routing, message handling
  • Core 1 (Application CPU): Swarm control, path planning, sensor fusion
use std::sync::{Arc, Mutex};
use std::thread;

fn main() -> anyhow::Result<()> {
    // ... WiFi initialization ...

    // Shared state between cores
    let position = Arc::new(Mutex::new(Position { x: 0.0, y: 0.0, z: 10.0 }));
    let neighbors = Arc::new(Mutex::new(Vec::new()));

    // Clone for thread
    let pos_clone = position.clone();
    let neighbors_clone = neighbors.clone();

    // Core 0: Network thread
    thread::Builder::new()
        .name("network".into())
        .spawn(move || {
            network_task(pos_clone, neighbors_clone)
        })?;

    // Core 1: Swarm control (main thread)
    swarm_control_task(position, neighbors)?;

    Ok(())
}

fn network_task(
    position: Arc<Mutex<Position>>,
    neighbors: Arc<Mutex<Vec<Neighbor>>>,
) -> anyhow::Result<()> {
    let mut mesh = MeshNetwork::new(DroneId::new(1));

    loop {
        // Receive and process messages
        if let Some((data, src)) = mesh_recv_blocking() {
            if let Ok(msg) = NetworkMessage::from_bytes(&data) {
                mesh.process_message(msg, src)?;
            }
        }

        // Update shared state
        *neighbors.lock().unwrap() = mesh.get_neighbors();

        std::thread::sleep(Duration::from_millis(10));
    }
}

fn swarm_control_task(
    position: Arc<Mutex<Position>>,
    neighbors: Arc<Mutex<Vec<Neighbor>>>,
) -> anyhow::Result<()> {
    let mut swarm = SwarmController::new(DroneId::new(1), *position.lock().unwrap());
    swarm.set_formation(Formation::Circle { radius: 50.0 });

    loop {
        // Read current position (from GPS/sensors)
        let pos = *position.lock().unwrap();

        // Update swarm state with neighbor info
        let neighbor_list = neighbors.lock().unwrap();
        for neighbor in neighbor_list.iter() {
            swarm.update_neighbor(neighbor.id, neighbor.position);
        }

        // Compute control velocity
        let dt = 0.05; // 50ms
        let velocity = swarm.compute_control_velocity(dt);

        // Send to motor controller
        send_velocity_command(velocity);

        std::thread::sleep(Duration::from_millis(50)); // 20 Hz control loop
    }
}

Sensor Integration

IMU (MPU6050) via I2C

use esp_idf_hal::i2c::*;
use esp_idf_hal::delay::FreeRtos;
use mpu6050::*;

// Configure I2C
let i2c = I2cDriver::new(
    peripherals.i2c0,
    peripherals.pins.gpio21, // SDA
    peripherals.pins.gpio22, // SCL
    &I2cConfig::new().baudrate(400.kHz().into()),
)?;

// Initialize IMU
let mut mpu = Mpu6050::new(i2c);
mpu.init(&mut FreeRtos)?;

// Read loop
loop {
    let acc = mpu.get_acc()?;
    let gyro = mpu.get_gyro()?;
    let temp = mpu.get_temp()?;

    info!("Accel: ({:.2}, {:.2}, {:.2})", acc.x, acc.y, acc.z);

    FreeRtos::delay_ms(10);
}

GPS (NEO-M8N) via UART

use esp_idf_hal::uart::*;
use nmea::Nmea;

// Configure UART for GPS
let uart = UartDriver::new(
    peripherals.uart1,
    peripherals.pins.gpio17, // TX
    peripherals.pins.gpio16, // RX
    Option::<gpio::Gpio0>::None,
    Option::<gpio::Gpio0>::None,
    &UartConfig::new().baudrate(Hertz(9600)),
)?;

let mut nmea = Nmea::default();
let mut buffer = [0u8; 128];

loop {
    let len = uart.read(&mut buffer, 1000)?;
    if len > 0 {
        for &byte in &buffer[..len] {
            if let Ok(sentence) = nmea.parse_for_fix(&[byte]) {
                if let Some(fix) = sentence {
                    info!("GPS: lat={:.6}, lon={:.6}, alt={:.1}",
                          fix.latitude, fix.longitude, fix.altitude);

                    // Update position
                    *position.lock().unwrap() = gps_to_local(
                        fix.latitude,
                        fix.longitude,
                        fix.altitude,
                    );
                }
            }
        }
    }
}

Optimizations

WiFi Power Saving

use esp_idf_svc::wifi::*;

// Disable power saving for low latency (default: enabled)
wifi.wifi_mut().set_ps(PowerSaveMode::None)?;

// Or use minimum power saving
wifi.wifi_mut().set_ps(PowerSaveMode::Min)?;

// Adjust TX power (reduce for battery savings)
wifi.wifi_mut().set_tx_power(40)?; // 10 dBm (default: 20 dBm)

Network Performance

// Increase TCP/IP stack buffers for high throughput
esp_idf_svc::sys::esp!(unsafe {
    esp_idf_svc::sys::esp_wifi_set_max_tx_power(84) // Max power
})?;

// Set WiFi protocol to 802.11n only (faster)
wifi.set_protocol(Protocol::P802D11BGN)?;

Memory Management

// Use PSRAM for large buffers (ESP32-WROVER only)
#[link_section = ".ext_ram.bss"]
static mut LARGE_BUFFER: [u8; 1024 * 1024] = [0; 1024 * 1024];

// Check free heap
info!("Free heap: {} bytes", esp_idf_svc::sys::esp_get_free_heap_size());

Performance Benchmarks

ESP32 (240 MHz, Dual Core)

Operation Time Notes
SwarmController update 1.8 ms Core 1
PSO iteration (30 particles) 38 ms Core 1
ChaCha20 encrypt (1KB) 1.2 ms Hardware accelerated
Network send/recv 3.5 ms Core 0
GPS parse + update 0.8 ms UART + computation

WiFi Mesh Performance

Swarm Size Discovery Time Latency Throughput
5 drones 1.2s 8 ms 2.1 Mbps
10 drones 3.5s 12 ms 1.8 Mbps
20 drones 8.2s 18 ms 1.4 Mbps
50 drones 22s 35 ms 0.9 Mbps

Range: ~300m line-of-sight, ~100m with obstacles


Power Consumption

Measurement (ESP32-DevKitC @ 3.3V)

Mode Current Power Notes
Deep sleep 10 μA 33 μW RTC only
Light sleep 0.8 mA 2.6 mW WiFi off
Idle (WiFi on) 80 mA 264 mW Connected, no TX
Active (TX) 180 mA 594 mW WiFi transmitting
Full load 240 mA 792 mW Both cores, WiFi, sensors

Battery Life Estimates

2500 mAh LiPo (3.7V): - Deep sleep: ~290 days - Light sleep: ~130 hours - Idle WiFi: ~13 hours - Active swarm: ~6-8 hours (duty cycle dependent)

Optimization tips: - Use light sleep between control loops (saves 90% power) - Reduce WiFi TX power if close to other drones - Batch network messages (reduces TX time)


Production Considerations

Watchdog Timer

use esp_idf_svc::sys::*;

// Initialize task watchdog (5 second timeout)
unsafe {
    esp!(esp_task_wdt_init(5, true))?;
    esp!(esp_task_wdt_add(std::ptr::null_mut()))?;
}

// Feed watchdog in main loop
loop {
    // ... swarm control logic ...

    unsafe {
        esp!(esp_task_wdt_reset())?;
    }
}

OTA (Over-The-Air) Updates

use esp_idf_svc::ota::*;

// Check for updates
let mut ota = EspOta::new()?;
let mut update = ota.initiate_update()?;

// Download firmware (from swarm leader or server)
let firmware_data = download_firmware_from_leader()?;

// Write firmware
update.write(&firmware_data)?;

// Complete update (reboots automatically)
update.complete()?;

Error Recovery

use esp_idf_svc::sys::*;

// Store crash info in RTC memory (survives reboot)
#[link_section = ".rtc.data"]
static mut CRASH_COUNT: u32 = 0;

fn main() -> anyhow::Result<()> {
    unsafe {
        CRASH_COUNT += 1;
        if CRASH_COUNT > 5 {
            // Too many crashes - enter safe mode
            info!("Entering safe mode after {} crashes", CRASH_COUNT);
            safe_mode();
        }
    }

    // ... normal operation ...

    // Reset crash counter on successful operation
    unsafe { CRASH_COUNT = 0; }

    Ok(())
}

Testing

Local Mesh Testing

# Terminal 1: Node 1
DRONE_ID=1 cargo espflash flash --release --monitor

# Terminal 2: Node 2 (different ESP32)
DRONE_ID=2 cargo espflash flash --release --monitor

# Terminal 3: Node 3
DRONE_ID=3 cargo espflash flash --release --monitor

Watch logs to verify mesh discovery and message exchange.

Simulation with QEMU

# Install QEMU for ESP32
cargo install espflash
espflash save-image --chip esp32 --release firmware.bin

# Run in QEMU (limited support for WiFi)
qemu-system-xtensa -M esp32 -nographic -kernel firmware.bin

Troubleshooting

"Failed to connect to ESP32"

Solutions: 1. Hold BOOT button while flashing 2. Check USB cable (data cable, not charge-only) 3. Install CH340/CP2102 USB driver 4. Try different baud rate: --baud 115200

WiFi connection fails

Check:

// Enable verbose WiFi logs
esp_idf_svc::log::set_target_level("wifi", log::LevelFilter::Debug)?;

Common issues: - Wrong SSID/password - WiFi channel mismatch - Too far from AP/router

Out of memory

Solutions: - Use ESP32-WROVER with PSRAM - Reduce buffer sizes - Enable CONFIG_SPIRAM_SUPPORT in sdkconfig


Example Projects

Complete ESP32 Swarm Node

examples/esp32_mesh_node.rs - Full implementation with: - Dual-core network + control - IMU + GPS sensor fusion - OTA update support - Power management - Mesh routing

ESP32 Ground Control Station

examples/esp32_gcs.rs - Web-based GCS: - WiFi AP mode for laptop connection - REST API for commands - WebSocket for telemetry - Web UI (HTML5 + JavaScript)


Next Steps


Need help with ESP32? Join the discussion