55Utilities for working with Pybricks ``firmware.zip`` files.
66"""
77
8+ import io
9+ import json
10+ import os
11+ import struct
812import sys
9- from typing import Literal , TypedDict , Union
13+ import zipfile
14+ from typing import BinaryIO , Literal , Optional , Tuple , TypedDict , Union
15+
1016
1117if sys .version_info < (3 , 10 ):
12- from typing_extensions import TypeGuard
18+ from typing_extensions import TypeGuard , TypeAlias
1319else :
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
1728class FirmwareMetadataV100 (
@@ -76,7 +87,7 @@ class FirmwareMetadataV200(
7687Type for data contained in ``firmware.metadata.json`` files of any 1.x version.
7788"""
7889
79- AnyFirmwareV2Metadata = FirmwareMetadataV200
90+ AnyFirmwareV2Metadata : TypeAlias = FirmwareMetadataV200
8091"""
8192Type 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