Skip to content

Commit a7058ae

Browse files
committed
firmware: move create_firmware_blob function
This moves the create_firmware function from the flash module to the firmware module since is strictly deals with the firmware.zip file. Also add the license text to the return value while we are making breaking changes. This way all info in the firmware.zip file is included in the return value. Also add the firmware module to the docs.
1 parent 8c8d3bd commit a7058ae

File tree

8 files changed

+196
-169
lines changed

8 files changed

+196
-169
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Experimental support for relative and nested imports.
1111
- Added support for `firmware.metadata.json` v2.0.0.
1212

13+
### Changed
14+
- Move/renamed `pybricksdev.flash.create_firmware` to `pybricksdev.firmware.create_firmware_blob`.
15+
- Changed return value of `pybricksdev.firmware.create_firmware_blob` to include license text.
16+
1317
## [1.0.0-alpha.30] - 2022-08-26
1418

1519
### Added

docs/api/firmware.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
===========================
2+
Pybricks firmware.zip files
3+
===========================
4+
5+
.. automodule:: pybricksdev.firmware
6+
:members:

docs/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ API documentation
77
:caption: Modules:
88

99
ble/index
10+
firmware
1011

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
("py:class", "bleak.backends.device.BLEDevice"),
6868
("py:exc", "asyncio.TimeoutError"),
6969
("py:class", "bleak.BleakClient"),
70+
("py:obj", "typing.Union"),
71+
("py:class", "os.PathLike"),
7072
]
7173

7274
add_module_names = False

pybricksdev/cli/flash.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
from ..compile import compile_file
4141
from ..connections.lego import REPLHub
4242
from ..dfu import flash_dfu
43-
from ..flash import BootloaderConnection, create_firmware
43+
from ..firmware import create_firmware_blob
44+
from ..flash import BootloaderConnection
4445
from ..tools import chunk
4546
from ..tools.checksum import xor_bytes
4647

@@ -293,7 +294,8 @@ async def flash_firmware(firmware_zip: BinaryIO, new_name: Optional[str]) -> Non
293294

294295
print("Creating firmware...")
295296

296-
firmware, metadata = await create_firmware(firmware_zip, new_name)
297+
# REVISIT: require accepting license agreement either interactively or by command line option
298+
firmware, metadata, license = await create_firmware_blob(firmware_zip, new_name)
297299
hub_kind = HubKind(metadata["device-id"])
298300

299301
if hub_kind in (HubKind.TECHNIC_SMALL, HubKind.TECHNIC_LARGE):

pybricksdev/firmware.py

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

8+
import io
9+
import json
10+
import os
11+
import struct
812
import sys
9-
from typing import Literal, TypedDict, Union
13+
import zipfile
14+
from typing import BinaryIO, Literal, Optional, Tuple, TypedDict, Union
15+
1016

1117
if sys.version_info < (3, 10):
12-
from typing_extensions import TypeGuard
18+
from typing_extensions import TypeGuard, TypeAlias
1319
else:
14-
from typing import TypeGuard
20+
from typing import TypeGuard, TypeAlias
21+
22+
import semver
23+
24+
from .compile import compile_file, save_script
25+
from .tools.checksum import crc32_checksum, sum_complement
1526

1627

1728
class FirmwareMetadataV100(
@@ -76,7 +87,7 @@ class FirmwareMetadataV200(
7687
Type for data contained in ``firmware.metadata.json`` files of any 1.x version.
7788
"""
7889

79-
AnyFirmwareV2Metadata = FirmwareMetadataV200
90+
AnyFirmwareV2Metadata: TypeAlias = FirmwareMetadataV200
8091
"""
8192
Type for data contained in ``firmware.metadata.json`` files of any 2.x version.
8293
"""
@@ -87,13 +98,161 @@ class FirmwareMetadataV200(
8798
"""
8899

89100

90-
def firmware_metadata_is_v1(
101+
def _firmware_metadata_is_v1(
91102
metadata: AnyFirmwareMetadata,
92103
) -> TypeGuard[AnyFirmwareV1Metadata]:
93104
return metadata["metadata-version"].startswith("1.")
94105

95106

96-
def firmware_metadata_is_v2(
107+
def _firmware_metadata_is_v2(
97108
metadata: AnyFirmwareMetadata,
98109
) -> TypeGuard[AnyFirmwareV2Metadata]:
99110
return metadata["metadata-version"].startswith("2.")
111+
112+
113+
async def _create_firmware_v1(
114+
metadata: AnyFirmwareV1Metadata, archive: zipfile.ZipFile, name: Optional[str]
115+
) -> bytearray:
116+
base = archive.open("firmware-base.bin").read()
117+
118+
if "main.py" in archive.namelist():
119+
main_py = io.TextIOWrapper(archive.open("main.py"))
120+
121+
mpy = await compile_file(
122+
save_script(main_py.read()),
123+
metadata["mpy-abi-version"],
124+
metadata["mpy-cross-options"],
125+
)
126+
else:
127+
mpy = b""
128+
129+
# start with base firmware binary blob
130+
firmware = bytearray(base)
131+
# pad with 0s until user-mpy-offset
132+
firmware.extend(0 for _ in range(metadata["user-mpy-offset"] - len(firmware)))
133+
# append 32-bit little-endian main.mpy file size
134+
firmware.extend(struct.pack("<I", len(mpy)))
135+
# append main.mpy file
136+
firmware.extend(mpy)
137+
# pad with 0s to align to 4-byte boundary
138+
firmware.extend(0 for _ in range(-len(firmware) % 4))
139+
140+
# Update hub name if given
141+
if name:
142+
if semver.compare(metadata["metadata-version"], "1.1.0") < 0:
143+
raise ValueError(
144+
"this firmware image does not support setting the hub name"
145+
)
146+
147+
name = name.encode() + b"\0"
148+
149+
max_size = metadata["max-hub-name-size"]
150+
151+
if len(name) > max_size:
152+
raise ValueError(
153+
f"name is too big - must be < {metadata['max-hub-name-size']} UTF-8 bytes"
154+
)
155+
156+
offset = metadata["hub-name-offset"]
157+
firmware[offset : offset + len(name)] = name
158+
159+
# Get checksum for this firmware
160+
if metadata["checksum-type"] == "sum":
161+
checksum = sum_complement(io.BytesIO(firmware), metadata["max-firmware-size"])
162+
elif metadata["checksum-type"] == "crc32":
163+
checksum = crc32_checksum(io.BytesIO(firmware), metadata["max-firmware-size"])
164+
else:
165+
raise ValueError(f"unsupported checksum type: {metadata['checksum-type']}")
166+
167+
# Append checksum to the firmware
168+
firmware.extend(struct.pack("<I", checksum))
169+
170+
return firmware
171+
172+
173+
async def _create_firmware_v2(
174+
metadata: AnyFirmwareV2Metadata, archive: zipfile.ZipFile, name: Optional[str]
175+
) -> bytearray:
176+
base = archive.open("firmware-base.bin").read()
177+
178+
# start with base firmware binary blob
179+
firmware = bytearray(base)
180+
181+
# Update hub name if given
182+
if name:
183+
name = name.encode() + b"\0"
184+
185+
max_size = metadata["hub-name-size"]
186+
187+
if len(name) > max_size:
188+
raise ValueError(
189+
f"name is too big - must be < {metadata['hub-name-size']} UTF-8 bytes"
190+
)
191+
192+
offset = metadata["hub-name-offset"]
193+
firmware[offset : offset + len(name)] = name
194+
195+
# Get checksum for this firmware
196+
if metadata["checksum-type"] == "sum":
197+
checksum = sum_complement(io.BytesIO(firmware), metadata["checksum-size"])
198+
elif metadata["checksum-type"] == "crc32":
199+
checksum = crc32_checksum(io.BytesIO(firmware), metadata["checksum-size"])
200+
else:
201+
raise ValueError(f"unsupported checksum type: {metadata['checksum-type']}")
202+
203+
# Append checksum to the firmware
204+
firmware.extend(struct.pack("<I", checksum))
205+
206+
return firmware
207+
208+
209+
async def create_firmware_blob(
210+
firmware_zip: Union[str, os.PathLike, BinaryIO], name: Optional[str] = None
211+
) -> Tuple[bytes, AnyFirmwareMetadata, str]:
212+
"""Creates a firmware blob from base firmware and an optional custom name.
213+
214+
.. note:: The firmware.zip file must contain the following files::
215+
216+
firmware-base.bin
217+
firmware.metadata.json
218+
ReadMe_OSS.txt
219+
220+
221+
v1.x also supports an optional ``main.py`` file that is appended to
222+
the firmware.
223+
224+
225+
Arguments:
226+
firmware_zip:
227+
Path to the firmware zip file or a file-like object.
228+
name:
229+
A custom name for the hub.
230+
231+
Returns:
232+
Tuple of composite binary blob for flashing, the metadata, and the
233+
license text.
234+
235+
Raises:
236+
ValueError:
237+
A name is given but the firmware does not support it or the name
238+
exceeds the alloted space in the firmware.
239+
240+
"""
241+
242+
with zipfile.ZipFile(firmware_zip) as archive:
243+
with archive.open("firmware.metadata.json") as f:
244+
metadata: AnyFirmwareMetadata = json.load(f)
245+
246+
with archive.open("ReadMe_OSS.txt") as f:
247+
license = f.read().decode()
248+
249+
if _firmware_metadata_is_v1(metadata):
250+
firmware = await _create_firmware_v1(metadata, archive, name)
251+
elif _firmware_metadata_is_v2(metadata):
252+
firmware = await _create_firmware_v2(metadata, archive, name)
253+
else:
254+
raise ValueError(
255+
f"unsupported metadata version: {metadata['metadata-version']}"
256+
)
257+
258+
return firmware, metadata, license

0 commit comments

Comments
 (0)