Add shared-slot nRF52 bootloader updates

This commit is contained in:
mikecarper committed 2026-08-21 00:55:01 -07:00
1 parent b7c9e37d09
commit 3de5fddf47
43 files changed
+1647 -211

No files matched your search

+2 -2
View File
@@ -70,8 +70,8 @@ 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; XIAO identity, vector, capability, explicit-confirmation 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 | opaque OTA relay behavior during TempRadio; 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_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_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) |
## Conventions (and how to add a suite)
+124 -6
View File
@@ -24,6 +24,8 @@ extern "C" {
using namespace mesh::ota;
static std::vector<uint8_t> boot_manifest_bytes();
TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x90), 0x90);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x9F), 0x9F);
@@ -41,9 +43,77 @@ TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x51), 0x00); // QSPI 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, 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};
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];
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(hw3401, "NRF_BL_239A0029_3401_DFU", 24));
EXPECT_EQ(0, memcmp(hw4631, "NRF_BL_239A0029_4631_DFU", 24));
EXPECT_NE(0, memcmp(hw3401, hw4631, sizeof(hw3401)));
EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak3401_name), 0x23818A80u);
EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak4631_name), 0x2D0DF000u);
uint8_t invalid[16] = {'B','A','D',0,'T','A','I','L',0,0,0,0,0,0,0,0};
EXPECT_FALSE(ota_bootloader_device_name_valid(shared_board_id, invalid));
memset(invalid, 'A', sizeof(invalid));
EXPECT_FALSE(ota_bootloader_device_name_valid(shared_board_id, invalid));
EXPECT_FALSE(ota_bootloader_board_id_valid(0));
EXPECT_FALSE(ota_bootloader_board_id_valid(UINT32_MAX));
auto raw = boot_manifest_bytes();
wr_u32le(raw.data() + 3, ota_bootloader_target_id(shared_board_id, rak3401_name));
memcpy(raw.data() + 57, hw3401, sizeof(hw3401));
MotaManifest manifest;
ASSERT_TRUE(mota_parse_manifest(raw.data(), raw.size(), manifest));
OtaBootloaderIdentity installed;
installed.present = installed.crc_ok = true;
installed.image_start = OTA_BOOT_IMAGE_START;
installed.image_size = OTA_BOOT_IMAGE_SIZE;
installed.board_id = shared_board_id;
memcpy(installed.device_name, rak3401_name, sizeof(rak3401_name));
uint8_t mid[4] = {0x11,0x22,0x33,0x44};
uint8_t hash8[8]; memcpy(hash8, manifest.image_hash, sizeof(hash8));
EXPECT_EQ(ota_bootloader_confirmation_gate(manifest, installed, mid, mid, hash8),
OTA_BOOT_CONFIRM_OK);
wr_u32le(raw.data() + 3, shared_board_id); // raw USB ID is not the generic wire target
ASSERT_TRUE(mota_parse_manifest(raw.data(), raw.size(), manifest));
EXPECT_EQ(ota_bootloader_confirmation_gate(manifest, installed, mid, mid, hash8),
OTA_BOOT_CONFIRM_TARGET);
struct KnownIdentity { uint32_t board_id; const char* name; uint32_t target_id; };
const KnownIdentity known[] = {
{0x239A0071u, "TOWER_V2_OTA", 0x1150F50Eu},
{0x239A0071u, "T096_DFU", 0x42354C85u},
{0x239A0071u, "T1_DFU", 0xFC556FFCu},
{0x239A0071u, "T114_DFU", 0x0C3F2902u},
{0x239A0071u, "MESH_POCKET_OTA",0x059277F4u},
{0x239A00B3u, "KeepteenLT1_OTA",0xDB2E7B51u},
{0x239A0029u, "MX25_DFU", 0x026AA982u},
{0x239A00B3u, "PROM_DFU", 0xAF79E8CCu},
{0x28860057u, "T1KE_DFU", 0xE6F5F03Fu},
{0x239A00DAu, "TNM3_DFU", 0x0CA41DB2u},
{0x239A0029u, "3401_DFU", 0x23818A80u},
{0x239A0029u, "4631_DFU", 0x2D0DF000u},
{0x239A0029u, "RTAG_DFU", 0xC72E9C9Cu},
};
uint32_t targets[sizeof(known) / sizeof(known[0])] = {0};
for (size_t i = 0; i < sizeof(known) / sizeof(known[0]); i++) {
uint8_t name[16] = {0};
ASSERT_LT(strlen(known[i].name), sizeof(name));
memcpy(name, known[i].name, strlen(known[i].name));
targets[i] = ota_bootloader_target_id(known[i].board_id, name);
EXPECT_EQ(targets[i], known[i].target_id);
for (size_t j = 0; j < i; j++) EXPECT_NE(targets[i], targets[j]);
}
}
static std::vector<uint8_t> boot_manifest_bytes() {
std::vector<uint8_t> m(MOTA_MFL, 0);
m[0] = MOTA_BOOT_FORMAT_VER;
@@ -146,6 +216,27 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
marker[15] = 1;
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
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,
(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(
internal_caps_image.data(), internal_caps_image.size(),
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
caps));
EXPECT_FALSE(ota_bootloader_caps_from_image(
internal_caps_image.data(), internal_caps_image.size(),
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE,
caps));
memcpy(internal_caps_image.data() + 24, internal_marker, sizeof(internal_marker));
internal_caps_image[24 + 12] =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE;
EXPECT_FALSE(ota_bootloader_caps_from_image(
internal_caps_image.data(), internal_caps_image.size(),
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
caps));
OtaBlCaps selected;
selected.present = true;
selected.apply_abi = 3;
@@ -176,20 +267,38 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
}
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,
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
qspi_profile, 0,0,0};
uint8_t image[64];
memset(image, 0xFF, sizeof(image));
memcpy(image + 1, marker, sizeof(marker));
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true).present);
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
memset(image, 0xFF, sizeof(image));
memcpy(image + 4, marker, sizeof(marker));
const OtaBlCaps caps = ota_bl_caps_scan_aligned(image, sizeof(image), true);
const OtaBlCaps caps = ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile);
ASSERT_TRUE(caps.present);
EXPECT_EQ(caps.apply_abi, 3u);
EXPECT_EQ(caps.storage_flags,
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE));
EXPECT_EQ(caps.storage_flags, qspi_profile);
// A malformed aligned magic decoy is ignored, but a second fully valid
// privileged marker makes the installed capability identity ambiguous.
memcpy(image + 24, marker, sizeof(marker));
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
memset(image + 24, 0xFF, sizeof(marker));
memcpy(image + 24, marker, sizeof(marker));
image[24 + 13] = 1;
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
// A structurally valid marker for the other storage profile is not an
// installed-profile candidate and cannot shadow the exact marker.
memcpy(image + 24, marker, sizeof(marker));
image[24 + 12] = OTA_BL_STORAGE_STAGE_CEILING |
OTA_BL_STORAGE_BOOT_UPDATE;
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
}
class FakeMotaSeederStream : public Stream {
@@ -1979,7 +2088,7 @@ TEST(OtaTransfer, BootloaderPackageRequiresCapableExplicitMidPull) {
OtaStoreRam<45000> capable_store;
capable.begin(OTA_XIAO_BOARD_ID_BASE, nullptr, nullptr);
capable.set_fetch_store(&capable_store);
capable.set_accept_full(true);
capable.set_accept_full(false); // internal nRF52 app FULL remains forbidden
capable.set_accept_bootloader(true);
capable.set_autofetch(OtaManager::AUTOFETCH_ANY);
capable.on_message(
@@ -1988,6 +2097,15 @@ TEST(OtaTransfer, BootloaderPackageRequiresCapableExplicitMidPull) {
EXPECT_EQ(capable.fetchState(), OtaManager::IDLE);
EXPECT_EQ(capable_store.staged_size(), 0u);
// Enabling the privileged bootloader path must not broaden the ordinary
// single-slot application codec policy or turn autofetch on for FULL apps.
const uint8_t app_mid[4] = {0x21, 0x22, 0x23, 0x24};
capable.on_message(
wire, make_have1(wire, sizeof(wire), app_mid, OTA_XIAO_BOARD_ID_BASE, 2,
CODEC_FULL, MFLAG_FULL));
EXPECT_EQ(capable.fetchState(), OtaManager::IDLE);
EXPECT_EQ(capable_store.staged_size(), 0u);
// An exact explicit pull admits the strict v3 manifest and opens the store for the 40 KiB payload.
EXPECT_EQ(capable.pull(mid, OTA_XIAO_BOARD_ID_BASE), OtaManager::PULL_STARTED);
deliver_manifest_fragment(capable, mid, 0, manifest.data(), OTA_MF_FRAG);
+126 -1
View File
@@ -18,6 +18,7 @@ static constexpr uint32_t APP_V6 = MOTA_NRF52_APP_BASE_S140_V6;
static constexpr uint32_t APP_V7 = MOTA_NRF52_APP_BASE_S140_V7;
static constexpr uint32_t LEGACY = MOTA_NRF52_STAGE_CEILING_LEGACY;
static constexpr uint32_t EXPANDED = MOTA_NRF52_STAGE_CEILING_EXPANDED;
static constexpr uint32_t SHARED_BOOT_START = MOTA_NRF52_SHARED_BOOT_STAGE_START;
static constexpr uint32_t APP_IMAGE_SIZE = 520u * 1024u;
static constexpr uint32_t APP_END_V6 = APP_V6 + APP_IMAGE_SIZE;
static constexpr uint32_t APP_END_V7 = APP_V7 + APP_IMAGE_SIZE;
@@ -190,10 +191,134 @@ TEST(OtaFlashPlan, RejectsAppOutsideSelectedRegion) {
TEST(OtaFlashPlan, RejectsUnknownCeiling) {
uint32_t start = 0x1234ABCD;
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, APP_V6, 0xE0000u, start));
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, APP_V6, 0xE1000u, start));
EXPECT_EQ(start, 0x1234ABCDu);
}
TEST(OtaFlashPlan, BootPackageUsesOneDynamicSharedInternalSlot) {
// v3 = header + fixed manifest + forty leaves + 40 KiB image + trailer.
const uint32_t total = 8u + 197u + 40u * 4u + 40u * 1024u + 5u;
ASSERT_EQ(total, MOTA_NRF52_BOOT_CONTAINER_SIZE);
uint32_t start = 0;
ASSERT_TRUE(mota_nrf52_shared_boot_stage_plan(
total, APP_V6, true, SHARED_BOOT_START, start));
EXPECT_EQ(start, SHARED_BOOT_START);
EXPECT_EQ(start, (EXPANDED - total) & ~(MOTA_NRF52_FLASH_PAGE - 1u));
// OTAFIX compacts payload offset +365 forward inside these same eleven
// pages; no independent scratch bank participates in the internal path.
const uint32_t payload_offset = 8u + 197u + 40u * 4u;
EXPECT_EQ(payload_offset, 365u);
EXPECT_LE(start + payload_offset + 40u * 1024u, EXPANDED);
EXPECT_LE(start + 40u * 1024u, EXPANDED);
// Runtime headroom, not a special linker, is authoritative.
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
total, APP_V6, true, SHARED_BOOT_START + 1u, start));
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
total - 1u, APP_V6, true, APP_V6, start));
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
total + 1u, APP_V6, true, APP_V6, start));
}
TEST(OtaFlashPlan, SharedInternalStoreKeepsNormalApplicationCeilingAndHandoff) {
EXPECT_TRUE(mota_nrf52_target_image_fits(
APP_V6, EXPANDED - APP_V6, EXPANDED));
EXPECT_FALSE(mota_nrf52_target_image_fits(
APP_V6, EXPANDED - APP_V6 + 1u, EXPANDED));
EXPECT_EQ(mota_nrf52_flash_stage_handoff(EXPANDED),
GPREGRET2_OTA_STAGE_EXPANDED);
EXPECT_EQ(mota_nrf52_flash_stage_handoff(LEGACY),
GPREGRET2_OTA_STAGE_LEGACY);
}
TEST(OtaFlashPlan, InternalOrdinaryDeltaUsesTheSameEd000Store) {
const uint32_t running_end = APP_V6 + 500u * 1024u;
const uint32_t delta_container = 28u * 1024u;
uint32_t staged = 0;
ASSERT_TRUE(mota_nrf52_stage_plan(delta_container, APP_V6, running_end,
EXPANDED, staged));
EXPECT_GE(staged, running_end);
EXPECT_LE(staged + delta_container, EXPANDED);
EXPECT_EQ(mota_nrf52_flash_stage_handoff(EXPANDED), 0xEDu);
}
TEST(OtaFlashPlan, LargeInternalOrdinaryDeltaMayStageBelowSharedBootStart) {
// An ordinary delta container can exceed the boot package's eleven pages.
// It bottom-aligns below E2000 and remains valid when detools'
// encoded workspace ends before the actual container start.
const uint32_t running_end = APP_V6 + 600u * 1024u;
const uint32_t delta_container = 80u * 1024u;
uint32_t staged = 0;
ASSERT_TRUE(mota_nrf52_stage_plan(delta_container, APP_V6, running_end,
EXPANDED, staged));
EXPECT_LT(staged, SHARED_BOOT_START);
EXPECT_GE(staged, running_end);
const uint32_t workspace = staged - APP_V6;
const uint32_t target_size = workspace;
EXPECT_TRUE(mota_nrf52_internal_patch_workspace_valid(
workspace, APP_V6, staged, target_size, EXPANDED));
EXPECT_FALSE(mota_nrf52_internal_patch_workspace_valid(
workspace + 1u, APP_V6, staged, target_size, EXPANDED));
EXPECT_FALSE(mota_nrf52_internal_patch_workspace_valid(
workspace, APP_V6, staged, EXPANDED - APP_V6 + 1u, EXPANDED));
}
TEST(OtaFlashPlan, MissingEndfRejectsEverySharedInternalPackageBeforeErase) {
uint32_t protected_end = 0;
ASSERT_TRUE(mota_nrf52_protected_app_end(
APP_V6, EXPANDED, false, 0, false, protected_end));
EXPECT_EQ(protected_end, APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY);
// A shared-internal-update build is linked through ED000, so the old 608 KiB
// estimate is not a safe erase floor for either package kind. Missing EndF
// disables all internal staging before begin()/reopen can erase a page.
EXPECT_FALSE(mota_nrf52_protected_app_end(
APP_V6, EXPANDED, false, 0, true, protected_end));
// The old rescue estimate would also admit this ordinary 80 KiB delta at
// D9000 even though a legal ED000-linked image can have live bytes there.
// A privileged shared-internal build therefore never calls stage_plan with
// that estimate: protected_app_end() above returns false/capacity zero.
const uint32_t ordinary_container = 80u * 1024u;
uint32_t unsafe_start = 0;
const uint32_t legacy_estimate =
APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY;
ASSERT_TRUE(mota_nrf52_stage_plan(
ordinary_container, APP_V6, legacy_estimate, EXPANDED, unsafe_start));
const uint32_t legal_live_tail = unsafe_start + MOTA_NRF52_FLASH_PAGE;
ASSERT_GT(legal_live_tail, legacy_estimate);
ASSERT_LT(legal_live_tail, EXPANDED);
EXPECT_FALSE(mota_nrf52_stage_plan(
ordinary_container, APP_V6, legal_live_tail, EXPANDED, protected_end));
// Model a legal linked image whose tail extends beyond the old 608 KiB
// estimate. The privileged planner rejects missing EndF even if the generic
// rescue fallback would appear to leave enough room.
const uint32_t real_tail = APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY + 0x8000u;
ASSERT_LT(real_tail, SHARED_BOOT_START);
uint32_t staged = 0;
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
MOTA_NRF52_BOOT_CONTAINER_SIZE, APP_V6, false, protected_end, staged));
ASSERT_TRUE(mota_nrf52_shared_boot_stage_plan(
MOTA_NRF52_BOOT_CONTAINER_SIZE, APP_V6, true, real_tail, staged));
EXPECT_EQ(staged, SHARED_BOOT_START);
}
TEST(OtaFlashPlan, ReopenBoundsUntrustedTotalBeforeManifestRead) {
const uint32_t start = SHARED_BOOT_START;
EXPECT_TRUE(mota_nrf52_container_span_valid(
start, EXPANDED, MOTA_NRF52_BOOT_CONTAINER_SIZE, 8u + 197u + 5u));
EXPECT_FALSE(mota_nrf52_container_span_valid(
start, EXPANDED, UINT32_MAX, 8u + 197u + 5u));
EXPECT_FALSE(mota_nrf52_container_span_valid(
start, EXPANDED, 8u + 197u + 4u, 8u + 197u + 5u));
EXPECT_FALSE(mota_nrf52_container_span_valid(
EXPANDED + 1u, EXPANDED, MOTA_NRF52_BOOT_CONTAINER_SIZE,
8u + 197u + 5u));
}
// out_start is only written on success - a rejected plan must not clobber the caller's variable.
TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
uint32_t start = 0x1234ABCD;
@@ -164,6 +164,21 @@ static mesh::Packet makeFloodPacket(uint8_t payload_type) {
return packet;
}
static mesh::Packet makeOtaManifestFragment(uint8_t format_version) {
mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA);
packet.payload_len = 12;
packet.payload[0] = mesh::ota::OTA_MANIFEST;
packet.payload[1] = 0x11;
packet.payload[2] = 0x22;
packet.payload[3] = 0x33;
packet.payload[4] = 0x44;
packet.payload[5] = 0; // fragment index
packet.payload[6] = 2; // fragment count
memcpy(packet.payload + 7, mesh::ota::MOTA_MAGIC, sizeof(mesh::ota::MOTA_MAGIC));
packet.payload[11] = format_version;
return packet;
}
TEST(RepeaterTransport, UnknownFloodPayloadIsRelayedWhenForwardingAllowsIt) {
TraceTestClock clock;
TraceTestRTC rtc;
@@ -233,6 +248,41 @@ TEST(RepeaterTransport, OtaDiscoveryRelaysInBackgroundOnlyDuringTempRadio) {
EXPECT_FALSE(node.canTransmit(&packet)); // queued-near-expiry packets cannot leak onto the normal channel
}
TEST(RepeaterTransport, AppV2AndBootV3ShareOta0cTempRadioRelayPolicy) {
// The package format is metadata inside an OTA_MANIFEST fragment. It must
// never select a second mesh payload type or bypass the repeater default
// `flood.filter.1 0x0C all suspend=tempradio` policy.
ASSERT_EQ(0x0C, PAYLOAD_TYPE_OTA);
const uint8_t formats[] = {
mesh::ota::MOTA_APP_FORMAT_VER,
mesh::ota::MOTA_BOOT_FORMAT_VER,
};
for (uint8_t format : formats) {
TraceTestClock clock;
TraceTestRTC rtc;
TraceTestRNG rng;
TraceTestRadio radio;
ForwardingTestTables tables;
StaticPoolPacketManager manager(12);
TraceTestMesh node(radio, clock, rng, rtc, manager, tables);
node.forwardFloods = true;
mesh::Packet packet = makeOtaManifestFragment(format);
ASSERT_EQ(PAYLOAD_TYPE_OTA, packet.getPayloadType());
ASSERT_EQ(format, packet.payload[11]);
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet));
EXPECT_EQ(0, tables.mark_seen_calls);
node.tempRadioActive = true;
mesh::DispatcherAction action = node.receivePacket(&packet);
EXPECT_NE(ACTION_RELEASE, action);
EXPECT_EQ(OTA_TRANSFER_TX_PRIORITY, (action >> 24) - 1);
EXPECT_EQ(1, tables.mark_seen_calls);
EXPECT_EQ(1, packet.getPathHashCount());
}
}
TEST(RepeaterTransport, OtaTransferRelaysAsPrimaryTrafficWithoutOtaManager) {
TraceTestClock clock;
TraceTestRTC rtc;