diff --git a/tools/mkdfu.py b/tools/mkdfu.py new file mode 100755 index 000000000..eecd00b55 --- /dev/null +++ b/tools/mkdfu.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# +# This program creates archives compatible with ESP32-S* ROM DFU implementation. +# +# The archives are in CPIO format. Each file which needs to be flashed is added to the archive +# as a separate file. In addition to that, a special index file, 'dfuinfo0.dat', is created. +# This file must be the first one in the archive. It contains binary structures describing each +# subsequent file (for example, where the file needs to be flashed/loaded). +# + +import argparse +from collections import namedtuple +import hashlib +import os +import struct +import zlib + +try: + import typing +except ImportError: + # Only used for type annotations + pass + +# CPIO ("new ASCII") format related things +CPIO_MAGIC = b"070701" +CPIO_STRUCT = b"=6s" + b"8s" * 13 +CPIOHeader = namedtuple( + "CPIOHeader", + [ + "magic", + "ino", + "mode", + "uid", + "gid", + "nlink", + "mtime", + "filesize", + "devmajor", + "devminor", + "rdevmajor", + "rdevminor", + "namesize", + "check", + ], +) +CPIO_TRAILER = "TRAILER!!!" + + +def make_cpio_header( + filename_len, file_len, is_trailer=False +): # type: (int, int, bool) -> CPIOHeader + """ Returns CPIOHeader for the given file name and file size """ + + def as_hex(val): # type: (int) -> bytes + return "{:08x}".format(val).encode("ascii") + + hex_0 = as_hex(0) + mode = hex_0 if is_trailer else as_hex(0o0100644) + nlink = as_hex(1) if is_trailer else hex_0 + return CPIOHeader( + magic=CPIO_MAGIC, + ino=hex_0, + mode=mode, + uid=hex_0, + gid=hex_0, + nlink=nlink, + mtime=hex_0, + filesize=as_hex(file_len), + devmajor=hex_0, + devminor=hex_0, + rdevmajor=hex_0, + rdevminor=hex_0, + namesize=as_hex(filename_len), + check=hex_0, + ) + + +# DFU format related things +# Structure of one entry in dfuinfo0.dat +DFUINFO_STRUCT = b" int + """ Calculate CRC32/JAMCRC of data, with an optional initial value """ + uint32_max = 0xFFFFFFFF + return uint32_max - (zlib.crc32(data, crc) & uint32_max) + + +def pad_bytes(b, multiple, padding=b"\x00"): # type: (bytes, int, bytes) -> bytes + """ Pad 'b' to a length divisible by 'multiple' """ + padded_len = (len(b) + multiple - 1) // multiple * multiple + return b + padding * (padded_len - len(b)) + + +class EspDfuWriter(object): + def __init__(self, dest_file): # type: (typing.BinaryIO) -> None + self.dest = dest_file + self.entries = [] # type: typing.List[bytes] + self.index = [] # type: typing.List[DFUInfo] + + def add_file(self, flash_addr, path): # type: (int, str) -> None + """ Add file to be written into flash at given address """ + with open(path, "rb") as f: + self._add_cpio_flash_entry(os.path.basename(path), flash_addr, f.read()) + + def finish(self): # type: () -> None + """ Write DFU file """ + # Prepare and add dfuinfo0.dat file + dfuinfo = b"".join([struct.pack(DFUINFO_STRUCT, *item) for item in self.index]) + self._add_cpio_entry(DFUINFO_FILE, dfuinfo, first=True) + + # Add CPIO archive trailer + self._add_cpio_entry(CPIO_TRAILER, b"", trailer=True) + + # Combine all the entries and pad the file + out_data = b"".join(self.entries) + cpio_block_size = 10240 + out_data = pad_bytes(out_data, cpio_block_size) + + # Add DFU suffix and CRC + out_data += struct.pack(DFUSUFFIX_STRUCT, *DFUSUFFIX_DEFAULT) + out_data += struct.pack(DFUCRC_STRUCT, dfu_crc(out_data)) + + # Finally write the entire binary + self.dest.write(out_data) + + def _add_cpio_flash_entry( + self, filename, flash_addr, data + ): # type: (str, int, bytes) -> None + md5 = hashlib.md5() + md5.update(data) + self.index.append( + DFUInfo( + address=flash_addr, + flags=0, + name=filename.encode("utf-8"), + md5=md5.digest(), + ) + ) + self._add_cpio_entry(filename, data) + + def _add_cpio_entry( + self, filename, data, first=False, trailer=False + ): # type: (str, bytes, bool, bool) -> None + filename_b = filename.encode("utf-8") + b"\x00" + cpio_header = make_cpio_header(len(filename_b), len(data), is_trailer=trailer) + entry = pad_bytes( + struct.pack(CPIO_STRUCT, *cpio_header) + filename_b, 4 + ) + pad_bytes(data, 4) + if not first: + self.entries.append(entry) + else: + self.entries.insert(0, entry) + + +def action_write(args): + writer = EspDfuWriter(args.output_file) + for addr, file in args.files: + writer.add_file(addr, file) + writer.finish() + + +class WriteArgsAction(argparse.Action): + """ Helper for argparse to parse argument pairs """ + + def __init__(self, *args, **kwargs): + super(WriteArgsAction, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # TODO: add validation + addr = 0 + result = [] + for i, value in enumerate(values): + if i % 2 == 0: + addr = int(value, 0) + else: + result.append((addr, value)) + + setattr(namespace, self.dest, result) + + +def main(): + parser = argparse.ArgumentParser("mkdfu") + + # Provision to add "info" command + subparsers = parser.add_subparsers(dest="command") + write_parser = subparsers.add_parser("write") + write_parser.add_argument("-o", "--output-file", type=argparse.FileType("wb")) + write_parser.add_argument( + "files", metavar="
", action=WriteArgsAction, nargs="+" + ) + + args = parser.parse_args() + print(repr(args)) + if args.command == "write": + action_write(args) + + +if __name__ == "__main__": + main()