Verify Pi nRF52 board identity before flashing

This commit is contained in:
mikecarper committed 2026-09-25 13:04:36 -07:00
1 parent efc85b1afe
commit 9638223138
3 files changed
+151

No files matched your search

@@ -0,0 +1,34 @@
{
"devices": {
"9352162A72082314": {
"board": "Heltec MeshTower V2 SD",
"bootloader_prefixes": ["heltec_mesh_tower_v2_sdcard"],
"firmware_prefixes": ["heltec_tower_v2"]
},
"651F8E496197F882": {
"board": "Heltec T096",
"bootloader_prefixes": ["heltec_t096"],
"firmware_prefixes": ["heltec_t096"]
},
"34A9141999729D5D": {
"board": "Seeed T1000-E",
"bootloader_prefixes": ["t1000_e"],
"firmware_prefixes": ["t1000e", "seeed_t1000_e"]
},
"0B81C9C68D8D01B4": {
"board": "RAK3401",
"bootloader_prefixes": ["wiscore_rak3401_auto"],
"firmware_prefixes": ["rak_3401"]
},
"9AB3B64C641BA927": {
"board": "RAK4631",
"bootloader_prefixes": ["wiscore_rak4631_auto"],
"firmware_prefixes": ["rak_4631"]
},
"93DA83ADF7B2092B": {
"board": "Heltec MeshPocket",
"bootloader_prefixes": ["heltec_mesh_pocket"],
"firmware_prefixes": ["mesh_pocket"]
}
}
}
+45
View File
@@ -0,0 +1,45 @@
"""The shared Heltec USB product string must never select a flash image."""
import json
from pathlib import Path
import unittest
from tools.hil.verified_nrf_flash import verify
INVENTORY = json.loads((Path(__file__).parent / "mercerwood_pi_nrf_inventory.json").read_text())
class VerifiedNrfFlashTest(unittest.TestCase):
def test_tower_accepts_tower_artifacts(self):
for artifact in (
"heltec_mesh_tower_v2_sdcard_bootloader-OTAFIX2.4.8_s140_6.1.1.zip",
"Heltec_tower_v2_companion_radio_full-v1.17.1.7.zip",
):
self.assertEqual(verify(INVENTORY, "9352162A72082314",
"Heltec MeshTower V2 SD", artifact),
"Heltec MeshTower V2 SD")
def test_t114_image_refused_for_tower_serial(self):
with self.assertRaisesRegex(ValueError, "not a Heltec MeshTower V2 SD firmware artifact"):
verify(INVENTORY, "9352162A72082314", "Heltec MeshTower V2 SD",
"Heltec_t114_companion_radio_full-v1.17.1.7.uf2")
def test_non_sd_bootloader_refused_for_sd_tower(self):
with self.assertRaisesRegex(ValueError, "not a Heltec MeshTower V2 SD bootloader artifact"):
verify(INVENTORY, "9352162A72082314", "Heltec MeshTower V2 SD",
"heltec_mesh_tower_v2_bootloader-OTAFIX2.4.8.zip")
def test_t114_board_name_refused_for_tower_serial(self):
with self.assertRaisesRegex(ValueError, "is Heltec MeshTower V2 SD, not Heltec T114"):
verify(INVENTORY, "9352162A72082314", "Heltec T114",
"Heltec_t114_companion_radio_full-v1.17.1.7.uf2")
def test_unknown_serial_refused(self):
with self.assertRaisesRegex(ValueError, "unrecorded USB serial"):
verify(INVENTORY, "UNKNOWN", "Heltec MeshTower V2 SD",
"Heltec_tower_v2_companion_radio_full.uf2")
if __name__ == "__main__":
unittest.main()
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Check a Pi-connected nRF52's *physical* serial before selecting a flash image.
USB product names are not board identities: several Heltec nRF52 boards expose
the same ``HT-n5262`` product string. This gate requires a reviewed inventory
entry for the stable chip serial and checks the selected artifact against it.
Run it before either serial DFU or a UF2 copy.
"""
import argparse
import json
import os
from pathlib import Path
def verify(inventory, serial_number, board, artifact_name):
devices = inventory.get("devices", {})
serial_key = serial_number.upper()
device = next((entry for key, entry in devices.items()
if key.upper() == serial_key), None)
if device is None:
raise ValueError(f"unrecorded USB serial {serial_number}; identify the physical board first")
actual_board = device["board"]
if board != actual_board:
raise ValueError(f"serial {serial_number} is {actual_board}, not {board}")
artifact = Path(artifact_name).name.lower()
if artifact.startswith("update-"):
artifact = artifact[len("update-"):]
kind = "bootloader" if "_bootloader" in artifact else "firmware"
allowed = device.get(f"{kind}_prefixes", [])
if not any(artifact.startswith(prefix.lower() + "_") or
artifact.startswith(prefix.lower() + "-") for prefix in allowed):
raise ValueError(f"{artifact_name} is not a {actual_board} {kind} artifact; expected one of {allowed}")
return actual_board
def verify_port(port_path, serial_number):
try:
from serial.tools import list_ports
except ImportError as exc:
raise ValueError("pyserial is required to verify the connected USB port") from exc
expected = os.path.realpath(port_path)
matches = [port for port in list_ports.comports()
if os.path.realpath(port.device) == expected]
if len(matches) != 1:
raise ValueError(f"USB port {port_path} is absent or ambiguous")
observed = (matches[0].serial_number or "").upper()
if observed != serial_number.upper():
raise ValueError(f"USB port {port_path} has serial {observed!r}, expected {serial_number}")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--inventory", type=Path, required=True)
parser.add_argument("--serial", required=True, help="Stable USB chip serial")
parser.add_argument("--board", required=True, help="Reviewed physical board name")
parser.add_argument("--artifact", type=Path, required=True)
parser.add_argument("--port", required=True, help="Current application or DFU serial port")
args = parser.parse_args(argv)
try:
inventory = json.loads(args.inventory.read_text())
verify(inventory, args.serial, args.board, args.artifact.name)
verify_port(args.port, args.serial)
if not args.artifact.is_file():
raise ValueError(f"artifact does not exist: {args.artifact}")
except (ValueError, OSError, KeyError, json.JSONDecodeError) as exc:
parser.exit(1, f"REFUSED: {exc}\n")
print(f"VERIFIED: {args.serial} = {args.board}; {args.artifact.name}; {args.port}")
if __name__ == "__main__":
main()