149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the one-time WildFire image from server-built firmware artifacts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import struct
|
|
import zlib
|
|
from pathlib import Path
|
|
|
|
|
|
BOOT_BASE = 0x10000000
|
|
APP_BASE = 0x10010000
|
|
APP_END = 0x10080000
|
|
METADATA_ADDRESS = 0x14000000
|
|
METADATA_MAGIC = 0x4F544131
|
|
METADATA_VERSION = 1
|
|
|
|
|
|
def parse_ihex(path: Path) -> tuple[dict[int, int], int | None]:
|
|
memory: dict[int, int] = {}
|
|
upper = 0
|
|
start_linear: int | None = None
|
|
for line_number, raw in enumerate(path.read_text(encoding="ascii").splitlines(), 1):
|
|
line = raw.strip()
|
|
if not line:
|
|
continue
|
|
if not line.startswith(":"):
|
|
raise ValueError(f"{path}:{line_number}: invalid Intel HEX record")
|
|
record = bytes.fromhex(line[1:])
|
|
if len(record) < 5 or sum(record) & 0xFF:
|
|
raise ValueError(f"{path}:{line_number}: invalid record checksum")
|
|
length = record[0]
|
|
if len(record) != length + 5:
|
|
raise ValueError(f"{path}:{line_number}: invalid record length")
|
|
offset = int.from_bytes(record[1:3], "big")
|
|
kind = record[3]
|
|
payload = record[4 : 4 + length]
|
|
if kind == 0:
|
|
address = upper + offset
|
|
for index, value in enumerate(payload):
|
|
absolute = address + index
|
|
previous = memory.get(absolute)
|
|
if previous is not None and previous != value:
|
|
raise ValueError(f"overlapping data at 0x{absolute:08X}")
|
|
memory[absolute] = value
|
|
elif kind == 2:
|
|
if length != 2:
|
|
raise ValueError("invalid extended segment record")
|
|
upper = int.from_bytes(payload, "big") << 4
|
|
elif kind == 4:
|
|
if length != 2:
|
|
raise ValueError("invalid extended linear record")
|
|
upper = int.from_bytes(payload, "big") << 16
|
|
elif kind == 5:
|
|
if length != 4:
|
|
raise ValueError("invalid start linear address record")
|
|
start_linear = int.from_bytes(payload, "big")
|
|
elif kind in (1, 3):
|
|
continue
|
|
else:
|
|
raise ValueError(f"unsupported Intel HEX record type {kind}")
|
|
if not memory:
|
|
raise ValueError("bootloader HEX contains no data")
|
|
return memory, start_linear
|
|
|
|
|
|
def record(address: int, kind: int, payload: bytes) -> str:
|
|
body = bytes((len(payload), (address >> 8) & 0xFF, address & 0xFF, kind)) + payload
|
|
checksum = (-sum(body)) & 0xFF
|
|
return ":" + (body + bytes((checksum,))).hex().upper()
|
|
|
|
|
|
def write_ihex(path: Path, memory: dict[int, int], start_linear: int | None) -> None:
|
|
lines: list[str] = []
|
|
current_upper: int | None = None
|
|
addresses = sorted(memory)
|
|
index = 0
|
|
while index < len(addresses):
|
|
start = addresses[index]
|
|
upper = start >> 16
|
|
if upper != current_upper:
|
|
lines.append(record(0, 4, upper.to_bytes(2, "big")))
|
|
current_upper = upper
|
|
low = start & 0xFFFF
|
|
payload = bytearray((memory[start],))
|
|
index += 1
|
|
while index < len(addresses) and len(payload) < 16:
|
|
next_address = addresses[index]
|
|
if next_address != start + len(payload) or next_address >> 16 != upper:
|
|
break
|
|
payload.append(memory[next_address])
|
|
index += 1
|
|
lines.append(record(low, 0, bytes(payload)))
|
|
if start_linear is not None:
|
|
lines.append(record(0, 5, start_linear.to_bytes(4, "big")))
|
|
lines.append(record(0, 1, b""))
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("\n".join(lines) + "\n", encoding="ascii", newline="\n")
|
|
|
|
|
|
def add_region(memory: dict[int, int], address: int, data: bytes, label: str) -> None:
|
|
for index, value in enumerate(data):
|
|
absolute = address + index
|
|
if absolute in memory:
|
|
raise ValueError(f"{label} overlaps existing data at 0x{absolute:08X}")
|
|
memory[absolute] = value
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--bootloader", type=Path, required=True)
|
|
parser.add_argument("--calibration", type=Path, required=True)
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
|
|
memory, start_linear = parse_ihex(args.bootloader)
|
|
if min(memory) < BOOT_BASE or max(memory) >= APP_BASE:
|
|
raise ValueError("bootloader data is outside the immutable 64 KiB region")
|
|
|
|
calibration = args.calibration.read_bytes()
|
|
if len(calibration) < 8 or APP_BASE + len(calibration) > APP_END:
|
|
raise ValueError("calibration firmware size is outside the application region")
|
|
crc32 = zlib.crc32(calibration) & 0xFFFFFFFF
|
|
metadata = struct.pack(
|
|
"<8I",
|
|
METADATA_MAGIC,
|
|
METADATA_VERSION,
|
|
len(calibration),
|
|
crc32,
|
|
(~len(calibration)) & 0xFFFFFFFF,
|
|
(~crc32) & 0xFFFFFFFF,
|
|
APP_BASE,
|
|
(~APP_BASE) & 0xFFFFFFFF,
|
|
)
|
|
add_region(memory, APP_BASE, calibration, "calibration firmware")
|
|
add_region(memory, METADATA_ADDRESS, metadata, "boot metadata")
|
|
write_ihex(args.output, memory, start_linear)
|
|
print(
|
|
"FACTORY_IMAGE_OK "
|
|
f"boot_bytes={sum(BOOT_BASE <= value < APP_BASE for value in memory)} "
|
|
f"app_bytes={len(calibration)} crc32=0x{crc32:08X} output={args.output}"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|