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
This commit is contained in:
Brian S. Stephan 2023-06-28 11:47:48 -05:00
parent 85d84144fc
commit 095fac19f1
Signed by: bss
GPG Key ID: 3DE06D3180895FCB
7 changed files with 85 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -1,7 +1,8 @@
"""Tests for the image builder module."""
import pytest
from gp2040ce_bintools.builder import FirmwareLengthError, pad_firmware_up_to_storage
from gp2040ce_bintools.builder import FirmwareLengthError, combine_firmware_and_config, pad_firmware_up_to_storage
from gp2040ce_bintools.storage import get_config_footer, get_storage_section
def test_padding_firmware(firmware_binary):
@ -10,6 +11,24 @@ def test_padding_firmware(firmware_binary):
assert len(padded) == 2088960
def test_firmware_plus_storage(firmware_binary, storage_dump):
"""Test that combining firmware and storage produces a valid combined binary."""
whole_board = combine_firmware_and_config(firmware_binary, storage_dump)
# if this is valid, we should be able to find the storage and footer again
storage = get_storage_section(whole_board)
footer_size, _, _ = get_config_footer(storage)
assert footer_size == 2032
def test_firmware_plus_config_binary(firmware_binary, config_binary):
"""Test that combining firmware and storage produces a valid combined binary."""
whole_board = combine_firmware_and_config(firmware_binary, config_binary)
# if this is valid, we should be able to find the storage and footer again
storage = get_storage_section(whole_board)
footer_size, _, _ = get_config_footer(storage)
assert footer_size == 2032
def test_padding_firmware_too_big(firmware_binary):
"""Test that firmware is padded to the expected size."""
with pytest.raises(FirmwareLengthError):

View File

@ -95,3 +95,15 @@ def test_config_from_whole_board_parses(whole_board_dump):
config = storage.get_config(storage.get_storage_section(whole_board_dump))
assert config.boardVersion == 'v0.7.2'
assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1
def test_pad_config_to_storage(config_binary):
"""Test that we can properly pad a config section to the correct storage section size."""
storage_section = storage.pad_config_to_storage_size(config_binary)
assert len(storage_section) == 8192
def test_pad_config_to_storage_raises(config_binary):
"""Test that we raise an exception if the config is bigger than the storage section."""
with pytest.raises(storage.ConfigLengthError):
_ = storage.pad_config_to_storage_size(config_binary * 5)