Some checks failed
Build and deploy Docs site to GitHub Pages / github-pages (push) Has been cancelled
PR Build Check / build (Heltec_v3_companion_radio_ble) (push) Has been cancelled
PR Build Check / build (Heltec_v3_repeater) (push) Has been cancelled
PR Build Check / build (Heltec_v3_room_server) (push) Has been cancelled
PR Build Check / build (LilyGo_Tlora_C6_repeater_) (push) Has been cancelled
PR Build Check / build (PicoW_repeater) (push) Has been cancelled
PR Build Check / build (RAK_4631_companion_radio_ble) (push) Has been cancelled
PR Build Check / build (RAK_4631_repeater) (push) Has been cancelled
PR Build Check / build (RAK_4631_room_server) (push) Has been cancelled
PR Build Check / build (wio-e5-mini_repeater) (push) Has been cancelled
153 lines
5.2 KiB
Python
153 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Convert .hex or .uf2 to nRF52 DFU .zip for Web Serial flashing.
|
|
|
|
Usage:
|
|
python3 create_dfu_zip.py firmware.hex firmware.dfu.zip
|
|
python3 create_dfu_zip.py firmware.uf2 firmware.dfu.zip
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import struct
|
|
import zipfile
|
|
import hashlib
|
|
|
|
def hex_to_bin(hex_path):
|
|
"""Convert Intel HEX to raw binary."""
|
|
from intelhex import IntelHex
|
|
ih = IntelHex(hex_path)
|
|
# Use the full address range or just the used portion
|
|
min_addr = ih.minaddr() if ih.minaddr() is not None else 0
|
|
max_addr = ih.maxaddr() if ih.maxaddr() is not None else 0
|
|
size = max_addr - min_addr + 1
|
|
# Align to page size (4KB for nRF52840)
|
|
page_size = 0x1000
|
|
aligned_size = ((size + page_size - 1) // page_size) * page_size
|
|
data = ih.tobinarray(start=min_addr, size=aligned_size)
|
|
return bytes(data), min_addr
|
|
|
|
def uf2_to_bin(uf2_path):
|
|
"""Convert UF2 to raw binary."""
|
|
FAMILY_NRF52840 = 0xADA52840
|
|
data = {}
|
|
with open(uf2_path, 'rb') as f:
|
|
while True:
|
|
block = f.read(512)
|
|
if not block or len(block) < 512:
|
|
break
|
|
magic0, magic1, flags, addr, size, seq, num = struct.unpack_from('<IIIIIII', block, 0)
|
|
if magic0 != 0x0A324655 or magic1 != 0x9E5D5157:
|
|
continue
|
|
payload = block[32:32+size]
|
|
for i in range(0, len(payload), 4):
|
|
word = struct.unpack_from('<I', payload, i)[0]
|
|
offset = addr + i
|
|
data[offset] = word
|
|
if not data:
|
|
raise ValueError("No valid UF2 blocks found")
|
|
addresses = sorted(data.keys())
|
|
min_addr = addresses[0] & ~0xFFF # align to 4K
|
|
max_addr = addresses[-1]
|
|
page_size = 0x1000
|
|
size = max_addr - min_addr + 1
|
|
aligned_size = ((size + page_size - 1) // page_size) * page_size
|
|
bin_data = bytearray(aligned_size)
|
|
for addr, word in data.items():
|
|
offset = addr - min_addr
|
|
if offset + 4 <= len(bin_data):
|
|
struct.pack_into('<I', bin_data, offset, word)
|
|
return bytes(bin_data), min_addr
|
|
|
|
def create_dfu_zip(bin_data, base_addr, fw_version=1, hw_version=52):
|
|
"""Create nRF DFU zip from binary data."""
|
|
|
|
# Build init packet (.dat) per nRF DFU spec
|
|
# Format (little-endian):
|
|
# [0x01] - signature (DFU init packet)
|
|
# [fw_type: 1] - 0x04 = application
|
|
# [sd_count: 4] - number of SD requirements (1)
|
|
# [sd_array: sd_count * 4] - SD requirement (0xFFFE = any)
|
|
# [hw_version: 4]
|
|
# [fw_version: 4]
|
|
# [fwid_type: 2] - 0x0001 = SHA-256 (not used, set to 0)
|
|
# [fwid_length: 2]
|
|
# [fwid: fwid_length]
|
|
|
|
# Simpler init packet format as expected by dfu.js:
|
|
# It expects init_packet_data with components.data array
|
|
|
|
init_packet = bytearray()
|
|
init_packet.append(0x01) # signature
|
|
init_packet.append(0x04) # fw_type: application
|
|
# softdevice requirements
|
|
init_packet += struct.pack('<I', 0xFFFE) # SD required: any
|
|
init_packet += struct.pack('<I', 0xFFFFFFFF) # terminator
|
|
init_packet += struct.pack('<I', hw_version) # HW version
|
|
init_packet += struct.pack('<I', fw_version) # FW version
|
|
# FWID (empty)
|
|
init_packet += struct.pack('<II', 0, 0) # type=0, len=0
|
|
|
|
# build manifest
|
|
manifest = {
|
|
"manifest": {
|
|
"application": {
|
|
"bin_file": "firmware.bin",
|
|
"dat_file": "firmware.dat",
|
|
"init_packet_data": {
|
|
"fw_version": fw_version,
|
|
"hw_version": hw_version,
|
|
"softdevice_req": [0xFFFE],
|
|
"components": [
|
|
{
|
|
"data": list(init_packet)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return manifest, bin_data, bytes(init_packet)
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print("Usage: create_dfu_zip.py <input.hex|uf2> <output.zip> [fw_version] [hw_version]")
|
|
sys.exit(1)
|
|
|
|
input_path = sys.argv[1]
|
|
output_path = sys.argv[2]
|
|
fw_version = int(sys.argv[3]) if len(sys.argv) > 3 else 1
|
|
hw_version = int(sys.argv[4]) if len(sys.argv) > 4 else 52
|
|
|
|
ext = os.path.splitext(input_path)[1].lower()
|
|
|
|
if ext == '.hex':
|
|
try:
|
|
bin_data, base_addr = hex_to_bin(input_path)
|
|
except ImportError:
|
|
print("Error: intelhex package required for .hex files.")
|
|
print("Install: pip install intelhex")
|
|
sys.exit(1)
|
|
elif ext == '.uf2':
|
|
bin_data, base_addr = uf2_to_bin(input_path)
|
|
else:
|
|
print(f"Unsupported format: {ext}")
|
|
sys.exit(1)
|
|
|
|
manifest, fw_bin, fw_dat = create_dfu_zip(bin_data, base_addr, fw_version, hw_version)
|
|
|
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
|
|
zf.writestr("firmware.bin", fw_bin)
|
|
zf.writestr("firmware.dat", fw_dat)
|
|
|
|
size = os.path.getsize(output_path)
|
|
print(f"DFU zip created: {output_path}")
|
|
print(f" Size: {size} bytes ({size/1024:.0f} KB)")
|
|
print(f" Firmware: {len(fw_bin)} bytes @ 0x{base_addr:08X}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|