micropython-stubber 1.23.1.post1__py3-none-any.whl → 1.23.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.
Files changed (152) hide show
  1. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/LICENSE +30 -30
  2. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/METADATA +4 -4
  3. micropython_stubber-1.23.2.dist-info/RECORD +158 -0
  4. mpflash/README.md +220 -220
  5. mpflash/libusb_flash.ipynb +203 -203
  6. mpflash/mpflash/add_firmware.py +98 -98
  7. mpflash/mpflash/ask_input.py +236 -236
  8. mpflash/mpflash/basicgit.py +284 -284
  9. mpflash/mpflash/bootloader/__init__.py +2 -2
  10. mpflash/mpflash/bootloader/activate.py +60 -60
  11. mpflash/mpflash/bootloader/detect.py +82 -82
  12. mpflash/mpflash/bootloader/manual.py +101 -101
  13. mpflash/mpflash/bootloader/micropython.py +12 -12
  14. mpflash/mpflash/bootloader/touch1200.py +36 -36
  15. mpflash/mpflash/cli_download.py +129 -129
  16. mpflash/mpflash/cli_flash.py +224 -216
  17. mpflash/mpflash/cli_group.py +111 -111
  18. mpflash/mpflash/cli_list.py +87 -87
  19. mpflash/mpflash/cli_main.py +39 -39
  20. mpflash/mpflash/common.py +210 -166
  21. mpflash/mpflash/config.py +44 -44
  22. mpflash/mpflash/connected.py +96 -77
  23. mpflash/mpflash/download.py +364 -364
  24. mpflash/mpflash/downloaded.py +130 -130
  25. mpflash/mpflash/errors.py +9 -9
  26. mpflash/mpflash/flash/__init__.py +55 -55
  27. mpflash/mpflash/flash/esp.py +59 -59
  28. mpflash/mpflash/flash/stm32.py +19 -19
  29. mpflash/mpflash/flash/stm32_dfu.py +104 -104
  30. mpflash/mpflash/flash/uf2/__init__.py +88 -88
  31. mpflash/mpflash/flash/uf2/boardid.py +15 -15
  32. mpflash/mpflash/flash/uf2/linux.py +136 -130
  33. mpflash/mpflash/flash/uf2/macos.py +42 -42
  34. mpflash/mpflash/flash/uf2/uf2disk.py +12 -12
  35. mpflash/mpflash/flash/uf2/windows.py +43 -43
  36. mpflash/mpflash/flash/worklist.py +170 -170
  37. mpflash/mpflash/list.py +106 -106
  38. mpflash/mpflash/logger.py +41 -41
  39. mpflash/mpflash/mpboard_id/__init__.py +93 -93
  40. mpflash/mpflash/mpboard_id/add_boards.py +251 -251
  41. mpflash/mpflash/mpboard_id/board.py +37 -37
  42. mpflash/mpflash/mpboard_id/board_id.py +86 -86
  43. mpflash/mpflash/mpboard_id/store.py +43 -43
  44. mpflash/mpflash/mpremoteboard/__init__.py +266 -266
  45. mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -141
  46. mpflash/mpflash/mpremoteboard/runner.py +140 -140
  47. mpflash/mpflash/vendor/click_aliases.py +91 -91
  48. mpflash/mpflash/vendor/dfu.py +165 -165
  49. mpflash/mpflash/vendor/pydfu.py +605 -605
  50. mpflash/mpflash/vendor/readme.md +2 -2
  51. mpflash/mpflash/versions.py +135 -135
  52. mpflash/poetry.lock +1599 -1599
  53. mpflash/pyproject.toml +65 -65
  54. mpflash/stm32_udev_rules.md +62 -62
  55. stubber/__init__.py +3 -3
  56. stubber/board/board_info.csv +193 -193
  57. stubber/board/boot.py +34 -34
  58. stubber/board/createstubs.py +1004 -986
  59. stubber/board/createstubs_db.py +826 -825
  60. stubber/board/createstubs_db_min.py +332 -331
  61. stubber/board/createstubs_db_mpy.mpy +0 -0
  62. stubber/board/createstubs_lvgl.py +741 -741
  63. stubber/board/createstubs_lvgl_min.py +741 -741
  64. stubber/board/createstubs_mem.py +767 -766
  65. stubber/board/createstubs_mem_min.py +307 -306
  66. stubber/board/createstubs_mem_mpy.mpy +0 -0
  67. stubber/board/createstubs_min.py +295 -294
  68. stubber/board/createstubs_mpy.mpy +0 -0
  69. stubber/board/fw_info.py +141 -141
  70. stubber/board/info.py +183 -183
  71. stubber/board/main.py +19 -19
  72. stubber/board/modulelist.txt +247 -247
  73. stubber/board/pyrightconfig.json +34 -34
  74. stubber/bulk/mcu_stubber.py +437 -437
  75. stubber/codemod/_partials/__init__.py +48 -48
  76. stubber/codemod/_partials/db_main.py +147 -147
  77. stubber/codemod/_partials/lvgl_main.py +77 -77
  78. stubber/codemod/_partials/modules_reader.py +80 -80
  79. stubber/codemod/add_comment.py +53 -53
  80. stubber/codemod/add_method.py +65 -65
  81. stubber/codemod/board.py +317 -317
  82. stubber/codemod/enrich.py +151 -145
  83. stubber/codemod/merge_docstub.py +284 -284
  84. stubber/codemod/modify_list.py +54 -54
  85. stubber/codemod/utils.py +56 -56
  86. stubber/commands/build_cmd.py +94 -94
  87. stubber/commands/cli.py +49 -49
  88. stubber/commands/clone_cmd.py +78 -78
  89. stubber/commands/config_cmd.py +29 -29
  90. stubber/commands/enrich_folder_cmd.py +71 -71
  91. stubber/commands/get_core_cmd.py +71 -71
  92. stubber/commands/get_docstubs_cmd.py +92 -92
  93. stubber/commands/get_frozen_cmd.py +117 -117
  94. stubber/commands/get_mcu_cmd.py +102 -102
  95. stubber/commands/merge_cmd.py +66 -66
  96. stubber/commands/publish_cmd.py +118 -118
  97. stubber/commands/stub_cmd.py +31 -31
  98. stubber/commands/switch_cmd.py +62 -62
  99. stubber/commands/variants_cmd.py +48 -48
  100. stubber/cst_transformer.py +178 -178
  101. stubber/data/board_info.csv +193 -193
  102. stubber/data/board_info.json +1729 -1729
  103. stubber/data/micropython_tags.csv +15 -15
  104. stubber/data/requirements-core-micropython.txt +38 -38
  105. stubber/data/requirements-core-pycopy.txt +39 -39
  106. stubber/downloader.py +37 -37
  107. stubber/freeze/common.py +72 -72
  108. stubber/freeze/freeze_folder.py +69 -69
  109. stubber/freeze/freeze_manifest_2.py +126 -126
  110. stubber/freeze/get_frozen.py +131 -131
  111. stubber/get_cpython.py +112 -112
  112. stubber/get_lobo.py +59 -59
  113. stubber/minify.py +423 -423
  114. stubber/publish/bump.py +86 -86
  115. stubber/publish/candidates.py +275 -275
  116. stubber/publish/database.py +18 -18
  117. stubber/publish/defaults.py +40 -40
  118. stubber/publish/enums.py +24 -24
  119. stubber/publish/helpers.py +29 -29
  120. stubber/publish/merge_docstubs.py +136 -132
  121. stubber/publish/missing_class_methods.py +51 -51
  122. stubber/publish/package.py +150 -150
  123. stubber/publish/pathnames.py +51 -51
  124. stubber/publish/publish.py +120 -120
  125. stubber/publish/pypi.py +42 -42
  126. stubber/publish/stubpackage.py +1055 -1051
  127. stubber/rst/__init__.py +9 -9
  128. stubber/rst/classsort.py +78 -78
  129. stubber/rst/lookup.py +533 -531
  130. stubber/rst/output_dict.py +401 -401
  131. stubber/rst/reader.py +814 -814
  132. stubber/rst/report_return.py +77 -77
  133. stubber/rst/rst_utils.py +541 -541
  134. stubber/stubber.py +38 -38
  135. stubber/stubs_from_docs.py +90 -90
  136. stubber/tools/manifestfile.py +654 -654
  137. stubber/tools/readme.md +6 -6
  138. stubber/update_fallback.py +117 -117
  139. stubber/update_module_list.py +123 -123
  140. stubber/utils/__init__.py +6 -6
  141. stubber/utils/config.py +137 -137
  142. stubber/utils/makeversionhdr.py +54 -54
  143. stubber/utils/manifest.py +90 -90
  144. stubber/utils/post.py +80 -80
  145. stubber/utils/repos.py +156 -156
  146. stubber/utils/stubmaker.py +139 -139
  147. stubber/utils/typed_config_toml.py +80 -80
  148. stubber/variants.py +106 -106
  149. micropython_stubber-1.23.1.post1.dist-info/RECORD +0 -159
  150. mpflash/basicgit.py +0 -288
  151. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/WHEEL +0 -0
  152. {micropython_stubber-1.23.1.post1.dist-info → micropython_stubber-1.23.2.dist-info}/entry_points.txt +0 -0
@@ -1,266 +1,266 @@
1
- """
2
- Module to run mpremote commands, and retry on failure or timeout
3
- """
4
-
5
- import sys
6
- import time
7
- from pathlib import Path
8
- from typing import List, Optional, Union
9
-
10
- import serial.tools.list_ports
11
- from rich.progress import track
12
- from tenacity import retry, stop_after_attempt, wait_fixed
13
-
14
- from mpflash.errors import MPFlashError
15
- from mpflash.logger import log
16
- from mpflash.mpboard_id.board_id import find_board_id_by_description
17
- from mpflash.mpremoteboard.runner import run
18
-
19
- if sys.version_info >= (3, 11):
20
- import tomllib # type: ignore
21
- else:
22
- import tomli as tomllib # type: ignore
23
-
24
- ###############################################################################################
25
- HERE = Path(__file__).parent
26
-
27
- OK = 0
28
- ERROR = -1
29
- RETRIES = 3
30
- ###############################################################################################
31
-
32
-
33
- class MPRemoteBoard:
34
- """Class to run mpremote commands"""
35
-
36
- def __init__(self, serialport: str = "", update: bool = False, *, location: str = ""):
37
- """
38
- Initialize MPRemoteBoard object.
39
-
40
- Parameters:
41
- - serialport (str): The serial port to connect to. Default is an empty string.
42
- - update (bool): Whether to update the MCU information. Default is False.
43
- """
44
- self.serialport: str = serialport
45
- self.firmware = {}
46
-
47
- self.connected = False
48
- self.path: Optional[Path] = None
49
- self.family = "unknown"
50
- self.description = ""
51
- self.version = ""
52
- self.port = ""
53
- self.board = ""
54
- self.cpu = ""
55
- self.arch = ""
56
- self.mpy = ""
57
- self.build = ""
58
- self.location = location
59
- self.toml = {}
60
- if update:
61
- self.get_mcu_info()
62
-
63
- def __str__(self):
64
- """
65
- Return a string representation of the MPRemoteBoard object.
66
-
67
- Returns:
68
- - str: The string representation of the object.
69
- """
70
- return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}, {self.version})"
71
-
72
- @staticmethod
73
- def connected_boards(bluetooth: bool = False, description: bool = False) -> List[str]:
74
- # TODO: rename to connected_comports
75
- """
76
- Get a list of connected comports.
77
-
78
- Parameters:
79
- - bluetooth (bool): Whether to include Bluetooth ports. Default is False.
80
-
81
- Returns:
82
- - List[str]: A list of connected board ports.
83
- """
84
- comports = serial.tools.list_ports.comports()
85
-
86
- if not bluetooth:
87
- # filter out bluetooth ports
88
- comports = [p for p in comports if "bluetooth" not in p.description.lower()]
89
- comports = [p for p in comports if "BTHENUM" not in p.hwid]
90
- if description:
91
- output = [
92
- f"{p.device} {(p.manufacturer + ' ') if p.manufacturer and not p.description.startswith(p.manufacturer) else ''}{p.description}"
93
- for p in comports
94
- ]
95
- else:
96
- output = [p.device for p in comports]
97
-
98
- if sys.platform == "win32":
99
- # Windows sort of comports by number - but fallback to device name
100
- return sorted(output, key=lambda x: int(x.split()[0][3:]) if x.split()[0][3:].isdigit() else x)
101
- # sort by device name
102
- return sorted(output)
103
-
104
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
105
- def get_mcu_info(self, timeout: int = 2):
106
- """
107
- Get MCU information from the connected board.
108
-
109
- Parameters:
110
- - timeout (int): The timeout value in seconds. Default is 2.
111
-
112
- Raises:
113
- - ConnectionError: If failed to get mcu_info for the serial port.
114
- """
115
- rc, result = self.run_command(
116
- ["run", str(HERE / "mpy_fw_info.py")],
117
- no_info=True,
118
- timeout=timeout,
119
- resume=True, # Avoid restarts
120
- )
121
- if rc != OK:
122
- raise ConnectionError(f"Failed to get mcu_info for {self.serialport}")
123
- # Ok we have the info, now parse it
124
- raw_info = result[0].strip()
125
- if raw_info.startswith("{") and raw_info.endswith("}"):
126
- info = eval(raw_info)
127
- self.family = info["family"]
128
- self.version = info["version"]
129
- self.build = info["build"]
130
- self.port = info["port"]
131
- self.cpu = info["cpu"]
132
- self.arch = info["arch"]
133
- self.mpy = info["mpy"]
134
- self.description = descr = info["board"]
135
- pos = descr.rfind(" with")
136
- short_descr = descr[:pos].strip() if pos != -1 else ""
137
- if board_name := find_board_id_by_description(descr, short_descr, version=self.version):
138
- self.board = board_name
139
- else:
140
- self.board = "UNKNOWN_BOARD"
141
- # get the board_info.toml
142
- self.get_board_info_toml()
143
- # now we know the board is connected
144
- self.connected = True
145
-
146
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(0.2), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
147
- def get_board_info_toml(self, timeout: int = 1):
148
- """
149
- Reads the content of the board_info.toml file from the connected board,
150
- and adds that to the board object.
151
-
152
- Parameters:
153
- - timeout (int): The timeout value in seconds.
154
-
155
- Raises:
156
- - ConnectionError: If failed to communicate with the serial port.
157
- """
158
- try:
159
- rc, result = self.run_command(
160
- ["cat", ":board_info.toml"],
161
- no_info=True,
162
- timeout=timeout,
163
- log_errors=False,
164
- )
165
- except Exception as e:
166
- raise ConnectionError(f"Failed to get board_info.toml for {self.serialport}: {e}")
167
- # this is optional - so only parse if we got the file
168
- self.toml = {}
169
- if rc in [OK]: # sometimes we get an -9 ???
170
- try:
171
- # Ok we have the info, now parse it
172
- self.toml = tomllib.loads("".join(result))
173
- log.debug(f"board_info.toml: {self.toml}")
174
- except Exception as e:
175
- log.error(f"Failed to parse board_info.toml: {e}")
176
- else:
177
- log.trace(f"Failed to read board_info.toml: {result}")
178
-
179
- def disconnect(self) -> bool:
180
- """
181
- Disconnect from a board.
182
-
183
- Returns:
184
- - bool: True if successfully disconnected, False otherwise.
185
- """
186
- if not self.connected:
187
- return True
188
- if not self.serialport:
189
- log.error("No port connected")
190
- self.connected = False
191
- return False
192
- log.info(f"Disconnecting from {self.serialport}")
193
- result = self.run_command(["disconnect"])[0] == OK
194
- self.connected = False
195
- return result
196
-
197
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2), reraise=True)
198
- def run_command(
199
- self,
200
- cmd: Union[str, List[str]],
201
- *,
202
- log_errors: bool = True,
203
- no_info: bool = False,
204
- timeout: int = 60,
205
- resume: bool = False,
206
- **kwargs,
207
- ):
208
- """
209
- Run mpremote with the given command.
210
-
211
- Parameters:
212
- - cmd (Union[str, List[str]]): The command to run, either a string or a list of strings.
213
- - log_errors (bool): Whether to log errors. Default is True.
214
- - no_info (bool): Whether to skip printing info. Default is False.
215
- - timeout (int): The timeout value in seconds. Default is 60.
216
-
217
- Returns:
218
- - bool: True if the command succeeded, False otherwise.
219
- """
220
- if isinstance(cmd, str):
221
- cmd = cmd.split(" ")
222
- prefix = [sys.executable, "-m", "mpremote"]
223
- if self.serialport:
224
- prefix += ["connect", self.serialport]
225
- # if connected add resume to keep state between commands
226
- if self.connected or resume:
227
- prefix += ["resume"]
228
- cmd = prefix + cmd
229
- log.debug(" ".join(cmd))
230
- result = run(cmd, timeout, log_errors, no_info, **kwargs)
231
- self.connected = result[0] == OK
232
- return result
233
-
234
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1))
235
- def mip_install(self, name: str) -> bool:
236
- """
237
- Install a micropython package.
238
-
239
- Parameters:
240
- - name (str): The name of the package to install.
241
-
242
- Returns:
243
- - bool: True if the installation succeeded, False otherwise.
244
- """
245
- # install createstubs to the board
246
- cmd = ["mip", "install", name]
247
- result = self.run_command(cmd)[0] == OK
248
- self.connected = True
249
- return result
250
-
251
- def wait_for_restart(self, timeout: int = 10):
252
- """wait for the board to restart"""
253
- for _ in track(
254
- range(timeout),
255
- description=f"Waiting for the board to restart ({timeout}s)",
256
- transient=True,
257
- show_speed=False,
258
- refresh_per_second=1,
259
- total=timeout,
260
- ):
261
- time.sleep(1)
262
- try:
263
- self.get_mcu_info()
264
- break
265
- except (ConnectionError, MPFlashError):
266
- pass
1
+ """
2
+ Module to run mpremote commands, and retry on failure or timeout
3
+ """
4
+
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+ from typing import List, Optional, Union
9
+
10
+ import serial.tools.list_ports
11
+ from rich.progress import track
12
+ from tenacity import retry, stop_after_attempt, wait_fixed
13
+
14
+ from mpflash.errors import MPFlashError
15
+ from mpflash.logger import log
16
+ from mpflash.mpboard_id.board_id import find_board_id_by_description
17
+ from mpflash.mpremoteboard.runner import run
18
+
19
+ if sys.version_info >= (3, 11):
20
+ import tomllib # type: ignore
21
+ else:
22
+ import tomli as tomllib # type: ignore
23
+
24
+ ###############################################################################################
25
+ HERE = Path(__file__).parent
26
+
27
+ OK = 0
28
+ ERROR = -1
29
+ RETRIES = 3
30
+ ###############################################################################################
31
+
32
+
33
+ class MPRemoteBoard:
34
+ """Class to run mpremote commands"""
35
+
36
+ def __init__(self, serialport: str = "", update: bool = False, *, location: str = ""):
37
+ """
38
+ Initialize MPRemoteBoard object.
39
+
40
+ Parameters:
41
+ - serialport (str): The serial port to connect to. Default is an empty string.
42
+ - update (bool): Whether to update the MCU information. Default is False.
43
+ """
44
+ self.serialport: str = serialport
45
+ self.firmware = {}
46
+
47
+ self.connected = False
48
+ self.path: Optional[Path] = None
49
+ self.family = "unknown"
50
+ self.description = ""
51
+ self.version = ""
52
+ self.port = ""
53
+ self.board = ""
54
+ self.cpu = ""
55
+ self.arch = ""
56
+ self.mpy = ""
57
+ self.build = ""
58
+ self.location = location # USB location
59
+ self.toml = {}
60
+ if update:
61
+ self.get_mcu_info()
62
+
63
+ def __str__(self):
64
+ """
65
+ Return a string representation of the MPRemoteBoard object.
66
+
67
+ Returns:
68
+ - str: The string representation of the object.
69
+ """
70
+ return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}, {self.version})"
71
+
72
+ @staticmethod
73
+ def connected_boards(bluetooth: bool = False, description: bool = False) -> List[str]:
74
+ # TODO: rename to connected_comports
75
+ """
76
+ Get a list of connected comports.
77
+
78
+ Parameters:
79
+ - bluetooth (bool): Whether to include Bluetooth ports. Default is False.
80
+
81
+ Returns:
82
+ - List[str]: A list of connected board ports.
83
+ """
84
+ comports = serial.tools.list_ports.comports()
85
+
86
+ if not bluetooth:
87
+ # filter out bluetooth ports
88
+ comports = [p for p in comports if "bluetooth" not in p.description.lower()]
89
+ comports = [p for p in comports if "BTHENUM" not in p.hwid]
90
+ if description:
91
+ output = [
92
+ f"{p.device} {(p.manufacturer + ' ') if p.manufacturer and not p.description.startswith(p.manufacturer) else ''}{p.description}"
93
+ for p in comports
94
+ ]
95
+ else:
96
+ output = [p.device for p in comports]
97
+
98
+ if sys.platform == "win32":
99
+ # Windows sort of comports by number - but fallback to device name
100
+ return sorted(output, key=lambda x: int(x.split()[0][3:]) if x.split()[0][3:].isdigit() else x)
101
+ # sort by device name
102
+ return sorted(output)
103
+
104
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
105
+ def get_mcu_info(self, timeout: int = 2):
106
+ """
107
+ Get MCU information from the connected board.
108
+
109
+ Parameters:
110
+ - timeout (int): The timeout value in seconds. Default is 2.
111
+
112
+ Raises:
113
+ - ConnectionError: If failed to get mcu_info for the serial port.
114
+ """
115
+ rc, result = self.run_command(
116
+ ["run", str(HERE / "mpy_fw_info.py")],
117
+ no_info=True,
118
+ timeout=timeout,
119
+ resume=True, # Avoid restarts
120
+ )
121
+ if rc != OK:
122
+ raise ConnectionError(f"Failed to get mcu_info for {self.serialport}")
123
+ # Ok we have the info, now parse it
124
+ raw_info = result[0].strip()
125
+ if raw_info.startswith("{") and raw_info.endswith("}"):
126
+ info = eval(raw_info)
127
+ self.family = info["family"]
128
+ self.version = info["version"]
129
+ self.build = info["build"]
130
+ self.port = info["port"]
131
+ self.cpu = info["cpu"]
132
+ self.arch = info["arch"]
133
+ self.mpy = info["mpy"]
134
+ self.description = descr = info["board"]
135
+ pos = descr.rfind(" with")
136
+ short_descr = descr[:pos].strip() if pos != -1 else ""
137
+ if board_name := find_board_id_by_description(descr, short_descr, version=self.version):
138
+ self.board = board_name
139
+ else:
140
+ self.board = "UNKNOWN_BOARD"
141
+ # get the board_info.toml
142
+ self.get_board_info_toml()
143
+ # now we know the board is connected
144
+ self.connected = True
145
+
146
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(0.2), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
147
+ def get_board_info_toml(self, timeout: int = 1):
148
+ """
149
+ Reads the content of the board_info.toml file from the connected board,
150
+ and adds that to the board object.
151
+
152
+ Parameters:
153
+ - timeout (int): The timeout value in seconds.
154
+
155
+ Raises:
156
+ - ConnectionError: If failed to communicate with the serial port.
157
+ """
158
+ try:
159
+ rc, result = self.run_command(
160
+ ["cat", ":board_info.toml"],
161
+ no_info=True,
162
+ timeout=timeout,
163
+ log_errors=False,
164
+ )
165
+ except Exception as e:
166
+ raise ConnectionError(f"Failed to get board_info.toml for {self.serialport}: {e}")
167
+ # this is optional - so only parse if we got the file
168
+ self.toml = {}
169
+ if rc in [OK]: # sometimes we get an -9 ???
170
+ try:
171
+ # Ok we have the info, now parse it
172
+ self.toml = tomllib.loads("".join(result))
173
+ log.debug(f"board_info.toml: {self.toml}")
174
+ except Exception as e:
175
+ log.error(f"Failed to parse board_info.toml: {e}")
176
+ else:
177
+ log.trace(f"Failed to read board_info.toml: {result}")
178
+
179
+ def disconnect(self) -> bool:
180
+ """
181
+ Disconnect from a board.
182
+
183
+ Returns:
184
+ - bool: True if successfully disconnected, False otherwise.
185
+ """
186
+ if not self.connected:
187
+ return True
188
+ if not self.serialport:
189
+ log.error("No port connected")
190
+ self.connected = False
191
+ return False
192
+ log.info(f"Disconnecting from {self.serialport}")
193
+ result = self.run_command(["disconnect"])[0] == OK
194
+ self.connected = False
195
+ return result
196
+
197
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2), reraise=True)
198
+ def run_command(
199
+ self,
200
+ cmd: Union[str, List[str]],
201
+ *,
202
+ log_errors: bool = True,
203
+ no_info: bool = False,
204
+ timeout: int = 60,
205
+ resume: bool = False,
206
+ **kwargs,
207
+ ):
208
+ """
209
+ Run mpremote with the given command.
210
+
211
+ Parameters:
212
+ - cmd (Union[str, List[str]]): The command to run, either a string or a list of strings.
213
+ - log_errors (bool): Whether to log errors. Default is True.
214
+ - no_info (bool): Whether to skip printing info. Default is False.
215
+ - timeout (int): The timeout value in seconds. Default is 60.
216
+
217
+ Returns:
218
+ - bool: True if the command succeeded, False otherwise.
219
+ """
220
+ if isinstance(cmd, str):
221
+ cmd = cmd.split(" ")
222
+ prefix = [sys.executable, "-m", "mpremote"]
223
+ if self.serialport:
224
+ prefix += ["connect", self.serialport]
225
+ # if connected add resume to keep state between commands
226
+ if self.connected or resume:
227
+ prefix += ["resume"]
228
+ cmd = prefix + cmd
229
+ log.debug(" ".join(cmd))
230
+ result = run(cmd, timeout, log_errors, no_info, **kwargs)
231
+ self.connected = result[0] == OK
232
+ return result
233
+
234
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1))
235
+ def mip_install(self, name: str) -> bool:
236
+ """
237
+ Install a micropython package.
238
+
239
+ Parameters:
240
+ - name (str): The name of the package to install.
241
+
242
+ Returns:
243
+ - bool: True if the installation succeeded, False otherwise.
244
+ """
245
+ # install createstubs to the board
246
+ cmd = ["mip", "install", name]
247
+ result = self.run_command(cmd)[0] == OK
248
+ self.connected = True
249
+ return result
250
+
251
+ def wait_for_restart(self, timeout: int = 10):
252
+ """wait for the board to restart"""
253
+ for _ in track(
254
+ range(timeout),
255
+ description=f"Waiting for the board to restart ({timeout}s)",
256
+ transient=True,
257
+ show_speed=False,
258
+ refresh_per_second=1,
259
+ total=timeout,
260
+ ):
261
+ time.sleep(1)
262
+ try:
263
+ self.get_mcu_info()
264
+ break
265
+ except (ConnectionError, MPFlashError):
266
+ pass