| #!/usr/bin/env python3 |
| |
| import argparse |
| import sys |
| import pathlib |
| import zlib |
| |
| missing_modules = [] |
| |
| try: |
| from bitstring import BitArray |
| except ImportError: |
| missing_modules.append("bitstring") |
| |
| try: |
| import construct |
| from construct import this, len_, Struct, Padded, CString, Rebuild, Computed, RawCopy, Checksum, Hex, Bytes, Int8ul, Int16ul, Int32ul, Const, Flag, Padding, Array, Prefixed, GreedyBytes, GreedyRange, PaddedString |
| except ImportError: |
| missing_modules.append("construct") |
| |
| if len(missing_modules) > 0: |
| error_text = "" |
| if 'build' in sys.argv: |
| error_text += """ |
| Warning: no dfu file has been generated (missing python3 dependencies) |
| """ |
| error_text += f""" |
| dfu requires the following python module(s): {', '.join(missing_modules)} |
| install with: python3 -m pip install {' '.join(missing_modules)} |
| """ |
| # Soft fail to avoid cmake error |
| print(error_text) |
| sys.exit(0) |
| |
| |
| DFU_SIGNATURE = b'DfuSe' |
| DFU_size = Rebuild(Int32ul, 0) |
| |
| |
| def DFU_file_length(ctx): |
| '''Compute the entire file size + 4 bytes for CRC |
| |
| The total DFU file length is ostensibly the actual |
| length in bytes of the resulting file. |
| |
| However DFU File Manager does not seem to agree, |
| since it's output size is 16 bytes short. |
| |
| Since this is suspiciously the same as the suffix |
| length in bytes, we omit that number to match |
| DFU File Manager's output. |
| |
| ''' |
| size = 11 # DFU Header Length |
| # size += 16 # DFU Suffix Length |
| for target in ctx.targets: |
| # Each target has a 274 byte header consisting |
| # of the following fields: |
| size += Const(DFU_SIGNATURE).sizeof() # szSignature ('Target' in bytes) |
| size += Int8ul.sizeof() # bAlternateSetting |
| size += Int8ul.sizeof() # bTargetNamed |
| size += Padding(3).sizeof() # Padding |
| size += Padded(255, CString('utf8')).sizeof() # szTargetName |
| size += Int32ul.sizeof() # dwTargetSize |
| size += Int32ul.sizeof() # dwNbElements |
| size += DFU_target_size(target) |
| |
| return size |
| |
| |
| def DFU_target_size(ctx): |
| '''Returns the size of the target binary data, plus the |
| dwElementAddress header, and dwElementSize byte count. |
| ''' |
| size = 0 |
| |
| try: |
| images = ctx.images |
| except AttributeError: |
| images = ctx['images'] |
| |
| size += sum([DFU_image_size(image) for image in images]) |
| return size |
| |
| |
| def DFU_image_size(image): |
| return len(image['data']) + Int32ul.sizeof() + Int32ul.sizeof() |
| |
| |
| DFU_image = Struct( |
| 'dwElementAddress' / Hex(Int32ul), # Data offset address for image |
| 'data' / Prefixed(Int32ul, GreedyBytes) |
| ) |
| |
| DFU_target = Struct( |
| 'szSignature' / Const(b'Target'), # DFU target identifier |
| 'bAlternateSetting' / Int8ul, # Gives device alternate setting for which this image can be used |
| 'bTargetNamed' / Flag, # Boolean determining if the target is named |
| Padding(3), # Mystery bytes! |
| 'szTargetName' / Padded(255, CString('utf8')), # Target name |
| # DFU File Manager does not initialise this |
| # memory, so our file will not exactly match |
| # its output. |
| 'dwTargetSize' / Rebuild(Int32ul, DFU_target_size), # Total size of target images |
| 'dwNbElements' / Rebuild(Int32ul, len_(this.images)), # Count the number of target images |
| 'images' / GreedyRange(DFU_image) |
| ) |
| |
| DFU_body = Struct( |
| 'szSignature' / Const(DFU_SIGNATURE), # DFU format identifier (changes on major revisions) |
| 'bVersion' / Const(1, Int8ul), # DFU format revision (changes on minor revisions) |
| 'DFUImageSize' / Rebuild(Int32ul, DFU_file_length), # Total DFU file length in bytes |
| 'bTargets' / Rebuild(Int8ul, len_(this.targets)), # Number of targets in the file |
| |
| 'targets' / GreedyRange(DFU_target), |
| |
| 'bcdDevice' / Int16ul, # Firmware version, or 0xffff if ignored |
| 'idProduct' / Hex(Int16ul), # USB product ID or 0xffff to ignore |
| 'idVendor' / Hex(Int16ul), # USB vendor ID or 0xffff to ignore |
| 'bcdDFU' / Const(0x011A, Int16ul), # DFU specification number |
| 'ucDfuSignature' / Const(b'UFD'), # 0x44, 0x46 and 0x55 ie 'DFU' but reversed |
| 'bLength' / Const(16, Int8ul) # Length of the DFU suffix in bytes |
| ) |
| |
| DFU = Struct( |
| 'fields' / RawCopy(DFU_body), |
| 'dwCRC' / Checksum(Int32ul, # CRC calculated over the whole file, except for itself |
| lambda data: 0xffffffff ^ zlib.crc32(data), |
| this.fields.data) |
| ) |
| |
| |
| def display_dfu_info(parsed): |
| print(f''' |
| Device: {parsed.fields.value.bcdDevice} |
| Target: {parsed.fields.value.idProduct:04x}:{parsed.fields.value.idVendor:04x} |
| Size: {parsed.fields.value.DFUImageSize:,} bytes |
| Targets: {parsed.fields.value.bTargets}''') |
| for target in parsed.fields.value.targets: |
| print(f''' |
| Name: {target.szTargetName} |
| Alternate Setting: {target.bAlternateSetting} |
| Size: {target.dwTargetSize:,} bytes |
| Images: {target.dwNbElements}''') |
| for image in target.images: |
| print(f''' |
| Offset: {image.dwElementAddress} |
| Size: {len(image.data):,} bytes |
| ''') |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser() |
| subparsers = parser.add_subparsers(help='Commands', dest='command') |
| parser_build = subparsers.add_parser('build', help='Build a DFU image') |
| parser_read = subparsers.add_parser('read', help='Read a DFU image') |
| parser_dump = subparsers.add_parser('dump', help='Dump binary from DFU image') |
| parser_dump.add_argument('--force', action='store_true') |
| parser_build.add_argument('--out', type=pathlib.Path, help='Output file', default=None) |
| parser_build.add_argument('--address', type=int, default=0x08000000) |
| parser_build.add_argument('--force', action='store_true') |
| parser.add_argument('file', type=pathlib.Path, help='Input file') |
| parser.add_argument('--verbose', action='store_true') |
| |
| args = parser.parse_args() |
| |
| if not args.file.is_file(): |
| raise parser.error(f'Invalid input file "{args.file}"') |
| |
| if args.command == 'build': |
| if not args.out.parent.is_dir(): |
| raise parser.error(f'Output directory "{args.out.parent}" does not exist!') |
| elif args.out.is_file() and not args.force: |
| raise parser.error(f'Existing output file "{args.out}", use --force to overwrite!') |
| |
| if not args.file.suffix == ".bin": |
| raise parser.error(f'Input file "{args.file}", is not a .bin file?') |
| |
| output = DFU.build({'fields': {'value': { |
| 'targets': [{ |
| 'bAlternateSetting': 0, |
| 'bTargetNamed': True, |
| 'szTargetName': 'ST...', |
| 'images': [{ |
| 'dwElementAddress': args.address, |
| 'data': open(args.file, 'rb').read() |
| }] |
| }], |
| 'bcdDevice': 0, |
| 'idProduct': 0x0000, |
| 'idVendor': 0x0483 |
| }}}) |
| |
| if args.verbose: |
| print(f'''Packing "{args.file}" into "{args.out}"''') |
| display_dfu_info(DFU.parse(output)) |
| |
| open(args.out, 'wb').write(output) |
| |
| if args.command == 'read' or args.command == 'dump': |
| try: |
| parsed = DFU.parse(open(args.file, 'rb').read()) |
| except construct.core.ConstructError as error: |
| parser.error(f'Invalid dfu file {args.file} ({error})') |
| |
| display_dfu_info(parsed) |
| |
| if args.command == 'dump': |
| |
| for target in parsed.fields.value.targets: |
| target_id = target.bAlternateSetting |
| for image in target.images: |
| address = image.dwElementAddress |
| data = image.data |
| dest = str(args.file).replace('.dfu', '') |
| filename = f"{dest}-{target_id}-{address}.bin" |
| |
| if pathlib.Path(filename).is_file() and not args.force: |
| raise parser.error(f'Existing output file "{filename}", use --force to overwrite!') |
| |
| print(f"Dumping image at {address} to {filename} ({len(data)} bytes)") |
| |
| open(filename, 'wb').write(data) |