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/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()