Add shared-slot nRF52 bootloader updates
This commit is contained in:
1 parent
b7c9e37d09
commit
3de5fddf47
43 files changed
+1647
-211
No files matched your search
@@ -40,7 +40,7 @@ public:
|
||||
|
||||
// A host-backed store is fully random-access, so it needs no pinned-meta RAM page and no layout planning.
|
||||
bool set_meta_size(uint32_t) override { return true; }
|
||||
bool plan_layout(bool, uint32_t, uint32_t, uint32_t) override { return true; }
|
||||
bool plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) override { return true; }
|
||||
|
||||
private:
|
||||
// One request/response transaction over the seeder link (mirrors SerialMotaSource). `arglen` is uint16 so
|
||||
|
||||
+113
-15
@@ -32,6 +32,9 @@
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
#endif
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
#include "OtaStoreFlashNrf52.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
@@ -462,8 +465,7 @@ void ota_reboot_to_apply() { // public: set the apply magic +
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_QSPI;
|
||||
#elif defined(OTA_FLASH_STORE)
|
||||
if (ota_nrf52_effective_stage_ceiling() == MOTA_NRF52_STAGE_CEILING_EXPANDED)
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_EXPANDED;
|
||||
stage_handoff = mota_nrf52_flash_stage_handoff(ota_nrf52_effective_stage_ceiling());
|
||||
#endif
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
@@ -480,15 +482,19 @@ void ota_reboot_to_apply() { // public: set the apply magic +
|
||||
}
|
||||
|
||||
void ota_reboot_to_bootloader_update() {
|
||||
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
source = GPREGRET2_OTA_STAGE_EXPANDED;
|
||||
#endif
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
if (sd_en) {
|
||||
sd_power_gpregret_clr(1, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(1, GPREGRET2_OTA_STAGE_QSPI);
|
||||
sd_power_gpregret_set(1, source);
|
||||
sd_power_gpregret_clr(0, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(0, GPREGRET_OTA_BOOTLOADER_UPDATE);
|
||||
} else {
|
||||
NRF_POWER->GPREGRET2 = GPREGRET2_OTA_STAGE_QSPI;
|
||||
NRF_POWER->GPREGRET2 = source;
|
||||
NRF_POWER->GPREGRET = GPREGRET_OTA_BOOTLOADER_UPDATE;
|
||||
}
|
||||
NVIC_SystemReset();
|
||||
@@ -584,10 +590,16 @@ static bool ota_apply_mota_nrf52_impl(const uint8_t* buf, uint32_t len,
|
||||
}
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t app_ceiling = mota_nrf52_application_ceiling();
|
||||
const uint32_t mota_start = (uint32_t)(uintptr_t)buf;
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
if (mota_start < app_base || mota_start >= stage_ceiling ||
|
||||
d.memory > mota_start - app_base) {
|
||||
if (!mota_nrf52_target_image_fits(app_base, m.image_size, app_ceiling)) {
|
||||
strcpy(msg, "target image exceeds linked application region");
|
||||
return false;
|
||||
}
|
||||
if (mota_start >= stage_ceiling ||
|
||||
!mota_nrf52_internal_patch_workspace_valid(
|
||||
d.memory, app_base, mota_start, m.image_size, app_ceiling)) {
|
||||
snprintf(msg, 159, "patch memory 0x%x exceeds staging at 0x%x",
|
||||
(unsigned)d.memory, (unsigned)mota_start);
|
||||
return false;
|
||||
@@ -779,6 +791,7 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
|
||||
static const uint32_t STEP = 512;
|
||||
uint8_t buf[STEP + OTA_BOOT_MANIFEST_SIZE - 1];
|
||||
uint8_t valid_identities = 0;
|
||||
uint8_t valid_caps = 0;
|
||||
for (uint32_t base = 0; base < OTA_BOOT_IMAGE_SIZE; base += STEP) {
|
||||
uint32_t len = OTA_BOOT_IMAGE_SIZE - base;
|
||||
if (len > sizeof(buf)) len = sizeof(buf);
|
||||
@@ -787,14 +800,14 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
|
||||
? OTA_BOOT_IMAGE_SIZE - base : STEP;
|
||||
for (uint32_t local = 0; local < starts; local++) {
|
||||
const uint32_t absolute = base + local;
|
||||
if (!caps.present && (absolute & 3u) == 0 && local + 16u <= len) {
|
||||
if ((absolute & 3u) == 0 && local + 16u <= len) {
|
||||
OtaBootloaderCapsMarker parsed;
|
||||
if (ota_bootloader_caps_marker_parse(buf + local, parsed) &&
|
||||
parsed.apply_abi >= MOTA_BOOT_FORMAT_VER &&
|
||||
(parsed.codec_mask & (1u << CODEC_FULL)) != 0 &&
|
||||
(parsed.storage_flags & (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) ==
|
||||
(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) {
|
||||
caps = parsed;
|
||||
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0) {
|
||||
if (++valid_caps != 1u) return false;
|
||||
if (parsed.storage_flags == ota_bootloader_update_storage_flags()) caps = parsed;
|
||||
}
|
||||
}
|
||||
if ((absolute & 3u) != 0 || local + OTA_BOOT_MANIFEST_SIZE > len) continue;
|
||||
@@ -808,7 +821,8 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
|
||||
}
|
||||
}
|
||||
}
|
||||
return valid_identities == 1u && candidate.present && candidate.crc_ok && caps.present;
|
||||
return valid_identities == 1u && valid_caps == 1u &&
|
||||
candidate.present && candidate.crc_ok && caps.present;
|
||||
}
|
||||
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
@@ -844,10 +858,7 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
}
|
||||
|
||||
const OtaBlCaps current_caps = ota_bootloader_caps();
|
||||
if (!current_caps.present || current_caps.apply_abi < MOTA_BOOT_FORMAT_VER ||
|
||||
(current_caps.codec_mask & (1u << CODEC_FULL)) == 0 ||
|
||||
(current_caps.storage_flags & (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) !=
|
||||
(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) {
|
||||
if (!ota_bootloader_self_update_caps_valid(current_caps)) {
|
||||
strcpy(msg, "installed bootloader cannot safely self-update from QSPI"); return false;
|
||||
}
|
||||
|
||||
@@ -893,6 +904,93 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_FLASH_STORE)
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
ApplyState& st, char* msg) {
|
||||
static const size_t CAP = 96;
|
||||
st = ApplyState();
|
||||
const uint32_t total = store.staged_size();
|
||||
const uint8_t* container = store.data();
|
||||
MotaManifest m;
|
||||
if (!container || total < 8u + MOTA_MFL + 5u || !mota_parse(container, total, m)) {
|
||||
strcpy(msg, "internal bootloader container parse failed"); return false;
|
||||
}
|
||||
switch (ota_bootloader_confirmation_gate(m, installed, actual_mid, operator_mid,
|
||||
operator_hash8)) {
|
||||
case OTA_BOOT_CONFIRM_OK: break;
|
||||
case OTA_BOOT_CONFIRM_NOT_BOOT_PACKAGE: strcpy(msg, "not a bootloader package"); return false;
|
||||
case OTA_BOOT_CONFIRM_GEOMETRY: strcpy(msg, "bootloader package geometry mismatch"); return false;
|
||||
case OTA_BOOT_CONFIRM_LOCAL_IDENTITY: strcpy(msg, "installed bootloader identity is invalid"); return false;
|
||||
case OTA_BOOT_CONFIRM_TARGET: strcpy(msg, "bootloader target identity mismatch"); return false;
|
||||
case OTA_BOOT_CONFIRM_HW_ID: strcpy(msg, "bootloader signed hw_id mismatch"); return false;
|
||||
case OTA_BOOT_CONFIRM_MID: strcpy(msg, "bootloader MID confirmation mismatch"); return false;
|
||||
case OTA_BOOT_CONFIRM_IMAGE_HASH: strcpy(msg, "bootloader image-hash confirmation mismatch"); return false;
|
||||
}
|
||||
|
||||
const OtaBlCaps current_caps = ota_bootloader_caps();
|
||||
if (!ota_bootloader_self_update_caps_valid(current_caps) ||
|
||||
current_caps.storage_flags != (OTA_BL_STORAGE_STAGE_CEILING |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE)) {
|
||||
strcpy(msg, "installed bootloader cannot safely self-update from internal flash"); return false;
|
||||
}
|
||||
|
||||
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
|
||||
SelfFwInfo fi;
|
||||
uint32_t expected_start;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (payload_off64 > UINT32_MAX || payload_off64 + OTA_BOOT_IMAGE_SIZE + 5u != total ||
|
||||
!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
|
||||
!mota_nrf52_shared_boot_stage_plan(
|
||||
total, app_base, true, app_base + fi.image_len, expected_start) ||
|
||||
store.write_start() != expected_start) {
|
||||
strcpy(msg, "internal bootloader container placement mismatch"); return false;
|
||||
}
|
||||
const uint32_t payload_off = (uint32_t)payload_off64;
|
||||
|
||||
VerifyResult vr = ota_verify(container, total, allow);
|
||||
st.manifest_ok = vr.parsed;
|
||||
st.sig_ok = vr.sig_ok;
|
||||
st.trusted = vr.trusted;
|
||||
st.image_size = m.image_size;
|
||||
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
|
||||
if (!vr.auto_appliable()) {
|
||||
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok)
|
||||
strcpy(msg, "bootloader payload hash mismatch");
|
||||
else if (!vr.sig_ok)
|
||||
strcpy(msg, "bootloader package signature invalid");
|
||||
else
|
||||
strcpy(msg, "bootloader signer is not in the trusted allowlist");
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t* image = container + payload_off;
|
||||
if (!ota_bootloader_vector_sane(image)) {
|
||||
strcpy(msg, "bootloader vector table is invalid"); return false;
|
||||
}
|
||||
OtaBootloaderIdentity candidate;
|
||||
OtaBootloaderCapsMarker candidate_caps;
|
||||
if (!ota_bootloader_identity_from_image(image, OTA_BOOT_IMAGE_SIZE, candidate) ||
|
||||
!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,
|
||||
candidate_caps)) {
|
||||
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
|
||||
}
|
||||
st.slot_ok = true;
|
||||
if (!store.approve_for_bootloader()) {
|
||||
snprintf(msg, CAP, "internal bootloader handoff failed: %s", store.last_error()); return false;
|
||||
}
|
||||
strcpy(msg, "trusted bootloader verified in internal flash; rebooting after this reply");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#else // native / other platforms
|
||||
|
||||
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
|
||||
|
||||
@@ -101,6 +101,17 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
ApplyState& st, char* msg);
|
||||
#endif
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && \
|
||||
defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
class OtaStoreFlashNrf52;
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
ApplyState& st, char* msg);
|
||||
#endif
|
||||
|
||||
// Commit the (already approved/armed) update and reboot into it - does NOT return. Call this only after
|
||||
// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows
|
||||
|
||||
@@ -33,6 +33,27 @@ static const uint8_t OTA_BL_STORAGE_SD = 0x01;
|
||||
static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02;
|
||||
static const uint8_t OTA_BL_STORAGE_QSPI = 0x04;
|
||||
static const uint8_t OTA_BL_STORAGE_BOOT_UPDATE = 0x08;
|
||||
static const uint8_t OTA_BL_STORAGE_KNOWN = 0x0F;
|
||||
|
||||
inline uint8_t ota_bootloader_update_storage_flags() {
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
// No external-storage bit means the ordinary internal flash store. The
|
||||
// same store holds either an app delta or a boot package; BOOT_UPDATE marks
|
||||
// only the privileged package capability, not a second storage backend.
|
||||
return OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
#elif defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
return OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_self_update_caps_valid(const OtaBlCaps& c) {
|
||||
const uint8_t required = ota_bootloader_update_storage_flags();
|
||||
return required != 0 && c.present && c.apply_abi >= 3u &&
|
||||
(c.codec_mask & 1u) != 0 && c.storage_flags == required;
|
||||
}
|
||||
|
||||
// Prefer a continuity-capable marker over a numerically newer legacy-looking candidate. Bootloader
|
||||
// images contain the marker magic in code/literal data as well as in the real structure, so the scan
|
||||
@@ -53,17 +74,33 @@ inline bool ota_bl_caps_prefer(const OtaBlCaps& current, uint16_t candidate_abi,
|
||||
// uint16_t fields and OTAFIX emits it aligned(4); accepting a byte-shifted magic
|
||||
// occurrence would let instruction/literal bytes masquerade as capabilities.
|
||||
inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len,
|
||||
bool require_boot_update) {
|
||||
bool require_boot_update,
|
||||
uint8_t exact_storage_flags = 0) {
|
||||
OtaBlCaps c;
|
||||
if (!bytes) return c;
|
||||
uint8_t privileged_count = 0;
|
||||
for (size_t off = 0; off + 16u <= len; off += 4u) {
|
||||
const uint8_t* p = bytes + off;
|
||||
if (p[0] != OTA_BL_MAGIC[0] || memcmp(p, OTA_BL_MAGIC, 8) != 0) continue;
|
||||
const uint16_t abi = (uint16_t)(p[8] | ((uint16_t)p[9] << 8));
|
||||
const uint16_t codecs = (uint16_t)(p[10] | ((uint16_t)p[11] << 8));
|
||||
const uint8_t storage = p[12];
|
||||
if (abi == 0 || abi == 0xFFFFu || codecs == 0 || (storage & ~0x0Fu) != 0 ||
|
||||
if (abi == 0 || abi == 0xFFFFu || codecs == 0 || (storage & ~OTA_BL_STORAGE_KNOWN) != 0 ||
|
||||
p[13] != 0 || p[14] != 0 || p[15] != 0) continue;
|
||||
const bool exact_profile = exact_storage_flags == 0 || storage == exact_storage_flags;
|
||||
if (require_boot_update && exact_profile && abi >= 3u && (codecs & 1u) != 0 &&
|
||||
(storage & OTA_BL_STORAGE_BOOT_UPDATE) != 0) {
|
||||
// A self-update build must have one unambiguous privileged marker. Do
|
||||
// not silently choose between two otherwise valid structures (including
|
||||
// two copies of the expected exact profile). Malformed magic/literal
|
||||
// decoys were filtered above and do not count.
|
||||
if (++privileged_count != 1u) return OtaBlCaps();
|
||||
c.present = true;
|
||||
c.apply_abi = abi;
|
||||
c.codec_mask = codecs;
|
||||
c.storage_flags = storage;
|
||||
continue;
|
||||
}
|
||||
if (ota_bl_caps_prefer(c, abi, storage, require_boot_update)) {
|
||||
c.present = true;
|
||||
c.apply_abi = abi;
|
||||
@@ -71,6 +108,7 @@ inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len,
|
||||
c.storage_flags = storage;
|
||||
}
|
||||
}
|
||||
if (require_boot_update && privileged_count != 1u) return OtaBlCaps();
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -80,8 +118,8 @@ inline OtaBlCaps ota_bootloader_caps() {
|
||||
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;
|
||||
return ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo),
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
true
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
true, ota_bootloader_update_storage_flags()
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
@@ -103,11 +141,18 @@ inline bool ota_bootloader_can_apply(uint8_t format_ver, uint8_t codec_id) {
|
||||
// the legacy 0xD4000 ceiling. Packages sized for the expanded window still require the newer bootloader.
|
||||
inline uint32_t ota_nrf52_effective_stage_ceiling(const OtaBlCaps& c) {
|
||||
const uint32_t desired = mota_nrf52_layout_stage_ceiling();
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
if (desired != MOTA_NRF52_STAGE_CEILING_EXPANDED ||
|
||||
!ota_bootloader_self_update_caps_valid(c))
|
||||
return MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
return desired;
|
||||
#else
|
||||
if (desired == MOTA_NRF52_STAGE_CEILING_EXPANDED) {
|
||||
if (!c.present || !(c.storage_flags & OTA_BL_STORAGE_STAGE_CEILING))
|
||||
return MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
}
|
||||
return desired;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline uint32_t ota_nrf52_effective_stage_ceiling() {
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "MotaContainer.h"
|
||||
#include "Multihash.h"
|
||||
#include "OtaBlInfo.h"
|
||||
#include "OtaByteIO.h"
|
||||
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
#if !defined(NRF52_PLATFORM) || !defined(OTA_QSPI_STORE)
|
||||
@@ -18,6 +22,15 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
#if !defined(NRF52_PLATFORM) || !defined(OTA_FLASH_STORE)
|
||||
#error "OTA_INTERNAL_BOOTLOADER_UPDATE requires nRF52 internal-flash staging"
|
||||
#endif
|
||||
#if defined(OTA_QSPI_STORE) || defined(OTA_SD_STORE) || defined(QSPIFLASH)
|
||||
#error "internal bootloader staging cannot share an external OTA/filesystem store"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
@@ -83,8 +96,30 @@ inline bool ota_xiao_bootloader_board_id(uint32_t board_id) {
|
||||
return board_id == OTA_XIAO_BOARD_ID_BASE || board_id == OTA_XIAO_BOARD_ID_SENSE;
|
||||
}
|
||||
|
||||
// Canonical signed .mota hw_id. It is intentionally distinct from application hw_id values and is
|
||||
// derived solely from the exact embedded board ID.
|
||||
inline bool ota_bootloader_board_id_valid(uint32_t board_id) {
|
||||
return board_id != 0u && board_id != UINT32_MAX;
|
||||
}
|
||||
|
||||
// New exact-board manifests use one to fifteen printable ASCII bytes followed by zero padding. Requiring
|
||||
// a terminator keeps the canonical signed hw_id at 31 bytes or less. The two already-deployed XIAO
|
||||
// identities retain their exact legacy name and signed hardware IDs.
|
||||
inline bool ota_bootloader_device_name_valid(uint32_t board_id, const uint8_t name[16]) {
|
||||
if (!name) return false;
|
||||
if (ota_xiao_bootloader_board_id(board_id))
|
||||
return memcmp(name, OTA_XIAO_BOOT_DEVICE_NAME, OTA_BOOT_DEVICE_NAME_SIZE) == 0;
|
||||
size_t end = 0;
|
||||
while (end < OTA_BOOT_DEVICE_NAME_SIZE && name[end] != 0) {
|
||||
if (name[end] < 0x21u || name[end] > 0x7Eu) return false;
|
||||
end++;
|
||||
}
|
||||
if (end == 0 || end == OTA_BOOT_DEVICE_NAME_SIZE) return false;
|
||||
for (size_t i = end; i < OTA_BOOT_DEVICE_NAME_SIZE; i++)
|
||||
if (name[i] != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy XIAO signed .mota hw_id. It is intentionally distinct from application hw_id values and is
|
||||
// derived from the exact embedded board ID for compatibility with already-deployed packages.
|
||||
inline bool ota_xiao_bootloader_hw_id(uint32_t board_id, uint8_t out[32]) {
|
||||
if (!out || !ota_xiao_bootloader_board_id(board_id)) return false;
|
||||
memset(out, 0, 32);
|
||||
@@ -94,6 +129,37 @@ inline bool ota_xiao_bootloader_hw_id(uint32_t board_id, uint8_t out[32]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Canonical generic bootloader identity. The board ID alone is not unique (several nRF52 families use
|
||||
// the same USB VID/PID-derived ID), so the signed identity includes the complete embedded device name.
|
||||
// Hashing all 32 padded bytes produces the bootloader package's collision-checked wire target ID.
|
||||
inline bool ota_bootloader_hw_id(uint32_t board_id, const uint8_t device_name[16],
|
||||
uint8_t out[32]) {
|
||||
if (!out || !ota_bootloader_board_id_valid(board_id) ||
|
||||
!ota_bootloader_device_name_valid(board_id, device_name)) return false;
|
||||
if (ota_xiao_bootloader_board_id(board_id)) return ota_xiao_bootloader_hw_id(board_id, out);
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
memset(out, 0, 32);
|
||||
memcpy(out, "NRF_BL_", 7);
|
||||
for (uint8_t i = 0; i < 8; i++)
|
||||
out[7 + i] = (uint8_t)hex[(board_id >> (28u - 4u * i)) & 0x0Fu];
|
||||
out[15] = '_';
|
||||
size_t n = 0;
|
||||
while (n < 15 && device_name[n]) n++;
|
||||
memcpy(out + 16, device_name, n);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_hw_id(const OtaBootloaderIdentity& identity, uint8_t out[32]);
|
||||
|
||||
inline uint32_t ota_bootloader_target_id(uint32_t board_id, const uint8_t device_name[16]) {
|
||||
if (ota_xiao_bootloader_board_id(board_id)) return board_id;
|
||||
uint8_t hw_id[32], digest[4];
|
||||
if (!ota_bootloader_hw_id(board_id, device_name, hw_id)) return 0;
|
||||
mh4(digest, hw_id, sizeof(hw_id));
|
||||
return rd_u32le(digest);
|
||||
}
|
||||
|
||||
inline uint32_t ota_boot_crc32_update(uint32_t crc, uint8_t value) {
|
||||
crc ^= value;
|
||||
for (uint8_t bit = 0; bit < 8; bit++)
|
||||
@@ -122,8 +188,8 @@ inline bool ota_bootloader_manifest_parse(const uint8_t* raw, uint32_t raw_offse
|
||||
ota_boot_rd16(raw + 10) != OTA_BOOT_MANIFEST_SIZE ||
|
||||
ota_boot_rd32(raw + 12) != OTA_BOOT_IMAGE_START ||
|
||||
ota_boot_rd32(raw + 16) != OTA_BOOT_IMAGE_SIZE ||
|
||||
!ota_xiao_bootloader_board_id(ota_boot_rd32(raw + 20)) ||
|
||||
memcmp(raw + 24, OTA_XIAO_BOOT_DEVICE_NAME, OTA_BOOT_DEVICE_NAME_SIZE) != 0) return false;
|
||||
!ota_bootloader_board_id_valid(ota_boot_rd32(raw + 20)) ||
|
||||
!ota_bootloader_device_name_valid(ota_boot_rd32(raw + 20), raw + 24)) return false;
|
||||
|
||||
out = OtaBootloaderIdentity();
|
||||
out.present = true;
|
||||
@@ -137,6 +203,18 @@ inline bool ota_bootloader_manifest_parse(const uint8_t* raw, uint32_t raw_offse
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_hw_id(const OtaBootloaderIdentity& identity, uint8_t out[32]) {
|
||||
return identity.present && identity.crc_ok &&
|
||||
ota_bootloader_hw_id(identity.board_id,
|
||||
reinterpret_cast<const uint8_t*>(identity.device_name), out);
|
||||
}
|
||||
|
||||
inline uint32_t ota_bootloader_target_id(const OtaBootloaderIdentity& identity) {
|
||||
return identity.present && identity.crc_ok
|
||||
? ota_bootloader_target_id(identity.board_id,
|
||||
reinterpret_cast<const uint8_t*>(identity.device_name)) : 0;
|
||||
}
|
||||
|
||||
// Validate an installed memory-mapped bootloader and recover its exact identity. Scan on word
|
||||
// boundaries to mirror OTAFIX's self-update validator.
|
||||
inline bool ota_bootloader_identity_from_image(const uint8_t* image, size_t image_size,
|
||||
@@ -187,7 +265,7 @@ inline bool ota_bootloader_caps_marker_parse(const uint8_t raw[16],
|
||||
static const uint8_t magic[8] = {'M','O','T','A','B','L','D','R'};
|
||||
if (!raw || memcmp(raw, magic, sizeof(magic)) != 0 ||
|
||||
raw[8] == 0 || (raw[8] == 0xFF && raw[9] == 0xFF) ||
|
||||
(raw[10] == 0 && raw[11] == 0) || (raw[12] & ~0x0Fu) != 0 ||
|
||||
(raw[10] == 0 && raw[11] == 0) || (raw[12] & ~OTA_BL_STORAGE_KNOWN) != 0 ||
|
||||
raw[13] != 0 || raw[14] != 0 || raw[15] != 0) return false;
|
||||
out.present = true;
|
||||
out.apply_abi = ota_boot_rd16(raw + 8);
|
||||
@@ -196,6 +274,25 @@ inline bool ota_bootloader_caps_marker_parse(const uint8_t raw[16],
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_caps_from_image(const uint8_t* image, size_t image_size,
|
||||
uint8_t exact_storage_flags,
|
||||
OtaBootloaderCapsMarker& out) {
|
||||
out = OtaBootloaderCapsMarker();
|
||||
if (!image || exact_storage_flags == 0) return false;
|
||||
uint8_t valid_count = 0;
|
||||
for (size_t off = 0; off + 16u <= image_size; off += 4u) {
|
||||
OtaBootloaderCapsMarker parsed;
|
||||
if (!ota_bootloader_caps_marker_parse(image + off, parsed) ||
|
||||
parsed.apply_abi < MOTA_BOOT_FORMAT_VER ||
|
||||
(parsed.codec_mask & (1u << CODEC_FULL)) == 0 ||
|
||||
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) == 0) continue;
|
||||
if (++valid_count != 1u) return false; // privileged marker identity must be unambiguous
|
||||
if (parsed.storage_flags != exact_storage_flags) continue;
|
||||
out = parsed;
|
||||
}
|
||||
return valid_count == 1u && out.present;
|
||||
}
|
||||
|
||||
enum OtaBootloaderConfirmGate : uint8_t {
|
||||
OTA_BOOT_CONFIRM_OK = 0,
|
||||
OTA_BOOT_CONFIRM_NOT_BOOT_PACKAGE,
|
||||
@@ -223,11 +320,13 @@ inline OtaBootloaderConfirmGate ota_bootloader_confirmation_gate(
|
||||
memcmp(m.base_hash, zero_base, sizeof(zero_base)) != 0)
|
||||
return OTA_BOOT_CONFIRM_GEOMETRY;
|
||||
if (!installed.present || !installed.crc_ok ||
|
||||
!ota_xiao_bootloader_board_id(installed.board_id))
|
||||
!ota_bootloader_board_id_valid(installed.board_id) ||
|
||||
!ota_bootloader_device_name_valid(
|
||||
installed.board_id, reinterpret_cast<const uint8_t*>(installed.device_name)))
|
||||
return OTA_BOOT_CONFIRM_LOCAL_IDENTITY;
|
||||
if (m.target_id != installed.board_id) return OTA_BOOT_CONFIRM_TARGET;
|
||||
if (m.target_id != ota_bootloader_target_id(installed)) return OTA_BOOT_CONFIRM_TARGET;
|
||||
uint8_t expected_hw[32];
|
||||
if (!ota_xiao_bootloader_hw_id(installed.board_id, expected_hw) || !m.hw_id ||
|
||||
if (!ota_bootloader_hw_id(installed, expected_hw) || !m.hw_id ||
|
||||
memcmp(m.hw_id, expected_hw, sizeof(expected_hw)) != 0)
|
||||
return OTA_BOOT_CONFIRM_HW_ID;
|
||||
if (!actual_mid || !operator_mid || memcmp(actual_mid, operator_mid, 4) != 0 ||
|
||||
|
||||
@@ -55,7 +55,7 @@ public:
|
||||
bool finalize() override;
|
||||
void checkpoint() override;
|
||||
bool reopen() override;
|
||||
bool plan_layout(bool, uint32_t, uint32_t, uint32_t) override { return true; }
|
||||
bool plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) override { return true; }
|
||||
|
||||
// MotaSource: completed files are advertised and read directly from SD on demand.
|
||||
uint8_t count() override { return _initialized ? _count : 0; }
|
||||
|
||||
+66
-32
@@ -170,9 +170,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
"announce | folder | config. LoRa install is disabled.");
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get <id> flash | install | "
|
||||
"bootloader | cancel | announce | self | folder | config | key");
|
||||
#else
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
|
||||
"cancel | announce | self | folder | config | key");
|
||||
#endif
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
strcpy(reply,
|
||||
@@ -220,10 +226,6 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
}
|
||||
const char* hw = (c.hw_id[0]) ? c.hw_id : "?";
|
||||
const char* tenv = ota_target_env_name(c.manager.target()); // env name, or "?" if not in the table
|
||||
int n = snprintf(reply, 160, "OTA | this fw %s (%uK) hw=%s | %s | serving:%s (%u) | keys:%u | target:%08X (%s)",
|
||||
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw, dl,
|
||||
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
||||
(unsigned)c.allow.count(), (unsigned)c.manager.target(), tenv ? tenv : "?");
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise.
|
||||
// blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c).
|
||||
@@ -238,12 +240,23 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
const char* bl_state = bl.present ? "apply" : "NONE";
|
||||
#endif
|
||||
const uint8_t last_rc = ota_bootloader_last_rc();
|
||||
if (n < 146) {
|
||||
if (ota_nrf52_boot_update_result(last_rc))
|
||||
n += snprintf(reply + n, 160 - n, " | bl:%s blup:%02X", bl_state, last_rc);
|
||||
else
|
||||
n += snprintf(reply + n, 160 - n, " | bl:%s blrc:%02X", bl_state, last_rc);
|
||||
}
|
||||
const char* rc_name = ota_nrf52_boot_update_result(last_rc) ? "blup" : "blrc";
|
||||
// Keep the fixed-width parser/diagnostic fields ahead of the variable download text and optional
|
||||
// human target name. hw is bounded to 32 bytes, so target + the complete blup/blrc token always fit
|
||||
// in the 160-byte reply; snprintf may truncate only the descriptive tail.
|
||||
snprintf(reply, 160,
|
||||
"OTA | this fw %s (%uK) hw=%s | target:%08X | bl:%s %s:%02X | %s | "
|
||||
"serving:%s (%u) | keys:%u | env:%s",
|
||||
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw,
|
||||
(unsigned)c.manager.target(), bl_state, rc_name, last_rc, dl,
|
||||
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
||||
(unsigned)c.allow.count(), tenv ? tenv : "?");
|
||||
#else
|
||||
snprintf(reply, 160,
|
||||
"OTA | this fw %s (%uK) hw=%s | %s | serving:%s (%u) | keys:%u | target:%08X (%s)",
|
||||
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw, dl,
|
||||
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
||||
(unsigned)c.allow.count(), (unsigned)c.manager.target(), tenv ? tenv : "?");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -334,21 +347,20 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
uint32_t age = (now - h->last_ms) / 1000; if (age > 99999) age = 99999;
|
||||
char ver[20]; ver_str(ver, sizeof ver, h->fw_version);
|
||||
// Target equality alone is not an install-safety claim. Surface local codec/bootloader limitations,
|
||||
// and flag the explicit rescue path when this internal-flash nRF52 has no valid running EndF.
|
||||
// and flag the explicit legacy rescue path when an app-only internal-flash nRF52 has no valid EndF.
|
||||
char hwbuf[16];
|
||||
const char* fit;
|
||||
const char* env = ota_target_env_name(h->target_id);
|
||||
const bool boot_package = (h->flags & MFLAG_BOOTLOADER) != 0;
|
||||
if (boot_package) {
|
||||
bool installable = false;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) && defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
||||
installable = h->flags == (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) &&
|
||||
h->codec == CODEC_FULL && bid.present && bid.crc_ok &&
|
||||
h->target_id == bid.board_id && list_bl.present &&
|
||||
list_bl.apply_abi >= MOTA_BOOT_FORMAT_VER &&
|
||||
(list_bl.storage_flags & (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) ==
|
||||
(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE);
|
||||
h->target_id == ota_bootloader_target_id(bid) &&
|
||||
ota_bootloader_self_update_caps_valid(list_bl);
|
||||
#endif
|
||||
fit = installable ? "yours" : "bootloader unsupported";
|
||||
} else if (myt && h->target_id == myt) {
|
||||
@@ -363,7 +375,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#endif
|
||||
#endif
|
||||
if (!installable) fit = "unsupported";
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
else if (!list_has_endf) fit = "no EndF; local recovery";
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
else if (!list_has_endf) fit = "rescue";
|
||||
#endif
|
||||
@@ -381,6 +395,12 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
|
||||
// ---- start fetching a specific catalogued mOTA (by list index or manifest_id) ----
|
||||
} else if (is_cmd(a, "pull|get|download", &rest)) {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE) && !defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
const char* pull_usage = "usage: ota pull <id> flash [rescue] | folder [validate] (see `ota ls`)";
|
||||
#else
|
||||
const char* pull_usage = "usage: ota pull <id> flash | folder [validate] (see `ota ls`)";
|
||||
#endif
|
||||
const char* p = rest; while (*p == ' ') p++;
|
||||
// split into "<selector> [destination]": selector = #N / N (catalogue index) or mid hex; destination =
|
||||
// flash | folder (MANDATORY - `folder` captures the .mota onto the connected motatool folder as <mid>.mota).
|
||||
@@ -388,7 +408,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
while (p[i] && p[i] != ' ' && i < (int)sizeof(selstr) - 1) { selstr[i] = p[i]; i++; }
|
||||
selstr[i] = 0;
|
||||
const char* dst = p + i; while (*dst == ' ') dst++;
|
||||
if (selstr[0] == 0) { strcpy(reply, "usage: ota pull <id> flash [rescue] | folder [validate] (see `ota ls`)"); return true; }
|
||||
if (selstr[0] == 0) { strcpy(reply, pull_usage); return true; }
|
||||
// resolve the catalogue row (index or explicit manifest_id)
|
||||
const OtaManager::CatRow* sel = nullptr; uint8_t mid[4];
|
||||
// An eight-digit all-numeric manifest ID is still an ID, not a huge list index. Bare short decimal
|
||||
@@ -435,7 +455,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
char destination[8] = {0}, option[10] = {0}, extra[2] = {0};
|
||||
int parts = sscanf(dst, "%7s %9s %1s", destination, option, extra);
|
||||
if (parts < 1 || parts > 2) {
|
||||
strcpy(reply, "ERR usage: ota pull <id> flash [rescue] | folder [validate]");
|
||||
snprintf(reply, 160, "ERR %s", pull_usage);
|
||||
return true;
|
||||
}
|
||||
bool to_flash = strcmp(destination, "flash") == 0;
|
||||
@@ -443,7 +463,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
bool validate = to_folder && parts == 2 && strcmp(option, "validate") == 0;
|
||||
bool rescue = to_flash && parts == 2 && strcmp(option, "rescue") == 0;
|
||||
if ((!to_flash && !to_folder) || (parts == 2 && !validate && !rescue)) {
|
||||
strcpy(reply, "ERR usage: ota pull <id> flash [rescue] | folder [validate]");
|
||||
snprintf(reply, 160, "ERR %s", pull_usage);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -468,7 +488,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
return true;
|
||||
#else
|
||||
if (selboot) {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) && defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) ||
|
||||
@@ -476,13 +497,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
strcpy(reply, "ERR malformed bootloader catalog row; capture it to folder for inspection");
|
||||
return true;
|
||||
}
|
||||
if (!bid.present || !bid.crc_ok || seltgt != bid.board_id) {
|
||||
strcpy(reply, "ERR bootloader package does not match this installed XIAO bootloader");
|
||||
if (!bid.present || !bid.crc_ok || seltgt != ota_bootloader_target_id(bid)) {
|
||||
strcpy(reply, "ERR bootloader package does not match this installed bootloader identity");
|
||||
return true;
|
||||
}
|
||||
if (!bl.present || bl.apply_abi < MOTA_BOOT_FORMAT_VER ||
|
||||
(bl.storage_flags & (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) !=
|
||||
(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE)) {
|
||||
if (!ota_bootloader_self_update_caps_valid(bl)) {
|
||||
strcpy(reply, "ERR installed bootloader lacks safe LoRa bootloader-update support");
|
||||
return true;
|
||||
}
|
||||
@@ -524,6 +543,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
!defined(OTA_QSPI_STORE)
|
||||
SelfFwInfo self;
|
||||
bool has_endf = ota_self_firmware(self) && self.valid;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
if (!has_endf) {
|
||||
strcpy(reply, "ERR no EndF; shared-slot builds refuse internal pulls; recover via USB/BLE DFU or SWD");
|
||||
return true;
|
||||
}
|
||||
if (rescue) {
|
||||
strcpy(reply, "ERR rescue is disabled on shared-slot bootloader-update builds; use `flash`");
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
if (!has_endf && !rescue) {
|
||||
snprintf(reply, 160,
|
||||
"ERR no EndF; retry `ota pull %s flash rescue`, then use `ota rescue install <hash16>`",
|
||||
@@ -538,6 +567,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
strcpy(reply, "ERR rescue is only for a running firmware with no valid EndF; use `flash`");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
if (rescue) {
|
||||
strcpy(reply, "ERR rescue is available only on internal-flash nRF52 builds");
|
||||
@@ -656,7 +686,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// the exact 8-byte base hash carried by the already-fetched package. The bootloader independently
|
||||
// hashes the running app and refuses a mismatch before writing any application flash.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE) && !defined(OTA_SEEDER_ONLY)
|
||||
!defined(OTA_QSPI_STORE) && !defined(OTA_SEEDER_ONLY) && \
|
||||
!defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
if (c.fetch_to_folder) {
|
||||
strcpy(reply, "ERR the complete update was captured to a folder, not staged for install; use `ota cancel`");
|
||||
return true;
|
||||
@@ -677,13 +708,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
char m2[100];
|
||||
bool ok = c.apply_fetched_rescue(operator_base_hash, m2);
|
||||
sprintf(reply, "%s | %s", ok ? "OK" : "ERR", m2);
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
strcpy(reply, "ERR rescue is disabled on shared-slot bootloader-update builds; use local DFU/SWD if EndF is invalid");
|
||||
#else
|
||||
strcpy(reply, "ERR rescue requires an internal-flash nRF52 LoRa OTA build");
|
||||
#endif
|
||||
|
||||
} else if (is_cmd(a, "bootloader|blupdate", &rest)) {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) && \
|
||||
defined(OTA_QSPI_BOOTLOADER_UPDATE) && !defined(OTA_SEEDER_ONLY)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
if (*rest == 0 || strcmp(rest, "status") == 0) {
|
||||
@@ -699,8 +732,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
||||
mesh::Utils::toHex(hashhx, staged.image_hash, 8);
|
||||
}
|
||||
snprintf(reply, 160, "BL board=%08X name=%s crc=%08X abi=%u caps=%02X | staged:%s mid=%s hash=%s",
|
||||
(unsigned)bid.board_id, bid.device_name, (unsigned)bid.crc32,
|
||||
snprintf(reply, 160, "BL board=%08X target=%08X name=%s crc=%08X abi=%u caps=%02X | staged:%s mid=%s hash=%s",
|
||||
(unsigned)bid.board_id, (unsigned)ota_bootloader_target_id(bid),
|
||||
bid.device_name, (unsigned)bid.crc32,
|
||||
bl.apply_abi, bl.storage_flags, ready ? "ready" : "none", midhx, hashhx);
|
||||
return true;
|
||||
}
|
||||
@@ -720,7 +754,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (c.apply_pending) { strcpy(reply, "ERR another update is already armed"); return true; }
|
||||
if (c.fetch_to_folder || c.manager.fetchState() != OtaManager::COMPLETE ||
|
||||
c.fetch_store.staged_size() == 0 || !c.manager.fetched_is_bootloader()) {
|
||||
strcpy(reply, "ERR no complete bootloader package in local QSPI storage"); return true;
|
||||
strcpy(reply, "ERR no complete bootloader package in local install storage"); return true;
|
||||
}
|
||||
char m2[100];
|
||||
const bool ok = c.apply_fetched_bootloader(operator_mid, operator_hash8, m2);
|
||||
|
||||
@@ -151,7 +151,14 @@ struct OtaContext {
|
||||
// Explicit recovery entry point for an internal-flash nRF52 whose normal EndF validation fails. This
|
||||
// is never called by auto-install; the CLI requires the package's exact 8-byte base hash.
|
||||
bool apply_fetched_rescue(const uint8_t operator_base_hash[8], char* msg) {
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
(void)operator_base_hash;
|
||||
strncpy(msg, "rescue is disabled on shared-slot bootloader-update builds", 96);
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
#else
|
||||
return apply_fetched_impl(operator_base_hash, msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool apply_fetched_impl(const uint8_t* rescue_base_hash, char* msg) {
|
||||
@@ -172,6 +179,13 @@ struct OtaContext {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
if (rescue_base_hash) {
|
||||
strncpy(msg, "rescue is disabled on shared-slot bootloader-update builds", 96);
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
if (sdCacheFetching()) {
|
||||
strncpy(msg, "refused: SD archive capture owns the OTA receive slot", 96);
|
||||
@@ -223,10 +237,10 @@ struct OtaContext {
|
||||
|
||||
bool apply_fetched_bootloader(const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8], char* msg) {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) && \
|
||||
defined(OTA_QSPI_BOOTLOADER_UPDATE) && !defined(OTA_SEEDER_ONLY)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
if (fetch_to_folder) {
|
||||
strncpy(msg, "refused: bootloader package is not in local QSPI storage", 96);
|
||||
strncpy(msg, "refused: bootloader package is not in local install storage", 96);
|
||||
msg[95] = 0; return false;
|
||||
}
|
||||
if (manager.fetchState() != OtaManager::COMPLETE || fetch_store.staged_size() == 0) {
|
||||
@@ -503,7 +517,8 @@ struct OtaContext {
|
||||
manager.set_apply_codec(CODEC_DETOOLS_SEQUENTIAL); // preferred (streams straight to the slot)
|
||||
manager.set_apply_codec2(CODEC_DETOOLS_INPLACE); // also accepted -> a single in-place .mota fits both
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE) && defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
manager.set_accept_bootloader(true);
|
||||
#else
|
||||
manager.set_accept_bootloader(false);
|
||||
|
||||
@@ -33,6 +33,14 @@ static const uint32_t MOTA_NRF52_EXTRAFS_START = 0x000D4000u;
|
||||
static const uint32_t MOTA_NRF52_BOOT_SCRATCH_START = 0x000E0000u;
|
||||
static const uint32_t MOTA_NRF52_BOOT_SCRATCH_END = 0x000EA000u;
|
||||
static const uint32_t MOTA_NRF52_APP_END = 0x000ED000u;
|
||||
// Internal-only targets do not reserve a second scratch bank. Their exact
|
||||
// 41,330-byte v3 package shares the normal bottom-aligned internal OTA store.
|
||||
// Under the 0xED000 filesystem ceiling it occupies eleven pages beginning at
|
||||
// 0xE2000; OTAFIX compacts the payload forward within this same slot before
|
||||
// handing that page-aligned source to the MBR. Admission requires a trustworthy
|
||||
// live EndF proving the running application ends at or below this address.
|
||||
static const uint32_t MOTA_NRF52_BOOT_CONTAINER_SIZE = 41330u;
|
||||
static const uint32_t MOTA_NRF52_SHARED_BOOT_STAGE_START = 0x000E2000u;
|
||||
static const uint32_t MOTA_NRF52_STAGE_CEILING_LEGACY = MOTA_NRF52_EXTRAFS_START;
|
||||
static const uint32_t MOTA_NRF52_STAGE_CEILING_EXPANDED = MOTA_NRF52_APP_END;
|
||||
// Compatibility name for code that refers to the fixed lower boundary. New staging code must use an
|
||||
@@ -45,6 +53,12 @@ static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u;
|
||||
|
||||
inline uint8_t mota_nrf52_flash_stage_handoff(uint32_t effective_stage_ceiling) {
|
||||
if (effective_stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED)
|
||||
return GPREGRET2_OTA_STAGE_EXPANDED;
|
||||
return GPREGRET2_OTA_STAGE_LEGACY;
|
||||
}
|
||||
|
||||
// Firmware without a valid EndF and older host tooling fall back to this conservative apply workspace.
|
||||
// New motatool builds read the firmware's appended layout record and derive memory_size from the actual
|
||||
// staged-container start; the app and bootloader both validate that per-patch value before writing.
|
||||
@@ -72,6 +86,13 @@ static_assert((MOTA_NRF52_STAGE_CEILING_LEGACY % MOTA_NRF52_FLASH_PAGE) == 0,
|
||||
"legacy staging ceiling must be page-aligned");
|
||||
static_assert((MOTA_NRF52_STAGE_CEILING_EXPANDED % MOTA_NRF52_FLASH_PAGE) == 0,
|
||||
"expanded staging ceiling must be page-aligned");
|
||||
static_assert((MOTA_NRF52_SHARED_BOOT_STAGE_START % MOTA_NRF52_FLASH_PAGE) == 0,
|
||||
"shared bootloader stage start must be page-aligned");
|
||||
static_assert(((MOTA_NRF52_APP_END - MOTA_NRF52_BOOT_CONTAINER_SIZE) &
|
||||
~(MOTA_NRF52_FLASH_PAGE - 1u)) == MOTA_NRF52_SHARED_BOOT_STAGE_START,
|
||||
"exact bootloader container must bottom-align at E2000");
|
||||
static_assert(MOTA_NRF52_APP_END - MOTA_NRF52_SHARED_BOOT_STAGE_START == 0xB000u,
|
||||
"shared bootloader slot must be exactly eleven pages");
|
||||
static_assert(MOTA_NRF52_STAGE_CEILING_LEGACY < MOTA_NRF52_STAGE_CEILING_EXPANDED,
|
||||
"legacy staging ceiling must precede expanded ceiling");
|
||||
static_assert(MOTA_NRF52_BOOT_SCRATCH_END - MOTA_NRF52_BOOT_SCRATCH_START == 0xA000u,
|
||||
@@ -150,6 +171,61 @@ inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base, uint32_t app_end,
|
||||
return first_free_page <= stage_ceiling ? stage_ceiling - first_free_page : 0;
|
||||
}
|
||||
|
||||
// Choose the erase-protection floor used while receiving/reopening an internal
|
||||
// flash container. A valid EndF provides the exact live-image extent. Legacy
|
||||
// app-only builds may opt into the conservative rescue fallback; a shared
|
||||
// internal bootloader-update build passes refuse_without_endf=true for every
|
||||
// package kind because its normal app linker may extend through 0xED000.
|
||||
inline bool mota_nrf52_protected_app_end(uint32_t app_base,
|
||||
uint32_t stage_ceiling,
|
||||
bool firmware_info_valid,
|
||||
uint32_t firmware_image_size,
|
||||
bool refuse_without_endf,
|
||||
uint32_t& out_app_end) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling)) return false;
|
||||
if (firmware_info_valid) {
|
||||
if (firmware_image_size > UINT32_MAX - app_base) return false;
|
||||
out_app_end = app_base + firmware_image_size;
|
||||
} else {
|
||||
if (refuse_without_endf) return false;
|
||||
if (MOTA_NRF52_FALLBACK_INPLACE_MEMORY > UINT32_MAX - app_base) return false;
|
||||
out_app_end = app_base + MOTA_NRF52_FALLBACK_INPLACE_MEMORY;
|
||||
}
|
||||
return out_app_end >= app_base && out_app_end <= stage_ceiling;
|
||||
}
|
||||
|
||||
// Bound a staged header's untrusted total before any caller uses that value as
|
||||
// a memory-mapped parse length. Subtraction is safe only after start<=ceiling.
|
||||
inline bool mota_nrf52_container_span_valid(uint32_t start,
|
||||
uint32_t stage_ceiling,
|
||||
uint32_t total_size,
|
||||
uint32_t minimum_size) {
|
||||
return start <= stage_ceiling && total_size >= minimum_size &&
|
||||
total_size <= stage_ceiling - start;
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_target_image_fits(uint32_t app_base, uint32_t image_size,
|
||||
uint32_t application_ceiling) {
|
||||
return image_size != 0 && app_base < application_ceiling &&
|
||||
image_size <= application_ceiling - app_base;
|
||||
}
|
||||
|
||||
// Internal in-place patches use whichever address the bottom-aligned container
|
||||
// actually starts at as their destructive workspace ceiling. A normal delta
|
||||
// may be larger than the boot package's eleven-page shared slot and start
|
||||
// below E2000; that remains safe when both the encoded detools memory and the
|
||||
// reconstructed application stop below their independent limits.
|
||||
inline bool mota_nrf52_internal_patch_workspace_valid(uint32_t memory_size,
|
||||
uint32_t app_base,
|
||||
uint32_t mota_start,
|
||||
uint32_t target_image_size,
|
||||
uint32_t application_ceiling) {
|
||||
return memory_size != 0 && mota_start >= app_base &&
|
||||
memory_size <= mota_start - app_base &&
|
||||
mota_nrf52_target_image_fits(app_base, target_image_size,
|
||||
application_ceiling);
|
||||
}
|
||||
|
||||
// Plan where to stage a received `.mota` of `total_size` bytes. It is placed bottom-aligned within the
|
||||
// highest flash page below stage_ceiling (the bootloader scans downward from there), and it must sit
|
||||
// ENTIRELY above the running image and below the filesystem region (ExtraFS/InternalFS - where user prefs
|
||||
@@ -170,6 +246,25 @@ inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32
|
||||
return true;
|
||||
}
|
||||
|
||||
// Privileged shared-slot admission. Unlike ordinary rescue staging, this
|
||||
// requires the caller to have a trustworthy live EndF (`app_end` is exact),
|
||||
// accepts only the fixed v3 package size, and proves the dynamic slot is wholly
|
||||
// above the running image before any erase/write begins.
|
||||
inline bool mota_nrf52_shared_boot_stage_plan(uint32_t total_size,
|
||||
uint32_t app_base,
|
||||
bool firmware_info_valid,
|
||||
uint32_t app_end,
|
||||
uint32_t& out_start) {
|
||||
uint32_t start;
|
||||
if (!firmware_info_valid || total_size != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
|
||||
!mota_nrf52_stage_plan(total_size, app_base, app_end,
|
||||
MOTA_NRF52_APP_END, start) ||
|
||||
start != MOTA_NRF52_SHARED_BOOT_STAGE_START)
|
||||
return false;
|
||||
out_start = start;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32_t app_end,
|
||||
uint32_t& out_start) {
|
||||
return mota_nrf52_stage_plan(total_size, app_base, app_end,
|
||||
|
||||
@@ -923,9 +923,8 @@ bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uin
|
||||
if (_fstate == COMPLETE && memcmp(mid, _fid, 4) == 0) return false; // already have it
|
||||
if (flags & ~MFLAG_KNOWN) return false;
|
||||
const bool bootloader = (flags & MFLAG_BOOTLOADER) != 0;
|
||||
if (!_archive_fetch && bootloader &&
|
||||
(!_accept_bootloader || !_have_desired_mid || codec != CODEC_FULL)) return false;
|
||||
if (!_archive_fetch && !codecOk(codec)) return false; // can't apply this codec
|
||||
if (!_archive_fetch && !fetchCodecOk(codec, bootloader, _have_desired_mid))
|
||||
return false; // can't or must not apply this class
|
||||
if (_have_desired_mid) // manual pull of a specific mid
|
||||
return memcmp(mid, _desired_mid, 4) == 0 && (_desired_target == 0 || target == _desired_target);
|
||||
if (_desired_target) return target == _desired_target; // cross-target want (role switch)
|
||||
@@ -1176,11 +1175,10 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
MotaManifest parsed;
|
||||
if (!mota_parse_manifest(mf, mfl, parsed)) { failFetch(FETCH_ERROR_MANIFEST); return; }
|
||||
if (parsed.hash_algo != HASH_ALGO_SHA256) { failFetch(FETCH_ERROR_HASH_ALGO); return; }
|
||||
if (!_archive_fetch && parsed.is_bootloader() &&
|
||||
(!_accept_bootloader || !_have_desired_mid)) {
|
||||
if (!_archive_fetch &&
|
||||
!fetchCodecOk(parsed.codec_id, parsed.is_bootloader(), _have_desired_mid)) {
|
||||
failFetch(FETCH_ERROR_CODEC); return; // bootloader packages are manual-pull only
|
||||
}
|
||||
if (!_archive_fetch && !codecOk(parsed.codec_id)) { failFetch(FETCH_ERROR_CODEC); return; }
|
||||
uint32_t payload_size = parsed.payload_size;
|
||||
uint8_t bsl = parsed.block_size_log2;
|
||||
if (bsl >= 32) { failFetch(FETCH_ERROR_GEOMETRY); return; }
|
||||
@@ -1204,7 +1202,8 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
// placement and refuse an unfittable fetch up front: a FULL payload streams to the inactive slot,
|
||||
// a delta's whole container is staged together. (image_size at mf+11, is_full from flags at mf+1.)
|
||||
bool is_full = parsed.is_full();
|
||||
if (!_fetch->plan_layout(is_full, parsed.image_size, payload_off, payload_size)) {
|
||||
if (!_fetch->plan_layout(is_full, parsed.image_size, payload_off, payload_size,
|
||||
parsed.is_bootloader())) {
|
||||
failFetch(FETCH_ERROR_STORAGE); return;
|
||||
}
|
||||
if (!_fetch->begin(total)) { failFetch(FETCH_ERROR_STORAGE); return; }
|
||||
@@ -1326,8 +1325,10 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
|
||||
if (!_fetch->read(8, mbuf, mread) || !mota_parse_manifest(mbuf, mread, m)) return false;
|
||||
if (want_mid && memcmp(m.merkle_root, want_mid, 4) != 0) return false; // a different fw is staged
|
||||
if (m.hash_algo != HASH_ALGO_SHA256) return false;
|
||||
if (!_archive_fetch && m.is_bootloader() && !_accept_bootloader) return false;
|
||||
if (!_archive_fetch && !codecOk(m.codec_id)) return false;
|
||||
// A bootloader partial is resumed only as part of the same explicit MID pull.
|
||||
// Boot-time resumeStaged(nullptr) must not silently re-adopt privileged data.
|
||||
const bool manual = want_mid != nullptr && _have_desired_mid;
|
||||
if (!_archive_fetch && !fetchCodecOk(m.codec_id, m.is_bootloader(), manual)) return false;
|
||||
uint32_t mfl = (uint32_t)(m.approval - m.manifest_start) + 4; // manifest-minus-leaves length
|
||||
uint32_t bs = m.block_size();
|
||||
if (bs == 0 || bs > OTA_MAX_BLOCK) return false;
|
||||
|
||||
@@ -364,6 +364,13 @@ public:
|
||||
bool codecOk(uint8_t c) const {
|
||||
return (c == CODEC_FULL && _accept_full) || c == _apply_codec || c == _apply_codec2;
|
||||
}
|
||||
// Bootloader FULL images are a separate, privileged install class. They are
|
||||
// admitted only for an explicit MID pull on a self-update-capable build;
|
||||
// accepting one must never turn ordinary application FULL images on for an
|
||||
// nRF52 single-slot target.
|
||||
bool fetchCodecOk(uint8_t c, bool bootloader, bool manual) const {
|
||||
return bootloader ? (_accept_bootloader && manual && c == CODEC_FULL) : codecOk(c);
|
||||
}
|
||||
|
||||
// Auto-fetch policy (manual `ota pull` always works regardless): 0=off (discover only), 1=any
|
||||
// compatible own-target advert, 2=only signed adverts. Conservative default = off.
|
||||
|
||||
@@ -57,8 +57,10 @@ public:
|
||||
// whole container is staged together (the decoder reads the patch from it at apply). image_size is the
|
||||
// reconstructed image; [payload_off, payload_off+payload_size) is the payload region in the container.
|
||||
// Return false if it cannot fit the backing store (the transfer is then refused before any block).
|
||||
virtual bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) {
|
||||
(void)is_full; (void)image_size; (void)payload_off; (void)payload_size; return true;
|
||||
virtual bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off,
|
||||
uint32_t payload_size, bool is_bootloader) {
|
||||
(void)is_full; (void)image_size; (void)payload_off; (void)payload_size;
|
||||
(void)is_bootloader; return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ bool OtaStoreFlashEsp32::layout() {
|
||||
|
||||
// Choose placement from the parsed manifest and refuse anything that won't fit, BEFORE any block is
|
||||
// staged. (See the header for the delta-vs-full layout rationale.)
|
||||
bool OtaStoreFlashEsp32::plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) {
|
||||
bool OtaStoreFlashEsp32::plan_layout(bool is_full, uint32_t image_size,
|
||||
uint32_t payload_off, uint32_t payload_size,
|
||||
bool is_bootloader) {
|
||||
if (is_bootloader) return false;
|
||||
if (!acquire()) return false;
|
||||
_full = is_full; _image_size = image_size; _meta_bytes = payload_off; _pay_size = payload_size;
|
||||
bool ok = layout();
|
||||
|
||||
@@ -84,7 +84,8 @@ class OtaStoreFlashEsp32 : public OtaStore {
|
||||
|
||||
public:
|
||||
~OtaStoreFlashEsp32() override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off,
|
||||
uint32_t payload_size, bool is_bootloader) override;
|
||||
bool begin(uint32_t total_size) override;
|
||||
bool write(uint32_t offset, const uint8_t* data, uint32_t len) override;
|
||||
bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "OtaSelf.h"
|
||||
#include "OtaBlInfo.h"
|
||||
#include "MotaContainer.h"
|
||||
#include "OtaDebug.h"
|
||||
#include "OtaByteIO.h" // align_down / rd_u32le (flash-page geometry + header read)
|
||||
#include <string.h>
|
||||
@@ -12,14 +13,55 @@
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// A valid EndF gives the exact live-image extent. Rescue/bring-up builds without one retain the old
|
||||
// 608 KiB protection floor so staging cannot begin in the middle of a normally sized application.
|
||||
// A valid EndF gives the exact live-image extent. Legacy app-only builds keep
|
||||
// the conservative rescue fallback. Shared internal bootloader-update builds
|
||||
// reject every package kind without EndF because their normal linker may place
|
||||
// live application bytes anywhere through the 0xED000 stage ceiling.
|
||||
static bool protected_app_end(uint32_t app_base, uint32_t stage_ceiling, uint32_t& app_end) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling)) return false;
|
||||
app_end = app_base + MOTA_NRF52_FALLBACK_INPLACE_MEMORY;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) app_end = app_base + fi.image_len;
|
||||
return app_end >= app_base && app_end <= stage_ceiling;
|
||||
const bool valid = ota_self_firmware(fi) && fi.valid;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
const bool refuse_without_endf = true;
|
||||
#else
|
||||
const bool refuse_without_endf = false;
|
||||
#endif
|
||||
return mota_nrf52_protected_app_end(
|
||||
app_base, stage_ceiling, valid, valid ? fi.image_len : 0u,
|
||||
refuse_without_endf, app_end);
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size,
|
||||
uint32_t payload_off, uint32_t payload_size,
|
||||
bool is_bootloader) {
|
||||
_planned_bootloader = false;
|
||||
_planned_total = 0;
|
||||
_planned_start = 0;
|
||||
if (!is_bootloader) return true;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
const uint64_t total64 = (uint64_t)payload_off + payload_size + 5u;
|
||||
if (!is_full || image_size != 40u * 1024u || payload_size != 40u * 1024u ||
|
||||
payload_off != 365u || total64 != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
|
||||
!ota_bootloader_self_update_caps_valid(ota_bootloader_caps()))
|
||||
return false;
|
||||
|
||||
// This is the release-blocking no-EndF gate: package kind is known before
|
||||
// begin(), and no page may be erased until the exact live app extent proves
|
||||
// the shared E2000..ED000 slot is free.
|
||||
SelfFwInfo fi;
|
||||
uint32_t start;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
|
||||
!mota_nrf52_shared_boot_stage_plan(
|
||||
(uint32_t)total64, app_base, true, app_base + fi.image_len, start))
|
||||
return false;
|
||||
_planned_bootloader = true;
|
||||
_planned_total = (uint32_t)total64;
|
||||
_planned_start = start;
|
||||
return true;
|
||||
#else
|
||||
(void)is_full; (void)image_size; (void)payload_off; (void)payload_size;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Write one whole 4 KB page from `buf` to flash (erase + program, ~85 ms). `buf` is PG bytes, 0xFF-padded
|
||||
@@ -88,18 +130,38 @@ uint8_t* OtaStoreFlashNrf52::write_slot(uint32_t pos) {
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
const bool planned_bootloader = _planned_bootloader;
|
||||
const uint32_t planned_total = _planned_total;
|
||||
const uint32_t planned_start = _planned_start;
|
||||
clear();
|
||||
|
||||
// never collide with the running application image (its extent comes from its EndF trailer)
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
uint32_t app_end;
|
||||
if (!protected_app_end(app_base, stage_ceiling, app_end)) return false;
|
||||
if (planned_bootloader) {
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
SelfFwInfo fi;
|
||||
uint32_t checked_start;
|
||||
if (stage_ceiling != MOTA_NRF52_APP_END || total_size != planned_total ||
|
||||
!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
|
||||
!mota_nrf52_shared_boot_stage_plan(
|
||||
total_size, app_base, true, app_base + fi.image_len, checked_start) ||
|
||||
checked_start != planned_start)
|
||||
return false;
|
||||
app_end = app_base + fi.image_len;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
} else if (!protected_app_end(app_base, stage_ceiling, app_end)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bottom-align below the selected ceiling and reject unless it sits above the running image. The
|
||||
// approval path later verifies the patch's detools workspace ends at/below this exact start.
|
||||
uint32_t start;
|
||||
if (!mota_nrf52_stage_plan(total_size, app_base, app_end, stage_ceiling, start)) return false;
|
||||
if (planned_bootloader && start != planned_start) return false;
|
||||
|
||||
_write_start = start;
|
||||
_stage_ceiling = stage_ceiling;
|
||||
@@ -109,6 +171,9 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
_pay_idx = 0;
|
||||
_flushed = false;
|
||||
_io_ok = true;
|
||||
_planned_bootloader = planned_bootloader;
|
||||
_planned_total = planned_total;
|
||||
_planned_start = planned_start;
|
||||
OTA_DBG("OTA flash: begin total=%u start=%08x app_end=%08x ceiling=%08x\n",
|
||||
(unsigned)total_size, (unsigned)start, (unsigned)app_end, (unsigned)stage_ceiling);
|
||||
return true; // no pre-erase: each page is erased by its own (single) flush
|
||||
@@ -160,6 +225,26 @@ bool OtaStoreFlashNrf52::finalize() {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
bool OtaStoreFlashNrf52::approve_for_bootloader() {
|
||||
if (!finalize() || _total < 8u + MOTA_MFL + 5u) return false;
|
||||
const uint32_t offset = 8u + MOTA_OFF_APPROVAL;
|
||||
if (offset + sizeof(APPROVAL_YES) > _total) return false;
|
||||
const uint32_t address = _write_start + offset;
|
||||
if (flash_nrf5x_write(address, APPROVAL_YES, sizeof(APPROVAL_YES)) < 0) {
|
||||
_io_ok = false;
|
||||
return false;
|
||||
}
|
||||
flash_nrf5x_flush();
|
||||
if (!flash_matches(address, APPROVAL_YES, sizeof(APPROVAL_YES))) {
|
||||
_io_ok = false;
|
||||
return false;
|
||||
}
|
||||
memcpy(_meta_page + offset, APPROVAL_YES, sizeof(APPROVAL_YES));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Persist mid-transfer progress so a reboot can resume. Order matters for consistency: flush the open
|
||||
// payload page FIRST, then page 0 (the leaf-progress markers) -- so every block whose leaf is now in flash
|
||||
// also has its payload in flash. Infrequent (every OTA_CHECKPOINT_BLOCKS blocks), so the 2 extra page
|
||||
@@ -184,8 +269,27 @@ bool OtaStoreFlashNrf52::reopen() {
|
||||
const uint8_t* p = (const uint8_t*)(uintptr_t)start;
|
||||
if (memcmp(p, MOTA_MAGIC, 4) != 0) continue;
|
||||
uint32_t total = rd_u32le(p + 4);
|
||||
if (!mota_nrf52_container_span_valid(
|
||||
start, stage_ceiling, total, 8u + MOTA_MFL + 5u))
|
||||
continue;
|
||||
uint32_t want; // must be valid + placed exactly where begin() would have staged it (same bounds fn)
|
||||
if (!mota_nrf52_stage_plan(total, app_base, app_end, stage_ceiling, want) || want != start) continue;
|
||||
MotaManifest manifest;
|
||||
const bool manifest_ok = mota_parse_manifest(p + 8u, MOTA_MFL, manifest);
|
||||
const bool bootloader = manifest_ok && manifest.is_bootloader();
|
||||
if (bootloader) {
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
|
||||
!mota_nrf52_shared_boot_stage_plan(
|
||||
total, app_base, true, app_base + fi.image_len, want) || want != start)
|
||||
continue;
|
||||
#else
|
||||
continue;
|
||||
#endif
|
||||
} else if (!mota_nrf52_stage_plan(
|
||||
total, app_base, app_end, stage_ceiling, want) || want != start) {
|
||||
continue;
|
||||
}
|
||||
_write_start = start;
|
||||
_stage_ceiling = stage_ceiling;
|
||||
_total = total;
|
||||
@@ -194,6 +298,9 @@ bool OtaStoreFlashNrf52::reopen() {
|
||||
_pay_idx = 0;
|
||||
_flushed = false;
|
||||
_io_ok = true;
|
||||
_planned_bootloader = bootloader;
|
||||
_planned_total = bootloader ? total : 0u;
|
||||
_planned_start = bootloader ? start : 0u;
|
||||
OTA_DBG("OTA flash: reopen total=%u start=%08x ceiling=%08x\n",
|
||||
(unsigned)total, (unsigned)start, (unsigned)stage_ceiling);
|
||||
return true;
|
||||
|
||||
@@ -40,6 +40,9 @@ class OtaStoreFlashNrf52 : public OtaStore {
|
||||
uint32_t _total = 0; // staged container size (0 = none)
|
||||
bool _flushed = false; // finalize() committed everything to flash
|
||||
bool _io_ok = true; // cleared on a failed/out-of-bounds flash write or readback mismatch
|
||||
bool _planned_bootloader = false; // manifest-kind decision made before begin()/any erase
|
||||
uint32_t _planned_total = 0;
|
||||
uint32_t _planned_start = 0;
|
||||
|
||||
uint8_t _meta_page[PG]; // pinned flash page 0 (header + manifest + leaves + 1st payload)
|
||||
uint8_t _pay_page[PG]; // sliding buffer for one payload page (index _pay_idx)
|
||||
@@ -56,6 +59,8 @@ class OtaStoreFlashNrf52 : public OtaStore {
|
||||
bool flush_page(uint32_t page_idx, const uint8_t* buf); // write + verify a full page
|
||||
|
||||
public:
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off,
|
||||
uint32_t payload_size, bool is_bootloader) override;
|
||||
bool begin(uint32_t total_size) override;
|
||||
bool write(uint32_t offset, const uint8_t* data, uint32_t len) override;
|
||||
bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override;
|
||||
@@ -64,12 +69,22 @@ public:
|
||||
void clear() override {
|
||||
_stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
_total = 0; _pay_idx = 0; _flushed = false; _io_ok = true;
|
||||
_planned_bootloader = false; _planned_total = 0; _planned_start = 0;
|
||||
}
|
||||
bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes <= PG; } // leaves must fit page 0
|
||||
bool finalize() override;
|
||||
void checkpoint() override; // persist page 0 (leaves) + the open payload page so a reboot can resume
|
||||
bool reopen() override; // re-attach to a container already staged in flash (scan for it)
|
||||
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
// Clear the manifest approval word only after the privileged signed-image gates pass. OTAFIX scans
|
||||
// this page-aligned shared container after the ED handoff and repeats the structural, identity,
|
||||
// payload-integrity, in-place compaction, and readback checks before self-write. APRV carries the app's completed
|
||||
// signature/allowlist and explicit-operator authorization decision across the reboot.
|
||||
bool approve_for_bootloader();
|
||||
const char* last_error() const { return _io_ok ? "" : "internal flash write/verify failed"; }
|
||||
#endif
|
||||
|
||||
// Contiguous view (flash is memory-mapped). VALID ONLY AFTER finalize() - before that, page 0 and the
|
||||
// tail are still in RAM. OtaManager/OtaCli/verify use this only once the transfer is COMPLETE.
|
||||
const uint8_t* data() const { return (_flushed && _io_ok) ? (const uint8_t*)(uintptr_t)_write_start : nullptr; }
|
||||
|
||||
@@ -676,7 +676,8 @@ uint32_t OtaStoreQspiNrf52::capacity() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t, uint32_t payload_size) {
|
||||
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t,
|
||||
uint32_t payload_size, bool) {
|
||||
// This is the start of a new manifest admission attempt. Clear diagnostics
|
||||
// left by the normal empty-store reopen probe, then latch the first error
|
||||
// from this attempt until the operator reads it or starts another attempt.
|
||||
|
||||
@@ -159,7 +159,8 @@ public:
|
||||
bool finalize() override;
|
||||
void checkpoint() override;
|
||||
bool reopen() override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off,
|
||||
uint32_t payload_size, bool is_bootloader) override;
|
||||
|
||||
// Sets APRV only after all app-side verification gates have passed.
|
||||
bool approve_for_bootloader();
|
||||
|
||||
@@ -368,7 +368,9 @@ bool OtaStoreSdNrf52::locate_file() {
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t image_size,
|
||||
uint32_t, uint32_t payload_size) {
|
||||
uint32_t, uint32_t payload_size,
|
||||
bool is_bootloader) {
|
||||
if (is_bootloader) return false;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
|
||||
image_size > MOTA_NRF52_APP_END - app_base) {
|
||||
|
||||
@@ -33,7 +33,8 @@ public:
|
||||
void checkpoint() override;
|
||||
bool reopen() override;
|
||||
bool plan_layout(bool is_full, uint32_t image_size,
|
||||
uint32_t payload_off, uint32_t payload_size) override;
|
||||
uint32_t payload_off, uint32_t payload_size,
|
||||
bool is_bootloader) override;
|
||||
|
||||
// Called only after the app has verified payload, base, signature and trust.
|
||||
// Writes APRV into the file, then publishes the raw-sector handoff record.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.
|
||||
// 566 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// 577 OTA-capable build targets. Maps target_id (= sha2-256:4 of the target name, LE uint32)
|
||||
// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT
|
||||
// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.
|
||||
// Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID;
|
||||
@@ -31,6 +31,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xf9986af5, "GAT562_30S_Mesh_Kit_room_server" },
|
||||
{ 0x98519a1d, "GAT562_Mesh_EVB_Pro_kiss_modem" },
|
||||
{ 0x06b71719, "GAT562_Mesh_EVB_Pro_repeater" },
|
||||
{ 0x6384719d, "GAT562_Mesh_EVB_Pro_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x4c0461c8, "GAT562_Mesh_EVB_Pro_room_server" },
|
||||
{ 0x4fa88992, "GAT562_Mesh_Tracker_Pro_companion_radio_ble" },
|
||||
{ 0xcd6604d0, "GAT562_Mesh_Tracker_Pro_companion_radio_usb" },
|
||||
@@ -101,6 +102,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x2f9f8e7d, "Heltec_t114_without_display_companion_radio_usb" },
|
||||
{ 0xc9dc5d8c, "Heltec_t114_without_display_repeater" },
|
||||
{ 0x34b0e5f3, "Heltec_t114_without_display_repeater_bridge_rs232" },
|
||||
{ 0xb4d0ad97, "Heltec_t114_without_display_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0xcfa0e233, "Heltec_t114_without_display_room_server" },
|
||||
{ 0x17d5e1ff, "Heltec_T190_companion_radio_ble_" },
|
||||
{ 0xc99313c6, "Heltec_T190_companion_radio_usb_" },
|
||||
@@ -112,6 +114,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x57172fcc, "Heltec_T190_room_server_observer_mqtt" },
|
||||
{ 0xa529b4f5, "Heltec_t1_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x85a8c944, "Heltec_tower_v2_repeater" },
|
||||
{ 0x3c60586d, "Heltec_tower_v2_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x0a9dbbf0, "Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0xb519a89f, "heltec_tracker_v1_1_repeater_observer_mqtt" },
|
||||
{ 0xa3e8d8ce, "heltec_tracker_v1_1_room_server_observer_mqtt" },
|
||||
@@ -271,6 +274,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xe5331f02, "ikoka_stick_nrf_33dbm_repeater" },
|
||||
{ 0xfba7177b, "ikoka_stick_nrf_33dbm_room_server" },
|
||||
{ 0xde7dc472, "KeepteenLT1_repeater" },
|
||||
{ 0x3625bb80, "KeepteenLT1_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x69df7765, "LilyGo_T-Echo-Lite_companion_radio_ble" },
|
||||
{ 0xe9fbc9be, "LilyGo_T-Echo-Lite_kiss_modem" },
|
||||
{ 0x40818863, "LilyGo_T-Echo-Lite_non_shell_companion_radio_ble" },
|
||||
@@ -344,6 +348,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xe6bff8f9, "M5Stack_Unit_C6L_repeater" },
|
||||
{ 0xb9cd26cb, "M5Stack_Unit_C6L_room_server" },
|
||||
{ 0x9bc34bd5, "Mesh_pocket_repeater" },
|
||||
{ 0x144504b0, "Mesh_pocket_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0xdbfe2675, "Meshadventurer_sx1262_companion_radio_ble" },
|
||||
{ 0xb00241d0, "Meshadventurer_sx1262_companion_radio_usb" },
|
||||
{ 0x88e77fee, "Meshadventurer_sx1262_kiss_modem" },
|
||||
@@ -368,6 +373,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x6ebf28cb, "meshnology_w12_sensor" },
|
||||
{ 0x2d589527, "meshnology_w12_terminal_chat" },
|
||||
{ 0x77d0810c, "Minewsemi_me25ls01_repeater" },
|
||||
{ 0x11b131e7, "Minewsemi_me25ls01_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0xf8259880, "Nano_G2_Ultra_repeater" },
|
||||
{ 0x063f1f00, "nibble_screen_connect_companion_radio_ble_" },
|
||||
{ 0xa8a79da5, "nibble_screen_connect_companion_radio_usb_" },
|
||||
@@ -389,6 +395,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xbd6a1c97, "ProMicro_kiss_modem" },
|
||||
{ 0xf4d98c12, "ProMicro_repeater" },
|
||||
{ 0xccac4283, "ProMicro_repeater_bridge_rs232_serial1" },
|
||||
{ 0xa68c09e9, "ProMicro_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x5f606274, "ProMicro_room_server" },
|
||||
{ 0x84923735, "ProMicro_sensor" },
|
||||
{ 0x2c0b8b05, "ProMicro_terminal_chat" },
|
||||
@@ -396,6 +403,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xcb6555ee, "R1Neo_companion_radio_usb" },
|
||||
{ 0xd60daf25, "R1Neo_kiss_modem" },
|
||||
{ 0x410bd800, "R1Neo_repeater" },
|
||||
{ 0x9aca380e, "R1Neo_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0xd8641710, "R1Neo_room_server" },
|
||||
{ 0x4d0b1601, "R1Neo_sensor" },
|
||||
{ 0xab2ac09e, "R1Neo_terminal_chat" },
|
||||
@@ -425,6 +433,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" },
|
||||
{ 0x8a7cb79b, "RAK_WisMesh_Tag_kiss_modem" },
|
||||
{ 0x4fa4ae55, "RAK_WisMesh_Tag_repeater" },
|
||||
{ 0xd1d61a8b, "RAK_WisMesh_Tag_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x9cb0b232, "RAK_WisMesh_Tag_room_server" },
|
||||
{ 0x51fa1a49, "RAK_WisMesh_Tag_sensor" },
|
||||
{ 0xdb73638b, "SenseCap_Solar_repeater" },
|
||||
@@ -458,6 +467,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xff4102d6, "t1000e_companion_radio_usb" },
|
||||
{ 0x2f9a9185, "t1000e_kiss_modem" },
|
||||
{ 0x000fc519, "t1000e_repeater" },
|
||||
{ 0x7b071fa0, "t1000e_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x3e55e34d, "t1000e_room_server" },
|
||||
{ 0x16482630, "T_Beam_S3_Supreme_SX1262_companion_radio_ble" },
|
||||
{ 0xce0f07ad, "T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps" },
|
||||
@@ -506,6 +516,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x37928233, "ThinkNode_M3_companion_radio_usb" },
|
||||
{ 0x88ac45da, "ThinkNode_M3_kiss_modem" },
|
||||
{ 0x087f99c0, "ThinkNode_M3_repeater" },
|
||||
{ 0xdf41affe, "ThinkNode_M3_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x2afe6c63, "ThinkNode_M3_room_server" },
|
||||
{ 0xf2d61a1c, "ThinkNode_M5_companion_radio_ble" },
|
||||
{ 0x14b1b097, "ThinkNode_M5_companion_radio_ble_ps" },
|
||||
|
||||
Reference in new issue
Block a user