How to Integrate ESP32-Bit-Pirate with ESP-IDF: A Complete Hybrid Firmware Guide

ESP32-Bit-Pirate runs on the Arduino framework but freely mixes native ESP-IDF APIs, allowing seamless integration of low-level hardware control without sacrificing Arduino convenience.

The ESP32-Bit-Pirate firmware (geo-tp/ESP32-Bit-Pirate) demonstrates a powerful hybrid architecture. While it builds as an Arduino project through PlatformIO, it directly includes ESP-IDF headers like esp_system.h, esp_http_server.h, and esp_sleep.h throughout the codebase. This approach gives developers access to the full ESP32 hardware capabilities while maintaining the approachable Arduino programming model.

Layered Architecture of ESP32-Bit-Pirate

Understanding how the firmware structures its ESP-IDF integration helps you extend or migrate it effectively.

Project Configuration Layer

The build system lives in platformio.ini. This file specifies the Arduino framework as the primary target but compiles with the ESP-IDF toolchain via pioarduino's platform-espressif32. The key configuration enables dual-framework access:

[env]
platform = https://github.com/pioarduino/platform-espressif32.git
framework = arduino
build_flags = 
    -I$PLATFORMIO_FRAMEWORK_ARDUINO_ESP32_PATH/tools/sdk/esp32/include

Source: platformio.ini — central build configuration defining board, framework, and ESP-IDF include paths.

Core Services Layer

Wrapper classes expose ESP-IDF functionality through clean C++ interfaces. These files include native headers directly and abstract hardware details from the rest of the application.

Key implementations:

Input and UI Layer

Terminal front-ends bridge user interaction with hardware control:

Both can call ESP-IDF APIs like gpio_get_level() when direct hardware access is needed.

Command Dispatch Layer

src/Dispatchers/ActionDispatcher.cpp parses CLI commands and routes them to protocol controllers. While most controllers use Arduino APIs, the dispatcher enables ESP-IDF calls for advanced features like Wi-Fi deauthentication.

Hardware Controller Layer

Individual protocol drivers (I2C, SPI, LoRa, Sub-GHz) are Arduino-compatible classes that leverage ESP-IDF drivers for timing, DMA, or power management. Example: src/Controllers/LoraController.cpp.

Utilities Layer

Helper functions for flash OTA and filesystem operations use ESP-IDF APIs: esp_ota_*, esp_partition_*, and esp_http_server. See src/Services/UtilityService.cpp.

How ESP-IDF Integration Works in Practice

The PlatformIO Bridge

PlatformIO's configuration creates a hybrid build environment. The framework = arduino directive provides the Arduino core, while the ESP-IDF toolchain and SDK headers remain accessible. This eliminates the traditional either/or choice between frameworks.

Direct ESP-IDF Includes

Any source file can access ESP-IDF functionality by including the appropriate header:

// In src/Boards/WaveshareS3Geek/WaveshareS3GeekWifiSetup.cpp (line 7)
#include <esp_sleep.h>

The build flags in platformio.ini ensure these headers resolve without additional configuration.

API Mixing Strategy

Most code uses Arduino classes (Serial, Wire, SPI) for convenience. When performance or special features are required, ESP-IDF functions are called directly. This hybrid codebase allows any ESP-IDF API invocation from within Arduino sketches.

Practical Code Examples for ESP-IDF Integration

Using ESP-IDF from an Arduino Sketch

Deep sleep implementation mixing both frameworks:

#include <esp_sleep.h>
#include <Arduino.h>

void goToDeepSleep(uint64_t sleep_us) {
    // Disable Arduino's Serial before sleeping
    Serial.end();
    
    // Configure wake-up source on GPIO 0
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);
    
    // Enter deep sleep via ESP-IDF
    esp_deep_sleep_start();
}

Source pattern: src/Boards/WaveshareS3Geek/WaveshareS3GeekWifiSetup.cpp.

Adding a Pure ESP-IDF Service

Create OTA update capability using native ESP-IDF APIs:

// src/Services/OtaService.cpp
#include <esp_ota_ops.h>
#include <esp_partition.h>
#include <Arduino.h>

bool start_ota_update(const char *url) {
    const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
    if (!next) return false;
    
    // Use esp_http_client to download, then:
    // esp_ota_begin, esp_ota_write, esp_ota_end, esp_ota_set_boot_partition
    // Full implementation requires HTTP client setup and error handling
    
    return true;
}

Expose via command dispatcher in src/Dispatchers/ActionDispatcher.cpp:

else if (cmd == "ota") {
    const char *url = args[0];
    bool ok = start_ota_update(url);
    Serial.println(ok ? "OTA started" : "OTA failed");
}

Converting to an ESP-IDF Component

For pure ESP-IDF builds, wrap the src/ directory as a component:


# components/bitpirate/CMakeLists.txt

idf_component_register(
    SRCS
        src/Views/WebTerminalView.cpp
        src/Views/SerialTerminalView.cpp
        src/Managers/UserInputManager.cpp
        src/Services/SystemService.cpp
    INCLUDE_DIRS
        src
    REQUIRES
        Arduino
        esp_http_server
        esp_wifi
)

target_compile_definitions(${COMPONENT_LIB} PRIVATE
    DEVICE_S3DEVKIT
    LED_PIN=48
    LED_TYPE_RGB=1
)

Application entry point using ESP-IDF's app_main():

// main/app_main.cpp
extern void bitpirate_setup();
extern void bitpirate_loop();

extern "C" void app_main(void) {
    bitpirate_setup();
    while (true) {
        bitpirate_loop();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

The original Arduino setup()/loop() from src/main.cpp become callable functions.

Essential Source Files for Integration Work

File Purpose Direct Link
platformio.ini Build configuration, ESP-IDF flags View source
src/main.cpp Arduino entry point View source
src/Services/SystemService.cpp ESP-IDF system API wrapper View source
src/Servers/WebSocketServer.h HTTP/WebSocket server View source
src/Dispatchers/ActionDispatcher.cpp Command routing, extension point View source
src/Views/WebTerminalView.cpp Web terminal implementation View source
src/Views/SerialTerminalView.cpp Serial CLI implementation View source
src/Managers/UserInputManager.cpp Input coordination View source

Configuration via Build Flags

The firmware relies on -D macros for hardware mapping and module enablement. When migrating to pure ESP-IDF, preserve these in CMakeLists.txt:

target_compile_definitions(${COMPONENT_LIB} PRIVATE
    -DDEVICE_S3DEVKIT
    -DLED_PIN=48
    -DLED_TYPE_RGB=1
)

These definitions match the build_flags pattern in platformio.ini, ensuring consistent behavior across build systems.

Summary

  • ESP32-Bit-Pirate integrates ESP-IDF through PlatformIO's hybrid build, enabling Arduino and native ESP-IDF APIs in the same project
  • Direct header inclusion (esp_*.h) in source files like SystemService.cpp provides low-level hardware access
  • Service wrappers abstract ESP-IDF complexity while preserving full API availability for extensions
  • Component-style migration is achievable by wrapping src/ with a CMakeLists.txt and converting main.cpp to app_main()
  • Build flag preservation maintains hardware configuration across Arduino and ESP-IDF build environments

Frequently Asked Questions

Can I use ESP-IDF APIs without leaving the Arduino framework?

Yes. ESP32-Bit-Pirate demonstrates this throughout its codebase. Simply #include <esp_*.h> in any .cpp file. The PlatformIO configuration in platformio.ini ensures ESP-IDF headers are in the include path. Files like WaveshareS3GeekWifiSetup.cpp use esp_sleep.h directly while remaining Arduino sketches.

How do I add a new ESP-IDF feature to the existing command set?

Implement your feature in a new service file under src/Services/, then register it in src/Dispatchers/ActionDispatcher.cpp. Add an else if branch that parses your command, calls the ESP-IDF API, and returns results via Serial.println() or the web terminal. The dispatcher's modular structure makes this straightforward.

What changes when converting to a pure ESP-IDF component?

The primary changes are: (1) replace platformio.ini with a CMakeLists.txt using idf_component_register(), (2) move src/ to a components/ subdirectory, (3) convert setup()/loop() to bitpirate_setup()/bitpirate_loop() called from app_main(), and (4) keep the Arduino core as a required component if you still need Arduino helpers. All ESP-IDF includes remain valid.

Does the hybrid approach impact firmware size or performance?

The hybrid build minimally impacts size since unused ESP-IDF features are excluded by the linker. Performance actually improves when ESP-IDF APIs replace Arduino wrappers for timing-critical operations. The firmware's architecture specifically uses ESP-IDF for operations requiring precise control—deep sleep, Wi-Fi management, and OTA updates—while keeping Arduino APIs for convenience elsewhere.

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 →