esplink 1.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.
- esplink/__init__.py +88 -0
- esplink/_log.py +73 -0
- esplink/aio.py +225 -0
- esplink/analog.py +82 -0
- esplink/ble.py +250 -0
- esplink/bridge.py +1374 -0
- esplink/camera.py +90 -0
- esplink/can.py +120 -0
- esplink/cli.py +235 -0
- esplink/compat/__init__.py +0 -0
- esplink/compat/blinka.py +210 -0
- esplink/compat/gpiozero.py +233 -0
- esplink/compat/luma.py +94 -0
- esplink/compat/rpi_gpio.py +133 -0
- esplink/compat/smbus.py +63 -0
- esplink/constants.py +404 -0
- esplink/drivers/README.md +76 -0
- esplink/drivers/__init__.py +156 -0
- esplink/drivers/ads1115.py +113 -0
- esplink/drivers/bh1750.py +85 -0
- esplink/drivers/bme280.py +220 -0
- esplink/drivers/dht.py +79 -0
- esplink/drivers/ds18b20.py +68 -0
- esplink/drivers/ds3231.py +114 -0
- esplink/drivers/hcsr04.py +38 -0
- esplink/drivers/inmp441.py +67 -0
- esplink/drivers/ir.py +90 -0
- esplink/drivers/max98357.py +65 -0
- esplink/drivers/mcp3202.py +52 -0
- esplink/drivers/mcp3208.py +53 -0
- esplink/drivers/motor.py +143 -0
- esplink/drivers/mpu6050.py +112 -0
- esplink/drivers/neopixel.py +83 -0
- esplink/drivers/nrf24.py +287 -0
- esplink/drivers/oled.py +187 -0
- esplink/drivers/pca9685.py +133 -0
- esplink/drivers/pcf8574.py +61 -0
- esplink/drivers/stepper.py +93 -0
- esplink/drivers/sx126x.py +282 -0
- esplink/drivers/sx127x.py +238 -0
- esplink/drivers/sx128x.py +285 -0
- esplink/errors.py +71 -0
- esplink/espnow.py +170 -0
- esplink/eth.py +106 -0
- esplink/firmware/__init__.py +10 -0
- esplink/firmware/bridge.esp32.huge_app.bin +0 -0
- esplink/flash.py +202 -0
- esplink/fs.py +196 -0
- esplink/gpio.py +154 -0
- esplink/i2c.py +102 -0
- esplink/i2s.py +90 -0
- esplink/manager.py +194 -0
- esplink/mcp/__init__.py +19 -0
- esplink/mcp/__main__.py +5 -0
- esplink/mcp/common.py +131 -0
- esplink/mcp/server.py +197 -0
- esplink/mcp/tools.py +392 -0
- esplink/mcpwm.py +47 -0
- esplink/net.py +341 -0
- esplink/nvs.py +87 -0
- esplink/onewire.py +96 -0
- esplink/ota.py +64 -0
- esplink/protocol.py +202 -0
- esplink/pwm.py +56 -0
- esplink/rmt.py +123 -0
- esplink/spi.py +51 -0
- esplink/transports/__init__.py +33 -0
- esplink/transports/ble.py +282 -0
- esplink/transports/mock.py +46 -0
- esplink/transports/serial.py +90 -0
- esplink/transports/tcp.py +206 -0
- esplink/uart.py +130 -0
- esplink/watch.py +232 -0
- esplink/wifi.py +212 -0
- esplink-1.0.2.dist-info/METADATA +52 -0
- esplink-1.0.2.dist-info/RECORD +78 -0
- esplink-1.0.2.dist-info/WHEEL +4 -0
- esplink-1.0.2.dist-info/entry_points.txt +4 -0
esplink/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""esplink — control every ESP32 peripheral from Python over USB or Bluetooth.
|
|
2
|
+
|
|
3
|
+
Flash the firmware once (Arduino library "esplink"), then:
|
|
4
|
+
|
|
5
|
+
from esplink import Bridge
|
|
6
|
+
|
|
7
|
+
with Bridge() as esp: # USB first, then Bluetooth, then Wi-Fi
|
|
8
|
+
esp.gpio.mode(2, "output")
|
|
9
|
+
esp.gpio.write(2, 1)
|
|
10
|
+
print(esp.adc.read(34))
|
|
11
|
+
print(esp.i2c.scan())
|
|
12
|
+
esp.wifi.connect("ssid", "password")
|
|
13
|
+
sock = esp.net.tcp_connect("example.com", 80) # TCP through the ESP32 radio
|
|
14
|
+
|
|
15
|
+
esp.nvs.set("runs", 1) # on-board key/value storage
|
|
16
|
+
esp.fs.mount("littlefs") # on-board files
|
|
17
|
+
esp.can.begin(tx=21, rx=22) # CAN bus
|
|
18
|
+
esp.ota.flash("new.bin") # update the firmware over the link
|
|
19
|
+
|
|
20
|
+
# device drivers in pure Python (RMT / 1-Wire / I2C primitives), all under
|
|
21
|
+
# esplink.drivers: dht, oled, neopixel, ds18b20, hcsr04, ir, stepper, ...
|
|
22
|
+
# ...or write/register your own — esp.<name>(...) — see esplink.drivers
|
|
23
|
+
# and docs/DRIVERS.md ("bring your own driver").
|
|
24
|
+
"""
|
|
25
|
+
from .bridge import Bridge, BridgeSet, Info
|
|
26
|
+
from .constants import Cap, ChipModel, Status
|
|
27
|
+
from .drivers import driver_names, register_driver
|
|
28
|
+
from .manager import BridgeManager, connect, disconnect_all
|
|
29
|
+
from .errors import (
|
|
30
|
+
AuthError,
|
|
31
|
+
BridgeError,
|
|
32
|
+
BridgeTimeoutError,
|
|
33
|
+
NoDeviceError,
|
|
34
|
+
ProtocolError,
|
|
35
|
+
RemoteError,
|
|
36
|
+
UnsupportedError,
|
|
37
|
+
)
|
|
38
|
+
from .transports import find_ports, find_wifi_devices
|
|
39
|
+
from .gpio import EdgeEvent
|
|
40
|
+
from .watch import WatchEvent
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_ble_devices(timeout: float = 5.0):
|
|
44
|
+
"""Scan for bridges advertising over Bluetooth (needs the [ble] extra)."""
|
|
45
|
+
from .transports.ble import find_ble_devices as _scan
|
|
46
|
+
|
|
47
|
+
return _scan(timeout)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def __getattr__(name):
|
|
51
|
+
# Lazy: importing AsyncBridge pulls in asyncio, so defer it until first use
|
|
52
|
+
# rather than charging every sync/USB-only script for the import.
|
|
53
|
+
if name == "AsyncBridge":
|
|
54
|
+
from .aio import AsyncBridge
|
|
55
|
+
|
|
56
|
+
return AsyncBridge
|
|
57
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
58
|
+
|
|
59
|
+
__version__ = "1.0.1"
|
|
60
|
+
|
|
61
|
+
# Grouped by role, not alphabetised: this list is the shape of the public API.
|
|
62
|
+
__all__ = [
|
|
63
|
+
"Bridge",
|
|
64
|
+
"AsyncBridge",
|
|
65
|
+
"BridgeSet",
|
|
66
|
+
"BridgeManager",
|
|
67
|
+
"connect",
|
|
68
|
+
"disconnect_all",
|
|
69
|
+
"register_driver",
|
|
70
|
+
"driver_names",
|
|
71
|
+
"find_ble_devices",
|
|
72
|
+
"find_wifi_devices",
|
|
73
|
+
"Info",
|
|
74
|
+
"Cap",
|
|
75
|
+
"ChipModel",
|
|
76
|
+
"Status",
|
|
77
|
+
"AuthError",
|
|
78
|
+
"BridgeError",
|
|
79
|
+
"BridgeTimeoutError",
|
|
80
|
+
"NoDeviceError",
|
|
81
|
+
"ProtocolError",
|
|
82
|
+
"RemoteError",
|
|
83
|
+
"UnsupportedError",
|
|
84
|
+
"find_ports",
|
|
85
|
+
"EdgeEvent",
|
|
86
|
+
"WatchEvent",
|
|
87
|
+
"__version__",
|
|
88
|
+
]
|
esplink/_log.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Library logger: "[esplink]" prefix + uvicorn-style colored level prefix.
|
|
2
|
+
|
|
3
|
+
Mirrors uvicorn.logging.DefaultFormatter (same colors, same ``LEVEL:``
|
|
4
|
+
padding) without depending on uvicorn: colors auto-enable only when stderr
|
|
5
|
+
is a terminal that supports ANSI, and NO_COLOR is honored.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
# uvicorn's palette: DEBUG cyan, INFO green, WARNING yellow, ERROR red,
|
|
14
|
+
# CRITICAL bright red.
|
|
15
|
+
_LEVEL_COLORS = {
|
|
16
|
+
logging.DEBUG: 36,
|
|
17
|
+
logging.INFO: 32,
|
|
18
|
+
logging.WARNING: 33,
|
|
19
|
+
logging.ERROR: 31,
|
|
20
|
+
logging.CRITICAL: 91,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _supports_color(stream) -> bool:
|
|
25
|
+
if os.environ.get("NO_COLOR"):
|
|
26
|
+
return False
|
|
27
|
+
if not getattr(stream, "isatty", lambda: False)():
|
|
28
|
+
return False
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
# Windows Terminal and VS Code already have VT processing enabled.
|
|
31
|
+
# For the classic Windows console, try to enable it here.
|
|
32
|
+
# If the call fails, ANSI escape sequences are not supported.
|
|
33
|
+
import ctypes
|
|
34
|
+
|
|
35
|
+
kernel32 = ctypes.windll.kernel32
|
|
36
|
+
handle = kernel32.GetStdHandle(-12) # STD_ERROR_HANDLE
|
|
37
|
+
mode = ctypes.c_uint32()
|
|
38
|
+
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
39
|
+
return False
|
|
40
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
41
|
+
return bool(kernel32.SetConsoleMode(
|
|
42
|
+
handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Formatter(logging.Formatter):
|
|
47
|
+
def __init__(self, use_colors: bool):
|
|
48
|
+
super().__init__("[esplink] %(levelprefix)s %(message)s")
|
|
49
|
+
self.use_colors = use_colors
|
|
50
|
+
|
|
51
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
52
|
+
pad = " " * max(0, 8 - len(record.levelname)) # right-pad level name to 8 chars, matching uvicorn's alignment
|
|
53
|
+
if self.use_colors:
|
|
54
|
+
color = _LEVEL_COLORS.get(record.levelno, 0)
|
|
55
|
+
record.levelprefix = (
|
|
56
|
+
f"\x1b[{color}m{record.levelname}\x1b[0m:{pad}")
|
|
57
|
+
else:
|
|
58
|
+
record.levelprefix = f"{record.levelname}:{pad}"
|
|
59
|
+
return super().format(record)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
log = logging.getLogger("esplink")
|
|
63
|
+
if not log.handlers:
|
|
64
|
+
if os.environ.get("ESPLINK_DEBUG"):
|
|
65
|
+
# Enable frame-level tracing (every request and response with command
|
|
66
|
+
# names) without modifying the script: ESPLINK_DEBUG=1 python app.py
|
|
67
|
+
log.setLevel(logging.DEBUG)
|
|
68
|
+
elif log.level == logging.NOTSET: # respect a level the app set first
|
|
69
|
+
log.setLevel(logging.INFO)
|
|
70
|
+
_handler = logging.StreamHandler(sys.stderr)
|
|
71
|
+
_handler.setFormatter(_Formatter(_supports_color(sys.stderr)))
|
|
72
|
+
log.addHandler(_handler)
|
|
73
|
+
log.propagate = False
|
esplink/aio.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""asyncio facade: ``await`` every bridge call and fan out concurrent I/O.
|
|
2
|
+
|
|
3
|
+
The ESP32 firmware is *already* concurrent — requests carry a sequence number,
|
|
4
|
+
many can be in flight at once, and a slow call on one task never stalls the
|
|
5
|
+
fast lane (see ``examples/basics/rtos_concurrency.py``). ``AsyncBridge`` simply
|
|
6
|
+
exposes that concurrency to ``asyncio`` so you can ``await`` bridge calls and
|
|
7
|
+
run them together with ``asyncio.gather``, instead of juggling threads yourself:
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from esplink import AsyncBridge
|
|
11
|
+
|
|
12
|
+
async def main():
|
|
13
|
+
async with AsyncBridge(ble=False) as esp:
|
|
14
|
+
await esp.gpio.mode(2, "output")
|
|
15
|
+
await esp.gpio.write(2, 1)
|
|
16
|
+
# both reads are issued together and overlap on the wire:
|
|
17
|
+
t, h = await asyncio.gather(esp.adc.read(34), esp.adc.read(35))
|
|
18
|
+
print(t, h)
|
|
19
|
+
|
|
20
|
+
asyncio.run(main())
|
|
21
|
+
|
|
22
|
+
Edge interrupts (``gpio.watch``) become an async stream — no callback thread:
|
|
23
|
+
|
|
24
|
+
async with AsyncBridge() as esp:
|
|
25
|
+
await esp.gpio.mode(4, "input_pullup")
|
|
26
|
+
await esp.gpio.watch(4, edge="falling", debounce_ms=30)
|
|
27
|
+
async for ev in esp.gpio_events():
|
|
28
|
+
print("pressed", ev.pin, ev.millis)
|
|
29
|
+
|
|
30
|
+
How it works (and what to expect): the firmware speaks a synchronous
|
|
31
|
+
request/response protocol and the underlying ``Bridge`` is fully thread-safe,
|
|
32
|
+
so each awaited call runs the matching ``Bridge`` method in a worker thread
|
|
33
|
+
(``asyncio.to_thread``). Calls awaited together genuinely overlap on the link
|
|
34
|
+
because ``Bridge`` pipelines concurrent requests and correlates replies by
|
|
35
|
+
sequence number — ``await`` here is about *the host not blocking*, not about
|
|
36
|
+
moving work onto the ESP32 (the firmware's task split already does that).
|
|
37
|
+
|
|
38
|
+
For tight inner loops or fire-and-forget streaming (e.g. pushing OLED frames),
|
|
39
|
+
the plain synchronous ``Bridge`` is still the right tool — there is nothing to
|
|
40
|
+
gain from awaiting each byte, and a thread per call would only add overhead.
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import asyncio
|
|
45
|
+
import functools
|
|
46
|
+
|
|
47
|
+
from . import constants as C
|
|
48
|
+
from .bridge import Bridge
|
|
49
|
+
from .gpio import EdgeEvent
|
|
50
|
+
from .watch import WatchEvent
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _to_coro(fn):
|
|
54
|
+
"""Wrap a blocking bridge callable so it runs in a thread and can be awaited."""
|
|
55
|
+
@functools.wraps(fn)
|
|
56
|
+
async def _call(*args, **kwargs):
|
|
57
|
+
return await asyncio.to_thread(fn, *args, **kwargs)
|
|
58
|
+
return _call
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _async_attr(attr):
|
|
62
|
+
"""Adapt a bound bridge attribute for async use: callables become coroutines
|
|
63
|
+
that run in a worker thread; plain values (info, caps, max_write, ...) pass
|
|
64
|
+
through. One uniform rule — nothing has to enumerate which calls touch the
|
|
65
|
+
wire, so a new Bridge method is awaitable automatically."""
|
|
66
|
+
return _to_coro(attr) if callable(attr) else attr
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _AsyncSubApi:
|
|
70
|
+
"""Proxies a sync sub-API (``esp.gpio``, ``esp.adc``, ...): every method
|
|
71
|
+
becomes awaitable; plain attributes (e.g. ``i2c.max_write``) pass through."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, target):
|
|
74
|
+
self._target = target
|
|
75
|
+
|
|
76
|
+
def __getattr__(self, name):
|
|
77
|
+
if name.startswith("_"): # don't expose the sub-API's internals
|
|
78
|
+
raise AttributeError(name)
|
|
79
|
+
return _async_attr(getattr(self._target, name))
|
|
80
|
+
|
|
81
|
+
def __dir__(self):
|
|
82
|
+
return dir(self._target)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _EventStream:
|
|
86
|
+
"""Async iterator over a firmware event, fed from the reader thread.
|
|
87
|
+
|
|
88
|
+
Registered with ``Bridge.on_event``; each event payload is handed to the
|
|
89
|
+
asyncio loop via ``call_soon_threadsafe`` and surfaces through ``async for``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, bridge: Bridge, cmd: int, loop, parse=None, maxsize: int = 0):
|
|
93
|
+
self._bridge = bridge
|
|
94
|
+
self._cmd = cmd
|
|
95
|
+
self._loop = loop
|
|
96
|
+
self._parse = parse
|
|
97
|
+
self._queue: asyncio.Queue = asyncio.Queue(maxsize)
|
|
98
|
+
bridge.on_event(cmd, self._on_event)
|
|
99
|
+
|
|
100
|
+
def _on_event(self, payload: bytes) -> None:
|
|
101
|
+
item = self._parse(payload) if self._parse else payload
|
|
102
|
+
if item is None: # parser rejected a malformed/short payload
|
|
103
|
+
return
|
|
104
|
+
# Hop from the reader thread onto the asyncio loop; drop if the queue is
|
|
105
|
+
# full (a bounded maxsize gives back-pressure without blocking the reader).
|
|
106
|
+
self._loop.call_soon_threadsafe(self._offer, item)
|
|
107
|
+
|
|
108
|
+
def _offer(self, item) -> None:
|
|
109
|
+
try:
|
|
110
|
+
self._queue.put_nowait(item)
|
|
111
|
+
except asyncio.QueueFull:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def __aiter__(self):
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
async def __anext__(self):
|
|
118
|
+
return await self._queue.get()
|
|
119
|
+
|
|
120
|
+
async def aclose(self) -> None:
|
|
121
|
+
"""Stop the stream and unregister it from the bridge (call when done, or
|
|
122
|
+
just break out of the ``async for`` and let it be garbage-collected)."""
|
|
123
|
+
self._bridge.off_event(self._cmd, self._on_event)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AsyncBridge:
|
|
127
|
+
"""``asyncio`` wrapper around :class:`Bridge`.
|
|
128
|
+
|
|
129
|
+
Accepts the same arguments as ``Bridge``. Connection happens off the event
|
|
130
|
+
loop, so use it as an async context manager (or ``await .connect()``):
|
|
131
|
+
|
|
132
|
+
>>> async with AsyncBridge(ble=False) as esp:
|
|
133
|
+
... await esp.gpio.write(2, 1)
|
|
134
|
+
|
|
135
|
+
The wrapped synchronous bridge is available as ``.bridge`` for anything not
|
|
136
|
+
surfaced here (events, ``on_event``, device drivers).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, *args, **kwargs):
|
|
140
|
+
# Bridge() connects (and blocks) in its constructor, so defer it. Args
|
|
141
|
+
# are passed straight through, so AsyncBridge("relays") selects a board
|
|
142
|
+
# exactly the way Bridge("relays") does.
|
|
143
|
+
self._args, self._kwargs = args, kwargs
|
|
144
|
+
self._bridge = None
|
|
145
|
+
self._owned = True # we opened it, so we close it
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def wrap(cls, bridge: Bridge) -> AsyncBridge:
|
|
149
|
+
"""Wrap an already-connected Bridge for ``await`` use, sharing its link.
|
|
150
|
+
|
|
151
|
+
Lets an async app reuse the one shared connection instead of opening a
|
|
152
|
+
second: ``esp = AsyncBridge.wrap(esplink.connect(ble=False))``.
|
|
153
|
+
``close()`` is then a no-op for the wrapper — whoever owns the Bridge
|
|
154
|
+
(e.g. the BridgeManager behind connect()) closes it.
|
|
155
|
+
"""
|
|
156
|
+
self = cls.__new__(cls)
|
|
157
|
+
self._args, self._kwargs = (), {}
|
|
158
|
+
self._bridge = bridge
|
|
159
|
+
self._owned = False
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def bridge(self) -> Bridge:
|
|
164
|
+
"""The underlying synchronous :class:`Bridge` (for events, ``on_event``,
|
|
165
|
+
device drivers, or any method not surfaced here). Raises if not yet
|
|
166
|
+
connected."""
|
|
167
|
+
if self._bridge is None:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
"AsyncBridge is not connected — use `async with AsyncBridge(...)` "
|
|
170
|
+
"or `await bridge.connect()`"
|
|
171
|
+
)
|
|
172
|
+
return self._bridge
|
|
173
|
+
|
|
174
|
+
async def connect(self) -> AsyncBridge:
|
|
175
|
+
"""Open the link off the event loop (Bridge() blocks while connecting).
|
|
176
|
+
Usually you don't call this directly — use ``async with AsyncBridge():``."""
|
|
177
|
+
if self._bridge is None:
|
|
178
|
+
self._bridge = await asyncio.to_thread(Bridge, *self._args,
|
|
179
|
+
**self._kwargs)
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
async def close(self) -> None:
|
|
183
|
+
"""Close the link off the event loop. No-op for a wrapper created with
|
|
184
|
+
:meth:`wrap` (whoever owns the underlying Bridge closes it)."""
|
|
185
|
+
if self._bridge is not None and self._owned:
|
|
186
|
+
await asyncio.to_thread(self._bridge.close)
|
|
187
|
+
self._bridge = None
|
|
188
|
+
|
|
189
|
+
async def __aenter__(self) -> AsyncBridge:
|
|
190
|
+
return await self.connect()
|
|
191
|
+
|
|
192
|
+
async def __aexit__(self, *exc) -> None:
|
|
193
|
+
await self.close()
|
|
194
|
+
|
|
195
|
+
def events(self, cmd: int, *, parse=None, maxsize: int = 0) -> _EventStream:
|
|
196
|
+
"""``async for`` over a raw firmware event ``cmd`` (payloads are bytes
|
|
197
|
+
unless ``parse`` is given). See ``gpio_events()`` for the GPIO edge case."""
|
|
198
|
+
return _EventStream(self.bridge, cmd, asyncio.get_running_loop(),
|
|
199
|
+
parse=parse, maxsize=maxsize)
|
|
200
|
+
|
|
201
|
+
def gpio_events(self, *, maxsize: int = 0) -> _EventStream:
|
|
202
|
+
"""``async for ev in esp.gpio_events()`` yields :class:`EdgeEvent`s from
|
|
203
|
+
every pin armed with ``await esp.gpio.watch(pin, ...)``."""
|
|
204
|
+
return self.events(C.GPIO_EDGE_EVT, parse=EdgeEvent.parse, maxsize=maxsize)
|
|
205
|
+
|
|
206
|
+
def watch_events(self, *, maxsize: int = 0) -> _EventStream:
|
|
207
|
+
"""``async for ev in esp.watch_events()`` yields :class:`WatchEvent`s
|
|
208
|
+
from every rule defined with ``await esp.watch.add(...)`` — the polled,
|
|
209
|
+
non-interrupt, threshold-based events."""
|
|
210
|
+
return self.events(C.WATCH_EVT, parse=WatchEvent.parse, maxsize=maxsize)
|
|
211
|
+
|
|
212
|
+
def __getattr__(self, name):
|
|
213
|
+
if name.startswith("_"): # never proxy private/dunder lookups
|
|
214
|
+
raise AttributeError(name)
|
|
215
|
+
attr = getattr(self.bridge, name)
|
|
216
|
+
if name in Bridge._SUBAPIS:
|
|
217
|
+
proxy = _AsyncSubApi(attr)
|
|
218
|
+
setattr(self, name, proxy) # cache: this name skips __getattr__ next time
|
|
219
|
+
return proxy
|
|
220
|
+
return _async_attr(attr)
|
|
221
|
+
|
|
222
|
+
def __dir__(self):
|
|
223
|
+
extra = (*Bridge._SUBAPIS, "bridge", "connect", "close",
|
|
224
|
+
"events", "gpio_events", "watch_events")
|
|
225
|
+
return [*super().__dir__(), *extra]
|
esplink/analog.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""ADC (oneshot reads), DAC (write + cosine generator), capacitive touch."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
from . import constants as C
|
|
7
|
+
|
|
8
|
+
# Attenuation level -> approximate full-scale input range on classic ESP32:
|
|
9
|
+
# 0 dB ≈ 1.1 V, 2.5 dB ≈ 1.5 V, 6 dB ≈ 2.2 V, 11 dB ≈ 3.3 V (default)
|
|
10
|
+
ATTEN = {0: 0, 2.5: 1, 6: 2, 11: 3, "0db": 0, "2.5db": 1, "6db": 2, "11db": 3}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Adc:
|
|
14
|
+
"""Oneshot ADC reads.
|
|
15
|
+
|
|
16
|
+
esp.adc.config(34, atten=11) # full ~3.3 V range
|
|
17
|
+
raw = esp.adc.read(34) # 0..4095
|
|
18
|
+
mv = esp.adc.read_mv(34) # calibrated millivolts
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, bridge):
|
|
22
|
+
self._b = bridge
|
|
23
|
+
|
|
24
|
+
def config(self, pin: int, atten: float = 11) -> None:
|
|
25
|
+
"""Set the input attenuation for a pin (0/2.5/6/11 dB; default 11 ≈ 3.3 V)."""
|
|
26
|
+
self._b.request(C.ADC_CONFIG, bytes([pin, ATTEN.get(atten, int(atten))]))
|
|
27
|
+
|
|
28
|
+
def read(self, pin: int) -> int:
|
|
29
|
+
"""Raw 12-bit reading (0..4095)."""
|
|
30
|
+
return struct.unpack(">H", self._b.request(C.ADC_READ, bytes([pin])))[0]
|
|
31
|
+
|
|
32
|
+
def read_mv(self, pin: int) -> int:
|
|
33
|
+
"""Calibrated millivolts."""
|
|
34
|
+
return struct.unpack(">H", self._b.request(C.ADC_READ_MV, bytes([pin])))[0]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Dac:
|
|
38
|
+
"""True 8-bit DAC — classic ESP32 (GPIO 25/26) and S2 (GPIO 17/18) only.
|
|
39
|
+
|
|
40
|
+
esp.dac.write(25, 128) # ~1.65 V
|
|
41
|
+
esp.dac.cosine(25, 1000) # 1 kHz cosine wave
|
|
42
|
+
esp.dac.disable(25) # stops the wave and releases the pin
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, bridge):
|
|
46
|
+
bridge.require(C.Cap.DAC, "DAC")
|
|
47
|
+
self._b = bridge
|
|
48
|
+
|
|
49
|
+
def write(self, pin: int, value: int) -> None:
|
|
50
|
+
"""Output value 0..255 (0..3.3 V)."""
|
|
51
|
+
self._b.request(C.DAC_WRITE, bytes([pin, value & 0xFF]))
|
|
52
|
+
|
|
53
|
+
def cosine(self, pin: int, freq_hz: int, *, scale: int = 0, offset: int = 0,
|
|
54
|
+
phase_180: bool = False) -> None:
|
|
55
|
+
"""Start the hardware cosine-wave generator (~130 Hz .. ~100 kHz).
|
|
56
|
+
|
|
57
|
+
scale: 0 = full amplitude, 1 = half, 2 = quarter, 3 = eighth.
|
|
58
|
+
"""
|
|
59
|
+
self._b.request(C.DAC_COSINE, struct.pack(">BIBbB", pin, freq_hz, scale & 3,
|
|
60
|
+
offset, 1 if phase_180 else 0))
|
|
61
|
+
|
|
62
|
+
def disable(self, pin: int) -> None:
|
|
63
|
+
"""Stop any cosine generator on the pin, turn the DAC off and release it.
|
|
64
|
+
|
|
65
|
+
A plain :meth:`write` also stops the generator, so there is nothing else
|
|
66
|
+
to call to take the pin back."""
|
|
67
|
+
self._b.request(C.DAC_DISABLE, bytes([pin]))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Touch:
|
|
71
|
+
"""Capacitive touch pads (classic ESP32: lower = touched; S2/S3: higher = touched).
|
|
72
|
+
|
|
73
|
+
val = esp.touch.read(4) # raw pad reading
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, bridge):
|
|
77
|
+
bridge.require(C.Cap.TOUCH, "touch sensing")
|
|
78
|
+
self._b = bridge
|
|
79
|
+
|
|
80
|
+
def read(self, pin: int) -> int:
|
|
81
|
+
"""Raw capacitive reading for a touch pad."""
|
|
82
|
+
return struct.unpack(">I", self._b.request(C.TOUCH_READ, bytes([pin])))[0]
|