feat: add web flasher (Vue 3 + Web Serial DFU) and DFU zip generation
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
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
This commit is contained in:
152
flasher/create_dfu_zip.py
Normal file
152
flasher/create_dfu_zip.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user