blob: 651f52eec9898e666dfcec0558a2f56bc4eeb4c6 [file] [log] [blame] [edit]
#!/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)