|
1 | 1 | #!/usr/bin/env python |
2 | | -import sys, math, zlib, struct |
| 2 | +import argparse, math, struct, time, zlib |
3 | 3 |
|
4 | 4 | def readfile(filepath): |
5 | 5 | try: |
6 | 6 | with open(filepath, "rb") as f: |
7 | 7 | return f.read() |
8 | | - except FileNotFoundError as err: |
9 | | - exit("\nERROR: Unable to open partition file '%s' !\n" % err.filename) |
| 8 | + except OSError as err: |
| 9 | + exit("\nERROR: Failed to read file '%s': %s\n" % (err.filename, err.strerror)) |
10 | 10 |
|
11 | | -if len(sys.argv) < 9: |
12 | | - exit("usage: mkfw.py [--esplay] output_file.fw 'description' tile.raw type subtype size label file.bin " |
13 | | - "[type subtype size label file.bin, ...]") |
14 | 11 |
|
15 | | -args = sys.argv[1:] |
| 12 | +def get_partitions(args, flash_offset = 0): |
| 13 | + print("\nPartitions:") |
| 14 | + partitions = [] |
| 15 | + # ota_next_id = 16 |
| 16 | + while len(args) >= 5: |
| 17 | + partype = int(args.pop(0), 0) |
| 18 | + subtype = int(args.pop(0), 0) |
| 19 | + size = int(args.pop(0), 0) |
| 20 | + label = args.pop(0) |
| 21 | + filename = args.pop(0) |
16 | 22 |
|
17 | | -try: |
18 | | - args.remove("--esplay") |
19 | | - fw_head = b"ESPLAY_FIRMWARE_V00_01" |
20 | | - fw_pack = "<22s40s8256s" |
21 | | -except: |
22 | | - fw_head = b"ODROIDGO_FIRMWARE_V00_01" |
23 | | - fw_pack = "<24s40s8256s" |
| 23 | + data = readfile(filename) if filename != "none" else b"" |
24 | 24 |
|
25 | | -fw_name = args.pop(0) |
26 | | -fw_desc = args.pop(0) |
27 | | -fw_tile = args.pop(0) |
28 | | -fw_size = 0 |
29 | | -fw_part = 0 |
| 25 | + flash_alignment = 0x10000 if partype == 0 else 0x1000 |
| 26 | + flash_offset = math.ceil(flash_offset / flash_alignment) * flash_alignment |
| 27 | + # Technically the size only has to be aligned to 0x1000, but it's wasted space if two apps follow eachother |
| 28 | + flash_size = math.ceil(max(size, len(data)) / flash_alignment) * flash_alignment |
30 | 29 |
|
31 | | -fw_data = struct.pack(fw_pack, fw_head, fw_desc.encode(), readfile(fw_tile)) |
| 30 | + print(" [%d]: type=%d, subtype=%d, size=%d (%d%% used), label=%s" |
| 31 | + % (len(partitions), partype, subtype, flash_size, len(data) / flash_size * 100, label)) |
32 | 32 |
|
33 | | -while len(args) >= 5: |
34 | | - partype = int(args.pop(0), 0) |
35 | | - subtype = int(args.pop(0), 0) |
36 | | - size = int(args.pop(0), 0) |
37 | | - label = args.pop(0) |
38 | | - filename = args.pop(0) |
| 33 | + if size != 0 and len(data) > size: |
| 34 | + print(" > WARNING: File larger than partition (+%d bytes), increased size to %d" |
| 35 | + % (len(data) - size, flash_size)) |
| 36 | + elif size != 0 and size != flash_size: |
| 37 | + print(" > WARNING: Incorrect alignment, adjusted size to %d" % (flash_size)) |
39 | 38 |
|
40 | | - if not subtype: |
41 | | - subtype = 16 + fw_part # OTA starts at 16 |
| 39 | + partitions.append((partype, subtype, label, flash_offset, flash_size, data)) |
| 40 | + flash_offset += flash_size |
42 | 41 |
|
43 | | - data = readfile(filename) |
44 | | - real_size = max(size, math.ceil(len(data) / 0x10000) * 0x10000) |
45 | | - usage = len(data) / real_size * 100 |
| 42 | + if len(args) > 0: |
| 43 | + print(f"WARNING: Trailing arguments (incomplete partition?): {args}") |
46 | 44 |
|
47 | | - print("[%d]: type=%d, subtype=%d, size=%d (%d%% used), label=%s" |
48 | | - % (fw_part, partype, subtype, real_size, usage, label)) |
| 45 | + print("Flash size: %.3f MB\n" % (flash_offset / 1048576)) |
| 46 | + return partitions |
49 | 47 |
|
50 | | - if real_size > size and size != 0: |
51 | | - print(" > WARNING: Partition smaller than file (+%d bytes), increasing size to %d" |
52 | | - % (len(data) - size, real_size)) |
53 | 48 |
|
54 | | - fw_data += struct.pack("<BBxx16sIII", partype, subtype, label.encode(), 0, real_size, len(data)) |
55 | | - fw_data += data |
56 | | - fw_size += real_size |
57 | | - fw_part += 1 |
| 49 | +def create_firmware(fw_type, partitions, icon_file, name, version, target): |
| 50 | + description = f"{name} {version}".encode() |
| 51 | + tile_bin = readfile(icon_file) if icon_file != "none" else b"\x00" * 8256 |
| 52 | + if fw_type == "odroid": |
| 53 | + fw_data = struct.pack("<24s40s8256s", b"ODROIDGO_FIRMWARE_V00_01", description, tile_bin) |
| 54 | + else: |
| 55 | + fw_data = struct.pack("<22s40s8256s", b"ESPLAY_FIRMWARE_V00_01", description, tile_bin) |
| 56 | + for partype, subtype, label, offset, size, data in partitions: |
| 57 | + fw_data += struct.pack("<BBxx16sIII", partype, subtype, label.encode(), 0, size, len(data)) |
| 58 | + fw_data += data |
| 59 | + return fw_data + struct.pack("I", zlib.crc32(fw_data)) |
58 | 60 |
|
59 | | -fw_data += struct.pack("I", zlib.crc32(fw_data)) |
60 | 61 |
|
61 | | -fp = open(fw_name, "wb") |
62 | | -fp.write(fw_data) |
63 | | -fp.close() |
| 62 | +def create_image(chip_type, partitions, bootloader_file, name, version, target): |
| 63 | + bootloader_offset, table_offset, prog_offset = { |
| 64 | + "esp32s3": (0x0000, 0x8000, 0x10000), |
| 65 | + "esp32p4": (0x2000, 0x8000, 0x10000), |
| 66 | + "esp32": (0x1000, 0x8000, 0x10000), |
| 67 | + }.get(chip_type) |
| 68 | + partitions = [ |
| 69 | + (1, 0x02, "nvs", 0x9000, 0x4000, b""), |
| 70 | + (1, 0x00, "otadata", 0xD000, 0x2000, b""), |
| 71 | + (1, 0x01, "phy_init", 0xF000, 0x1000, b""), |
| 72 | + ] + partitions |
| 73 | + output_data = bytearray(b"\xFF" * max(p[3] + p[4] for p in partitions)) |
64 | 74 |
|
65 | | -print("\nTotal size: %.3f MB" % (fw_size / 1048576)) |
66 | | -print("\nFile '%s' successfully created.\n" % (fw_name)) |
| 75 | + # It would be better to call gen_esp32part.py but it's a hassle to make it work for all our supported platforms... |
| 76 | + table_bin = b"" |
| 77 | + for partype, subtype, label, offset, size, data in partitions: |
| 78 | + table_bin += struct.pack("<2sBBLL16sL", b"\xAA\x50", partype, subtype, offset, size, label.encode(), 0) |
| 79 | + table_bin += b"\xFF" * (0xC00 - len(table_bin)) # Pad table with 0xFF otherwise it will be invalid |
| 80 | + # The lack of an MD5 entry is deliberate to maintain compatibility with very old bootloaders |
| 81 | + output_data[table_offset : table_offset + len(table_bin)] = table_bin |
| 82 | + |
| 83 | + bootloader_bin = readfile(bootloader_file) |
| 84 | + output_data[bootloader_offset : bootloader_offset + len(bootloader_bin)] = bootloader_bin |
| 85 | + |
| 86 | + for partype, subtype, label, offset, size, data in partitions: |
| 87 | + output_data[offset : offset + len(data)] = data |
| 88 | + |
| 89 | + # Append the information structure used by retro-go's updater. |
| 90 | + # (Maybe I could hide it somewhere else to avoid changing the size of the image) |
| 91 | + output_data += struct.pack( |
| 92 | + "<8s28s28s28sII156s", |
| 93 | + "RG_IMG_0".encode(), # Magic number "RG01" |
| 94 | + name.encode(), # Project name |
| 95 | + version.encode(), # Project version |
| 96 | + target.encode(), # Project target device |
| 97 | + int(time.time()), # Unix timestamp |
| 98 | + zlib.crc32(output_data),# CRC of the image not including this footer |
| 99 | + b"\xff" * 156, # 0xFF padding of the reserved area |
| 100 | + ) |
| 101 | + return output_data |
| 102 | + |
| 103 | + |
| 104 | +if __name__ == "__main__": |
| 105 | + parser = argparse.ArgumentParser(description="Retro-Go firmware tool") |
| 106 | + parser.add_argument("--type", choices=["odroid", "esplay", "esp32", "esp32s3", "esp32p4"], default="odroid") |
| 107 | + parser.add_argument("--bootloader", default="none") |
| 108 | + parser.add_argument("--name", default="unknown") |
| 109 | + parser.add_argument("--icon", default="none") |
| 110 | + parser.add_argument("--version", default="unknown") |
| 111 | + parser.add_argument("--target", default="unknown") |
| 112 | + parser.add_argument("output_file") |
| 113 | + parser.add_argument("partitions", nargs="*", metavar="(type subtype size label file.bin)") |
| 114 | + args = parser.parse_args() |
| 115 | + |
| 116 | + if args.type in ["odroid", "esplay"]: |
| 117 | + partitions = get_partitions(args.partitions) |
| 118 | + data = create_firmware(args.type, partitions, args.icon, args.name, args.version, args.target) |
| 119 | + else: |
| 120 | + partitions = get_partitions(args.partitions, 0x10000) |
| 121 | + data = create_image(args.type, partitions, args.bootloader, args.name, args.version, args.target) |
| 122 | + with open(args.output_file, "wb") as fp: |
| 123 | + fp.write(data) |
| 124 | + |
| 125 | + print("File '%s' successfully created.\n" % (args.output_file)) |
0 commit comments