micropython-stubber 1.17.5__py3-none-any.whl → 1.19.0__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.
- {micropython_stubber-1.17.5.dist-info → micropython_stubber-1.19.0.dist-info}/METADATA +7 -6
- {micropython_stubber-1.17.5.dist-info → micropython_stubber-1.19.0.dist-info}/RECORD +71 -52
- mpflash/README.md +22 -3
- mpflash/libusb_flash.ipynb +203 -0
- mpflash/mpflash/ask_input.py +234 -0
- mpflash/mpflash/cli_download.py +107 -0
- mpflash/mpflash/cli_flash.py +165 -0
- mpflash/mpflash/cli_group.py +41 -8
- mpflash/mpflash/cli_list.py +41 -0
- mpflash/mpflash/cli_main.py +13 -8
- mpflash/mpflash/common.py +33 -122
- mpflash/mpflash/config.py +9 -0
- mpflash/mpflash/{downloader.py → download.py} +112 -120
- mpflash/mpflash/downloaded.py +108 -0
- mpflash/mpflash/errors.py +5 -0
- mpflash/mpflash/flash.py +69 -0
- mpflash/mpflash/flash_esp.py +17 -23
- mpflash/mpflash/flash_stm32.py +16 -113
- mpflash/mpflash/flash_stm32_cube.py +111 -0
- mpflash/mpflash/flash_stm32_dfu.py +101 -0
- mpflash/mpflash/flash_uf2.py +8 -8
- mpflash/mpflash/flash_uf2_linux.py +25 -12
- mpflash/mpflash/flash_uf2_windows.py +24 -12
- mpflash/mpflash/list.py +34 -37
- mpflash/mpflash/logger.py +12 -13
- mpflash/mpflash/mpboard_id/__init__.py +96 -0
- mpflash/mpflash/mpboard_id/board_id.py +63 -0
- mpflash/mpflash/mpboard_id/board_info.csv +2213 -0
- mpflash/mpflash/mpboard_id/board_info.json +19910 -0
- mpflash/mpflash/mpremoteboard/__init__.py +208 -0
- mpflash/mpflash/mpremoteboard/mpy_fw_info.py +141 -0
- {stubber/bulk → mpflash/mpflash/mpremoteboard}/runner.py +22 -5
- mpflash/mpflash/vendor/dfu.py +164 -0
- mpflash/mpflash/vendor/pydfu.py +605 -0
- mpflash/mpflash/vendor/readme.md +3 -0
- mpflash/mpflash/vendor/versions.py +113 -0
- mpflash/mpflash/worklist.py +147 -0
- mpflash/poetry.lock +411 -595
- mpflash/pyproject.toml +24 -8
- mpflash/stm32_udev_rules.md +63 -0
- stubber/__init__.py +1 -1
- stubber/basicgit.py +1 -0
- stubber/board/createstubs.py +10 -4
- stubber/board/createstubs_db.py +11 -5
- stubber/board/createstubs_db_min.py +61 -58
- stubber/board/createstubs_db_mpy.mpy +0 -0
- stubber/board/createstubs_mem.py +11 -5
- stubber/board/createstubs_mem_min.py +56 -53
- stubber/board/createstubs_mem_mpy.mpy +0 -0
- stubber/board/createstubs_min.py +54 -51
- stubber/board/createstubs_mpy.mpy +0 -0
- stubber/bulk/mcu_stubber.py +9 -5
- stubber/codemod/_partials/db_main.py +14 -25
- stubber/codemod/_partials/lvgl_main.py +2 -2
- stubber/codemod/board.py +10 -3
- stubber/commands/clone_cmd.py +7 -7
- stubber/commands/config_cmd.py +3 -0
- stubber/freeze/get_frozen.py +0 -2
- stubber/publish/candidates.py +1 -1
- stubber/publish/package.py +1 -1
- stubber/publish/pathnames.py +1 -1
- stubber/publish/stubpackage.py +1 -0
- stubber/rst/lookup.py +1 -1
- stubber/tools/manifestfile.py +5 -3
- stubber/utils/config.py +26 -36
- stubber/utils/repos.py +2 -2
- stubber/utils/versions.py +1 -0
- mpflash/mpflash/flasher.py +0 -287
- stubber/bulk/board_id.py +0 -40
- stubber/bulk/mpremoteboard.py +0 -141
- {micropython_stubber-1.17.5.dist-info → micropython_stubber-1.19.0.dist-info}/LICENSE +0 -0
- {micropython_stubber-1.17.5.dist-info → micropython_stubber-1.19.0.dist-info}/WHEEL +0 -0
- {micropython_stubber-1.17.5.dist-info → micropython_stubber-1.19.0.dist-info}/entry_points.txt +0 -0
- /mpflash/mpflash/{uf2_boardid.py → flash_uf2_boardid.py} +0 -0
@@ -0,0 +1,208 @@
|
|
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 loguru import logger as log
|
12
|
+
from rich.progress import track
|
13
|
+
from tenacity import retry, stop_after_attempt, wait_fixed
|
14
|
+
|
15
|
+
from mpflash.errors import MPFlashError
|
16
|
+
from mpflash.mpboard_id.board_id import find_board_id
|
17
|
+
from mpflash.mpremoteboard.runner import run
|
18
|
+
|
19
|
+
###############################################################################################
|
20
|
+
# TODO : make this a bit nicer
|
21
|
+
HERE = Path(__file__).parent
|
22
|
+
|
23
|
+
OK = 0
|
24
|
+
ERROR = -1
|
25
|
+
RETRIES = 3
|
26
|
+
###############################################################################################
|
27
|
+
|
28
|
+
|
29
|
+
class MPRemoteBoard:
|
30
|
+
"""Class to run mpremote commands"""
|
31
|
+
|
32
|
+
def __init__(self, serialport: str = "", update: bool = False):
|
33
|
+
"""
|
34
|
+
Initialize MPRemoteBoard object.
|
35
|
+
|
36
|
+
Parameters:
|
37
|
+
- serialport (str): The serial port to connect to. Default is an empty string.
|
38
|
+
- update (bool): Whether to update the MCU information. Default is False.
|
39
|
+
"""
|
40
|
+
self.serialport = serialport
|
41
|
+
self.firmware = {}
|
42
|
+
|
43
|
+
self.connected = False
|
44
|
+
self.path: Optional[Path] = None
|
45
|
+
self.family = "unknown"
|
46
|
+
self.description = ""
|
47
|
+
self.version = ""
|
48
|
+
self.port = ""
|
49
|
+
self.board = ""
|
50
|
+
self.cpu = ""
|
51
|
+
self.arch = ""
|
52
|
+
self.mpy = ""
|
53
|
+
self.build = ""
|
54
|
+
if update:
|
55
|
+
self.get_mcu_info()
|
56
|
+
|
57
|
+
def __str__(self):
|
58
|
+
"""
|
59
|
+
Return a string representation of the MPRemoteBoard object.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
- str: The string representation of the object.
|
63
|
+
"""
|
64
|
+
return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}, {self.version})"
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def connected_boards(bluetooth: bool = False) -> List[str]:
|
68
|
+
"""
|
69
|
+
Get a list of connected boards.
|
70
|
+
|
71
|
+
Parameters:
|
72
|
+
- bluetooth (bool): Whether to include Bluetooth ports. Default is False.
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
- List[str]: A list of connected board ports.
|
76
|
+
"""
|
77
|
+
ports = serial.tools.list_ports.comports()
|
78
|
+
|
79
|
+
if not bluetooth:
|
80
|
+
# filter out bluetooth ports
|
81
|
+
ports = [p for p in ports if "bluetooth" not in p.description.lower()]
|
82
|
+
ports = [p for p in ports if "BTHENUM" not in p.hwid]
|
83
|
+
|
84
|
+
return sorted([p.device for p in ports])
|
85
|
+
|
86
|
+
@retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
|
87
|
+
def get_mcu_info(self, timeout: int = 2):
|
88
|
+
"""
|
89
|
+
Get MCU information from the connected board.
|
90
|
+
|
91
|
+
Parameters:
|
92
|
+
- timeout (int): The timeout value in seconds. Default is 2.
|
93
|
+
|
94
|
+
Raises:
|
95
|
+
- ConnectionError: If failed to get mcu_info for the serial port.
|
96
|
+
"""
|
97
|
+
rc, result = self.run_command(
|
98
|
+
["run", str(HERE / "mpy_fw_info.py")],
|
99
|
+
no_info=True,
|
100
|
+
timeout=timeout,
|
101
|
+
)
|
102
|
+
if rc != OK:
|
103
|
+
raise ConnectionError(f"Failed to get mcu_info for {self.serialport}")
|
104
|
+
# Ok we have the info, now parse it
|
105
|
+
s = result[0].strip()
|
106
|
+
if s.startswith("{") and s.endswith("}"):
|
107
|
+
info = eval(s)
|
108
|
+
self.family = info["family"]
|
109
|
+
self.version = info["version"]
|
110
|
+
self.build = info["build"]
|
111
|
+
self.port = info["port"]
|
112
|
+
self.cpu = info["cpu"]
|
113
|
+
self.arch = info["arch"]
|
114
|
+
self.mpy = info["mpy"]
|
115
|
+
self.description = descr = info["board"]
|
116
|
+
pos = descr.rfind(" with")
|
117
|
+
short_descr = descr[:pos].strip() if pos != -1 else ""
|
118
|
+
if board_name := find_board_id(descr, short_descr):
|
119
|
+
self.board = board_name
|
120
|
+
else:
|
121
|
+
self.board = "UNKNOWN"
|
122
|
+
|
123
|
+
def disconnect(self) -> bool:
|
124
|
+
"""
|
125
|
+
Disconnect from a board.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
- bool: True if successfully disconnected, False otherwise.
|
129
|
+
"""
|
130
|
+
if not self.connected:
|
131
|
+
return True
|
132
|
+
if not self.serialport:
|
133
|
+
log.error("No port connected")
|
134
|
+
self.connected = False
|
135
|
+
return False
|
136
|
+
log.info(f"Disconnecting from {self.serialport}")
|
137
|
+
result = self.run_command(["disconnect"])[0] == OK
|
138
|
+
self.connected = False
|
139
|
+
return result
|
140
|
+
|
141
|
+
@retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2), reraise=True)
|
142
|
+
def run_command(
|
143
|
+
self,
|
144
|
+
cmd: Union[str, List[str]],
|
145
|
+
*,
|
146
|
+
log_errors: bool = True,
|
147
|
+
no_info: bool = False,
|
148
|
+
timeout: int = 60,
|
149
|
+
**kwargs,
|
150
|
+
):
|
151
|
+
"""
|
152
|
+
Run mpremote with the given command.
|
153
|
+
|
154
|
+
Parameters:
|
155
|
+
- cmd (Union[str, List[str]]): The command to run, either a string or a list of strings.
|
156
|
+
- log_errors (bool): Whether to log errors. Default is True.
|
157
|
+
- no_info (bool): Whether to skip printing info. Default is False.
|
158
|
+
- timeout (int): The timeout value in seconds. Default is 60.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
- bool: True if the command succeeded, False otherwise.
|
162
|
+
"""
|
163
|
+
if isinstance(cmd, str):
|
164
|
+
cmd = cmd.split(" ")
|
165
|
+
prefix = [sys.executable, "-m", "mpremote"]
|
166
|
+
if self.serialport:
|
167
|
+
prefix += ["connect", self.serialport]
|
168
|
+
# if connected add resume to keep state between commands
|
169
|
+
if self.connected:
|
170
|
+
prefix += ["resume"]
|
171
|
+
cmd = prefix + cmd
|
172
|
+
log.debug(" ".join(cmd))
|
173
|
+
result = run(cmd, timeout, log_errors, no_info, **kwargs)
|
174
|
+
self.connected = result[0] == OK
|
175
|
+
return result
|
176
|
+
|
177
|
+
@retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1))
|
178
|
+
def mip_install(self, name: str) -> bool:
|
179
|
+
"""
|
180
|
+
Install a micropython package.
|
181
|
+
|
182
|
+
Parameters:
|
183
|
+
- name (str): The name of the package to install.
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
- bool: True if the installation succeeded, False otherwise.
|
187
|
+
"""
|
188
|
+
# install createstubs to the board
|
189
|
+
cmd = ["mip", "install", name]
|
190
|
+
result = self.run_command(cmd)[0] == OK
|
191
|
+
self.connected = True
|
192
|
+
return result
|
193
|
+
|
194
|
+
def wait_for_restart(self, timeout: int = 10):
|
195
|
+
"""wait for the board to restart"""
|
196
|
+
for _ in track(
|
197
|
+
range(timeout),
|
198
|
+
description="Waiting for the board to restart",
|
199
|
+
transient=True,
|
200
|
+
get_time=lambda: time.time(),
|
201
|
+
show_speed=False,
|
202
|
+
):
|
203
|
+
time.sleep(1)
|
204
|
+
try:
|
205
|
+
self.get_mcu_info()
|
206
|
+
break
|
207
|
+
except (ConnectionError, MPFlashError):
|
208
|
+
pass
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# %%micropython
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
|
5
|
+
|
6
|
+
def _build(s):
|
7
|
+
# extract build from sys.version or os.uname().version if available
|
8
|
+
# sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
|
9
|
+
# sys.implementation.version: 'v1.13-103-gb137d064e'
|
10
|
+
if not s:
|
11
|
+
return ""
|
12
|
+
s = s.split(" on ", 1)[0] if " on " in s else s
|
13
|
+
if s.startswith("v"):
|
14
|
+
if not "-" in s:
|
15
|
+
return ""
|
16
|
+
b = s.split("-")[1]
|
17
|
+
return b
|
18
|
+
if not "-preview" in s:
|
19
|
+
return ""
|
20
|
+
b = s.split("-preview")[1].split(".")[1]
|
21
|
+
return b
|
22
|
+
|
23
|
+
|
24
|
+
def _version_str(version: tuple): # -> str:
|
25
|
+
v_str = ".".join([str(n) for n in version[:3]])
|
26
|
+
if len(version) > 3 and version[3]:
|
27
|
+
v_str += "-" + version[3]
|
28
|
+
return v_str
|
29
|
+
|
30
|
+
|
31
|
+
def _info(): # type:() -> dict[str, str]
|
32
|
+
# sourcery skip: use-contextlib-suppress, use-fstring-for-formatting, use-named-expression
|
33
|
+
info = dict(
|
34
|
+
{
|
35
|
+
"family": sys.implementation[0], # type: ignore
|
36
|
+
"version": "",
|
37
|
+
"build": "",
|
38
|
+
"ver": "",
|
39
|
+
"port": "stm32" if sys.platform.startswith("pyb") else sys.platform, # port: esp32 / win32 / linux / stm32
|
40
|
+
"board": "GENERIC",
|
41
|
+
"cpu": "",
|
42
|
+
"mpy": "",
|
43
|
+
"arch": "",
|
44
|
+
}
|
45
|
+
)
|
46
|
+
try:
|
47
|
+
info["version"] = _version_str(sys.implementation.version)
|
48
|
+
except AttributeError:
|
49
|
+
pass
|
50
|
+
try:
|
51
|
+
machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine
|
52
|
+
info["board"] = machine.strip()
|
53
|
+
info["cpu"] = machine.split("with")[-1].strip() if "with" in machine else ""
|
54
|
+
info["mpy"] = (
|
55
|
+
sys.implementation._mpy
|
56
|
+
if "_mpy" in dir(sys.implementation)
|
57
|
+
else sys.implementation.mpy if "mpy" in dir(sys.implementation) else ""
|
58
|
+
)
|
59
|
+
except (AttributeError, IndexError):
|
60
|
+
pass
|
61
|
+
|
62
|
+
try:
|
63
|
+
if hasattr(sys, "version"):
|
64
|
+
info["build"] = _build(sys.version)
|
65
|
+
elif hasattr(os, "uname"):
|
66
|
+
info["build"] = _build(os.uname()[3])
|
67
|
+
if not info["build"]:
|
68
|
+
# extract build from uname().release if available
|
69
|
+
info["build"] = _build(os.uname()[2])
|
70
|
+
except (AttributeError, IndexError):
|
71
|
+
pass
|
72
|
+
# avoid build hashes
|
73
|
+
if info["build"] and len(info["build"]) > 5:
|
74
|
+
info["build"] = ""
|
75
|
+
|
76
|
+
if info["version"] == "" and sys.platform not in ("unix", "win32"):
|
77
|
+
try:
|
78
|
+
u = os.uname()
|
79
|
+
info["version"] = u.release
|
80
|
+
except (IndexError, AttributeError, TypeError):
|
81
|
+
pass
|
82
|
+
# detect families
|
83
|
+
for fam_name, mod_name, mod_thing in [
|
84
|
+
("pycopy", "pycopy", "const"),
|
85
|
+
("pycom", "pycom", "FAT"),
|
86
|
+
("ev3-pybricks", "pybricks.hubs", "EV3Brick"),
|
87
|
+
]:
|
88
|
+
try:
|
89
|
+
_t = __import__(mod_name, None, None, (mod_thing))
|
90
|
+
info["family"] = fam_name
|
91
|
+
del _t
|
92
|
+
break
|
93
|
+
except (ImportError, KeyError):
|
94
|
+
pass
|
95
|
+
|
96
|
+
if info["family"] == "ev3-pybricks":
|
97
|
+
info["release"] = "2.0.0"
|
98
|
+
|
99
|
+
if info["family"] == "micropython":
|
100
|
+
if (
|
101
|
+
info["version"]
|
102
|
+
and info["version"].endswith(".0")
|
103
|
+
and info["version"] >= "1.10.0" # versions from 1.10.0 to 1.20.0 do not have a micro .0
|
104
|
+
and info["version"] <= "1.19.9"
|
105
|
+
):
|
106
|
+
# drop the .0 for newer releases
|
107
|
+
info["version"] = info["version"][:-2]
|
108
|
+
|
109
|
+
# spell-checker: disable
|
110
|
+
if "mpy" in info and info["mpy"]: # mpy on some v1.11+ builds
|
111
|
+
sys_mpy = int(info["mpy"])
|
112
|
+
# .mpy architecture
|
113
|
+
arch = [
|
114
|
+
None,
|
115
|
+
"x86",
|
116
|
+
"x64",
|
117
|
+
"armv6",
|
118
|
+
"armv6m",
|
119
|
+
"armv7m",
|
120
|
+
"armv7em",
|
121
|
+
"armv7emsp",
|
122
|
+
"armv7emdp",
|
123
|
+
"xtensa",
|
124
|
+
"xtensawin",
|
125
|
+
][sys_mpy >> 10]
|
126
|
+
if arch:
|
127
|
+
info["arch"] = arch
|
128
|
+
# .mpy version.minor
|
129
|
+
info["mpy"] = "v{}.{}".format(sys_mpy & 0xFF, sys_mpy >> 8 & 3)
|
130
|
+
# simple to use version[-build] string avoiding f-strings for backward compat
|
131
|
+
info["ver"] = (
|
132
|
+
"v{version}-{build}".format(version=info["version"], build=info["build"])
|
133
|
+
if info["build"]
|
134
|
+
else "v{version}".format(version=info["version"])
|
135
|
+
)
|
136
|
+
|
137
|
+
return info
|
138
|
+
|
139
|
+
|
140
|
+
print(_info())
|
141
|
+
del _info, _build, _version_str
|
@@ -11,6 +11,7 @@ from loguru import logger as log
|
|
11
11
|
|
12
12
|
LogTagList = List[str]
|
13
13
|
|
14
|
+
|
14
15
|
@dataclass
|
15
16
|
class LogTags:
|
16
17
|
reset_tags: LogTagList
|
@@ -26,6 +27,7 @@ def run(
|
|
26
27
|
log_errors: bool = True,
|
27
28
|
no_info: bool = False,
|
28
29
|
*,
|
30
|
+
log_warnings: bool = False,
|
29
31
|
reset_tags: Optional[LogTagList] = None,
|
30
32
|
error_tags: Optional[LogTagList] = None,
|
31
33
|
warning_tags: Optional[LogTagList] = None,
|
@@ -55,13 +57,24 @@ def run(
|
|
55
57
|
The return code and the output as a list of strings
|
56
58
|
"""
|
57
59
|
if not reset_tags:
|
58
|
-
reset_tags = [
|
60
|
+
reset_tags = [
|
61
|
+
"rst cause:1, boot mode:",
|
62
|
+
"rst cause:2, boot mode:",
|
63
|
+
"rst cause:3, boot mode:",
|
64
|
+
"rst cause:4, boot mode:",
|
65
|
+
]
|
66
|
+
# 0 -> normal startup by power on
|
67
|
+
# 1 -> hardware watch dog reset
|
68
|
+
# 2 -> software watch dog reset (From an exception)
|
69
|
+
# 3 -> software watch dog reset system_restart (Possibly unfed watchdog got angry)
|
70
|
+
# 4 -> soft restart (Possibly with a restart command)
|
71
|
+
# 5 -> wake up from deep-sleep
|
59
72
|
if not error_tags:
|
60
73
|
error_tags = ["Traceback ", "Error: ", "Exception: ", "ERROR :", "CRIT :"]
|
61
74
|
if not warning_tags:
|
62
|
-
warning_tags = ["WARN :", "TRACE :"]
|
75
|
+
warning_tags = ["WARN :", "TRACE :"]
|
63
76
|
if not success_tags:
|
64
|
-
success_tags = [
|
77
|
+
success_tags = []
|
65
78
|
if not ignore_tags:
|
66
79
|
ignore_tags = [' File "<stdin>",']
|
67
80
|
|
@@ -81,7 +94,8 @@ def run(
|
|
81
94
|
|
82
95
|
def timed_out():
|
83
96
|
proc.kill()
|
84
|
-
|
97
|
+
if log_warnings:
|
98
|
+
log.warning(f"Command {cmd} timed out after {timeout} seconds")
|
85
99
|
|
86
100
|
timer = Timer(timeout, timed_out)
|
87
101
|
try:
|
@@ -111,13 +125,16 @@ def run(
|
|
111
125
|
continue
|
112
126
|
else:
|
113
127
|
if not no_info:
|
128
|
+
if line.startswith(("INFO : ", "WARN : ", "ERROR : ")):
|
129
|
+
line = line[8:].lstrip()
|
114
130
|
log.info(line)
|
115
131
|
if proc.stderr and log_errors:
|
116
132
|
for line in proc.stderr:
|
117
133
|
log.warning(line)
|
134
|
+
except UnicodeDecodeError as e:
|
135
|
+
log.error(f"Failed to decode output: {e}")
|
118
136
|
finally:
|
119
137
|
timer.cancel()
|
120
138
|
|
121
139
|
proc.wait(timeout=1)
|
122
140
|
return proc.returncode or 0, output
|
123
|
-
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# sourcery skip: require-parameter-annotation
|
2
|
+
# sourcery skip: replace-interpolation-with-fstring
|
3
|
+
#!/usr/bin/python
|
4
|
+
|
5
|
+
# Written by Antonio Galea - 2010/11/18
|
6
|
+
# Distributed under Gnu LGPL 3.0
|
7
|
+
# see http://www.gnu.org/licenses/lgpl-3.0.txt
|
8
|
+
|
9
|
+
import os
|
10
|
+
import struct
|
11
|
+
import sys
|
12
|
+
import zlib
|
13
|
+
from optparse import OptionParser
|
14
|
+
|
15
|
+
DEFAULT_DEVICE = "0x0483:0xdf11"
|
16
|
+
|
17
|
+
|
18
|
+
def named(tuple, names):
|
19
|
+
return dict(zip(names.split(), tuple))
|
20
|
+
|
21
|
+
|
22
|
+
def consume(fmt, data, names):
|
23
|
+
n = struct.calcsize(fmt)
|
24
|
+
return named(struct.unpack(fmt, data[:n]), names), data[n:]
|
25
|
+
|
26
|
+
|
27
|
+
def cstring(string):
|
28
|
+
return string.split(b"\0", 1)[0]
|
29
|
+
|
30
|
+
|
31
|
+
def compute_crc(data):
|
32
|
+
return 0xFFFFFFFF & -zlib.crc32(data) - 1
|
33
|
+
|
34
|
+
|
35
|
+
def parse(file, dump_images=False):
|
36
|
+
print('File: "%s"' % file)
|
37
|
+
data = open(file, "rb").read()
|
38
|
+
crc = compute_crc(data[:-4])
|
39
|
+
prefix, data = consume("<5sBIB", data, "signature version size targets")
|
40
|
+
print("%(signature)s v%(version)d, image size: %(size)d, targets: %(targets)d" % prefix)
|
41
|
+
for t in range(prefix["targets"]):
|
42
|
+
tprefix, data = consume("<6sBI255s2I", data, "signature altsetting named name size elements")
|
43
|
+
tprefix["num"] = t
|
44
|
+
if tprefix["named"]:
|
45
|
+
tprefix["name"] = cstring(tprefix["name"])
|
46
|
+
else:
|
47
|
+
tprefix["name"] = ""
|
48
|
+
print(
|
49
|
+
'%(signature)s %(num)d, alt setting: %(altsetting)s, name: "%(name)s", size: %(size)d, elements: %(elements)d'
|
50
|
+
% tprefix
|
51
|
+
)
|
52
|
+
tsize = tprefix["size"]
|
53
|
+
target, data = data[:tsize], data[tsize:]
|
54
|
+
for e in range(tprefix["elements"]):
|
55
|
+
eprefix, target = consume("<2I", target, "address size")
|
56
|
+
eprefix["num"] = e
|
57
|
+
print(" %(num)d, address: 0x%(address)08x, size: %(size)d" % eprefix)
|
58
|
+
esize = eprefix["size"]
|
59
|
+
image, target = target[:esize], target[esize:]
|
60
|
+
if dump_images:
|
61
|
+
out = "%s.target%d.image%d.bin" % (file, t, e)
|
62
|
+
open(out, "wb").write(image)
|
63
|
+
print(' DUMPED IMAGE TO "%s"' % out)
|
64
|
+
if len(target):
|
65
|
+
print("target %d: PARSE ERROR" % t)
|
66
|
+
suffix = named(struct.unpack("<4H3sBI", data[:16]), "device product vendor dfu ufd len crc")
|
67
|
+
print(
|
68
|
+
"usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x"
|
69
|
+
% suffix
|
70
|
+
)
|
71
|
+
if crc != suffix["crc"]:
|
72
|
+
print("CRC ERROR: computed crc32 is 0x%08x" % crc)
|
73
|
+
data = data[16:]
|
74
|
+
if data:
|
75
|
+
print("PARSE ERROR")
|
76
|
+
|
77
|
+
|
78
|
+
def build(file, targets, device=DEFAULT_DEVICE):
|
79
|
+
data = b""
|
80
|
+
for _, target in enumerate(targets):
|
81
|
+
tdata = b""
|
82
|
+
for image in target:
|
83
|
+
# pad image to 8 bytes (needed at least for L476)
|
84
|
+
pad = (8 - len(image["data"]) % 8) % 8
|
85
|
+
image["data"] = image["data"] + bytes(bytearray(8)[0:pad])
|
86
|
+
#
|
87
|
+
tdata += struct.pack("<2I", image["address"], len(image["data"])) + image["data"]
|
88
|
+
tdata = struct.pack("<6sBI255s2I", b"Target", 0, 1, b"ST...", len(tdata), len(target)) + tdata
|
89
|
+
data += tdata
|
90
|
+
data = struct.pack("<5sBIB", b"DfuSe", 1, len(data) + 11, len(targets)) + data
|
91
|
+
v, d = map(lambda x: int(x, 0) & 0xFFFF, device.split(":", 1))
|
92
|
+
data += struct.pack("<4H3sB", 0, d, v, 0x011A, b"UFD", 16)
|
93
|
+
crc = compute_crc(data)
|
94
|
+
data += struct.pack("<I", crc)
|
95
|
+
open(file, "wb").write(data)
|
96
|
+
|
97
|
+
|
98
|
+
if __name__ == "__main__":
|
99
|
+
usage = """
|
100
|
+
%prog [-d|--dump] infile.dfu
|
101
|
+
%prog {-b|--build} address:file.bin [-b address:file.bin ...] [{-D|--device}=vendor:device] outfile.dfu"""
|
102
|
+
parser = OptionParser(usage=usage)
|
103
|
+
parser.add_option(
|
104
|
+
"-b",
|
105
|
+
"--build",
|
106
|
+
action="append",
|
107
|
+
dest="binfiles",
|
108
|
+
help="build a DFU file from given BINFILES",
|
109
|
+
metavar="BINFILES",
|
110
|
+
)
|
111
|
+
parser.add_option(
|
112
|
+
"-D",
|
113
|
+
"--device",
|
114
|
+
action="store",
|
115
|
+
dest="device",
|
116
|
+
help="build for DEVICE, defaults to %s" % DEFAULT_DEVICE,
|
117
|
+
metavar="DEVICE",
|
118
|
+
)
|
119
|
+
parser.add_option(
|
120
|
+
"-d",
|
121
|
+
"--dump",
|
122
|
+
action="store_true",
|
123
|
+
dest="dump_images",
|
124
|
+
default=False,
|
125
|
+
help="dump contained images to current directory",
|
126
|
+
)
|
127
|
+
(options, args) = parser.parse_args()
|
128
|
+
|
129
|
+
if options.binfiles and len(args) == 1:
|
130
|
+
target = []
|
131
|
+
for arg in options.binfiles:
|
132
|
+
try:
|
133
|
+
address, binfile = arg.split(":", 1)
|
134
|
+
except ValueError:
|
135
|
+
print("Address:file couple '%s' invalid." % arg)
|
136
|
+
sys.exit(1)
|
137
|
+
try:
|
138
|
+
address = int(address, 0) & 0xFFFFFFFF
|
139
|
+
except ValueError:
|
140
|
+
print("Address %s invalid." % address)
|
141
|
+
sys.exit(1)
|
142
|
+
if not os.path.isfile(binfile):
|
143
|
+
print("Unreadable file '%s'." % binfile)
|
144
|
+
sys.exit(1)
|
145
|
+
target.append({"address": address, "data": open(binfile, "rb").read()})
|
146
|
+
outfile = args[0]
|
147
|
+
device = DEFAULT_DEVICE
|
148
|
+
if options.device:
|
149
|
+
device = options.device
|
150
|
+
try:
|
151
|
+
v, d = map(lambda x: int(x, 0) & 0xFFFF, device.split(":", 1))
|
152
|
+
except:
|
153
|
+
print("Invalid device '%s'." % device)
|
154
|
+
sys.exit(1)
|
155
|
+
build(outfile, [target], device)
|
156
|
+
elif len(args) == 1:
|
157
|
+
infile = args[0]
|
158
|
+
if not os.path.isfile(infile):
|
159
|
+
print("Unreadable file '%s'." % infile)
|
160
|
+
sys.exit(1)
|
161
|
+
parse(infile, dump_images=options.dump_images)
|
162
|
+
else:
|
163
|
+
parser.print_help()
|
164
|
+
sys.exit(1)
|