diff --git a/README.md b/README.md index 4111b4d..35a3c07 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,22 @@ As above, plus also `pip install -Ur requirements/requirements-dev.txt` to get l ## Tools +In all cases, online help can be retrieved by providing the `-h` or ``--help`` flags to the below programs. + +### 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. + +Sample usage: + +``` +% concatenate build/GP2040-CE_foo_bar.bin storage-dump.bin new-firmware-with-config.bin +``` + ### visualize-storage **visualize-storage** reads a dump of a GP2040-CE board's flash storage section, where the configuration lives, and @@ -121,6 +137,7 @@ context. The storage section of a GP2040-CE board is a reserved 8 KB starting at ``` And to dump your whole board: + ``` % picotool save -a whole-board.bin ``` diff --git a/gp2040ce_bintools/builder.py b/gp2040ce_bintools/builder.py new file mode 100644 index 0000000..313f88a --- /dev/null +++ b/gp2040ce_bintools/builder.py @@ -0,0 +1,69 @@ +"""Build binary files for a GP2040-CE board.""" +import argparse +import logging + +from gp2040ce_bintools import core_parser +from gp2040ce_bintools.storage import STORAGE_LOCATION + +logger = logging.getLogger(__name__) + + +################# +# LIBRARY ITEMS # +################# + + +class FirmwareLengthError(ValueError): + """Exception raised when the firmware is too large to fit the known storage location.""" + + +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. + + Args: + firmware_filename: filename of the firmware binary to read + storage_filename: filename of the storage section to read + 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()) + with open(combined_filename, 'wb') as combined: + combined.write(new_binary) + + +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 + Returns: + the resulting padded binary as a bytearray + Raises: + FirmwareLengthError: if the firmware is larger than the storage location + """ + bytes_to_pad = STORAGE_LOCATION - len(firmware) + logger.debug("firmware is length %s, padding %s bytes", len(firmware), bytes_to_pad) + if bytes_to_pad < 0: + 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)) + + +############ +# COMMANDS # +############ + +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.", + 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('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) diff --git a/pyproject.toml b/pyproject.toml index f219e86..eb40e1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dev = ["bandit", "decorator", "flake8", "flake8-blind-except", "flake8-builtins" "flake8-pyproject", "mypy", "pip-tools", "pytest", "pytest-cov", "setuptools-scm", "tox"] [project.scripts] +concatenate = "gp2040ce_bintools.builder:concatenate" visualize-storage = "gp2040ce_bintools.storage:visualize" [tool.flake8] diff --git a/tests/conftest.py b/tests/conftest.py index 23fdcae..4062aa9 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 firmware_binary(): + """Read in a test GP2040-CE firmware binary file.""" + filename = os.path.join(HERE, 'test-files', 'test-firmware.bin') + with open(filename, 'rb') as file: + content = file.read() + + yield content + + @pytest.fixture def storage_dump(): """Read in a test storage dump file (101FE000-10200000) of a GP2040-CE board.""" diff --git a/tests/test-files/test-firmware.bin b/tests/test-files/test-firmware.bin new file mode 100644 index 0000000..8827ffb Binary files /dev/null and b/tests/test-files/test-firmware.bin differ diff --git a/tests/test_builder.py b/tests/test_builder.py new file mode 100644 index 0000000..a5829a9 --- /dev/null +++ b/tests/test_builder.py @@ -0,0 +1,16 @@ +"""Tests for the image builder module.""" +import pytest + +from gp2040ce_bintools.builder import FirmwareLengthError, pad_firmware_up_to_storage + + +def test_padding_firmware(firmware_binary): + """Test that firmware is padded to the expected size.""" + padded = pad_firmware_up_to_storage(firmware_binary) + assert len(padded) == 2088960 + + +def test_padding_firmware_too_big(firmware_binary): + """Test that firmware is padded to the expected size.""" + with pytest.raises(FirmwareLengthError): + _ = pad_firmware_up_to_storage(firmware_binary + firmware_binary + firmware_binary) diff --git a/tests/test_commands.py b/tests/test_commands.py index fa2a681..831c613 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,5 +1,6 @@ """Test our tools themselves to make sure they adhere to certain flags.""" import json +import os from subprocess import run from gp2040ce_bintools import __version__ @@ -18,6 +19,17 @@ def test_help_flag(): assert 'Read the configuration section from a dump of a GP2040-CE board' in result.stdout +def test_concatenate_invocation(tmpdir): + """Test that a normal invocation against a dump works.""" + out_filename = os.path.join(tmpdir, 'out.bin') + _ = run(['concatenate', 'tests/test-files/test-firmware.bin', 'tests/test-files/test-storage-area.bin', + out_filename]) + with open(out_filename, 'rb') as out_file, open('tests/test-files/test-storage-area.bin', 'rb') as storage_file: + out = out_file.read() + storage = storage_file.read() + assert out[2088960:2097152] == storage + + def test_storage_dump_invocation(): """Test that a normal invocation against a dump works.""" result = run(['visualize-storage', '-P', 'tests/test-files/proto-files', 'tests/test-files/test-storage-area.bin'],