mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 17:41:19 +00:00
Support compatible RAK bootloader updates with adaptive app storage
This commit is contained in:
1 parent
793531e16b
commit
346cf2fd1f
12 files changed
+430
-22
No files matched your search
@@ -1257,9 +1257,7 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
}
|
||||
|
||||
const OtaBlCaps current_caps = ota_bootloader_update_caps();
|
||||
if (!ota_bootloader_self_update_caps_valid(current_caps) ||
|
||||
current_caps.storage_flags != (OTA_BL_STORAGE_STAGE_CEILING |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE)) {
|
||||
if (!ota_bootloader_self_update_caps_valid(current_caps)) {
|
||||
strcpy(msg, "installed bootloader cannot safely self-update from internal flash"); return false;
|
||||
}
|
||||
|
||||
@@ -1303,10 +1301,23 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
!ota_bootloader_identity_matches(installed, candidate) ||
|
||||
!ota_bootloader_caps_from_image(
|
||||
image, OTA_BOOT_IMAGE_SIZE,
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
||||
current_caps.storage_flags,
|
||||
candidate_caps)) {
|
||||
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
|
||||
}
|
||||
if (current_caps.storage_flags == OTA_BL_PROFILE_RAK_AUTO_BOOT_UPDATE &&
|
||||
(!ota_bootloader_supports_hybrid(ota_ram_caps_scan_aligned(image, OTA_BOOT_IMAGE_SIZE)) ||
|
||||
ota_boot_rd32(image) > MOTA_NRF52_HYBRID_RAM_START)) {
|
||||
strcpy(msg, "candidate adaptive bootloader must retain the hybrid RAM arena"); return false;
|
||||
}
|
||||
#if defined(OTA_RAK_AUTO_STORE)
|
||||
const uint8_t optional_storage = ota_bl_optional_app_storage(
|
||||
(const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START, OTA_BOOT_IMAGE_SIZE);
|
||||
if (optional_storage != 0 &&
|
||||
ota_bl_optional_app_storage(image, OTA_BOOT_IMAGE_SIZE) != optional_storage) {
|
||||
strcpy(msg, "candidate must retain optional RAK application storage"); return false;
|
||||
}
|
||||
#endif
|
||||
const OtaBootloaderContinuityGate continuity = ota_bootloader_continuity_gate(
|
||||
installed, candidate, m.fw_version, OTA_BOOT_CONTINUITY_FAMILY_S140,
|
||||
ota_runtime_softdevice_fwid(), mota_nrf52_app_base(),
|
||||
|
||||
@@ -28,6 +28,7 @@ struct OtaBlCaps {
|
||||
uint16_t apply_abi = 0; // max .mota format_ver the bootloader can apply
|
||||
uint16_t codec_mask = 0; // bit i set => can apply codec_id i (in-place delta = bit 2)
|
||||
uint8_t storage_flags = 0; // OTA_BL_STORAGE_* capability bits
|
||||
uint8_t optional_app_storage = 0; // separate MOTASTOR record, never a boot-update backend
|
||||
};
|
||||
|
||||
// Independent marker for the reset-retained hybrid source. Keep this separate
|
||||
@@ -93,6 +94,8 @@ static const uint8_t OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE =
|
||||
static const uint8_t OTA_BL_PROFILE_QSPI_BOOT_UPDATE =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
static const uint8_t OTA_BL_PROFILE_RAK_AUTO_BOOT_UPDATE =
|
||||
OTA_BL_PROFILE_QSPI_BOOT_UPDATE | OTA_BL_STORAGE_HEADER_W25;
|
||||
// A successor must retain both application update paths used by qualified
|
||||
// external stores: bit 0 CODEC_FULL and bit 2 CODEC_DETOOLS_INPLACE.
|
||||
static const uint16_t OTA_BL_REQUIRED_APP_CODEC_MASK = 0x0005u;
|
||||
@@ -116,10 +119,15 @@ inline uint8_t ota_bootloader_update_storage_flags() {
|
||||
|
||||
inline bool ota_bootloader_self_update_caps_valid(const OtaBlCaps& c) {
|
||||
const uint8_t required = ota_bootloader_update_storage_flags();
|
||||
const bool profile_ok = c.storage_flags == required
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_RAK_AUTO_STORE)
|
||||
|| c.storage_flags == OTA_BL_PROFILE_RAK_AUTO_BOOT_UPDATE
|
||||
#endif
|
||||
;
|
||||
return required != 0 && c.present && c.apply_abi >= 3u &&
|
||||
(c.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
|
||||
OTA_BL_REQUIRED_APP_CODEC_MASK &&
|
||||
c.storage_flags == required;
|
||||
profile_ok;
|
||||
}
|
||||
|
||||
// Prefer a continuity-capable marker over a numerically newer legacy-looking candidate. Bootloader
|
||||
@@ -209,8 +217,30 @@ inline OtaBlCaps ota_bl_legacy_app_caps_scan_halfword(const uint8_t* bytes,
|
||||
return c;
|
||||
}
|
||||
|
||||
// Unified RAK loaders preserve the deployed board's privileged MOTABLDR=0x0A
|
||||
// contract. A separate record advertises optional APPLICATION storage. It is
|
||||
// adjacent to the unchanged, unique 64 KiB RAM marker and grants no new
|
||||
// bootloader staging backend.
|
||||
inline uint8_t ota_bl_optional_app_storage(const uint8_t* bytes, size_t len) {
|
||||
static const uint8_t record[32] = {
|
||||
'M', 'O', 'T', 'A', 'R', 'A', 'M', 'A', 1, 0, 72, 0, 0, 0, 1, 0,
|
||||
'M', 'O', 'T', 'A', 'S', 'T', 'O', 'R', 1, 0, 16, 0, 0x14, 0, 0, 0,
|
||||
};
|
||||
if (!bytes || !ota_bootloader_supports_hybrid(ota_ram_caps_scan_aligned(bytes, len))) return 0;
|
||||
unsigned matches = 0;
|
||||
for (size_t off = 0; off + sizeof(record) <= len; off += 4) {
|
||||
if (memcmp(bytes + off, record, sizeof(record)) == 0 && ++matches > 1) return 0;
|
||||
}
|
||||
return matches == 1 ? OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25 : 0;
|
||||
}
|
||||
|
||||
inline OtaBlCaps ota_bl_app_caps_scan(const uint8_t* bytes, size_t len) {
|
||||
const OtaBlCaps aligned = ota_bl_caps_scan_aligned(bytes, len, false);
|
||||
OtaBlCaps aligned = ota_bl_caps_scan_aligned(bytes, len, false);
|
||||
if (aligned.present && aligned.apply_abi >= 3 &&
|
||||
aligned.storage_flags == OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE) {
|
||||
aligned.optional_app_storage = ota_bl_optional_app_storage(bytes, len);
|
||||
aligned.storage_flags |= aligned.optional_app_storage;
|
||||
}
|
||||
if (aligned.present) return aligned;
|
||||
return ota_bl_legacy_app_caps_scan_halfword(bytes, len);
|
||||
}
|
||||
@@ -255,8 +285,15 @@ inline OtaBlCaps ota_bootloader_update_caps() {
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START;
|
||||
const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_RAK_AUTO_STORE)
|
||||
// Unified RAK applications support the existing internal board identity as
|
||||
// well as the adaptive successor. Both require one privileged marker.
|
||||
const OtaBlCaps caps = ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo), true);
|
||||
return ota_bootloader_self_update_caps_valid(caps) ? caps : OtaBlCaps();
|
||||
#else
|
||||
return ota_bl_update_caps_scan_aligned(
|
||||
lo, (size_t)(hi - lo), ota_bootloader_update_storage_flags());
|
||||
#endif
|
||||
#else
|
||||
return OtaBlCaps();
|
||||
#endif
|
||||
|
||||
@@ -360,7 +360,7 @@ struct OtaContext {
|
||||
const OtaBootloaderIdentity& installed = bootloaderIdentity();
|
||||
#if defined(OTA_RAK_AUTO_STORE)
|
||||
if (!fetch_store.usesInternal()) {
|
||||
strncpy(msg, "bootloader LoRa update needs the internal OTAFIX profile", 96);
|
||||
strncpy(msg, "bootloader package must be staged in internal flash", 96);
|
||||
msg[95] = 0; return false;
|
||||
}
|
||||
bool ok = ota_prepare_bootloader_update_nrf52(
|
||||
@@ -704,7 +704,7 @@ struct OtaContext {
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
#if defined(OTA_RAK_AUTO_STORE)
|
||||
manager.set_accept_bootloader(fetch_store.usesInternal() &&
|
||||
manager.set_accept_bootloader(
|
||||
ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()));
|
||||
#else
|
||||
manager.set_accept_bootloader(true);
|
||||
|
||||
@@ -15,11 +15,14 @@ inline RakStorageChoice rak_storage_choice(uint8_t detected,
|
||||
bool qspi_bootloader,
|
||||
bool identity_valid,
|
||||
const char* device_name,
|
||||
bool rak3401) {
|
||||
bool rak3401,
|
||||
bool optional_app_storage = false) {
|
||||
if (detected > 3u || detected == 3u) return RakStorageChoice::Unsafe;
|
||||
const char* auto_name = rak3401 ? "3401_AUTO_DFU" : "4631_AUTO_DFU";
|
||||
const char* board_name = rak3401 ? "3401_DFU" : "4631_DFU";
|
||||
const bool merged_bootloader = identity_valid && device_name &&
|
||||
strcmp(device_name, auto_name) == 0;
|
||||
(strcmp(device_name, auto_name) == 0 ||
|
||||
(optional_app_storage && strcmp(device_name, board_name) == 0));
|
||||
if (detected == 0u) return !qspi_bootloader || merged_bootloader
|
||||
? RakStorageChoice::Internal : RakStorageChoice::Unsafe;
|
||||
if (!qspi_bootloader) return RakStorageChoice::Internal;
|
||||
|
||||
@@ -14,12 +14,14 @@ namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// One RAK application image supports the board's internal OTA store and its
|
||||
// explicitly matched external NOR. Selection is fixed for the lifetime of an
|
||||
// app boot; no staged container is ever moved between backends.
|
||||
// explicitly matched external NOR. Application placement follows the detected
|
||||
// hardware. Bootloader packages always use internal flash; select the backend
|
||||
// before begin(), never move a partially fetched container between stores.
|
||||
class OtaStoreAdaptiveNrf52 : public OtaStore {
|
||||
enum Mode : uint8_t { UNSELECTED, INTERNAL_MODE, QSPI_MODE, UNSAFE };
|
||||
mutable Mode _mode = UNSELECTED;
|
||||
mutable const char* _reason = "not probed";
|
||||
bool _bootloader_store = false;
|
||||
// Only one backend is usable for a given board/bootloader pairing. Sharing
|
||||
// their two flash-page buffers saves 8 KiB of scarce nRF52840 runtime RAM.
|
||||
union StoreStorage {
|
||||
@@ -39,6 +41,26 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
|
||||
_mode = QSPI_MODE;
|
||||
}
|
||||
|
||||
bool selectBootloader() {
|
||||
if (!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())) return false;
|
||||
if (_mode != INTERNAL_MODE) {
|
||||
if (_mode == QSPI_MODE) _storage.external.~OtaStoreQspiNrf52();
|
||||
activateInternal();
|
||||
}
|
||||
_bootloader_store = true;
|
||||
_reason = "bootloader package in internal flash";
|
||||
return true;
|
||||
}
|
||||
|
||||
void selectApplication() {
|
||||
if (_bootloader_store) {
|
||||
_storage.internal.~OtaStoreFlashNrf52();
|
||||
_mode = UNSELECTED;
|
||||
_bootloader_store = false;
|
||||
}
|
||||
select();
|
||||
}
|
||||
|
||||
void select() const {
|
||||
if (_mode != UNSELECTED) return;
|
||||
const uint8_t detected = OtaStoreQspiNrf52::autoDetect();
|
||||
@@ -66,7 +88,8 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
|
||||
#endif
|
||||
const RakStorageChoice choice = rak_storage_choice(
|
||||
detected, qspi_bootloader, identity_valid,
|
||||
identity_valid ? identity.device_name : nullptr, rak3401);
|
||||
identity_valid ? identity.device_name : nullptr, rak3401,
|
||||
caps.optional_app_storage == (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25));
|
||||
if (choice == RakStorageChoice::Internal) {
|
||||
activateInternal();
|
||||
_reason = detected == 0u ? "no external NOR" :
|
||||
@@ -138,12 +161,31 @@ public:
|
||||
}
|
||||
bool finalize() override { OtaStore* s = active(); return s && s->finalize(); }
|
||||
void checkpoint() override { OtaStore* s = active(); if (s) s->checkpoint(); }
|
||||
bool reopen() override { OtaStore* s = active(); return s && s->reopen(); }
|
||||
bool reopen() override { return reopenFor(nullptr, 0u); }
|
||||
bool reopenFor(const uint8_t* mid, uint32_t target) override {
|
||||
OtaStore* s = active(); return s && s->reopenFor(mid, target);
|
||||
selectApplication();
|
||||
OtaStore* s = active();
|
||||
if (s && s->reopenFor(mid, target)) return true;
|
||||
// Only an explicit MID pull may resume privileged data. Automatic resume
|
||||
// stays on the application backend. Validate kind before adopting this
|
||||
// alternative store; the manager repeats all manifest and block checks.
|
||||
if (!mid || _mode == INTERNAL_MODE || !selectBootloader()) return false;
|
||||
uint8_t raw[MOTA_MFL];
|
||||
MotaManifest manifest;
|
||||
if (_storage.internal.reopenFor(mid, target) &&
|
||||
_storage.internal.read(8u, raw, sizeof(raw)) &&
|
||||
mota_parse_manifest(raw, sizeof(raw), manifest) && manifest.is_bootloader())
|
||||
return true;
|
||||
selectApplication();
|
||||
return false;
|
||||
}
|
||||
bool plan_layout(bool full, uint32_t image, uint32_t payload_off,
|
||||
uint32_t payload_size, bool bootloader) override {
|
||||
if (bootloader) {
|
||||
if (!selectBootloader()) return false;
|
||||
} else {
|
||||
selectApplication();
|
||||
}
|
||||
OtaStore* s = active();
|
||||
return s && s->plan_layout(full, image, payload_off, payload_size, bootloader);
|
||||
}
|
||||
|
||||
@@ -763,6 +763,31 @@ TEST(OtaHybridHandoff, EncodesExactGeometryAndRejectsCorruption) {
|
||||
MOTA_NRF52_FLASH_PAGE, ram, hash));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, OptionalRakApplicationStoragePreservesLegacyBootUpdateContract) {
|
||||
uint8_t image[128] = {};
|
||||
const uint8_t boot[16] = {'M','O','T','A','B','L','D','R', 3,0,5,0,0x0A,0,0,0};
|
||||
const uint8_t bundle[32] = {
|
||||
'M','O','T','A','R','A','M','A',1,0,72,0,0,0,1,0,
|
||||
'M','O','T','A','S','T','O','R',1,0,16,0,0x14,0,0,0,
|
||||
};
|
||||
memcpy(image, boot, sizeof(boot));
|
||||
memcpy(image + 32, bundle, sizeof(bundle));
|
||||
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).storage_flags, 0x1Eu);
|
||||
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).optional_app_storage, 0x14u);
|
||||
EXPECT_EQ(ota_bl_update_caps_scan_aligned(image, sizeof(image), 0x0A).storage_flags, 0x0Au);
|
||||
for (unsigned byte : {32u, 40u, 42u, 46u, 48u, 56u, 58u, 60u, 63u}) {
|
||||
image[byte] ^= 1;
|
||||
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).storage_flags, 0x0Au);
|
||||
image[byte] ^= 1;
|
||||
}
|
||||
memcpy(image + 80, bundle, sizeof(bundle));
|
||||
EXPECT_EQ(ota_bl_optional_app_storage(image, sizeof(image)), 0u);
|
||||
memset(image + 80, 0, sizeof(bundle));
|
||||
memset(image + 48, 0, 16); // genuine old board has no optional record
|
||||
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).storage_flags, 0x0Au);
|
||||
EXPECT_TRUE(ota_bl_update_caps_scan_aligned(image, sizeof(image), 0x0A).present);
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, LegacyAndCurrentBootloadersHaveSeparateCapabilityViews) {
|
||||
const uint8_t internal_profile = OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE;
|
||||
const uint8_t legacy_marker[16] = {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Exercise the actual adaptive store with simulated internal and QSPI media."""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class AdaptiveBootloaderTest(unittest.TestCase):
|
||||
def test_storage_switch_and_manual_resume(self):
|
||||
header = (ROOT / "src/helpers/ota/OtaStoreAdaptiveNrf52.h").read_text()
|
||||
body = header[header.index("class OtaStoreAdaptiveNrf52"):
|
||||
header.index("} // namespace ota")]
|
||||
source = r'''
|
||||
#include <cassert>
|
||||
#include <new>
|
||||
#include "src/helpers/ota/OtaStore.h"
|
||||
#include "src/helpers/ota/OtaBlInfo.h"
|
||||
#include "src/helpers/ota/OtaRakStoragePolicy.h"
|
||||
using namespace mesh::ota;
|
||||
namespace mesh { namespace ota {
|
||||
static bool capable = true, boot_resume = false, app_resume = false;
|
||||
static unsigned detected = 2, internal_begins = 0, external_begins = 0;
|
||||
static unsigned internal_live = 0, external_live = 0;
|
||||
struct OtaBootloaderIdentity { bool crc_ok = true; const char* device_name = "4631_DFU"; };
|
||||
bool ota_installed_bootloader_identity(OtaBootloaderIdentity&) { return true; }
|
||||
struct MotaManifest { bool is_bootloader() const { return boot_resume; } };
|
||||
bool mota_parse_manifest(const uint8_t*, size_t, MotaManifest&) { return true; }
|
||||
OtaBlCaps simulatedCaps() {
|
||||
OtaBlCaps c; c.present = true; c.apply_abi = capable ? 3 : 2;
|
||||
c.codec_mask = 5; c.storage_flags = capable ? 0x1e : 0x16;
|
||||
c.optional_app_storage = capable ? 0x14 : 0; return c;
|
||||
}
|
||||
OtaBlCaps simulatedBootCaps() {
|
||||
auto c = simulatedCaps(); c.storage_flags = capable ? 0x0a : 0x02;
|
||||
c.optional_app_storage = 0; return c;
|
||||
}
|
||||
class Store : public OtaStore {
|
||||
public:
|
||||
uint32_t size = 0;
|
||||
bool begin(uint32_t n) override { size = n; return true; }
|
||||
bool write(uint32_t, const uint8_t*, uint32_t) override { return true; }
|
||||
bool read(uint32_t, uint8_t*, uint32_t) const override { return true; }
|
||||
uint32_t capacity() const override { return 65536; }
|
||||
uint32_t staged_size() const override { return size; }
|
||||
void clear() override { size = 0; }
|
||||
};
|
||||
class OtaStoreFlashNrf52 : public Store {
|
||||
public:
|
||||
OtaStoreFlashNrf52() { ++internal_live; assert(external_live == 0); }
|
||||
~OtaStoreFlashNrf52() { --internal_live; }
|
||||
bool begin(uint32_t n) override { ++internal_begins; return Store::begin(n); }
|
||||
bool reopenFor(const uint8_t*, uint32_t) override { return boot_resume; }
|
||||
};
|
||||
class OtaStoreQspiNrf52 : public Store {
|
||||
public:
|
||||
OtaStoreQspiNrf52() { ++external_live; assert(internal_live == 0); }
|
||||
~OtaStoreQspiNrf52() { --external_live; }
|
||||
static uint8_t autoDetect() { return detected; }
|
||||
static bool headerW25Detected() { return true; }
|
||||
uint32_t jedec_id() const { return 0xef4015; }
|
||||
uint8_t status1() const { return 0; }
|
||||
const char* last_stage() const { return "ok"; }
|
||||
const char* last_error() const { return "ok"; }
|
||||
bool begin(uint32_t n) override { ++external_begins; return Store::begin(n); }
|
||||
bool reopenFor(const uint8_t*, uint32_t) override { return app_resume; }
|
||||
};
|
||||
#define ota_bootloader_app_caps simulatedCaps
|
||||
#define ota_bootloader_update_caps simulatedBootCaps
|
||||
'''
|
||||
source += body
|
||||
source += r'''
|
||||
} }
|
||||
int main() {
|
||||
uint8_t mid[4] = {1, 2, 3, 4};
|
||||
{
|
||||
OtaStoreAdaptiveNrf52 store;
|
||||
assert(store.usesExternal());
|
||||
assert(store.plan_layout(true, 40960, 365, 40960, true));
|
||||
assert(store.usesInternal());
|
||||
assert(store.begin(41330));
|
||||
assert(internal_begins == 1 && external_begins == 0);
|
||||
assert(store.plan_layout(true, 100000, 365, 100000, false));
|
||||
assert(store.usesExternal());
|
||||
assert(store.begin(100370));
|
||||
assert(external_begins == 1);
|
||||
boot_resume = true;
|
||||
assert(!store.reopenFor(nullptr, 0));
|
||||
assert(store.usesExternal());
|
||||
assert(store.reopenFor(mid, 0));
|
||||
assert(store.usesInternal());
|
||||
boot_resume = false;
|
||||
assert(!store.reopenFor(mid, 0));
|
||||
assert(store.usesExternal());
|
||||
app_resume = true;
|
||||
assert(store.reopenFor(mid, 0));
|
||||
assert(store.usesExternal());
|
||||
capable = false;
|
||||
assert(!store.plan_layout(true, 40960, 365, 40960, true));
|
||||
assert(store.usesExternal());
|
||||
}
|
||||
assert(internal_live == 0 && external_live == 0);
|
||||
capable = true; detected = 0;
|
||||
{
|
||||
OtaStoreAdaptiveNrf52 store;
|
||||
assert(store.usesInternal());
|
||||
assert(store.plan_layout(true, 40960, 365, 40960, true));
|
||||
assert(store.begin(41330));
|
||||
assert(store.plan_layout(false, 40960, 365, 40960, false));
|
||||
assert(store.usesInternal());
|
||||
}
|
||||
assert(internal_live == 0 && external_live == 0);
|
||||
for (uint8_t flags : {uint8_t(0x0a), uint8_t(0x1e)}) {
|
||||
OtaBlCaps c = simulatedCaps(); c.storage_flags = flags;
|
||||
assert(ota_bootloader_self_update_caps_valid(c));
|
||||
c.apply_abi = 2; assert(!ota_bootloader_self_update_caps_valid(c));
|
||||
}
|
||||
for (uint8_t flags : {uint8_t(0x09), uint8_t(0x0e), uint8_t(0x16), uint8_t(0x1f)}) {
|
||||
OtaBlCaps c = simulatedCaps(); c.storage_flags = flags;
|
||||
assert(!ota_bootloader_self_update_caps_valid(c));
|
||||
}
|
||||
}
|
||||
'''
|
||||
source = "#include <initializer_list>\n" + source
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cpp = Path(directory) / "adaptive.cpp"
|
||||
exe = Path(directory) / "adaptive"
|
||||
cpp.write_text(source)
|
||||
subprocess.run([
|
||||
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-DOTA_RAK_AUTO_STORE", "-DOTA_INTERNAL_BOOTLOADER_UPDATE",
|
||||
"-I", str(ROOT), str(cpp), "-o", str(exe),
|
||||
], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -25,6 +25,14 @@ int main() {
|
||||
const auto Q = RakStorageChoice::Qspi;
|
||||
const auto U = RakStorageChoice::Unsafe;
|
||||
for (bool rak3401 : {false, true}) {
|
||||
const char* board = rak3401 ? "3401_DFU" : "4631_DFU";
|
||||
assert(rak_storage_choice(0, true, true, board, rak3401, true) == I);
|
||||
assert(rak_storage_choice(2, true, true, board, rak3401, true) == Q);
|
||||
assert(rak_storage_choice(2, true, true, board, rak3401, false) == U);
|
||||
assert(rak_storage_choice(1, true, true, board, rak3401, true) == (rak3401 ? U : Q));
|
||||
assert(rak_storage_choice(3, true, true, board, rak3401, true) == U);
|
||||
assert(rak_storage_choice(2, true, true,
|
||||
rak3401 ? "4631_DFU" : "3401_DFU", rak3401, true) == U);
|
||||
assert(rak_storage_choice(0, false, false, nullptr, rak3401) == I);
|
||||
assert(rak_storage_choice(0, true, false, nullptr, rak3401) == U);
|
||||
assert(rak_storage_choice(0, true, true,
|
||||
|
||||
@@ -1089,12 +1089,19 @@ def compatible_mota(info: MotaInfo, target: TargetInfo) -> tuple[bool, str]:
|
||||
return False, "bootloader self-update requires installed ABI 3 or newer"
|
||||
if target.bootloader_codecs != 0x5:
|
||||
return False, "bootloader self-update requires FULL|INPLACE codecs 0x5"
|
||||
expected_storage = 0x09 if target.nrf_sd else 0x0E if target.nrf_qspi else 0x0A
|
||||
adaptive_bootloader = target.boot_hw_id in (
|
||||
"NRF_BL_239A0029_3401_AUTO_DFU", "NRF_BL_239A0029_4631_AUTO_DFU",
|
||||
)
|
||||
internal_rak_bootloader = target.boot_hw_id in (
|
||||
"NRF_BL_239A0029_3401_DFU", "NRF_BL_239A0029_4631_DFU",
|
||||
)
|
||||
expected_storage = (0x0A if internal_rak_bootloader else 0x1E if adaptive_bootloader else
|
||||
0x09 if target.nrf_sd else 0x0E if target.nrf_qspi else 0x0A)
|
||||
if (
|
||||
target.boot_storage != expected_storage
|
||||
or info.bootloader_storage != expected_storage
|
||||
):
|
||||
return False, "bootloader package, installed capabilities, and application storage differ"
|
||||
return False, "bootloader package and installed capabilities differ"
|
||||
if info.block_size > target.max_block_size:
|
||||
return False, f"bootloader block exceeds destination maxblk:{target.max_block_size}"
|
||||
return True, ""
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Adaptive bootloader packages use internal flash regardless of app storage."""
|
||||
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
|
||||
import lora_ota as ota
|
||||
|
||||
|
||||
class RakBootloaderCompatibilityTest(unittest.TestCase):
|
||||
def fixture(self, name, target_id, external):
|
||||
hw = f"NRF_BL_239A0029_{name}_DFU"
|
||||
package = ota.MotaInfo(
|
||||
path=None, blob=b"", flags=ota.MOTA_FLAG_FULL | ota.MOTA_FLAG_BOOTLOADER,
|
||||
target_id=target_id, fw_version=0x02040A02, image_size=40960,
|
||||
payload_size=40960, block_size=1024, merkle_root=b"\0" * 4,
|
||||
image_hash=b"\0" * 8, codec_id=0, hw_id=hw,
|
||||
base_hash=b"\0" * 8, payload_offset=365, bootloader_storage=0x0A,
|
||||
)
|
||||
target = ota.TargetInfo(
|
||||
name=name, target_id=1, base_hash=b"\0" * 8, platform="nrf52",
|
||||
nrf_sd=False, nrf_qspi=external, hw_id=name,
|
||||
bootloader_version="OTAFIX2.4.10-preview.1", bootloader_abi=3,
|
||||
bootloader_codecs=5, status="", self_status="",
|
||||
boot_target_id=target_id, boot_hw_id=hw, boot_storage=0x0A,
|
||||
)
|
||||
return package, target
|
||||
|
||||
def test_internal_boot_update_with_either_application_backend(self):
|
||||
for name, target_id in (("3401", 0x23818A80), ("4631", 0x2D0DF000)):
|
||||
for external in (False, True):
|
||||
with self.subTest(board=name, external=external):
|
||||
package, target = self.fixture(name, target_id, external)
|
||||
self.assertTrue(ota.compatible_mota(package, target)[0])
|
||||
for flags in (0x1E, 0x0E, 0x16):
|
||||
self.assertFalse(ota.compatible_mota(
|
||||
replace(package, bootloader_storage=flags), target)[0])
|
||||
self.assertFalse(ota.compatible_mota(
|
||||
package, replace(target, boot_storage=flags))[0])
|
||||
self.assertFalse(ota.compatible_mota(
|
||||
package, replace(target, bootloader_abi=2))[0])
|
||||
self.assertFalse(ota.compatible_mota(
|
||||
package, replace(target, boot_hw_id="NRF_BL_239A0029_4631_AUTO_DFU"))[0])
|
||||
self.assertFalse(ota.compatible_mota(
|
||||
package, replace(target, boot_target_id=target_id ^ 1))[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+36
-3
@@ -108,11 +108,17 @@ BOOT_STORAGE_SD = 0x01
|
||||
BOOT_STORAGE_STAGE_CEILING = 0x02
|
||||
XIAO_BOOT_STORAGE_QSPI = 0x04
|
||||
XIAO_BOOT_STORAGE_UPDATE = 0x08
|
||||
BOOT_STORAGE_KNOWN = 0x0F
|
||||
BOOT_STORAGE_HEADER_W25 = 0x10
|
||||
BOOT_STORAGE_KNOWN = 0x1F
|
||||
BOOT_STORAGE_SD_UPDATE = BOOT_STORAGE_SD | XIAO_BOOT_STORAGE_UPDATE
|
||||
BOOT_STORAGE_QSPI_UPDATE = (BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_QSPI |
|
||||
XIAO_BOOT_STORAGE_UPDATE)
|
||||
BOOT_STORAGE_INTERNAL_UPDATE = BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_UPDATE
|
||||
BOOT_STORAGE_RAK_AUTO_UPDATE = BOOT_STORAGE_QSPI_UPDATE | BOOT_STORAGE_HEADER_W25
|
||||
RAK_AUTO_BOOTLOADER_IDENTITIES = (
|
||||
(0x239A0029, "3401_AUTO_DFU"),
|
||||
(0x239A0029, "4631_AUTO_DFU"),
|
||||
)
|
||||
BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) |
|
||||
(1 << CODEC_DETOOLS_INPLACE))
|
||||
|
||||
@@ -696,7 +702,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
|
||||
identities = (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
) + INTERNAL_BOOTLOADER_IDENTITIES
|
||||
) + INTERNAL_BOOTLOADER_IDENTITIES + RAK_AUTO_BOOTLOADER_IDENTITIES
|
||||
app_ids = set(application_target_ids)
|
||||
targets = {}
|
||||
for identity in identities:
|
||||
@@ -714,6 +720,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
|
||||
|
||||
def bootloader_identity_is_buildable(board_id: int, device_name: str) -> bool:
|
||||
return ((board_id, device_name) in INTERNAL_BOOTLOADER_IDENTITIES or
|
||||
(board_id, device_name) in RAK_AUTO_BOOTLOADER_IDENTITIES or
|
||||
(board_id, device_name) in (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
|
||||
@@ -734,6 +741,8 @@ def bootloader_qualified_platform_profile(board_id: int, device_name: str):
|
||||
def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
|
||||
"""Return the exact allowed capability-marker profiles for one identity."""
|
||||
identity = (board_id, device_name)
|
||||
if identity in RAK_AUTO_BOOTLOADER_IDENTITIES:
|
||||
return (BOOT_STORAGE_RAK_AUTO_UPDATE,)
|
||||
if identity in ((XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")):
|
||||
return (BOOT_STORAGE_QSPI_UPDATE,)
|
||||
@@ -836,7 +845,7 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
|
||||
if valid_count != 1:
|
||||
return None
|
||||
if storage in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
|
||||
BOOT_STORAGE_INTERNAL_UPDATE):
|
||||
BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_RAK_AUTO_UPDATE):
|
||||
found = storage
|
||||
return found if valid_count == 1 else None
|
||||
|
||||
@@ -845,6 +854,19 @@ def xiao_bootloader_caps_ok(image: bytes) -> bool:
|
||||
return bootloader_caps_storage(image) == BOOT_STORAGE_QSPI_UPDATE
|
||||
|
||||
|
||||
def bootloader_optional_app_storage(image: bytes) -> int:
|
||||
record = (b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536) +
|
||||
b"MOTASTOR" + struct.pack("<HHBBBB", 1, 16, 0x14, 0, 0, 0))
|
||||
offsets = [off for off in range(0, len(image) - 7, 4)
|
||||
if image[off:off + 8] == b"MOTASTOR"]
|
||||
if not offsets:
|
||||
return 0
|
||||
if (len(offsets) != 1 or offsets[0] < 16 or
|
||||
image[offsets[0] - 16:offsets[0] + 16] != record):
|
||||
raise ValueError("optional RAK application storage marker is invalid or ambiguous")
|
||||
return 0x14
|
||||
|
||||
|
||||
def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
|
||||
signed_hw_id: Optional[bytes] = None) -> BootloaderIdentity:
|
||||
if len(image) != XIAO_BOOT_IMAGE_SIZE:
|
||||
@@ -876,6 +898,17 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
|
||||
if actual_storage not in expected_storage:
|
||||
expected = "/".join(f"0x{value:02X}" for value in expected_storage)
|
||||
raise ValueError(f"bootloader lacks exact ABI 3 self-update capabilities {expected}")
|
||||
optional_storage = bootloader_optional_app_storage(image)
|
||||
if optional_storage and (actual_storage != BOOT_STORAGE_INTERNAL_UPDATE or
|
||||
identity.board_id != 0x239A0029 or
|
||||
identity.device_name not in ("3401_DFU", "4631_DFU")):
|
||||
raise ValueError("optional RAK application storage requires the deployed board identity")
|
||||
if actual_storage == BOOT_STORAGE_RAK_AUTO_UPDATE or optional_storage:
|
||||
ram_marker = b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
|
||||
count = sum(image[off:off + 16] == ram_marker
|
||||
for off in range(0, len(image) - 15, 4))
|
||||
if count != 1 or struct.unpack_from("<I", image)[0] > 0x20030000:
|
||||
raise ValueError("adaptive bootloader must retain its 64 KiB hybrid RAM arena")
|
||||
expected_platform = bootloader_qualified_platform_profile(
|
||||
identity.board_id, identity.device_name)
|
||||
if expected_platform is not None:
|
||||
|
||||
+55
-2
@@ -1500,7 +1500,7 @@ def test_xiao_bootloader_caps_rejects_malformed_or_unaligned_markers():
|
||||
malformed_decoy = bytearray(caps())
|
||||
struct.pack_into("<8sHHB3s", malformed_decoy, 24,
|
||||
ml.XIAO_BOOT_CAPS_MAGIC, ml.BOOT_FORMAT_VER,
|
||||
ml.BOOT_REQUIRED_APP_CODEC_MASK, 0x1C, b"\0\0\0")
|
||||
ml.BOOT_REQUIRED_APP_CODEC_MASK, 0x2C, b"\0\0\0")
|
||||
assert ml.bootloader_caps_storage(bytes(malformed_decoy)) == \
|
||||
ml.BOOT_STORAGE_QSPI_UPDATE
|
||||
|
||||
@@ -1672,7 +1672,9 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
for value in re.findall(r"\{ 0x([0-9a-fA-F]{8}),", target_header)
|
||||
}
|
||||
audited = ml.audit_bootloader_target_inventory(app_targets)
|
||||
assert len(audited) == len(expected) + 2 # generic internal plus two legacy XIAOs
|
||||
assert len(audited) == len(expected) + 4 # internal, two XIAOs, two adaptive RAKs
|
||||
assert audited[0xD04AB3AB] == (0x239A0029, "3401_AUTO_DFU")
|
||||
assert audited[0xFEEAFD1B] == (0x239A0029, "4631_AUTO_DFU")
|
||||
|
||||
# Generic parsing remains useful for inspecting a future candidate, but a
|
||||
# signing/build call fails closed until its exact identity is qualified.
|
||||
@@ -1762,6 +1764,57 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
|
||||
pass
|
||||
|
||||
|
||||
def test_adaptive_bootloader_preserves_update_and_hybrid_capabilities():
|
||||
for name in ("3401_AUTO_DFU", "4631_AUTO_DFU"):
|
||||
image = _generic_bootloader_image(
|
||||
device_name=name, storage=ml.BOOT_STORAGE_RAK_AUTO_UPDATE)
|
||||
marker = b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
|
||||
def add_ram(data):
|
||||
struct.pack_into("<I", data, 0, 0x20030000)
|
||||
data[0x90:0xA0] = marker
|
||||
image = _rewrite_boot_image(image, add_ram)
|
||||
assert ml.validate_bootloader_image(image).device_name == name
|
||||
assert ml.bootloader_caps_storage(image) == 0x1E
|
||||
mutations = (
|
||||
lambda data: data.__setitem__(slice(0x90, 0xA0), b"\xff" * 16),
|
||||
lambda data: data.__setitem__(slice(0xA0, 0xB0), marker),
|
||||
lambda data: struct.pack_into("<I", data, 0, 0x20040000),
|
||||
lambda data: data.__setitem__(0x8C, 0x16),
|
||||
lambda data: data.__setitem__(0x88, 2),
|
||||
)
|
||||
for mutate in mutations:
|
||||
try:
|
||||
ml.validate_bootloader_image(_rewrite_boot_image(image, mutate))
|
||||
assert False, "invalid adaptive successor was accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_compatible_rak_keeps_legacy_contract_and_optional_application_storage():
|
||||
marker = b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
|
||||
optional = b"MOTASTOR" + struct.pack("<HHB3x", 1, 16, 0x14)
|
||||
for name in ("3401_DFU", "4631_DFU"):
|
||||
def add_storage(data):
|
||||
struct.pack_into("<I", data, 0, 0x20030000)
|
||||
data[0x90:0xB0] = marker + optional
|
||||
image = _rewrite_boot_image(_generic_bootloader_image(device_name=name), add_storage)
|
||||
assert ml.validate_bootloader_image(image).device_name == name
|
||||
assert ml.bootloader_caps_storage(image) == 0x0A
|
||||
assert ml.bootloader_optional_app_storage(image) == 0x14
|
||||
for mutate in (
|
||||
lambda raw: raw.__setitem__(0xAC, 0x04),
|
||||
lambda raw: raw.__setitem__(0xAD, 1),
|
||||
lambda raw: raw.__setitem__(slice(0xB0, 0xC0), optional),
|
||||
lambda raw: raw.__setitem__(slice(0x90, 0xA0), b"\xff" * 16),
|
||||
lambda raw: struct.pack_into("<I", raw, 0, 0x20040000),
|
||||
):
|
||||
try:
|
||||
ml.validate_bootloader_image(_rewrite_boot_image(image, mutate))
|
||||
assert False, "malformed optional application storage accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_bootloader_builder_rejects_wrong_identity_geometry_and_continuity():
|
||||
priv = Ed25519PrivateKey.generate()
|
||||
image = _xiao_bootloader_image()
|
||||
|
||||
Reference in new issue
Block a user