# How the Multiple Pedal Synchronization and Assignment System Works in DIY Sim-Racing FFB Pedals

> Explore the DIY Sim-Racing FFB Pedal system's multiple pedal synchronization and assignment. Learn how ESP-NOW, ESP-32 firmware, and a bridge board enable seamless wireless control for your sim rig.

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

---

**The DIY Sim-Racing FFB Pedal system synchronizes up to three wireless pedals through a three-layer architecture where the SimHub plugin sends assignment commands via ESP-NOW, the ESP-32 firmware persists the role to EEPROM, and the bridge board advertises availability back to the UI.**

The `diy-sim-racing-ffb-pedal` repository by chrgri implements a complete wireless pedal ecosystem using ESP-32 microcontrollers and a SimHub plugin. Understanding how this **multiple pedal synchronization and assignment system** coordinates clutch, brake, and throttle roles across independent hardware units requires examining the interplay between the C# configuration interface and the embedded C++ firmware.

## System Architecture Overview

The synchronization mechanism operates through three tightly-coupled layers that handle distinct responsibilities:

- **SimHub Plugin (UI Layer)**: Manages user interaction and constructs assignment payloads in [`AssignmentConfigurationWindow.xaml.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/AssignmentConfigurationWindow.xaml.cs)
- **Wireless Transport (ESP-NOW Layer)**: Delivers commands via the [`ESPNOW_lib.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESPNOW_lib.h) protocol implementation
- **Persistence Layer (Firmware)**: Stores assignments in EEPROM and reboots the device, implemented in [`ESP32/src/Main.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/src/Main.cpp)

Each physical pedal unit maintains a `dap_assignement_reg` structure containing its `deviceID` (clutch, brake, or throttle), which determines how the bridge interprets incoming sensor data.

## The Assignment Workflow

### SimHub UI Layer: Initiating the Assignment

When a user assigns an unassigned pedal to a specific role, the plugin creates a `DAP_action_st` payload targeting the pedal's MAC address. The process begins in [`SimHubPlugin/UIFunction/AssignmentConfigurationWindow.xaml.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/SimHubPlugin/UIFunction/AssignmentConfigurationWindow.xaml.cs):

```csharp
// Lines 11-22: Constructing the assignment action
DAP_action_st tmp_action = default;
tmp_action.payloadHeader_.version      = (byte)Constants.pedalConfigPayload_version;
tmp_action.payloadHeader_.payloadType  = (byte)Constants.pedalActionPayload_type;
tmp_action.payloadHeader_.PedalTag    = (byte)_pedalSelect;                     // Index of unassigned pedal MAC
tmp_action.payloadPedalAction_.system_action_u8 = (byte)_pedalActionId[_plugin.Settings.table_selected];

_plugin.SendPedalActionWireless(tmp_action, (byte)_pedalSelect);

```

The `system_action_u8` field receives one of three enum values defined in [`SimHubPlugin/VariablesStruct/constants.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/SimHubPlugin/VariablesStruct/constants.cs): `SET_ASSIGNMENT_0` (clutch), `SET_ASSIGNMENT_1` (brake), or `SET_ASSIGNMENT_2` (throttle). The `SendPedalActionWireless` method then transmits this packet via ESP-NOW to the target MAC address stored in `_calculations.unassignedPedalMacaddress`.

### Wireless Transport Layer: ESP-NOW Protocol Handling

Upon reception, the ESP-32 firmware in [`ESP32/include/ESPNOW_lib.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/include/ESPNOW_lib.h) decodes the action inside the `onRecv` callback:

```c
// Excerpt from ESPNOW_lib.h (lines 63-80)
if (dap_actions_st.payloadPedalAction_.system_action_u8 == (uint8_t)PedalSystemAction::SET_ASSIGNMENT_0 && commandForAssignment_b)
{
    dap_assignement_reg.deviceID = PEDAL_ID_CLUTCH;
    assignmentUpdate_b = true;
    assignmentUpdateBuzzer_b = true;
}

```

The same conditional block exists for `SET_ASSIGNMENT_1` (mapping to `PEDAL_ID_BRAKE`) and `SET_ASSIGNMENT_2` (mapping to `PEDAL_ID_THROTTLE`). Setting `assignmentUpdate_b` triggers the persistence routine, while `assignmentUpdateBuzzer_b` provides audible feedback confirming receipt.

### EEPROM Persistence and Runtime Activation

The main loop in [`ESP32/src/Main.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/src/Main.cpp) (around line 3640) checks the update flag and commits the new assignment to non-volatile storage:

```c
if (assignmentUpdate_b)
{
    assignmentUpdate_b = false;
    writeAssignmentToEeprom();   // Persists dap_assignement_reg struct with checksum
    delay(1000);
    ESP.restart();               // Reboot to advertise new deviceID
}

```

The `writeAssignmentToEeprom()` function (lines 799-804) writes the complete `dap_assignement_reg` structure—including the `deviceID`, `payloadType`, `magicKey`, and CRC—to a fixed EEPROM offset. The mandatory `ESP.restart()` ensures the pedal initializes with its new logical identity and begins reporting the correct pedal type in subsequent bridge state packets.

## Bridge Synchronization and UI Updates

After the ESP-32 reboots, it includes its assigned role in every bridge state transmission. The SimHub plugin receives this in `SimHubPlugin/UICallback/BridgeSerialTimer*.cs`:

```csharp
// Line 455: Updating pedal availability status
Plugin._calculations.PedalAvailability[0] = bridge_state.payloadBridgeState_.Pedal_availability_0 == 1;

```

The `PedalAvailability` boolean array indicates which logical pedals (indices 0, 1, 2 for clutch, brake, throttle) are currently connected and synchronized. When the UI detects a transition from unassigned to assigned, [`AssignmentConfigurationWindow.xaml.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/AssignmentConfigurationWindow.xaml.cs) (lines 65-73) updates the interface to disable the assignment button and display the current role, completing the synchronization loop.

## Practical Code Examples

### Triggering Assignment Programmatically in C#

To assign a detected pedal programmatically from the SimHub plugin:

```csharp
int roleIdx = 1; // 0=Clutch, 1=Brake, 2=Throttle
int macIndex = 2; // Third unassigned MAC in the discovery list

var action = new DAP_action_st();
action.payloadHeader_.version      = (byte)Constants.pedalConfigPayload_version;
action.payloadHeader_.payloadType  = (byte)Constants.pedalActionPayload_type;
action.payloadHeader_.PedalTag    = (byte)macIndex;

var assignments = new[] {
    (int)PedalSystemAction.SET_ASSIGNMENT_0,
    (int)PedalSystemAction.SET_ASSIGNMENT_1,
    (int)PedalSystemAction.SET_ASSIGNMENT_2
};
action.payloadPedalAction_.system_action_u8 = (byte)assignments[roleIdx];

_plugin.SendPedalActionWireless(action, (byte)macIndex);

```

### Handling Assignment in ESP-32 Firmware

To process the assignment command on the embedded side:

```c
// Inside ESPNOW_lib.h receive callback
if (dap_actions_st.payloadPedalAction_.system_action_u8 == (uint8_t)PedalSystemAction::SET_ASSIGNMENT_1)
{
    dap_assignement_reg.deviceID = PEDAL_ID_BRAKE;  // Assign as brake
    assignmentUpdate_b = true;                      // Trigger EEPROM write
    assignmentUpdateBuzzer_b = true;                // Audible confirmation
}

```

### Clearing Previous Assignments

To clear an assignment and return a pedal to unassigned status, the system uses `PedalSystemAction.CLEAR_ASSIGNMENT` (defined in [`constants.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/constants.cs)), which calls `clearAssignmentToEeprom()` in [`Main.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.cpp) to reset the EEPROM region.

## Summary

- **Three-layer architecture**: The system combines a SimHub C# UI, ESP-NOW wireless transport, and ESP-32 EEPROM persistence to manage pedal roles

- **Enum-based assignment**: `SET_ASSIGNMENT_0/1/2` map to clutch/brake/throttle through the `PedalSystemAction` enum in [`constants.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/constants.cs)
- **Persistent storage**: Assignments survive reboots via `writeAssignmentToEeprom()` and require an `ESP.restart()` to activate
- **Bridge synchronization**: `BridgeSerialTimer*.cs` maintains `PedalAvailability` state, keeping the UI synchronized with hardware connections
- **MAC-based targeting**: Unassigned pedals are identified by their unique MAC addresses stored in `_calculations.unassignedPedalMacaddress`

## Frequently Asked Questions

### How does the system distinguish between clutch, brake, and throttle assignments?

The system uses the `PedalSystemAction` enum defined in [`SimHubPlugin/VariablesStruct/constants.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/SimHubPlugin/VariablesStruct/constants.cs) to specify roles. Values `SET_ASSIGNMENT_0`, `SET_ASSIGNMENT_1`, and `SET_ASSIGNMENT_2` map to `PEDAL_ID_CLUTCH`, `PEDAL_ID_BRAKE`, and `PEDAL_ID_THROTTLE` respectively in the ESP-32 firmware. The `deviceID` field in the `dap_assignement_reg` structure stores this assignment persistently in EEPROM.

### Why does the ESP-32 restart after receiving an assignment command?

The firmware calls `ESP.restart()` in [`ESP32/src/Main.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/src/Main.cpp) after invoking `writeAssignmentToEeprom()` to ensure the pedal initializes with its new logical identity from non-volatile storage. This reboot guarantees that the `dap_assignement_reg.deviceID` is active before the pedal begins transmitting bridge state packets, preventing identity confusion during the synchronization process.

### What prevents multiple pedals from claiming the same logical role?

The assignment logic in [`AssignmentConfigurationWindow.xaml.cs`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/AssignmentConfigurationWindow.xaml.cs) filters the `_unassignedPedalList` to show only pedals without current role assignments. Once a pedal is assigned, the `PedalAvailability` array updates via `BridgeSerialTimer*.cs`, and the UI disables the assignment button for that logical slot. The firmware does not enforce exclusive roles at the ESP-32 level; coordination happens through the SimHub plugin's state management.

### Can assignments persist across power cycles without the SimHub plugin?

Yes. The `writeAssignmentToEeprom()` function in [`ESP32/src/Main.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/src/Main.cpp) writes the assignment to the ESP-32's EEPROM with a checksum. When the pedal boots, it reads this stored configuration from EEPROM and assumes the assigned role immediately, allowing the hardware to function with the bridge board even if the SimHub PC is offline.