Skip to content

HamzaYslmn/python-esp-bridge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

70 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

python-esp-bridge

Connect an ESP32 to a Raspberry Pi (or any PC) over USB or Bluetooth and drive every ESP32 peripheral live from Python β€” GPIO, PWM, ADC, DAC, capacitive touch, I2C, SPI, extra UARTs, RMT pulse trains (NeoPixels, IR, DHT, ultrasonic, steppers), 1-Wire, CAN bus, I2S audio, files (LittleFS/SD), NVS storage, deep sleep, Wi-Fi (including TCP/UDP sockets through the ESP32 radio), Ethernet, camera, BLE and ESP-NOW β€” plus firmware updates over the link itself. Flash the bridge firmware once; after that, everything is Python on the host. No reflashing per project.

The design rule: the firmware exposes minimal hardware primitives; device protocols (WS2812 timing, NEC IR, DHT decoding, 1-Wire search, stepper ramps) are implemented in Python where they are easy to read, test and extend.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  USB serial (≀921600 Bd) or BLE  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pi / PC        β”‚ ───────────────────────────────► β”‚ ESP32 (bridge fw)   β”‚
β”‚ Python:        β”‚   binary protocol, COBS+CRC16    β”‚ FreeRTOS tasks:     β”‚
β”‚  espbridge     β”‚ ◄─────────────────────────────── β”‚  tx / rx / network  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        replies + async events    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Oled

Quick start

Works on Raspberry Pi OS, Linux, Windows, and macOS (requires Python β‰₯ 3.11).

  1. Flash the firmware once. No Arduino IDE needed β€” flash the bundled prebuilt firmware straight from the host (lists the serial ports, you pick one, it writes a Huge APP image with esptool):

    uvx --from "python-esp-bridge[flash]" flash   # zero-install via uv

    Prefer building it yourself? Install the python esp bridge library (Arduino IDE Library Manager), open File β†’ Examples β†’ python esp bridge β†’ Bridge, pick partition scheme Huge APP, hit Upload. The whole sketch is EspBridge.begin();. Details: docs/FIRMWARE.md.

  2. Install the Python library on the Pi/PC β€” with pip:

    pip install python-esp-bridge            # USB + Bluetooth, both included
    pip install "python-esp-bridge[oled]"    # + Pillow, for OLED displays
    pip install "python-esp-bridge[mcp]"     # + the MCP server (espbridge-mcp)
    pip install "python-esp-bridge[flash]"   # + esptool, for `espbridge flash`

    ...or with uv:

    uv add python-esp-bridge                 # into a uv project (USB + Bluetooth)
    uv add "python-esp-bridge[oled]"         # with an extra (oled / mcp / all)
    uv pip install python-esp-bridge         # ...or into the active environment

    Bluetooth works out of the box (no extra); the old [ble] extra is kept as a no-op for back-compat.

  3. Go:

    from espbridge import Bridge
    
    with Bridge() as esp:                      # Bluetooth first, then USB serial
        print(esp.info)                        # chip, MAC, capabilities
    
        esp.gpio.mode(2, "output")             # like RPi GPIO, but on the ESP32
        esp.gpio.write(2, 1)                    # returns the pin's read-back level
        esp.gpio.write(2, 1, verify=True)      # ...and raises if it didn't take
        print(esp.adc.read_mv(34), "mV")
        esp.dac.write(25, 128)                 # true analog out (classic ESP32)
        esp.pwm.servo(13, angle=90)
    
        esp.i2c.init(sda=21, scl=22)
        print([hex(a) for a in esp.i2c.scan()])
    
        esp.wifi.connect("ssid", "password")   # the ESP32's radio...
        status, body = esp.net.http_get("http://example.com/")  # ...as your modem

    Or with no USB cable at all β€” boards advertise as espbridge_<mac> (plus your custom name) and require a password (default espbridge, change it via EspBridge.begin("yourpassword") in the sketch):

    with Bridge(ble=True, password="espbridge") as esp:   # over Bluetooth
        esp.gpio.write(2, 1)

    Bridge() prefers Bluetooth and falls back to USB serial; pass ble=False for USB / COM only (Bluetooth off), ble="name" to pin a specific board over Bluetooth, or port="COM7" for a specific serial port.

    espbridge on the command line prints connection info; espbridge ports lists candidate serial ports; espbridge scan probes every attached board and espbridge scan --ble finds bridges advertising over Bluetooth.

Features

module highlights
GPIO modes incl. pull-up/down & open-drain, batch writes, writes return the pin's read-back level for confirmation (verify= raises on mismatch), edge interrupts with debounce β†’ Python callbacks
ADC raw + calibrated mV, attenuation config (ADC2/Wi-Fi conflict guarded)
DAC 8-bit output + hardware cosine generator (classic ESP32 / S2)
PWM LEDC: any pin, freq/resolution, duty_pct, tone, servo
Touch capacitive touch pad reads
I2C 2 buses, scan, write/read, register helpers, repeated-start
SPI 2 hosts, full-duplex transfers, CS handling
UART UART1/2 bridged: write from Python, RX streamed back as events
Wi-Fi scan, STA join, AP mode, status/RSSI, state events
NET TCP client/server + UDP through the ESP32 radio, socket-like API, credit-window flow control
BLE scan, advertise, GATT server (notify/write callbacks), GATT client
ESP-NOW connectionless ESP32↔ESP32 messaging: peers + broadcast, delivery ACKs, RX with RSSI, PMK/LMK encryption β€” coexists with Wi-Fi and BLE
RMT generic pulse-train play/capture β€” the one primitive behind neopixel, ir, dht, hcsr04, stepper (below)
1-Wire bus primitives on any pin; ROM search + CRC8 in Python (esp.onewire, DS18B20 driver included)
CAN TWAI controller: 25k–1M bit/s, filters, send/recv + callbacks (esp.can; transceiver chip required)
I2S PCM in/out for MEMS mics & DACs/amps (esp.i2s; link bandwidth caps rates ~16-bit/32 kHz mono)
Files LittleFS on internal flash + SD cards: open/read/write/list/… (esp.fs)
NVS persistent key/value storage on the board (esp.nvs)
Sleep deep + light sleep with timer/GPIO wake (esp.deep_sleep(); see chip notes)
OTA reflash the firmware over USB or Bluetooth (esp.ota.flash("fw.bin"); dual-app partition scheme)
Ethernet RMII (WT32-ETH01, Olimex POE…) or SPI (W5500) β€” NET sockets ride it automatically (firmware opt-in)
Camera JPEG snapshots from ESP32-CAM / XIAO-S3-Sense / ESP-EYE (firmware opt-in, PSRAM)
MCPWM complementary PWM pair with hardware deadtime for H-bridges (esp.mcpwm; not on S2/C3)

Device drivers in pure Python (over the RMT/1-Wire/I2C primitives β€” no firmware changes to add your own):

from espbridge.drivers.neopixel import NeoPixel   # WS2812/SK6812 strips
from espbridge.drivers.dht import DHT             # DHT11/DHT22 temp+humidity
from espbridge.drivers.ds18b20 import DS18B20     # 1-Wire thermometers (multi-drop)
from espbridge.drivers.hcsr04 import HCSR04       # ultrasonic ranging
from espbridge.drivers.ir import IrSender, IrReceiver  # NEC remotes + raw IR
from espbridge.drivers.stepper import Stepper     # A4988/DRV8825 with ramps

NeoPixel(esp, pin=5, n=30).fill((0, 0, 64))
print(DHT(esp, 4).read())                 # (23.1, 65.5)
Stepper(esp, step_pin=12, dir_pin=14).move(400, speed=800, accel=1600)

Bring your own driver

Those are reference implementations, not the limit. Every bundled driver lives in espbridge/drivers/, and a driver is just a Python class whose constructor takes the bridge and talks to a device over the primitives above β€” drivers/dht.py is 75 lines. So any sensor, display or protocol is a host-side class away, with no firmware change:

class MyTempSensor:                          # any class taking the bridge first
    def __init__(self, esp, address=0x48):
        self._i2c, self._addr = esp.i2c, address
    def read_c(self):
        hi, lo = self._i2c.read_reg(self._addr, 0x00, 2)
        return ((hi << 8 | lo) >> 4) * 0.0625

MyTempSensor(esp).read_c()                    # works as-is, nothing to register

Register a name for the esp.<name>(...) sugar, or ship a pip package that others install and your driver shows up on every bridge automatically:

from espbridge import register_driver
register_driver("mytemp", MyTempSensor)
esp.mytemp(address=0x48).read_c()             # == MyTempSensor(esp, address=0x48)

espbridge drivers lists everything available (the bundled drivers/ and any installed plugins). Full guide: docs/DRIVERS.md. And if a driver already exists in the Adafruit / luma / gpiozero / smbus2 ecosystems, it runs unchanged through the compat shims β€” no rewrite needed.

The firmware is fully event-driven on FreeRTOS: serial TX, command handling and the network stack run as separate tasks, so a blocking Wi-Fi/BLE operation never delays a GPIO read (~1 ms round-trips at 921600 Bd).

Concurrency & integration

A board's link can't be opened twice, but one Bridge is thread-safe β€” share it across threads and their requests pipeline on the wire, correlated by sequence number, so a slow call on one thread never stalls a fast call on another (the firmware runs a matching task split; see rtos_concurrency.py).

For easy integration, don't pass a Bridge around β€” call connect() anywhere and get the same shared, auto-reconnecting link:

import espbridge

esp = espbridge.connect(ble=False)      # same live link from any thread/module
esp.gpio.write(2, 1)                     # safe to call concurrently

# e.g. a FastAPI/Flask route β€” every request shares the one connection:
@app.get("/adc/{pin}")
def read(pin: int):
    return {"mV": espbridge.connect(ble=False).adc.read_mv(pin)}

For await, wrap any bridge β€” fan out concurrent I/O with asyncio.gather (async_fanout.py):

from espbridge import AsyncBridge

async with AsyncBridge(ble=False) as esp:        # or AsyncBridge.wrap(espbridge.connect())
    t, h = await asyncio.gather(esp.adc.read(34), esp.adc.read(35))

Multiple processes? One process owns the link (e.g. the MCP or an HTTP server) and the others talk to it. See shared_connection.py.

Use the libraries you already know

espbridge speaks the wire protocols of the popular Python hardware ecosystems, so existing code, drivers and tutorials run unchanged β€” the ESP32's pins just take the place of the Pi's:

gpiozero β€” full pin factory (LED, Button, PWMLED, edge callbacks, …):

from gpiozero import LED, Button
from espbridge.compat.gpiozero import EspBridgeFactory

factory = EspBridgeFactory(esp)
led, btn = LED(2, pin_factory=factory), Button(4, pin_factory=factory)
btn.when_pressed = led.toggle

Adafruit CircuitPython drivers (hundreds of sensors/displays) β€” busio/digitalio-compatible I2C, SPI and DigitalInOut:

from adafruit_bme280.basic import Adafruit_BME280_I2C
from espbridge.compat.blinka import I2C

bme = Adafruit_BME280_I2C(I2C(esp))     # the driver doesn't know it's bridged
print(bme.temperature)

smbus2 β€” classic Pi I2C code, unchanged:

from espbridge.compat.smbus import SMBus
bus = SMBus(esp)                        # instead of smbus2.SMBus(1)
temp = bus.read_byte_data(0x48, 0x00)

luma.oled / luma.lcd β€” I2C and SPI display interfaces (LumaI2C, LumaSPI), RPi.GPIO β€” espbridge.compat.rpi_gpio shim, and the native objects follow stdlib conventions too: UART ports are pyserial-like (in_waiting, readline), bridged TCP/UDP sockets support settimeout/recv/sendall.

I2C OLEDs (SSD1306 / SH1106 / the ubiquitous clones) are supported directly β€” pip install "python-esp-bridge[oled]", draw with PIL:

from espbridge.drivers.oled import OLED

oled = OLED(esp)                # bus init + auto-detect + clone-safe power-up
with oled.draw() as d:          # d is a PIL ImageDraw
    d.text((0, 10), "Hello!", fill="white")

Drive it from an AI agent (MCP)

Expose the whole bridge to an LLM as a Model Context Protocol server β€” 100+ tools covering every peripheral (GPIO, ADC/DAC, PWM, I2C, SPI, UART, Wi-Fi, NVS, filesystem, 1-Wire, ESP-NOW, CAN, MCPWM, Ethernet, camera, OTA). The agent can then read sensors, toggle pins, scan I2C and more in plain language.

Install the server once, then plug in the board β€” the port auto-detects:

uv tool install "python-esp-bridge[mcp]"     # or: pip install "python-esp-bridge[mcp]"

Works with Claude Code, Gemini CLI, Codex CLI, Antigravity, Cursor/Windsurf and Ollama β€” all launch the same espbridge-mcp command. This repo ships ready configs for Claude Code (.mcp.json) and Gemini CLI (.gemini/settings.json): just open the assistant in the repo. Every other client uses the same one-line config (key mcpServers):

{ "mcpServers": {
    "espbridge": { "command": "espbridge-mcp", "args": [] }
} }

Tools are grouped by peripheral (gpio_*, i2c_*, wifi_*, …); raw byte payloads go in and out as hex strings. Embed it in your own server with from espbridge.mcp import build_server. Per-assistant setup (incl. Codex, Antigravity, Ollama): docs/MCP.md.

Multiple ESP32s

Give each board a persistent name once (espbridge -p COM7 set-name relays β€” stored in the ESP32's flash, survives reboots and port renumbering), then:

import espbridge
from espbridge import Bridge

esp = Bridge(name="relays")                  # or Bridge(mac="aa:bb:cc:dd:ee:ff")

with espbridge.connect_all() as boards:    # or just open all of them
    boards.by_name("sensors").adc.read(34)
    boards.by_name("relays").gpio.write(2, 1)

Troubleshooting

Errors name the command and say what to check (I2C_WRITE (0x4003) failed: IO β€” no ACK on the wire β€” check wiring, power, device address and pull-ups). A timeout additionally pings the board so the message tells you whether the link itself died or a single frame got lost. Useful knobs:

esp = Bridge(retries=1)         # default: re-send safe commands once on timeout
esp.free_heap()                 # heap + dropped-frame counters from the firmware
ESPBRIDGE_DEBUG=1 python app.py   # trace every request/response with names

Lost frames on a busy link are also prevented now: pipelined bursts (OLED frames, NeoPixel updates) are automatically throttled to what the firmware's link buffer can absorb, on both USB serial and Bluetooth.

Repo layout

The repo root is the Arduino library (so it's publishable to the Arduino Library Manager); the Python package lives under python/.

path what
src/ + examples/Bridge/ Arduino library β€” the flash-once firmware (C/C++) + its example sketch (EspBridge.begin())
library.properties, keywords.txt Arduino Library Manager metadata (at the repo root, as the registry requires)
python/ Python package python-esp-bridge (import espbridge) with its own tests/ and grouped examples/ (basics/, devices/, system/, wireless/, network/, displays/, compat/)
docs/MCP.md MCP server (espbridge-mcp): drive the bridge from an AI agent
docs/PROTOCOL.md binary wire protocol spec (framing, transports, auth)
docs/FIRMWARE.md firmware flashing, partition scheme & build-flag reference

Supported hardware

Primary target: classic ESP32 DevKits (ESP-32S / ESP-32D, 30- and 38-pin, CP2102/CH340 USB). ESP32-S2/S3/C3/C6/H2 build via the same sketch (native USB; ESP-NOW works everywhere; no DAC on S3/C3/C6/H2). Capabilities are reported by the firmware at connect time, so the Python API fails fast with a clear error for anything your chip lacks.

Tested with arduino-esp32 core 3.3.6 on classic ESP32 (CP2102), flashed with the Minimal SPIFFS partition scheme (1.9 MB app + OTA β€” the firmware is ~95% of that slot; Huge APP also works if you don't need OTA). Verified: USB at 1.5 Mbaud, the BLE link, ESP-NOW, and Wi-Fi/BLE/ESP-NOW coexistence β€” see python/examples/wireless/stress_test.py for the soak/coex suite used.

Bluetooth note: arduino-esp32 core 3.x ships the NimBLE host on S3/C3/C6/H2 β€” the bridge's Bluetooth code (BLE link + esp.ble) speaks Bluedroid, so on those chips the firmware currently builds USB-only. Classic ESP32 keeps Bluedroid: full BLE link + Wi-Fi + ESP-NOW coexistence.

Classic-ESP32 IRAM trade-off: with Wi-Fi + Bluetooth both loaded the chip's instruction RAM is full, so the default classic build skips SD-card support (LittleFS still works) and deep/light sleep. Build with BRIDGE_ENABLE_BLE 0 (USB-only) to get SD + sleep back; S2/S3/C3/C6/H2 have everything regardless. The Python API raises a clear UnsupportedError either way (Cap.SLEEP, Cap.SDMMC probing).

Per-chip support matrix (v0.3.5 modules)

ESP32 S2 S3 C3 C6 H2
RMT / 1-Wire / CAN / I2S / NVS / OTA βœ“ βœ“ βœ“ βœ“ βœ“ βœ“
LittleFS βœ“ βœ“ βœ“ βœ“ βœ“ βœ“
SD (SPI) / sleep BLE off only βœ“ βœ“ βœ“ βœ“ βœ“
SDMMC slot BLE off only β€” βœ“ β€” β€” β€”
MCPWM (deadtime pair) βœ“ β€” βœ“ β€” βœ“ βœ“
Camera (opt-in) βœ“ (PSRAM) βœ“ (PSRAM) βœ“ (PSRAM) β€” β€” β€”
Ethernet RMII (opt-in) βœ“ β€” β€” β€” β€” β€”
Ethernet SPI W5500 (opt-in) βœ“ βœ“ βœ“ βœ“ βœ“ βœ“

About

Esp32 Python bridge, RPI bridge, its bridge, just πŸŒ‰

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors