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.
@@ -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()
@@ -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