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

> Learn to integrate ESP32-Bit-Pirate with ESP-IDF for hybrid firmware. Combine Arduino convenience with native ESP-IDF APIs for powerful hardware control.

- Repository: [Geo/ESP32-Bit-Pirate](https://github.com/geo-tp/ESP32-Bit-Pirate)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/esp_system.h), [`esp_http_server.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/esp_http_server.h), and [`esp_sleep.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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:

```ini
[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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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:
- [`src/Services/SystemService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/SystemService.cpp) — chip info, OTA, partition handling
- [`src/Servers/WebSocketServer.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Servers/WebSocketServer.h) — HTTP/WebSocket server using [`esp_http_server.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/esp_http_server.h)

### Input and UI Layer

Terminal front-ends bridge user interaction with hardware control:

- [`src/Views/SerialTerminalView.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Views/SerialTerminalView.cpp) — Arduino `Serial` streams
- [`src/Views/WebTerminalView.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Views/WebTerminalView.cpp) — Browser-based terminal mixing `Print` with ESP-IDF HTTP server

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

### Command Dispatch Layer

[`src/Dispatchers/ActionDispatcher.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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:

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

```

The build flags in [`platformio.ini`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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:

```cpp
#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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Boards/WaveshareS3Geek/WaveshareS3GeekWifiSetup.cpp).

### Adding a Pure ESP-IDF Service

Create OTA update capability using native ESP-IDF APIs:

```cpp
// 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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Dispatchers/ActionDispatcher.cpp):

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

```cmake

# 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()`:

```cpp
// 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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/main.cpp) become callable functions.

## Essential Source Files for Integration Work

| File | Purpose | Direct Link |
|------|---------|-------------|
| [`platformio.ini`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/platformio.ini) | Build configuration, ESP-IDF flags | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/platformio.ini) |
| [`src/main.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/main.cpp) | Arduino entry point | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/main.cpp) |
| [`src/Services/SystemService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/SystemService.cpp) | ESP-IDF system API wrapper | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Services/SystemService.cpp) |
| [`src/Servers/WebSocketServer.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Servers/WebSocketServer.h) | HTTP/WebSocket server | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Servers/WebSocketServer.h) |
| [`src/Dispatchers/ActionDispatcher.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Dispatchers/ActionDispatcher.cpp) | Command routing, extension point | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Dispatchers/ActionDispatcher.cpp) |
| [`src/Views/WebTerminalView.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Views/WebTerminalView.cpp) | Web terminal implementation | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Views/WebTerminalView.cpp) |
| [`src/Views/SerialTerminalView.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Views/SerialTerminalView.cpp) | Serial CLI implementation | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Views/SerialTerminalView.cpp) |
| [`src/Managers/UserInputManager.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Managers/UserInputManager.cpp) | Input coordination | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Managers/UserInputManager.cpp) |

## 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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/CMakeLists.txt):

```cmake
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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/CMakeLists.txt) and converting [`main.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/platformio.ini) ensures ESP-IDF headers are in the include path. Files like [`WaveshareS3GeekWifiSetup.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/WaveshareS3GeekWifiSetup.cpp) use [`esp_sleep.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/platformio.ini) with a [`CMakeLists.txt`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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.