Add MeshTower SD bootloader updates over LoRa

This commit is contained in:
mikecarper committed 2026-08-21 09:55:15 -07:00
1 parent 9e4d246ac6
commit 3d78d9c846
26 files changed
+763 -114

No files matched your search

+1 -1
View File
@@ -70,7 +70,7 @@ does not reflect the GoogleTest count -- run the built binary directly
| `test_user_gpio` | `src/helpers/UserGpio.cpp`, `UserGpioReplyTracker.h` | board-approved pins, get/set/reset, timed nonblocking transitions, duplicate suppression, rollover, and completion-reply routing |
| `test_utf8_helpers` | `src/helpers/UTF8Helpers.h` | byte-limit truncation at complete code-point boundaries and rejection of malformed or truncated UTF-8 |
| `test_wifi_ota_seeder_policy` | `src/helpers/WiFiOtaSeederPolicy.h`, `WiFiOtaSeederStatus.h` | listener state versus network availability, serial/TCP folder ownership, detach detection, and bounded status formatting |
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; legacy XIAO and generic embedded identity, vector, capability, explicit-confirmation, codec-isolation, shared-slot placement/no-EndF erase gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; legacy XIAO, generic internal, and exact MeshTower V2 SD embedded identity, vector, capability, explicit-confirmation, codec-isolation, scratch-headroom/shared-slot no-EndF gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
| `test_trace_retry` | `src/Mesh.cpp` retry and relay policy | app-v2 and boot-v3 traffic sharing `PAYLOAD_TYPE_OTA=0x0C` and the TempRadio suspend policy; opaque OTA relay behavior; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing |
| `test_utils` | `src/Utils.cpp` | `Utils::toHex` (upstream) |
+75 -3
View File
@@ -16,6 +16,8 @@
#include "helpers/ota/OtaManager.h"
#include "helpers/ota/OtaApply.h"
#include "helpers/ota/OtaBlInfo.h"
#include "helpers/ota/OtaFlashLayout_nrf52.h"
#include "helpers/ota/OtaSdBootToken.h"
#include "mota_vectors.h" // auto-generated by tools/mota/gen_vectors.py
extern "C" {
@@ -41,12 +43,74 @@ TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x00), 0x00);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x4C), 0x00); // shutdown reason
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x51), 0x00); // QSPI handoff
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x53), 0x00); // SD handoff
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xD4), 0x00); // legacy handoff
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xED), 0x00); // expanded handoff
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xE0), 0x00); // retired handoff remains non-result
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0x00);
}
TEST(OtaBootPackage, StorageProfilesAndSdGeometryAreExact) {
EXPECT_EQ(OTA_BL_PROFILE_SD_BOOT_UPDATE, 0x09u);
EXPECT_EQ(OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE, 0x0Au);
EXPECT_EQ(OTA_BL_PROFILE_QSPI_BOOT_UPDATE, 0x0Eu);
EXPECT_EQ(OTA_BL_REQUIRED_APP_CODEC_MASK, 0x0005u);
EXPECT_EQ(GPREGRET2_OTA_STAGE_SD, 0x53u);
EXPECT_TRUE(ota_bootloader_image_geometry_valid(
OTA_BOOT_IMAGE_SIZE, OTA_BOOT_IMAGE_SIZE));
EXPECT_FALSE(ota_bootloader_image_geometry_valid(
OTA_BOOT_IMAGE_SIZE - 1u, OTA_BOOT_IMAGE_SIZE));
EXPECT_FALSE(ota_bootloader_image_geometry_valid(
OTA_BOOT_IMAGE_SIZE, OTA_BOOT_IMAGE_SIZE - 1u));
EXPECT_TRUE(ota_bootloader_scratch_headroom_valid(
true, MOTA_NRF52_APP_BASE_S140_V6,
OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6,
OTA_BOOT_SCRATCH_START));
EXPECT_FALSE(ota_bootloader_scratch_headroom_valid(
false, MOTA_NRF52_APP_BASE_S140_V6, 1u, OTA_BOOT_SCRATCH_START));
EXPECT_FALSE(ota_bootloader_scratch_headroom_valid(
true, MOTA_NRF52_APP_BASE_S140_V6,
OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6 + 1u,
OTA_BOOT_SCRATCH_START));
const uint32_t safe_span = OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6;
const uint32_t live_size = safe_span - 0x1000u;
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
OTA_BOOT_BANK_ERASED_FALLBACK, 0xFFFFu, UINT32_MAX, live_size, safe_span));
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
OTA_BOOT_BANK_VALID_APP, 0u, UINT32_MAX, live_size, safe_span));
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
OTA_BOOT_BANK_VALID_APP, 1u, live_size, live_size, safe_span));
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
OTA_BOOT_BANK_VALID_APP, 1u, live_size - 1u, live_size, safe_span));
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
OTA_BOOT_BANK_VALID_APP, 1u, safe_span + 1u, live_size, safe_span));
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
0x00FFu, 0u, 0u, live_size, safe_span));
}
TEST(OtaBootPackage, SdApprovalTokenBindsTheAuthenticatedImageHash) {
uint8_t hash[32];
for (uint8_t i = 0; i < sizeof(hash); i++) hash[i] = i;
uint8_t token[MOTA_SD_BOOT_TOKEN_LEN];
mota_sd_boot_token_encode(token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash);
EXPECT_EQ(0, memcmp(token, "MOTASDBL", 8));
EXPECT_EQ(mota_sd_boot_token_rd32(token + 8), 1u);
EXPECT_EQ(mota_sd_boot_token_rd32(token + 12), 64u);
EXPECT_EQ(mota_sd_boot_token_rd32(token + 16), MOTA_NRF52_BOOT_CONTAINER_SIZE);
EXPECT_EQ(mota_sd_boot_token_rd32(token + 20), ~MOTA_NRF52_BOOT_CONTAINER_SIZE);
EXPECT_EQ(0, memcmp(token + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, hash, sizeof(hash)));
EXPECT_TRUE(mota_sd_boot_token_valid(
token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash));
token[MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET] ^= 1u;
EXPECT_FALSE(mota_sd_boot_token_valid(
token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash));
token[MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET] ^= 1u;
EXPECT_FALSE(mota_sd_boot_token_valid(
token, MOTA_NRF52_BOOT_CONTAINER_SIZE + 1u, hash));
}
TEST(OtaBootPackage, GenericIdentityUsesBoardAndNameForCollisionSafeTarget) {
const uint32_t shared_board_id = 0x239A0029u;
const uint8_t rak3401_name[16] = {'3','4','0','1','_','D','F','U',0,0,0,0,0,0,0,0};
@@ -209,7 +273,7 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
image[0x300] ^= 1;
EXPECT_FALSE(ota_bootloader_identity_from_image(image.data(), image.size(), identity));
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 1,0,
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
OtaBootloaderCapsMarker caps;
EXPECT_TRUE(ota_bootloader_caps_marker_parse(marker, caps));
@@ -218,7 +282,7 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
std::vector<uint8_t> internal_caps_image(64, 0xFF);
const uint8_t internal_marker[16] = {
'M','O','T','A','B','L','D','R', 3,0, 1,0,
'M','O','T','A','B','L','D','R', 3,0, 5,0,
(uint8_t)(OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
memcpy(internal_caps_image.data() + 4, internal_marker, sizeof(internal_marker));
EXPECT_TRUE(ota_bootloader_caps_from_image(
@@ -237,6 +301,14 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
caps));
std::vector<uint8_t> full_only_caps(32, 0xFF);
memcpy(full_only_caps.data() + 4, internal_marker, sizeof(internal_marker));
full_only_caps[4 + 10] = 1;
EXPECT_FALSE(ota_bootloader_caps_from_image(
full_only_caps.data(), full_only_caps.size(),
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
caps));
OtaBlCaps selected;
selected.present = true;
selected.apply_abi = 3;
@@ -270,7 +342,7 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
const uint8_t qspi_profile =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_BOOT_UPDATE;
const uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 1,0,
const uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
qspi_profile, 0,0,0};
uint8_t image[64];
memset(image, 0xFF, sizeof(image));