python-esp-bridge 0.0.2__py3-none-any.whl → 0.1.0__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 +13 -3
- espbridge/bridge.py +102 -16
- espbridge/cli.py +49 -2
- espbridge/constants.py +11 -1
- espbridge/errors.py +5 -0
- espbridge/transport.py +4 -120
- espbridge/transports/__init__.py +22 -0
- espbridge/transports/ble.py +162 -0
- espbridge/transports/mock.py +46 -0
- espbridge/transports/serial.py +83 -0
- {python_esp_bridge-0.0.2.dist-info → python_esp_bridge-0.1.0.dist-info}/METADATA +4 -2
- {python_esp_bridge-0.0.2.dist-info → python_esp_bridge-0.1.0.dist-info}/RECORD +14 -10
- {python_esp_bridge-0.0.2.dist-info → python_esp_bridge-0.1.0.dist-info}/WHEEL +0 -0
- {python_esp_bridge-0.0.2.dist-info → python_esp_bridge-0.1.0.dist-info}/entry_points.txt +0 -0
espbridge/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""espbridge — control every ESP32 peripheral from Python over USB serial.
|
|
2
2
|
|
|
3
|
-
Flash
|
|
3
|
+
Flash firmware/firmware.ino once, then:
|
|
4
4
|
|
|
5
5
|
from espbridge import Bridge
|
|
6
6
|
|
|
@@ -15,6 +15,7 @@ Flash esp/esp.ino once, then:
|
|
|
15
15
|
from .bridge import Bridge, BridgeSet, Info, connect_all
|
|
16
16
|
from .constants import Cap, ChipModel, Status
|
|
17
17
|
from .errors import (
|
|
18
|
+
AuthError,
|
|
18
19
|
BridgeError,
|
|
19
20
|
BridgeTimeoutError,
|
|
20
21
|
NoDeviceError,
|
|
@@ -22,18 +23,27 @@ from .errors import (
|
|
|
22
23
|
RemoteError,
|
|
23
24
|
UnsupportedError,
|
|
24
25
|
)
|
|
25
|
-
from .
|
|
26
|
+
from .transports import find_ports
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
def find_ble_devices(timeout: float = 5.0):
|
|
30
|
+
"""Scan for bridges advertising over Bluetooth (needs the [ble] extra)."""
|
|
31
|
+
from .transports.ble import find_ble_devices as _scan
|
|
32
|
+
|
|
33
|
+
return _scan(timeout)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
28
36
|
|
|
29
37
|
__all__ = [
|
|
30
38
|
"Bridge",
|
|
31
39
|
"BridgeSet",
|
|
32
40
|
"connect_all",
|
|
41
|
+
"find_ble_devices",
|
|
33
42
|
"Info",
|
|
34
43
|
"Cap",
|
|
35
44
|
"ChipModel",
|
|
36
45
|
"Status",
|
|
46
|
+
"AuthError",
|
|
37
47
|
"BridgeError",
|
|
38
48
|
"BridgeTimeoutError",
|
|
39
49
|
"NoDeviceError",
|
espbridge/bridge.py
CHANGED
|
@@ -10,6 +10,7 @@ from dataclasses import dataclass
|
|
|
10
10
|
|
|
11
11
|
from . import constants as C
|
|
12
12
|
from .errors import (
|
|
13
|
+
AuthError,
|
|
13
14
|
BridgeError,
|
|
14
15
|
BridgeTimeoutError,
|
|
15
16
|
NoDeviceError,
|
|
@@ -18,7 +19,7 @@ from .errors import (
|
|
|
18
19
|
UnsupportedError,
|
|
19
20
|
)
|
|
20
21
|
from .protocol import Frame, FrameSplitter, decode_frame, encode_frame
|
|
21
|
-
from .
|
|
22
|
+
from .transports import SerialTransport, find_ports
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@dataclass(frozen=True)
|
|
@@ -78,6 +79,12 @@ class Bridge:
|
|
|
78
79
|
|
|
79
80
|
>>> esp = Bridge(name="relays")
|
|
80
81
|
>>> esp = Bridge(mac="24:a1:60:12:34:56")
|
|
82
|
+
|
|
83
|
+
Over Bluetooth instead of USB (firmware default password "espbridge",
|
|
84
|
+
change it at the top of firmware/firmware.ino):
|
|
85
|
+
|
|
86
|
+
>>> esp = Bridge(ble=True) # the only advertising bridge
|
|
87
|
+
>>> esp = Bridge(ble="relays", password="espbridge")
|
|
81
88
|
"""
|
|
82
89
|
|
|
83
90
|
def __init__(
|
|
@@ -86,6 +93,8 @@ class Bridge:
|
|
|
86
93
|
*,
|
|
87
94
|
name: str | None = None,
|
|
88
95
|
mac: str | None = None,
|
|
96
|
+
ble: bool | str = False,
|
|
97
|
+
password: str | None = None,
|
|
89
98
|
baud: int = 115200,
|
|
90
99
|
upgrade_baud: bool = True,
|
|
91
100
|
target_baud: int | None = None,
|
|
@@ -98,18 +107,22 @@ class Bridge:
|
|
|
98
107
|
self.reset_on_exit = False # set for real only once connected (see below)
|
|
99
108
|
self.info: Info | None = None
|
|
100
109
|
|
|
101
|
-
#
|
|
110
|
+
# Candidates: (transport factory, label, usb_chip).
|
|
102
111
|
if transport is not None:
|
|
103
|
-
candidates = [(transport
|
|
112
|
+
candidates = [(lambda t=transport: t, "transport",
|
|
113
|
+
getattr(transport, "usb_chip", None))]
|
|
114
|
+
elif ble:
|
|
115
|
+
candidates = self._ble_candidates(ble, name, mac)
|
|
104
116
|
elif port is not None:
|
|
105
117
|
chip = next((p.usb_chip for p in find_ports() if p.device == port), None)
|
|
106
|
-
candidates = [(
|
|
118
|
+
candidates = [(lambda p=port, c=chip: SerialTransport(p, baud, usb_chip=c),
|
|
119
|
+
port, chip)]
|
|
107
120
|
else:
|
|
108
121
|
ports = find_ports()
|
|
109
122
|
if not ports:
|
|
110
123
|
raise NoDeviceError(
|
|
111
124
|
"no ESP32 serial port found (CP210x/CH340/CH9102/native USB); "
|
|
112
|
-
"pass port='COM5' / '/dev/ttyUSB0' explicitly"
|
|
125
|
+
"pass port='COM5' / '/dev/ttyUSB0' explicitly — or ble=True"
|
|
113
126
|
)
|
|
114
127
|
if name is None and mac is None and len(ports) > 1:
|
|
115
128
|
names = ", ".join(p.device for p in ports)
|
|
@@ -117,41 +130,92 @@ class Bridge:
|
|
|
117
130
|
f"multiple ESP32-like ports found ({names}); pass port=, "
|
|
118
131
|
f"name= or mac= — or use espbridge.connect_all()"
|
|
119
132
|
)
|
|
120
|
-
candidates = [(
|
|
133
|
+
candidates = [(lambda p=p: SerialTransport(p.device, baud, usb_chip=p.usb_chip),
|
|
134
|
+
p.device, p.usb_chip) for p in ports]
|
|
121
135
|
|
|
122
136
|
probing = len(candidates) > 1
|
|
123
137
|
errors: list[str] = []
|
|
124
|
-
for
|
|
138
|
+
for factory, label, chip in candidates:
|
|
125
139
|
self._reset_state()
|
|
126
140
|
try:
|
|
127
|
-
self._t =
|
|
141
|
+
self._t = factory()
|
|
128
142
|
except Exception as e:
|
|
129
|
-
errors.append(f"{
|
|
143
|
+
errors.append(f"{label}: {e}")
|
|
130
144
|
continue
|
|
131
145
|
self._reader = threading.Thread(target=self._read_loop, daemon=True,
|
|
132
146
|
name="espbridge-reader")
|
|
133
147
|
self._reader.start()
|
|
134
148
|
try:
|
|
135
|
-
self.
|
|
149
|
+
if getattr(self._t, "needs_auth", False):
|
|
150
|
+
self._auth(password)
|
|
151
|
+
self._handshake(reset_on_open=False)
|
|
152
|
+
else:
|
|
153
|
+
self._handshake(reset_on_open)
|
|
136
154
|
assert self.info is not None
|
|
137
155
|
if self._matches(name, mac):
|
|
138
|
-
if upgrade_baud:
|
|
156
|
+
if upgrade_baud and getattr(self._t, "has_baud", True):
|
|
139
157
|
self._upgrade_baud(baud, target_baud)
|
|
140
158
|
self.reset_on_exit = reset_on_exit
|
|
141
159
|
return
|
|
142
|
-
errors.append(f"{
|
|
160
|
+
errors.append(f"{label}: name={self.info.name!r} "
|
|
143
161
|
f"mac={self.info.mac} (no match)")
|
|
144
162
|
self.close()
|
|
145
163
|
except (BridgeTimeoutError, ProtocolError) as e:
|
|
146
164
|
self.close()
|
|
147
165
|
if not probing:
|
|
148
166
|
raise
|
|
149
|
-
errors.append(f"{
|
|
167
|
+
errors.append(f"{label}: {e}")
|
|
150
168
|
except BaseException:
|
|
151
169
|
self.close()
|
|
152
170
|
raise
|
|
153
171
|
raise NoDeviceError("no matching bridge found — " + "; ".join(errors))
|
|
154
172
|
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _ble_candidates(ble: bool | str, name: str | None, mac: str | None):
|
|
175
|
+
from .transports.ble import BleTransport, find_ble_devices
|
|
176
|
+
|
|
177
|
+
target = ble if isinstance(ble, str) else None
|
|
178
|
+
devs = find_ble_devices()
|
|
179
|
+
if target is not None:
|
|
180
|
+
tmac = _norm_mac(target)
|
|
181
|
+
devs = [d for d in devs
|
|
182
|
+
if d.name == target or d.device_name == target
|
|
183
|
+
or _norm_mac(d.address) == tmac or _norm_mac(d.mac) == tmac]
|
|
184
|
+
# The advertised name carries the bridge MAC and custom name: narrow
|
|
185
|
+
# the candidates before connecting when the caller passed name=/mac=.
|
|
186
|
+
# On no match keep the full list (adv data can be stale/truncated) —
|
|
187
|
+
# the post-connect _matches() check stays authoritative either way.
|
|
188
|
+
if mac is not None:
|
|
189
|
+
devs = [d for d in devs if _norm_mac(d.mac) == _norm_mac(mac)] or devs
|
|
190
|
+
if name is not None:
|
|
191
|
+
devs = [d for d in devs if d.device_name == name] or devs
|
|
192
|
+
if not devs:
|
|
193
|
+
what = f" named/at {target!r}" if target else ""
|
|
194
|
+
raise NoDeviceError(
|
|
195
|
+
f"no bridge{what} found over Bluetooth — is the board powered, "
|
|
196
|
+
f"in range, and flashed with BRIDGE_BLE_LINK enabled?"
|
|
197
|
+
)
|
|
198
|
+
if target is None and name is None and mac is None and len(devs) > 1:
|
|
199
|
+
names = ", ".join(d.name or d.address for d in devs)
|
|
200
|
+
raise NoDeviceError(
|
|
201
|
+
f"multiple bridges advertising ({names}); pass ble='name-or-mac'"
|
|
202
|
+
)
|
|
203
|
+
return [(lambda d=d: BleTransport(d.address),
|
|
204
|
+
f"BLE {d.name or d.address}", None) for d in devs]
|
|
205
|
+
|
|
206
|
+
def _auth(self, password: str | None) -> None:
|
|
207
|
+
"""Authenticate a wireless link (SYS_AUTH) before the handshake."""
|
|
208
|
+
pw = (C.DEFAULT_PASSWORD if password is None else password).encode()
|
|
209
|
+
try:
|
|
210
|
+
self.request(C.SYS_AUTH, pw, timeout=5.0)
|
|
211
|
+
except RemoteError as e:
|
|
212
|
+
if e.status == C.Status.DENIED:
|
|
213
|
+
raise AuthError(
|
|
214
|
+
"bridge rejected the password — check BRIDGE_PASSWORD at "
|
|
215
|
+
"the top of firmware/firmware.ino"
|
|
216
|
+
) from None
|
|
217
|
+
raise
|
|
218
|
+
|
|
155
219
|
def _reset_state(self) -> None:
|
|
156
220
|
self._splitter = FrameSplitter()
|
|
157
221
|
self._pending: dict[int, _Pending] = {}
|
|
@@ -203,13 +267,13 @@ class Bridge:
|
|
|
203
267
|
continue
|
|
204
268
|
if not self._ready.is_set():
|
|
205
269
|
raise BridgeTimeoutError(
|
|
206
|
-
"no response from bridge firmware — is it flashed? (
|
|
270
|
+
"no response from bridge firmware — is it flashed? (firmware/README.md)"
|
|
207
271
|
)
|
|
208
272
|
assert self.info is not None
|
|
209
273
|
if self.info.protocol != C.PROTOCOL_VERSION:
|
|
210
274
|
raise ProtocolError(
|
|
211
275
|
f"protocol mismatch: firmware speaks v{self.info.protocol}, "
|
|
212
|
-
f"this library v{C.PROTOCOL_VERSION} — reflash
|
|
276
|
+
f"this library v{C.PROTOCOL_VERSION} — reflash firmware.ino or "
|
|
213
277
|
f"update python-esp-bridge"
|
|
214
278
|
)
|
|
215
279
|
|
|
@@ -232,7 +296,29 @@ class Bridge:
|
|
|
232
296
|
continue
|
|
233
297
|
# Could not talk at the new baud: fall back.
|
|
234
298
|
self._t.set_baudrate(current)
|
|
235
|
-
|
|
299
|
+
try:
|
|
300
|
+
self.ping(b"fallback")
|
|
301
|
+
except BridgeTimeoutError:
|
|
302
|
+
# The firmware already switched to `target` and can't hear us.
|
|
303
|
+
# Reopen the port: the open-time DTR/RTS toggle resets the board
|
|
304
|
+
# (reliable even where a manual reset pulse is not).
|
|
305
|
+
self._reconnect(current)
|
|
306
|
+
|
|
307
|
+
def _reconnect(self, baud: int) -> None:
|
|
308
|
+
"""Reopen the serial port and redo the handshake (recovers a dead link)."""
|
|
309
|
+
port = getattr(getattr(self._t, "ser", None), "port", None)
|
|
310
|
+
chip = getattr(self._t, "usb_chip", None)
|
|
311
|
+
if port is None:
|
|
312
|
+
raise BridgeTimeoutError("link lost and transport cannot be reopened")
|
|
313
|
+
self._t.close()
|
|
314
|
+
self._reader.join(timeout=1.0)
|
|
315
|
+
time.sleep(0.3) # let the device reboot from the close-time reset
|
|
316
|
+
self._reset_state()
|
|
317
|
+
self._t = SerialTransport(port, baud, usb_chip=chip)
|
|
318
|
+
self._reader = threading.Thread(target=self._read_loop, daemon=True,
|
|
319
|
+
name="espbridge-reader")
|
|
320
|
+
self._reader.start()
|
|
321
|
+
self._handshake(reset_on_open=True)
|
|
236
322
|
|
|
237
323
|
def close(self) -> None:
|
|
238
324
|
if self._closing:
|
espbridge/cli.py
CHANGED
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
from . import __version__
|
|
8
8
|
from .bridge import Bridge, connect_all
|
|
9
9
|
from .errors import BridgeError
|
|
10
|
-
from .
|
|
10
|
+
from .transports import find_ports
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _print_info(esp: Bridge) -> None:
|
|
@@ -32,10 +32,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
32
32
|
ap.add_argument("--version", action="version", version=f"espbridge {__version__}")
|
|
33
33
|
ap.add_argument("-p", "--port", help="serial port (default: auto-detect)")
|
|
34
34
|
ap.add_argument("-n", "--name", help="select device by stored name")
|
|
35
|
+
ap.add_argument("-b", "--ble", nargs="?", const=True, metavar="NAME_OR_MAC",
|
|
36
|
+
help="connect over Bluetooth instead of USB")
|
|
37
|
+
ap.add_argument("--password", help="Bluetooth link password "
|
|
38
|
+
"(default: 'espbridge')")
|
|
35
39
|
ap.add_argument("--no-baud-upgrade", action="store_true",
|
|
36
40
|
help="stay at 115200 instead of upgrading the link speed")
|
|
37
41
|
sub = ap.add_subparsers(dest="cmd")
|
|
38
42
|
sub.add_parser("ports", help="list ESP32-like serial ports")
|
|
43
|
+
p_scan = sub.add_parser("scan", help="connect to every attached device and "
|
|
44
|
+
"list port/name/chip/mac")
|
|
45
|
+
p_scan.add_argument("--ble", action="store_true", dest="scan_ble",
|
|
46
|
+
help="scan for bridges advertising over Bluetooth")
|
|
39
47
|
sub.add_parser("info", help="connect and print firmware/chip info (default; "
|
|
40
48
|
"shows every device when several are attached)")
|
|
41
49
|
p_name = sub.add_parser("set-name", help="store a device name on the ESP32 (NVS)")
|
|
@@ -43,8 +51,25 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
43
51
|
args = ap.parse_args(argv)
|
|
44
52
|
|
|
45
53
|
kwargs = dict(upgrade_baud=not args.no_baud_upgrade)
|
|
54
|
+
if args.ble:
|
|
55
|
+
kwargs["ble"] = args.ble
|
|
56
|
+
if args.password is not None:
|
|
57
|
+
kwargs["password"] = args.password
|
|
46
58
|
|
|
47
59
|
try:
|
|
60
|
+
if args.cmd == "scan" and args.scan_ble:
|
|
61
|
+
from .transports.ble import find_ble_devices
|
|
62
|
+
|
|
63
|
+
devs = find_ble_devices()
|
|
64
|
+
if not devs:
|
|
65
|
+
print("no bridges advertising over Bluetooth")
|
|
66
|
+
return 1
|
|
67
|
+
print(f"{'NAME':<16s} {'MAC':<18s} {'ADVERTISED':<32s} RSSI")
|
|
68
|
+
for d in devs:
|
|
69
|
+
print(f"{d.device_name or '-':<16s} {d.mac or '-':<18s} "
|
|
70
|
+
f"{d.name:<32s} {d.rssi} dBm")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
48
73
|
if args.cmd == "ports":
|
|
49
74
|
ports = find_ports()
|
|
50
75
|
if not ports:
|
|
@@ -54,14 +79,36 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
54
79
|
print(f"{p.device}\t{p.usb_chip}\t{p.description}")
|
|
55
80
|
return 0
|
|
56
81
|
|
|
82
|
+
if args.cmd == "scan":
|
|
83
|
+
ports = find_ports()
|
|
84
|
+
if not ports:
|
|
85
|
+
print("no ESP32-like serial ports found")
|
|
86
|
+
return 1
|
|
87
|
+
print(f"{'PORT':<12s} {'NAME':<16s} {'CHIP':<10s} {'MAC':<17s} FW")
|
|
88
|
+
rc = 0
|
|
89
|
+
for p in ports:
|
|
90
|
+
try:
|
|
91
|
+
# info only — skip the baud upgrade for a quicker probe
|
|
92
|
+
with Bridge(p.device, upgrade_baud=False) as esp:
|
|
93
|
+
info = esp.info
|
|
94
|
+
fw = ".".join(map(str, info.fw_version))
|
|
95
|
+
print(f"{p.device:<12s} {info.name or '-':<16s} "
|
|
96
|
+
f"{info.chip.name:<10s} {info.mac:<17s} v{fw}")
|
|
97
|
+
except BridgeError as e:
|
|
98
|
+
print(f"{p.device:<12s} error: {e}")
|
|
99
|
+
rc = 1
|
|
100
|
+
return rc
|
|
101
|
+
|
|
57
102
|
if args.cmd == "set-name":
|
|
58
103
|
with Bridge(args.port, name=args.name, **kwargs) as esp:
|
|
59
104
|
esp.set_name(args.new_name)
|
|
60
105
|
print(f"{esp.info.mac} is now named {args.new_name!r}")
|
|
106
|
+
print("(the Bluetooth advertised name updates on next reset)")
|
|
61
107
|
return 0
|
|
62
108
|
|
|
63
109
|
# default: info
|
|
64
|
-
if args.port is None and args.name is None and
|
|
110
|
+
if args.port is None and args.name is None and not args.ble \
|
|
111
|
+
and len(find_ports()) > 1:
|
|
65
112
|
with connect_all(**kwargs) as boards:
|
|
66
113
|
for i, esp in enumerate(boards):
|
|
67
114
|
if i:
|
espbridge/constants.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""python-esp-bridge — shared protocol contract.
|
|
2
2
|
|
|
3
|
-
MUST stay in sync with
|
|
3
|
+
MUST stay in sync with firmware/src/espbridge/commands.h.
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -34,6 +34,7 @@ class Status(enum.IntEnum):
|
|
|
34
34
|
WIFI = 0x0A
|
|
35
35
|
SOCKET = 0x0B
|
|
36
36
|
CRC = 0x0C
|
|
37
|
+
DENIED = 0x0D # wireless link not authenticated (see SYS_AUTH)
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class Cap(enum.IntFlag):
|
|
@@ -46,6 +47,7 @@ class Cap(enum.IntFlag):
|
|
|
46
47
|
PSRAM = 1 << 6
|
|
47
48
|
NATIVE_USB = 1 << 7
|
|
48
49
|
BLE_FW = 1 << 8
|
|
50
|
+
BLE_LINK = 1 << 9 # bridge reachable over the BLE transport
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
class ChipModel(enum.IntEnum):
|
|
@@ -82,6 +84,7 @@ SYS_SET_BAUD = _cmd(MOD_SYS, 0x03)
|
|
|
82
84
|
SYS_RESET = _cmd(MOD_SYS, 0x04)
|
|
83
85
|
SYS_FREE_HEAP = _cmd(MOD_SYS, 0x05)
|
|
84
86
|
SYS_SET_NAME = _cmd(MOD_SYS, 0x06)
|
|
87
|
+
SYS_AUTH = _cmd(MOD_SYS, 0x07)
|
|
85
88
|
SYS_READY = _cmd(MOD_SYS, 0x80)
|
|
86
89
|
SYS_LOG = _cmd(MOD_SYS, 0x81)
|
|
87
90
|
|
|
@@ -189,6 +192,13 @@ KNOWN_USB_IDS = {
|
|
|
189
192
|
(0x303A, None): "native", # Espressif native USB (S2/S3/C3/...)
|
|
190
193
|
}
|
|
191
194
|
|
|
195
|
+
# BLE link: Nordic-UART-style GATT service the firmware exposes as a transport.
|
|
196
|
+
# MUST stay in sync with firmware link_ble.cpp.
|
|
197
|
+
BLE_LINK_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
|
|
198
|
+
BLE_LINK_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" # host -> board (write)
|
|
199
|
+
BLE_LINK_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" # board -> host (notify)
|
|
200
|
+
DEFAULT_PASSWORD = "espbridge" # firmware default; change in firmware.ino
|
|
201
|
+
|
|
192
202
|
# Safe upgraded baud per bridge chip (conservative defaults; CH340 can do 2M).
|
|
193
203
|
UPGRADE_BAUD = {
|
|
194
204
|
"cp210x": 921600,
|
espbridge/errors.py
CHANGED
|
@@ -24,6 +24,11 @@ class UnsupportedError(BridgeError):
|
|
|
24
24
|
"""The connected chip lacks this capability (e.g. DAC on ESP32-S3)."""
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class AuthError(BridgeError):
|
|
28
|
+
"""The bridge rejected the wireless-link password (see BRIDGE_PASSWORD
|
|
29
|
+
at the top of firmware/firmware.ino)."""
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
class RemoteError(BridgeError):
|
|
28
33
|
"""The firmware returned an error status for a request."""
|
|
29
34
|
|
espbridge/transport.py
CHANGED
|
@@ -1,121 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
from
|
|
1
|
+
"""Back-compat shim — transports now live in espbridge.transports.*"""
|
|
2
|
+
from .transports.serial import PortInfo, SerialTransport, autodetect_port, find_ports
|
|
3
|
+
from .transports.mock import MockTransport
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
5
|
+
__all__ = ["PortInfo", "SerialTransport", "MockTransport", "autodetect_port", "find_ports"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Transports: byte links the Bridge protocol runs over (serial, BLE, mock).
|
|
2
|
+
|
|
3
|
+
A transport provides:
|
|
4
|
+
read() -> bytes # whatever is available (may block briefly, b"" ok)
|
|
5
|
+
write(data: bytes) # send raw bytes
|
|
6
|
+
close()
|
|
7
|
+
set_baudrate(baud) # serial links only; no-op elsewhere
|
|
8
|
+
pulse_reset() # serial links only; no-op elsewhere
|
|
9
|
+
usb_chip: str | None # adapter hint for baud upgrade ("cp210x", ...)
|
|
10
|
+
has_baud: bool # False to skip baud negotiation entirely
|
|
11
|
+
needs_auth: bool # True = Bridge sends SYS_AUTH before the handshake
|
|
12
|
+
"""
|
|
13
|
+
from .serial import PortInfo, SerialTransport, autodetect_port, find_ports
|
|
14
|
+
from .mock import MockTransport
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"PortInfo",
|
|
18
|
+
"SerialTransport",
|
|
19
|
+
"MockTransport",
|
|
20
|
+
"autodetect_port",
|
|
21
|
+
"find_ports",
|
|
22
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Bluetooth (BLE) transport — talk to the bridge with no USB cable.
|
|
2
|
+
|
|
3
|
+
The firmware advertises a Nordic-UART-style GATT service (see firmware
|
|
4
|
+
link_ble.cpp); this transport carries the exact same COBS frame stream over
|
|
5
|
+
it. Requires the `bleak` package: ``pip install python-esp-bridge[ble]``.
|
|
6
|
+
|
|
7
|
+
bleak is asyncio-based; a private event loop runs in a daemon thread so the
|
|
8
|
+
transport exposes the same blocking read()/write() interface as the serial
|
|
9
|
+
one. Notifications land in a queue that read() drains.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import queue
|
|
14
|
+
import threading
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from ..constants import BLE_LINK_RX_UUID, BLE_LINK_SERVICE_UUID, BLE_LINK_TX_UUID
|
|
18
|
+
from ..errors import BridgeError, NoDeviceError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ADV_NAME_PREFIX = "espbridge_" # firmware advertises espbridge_<mac>[_<name>]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class BleDeviceInfo:
|
|
26
|
+
name: str # advertised name: "espbridge_c049efd03fe0" or "espbridge_c049efd03fe0_relays"
|
|
27
|
+
address: str # MAC on Windows/Linux, CoreBluetooth UUID on macOS
|
|
28
|
+
rssi: int
|
|
29
|
+
|
|
30
|
+
def _parts(self) -> tuple[str, str]:
|
|
31
|
+
rest = self.name.removeprefix(ADV_NAME_PREFIX)
|
|
32
|
+
hexpart, _, custom = rest.partition("_")
|
|
33
|
+
if len(hexpart) == 12 and all(c in "0123456789abcdef" for c in hexpart):
|
|
34
|
+
return hexpart, custom
|
|
35
|
+
return "", ""
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def mac(self) -> str:
|
|
39
|
+
"""Bridge MAC (matches Info.mac) parsed from the advertised name."""
|
|
40
|
+
hexpart, _ = self._parts()
|
|
41
|
+
return ":".join(hexpart[i : i + 2] for i in range(0, 12, 2)) if hexpart else ""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def device_name(self) -> str:
|
|
45
|
+
"""User-assigned name (espbridge set-name), "" when unset."""
|
|
46
|
+
return self._parts()[1]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_bleak():
|
|
50
|
+
try:
|
|
51
|
+
import bleak # noqa: F401
|
|
52
|
+
except ImportError:
|
|
53
|
+
raise BridgeError(
|
|
54
|
+
"the Bluetooth transport needs the 'bleak' package — "
|
|
55
|
+
"install with: pip install python-esp-bridge[ble]"
|
|
56
|
+
) from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_ble_devices(timeout: float = 5.0) -> list[BleDeviceInfo]:
|
|
60
|
+
"""Scan for bridges advertising the BLE link service."""
|
|
61
|
+
_require_bleak()
|
|
62
|
+
import asyncio
|
|
63
|
+
|
|
64
|
+
from bleak import BleakScanner
|
|
65
|
+
|
|
66
|
+
async def _scan():
|
|
67
|
+
found = await BleakScanner.discover(
|
|
68
|
+
timeout=timeout, return_adv=True,
|
|
69
|
+
service_uuids=[BLE_LINK_SERVICE_UUID],
|
|
70
|
+
)
|
|
71
|
+
return [
|
|
72
|
+
BleDeviceInfo(dev.name or "", dev.address, adv.rssi)
|
|
73
|
+
for dev, adv in found.values()
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
return asyncio.run(_scan())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BleTransport:
|
|
80
|
+
"""BLE GATT transport with the same blocking interface as SerialTransport."""
|
|
81
|
+
|
|
82
|
+
has_baud = False # wireless: no baud negotiation
|
|
83
|
+
needs_auth = True # firmware requires SYS_AUTH before other commands
|
|
84
|
+
usb_chip = None
|
|
85
|
+
|
|
86
|
+
def __init__(self, address: str, *, connect_timeout: float = 10.0):
|
|
87
|
+
_require_bleak()
|
|
88
|
+
import asyncio
|
|
89
|
+
|
|
90
|
+
self.address = address
|
|
91
|
+
self._rx: queue.Queue[bytes] = queue.Queue()
|
|
92
|
+
self._loop = asyncio.new_event_loop()
|
|
93
|
+
self._thread = threading.Thread(target=self._loop.run_forever,
|
|
94
|
+
daemon=True, name="espbridge-ble")
|
|
95
|
+
self._thread.start()
|
|
96
|
+
self._client = None
|
|
97
|
+
try:
|
|
98
|
+
self._run(self._connect(address, connect_timeout), connect_timeout + 5)
|
|
99
|
+
except BaseException:
|
|
100
|
+
self._stop_loop()
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
def _run(self, coro, timeout: float):
|
|
104
|
+
import asyncio
|
|
105
|
+
|
|
106
|
+
fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
107
|
+
return fut.result(timeout)
|
|
108
|
+
|
|
109
|
+
async def _connect(self, address: str, timeout: float) -> None:
|
|
110
|
+
from bleak import BleakClient
|
|
111
|
+
from bleak.exc import BleakError
|
|
112
|
+
|
|
113
|
+
client = BleakClient(address, timeout=timeout)
|
|
114
|
+
try:
|
|
115
|
+
await client.connect()
|
|
116
|
+
await client.start_notify(BLE_LINK_TX_UUID, self._on_notify)
|
|
117
|
+
except BleakError as e:
|
|
118
|
+
raise NoDeviceError(f"BLE connect to {address} failed: {e}") from None
|
|
119
|
+
self._client = client
|
|
120
|
+
# write-without-response payload limit for this connection
|
|
121
|
+
self._chunk = max(20, client.mtu_size - 3)
|
|
122
|
+
|
|
123
|
+
def _on_notify(self, _char, data: bytearray) -> None:
|
|
124
|
+
self._rx.put(bytes(data))
|
|
125
|
+
|
|
126
|
+
async def _write(self, data: bytes) -> None:
|
|
127
|
+
for i in range(0, len(data), self._chunk):
|
|
128
|
+
await self._client.write_gatt_char(
|
|
129
|
+
BLE_LINK_RX_UUID, data[i : i + self._chunk], response=False)
|
|
130
|
+
|
|
131
|
+
async def _disconnect(self) -> None:
|
|
132
|
+
if self._client is not None:
|
|
133
|
+
try:
|
|
134
|
+
await self._client.disconnect()
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def read(self) -> bytes:
|
|
139
|
+
try:
|
|
140
|
+
return self._rx.get(timeout=0.05)
|
|
141
|
+
except queue.Empty:
|
|
142
|
+
return b""
|
|
143
|
+
|
|
144
|
+
def write(self, data: bytes) -> None:
|
|
145
|
+
self._run(self._write(data), timeout=10.0)
|
|
146
|
+
|
|
147
|
+
def set_baudrate(self, baud: int) -> None:
|
|
148
|
+
pass # wireless
|
|
149
|
+
|
|
150
|
+
def pulse_reset(self) -> None:
|
|
151
|
+
pass # no DTR/RTS lines over the air
|
|
152
|
+
|
|
153
|
+
def _stop_loop(self) -> None:
|
|
154
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
155
|
+
self._thread.join(timeout=2.0)
|
|
156
|
+
|
|
157
|
+
def close(self) -> None:
|
|
158
|
+
try:
|
|
159
|
+
self._run(self._disconnect(), timeout=5.0)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
self._stop_loop()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""In-memory transport for hardware-free tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MockTransport:
|
|
6
|
+
"""In-memory transport for hardware-free tests.
|
|
7
|
+
|
|
8
|
+
`responder(data: bytes)` is called with everything the host writes; it can
|
|
9
|
+
push firmware->host bytes back via `inject()`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
has_baud = True # exercises the baud-upgrade code path in tests
|
|
13
|
+
needs_auth = False # tests flip this to emulate the wireless link
|
|
14
|
+
|
|
15
|
+
def __init__(self, responder=None):
|
|
16
|
+
import queue
|
|
17
|
+
|
|
18
|
+
self.usb_chip = None
|
|
19
|
+
self._rx: queue.Queue[bytes] = queue.Queue()
|
|
20
|
+
self.responder = responder
|
|
21
|
+
self.closed = False
|
|
22
|
+
self.baud = 115200
|
|
23
|
+
|
|
24
|
+
def inject(self, data: bytes) -> None:
|
|
25
|
+
self._rx.put(data)
|
|
26
|
+
|
|
27
|
+
def read(self) -> bytes:
|
|
28
|
+
import queue
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
return self._rx.get(timeout=0.05)
|
|
32
|
+
except queue.Empty:
|
|
33
|
+
return b""
|
|
34
|
+
|
|
35
|
+
def write(self, data: bytes) -> None:
|
|
36
|
+
if self.responder is not None:
|
|
37
|
+
self.responder(data)
|
|
38
|
+
|
|
39
|
+
def set_baudrate(self, baud: int) -> None:
|
|
40
|
+
self.baud = baud
|
|
41
|
+
|
|
42
|
+
def pulse_reset(self) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
self.closed = True
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Serial (USB) transport with ESP32 auto-detection."""
|
|
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
|
+
has_baud = True
|
|
50
|
+
needs_auth = False # USB implies physical access
|
|
51
|
+
|
|
52
|
+
def __init__(self, port: str, baud: int = 115200, usb_chip: str | None = None):
|
|
53
|
+
import serial
|
|
54
|
+
|
|
55
|
+
self.usb_chip = usb_chip
|
|
56
|
+
self.ser = serial.Serial(port, baudrate=baud, timeout=0.05, write_timeout=2.0)
|
|
57
|
+
|
|
58
|
+
def read(self) -> bytes:
|
|
59
|
+
data = self.ser.read(1) # blocks up to `timeout`
|
|
60
|
+
waiting = self.ser.in_waiting
|
|
61
|
+
if data and waiting:
|
|
62
|
+
data += self.ser.read(waiting)
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
def write(self, data: bytes) -> None:
|
|
66
|
+
self.ser.write(data)
|
|
67
|
+
|
|
68
|
+
def set_baudrate(self, baud: int) -> None:
|
|
69
|
+
self.ser.baudrate = baud
|
|
70
|
+
self.ser.reset_input_buffer()
|
|
71
|
+
|
|
72
|
+
def pulse_reset(self) -> None:
|
|
73
|
+
"""Toggle RTS/DTR the way esptool does to hard-reset the ESP32."""
|
|
74
|
+
self.ser.dtr = False
|
|
75
|
+
self.ser.rts = True
|
|
76
|
+
time.sleep(0.1)
|
|
77
|
+
self.ser.rts = False
|
|
78
|
+
|
|
79
|
+
def close(self) -> None:
|
|
80
|
+
try:
|
|
81
|
+
self.ser.close()
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-esp-bridge
|
|
3
|
-
Version: 0.0
|
|
4
|
-
Summary: Control every ESP32 peripheral from Python over USB serial — GPIO, ADC, DAC, PWM, touch, I2C, SPI, UART, Wi-Fi sockets, BLE
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Control every ESP32 peripheral from Python over USB serial or Bluetooth — GPIO, ADC, DAC, PWM, touch, I2C, SPI, UART, Wi-Fi sockets, BLE
|
|
5
5
|
Project-URL: Homepage, https://github.com/HamzaYslmn/python-esp-bridge
|
|
6
6
|
Author: HamzaYslmn
|
|
7
7
|
Keywords: ble,bridge,esp32,firmata,gpio,i2c,raspberry-pi,serial,spi
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Requires-Dist: pyserial>=3.5
|
|
10
|
+
Provides-Extra: ble
|
|
11
|
+
Requires-Dist: bleak>=0.22; extra == 'ble'
|
|
10
12
|
Provides-Extra: oled
|
|
11
13
|
Requires-Dist: pillow>=10; extra == 'oled'
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
espbridge/__init__.py,sha256=
|
|
1
|
+
espbridge/__init__.py,sha256=6VVe2-7SouV2rk7hgoSKceNaqzWEcw4bt8mCh_P3biA,1340
|
|
2
2
|
espbridge/analog.py,sha256=zv1EFVZC2CSNBr-J0wvQw2QZIdKQzhSrknfYmaOkSig,2247
|
|
3
3
|
espbridge/ble.py,sha256=fuTWlVoqCiDlu0e0-6r4Trq1VR_7skZwSFtpIgzZNyc,8321
|
|
4
|
-
espbridge/bridge.py,sha256=
|
|
5
|
-
espbridge/cli.py,sha256=
|
|
6
|
-
espbridge/constants.py,sha256=
|
|
7
|
-
espbridge/errors.py,sha256=
|
|
4
|
+
espbridge/bridge.py,sha256=qZ7eMboWXrqtdyh2vFr2gMXV5PeGgKUMGpiqp9FNZ68,21529
|
|
5
|
+
espbridge/cli.py,sha256=AU77HpRtBtD0iKNs7e9JVetW7IGhJAj_HY_wCVyDvk8,5299
|
|
6
|
+
espbridge/constants.py,sha256=oNLij2gfgTbj75M16ByaOZ4U0fn7X1X0AWNUuFNOdE0,5419
|
|
7
|
+
espbridge/errors.py,sha256=111gvpi0J3-ioZ71X-yORDlSZvWWp3eSsy4BTZKUYbI,1203
|
|
8
8
|
espbridge/gpio.py,sha256=daaE7GgjYxGBnGpFbkKMTsqLmyao8e5Hs_1dM3uFWFc,2384
|
|
9
9
|
espbridge/i2c.py,sha256=S7uISLcMOMFTGIfOeL8li4TIG9slI6E3G380u7Q9ACM,2772
|
|
10
10
|
espbridge/net.py,sha256=PHVg2OQYh5Ap-Zdi1C9vW-qEohExzwgBT-03ImCElYI,9634
|
|
@@ -12,7 +12,7 @@ espbridge/oled.py,sha256=UYEDZmfl8UwCw4B36Awk_uGdROR2W5jRpUeOfD1sCJ0,8072
|
|
|
12
12
|
espbridge/protocol.py,sha256=kIns3gXxZ7wZABY5En5YeK1MgtT86-GVr4_JBMqIhTI,4405
|
|
13
13
|
espbridge/pwm.py,sha256=S2VzG6lh0GRM-WUaXp6uHbKZ8dvlRVs4T7WW_nCxj0o,1669
|
|
14
14
|
espbridge/spi.py,sha256=eNdrhPlpdgPPIzI4OHIC0vJ3dQnSX2W1m6lw6_n5DXk,1688
|
|
15
|
-
espbridge/transport.py,sha256=
|
|
15
|
+
espbridge/transport.py,sha256=03fJ2xZJgCu-_QEY7Bn08jVIbew2tlSKSRvt-AZGJ5E,295
|
|
16
16
|
espbridge/uart.py,sha256=-PgCjr5iMkhp9b_PYr_qPNYBwPfjYv4Ancjq0bxgWtc,3870
|
|
17
17
|
espbridge/wifi.py,sha256=q7FH-9DoClSyR7ibGCb2KHkNTP4NqrqNo6gtn9SAzpw,4198
|
|
18
18
|
espbridge/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -21,7 +21,11 @@ espbridge/compat/gpiozero.py,sha256=PMD7_yUPH0CF9mycf4XZeollH-G54MeSy3X72vXNZJY,
|
|
|
21
21
|
espbridge/compat/luma.py,sha256=iErusKC4dccFzN5qK1HfZ_q0rt50j8L9SK0ZBtc9nlY,3382
|
|
22
22
|
espbridge/compat/rpi_gpio.py,sha256=YwOXbXdNLVVHv1CbODV_tCNg59iXWEuhpBCkAaSZnF8,3346
|
|
23
23
|
espbridge/compat/smbus.py,sha256=k8h1VQe6SzqRKvtfsuVzw8YFDjceedDdod5JtG0QX0s,2519
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
espbridge/transports/__init__.py,sha256=0EFGmisSGAuhZx7UQ_RXDoNz8Pd2AqdqWCNOeLrYlzw,813
|
|
25
|
+
espbridge/transports/ble.py,sha256=IQYVdwSB2txYBjm3o9RtYSTP5wGBGHJKzakVsdn6qgc,5383
|
|
26
|
+
espbridge/transports/mock.py,sha256=WtIMB8FxinYnM7ns0bRL8lHB_Czn33T5PobGDnvWRhY,1205
|
|
27
|
+
espbridge/transports/serial.py,sha256=PZcDRg8QG7-Y4MrBKfnm6DCC_2iO_ZOtztZsKxviVjo,2505
|
|
28
|
+
python_esp_bridge-0.1.0.dist-info/METADATA,sha256=7WQYnWCLrjWW1ntL8QVVST1CsX1vuaWRQXq7azlI3G4,1469
|
|
29
|
+
python_esp_bridge-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
30
|
+
python_esp_bridge-0.1.0.dist-info/entry_points.txt,sha256=xXm9lM8iMGXRhFiDBZrncBlA_RuWOSCxUO2_v7jGuGE,49
|
|
31
|
+
python_esp_bridge-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|