How to Set Up ESPNOW Wireless Peer-to-Peer Communication Between Pedals
Enable ESPNOW wireless communication between DIY sim racing pedals by defining ESPNOW_Enable in Main.h, triggering the pairing workflow, and letting the bridge firmware automatically store peer MAC addresses for persistent peer-to-peer telemetry exchange.
The chrgri/diy-sim-racing-ffb-pedal project implements ESP-NOW (ESPNOW) to transmit pedal position, rudder data, and joystick values wirelessly between three pedal boards (clutch, brake, throttle) and a central bridge board. This guide explains the exact firmware configuration, pairing workflow, and code paths required to establish ESPNOW wireless peer-to-peer communication between pedals.
Architectural Overview of ESPNOW Communication
The system uses a broadcast-based peer-to-peer topology where all pedals share the same Wi-Fi channel and use the broadcast MAC address (0xFF:0xFF:0xFF:0xFF:0xFF:0xFF) for discovery. Once the bridge stores a pedal's MAC address, it adds the device as a formal ESP-NOW peer for unicast communication.
| Component | Role | Key ESPNOW Elements |
|---|---|---|
| Pedal Firmware | Generates pedal state and initiates pairing | ESPNOW_SyncTask creates DAP_ESPPairing_st packets and broadcasts DAP_Joystick_Message |
| Bridge Firmware | Receives pedal messages and manages peer list | onRecv callback parses pairing packets, stores MACs in _ESP_pairing_reg, calls ESPNow.add_peer |
| Common Types | Defines payload identifiers | DAP_PAYLOAD_TYPE_ESPNOW_PAIRING, DAP_PAYLOAD_TYPE_ESPNOW_JOYSTICK, DAP_PAYLOAD_TYPE_ESPNOW_RUDDER |
| ESPNOW Library | Holds MAC tables and status flags | broadcast_mac, ESPNOW_status, ESPNow_pairing_action_b |
Enabling ESPNOW in the Firmware
ESPNOW wireless communication requires explicit compilation flags in both the pedal and bridge projects. The firmware uses conditional compilation to include ESP-NOW headers and tasks.
Configuring the Pedal Firmware
Open Firmware_for_V3/PedalFirmware/include/Main.h and verify the ESP-NOW block for your PCB version:
#if PCB_VERSION == 3
#define ESPNOW_Enable // <‑‑ enables ESP‑NOW wireless communication
#define ESPNow_ESP32 // use ESP‑NOW on ESP32‑S2/‑S3
#endif
Source: Main.h – V3 block
Ensure the ESP-NOW library header is included conditionally:
#ifdef ESPNOW_Enable
#include "ESPNOW_lib.h"
#endif
Source: Main.h – include
Configuring the Bridge Firmware
Repeat the same steps in Firmware_for_V3/BridgeFirmware/include/Main.h. The bridge must also define ESPNOW_Enable to compile the onRecv callback and peer management logic.
Pairing Workflow for Peer-to-Peer Setup
The pairing process establishes the MAC address registry that enables directed ESP-NOW communication between pedals and the bridge. The workflow uses a 20-second pairing window during which pedals broadcast their identity packets.
Step 1: Initialize ESP-NOW on the Pedal
The ESPNOW_SyncTask calls ESPNow_initialize() on its first execution when ESPNow_initial_status == false:
if (!ESPNow_initial_status) {
ESPNow_initialize();
ESPNow_initial_status = true;
}
Source: Main.cpp – init
Step 2: Trigger Pairing Mode
Activate pairing by pressing the hardware pairing button (defined by Pairing_GPIO) or by setting the software flag:
// Hardware button check
if (digitalRead(Pairing_GPIO) == LOW || software_pairing_action_b) {
ESPNow_pairing_action_b = true;
}
Source: Main.cpp – button handling
Step 3: Build and Broadcast Pairing Packets
While ESPNow_pairing_action_b is true, the pedal constructs a DAP_ESPPairing_st packet containing its device ID, payload type, and CRC checksum:
dap_esppairing_lcl.payloadESPNowInfo_._deviceID = espnow_dap_config_st.payLoadPedalConfig_.pedal_type;
dap_esppairing_lcl.payLoadHeader_.payloadType = DAP_PAYLOAD_TYPE_ESPNOW_PAIRING;
dap_esppairing_lcl.payLoadHeader_.PedalTag = espnow_dap_config_st.payLoadPedalConfig_.pedal_type;
dap_esppairing_lcl.payLoadHeader_.version = DAP_VERSION_CONFIG;
uint16_t crc = checksumCalculator((uint8_t*)(&dap_esppairing_lcl.payLoadHeader_),
sizeof(dap_esppairing_lcl.payLoadHeader_) + sizeof(dap_esppairing_lcl.payloadESPNowInfo_));
dap_esppairing_lcl.payloadFooter_.checkSum = crc;
ESPNow.send_message(broadcast_mac, (uint8_t*)&dap_esppairing_lcl, sizeof(dap_esppairing_lcl));
Source: Main.cpp – pairing packet
Step 4: Bridge Receives and Stores Peers
The bridge's onRecv callback detects pairing packets by checking the payload type DAP_PAYLOAD_TYPE_ESPNOW_PAIRING. It stores the sender's MAC address in _ESP_pairing_reg and flags the registry for EEPROM persistence:
if (data_len == sizeof(DAP_ESPPairing_st)) {
memcpy(&dap_esppairing_st, data, sizeof(DAP_ESPPairing_st));
uint8_t id = dap_esppairing_st.payloadESPNowInfo_._deviceID; // 0=clutch, 1=brake, 2=throttle, 3=bridge
if (id < 4) {
memcpy(&_ESP_pairing_reg.Pair_mac[id], mac_addr, 6);
_ESP_pairing_reg.Pair_status[id] = 1;
UpdatePairingToEeprom = true;
}
}
Source: Bridge onRecv – pairing
Step 5: Add Peers for Unicast Communication
After the pairing window closes or immediately upon receiving valid pairing data, the bridge calls ESPNow.add_peer(mac) for each stored MAC address. This registers the devices as formal ESP-NOW peers, enabling efficient unicast communication:
for (int i = 0; i < 4; i++) {
if (_ESP_pairing_reg.Pair_status[i] == 1) {
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, _ESP_pairing_reg.Pair_mac[i], 6);
peerInfo.channel = 0; // use current channel
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
Serial.printf("Peer %d added successfully\n", i);
}
}
}
Source: Bridge – add peer
Sending and Receiving Data
Once pairing completes, the system transitions to normal operation where pedals broadcast telemetry at fixed intervals.
Broadcasting Joystick Data from Pedals
The ESPNOW_SyncTask periodically calls ESPNow_Joystick_Broadcast() to transmit controller values. This function populates a DAP_Joystick_Message structure with the current pedal position and transmits it to the broadcast MAC:
void ESPNow_Joystick_Broadcast(int32_t controllerValue) {
_dap_joystick_message.payloadtype = DAP_PAYLOAD_TYPE_ESPNOW_JOYSTICK;
_dap_joystick_message.cycleCnt_u64++;
_dap_joystick_message.timeSinceBoot_i64 = esp_timer_get_time() / 1000;
_dap_joystick_message.controllerValue_i32 = controllerValue;
// Set pedal_status based on current operational mode
_dap_joystick_message.pedal_status = current_pedal_status;
esp_now_send(broadcast_mac, (uint8_t*)&_dap_joystick_message, sizeof(_dap_joystick_message));
}
Source: ESPNOW_lib.h – joystick broadcast
Processing Data on the Bridge
The bridge's onRecv callback distinguishes between payload types using the header identifiers defined in DiyActivePedal_types.h. It handles joystick data (type 160) and rudder data (type 150) by parsing the payloads and forwarding them to the PC via USB serial:
void onRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
if (data_len == sizeof(DAP_Joystick_Message)) {
DAP_Joystick_Message msg;
memcpy(&msg, data, sizeof(msg));
if (msg.payloadtype == DAP_PAYLOAD_TYPE_ESPNOW_JOYSTICK) {
// Forward to SimHub or PC application
Serial.write((uint8_t*)&msg, sizeof(msg));
}
}
else if (data_len == sizeof(DAP_Rudder_st)) {
// Handle rudder-specific telemetry
DAP_Rudder_st rudder;
memcpy(&rudder, data, sizeof(rudder));
processRudderData(rudder);
}
}
Key Source Files and Constants
Understanding the file structure is essential for debugging and extending the ESPNOW wireless communication between pedals.
| File | Role | Key Contents |
|---|---|---|
Firmware_for_V3/PedalFirmware/include/Main.h |
Compilation flags | ESPNOW_Enable, ESPNow_ESP32, conditional includes |
Firmware_for_V3/PedalFirmware/include/ESPNOW_lib.h |
MAC definitions and broadcast helpers | broadcast_mac, ESPNow_Joystick_Broadcast(), status flags |
Firmware_for_V3/PedalFirmware/include/DiyActivePedal_types.h |
Payload type identifiers | DAP_PAYLOAD_TYPE_ESPNOW_PAIRING (0x01), DAP_PAYLOAD_TYPE_ESPNOW_JOYSTICK (0xA0), DAP_PAYLOAD_TYPE_ESPNOW_RUDDER (0x96) |
Firmware_for_V3/PedalFirmware/src/Main.cpp |
Core ESP-NOW task implementation | ESPNOW_SyncTask, pairing packet construction, ESPNow_initialize() |
Firmware_for_V3/BridgeFirmware/src/Main.cpp |
Peer management and data reception | onRecv callback, _ESP_pairing_reg storage, ESPNow.add_peer() |
Critical Timing Parameters:
REPETITION_INTERVAL_ESPNOW_TASK: 2 ms task interval for the sync loop- Pairing timeout: 20 000 ms (20 seconds)
- Joystick packet interval: 2 ms (configurable via
joystickPacketInterval) - Rudder packet interval: 3 ms (configurable via
rudderPacketInterval)
Summary
- ESPNOW wireless peer-to-peer communication between pedals requires defining
ESPNOW_EnableinMain.hfor both pedal and bridge firmware before compilation. - The pairing workflow uses broadcast MAC addresses to discover devices, stores peer MACs in
_ESP_pairing_reg, and persists them to EEPROM for automatic reconnection on subsequent power cycles. - Data transmission occurs via
ESPNow_Joystick_Broadcast()on the pedals and is handled by theonRecvcallback on the bridge, using payload types defined inDiyActivePedal_types.h. - All three pedals and the bridge must share the same Wi-Fi channel and use the broadcast MAC (
0xFF:0xFF:0xFF:0xFF:0xFF:0xFF) during the discovery phase.
Frequently Asked Questions
How do I know if ESPNOW pairing was successful?
Successful pairing is indicated by serial output on the bridge showing received MAC addresses and peer addition confirmations. In Firmware_for_V3/BridgeFirmware/src/Main.cpp, the onRecv function logs when it stores a MAC in _ESP_pairing_reg, and the peer addition loop prints confirmation when esp_now_add_peer() returns ESP_OK. If you see joystick data appearing in the bridge serial monitor after the 20-second pairing window, the ESPNOW wireless peer-to-peer communication between pedals is fully established.
Can I use ESPNOW without the bridge firmware?
No, the current architecture requires the bridge firmware to receive and forward ESP-NOW packets to the PC. The pedals broadcast telemetry using ESPNow_Joystick_Broadcast() in ESPNOW_lib.h, but the SimHub integration and USB HID output are handled exclusively by the bridge's onRecv callback in BridgeFirmware/src/Main.cpp. Without the bridge, there is no endpoint to process the DAP_PAYLOAD_TYPE_ESPNOW_JOYSTICK packets.
What is the maximum range for ESPNOW communication between pedals?
ESPNOW typically achieves a range of 100-200 meters in open air using the ESP32's 2.4 GHz radio, though the actual range in a sim racing setup depends on physical obstructions and Wi-Fi congestion. The diy-sim-racing-ffb-pedal firmware does not implement specific range-extending features, but it does use a fixed channel and broadcast MAC during pairing to maximize discovery reliability. For optimal performance, position the bridge within line-of-sight of the pedals and away from 2.4 GHz interference sources.
How do I reset the pairing registry if I replace a pedal?
To reset the ESPNOW pairing registry, clear the EEPROM storage on the bridge firmware. In Firmware_for_V3/BridgeFirmware/src/Main.cpp, the _ESP_pairing_reg structure stores paired MAC addresses and is persisted to EEPROM when UpdatePairingToEeprom is true. You can trigger a factory reset by either re-flashing the bridge firmware (which clears EEPROM) or by implementing a reset routine that sets all Pair_status entries to 0 and calls EEPROM.put(). After clearing, you must re-pair all pedals using the 20-second pairing window.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →