this allows for putting a JSON representation of a config into the user config area of a binary to be flashed on the board. this allows for conveying configs as simple JSON files and using them to convey specific binaries by parts this is the start of the support to use JSON files on a *new* section of the binary reserved for board default configs Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
233 lines
11 KiB
Python
233 lines
11 KiB
Python
"""Build binary files for a GP2040-CE board.
|
|
|
|
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
|
|
SPDX-License-Identifier: MIT
|
|
"""
|
|
import argparse
|
|
import copy
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from google.protobuf.message import Message
|
|
|
|
from gp2040ce_bintools import core_parser
|
|
from gp2040ce_bintools.rp2040 import get_bootsel_endpoints, read, write
|
|
from gp2040ce_bintools.storage import (STORAGE_BINARY_LOCATION, STORAGE_BOOTSEL_ADDRESS, STORAGE_SIZE,
|
|
get_config_from_json, pad_config_to_storage_size, serialize_config_with_footer)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
GP2040CE_START_ADDRESS = 0x10000000
|
|
GP2040CE_SIZE = 2 * 1024 * 1024
|
|
|
|
|
|
#################
|
|
# LIBRARY ITEMS #
|
|
#################
|
|
|
|
|
|
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,
|
|
replace_extra: bool = False) -> 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
|
|
replace_extra: if larger than normal firmware files should have their overage replaced
|
|
Returns:
|
|
the resulting correctly-offset binary suitable for a GP2040-CE board
|
|
"""
|
|
return (pad_firmware_up_to_storage(firmware_binary, or_truncate=replace_extra) +
|
|
pad_config_to_storage_size(config_binary))
|
|
|
|
|
|
def concatenate_firmware_and_storage_files(firmware_filename: str, binary_user_config_filename: Optional[str] = None,
|
|
json_user_config_filename: Optional[str] = None,
|
|
combined_filename: str = '', usb: bool = False,
|
|
replace_extra: bool = False) -> None:
|
|
"""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
|
|
binary_user_config_filename: filename of the user config section to read, in binary format
|
|
json_user_config_filename: filename of the user config section to read, in JSON format
|
|
combined_filename: filename of where to write the combine binary
|
|
replace_extra: if larger than normal firmware files should have their overage replaced
|
|
"""
|
|
new_binary = None
|
|
if binary_user_config_filename:
|
|
with open(firmware_filename, 'rb') as firmware, open(binary_user_config_filename, 'rb') as storage:
|
|
new_binary = combine_firmware_and_config(bytearray(firmware.read()), bytearray(storage.read()),
|
|
replace_extra=replace_extra)
|
|
elif json_user_config_filename:
|
|
with open(firmware_filename, 'rb') as firmware, open(json_user_config_filename, 'r') as json_file:
|
|
config = get_config_from_json(json_file.read())
|
|
serialized_config = serialize_config_with_footer(config)
|
|
new_binary = combine_firmware_and_config(bytearray(firmware.read()), serialized_config,
|
|
replace_extra=replace_extra)
|
|
|
|
if not new_binary:
|
|
raise ValueError("no means to create a binary was provided")
|
|
|
|
if combined_filename:
|
|
with open(combined_filename, 'wb') as combined:
|
|
combined.write(new_binary)
|
|
if usb:
|
|
endpoint_out, endpoint_in = get_bootsel_endpoints()
|
|
write(endpoint_out, endpoint_in, GP2040CE_START_ADDRESS, bytes(new_binary))
|
|
|
|
|
|
def get_gp2040ce_from_usb() -> tuple[bytes, object, object]:
|
|
"""Read the firmware + config sections from a USB device.
|
|
|
|
Returns:
|
|
the bytes from the board, along with the USB out and in endpoints for reference
|
|
"""
|
|
# open the USB device and get the config
|
|
endpoint_out, endpoint_in = get_bootsel_endpoints()
|
|
logger.debug("reading DEVICE ID %s:%s, bus %s, address %s", hex(endpoint_out.device.idVendor),
|
|
hex(endpoint_out.device.idProduct), endpoint_out.device.bus, endpoint_out.device.address)
|
|
content = read(endpoint_out, endpoint_in, GP2040CE_START_ADDRESS, GP2040CE_SIZE)
|
|
return content, endpoint_out, endpoint_in
|
|
|
|
|
|
def pad_firmware_up_to_storage(firmware: bytes, or_truncate: bool = False) -> bytearray:
|
|
"""Provide a copy of the firmware padded with zero bytes up to the provided position.
|
|
|
|
Args:
|
|
firmware: the firmware binary to process
|
|
or_truncate: if the firmware is longer than expected, just return the max size
|
|
Returns:
|
|
the resulting padded binary as a bytearray
|
|
Raises:
|
|
FirmwareLengthError: if the firmware is larger than the storage location
|
|
"""
|
|
bytes_to_pad = STORAGE_BINARY_LOCATION - len(firmware)
|
|
logger.debug("firmware is length %s, padding %s bytes", len(firmware), bytes_to_pad)
|
|
if bytes_to_pad < 0:
|
|
if or_truncate:
|
|
return bytearray(firmware[0:STORAGE_BINARY_LOCATION])
|
|
raise FirmwareLengthError(f"provided firmware binary is larger than the start of "
|
|
f"storage at {STORAGE_BINARY_LOCATION}!")
|
|
|
|
return bytearray(firmware) + bytearray(b'\x00' * bytes_to_pad)
|
|
|
|
|
|
def replace_config_in_binary(board_binary: bytearray, config_binary: bytearray) -> bytearray:
|
|
"""Given (presumed) whole board and config binaries, combine the two to one, with proper offsets for GP2040-CE.
|
|
|
|
Whatever is in the board binary is not sanity checked, and is overwritten. If it is
|
|
too small to be a board dump, it is nonetheless expanded and overwritten to fit the
|
|
proper size.
|
|
|
|
Args:
|
|
board_binary: binary data of a whole board dump from a GP2040-CE board
|
|
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
|
|
"""
|
|
if len(board_binary) < STORAGE_BINARY_LOCATION + STORAGE_SIZE:
|
|
# this is functionally the same, since this doesn't sanity check the firmware
|
|
return combine_firmware_and_config(board_binary, config_binary)
|
|
else:
|
|
new_binary = bytearray(copy.copy(board_binary))
|
|
new_config = pad_config_to_storage_size(config_binary)
|
|
new_binary[STORAGE_BINARY_LOCATION:(STORAGE_BINARY_LOCATION + STORAGE_SIZE)] = new_config
|
|
return new_binary
|
|
|
|
|
|
def write_new_config_to_filename(config: Message, filename: str, inject: bool = False) -> None:
|
|
"""Serialize the provided config to the specified file.
|
|
|
|
The file may be replaced, creating a configuration section-only binary, or appended to
|
|
an existing file that is grown to place the config section in the proper location.
|
|
|
|
Args:
|
|
config: the Protobuf configuration to write to disk
|
|
filename: the filename to write the serialized configuration to
|
|
inject: if True, the file is read in and has its storage section replaced; if False,
|
|
the whole file is replaced
|
|
"""
|
|
if inject:
|
|
config_binary = serialize_config_with_footer(config)
|
|
with open(filename, 'rb') as file:
|
|
existing_binary = file.read()
|
|
binary = replace_config_in_binary(bytearray(existing_binary), config_binary)
|
|
else:
|
|
binary = serialize_config_with_footer(config)
|
|
|
|
with open(filename, 'wb') as file:
|
|
file.write(binary)
|
|
|
|
|
|
def write_new_config_to_usb(config: Message, endpoint_out: object, endpoint_in: object):
|
|
"""Serialize the provided config to a device over USB, in the proper location for a GP2040-CE board.
|
|
|
|
Args:
|
|
config: the Protobuf configuration to write to a RP2040 board in BOOTSEL mode
|
|
endpoint_out: the USB endpoint to write to
|
|
endpoint_in: the USB endpoint to read from
|
|
"""
|
|
serialized = serialize_config_with_footer(config)
|
|
# we don't write the whole area, just the minimum from the end of the storage section
|
|
# nevertheless, the USB device needs writes to start at 256 byte boundaries
|
|
logger.debug("serialized: %s", serialized)
|
|
# not sure why this minimal padding isn't working but it leads to corruption
|
|
# maybe claims that erase need to be on 4096 byte sectors?
|
|
# padding = 256 - (len(serialized) % 256)
|
|
padding = 4096 - (len(serialized) % 4096)
|
|
logger.debug("length: %s with %s bytes of padding", len(serialized), padding)
|
|
binary = bytearray(b'\x00' * padding) + serialized
|
|
logger.debug("binary for writing: %s", binary)
|
|
write(endpoint_out, endpoint_in, STORAGE_BOOTSEL_ADDRESS + (STORAGE_SIZE - len(binary)), bytes(binary))
|
|
|
|
|
|
############
|
|
# 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 or config .bin "
|
|
"into one file suitable for flashing onto a board.",
|
|
parents=[core_parser],
|
|
)
|
|
parser.add_argument('--replace-extra', action='store_true',
|
|
help="if the firmware file is larger than the location of storage, perhaps because it's "
|
|
"actually a full board dump, overwrite its config section with the config binary")
|
|
parser.add_argument('firmware_filename', help=".bin file of a GP2040-CE firmware, probably from a build")
|
|
user_config_group = parser.add_mutually_exclusive_group(required=True)
|
|
user_config_group.add_argument('--binary-user-config-filename',
|
|
help=".bin file of a GP2040-CE user config w/footer")
|
|
user_config_group.add_argument('--json-user-config-filename', help=".json file of a GP2040-CE user config")
|
|
output_group = parser.add_mutually_exclusive_group(required=True)
|
|
output_group.add_argument('--usb', action='store_true', help="write the resulting firmware + storage to USB")
|
|
output_group.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,
|
|
binary_user_config_filename=args.binary_user_config_filename,
|
|
json_user_config_filename=args.json_user_config_filename,
|
|
combined_filename=args.new_binary_filename, usb=args.usb,
|
|
replace_extra=args.replace_extra)
|
|
|
|
|
|
def dump_gp2040ce():
|
|
"""Copy the whole GP2040-CE section off of a BOOTSEL mode board."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Read the GP2040-CE firmware + storage section off of a connected USB RP2040 in BOOTSEL mode.",
|
|
parents=[core_parser],
|
|
)
|
|
parser.add_argument('binary_filename', help="output .bin file of the resulting firmware + storage")
|
|
|
|
args, _ = parser.parse_known_args()
|
|
content, _, _ = get_gp2040ce_from_usb()
|
|
with open(args.binary_filename, 'wb') as out_file:
|
|
out_file.write(content)
|