# Implementing Custom Protocols with ESP32-Bit-Pirate Bit-Banging: A Complete Guide

> Implement custom protocols with ESP32-Bit-Pirate bit-banging using reusable primitives. Compose arbitrary protocols via GPIO-level operations without firmware changes. Get the complete guide now.

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

---

**The ESP32-Bit-Pirate enables custom protocol implementation through reusable bit-bang primitives exposed by its service-oriented architecture, allowing arbitrary protocols to be composed via GPIO-level operations without modifying core firmware.**

The ESP32-Bit-Pirate firmware implements a modular **service-oriented architecture** where each digital protocol operates as an independent service. These services expose low-level primitives—`writeBit`, `readBit`, `togglePin`, and bulk operations—that you can combine to implement **implementing custom protocols with ESP32-Bit-Pirate bit-banging** for virtually any digital signaling scheme.

## Architecture Overview

The firmware organizes functionality into four distinct layers. Understanding these layers is essential for effective custom protocol development.

| Layer | Files | Responsibility |
|-------|-------|----------------|
| **Hardware abstraction** | `src/Boards/*` | Board-specific GPIO mappings and pin configurations |
| **Service layer** | `src/Services/*` | Protocol-specific bit-bang API implementations |
| **Model layer** | `src/Models/*` | Generic data structures for bytecode and bitstreams |
| **Command parser** | `src/Commands/*` | CLI command routing and registration |

The **service layer** is where custom protocol implementation primarily occurs. In [`src/Services/JtagService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/JtagService.h), you'll find generic bit-bang primitives including `writeBit`, `shiftArray`, and `sendData`. Similarly, [`src/Services/TwoWireService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/TwoWireService.h) provides I²C-style operations through `i2cBitBangWriteBit` that work on arbitrary GPIO pairs.

## Bit-Bang Primitives Available

Each service exposes timing-accurate GPIO controls suitable for custom protocol construction:

- **`writeBit(bool)`** — Single bit output with configurable timing
- **`writeBits(uint32_t, int)`** — Bulk bit transfer for efficient transmission
- **`togglePin(uint8_t)`** — Raw GPIO pin manipulation
- **`i2cBitBangWriteBit(scl, sda, level, delay_us)`** — Clock-data paired output
- **`sendBinRaw_(vector<uint8_t>, te_us, bits, msb_first, invert)`** — High-level raw transmission

These primitives operate directly on GPIO pins, enabling any protocol expressible as precise 0/1 level sequences.

## Implementing a Manchester-Encoded Protocol

Manchester encoding represents each data bit as a transition rather than a level. Here's a complete implementation using the `I2cService` bit-bang primitives.

### Encoding Helper

```cpp
// File: src/Commands/CustomProtocolCommands.cpp
#include "Services/I2cService.h"
#include "Models/Bpio2.h"
#include <vector>
#include <string>

// Manchester encode: 0 → low-to-high, 1 → high-to-low
static std::vector<bool> manchesterEncode(uint8_t byte) {
    std::vector<bool> bits;
    for (int i = 0; i < 8; ++i) {
        bool b = (byte >> i) & 0x01;
        bits.push_back(b ? true : false);   // first half
        bits.push_back(b ? false : true);   // second half (inverted)
    }
    return bits;
}

```

### Command Implementation

```cpp
void cmdManTx(const std::vector<std::string>& args) {
    if (args.size() != 2) {
        Serial.println(F("Usage: mantx <hex-byte>"));
        return;
    }

    uint8_t payload = strtoul(args[1].c_str(), nullptr, 16);
    auto encoded = manchesterEncode(payload);

    auto& i2c = I2cService::instance();
    i2c.configurePins(GPIO_NUM_22, GPIO_NUM_21);  // SCL, SDA
    i2c.setBitBangMode(true);                      // enable raw bit-bang

    const uint32_t halfPeriodUs = 200;
    for (bool level : encoded) {
        i2c.i2cBitBangWriteBit(
            GPIO_NUM_22,      // clock pin
            GPIO_NUM_21,      // data pin
            level,            // level to output
            halfPeriodUs      // timing per half-bit
        );
    }
    Serial.println(F("Manchester packet sent"));
}

```

The `i2cBitBangWriteBit` function in [`src/Services/TwoWireService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/TwoWireService.h) handles the microsecond-precise timing while you supply the logical bit sequence.

## Raw Sub-GHz Protocol Implementation

For radio-frequency protocols, `SubGhzService` provides `sendBinRaw_`—a high-level wrapper that abstracts the underlying bit-bang operations.

```cpp
// File: src/Commands/CustomProtocolCommands.cpp
#include "Services/SubGhzService.h"

void cmdRawSubGhz(const std::vector<std::string>& args) {
    if (args.size() != 2) {
        Serial.println(F("Usage: rawsub <hex-bitstream>"));
        return;
    }

    std::vector<uint8_t> rawBytes;
    const std::string& hex = args[1];
    for (size_t i = 0; i < hex.length(); i += 2) {
        uint8_t byte = strtoul(hex.substr(i, 2).c_str(), nullptr, 16);
        rawBytes.push_back(byte);
    }

    const int te_us = 250;                           // bit period
    const int bits = 8 * rawBytes.size();            // total bits
    bool ok = SubGhzService::instance().sendBinRaw_(
        rawBytes,    // payload
        te_us,       // microseconds per bit
        bits,        // bit count
        true,        // MSB-first
        false        // no inversion
    );
    Serial.println(ok ? F("Raw Sub-GHz sent") : F("Failed"));
}

```

The `sendBinRaw_` method in [`src/Services/SubGhzService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/SubGhzService.h) internally calls low-level bit-bang routines, allowing custom OOK, FSK, or proprietary modulations through timing and polarity adjustments.

## Registering Custom Commands

New protocols must be exposed through the command parser in [`src/Commands/ProtocolCommands.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Commands/ProtocolCommands.cpp) or a dedicated file:

```cpp
// File: src/Commands/CommandParser.cpp
void registerCustomCommands() {
    CommandParser::instance().addCommand(
        "mantx",
        "Transmit Manchester-encoded byte (hex)",
        cmdManTx);

    CommandParser::instance().addCommand(
        "rawsub",
        "Send raw Sub-GHz bitstream (hex)",
        cmdRawSubGhz);
}

```

Once registered, commands are available across all three interfaces: **serial console**, **web CLI**, and **standalone mode**.

## Key Source Files for Custom Protocol Development

| File | Purpose |
|------|---------|
| [`src/Services/JtagService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/JtagService.h) | Generic bit-bang: `writeBit`, `shiftArray`, `sendData` |
| [`src/Services/TwoWireService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/TwoWireService.h) | I²C-style bit-bang: `i2cBitBangWriteBit` |
| [`src/Services/SubGhzService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/SubGhzService.h) | RF transmission: `sendBinRaw_` |
| [`src/Models/Bpio2.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Models/Bpio2.h) | Bytecode/bitstream data structures |
| [`src/Commands/ProtocolCommands.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Commands/ProtocolCommands.cpp) | Command registration patterns |

## Timing Considerations for Bit-Banging

The ESP32-Bit-Pirate achieves precise timing through:

1. **Busy-wait delays** in service implementations rather than RTOS task delays
2. **Configurable `te_us` parameters** per bit or per transition
3. **GPIO direct register access** for sub-microsecond switching where hardware permits

For protocols requiring sub-10µs precision, verify your ESP32 variant's GPIO performance and consider the `GPIO_NUM_*` constants defined in [`src/Boards/Common/Views/St7789SpiDeviceView.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Boards/Common/Views/St7789SpiDeviceView.h) for valid pin assignments.

## Summary

- **ESP32-Bit-Pirate bit-banging** relies on service-layer primitives that abstract hardware-specific GPIO operations
- **`I2cService`** and **`TwoWireService`** provide clocked and unclocked bit-bang modes for wired protocols
- **`SubGhzService`** enables custom RF protocols through `sendBinRaw_` with configurable timing
- **Command registration** in [`ProtocolCommands.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/ProtocolCommands.cpp) exposes new protocols to all user interfaces
- **No core firmware modification** is required—custom protocols compose existing primitives

## Frequently Asked Questions

### What is the minimum bit period achievable with ESP32-Bit-Pirate bit-banging?

Practical limits depend on the ESP32 variant and GPIO configuration. The firmware uses microsecond-precision delays (`te_us` parameters), so sub-10µs bit periods are achievable on supported hardware. For exact limits, profile `i2cBitBangWriteBit` or `sendBinRaw_` calls on your specific board.

### Can I implement protocols requiring synchronous bidirectional communication?

Yes. The service primitives include `readBit` operations alongside `writeBit`. Implementations in [`src/Services/JtagService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/JtagService.h) demonstrate bidirectional patterns—examine `shiftArray` for simultaneous read-write operations suitable for SPI-like protocols.

### Do I need to reflash firmware for every protocol change?

Protocol logic resides in the service and command layers. If you modify only [`ProtocolCommands.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/ProtocolCommands.cpp) or add modular command files, you rebuild and reflash. For rapid prototyping, consider using the bytecode interpreter referenced in [`src/Models/Bpio2.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Models/Bpio2.h) to define protocols without C++ recompilation.

### How do I select GPIO pins for custom protocols?

Consult [`src/Boards/Common/Views/St7789SpiDeviceView.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Boards/Common/Views/St7789SpiDeviceView.h) for board-specific pin mappings. The `configurePins()` method on services accepts `GPIO_NUM_*` constants. Ensure selected pins support your required direction (input/output) and electrical characteristics for the target protocol.