Skip to content

Commit 490f4c1

Browse files
committed
python-esp-bridge v0.0.1
Flash an ESP32 once, then drive every peripheral live from Python over USB serial - GPIO, PWM, ADC, DAC, touch, I2C, SPI, UART, Wi-Fi with TCP/UDP sockets through the radio, and BLE. Firmware (esp/, Arduino core 3.x, FreeRTOS): - Binary wire protocol: COBS framing + CRC16, sequence-correlated requests, asynchronous events, baud negotiation up to 921600 - Three-task architecture (bridge_tx / bridge_rx / bridge_net) pinned to the app core - blocking Wi-Fi/BLE work never stalls GPIO/I2C/SPI - Modules: GPIO with edge interrupts + debounce, ADC, DAC + cosine generator, touch, LEDC PWM, I2C, SPI, UART, Wi-Fi STA/AP/scan, TCP/UDP socket proxy with credit-window flow control, BLE scan/advertise/GATT - Persistent device name in NVS for multi-board setups - Targets: classic ESP32 (primary) and ESP32-S3, capability flags at connect time Python package (src/, PyPI "python-esp-bridge", import "espbridge"): - Bridge with port auto-detection (CP210x/CH340/CH9102/native USB), reader thread, request correlation, event callbacks - Multi-device: Bridge(name=...), Bridge(mac=...), connect_all() - Sub-APIs: gpio, adc, dac, touch, pwm, i2c, spi, uart, wifi, net, ble - Native OLED driver: SSD1306/SH1106 and the ubiquitous clones, PIL drawing, dual-chip-safe power-up, named panel controls (contrast, invert, flip, offsets), I2C transactions sized to Wire buffer limits - Ecosystem integrations: gpiozero pin factory, CircuitPython busio/digitalio shim (Adafruit drivers run unchanged), smbus2 API, luma.oled/luma.lcd interfaces, RPi.GPIO-style module - espbridge CLI: info, ports, set-name Also: 68 hardware-free tests against a fake firmware, runnable examples (native API in examples/, one folder per ecosystem), wire-protocol spec in docs/PROTOCOL.md, CI for tests + firmware compile (esp32, esp32s3) and PyPI trusted publishing on version tags.
1 parent 35b9f76 commit 490f4c1

81 files changed

Lines changed: 7635 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: HamzaYslmn

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
pytest:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python: ["3.10", "3.13"]
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: astral-sh/setup-uv@v5
19+
with:
20+
python-version: ${{ matrix.python }}
21+
22+
- name: Run tests
23+
run: uv run --with pytest --with pyserial --with pillow --with gpiozero pytest tests/ -q

.github/workflows/pypi.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Build & Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
pull_request:
8+
9+
jobs:
10+
# MARK: - Build sdist + universal wheel
11+
# espbridge is pure-Python, so one `py3-none-any` wheel covers every platform.
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: astral-sh/setup-uv@v5
18+
19+
- name: Build sdist + wheel
20+
working-directory: src
21+
run: uv build -o ../dist/dist-build
22+
23+
- uses: actions/upload-artifact@v4
24+
with:
25+
name: dist
26+
path: dist/dist-build/*
27+
28+
- name: Clean local build folder
29+
run: rm -rf dist/dist-build
30+
31+
# MARK: - Publish to PyPI on tag push (trusted publishing via OIDC)
32+
publish:
33+
needs: build
34+
if: startsWith(github.ref, 'refs/tags/v')
35+
# Triggered only by v*.*.* tags (see `on.push.tags` above).
36+
runs-on: ubuntu-latest
37+
environment: pypi
38+
permissions:
39+
id-token: write
40+
steps:
41+
- uses: actions/download-artifact@v4
42+
with:
43+
name: dist
44+
path: dist/dist-build
45+
46+
- uses: pypa/gh-action-pypi-publish@release/v1
47+
with:
48+
packages-dir: dist/dist-build

LICENSE

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
Forza Horizon — DualSense Adaptive Triggers
2+
Attribution & Sponsor License (v1.0)
3+
Copyright (C) 2025 Hamza Yeşilmen ("HamzaYslmn")
4+
5+
================================================================================
6+
ATTRIBUTION & SPONSOR LICENSE
7+
================================================================================
8+
9+
This is a custom, source-available license. It is NOT an OSI-approved or
10+
FSF-approved "open source" / "free software" license. Read these terms before
11+
using, copying, modifying, or distributing this software ("the Software").
12+
13+
1. GRANT OF RIGHTS
14+
15+
Subject to the conditions below, the copyright holder grants you a
16+
worldwide, royalty-free, non-exclusive license to:
17+
18+
(a) use and run the Software for any purpose, personal or commercial;
19+
(b) study and modify the Software and create derivative works; and
20+
(c) reproduce and distribute the Software and your derivative works, in
21+
source or compiled form.
22+
23+
2. CONDITIONS
24+
25+
The rights in Section 1 are granted only if you meet ALL of the following
26+
conditions in every copy or derivative work you distribute or make publicly
27+
available (including over a network):
28+
29+
2.1 ATTRIBUTION.
30+
You must preserve, and must not remove, hide, or alter, the following
31+
attribution:
32+
33+
"Originally created by Hamza Yeşilmen (HamzaYslmn)."
34+
Source: https://github.com/HamzaYslmn/python-esp-bridge
35+
36+
You may add your own attribution for your changes, but not in a way that
37+
removes, obscures, or misrepresents the attribution above.
38+
39+
2.2 SPONSOR LINK.
40+
You must include the following sponsor link, in a place where users of
41+
your distribution can reasonably find it:
42+
43+
Sponsor: https://github.com/sponsors/HamzaYslmn
44+
45+
If your distribution has any interactive user interface (for example an
46+
"about", "credits", or "settings" screen), the sponsor link and the
47+
attribution in 2.1 must be visible there in a form the user can read and
48+
follow. If there is no such interface, they must remain in the
49+
accompanying documentation (such as a README) and in the source files in
50+
which they already appear.
51+
52+
2.3 LICENSE NOTICE.
53+
You must include a copy of this license with every distribution, and you
54+
must keep intact all notices stating that this license applies.
55+
56+
2.4 NO MISREPRESENTATION OF ORIGIN.
57+
You must not misrepresent the origin of the Software, and you must mark
58+
modified versions in a reasonable way as different from the original so
59+
they are not mistaken for it.
60+
61+
3. NO USE OF NAME FOR PUBLICITY
62+
63+
This license does not grant any right to use the author's name, the
64+
"HamzaYslmn" mark, or the project name for advertising, endorsement, or
65+
publicity beyond the factual attribution required in Section 2.1, without
66+
prior written permission.
67+
68+
4. TERMINATION
69+
70+
If you fail to comply with these conditions, the rights granted in Section 1
71+
terminate automatically. They are reinstated if you cure the violation
72+
within 30 days of becoming aware of it.
73+
74+
5. DISCLAIMER OF WARRANTY AND LIABILITY
75+
76+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
77+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
78+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
79+
AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
80+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
81+
OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
82+
SOFTWARE.
83+
84+
================================================================================
85+
For other licensing arrangements, contact the copyright holder.
86+
================================================================================

README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# python-esp-bridge
2+
3+
Plug an ESP32 into a Raspberry Pi (or any PC) over USB and drive **every**
4+
ESP32 peripheral live from Python — GPIO, PWM, ADC, DAC, capacitive touch,
5+
I2C, SPI, extra UARTs, Wi-Fi (including TCP/UDP sockets through the ESP32
6+
radio) and BLE. Flash the bridge firmware **once**; after that, everything is
7+
Python on the host. No reflashing per project.
8+
9+
```
10+
┌────────────────┐ USB serial, binary protocol ┌─────────────────────┐
11+
│ Pi / PC │ ───────────────────────────────►│ ESP32 (bridge fw) │
12+
│ Python: │ COBS+CRC16, up to 921600 Bd │ FreeRTOS tasks: │
13+
│ espbridge │ ◄─────────────────────────────── │ tx / rx / network │
14+
└────────────────┘ replies + async events └─────────────────────┘
15+
```
16+
17+
## Quick start
18+
19+
1. **Flash the firmware once** — open [`esp/esp.ino`](esp/) in Arduino IDE
20+
(esp32 core 3.x, partition scheme *Huge APP*), hit Upload.
21+
Details: [`esp/README.md`](esp/README.md).
22+
2. **Install the Python library** on the Pi/PC:
23+
24+
```sh
25+
pip install python-esp-bridge
26+
```
27+
28+
3. **Go:**
29+
30+
```python
31+
from espbridge import Bridge
32+
33+
with Bridge() as esp: # auto-detects the USB port
34+
print(esp.info) # chip, MAC, capabilities
35+
36+
esp.gpio.mode(2, "output") # like RPi GPIO, but on the ESP32
37+
esp.gpio.write(2, 1)
38+
print(esp.adc.read_mv(34), "mV")
39+
esp.dac.write(25, 128) # true analog out (classic ESP32)
40+
esp.pwm.servo(13, angle=90)
41+
42+
esp.i2c.init(sda=21, scl=22)
43+
print([hex(a) for a in esp.i2c.scan()])
44+
45+
esp.wifi.connect("ssid", "password") # the ESP32's radio...
46+
status, body = esp.net.http_get("http://example.com/") # ...as your modem
47+
```
48+
49+
`espbridge` on the command line prints connection info; `espbridge ports`
50+
lists candidate serial ports.
51+
52+
## Features
53+
54+
| module | highlights |
55+
|--------|------------|
56+
| GPIO | modes incl. pull-up/down & open-drain, batch writes, edge interrupts with debounce → Python callbacks |
57+
| ADC | raw + calibrated mV, attenuation config (ADC2/Wi-Fi conflict guarded) |
58+
| DAC | 8-bit output + hardware cosine generator (classic ESP32 / S2) |
59+
| PWM | LEDC: any pin, freq/resolution, `duty_pct`, `tone`, `servo` |
60+
| Touch | capacitive touch pad reads |
61+
| I2C | 2 buses, scan, write/read, register helpers, repeated-start |
62+
| SPI | 2 hosts, full-duplex transfers, CS handling |
63+
| UART | UART1/2 bridged: write from Python, RX streamed back as events |
64+
| Wi-Fi | scan, STA join, AP mode, status/RSSI, state events |
65+
| NET | TCP client/server + UDP **through the ESP32 radio**, socket-like API, credit-window flow control |
66+
| BLE | scan, advertise, GATT server (notify/write callbacks), GATT client |
67+
68+
The firmware is fully event-driven on FreeRTOS: serial TX, command handling
69+
and the network stack run as separate tasks, so a blocking Wi-Fi/BLE
70+
operation never delays a GPIO read (~1 ms round-trips at 921600 Bd).
71+
72+
## Use the libraries you already know
73+
74+
espbridge speaks the wire protocols of the popular Python hardware ecosystems,
75+
so existing code, drivers and tutorials run unchanged — the ESP32's pins just
76+
take the place of the Pi's:
77+
78+
**gpiozero** — full pin factory (LED, Button, PWMLED, edge callbacks, …):
79+
80+
```python
81+
from gpiozero import LED, Button
82+
from espbridge.compat.gpiozero import EspBridgeFactory
83+
84+
factory = EspBridgeFactory(esp)
85+
led, btn = LED(2, pin_factory=factory), Button(4, pin_factory=factory)
86+
btn.when_pressed = led.toggle
87+
```
88+
89+
**Adafruit CircuitPython drivers** (hundreds of sensors/displays) — busio/digitalio-compatible I2C, SPI and DigitalInOut:
90+
91+
```python
92+
from adafruit_bme280.basic import Adafruit_BME280_I2C
93+
from espbridge.compat.blinka import I2C
94+
95+
bme = Adafruit_BME280_I2C(I2C(esp)) # the driver doesn't know it's bridged
96+
print(bme.temperature)
97+
```
98+
99+
**smbus2** — classic Pi I2C code, unchanged:
100+
101+
```python
102+
from espbridge.compat.smbus import SMBus
103+
bus = SMBus(esp) # instead of smbus2.SMBus(1)
104+
temp = bus.read_byte_data(0x48, 0x00)
105+
```
106+
107+
**luma.oled / luma.lcd** — I2C and SPI display interfaces (`LumaI2C`, `LumaSPI`),
108+
**RPi.GPIO**`espbridge.compat.rpi_gpio` shim, and the native objects follow
109+
stdlib conventions too: UART ports are pyserial-like (`in_waiting`, `readline`),
110+
bridged TCP/UDP sockets support `settimeout`/`recv`/`sendall`.
111+
112+
I2C OLEDs (SSD1306 / SH1106 / the ubiquitous clones) are supported directly —
113+
`pip install "python-esp-bridge[oled]"`, draw with PIL:
114+
115+
```python
116+
from espbridge.oled import OLED
117+
118+
oled = OLED(esp) # bus init + auto-detect + clone-safe power-up
119+
with oled.draw() as d: # d is a PIL ImageDraw
120+
d.text((0, 10), "Hello!", fill="white")
121+
```
122+
123+
### Multiple ESP32s
124+
125+
Give each board a persistent name once (`espbridge -p COM7 set-name relays`
126+
stored in the ESP32's flash, survives reboots and port renumbering), then:
127+
128+
```python
129+
import espbridge
130+
from espbridge import Bridge
131+
132+
esp = Bridge(name="relays") # or Bridge(mac="aa:bb:cc:dd:ee:ff")
133+
134+
with espbridge.connect_all() as boards: # or just open all of them
135+
boards.by_name("sensors").adc.read(34)
136+
boards.by_name("relays").gpio.write(2, 1)
137+
```
138+
139+
## Repo layout
140+
141+
| path | what |
142+
|------|------|
143+
| [`esp/`](esp/) | Arduino firmware (flash once) |
144+
| [`src/`](src/) | Python package `python-esp-bridge` (import `espbridge`) |
145+
| [`examples/`](examples/) | native API examples in the root; per-ecosystem folders (`gpiozero/`, `adafruit/`, `luma/`, `smbus/`, `rpi_gpio/`) for the compat shims |
146+
| [`tests/`](tests/) | hardware-free protocol/bridge tests (`pytest tests/`) |
147+
| [`docs/PROTOCOL.md`](docs/PROTOCOL.md) | binary wire protocol spec |
148+
149+
## Supported hardware
150+
151+
Primary target: classic **ESP32** DevKits (ESP-32S / ESP-32D, 30- and 38-pin,
152+
CP2102/CH340 USB). **ESP32-S3** builds via the same sketch (native USB; no DAC,
153+
BLE-only). Capabilities are reported by the firmware at connect time, so the
154+
Python API fails fast with a clear error for anything your chip lacks.

0 commit comments

Comments
 (0)