mxdbg 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mxdbg.py ADDED
@@ -0,0 +1,2050 @@
1
+ """
2
+ * Copyright (c) 2024 MixoSense Technology Ltd <contact@mixosense.com>.
3
+ *
4
+ * All rights are reserved.
5
+ * Proprietary and confidential.
6
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
7
+ * Any use is subject to an appropriate license granted by MixoSense Technology
8
+ * Ltd.
9
+ """
10
+
11
+ import ctypes
12
+ import os
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from types import MappingProxyType
18
+
19
+ try:
20
+ from loguru import logger
21
+ except ImportError:
22
+ print("Please install loguru: pip install loguru")
23
+ raise ImportError
24
+
25
+ try:
26
+ import toml
27
+ except ImportError:
28
+ print("Please install toml: pip install toml")
29
+ raise ImportError
30
+
31
+ try:
32
+ import serial.tools.list_ports
33
+ from serial import Serial
34
+ except ImportError:
35
+ print("Please install pyserial: pip install pyserial")
36
+ raise ImportError
37
+
38
+
39
+ class MXDBG:
40
+ def __init__(self, *args, **kwargs) -> None:
41
+ self._client = None
42
+ self.__pwm_states = [False, False, False]
43
+ self.__crc_enable = True
44
+ self.__mxdbg_header_file = None
45
+ self.__mxdbg_toml_path = None
46
+
47
+ self.version, self.task_cmd, self.__error_map = self.__parse_and_map()
48
+
49
+ __constants_gpio_mode__ = {
50
+ "GPIO_MODE_DISABLE": 0,
51
+ "GPIO_MODE_INPUT": 1,
52
+ "GPIO_MODE_OUTPUT": 2,
53
+ "GPIO_MODE_OUTPUT_OD": 6,
54
+ "GPIO_MODE_INPUT_OUTPUT_OD": 7,
55
+ "GPIO_MODE_INPUT_OUTPUT": 3,
56
+ }
57
+
58
+ self.gpio_mode = MappingProxyType(__constants_gpio_mode__)
59
+
60
+ __constants_usb_info__ = {
61
+ "USB_VID": 0x303A,
62
+ }
63
+
64
+ self.usb_info = MappingProxyType(__constants_usb_info__)
65
+
66
+ __constants_spi_common_bus_flag__ = {
67
+ "SPICOMMON_BUSFLAG_SLAVE": 0, # Initialize I/O in slave mode
68
+ "SPICOMMON_BUSFLAG_MASTER": 1 << 0, # Initialize I/O in master mode
69
+ "SPICOMMON_BUSFLAG_IOMUX_PINS": 1 << 1, # Check using iomux pins.
70
+ # Or indicates the pins are configured through the IO mux rather than GPIO matrix.
71
+ "SPICOMMON_BUSFLAG_GPIO_PINS": 1
72
+ << 2, # Force the signals to be routed through GPIO matrix.
73
+ "SPICOMMON_BUSFLAG_SCLK": 1
74
+ << 3, # Check existing of SCLK pin. Or indicates CLK line initialized.
75
+ "SPICOMMON_BUSFLAG_MISO": 1
76
+ << 4, # Check existing of MISO pin. Or indicates MISO line initialized.
77
+ "SPICOMMON_BUSFLAG_MOSI": 1
78
+ << 5, # Check existing of MOSI pin. Or indicates MOSI line initialized.
79
+ # Check MOSI and MISO pins can output. Or indicates bus able to work under DIO mode.
80
+ "SPICOMMON_BUSFLAG_DUAL": 1 << 6,
81
+ # Check existing of WP and HD pins. Or indicates WP & HD pins initialized.
82
+ "SPICOMMON_BUSFLAG_WPHD": 1 << 7,
83
+ # Check existing of MOSI and MISO pins as output. Or indicates bus able to work under QIO mode.
84
+ # Check existing of MOSI/MISO/WP/HD pins as output. Or indicates bus able to work under QIO mode.
85
+ "SPICOMMON_BUSFLAG_QUAD": 1 << 6 | 1 << 7,
86
+ # Check existing of IO4~IO7 pins. Or indicates IO4~IO7 pins initialized.
87
+ "SPICOMMON_BUSFLAG_IO4_IO7": 1 << 8,
88
+ # Check existing of MOSI/MISO/WP/HD/SPIIO4/SPIIO5/SPIIO6/SPIIO7 pins as output. Or indicates bus able to work under octal mode.
89
+ "SPICOMMON_BUSFLAG_OCTAL": 1 << 6 | 1 << 7 | 1 << 8,
90
+ "SPICOMMON_BUSFLAG_NATIVE_PINS": 1 << 1, # Check using iomux pins.
91
+ }
92
+
93
+ self.spi_common_bus_flag = MappingProxyType(__constants_spi_common_bus_flag__)
94
+
95
+ __constants_spi_device__ = {
96
+ "SPI_DEVICE_NONE": 0,
97
+ "SPI_DEVICE_TXBIT_LSBFIRST": 1
98
+ << 0, # Transmit command/address/data LSB first instead of the default MSB first
99
+ "SPI_DEVICE_RXBIT_LSBFIRST": 1
100
+ << 1, # Receive data LSB first instead of the default MSB first
101
+ "SPI_DEVICE_BIT_LSBFIRST": 1 << 0
102
+ | 1 << 1, # Transmit and receive LSB first
103
+ "SPI_DEVICE_3WIRE": 1
104
+ << 2, # Use MOSI (=spid) for both sending and receiving data
105
+ "SPI_DEVICE_POSITIVE_CS": 1
106
+ << 3, # Make CS positive during a transaction instead of negative
107
+ "SPI_DEVICE_HALFDUPLEX": 1
108
+ << 4, # Transmit data before receiving it, instead of simultaneously
109
+ "SPI_DEVICE_CLK_AS_CS": 1 << 5, # Output clock on CS line if CS is active
110
+ # There are timing issue when reading at high frequency (the frequency is related to whether iomux pins are used, valid time after slave sees the clock).
111
+ "SPI_DEVICE_NO_DUMMY": 1 << 6,
112
+ # In half-duplex mode, the driver automatically inserts dummy bits before reading phase to fix the timing issue. Set this flag to disable this feature.
113
+ "SPI_DEVICE_DDRCLK": 1 << 7,
114
+ # In full-duplex mode, however, the hardware cannot use dummy bits, so there is no way to prevent data being read from getting corrupted.
115
+ "SPI_DEVICE_NO_RETURN_RESULT": 1 << 8,
116
+ }
117
+
118
+ self.spi_device = MappingProxyType(__constants_spi_device__)
119
+
120
+ self.__all_valid_pins = [
121
+ -1,
122
+ 1,
123
+ 2,
124
+ 4,
125
+ 5,
126
+ 6,
127
+ 7,
128
+ 8,
129
+ 9,
130
+ 10,
131
+ 11,
132
+ 12,
133
+ 13,
134
+ 14,
135
+ 15,
136
+ 16,
137
+ 17,
138
+ 18,
139
+ 33,
140
+ 34,
141
+ 35,
142
+ 36,
143
+ 37,
144
+ 38,
145
+ 39,
146
+ 40,
147
+ 41,
148
+ 42,
149
+ ]
150
+ self.__pwm_used_pins = [16, 17, 18]
151
+ self.__i2c_used_pins = [10, 11]
152
+ self.__spi_used_pins = [12, 13, 14, 15]
153
+ self.__gpio_used_pins = set()
154
+
155
+ self.__gpio_valid_pins = None
156
+ self.__i2c_valid_pins = None
157
+ self.__spi_valid_pins = None
158
+ self.__pwm_valid_pins = None
159
+
160
+ self.__pca9557pw_addr = 0x18
161
+ self.__tca9555pwr_addr = 0x20
162
+ self.__extboard_version = None
163
+
164
+ self.__expand_io_init_status = False
165
+ self.__power_init_status = False
166
+ self.__expand_io_mode_bitmask = 0x0000
167
+
168
+ # TODO: auto-connect should be made optional
169
+ self.connect(**kwargs)
170
+
171
+ @property
172
+ def pwm_valid_pins(self) -> list:
173
+ if self.__pwm_valid_pins is None:
174
+ self.__pwm_valid_pins = list(
175
+ set(self.__all_valid_pins)
176
+ - set(self.__gpio_used_pins)
177
+ - set(self.__spi_used_pins)
178
+ - set(self.__i2c_used_pins)
179
+ )
180
+ return self.__pwm_valid_pins
181
+
182
+ @property
183
+ def i2c_valid_pins(self) -> list:
184
+ if self.__i2c_valid_pins is None:
185
+ self.__i2c_valid_pins = list(
186
+ set(self.__all_valid_pins)
187
+ - set(self.__pwm_used_pins)
188
+ - set(self.__spi_used_pins)
189
+ - set(self.__gpio_used_pins)
190
+ )
191
+ return self.__i2c_valid_pins
192
+
193
+ @property
194
+ def spi_valid_pins(self) -> list:
195
+ if self.__spi_valid_pins is None:
196
+ self.__spi_valid_pins = list(
197
+ set(self.__all_valid_pins)
198
+ - set(self.__pwm_used_pins)
199
+ - set(self.__i2c_used_pins)
200
+ - set(self.__gpio_used_pins)
201
+ )
202
+ return self.__spi_valid_pins
203
+
204
+ @property
205
+ def gpio_valid_pins(self) -> list:
206
+ if self.__gpio_valid_pins is None:
207
+ self.__gpio_valid_pins = list(
208
+ set(self.__all_valid_pins)
209
+ - set(self.__pwm_used_pins)
210
+ - set(self.__spi_used_pins)
211
+ - set(self.__i2c_used_pins)
212
+ )
213
+ return self.__gpio_valid_pins
214
+
215
+ def __mark_pwm_used(self, pin) -> None:
216
+
217
+ if pin == -1:
218
+ logger.warning("PWM is not supported to set pin to -1.")
219
+ return
220
+
221
+ if pin in self.pwm_valid_pins:
222
+ self.__pwm_used_pins.append(pin)
223
+ self.__pwm_valid_pins = None
224
+ else:
225
+ raise ValueError(
226
+ f"Pin {pin} is not a valid PWM pin or already used by other module."
227
+ )
228
+
229
+ def __mark_i2c_used(self, pin) -> None:
230
+
231
+ if pin == -1:
232
+ logger.warning("I2C is not supported to set pin to -1.")
233
+ return
234
+
235
+ if pin in self.i2c_valid_pins:
236
+ self.__i2c_used_pins.append(pin)
237
+ self.__i2c_valid_pins = None
238
+ else:
239
+ raise ValueError(
240
+ f"Pin {pin} is not a valid I2C pin or already used by other module."
241
+ )
242
+
243
+ def __mark_spi_used(self, pin) -> None:
244
+
245
+ if pin == -1:
246
+ return
247
+
248
+ if pin in self.spi_valid_pins:
249
+ self.__spi_used_pins.append(pin)
250
+ self.__spi_valid_pins = None
251
+ else:
252
+ raise ValueError(
253
+ f"Pin {pin} is not a valid SPI pin or already used by other module."
254
+ )
255
+
256
+ def __mark_gpio_used(self, pin) -> None:
257
+
258
+ if pin == -1:
259
+ logger.warning("GPIO is not supported to set pin to -1.")
260
+ return
261
+
262
+ if pin in self.gpio_valid_pins:
263
+ self.__gpio_used_pins.add(pin)
264
+ self.__gpio_valid_pins = None
265
+ else:
266
+ raise ValueError(
267
+ f"Pin {pin} is not a valid GPIO pin or already used by other module."
268
+ )
269
+
270
+ def mark_pin_free(self, pin) -> None:
271
+ """
272
+ @brief check all the used pins if the pin was in it, then remove it.
273
+ @param pin: pin number. `-1` is not supported.
274
+ """
275
+
276
+ if pin == -1:
277
+ logger.warning("Pin is not supported to be set as -1.")
278
+ raise ValueError("Pin is not supported to be set as -1.")
279
+
280
+ if pin in self.__gpio_used_pins:
281
+ self.__gpio_used_pins.remove(pin)
282
+ self.__gpio_valid_pins = None
283
+ elif pin in self.__spi_used_pins:
284
+ self.__spi_used_pins.remove(pin)
285
+ self.__spi_valid_pins = None
286
+ elif pin in self.__i2c_used_pins:
287
+ self.__i2c_used_pins.remove(pin)
288
+ self.__i2c_valid_pins = None
289
+ elif pin in self.__pwm_used_pins:
290
+ self.__pwm_used_pins.remove(pin)
291
+ self.__pwm_valid_pins = None
292
+ else:
293
+ raise ValueError(
294
+ f"Pin {pin} is not used by any module. Or it is not a valid pin."
295
+ )
296
+
297
+ def __find_ports(self) -> list[str]:
298
+ """按 USB_VID 探测所有 ESP32 串口(如 ['/dev/ttyACM0']),无则返回空列表。"""
299
+ ports = []
300
+ for port in sorted(serial.tools.list_ports.comports()):
301
+ if port.vid is None or port.pid is None:
302
+ continue
303
+ if self.usb_info["USB_VID"] == port.vid:
304
+ ports.append(port.name)
305
+ return ports
306
+
307
+ def __find_port(self) -> str | None:
308
+ """按 USB_VID 探测 ESP32 串口,取第一个;未找到返回 None。"""
309
+ ports = self.__find_ports()
310
+ return ports[0] if ports else None
311
+
312
+ def connect(self, port: str | None = None, **kwargs) -> None:
313
+ """连接 ESP32。
314
+
315
+ @param port: 串口(如 '/dev/ttyACM0' / 'COM3');None 时按 USB_VID=0x303A 自动探测。
316
+ 显式指定端口可支持 CH340/FT232/CP2102 等 USB 转串口方案及多设备场景。
317
+ """
318
+ _port = port
319
+ retry_times = 10
320
+ while True:
321
+ if _port is None:
322
+ _port = self.__find_port()
323
+
324
+ if _port is not None:
325
+ break
326
+ else:
327
+ retry_times -= 1
328
+ time.sleep(1)
329
+
330
+ if retry_times == 0:
331
+ raise ValueError("No device found.")
332
+
333
+ try:
334
+ if sys.platform != "win32" and not _port.startswith("/"):
335
+ _port = f"/dev/{_port}"
336
+
337
+ # use serial port to connect
338
+ self._client = Serial(_port, 115200, timeout=5, write_timeout=1, **kwargs)
339
+ logger.info(f"Using serial port to connect. Port: {_port}")
340
+
341
+ except Exception as e:
342
+ raise ValueError(f"Failed to connect: {e}")
343
+
344
+ logger.info(
345
+ "Using ESP32-S3R8. Software version: v{}.{}".format(
346
+ self.version["MAJOR"], self.version["MINOR"]
347
+ )
348
+ )
349
+
350
+ def disconnect(self) -> None:
351
+ self._client.close()
352
+
353
+ def flash_firmware(
354
+ self, port: str | None = None, confirm: bool = True
355
+ ) -> tuple[bool, str]:
356
+ """烧录包内固件到 ESP32(无需本地安装 ESP-IDF)。
357
+
358
+ @param port: 串口(如 '/dev/ttyACM0');None 时按 USB_VID=0x303A 自动探测,
359
+ 检测到多个设备时会交互式询问选择
360
+ @param confirm: 烧录前交互确认(默认 True);脚本化场景可传 False 跳过
361
+ @return: (是否成功, esptool 输出或错误信息)
362
+ @note: 烧录前会释放当前占用的串口;烧录后设备复位,需重新创建 MXDBG() 实例
363
+ """
364
+ # 释放已占用的串口(esptool 需要独占端口)
365
+ if getattr(self, "_client", None) is not None and self._client.is_open:
366
+ self._client.close()
367
+
368
+ fw_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "firmware")
369
+ firmware = {
370
+ name: os.path.join(fw_dir, name)
371
+ for name in ("bootloader.bin", "partition-table.bin", "mxdbg.bin")
372
+ }
373
+ for path in firmware.values():
374
+ if not os.path.exists(path):
375
+ return (
376
+ False,
377
+ (
378
+ f"firmware not bundled (missing {os.path.basename(path)}). "
379
+ "Reinstall mxdbg with firmware included."
380
+ ),
381
+ )
382
+
383
+ if port is None:
384
+ # 多设备时交互式选择
385
+ found = self.__select_port()
386
+ if found is None:
387
+ return (
388
+ False,
389
+ (
390
+ "No device found. Connect ESP32 and check serial "
391
+ "permissions (Linux: add user to dialout group)."
392
+ ),
393
+ )
394
+ port = found
395
+ elif not re.fullmatch(r"[A-Za-z0-9_/.-]+", port):
396
+ return False, f"invalid port: {port!r}"
397
+
398
+ if confirm and not self.__confirm_flash(port):
399
+ return False, "flashing cancelled by user."
400
+
401
+ return self.__esptool_flash(port, firmware)
402
+
403
+ def __select_port(self) -> str | None:
404
+ """探测 ESP32 设备;多个时交互式询问选择,返回完整端口路径或 None。"""
405
+ ports = self.__find_ports()
406
+ if not ports:
407
+ return None
408
+ if sys.platform != "win32":
409
+ ports = [f"/dev/{p}" for p in ports]
410
+ if len(ports) == 1:
411
+ return ports[0]
412
+
413
+ print("检测到多个 ESP32 设备:")
414
+ for i, p in enumerate(ports):
415
+ print(f" [{i}] {p}")
416
+ try:
417
+ choice = input(
418
+ f"请选择要烧录的设备 [0-{len(ports) - 1}] (默认 0): "
419
+ ).strip()
420
+ except EOFError:
421
+ return ports[0]
422
+ if choice == "":
423
+ return ports[0]
424
+ try:
425
+ idx = int(choice)
426
+ except ValueError:
427
+ return None
428
+ return ports[idx] if 0 <= idx < len(ports) else None
429
+
430
+ def __confirm_flash(self, port: str) -> bool:
431
+ """烧录前交互确认。"""
432
+ try:
433
+ answer = input(f"确认烧录固件到 {port}? [y/N] ").strip().lower()
434
+ except EOFError:
435
+ return False
436
+ return answer in ("y", "yes")
437
+
438
+ def __esptool_flash(self, port: str, firmware: dict[str, str]) -> tuple[bool, str]:
439
+ """调用 esptool 烧录固件;烧录参数与构建配置一致(build/flasher_args.json):dio / 2MB / 80m。"""
440
+ cmd = [
441
+ sys.executable,
442
+ "-m",
443
+ "esptool",
444
+ "--chip",
445
+ "esp32s3",
446
+ "-p",
447
+ port,
448
+ "--before",
449
+ "default_reset",
450
+ "--after",
451
+ "hard_reset",
452
+ "write_flash",
453
+ "--flash_mode",
454
+ "dio",
455
+ "--flash_size",
456
+ "2MB",
457
+ "--flash_freq",
458
+ "80m",
459
+ "0x0",
460
+ firmware["bootloader.bin"],
461
+ "0x8000",
462
+ firmware["partition-table.bin"],
463
+ "0x10000",
464
+ firmware["mxdbg.bin"],
465
+ ]
466
+ try:
467
+ result = subprocess.run(
468
+ cmd, capture_output=True, text=True, timeout=300, check=False
469
+ )
470
+ except FileNotFoundError:
471
+ return False, "esptool not installed. Run: pip install esptool"
472
+ except subprocess.TimeoutExpired:
473
+ return (
474
+ False,
475
+ (
476
+ "esptool timed out. If using USB-JTAG, hold BOOT + press RST "
477
+ "to enter download mode."
478
+ ),
479
+ )
480
+
481
+ output = (result.stdout + result.stderr).strip()
482
+ if result.returncode != 0:
483
+ hint = ""
484
+ if "Failed to connect" in output:
485
+ hint = " (connect failed: hold BOOT + press RST to enter download mode)"
486
+ return False, output + hint
487
+ return True, output or "Flash OK"
488
+
489
+ def __read(self, timeout=2) -> bytearray:
490
+
491
+ data = bytearray()
492
+ start_time = time.time()
493
+
494
+ while True:
495
+ # 检查超时
496
+ if time.time() - start_time > timeout:
497
+ if len(data) == 0:
498
+ raise TimeoutError("Data read timeout. No data received.")
499
+ else:
500
+ raise TimeoutError(f"Data read timeout. Data received: {data}")
501
+
502
+ if self._client.in_waiting > 0:
503
+ temp_data = self._client.read(self._client.in_waiting)
504
+ data += temp_data
505
+
506
+ if (
507
+ len(data) >= 5
508
+ and self.__check_crc(data)
509
+ and data[:5] == bytearray("mxdbg", "utf-8")
510
+ ):
511
+ break
512
+
513
+ time.sleep(0.01)
514
+
515
+ return data
516
+
517
+ def __write(self, data) -> None:
518
+ self._client.write(data)
519
+
520
+ def __task_execute(self, cmd, data: list) -> tuple:
521
+
522
+ write_data = self.__data_pack(cmd, data)
523
+
524
+ try:
525
+ self.__write(write_data)
526
+ except Exception as e:
527
+ raise ValueError(f"Failed to write data: {e}")
528
+
529
+ read_data = self.__read()
530
+
531
+ ret, temp_data = self.__data_unpack(cmd, read_data)
532
+
533
+ return ret, temp_data
534
+
535
+ def __data_decompose(self, data: int, bytes_num: int = 4) -> tuple:
536
+ return [(data >> (8 * (bytes_num - 1 - i))) & 0xFF for i in range(bytes_num)]
537
+
538
+ def __hexdump(self, data: bytearray, base_address=0x3FC9900C) -> None:
539
+ logger.debug("hex:")
540
+
541
+ # 按行分组数据,每行16字节
542
+ for i in range(0, len(data), 16):
543
+ # 显示16字节的 hex 值,每8字节后加一个 '\t'
544
+ line_data = data[i : i + 16]
545
+ for j, byte in enumerate(line_data):
546
+ print(f"{byte:02X} ", end="")
547
+ if (j + 1) % 8 == 0:
548
+ print("\t", end="")
549
+
550
+ # 计算并显示 ASCII 表示,非可见字符替换为 '.'
551
+ ascii_repr = "".join(chr(b) if 32 <= b <= 126 else "." for b in line_data)
552
+ print(f"|{ascii_repr}|")
553
+
554
+ def __calculate_crc(self, data: bytearray) -> bytearray:
555
+ """
556
+ @brief Calculate CRC16 for the given data.
557
+ @param data: Data to calculate CRC16.
558
+ @return: CRC16 value.
559
+ """
560
+
561
+ crc = 0xFFFF
562
+
563
+ for byte in data:
564
+ crc ^= byte
565
+ for _ in range(8):
566
+ crc = (crc >> 1) ^ (0xA001 if (crc & 1) else 0)
567
+
568
+ high, low = ((crc >> 8) & 0xFF), (crc & 0xFF)
569
+
570
+ return bytearray([high, low])
571
+
572
+ def __check_crc(self, data: bytearray) -> bool:
573
+ """
574
+ @brief Check CRC16 for the given data.
575
+ @param data: Data to check CRC16.
576
+ @return: True if CRC16 is correct, False otherwise.
577
+ """
578
+
579
+ if not self.__crc_enable:
580
+ return True
581
+
582
+ high, low = self.__calculate_crc(data[:-2])
583
+
584
+ # logger.debug(f"CRC Check: {high}, {low}, {data[-2]}, {data[-1]}")
585
+
586
+ return bool(high == data[-2] and low == data[-1])
587
+
588
+ def __data_pack(self, cmd: int, data: list) -> bytearray:
589
+ """
590
+ @brief Package data with header and CRC16.
591
+ @param data: Data to package.
592
+ """
593
+
594
+ # data sent format
595
+ # | 5 bytes | 1 byte | 1 byte | 1 byte | n bytes | 1 byte | 2 bytes |
596
+ # |---------|--------|--------|--------|---------|--------|---------|
597
+ # | mxdbg | : | cmd | : | data | : | crc |
598
+
599
+ temp_data = bytearray("mxdbg:", "utf-8")
600
+ temp_data += bytearray([cmd])
601
+ temp_data += bytearray(":", "utf-8")
602
+ temp_data += bytearray(data)
603
+ temp_data += bytearray(":", "utf-8")
604
+
605
+ high, low = self.__calculate_crc(temp_data)
606
+ temp_data += bytearray([high, low])
607
+
608
+ return temp_data
609
+
610
+ def __data_unpack(self, cmd, data: bytearray) -> tuple:
611
+ """
612
+ @brief Unpackage data.
613
+ @param data: Data to unpackage.
614
+ @return: cmd and data.
615
+ """
616
+
617
+ # data received format
618
+ # | 5 bytes | 1 byte | 1 byte | 1 byte | 4 bytes | 1 byte | n bytes | 1 byte | 2 bytes |
619
+ # |---------|--------|--------|--------|---------|--------|---------|--------|---------|
620
+ # | mxdbg | : | cmd | : | ret | : | data | : | crc |
621
+
622
+ # logger.debug("Data: {}".format(["{:02X}".format(d) for d in list(data)]))
623
+
624
+ # check crc first
625
+ temp_data = data
626
+ if not self.__check_crc(temp_data):
627
+ raise ValueError("CRC check failed.")
628
+
629
+ # check header
630
+ header = data[:5]
631
+ if header != bytearray("mxdbg", "utf-8"):
632
+ raise ValueError("Invalid header.")
633
+
634
+ # check cmd
635
+ cmd_ = data[6]
636
+ if cmd_ != cmd:
637
+ raise ValueError("Invalid cmd.")
638
+
639
+ ret = (
640
+ data[8:12][0] << 24
641
+ | data[8:12][1] << 16
642
+ | data[8:12][2] << 8
643
+ | data[8:12][3]
644
+ )
645
+ ret = ctypes.c_int32(ret).value
646
+
647
+ if len(data) > 15: # data exists
648
+ temp_data = data[13:-3]
649
+ else:
650
+ temp_data = None
651
+
652
+ return ret, temp_data
653
+
654
+ def __parse_version(self, lines) -> tuple[int, int]:
655
+ """Parse VERSION_MAJOR and VERSION_MINOR from header file."""
656
+ try:
657
+ major_pattern = re.compile(r"#define\s+VERSION_MAJOR\s+(\d+)")
658
+ minor_pattern = re.compile(r"#define\s+VERSION_MINOR\s+(\d+)")
659
+
660
+ for line in lines:
661
+ major_match = major_pattern.search(line)
662
+ if major_match:
663
+ version_major = int(major_match.group(1))
664
+
665
+ minor_match = minor_pattern.search(line)
666
+ if minor_match:
667
+ version_minor = int(minor_match.group(1))
668
+ except Exception as e:
669
+ raise ValueError(f"Error parsing version: {e}")
670
+
671
+ return version_major, version_minor
672
+
673
+ def __parse_task_cmd(self, lines) -> dict:
674
+ """Parse TASK commands from header file."""
675
+ task_cmd = {}
676
+ try:
677
+ cmd_pattern = re.compile(r"TASK_(\w+)\s*=\s*(0x[0-9A-Fa-f]+|\d+),?")
678
+ for line in lines:
679
+ match = cmd_pattern.search(line)
680
+ if match:
681
+ cmd_name = f"TASK_{match.group(1)}"
682
+ cmd_id = int(match.group(2), 0)
683
+ task_cmd[cmd_name] = cmd_id
684
+ except Exception as e:
685
+ raise ValueError(f"Error parsing task commands: {e}")
686
+ return task_cmd
687
+
688
+ def __parse_error_map(self, lines, task_cmd, version_major, version_minor) -> dict:
689
+ """Parse error codes from header file, including MXDBG_ERR and ESPRESSIF_ERR."""
690
+ error_map = {}
691
+ try:
692
+ # Regular expression to match ERR_SYNTHESIS with parameters
693
+ err_synthesis_pattern = re.compile(
694
+ r"#define\s+(MXDBG_ERR_\w+)\s+ERR_SYNTHESIS\((.+?)\)\s*/\*!\s*(.+?)\s*\*/"
695
+ )
696
+
697
+ # Regular expression to match simpler error patterns (e.g., ESPRESSIF_ERR)
698
+ simple_error_pattern = re.compile(
699
+ r"#define\s+(ESPRESSIF_ERR_\w+|ESPRESSIF_[A-Z_]+)\s+(0x[0-9A-Fa-f]+|\d+)\s*/\*!\s*(.+?)\s*\*/"
700
+ )
701
+
702
+ # Parse ERR_SYNTHESIS-based errors
703
+ for line in lines:
704
+ match = err_synthesis_pattern.search(line)
705
+ if match:
706
+ error_name = match.group(1) # e.g., "MXDBG_ERR_SPI_CONFIG_FAILED"
707
+ synthesis_args = match.group(2) # e.g., "TASK_SPI_CONFIG, 0x00"
708
+ error_desc = match.group(3).strip()
709
+
710
+ # Parse ERR_SYNTHESIS arguments
711
+ args = synthesis_args.split(",")
712
+ if len(args) == 2:
713
+ task_name = args[0].strip()
714
+ ret_code = int(args[1].strip(), 0)
715
+
716
+ # Use numeric literal directly, otherwise look up in task_cmd
717
+ task_id = (
718
+ int(task_name, 0)
719
+ if task_name.startswith("0x")
720
+ else task_cmd.get(task_name, 0xFF)
721
+ )
722
+
723
+ # Compute error code based on ERR_SYNTHESIS logic
724
+ version_combined = (version_major << 24) | (version_minor << 16)
725
+ error_code = version_combined | (task_id << 8) | ret_code
726
+
727
+ error_map[error_name] = {
728
+ "code": ctypes.c_int32(error_code).value,
729
+ "desc": error_desc.replace("< ", ""),
730
+ }
731
+ continue
732
+
733
+ # Parse simpler error patterns (ESPRESSIF_ERR)
734
+ match = simple_error_pattern.search(line)
735
+ if match:
736
+ error_name = match.group(1)
737
+ error_code = int(match.group(2), 0)
738
+ error_desc = match.group(3).strip()
739
+ # error_map[error_name] = (ctypes.c_int32(error_code).value, error_desc.replace('< ', ''))
740
+ error_map[error_name] = {
741
+ "code": ctypes.c_int32(error_code).value,
742
+ "desc": error_desc.replace("< ", ""),
743
+ }
744
+
745
+ except Exception as e:
746
+ raise ValueError(f"Error parsing error map: {e}")
747
+
748
+ return error_map
749
+
750
+ def __compute_error_code(
751
+ self, version_major, version_minor, task_id, ret_code
752
+ ) -> int:
753
+ """Compute the error code based on task_id and ret_code."""
754
+ return (version_major << 24) | (version_minor << 16) | (task_id << 8) | ret_code
755
+
756
+ def __save_to_toml(self, path, version, task_cmd, error_map) -> None:
757
+ """Save parsed data to TOML file with additional annotations for version."""
758
+ try:
759
+ data = {"version": version, "task_cmd": task_cmd, "error_map": error_map}
760
+
761
+ with open(path, "w") as f:
762
+ toml.dump(data, f)
763
+
764
+ except Exception as e:
765
+ raise ValueError(f"Failed to save mxdbg.toml: {e}")
766
+
767
+ def __parse_and_map(self) -> tuple:
768
+
769
+ version = 0x00
770
+ task_cmd = {}
771
+ error_map = {}
772
+
773
+ header_file = os.path.join(os.path.dirname(__file__), "../main/mxdbg.h")
774
+ if os.path.exists(header_file):
775
+ self.__mxdbg_header_file = header_file
776
+ self.__mxdbg_toml_path = os.path.join(
777
+ os.path.dirname(__file__), "mxdbg.toml"
778
+ )
779
+
780
+ else:
781
+ for f in os.listdir(os.path.dirname(__file__)):
782
+ if f.endswith(".toml"):
783
+ if f == "mxdbg.toml":
784
+ self.__mxdbg_toml_path = os.path.join(
785
+ os.path.dirname(__file__), f
786
+ )
787
+ break
788
+ elif f.endswith(".h") and f == "mxdbg.h":
789
+ self.__mxdbg_header_file = os.path.join(
790
+ os.path.dirname(__file__), f
791
+ )
792
+ break
793
+
794
+ if self.__mxdbg_toml_path is None and self.__mxdbg_header_file is None:
795
+ raise FileNotFoundError(
796
+ "Please provide mxdbg.toml or mxdbg.h file to parse."
797
+ )
798
+
799
+ if self.__mxdbg_header_file:
800
+ try:
801
+ with open(self.__mxdbg_header_file, "r") as f:
802
+ lines = f.readlines()
803
+
804
+ version_major, version_minor = self.__parse_version(lines)
805
+ version = {"MAJOR": version_major, "MINOR": version_minor}
806
+
807
+ task_cmd = self.__parse_task_cmd(lines)
808
+
809
+ error_map = self.__parse_error_map(
810
+ lines, task_cmd, version_major, version_minor
811
+ )
812
+
813
+ self.__save_to_toml(
814
+ self.__mxdbg_toml_path, version, task_cmd, error_map
815
+ )
816
+
817
+ except Exception as e:
818
+ raise ValueError(f"Failed to parse error map: {e}")
819
+
820
+ else:
821
+ try:
822
+ with open(self.__mxdbg_toml_path, "r") as f:
823
+ data = toml.load(f)
824
+
825
+ version = data.get("version", version)
826
+ task_cmd = data.get("task_cmd", task_cmd)
827
+ error_map = data.get("error_map", error_map)
828
+
829
+ except Exception as e:
830
+ raise ValueError(f"Failed to load mxdbg.toml: {e}")
831
+
832
+ return version, task_cmd, error_map
833
+
834
+ def __check_ret_code(self, cmd: int, ret: int) -> None:
835
+
836
+ if ret != 0:
837
+ error_code = self.__compute_error_code(
838
+ self.version["MAJOR"], self.version["MINOR"], cmd, ret
839
+ )
840
+ error_desc = "Unknown error."
841
+ for key in self.__error_map:
842
+ if self.__error_map[key]["code"] == error_code:
843
+ error_desc = self.__error_map[key]["desc"]
844
+ break
845
+ logger.error(
846
+ f"Error code: 0x{ctypes.c_uint32(error_code).value:08X}, Description: {error_desc}"
847
+ )
848
+
849
+ def i2c_find_slave(self, port: int = 0) -> tuple[bool, list | None]:
850
+ """
851
+ @brief Find I2C slave devices.
852
+ @param port: I2C port number. Default is `0`.
853
+ @return: List of I2C slave devices.
854
+ """
855
+
856
+ found_device_list = []
857
+ for slave_id in range(0x04, 0x7F):
858
+ try:
859
+ ret, _data = self.i2c_write_read(slave_id, [0x00], 0, port=port)
860
+ if ret:
861
+ found_device_list.append(slave_id)
862
+ except Exception:
863
+ continue
864
+
865
+ if found_device_list != []:
866
+ found_device_list = [f"0x{slave_id:02X}" for slave_id in found_device_list]
867
+ return True, found_device_list
868
+ else:
869
+ return False, None
870
+
871
+ def i2c_write_read(
872
+ self,
873
+ slave_id: int,
874
+ write_list: list,
875
+ read_length: int,
876
+ port: int = 0,
877
+ slave_id_10_bit: bool = False,
878
+ ) -> tuple[bool, list | None]:
879
+ """
880
+ @brief Write and read data from I2C slave device. The default I2C pin is SDA: `10`, SCL: `11`.
881
+ @param slave_id: I2C slave device address. (e.g., `0x04`.)
882
+ @param write_list: Data to write to I2C slave device. (It should be a list of bytes. e.g., `[0x00, 0x01]` or `[]`.)
883
+ @param read_length: Length of data to read from I2C slave device.
884
+ @param port: I2C port number.( It should be `0` (in default) or `1`.)
885
+ @param slave_id_10_bit: True if 10-bit slave address, False otherwise.
886
+ @return: Data read from I2C slave device.
887
+ """
888
+
889
+ if port not in [0, 1]:
890
+ logger.error("Invalid port number, the available port number is 0 or 1.")
891
+ raise ValueError(
892
+ "Invalid port number, the available port number is 0 or 1."
893
+ )
894
+
895
+ if slave_id < 0x04 or slave_id > 0x07FF:
896
+ logger.error(
897
+ "Invalid slave, the available slave id is between 0x04 and 0x07FF."
898
+ )
899
+ raise ValueError(
900
+ "Invalid slave, the available slave id is between 0x04 and 0x07FF."
901
+ )
902
+
903
+ # 构造要发送的数据包
904
+ slave_id = slave_id if not slave_id_10_bit else slave_id & 0x07FF
905
+ write_len = len(write_list)
906
+ read_len = read_length
907
+
908
+ if write_len <= 0 and read_len <= 0:
909
+ logger.error("Write length and read length should be greater than 0.")
910
+ raise ValueError("Write length and read length should be greater than 0.")
911
+
912
+ """
913
+ * value format received:
914
+ * | 5 bytes | 1 byte | 1 byte | 1 byte | 1 byte | 2 bytes | 4 bytes | 4 bytes | n bytes | 1 byte | 2 bytes |
915
+ * |---------|--------|--------|--------|----------|----------|--------------|-------------|-----------------|--------|---------|
916
+ * | mxdbg | : | cmd | : | i2c_port | slave_id | write_length | read_length | write_data_list | : | crc |
917
+ """
918
+
919
+ i2c_data_temp = [
920
+ port,
921
+ (slave_id & 0xFF00) >> 8,
922
+ slave_id & 0x00FF,
923
+ (write_len & 0xFF000000) >> 24,
924
+ (write_len & 0x00FF0000) >> 16,
925
+ (write_len & 0x0000FF00) >> 8,
926
+ write_len & 0x000000FF,
927
+ (read_len & 0xFF000000) >> 24,
928
+ (read_len & 0x00FF0000) >> 16,
929
+ (read_len & 0x0000FF00) >> 8,
930
+ read_len & 0x000000FF,
931
+ ]
932
+ i2c_data_temp += write_list
933
+
934
+ # 执行任务并读取返回的数据
935
+ ret, data = self.__task_execute(
936
+ self.task_cmd["TASK_I2C_WRITE_READ"], i2c_data_temp
937
+ )
938
+
939
+ data = list(data) if data is not None else None
940
+
941
+ if ret != 0:
942
+ # logger.error(f"Error: {ret}")
943
+ return False, None
944
+ else:
945
+ return True, data
946
+
947
+ def i2c_config(
948
+ self,
949
+ port: int = 0,
950
+ freq: int = 400000,
951
+ sda_pin: int = 10,
952
+ scl_pin: int = 11,
953
+ sda_pullup: bool = True,
954
+ scl_pullup: bool = True,
955
+ ) -> bool | tuple[bool, None]:
956
+ """
957
+ @brief Configure I2C port. ***Please do not modify the configuration of PORT1***.
958
+ @param port: I2C port number.( It should be `0` (in default) or `1`.)
959
+ @param freq: I2C frequency. (Default is `400000`.)
960
+ @param sda_pin: SDA pin number. (Default is `10`.)
961
+ @param scl_pin: SCL pin number. (Default is `11`.)
962
+ @param sda_pullup: SDA pull up resistor. (`True`: enable, `False`: disable.)
963
+ @param scl_pullup: SCL pull up resistor. (`True`: enable, `False`: disable.)
964
+ @return: Return True if success, False otherwise.
965
+ """
966
+
967
+ if port not in [0, 1]:
968
+ logger.error("Invalid port number.")
969
+ return False
970
+
971
+ self.__i2c_valid_pins = None
972
+
973
+ if (sda_pin not in self.i2c_valid_pins) or (scl_pin not in self.i2c_valid_pins):
974
+ logger.error("Invalid pin number.")
975
+ return False, None
976
+ elif sda_pin in self.__i2c_used_pins and scl_pin in self.__i2c_used_pins:
977
+ pass
978
+
979
+ if sda_pin == scl_pin:
980
+ logger.error("SDA and SCL pins should be different.")
981
+ return False, None
982
+
983
+ port = ctypes.c_uint8(port).value
984
+ freq = ctypes.c_uint32(freq).value
985
+ sda_pin = ctypes.c_uint8(sda_pin).value
986
+ scl_pin = ctypes.c_uint8(scl_pin).value
987
+ sda_pullup = 0xF if sda_pullup else 0x0
988
+ scl_pullup = 0xF if scl_pullup else 0x0
989
+
990
+ i2c_data_temp = [
991
+ port,
992
+ (freq & 0xFF000000) >> 24,
993
+ (freq & 0x00FF0000) >> 16,
994
+ (freq & 0x0000FF00) >> 8,
995
+ freq & 0x000000FF,
996
+ sda_pin,
997
+ scl_pin,
998
+ (sda_pullup << 4 | scl_pullup),
999
+ ]
1000
+
1001
+ ret, _data = self.__task_execute(
1002
+ self.task_cmd["TASK_I2C_CONFIG"], i2c_data_temp
1003
+ )
1004
+
1005
+ if ret != 0:
1006
+ logger.error(f"Error: {ret}")
1007
+ return False, None
1008
+ else:
1009
+ for pin in self.__i2c_used_pins:
1010
+ self.mark_pin_free(pin)
1011
+ for pin in [sda_pin, scl_pin]:
1012
+ self.__mark_i2c_used(pin)
1013
+ return True, None
1014
+
1015
+ def gpio_write_read(
1016
+ self, pin: int, level: int | None = None
1017
+ ) -> tuple[bool, int | None]:
1018
+ """
1019
+ @brief Write or read GPIO pin. This API will read GPIO's level if you set `level` to `None`, otherwise it will be in write mode.
1020
+ @param pin: GPIO pin number.
1021
+ @param level: GPIO level. (`0`: Low, `1`: High. `None` in default.)
1022
+ @return: Return True and data if success, False otherwise.
1023
+ """
1024
+
1025
+ if pin not in self.gpio_valid_pins:
1026
+ logger.error("Invalid pin number.")
1027
+ return False, None
1028
+
1029
+ pin = ctypes.c_uint8(pin).value
1030
+
1031
+ if level is None: # read mode
1032
+ operation = 1
1033
+ gpio_data_temp = [operation, pin, 0]
1034
+ else: # write mode
1035
+ operation = 0
1036
+ gpio_data_temp = [operation, pin, ((ctypes.c_uint8(level).value) & 0x01)]
1037
+
1038
+ ret, data = self.__task_execute(
1039
+ self.task_cmd["TASK_GPIO_WRITE_READ"], gpio_data_temp
1040
+ )
1041
+
1042
+ if ret == 0:
1043
+ return True, None if (operation == 0) else data[0]
1044
+ else:
1045
+ logger.error(f"Error: {ret}")
1046
+ return False, None
1047
+
1048
+ def gpio_config(self, pin: int, mode: int, pull_up: bool, pull_down: bool) -> bool:
1049
+ """
1050
+ @brief Configure GPIO pin.
1051
+ @param pin: GPIO pin number.
1052
+ @param mode: GPIO mode. (`0x00`: Disable, `0x01`: Input, `0x02`: Output, `0x03`: Input/Output, `0x06`: Output Open Drain, `0x07`: Input/Output Open Drain)
1053
+ @param pull_up: Pull up resistor. (`True`: Enable, `False`: Disable.)
1054
+ @param pull_down: Pull down resistor. (`True`: Enable, `False`: Disable.)
1055
+ @return: Return value. 0: Success, other: Error.
1056
+ """
1057
+
1058
+ self.__gpio_valid_pins = None
1059
+
1060
+ if pin not in self.gpio_valid_pins:
1061
+ logger.error("Invalid pin number.")
1062
+ raise ValueError("Invalid pin number.")
1063
+
1064
+ # assert mode in self.gpio_mode, "Invalid mode."
1065
+ if mode not in [v for k, v in self.gpio_mode.items()]:
1066
+ logger.error("Invalid mode.")
1067
+ raise ValueError("Invalid mode.")
1068
+
1069
+ pin = ctypes.c_uint8(pin).value
1070
+ mode = ctypes.c_uint8(mode).value
1071
+ pull_up = ctypes.c_uint8(0x01 if pull_up else 0x00).value
1072
+ pull_down = ctypes.c_uint8(0x01 if pull_down else 0x00).value
1073
+
1074
+ gpio_data_temp = [pin, mode, pull_up, pull_down]
1075
+ ret, _data = self.__task_execute(
1076
+ self.task_cmd["TASK_GPIO_CONFIG"], gpio_data_temp
1077
+ )
1078
+
1079
+ if ret != 0:
1080
+ logger.error(f"Error: {ret}")
1081
+ return False
1082
+ else:
1083
+ self.__mark_gpio_used(pin)
1084
+ return True
1085
+
1086
+ def spi_write_read(
1087
+ self, write_list: list, read_length: int
1088
+ ) -> tuple[bool, list | None]:
1089
+ """
1090
+ @brief Write and read data from SPI slave device. (The default SPI pins are MISO: `12`, MOSI: `13`, SCLK: `14`, CS: `15`.)
1091
+ @param write_list: Data to write to SPI slave device. (It should be a list of bytes. e.g., `[0x00, 0x01]` or `[]`.)
1092
+ @param read_length: Length of data to read from SPI slave device.
1093
+ @return: Data read from SPI slave device.
1094
+ """
1095
+
1096
+ write_len = ctypes.c_uint32(len(write_list)).value
1097
+ read_len = ctypes.c_uint32(read_length).value
1098
+
1099
+ if (write_len == 0) and (read_len == 0):
1100
+ logger.error("Write length and read length should not be zero.")
1101
+ return False, None
1102
+ else:
1103
+ if read_len > write_len:
1104
+ logger.error("Read length should not be larger than write length.")
1105
+ return False, None
1106
+
1107
+ spi_data_temp = [
1108
+ (write_len & 0xFF000000) >> 24,
1109
+ (write_len & 0x00FF0000) >> 16,
1110
+ (write_len & 0x0000FF00) >> 8,
1111
+ (write_len & 0x000000FF) >> 0,
1112
+ (read_len & 0xFF000000) >> 24,
1113
+ (read_len & 0x00FF0000) >> 16,
1114
+ (read_len & 0x0000FF00) >> 8,
1115
+ (read_len & 0x000000FF) >> 0,
1116
+ ]
1117
+ spi_data_temp += write_list
1118
+
1119
+ ret, data = self.__task_execute(
1120
+ self.task_cmd["TASK_SPI_WRITE_READ"], spi_data_temp
1121
+ )
1122
+
1123
+ self.__check_ret_code(self.task_cmd["TASK_SPI_WRITE_READ"], ret)
1124
+
1125
+ if ret == 0:
1126
+ if read_len == 0:
1127
+ return True, None
1128
+ else:
1129
+ return True, list(data)
1130
+ else:
1131
+ return False, None
1132
+
1133
+ # TODO: refactor 25+ parameters into a dataclass (小接口 principle)
1134
+ def spi_config(
1135
+ self,
1136
+ miso_io_num: int = 12,
1137
+ mosi_io_num: int = 13,
1138
+ sclk_io_num: int = 14,
1139
+ cs_io_num: int = 15,
1140
+ common_bus_flags: int = 0,
1141
+ freq: int = 1000000,
1142
+ mode: int = 3,
1143
+ quadwp_io_num: int = -1,
1144
+ quadhd_io_num: int = -1,
1145
+ data4_io_num: int = -1,
1146
+ data5_io_num: int = -1,
1147
+ data6_io_num: int = -1,
1148
+ data7_io_num: int = -1,
1149
+ max_transfer_sz: int = 4096,
1150
+ isr_cpu_id: int = 0,
1151
+ intr_flags: int = 0,
1152
+ command_bits: int = 0,
1153
+ address_bits: int = 0,
1154
+ dummy_bits: int = 0,
1155
+ duty_cycle_pos: int = 0,
1156
+ cs_ena_pretrans: int = 0,
1157
+ cs_ena_posttrans: int = 0,
1158
+ input_delay_ns: int = 0,
1159
+ device_interface_flags: int = 0,
1160
+ queue_size: int = 7,
1161
+ ) -> tuple[bool, None]:
1162
+ """
1163
+ @brief Configure SPI.
1164
+ @param miso_io_num: MISO pin number. (Default is `12`. Set to `-1` if not used.)
1165
+ @param mosi_io_num: MOSI pin number. (Default is `13`.)
1166
+ @param sclk_io_num: SCLK pin number.( Default is `14`.)
1167
+ @param cs_io_num: CS pin number. (Default is 15. Set to `-1` if not used.)
1168
+ @param quadwp_io_num: QUADWP pin number. (Default is `-1`.)
1169
+ @param quadhd_io_num: QUADHD pin number. (Default is `-1`.)
1170
+ @param data4_io_num: DATA4 pin number. (Default is `-1`.)
1171
+ @param data5_io_num: DATA5 pin number. (Default is `-1`.)
1172
+ @param data6_io_num: DATA6 pin number. (Default is `-1`.)
1173
+ @param data7_io_num: DATA7 pin number. (Default is `-1`.)
1174
+ @param max_transfer_sz: Maximum transfer size. (Default is `4096`.)
1175
+ @param common_bus_flags: Common bus flags. (Default is `0`.)
1176
+ @param isr_cpu_id: ISR CPU ID. (Default is `0`.)
1177
+ @param intr_flags: Interrupt flags. (Default is `0`.)
1178
+ @param command_bits: Command bits. (Default is `0`.)
1179
+ @param address_bits: Address bits. (Default is `0`.)
1180
+ @param dummy_bits: Dummy bits. (Default is `0`.)
1181
+ @param mode: SPI mode. (Default is `3`. Available values are `0`, `1`, `2`, `3`.)
1182
+ @param duty_cycle_pos: Duty cycle position. (Default is `0`.)
1183
+ @param cs_ena_pretrans: CS enable pretrans. (Default is `0`.)
1184
+ @param cs_ena_posttrans: CS enable posttrans. (Default is `0`.)
1185
+ @param freq: SPI frequency. (Default is `1000000`.)
1186
+ @param input_delay_ns: Input delay in ns. (Default is `0`.)
1187
+ @param device_interface_flags: Device interface flags. (Default is `0`. Set `(SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE)` if 3-wired half duplex mode.
1188
+ @param queue_size: Queue size. Default is `7`.)
1189
+ @return: Return True if success, False otherwise.
1190
+ """
1191
+
1192
+ for pin in [
1193
+ mosi_io_num,
1194
+ miso_io_num,
1195
+ sclk_io_num,
1196
+ cs_io_num,
1197
+ quadhd_io_num,
1198
+ quadwp_io_num,
1199
+ data4_io_num,
1200
+ data5_io_num,
1201
+ data6_io_num,
1202
+ data7_io_num,
1203
+ ]:
1204
+ if pin != -1 and (
1205
+ (pin not in self.spi_valid_pins) and (pin not in self.__spi_used_pins)
1206
+ ):
1207
+ logger.error(f"Invalid pin number: {pin}")
1208
+ return False, None
1209
+
1210
+ if mode not in [0, 1, 2, 3]:
1211
+ raise ValueError("Invalid mode.")
1212
+ if common_bus_flags not in self.spi_common_bus_flag.values():
1213
+ raise ValueError("Invalid common bus flags.")
1214
+ if isr_cpu_id not in [0, 1, 2]:
1215
+ raise ValueError("Invalid ISR CPU ID.")
1216
+
1217
+ spi_config_data_temp = []
1218
+ spi_config_data_temp.extend(self.__data_decompose(mosi_io_num))
1219
+ spi_config_data_temp.extend(self.__data_decompose(miso_io_num))
1220
+ spi_config_data_temp.extend(self.__data_decompose(sclk_io_num))
1221
+ spi_config_data_temp.extend(self.__data_decompose(quadwp_io_num))
1222
+ spi_config_data_temp.extend(self.__data_decompose(quadhd_io_num))
1223
+ spi_config_data_temp.extend(self.__data_decompose(data4_io_num))
1224
+ spi_config_data_temp.extend(self.__data_decompose(data5_io_num))
1225
+ spi_config_data_temp.extend(self.__data_decompose(data6_io_num))
1226
+ spi_config_data_temp.extend(self.__data_decompose(data7_io_num))
1227
+ spi_config_data_temp.extend(self.__data_decompose(max_transfer_sz, 2))
1228
+ spi_config_data_temp.extend(self.__data_decompose(common_bus_flags))
1229
+ spi_config_data_temp.extend(self.__data_decompose(isr_cpu_id, 1))
1230
+ spi_config_data_temp.extend(self.__data_decompose(intr_flags))
1231
+ spi_config_data_temp.extend(self.__data_decompose(command_bits, 1))
1232
+ spi_config_data_temp.extend(self.__data_decompose(address_bits, 1))
1233
+ spi_config_data_temp.extend(self.__data_decompose(dummy_bits, 1))
1234
+ spi_config_data_temp.extend(self.__data_decompose(mode, 1))
1235
+ spi_config_data_temp.extend(self.__data_decompose(duty_cycle_pos, 2))
1236
+ spi_config_data_temp.extend(self.__data_decompose(cs_ena_pretrans, 2))
1237
+ spi_config_data_temp.extend(self.__data_decompose(cs_ena_posttrans, 1))
1238
+ spi_config_data_temp.extend(self.__data_decompose(freq))
1239
+ spi_config_data_temp.extend(self.__data_decompose(input_delay_ns))
1240
+ spi_config_data_temp.extend(self.__data_decompose(cs_io_num))
1241
+ spi_config_data_temp.extend(self.__data_decompose(device_interface_flags))
1242
+ spi_config_data_temp.extend(self.__data_decompose(queue_size))
1243
+
1244
+ self.__hexdump(spi_config_data_temp)
1245
+
1246
+ ret, _data = self.__task_execute(
1247
+ self.task_cmd["TASK_SPI_CONFIG"], spi_config_data_temp
1248
+ )
1249
+ self.__check_ret_code(self.task_cmd["TASK_SPI_CONFIG"], ret)
1250
+
1251
+ if ret == 0:
1252
+ for pin in self.__spi_used_pins:
1253
+ self.mark_pin_free(pin)
1254
+ for pin in [
1255
+ mosi_io_num,
1256
+ miso_io_num,
1257
+ sclk_io_num,
1258
+ cs_io_num,
1259
+ quadhd_io_num,
1260
+ quadwp_io_num,
1261
+ data4_io_num,
1262
+ data5_io_num,
1263
+ data6_io_num,
1264
+ data7_io_num,
1265
+ ]:
1266
+ self.__mark_spi_used(pin)
1267
+ return True, None
1268
+ else:
1269
+ return False, None
1270
+
1271
+ def spi_read_image(self) -> tuple[bool, bytearray | None]:
1272
+ """
1273
+ @brief Read image data from SPI slave device.
1274
+ @return: Image data.
1275
+ """
1276
+
1277
+ ret, data = self.__task_execute(self.task_cmd["TASK_SPI_READ_IMAGE"], [])
1278
+
1279
+ if ret == 0:
1280
+ return True, data
1281
+ else:
1282
+ return False, None
1283
+
1284
+ def pwm_run_stop(
1285
+ self, pwm_running_state: bool, channel: int = 0
1286
+ ) -> bool | tuple[bool, int]:
1287
+ """
1288
+ @brief Run or stop PWM. It will generate a PWM signal with 10KHz frequency and 25% duty cycle in default.
1289
+ @param pwm_running_state: True to run PWM, False to stop PWM.
1290
+ @param channel: PWM channel. Default is 0. Available channels are 0, 1, 2.
1291
+ @return: Return True if success, False otherwise.
1292
+ """
1293
+
1294
+ if channel not in [0, 1, 2]:
1295
+ logger.error("Invalid channel number. Available channels are 0, 1, 2.")
1296
+ return False
1297
+
1298
+ channel = ctypes.c_uint8(channel).value
1299
+ run_state = ctypes.c_uint8(pwm_running_state).value
1300
+ ret, _data = self.__task_execute(
1301
+ self.task_cmd["TASK_PWM_RUN_STOP"], [channel, run_state]
1302
+ )
1303
+
1304
+ self.__check_ret_code(self.task_cmd["TASK_PWM_RUN_STOP"], ret)
1305
+
1306
+ if pwm_running_state:
1307
+ self.__pwm_states[channel] = True
1308
+ else:
1309
+ self.__pwm_states[channel] = False
1310
+
1311
+ return self.__pwm_states[channel], ret
1312
+
1313
+ def pwm_config(
1314
+ self,
1315
+ pin: int = 16,
1316
+ freq: int = 10000,
1317
+ duty: float = 0.5,
1318
+ channel: int = 0,
1319
+ resolution_hz: int = 80000000,
1320
+ ) -> tuple[bool, None]:
1321
+ """
1322
+ @brief Configure PWM.
1323
+ @param pin: PWM pin number. (Channel 0: `16` (in default); Channel 1: `17` (in default); Channel 2: `18` (in default).)
1324
+ @param freq: PWM frequency. Unit: Hz; (Default is `10000` (10KHz). Frequency should be far less than timer resolution.)
1325
+ @param duty: PWM duty cycle. (Default is `0.5` (50%). Duty cycle should be in the range of `0.0` (0%) to `1.0` (100%).)
1326
+ @param channel: PWM channel. (Default is `0`. Available channels are `0`, `1`, `2`.)
1327
+ @return: Return True if success, False otherwise.
1328
+ """
1329
+
1330
+ self.__pwm_valid_pins = None
1331
+
1332
+ if pin not in self.pwm_valid_pins:
1333
+ logger.error("Invalid pin number.")
1334
+ return False, None
1335
+
1336
+ if duty < 0 or duty > 1:
1337
+ logger.error(
1338
+ "Invalid duty cycle. Duty cycle should be in the range of 0.0 to 1.0"
1339
+ )
1340
+ return False, None
1341
+
1342
+ if resolution_hz > 80_000_000:
1343
+ logger.error(
1344
+ "Invalid timer resolution. Timer resolution should be less than 80MHz."
1345
+ )
1346
+ return False, None
1347
+ else:
1348
+ coeff = 80_000_000 // resolution_hz
1349
+
1350
+ timer_resolution = resolution_hz
1351
+
1352
+ if freq > timer_resolution:
1353
+ logger.error(
1354
+ "Invalid frequency. Frequency should be less than timer resolution."
1355
+ )
1356
+ return False, None
1357
+ else:
1358
+ freq /= coeff
1359
+ freq = int(freq)
1360
+
1361
+ timer_resolution = ctypes.c_uint32(timer_resolution).value
1362
+ period_ticks = ctypes.c_uint32(resolution_hz // freq).value
1363
+ duty_ticks = int(period_ticks * duty)
1364
+
1365
+ if not (0 < period_ticks < 65535):
1366
+ raise ValueError(
1367
+ "Invalid period ticks. Should be in the range of 1 to 65535."
1368
+ )
1369
+
1370
+ pin = ctypes.c_uint8(pin).value
1371
+ channel = ctypes.c_uint8(channel).value
1372
+
1373
+ logger.debug(f"Period Ticks: {period_ticks}, Duty Ticks: {duty_ticks}")
1374
+
1375
+ pwm_data_temp = [
1376
+ (channel & 0xFF),
1377
+ (pin & 0xFF),
1378
+ (period_ticks & 0xFF000000) >> 24,
1379
+ (period_ticks & 0x00FF0000) >> 16,
1380
+ (period_ticks & 0x0000FF00) >> 8,
1381
+ period_ticks & 0x000000FF,
1382
+ (duty_ticks & 0xFF000000) >> 24,
1383
+ (duty_ticks & 0x00FF0000) >> 16,
1384
+ (duty_ticks & 0x0000FF00) >> 8,
1385
+ duty_ticks & 0x000000FF,
1386
+ (timer_resolution & 0xFF000000) >> 24,
1387
+ (timer_resolution & 0x00FF0000) >> 16,
1388
+ (timer_resolution & 0x0000FF00) >> 8,
1389
+ timer_resolution & 0x000000FF,
1390
+ ]
1391
+
1392
+ ret, _ = self.__task_execute(self.task_cmd["TASK_PWM_CONFIG"], pwm_data_temp)
1393
+
1394
+ self.__check_ret_code(self.task_cmd["TASK_PWM_CONFIG"], ret)
1395
+
1396
+ if ret == 0:
1397
+ old_pins = list(self.__pwm_used_pins)
1398
+ for old_pin in old_pins:
1399
+ self.mark_pin_free(old_pin)
1400
+ self.__mark_pwm_used(pin)
1401
+
1402
+ if self.__pwm_states[channel]:
1403
+ self.pwm_run_stop(True, channel)
1404
+
1405
+ return True, None
1406
+
1407
+ def usb_config(self, crc_enable: bool = False) -> tuple[bool, None]:
1408
+ """
1409
+ @brief Configure USB.
1410
+ @param crc_enable: (`True`: enable CRC, `False`: disable CRC.)
1411
+ @return: Return True if success, False otherwise.
1412
+ """
1413
+
1414
+ crc_enable = ctypes.c_uint8(crc_enable).value
1415
+ self.__crc_enable = crc_enable
1416
+
1417
+ ret, _data = self.__task_execute(self.task_cmd["TASK_USB_CONFIG"], [crc_enable])
1418
+
1419
+ if ret == 0:
1420
+ return True, None
1421
+ else:
1422
+ return False, None
1423
+
1424
+ def power_init(self) -> bool:
1425
+ """
1426
+ @brief Initialize power.
1427
+ @return: Return True if success, False otherwise.
1428
+ """
1429
+
1430
+ if not self.__expand_io_init_status:
1431
+ ret = self.expand_io_init()
1432
+ if ret is not True:
1433
+ return False
1434
+
1435
+ if not self.__power_init_status:
1436
+ match self.__extboard_version:
1437
+ case "v0.1":
1438
+ # set IO1 to IO3 as output
1439
+
1440
+ ret, data = self.i2c_write_read(
1441
+ self.__pca9557pw_addr, [0x03], 1, port=1
1442
+ )
1443
+ if ret is not True:
1444
+ logger.error("PCA9557PW read failed.")
1445
+ raise ValueError("PCA9557PW read failed.")
1446
+
1447
+ bit_mask = data[0]
1448
+ bit_mask &= 0xE1 # 1110 0001
1449
+
1450
+ ret, data = self.i2c_write_read(
1451
+ self.__pca9557pw_addr, [0x03, bit_mask], 0, port=1
1452
+ )
1453
+ if ret is not True:
1454
+ logger.error("PCA9557PW write failed.")
1455
+ raise ValueError("PCA9557PW write failed.")
1456
+
1457
+ self.__expand_io_mode_bitmask = bit_mask
1458
+ self.__power_init_status = True
1459
+
1460
+ case "v0.2.1":
1461
+ ret, data = self.i2c_write_read(
1462
+ self.__tca9555pwr_addr, [0x06], 2, port=1
1463
+ )
1464
+ if ret is not True:
1465
+ logger.error("TCA9555PWR read failed.")
1466
+ raise ValueError("TCA9555PWR read failed.")
1467
+
1468
+ bit_mask = data[1] << 8 | data[0]
1469
+ bit_mask &= 0xFC00 # 1111 1100 0000 0000
1470
+
1471
+ ret, data = self.i2c_write_read(
1472
+ self.__tca9555pwr_addr,
1473
+ [0x06, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
1474
+ 0,
1475
+ port=1,
1476
+ )
1477
+ if ret is not True:
1478
+ logger.error("TCA9555PWR write failed.")
1479
+ raise ValueError("TCA9555PWR write failed.")
1480
+
1481
+ self.__expand_io_mode_bitmask = bit_mask
1482
+ self.__power_init_status = True
1483
+
1484
+ case _:
1485
+ logger.error("Invalid extension board version.")
1486
+ raise ValueError("Invalid extension board version.")
1487
+
1488
+ else:
1489
+ logger.info("Power already initialized.")
1490
+
1491
+ return True
1492
+
1493
+ def power_control(
1494
+ self, communication_type: str = "SPI", power_type: str = "1V8"
1495
+ ) -> bool:
1496
+ """
1497
+ @brief Control power for communication type.
1498
+ @param communication_type: Communication types which are supported to use level shifter. (`SPI` or `I2C` are available.)
1499
+ @param power_type: Power types which are supported to swich power. (`1V8`, `1V2` or `0V` are supported in ***Extboard v0.1***, while `3V3`, `1V8`, `1V2` or `0V` are supported in ***Extboard v0.2.1***.)
1500
+ @return: Return True if success, False otherwise.
1501
+ """
1502
+
1503
+ communication_type = communication_type.upper()
1504
+ power_type = power_type.upper()
1505
+
1506
+ match self.__extboard_version:
1507
+ case "v0.1":
1508
+ if communication_type not in ["SPI", "I2C"]:
1509
+ logger.error(
1510
+ "Invalid communication type. Only SPI and I2C are supported."
1511
+ )
1512
+ return False
1513
+
1514
+ if power_type not in ["1V8", "1V2", "0V"]:
1515
+ logger.error(
1516
+ "Invalid power type. Only 1V8, 1V2 and 0V are supported."
1517
+ )
1518
+ return False
1519
+
1520
+ ret, data = self.i2c_write_read(
1521
+ self.__pca9557pw_addr, [0x01], 1, port=1
1522
+ )
1523
+ if ret is not True:
1524
+ logger.error("PCA9557PW read failed.")
1525
+ raise ValueError("PCA9557PW read failed.")
1526
+
1527
+ bit_mask = data[0]
1528
+
1529
+ match communication_type:
1530
+ case "SPI":
1531
+ match power_type:
1532
+ case "1V8": # IO3, bit 3
1533
+ bit_mask |= 0x08 # 0000 1000
1534
+ bit_mask &= 0xFD # 1111 1101
1535
+
1536
+ case "1V2": # IO1, bit 1
1537
+ bit_mask |= 0x02 # 0000 0010
1538
+ bit_mask &= 0xF7 # 1111 0111
1539
+
1540
+ case "0V": # set IO3 and IO1 to 0
1541
+ bit_mask &= 0xF5 # 1111 0101
1542
+
1543
+ case "3V3":
1544
+ logger.error(
1545
+ "Invalid power type. 3V3 is not supported in Extboard v0.1."
1546
+ )
1547
+ return False
1548
+
1549
+ case _:
1550
+ logger.error(
1551
+ "Invalid power type. Only 1V8, 1V2 and 0V are supported."
1552
+ )
1553
+ return False
1554
+
1555
+ case "I2C":
1556
+ match power_type:
1557
+ case "1V8": # IO4, bit 4
1558
+ bit_mask |= 0x10 # 0001 0000
1559
+ bit_mask &= 0xFB # 1111 1011
1560
+
1561
+ case "1V2": # IO2, bit 2
1562
+ bit_mask |= 0x04 # 0000 0100
1563
+ bit_mask &= 0xEF # 1110 1111
1564
+
1565
+ case "0V": # set IO4 and IO2 to 0
1566
+ bit_mask &= 0xEB # 1110 1011
1567
+
1568
+ case "3V3":
1569
+ logger.error(
1570
+ "Invalid power type. 3V3 is not supported in Extboard v0.1."
1571
+ )
1572
+ return False
1573
+
1574
+ case _:
1575
+ logger.error(
1576
+ "Invalid power type. Only 1V8, 1V2 and 0V are supported."
1577
+ )
1578
+ return False
1579
+
1580
+ ret, data = self.i2c_write_read(
1581
+ self.__pca9557pw_addr, [0x01, bit_mask], 0, port=1
1582
+ )
1583
+ if ret is not True:
1584
+ logger.error("PCA9557PW write failed.")
1585
+ raise ValueError("PCA9557PW write failed.")
1586
+
1587
+ case "v0.2.1":
1588
+ if power_type not in ["3V3", "1V8", "1V2", "0V"]:
1589
+ logger.error(
1590
+ "Invalid power type. Only 3V3, 1V8, 1V2 and 0V are supported."
1591
+ )
1592
+ return False
1593
+
1594
+ ret, data = self.i2c_write_read(
1595
+ self.__tca9555pwr_addr, [0x02], 2, port=1
1596
+ )
1597
+ if ret is not True:
1598
+ logger.error("TCA9555PWR read failed.")
1599
+ raise ValueError("TCA9555PWR read failed.")
1600
+
1601
+ bit_mask = data[1] << 8 | data[0]
1602
+
1603
+ """
1604
+ REG 0x02
1605
+ | O 0.7 | O 0.6 | O 0.5 | O 0.4 | O 0.3 | O 0.2 | O 0.1 | O 0.0 |
1606
+ |-------|-------|---------|---------|---------|---------|---------|---------|
1607
+ | 1V8_2 | 1V8_1 | 3V3_I2C | 1V8_I2C | 1V2_I2C | 3V3_SPI | 1V8_SPI | 1V2_SPI |
1608
+
1609
+ REG 0x03
1610
+ | O 1.7 | O 1.6 | O 1.5 | O 1.4 | O 1.3 | O 1.2 | O 1.1 | O 1.0 |
1611
+ |-------|-------|-------|-------|-------|-------|-------|-------|
1612
+ | IO17 | IO16 | IO15 | IO14 | IO13 | IO12 | 3V3_2 | 3V3_1 |
1613
+ """
1614
+
1615
+ match communication_type:
1616
+ case "SPI":
1617
+ match power_type:
1618
+ case "3V3":
1619
+ bit_mask |= 0x0004 # 0000 0000 0000 0100
1620
+ bit_mask &= 0xFFFC # 1111 1111 1111 1100
1621
+
1622
+ case "1V8":
1623
+ bit_mask |= 0x0002 # 0000 0000 0000 0010
1624
+ bit_mask &= 0xFFFA # 1111 1111 1111 1010
1625
+
1626
+ case "1V2":
1627
+ bit_mask |= 0x0001 # 0000 0000 0000 0001
1628
+ bit_mask &= 0xFFF9 # 1111 1111 1111 1001
1629
+
1630
+ case "0V":
1631
+ bit_mask &= 0xFFF8 # 1111 1111 1111 1000
1632
+
1633
+ case _:
1634
+ logger.error(
1635
+ "Invalid power type. Only 3V3, 1V8, 1V2 and 0V are supported."
1636
+ )
1637
+ return False
1638
+
1639
+ case "I2C":
1640
+ match power_type:
1641
+ case "3V3":
1642
+ bit_mask |= 0x0020 # 0000 0000 0010 0000
1643
+ bit_mask &= 0xFFE7 # 1111 1111 1110 0111
1644
+
1645
+ case "1V8":
1646
+ bit_mask |= 0x0010 # 0000 0000 0001 0000
1647
+ bit_mask &= 0xFFD7 # 1111 1111 1101 0111
1648
+
1649
+ case "1V2":
1650
+ bit_mask |= 0x0008 # 0000 0000 0000 1000
1651
+ bit_mask &= 0xFFCF # 1111 1111 1100 1111
1652
+
1653
+ case "0V":
1654
+ bit_mask &= 0xFFC7 # 1111 1111 1100 0111
1655
+
1656
+ case _:
1657
+ logger.error(
1658
+ "Invalid power type. Only 3V3, 1V8, 1V2 and 0V are supported."
1659
+ )
1660
+ return False
1661
+
1662
+ case _:
1663
+ logger.error(
1664
+ "Invalid communication type. Only SPI and I2C are supported."
1665
+ )
1666
+ return False
1667
+
1668
+ ret, data = self.i2c_write_read(
1669
+ self.__tca9555pwr_addr,
1670
+ [0x02, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
1671
+ 0,
1672
+ port=1,
1673
+ )
1674
+ if ret is not True:
1675
+ logger.error("TCA9555PWR write failed.")
1676
+ raise ValueError("TCA9555PWR write failed.")
1677
+
1678
+ case _:
1679
+ logger.error("Invalid extension board version.")
1680
+ raise ValueError("Invalid extension board version.")
1681
+
1682
+ return True
1683
+
1684
+ def expand_io_init(self) -> bool:
1685
+ """
1686
+ @brief Initialize expand IO.
1687
+ @return: Return True if success, False otherwise.
1688
+ """
1689
+
1690
+ ret, board_version = self.get_extboard_version()
1691
+ if ret is not True:
1692
+ return False
1693
+ if board_version is None:
1694
+ return False
1695
+
1696
+ if not self.__expand_io_init_status:
1697
+ if board_version == "v0.1":
1698
+ reg_list = [
1699
+ [0x02, 0x00], # set all pins as normal polarity
1700
+ [0x03, 0xFF], # set all pins as input mode
1701
+ [0x01, 0x00], # set all pins as low level
1702
+ ]
1703
+
1704
+ for reg in reg_list:
1705
+ ret, _data = self.i2c_write_read(
1706
+ self.__pca9557pw_addr, reg, 0, port=1
1707
+ )
1708
+ if not ret:
1709
+ logger.error("PCA9557PW init failed.")
1710
+ raise ValueError("PCA9557PW init failed.")
1711
+
1712
+ self.__expand_io_mode_bitmask = 0xFF
1713
+
1714
+ elif board_version == "v0.2.1":
1715
+ reg_list = [
1716
+ [0x06, 0xFF, 0xFF], # set all pins as input mode
1717
+ [0x04, 0x00, 0x00], # disable polarity inversion
1718
+ [0x02, 0x00, 0x00], # set all pins as low level
1719
+ ]
1720
+
1721
+ for reg_data in reg_list:
1722
+ _ret, _data = self.i2c_write_read(
1723
+ self.__tca9555pwr_addr, reg_data, 0, port=1
1724
+ )
1725
+ if not _ret:
1726
+ logger.error("TCA9555PWR init failed.")
1727
+ raise ValueError("TCA9555PWR init failed.")
1728
+
1729
+ self.__expand_io_mode_bitmask = 0xFFFF
1730
+
1731
+ self.__expand_io_init_status = True
1732
+ return True
1733
+
1734
+ def expand_io_config(self, pin: int, mode: int) -> bool:
1735
+ """
1736
+ @brief Configure expand IO pin mode.
1737
+ @param pin: Expanded pin number on ExtBoard. (`IO0`, `IO5`, `IO6`, `IO7` are supported in ***Extboard v0.1***, while `IO6` (`1V8_1`), `IO7` (`1V8_2`), `IO10`(`3V3_1`), `IO11`(`3V3_2`), `IO12`, `IO13`, `IO14`, `IO15`, `IO16`, `IO17` are supported in ***Extboard v0.2.1***.)
1738
+ @param mode: Pin mode. `0`: Input, `1`: Output.
1739
+ """
1740
+
1741
+ if mode not in [0, 1]:
1742
+ raise ValueError("Invalid mode. Only 0 and 1 are supported.")
1743
+
1744
+ if not self.__expand_io_init_status:
1745
+ self.expand_io_init()
1746
+
1747
+ match self.__extboard_version:
1748
+ case "v0.1":
1749
+ if pin not in [0, 5, 6, 7]:
1750
+ raise ValueError(
1751
+ "Invalid pin number. Only 0, 5, 6, 7 are supported."
1752
+ )
1753
+
1754
+ ret, data = self.i2c_write_read(
1755
+ self.__pca9557pw_addr, [0x03], 1, port=1
1756
+ )
1757
+ if ret is not True:
1758
+ logger.error("PCA9557PW read failed.")
1759
+ raise ValueError("PCA9557PW read failed.")
1760
+
1761
+ bit_mask = data[0]
1762
+ bit_mask = (
1763
+ (bit_mask & ~(1 << pin)) if mode == 1 else (bit_mask | (1 << pin))
1764
+ )
1765
+
1766
+ ret, data = self.i2c_write_read(
1767
+ self.__pca9557pw_addr, [0x03, bit_mask], 0, port=1
1768
+ )
1769
+ if ret is not True:
1770
+ logger.error("PCA9557PW write failed.")
1771
+ raise ValueError("PCA9557PW write failed.")
1772
+
1773
+ self.__expand_io_mode_bitmask = bit_mask
1774
+
1775
+ case "v0.2.1":
1776
+ """
1777
+ REG 0x02
1778
+ | O 0.7 | O 0.6 | O 0.5 | O 0.4 | O 0.3 | O 0.2 | O 0.1 | O 0.0 |
1779
+ |------------|------------|---------|---------|---------|---------|---------|---------|
1780
+ | (IO7)1V8_2 | (IO6)1V8_1 | 3V3_I2C | 1V8_I2C | 1V2_I2C | 3V3_SPI | 1V8_SPI | 1V2_SPI |
1781
+
1782
+ REG 0x03
1783
+ | O 1.7 | O 1.6 | O 1.5 | O 1.4 | O 1.3 | O 1.2 | O 1.1 | O 1.0 |
1784
+ |-------|-------|-------|-------|-------|-------|-------|-------|
1785
+ | IO17 | IO16 | IO15 | IO14 | IO13 | IO12 | 3V3_2 | 3V3_1 |
1786
+ """
1787
+
1788
+ if pin not in [12, 13, 14, 15, 16, 17]:
1789
+ raise ValueError(
1790
+ "Invalid pin number. Only 12, 13, 14, 15, 16, 17 are supported."
1791
+ )
1792
+
1793
+ ret, data = self.i2c_write_read(
1794
+ self.__tca9555pwr_addr, [0x06], 2, port=1
1795
+ )
1796
+ if ret is not True:
1797
+ logger.error("TCA9555PWR read failed.")
1798
+ raise ValueError("TCA9555PWR read failed.")
1799
+
1800
+ bit_mask = data[1] << 8 | data[0]
1801
+ if pin > 7:
1802
+ pin -= 2
1803
+
1804
+ bit_mask = (
1805
+ (bit_mask & ~(1 << pin)) if mode == 1 else (bit_mask | (1 << pin))
1806
+ )
1807
+
1808
+ ret, data = self.i2c_write_read(
1809
+ self.__tca9555pwr_addr,
1810
+ [0x06, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
1811
+ 0,
1812
+ port=1,
1813
+ )
1814
+ if ret is not True:
1815
+ logger.error("TCA9555PWR write failed.")
1816
+ raise ValueError("TCA9555PWR write failed.")
1817
+
1818
+ self.__expand_io_mode_bitmask = bit_mask
1819
+
1820
+ case _:
1821
+ logger.error("Invalid extension board version.")
1822
+ raise ValueError("Invalid extension board version.")
1823
+
1824
+ return True
1825
+
1826
+ def expand_io_write_read(
1827
+ self, pin: int, level: int | None = None
1828
+ ) -> tuple[bool, int | None]:
1829
+ """
1830
+ @brief Write or read level of expand IO pin.
1831
+ @param pin: Expanded pin number on ExtBoard. (`IO0`, `IO5`, `IO6`, `IO7` are supported in ***Extboard v0.1***, while `IO6` (`1V8_1`), `IO7` (`1V8_2`), `IO10`(`3V3_1`), `IO11`(`3V3_2`), `IO12`, `IO13`, `IO14`, `IO15`, `IO16`, `IO17` are supported in ***Extboard v0.2.1***.)
1832
+ @param level: Level to write. `0`: Low, `1`: High. None in default for read mode.
1833
+ """
1834
+
1835
+ if level not in [0, 1] and level is not None:
1836
+ raise ValueError("Invalid level. Only 0 and 1 are supported.")
1837
+
1838
+ match self.__extboard_version:
1839
+ case "v0.1":
1840
+ expand_pin_mapping = {
1841
+ "IO0": 0,
1842
+ "IO5": 5,
1843
+ "IO6": 6,
1844
+ "IO7": 7,
1845
+ }
1846
+
1847
+ if isinstance(pin, str):
1848
+ pin = pin.upper()
1849
+
1850
+ if pin not in expand_pin_mapping:
1851
+ raise ValueError(
1852
+ "Invalid pin number. Only IO0, IO5, IO6, IO7 are supported."
1853
+ )
1854
+ else:
1855
+ pin = expand_pin_mapping[pin]
1856
+
1857
+ if pin not in [0, 5, 6, 7]:
1858
+ raise ValueError(
1859
+ "Invalid pin number. Only 0, 5, 6, 7 are supported."
1860
+ )
1861
+
1862
+ # read level
1863
+ if level is None:
1864
+ ret, data = self.i2c_write_read(
1865
+ self.__pca9557pw_addr,
1866
+ [0x00]
1867
+ if (self.__expand_io_mode_bitmask >> pin) & 0x01 == 1
1868
+ else [0x01],
1869
+ 1,
1870
+ port=1,
1871
+ )
1872
+ if ret is not True:
1873
+ logger.error("PCA9557PW read failed.")
1874
+ raise ValueError("PCA9557PW read failed.")
1875
+
1876
+ level_ = (data[0] >> pin) & 0x01
1877
+
1878
+ return True, level_
1879
+
1880
+ # write level
1881
+ else:
1882
+ if self.__expand_io_mode_bitmask >> pin & 0x01 == 1:
1883
+ raise ValueError(
1884
+ "Pin is in input mode. Use expand_io_config() to set pin to output mode first."
1885
+ )
1886
+
1887
+ ret, data = self.i2c_write_read(
1888
+ self.__pca9557pw_addr, [0x00], 1, port=1
1889
+ )
1890
+ if ret is not True:
1891
+ logger.error("PCA9557PW read failed.")
1892
+ raise ValueError("PCA9557PW read failed.")
1893
+
1894
+ if level == 1:
1895
+ bit_mask = data[0] | (1 << pin)
1896
+ else:
1897
+ bit_mask = data[0] & ~(1 << pin)
1898
+
1899
+ ret, data = self.i2c_write_read(
1900
+ self.__pca9557pw_addr, [0x01, bit_mask], 0, port=1
1901
+ )
1902
+ if ret is not True:
1903
+ logger.error("PCA9557PW write failed.")
1904
+ raise ValueError("PCA9557PW write failed.")
1905
+
1906
+ return True, None
1907
+
1908
+ case "v0.2.1":
1909
+ expand_pin_mapping = {
1910
+ "IO6": 6,
1911
+ "IO7": 7,
1912
+ "IO10": 10,
1913
+ "IO11": 11,
1914
+ "IO12": 12,
1915
+ "IO13": 13,
1916
+ "IO14": 14,
1917
+ "IO15": 15,
1918
+ "IO16": 16,
1919
+ "IO17": 17,
1920
+ "1V8_1": 6,
1921
+ "1V8_2": 7,
1922
+ "3V3_1": 10,
1923
+ "3V3_2": 11,
1924
+ }
1925
+
1926
+ if isinstance(pin, str):
1927
+ pin = pin.upper()
1928
+
1929
+ if pin not in expand_pin_mapping:
1930
+ raise ValueError(
1931
+ "Invalid pin number. Only IO6, IO7, IO10, IO11, IO12, IO13, IO14, IO15, IO16, IO17, 1V8_1, 1V8_2, 3V3_1, 3V3_2 are supported."
1932
+ )
1933
+ else:
1934
+ pin = expand_pin_mapping[pin]
1935
+
1936
+ if pin not in [6, 7, 10, 11, 12, 13, 14, 15, 16, 17]:
1937
+ raise ValueError(
1938
+ "Invalid pin number. Only 6, 7, 10, 11, 12, 13, 14, 15, 16, 17 are supported."
1939
+ )
1940
+
1941
+ if pin > 7:
1942
+ pin -= 2
1943
+
1944
+ # read level
1945
+ if level is None:
1946
+ ret, data = self.i2c_write_read(
1947
+ self.__tca9555pwr_addr, [0x00], 2, port=1
1948
+ )
1949
+ if ret is not True:
1950
+ logger.error("TCA9555PWR read failed.")
1951
+ raise ValueError("TCA9555PWR read failed.")
1952
+
1953
+ level_ = (data[1] << 8 | data[0]) >> pin & 0x01
1954
+
1955
+ return True, level_
1956
+
1957
+ # write level
1958
+ else:
1959
+ if self.__expand_io_mode_bitmask >> pin & 0x01 == 1:
1960
+ raise ValueError(
1961
+ "Pin is in input mode. Use expand_io_config() to set pin to output mode first."
1962
+ )
1963
+
1964
+ ret, data = self.i2c_write_read(
1965
+ self.__tca9555pwr_addr, [0x00], 2, port=1
1966
+ )
1967
+ if ret is not True:
1968
+ logger.error("TCA9555PWR read failed.")
1969
+ raise ValueError("TCA9555PWR read failed.")
1970
+
1971
+ if level == 1:
1972
+ bit_mask = (data[1] << 8 | data[0]) | (1 << pin)
1973
+ else:
1974
+ bit_mask = (data[1] << 8 | data[0]) & ~(1 << pin)
1975
+
1976
+ ret, data = self.i2c_write_read(
1977
+ self.__tca9555pwr_addr,
1978
+ [0x02, bit_mask & 0x00FF, (bit_mask & 0xFF00) >> 8],
1979
+ 0,
1980
+ port=1,
1981
+ )
1982
+ if ret is not True:
1983
+ logger.error("TCA9555PWR write failed.")
1984
+ raise ValueError("TCA9555PWR write failed.")
1985
+
1986
+ return True, None
1987
+
1988
+ case _:
1989
+ logger.error("Invalid extension board version.")
1990
+ raise ValueError("Invalid extension board version.")
1991
+
1992
+ def restart(self) -> bool:
1993
+ """
1994
+ @brief Restart the device.
1995
+ """
1996
+
1997
+ ret, _data = self.__task_execute(self.task_cmd["TASK_RESET_DEVICE"], [])
1998
+ if ret != 0:
1999
+ logger.error("Restart failed.")
2000
+ raise ValueError("Restart failed.")
2001
+
2002
+ return True
2003
+
2004
+ def get_extboard_version(self) -> tuple[bool, str | None]:
2005
+ """
2006
+ @brief Get extension board version.
2007
+ @return: Extension board version.
2008
+ """
2009
+
2010
+ ret, data = self.i2c_find_slave(port=1)
2011
+ if ret is not True:
2012
+ # Save original I2C1 pin config
2013
+ orig_sda = self.__i2c_used_pins[0] if len(self.__i2c_used_pins) > 0 else 10
2014
+ orig_scl = self.__i2c_used_pins[1] if len(self.__i2c_used_pins) > 1 else 11
2015
+
2016
+ # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BUG FIXED FOR v0.2.1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
2017
+ ret, data = self.i2c_config(
2018
+ port=1,
2019
+ freq=400000,
2020
+ sda_pin=41,
2021
+ scl_pin=42,
2022
+ sda_pullup=False,
2023
+ scl_pullup=False,
2024
+ )
2025
+ logger.info("I2C config for extension board.")
2026
+ if ret is not True:
2027
+ logger.error("I2C config failed.")
2028
+ raise ValueError("I2C config failed.")
2029
+
2030
+ ret, data = self.i2c_find_slave(port=1)
2031
+ if ret is not True:
2032
+ logger.info("No extension board found.")
2033
+ # Restore original I2C1 pin config
2034
+ self.i2c_config(port=1, freq=400000, sda_pin=orig_sda, scl_pin=orig_scl)
2035
+ return False, None
2036
+ # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BUG FIXED FOR v0.2.1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
2037
+
2038
+ if self.__pca9557pw_addr in [int(i, 16) for i in data]:
2039
+ self.__extboard_version = "v0.1"
2040
+ logger.info("Extension board version: v0.1")
2041
+
2042
+ elif self.__tca9555pwr_addr in [int(i, 16) for i in data]:
2043
+ self.__extboard_version = "v0.2.1"
2044
+ logger.info("Extension board version: v0.2.1")
2045
+
2046
+ else:
2047
+ logger.error("Unknown extension board.")
2048
+ return False, None
2049
+
2050
+ return True, self.__extboard_version