Skip to content

Commit 50e1f0e

Browse files
committed
firmware: add support for firmware.metadata.json v2.0.0
pybricks-micropython has a new metadata file format. Support for appending a user main.py file when flashing firmware is dropped in this version.
1 parent 589822f commit 50e1f0e

File tree

6 files changed

+164
-40
lines changed

6 files changed

+164
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- Experimental support for relative and nested imports.
11+
- Added support for `firmware.metadata.json` v2.0.0.
1112

1213
## [1.0.0-alpha.30] - 2022-08-26
1314

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pybricksdev/firmware.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@
55
Utilities for working with Pybricks ``firmware.zip`` files.
66
"""
77

8+
import sys
89
from typing import Literal, TypedDict, Union
910

11+
if sys.version_info < (3, 10):
12+
from typing_extensions import TypeGuard
13+
else:
14+
from typing import TypeGuard
15+
1016

1117
class FirmwareMetadataV100(
1218
TypedDict(
1319
"V100",
1420
{
1521
"metadata-version": Literal["1.0.0"],
1622
"firmware-version": str,
17-
"device-id": Union[
18-
Literal[0x40], Literal[0x41], Literal[0x80], Literal[0x81]
19-
],
20-
"checksum-type": Union[Literal["sum"], Literal["crc32"]],
23+
"device-id": Literal[0x40, 0x41, 0x80, 0x81],
24+
"checksum-type": Literal["sum", "crc32"],
2125
"mpy-abi-version": int,
2226
"mpy-cross-options": list[str],
2327
"user-mpy-offset": int,
@@ -48,12 +52,53 @@ class FirmwareMetadataV110(
4852
"""
4953

5054

51-
AnyFirmwareMetadata = Union[FirmwareMetadataV100, FirmwareMetadataV110]
55+
class FirmwareMetadataV200(
56+
TypedDict(
57+
"V200",
58+
{
59+
"metadata-version": Literal["2.0.0"],
60+
"firmware-version": str,
61+
"device-id": Literal[0x40, 0x41, 0x80, 0x81, 83],
62+
"checksum-type": Literal["sum", "crc32"],
63+
"checksum-size": int,
64+
"hub-name-offset": int,
65+
"hub-name-size": int,
66+
},
67+
)
68+
):
69+
"""
70+
Type for data contained in v2.0.0 ``firmware.metadata.json`` files.
71+
"""
72+
73+
74+
AnyFirmwareV1Metadata = Union[FirmwareMetadataV100, FirmwareMetadataV110]
75+
"""
76+
Type for data contained in ``firmware.metadata.json`` files of any 1.x version.
77+
"""
78+
79+
AnyFirmwareV2Metadata = FirmwareMetadataV200
80+
"""
81+
Type for data contained in ``firmware.metadata.json`` files of any 2.x version.
82+
"""
83+
84+
AnyFirmwareMetadata = Union[AnyFirmwareV1Metadata, AnyFirmwareV2Metadata]
5285
"""
5386
Type for data contained in ``firmware.metadata.json`` files of any version.
5487
"""
5588

5689

90+
def firmware_metadata_is_v1(
91+
metadata: AnyFirmwareMetadata,
92+
) -> TypeGuard[AnyFirmwareV1Metadata]:
93+
return metadata["metadata-version"].startswith("1.")
94+
95+
96+
def firmware_metadata_is_v2(
97+
metadata: AnyFirmwareMetadata,
98+
) -> TypeGuard[AnyFirmwareV2Metadata]:
99+
return metadata["metadata-version"].startswith("2.")
100+
101+
57102
class ExtendedFirmwareMetadata(
58103
FirmwareMetadataV110, TypedDict("Extended", {"firmware-sha256": str})
59104
):

pybricksdev/flash.py

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SPDX-License-Identifier: MIT
2-
# Copyright (c) 2019-2021 The Pybricks Authors
2+
# Copyright (c) 2019-2022 The Pybricks Authors
33

44
import asyncio
55
import hashlib
@@ -9,7 +9,6 @@
99
import os
1010
import platform
1111
import struct
12-
import sys
1312
import zipfile
1413
from collections import namedtuple
1514
from typing import BinaryIO, Dict, List, Optional, Tuple, Union
@@ -22,37 +21,22 @@
2221
from .ble.lwp3.bootloader import BootloaderCommand
2322
from .ble.lwp3.bytecodes import HubKind
2423
from .compile import compile_file, save_script
25-
from .firmware import ExtendedFirmwareMetadata, AnyFirmwareMetadata
24+
from .firmware import (
25+
AnyFirmwareV1Metadata,
26+
AnyFirmwareV2Metadata,
27+
ExtendedFirmwareMetadata,
28+
AnyFirmwareMetadata,
29+
firmware_metadata_is_v1,
30+
firmware_metadata_is_v2,
31+
)
2632
from .tools.checksum import crc32_checksum, sum_complement
2733

2834
logger = logging.getLogger(__name__)
2935

3036

31-
async def create_firmware(
32-
firmware_zip: Union[str, os.PathLike, BinaryIO], name: Optional[str] = None
33-
) -> Tuple[bytes, ExtendedFirmwareMetadata]:
34-
"""Creates a firmware blob from base firmware and main.mpy file.
35-
36-
Arguments:
37-
firmware_zip:
38-
Path to the firmware zip file or a file-like object.
39-
name:
40-
A custom name for the hub.
41-
42-
Returns:
43-
Composite binary blob with correct padding and checksum and
44-
extended metadata for this firmware file.
45-
46-
Raises:
47-
ValueError:
48-
A name is given but the firmware does not support it or the name
49-
exceeds the alloted space in the firmware.
50-
51-
"""
52-
53-
archive = zipfile.ZipFile(firmware_zip)
54-
metadata: AnyFirmwareMetadata = json.load(archive.open("firmware.metadata.json"))
55-
37+
async def _create_firmware_v1(
38+
metadata: AnyFirmwareV1Metadata, archive: zipfile.ZipFile, name: Optional[str]
39+
) -> bytearray:
5640
base = archive.open("firmware-base.bin").read()
5741

5842
if "main.py" in archive.namelist():
@@ -79,7 +63,6 @@ async def create_firmware(
7963

8064
# Update hub name if given
8165
if name:
82-
8366
if semver.compare(metadata["metadata-version"], "1.1.0") < 0:
8467
raise ValueError(
8568
"this firmware image does not support setting the hub name"
@@ -88,6 +71,7 @@ async def create_firmware(
8871
name = name.encode() + b"\0"
8972

9073
max_size = metadata["max-hub-name-size"]
74+
9175
if len(name) > max_size:
9276
raise ValueError(
9377
f"name is too big - must be < {metadata['max-hub-name-size']} UTF-8 bytes"
@@ -102,16 +86,90 @@ async def create_firmware(
10286
elif metadata["checksum-type"] == "crc32":
10387
checksum = crc32_checksum(io.BytesIO(firmware), metadata["max-firmware-size"])
10488
else:
105-
print(f'Unknown checksum type "{metadata["checksum-type"]}"', file=sys.stderr)
106-
exit(1)
89+
raise ValueError(f"unsupported checksum type: {metadata['checksum-type']}")
90+
91+
# Append checksum to the firmware
92+
firmware.extend(struct.pack("<I", checksum))
93+
94+
return firmware
95+
96+
97+
async def _create_firmware_v2(
98+
metadata: AnyFirmwareV2Metadata, archive: zipfile.ZipFile, name: Optional[str]
99+
) -> bytearray:
100+
base = archive.open("firmware-base.bin").read()
101+
102+
# start with base firmware binary blob
103+
firmware = bytearray(base)
104+
105+
# Update hub name if given
106+
if name:
107+
name = name.encode() + b"\0"
108+
109+
max_size = metadata["hub-name-size"]
110+
111+
if len(name) > max_size:
112+
raise ValueError(
113+
f"name is too big - must be < {metadata['hub-name-size']} UTF-8 bytes"
114+
)
115+
116+
offset = metadata["hub-name-offset"]
117+
firmware[offset : offset + len(name)] = name
118+
119+
# Get checksum for this firmware
120+
if metadata["checksum-type"] == "sum":
121+
checksum = sum_complement(io.BytesIO(firmware), metadata["checksum-size"])
122+
elif metadata["checksum-type"] == "crc32":
123+
checksum = crc32_checksum(io.BytesIO(firmware), metadata["checksum-size"])
124+
else:
125+
raise ValueError(f"unsupported checksum type: {metadata['checksum-type']}")
107126

108127
# Append checksum to the firmware
109128
firmware.extend(struct.pack("<I", checksum))
110129

111-
# Add extended metadata needed by install_pybricks.py
112-
metadata["firmware-sha256"] = hashlib.sha256(firmware).hexdigest()
130+
return firmware
131+
132+
133+
async def create_firmware(
134+
firmware_zip: Union[str, os.PathLike, BinaryIO], name: Optional[str] = None
135+
) -> Tuple[bytes, ExtendedFirmwareMetadata]:
136+
"""Creates a firmware blob from base firmware and an optional custom name.
137+
138+
Arguments:
139+
firmware_zip:
140+
Path to the firmware zip file or a file-like object.
141+
name:
142+
A custom name for the hub.
143+
144+
Returns:
145+
Composite binary blob with correct padding and checksum and
146+
extended metadata for this firmware file.
147+
148+
Raises:
149+
ValueError:
150+
A name is given but the firmware does not support it or the name
151+
exceeds the alloted space in the firmware.
152+
153+
"""
154+
155+
with zipfile.ZipFile(firmware_zip) as archive:
156+
metadata: AnyFirmwareMetadata = json.load(
157+
archive.open("firmware.metadata.json")
158+
)
159+
160+
if firmware_metadata_is_v1(metadata):
161+
firmware = await _create_firmware_v1(metadata, archive, name)
162+
elif firmware_metadata_is_v2(metadata):
163+
firmware = await _create_firmware_v2(metadata, archive, name)
164+
else:
165+
raise ValueError(
166+
f"unsupported metadata version: {metadata['metadata-version']}"
167+
)
168+
169+
# Add extended metadata needed by install_pybricks.py
170+
metadata["firmware-sha256"] = hashlib.sha256(firmware).hexdigest()
113171

114-
return firmware, metadata
172+
return firmware, metadata
115173

116174

117175
# NAME, PAYLOAD_SIZE requirement

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ prompt-toolkit = "^3.0.18"
4343
Rx = "^3.2.0"
4444
mpy-cross-v6 = "^1.0.0"
4545
packaging = "^21.3"
46+
typing-extensions = "^4.3.0"
4647

4748
[tool.poetry.dev-dependencies]
4849
black = "^22.3.0"

tests/test_firmware.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from pybricksdev.firmware import firmware_metadata_is_v1, firmware_metadata_is_v2
2+
3+
4+
def test_firmware_v1():
5+
assert firmware_metadata_is_v1({"metadata-version": "1.0.0"})
6+
assert firmware_metadata_is_v1({"metadata-version": "1.1.0"})
7+
assert not firmware_metadata_is_v1({"metadata-version": "2.0.0"})
8+
assert not firmware_metadata_is_v1({"metadata-version": "2.1.0"})
9+
assert not firmware_metadata_is_v1({"metadata-version": "3.0.0"})
10+
assert not firmware_metadata_is_v1({"metadata-version": "3.1.0"})
11+
12+
13+
def test_firmware_v2():
14+
assert not firmware_metadata_is_v2({"metadata-version": "1.0.0"})
15+
assert not firmware_metadata_is_v2({"metadata-version": "1.1.0"})
16+
assert firmware_metadata_is_v2({"metadata-version": "2.0.0"})
17+
assert firmware_metadata_is_v2({"metadata-version": "2.1.0"})
18+
assert not firmware_metadata_is_v2({"metadata-version": "3.0.0"})
19+
assert not firmware_metadata_is_v2({"metadata-version": "3.1.0"})

0 commit comments

Comments
 (0)