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
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
```

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"]
[project.scripts]
concatenate = "gp2040ce_bintools.builder:concatenate"
visualize-storage = "gp2040ce_bintools.storage:visualize"
[tool.flake8]

View File

@ -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."""

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."""
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'],