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_Enable in Main.h for 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 the onRecv callback on the bridge, using payload types defined in DiyActivePedal_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →