Guide to Wireless Mesh AV Fleets: Coordinating a Multi-Vehicle Swarm Using ESP-NOW Protocol and 3-Wheel Chassis

Wireless Mesh AV Fleets: Coordinating a Multi‑Vehicle Swarm Using ESP‑NOW and 3‑Wheel Chassis

In this tutorial you learn how to build, configure, and program a wireless mesh of autonomous vehicles (AVs) that communicate through the ESP‑NOW protocol. The guide focuses on a lightweight 3‑wheel chassis, making the swarm agile, easy to maintain, and cost‑effective for indoor or outdoor deployments.

“A mesh network behaves like a living organism – every node talks, learns, and adapts in real time.”

Table of Contents

  1. Overview of Mesh AV Fleets
  2. Hardware Selection – 3‑Wheel Chassis & ESP‑32
  3. Getting Started with ESP‑NOW
  4. Designing the Mesh Protocol
  5. Full Code Example
  6. Testing & Debugging Tips
  7. Conclusion & Next Steps

Overview of Mesh AV Fleets

A wireless mesh AV fleet consists of multiple autonomous vehicles that share state information without a central router. Each vehicle can forward packets, so the network self‑heals when a node leaves or fails. Using ESP‑NOW, the fleet achieves sub‑millisecond latency, low power draw, and encryption—all critical for real‑time swarm coordination.

Why Choose ESP‑NOW?

  • Operates on the 2.4 GHz band with direct device‑to‑device messaging.
  • No Wi‑Fi router required – perfect for remote or RF‑restricted zones.
  • Supports up to 20 peers per ESP‑32, enough for small‑to‑medium swarms.
  • Built‑in ACK and retransmission handling ensures reliability.

Hardware Selection – 3‑Wheel Chassis & ESP‑32

The 3‑wheel chassis offers a stable base while allowing tight turning radii. Pair it with an ESP‑32 development board (e.g., WROOM‑32) for seamless ESP‑NOW integration.

Component Key Specs Recommended Model
Microcontroller Dual‑core, 240 MHz, 520 KB SRAM Espressif ESP‑32 WROOM‑32
Chassis 3‑wheel, 150 mm wheelbase, aluminum frame Tamiya 3‑Wheel Mini‑Kit
Motor Driver Continuous 12 A, PWM compatible DRV8833 Dual H‑Bridge
Power 7.4 V Li‑Po, 2000 mAh Turnigy Nano‑Tech 7.4V 2000 mAh

Wiring Quick‑Start

  1. Connect motor driver INA/INB pins to ESP‑32 GPIO 14, 27, 33, 25.
  2. Link motor power terminals to the Li‑Po pack (respect polarity).
  3. Attach a 5 V regulator to power the ESP‑32 from the same battery.
  4. Mount a small 2 cm ultrasonic sensor on the front for obstacle avoidance (optional).

Getting Started with ESP‑NOW

First, install the ESP‑Now library via the Arduino IDE. The following snippet initializes ESP‑NOW and registers a peer.

#include <WiFi.h>
#include <esp_now.h>

#define CHANNEL 1
#define MAX_PEERS 20

// Structure for transmitting vehicle state
typedef struct __attribute__((packed)) {
    uint8_t id;
    float   x;
    float   y;
    float   heading;
    uint8_t battery; // percent
} VehicleState;

// Global variables
VehicleState myState = {0};
uint8_t broadcastAddress[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};

void onDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) {
    VehicleState incoming;
    memcpy(&incoming, incomingData, sizeof(VehicleState));
    // Handle received state (e.g., update local map)
}

void setup() {
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.disconnect(); // Ensure clean start

    if (esp_now_init() != ESP_OK) {
        Serial.println("ESP‑NOW init failed");
        return;
    }

    esp_now_register_recv_cb(onDataRecv);

    // Add broadcast peer (all nodes listen to this address)
    esp_now_peer_info_t peerInfo = {};
    memcpy(peerInfo.peer_addr, broadcastAddress, 6);
    peerInfo.channel = CHANNEL;
    peerInfo.encrypt = false;

    if (!esp_now_add_peer(&peerInfo)) {
        Serial.println("Failed to add broadcast peer");
    }

    // Set device ID (unique per vehicle)
    myState.id = random(1, 250);
}

void loop() {
    // Example: update position from dead‑reckoning (replace with real sensors)
    myState.x += 0.01;
    myState.y += 0.00;
    myState.heading = fmod(myState.heading + 1.0, 360.0);
    myState.battery = 95; // dummy value

    // Broadcast state to the mesh
    esp_now_send(broadcastAddress, (uint8_t*)&myState, sizeof(VehicleState));
    delay(100); // 10 Hz update rate
}

Designing the Mesh Protocol

The mesh protocol decides how each vehicle shares its state and reacts to peers. Below is a simple flowchart described in prose:

  • Heartbeat – Every 100 ms each node broadcasts its VehicleState.
  • Neighbour Table – Each vehicle keeps a table of the last 10 messages per peer, using a timestamp to discard stale entries.
  • Collision Avoidance – If two vehicles predict a trajectory overlap within 30 cm, the one with the higher id yields.
  • Dynamic Leader Election – The vehicle with the lowest battery level becomes a temporary “relay” to forward messages for nodes at the edge of the mesh.

Implement the neighbour table with a fixed‑size array; it stays memory‑light on the ESP‑32.

Sample Neighbour Table Structure

typedef struct {
    uint8_t   id;
    float     x;
    float     y;
    float     heading;
    uint8_t   battery;
    unsigned long lastSeen; // millis()
} PeerInfo;

PeerInfo peers[MAX_PEERS];

Full Code Example – Swarm Coordination

The complete sketch below integrates the neighbour table, simple obstacle avoidance, and leader‑relay logic.

#include <WiFi.h>
#include <esp_now.h>

#define CHANNEL 1
#define MAX_PEERS 20
#define UPDATE_RATE 100   // ms
#define COLLISION_DIST 0.30f // meters

// ---------- Data Structures ----------
typedef struct __attribute__((packed)) {
    uint8_t id;
    float   x;
    float   y;
    float   heading;
    uint8_t battery;
} VehicleState;

typedef struct {
    VehicleState state;
    unsigned long lastSeen;
} PeerInfo;

// ---------- Global Variables ----------
VehicleState myState = {0};
PeerInfo peers[MAX_PEERS];
uint8_t broadcastAddr[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};

void onDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
    if (len != sizeof(VehicleState)) return;
    VehicleState inc;
    memcpy(&inc, data, sizeof(VehicleState));

    // Update or insert peer
    int idx = -1;
    for (int i = 0; i < MAX_PEERS; i++) {
        if (peers[i].state.id == inc.id) { idx = i; break; }
    }
    if (idx == -1) {
        // Find empty slot
        for (int i = 0; i < MAX_PEERS; i++) {
            if (peers[i].state.id == 0) { idx = i; break; }
        }
    }
    if (idx != -1) {
        peers[idx].state = inc;
        peers[idx].lastSeen = millis();
    }
}

// ---------- Helper Functions ----------
float distance(float x1, float y1, float x2, float y2) {
    return sqrt(sq(x2 - x1) + sq(y2 - y1));
}

// Simple avoidance: stop if any peer is too close ahead
bool shouldYield() {
    for (int i = 0; i < MAX_PEERS; i++) {
        if (peers[i].state.id == 0) continue;
        if (millis() - peers[i].lastSeen > 500) continue; // stale

        // Predict relative position in 0.5 s (linear assumption)
        float aheadX = myState.x + 0.5f * cos(radians(myState.heading));
        float aheadY = myState.y + 0.5f * sin(radians(myState.heading));

        float d = distance(aheadX, aheadY,
                           peers[i].state.x,
                           peers[i].state.y);
        if (d < COLLISION_DIST && myState.id > peers[i].state.id) {
            return true; // lower‑ID vehicle has priority
        }
    }
    return false;
}

// ---------- Setup ----------
void setup() {
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.disconnect();

    if (esp_now_init() != ESP_OK) {
        Serial.println("ESP‑NOW init failed");
        while (true);
    }

    esp_now_register_recv_cb(onDataRecv);

    esp_now_peer_info_t broadcastPeer = {};
    memcpy(broadcastPeer.peer_addr, broadcastAddr, 6);
    broadcastPeer.channel = CHANNEL;
    broadcastPeer.encrypt = false;
    esp_now_add_peer(&broadcastPeer);

    myState.id = random(1, 250);
    myState.x = 0.0;
    myState.y = 0.0;
    myState.heading = random(0, 360);
    myState.battery = 100;
}

// ---------- Main Loop ----------
void loop() {
    static unsigned long lastUpdate = 0;
    if (millis() - lastUpdate >= UPDATE_RATE) {
        // Update simulated position
        if (!shouldYield()) {
            myState.x += 0.02 * cos(radians(myState.heading));
            myState.y += 0.02 * sin(radians(myState.heading));
        } else {
            // Simple stop – real robots could turn instead
        }

        // Simulated battery drain
        if (myState.battery > 0) myState.battery--;

        // Broadcast current state
        esp_now_send(broadcastAddr, (uint8_t*)&myState, sizeof(VehicleState));

        lastUpdate = millis();
    }

    // Optional: implement leader‑relay logic here
}

Testing & Debugging Tips

  • Serial Monitor – Print received VehicleState IDs every second to verify peer discovery.
  • LED Indicator – Connect an LED to GPIO 2; toggle it on every successful broadcast to spot packet loss visually.
  • Range Test – Place two vehicles 5 m apart. If messages fail, reduce Wi‑Fi channel interference (pick channel 1, 6, or 11).
  • Power Profiling

Comments

Popular posts from this blog

Guide to Low-Cost Agricultural Surveying: Designing an Outdoor Rover via APM Rover Firmware and 3D Printed Chassis

Guide to Simulating and Building a Mecanum-Wheel Omnidirectional Robot using FreeRTOS and ESP32