mpflash 1.26.2__py3-none-any.whl → 1.26.4__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.
- mpflash/__init__.py +1 -0
- mpflash/ask_input.py +11 -4
- mpflash/cli_flash.py +28 -29
- mpflash/connected.py +0 -2
- mpflash/db/boards_version.txt +1 -0
- mpflash/db/core.py +2 -2
- mpflash/db/gather_boards.py +67 -17
- mpflash/db/loader.py +15 -2
- mpflash/db/micropython_boards.zip +0 -0
- mpflash/download/__init__.py +5 -9
- mpflash/download/jid.py +38 -44
- mpflash/flash/__init__.py +13 -13
- mpflash/flash/uf2/__init__.py +16 -5
- mpflash/flash/worklist.py +299 -132
- mpflash/logger.py +79 -5
- mpflash/mpremoteboard/__init__.py +10 -8
- mpflash/mpremoteboard/runner.py +15 -4
- {mpflash-1.26.2.dist-info → mpflash-1.26.4.dist-info}/METADATA +32 -6
- {mpflash-1.26.2.dist-info → mpflash-1.26.4.dist-info}/RECORD +22 -21
- {mpflash-1.26.2.dist-info → mpflash-1.26.4.dist-info}/WHEEL +0 -0
- {mpflash-1.26.2.dist-info → mpflash-1.26.4.dist-info}/entry_points.txt +0 -0
- {mpflash-1.26.2.dist-info → mpflash-1.26.4.dist-info}/licenses/LICENSE +0 -0
mpflash/flash/worklist.py
CHANGED
@@ -1,5 +1,30 @@
|
|
1
|
-
"""Worklist for updating boards
|
1
|
+
"""Worklist for updating boards.
|
2
2
|
|
3
|
+
This module provides functionality for creating worklists - collections of board-firmware
|
4
|
+
pairs that need to be flashed.
|
5
|
+
|
6
|
+
The API provides a clean, maintainable interface:
|
7
|
+
|
8
|
+
```python
|
9
|
+
from mpflash.flash.worklist import create_worklist, WorklistConfig
|
10
|
+
|
11
|
+
# Simple auto-detection
|
12
|
+
config = WorklistConfig.for_auto_detection("1.22.0")
|
13
|
+
tasks = create_auto_worklist(connected_comports, config)
|
14
|
+
|
15
|
+
# Or use the high-level function
|
16
|
+
tasks = create_worklist("1.22.0", connected_comports=boards)
|
17
|
+
|
18
|
+
# Manual board specification
|
19
|
+
tasks = create_worklist("1.22.0", serial_ports=["COM1"], board_id="ESP32_GENERIC")
|
20
|
+
|
21
|
+
# Filtered boards
|
22
|
+
tasks = create_worklist("1.22.0", connected_comports=all_boards, include_ports=["COM*"])
|
23
|
+
```
|
24
|
+
|
25
|
+
"""
|
26
|
+
|
27
|
+
from dataclasses import dataclass
|
3
28
|
from typing import List, Optional, Tuple
|
4
29
|
|
5
30
|
from loguru import logger as log
|
@@ -15,170 +40,312 @@ from mpflash.mpboard_id import find_known_board
|
|
15
40
|
from mpflash.mpremoteboard import MPRemoteBoard
|
16
41
|
|
17
42
|
# #########################################################################################################
|
18
|
-
|
19
|
-
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass
|
46
|
+
class FlashTask:
|
47
|
+
"""Represents a single board-firmware flashing task."""
|
48
|
+
|
49
|
+
board: MPRemoteBoard
|
50
|
+
firmware: Optional[Firmware]
|
51
|
+
|
52
|
+
@property
|
53
|
+
def is_valid(self) -> bool:
|
54
|
+
"""Check if the task has both board and firmware."""
|
55
|
+
return self.firmware is not None
|
56
|
+
|
57
|
+
@property
|
58
|
+
def board_id(self) -> str:
|
59
|
+
"""Get the board ID for this task."""
|
60
|
+
return self.board.board_id
|
61
|
+
|
62
|
+
@property
|
63
|
+
def firmware_version(self) -> str:
|
64
|
+
"""Get the firmware version for this task."""
|
65
|
+
return self.firmware.version if self.firmware else "unknown"
|
66
|
+
|
67
|
+
|
68
|
+
@dataclass
|
69
|
+
class WorklistConfig:
|
70
|
+
"""Configuration for creating worklists."""
|
71
|
+
|
72
|
+
version: str
|
73
|
+
include_ports: Optional[List[str]] = None
|
74
|
+
ignore_ports: Optional[List[str]] = None
|
75
|
+
board_id: Optional[str] = None
|
76
|
+
custom_firmware: bool = False
|
77
|
+
|
78
|
+
def __post_init__(self):
|
79
|
+
if self.include_ports is None:
|
80
|
+
self.include_ports = []
|
81
|
+
if self.ignore_ports is None:
|
82
|
+
self.ignore_ports = []
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def for_auto_detection(cls, version: str) -> "WorklistConfig":
|
86
|
+
"""Create config for automatic board detection."""
|
87
|
+
return cls(version=version)
|
88
|
+
|
89
|
+
@classmethod
|
90
|
+
def for_manual_boards(cls, version: str, board_id: str, custom_firmware: bool = False) -> "WorklistConfig":
|
91
|
+
"""Create config for manually specified boards."""
|
92
|
+
return cls(version=version, board_id=board_id, custom_firmware=custom_firmware)
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def for_filtered_boards(
|
96
|
+
cls, version: str, include_ports: Optional[List[str]] = None, ignore_ports: Optional[List[str]] = None
|
97
|
+
) -> "WorklistConfig":
|
98
|
+
"""Create config for filtered board selection."""
|
99
|
+
return cls(version=version, include_ports=include_ports or [], ignore_ports=ignore_ports or [])
|
100
|
+
|
101
|
+
|
102
|
+
FlashTaskList: TypeAlias = List[FlashTask]
|
103
|
+
|
20
104
|
# #########################################################################################################
|
21
105
|
|
22
106
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
) -> WorkList:
|
27
|
-
"""Builds a list of boards to update based on the connected boards and the firmwares available locally in the firmware folder.
|
107
|
+
def _create_flash_task(board: MPRemoteBoard, firmware: Optional[Firmware]) -> FlashTask:
|
108
|
+
"""Create a FlashTask from board and firmware."""
|
109
|
+
return FlashTask(board=board, firmware=firmware)
|
28
110
|
|
29
|
-
Args:
|
30
|
-
conn_boards (List[MPRemoteBoard]): List of connected boards
|
31
|
-
target_version (str): Target firmware version
|
32
|
-
selector (Optional[Dict[str, str]], optional): Selector for filtering firmware. Defaults to None.
|
33
111
|
|
34
|
-
|
35
|
-
|
36
|
-
""
|
37
|
-
|
38
|
-
wl: WorkList = []
|
39
|
-
for mcu in conn_boards:
|
40
|
-
if mcu.family not in ("micropython", "unknown"):
|
41
|
-
log.warning(f"Skipping flashing {mcu.family} {mcu.port} {mcu.board} on {mcu.serialport} as it is not a MicroPython firmware")
|
42
|
-
continue
|
43
|
-
board_firmwares = find_downloaded_firmware(
|
44
|
-
board_id=f"{mcu.board}-{mcu.variant}" if mcu.variant else mcu.board,
|
45
|
-
version=target_version,
|
46
|
-
port=mcu.port,
|
47
|
-
)
|
48
|
-
|
49
|
-
if not board_firmwares:
|
50
|
-
log.warning(f"No {target_version} firmware found for {mcu.board} on {mcu.serialport}.")
|
51
|
-
wl.append((mcu, None))
|
52
|
-
continue
|
112
|
+
def _find_firmware_for_board(board: MPRemoteBoard, version: str, custom: bool = False) -> Optional[Firmware]:
|
113
|
+
"""Find appropriate firmware for a board."""
|
114
|
+
board_id = f"{board.board}-{board.variant}" if board.variant else board.board
|
115
|
+
firmwares = find_downloaded_firmware(board_id=board_id, version=version, port=board.port, custom=custom)
|
53
116
|
|
54
|
-
|
55
|
-
|
117
|
+
if not firmwares:
|
118
|
+
log.warning(f"No {version} firmware found for {board.board} on {board.serialport}.")
|
119
|
+
return None
|
56
120
|
|
57
|
-
|
58
|
-
|
59
|
-
log.info(f"Found {target_version} firmware {fw_info.firmware_file} for {mcu.board} on {mcu.serialport}.")
|
60
|
-
wl.append((mcu, fw_info))
|
61
|
-
return wl
|
121
|
+
if len(firmwares) > 1:
|
122
|
+
log.warning(f"Multiple {version} firmwares found for {board.board} on {board.serialport}.")
|
62
123
|
|
124
|
+
# Use the most recent matching firmware
|
125
|
+
firmware = firmwares[-1]
|
126
|
+
log.info(f"Found {version} firmware {firmware.firmware_file} for {board.board} on {board.serialport}.")
|
127
|
+
return firmware
|
63
128
|
|
64
|
-
def manual_worklist(
|
65
|
-
serial: List[str],
|
66
|
-
*,
|
67
|
-
board_id: str,
|
68
|
-
version: str,
|
69
|
-
custom: bool = False,
|
70
|
-
) -> WorkList:
|
71
|
-
"""Create a worklist for manually specified boards."""
|
72
|
-
log.debug(f"manual_worklist: {len(serial)} serial ports, board_id: {board_id}, version: {version}")
|
73
|
-
wl: WorkList = []
|
74
|
-
for comport in serial:
|
75
|
-
log.trace(f"Manual updating {comport} to {board_id} {version}")
|
76
|
-
wl.append(manual_board(comport, board_id=board_id, version=version, custom=custom))
|
77
|
-
return wl
|
78
|
-
|
79
|
-
|
80
|
-
def manual_board(
|
81
|
-
serial: str,
|
82
|
-
*,
|
83
|
-
board_id: str,
|
84
|
-
version: str,
|
85
|
-
custom: bool = False,
|
86
|
-
) -> FlashItem:
|
87
|
-
"""Create a Flash work item for a single board specified manually.
|
88
129
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
version (str): Firmware version
|
130
|
+
def _create_manual_board(serial_port: str, board_id: str, version: str, custom: bool = False) -> FlashTask:
|
131
|
+
"""Create a FlashTask for manually specified board parameters."""
|
132
|
+
log.debug(f"Creating manual board task: {serial_port} {board_id} {version}")
|
93
133
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
log.debug(f"manual_board: {serial} {board_id} {version}")
|
98
|
-
mcu = MPRemoteBoard(serial)
|
99
|
-
# Lookup the matching port and cpu in board_info based in the board name
|
134
|
+
board = MPRemoteBoard(serial_port)
|
135
|
+
|
136
|
+
# Look up board information
|
100
137
|
try:
|
101
138
|
info = find_known_board(board_id)
|
102
|
-
|
103
|
-
#
|
104
|
-
mcu.cpu = info.mcu
|
139
|
+
board.port = info.port
|
140
|
+
board.cpu = info.mcu # Need CPU type for esptool
|
105
141
|
except (LookupError, MPFlashError) as e:
|
106
142
|
log.error(f"Board {board_id} not found in board database")
|
107
143
|
log.exception(e)
|
108
|
-
return (
|
109
|
-
mcu.board = board_id
|
110
|
-
firmwares = find_downloaded_firmware(board_id=board_id, version=version, port=mcu.port, custom=custom)
|
111
|
-
if not firmwares:
|
112
|
-
log.trace(f"No firmware found for {mcu.port} {board_id} version {version}")
|
113
|
-
return (mcu, None)
|
114
|
-
# use the most recent matching firmware
|
115
|
-
return (mcu, firmwares[-1]) # type: ignore
|
144
|
+
return _create_flash_task(board, None)
|
116
145
|
|
146
|
+
board.board = board_id
|
147
|
+
firmware = _find_firmware_for_board(board, version, custom)
|
148
|
+
return _create_flash_task(board, firmware)
|
117
149
|
|
118
|
-
|
119
|
-
|
120
|
-
|
150
|
+
|
151
|
+
def _filter_connected_comports(
|
152
|
+
all_boards: List[MPRemoteBoard],
|
153
|
+
include: List[str],
|
154
|
+
ignore: List[str],
|
155
|
+
) -> List[MPRemoteBoard]:
|
156
|
+
"""Filter connected boards based on include/ignore patterns."""
|
157
|
+
try:
|
158
|
+
allowed_ports = [
|
159
|
+
p.device
|
160
|
+
for p in filtered_portinfos(
|
161
|
+
ignore=ignore,
|
162
|
+
include=include,
|
163
|
+
bluetooth=False,
|
164
|
+
)
|
165
|
+
]
|
166
|
+
return [board for board in all_boards if board.serialport in allowed_ports]
|
167
|
+
except ConnectionError as e:
|
168
|
+
log.error(f"Error connecting to boards: {e}")
|
169
|
+
return []
|
170
|
+
|
171
|
+
|
172
|
+
# #########################################################################################################
|
173
|
+
|
174
|
+
|
175
|
+
# High-level API functions
|
176
|
+
# #########################################################################################################
|
177
|
+
|
178
|
+
|
179
|
+
def create_worklist(
|
121
180
|
version: str,
|
122
|
-
|
123
|
-
|
181
|
+
*,
|
182
|
+
connected_comports: Optional[List[MPRemoteBoard]] = None,
|
183
|
+
serial_ports: Optional[List[str]] = None,
|
184
|
+
board_id: Optional[str] = None,
|
185
|
+
include_ports: Optional[List[str]] = None,
|
186
|
+
ignore_ports: Optional[List[str]] = None,
|
187
|
+
custom_firmware: bool = False,
|
188
|
+
) -> FlashTaskList:
|
189
|
+
"""High-level function to create a worklist based on different scenarios.
|
190
|
+
|
191
|
+
This function automatically determines the appropriate worklist creation method
|
192
|
+
based on the provided parameters.
|
124
193
|
|
125
194
|
Args:
|
126
|
-
|
127
|
-
|
195
|
+
version: Target firmware version
|
196
|
+
connected_comports: Pre-detected connected boards (for auto mode)
|
197
|
+
serial_ports: Specific serial ports to use (for manual mode)
|
198
|
+
board_id: Board ID to use with serial_ports (required for manual mode)
|
199
|
+
include_ports: Port patterns to include (for filtered mode)
|
200
|
+
ignore_ports: Port patterns to ignore (for filtered mode)
|
201
|
+
custom_firmware: Whether to use custom firmware
|
128
202
|
|
129
203
|
Returns:
|
130
|
-
|
204
|
+
List of FlashTask objects
|
205
|
+
|
206
|
+
Raises:
|
207
|
+
ValueError: If parameters are inconsistent or missing required values
|
208
|
+
|
209
|
+
Examples:
|
210
|
+
# Auto-detect firmware for connected boards
|
211
|
+
tasks = create_worklist("1.22.0", connected_comports=boards)
|
212
|
+
|
213
|
+
# Manual specification
|
214
|
+
tasks = create_worklist("1.22.0", serial_ports=["COM1"], board_id="ESP32_GENERIC")
|
215
|
+
|
216
|
+
# Filtered boards
|
217
|
+
tasks = create_worklist("1.22.0", connected_comports=all_boards, include_ports=["COM*"])
|
131
218
|
"""
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
show_mcus(conn_boards)
|
137
|
-
return todo
|
219
|
+
# Manual mode: specific serial ports with board_id
|
220
|
+
if serial_ports and board_id:
|
221
|
+
config = WorklistConfig.for_manual_boards(version, board_id, custom_firmware)
|
222
|
+
return create_manual_worklist(serial_ports, config)
|
138
223
|
|
224
|
+
# Auto mode with filtering
|
225
|
+
if connected_comports and (include_ports or ignore_ports):
|
226
|
+
config = WorklistConfig.for_filtered_boards(version, include_ports, ignore_ports)
|
227
|
+
return create_filtered_worklist(connected_comports, config)
|
139
228
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
229
|
+
# Simple auto mode
|
230
|
+
if connected_comports:
|
231
|
+
config = WorklistConfig.for_auto_detection(version)
|
232
|
+
return create_auto_worklist(connected_comports, config)
|
233
|
+
|
234
|
+
# Error cases
|
235
|
+
if serial_ports and not board_id:
|
236
|
+
raise ValueError("board_id is required when specifying serial_ports for manual mode")
|
237
|
+
|
238
|
+
if not connected_comports and not serial_ports:
|
239
|
+
raise ValueError("Either connected_comports or serial_ports must be provided")
|
240
|
+
|
241
|
+
raise ValueError("Invalid combination of parameters")
|
242
|
+
|
243
|
+
|
244
|
+
# New, simplified API functions
|
245
|
+
# #########################################################################################################
|
246
|
+
|
247
|
+
|
248
|
+
def create_auto_worklist(
|
249
|
+
connected_comports: List[MPRemoteBoard],
|
250
|
+
config: WorklistConfig,
|
251
|
+
) -> FlashTaskList:
|
252
|
+
"""Create a worklist by automatically detecting firmware for connected boards.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
connected_comports: List of connected MicroPython boards
|
256
|
+
config: Configuration for the worklist creation
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
List of FlashTask objects
|
147
260
|
"""
|
148
|
-
|
149
|
-
|
261
|
+
log.debug(f"Creating auto worklist for {len(connected_comports)} boards, target version: {config.version}")
|
262
|
+
|
263
|
+
tasks: FlashTaskList = []
|
264
|
+
for board in connected_comports:
|
265
|
+
if board.family not in ("micropython", "unknown"):
|
266
|
+
log.warning(
|
267
|
+
f"Skipping flashing {board.family} {board.port} {board.board} on {board.serialport} as it is not a MicroPython firmware"
|
268
|
+
)
|
269
|
+
continue
|
270
|
+
|
271
|
+
firmware = _find_firmware_for_board(board, config.version, config.custom_firmware)
|
272
|
+
tasks.append(_create_flash_task(board, firmware))
|
273
|
+
|
274
|
+
return tasks
|
275
|
+
|
276
|
+
|
277
|
+
def create_manual_worklist(
|
278
|
+
serial_ports: List[str],
|
279
|
+
config: WorklistConfig,
|
280
|
+
) -> FlashTaskList:
|
281
|
+
"""Create a worklist for manually specified boards and firmware.
|
150
282
|
|
151
283
|
Args:
|
152
|
-
|
284
|
+
serial_ports: List of serial port identifiers
|
285
|
+
config: Configuration including board_id and version
|
153
286
|
|
154
287
|
Returns:
|
155
|
-
|
288
|
+
List of FlashTask objects
|
156
289
|
"""
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
290
|
+
if not config.board_id:
|
291
|
+
raise ValueError("board_id must be specified for manual worklist creation")
|
292
|
+
|
293
|
+
log.debug(f"Creating manual worklist for {len(serial_ports)} ports, board_id: {config.board_id}, version: {config.version}")
|
294
|
+
|
295
|
+
tasks: FlashTaskList = []
|
296
|
+
for port in serial_ports:
|
297
|
+
log.trace(f"Manual updating {port} to {config.board_id} {config.version}")
|
298
|
+
task = _create_manual_board(port, config.board_id, config.version, config.custom_firmware)
|
299
|
+
tasks.append(task)
|
162
300
|
|
301
|
+
return tasks
|
163
302
|
|
164
|
-
|
303
|
+
|
304
|
+
def create_filtered_worklist(
|
165
305
|
all_boards: List[MPRemoteBoard],
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
306
|
+
config: WorklistConfig,
|
307
|
+
) -> FlashTaskList:
|
308
|
+
"""Create a worklist for filtered connected boards.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
all_boards: All available connected boards
|
312
|
+
config: Configuration including include/ignore patterns and version
|
313
|
+
|
314
|
+
Returns:
|
315
|
+
List of FlashTask objects
|
316
|
+
"""
|
317
|
+
log.debug(
|
318
|
+
f"Creating filtered worklist from {len(all_boards)} boards, include: {config.include_ports}, ignore: {config.ignore_ports}, version: {config.version}"
|
319
|
+
)
|
320
|
+
|
321
|
+
filtered_boards = _filter_connected_comports(all_boards, config.include_ports or [], config.ignore_ports or [])
|
322
|
+
if not filtered_boards:
|
323
|
+
log.warning("No boards match the filtering criteria")
|
183
324
|
return []
|
184
|
-
|
325
|
+
|
326
|
+
return create_auto_worklist(filtered_boards, config)
|
327
|
+
|
328
|
+
|
329
|
+
def create_single_board_worklist(
|
330
|
+
serial_port: str,
|
331
|
+
config: WorklistConfig,
|
332
|
+
) -> FlashTaskList:
|
333
|
+
"""Create a worklist for a single serial port with automatic detection.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
serial_port: Serial port identifier
|
337
|
+
config: Configuration with version information
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
List of FlashTask objects (typically containing one item)
|
341
|
+
"""
|
342
|
+
log.debug(f"Creating single board worklist: {serial_port} version: {config.version}")
|
343
|
+
log.trace(f"Auto updating {serial_port} to {config.version}")
|
344
|
+
|
345
|
+
connected_comports = [MPRemoteBoard(serial_port)]
|
346
|
+
tasks = create_auto_worklist(connected_comports, config)
|
347
|
+
show_mcus(connected_comports)
|
348
|
+
return tasks
|
349
|
+
|
350
|
+
|
351
|
+
# End of worklist.py module
|
mpflash/logger.py
CHANGED
@@ -3,6 +3,14 @@ Logger setup for CLI tools with Unicode-safe output.
|
|
3
3
|
|
4
4
|
Ensures log messages are compatible with the current console encoding.
|
5
5
|
Removes or replaces Unicode icons if the encoding is not UTF-8.
|
6
|
+
Prevents Loguru colorization errors with angle bracket notation.
|
7
|
+
|
8
|
+
Usage for external packages:
|
9
|
+
from mpflash.logger import setup_external_logger_safety
|
10
|
+
setup_external_logger_safety()
|
11
|
+
|
12
|
+
This is particularly important when using packages like micropython-stubber
|
13
|
+
that may log messages containing angle bracket notation like <board_default>.
|
6
14
|
"""
|
7
15
|
|
8
16
|
import functools
|
@@ -28,12 +36,23 @@ def _is_utf8_encoding() -> bool:
|
|
28
36
|
return False
|
29
37
|
|
30
38
|
|
31
|
-
def
|
39
|
+
def _sanitize_message(message: str) -> str:
|
40
|
+
"""
|
41
|
+
Sanitize log messages to prevent Loguru colorization issues.
|
42
|
+
|
43
|
+
Escapes angle brackets that could be interpreted as color tags.
|
44
|
+
This prevents errors when logging documentation with placeholders like <board_default>.
|
45
|
+
"""
|
46
|
+
# Escape angle brackets to prevent them from being interpreted as color tags
|
47
|
+
return message.replace("<", "\\<").replace(">", "\\>")
|
48
|
+
|
49
|
+
|
50
|
+
def _log_formatter(record) -> str:
|
32
51
|
"""
|
33
52
|
Log message formatter for loguru and rich.
|
34
53
|
|
35
54
|
Removes Unicode icons if console encoding is not UTF-8.
|
36
|
-
Handles messages containing curly braces safely.
|
55
|
+
Handles messages containing curly braces and angle brackets safely.
|
37
56
|
"""
|
38
57
|
color_map = {
|
39
58
|
"TRACE": "cyan",
|
@@ -50,8 +69,12 @@ def _log_formatter(record: dict) -> str:
|
|
50
69
|
icon = record["level"].icon
|
51
70
|
else:
|
52
71
|
icon = record["level"].name # fallback to text
|
53
|
-
|
54
|
-
|
72
|
+
|
73
|
+
# Sanitize the message to prevent format conflicts and colorization errors
|
74
|
+
safe_message = _sanitize_message(record["message"])
|
75
|
+
# Escape curly braces to prevent format conflicts
|
76
|
+
safe_message = safe_message.replace("{", "{{").replace("}", "}}")
|
77
|
+
|
55
78
|
# Use string concatenation to avoid f-string format conflicts
|
56
79
|
time_part = "[not bold green]{time:HH:mm:ss}[/not bold green]"
|
57
80
|
message_part = f"[{lvl_color}]{safe_message}[/{lvl_color}]"
|
@@ -63,6 +86,7 @@ def set_loglevel(loglevel: str) -> None:
|
|
63
86
|
Set the log level for the logger.
|
64
87
|
|
65
88
|
Ensures Unicode safety for log output and handles format errors.
|
89
|
+
Disables colorization to prevent angle bracket interpretation issues.
|
66
90
|
"""
|
67
91
|
try:
|
68
92
|
log.remove()
|
@@ -77,7 +101,57 @@ def set_loglevel(loglevel: str) -> None:
|
|
77
101
|
# Fallback to simple text output if formatting fails
|
78
102
|
console.print(f"[LOG FORMAT ERROR] {message} (Error: {e})")
|
79
103
|
|
80
|
-
|
104
|
+
# Disable colorization completely to prevent angle bracket interpretation as color tags
|
105
|
+
log.add(
|
106
|
+
safe_format_wrapper,
|
107
|
+
level=loglevel.upper(),
|
108
|
+
colorize=False, # This prevents Loguru from parsing angle brackets as color tags
|
109
|
+
format=_log_formatter,
|
110
|
+
) # type: ignore
|
111
|
+
|
112
|
+
|
113
|
+
# def configure_safe_logging() -> None:
|
114
|
+
# """
|
115
|
+
# Configure logging to be safe from colorization errors.
|
116
|
+
|
117
|
+
# This function helps prevent issues when external packages
|
118
|
+
# (like micropython-stubber) log messages with angle brackets
|
119
|
+
# that could be misinterpreted as color tags.
|
120
|
+
# """
|
121
|
+
# # Remove all existing handlers to start fresh
|
122
|
+
# try:
|
123
|
+
# log.remove()
|
124
|
+
# except ValueError:
|
125
|
+
# pass
|
126
|
+
|
127
|
+
# # Add a completely safe handler with no colorization
|
128
|
+
# log.add(
|
129
|
+
# sys.stderr,
|
130
|
+
# level="TRACE",
|
131
|
+
# colorize=False, # Completely disable colorization
|
132
|
+
# format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
|
133
|
+
# )
|
134
|
+
|
135
|
+
|
136
|
+
# def setup_external_logger_safety() -> None:
|
137
|
+
# """
|
138
|
+
# Setup safe logging configuration for external packages.
|
139
|
+
|
140
|
+
# Call this function before running tools that might log messages
|
141
|
+
# with angle bracket notation (like micropython-stubber) to prevent
|
142
|
+
# Loguru colorization errors.
|
143
|
+
# """
|
144
|
+
# import logging
|
145
|
+
|
146
|
+
# # Configure the root logger to be safe
|
147
|
+
# logging.basicConfig(
|
148
|
+
# level=logging.DEBUG,
|
149
|
+
# format="%(asctime)s | %(levelname)s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
|
150
|
+
# handlers=[logging.StreamHandler(sys.stderr)],
|
151
|
+
# )
|
152
|
+
|
153
|
+
# # Also configure loguru for safety
|
154
|
+
# configure_safe_logging()
|
81
155
|
|
82
156
|
|
83
157
|
def make_quiet() -> None:
|
@@ -10,13 +10,12 @@ from pathlib import Path
|
|
10
10
|
from typing import List, Optional, Union
|
11
11
|
|
12
12
|
import serial.tools.list_ports
|
13
|
-
from rich.progress import track
|
14
|
-
from tenacity import retry, stop_after_attempt, wait_fixed
|
15
|
-
|
16
13
|
from mpflash.errors import MPFlashError
|
17
14
|
from mpflash.logger import log
|
18
15
|
from mpflash.mpboard_id.board_id import find_board_id_by_description
|
19
16
|
from mpflash.mpremoteboard.runner import run
|
17
|
+
from rich.progress import track
|
18
|
+
from tenacity import retry, stop_after_attempt, wait_fixed
|
20
19
|
|
21
20
|
if sys.version_info >= (3, 11):
|
22
21
|
import tomllib # type: ignore
|
@@ -64,13 +63,17 @@ class MPRemoteBoard:
|
|
64
63
|
self.build = ""
|
65
64
|
self.location = location # USB location
|
66
65
|
self.toml = {}
|
67
|
-
portinfo = list(serial.tools.list_ports.grep(serialport))
|
66
|
+
portinfo = list(serial.tools.list_ports.grep(serialport))
|
68
67
|
if not portinfo or len(portinfo) != 1:
|
69
68
|
self.vid = 0x00
|
70
69
|
self.pid = 0x00
|
71
70
|
else:
|
72
|
-
|
73
|
-
|
71
|
+
try:
|
72
|
+
self.vid = portinfo[0].vid # type: ignore
|
73
|
+
self.pid = portinfo[0].pid # type: ignore
|
74
|
+
except Exception:
|
75
|
+
self.vid = 0x00
|
76
|
+
self.pid = 0x00
|
74
77
|
if update:
|
75
78
|
self.get_mcu_info()
|
76
79
|
|
@@ -125,7 +128,7 @@ class MPRemoteBoard:
|
|
125
128
|
return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}{f'-{self.variant}' if self.variant else ''}, {self.version})"
|
126
129
|
|
127
130
|
@staticmethod
|
128
|
-
def
|
131
|
+
def connected_comports(
|
129
132
|
bluetooth: bool = False, description: bool = False
|
130
133
|
) -> List[str]:
|
131
134
|
# TODO: rename to connected_comports
|
@@ -206,7 +209,6 @@ class MPRemoteBoard:
|
|
206
209
|
descr, short_descr, version=self.version
|
207
210
|
)
|
208
211
|
self.board_id = board_name or "UNKNOWN_BOARD"
|
209
|
-
# TODO: Get the variant as well
|
210
212
|
# get the board_info.toml
|
211
213
|
self.get_board_info_toml()
|
212
214
|
# TODO: get board_id from the toml file if it exists
|