concatenate command: combine firmware and storage into one file

This commit is contained in:
Brian S. Stephan 2023-06-26 15:22:01 -05:00
parent c9b443f993
commit 85d84144fc
Signed by: bss
GPG Key ID: 3DE06D3180895FCB
7 changed files with 125 additions and 0 deletions

View File

@ -25,6 +25,22 @@ As above, plus also `pip install -Ur requirements/requirements-dev.txt` to get l
## Tools ## 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
**visualize-storage** reads a dump of a GP2040-CE board's flash storage section, where the configuration lives, and **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: And to dump your whole board:
``` ```
% picotool save -a whole-board.bin % picotool save -a whole-board.bin
``` ```

View File

@ -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)

View File

@ -19,6 +19,7 @@ dev = ["bandit", "decorator", "flake8", "flake8-blind-except", "flake8-builtins"
"flake8-pyproject", "mypy", "pip-tools", "pytest", "pytest-cov", "setuptools-scm", "tox"] "flake8-pyproject", "mypy", "pip-tools", "pytest", "pytest-cov", "setuptools-scm", "tox"]
[project.scripts] [project.scripts]
concatenate = "gp2040ce_bintools.builder:concatenate"
visualize-storage = "gp2040ce_bintools.storage:visualize" visualize-storage = "gp2040ce_bintools.storage:visualize"
[tool.flake8] [tool.flake8]

View File

@ -6,6 +6,16 @@ import pytest
HERE = os.path.dirname(os.path.abspath(__file__)) 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 @pytest.fixture
def storage_dump(): def storage_dump():
"""Read in a test storage dump file (101FE000-10200000) of a GP2040-CE board.""" """Read in a test storage dump file (101FE000-10200000) of a GP2040-CE board."""

Binary file not shown.

16
tests/test_builder.py Normal file
View File

@ -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)

View File

@ -1,5 +1,6 @@
"""Test our tools themselves to make sure they adhere to certain flags.""" """Test our tools themselves to make sure they adhere to certain flags."""
import json import json
import os
from subprocess import run from subprocess import run
from gp2040ce_bintools import __version__ 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 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(): def test_storage_dump_invocation():
"""Test that a normal invocation against a dump works.""" """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'], result = run(['visualize-storage', '-P', 'tests/test-files/proto-files', 'tests/test-files/test-storage-area.bin'],