# 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](docs/img/oled.png)

## Quick start

1. **Flash the firmware once** — 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`](docs/FIRMWARE.md).
2. **Install the Python library** on the Pi/PC:

   ```sh
   pip install python-esp-bridge          # USB only
   pip install "python-esp-bridge[ble]"   # + Bluetooth support
   ```

3. **Go:**

   ```python
   from espbridge import Bridge

   with Bridge() as esp:                      # auto-detects the USB port
       print(esp.info)                        # chip, MAC, capabilities

       esp.gpio.mode(2, "output")             # like RPi GPIO, but on the ESP32
       esp.gpio.write(2, 1)
       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):

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

   `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, 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 primitives — no
firmware changes to add your own):

```python
from espbridge.neopixel import NeoPixel   # WS2812/SK6812 strips
from espbridge.dht import DHT             # DHT11/DHT22 temp+humidity
from espbridge.ds18b20 import DS18B20     # 1-Wire thermometers (multi-drop)
from espbridge.hcsr04 import HCSR04       # ultrasonic ranging
from espbridge.ir import IrSender, IrReceiver  # NEC remotes + raw IR
from espbridge.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)
```

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).

## 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, …):

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

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

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

```python
from espbridge.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")
```

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

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

```python
esp = Bridge(retries=1)         # default: re-send safe commands once on timeout
esp.free_heap()                 # heap + dropped-frame counters from the firmware
```

```bash
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/`](src/) + [`examples/Bridge/`](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/) | Python package `python-esp-bridge` (import `espbridge`) with its own `tests/` and grouped `examples/` (`basics/`, `devices/`, `system/`, `wireless/`, `network/`, `displays/`, `compat/`) |
| [`docs/PROTOCOL.md`](docs/PROTOCOL.md) | binary wire protocol spec (framing, transports, auth) |
| [`docs/FIRMWARE.md`](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** build via the same sketch (native USB;
ESP-NOW works everywhere; no DAC on S3/C3). Capabilities are reported by the
firmware at connect time, so the Python API fails fast with a clear error for
anything your chip lacks.

> **Bluetooth note:** arduino-esp32 core 3.3.x ships the NimBLE host on
> S3/C3/C6 — 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 have
> everything regardless. The Python API raises a clear `UnsupportedError`
> either way (`Cap.SLEEP`, `Cap.SDMMC` probing).

### Per-chip support matrix (v0.3.0 modules)

| | ESP32 | S2 | S3 | C3 | C6 |
|---|---|---|---|---|---|
| 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) | ✓ | ✓ | ✓ | ✓ | ✓ |
