Verify Pi nRF52 board identity before flashing
This commit is contained in:
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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user