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 ]
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.
}







