Networking Cover

10.NETWORKING & COMMUNICATIONS

[LINKAGE STATUS: ESTABLISHING ESP-NOW WIRELESS PROTOCOLS]

Project Objective

For this assignment, I designed and coded a wireless system using the ESP-NOW protocol to interconnect two Seeed Studio XIAO ESP32-C6 boards. This infrastructure serves directly as the mission-critical telemetry and input link for my Final Project (an Electric Skateboard), working as an ultra-low latency remote controller.

SELECT SYSTEM COMPONENT TO VIEW DOCUMENTATION.

[ UNIT: TRANSMITTER_NODE_SETUP ]

STEP 01

MAC

The first step to start using ESP-NOW communication is to obtain the MAC (Media Access Control) addresses of each of the ESP boards that will be used, since the MAC addresses function like a fingerprint, allowing us to configure the transmitters and receivers in this communication.

STEP 02

Obtaining MAC Address

To build an ESP-NOW peer-to-peer bond, we need the target device's unique MAC address. In order to do so I used the following ARDUINO IDE code to read its address on the terminal.

#include <WiFi.h>

#include 

void setup() {
  // Initialize the serial monitor at 115200 baud
  Serial.begin(115200);
  delay(2000); // Wait for the serial monitor to open

  // Wi-Fi must be in Station (STA) mode to read its Wi-Fi MAC address
  WiFi.mode(WIFI_STA);

  Serial.println();
  Serial.println("------------------------------------");
  Serial.print("The MAC adress of this ESP is: ");
  Serial.println(WiFi.macAddress());
  Serial.println("------------------------------------");
}

void loop() {
}
STEP 03

Initializing ESP-NOW

Now, in order to test communication between boards, it is necessary to designate a transmitter and a receiver and power both boards. In my case, I programmed a XIAOESP32C6 to send a message to another identical board every so often, and the receiving board printed it on the terminal.

STEP 04

Reading Packet Payload

In order to read the communication I opened my serial monitor, where I could check the messages that the XIAO was recieving.

STEP 1 / 4
TEST_v1.0

INITIAL_LINK_BASIC_DIAGNOSTICS

TRANSMITTER
#include <WiFi.h>
#include <esp_now.h>
// --- OSCAR'S BOARD MAC ADDRESS (TARGET) ---
uint8_t broadcastAddress[] = {0x98, 0xA3, 0x16, 0x8D, 0xFB, 0x18};

// Data structure to send
typedef struct struct_message {
    char text[32];
    int counter;
} struct_message;

struct_message myData;
esp_now_peer_info_t peerInfo;

// === ESP32-C6 COMPATIBLE CALLBACK ===
// Uses 'const wifi_tx_info_t *tx_info' instead of 'const uint8_t *mac_addr'
void OnDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) {
  Serial.print("\r\nDelivery Status: ");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Successfully delivered to Oscar!" : "Error: Oscar did not receive the data");
}
 
void setup() {
  Serial.begin(115200);
  delay(2000);
 
  // Set Wi-Fi to Station Mode
  WiFi.mode(WIFI_STA);

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  // Register delivery callback function
  esp_now_register_send_cb(OnDataSent);
  
  // Register peer settings (Oscar's board)
  memcpy(peerInfo.peer_addr, broadcastAddress, 6);
  peerInfo.channel = 0;  
  peerInfo.encrypt = false;
  
  if (esp_now_add_peer(&peerInfo) != ESP_OK){
    Serial.println("Failed to add Oscar's board as peer");
    return;
  }
}
 
void loop() {
  static int messageCount = 0;
  messageCount++;

  // Package the payload layout
  strcpy(myData.text, "Message from Carlos");
  myData.counter = messageCount;
  
  Serial.print("Sending packet number: ");
  Serial.println(myData.counter);

  // Transmit payload over peer link
  esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
   
  if (result != ESP_OK) {
    Serial.println("Hardware transmission error");
  }
  
  delay(2000);
}
RECIEVER
#include <WiFi.h>
#include <esp_now.h>

// Data structure MUST MATCH EXACTLY with the Transmitter's structure
typedef struct struct_message {
    char text[32];
    int counter;
} struct_message;

struct_message incomingReadings;

// === ESP32-C6 COMPATIBLE CALLBACK ===
// Uses 'const esp_now_recv_info_t *recv_info' to ensure compatibility with newer core versions
void OnDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *incomingData, int len) {
  // Copy the bytes received over the air into our local structure layout
  memcpy(&incomingReadings, incomingData, sizeof(incomingReadings));
  
  Serial.println();
  Serial.println("======= DATA RECEIVED FROM TRANSMITTER =======");
  Serial.print("Bytes received: ");
  Serial.println(len);
  Serial.print("Text String: ");
  Serial.println(incomingReadings.text);
  Serial.print("Packet Count: ");
  Serial.println(incomingReadings.counter);
  Serial.println("==============================================");
}
 
void setup() {
  Serial.begin(115200);
  delay(2000);
  
  // Set Wi-Fi to Station Mode
  WiFi.mode(WIFI_STA);

  // Initialize ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }
  
  // Register reception callback function
  esp_now_register_recv_cb(OnDataRecv);
}
 
void loop() {
  // Left blank intentionally. All data processing occurs asynchronously 
  // inside 'OnDataRecv' whenever the antenna flags an incoming wireless packet.
}
TEST_v4.0

INITIAL_LINK_BASIC_DIAGNOSTICS

BASIC_PING_PONG_VIDEO
EMITTER
#include <WiFi.h>
#include <esp_now.h>
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>

// Initialize SH1106 OLED screen in Hardware I2C mode
// Automatically maps to XIAO C6 native pins: SDA (D4) and SCL (D5)
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

const int pinX = A0; 
const int pinY = A1; 
const int pinButton = D2; // Joystick SW pin mapped to D2

uint8_t receiverAddress[] = {0x58, 0xE6, 0xC5, 0x15, 0x8A, 0x44};

typedef struct struct_message {
    char direction[20];
} struct_message;

struct_message sendData;
esp_now_peer_info_t peerInfo;

// --- DYNAMIC CALIBRATION VARIABLES ---
int centerX = 2048; 
int centerY = 2048; 
int lowRangeX, highRangeX;
int lowRangeY, highRangeY;

const int DEAD_ZONE = 400; 
const int SAMPLES = 15;     

// --- BUTTON DEBOUNCE VARIABLES ---
bool communicationActive = true;   // Transmission toggling state
int lastButtonState = HIGH;     
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_TIME = 50; // Set to standard 50ms

// Global variables for comfortable screen updates
String currentDir = "Center";
String lastDirSent = "";

// Average filtering function for stable analog read values
int getStableRead(int pin) {
    long sum = 0;
    for (int i = 0; i < SAMPLES; i++) {
        sum += analogRead(pin);
        delayMicroseconds(50);
    }
    return sum / SAMPLES;
}

// Function responsible for rendering the UI on the OLED display
void updateDisplay() {
    u8g2.clearBuffer();          
    
    // Draw outer border frame
    u8g2.drawFrame(0, 0, 128, 64);
    
    // Set font for top status bar labels
    u8g2.setFont(u8g2_font_6x10_tr); 
    u8g2.drawStr(10, 15, "SYSTEM:");
    
    // Display whether the remote is transmitting or locked (safety stop)
    if (communicationActive) {
        u8g2.drawStr(65, 15, "ONLINE");
    } else {
        u8g2.setFont(u8g2_font_6x10_tf); // Attention/bold-enhanced font layout
        u8g2.drawStr(65, 15, "[STOP]");
    }
    
    // Aesthetic horizontal dividing line
    u8g2.drawHLine(5, 22, 118);

    // Label for the dynamic command output
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(12, 36, "REMOTE STATE:");

    // Configure a large, readable font for the active direction text
    u8g2.setFont(u8g2_font_fub14_tf); 
    
    // Render dynamic text centered on fixed coordinates based on character length
    if (!communicationActive)       u8g2.drawStr(36, 56, "STOP");
    else if (currentDir == "Center")    u8g2.drawStr(22, 56, "Center");
    else if (currentDir == "Up")        u8g2.drawStr(44, 56, "Up");
    else if (currentDir == "Down")      u8g2.drawStr(32, 56, "Down");
    else if (currentDir == "Right")     u8g2.drawStr(25, 56, "Right");
    else if (currentDir == "Left")      u8g2.drawStr(32, 56, "Left");

    u8g2.sendBuffer(); 
}

void setup() {
    Serial.begin(115200);
    
    // Initialize OLED display peripheral
    u8g2.begin();
    
    // Flash quick screen message during boot process
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(15, 30, "CALIBRATING...");
    u8g2.sendBuffer();

    pinMode(pinButton, INPUT_PULLUP);

    WiFi.mode(WIFI_STA);

    if (esp_now_init() != ESP_OK) {
        Serial.println("Error initializing ESP-NOW");
        return;
    }

    memcpy(peerInfo.peer_addr, receiverAddress, 6);
    peerInfo.channel = 0;  
    peerInfo.encrypt = false;
    
    if (esp_now_add_peer(&peerInfo) != ESP_OK) {
        Serial.println("Error adding peer receiver");
        return;
    }

    // --- AUTO-CALIBRATION PROCESS ---
    Serial.println("Calibrating joystick... DO NOT TOUCH THE STICK");
    delay(500); 
    
    centerX = getStableRead(pinX);
    centerY = getStableRead(pinY);
    
    lowRangeX = centerX - DEAD_ZONE;
    highRangeX = centerX + DEAD_ZONE;
    lowRangeY = centerY - DEAD_ZONE;
    highRangeY = centerY + DEAD_ZONE;

    Serial.println("Calibration completed successfully.");
    
    // Render initial baseline frame interface
    updateDisplay();
}

void loop() {
    // -----------------------------------------------------------------
    // BUTTON DEBOUNCE LOGIC (PIN D2)
    // -----------------------------------------------------------------
    int buttonReading = digitalRead(pinButton);

    // If hardware state changed (due to bounce noise or genuine click), reset timer
    if (buttonReading != lastButtonState) {
        lastDebounceTime = millis(); 
        lastButtonState = buttonReading; // Update tracking state immediately to catch transitions
    }

    // Evaluate hardware action only after holding stable for DEBOUNCE_TIME
    if ((millis() - lastDebounceTime) > DEBOUNCE_TIME) {
        // Static tracker to latch edge processing and avoid continuous loops on long presses
        static bool buttonProcessed = false;

        if (buttonReading == LOW && !buttonProcessed) {
            communicationActive = !communicationActive; 
            
            Serial.print("--- REMOTE CONTROL --- Transmission: ");
            Serial.println(communicationActive ? "ENABLED" : "DISABLED (STOP)");
            
            if (!communicationActive) {
                strcpy(sendData.direction, "STOP");
                esp_now_send(receiverAddress, (uint8_t *) &sendData, sizeof(sendData));
                lastDirSent = "STOP";
            }
            // Force immediate UI display update upon safety flag modification
            updateDisplay();
            
            buttonProcessed = true; 
        } 
        else if (buttonReading == HIGH) {
            buttonProcessed = false; // Release lock flag when physical contact opens
        }
    }

    // -----------------------------------------------------------------
    // JOYSTICK INPUT CONTROL LOGIC
    // -----------------------------------------------------------------
    if (communicationActive) {
        int valueX = getStableRead(pinX);
        int valueY = getStableRead(pinY);

        String prevDir = currentDir; // Cache previous string direction to evaluate graphic updates
        currentDir = "Center";

        // Dynamic coordinate mapping based on baseline thresholds
        if (valueX < lowRangeX) {
            currentDir = "Left";
        } else if (valueX > highRangeX) {
            currentDir = "Right";
        } else if (valueY < lowRangeY) {
            currentDir = "Up"; 
        } else if (valueY > highRangeY) {
            currentDir = "Down";
        }

        // Transmit packet over air if physical layout shifted or upon neutral re-centering
        if (currentDir != "Center" || lastDirSent != "Center") {
            currentDir.toCharArray(sendData.direction, sizeof(sendData.direction));
            esp_err_t result = esp_now_send(receiverAddress, (uint8_t *) &sendData, sizeof(sendData));
            
            if (result == ESP_OK) {
                Serial.print("Sent: "); Serial.println(sendData.direction);
                lastDirSent = currentDir;
            }
        }

        // Refresh screen instantly if directional mapping state changed
        if (currentDir != prevDir) {
            updateDisplay();
        }

    } else {
        // Clamp to safety state during lockdown loop sequences
        currentDir = "Center";
    }

    delay(50); 
}
BASIC_RX_TEST.INO
#include <WiFi.h>
#include <esp_now.h>
#include <VescUart.h>

#define RX_PIN 17  // Pin D7
#define TX_PIN 16  // Pin D6

VescUart vesc;

// Baseline 5.0A current test limit for 5060 140 KV motor benchmarks
const float FORWARD_TEST_CURRENT = 5.0;  
const float BRAKE_TEST_CURRENT   = -5.0; 

typedef struct struct_message {
    char direction[20];
} struct_message;

struct_message receivedData;

// === ESP-NOW ASYNCHRONOUS RECEPTION CALLBACK ===
void OnDataRecv(const esp_now_recv_info_t * recv_info, const uint8_t *incomingData, int len) {
    memcpy(&receivedData, incomingData, sizeof(receivedData));
    
    Serial.print("Joystick: ");
    Serial.println(receivedData.direction);

    // CRITICAL ALIGNMENT: Matches English payload layouts ("Up" and "Down")
    if (strcmp(receivedData.direction, "Up") == 0 || strcmp(receivedData.direction, "Forward") == 0) {
        Serial.println("-> Match found! Injecting 5.0A to VESC via UART for forward throttle...");
        vesc.setCurrent(FORWARD_TEST_CURRENT);
    } 
    else if (strcmp(receivedData.direction, "Down") == 0 || strcmp(receivedData.direction, "Backward") == 0) {
        Serial.println("-> Match found! Injecting -5.0A to VESC via UART for braking/reverse...");
        vesc.setCurrent(BRAKE_TEST_CURRENT);
    } 
    else {
        // Safe default sequence for "Center", "Left", or "Right" inputs
        vesc.setCurrent(0.0);
    }
}

void setup() {
    Serial.begin(115200);
    
    // Initialize hardware serial link to communicate with VescUart protocol
    Serial1.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);
    delay(100); 
    
    vesc.setSerialPort(&Serial1);
    
    WiFi.mode(WIFI_STA);
    WiFi.disconnect();

    if (esp_now_init() != ESP_OK) {
        Serial.println("Error initializing ESP-NOW");
        return;
    }
    
    esp_now_register_recv_cb(OnDataRecv);
    Serial.println("Receiver setup calibrated. Listening for active 'Up' and 'Down' wireless payloads.");
}

void loop() {
    // Left blank intentionally. System runs fully asynchronous through wireless interrupts.
}