How to Implement OTA Firmware Updates and WiFi Configuration for the DIY Sim-Racing FFB Pedal

The DIY Sim-Racing FFB Pedal implements over-the-air (OTA) firmware updates through a three-stage process: triggering via the OTA_enable_b flag, initializing WiFi in station or AP mode while de-initializing ESP-NOW, and using the ESP32OTAPull library to download and flash binaries from JSON-defined URLs.

The chrgri/diy-sim-racing-ffb-pedal repository provides a complete ESP32-based force-feedback pedal system with robust remote update capabilities. The firmware supports both client-mode OTA (connecting to existing networks) and AP-mode OTA (hosting a configuration portal), controlled through compile-time macros and runtime flags defined in Main.cpp and OTA_Pull.h.

Overview of the OTA Architecture

The OTA subsystem operates as a state machine within the OTATask FreeRTOS task. When activated, the system transitions through three distinct phases: trigger detection, network initialization, and binary flashing. This architecture ensures that ESP-NOW communication (used for pedal telemetry) is safely disabled before WiFi radio resources are claimed, preventing protocol conflicts.

The implementation relies on the external ESP32OTAPull library (fetched via PlatformIO) to handle HTTP downloads, JSON parsing, and partition writing. The pedal firmware wraps this library with hardware-specific initialization routines and user feedback mechanisms (LEDs and buzzer).

Stage 1: Triggering the OTA Update

Setting the OTA Enable Flag

OTA activation begins when any external interface sets the global boolean OTA_enable_b to true. This flag is monitored continuously within the OTATask loop in Firmware_for_V3/PedalFirmware/src/Main.cpp. Valid triggers include serial commands, ESP-NOW packets from a bridge device, or button combinations.

When triggered, the firmware immediately sets companion flags to manage peripheral states:

// Serial command handler excerpt from Main.cpp
if (cmd == "ota") {
    Serial.println("Get OTA command");
    OTA_enable_b = true;                  // Enable OTA routine
    OTA_enable_start = true;              // Immediate start flag
    ESPNow_OTA_enable = false;            // Disable ESP-NOW radio
}

Populating WiFi Credentials

Concurrent with flag activation, the triggering mechanism must populate the DAP_otaWifiInfo_st structure. This struct, defined in Firmware_for_V3/PedalFirmware/include/OTA_Pull.h, carries all network parameters and update preferences:

struct DAP_otaWifiInfo_st {
    uint8_t payloadType;
    uint8_t device_ID;
    uint8_t wifi_action;      // 0 = Station mode, 1 = AP mode
    uint8_t mode_select;      // 1=Main, 2=Dev, 3=Daily build
    uint8_t SSID_Length;
    uint8_t PASS_Length;
    uint8_t WIFI_SSID[30];
    uint8_t WIFI_PASS[30];
};

The mode_select field determines which JSON endpoint the OTA checker queries, allowing users to subscribe to stable releases, development branches, or daily builds.

Stage 2: WiFi Initialization and ESP-NOW Teardown

De-initializing ESP-NOW

Before activating WiFi, the OTATask performs critical resource management. ESP-NOW, the protocol used for low-latency pedal communication, utilizes the same radio hardware as WiFi station mode. The firmware explicitly de-initializes ESP-NOW to prevent conflicts:

// Inside OTATask loop (Main.cpp)
if (OTA_enable_b) {
    // Disable ESP-NOW to free radio resources
    ESPNow_OTA_enable = false;
    // ... LED feedback and buzzer indication
}

Station Mode vs. AP Mode

The firmware supports two distinct WiFi configurations, selected by the wifi_action field in the OTA structure.

Station Mode (wifi_action == 0) connects to an existing infrastructure network. The wifi_initialized() function in OTA_Pull.h handles the connection sequence:

// OTA_Pull.h – Station mode initialization
void wifi_initialized(char* Wifi_SSID, char* Wifi_PASS) {
    Serial.print("SSID: "); Serial.print(Wifi_SSID);
    Serial.print(" PASS: "); Serial.println(Wifi_PASS);
    
    WiFi.mode(WIFI_STA);
    WiFi.disconnect();
    delay(100);
    WiFi.begin(Wifi_SSID, Wifi_PASS);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nWiFi connected");
    Serial.print("RSSI: "); Serial.println(WiFi.RSSI());
}

AP Mode (wifi_action == 1) creates a soft access point with a captive web portal. This mode uses ota_wifi_initialize() and the WebServer class defined in ota.h, allowing users to upload firmware binaries via a browser without internet connectivity.

Stage 3: Downloading and Flashing Firmware

JSON Version Checking

Once WiFi is established, the OTATask instantiates the ESP32OTAPull class and queries a remote JSON manifest. The URL is selected based on the mode_select value from the OTA structure:

// URL definitions in OTA_Pull.h
#define JSON_URL_dev   "https://raw.githubusercontent.com/gilphilbert/pedal-flasher/main/json/dev/Version_ControlBoard.json"
#define JSON_URL_main  "https://raw.githubusercontent.com/gilphilbert/pedal-flasher/main/json/main/Version_ControlBoard.json"
#define JSON_URL_daily "https://raw.githubusercontent.com/ChrGri/DIY-Sim-Racing-FFB-Pedal/develop/OTA/DailyBuild/json/Version_ControlBoard.json"

The CheckForOTAUpdate() method compares the remote version string against the local DAP_FIRMWARE_VERSION (or forces an update if wifi_action == 1 by passing "0.0.0" as the version).

Binary Download and Partition Writing

If the JSON indicates a newer version, the library streams the binary from the provided URL directly to the ESP32's OTA partition. The pedal firmware configures the update to not automatically boot the new image until explicitly verified, using the UPDATE_BUT_NO_BOOT flag:

// Core OTA logic in Main.cpp
ESP32OTAPull ota;
ota.SetCallback(OTAcallback);
ota.OverrideBoard(CONTROL_BOARD);

// Force update check
const char *ver = (_dap_OtaWifiInfo_st.wifi_action == 1) ? "0.0.0" : DAP_FIRMWARE_VERSION;
char *version_tag = strdup(ver);

// Execute update check and download
int ret = ota.CheckForOTAUpdate(url, version_tag, ESP32OTAPull::UPDATE_BUT_NO_BOOT);
OTA_update_status = ret;

Restart and Verification

Upon successful download (ret == ESP32OTAPull::UPDATE_OK), the firmware immediately restarts the ESP32 to boot into the new firmware:

if (ret == ESP32OTAPull::UPDATE_OK) {
    ESP.restart();
}

Throughout the process, the firmware provides haptic and visual feedback via the Buzzer class and NeoPixel LEDs, indicating connection status, download progress, and success or failure states.

Alternative: AP Mode Web Interface

For environments without internet access, the firmware includes a captive portal implementation in ota.h. When wifi_action is set to AP mode, the ESP32 creates a soft access point and serves a gzipped HTML page containing a JavaScript uploader:

// ota.h – Web server setup for AP mode
const char *host = "esp32";
const char *password = "pedaladmin";
WebServer server(80);

void handleRoot() {
    server.sendHeader("Content-Encoding", "gzip");
    server.send(200, "text/html", (const char *)jquery_min_js_v3_2_1_gz,
                jquery_min_js_v3_2_1_gz_len);
}

Users connect to the esp32 network, enter the password pedaladmin, and upload firmware binaries directly through the browser interface.

Compile-Time Configuration

The OTA subsystem is conditionally compiled using macros defined in Firmware_for_V3/PedalFirmware/include/Main.h. This allows developers to create firmware variants with or without wireless update capabilities:

  • #define OTA_update – Enables client-mode OTA (connects to existing WiFi networks)
  • #define OTA_update_ESP32 – Enables AP-mode OTA (creates a web server soft-AP)
  • No OTA macro defined – Falls back to the minimal ota.h implementation containing only WiFi utilities

These macros guard the relevant code sections throughout Main.cpp and OTA_Pull.h, ensuring that unused networking code is excluded from builds where OTA is not required.

Summary

  • Trigger Mechanism: OTA begins when OTA_enable_b is set to true and the DAP_otaWifiInfo_st structure is populated with WiFi credentials and target URLs.
  • Network Transition: The OTATask in Main.cpp de-initializes ESP-NOW to free radio resources, then initializes WiFi in either station mode (wifi_initialized) or AP mode (ota_wifi_initialize).
  • Update Logic: The ESP32OTAPull library checks JSON manifests from JSON_URL_main, JSON_URL_dev, or JSON_URL_daily, downloads newer binaries, and flashes them to the OTA partition before restarting the ESP32.
  • Configuration: Compile-time macros (OTA_update, OTA_update_ESP32) in Main.h control which OTA features are included in the build.

Frequently Asked Questions

How do I trigger an OTA update manually via serial?

Send the command ota through the serial console. The firmware in Main.cpp detects this command, sets OTA_enable_b = true, disables ESP-NOW communication, and begins the WiFi connection sequence. Ensure you have previously configured the WiFi credentials through the DAP_otaWifiInfo_st structure or hardcoded them in your build.

What is the difference between Main, Dev, and Daily build channels?

The mode_select field in the DAP_otaWifiInfo_st structure determines which JSON manifest the firmware queries. Setting mode_select = 1 targets the stable Main channel (JSON_URL_main), 2 targets the Dev channel for beta testing (JSON_URL_dev), and 3 targets the Daily build channel (JSON_URL_daily) for bleeding-edge updates.

Can I update the firmware without internet access?

Yes, by using AP Mode (wifi_action = 1). In this mode, the ESP32 creates a soft access point named esp32 with password pedaladmin and serves a web upload interface from ota.h. Users connect to this WiFi network with a phone or laptop and upload the firmware binary directly through the browser, bypassing the need for internet connectivity or JSON manifests.

Why does the pedal disable ESP-NOW during OTA updates?

ESP-NOW and WiFi station mode share the same 2.4 GHz radio hardware on the ESP32. The OTATask explicitly sets ESPNow_OTA_enable = false and de-initializes the ESP-NOW stack before calling wifi_initialized() to prevent radio resource conflicts. This ensures stable TCP/IP connectivity required for downloading firmware binaries.

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 →