btbricks 0.2.1__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.
- btbricks/__init__.py +52 -0
- btbricks/bt.py +1197 -0
- btbricks/bthub.py +218 -0
- btbricks/ctrl_plus.py +8 -0
- btbricks-0.2.1.dist-info/METADATA +232 -0
- btbricks-0.2.1.dist-info/RECORD +13 -0
- btbricks-0.2.1.dist-info/WHEEL +5 -0
- btbricks-0.2.1.dist-info/licenses/LICENSE +21 -0
- btbricks-0.2.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_bthub.py +102 -0
- tests/test_constants.py +77 -0
- tests/test_imports.py +94 -0
btbricks/bt.py
ADDED
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
### Bluetooth Low Energy (BLE) connection and communication tools
|
|
2
|
+
### for MINDSTORMS robots and LMS-ESP32
|
|
3
|
+
### Supports Midi, Nordic UART, BLE Remote app, and LEGO Protocol: LPF2/LPUP/CTRL-PLUS.
|
|
4
|
+
|
|
5
|
+
### (c) 2023 Anton's Mindstorms & Ste7an
|
|
6
|
+
|
|
7
|
+
# Warning! This does NOT work on SPIKE Prime firmware.
|
|
8
|
+
# Flash your SPIKE Prime with MINDSTORMS firmware if you want to use bluetooth.
|
|
9
|
+
# See https://docs.antonsmindstorms.com for tutorial and explanation.
|
|
10
|
+
|
|
11
|
+
## TODO
|
|
12
|
+
## Fix occasional packet loss on very large notify payloads:
|
|
13
|
+
# Maybe notify size or delay
|
|
14
|
+
|
|
15
|
+
## Clean up Ble_handler: Move connection code to LEGO and UART classes.
|
|
16
|
+
|
|
17
|
+
## Fix mem allocation and scheduling in IRQ's:
|
|
18
|
+
# Note: If schedule() is called from a preempting IRQ,
|
|
19
|
+
# when memory allocation is not allowed and the callback
|
|
20
|
+
# to be passed to schedule() is a bound method, passing this
|
|
21
|
+
# directly will fail. This is because creating a reference to a
|
|
22
|
+
# bound method causes memory allocation. A solution is to
|
|
23
|
+
# create a reference to the method in the class constructor
|
|
24
|
+
# and to pass that reference to schedule().
|
|
25
|
+
# This is discussed in detail here reference documentation under
|
|
26
|
+
# “Creation of Python objects”.
|
|
27
|
+
|
|
28
|
+
import struct
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from utime import sleep_ms, ticks_diff, ticks_ms
|
|
32
|
+
from micropython import const, schedule, alloc_emergency_exception_buf
|
|
33
|
+
import ubluetooth
|
|
34
|
+
|
|
35
|
+
alloc_emergency_exception_buf(100)
|
|
36
|
+
if not "FLAG_INDICATE" in dir(ubluetooth):
|
|
37
|
+
# We're on SPIKE Prime, old version of ubluetooth
|
|
38
|
+
print("WARNING SPIKE Prime not supported for Ble. Use MINDSTORMS Firmware.")
|
|
39
|
+
raise Exception("Firmware not supported")
|
|
40
|
+
except:
|
|
41
|
+
# Polyfill for automated testing purposes
|
|
42
|
+
def const(x):
|
|
43
|
+
return x
|
|
44
|
+
|
|
45
|
+
def schedule(fn):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def alloc_emergency_exception_buf(n):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
class ubluetooth:
|
|
52
|
+
def UUID(_):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
print("Import failed. Not on micropython?")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
TARGET_MTU = const(184) # Try to negotiate this packet size for UART
|
|
59
|
+
MAX_NOTIFY = const(100) # Somehow notify with the full mtu is unstable. Memory issue?
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
L_STICK_HOR = const(0)
|
|
63
|
+
L_STICK_VER = const(1)
|
|
64
|
+
R_STICK_HOR = const(2)
|
|
65
|
+
R_STICK_VER = const(3)
|
|
66
|
+
L_TRIGGER = const(4)
|
|
67
|
+
R_TRIGGER = const(5)
|
|
68
|
+
SETTING1 = const(6)
|
|
69
|
+
SETTING2 = const(7)
|
|
70
|
+
BUTTONS = const(8)
|
|
71
|
+
|
|
72
|
+
_NOTIFY_ENABLE = const(1)
|
|
73
|
+
_INDICATE_ENABLE = const(2)
|
|
74
|
+
_FLAG_READ = 0x02
|
|
75
|
+
_FLAG_WRITE_NO_RESPONSE = 0x04
|
|
76
|
+
_FLAG_WRITE = 0x08
|
|
77
|
+
_FLAG_NOTIFY = 0x10
|
|
78
|
+
_FLAG_INDICATE = 0x20
|
|
79
|
+
|
|
80
|
+
_IRQ_CENTRAL_CONNECT = const(1)
|
|
81
|
+
_IRQ_CENTRAL_DISCONNECT = const(2)
|
|
82
|
+
_IRQ_GATTS_WRITE = const(3)
|
|
83
|
+
_IRQ_SCAN_RESULT = const(5)
|
|
84
|
+
_IRQ_SCAN_DONE = const(6)
|
|
85
|
+
_IRQ_PERIPHERAL_CONNECT = const(7)
|
|
86
|
+
_IRQ_PERIPHERAL_DISCONNECT = const(8)
|
|
87
|
+
_IRQ_GATTC_SERVICE_RESULT = const(9)
|
|
88
|
+
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
|
|
89
|
+
_IRQ_GATTC_READ_RESULT = const(15)
|
|
90
|
+
_IRQ_GATTC_NOTIFY = const(18)
|
|
91
|
+
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
|
|
92
|
+
_IRQ_GATTC_SERVICE_DONE = const(10)
|
|
93
|
+
_IRQ_GATTC_WRITE_DONE = const(17)
|
|
94
|
+
_IRQ_GATTC_READ_DONE = const(16)
|
|
95
|
+
_IRQ_MTU_EXCHANGED = const(21)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Helpers for generating BLE advertising payloads.
|
|
99
|
+
# Advertising payloads are repeated packets of the following form:
|
|
100
|
+
# 1 byte data length (N + 1)
|
|
101
|
+
# 1 byte type (see constants below)
|
|
102
|
+
# N bytes type-specific data
|
|
103
|
+
|
|
104
|
+
_ADV_TYPE_FLAGS = const(0x01)
|
|
105
|
+
_ADV_TYPE_NAME = const(0x09)
|
|
106
|
+
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
|
|
107
|
+
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
|
|
108
|
+
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
|
|
109
|
+
# _ADV_TYPE_UUID16_MORE = const(0x2)
|
|
110
|
+
# _ADV_TYPE_UUID32_MORE = const(0x4)
|
|
111
|
+
# _ADV_TYPE_UUID128_MORE = const(0x6)
|
|
112
|
+
_ADV_TYPE_APPEARANCE = const(0x19)
|
|
113
|
+
|
|
114
|
+
# UART
|
|
115
|
+
_UART_UUID = ubluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
116
|
+
_UART_TX_UUID = ubluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
117
|
+
_UART_RX_UUID = ubluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
118
|
+
_UART_TX = (
|
|
119
|
+
_UART_TX_UUID,
|
|
120
|
+
_FLAG_NOTIFY | _FLAG_READ, # write is not really needed here.
|
|
121
|
+
)
|
|
122
|
+
_UART_RX = (
|
|
123
|
+
_UART_RX_UUID,
|
|
124
|
+
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE, # read should not be needed here.
|
|
125
|
+
)
|
|
126
|
+
_UART_SERVICE = (
|
|
127
|
+
_UART_UUID,
|
|
128
|
+
(_UART_TX, _UART_RX),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# LEGO
|
|
132
|
+
_LEGO_SERVICE_UUID = ubluetooth.UUID("00001623-1212-EFDE-1623-785FEABCD123")
|
|
133
|
+
_LEGO_SERVICE_CHAR = ubluetooth.UUID("00001624-1212-EFDE-1623-785FEABCD123")
|
|
134
|
+
|
|
135
|
+
# MIDI
|
|
136
|
+
MIDI_SERVICE_UUID = ubluetooth.UUID("03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
|
|
137
|
+
MIDI_CHAR_UUID = ubluetooth.UUID("7772E5DB-3868-4112-A1A9-F2669D106BF3")
|
|
138
|
+
MIDI_CHAR = (
|
|
139
|
+
MIDI_CHAR_UUID,
|
|
140
|
+
_FLAG_NOTIFY | _FLAG_READ | _FLAG_WRITE_NO_RESPONSE,
|
|
141
|
+
)
|
|
142
|
+
MIDI_SERVICE = (
|
|
143
|
+
MIDI_SERVICE_UUID,
|
|
144
|
+
(MIDI_CHAR,),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# MIDI Note conversion and scales
|
|
148
|
+
# From C3 - A and B are above G
|
|
149
|
+
# Semitones A B C D E F G
|
|
150
|
+
NOTE_OFFSET = [21, 23, 12, 14, 16, 17, 19]
|
|
151
|
+
|
|
152
|
+
#: Chord styles for the play_chord method of the MidiController class.
|
|
153
|
+
CHORD_STYLES = {
|
|
154
|
+
# Note (half tone) offsets from base note
|
|
155
|
+
"M": (0, 4, 7, 12),
|
|
156
|
+
"m": (0, 3, 7, 12),
|
|
157
|
+
"7": (0, 4, 7, 10),
|
|
158
|
+
"m7": (0, 3, 7, 10),
|
|
159
|
+
"M7": (0, 4, 7, 11),
|
|
160
|
+
"sus4": (0, 5, 7, 12),
|
|
161
|
+
"sus2": (0, 2, 7, 12),
|
|
162
|
+
"dim7": (0, 3, 6, 10),
|
|
163
|
+
"P": (0, 7, 12, 19), # Power chord
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def note_parser(note):
|
|
168
|
+
# Note parser from "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git"
|
|
169
|
+
"""If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g.
|
|
170
|
+
"C4" will return 60, "C#4" will return 61. If note is not a string it will simply be returned.
|
|
171
|
+
|
|
172
|
+
:param note: Either 0-127 int or a str representing the note, e.g. "C#4"
|
|
173
|
+
"""
|
|
174
|
+
midi_note = note
|
|
175
|
+
if isinstance(note, str):
|
|
176
|
+
if len(note) < 2:
|
|
177
|
+
raise ValueError("Bad note format")
|
|
178
|
+
noteidx = ord(note[0].upper()) - 65 # 65 os ord('A')
|
|
179
|
+
if not 0 <= noteidx <= 6:
|
|
180
|
+
raise ValueError("Bad note")
|
|
181
|
+
sharpen = 0
|
|
182
|
+
if note[1] == "#":
|
|
183
|
+
sharpen = 1
|
|
184
|
+
elif note[1] == "b":
|
|
185
|
+
sharpen = -1
|
|
186
|
+
midi_note = int(note[1 + abs(sharpen) :]) * 12 + NOTE_OFFSET[noteidx] + sharpen
|
|
187
|
+
return midi_note
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
|
|
191
|
+
"""
|
|
192
|
+
Generate advertising payload.
|
|
193
|
+
|
|
194
|
+
:param limited_disc: Limited discoverable mode. Determines whether the device can be discoverable for a limited period. Default value is ``False``.
|
|
195
|
+
:type limited_disc: bool
|
|
196
|
+
:param br_edr: BR/EDR support (Basic Rate/Enhanced Data Rate). Determines whether the device supports classic Bluetooth. Default value is ``False``.
|
|
197
|
+
:type br_edr: bool
|
|
198
|
+
:param name: Name of the device to be advertised. Default value is ``None``.
|
|
199
|
+
:type name: str
|
|
200
|
+
:param services: List of services offered by the device, typically identified by UUIDs. Default value is ``None``.
|
|
201
|
+
:type services: list
|
|
202
|
+
:param appearance: Appearance category code, describing the visual appearance of the device (e.g., phone, keyboard). Default value is ``False``.
|
|
203
|
+
:type appearance: int
|
|
204
|
+
:return: An array of bytes with the specified payload.
|
|
205
|
+
:rtype: list[bytes]
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
payload = bytearray()
|
|
209
|
+
|
|
210
|
+
def _append(adv_type, value):
|
|
211
|
+
"""Auxiliary function to populate the payload array"""
|
|
212
|
+
nonlocal payload
|
|
213
|
+
payload += struct.pack("BB", len(value) + 1, adv_type) + value
|
|
214
|
+
|
|
215
|
+
_append(
|
|
216
|
+
_ADV_TYPE_FLAGS,
|
|
217
|
+
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if name:
|
|
221
|
+
_append(_ADV_TYPE_NAME, name)
|
|
222
|
+
|
|
223
|
+
if services:
|
|
224
|
+
for uuid in services:
|
|
225
|
+
b = bytes(uuid)
|
|
226
|
+
if len(b) == 2:
|
|
227
|
+
_append(_ADV_TYPE_UUID16_COMPLETE, b)
|
|
228
|
+
elif len(b) == 4:
|
|
229
|
+
_append(_ADV_TYPE_UUID32_COMPLETE, b)
|
|
230
|
+
elif len(b) == 16:
|
|
231
|
+
_append(_ADV_TYPE_UUID128_COMPLETE, b)
|
|
232
|
+
|
|
233
|
+
# See org.bluetooth.characteristic.gap.appearance.xml
|
|
234
|
+
if appearance:
|
|
235
|
+
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
|
|
236
|
+
|
|
237
|
+
return payload
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _decode_field(payload, adv_type):
|
|
241
|
+
"""
|
|
242
|
+
Decode field from BLE Advertising payload.
|
|
243
|
+
|
|
244
|
+
:param payload: Payload of the message.
|
|
245
|
+
:type payload: bytearray
|
|
246
|
+
:param adv_type: Type of the field to decode. See constants starting with ``_ADV_TYPE_`` for possible values.
|
|
247
|
+
:type adv_type: int
|
|
248
|
+
:return: An list with the decoded field values.
|
|
249
|
+
"""
|
|
250
|
+
i = 0
|
|
251
|
+
result = []
|
|
252
|
+
while i + 1 < len(payload):
|
|
253
|
+
if payload[i + 1] == adv_type:
|
|
254
|
+
result.append(payload[i + 2 : i + payload[i] + 1])
|
|
255
|
+
i += 1 + payload[i]
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _decode_name(payload):
|
|
260
|
+
"""
|
|
261
|
+
Decode name from BLE Advertising payload.
|
|
262
|
+
|
|
263
|
+
:param payload: Payload of the message.
|
|
264
|
+
:type payload: bytearray
|
|
265
|
+
"""
|
|
266
|
+
n = _decode_field(payload, _ADV_TYPE_NAME)
|
|
267
|
+
return str(n[0], "utf-8") if n else ""
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _decode_services(payload):
|
|
271
|
+
"""
|
|
272
|
+
Decode service UUIDs from BLE Advertising payload.
|
|
273
|
+
|
|
274
|
+
:param payload: Payload of the message.
|
|
275
|
+
:type payload: bytearray
|
|
276
|
+
|
|
277
|
+
:return: A list of UUID objects representing the services.
|
|
278
|
+
"""
|
|
279
|
+
services = []
|
|
280
|
+
for u in _decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
|
|
281
|
+
services.append(ubluetooth.UUID(struct.unpack("<h", u)[0]))
|
|
282
|
+
for u in _decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
|
|
283
|
+
services.append(ubluetooth.UUID(struct.unpack("<d", u)[0]))
|
|
284
|
+
for u in _decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
|
|
285
|
+
services.append(ubluetooth.UUID(u))
|
|
286
|
+
return services
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class BLEHandler:
|
|
290
|
+
"""
|
|
291
|
+
Basic Bluetooth Low Energy class that can be a central or peripheral or both.
|
|
292
|
+
The central always connects to a peripheral. The Peripheral just advertises.
|
|
293
|
+
Instantiate a BLEHandler and pass it to the UARTCentral and UARTPeripheral class,
|
|
294
|
+
if you want to use both classes on the same device.
|
|
295
|
+
|
|
296
|
+
:param debug: Keep a log of events in the log property. WARNING: Debug log is kept in memory. Long transactions will lead to memory errors!
|
|
297
|
+
:type debug: bool
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, debug=False):
|
|
301
|
+
self._ble = ubluetooth.BLE()
|
|
302
|
+
self._ble.config(rxbuf=TARGET_MTU)
|
|
303
|
+
self._ble.active(True)
|
|
304
|
+
try:
|
|
305
|
+
self._ble.gap_disconnect(1025) # Disconnect in case of previous crash
|
|
306
|
+
except:
|
|
307
|
+
pass
|
|
308
|
+
self._ble.irq(self._irq)
|
|
309
|
+
self.debug = debug
|
|
310
|
+
self.log_size = 200
|
|
311
|
+
self._reset()
|
|
312
|
+
|
|
313
|
+
def _reset(self):
|
|
314
|
+
self._connected_central = -1 # Only one central can connect. -1 is not connected.
|
|
315
|
+
self._scan_result_callback = None
|
|
316
|
+
self._scan_done_callback = None
|
|
317
|
+
self._write_done_callbacks = {}
|
|
318
|
+
self._disconn_callbacks = {}
|
|
319
|
+
self._central_conn_callback = None # Used when centrals connect
|
|
320
|
+
self._central_disconn_callback = None # Used when centrals disconnect
|
|
321
|
+
self._char_result_callback = None
|
|
322
|
+
self._write_callbacks = {}
|
|
323
|
+
self._notify_callbacks = {}
|
|
324
|
+
self._search_name = None
|
|
325
|
+
self.connecting_uart = False
|
|
326
|
+
self.connecting_lego = False
|
|
327
|
+
self._read_data = {}
|
|
328
|
+
self._start_handle = None
|
|
329
|
+
self._end_handle = None
|
|
330
|
+
self.mtu = 20
|
|
331
|
+
if self.debug:
|
|
332
|
+
# Reserve log_size bytes and track the index of the last written byte.
|
|
333
|
+
self.log_data = bytearray(self.log_size)
|
|
334
|
+
self.log_idx = 0
|
|
335
|
+
else:
|
|
336
|
+
self.log_data = b""
|
|
337
|
+
|
|
338
|
+
def info(self, *messages):
|
|
339
|
+
"""
|
|
340
|
+
Saves messages to the log if debug is enabled.
|
|
341
|
+
|
|
342
|
+
:param messages: Messages to save to the log
|
|
343
|
+
:type messages: str
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
:Example:
|
|
347
|
+
|
|
348
|
+
.. code-block:: python
|
|
349
|
+
|
|
350
|
+
self.info(var1, var2, var3)
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
if self.debug:
|
|
354
|
+
for m in messages:
|
|
355
|
+
d = bytes(str(m), "utf8")
|
|
356
|
+
l = len(d)
|
|
357
|
+
if self.log_idx + l > self.log_size:
|
|
358
|
+
self.log_idx = 0
|
|
359
|
+
self.log_data[self.log_idx : self.log_idx + l] = d
|
|
360
|
+
self.log_idx += l
|
|
361
|
+
if self.log_idx < self.log_size:
|
|
362
|
+
self.log_data[self.log_idx] = 10 # 10 is ascii for \n newline
|
|
363
|
+
self.log_idx += 1
|
|
364
|
+
else:
|
|
365
|
+
self.log_idx = 0
|
|
366
|
+
|
|
367
|
+
def print_log(self):
|
|
368
|
+
"""Prints the log to the console and clears it."""
|
|
369
|
+
for l in self.log_data[self.log_idx :].decode("utf8").split("\n"):
|
|
370
|
+
print(l)
|
|
371
|
+
for l in self.log_data[: self.log_idx].decode("utf8").split("\n"):
|
|
372
|
+
print(l)
|
|
373
|
+
self.log_data = bytearray(self.log_size)
|
|
374
|
+
self.log_idx = 0
|
|
375
|
+
|
|
376
|
+
def _irq(self, event, data):
|
|
377
|
+
if event == _IRQ_SCAN_RESULT:
|
|
378
|
+
addr_type, addr, adv_type, rssi, adv_data = data
|
|
379
|
+
name = _decode_name(adv_data) or "?"
|
|
380
|
+
services = _decode_services(adv_data)
|
|
381
|
+
# self.info(self._search_payload == adv_data) # This works TODO: Implement properly
|
|
382
|
+
self.info("Found: ", name, " with services: ", services)
|
|
383
|
+
if self.connecting_uart:
|
|
384
|
+
if name == self._search_name and _UART_UUID in services:
|
|
385
|
+
# Found a potential device, remember it
|
|
386
|
+
self._addr_type = addr_type
|
|
387
|
+
self._addr = bytes(
|
|
388
|
+
addr
|
|
389
|
+
) # Note: addr buffer is owned by caller so need to copy it.
|
|
390
|
+
# ... and stop scanning. This triggers the IRQ_SCAN_DONE and the on_scan callback.
|
|
391
|
+
self.stop_scan()
|
|
392
|
+
if self.connecting_lego:
|
|
393
|
+
if _LEGO_SERVICE_UUID in services:
|
|
394
|
+
self._addr_type = addr_type
|
|
395
|
+
self._addr = bytes(addr)
|
|
396
|
+
self._adv_type = adv_type
|
|
397
|
+
self._name = _decode_name(adv_data)
|
|
398
|
+
self._services = _decode_services(adv_data)
|
|
399
|
+
self.stop_scan()
|
|
400
|
+
if self._scan_result_callback:
|
|
401
|
+
self._scan_result_callback(addr_type, addr, name, services)
|
|
402
|
+
|
|
403
|
+
elif event == _IRQ_SCAN_DONE:
|
|
404
|
+
if self.connecting_uart:
|
|
405
|
+
if self._addr_type is not None:
|
|
406
|
+
print("Found peripheral:", self._search_name)
|
|
407
|
+
sleep_ms(500)
|
|
408
|
+
self._ble.gap_connect(self._addr_type, self._addr)
|
|
409
|
+
else:
|
|
410
|
+
self.connecting_uart = False
|
|
411
|
+
self.info("No uart peripheral '{}' found.".format(self._search_name))
|
|
412
|
+
elif self.connecting_lego:
|
|
413
|
+
if self._addr_type is not None:
|
|
414
|
+
print("Found SMART Hub:", self._name)
|
|
415
|
+
sleep_ms(500)
|
|
416
|
+
self._ble.gap_connect(self._addr_type, self._addr)
|
|
417
|
+
else:
|
|
418
|
+
self.connecting_lego = False
|
|
419
|
+
self.info("LEGO Smart hub found.")
|
|
420
|
+
if self._scan_done_callback:
|
|
421
|
+
self._scan_done_callback(data)
|
|
422
|
+
|
|
423
|
+
elif event == _IRQ_PERIPHERAL_CONNECT:
|
|
424
|
+
# Connect to peripheral successful.
|
|
425
|
+
conn_handle, addr_type, addr = data
|
|
426
|
+
if self.connecting_uart or self.connecting_lego:
|
|
427
|
+
self._conn_handle = conn_handle
|
|
428
|
+
self._ble.gattc_discover_services(conn_handle)
|
|
429
|
+
|
|
430
|
+
elif event == _IRQ_PERIPHERAL_DISCONNECT:
|
|
431
|
+
# Disconnect (either initiated by us or the remote end).
|
|
432
|
+
conn_handle, _, _ = data
|
|
433
|
+
if conn_handle in self._disconn_callbacks:
|
|
434
|
+
if self._disconn_callbacks[conn_handle]:
|
|
435
|
+
self._disconn_callbacks[conn_handle]()
|
|
436
|
+
# TODO Also delete any notify callbacks
|
|
437
|
+
|
|
438
|
+
elif event == _IRQ_GATTC_SERVICE_RESULT:
|
|
439
|
+
# Connected device returned a service.
|
|
440
|
+
conn_handle, start_handle, end_handle, uuid = data
|
|
441
|
+
if uuid == _UART_UUID or uuid == _LEGO_SERVICE_UUID:
|
|
442
|
+
# Save handles until SERVICE_DONE
|
|
443
|
+
self._start_handle = start_handle
|
|
444
|
+
self._end_handle = end_handle
|
|
445
|
+
|
|
446
|
+
elif event == _IRQ_GATTC_SERVICE_DONE:
|
|
447
|
+
# Service query complete.
|
|
448
|
+
if self._start_handle and self._end_handle:
|
|
449
|
+
self._ble.gattc_discover_characteristics(
|
|
450
|
+
self._conn_handle, self._start_handle, self._end_handle
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
self.info("Failed to find requested gatt service.")
|
|
454
|
+
|
|
455
|
+
elif event == _IRQ_MTU_EXCHANGED:
|
|
456
|
+
# A remote central negotiated a new mtu
|
|
457
|
+
# Store it to control large transfers
|
|
458
|
+
# TODO: find out if mtu is conn_handle dependent...
|
|
459
|
+
# Let's assume it isn't.
|
|
460
|
+
conn_handle, mtu = data
|
|
461
|
+
self.mtu = mtu
|
|
462
|
+
self.info("Mtu:", mtu)
|
|
463
|
+
|
|
464
|
+
elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
|
|
465
|
+
# Connected device returned a characteristic.
|
|
466
|
+
conn_handle, def_handle, value_handle, properties, uuid = data
|
|
467
|
+
if self.connecting_uart:
|
|
468
|
+
if uuid == _UART_RX_UUID:
|
|
469
|
+
self._rx_handle = value_handle
|
|
470
|
+
elif uuid == _UART_TX_UUID:
|
|
471
|
+
self._tx_handle = value_handle
|
|
472
|
+
if all((self._conn_handle, self._rx_handle, self._tx_handle)):
|
|
473
|
+
self.connecting_uart = False
|
|
474
|
+
elif self.connecting_lego:
|
|
475
|
+
if uuid == _LEGO_SERVICE_CHAR:
|
|
476
|
+
self._lego_value_handle = value_handle
|
|
477
|
+
self.connecting_lego = False # We're done
|
|
478
|
+
if self._char_result_callback:
|
|
479
|
+
self._char_result_callback(conn_handle, value_handle, uuid)
|
|
480
|
+
|
|
481
|
+
elif event == _IRQ_GATTC_WRITE_DONE:
|
|
482
|
+
# This event fires in a central, when it is done writing
|
|
483
|
+
# to a remote peripheral.
|
|
484
|
+
# Call the callback on the value handle that has finished if there is one.
|
|
485
|
+
# The callback function should check for the value handle.
|
|
486
|
+
conn_handle, value_handle, status = data
|
|
487
|
+
self.info("Write done on", conn_handle, "in value", value_handle)
|
|
488
|
+
if conn_handle in self._write_done_callbacks:
|
|
489
|
+
self._write_done_callbacks[conn_handle](value_handle, status)
|
|
490
|
+
|
|
491
|
+
elif event == _IRQ_GATTC_NOTIFY:
|
|
492
|
+
conn_handle, value_handle, notify_data = data
|
|
493
|
+
notify_data = bytes(notify_data)
|
|
494
|
+
self.info("Notify:", conn_handle, value_handle, notify_data)
|
|
495
|
+
if conn_handle in self._notify_callbacks:
|
|
496
|
+
if self._notify_callbacks[conn_handle]:
|
|
497
|
+
schedule(self._notify_callbacks[conn_handle], notify_data)
|
|
498
|
+
|
|
499
|
+
elif event == _IRQ_GATTC_READ_RESULT:
|
|
500
|
+
# A read completed successfully and returns data
|
|
501
|
+
conn_handle, value_handle, char_data = data
|
|
502
|
+
char_data = bytes(char_data)
|
|
503
|
+
self.info("Read:", conn_handle, value_handle, char_data)
|
|
504
|
+
self._read_data[(value_handle << 12) + conn_handle] = char_data
|
|
505
|
+
|
|
506
|
+
elif event == _IRQ_CENTRAL_CONNECT:
|
|
507
|
+
conn_handle, addr_type, addr = data
|
|
508
|
+
self.info("New connection ", conn_handle)
|
|
509
|
+
self._connected_central = conn_handle
|
|
510
|
+
if self._central_conn_callback:
|
|
511
|
+
self._central_conn_callback(*data)
|
|
512
|
+
|
|
513
|
+
elif event == _IRQ_CENTRAL_DISCONNECT:
|
|
514
|
+
conn_handle, addr_type, addr = data
|
|
515
|
+
self.info("Disconnected ", conn_handle)
|
|
516
|
+
self._connected_central = -1
|
|
517
|
+
if self._central_disconn_callback:
|
|
518
|
+
self._central_disconn_callback(conn_handle)
|
|
519
|
+
|
|
520
|
+
elif event == _IRQ_GATTS_WRITE:
|
|
521
|
+
# A client/central has written to a characteristic or descriptor.
|
|
522
|
+
# Get the value and trigger the callback.
|
|
523
|
+
conn_handle, value_handle = data
|
|
524
|
+
value = self._ble.gatts_read(value_handle)
|
|
525
|
+
self.info("Client/central wrote:", conn_handle, value)
|
|
526
|
+
if value_handle in self._write_callbacks:
|
|
527
|
+
if self._write_callbacks[value_handle]:
|
|
528
|
+
self._write_callbacks[value_handle](value)
|
|
529
|
+
|
|
530
|
+
else:
|
|
531
|
+
self.info("Unhandled event: ", hex(event), "data:", data)
|
|
532
|
+
|
|
533
|
+
def advertise(self, payload, interval_us=100000):
|
|
534
|
+
"""
|
|
535
|
+
Advertise a BLE payload for a given time interval.
|
|
536
|
+
Create the payload with the _advertising_payload() function.
|
|
537
|
+
"""
|
|
538
|
+
print("Started advertising")
|
|
539
|
+
self._ble.gap_advertise(interval_us, adv_data=payload)
|
|
540
|
+
|
|
541
|
+
def on_write(self, value_handle, callback):
|
|
542
|
+
"""
|
|
543
|
+
Register a callback on the peripheral (server) side for when a client (central) writes to a characteristic or descriptor.
|
|
544
|
+
It's time to process the received data!
|
|
545
|
+
|
|
546
|
+
:param value_handle: The handle of the characteristic or descriptor to register the callback for.
|
|
547
|
+
:type value_handle: int
|
|
548
|
+
:param callback: The callback function to call when a client writes to the characteristic or descriptor.
|
|
549
|
+
:type callback: function
|
|
550
|
+
"""
|
|
551
|
+
self._write_callbacks[value_handle] = callback
|
|
552
|
+
|
|
553
|
+
def on_write_done(self, conn_handle, callback):
|
|
554
|
+
"""
|
|
555
|
+
Register a client (central) callback for when that client (central) is done writing to a characteristic or descriptor.
|
|
556
|
+
This helps you to avoid writing too much data to the peripheral too soon.
|
|
557
|
+
|
|
558
|
+
:param conn_handle: The handle of the connection to register the callback for.
|
|
559
|
+
:type conn_handle: int
|
|
560
|
+
:param callback: The callback function to call when a client writes to the characteristic or descriptor.
|
|
561
|
+
:type callback: function
|
|
562
|
+
"""
|
|
563
|
+
self._write_done_callbacks[conn_handle] = callback
|
|
564
|
+
|
|
565
|
+
def notify(self, data, val_handle, conn_handle=None):
|
|
566
|
+
"""
|
|
567
|
+
Notify connected central interested in the value handle,
|
|
568
|
+
with the given data. gatts_notify is similar to gatts_indicate, but has not
|
|
569
|
+
acknowledgement, and thus raises no _IRQ_GATSS_INDICATE_DONE.
|
|
570
|
+
|
|
571
|
+
:param data: The data to send to the central(s).
|
|
572
|
+
:type data: bytes
|
|
573
|
+
:param val_handle: The handle of the characteristic or descriptor to notify.
|
|
574
|
+
:type val_handle: int
|
|
575
|
+
:param conn_handle: The handle of the connection to notify. If None, notify all connected centrals.
|
|
576
|
+
"""
|
|
577
|
+
self._ble.gatts_write(val_handle, data)
|
|
578
|
+
# No send_update=True in gatts_write in the Inventor firmware, so notifying explicitly:
|
|
579
|
+
if conn_handle is not None:
|
|
580
|
+
self._ble.gatts_notify(conn_handle, val_handle)
|
|
581
|
+
elif self._connected_central >= 0:
|
|
582
|
+
self._ble.gatts_notify(self._connected_central, val_handle)
|
|
583
|
+
|
|
584
|
+
def scan(self):
|
|
585
|
+
"""
|
|
586
|
+
Start scanning for BLE peripherals. Scan results will be returned in the IRQ handler.
|
|
587
|
+
"""
|
|
588
|
+
self._ble.gap_scan(20000, 30000, 30000)
|
|
589
|
+
|
|
590
|
+
def stop_scan(self):
|
|
591
|
+
"""
|
|
592
|
+
Stop scanning for BLE peripherals.
|
|
593
|
+
"""
|
|
594
|
+
self._ble.gap_scan(None)
|
|
595
|
+
|
|
596
|
+
def connect_uart(
|
|
597
|
+
self, name="robot", on_disconnect=None, on_notify=None, on_write_done=None, time_out=10
|
|
598
|
+
):
|
|
599
|
+
"""
|
|
600
|
+
Connect to a BLE Peripheral that advertises with a certain name, and has a UART service.
|
|
601
|
+
This method is meant for BLE Centrals
|
|
602
|
+
|
|
603
|
+
:param name: The name of the peripheral to search for and connect to
|
|
604
|
+
:type name: str
|
|
605
|
+
:param on_disconnect: Callback function to call when the peripheral disconnects
|
|
606
|
+
:type on_disconnect: function
|
|
607
|
+
:param on_notify: Callback function to call when the peripheral notifies the central
|
|
608
|
+
:type on_notify: function
|
|
609
|
+
:param on_write_done: Callback function to call when the peripheral is done writing to the central
|
|
610
|
+
:type on_write_done: function
|
|
611
|
+
"""
|
|
612
|
+
# TODO: Create a generic connecting function that encodes the searched-for advertising data
|
|
613
|
+
# self._search_payload = _advertising_payload(name=name, services=[_UART_UUID])
|
|
614
|
+
# and searches for a match.
|
|
615
|
+
# Then make connect_uart and connect_lego call that DRYer function.
|
|
616
|
+
|
|
617
|
+
self._search_name = name
|
|
618
|
+
self.connecting_uart = True
|
|
619
|
+
self._conn_handle = None
|
|
620
|
+
self._start_handle = None
|
|
621
|
+
self._end_handle = None
|
|
622
|
+
self._rx_handle = None
|
|
623
|
+
self._tx_handle = None
|
|
624
|
+
self._addr_type = None
|
|
625
|
+
self._addr = None
|
|
626
|
+
|
|
627
|
+
self.scan()
|
|
628
|
+
for i in range(time_out):
|
|
629
|
+
|
|
630
|
+
if self.debug:
|
|
631
|
+
self.print_log()
|
|
632
|
+
else:
|
|
633
|
+
print("Connecting to UART Peripheral:", name)
|
|
634
|
+
sleep_ms(1000)
|
|
635
|
+
if not self.connecting_uart:
|
|
636
|
+
break
|
|
637
|
+
if self._rx_handle:
|
|
638
|
+
self._notify_callbacks[self._conn_handle] = on_notify
|
|
639
|
+
self._disconn_callbacks[self._conn_handle] = on_disconnect
|
|
640
|
+
self._write_done_callbacks[self._conn_handle] = on_write_done
|
|
641
|
+
|
|
642
|
+
# Increase packet size
|
|
643
|
+
self._ble.config(mtu=TARGET_MTU)
|
|
644
|
+
self._ble.gattc_exchange_mtu(self._conn_handle)
|
|
645
|
+
sleep_ms(60)
|
|
646
|
+
# Store the result of the mtu negotiation.
|
|
647
|
+
self.mtu = self._ble.config("mtu") - 4 # Some overhead bytes in max msg size.
|
|
648
|
+
|
|
649
|
+
self.connecting_uart = False
|
|
650
|
+
return self._conn_handle, self._rx_handle, self._tx_handle
|
|
651
|
+
|
|
652
|
+
def connect_lego(self, time_out=10):
|
|
653
|
+
"""
|
|
654
|
+
Connect to a LEGO Smart Hub that advertises with a LEGO service.
|
|
655
|
+
LEGO Hubs are advertising when their leds are blinking, just after turning them on.
|
|
656
|
+
"""
|
|
657
|
+
self.connecting_lego = True
|
|
658
|
+
self._conn_handle = None
|
|
659
|
+
self._start_handle = None
|
|
660
|
+
self._end_handle = None
|
|
661
|
+
self._lego_value_handle = None
|
|
662
|
+
self._addr_type = None
|
|
663
|
+
self._addr = None
|
|
664
|
+
self.scan()
|
|
665
|
+
for i in range(time_out):
|
|
666
|
+
if self.debug:
|
|
667
|
+
self.print_log()
|
|
668
|
+
else:
|
|
669
|
+
print("Connecting to a LEGO Smart Hub...")
|
|
670
|
+
sleep_ms(1000)
|
|
671
|
+
if not self.connecting_lego:
|
|
672
|
+
break
|
|
673
|
+
self.connecting_lego = False
|
|
674
|
+
return self._conn_handle
|
|
675
|
+
|
|
676
|
+
def uart_write(self, value, conn_handle, rx_handle=12, response=False):
|
|
677
|
+
self._ble.gattc_write(conn_handle, rx_handle, value, 1 if response else 0)
|
|
678
|
+
self.info("GATTC Written ", value)
|
|
679
|
+
|
|
680
|
+
def lego_write(self, value, conn_handle=None, response=False):
|
|
681
|
+
if not conn_handle:
|
|
682
|
+
conn_handle = self._conn_handle
|
|
683
|
+
if self._lego_value_handle and conn_handle is not None:
|
|
684
|
+
self._ble.gattc_write(conn_handle, self._lego_value_handle, value, 1 if response else 0)
|
|
685
|
+
self.info("GATTC Written ", value)
|
|
686
|
+
|
|
687
|
+
def enable_notify(self, conn_handle, desc_handle, callback=None):
|
|
688
|
+
self._ble.gattc_write(conn_handle, desc_handle, struct.pack("<h", _NOTIFY_ENABLE), 0)
|
|
689
|
+
if callback:
|
|
690
|
+
self._notify_callbacks[conn_handle] = callback
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class MidiController:
|
|
694
|
+
"""
|
|
695
|
+
Class for a MIDI BLE Controller. Turn your MINDSTORMS hub or LMS-ESP32 into a MIDI musical instrument!
|
|
696
|
+
|
|
697
|
+
:param name: The name of the MIDI controller
|
|
698
|
+
:type name: str
|
|
699
|
+
:param ble_handler: A BLEHandler instance. If None, a new one will be created.
|
|
700
|
+
:type ble_handler: BLEHandler
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
def __init__(self, name="amh-midi", ble_handler=None):
|
|
704
|
+
if ble_handler is None:
|
|
705
|
+
self.ble_handler = BLEHandler()
|
|
706
|
+
else:
|
|
707
|
+
self.ble_handler = ble_handler
|
|
708
|
+
((self.handle_midi,),) = self.ble_handler._ble.gatts_register_services((MIDI_SERVICE,))
|
|
709
|
+
self.ble_handler.advertise(advertising_payload(name=name[:8], services=[MIDI_SERVICE_UUID]))
|
|
710
|
+
|
|
711
|
+
def write_midi_msg(self, cmd, data0, data1):
|
|
712
|
+
"""
|
|
713
|
+
Timestamps and writes a MIDI message to the BLE GATT server.
|
|
714
|
+
See https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message for MIDI message format.
|
|
715
|
+
|
|
716
|
+
:param cmd: MIDI command byte
|
|
717
|
+
:type cmd: byte or int
|
|
718
|
+
:param data0: MIDI data byte 0
|
|
719
|
+
:type data0: byte or int
|
|
720
|
+
:param data1: MIDI data byte 1
|
|
721
|
+
:type data1: byte or int
|
|
722
|
+
"""
|
|
723
|
+
d = bytearray(5)
|
|
724
|
+
timestamp_ms = ticks_ms()
|
|
725
|
+
d[0] = (timestamp_ms >> 7 & 0x3F) | 0x80
|
|
726
|
+
d[1] = 0x80 | (timestamp_ms & 0x7F)
|
|
727
|
+
d[2] = cmd
|
|
728
|
+
d[3] = data0
|
|
729
|
+
d[4] = data1
|
|
730
|
+
self.ble_handler.notify(d, self.handle_midi)
|
|
731
|
+
|
|
732
|
+
def write_midi_notes(self, notes, velocity=0, on=True, channel=0):
|
|
733
|
+
"""
|
|
734
|
+
Timestamps and writes multiple MIDI notes to the BLE GATT server.
|
|
735
|
+
|
|
736
|
+
:param notes: list of MIDI note numbers
|
|
737
|
+
:type notes: bytearray or list of int
|
|
738
|
+
:param velocity: velocity
|
|
739
|
+
:type velocity: byte or int
|
|
740
|
+
:param on: Turn notes on if True, off if false. Default True.
|
|
741
|
+
:type on: bool
|
|
742
|
+
:param channel: MIDI Channel, default 0
|
|
743
|
+
:type channel: int
|
|
744
|
+
"""
|
|
745
|
+
d = bytearray(3 + 2 * len(notes))
|
|
746
|
+
timestamp_ms = ticks_ms()
|
|
747
|
+
d[0] = (timestamp_ms >> 7 & 0x3F) | 0x80
|
|
748
|
+
d[1] = 0x80 | (timestamp_ms & 0x7F)
|
|
749
|
+
d[2] = 0x90 + channel if on else 0x80 + channel
|
|
750
|
+
for i in range(len(notes)):
|
|
751
|
+
d[3 + i * 2] = notes[i]
|
|
752
|
+
d[4 + i * 2] = velocity
|
|
753
|
+
self.ble_handler.notify(d, self.handle_midi)
|
|
754
|
+
|
|
755
|
+
def note_on(self, note, velocity):
|
|
756
|
+
"""
|
|
757
|
+
Send a MIDI 'note on' message.
|
|
758
|
+
|
|
759
|
+
:param note: The note to play. Can be a MIDI note number (0-127) or a string like "C4" or "C#4"
|
|
760
|
+
:type note: byte or int or str
|
|
761
|
+
:param velocity: The velocity of the note key press (0-127)
|
|
762
|
+
:type velocity: byte or int
|
|
763
|
+
"""
|
|
764
|
+
self.write_midi_msg(0x90, note_parser(note), velocity)
|
|
765
|
+
|
|
766
|
+
def note_off(self, note, velocity=0):
|
|
767
|
+
"""
|
|
768
|
+
Send a MIDI 'note off' message.
|
|
769
|
+
|
|
770
|
+
:param note: The note to stop playing. Can be a MIDI note number (0-127) or a string like "C4" or "C#4"
|
|
771
|
+
:type note: byte or int or str
|
|
772
|
+
:param velocity: The velocity of the note key release (0-127)
|
|
773
|
+
:type velocity: byte or int
|
|
774
|
+
"""
|
|
775
|
+
self.write_midi_msg(0x80, note_parser(note), velocity)
|
|
776
|
+
|
|
777
|
+
def control_change(self, control, value):
|
|
778
|
+
"""
|
|
779
|
+
Send a MIDI CC 'control change' message. Handy for your ableton live controller.
|
|
780
|
+
|
|
781
|
+
:param control: The control number (0-127)
|
|
782
|
+
:type control: byte or int
|
|
783
|
+
:param value: The value of the control (0-127)
|
|
784
|
+
:type value: byte or int
|
|
785
|
+
"""
|
|
786
|
+
self.write_midi_msg(0xB0, control, value)
|
|
787
|
+
|
|
788
|
+
def chord_on(self, base, velocity, style="M"):
|
|
789
|
+
"""
|
|
790
|
+
Start playing a MIDI chord.
|
|
791
|
+
|
|
792
|
+
:param base: The base note of the chord. Can be a MIDI note number (0-127) or a string like "C4" or "C#4"
|
|
793
|
+
:type base: byte or int or str
|
|
794
|
+
:param velocity: The velocity of the chord key press (0-127)
|
|
795
|
+
:type velocity: byte or int
|
|
796
|
+
:param style: Chord style. See CHORD_STYLES for possible values.
|
|
797
|
+
"""
|
|
798
|
+
base = note_parser(base)
|
|
799
|
+
notes = [base + offset for offset in CHORD_STYLES[style]]
|
|
800
|
+
self.write_midi_notes(notes, velocity)
|
|
801
|
+
|
|
802
|
+
def chord_off(self, base, velocity=0, style="M"):
|
|
803
|
+
"""
|
|
804
|
+
Stop playing a MIDI chord.
|
|
805
|
+
"""
|
|
806
|
+
base = note_parser(base)
|
|
807
|
+
notes = [base + offset for offset in CHORD_STYLES[style]]
|
|
808
|
+
self.write_midi_notes(notes, velocity, on=False)
|
|
809
|
+
|
|
810
|
+
def play_chord(self, base, style="M", duration=1000):
|
|
811
|
+
"""
|
|
812
|
+
Play a MIDI chord for a given duration.
|
|
813
|
+
|
|
814
|
+
:param base: The base note of the chord. Can be a MIDI note number (0-127) or a string like "C4" or "C#4"
|
|
815
|
+
:type base: byte or int or str
|
|
816
|
+
:param style: Chord style. See CHORD_STYLES for possible values.
|
|
817
|
+
:param duration: The duration of the chord in milliseconds
|
|
818
|
+
:type duration: int
|
|
819
|
+
|
|
820
|
+
"""
|
|
821
|
+
self.chord_on(base, 100, style)
|
|
822
|
+
sleep_ms(duration * 7 // 10)
|
|
823
|
+
self.chord_off(base, 100, style)
|
|
824
|
+
sleep_ms(duration * 3 // 10)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
class BleUARTBase:
|
|
828
|
+
"""
|
|
829
|
+
Base class with a buffer for UART methods any(), read()
|
|
830
|
+
"""
|
|
831
|
+
|
|
832
|
+
READS_PER_MS = 10
|
|
833
|
+
|
|
834
|
+
def __init__(self, additive_buffer=True):
|
|
835
|
+
self.additive_buffer = additive_buffer
|
|
836
|
+
self.read_buffer = b""
|
|
837
|
+
|
|
838
|
+
def _on_rx(self, data):
|
|
839
|
+
if data:
|
|
840
|
+
if self.additive_buffer:
|
|
841
|
+
self.read_buffer += data
|
|
842
|
+
else:
|
|
843
|
+
self.read_buffer = data
|
|
844
|
+
|
|
845
|
+
def any(self):
|
|
846
|
+
"""
|
|
847
|
+
Returns the number of bytes in the read buffer.
|
|
848
|
+
"""
|
|
849
|
+
return len(self.read_buffer)
|
|
850
|
+
|
|
851
|
+
def read(self, n=-1):
|
|
852
|
+
"""
|
|
853
|
+
Read data from remote.
|
|
854
|
+
|
|
855
|
+
:param n: The number of bytes to read. If n is negative or omitted, read all data available.
|
|
856
|
+
:type n: int
|
|
857
|
+
"""
|
|
858
|
+
bufsize = len(self.read_buffer)
|
|
859
|
+
if n < 0 or n > bufsize:
|
|
860
|
+
n = bufsize
|
|
861
|
+
data = self.read_buffer[:n]
|
|
862
|
+
self.read_buffer = self.read_buffer[n:]
|
|
863
|
+
return data
|
|
864
|
+
|
|
865
|
+
def readline(self):
|
|
866
|
+
"""
|
|
867
|
+
Read a line from remote. A line is terminated with a newline character. ``\\n``
|
|
868
|
+
"""
|
|
869
|
+
data = b""
|
|
870
|
+
tries = 0
|
|
871
|
+
while tries < 50: # 1s timeout
|
|
872
|
+
c = self.read(1)
|
|
873
|
+
if c == b"\n":
|
|
874
|
+
break
|
|
875
|
+
elif c == b"":
|
|
876
|
+
if not self.is_connected():
|
|
877
|
+
break
|
|
878
|
+
tries += 1
|
|
879
|
+
sleep_ms(25)
|
|
880
|
+
else:
|
|
881
|
+
tries = 0
|
|
882
|
+
data += c
|
|
883
|
+
return data.decode("UTF")
|
|
884
|
+
|
|
885
|
+
def writeline(self, data: str):
|
|
886
|
+
"""
|
|
887
|
+
Write data to remote and terminate with an added newline character. ``\\n``
|
|
888
|
+
"""
|
|
889
|
+
self.write(data + "\n")
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
class UARTPeripheral(BleUARTBase):
|
|
893
|
+
"""
|
|
894
|
+
Class for a Nordic UART BLE server/peripheral.
|
|
895
|
+
It will advertise as the given name and populate the UART services and characteristics
|
|
896
|
+
|
|
897
|
+
:param name: The name of the peripheral
|
|
898
|
+
:type name: str
|
|
899
|
+
:param ble_handler: A BLEHandler instance. If None, a new one will be created.
|
|
900
|
+
:type ble_handler: BLEHandler
|
|
901
|
+
:param additive_buffer: If True, the read buffer will be added to on each read. If False, the read buffer will be overwritten on each read.
|
|
902
|
+
:type additive_buffer: bool
|
|
903
|
+
"""
|
|
904
|
+
|
|
905
|
+
def __init__(self, name="robot", ble_handler: BLEHandler = None, additive_buffer=True):
|
|
906
|
+
super().__init__(additive_buffer)
|
|
907
|
+
self.name = name
|
|
908
|
+
if ble_handler is None:
|
|
909
|
+
ble_handler = BLEHandler()
|
|
910
|
+
self.ble_handler = ble_handler
|
|
911
|
+
|
|
912
|
+
((self._handle_tx, self._handle_rx),) = self.ble_handler._ble.gatts_register_services(
|
|
913
|
+
(_UART_SERVICE,)
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
self.ble_handler.on_write(self._handle_rx, self._on_rx)
|
|
917
|
+
self.ble_handler_central_disconn_callback = self._on_disconnect
|
|
918
|
+
|
|
919
|
+
# Characteristics and descriptors have a default maximum size of 20 bytes.
|
|
920
|
+
# Anything written to them by a client will be truncated to this length.
|
|
921
|
+
# However, any local write will increase the maximum size, so if you want
|
|
922
|
+
# to allow larger writes from a client to a given characteristic,
|
|
923
|
+
# use gatts_write after registration.
|
|
924
|
+
|
|
925
|
+
# Increase buffer size to fit MTU
|
|
926
|
+
self.ble_handler._ble.gatts_set_buffer(self._handle_rx, TARGET_MTU)
|
|
927
|
+
|
|
928
|
+
# Stretch buffer
|
|
929
|
+
self.ble_handler._ble.gatts_write(self._handle_rx, bytes(TARGET_MTU))
|
|
930
|
+
|
|
931
|
+
# Flush
|
|
932
|
+
_ = self.ble_handler._ble.gatts_read(self._handle_rx)
|
|
933
|
+
self.ble_handler.advertise(advertising_payload(name=self.name, services=[_UART_UUID]))
|
|
934
|
+
|
|
935
|
+
def is_connected(self):
|
|
936
|
+
return self.ble_handler._connected_central >= 0
|
|
937
|
+
|
|
938
|
+
def _on_disconnect(self, conn_handle):
|
|
939
|
+
# Flush buffer
|
|
940
|
+
self.read()
|
|
941
|
+
|
|
942
|
+
def write(self, data):
|
|
943
|
+
"""
|
|
944
|
+
Write uart data to remote. This is a blocking call.
|
|
945
|
+
"""
|
|
946
|
+
if self.is_connected():
|
|
947
|
+
try:
|
|
948
|
+
for i in range(0, len(data), MAX_NOTIFY):
|
|
949
|
+
self.ble_handler.notify(data[i : i + MAX_NOTIFY], val_handle=self._handle_tx)
|
|
950
|
+
sleep_ms(10)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
print("Error writing:", e, data, type(data))
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
class UARTCentral(BleUARTBase):
|
|
956
|
+
# """Class to connect to single BLE Peripheral as a Central
|
|
957
|
+
|
|
958
|
+
# Instantiate more 'centrals' with the same ble handler to connect to
|
|
959
|
+
# multiple peripherals. Things will probably break if you instantiate
|
|
960
|
+
# multiple ble handlers. (EALREADY)
|
|
961
|
+
|
|
962
|
+
# """
|
|
963
|
+
def __init__(self, ble_handler: BLEHandler = None, additive_buffer=True):
|
|
964
|
+
super().__init__(additive_buffer)
|
|
965
|
+
|
|
966
|
+
if ble_handler is None:
|
|
967
|
+
ble_handler = BLEHandler()
|
|
968
|
+
self.ble_handler = ble_handler
|
|
969
|
+
|
|
970
|
+
self._on_disconnect()
|
|
971
|
+
|
|
972
|
+
def _on_disconnect(self):
|
|
973
|
+
# The on_disconnect callback is linked to our conn_handle
|
|
974
|
+
# in _IRQ_PERIPHERAL_DISCONNECT.
|
|
975
|
+
# so no need to check which conn handle it was.
|
|
976
|
+
# Reset up all properties
|
|
977
|
+
self._conn_handle = None
|
|
978
|
+
self._periph_name = None
|
|
979
|
+
self._tx_handle = 9 # None
|
|
980
|
+
self._rx_handle = 12 # None
|
|
981
|
+
self.writing = False
|
|
982
|
+
self.reading = False
|
|
983
|
+
|
|
984
|
+
def _on_write_done(self, value_handle, status):
|
|
985
|
+
# After writing to a server, this is called when writing is done.
|
|
986
|
+
# Status = 0 when the write was succesful.
|
|
987
|
+
# Value handle is 65555 something, not the rx_handle. Strange.
|
|
988
|
+
self.writing = False
|
|
989
|
+
|
|
990
|
+
def connect(self, name="robot"):
|
|
991
|
+
"""
|
|
992
|
+
Search for and connect to a peripheral with a given name.
|
|
993
|
+
|
|
994
|
+
:param name: The name of the peripheral to connect to
|
|
995
|
+
:type name: str
|
|
996
|
+
"""
|
|
997
|
+
self._periph_name = name
|
|
998
|
+
self._conn_handle, self._rx_handle, self._tx_handle = self.ble_handler.connect_uart(
|
|
999
|
+
name,
|
|
1000
|
+
on_disconnect=self._on_disconnect,
|
|
1001
|
+
on_notify=self._on_rx,
|
|
1002
|
+
on_write_done=self._on_write_done,
|
|
1003
|
+
) # Blocks until timeout or device with the right name found
|
|
1004
|
+
return self.is_connected()
|
|
1005
|
+
|
|
1006
|
+
def is_connected(self):
|
|
1007
|
+
return self._conn_handle is not None
|
|
1008
|
+
|
|
1009
|
+
def disconnect(self):
|
|
1010
|
+
if self.is_connected():
|
|
1011
|
+
self.ble_handler._ble.gap_disconnect(self._conn_handle)
|
|
1012
|
+
|
|
1013
|
+
def write(self, data):
|
|
1014
|
+
"""
|
|
1015
|
+
Write uart data to remote. This is a blocking call and will wait until writing is done.
|
|
1016
|
+
|
|
1017
|
+
:param data: The data to write to the peripheral
|
|
1018
|
+
:type data: bytes
|
|
1019
|
+
"""
|
|
1020
|
+
if self.is_connected():
|
|
1021
|
+
try:
|
|
1022
|
+
# Chop data in mtu-sizes packages
|
|
1023
|
+
for i in range(0, len(data), self.ble_handler.mtu):
|
|
1024
|
+
tries = 0
|
|
1025
|
+
while tries < 50:
|
|
1026
|
+
if not self.writing: # Only send when writing is done
|
|
1027
|
+
self.writing = True
|
|
1028
|
+
partial = data[i : i + self.ble_handler.mtu]
|
|
1029
|
+
self.ble_handler.uart_write(
|
|
1030
|
+
partial, self._conn_handle, self._rx_handle, response=True
|
|
1031
|
+
)
|
|
1032
|
+
break
|
|
1033
|
+
else:
|
|
1034
|
+
# Wait some more until writing is done.
|
|
1035
|
+
tries += 1
|
|
1036
|
+
sleep_ms(5)
|
|
1037
|
+
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
print("Error writing:", partial, type(partial), len(partial), e)
|
|
1040
|
+
|
|
1041
|
+
def fast_write(self, data):
|
|
1042
|
+
"""
|
|
1043
|
+
Write to server/peripheral as fast as possible. Non-blocking.
|
|
1044
|
+
- Data is truncated to mtu
|
|
1045
|
+
- No pause after writing. Writing too often can crash the ble stack. Be careful
|
|
1046
|
+
|
|
1047
|
+
:param data: The data to write to the peripheral
|
|
1048
|
+
:type data: bytes
|
|
1049
|
+
"""
|
|
1050
|
+
if self.is_connected():
|
|
1051
|
+
try:
|
|
1052
|
+
self.ble_handler.uart_write(
|
|
1053
|
+
data[: self.ble_handler.mtu], self._conn_handle, self._rx_handle, response=False
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
except Exception as e:
|
|
1057
|
+
print("Error writing:", e, data)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
class RCReceiver(UARTPeripheral):
|
|
1061
|
+
"""
|
|
1062
|
+
Class for an Remote Control Receiver. It reads and processes gamepad or remote control data.
|
|
1063
|
+
It will advertise as the given name.
|
|
1064
|
+
|
|
1065
|
+
:param name: The name of this peripheral to advertise as. Default: "robot"
|
|
1066
|
+
:type name: str
|
|
1067
|
+
:param ble_handler: A BLEHandler instance. If None, a new one will be created.
|
|
1068
|
+
:type ble_handler: BLEHandler
|
|
1069
|
+
"""
|
|
1070
|
+
|
|
1071
|
+
def __init__(self, **kwargs):
|
|
1072
|
+
super().__init__(additive_buffer=False, **kwargs)
|
|
1073
|
+
self.read_buffer = bytearray(struct.calcsize("bbbbBBhhB"))
|
|
1074
|
+
|
|
1075
|
+
def button_pressed(self, button):
|
|
1076
|
+
"""
|
|
1077
|
+
Returns True if the given button is pressed on the remote control.
|
|
1078
|
+
|
|
1079
|
+
:param button: The button number to check. 1-8
|
|
1080
|
+
:type button: int
|
|
1081
|
+
"""
|
|
1082
|
+
if 0 < button < 9:
|
|
1083
|
+
return self.controller_state(BUTTONS) & 1 << button - 1
|
|
1084
|
+
else:
|
|
1085
|
+
return False
|
|
1086
|
+
|
|
1087
|
+
def controller_state(self, *indices):
|
|
1088
|
+
"""
|
|
1089
|
+
Returns the controller state as a list of 9 integers:
|
|
1090
|
+
[left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger,
|
|
1091
|
+
right_trigger, left_setting, right_setting, buttons]
|
|
1092
|
+
|
|
1093
|
+
:param indices: The items of the selection of controller states to return.
|
|
1094
|
+
If omitted, the whole list is returned. Use these constants:
|
|
1095
|
+
`L_STICK_HOR, L_STICK_VER, R_STICK_HOR, R_STICK_VER, L_TRIGGER, R_TRIGGER,
|
|
1096
|
+
SETTING1, SETTING2, BUTTONS`
|
|
1097
|
+
|
|
1098
|
+
:type indices: int
|
|
1099
|
+
|
|
1100
|
+
Use the controller state L_STICK indices to get only left stick values::
|
|
1101
|
+
|
|
1102
|
+
left_stick_x, left_stick_y, = rc.controller_state(L_STICK_HOR, L_STICK_VER)
|
|
1103
|
+
|
|
1104
|
+
"""
|
|
1105
|
+
try:
|
|
1106
|
+
controller_state = struct.unpack("bbbbBBhhB", self.read_buffer)
|
|
1107
|
+
except:
|
|
1108
|
+
controller_state = [0] * 9
|
|
1109
|
+
if indices:
|
|
1110
|
+
if len(indices) == 1:
|
|
1111
|
+
return controller_state[indices[0]]
|
|
1112
|
+
else:
|
|
1113
|
+
return [controller_state[i] for i in indices]
|
|
1114
|
+
else:
|
|
1115
|
+
return controller_state
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
class RCTransmitter(UARTCentral):
|
|
1119
|
+
"""
|
|
1120
|
+
Class for a Remote control transmitter. It sends gamepad or remote control data to a receiver.
|
|
1121
|
+
|
|
1122
|
+
:param name: The name of the peripheral to search for and connect to. Default: "robot"
|
|
1123
|
+
:type name: str
|
|
1124
|
+
:param ble_handler: A BLEHandler instance. If None, a new one will be created.
|
|
1125
|
+
:type ble_handler: BLEHandler
|
|
1126
|
+
"""
|
|
1127
|
+
|
|
1128
|
+
def __init__(self, **kwargs):
|
|
1129
|
+
super().__init__(**kwargs)
|
|
1130
|
+
# An empty 9-item list. Order is important.
|
|
1131
|
+
self.controller_state = [0] * 9
|
|
1132
|
+
self.last_write = 0
|
|
1133
|
+
|
|
1134
|
+
@staticmethod
|
|
1135
|
+
def clamp_int(n, floor=-100, ceiling=100):
|
|
1136
|
+
return max(min(round(n), ceiling), floor)
|
|
1137
|
+
|
|
1138
|
+
def set_button(self, num, pressed):
|
|
1139
|
+
"""
|
|
1140
|
+
Set a button to pressed or not pressed.
|
|
1141
|
+
|
|
1142
|
+
:param num: The button number to set. 1-8
|
|
1143
|
+
:type num: int
|
|
1144
|
+
:param pressed: True or False
|
|
1145
|
+
:type pressed: bool
|
|
1146
|
+
"""
|
|
1147
|
+
if 0 < num < 9:
|
|
1148
|
+
bitmask = 0b1 << (num - 1)
|
|
1149
|
+
if pressed:
|
|
1150
|
+
self.controller_state[BUTTONS] |= bitmask
|
|
1151
|
+
else:
|
|
1152
|
+
self.controller_state[BUTTONS] &= ~bitmask
|
|
1153
|
+
|
|
1154
|
+
def set_stick(self, stick, value):
|
|
1155
|
+
"""
|
|
1156
|
+
Set a stick value. Value should be between -100 and 100.
|
|
1157
|
+
|
|
1158
|
+
:param stick: The stick to set. Use these constants: L_STICK_HOR, L_STICK_VER, R_STICK_HOR, R_STICK_VER
|
|
1159
|
+
:type stick: int
|
|
1160
|
+
:param value: The value to set. Should be between -100 and 100.
|
|
1161
|
+
:type value: int
|
|
1162
|
+
"""
|
|
1163
|
+
self.controller_state[stick] = self.clamp_int(value)
|
|
1164
|
+
|
|
1165
|
+
def set_trigger(self, trig, value):
|
|
1166
|
+
"""
|
|
1167
|
+
Set a gamepad shoulder trigger value. Value should be between 0 and 200.
|
|
1168
|
+
|
|
1169
|
+
:param trig: The trigger to set. Use these constants: L_TRIGGER, R_TRIGGER
|
|
1170
|
+
:type trig: int
|
|
1171
|
+
:param value: The value to set. Should be between 0 and 200.
|
|
1172
|
+
:type value: int
|
|
1173
|
+
"""
|
|
1174
|
+
self.controller_state[trig] = self.clamp_int(value, 0, 200)
|
|
1175
|
+
|
|
1176
|
+
def set_setting(self, setting, value):
|
|
1177
|
+
"""
|
|
1178
|
+
Set a parameter dial setting.
|
|
1179
|
+
|
|
1180
|
+
:param setting: The setting to set. Use these constants: SETTING1, SETTING2
|
|
1181
|
+
:type setting: int
|
|
1182
|
+
:param value: The value to set. Should be between -32768 and 32767.
|
|
1183
|
+
:type value: int
|
|
1184
|
+
"""
|
|
1185
|
+
self.controller_state[setting] = self.clamp_int(value, -(2**15), 2**15)
|
|
1186
|
+
|
|
1187
|
+
def transmit(self):
|
|
1188
|
+
"""
|
|
1189
|
+
Send the controller state to the receiver.
|
|
1190
|
+
This call will wait if you write again within 15ms.
|
|
1191
|
+
"""
|
|
1192
|
+
# Don't send too often.
|
|
1193
|
+
while ticks_diff(ticks_ms(), self.last_write) < 15:
|
|
1194
|
+
sleep_ms(1)
|
|
1195
|
+
value = struct.pack("bbbbBBhhB", *self.controller_state)
|
|
1196
|
+
self.fast_write(value)
|
|
1197
|
+
self.last_write = ticks_ms()
|