# How to Set Up ESPNOW Wireless Peer-to-Peer Communication Between Pedals

> Set up ESPNOW wireless peer to peer communication for your DIY sim racing pedals. Enable ESPNOW in Main.h and let the firmware handle persistent telemetry exchange.

- Repository: [chrgri/diy-sim-racing-ffb-pedal](https://github.com/chrgri/diy-sim-racing-ffb-pedal)
- Tags: how-to-guide
- Published: 2026-02-27

---

**Enable ESPNOW wireless communication between DIY sim racing pedals by defining `ESPNOW_Enable` in [`Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/include/Main.h) and verify the ESP-NOW block for your PCB version:

```c
#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](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/include/Main.h#L44-L52)

Ensure the ESP-NOW library header is included conditionally:

```c
#ifdef ESPNOW_Enable
#include "ESPNOW_lib.h"
#endif

```

Source: [Main.h – include](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/include/Main.h#L293-L296)

### Configuring the Bridge Firmware

Repeat the same steps in [`Firmware_for_V3/BridgeFirmware/include/Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`:

```c
if (!ESPNow_initial_status) {
    ESPNow_initialize();
    ESPNow_initial_status = true;
}

```

Source: [Main.cpp – init](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/src/Main.cpp#L90-L99)

### Step 2: Trigger Pairing Mode

Activate pairing by pressing the hardware pairing button (defined by `Pairing_GPIO`) or by setting the software flag:

```c
// Hardware button check
if (digitalRead(Pairing_GPIO) == LOW || software_pairing_action_b) {
    ESPNow_pairing_action_b = true;
}

```

Source: [Main.cpp – button handling](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/src/Main.cpp#L1004-L1018)

### 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:

```c
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](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/src/Main.cpp#L1023-L1039)

### 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:

```c
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](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/BridgeFirmware/src/Main.cpp#L1449-L1462)

### 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:

```c
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](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/BridgeFirmware/src/Main.cpp#L1725-L1763)

## 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:

```c
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](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/include/ESPNOW_lib.h#L101-L109)

### Processing Data on the Bridge

The bridge's `onRecv` callback distinguishes between payload types using the header identifiers defined in [`DiyActivePedal_types.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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:

```c
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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Firmware_for_V3/PedalFirmware/include/Main.h) | Compilation flags | `ESPNOW_Enable`, `ESPNow_ESP32`, conditional includes |
| [`Firmware_for_V3/PedalFirmware/include/ESPNOW_lib.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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.