Skip to content

Commit 52c673d

Browse files
authored
Add script to generate json containing information to run fwuploader tool (#38)
* [skip changelog] Create generator script to create boards.json * [skip changelog] Update generator script to gather upload info * [skip changelog] Change download url and generated json name * [skip changelog] Small enhancements in generator script * [skip changelog] Reduce function complexity
1 parent 79da353 commit 52c673d

File tree

2 files changed

+806
-0
lines changed

2 files changed

+806
-0
lines changed

generator/generator.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import argparse
2+
import subprocess
3+
import sys
4+
import json
5+
import hashlib
6+
import shutil
7+
from pathlib import Path
8+
9+
DOWNLOAD_URL = "https://downloads.arduino.cc/arduino-fwuploader"
10+
FQBNS = {
11+
"mkr1000": "arduino:samd:mkr1000",
12+
"mkrwifi1010": "arduino:samd:mkrwifi1010",
13+
"nano_33_iot": "arduino:samd:nano_33_iot",
14+
"mkrvidor4000": "arduino:samd:mkrvidor4000",
15+
"uno2018": "arduino:megaavr:uno2018",
16+
"mkrnb1500": "arduino:samd:mkrnb1500",
17+
"nanorp2040connect": "arduino:mbed_nano:nanorp2040connect",
18+
}
19+
20+
21+
# Runs arduino-cli, doesn't handle errors at all because am lazy
22+
def arduino_cli(cli_path, args=[]):
23+
res = subprocess.run([cli_path, *args], capture_output=True, text=True)
24+
return res.stdout
25+
26+
27+
# Generates file SHA256
28+
def sha2(file_path):
29+
with open(file_path, "rb") as f:
30+
return hashlib.sha256(f.read()).hexdigest()
31+
32+
33+
def split_property_and_drop_first_level(s):
34+
(k, v) = s.strip().split("=", maxsplit=1)
35+
k = ".".join(k.split(".", maxsplit=1)[1:])
36+
return (k, v)
37+
38+
39+
# Generate and copy loader Sketch binary data for specified board
40+
def create_loader_data(simple_fqbn, binary):
41+
loader_path = f"firmwares/loader/{simple_fqbn}/loader.bin"
42+
loader = Path(__file__).parent / loader_path
43+
loader.parent.mkdir(parents=True, exist_ok=True)
44+
shutil.copyfile(binary, loader)
45+
46+
file_hash = sha2(loader)
47+
48+
return {
49+
"url": f"{DOWNLOAD_URL}/{loader_path}",
50+
"checksum": f"SHA-256:{file_hash}",
51+
"size": f"{loader.stat().st_size}",
52+
}
53+
54+
55+
# Generate and copy all firmware binary data for specified board
56+
def create_firmware_data(binary, module, version):
57+
binary_name = binary.name
58+
firmware_path = f"firmwares/{module}/{version}/{binary_name}"
59+
firmware = Path(__file__).parent / firmware_path
60+
firmware.parent.mkdir(parents=True, exist_ok=True)
61+
shutil.copyfile(binary, firmware)
62+
63+
file_hash = sha2(firmware)
64+
65+
return {
66+
"version": version,
67+
"url": f"{DOWNLOAD_URL}/{firmware_path}",
68+
"checksum": f"SHA-256:{file_hash}",
69+
"size": f"{firmware.stat().st_size}",
70+
}
71+
72+
73+
def get_uploader_id(tools, tool_executable):
74+
for t in tools:
75+
if t["name"] == tool_executable:
76+
packager = t["packager"]
77+
name = t["name"]
78+
version = t["version"]
79+
return f"{packager}:{name}@{version}"
80+
81+
82+
def create_upload_data(fqbn, installed_cores):
83+
upload_data = {}
84+
# Assume we're on Linux
85+
arduino15 = Path.home() / ".arduino15"
86+
87+
board_id = fqbn.split(":")[2]
88+
core_id = ":".join(fqbn.split(":")[:2])
89+
90+
# Get the core install dir
91+
core = installed_cores[core_id]
92+
(maintainer, arch) = core_id.split(":")
93+
core_install_dir = (
94+
arduino15 / "packages" / maintainer / "hardware" / arch / core["installed"]
95+
)
96+
97+
with open(core_install_dir / "boards.txt") as f:
98+
boards_txt = f.readlines()
99+
100+
board_upload_data = {}
101+
for line in boards_txt:
102+
if line.startswith(f"{board_id}.upload"):
103+
(k, v) = split_property_and_drop_first_level(line)
104+
board_upload_data[k] = v
105+
106+
tool = board_upload_data["upload.tool"]
107+
108+
with open(core_install_dir / "platform.txt") as f:
109+
platform_txt = f.readlines()
110+
111+
platform_upload_data = {}
112+
for line in platform_txt:
113+
if line.startswith(f"tools.{tool}"):
114+
(k, v) = split_property_and_drop_first_level(line)
115+
platform_upload_data[k] = v
116+
117+
# We assume the installed.json exist
118+
with open(core_install_dir / "installed.json") as f:
119+
installed_json_data = json.load(f)
120+
121+
if f"{tool}.cmd" in platform_upload_data:
122+
tool_executable = platform_upload_data[f"{tool}.cmd"]
123+
elif f"{tool}.cmd.path" in platform_upload_data:
124+
tool_executable = platform_upload_data[f"{tool}.cmd.path"].split("/")[-1]
125+
126+
if tool_executable == "rp2040load":
127+
tool_executable = "rp2040tools"
128+
129+
tools = installed_json_data["packages"][0]["platforms"][0]["toolsDependencies"]
130+
upload_data["uploader"] = get_uploader_id(tools, tool_executable)
131+
132+
# We already store the tool name in a different manner
133+
del board_upload_data["upload.tool"]
134+
# Save also all the upload properties
135+
for k, v in board_upload_data.items():
136+
if v:
137+
upload_data[k] = v
138+
139+
# Get the command used to upload and modifies it a bit
140+
command = (
141+
platform_upload_data[f"{tool}.upload.pattern"]
142+
.replace("{path}/{cmd}", "{uploader}")
143+
.replace("{cmd.path}", "{uploader}")
144+
.replace("{build.path}/{build.project_name}", "{loader.sketch}")
145+
.replace('\\"', "")
146+
)
147+
148+
upload_data["uploader.command"] = command
149+
150+
return upload_data
151+
152+
153+
def generate_boards_json(input_data, arduino_cli_path):
154+
boards = {
155+
"arduino:samd:mkr1000": {"fqbn": "arduino:samd:mkr1000", "firmware": []},
156+
"arduino:samd:mkrwifi1010": {
157+
"fqbn": "arduino:samd:mkrwifi1010",
158+
"firmware": [],
159+
},
160+
"arduino:samd:nano_33_iot": {
161+
"fqbn": "arduino:samd:nano_33_iot",
162+
"firmware": [],
163+
},
164+
"arduino:samd:mkrvidor4000": {
165+
"fqbn": "arduino:samd:mkrvidor4000",
166+
"firmware": [],
167+
},
168+
"arduino:megaavr:uno2018": {"fqbn": "arduino:megaavr:uno2018", "firmware": []},
169+
"arduino:samd:mkrnb1500": {"fqbn": "arduino:samd:mkrnb1500", "firmware": []},
170+
"arduino:mbed_nano:nanorp2040connect": {
171+
"fqbn": "arduino:mbed_nano:nanorp2040connect",
172+
"firmware": [],
173+
},
174+
}
175+
176+
# Gets the installed cores
177+
res = arduino_cli(
178+
cli_path=arduino_cli_path, args=["core", "list", "--format", "json"]
179+
)
180+
installed_cores = {c["id"]: c for c in json.loads(res)}
181+
182+
# Verify all necessary cores are installed
183+
# TODO: Should we check that the latest version is installed too?
184+
for fqbn in boards.keys():
185+
core_id = ":".join(fqbn.split(":")[:2])
186+
if core_id not in installed_cores:
187+
print(f"Board {fqbn} is not installed, install its core {core_id}")
188+
sys.exit(1)
189+
190+
for pseudo_fqbn, data in input_data.items():
191+
fqbn = FQBNS[pseudo_fqbn]
192+
simple_fqbn = fqbn.replace(":", ".")
193+
194+
for _, v in data.items():
195+
item = v[0]
196+
binary = Path(item["Path"])
197+
198+
if item["IsLoader"]:
199+
boards[fqbn]["loader_sketch"] = create_loader_data(simple_fqbn, binary)
200+
else:
201+
module, version = item["version"].split("/")
202+
boards[fqbn]["firmware"].append(
203+
create_firmware_data(binary, module, version)
204+
)
205+
boards[fqbn]["module"] = module
206+
207+
res = arduino_cli(
208+
cli_path=arduino_cli_path,
209+
args=["board", "search", fqbn, "--format", "json"],
210+
)
211+
# Gets the board name
212+
for board in json.loads(res):
213+
if board["fqbn"] == fqbn:
214+
boards[fqbn]["name"] = board["name"]
215+
break
216+
217+
boards[fqbn].update(create_upload_data(fqbn, installed_cores))
218+
219+
boards_json = []
220+
for _, b in boards.items():
221+
boards_json.append(b)
222+
223+
return boards_json
224+
225+
226+
if __name__ == "__main__":
227+
parser = argparse.ArgumentParser(prog="generator.py")
228+
parser.add_argument(
229+
"-a",
230+
"--arduino-cli",
231+
default="arduino-cli",
232+
help="Path to arduino-cli executable",
233+
required=True,
234+
)
235+
args = parser.parse_args(sys.argv[1:])
236+
237+
# raw_boards.json has been generated using --get_available_for FirmwareUploader flag.
238+
# It has been edited a bit to better handle parsing.
239+
with open("raw_boards.json", "r") as f:
240+
raw_boards = json.load(f)
241+
242+
boards_json = generate_boards_json(raw_boards, args.arduino_cli)
243+
244+
Path("boards").mkdir()
245+
246+
with open("boards/board_index.json", "w") as f:
247+
json.dump(boards_json, f, indent=2)
248+
249+
# board_index.json must be formatted like so:
250+
#
251+
# {
252+
# "name": "MKR 1000",
253+
# "fqbn": "arduino:samd:mkr1000",
254+
# "module": "WINC_1500",
255+
# "firmware": [
256+
# {
257+
# "version": "19.6.1",
258+
# "url": "https://downloads.arduino.cc/firmwares/WINC_1500/19.6.1/m2m_aio_3a0.bin",
259+
# "checksum": "SHA-256:de0c6b1621aa15996432559efb5d8a29885f62bde145937eee99883bfa129f97",
260+
# "size": "359356",
261+
# },
262+
# {
263+
# "version": "19.5.4",
264+
# "url": "https://downloads.arduino.cc/firmwares/WINC_1500/19.5.4/m2m_aio_3a0.bin",
265+
# "checksum": "SHA-256:71e5a805e60f96e6968414670d8a414a03cb610fd4b020f47ab53f5e1ff82a13",
266+
# "size": "413604",
267+
# },
268+
# ],
269+
# "loader_sketch": {
270+
# "url": "https://downloads.arduino.cc/firmwares/loader/arduino.samd.mkr1000/loader.bin",
271+
# "checksum": "SHA-256:71e5a805e60f96e6968414670d8a414a03cb610fd4b020f47ab53f5e1ff82a13",
272+
# "size": "39287",
273+
# },
274+
# "uploader": "arduino:bossac@1.7.0",
275+
# "uploader.command": "{uploader} --port={upload.port} -U true -i -e -w -v {loader.sketch} -R",
276+
# "uploader.requires_1200_bps_touch": "true",
277+
# "uploader.requires_port_change": "true",
278+
# }

0 commit comments

Comments
 (0)