python-esp-bridge 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- espbridge/__init__.py +45 -0
- espbridge/analog.py +64 -0
- espbridge/ble.py +223 -0
- espbridge/bridge.py +495 -0
- espbridge/cli.py +80 -0
- espbridge/compat/__init__.py +0 -0
- espbridge/compat/blinka.py +209 -0
- espbridge/compat/gpiozero.py +233 -0
- espbridge/compat/luma.py +90 -0
- espbridge/compat/rpi_gpio.py +121 -0
- espbridge/compat/smbus.py +63 -0
- espbridge/constants.py +199 -0
- espbridge/errors.py +38 -0
- espbridge/gpio.py +72 -0
- espbridge/i2c.py +65 -0
- espbridge/net.py +279 -0
- espbridge/oled.py +182 -0
- espbridge/protocol.py +159 -0
- espbridge/pwm.py +41 -0
- espbridge/spi.py +43 -0
- espbridge/transport.py +121 -0
- espbridge/uart.py +111 -0
- espbridge/wifi.py +128 -0
- python_esp_bridge-0.0.2.dist-info/METADATA +36 -0
- python_esp_bridge-0.0.2.dist-info/RECORD +27 -0
- python_esp_bridge-0.0.2.dist-info/WHEEL +4 -0
- python_esp_bridge-0.0.2.dist-info/entry_points.txt +2 -0
espbridge/spi.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""SPI master, full-duplex transfers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
from . import constants as C
|
|
7
|
+
|
|
8
|
+
# Max data per TRANSFER frame: payload cap minus host(1) + cs(1) header bytes.
|
|
9
|
+
_CHUNK = C.MAX_PAYLOAD - 2
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Spi:
|
|
13
|
+
def __init__(self, bridge):
|
|
14
|
+
self._b = bridge
|
|
15
|
+
|
|
16
|
+
def init(self, *, sck: int = 18, miso: int = 19, mosi: int = 23,
|
|
17
|
+
freq: int = 1_000_000, mode: int = 0, msb_first: bool = True,
|
|
18
|
+
host: int = 0) -> None:
|
|
19
|
+
self._b.request(C.SPI_INIT, struct.pack(">BbbbIBB", host, sck, miso, mosi,
|
|
20
|
+
freq, mode, 1 if msb_first else 0))
|
|
21
|
+
|
|
22
|
+
def transfer(self, tx: bytes, *, cs: int | None = None, host: int = 0) -> bytes:
|
|
23
|
+
"""Full-duplex transfer; returns the bytes clocked in (len == len(tx)).
|
|
24
|
+
|
|
25
|
+
With `cs` given, CS is held low for the whole transfer — limited to
|
|
26
|
+
one frame (≤ 2046 bytes). Without `cs`, any length (chunked).
|
|
27
|
+
"""
|
|
28
|
+
tx = bytes(tx)
|
|
29
|
+
cs_b = 255 if cs is None else cs # i8 -1 == 255 unsigned
|
|
30
|
+
if cs is not None and len(tx) > _CHUNK:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"transfers with CS are limited to {_CHUNK} bytes per call "
|
|
33
|
+
"(CS would deassert between chunks); split the transfer or manage "
|
|
34
|
+
"CS manually via gpio"
|
|
35
|
+
)
|
|
36
|
+
rx = bytearray()
|
|
37
|
+
for off in range(0, len(tx), _CHUNK):
|
|
38
|
+
chunk = tx[off : off + _CHUNK]
|
|
39
|
+
rx += self._b.request(C.SPI_TRANSFER, bytes([host, cs_b]) + chunk, timeout=10.0)
|
|
40
|
+
return bytes(rx)
|
|
41
|
+
|
|
42
|
+
def deinit(self, host: int = 0) -> None:
|
|
43
|
+
self._b.request(C.SPI_DEINIT, bytes([host]))
|
espbridge/transport.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Serial transport with ESP32 auto-detection, plus an in-memory mock for tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .constants import KNOWN_USB_IDS
|
|
8
|
+
from .errors import NoDeviceError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class PortInfo:
|
|
13
|
+
device: str # e.g. "COM5" or "/dev/ttyUSB0"
|
|
14
|
+
usb_chip: str | None # "cp210x" | "ch340" | "ch9102" | "native" | None
|
|
15
|
+
description: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_ports() -> list[PortInfo]:
|
|
19
|
+
"""List serial ports that look like an ESP32 (by USB VID/PID)."""
|
|
20
|
+
from serial.tools import list_ports
|
|
21
|
+
|
|
22
|
+
found: list[PortInfo] = []
|
|
23
|
+
for p in list_ports.comports():
|
|
24
|
+
if p.vid is None:
|
|
25
|
+
continue
|
|
26
|
+
for (vid, pid), chip in KNOWN_USB_IDS.items():
|
|
27
|
+
if p.vid == vid and (pid is None or p.pid == pid):
|
|
28
|
+
found.append(PortInfo(p.device, chip, p.description or ""))
|
|
29
|
+
break
|
|
30
|
+
return found
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def autodetect_port() -> PortInfo:
|
|
34
|
+
ports = find_ports()
|
|
35
|
+
if not ports:
|
|
36
|
+
raise NoDeviceError(
|
|
37
|
+
"no ESP32 serial port found (CP210x/CH340/CH9102/native USB); "
|
|
38
|
+
"pass port='COM5' / '/dev/ttyUSB0' explicitly"
|
|
39
|
+
)
|
|
40
|
+
if len(ports) > 1:
|
|
41
|
+
names = ", ".join(p.device for p in ports)
|
|
42
|
+
raise NoDeviceError(f"multiple ESP32-like ports found ({names}); pass port= explicitly")
|
|
43
|
+
return ports[0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SerialTransport:
|
|
47
|
+
"""Thin pyserial wrapper. read() returns whatever bytes are available."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, port: str, baud: int = 115200, usb_chip: str | None = None):
|
|
50
|
+
import serial
|
|
51
|
+
|
|
52
|
+
self.usb_chip = usb_chip
|
|
53
|
+
self.ser = serial.Serial(port, baudrate=baud, timeout=0.05, write_timeout=2.0)
|
|
54
|
+
|
|
55
|
+
def read(self) -> bytes:
|
|
56
|
+
data = self.ser.read(1) # blocks up to `timeout`
|
|
57
|
+
waiting = self.ser.in_waiting
|
|
58
|
+
if data and waiting:
|
|
59
|
+
data += self.ser.read(waiting)
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
def write(self, data: bytes) -> None:
|
|
63
|
+
self.ser.write(data)
|
|
64
|
+
|
|
65
|
+
def set_baudrate(self, baud: int) -> None:
|
|
66
|
+
self.ser.baudrate = baud
|
|
67
|
+
self.ser.reset_input_buffer()
|
|
68
|
+
|
|
69
|
+
def pulse_reset(self) -> None:
|
|
70
|
+
"""Toggle RTS/DTR the way esptool does to hard-reset the ESP32."""
|
|
71
|
+
self.ser.dtr = False
|
|
72
|
+
self.ser.rts = True
|
|
73
|
+
time.sleep(0.1)
|
|
74
|
+
self.ser.rts = False
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
try:
|
|
78
|
+
self.ser.close()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MockTransport:
|
|
84
|
+
"""In-memory transport for hardware-free tests.
|
|
85
|
+
|
|
86
|
+
`responder(data: bytes)` is called with everything the host writes; it can
|
|
87
|
+
push firmware->host bytes back via `inject()`.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, responder=None):
|
|
91
|
+
import queue
|
|
92
|
+
|
|
93
|
+
self.usb_chip = None
|
|
94
|
+
self._rx: queue.Queue[bytes] = queue.Queue()
|
|
95
|
+
self.responder = responder
|
|
96
|
+
self.closed = False
|
|
97
|
+
self.baud = 115200
|
|
98
|
+
|
|
99
|
+
def inject(self, data: bytes) -> None:
|
|
100
|
+
self._rx.put(data)
|
|
101
|
+
|
|
102
|
+
def read(self) -> bytes:
|
|
103
|
+
import queue
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
return self._rx.get(timeout=0.05)
|
|
107
|
+
except queue.Empty:
|
|
108
|
+
return b""
|
|
109
|
+
|
|
110
|
+
def write(self, data: bytes) -> None:
|
|
111
|
+
if self.responder is not None:
|
|
112
|
+
self.responder(data)
|
|
113
|
+
|
|
114
|
+
def set_baudrate(self, baud: int) -> None:
|
|
115
|
+
self.baud = baud
|
|
116
|
+
|
|
117
|
+
def pulse_reset(self) -> None:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def close(self) -> None:
|
|
121
|
+
self.closed = True
|
espbridge/uart.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Secondary UARTs (1, 2): write from host, RX streamed back as events."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import struct
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
from . import constants as C
|
|
8
|
+
|
|
9
|
+
# Per-request write chunk: bounds single-request size (and how long the
|
|
10
|
+
# firmware's synchronous UART write can stall on slow baud rates).
|
|
11
|
+
_WRITE_CHUNK = 1024
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UartPort:
|
|
15
|
+
"""Bridged UART with a pyserial-like surface (read/write/in_waiting/...)."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, bridge, port: int):
|
|
18
|
+
self._b = bridge
|
|
19
|
+
self.port = port
|
|
20
|
+
self.timeout: float | None = None # default for read()/read_until()
|
|
21
|
+
self._buf = bytearray()
|
|
22
|
+
self._cond = threading.Condition()
|
|
23
|
+
self._callbacks: list = []
|
|
24
|
+
|
|
25
|
+
def _feed(self, data: bytes) -> None:
|
|
26
|
+
with self._cond:
|
|
27
|
+
self._buf += data
|
|
28
|
+
self._cond.notify_all()
|
|
29
|
+
for cb in list(self._callbacks):
|
|
30
|
+
cb(data)
|
|
31
|
+
|
|
32
|
+
def write(self, data: bytes) -> None:
|
|
33
|
+
data = bytes(data)
|
|
34
|
+
for off in range(0, len(data), _WRITE_CHUNK):
|
|
35
|
+
self._b.request(C.UART_WRITE, bytes([self.port]) + data[off : off + _WRITE_CHUNK])
|
|
36
|
+
|
|
37
|
+
def read(self, n: int | None = None, timeout: float | None = None) -> bytes:
|
|
38
|
+
"""Read up to n buffered bytes (all if n is None); waits up to `timeout`
|
|
39
|
+
(default: self.timeout, pyserial-style; None blocks until data)."""
|
|
40
|
+
timeout = self.timeout if timeout is None else timeout
|
|
41
|
+
with self._cond:
|
|
42
|
+
if not self._buf and timeout != 0:
|
|
43
|
+
self._cond.wait(timeout)
|
|
44
|
+
n = len(self._buf) if n is None else min(n, len(self._buf))
|
|
45
|
+
data = bytes(self._buf[:n])
|
|
46
|
+
del self._buf[:n]
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def in_waiting(self) -> int:
|
|
51
|
+
"""Bytes already received and buffered (pyserial-compatible)."""
|
|
52
|
+
with self._cond:
|
|
53
|
+
return len(self._buf)
|
|
54
|
+
|
|
55
|
+
def reset_input_buffer(self) -> None:
|
|
56
|
+
with self._cond:
|
|
57
|
+
self._buf.clear()
|
|
58
|
+
|
|
59
|
+
def flush(self) -> None:
|
|
60
|
+
pass # writes are synchronous requests; nothing left to flush
|
|
61
|
+
|
|
62
|
+
def readline(self, timeout: float | None = None) -> bytes:
|
|
63
|
+
t = timeout if timeout is not None else (self.timeout or 2.0)
|
|
64
|
+
return self.read_until(b"\n", t)
|
|
65
|
+
|
|
66
|
+
def read_until(self, sep: bytes = b"\n", timeout: float = 2.0) -> bytes:
|
|
67
|
+
import time
|
|
68
|
+
deadline = time.monotonic() + timeout
|
|
69
|
+
with self._cond:
|
|
70
|
+
while True:
|
|
71
|
+
i = self._buf.find(sep)
|
|
72
|
+
if i >= 0:
|
|
73
|
+
data = bytes(self._buf[: i + len(sep)])
|
|
74
|
+
del self._buf[: i + len(sep)]
|
|
75
|
+
return data
|
|
76
|
+
remaining = deadline - time.monotonic()
|
|
77
|
+
if remaining <= 0:
|
|
78
|
+
data = bytes(self._buf)
|
|
79
|
+
self._buf.clear()
|
|
80
|
+
return data
|
|
81
|
+
self._cond.wait(remaining)
|
|
82
|
+
|
|
83
|
+
def on_rx(self, callback) -> None:
|
|
84
|
+
"""callback(bytes) on every RX chunk (runs on the reader thread)."""
|
|
85
|
+
self._callbacks.append(callback)
|
|
86
|
+
|
|
87
|
+
def close(self) -> None:
|
|
88
|
+
self._b.request(C.UART_DEINIT, bytes([self.port]))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Uart:
|
|
92
|
+
def __init__(self, bridge):
|
|
93
|
+
self._b = bridge
|
|
94
|
+
self._ports: dict[int, UartPort] = {}
|
|
95
|
+
bridge.on_event(C.UART_RX_EVT, self._on_rx)
|
|
96
|
+
|
|
97
|
+
def _on_rx(self, payload: bytes) -> None:
|
|
98
|
+
if len(payload) < 2:
|
|
99
|
+
return
|
|
100
|
+
port = self._ports.get(payload[0])
|
|
101
|
+
if port is not None:
|
|
102
|
+
port._feed(payload[1:])
|
|
103
|
+
|
|
104
|
+
def init(self, *, port: int = 1, tx: int = 17, rx: int = 16,
|
|
105
|
+
baud: int = 115_200) -> UartPort:
|
|
106
|
+
self._b.request(C.UART_INIT, struct.pack(">BbbI", port, tx, rx, baud))
|
|
107
|
+
p = self._ports.get(port)
|
|
108
|
+
if p is None:
|
|
109
|
+
p = UartPort(self._b, port)
|
|
110
|
+
self._ports[port] = p
|
|
111
|
+
return p
|
espbridge/wifi.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Wi-Fi management: scan, STA connect, AP mode, status."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from . import constants as C
|
|
9
|
+
from .errors import BridgeTimeoutError
|
|
10
|
+
from .protocol import ip_str as _ip
|
|
11
|
+
from .protocol import lp
|
|
12
|
+
|
|
13
|
+
WL_CONNECTED = 3 # wl_status_t
|
|
14
|
+
|
|
15
|
+
AUTH_MODES = {0: "open", 1: "wep", 2: "wpa_psk", 3: "wpa2_psk", 4: "wpa_wpa2_psk",
|
|
16
|
+
5: "wpa2_enterprise", 6: "wpa3_psk", 7: "wpa2_wpa3_psk", 8: "wapi_psk"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Network:
|
|
21
|
+
ssid: str
|
|
22
|
+
rssi: int
|
|
23
|
+
bssid: str
|
|
24
|
+
channel: int
|
|
25
|
+
auth: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class WifiStatus:
|
|
30
|
+
status: int
|
|
31
|
+
ip: str
|
|
32
|
+
gateway: str
|
|
33
|
+
netmask: str
|
|
34
|
+
rssi: int
|
|
35
|
+
channel: int
|
|
36
|
+
mac: str
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def connected(self) -> bool:
|
|
40
|
+
return self.status == WL_CONNECTED
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Wifi:
|
|
44
|
+
def __init__(self, bridge):
|
|
45
|
+
self._b = bridge
|
|
46
|
+
self._scan_results: list[Network] = []
|
|
47
|
+
self._scan_done = threading.Event()
|
|
48
|
+
self._state_callbacks: list = []
|
|
49
|
+
bridge.on_event(C.WIFI_SCAN_RES, self._on_scan_res)
|
|
50
|
+
bridge.on_event(C.WIFI_SCAN_DONE, self._on_scan_done)
|
|
51
|
+
bridge.on_event(C.WIFI_STATE_EVT, self._on_state)
|
|
52
|
+
|
|
53
|
+
# ---- events ---------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _on_scan_res(self, p: bytes) -> None:
|
|
56
|
+
if len(p) < 12:
|
|
57
|
+
return
|
|
58
|
+
ssid = p[12 : 12 + p[11]].decode("utf-8", "replace")
|
|
59
|
+
rssi = int.from_bytes(p[2:3], "big", signed=True)
|
|
60
|
+
bssid = ":".join(f"{x:02x}" for x in p[5:11])
|
|
61
|
+
self._scan_results.append(
|
|
62
|
+
Network(ssid, rssi, bssid, p[4], AUTH_MODES.get(p[3], str(p[3])))
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _on_scan_done(self, p: bytes) -> None:
|
|
66
|
+
self._scan_done.set()
|
|
67
|
+
|
|
68
|
+
def _on_state(self, p: bytes) -> None:
|
|
69
|
+
if not p:
|
|
70
|
+
return
|
|
71
|
+
event = {1: "connected", 2: "got_ip", 3: "disconnected"}.get(p[0], str(p[0]))
|
|
72
|
+
ip = _ip(p[1:5]) if len(p) >= 5 else "0.0.0.0"
|
|
73
|
+
for cb in list(self._state_callbacks):
|
|
74
|
+
cb(event, ip)
|
|
75
|
+
|
|
76
|
+
def on_state(self, callback) -> None:
|
|
77
|
+
"""callback(event: str, ip: str) on connect/got_ip/disconnect."""
|
|
78
|
+
self._state_callbacks.append(callback)
|
|
79
|
+
|
|
80
|
+
# ---- commands ----------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def scan(self, timeout: float = 15.0) -> list[Network]:
|
|
83
|
+
self._scan_results = []
|
|
84
|
+
self._scan_done.clear()
|
|
85
|
+
self._b.request(C.WIFI_SCAN)
|
|
86
|
+
if not self._scan_done.wait(timeout):
|
|
87
|
+
raise BridgeTimeoutError("Wi-Fi scan did not finish")
|
|
88
|
+
return sorted(self._scan_results, key=lambda n: -n.rssi)
|
|
89
|
+
|
|
90
|
+
def connect(self, ssid: str, password: str = "", *, wait: bool = True,
|
|
91
|
+
timeout: float = 20.0) -> WifiStatus:
|
|
92
|
+
self._b.request(C.WIFI_CONNECT, lp(ssid) + lp(password))
|
|
93
|
+
if not wait:
|
|
94
|
+
return self.status()
|
|
95
|
+
deadline = time.monotonic() + timeout
|
|
96
|
+
while time.monotonic() < deadline:
|
|
97
|
+
st = self.status()
|
|
98
|
+
if st.connected and st.ip != "0.0.0.0":
|
|
99
|
+
return st
|
|
100
|
+
time.sleep(0.25)
|
|
101
|
+
raise BridgeTimeoutError(f"could not join {ssid!r} within {timeout}s")
|
|
102
|
+
|
|
103
|
+
def disconnect(self) -> None:
|
|
104
|
+
self._b.request(C.WIFI_DISCONNECT)
|
|
105
|
+
|
|
106
|
+
def status(self) -> WifiStatus:
|
|
107
|
+
p = self._b.request(C.WIFI_STATUS)
|
|
108
|
+
return WifiStatus(
|
|
109
|
+
status=p[0],
|
|
110
|
+
ip=_ip(p[1:5]),
|
|
111
|
+
gateway=_ip(p[5:9]),
|
|
112
|
+
netmask=_ip(p[9:13]),
|
|
113
|
+
rssi=int.from_bytes(p[13:14], "big", signed=True),
|
|
114
|
+
channel=p[14],
|
|
115
|
+
mac=":".join(f"{x:02x}" for x in p[15:21]),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def ap_start(self, ssid: str, password: str = "", *, channel: int = 1,
|
|
119
|
+
max_conn: int = 4) -> str:
|
|
120
|
+
"""Start an access point; returns its IP (clients usually get 192.168.4.x)."""
|
|
121
|
+
payload = lp(ssid) + lp(password) + bytes([channel, max_conn])
|
|
122
|
+
return _ip(self._b.request(C.WIFI_AP_START, payload, timeout=5.0))
|
|
123
|
+
|
|
124
|
+
def ap_stop(self) -> None:
|
|
125
|
+
self._b.request(C.WIFI_AP_STOP)
|
|
126
|
+
|
|
127
|
+
def hostname(self, name: str) -> None:
|
|
128
|
+
self._b.request(C.WIFI_HOSTNAME, lp(name))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-esp-bridge
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Control every ESP32 peripheral from Python over USB serial — GPIO, ADC, DAC, PWM, touch, I2C, SPI, UART, Wi-Fi sockets, BLE
|
|
5
|
+
Project-URL: Homepage, https://github.com/HamzaYslmn/python-esp-bridge
|
|
6
|
+
Author: HamzaYslmn
|
|
7
|
+
Keywords: ble,bridge,esp32,firmata,gpio,i2c,raspberry-pi,serial,spi
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: pyserial>=3.5
|
|
10
|
+
Provides-Extra: oled
|
|
11
|
+
Requires-Dist: pillow>=10; extra == 'oled'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# python-esp-bridge
|
|
15
|
+
|
|
16
|
+
Control every ESP32 peripheral from Python over USB serial — GPIO, PWM, ADC,
|
|
17
|
+
DAC, touch, I2C, SPI, UART, Wi-Fi (with TCP/UDP sockets through the ESP32
|
|
18
|
+
radio) and BLE. Flash the bridge firmware once, then it's all Python.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from espbridge import Bridge
|
|
22
|
+
|
|
23
|
+
with Bridge() as esp: # auto-detects the USB port
|
|
24
|
+
esp.gpio.mode(2, "output")
|
|
25
|
+
esp.gpio.write(2, 1)
|
|
26
|
+
print(esp.adc.read_mv(34), "mV")
|
|
27
|
+
esp.i2c.init(sda=21, scl=22)
|
|
28
|
+
print(esp.i2c.scan())
|
|
29
|
+
esp.wifi.connect("ssid", "password")
|
|
30
|
+
status, body = esp.net.http_get("http://example.com/")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Firmware (flash once with Arduino IDE) and full docs:
|
|
34
|
+
**<https://github.com/HamzaYslmn/python-esp-bridge>**
|
|
35
|
+
- Works on Raspberry Pi OS, Linux, Windows, macOS (Python ≥ 3.10, pyserial).
|
|
36
|
+
- `espbridge` CLI: connection info; `espbridge ports`: list candidate ports.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
espbridge/__init__.py,sha256=J6wt84grWASJUf09iWopqoXdbONxV17rRlYBswsDIkU,1063
|
|
2
|
+
espbridge/analog.py,sha256=zv1EFVZC2CSNBr-J0wvQw2QZIdKQzhSrknfYmaOkSig,2247
|
|
3
|
+
espbridge/ble.py,sha256=fuTWlVoqCiDlu0e0-6r4Trq1VR_7skZwSFtpIgzZNyc,8321
|
|
4
|
+
espbridge/bridge.py,sha256=E6kHPq6NYIkROn-e4pFqcZtp3l8DCXGlO5FDOIPYhT8,17358
|
|
5
|
+
espbridge/cli.py,sha256=6gVeDrb7AbR-vGDnJlbBhhQ0J9HBxBZmbAzjcLh8rZM,3101
|
|
6
|
+
espbridge/constants.py,sha256=btS0wKrp6MfsT28kqcqYL7I9dNHVWlYdXPw1P3Dgxzc,4816
|
|
7
|
+
espbridge/errors.py,sha256=gYKhAohajGVGXiuzkC-i2KzvygvbgMZoqnJ8x-F7HyI,1051
|
|
8
|
+
espbridge/gpio.py,sha256=daaE7GgjYxGBnGpFbkKMTsqLmyao8e5Hs_1dM3uFWFc,2384
|
|
9
|
+
espbridge/i2c.py,sha256=S7uISLcMOMFTGIfOeL8li4TIG9slI6E3G380u7Q9ACM,2772
|
|
10
|
+
espbridge/net.py,sha256=PHVg2OQYh5Ap-Zdi1C9vW-qEohExzwgBT-03ImCElYI,9634
|
|
11
|
+
espbridge/oled.py,sha256=UYEDZmfl8UwCw4B36Awk_uGdROR2W5jRpUeOfD1sCJ0,8072
|
|
12
|
+
espbridge/protocol.py,sha256=kIns3gXxZ7wZABY5En5YeK1MgtT86-GVr4_JBMqIhTI,4405
|
|
13
|
+
espbridge/pwm.py,sha256=S2VzG6lh0GRM-WUaXp6uHbKZ8dvlRVs4T7WW_nCxj0o,1669
|
|
14
|
+
espbridge/spi.py,sha256=eNdrhPlpdgPPIzI4OHIC0vJ3dQnSX2W1m6lw6_n5DXk,1688
|
|
15
|
+
espbridge/transport.py,sha256=rJmUyalr3-2ilVUGnhseV9alewmnD3v7QT_75ho31i4,3432
|
|
16
|
+
espbridge/uart.py,sha256=-PgCjr5iMkhp9b_PYr_qPNYBwPfjYv4Ancjq0bxgWtc,3870
|
|
17
|
+
espbridge/wifi.py,sha256=q7FH-9DoClSyR7ibGCb2KHkNTP4NqrqNo6gtn9SAzpw,4198
|
|
18
|
+
espbridge/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
espbridge/compat/blinka.py,sha256=Pke1QWnXx6yoXR8T62XQBofSYUciyEHBTpw5fLIRo9c,6924
|
|
20
|
+
espbridge/compat/gpiozero.py,sha256=PMD7_yUPH0CF9mycf4XZeollH-G54MeSy3X72vXNZJY,7841
|
|
21
|
+
espbridge/compat/luma.py,sha256=iErusKC4dccFzN5qK1HfZ_q0rt50j8L9SK0ZBtc9nlY,3382
|
|
22
|
+
espbridge/compat/rpi_gpio.py,sha256=YwOXbXdNLVVHv1CbODV_tCNg59iXWEuhpBCkAaSZnF8,3346
|
|
23
|
+
espbridge/compat/smbus.py,sha256=k8h1VQe6SzqRKvtfsuVzw8YFDjceedDdod5JtG0QX0s,2519
|
|
24
|
+
python_esp_bridge-0.0.2.dist-info/METADATA,sha256=Cl44L_9aK_dV7oVYofm3KsPKACM4P5hJQ_lbdY0FoTQ,1393
|
|
25
|
+
python_esp_bridge-0.0.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
26
|
+
python_esp_bridge-0.0.2.dist-info/entry_points.txt,sha256=xXm9lM8iMGXRhFiDBZrncBlA_RuWOSCxUO2_v7jGuGE,49
|
|
27
|
+
python_esp_bridge-0.0.2.dist-info/RECORD,,
|