From 095fac19f15bc99f6848386a2d78781684d03ddf Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Wed, 28 Jun 2023 11:47:48 -0500 Subject: [PATCH] properly account for combining config section with firmware prior to this, the concatenate assumed it was concatenating a firmware with a full *storage section*, e.g. the already-padded 8192 bytes, but it's equally valuable now that I'm creating configs to have just a config section + footer, which needs to be padded 8192. now concatenate supports both --- README.md | 8 ++++---- gp2040ce_bintools/builder.py | 28 ++++++++++++++++++++-------- gp2040ce_bintools/storage.py | 19 +++++++++++++++++++ tests/conftest.py | 10 ++++++++++ tests/test-files/test-config.bin | Bin 0 -> 2046 bytes tests/test_builder.py | 21 ++++++++++++++++++++- tests/test_storage.py | 12 ++++++++++++ 7 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 tests/test-files/test-config.bin diff --git a/README.md b/README.md index 35a3c07..5871556 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ In all cases, online help can be retrieved by providing the `-h` or ``--help`` f ### concatenate **concatenate** combines a GP2040-CE firmware .bin file (such as from a fresh build) and a GP2040-CE board's storage -section .bin, and produces a properly-offset .bin file suitable for flashing to a board. This may be useful to ensure -the board is flashed with a particular configuration, for instances such as producing a binary to flash many boards with -a particular configuration (specific customizations, etc.), or keeping documented backups of what you're testing with -during development. +section .bin or config (with footer) .bin, and produces a properly-offset .bin file suitable for flashing to a board. +This may be useful to ensure the board is flashed with a particular configuration, for instances such as producing a +binary to flash many boards with a particular configuration (specific customizations, etc.), or keeping documented +backups of what you're testing with during development. Sample usage: diff --git a/gp2040ce_bintools/builder.py b/gp2040ce_bintools/builder.py index 313f88a..84fa44e 100644 --- a/gp2040ce_bintools/builder.py +++ b/gp2040ce_bintools/builder.py @@ -3,7 +3,7 @@ import argparse import logging from gp2040ce_bintools import core_parser -from gp2040ce_bintools.storage import STORAGE_LOCATION +from gp2040ce_bintools.storage import STORAGE_LOCATION, pad_config_to_storage_size logger = logging.getLogger(__name__) @@ -17,6 +17,18 @@ class FirmwareLengthError(ValueError): """Exception raised when the firmware is too large to fit the known storage location.""" +def combine_firmware_and_config(firmware_binary: bytearray, config_binary: bytearray) -> bytearray: + """Given firmware and config binaries, combine the two to one, with proper offsets for GP2040-CE. + + Args: + firmware_binary: binary data of the raw GP2040-CE firmware, probably but not necessarily unpadded + config_binary: binary data of board config + footer, possibly padded to be a full storage section + Returns: + the resulting correctly-offset binary suitable for a GP2040-CE board + """ + return pad_firmware_up_to_storage(firmware_binary) + pad_config_to_storage_size(config_binary) + + def concatenate_firmware_and_storage_files(firmware_filename: str, storage_filename: str, combined_filename: str): """Open the provided binary files and combine them into one combined GP2040-CE with config file. @@ -26,7 +38,7 @@ def concatenate_firmware_and_storage_files(firmware_filename: str, storage_filen combined_filename: filename of where to write the combine binary """ with open(firmware_filename, 'rb') as firmware, open(storage_filename, 'rb') as storage: - new_binary = pad_firmware_up_to_storage(firmware.read()) + bytearray(storage.read()) + new_binary = combine_firmware_and_config(bytearray(firmware.read()), bytearray(storage.read())) with open(combined_filename, 'wb') as combined: combined.write(new_binary) @@ -35,7 +47,7 @@ def pad_firmware_up_to_storage(firmware: bytes) -> bytearray: """Provide a copy of the firmware padded with zero bytes up to the provided position. Args: - firmware: the read-in binary file to process + firmware: the firmware binary to process Returns: the resulting padded binary as a bytearray Raises: @@ -47,7 +59,7 @@ def pad_firmware_up_to_storage(firmware: bytes) -> bytearray: raise FirmwareLengthError(f"provided firmware binary is larger than the start of " f"storage at {STORAGE_LOCATION}!") - return bytes(bytearray(firmware) + bytearray(b'\x00' * bytes_to_pad)) + return bytearray(firmware) + bytearray(b'\x00' * bytes_to_pad) ############ @@ -57,13 +69,13 @@ def pad_firmware_up_to_storage(firmware: bytes) -> bytearray: def concatenate(): """Combine a built firmware .bin and a storage .bin.""" parser = argparse.ArgumentParser( - description="Combine a compiled GP2040-CE firmware-only .bin and an existing storage area .bin into one file " - "suitable for flashing onto a board.", + description="Combine a compiled GP2040-CE firmware-only .bin and an existing storage area or config .bin " + "into one file suitable for flashing onto a board.", parents=[core_parser], ) parser.add_argument('firmware_filename', help=".bin file of a GP2040-CE firmware, probably from a build") - parser.add_argument('storage_filename', help=".bin file of a GP2040-CE board's storage section") + parser.add_argument('config_filename', help=".bin file of a GP2040-CE board's storage section or config w/footer") parser.add_argument('new_binary_filename', help="output .bin file of the resulting firmware + storage") args, _ = parser.parse_known_args() - concatenate_firmware_and_storage_files(args.firmware_filename, args.storage_filename, args.new_binary_filename) + concatenate_firmware_and_storage_files(args.firmware_filename, args.config_filename, args.new_binary_filename) diff --git a/gp2040ce_bintools/storage.py b/gp2040ce_bintools/storage.py index 629cb15..79169e1 100644 --- a/gp2040ce_bintools/storage.py +++ b/gp2040ce_bintools/storage.py @@ -114,6 +114,25 @@ def get_storage_section(content: bytes) -> bytes: logger.debug("returning bytes from %s to %s", hex(STORAGE_LOCATION), hex(STORAGE_LOCATION + STORAGE_SIZE)) return content[STORAGE_LOCATION:(STORAGE_LOCATION + STORAGE_SIZE)] + +def pad_config_to_storage_size(config: bytes) -> bytearray: + """Provide a copy of the config (with footer) padded with zero bytes to be the proper storage section size. + + Args: + firmware: the config section binary to process + Returns: + the resulting padded binary as a bytearray + Raises: + FirmwareLengthError: if the is larger than the storage location + """ + bytes_to_pad = STORAGE_SIZE - len(config) + logger.debug("config is length %s, padding %s bytes", len(config), bytes_to_pad) + if bytes_to_pad < 0: + raise ConfigLengthError(f"provided config binary is larger than the allowed storage of " + f"storage at {STORAGE_SIZE} bytes!") + + return bytearray(b'\x00' * bytes_to_pad) + bytearray(config) + ############ # COMMANDS # ############ diff --git a/tests/conftest.py b/tests/conftest.py index 4062aa9..22df89f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,16 @@ import pytest HERE = os.path.dirname(os.path.abspath(__file__)) +@pytest.fixture +def config_binary(): + """Read in a test GP2040-CE configuration, Protobuf serialized binary form with footer.""" + filename = os.path.join(HERE, 'test-files', 'test-config.bin') + with open(filename, 'rb') as file: + content = file.read() + + yield content + + @pytest.fixture def firmware_binary(): """Read in a test GP2040-CE firmware binary file.""" diff --git a/tests/test-files/test-config.bin b/tests/test-files/test-config.bin new file mode 100644 index 0000000000000000000000000000000000000000..0229fd230d3eb8578e60f31237e3425f7f3c4c3d GIT binary patch literal 2046 zcmeHI!D}N`82^25lFV!xGh=jRtT8i0&>_?^1Yu(=ZRo|)i?JYgEbN-+o2Kc!2=xyTzsvl7-|xNm z{oaH5J|LNR-<`iOU(6U9Ghi|s4qTK_!K|B%X=KDQF`>|GOp6&TAJa5LDTMh%G0dyw zFt06#`DFeX?GYnm8ur7E=BlMsWoG4EHMt(ICF*+Pp^ft%J=UglmmVz?v^C>pvueNO z{7UYVrGP58XeCjdTYtRvL_OcgZOnW0Y@43$(hJ4!G*JyEY;qP{?`us;4!Lr(~01wFDKZyyeD&Of8@!0i3c)8^GiSC zbHM#i_=Km3gYKyhCOoThD8bSnCw#7y9C(WVniq0g{fIw*y-&Y*OUH6x4fqHB*0nFZ zw-!XA=P&&}xOYcw_GzDcP&yBq*Za+pex=j8DbCk9f4AS_H_}zrvJL?2RX*sbJi92A zrr3UErqlUM=T|qO{uSEUyl~CR>8;!m)S)Z*-9JaU>|)v)f+`L`cLqlbi<`O?2u*~- zJHzJaD6MYt0QFN&yyGWMd22)?-HV-a>xMF%5b;&Nxk^3|>HKp^75e?s z6juhJ$tdVWVN#B?Ei>TM*-84e(h2{M{~--!10JDGD1SwwQH|S3(T$gs3ay~Z*0tKX zdaM!OP(4btX{JlbE#i})o`kn0yd&XV3HuV>lkkCr4<#H(__lE`gdM^z!6)ny4hV;YfN+~IBpeYuhBiZ&VT+;1u+6Z; zu*={x>@gfL95Mt9t3NBE^TUAX{G#*DW(A(S2q!awVphcSZ=R{@PPBA9QjYCM$yx;K z5o|