diff --git a/README.md b/README.md index 1f34aec..ea00397 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,8 @@ forcedSetupOptions { ### Dumping the storage section -The storage section of a GP2040-CE board starts at `0x101FE000`. A current limitation of the **visualize-storage** tool -is that it can only read the Protobuf serialized data, not the footer that is also used as part of the storage engine. -As such, currently, the binary is expected to be truncated slightly. This will be improved to read the storage footer in -the near future, but for now, know your `picotool` invocation will be slightly different. - -To dump your board's storage: +The storage section of a GP2040-CE board is a reserved 8 KB starting at `0x101FE000`. To dump your board's storage: ``` -% picotool save -r 101FE000 101FFFF4 memory.bin +% picotool save -r 101FE000 10200000 memory.bin ``` diff --git a/gp2040ce_bintools/storage.py b/gp2040ce_bintools/storage.py index 7fc266d..0142c32 100644 --- a/gp2040ce_bintools/storage.py +++ b/gp2040ce_bintools/storage.py @@ -1,35 +1,73 @@ """Interact with the protobuf config from a picotool flash dump of a GP2040-CE board.""" import argparse -import pprint +import logging from gp2040ce_bintools import core_parser, get_config_pb2 +logger = logging.getLogger(__name__) -def get_config(filename): - """Load the protobuf section of an flash and display the contents.""" - with open(filename, 'rb') as dump: - # read off the unused space - while True: - byte = dump.read(1) - if byte != b'\x00': - break - content = byte + dump.read() +FOOTER_SIZE = 12 +FOOTER_MAGIC = b'\x65\xe3\xf1\xd2' + + +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 + if len(content) < FOOTER_SIZE: + raise ValueError("provided content is not large enough to have a config footer!") + + footer = content[-FOOTER_SIZE:] + 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 + + +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) + config.ParseFromString(content[-(size+FOOTER_SIZE):-FOOTER_SIZE]) + logger.debug("parsed: %s", config) return config def visualize(): - """Pretty print the contents of GP2040-CE's storage.""" + """Print the contents of GP2040-CE's storage.""" parser = argparse.ArgumentParser( - description="Read a the configuration storage section from a GP2040-CE board dump and print out its contents.", + description="Read the configuration section from a dump of a GP2040-CE board's storage section and print out " + "its contents.", parents=[core_parser], ) - parser.add_argument('filename', help=".bin file of a GP2040-CE board's storage section, bytes 101FE000-101FFFF4 " - "(e.g. picotool save -r 101FE000 101FFFF4 memory.bin") + parser.add_argument('filename', help=".bin file of a GP2040-CE board's storage section, bytes 101FE000-10200000") args, _ = parser.parse_known_args() + with open(args.filename, 'rb') as dump: + content = dump.read() - config = get_config(args.filename) - pprint.pprint(config) + config = get_config(content) + print(config)