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
**concatenate** combines a GP2040-CE firmware .bin file (such as from a fresh build) and a GP2040-CE board's storage **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 section .bin or config (with footer) .bin, and produces a properly-offset .bin file suitable for flashing to a board.
the board is flashed with a particular configuration, for instances such as producing a binary to flash many boards with This may be useful to ensure the board is flashed with a particular configuration, for instances such as producing a
a particular configuration (specific customizations, etc.), or keeping documented backups of what you're testing with binary to flash many boards with a particular configuration (specific customizations, etc.), or keeping documented
during development. backups of what you're testing with during development.
Sample usage: Sample usage:

View File

@ -3,7 +3,7 @@ import argparse
import logging import logging
from gp2040ce_bintools import core_parser 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__) 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.""" """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): 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. """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 combined_filename: filename of where to write the combine binary
""" """
with open(firmware_filename, 'rb') as firmware, open(storage_filename, 'rb') as storage: 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: with open(combined_filename, 'wb') as combined:
combined.write(new_binary) 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. """Provide a copy of the firmware padded with zero bytes up to the provided position.
Args: Args:
firmware: the read-in binary file to process firmware: the firmware binary to process
Returns: Returns:
the resulting padded binary as a bytearray the resulting padded binary as a bytearray
Raises: 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 " raise FirmwareLengthError(f"provided firmware binary is larger than the start of "
f"storage at {STORAGE_LOCATION}!") 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(): def concatenate():
"""Combine a built firmware .bin and a storage .bin.""" """Combine a built firmware .bin and a storage .bin."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Combine a compiled GP2040-CE firmware-only .bin and an existing storage area .bin into one file " description="Combine a compiled GP2040-CE firmware-only .bin and an existing storage area or config .bin "
"suitable for flashing onto a board.", "into one file suitable for flashing onto a board.",
parents=[core_parser], parents=[core_parser],
) )
parser.add_argument('firmware_filename', help=".bin file of a GP2040-CE firmware, probably from a build") 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") parser.add_argument('new_binary_filename', help="output .bin file of the resulting firmware + storage")
args, _ = parser.parse_known_args() 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)) logger.debug("returning bytes from %s to %s", hex(STORAGE_LOCATION), hex(STORAGE_LOCATION + STORAGE_SIZE))
return content[STORAGE_LOCATION:(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 # # COMMANDS #
############ ############

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 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 @pytest.fixture
def firmware_binary(): def firmware_binary():
"""Read in a test GP2040-CE firmware binary file.""" """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.""" """Tests for the image builder module."""
import pytest 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): def test_padding_firmware(firmware_binary):
@ -10,6 +11,24 @@ def test_padding_firmware(firmware_binary):
assert len(padded) == 2088960 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): def test_padding_firmware_too_big(firmware_binary):
"""Test that firmware is padded to the expected size.""" """Test that firmware is padded to the expected size."""
with pytest.raises(FirmwareLengthError): 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)) config = storage.get_config(storage.get_storage_section(whole_board_dump))
assert config.boardVersion == 'v0.7.2' assert config.boardVersion == 'v0.7.2'
assert config.hotkeyOptions.hotkeyF1Up.dpadMask == 1 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)