2023-06-20 12:59:47 -05:00
|
|
|
"""Interact with the protobuf config from a picotool flash dump of a GP2040-CE board."""
|
2023-06-20 12:50:32 -05:00
|
|
|
import argparse
|
2023-06-20 18:49:21 -05:00
|
|
|
import logging
|
2023-06-20 12:50:32 -05:00
|
|
|
|
|
|
|
from gp2040ce_bintools import core_parser, get_config_pb2
|
|
|
|
|
2023-06-20 18:49:21 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
2023-06-20 12:50:32 -05:00
|
|
|
|
2023-06-21 15:20:21 -05:00
|
|
|
STORAGE_LOCATION = 0x1FE000
|
|
|
|
STORAGE_SIZE = 8192
|
|
|
|
|
2023-06-20 18:49:21 -05:00
|
|
|
FOOTER_SIZE = 12
|
|
|
|
FOOTER_MAGIC = b'\x65\xe3\xf1\xd2'
|
|
|
|
|
|
|
|
|
2023-06-21 12:27:45 -05:00
|
|
|
###############
|
|
|
|
# LIB METHODS #
|
|
|
|
###############
|
|
|
|
|
|
|
|
|
|
|
|
def get_config(content: bytes) -> dict:
|
|
|
|
"""Read the config from a GP2040-CE storage section.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
content: bytes from a GP2040-CE board's storage section
|
|
|
|
Returns:
|
|
|
|
the parsed configuration
|
|
|
|
"""
|
|
|
|
size, _, _ = get_config_footer(content)
|
|
|
|
|
|
|
|
config_pb2 = get_config_pb2()
|
|
|
|
config = config_pb2.Config()
|
|
|
|
config.ParseFromString(content[-(size + FOOTER_SIZE):-FOOTER_SIZE])
|
|
|
|
logger.debug("parsed: %s", config)
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
2023-06-20 18:49:21 -05:00
|
|
|
def get_config_footer(content: bytes) -> tuple[int, int, str]:
|
|
|
|
"""Confirm and retrieve the config footer from a series of bytes of GP2040-CE storage.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
content: bytes from a GP2040-CE board's storage section
|
|
|
|
Returns:
|
|
|
|
the discovered config size, config CRC, and magic from the config footer
|
|
|
|
Raises:
|
|
|
|
ValueError: if the provided bytes are not a config footer
|
|
|
|
"""
|
|
|
|
# last 12 bytes are the footer
|
2023-06-21 15:20:21 -05:00
|
|
|
logger.debug("length of content to look for footer in: %s", len(content))
|
2023-06-20 18:49:21 -05:00
|
|
|
if len(content) < FOOTER_SIZE:
|
|
|
|
raise ValueError("provided content is not large enough to have a config footer!")
|
|
|
|
|
|
|
|
footer = content[-FOOTER_SIZE:]
|
2023-06-21 15:20:21 -05:00
|
|
|
logger.debug("suspected footer magic: %s", footer[-4:])
|
2023-06-20 18:49:21 -05:00
|
|
|
if footer[-4:] != FOOTER_MAGIC:
|
|
|
|
raise ValueError("content's magic is not as expected!")
|
|
|
|
|
|
|
|
config_size = int.from_bytes(reversed(footer[:4]), 'big')
|
|
|
|
config_crc = int.from_bytes(reversed(footer[4:8]), 'big')
|
|
|
|
config_magic = f'0x{footer[8:12].hex()}'
|
|
|
|
|
|
|
|
# one last sanity check
|
|
|
|
if len(content) < config_size + FOOTER_SIZE:
|
|
|
|
raise ValueError("provided content is not large enough according to the config footer!")
|
|
|
|
|
|
|
|
logger.debug("detected footer (size:%s, crc:%s, magic:%s", config_size, config_crc, config_magic)
|
|
|
|
return config_size, config_crc, config_magic
|
|
|
|
|
|
|
|
|
2023-06-21 15:20:21 -05:00
|
|
|
def get_config_from_file(filename: str, whole_board: bool = False) -> dict:
|
|
|
|
"""Read the specified file (memory dump or whole board dump) and get back its config section.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
filename: the filename of the file to open and read
|
2023-06-21 17:54:27 -05:00
|
|
|
whole_board: optional, if true, attempt to find the storage section from its normal location on a board
|
2023-06-21 15:20:21 -05:00
|
|
|
Returns:
|
|
|
|
the parsed configuration
|
|
|
|
"""
|
|
|
|
with open(filename, 'rb') as dump:
|
|
|
|
content = dump.read()
|
|
|
|
|
|
|
|
if whole_board:
|
|
|
|
return get_config(get_storage_section(content))
|
|
|
|
else:
|
|
|
|
return get_config(content)
|
|
|
|
|
|
|
|
|
|
|
|
def get_storage_section(content: bytes) -> bytes:
|
|
|
|
"""Pull out what should be the GP2040-CE storage section from a whole board dump.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
content: bytes of a GP2040-CE whole board dump
|
|
|
|
Returns:
|
|
|
|
the presumed storage section from the binary
|
|
|
|
"""
|
|
|
|
# a whole board must be at least as big as the known fences
|
|
|
|
logger.debug("length of content to look for storage in: %s", len(content))
|
|
|
|
if len(content) < STORAGE_LOCATION + STORAGE_SIZE:
|
|
|
|
raise ValueError("provided content is not large enough to have a storage section!")
|
|
|
|
|
|
|
|
logger.debug("returning bytes from %s to %s", hex(STORAGE_LOCATION), hex(STORAGE_LOCATION + STORAGE_SIZE))
|
|
|
|
return content[STORAGE_LOCATION:(STORAGE_LOCATION + STORAGE_SIZE)]
|
|
|
|
|
2023-06-21 12:27:45 -05:00
|
|
|
############
|
|
|
|
# COMMANDS #
|
|
|
|
############
|
2023-06-20 12:50:32 -05:00
|
|
|
|
|
|
|
|
|
|
|
def visualize():
|
2023-06-20 18:49:21 -05:00
|
|
|
"""Print the contents of GP2040-CE's storage."""
|
2023-06-20 12:50:32 -05:00
|
|
|
parser = argparse.ArgumentParser(
|
2023-06-20 18:49:21 -05:00
|
|
|
description="Read the configuration section from a dump of a GP2040-CE board's storage section and print out "
|
|
|
|
"its contents.",
|
2023-06-20 12:50:32 -05:00
|
|
|
parents=[core_parser],
|
|
|
|
)
|
2023-06-21 15:20:21 -05:00
|
|
|
parser.add_argument('--whole-board', action='store_true', help="indicate the binary file is a whole board dump")
|
|
|
|
parser.add_argument('filename', help=".bin file of a GP2040-CE board's storage section, bytes 101FE000-10200000, "
|
|
|
|
"or of a GP2040-CE's whole board dump if --whole-board is specified")
|
2023-06-20 12:50:32 -05:00
|
|
|
args, _ = parser.parse_known_args()
|
2023-06-21 15:20:21 -05:00
|
|
|
print(get_config_from_file(args.filename, whole_board=args.whole_board))
|