pupremote 2.1.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.
lpf2.py ADDED
@@ -0,0 +1,545 @@
1
+ # LPF2 class allows communication between LEGO SPIKE Prime and third party devices.
2
+ __author__ = "Anton Vanhoucke & Ste7an"
3
+ __copyright__ = "Copyright 2023, 2024 AntonsMindstorms.com"
4
+ __license__ = "GPL"
5
+ __version__ = "1.5"
6
+ __status__ = "Production"
7
+
8
+ import machine
9
+ import struct
10
+ import utime
11
+ try:
12
+ from micropython import const
13
+ except ImportError:
14
+ def const(i):
15
+ return i
16
+
17
+ from sys import implementation
18
+
19
+ # OpenMV board platform type
20
+ # sys.implementation[2]: OPENMV4P-STM32H743
21
+ OPENMV = const(0)
22
+ # LMS-ESP32 board platform type
23
+ # sys.implementation[2]: ESP32 module (lvgl,ulab,spiram) with ESP32
24
+ ESP32 = const(1)
25
+ # OpenMV RT board platform type
26
+ # sys.implementation[2]: OpenMV IMXRT1060-MIMXRT1062DVJ6A
27
+ OPENMVRT = const(2)
28
+
29
+ MAX_PKT = const(32)
30
+
31
+ BYTE_NACK = const(0x02)
32
+ BYTE_ACK = const(0x04)
33
+ CMD_Type = const(0x40) # @, set sensor type command
34
+ CMD_Select = const(0x43) # C, sets modes on the fly
35
+ CMD_MODES = const(0x41) # I, set mode type command
36
+ CMD_EXT_MODE = const(0x46)
37
+ CMD_Baud = const(0x52) # R, set the transmission baud rate
38
+ CMD_Vers = const(0x5F) # _, set the version number
39
+ MSG_INFO = const(0x80) # name command
40
+ MSG_DATA = const(0xC0) # data command
41
+ MSG_EXT_MODE = const(0x46)
42
+ EXT_MODE_0 = const(0x00)
43
+ EXT_MODE_8 = const(0x08) # only used for extended mode > 7
44
+ CMD_LLL_SHIFT = const(3)
45
+ MSG_INFO_PLUS8 = const(0x20)
46
+
47
+ LEN_4 = const(2 << CMD_LLL_SHIFT)
48
+ LEN_2 = const(1 << CMD_LLL_SHIFT)
49
+ LEN_8 = const(3 << CMD_LLL_SHIFT)
50
+
51
+ NAME = const(0x0)
52
+ RAW = const(0x1)
53
+ PCT = const(0x2)
54
+ SI = const(0x3)
55
+ SYM = const(0x4)
56
+ FUNCTION_MAP = const(0x5)
57
+ FMT = const(0x80)
58
+
59
+ DATA8 = const(0)
60
+ DATA16 = const(1)
61
+ DATA32 = const(2)
62
+ DATAF = const(3)
63
+
64
+ # Input/Output Mapping flags, can be combined with |
65
+ WITH_NULL = const(2**7) # Supports NULL value
66
+ FUNC_2 = const(2**6) # Supports Functional Mapping 2.0+
67
+ ABSOLUTE = const(16) # ABS (Absolute [min..max])
68
+ RELATIVE = const(8) # REL (Relative [-1..1])
69
+ DISCRETE = const(4) # DIS (Discrete [0, 1, 2, 3])
70
+
71
+ STRUCT_FMT = ("B", "H", "I", "f")
72
+
73
+ HEARTBEAT_PERIOD = const(1000) # time of inactivity after which we reset sensor
74
+
75
+
76
+ def __num_bits(x):
77
+ # Return the number of bits required to represent x
78
+ n = 0
79
+ while x > 0:
80
+ x >>= 1
81
+ n += 1
82
+ return n
83
+
84
+
85
+ class LPF2(object):
86
+ def __init__(
87
+ self,
88
+ modes,
89
+ sensor_id=62,
90
+ debug=False,
91
+ max_packet_size=MAX_PKT,
92
+ rx=None,
93
+ tx=None,
94
+ uart_n=None,
95
+ ):
96
+ self.modes = modes
97
+ self.current_mode = 0
98
+ self.sensor_id = sensor_id
99
+ self.connected = False
100
+ self.payloads = {}
101
+ self.last_nack = 0
102
+ self.debug = debug
103
+ self.max_packet_size = max_packet_size
104
+ self.UART_N = uart_n
105
+ self.TX_PIN_N = tx
106
+ self.RX_PIN_N = rx
107
+ if "RT1060" in implementation[2]:
108
+ self.BOARD = OPENMVRT
109
+ if uart_n == None:
110
+ self.UART_N = 1
111
+ print("OpenMV RT defaults loaded")
112
+ elif "OPENMV4" in implementation[2]:
113
+ self.BOARD = OPENMV
114
+ import pyb
115
+ self.pyb = pyb
116
+ if uart_n == None:
117
+ self.UART_N = 3
118
+ print("OpenMV H7 defaults loaded")
119
+ else:
120
+ self.BOARD = ESP32
121
+ try:
122
+ from lms_esp32 import RX_PIN,TX_PIN
123
+ except ImportError:
124
+ RX_PIN = 18
125
+ TX_PIN = 19
126
+ if tx == None:
127
+ self.TX_PIN_N = TX_PIN
128
+ if rx == None:
129
+ self.RX_PIN_N = RX_PIN
130
+ if uart_n == None:
131
+ self.UART_N = 2
132
+ print(
133
+ "LMS-ESP32 defaults loaded, with rx={}, tx={}".format(self.RX_PIN_N, self.TX_PIN_N)
134
+ )
135
+
136
+ @staticmethod
137
+ def mode(
138
+ name,
139
+ size=1,
140
+ data_type=DATA8,
141
+ writable=0, # Leaving this for bw compatibility
142
+ format="3.0",
143
+ raw_range=[0, 100],
144
+ percent_range=[0, 100],
145
+ si_range=[0, 100],
146
+ symbol="",
147
+ functionmap=[ABSOLUTE, ABSOLUTE], # [in (to hub), out (from hub)]
148
+ view=True,
149
+ ):
150
+ fig, dec = format.split(".")
151
+ total_data_size = size * 2**data_type # Byte size of data set.
152
+ # Find the power of 2 that is greater than the length of the data
153
+ # -1 because of the header byte.
154
+ bit_size = __num_bits(total_data_size - 1)
155
+ mode_list = [
156
+ name, # 0
157
+ [size, data_type, int(fig), int(dec)], # 1
158
+ raw_range, # 2
159
+ percent_range, # 3
160
+ si_range, # 4
161
+ symbol, # 5
162
+ functionmap, # 6
163
+ view and functionmap[0], # 7
164
+ total_data_size, # 8
165
+ bit_size, # 9
166
+ ]
167
+ return mode_list
168
+
169
+ def init_pins(self):
170
+ if self.BOARD == ESP32:
171
+ self.rx_pin = machine.Pin(self.RX_PIN_N, machine.Pin.IN)
172
+ self.tx_pin = machine.Pin(
173
+ self.TX_PIN_N, machine.Pin.OUT, machine.Pin.PULL_DOWN
174
+ )
175
+ elif self.BOARD == OPENMVRT:
176
+ self.rx_pin = machine.Pin("P5", machine.Pin.IN)
177
+ self.tx_pin = machine.Pin("P4", machine.Pin.OUT, machine.Pin.PULL_DOWN)
178
+ elif self.BOARD == OPENMV:
179
+ self.rx_pin = self.pyb.Pin("P5", self.pyb.Pin.IN)
180
+ self.tx_pin = self.pyb.Pin("P4", self.pyb.Pin.OUT_PP)
181
+
182
+ def wrt_tx_pin(self, val, wait):
183
+ # Reinit pin to deal with cable unplugging and re-plugging
184
+ self.tx_pin.value(val)
185
+ utime.sleep_ms(wait)
186
+
187
+ def slow_uart(self):
188
+ if self.BOARD == ESP32:
189
+ self.uart = machine.UART(
190
+ self.UART_N,
191
+ baudrate=2400,
192
+ rx=self.RX_PIN_N,
193
+ tx=self.TX_PIN_N,
194
+ )
195
+
196
+ elif self.BOARD == OPENMVRT:
197
+ self.uart = machine.UART(self.UART_N, 2400)
198
+
199
+ elif self.BOARD == OPENMV:
200
+ self.uart = self.pyb.UART(self.UART_N, 2400)
201
+
202
+ def fast_uart(self):
203
+ if self.BOARD == ESP32:
204
+ self.uart = machine.UART(
205
+ self.UART_N,
206
+ baudrate=115200,
207
+ rx=self.RX_PIN_N,
208
+ tx=self.TX_PIN_N,
209
+ )
210
+
211
+ elif self.BOARD == OPENMVRT:
212
+ self.uart = machine.UART(self.UART_N, 115200)
213
+ utime.sleep_ms(5)
214
+
215
+ elif self.BOARD == OPENMV:
216
+ self.uart = self.pyb.UART(self.UART_N, 115200)
217
+
218
+ # -------- Payload definition
219
+
220
+ def load_payload(self, data, mode=None):
221
+ if mode is None:
222
+ mode = self.current_mode
223
+ data_type = self.modes[mode][1][1]
224
+ if isinstance(data, bytes):
225
+ bin_data = data
226
+ elif isinstance(data, bytearray):
227
+ bin_data = data
228
+ elif isinstance(data, list):
229
+ # We have a list of integers. Pack them as bytes.
230
+ bin_data = struct.pack("%d" % len(data) + STRUCT_FMT[data_type], *data)
231
+ elif isinstance(data, float) or isinstance(data, int):
232
+ bin_data = struct.pack(STRUCT_FMT[data_type], data)
233
+ elif isinstance(data, str):
234
+ # String. Convert to bytes of max size.
235
+ bin_data = bytes(data, "UTF-8")[: self.max_packet_size]
236
+ else:
237
+ raise ValueError("Wrong data type: %s" % type(data))
238
+
239
+ bytesize = self.modes[mode][8]
240
+ bit = self.modes[mode][9]
241
+
242
+ assert len(bin_data) > 0, "Payload is empty"
243
+ assert len(bin_data) <= bytesize, "Wrong payload size"
244
+
245
+ payload = bytearray(2**bit + 5)
246
+ payload[0] = MSG_EXT_MODE
247
+ payload[1] = EXT_MODE_0 if mode < 8 else EXT_MODE_8
248
+ payload[2] = 0xFF ^ payload[0] ^ payload[1]
249
+ cksm = 0xFF
250
+ payload[3] = MSG_DATA | (bit << CMD_LLL_SHIFT) | (mode & 7)
251
+ cksm ^= payload[3]
252
+ for i in range(len(bin_data)):
253
+ payload[i + 4] = bin_data[i]
254
+ cksm ^= bin_data[i]
255
+ payload[-1] = cksm # No need to checksum zero bytes.
256
+
257
+ self.payloads[mode] = payload
258
+
259
+ def send_payload(self, data=None, mode=None):
260
+ """
261
+ Convert bytes of data to a proper LPF2 payload,
262
+ save it to the payload of the current mode,
263
+ and write it to the hub. If there is no data, just
264
+ send current payload.
265
+ """
266
+ if not self.connected:
267
+ if self.debug:
268
+ print("Write payload, but not connected.")
269
+ return
270
+ if mode == None:
271
+ mode = self.current_mode
272
+ if data != None:
273
+ self.load_payload(data, mode)
274
+ self.write(self.payloads[mode])
275
+
276
+ def update_payload(self, data, mode):
277
+ if mode == self.current_mode:
278
+ self.send_payload(data, mode)
279
+ else:
280
+ self.load_payload(data, mode)
281
+
282
+ # ----- comm stuff
283
+
284
+ def flush(self):
285
+ return self.uart.read(self.uart.any())
286
+
287
+ @staticmethod
288
+ def str_b(b):
289
+ return " ".join([hex(c) for c in b])
290
+
291
+ def readchar(self):
292
+ if self.uart.any():
293
+ c = self.uart.read(1)
294
+ else: # Try again once
295
+ utime.sleep_ms(1)
296
+ if self.uart.any():
297
+ c = self.uart.read(1)
298
+ else:
299
+ return -1
300
+ if c == None:
301
+ return -1
302
+ else:
303
+ if self.debug:
304
+ print(f"\033[91m {self.str_b(c)}\033[0m", end=" ")
305
+ return ord(c)
306
+
307
+ def heartbeat(self):
308
+ if not self.connected:
309
+ print("Checking heartbeat, but not connected. Initializing.")
310
+ self.connect()
311
+ return
312
+
313
+ if (utime.ticks_ms() - self.last_nack) > HEARTBEAT_PERIOD:
314
+ print("Checking heartbeat, but line is dead. Re-initializing.")
315
+ self.connected = False
316
+ self.connect()
317
+ return
318
+
319
+ b = self.readchar() # Read in any heartbeat or command bytes
320
+ if b > 0: # There is data, let's see what it is.
321
+ if b == BYTE_NACK:
322
+ # Regular heartbeat pulse from the hub.
323
+ self.last_nack = utime.ticks_ms() # reset heartbeat timer
324
+ # Resend latest data, just in case
325
+ self.send_payload()
326
+
327
+ elif b == CMD_Select:
328
+ self.last_nack = utime.ticks_ms() # reset heartbeat timer
329
+ # The hub is asking us to change mode.
330
+ mode = self.readchar()
331
+ cksm = self.readchar()
332
+ # Calculate the checksum for two bytes.
333
+ if cksm == 0xFF ^ CMD_Select ^ mode:
334
+ self.current_mode = mode
335
+ self.send_payload()
336
+ if self.debug:
337
+ print(f"Mode switched to {mode}")
338
+
339
+ elif b == CMD_EXT_MODE:
340
+ self.last_nack = utime.ticks_ms() # reset heartbeat timer
341
+ ext_mode = self.readchar() # 0x00 or 0x08
342
+ cksm = self.readchar() # 0xb9 or 0xb1
343
+
344
+ if cksm == 0xFF ^ CMD_EXT_MODE ^ ext_mode:
345
+ b = self.readchar() # CMD_Data | LENGTH | MODE
346
+
347
+ # Bitmask and then shift to get the LENGTH (=size exponent) of the data
348
+ size = 2 ** ((b & 0b111000) >> 3)
349
+
350
+ # Bitmask to get the mode number
351
+ # TODO test if setting current mode is part of the protocol
352
+ wrt_mode = (b & 0b111) + ext_mode
353
+
354
+ # Keep track of the checksum while reading data
355
+ ck = 0xFF ^ b
356
+
357
+ buf = bytearray(size)
358
+ for i in range(size):
359
+ buf[i] = self.readchar()
360
+ # Keep track of the checksum
361
+ ck ^= buf[i]
362
+
363
+ if ck == self.readchar():
364
+ return buf, wrt_mode
365
+ else:
366
+ print(
367
+ "Checksum error. Try reducing max_packet_size to 16 if using Pybricks."
368
+ )
369
+ else:
370
+ if self.debug:
371
+ buf = self.flush()
372
+ print(f"Unhandled data from hub {hex(b)} {self.str_b(buf)}")
373
+
374
+ def write(self, array):
375
+ if self.debug:
376
+ print("\n>> ", self.str_b(array))
377
+ return self.uart.write(array)
378
+
379
+ @staticmethod
380
+ def calc_cksm(array):
381
+ chksm = 0xFF
382
+ for b in array:
383
+ chksm ^= b
384
+ return chksm
385
+
386
+ def addChksm(self, array):
387
+ return array + self.calc_cksm(array).to_bytes(1, "little")
388
+
389
+ # ---- settup definitions
390
+
391
+ def setType(self, sensorType):
392
+ return self.addChksm(bytearray([CMD_Type, sensorType]))
393
+
394
+ def defineBaud(self, baud):
395
+ rate = baud.to_bytes(4, "little")
396
+ return self.addChksm(bytearray([CMD_Baud]) + rate)
397
+
398
+ @staticmethod
399
+ def str_vers_to_4_bytes(str_vers: str) -> bytes:
400
+ stvb = bytes([int(n)&0xFF for n in str_vers.split(".")])
401
+ if len(stvb) >= 4:
402
+ return stvb[:4]
403
+ return b"\x00" * (4 - len(stvb)) + stvb
404
+
405
+ def defineVers(self, hardware: str, software: str):
406
+ return self.addChksm(
407
+ bytearray([CMD_Vers])
408
+ + self.str_vers_to_4_bytes(hardware)
409
+ + self.str_vers_to_4_bytes(software)
410
+ )
411
+
412
+ def str_info(self, data, num, info_type):
413
+ if isinstance(data, str): # Convert and truncate
414
+ dt = bytearray(data, "UTF-8")[: self.max_packet_size]
415
+ else: # Bytes, or bytearray. Just truncate.
416
+ dt = bytearray(data)[: self.max_packet_size]
417
+ exp = __num_bits(len(dt) - 1)
418
+ pl = bytearray(2**exp)
419
+ pl[: len(dt)] = dt
420
+ return self.addChksm(
421
+ bytearray([MSG_INFO | exp << CMD_LLL_SHIFT | num, info_type]) + pl
422
+ )
423
+
424
+ def buildFunctMap(self, fmap, num, info_type):
425
+ return self.addChksm(
426
+ bytearray([MSG_INFO | LEN_2 | num, info_type, fmap[0], fmap[1]])
427
+ )
428
+
429
+ def buildFormat(self, fmt, num, info_type):
430
+ sampleSize = fmt[0] & 0xFF
431
+ dataType = fmt[1] & 0xFF
432
+ figures = fmt[2] & 0xFF
433
+ decimals = fmt[3] & 0xFF
434
+ return self.addChksm(
435
+ bytearray(
436
+ [
437
+ MSG_INFO | LEN_4 | num,
438
+ info_type,
439
+ sampleSize,
440
+ dataType,
441
+ figures,
442
+ decimals,
443
+ ]
444
+ )
445
+ )
446
+
447
+ def buildRange(self, settings, num, rangeType):
448
+ exp = 3 << CMD_LLL_SHIFT
449
+ minVal = struct.pack("<f", settings[0])
450
+ maxVal = struct.pack("<f", settings[1])
451
+ return self.addChksm(
452
+ bytearray([MSG_INFO | exp | num, rangeType]) + minVal + maxVal
453
+ )
454
+
455
+ def defineModes(self):
456
+ n_modes = len(self.modes) - 1
457
+ n_views = [m[7] for m in self.modes].count(True) - 1
458
+ return self.addChksm(
459
+ bytearray(
460
+ [
461
+ CMD_MODES | LEN_4,
462
+ min(n_modes, 7),
463
+ min(n_views, 7),
464
+ n_modes,
465
+ n_views,
466
+ ]
467
+ )
468
+ )
469
+
470
+ def setupMode(self, mode, num):
471
+ self.load_payload(b"\x00" * mode[8], num) # Store empty payload for this mode
472
+ plus_8 = 0x00
473
+ if num > 7:
474
+ num -= 8
475
+ plus_8 = MSG_INFO_PLUS8
476
+ self.write(self.str_info(mode[0], num, NAME | plus_8)) # write name
477
+ self.write(self.buildRange(mode[2], num, RAW | plus_8)) # write RAW range
478
+ self.write(self.buildRange(mode[3], num, PCT | plus_8)) # write Percent range
479
+ self.write(self.buildRange(mode[4], num, SI | plus_8)) # write SI range
480
+ self.write(self.str_info(mode[5], num, SYM | plus_8)) # write symbol
481
+ self.write(
482
+ self.buildFunctMap(mode[6], num, FUNCTION_MAP | plus_8)
483
+ ) # write Function Map
484
+ self.write(self.buildFormat(mode[1], num, FMT | plus_8)) # write format
485
+
486
+ # ----- Start everything up
487
+
488
+ def connect(self):
489
+ assert len(self.modes) > 0, "No modes (commands) defined"
490
+ fast_uart_hub = False
491
+ self.init_pins()
492
+ self.wrt_tx_pin(1, 5) # Say hello!
493
+ self.wrt_tx_pin(0, 0)
494
+ for i in range(25): # Wait for AOK
495
+ n = 0
496
+ while self.rx_pin.value() == 1:
497
+ utime.sleep_ms(1)
498
+ if n > 20:
499
+ break
500
+ n += 1
501
+ if self.debug:
502
+ print(i, "falling after ms high:", n)
503
+ if i > 10 and (n > 21 or n < 16):
504
+ fast_uart_hub = True
505
+ if self.debug:
506
+ print("Fast uart handshake after drops: ",n)
507
+ break
508
+ while self.rx_pin.value() == 0:
509
+ utime.sleep_ms(1)
510
+ # Wait until rise again
511
+
512
+ if fast_uart_hub:
513
+ self.fast_uart()
514
+ utime.sleep_ms(5)
515
+ self.write(b"\x04")
516
+ else:
517
+ self.slow_uart()
518
+ self.write(b"\x00")
519
+ self.write(self.setType(self.sensor_id))
520
+ self.write(self.defineModes()) # tell how many modes
521
+ self.write(self.defineBaud(115200))
522
+ self.write(self.defineVers("0.1", __version__))
523
+ num = len(self.modes) - 1
524
+ for mode in reversed(self.modes):
525
+ utime.sleep_ms(20)
526
+ self.setupMode(mode, num)
527
+ num -= 1
528
+
529
+ self.write(b"\x04") # ACK
530
+ end = utime.ticks_ms() + 2500
531
+ while utime.ticks_ms() < end: # Wait for ack
532
+ data = self.readchar()
533
+ if data == BYTE_ACK:
534
+ self.connected = True
535
+ break
536
+
537
+ if self.connected:
538
+ self.last_nack = utime.ticks_ms()
539
+ print("\nSuccessfully connected to hub with sensor id {}".format(self.sensor_id))
540
+ if not fast_uart_hub:
541
+ self.fast_uart()
542
+ else:
543
+ print("\nFailed to connect to hub")
544
+
545
+