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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""CircuitPython / Adafruit-Blinka compatible I2C, SPI and DigitalInOut.
|
|
2
|
+
|
|
3
|
+
Implements the same duck-typed protocols as ``busio.I2C``, ``busio.SPI`` and
|
|
4
|
+
``digitalio.DigitalInOut``, so the hundreds of ``adafruit-circuitpython-*``
|
|
5
|
+
driver libraries run unchanged through the bridge:
|
|
6
|
+
|
|
7
|
+
pip install adafruit-circuitpython-bme280
|
|
8
|
+
|
|
9
|
+
from espbridge import Bridge
|
|
10
|
+
from espbridge.compat.blinka import I2C
|
|
11
|
+
from adafruit_bme280.basic import Adafruit_BME280_I2C
|
|
12
|
+
|
|
13
|
+
with Bridge() as esp:
|
|
14
|
+
bme = Adafruit_BME280_I2C(I2C(esp))
|
|
15
|
+
print(bme.temperature, bme.relative_humidity)
|
|
16
|
+
|
|
17
|
+
SPI devices work the same way, with CS handled by ``DigitalInOut`` exactly as
|
|
18
|
+
``adafruit_bus_device.spi_device.SPIDevice`` expects.
|
|
19
|
+
No Adafruit package is imported here — only their wire protocols are spoken.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class I2C:
|
|
27
|
+
"""busio.I2C-compatible bus on the ESP32's I2C."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, esp, *, sda: int = 21, scl: int = 22,
|
|
30
|
+
frequency: int = 400_000, bus: int = 0, init: bool = True):
|
|
31
|
+
self._i2c = esp.i2c
|
|
32
|
+
self._bus = bus
|
|
33
|
+
self._lock = threading.Lock()
|
|
34
|
+
if init:
|
|
35
|
+
self._i2c.init(sda=sda, scl=scl, freq=frequency, bus=bus)
|
|
36
|
+
|
|
37
|
+
# -- locking protocol (drivers call these around every transaction) ------
|
|
38
|
+
def try_lock(self) -> bool:
|
|
39
|
+
return self._lock.acquire(blocking=False)
|
|
40
|
+
|
|
41
|
+
def unlock(self) -> None:
|
|
42
|
+
self._lock.release()
|
|
43
|
+
|
|
44
|
+
# -- transfers -------------------------------------------------------------
|
|
45
|
+
def scan(self) -> list[int]:
|
|
46
|
+
return self._i2c.scan(self._bus)
|
|
47
|
+
|
|
48
|
+
def writeto(self, address: int, buffer, *, start: int = 0, end=None) -> None:
|
|
49
|
+
end = len(buffer) if end is None else end
|
|
50
|
+
self._i2c.write(address, bytes(buffer[start:end]), self._bus)
|
|
51
|
+
|
|
52
|
+
def readfrom_into(self, address: int, buffer, *, start: int = 0, end=None) -> None:
|
|
53
|
+
end = len(buffer) if end is None else end
|
|
54
|
+
data = self._i2c.read(address, end - start, self._bus)
|
|
55
|
+
buffer[start:end] = data
|
|
56
|
+
|
|
57
|
+
def writeto_then_readfrom(self, address: int, out_buffer, in_buffer, *,
|
|
58
|
+
out_start: int = 0, out_end=None,
|
|
59
|
+
in_start: int = 0, in_end=None) -> None:
|
|
60
|
+
out_end = len(out_buffer) if out_end is None else out_end
|
|
61
|
+
in_end = len(in_buffer) if in_end is None else in_end
|
|
62
|
+
data = self._i2c.write_read(address, bytes(out_buffer[out_start:out_end]),
|
|
63
|
+
in_end - in_start, self._bus)
|
|
64
|
+
in_buffer[in_start:in_end] = data
|
|
65
|
+
|
|
66
|
+
def deinit(self) -> None:
|
|
67
|
+
self._i2c.deinit(self._bus)
|
|
68
|
+
|
|
69
|
+
def __enter__(self):
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, *exc):
|
|
73
|
+
self.deinit()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SPI:
|
|
77
|
+
"""busio.SPI-compatible bus on the ESP32's SPI.
|
|
78
|
+
|
|
79
|
+
Chip select is *not* handled here — pass a ``DigitalInOut`` to
|
|
80
|
+
``adafruit_bus_device.spi_device.SPIDevice``, exactly like on a Pi.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, esp, *, sck: int = 18, mosi: int = 23, miso: int = 19,
|
|
84
|
+
host: int = 0):
|
|
85
|
+
self._spi = esp.spi
|
|
86
|
+
self._host = host
|
|
87
|
+
self._pins = (sck, miso, mosi)
|
|
88
|
+
self._lock = threading.Lock()
|
|
89
|
+
self._frequency = 100_000
|
|
90
|
+
self.configure() # CircuitPython default: 100 kHz, mode 0
|
|
91
|
+
|
|
92
|
+
def try_lock(self) -> bool:
|
|
93
|
+
return self._lock.acquire(blocking=False)
|
|
94
|
+
|
|
95
|
+
def unlock(self) -> None:
|
|
96
|
+
self._lock.release()
|
|
97
|
+
|
|
98
|
+
def configure(self, *, baudrate: int = 100_000, polarity: int = 0,
|
|
99
|
+
phase: int = 0, bits: int = 8) -> None:
|
|
100
|
+
if bits != 8:
|
|
101
|
+
raise ValueError("only 8-bit SPI transfers are supported")
|
|
102
|
+
sck, miso, mosi = self._pins
|
|
103
|
+
self._frequency = baudrate
|
|
104
|
+
self._spi.init(sck=sck, miso=miso, mosi=mosi, freq=baudrate,
|
|
105
|
+
mode=(polarity << 1) | phase, host=self._host)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def frequency(self) -> int:
|
|
109
|
+
return self._frequency
|
|
110
|
+
|
|
111
|
+
def write(self, buffer, *, start: int = 0, end=None) -> None:
|
|
112
|
+
end = len(buffer) if end is None else end
|
|
113
|
+
self._spi.transfer(bytes(buffer[start:end]), host=self._host)
|
|
114
|
+
|
|
115
|
+
def readinto(self, buffer, *, start: int = 0, end=None, write_value: int = 0) -> None:
|
|
116
|
+
end = len(buffer) if end is None else end
|
|
117
|
+
rx = self._spi.transfer(bytes([write_value]) * (end - start), host=self._host)
|
|
118
|
+
buffer[start:end] = rx
|
|
119
|
+
|
|
120
|
+
def write_readinto(self, out_buffer, in_buffer, *,
|
|
121
|
+
out_start: int = 0, out_end=None,
|
|
122
|
+
in_start: int = 0, in_end=None) -> None:
|
|
123
|
+
out_end = len(out_buffer) if out_end is None else out_end
|
|
124
|
+
in_end = len(in_buffer) if in_end is None else in_end
|
|
125
|
+
if (out_end - out_start) != (in_end - in_start):
|
|
126
|
+
raise ValueError("buffer slices must be of equal length")
|
|
127
|
+
rx = self._spi.transfer(bytes(out_buffer[out_start:out_end]), host=self._host)
|
|
128
|
+
in_buffer[in_start:in_end] = rx
|
|
129
|
+
|
|
130
|
+
def deinit(self) -> None:
|
|
131
|
+
self._spi.deinit(self._host)
|
|
132
|
+
|
|
133
|
+
def __enter__(self):
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __exit__(self, *exc):
|
|
137
|
+
self.deinit()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Direction:
|
|
141
|
+
INPUT = "input"
|
|
142
|
+
OUTPUT = "output"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Pull:
|
|
146
|
+
UP = "up"
|
|
147
|
+
DOWN = "down"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DigitalInOut:
|
|
151
|
+
"""digitalio.DigitalInOut-compatible pin (used for CS lines and plain GPIO)."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, esp, pin: int):
|
|
154
|
+
self._gpio = esp.gpio
|
|
155
|
+
self.pin = pin
|
|
156
|
+
self._direction = Direction.INPUT
|
|
157
|
+
self._pull = None
|
|
158
|
+
self._value = False
|
|
159
|
+
self._gpio.mode(pin, "input")
|
|
160
|
+
|
|
161
|
+
def switch_to_output(self, value: bool = False, drive_mode=None) -> None:
|
|
162
|
+
self._direction = Direction.OUTPUT
|
|
163
|
+
self._gpio.mode(self.pin, "output")
|
|
164
|
+
self.value = value
|
|
165
|
+
|
|
166
|
+
def switch_to_input(self, pull=None) -> None:
|
|
167
|
+
self._direction = Direction.INPUT
|
|
168
|
+
self._pull = pull
|
|
169
|
+
self._gpio.mode(self.pin, {Pull.UP: "input_pullup",
|
|
170
|
+
Pull.DOWN: "input_pulldown"}.get(pull, "input"))
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def direction(self):
|
|
174
|
+
return self._direction
|
|
175
|
+
|
|
176
|
+
@direction.setter
|
|
177
|
+
def direction(self, value) -> None:
|
|
178
|
+
if value == Direction.OUTPUT:
|
|
179
|
+
self.switch_to_output()
|
|
180
|
+
else:
|
|
181
|
+
self.switch_to_input()
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def value(self) -> bool:
|
|
185
|
+
if self._direction == Direction.OUTPUT:
|
|
186
|
+
return self._value
|
|
187
|
+
return bool(self._gpio.read(self.pin))
|
|
188
|
+
|
|
189
|
+
@value.setter
|
|
190
|
+
def value(self, val: bool) -> None:
|
|
191
|
+
self._value = bool(val)
|
|
192
|
+
self._gpio.write(self.pin, self._value)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def pull(self):
|
|
196
|
+
return self._pull
|
|
197
|
+
|
|
198
|
+
@pull.setter
|
|
199
|
+
def pull(self, value) -> None:
|
|
200
|
+
self.switch_to_input(value)
|
|
201
|
+
|
|
202
|
+
def deinit(self) -> None:
|
|
203
|
+
self._gpio.mode(self.pin, "input")
|
|
204
|
+
|
|
205
|
+
def __enter__(self):
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
def __exit__(self, *exc):
|
|
209
|
+
self.deinit()
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""gpiozero pin factory — LED, Button, PWMLED & friends on ESP32 pins.
|
|
2
|
+
|
|
3
|
+
pip install gpiozero
|
|
4
|
+
|
|
5
|
+
from gpiozero import LED, Button
|
|
6
|
+
from espbridge import Bridge
|
|
7
|
+
from espbridge.compat.gpiozero import EspBridgeFactory
|
|
8
|
+
|
|
9
|
+
esp = Bridge()
|
|
10
|
+
factory = EspBridgeFactory(esp)
|
|
11
|
+
|
|
12
|
+
led = LED(2, pin_factory=factory) # ESP32 GPIO numbers
|
|
13
|
+
btn = Button(4, pin_factory=factory)
|
|
14
|
+
btn.when_pressed = led.toggle
|
|
15
|
+
|
|
16
|
+
# or make it the default for every gpiozero device:
|
|
17
|
+
from gpiozero import Device
|
|
18
|
+
Device.pin_factory = factory
|
|
19
|
+
|
|
20
|
+
Supported: input/output, pull-up/down, edge detection with debounce
|
|
21
|
+
(``when_pressed``/``when_changed``), and PWM (``PWMLED``, ``value=0.5``).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
from gpiozero.exc import PinInvalidFunction, PinInvalidPin, PinInvalidPull, PinSetInput
|
|
28
|
+
from gpiozero.pins import BoardInfo, Factory, HeaderInfo, Pin, PinInfo
|
|
29
|
+
|
|
30
|
+
_PWM_RES_BITS = 12
|
|
31
|
+
_PWM_MAX = (1 << _PWM_RES_BITS) - 1
|
|
32
|
+
_EDGE_MAP = {"both": "change", "rising": "rising", "falling": "falling"}
|
|
33
|
+
_PULL_MODES = {"floating": "input", "up": "input_pullup", "down": "input_pulldown"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _board_info(gpio_count: int, chip: str) -> BoardInfo:
|
|
37
|
+
pins = {
|
|
38
|
+
n: PinInfo(number=n, name=f"GPIO{n}",
|
|
39
|
+
names=frozenset({n, str(n), f"GPIO{n}"}),
|
|
40
|
+
pull="", row=n + 1, col=1, interfaces=frozenset({"gpio"}))
|
|
41
|
+
for n in range(gpio_count)
|
|
42
|
+
}
|
|
43
|
+
header = HeaderInfo(name="GPIO", rows=gpio_count, columns=1, pins=pins)
|
|
44
|
+
return BoardInfo(
|
|
45
|
+
revision="espbridge", model=chip, pcb_revision="1.0", released="2026",
|
|
46
|
+
soc=chip, manufacturer="Espressif", memory=0, storage="flash",
|
|
47
|
+
usb=1, usb3=0, ethernet=0, eth_speed=0, wifi=True, bluetooth=True,
|
|
48
|
+
csi=0, dsi=0, headers={"GPIO": header}, board="",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EspBridgePin(Pin):
|
|
53
|
+
def __init__(self, factory: "EspBridgeFactory", info: PinInfo):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self._factory = factory
|
|
56
|
+
self._info = info
|
|
57
|
+
self._gpio = factory._esp.gpio
|
|
58
|
+
self._pwm = factory._esp.pwm
|
|
59
|
+
self._n = info.number
|
|
60
|
+
|
|
61
|
+
self._function = "input"
|
|
62
|
+
self._pull = "floating"
|
|
63
|
+
self._frequency: float | None = None
|
|
64
|
+
self._duty = 0.0
|
|
65
|
+
self._bounce: float | None = None
|
|
66
|
+
self._edges = "both"
|
|
67
|
+
self._when_changed = None
|
|
68
|
+
self._watching = False
|
|
69
|
+
self._gpio.mode(self._n, "input")
|
|
70
|
+
|
|
71
|
+
def __repr__(self):
|
|
72
|
+
return f"<EspBridgePin GPIO{self._n}>"
|
|
73
|
+
|
|
74
|
+
# ---- identity -------------------------------------------------------------
|
|
75
|
+
def _get_info(self):
|
|
76
|
+
return self._info
|
|
77
|
+
|
|
78
|
+
# ---- function -------------------------------------------------------------
|
|
79
|
+
def _get_function(self):
|
|
80
|
+
return self._function
|
|
81
|
+
|
|
82
|
+
def _set_function(self, value):
|
|
83
|
+
if value not in ("input", "output"):
|
|
84
|
+
raise PinInvalidFunction(f"invalid function {value!r} for {self!r}")
|
|
85
|
+
self._function = value
|
|
86
|
+
if value == "input":
|
|
87
|
+
self._gpio.mode(self._n, _PULL_MODES[self._pull])
|
|
88
|
+
else:
|
|
89
|
+
self._gpio.mode(self._n, "output")
|
|
90
|
+
|
|
91
|
+
def output_with_state(self, state):
|
|
92
|
+
self._function = "output"
|
|
93
|
+
self._gpio.mode(self._n, "output")
|
|
94
|
+
self._set_state(state)
|
|
95
|
+
|
|
96
|
+
def input_with_pull(self, pull):
|
|
97
|
+
self._pull = pull
|
|
98
|
+
self._set_function("input")
|
|
99
|
+
|
|
100
|
+
# ---- state ------------------------------------------------------------------
|
|
101
|
+
def _get_state(self):
|
|
102
|
+
if self._frequency is not None:
|
|
103
|
+
return self._duty
|
|
104
|
+
return self._gpio.read(self._n)
|
|
105
|
+
|
|
106
|
+
def _set_state(self, value):
|
|
107
|
+
if self._frequency is not None:
|
|
108
|
+
self._duty = max(0.0, min(1.0, float(value)))
|
|
109
|
+
self._pwm.write(self._n, round(self._duty * _PWM_MAX))
|
|
110
|
+
elif self._function == "input":
|
|
111
|
+
raise PinSetInput(f"cannot set state of input {self!r}")
|
|
112
|
+
else:
|
|
113
|
+
self._gpio.write(self._n, bool(value))
|
|
114
|
+
|
|
115
|
+
# ---- pull ---------------------------------------------------------------------
|
|
116
|
+
def _get_pull(self):
|
|
117
|
+
return self._pull
|
|
118
|
+
|
|
119
|
+
def _set_pull(self, value):
|
|
120
|
+
if value not in _PULL_MODES:
|
|
121
|
+
raise PinInvalidPull(f"invalid pull {value!r} for {self!r}")
|
|
122
|
+
self._pull = value
|
|
123
|
+
if self._function == "input":
|
|
124
|
+
self._gpio.mode(self._n, _PULL_MODES[value])
|
|
125
|
+
|
|
126
|
+
# ---- PWM -----------------------------------------------------------------------
|
|
127
|
+
def _get_frequency(self):
|
|
128
|
+
return self._frequency
|
|
129
|
+
|
|
130
|
+
def _set_frequency(self, value):
|
|
131
|
+
if value is None:
|
|
132
|
+
if self._frequency is not None:
|
|
133
|
+
self._pwm.detach(self._n)
|
|
134
|
+
self._frequency = None
|
|
135
|
+
if self._function == "output":
|
|
136
|
+
self._gpio.mode(self._n, "output")
|
|
137
|
+
self._gpio.write(self._n, False)
|
|
138
|
+
else:
|
|
139
|
+
self._pwm.attach(self._n, int(value), _PWM_RES_BITS)
|
|
140
|
+
self._frequency = float(value)
|
|
141
|
+
self._pwm.write(self._n, round(self._duty * _PWM_MAX))
|
|
142
|
+
|
|
143
|
+
# ---- edge detection ----------------------------------------------------------------
|
|
144
|
+
def _get_bounce(self):
|
|
145
|
+
return self._bounce
|
|
146
|
+
|
|
147
|
+
def _set_bounce(self, value):
|
|
148
|
+
self._bounce = value
|
|
149
|
+
self._rearm()
|
|
150
|
+
|
|
151
|
+
def _get_edges(self):
|
|
152
|
+
return self._edges
|
|
153
|
+
|
|
154
|
+
def _set_edges(self, value):
|
|
155
|
+
if value not in ("both", "rising", "falling", "none"):
|
|
156
|
+
raise PinInvalidFunction(f"invalid edges {value!r}")
|
|
157
|
+
self._edges = value
|
|
158
|
+
self._rearm()
|
|
159
|
+
|
|
160
|
+
def _get_when_changed(self):
|
|
161
|
+
return self._when_changed
|
|
162
|
+
|
|
163
|
+
def _set_when_changed(self, value):
|
|
164
|
+
self._when_changed = value
|
|
165
|
+
self._rearm()
|
|
166
|
+
|
|
167
|
+
def _rearm(self):
|
|
168
|
+
if self._watching:
|
|
169
|
+
self._gpio.unwatch(self._n)
|
|
170
|
+
self._watching = False
|
|
171
|
+
if self._when_changed is not None and self._edges != "none":
|
|
172
|
+
debounce_ms = int((self._bounce or 0) * 1000)
|
|
173
|
+
self._gpio.watch(self._n, _EDGE_MAP[self._edges],
|
|
174
|
+
debounce_ms=debounce_ms, callback=self._on_edge)
|
|
175
|
+
self._watching = True
|
|
176
|
+
|
|
177
|
+
def _on_edge(self, event):
|
|
178
|
+
cb = self._when_changed
|
|
179
|
+
if cb is not None:
|
|
180
|
+
cb(self._factory.ticks(), event.level)
|
|
181
|
+
|
|
182
|
+
def close(self):
|
|
183
|
+
try:
|
|
184
|
+
if self._watching:
|
|
185
|
+
self._gpio.unwatch(self._n)
|
|
186
|
+
self._watching = False
|
|
187
|
+
if self._frequency is not None:
|
|
188
|
+
self._pwm.detach(self._n)
|
|
189
|
+
self._frequency = None
|
|
190
|
+
self._gpio.mode(self._n, "input")
|
|
191
|
+
except Exception:
|
|
192
|
+
pass # bridge may already be closed (e.g. at interpreter exit)
|
|
193
|
+
self._function = "input"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class EspBridgeFactory(Factory):
|
|
197
|
+
"""gpiozero pin factory backed by an espbridge Bridge."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, esp):
|
|
200
|
+
super().__init__()
|
|
201
|
+
self._esp = esp
|
|
202
|
+
self.pins: dict = {}
|
|
203
|
+
gpio_count = esp.info.gpio_count if esp.info is not None else 40
|
|
204
|
+
chip = esp.info.chip.name if esp.info is not None else "ESP32"
|
|
205
|
+
self._board_info = _board_info(gpio_count, chip)
|
|
206
|
+
|
|
207
|
+
def _get_board_info(self):
|
|
208
|
+
return self._board_info
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def board_info(self):
|
|
212
|
+
return self._board_info
|
|
213
|
+
|
|
214
|
+
def pin(self, name):
|
|
215
|
+
for _header, info in self.board_info.find_pin(name):
|
|
216
|
+
pin = self.pins.get(info)
|
|
217
|
+
if pin is None:
|
|
218
|
+
pin = EspBridgePin(self, info)
|
|
219
|
+
self.pins[info] = pin
|
|
220
|
+
return pin
|
|
221
|
+
raise PinInvalidPin(f"{name} is not a valid pin name")
|
|
222
|
+
|
|
223
|
+
def ticks(self):
|
|
224
|
+
return time.monotonic()
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def ticks_diff(later, earlier):
|
|
228
|
+
return later - earlier
|
|
229
|
+
|
|
230
|
+
def close(self):
|
|
231
|
+
for pin in list(self.pins.values()):
|
|
232
|
+
pin.close()
|
|
233
|
+
self.pins.clear()
|
espbridge/compat/luma.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Adapter that lets the luma.oled / luma.lcd display libraries drive their
|
|
2
|
+
displays through the bridge's I2C — so you get the battle-tested drivers
|
|
3
|
+
(SSD1306, SH1106, SSD1309, ...) and PIL drawing, while the ESP32 just relays
|
|
4
|
+
I2C traffic.
|
|
5
|
+
|
|
6
|
+
pip install luma.oled
|
|
7
|
+
|
|
8
|
+
from luma.oled.device import ssd1306 # or sh1106, ...
|
|
9
|
+
from espbridge.compat.luma import LumaI2C
|
|
10
|
+
|
|
11
|
+
esp.i2c.init(sda=21, scl=22, freq=400_000)
|
|
12
|
+
device = ssd1306(LumaI2C(esp.i2c, addr=0x3C))
|
|
13
|
+
|
|
14
|
+
This module deliberately does not import luma — it only implements the
|
|
15
|
+
``command``/``data``/``cleanup`` serial-interface protocol luma expects.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
_CMD_MODE = 0x00 # control byte: command stream follows
|
|
20
|
+
_DATA_MODE = 0x40 # control byte: display data follows
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LumaI2C:
|
|
24
|
+
"""luma serial interface backed by an espbridge I2c bus."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, i2c, addr: int = 0x3C, bus: int = 0):
|
|
27
|
+
self._i2c = i2c
|
|
28
|
+
self._addr = addr
|
|
29
|
+
self._bus = bus
|
|
30
|
+
# largest data write minus the control byte (128 on old firmware,
|
|
31
|
+
# whose Wire TX buffer silently truncates longer transmissions)
|
|
32
|
+
self._chunk = getattr(i2c, "max_write", 2046) - 1
|
|
33
|
+
|
|
34
|
+
def command(self, *cmd: int) -> None:
|
|
35
|
+
self._i2c.write(self._addr, bytes([_CMD_MODE, *cmd]), self._bus)
|
|
36
|
+
|
|
37
|
+
def data(self, data) -> None:
|
|
38
|
+
data = bytes(bytearray(data)) # luma may pass a list of ints
|
|
39
|
+
for off in range(0, len(data), self._chunk):
|
|
40
|
+
self._i2c.write(self._addr,
|
|
41
|
+
bytes([_DATA_MODE]) + data[off : off + self._chunk],
|
|
42
|
+
self._bus)
|
|
43
|
+
|
|
44
|
+
def cleanup(self) -> None:
|
|
45
|
+
pass # nothing to release; the Bridge owns the link
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LumaSPI:
|
|
49
|
+
"""luma serial interface for SPI displays (SSD1306-SPI, ST77xx, ILI9341, ...).
|
|
50
|
+
|
|
51
|
+
from luma.lcd.device import st7735
|
|
52
|
+
device = st7735(LumaSPI(esp, dc=4, rst=16, cs=5), width=160, height=128)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, esp, *, dc: int, rst: int | None = None, cs: int | None = None,
|
|
56
|
+
sck: int = 18, mosi: int = 23, miso: int = 19,
|
|
57
|
+
freq: int = 8_000_000, host: int = 0, init_bus: bool = True):
|
|
58
|
+
self._spi = esp.spi
|
|
59
|
+
self._gpio = esp.gpio
|
|
60
|
+
self._dc = dc
|
|
61
|
+
self._cs = cs
|
|
62
|
+
self._host = host
|
|
63
|
+
if init_bus:
|
|
64
|
+
self._spi.init(sck=sck, miso=miso, mosi=mosi, freq=freq, host=host)
|
|
65
|
+
self._gpio.mode(dc, "output")
|
|
66
|
+
if rst is not None: # power-on reset pulse, as luma's own spi interface does
|
|
67
|
+
import time
|
|
68
|
+
self._gpio.mode(rst, "output")
|
|
69
|
+
self._gpio.write(rst, 0)
|
|
70
|
+
time.sleep(0.01)
|
|
71
|
+
self._gpio.write(rst, 1)
|
|
72
|
+
time.sleep(0.01)
|
|
73
|
+
|
|
74
|
+
def _send(self, data: bytes) -> None:
|
|
75
|
+
# CS is asserted per chunk; display controllers latch per byte, so
|
|
76
|
+
# chunk boundaries are harmless.
|
|
77
|
+
max_chunk = 2046 # MAX_PAYLOAD minus the transfer header
|
|
78
|
+
for off in range(0, len(data), max_chunk):
|
|
79
|
+
self._spi.transfer(data[off : off + max_chunk], cs=self._cs, host=self._host)
|
|
80
|
+
|
|
81
|
+
def command(self, *cmd: int) -> None:
|
|
82
|
+
self._gpio.write(self._dc, 0)
|
|
83
|
+
self._send(bytes(cmd))
|
|
84
|
+
|
|
85
|
+
def data(self, data) -> None:
|
|
86
|
+
self._gpio.write(self._dc, 1)
|
|
87
|
+
self._send(bytes(bytearray(data)))
|
|
88
|
+
|
|
89
|
+
def cleanup(self) -> None:
|
|
90
|
+
pass
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Drop-in-ish RPi.GPIO shim backed by the ESP32 bridge.
|
|
2
|
+
|
|
3
|
+
from espbridge.compat import rpi_gpio as GPIO
|
|
4
|
+
|
|
5
|
+
GPIO.setmode(GPIO.BCM) # pin numbers are ESP32 GPIO numbers
|
|
6
|
+
GPIO.setup(2, GPIO.OUT)
|
|
7
|
+
GPIO.output(2, GPIO.HIGH)
|
|
8
|
+
GPIO.add_event_detect(4, GPIO.FALLING, callback=lambda ch: print(ch))
|
|
9
|
+
|
|
10
|
+
Uses a process-wide Bridge (auto-detected port) unless you call attach(bridge).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ..bridge import Bridge
|
|
15
|
+
|
|
16
|
+
BCM = "BCM"
|
|
17
|
+
BOARD = "BOARD" # accepted but identical: numbering is always ESP32 GPIO
|
|
18
|
+
OUT = "out"
|
|
19
|
+
IN = "in"
|
|
20
|
+
HIGH = 1
|
|
21
|
+
LOW = 0
|
|
22
|
+
PUD_UP = "up"
|
|
23
|
+
PUD_DOWN = "down"
|
|
24
|
+
PUD_OFF = "off"
|
|
25
|
+
RISING = "rising"
|
|
26
|
+
FALLING = "falling"
|
|
27
|
+
BOTH = "change"
|
|
28
|
+
|
|
29
|
+
_bridge: Bridge | None = None
|
|
30
|
+
_owned = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def attach(bridge: Bridge) -> None:
|
|
34
|
+
"""Use an existing Bridge instead of auto-connecting."""
|
|
35
|
+
global _bridge, _owned
|
|
36
|
+
_bridge = bridge
|
|
37
|
+
_owned = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _b() -> Bridge:
|
|
41
|
+
global _bridge, _owned
|
|
42
|
+
if _bridge is None:
|
|
43
|
+
_bridge = Bridge()
|
|
44
|
+
_owned = True
|
|
45
|
+
return _bridge
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def setmode(mode) -> None: # numbering is always ESP32 GPIO numbers
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def setwarnings(flag: bool) -> None:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def setup(channel, direction, pull_up_down=PUD_OFF, initial=None) -> None:
|
|
57
|
+
channels = channel if isinstance(channel, (list, tuple)) else [channel]
|
|
58
|
+
for ch in channels:
|
|
59
|
+
if direction == OUT:
|
|
60
|
+
_b().gpio.mode(ch, "output")
|
|
61
|
+
if initial is not None:
|
|
62
|
+
_b().gpio.write(ch, initial)
|
|
63
|
+
else:
|
|
64
|
+
mode = {PUD_UP: "input_pullup", PUD_DOWN: "input_pulldown"}.get(
|
|
65
|
+
pull_up_down, "input")
|
|
66
|
+
_b().gpio.mode(ch, mode)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def output(channel, value) -> None:
|
|
70
|
+
channels = channel if isinstance(channel, (list, tuple)) else [channel]
|
|
71
|
+
values = value if isinstance(value, (list, tuple)) else [value] * len(channels)
|
|
72
|
+
if len(channels) > 1:
|
|
73
|
+
_b().gpio.write_many(dict(zip(channels, values)))
|
|
74
|
+
else:
|
|
75
|
+
_b().gpio.write(channels[0], values[0])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def input(channel) -> int: # noqa: A001 — mirrors RPi.GPIO's name
|
|
79
|
+
return _b().gpio.read(channel)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def add_event_detect(channel, edge, callback=None, bouncetime=0) -> None:
|
|
83
|
+
cb = (lambda ev, _cb=callback: _cb(ev.pin)) if callback else None
|
|
84
|
+
_b().gpio.watch(channel, edge, debounce_ms=bouncetime, callback=cb)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def remove_event_detect(channel) -> None:
|
|
88
|
+
_b().gpio.unwatch(channel)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PWM:
|
|
92
|
+
"""RPi.GPIO-style PWM object."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, channel: int, frequency: float):
|
|
95
|
+
self.channel = channel
|
|
96
|
+
self.frequency = int(frequency)
|
|
97
|
+
self._duty = 0.0
|
|
98
|
+
|
|
99
|
+
def start(self, duty_cycle: float) -> None:
|
|
100
|
+
_b().pwm.attach(self.channel, self.frequency, 12)
|
|
101
|
+
self.ChangeDutyCycle(duty_cycle)
|
|
102
|
+
|
|
103
|
+
def ChangeDutyCycle(self, duty_cycle: float) -> None:
|
|
104
|
+
self._duty = duty_cycle
|
|
105
|
+
_b().pwm.duty_pct(self.channel, duty_cycle)
|
|
106
|
+
|
|
107
|
+
def ChangeFrequency(self, frequency: float) -> None:
|
|
108
|
+
self.frequency = int(frequency)
|
|
109
|
+
_b().pwm.attach(self.channel, self.frequency, 12)
|
|
110
|
+
_b().pwm.duty_pct(self.channel, self._duty)
|
|
111
|
+
|
|
112
|
+
def stop(self) -> None:
|
|
113
|
+
_b().pwm.detach(self.channel)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cleanup(channel=None) -> None:
|
|
117
|
+
global _bridge, _owned
|
|
118
|
+
if _bridge is not None and _owned:
|
|
119
|
+
_bridge.close()
|
|
120
|
+
_bridge = None
|
|
121
|
+
_owned = False
|