diff --git a/docs/ota_nrf52_bootloader_update.md b/docs/ota_nrf52_bootloader_update.md index 11d8b984..179f7423 100644 --- a/docs/ota_nrf52_bootloader_update.md +++ b/docs/ota_nrf52_bootloader_update.md @@ -103,7 +103,8 @@ manifest identity: | `ThinkNode_M3_repeater_lora_ota_no_external_sensors` | `239A00DA / TNM3_DFU` | `0CA41DB2` | | `RAK_3401_repeater_lora_ota_no_external_sensors` | `239A0029 / 3401_DFU` | `23818A80` | | RAK4631 lean repeater and both lean RS232 bridges | `239A0029 / 4631_DFU` | `2D0DF000` | -| GAT562 30S/Tracker Pro/EVB Pro and R1Neo lean carrier aliases | `239A0029 / 4631_DFU` | `2D0DF000` | +| GAT562 30S/Tracker Pro/EVB Pro lean targets | `239A0029 / GAT562_DFU` | `D50D2D44` | +| `R1Neo_repeater_lora_ota_no_external_sensors` | `239A0029 / 4631_DFU` | `2D0DF000` | | `RAK_WisMesh_Tag_repeater_lora_ota_no_external_sensors` | `239A0029 / RTAG_DFU` | `C72E9C9C` | Board IDs are not globally unique. For generic targets, the signed hardware ID @@ -118,6 +119,12 @@ duplicates and collisions with application target IDs. Generic image parsing can inspect a future canonical identity, but signing/building a package fails until that exact identity is in the qualified inventory. +Earlier GAT562 installations may report the RAK-compatible `4631_DFU` +identity. The installed and candidate identity pair must match exactly, so a +remote bootloader update cannot migrate one of those devices to `GAT562_DFU`. +Provision the GAT562-specific OTAFIX bootloader once through USB/BLE DFU or SWD; +subsequent signed bootloader mOTA updates use target `D50D2D44`. + ## Embedded continuity and version policy Every remotely supplied successor retains the legacy 44-byte CRC-valid `BLMF` diff --git a/test/test_ota/test_ota_core.cpp b/test/test_ota/test_ota_core.cpp index a43defbc..ed979ba1 100644 --- a/test/test_ota/test_ota_core.cpp +++ b/test/test_ota/test_ota_core.cpp @@ -378,14 +378,19 @@ TEST(OtaBootPackage, SdApprovalTokenBindsTheAuthenticatedImageHash) { TEST(OtaBootPackage, GenericIdentityUsesBoardAndNameForCollisionSafeTarget) { const uint32_t shared_board_id = 0x239A0029u; + const uint8_t gat562_name[16] = {'G','A','T','5','6','2','_','D','F','U',0,0,0,0,0,0}; const uint8_t rak3401_name[16] = {'3','4','0','1','_','D','F','U',0,0,0,0,0,0,0,0}; const uint8_t rak4631_name[16] = {'4','6','3','1','_','D','F','U',0,0,0,0,0,0,0,0}; - uint8_t hw3401[32], hw4631[32]; + uint8_t hw_gat562[32], hw3401[32], hw4631[32]; + ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, gat562_name, hw_gat562)); ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, rak3401_name, hw3401)); ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, rak4631_name, hw4631)); + EXPECT_EQ(0, memcmp(hw_gat562, "NRF_BL_239A0029_GAT562_DFU", 26)); EXPECT_EQ(0, memcmp(hw3401, "NRF_BL_239A0029_3401_DFU", 24)); EXPECT_EQ(0, memcmp(hw4631, "NRF_BL_239A0029_4631_DFU", 24)); + EXPECT_NE(0, memcmp(hw_gat562, hw4631, sizeof(hw_gat562))); EXPECT_NE(0, memcmp(hw3401, hw4631, sizeof(hw3401))); + EXPECT_EQ(ota_bootloader_target_id(shared_board_id, gat562_name), 0xD50D2D44u); EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak3401_name), 0x23818A80u); EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak4631_name), 0x2D0DF000u); @@ -418,6 +423,7 @@ TEST(OtaBootPackage, GenericIdentityUsesBoardAndNameForCollisionSafeTarget) { struct KnownIdentity { uint32_t board_id; const char* name; uint32_t target_id; }; const KnownIdentity known[] = { + {0x239A0029u, "GAT562_DFU", 0xD50D2D44u}, {0x239A0071u, "TOWER_V2_OTA", 0x1150F50Eu}, {0x239A0071u, "T096_DFU", 0x42354C85u}, {0x239A0071u, "T1_DFU", 0xFC556FFCu}, diff --git a/tools/mota/motalib.py b/tools/mota/motalib.py index e7a65673..506791b1 100644 --- a/tools/mota/motalib.py +++ b/tools/mota/motalib.py @@ -119,6 +119,7 @@ BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) | # aliases use the same installed bootloader identity and therefore do not add # rows here. INTERNAL_BOOTLOADER_IDENTITIES = ( + (0x239A0029, "GAT562_DFU"), (0x239A0071, "TOWER_V2_OTA"), (0x239A0071, "T096_DFU"), (0x239A0071, "T1_DFU"), diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py index 0ec64ed7..f378c16b 100644 --- a/tools/mota/test_mota.py +++ b/tools/mota/test_mota.py @@ -53,6 +53,13 @@ def test_internal_bootloader_target_wiring_is_central_and_not_duplicated(): ] assert len(inventory) == len(set(inventory)) assert len(inventory) == 21 + gat_targets = { + "GAT562_30S_Mesh_Kit_repeater_lora_ota_no_external_sensors", + "GAT562_Mesh_Tracker_Pro_repeater_lora_ota_no_external_sensors", + "GAT562_Mesh_EVB_Pro_repeater_lora_ota_no_external_sensors", + } + assert gat_targets <= set(inventory) + assert not any("GAT562_Mesh_Watch13" in target for target in inventory) target_header = (root / "src/helpers/ota/OtaTargets.h").read_text(encoding="utf-8") for target in inventory: @@ -1379,6 +1386,38 @@ def test_generic_internal_bootloader_build_parse_and_strict_contract(): pass +def test_gat562_internal_bootloader_profile_is_exact(): + priv = Ed25519PrivateKey.generate() + image = _generic_bootloader_image(device_name="GAT562_DFU") + identity = ml.validate_bootloader_image(image) + assert (identity.board_id, identity.device_name) == (0x239A0029, "GAT562_DFU") + profile = ml.bootloader_qualified_platform_profile( + identity.board_id, identity.device_name) + assert profile == ( + ml.BOOT_CONTINUITY_FAMILY_S140, + ml.BOOTLOADER_S140_V6_FWID, + ml.NRF52_APP_BASE_S140_V6, + ml.BOOT_CONTINUITY_LAYOUT_ABI, + ) + storages = ml.bootloader_qualified_storage_profiles( + identity.board_id, identity.device_name) + assert storages == (ml.BOOT_STORAGE_INTERNAL_UPDATE,) + + target = ml.bootloader_target_id(identity.board_id, identity.device_name) + expected_hw = b"NRF_BL_239A0029_GAT562_DFU".ljust(32, b"\0") + assert target == 0xD50D2D44 + assert ml.bootloader_hw_id(identity.board_id, identity.device_name) == expected_hw + manifest = ml.build_manifest( + target_id=target, fw_version=identity.boot_version, + image_size=len(image), payload=image, block_size=1024, + image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, is_full=True, + sign_priv=priv, bootloader=True) + parsed = ml.parse_container(ml.build_container(manifest, image)) + assert parsed.manifest.target_id == target + assert parsed.manifest.hw_id == expected_hw + assert ml.verify(parsed, expect_pub=ml.ed25519_public_bytes(priv)) == [] + + def test_meshtower_sd_bootloader_profile_builds_the_same_exact_identity(): priv = Ed25519PrivateKey.generate() image = _generic_bootloader_image( @@ -1418,6 +1457,7 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets(): import re expected = { + (0x239A0029, "GAT562_DFU"): 0xD50D2D44, (0x239A0071, "TOWER_V2_OTA"): 0x1150F50E, (0x239A0071, "T096_DFU"): 0x42354C85, (0x239A0071, "T1_DFU"): 0xFC556FFC,