Add external QSPI staging for nRF52 OTA
This commit is contained in:
1 parent
c98dfb6fe1
commit
e287a1ffa2
40 files changed
+1671
-157
No files matched your search
@@ -29,6 +29,9 @@
|
||||
#if defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#endif
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
@@ -442,7 +445,9 @@ bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, Ap
|
||||
|
||||
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
|
||||
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
#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;
|
||||
#endif
|
||||
@@ -589,28 +594,33 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowli
|
||||
return ota_apply_mota_nrf52_impl(buf, len, allow, operator_base_hash, local_target_id, st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
#if defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
|
||||
static const size_t NRF52_APPLY_MSG_CAP = 96;
|
||||
|
||||
template <typename Store>
|
||||
static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& allow,
|
||||
uint8_t storage_flag, const char* storage_name,
|
||||
ApplyState& st, char* msg) {
|
||||
st = ApplyState();
|
||||
uint8_t hdr[8], manifest[MOTA_MFL];
|
||||
uint32_t total = store.staged_size();
|
||||
if (total < 8 + MOTA_MFL + 5 || !store.read(0, hdr, sizeof(hdr)) ||
|
||||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
|
||||
!store.read(8, manifest, sizeof(manifest))) {
|
||||
strcpy(msg, "SD container parse failed");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s container parse failed", storage_name);
|
||||
return false;
|
||||
}
|
||||
MotaManifest m;
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
|
||||
strcpy(msg, "SD manifest parse failed");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s manifest parse failed", storage_name);
|
||||
return false;
|
||||
}
|
||||
const bool full = m.is_full() && m.codec_id == CODEC_FULL &&
|
||||
m.payload_size == m.image_size;
|
||||
const bool delta = !m.is_full() && m.codec_id == CODEC_DETOOLS_INPLACE;
|
||||
if (!full && !delta) {
|
||||
strcpy(msg, "nRF52 SD bootloader accepts full or in-place delta only");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"nRF52 %s bootloader accepts full or in-place delta only", storage_name);
|
||||
return false;
|
||||
}
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
@@ -624,14 +634,15 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
st.manifest_ok = true;
|
||||
|
||||
OtaBlCaps bl = ota_bootloader_caps();
|
||||
if (!bl.present || !(bl.storage_flags & OTA_BL_STORAGE_SD)) {
|
||||
strcpy(msg, "this bootloader has no SD OTA support - update the bootloader first");
|
||||
if (!bl.present || !(bl.storage_flags & storage_flag)) {
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"this bootloader has no %s OTA support - update the bootloader first", storage_name);
|
||||
return false;
|
||||
}
|
||||
if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
|
||||
snprintf(msg, 159,
|
||||
"bootloader cannot apply this SD update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
|
||||
bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"bootloader cannot apply this %s update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
|
||||
storage_name, bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -639,12 +650,13 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
st.sig_ok = vr.sig_ok;
|
||||
st.trusted = vr.trusted;
|
||||
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok) {
|
||||
strcpy(msg, "payload hash mismatch (incomplete or corrupt SD .mota)");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"payload hash mismatch (incomplete or corrupt %s .mota)", storage_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
SelfFwInfo fi;
|
||||
if (delta) {
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid) {
|
||||
strcpy(msg, "cannot read running firmware (no EndF)");
|
||||
return false;
|
||||
@@ -659,16 +671,51 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; }
|
||||
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
|
||||
}
|
||||
if (delta) {
|
||||
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
|
||||
uint8_t patch_header[32]; // fixed byte + five detools varints (at most 26 bytes for uint32)
|
||||
uint32_t header_len = m.payload_size < sizeof(patch_header) ? m.payload_size : sizeof(patch_header);
|
||||
InplacePatchDims d;
|
||||
if (payload_off64 > UINT32_MAX || payload_off64 + m.payload_size + 5u != total ||
|
||||
!store.read((uint32_t)payload_off64, patch_header, header_len) ||
|
||||
!parse_inplace_patch_dims(patch_header, header_len, d)) {
|
||||
strcpy(msg, "bad in-place patch header");
|
||||
return false;
|
||||
}
|
||||
if (!mota_nrf52_external_patch_geometry_valid(
|
||||
d.memory, d.segment, d.shift, d.from, d.to,
|
||||
MOTA_NRF52_APP_END - app_base, fi.image_len, m.image_size)) {
|
||||
strcpy(msg, "invalid in-place patch geometry");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!store.approve_for_bootloader()) {
|
||||
snprintf(msg, 159, "SD handoff failed: %s", store.last_error());
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s handoff failed: %s", storage_name,
|
||||
store.last_error());
|
||||
return false;
|
||||
}
|
||||
sprintf(msg, "verified%s %s image on SD; rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta");
|
||||
snprintf(msg, NRF52_APPLY_MSG_CAP,
|
||||
"verified%s %s image on %s; rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta",
|
||||
storage_name);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", st, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", st, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#else // native / other platforms
|
||||
|
||||
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
|
||||
|
||||
@@ -86,6 +86,11 @@ class OtaStoreSdNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
class OtaStoreQspiNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store,
|
||||
const SignerAllowlist& allow, 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
|
||||
|
||||
@@ -30,6 +30,7 @@ struct OtaBlCaps {
|
||||
|
||||
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;
|
||||
|
||||
// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52.
|
||||
inline OtaBlCaps ota_bootloader_caps() {
|
||||
|
||||
+35
-10
@@ -146,7 +146,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
snprintf(reply, 160,
|
||||
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
|
||||
"announce | folder | config. LoRa install is disabled.");
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
|
||||
"cancel | announce | self | folder | config | key");
|
||||
@@ -194,7 +195,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// 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).
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
#if defined(OTA_SD_STORE)
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
const char* bl_state = !bl.present ? "NONE" :
|
||||
(bl.storage_flags & OTA_BL_STORAGE_QSPI) ? "QSPI" : "NO-QSPI";
|
||||
#elif defined(OTA_SD_STORE)
|
||||
const char* bl_state = !bl.present ? "NONE" :
|
||||
(bl.storage_flags & OTA_BL_STORAGE_SD) ? "SD" : "NO-SD";
|
||||
#else
|
||||
@@ -269,7 +273,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#if defined(NRF52_PLATFORM)
|
||||
const OtaBlCaps& list_bl = c.bootloaderCaps();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
SelfFwInfo list_self;
|
||||
bool list_has_endf = ota_self_firmware(list_self) && list_self.valid;
|
||||
#endif
|
||||
@@ -302,10 +307,13 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
&& h->codec < 16 && (list_bl.codec_mask & (1u << h->codec));
|
||||
#if defined(OTA_SD_STORE)
|
||||
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_SD);
|
||||
#elif defined(OTA_QSPI_STORE)
|
||||
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_QSPI);
|
||||
#endif
|
||||
#endif
|
||||
if (!installable) fit = "unsupported";
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
else if (!list_has_endf) fit = "rescue";
|
||||
#endif
|
||||
else fit = "same target";
|
||||
@@ -428,9 +436,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
strcpy(reply, "ERR bootloader cannot apply an update staged on SD; update it over USB first");
|
||||
return true;
|
||||
}
|
||||
#elif defined(OTA_QSPI_STORE)
|
||||
if (!(bl.storage_flags & OTA_BL_STORAGE_QSPI)) {
|
||||
strcpy(reply, "ERR bootloader cannot apply an update staged on QSPI; update it over USB first");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE)
|
||||
SelfFwInfo self;
|
||||
bool has_endf = ota_self_firmware(self) && self.valid;
|
||||
if (!has_endf && !rescue) {
|
||||
@@ -489,7 +503,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
|
||||
c.fetch_to_folder = false;
|
||||
c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE) && !defined(OTA_QSPI_STORE)
|
||||
c.manager.set_accept_full(false);
|
||||
#endif
|
||||
c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0;
|
||||
@@ -517,7 +531,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store.
|
||||
const OtaBlCaps& bl = c.bootloaderCaps(); // cached (flash scanned once)
|
||||
#if defined(OTA_SD_STORE)
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
uint32_t qspi_capacity = c.fetch_store.capacity();
|
||||
n += snprintf(reply + n, 160 - n, " | QSPI store:%s%uK",
|
||||
qspi_capacity ? "" : "ERR ", (unsigned)(qspi_capacity / 1024));
|
||||
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_QSPI))
|
||||
snprintf(reply + n, 160 - n, " | bootloader: QSPI apply OK (abi=%u codecs=0x%x)",
|
||||
bl.apply_abi, bl.codec_mask);
|
||||
else
|
||||
snprintf(reply + n, 160 - n,
|
||||
" | bootloader: NO QSPI mota-apply support (install will refuse)");
|
||||
#elif defined(OTA_SD_STORE)
|
||||
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_SD))
|
||||
snprintf(reply + n, 160 - n, " | bootloader: SD apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask);
|
||||
else
|
||||
@@ -538,7 +562,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// deliberately not an alias or automatic fallback: the operator must name `install` and provide
|
||||
// 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_SEEDER_ONLY)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
|
||||
!defined(OTA_QSPI_STORE) && !defined(OTA_SEEDER_ONLY)
|
||||
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;
|
||||
@@ -827,7 +852,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
strcpy(reply, "ERR completed fetch is in the host folder, not local verification storage");
|
||||
return true;
|
||||
}
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) {
|
||||
VerifyResult r = ota_verify(static_cast<const OtaStore&>(c.fetch_store), c.allow);
|
||||
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
|
||||
@@ -837,7 +862,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
}
|
||||
#endif
|
||||
const uint8_t* buf; uint32_t len;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0;
|
||||
#else
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) { buf = c.fetch_store.data(); len = c.fetch_store.staged_size(); }
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include "OtaFormat.h"
|
||||
#include "OtaSelf.h" // ota_self_firmware() - prefer self-describing EndF identity at begin()
|
||||
#include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#include "OtaStoreSdNrf52.h"
|
||||
#include "OtaCacheSdNrf52.h"
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
@@ -31,12 +33,11 @@
|
||||
#endif
|
||||
|
||||
// Per-device OTA singleton shared by the CLI (OtaCli) and the mesh adapter (the example's MyMesh).
|
||||
// Holds the session engine, a staging store (fetch), a RAM serve buffer, and the signer allowlist.
|
||||
// nRF52 stages into FLASH (OtaStoreFlashNrf52): a delta can be 100 KB+, too big to hold in RAM, and the
|
||||
// COMPLETE container must persist so the bootloader can apply it after reboot. A flash page-erase halts
|
||||
// the CPU (~85 ms) and starves the LoRa RX, so the store COALESCES writes to the 4 KB page (the erase
|
||||
// unit) and commits each page once, off the per-packet path (see OtaManager.h) - RAM stays O(one page).
|
||||
// (v1 has no mid-transfer resume; an interrupted fetch simply restarts.) ESP32/native use the RAM store.
|
||||
// Holds the session engine, a persistent staging store (fetch), a RAM serve buffer, and the signer
|
||||
// allowlist. nRF52 uses one of three stores selected by the exact target: internal flash for in-place
|
||||
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. Flash-backed stores
|
||||
// coalesce writes at their erase-page boundary and checkpoint payload before leaf metadata, so an
|
||||
// interrupted fetch can be verified and resumed without trusting stale progress markers.
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
@@ -50,7 +51,8 @@ class FolderMotaStore; // pull destination over the seeder link (full type onl
|
||||
#define OTA_SERVE_BUF_SIZE 1
|
||||
// nRF52 self-serving streams from flash; this buffer is only for the manual `ota dev stage` helper.
|
||||
// Keep it to one flash page so the OTA singleton does not consume another 16 KB of scarce SRAM.
|
||||
#elif defined(NRF52_PLATFORM) && (defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE))
|
||||
#elif defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
#define OTA_SERVE_BUF_SIZE 4096
|
||||
#else
|
||||
#define OTA_SERVE_BUF_SIZE 16384
|
||||
@@ -67,6 +69,8 @@ struct OtaContext {
|
||||
// store object for OtaManager, while folder captures replace it with the
|
||||
// host-backed FolderMotaStore for the duration of the pull.
|
||||
OtaStoreRam<1> fetch_store;
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
OtaStoreQspiNrf52 fetch_store; // persistent raw QSPI staging, full + in-place delta
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
OtaStoreSdNrf52 fetch_store; // MeshTower V2: persistent SD staging, full + delta
|
||||
OtaCacheSdNrf52 sd_cache; // persistent capture + source for every OTA container heard
|
||||
@@ -161,7 +165,7 @@ struct OtaContext {
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
#else
|
||||
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE)
|
||||
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
|
||||
if (rescue_base_hash) {
|
||||
strncpy(msg, "rescue is only for internal-flash nRF52 builds", 96);
|
||||
msg[95] = 0;
|
||||
@@ -191,7 +195,7 @@ struct OtaContext {
|
||||
}
|
||||
}
|
||||
bool ok;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
if (rescue_base_hash) {
|
||||
@@ -446,7 +450,7 @@ struct OtaContext {
|
||||
manager.set_accept_full(true);
|
||||
manager.set_autofetch(OtaManager::AUTOFETCH_OFF);
|
||||
autoinstall = AUTOINSTALL_OFF;
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#elif defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
manager.set_accept_full(true);
|
||||
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
|
||||
@@ -37,6 +37,7 @@ static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
|
||||
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
|
||||
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;
|
||||
|
||||
// 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
|
||||
@@ -106,6 +107,20 @@ inline bool mota_nrf52_layout_valid(uint32_t app_base) {
|
||||
return mota_nrf52_layout_valid(app_base, mota_nrf52_layout_stage_ceiling());
|
||||
}
|
||||
|
||||
// Validate the five sizes encoded at the start of a detools in-place patch
|
||||
// before an external SD/QSPI handoff is approved. External media leaves the
|
||||
// complete application region available as workspace; the bootloader repeats
|
||||
// these checks before its first destructive write.
|
||||
inline bool mota_nrf52_external_patch_geometry_valid(uint32_t memory, uint32_t segment,
|
||||
uint32_t shift, uint32_t from,
|
||||
uint32_t to, uint32_t workspace_span,
|
||||
uint32_t running_image_size,
|
||||
uint32_t target_image_size) {
|
||||
return memory != 0 && memory <= workspace_span && segment == MOTA_NRF52_FLASH_PAGE &&
|
||||
shift <= memory && shift % segment == 0 && from <= memory - shift &&
|
||||
from == running_image_size && to == target_image_size && to <= memory;
|
||||
}
|
||||
|
||||
inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base, uint32_t app_end,
|
||||
uint32_t stage_ceiling) {
|
||||
if (!mota_nrf52_layout_valid(app_base, stage_ceiling) || app_end < app_base ||
|
||||
|
||||
@@ -0,0 +1,701 @@
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
|
||||
#include "OtaByteIO.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include "hal/nrf_qspi.h"
|
||||
#include "nrf.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(OTA_QSPI_SCK_PHYSICAL_PIN) && defined(OTA_QSPI_SCK_ARDUINO_PIN)
|
||||
#error "QSPI SCK must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_SCK_PHYSICAL_PIN) && !defined(OTA_QSPI_SCK_ARDUINO_PIN)
|
||||
#define OTA_QSPI_SCK_ARDUINO_PIN PIN_QSPI_SCK
|
||||
#endif
|
||||
#if defined(OTA_QSPI_CS_PHYSICAL_PIN) && defined(OTA_QSPI_CS_ARDUINO_PIN)
|
||||
#error "QSPI CS must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_CS_PHYSICAL_PIN) && !defined(OTA_QSPI_CS_ARDUINO_PIN)
|
||||
#define OTA_QSPI_CS_ARDUINO_PIN PIN_QSPI_CS
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO0_PHYSICAL_PIN) && defined(OTA_QSPI_IO0_ARDUINO_PIN)
|
||||
#error "QSPI IO0 must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_IO0_PHYSICAL_PIN) && !defined(OTA_QSPI_IO0_ARDUINO_PIN)
|
||||
#define OTA_QSPI_IO0_ARDUINO_PIN PIN_QSPI_IO0
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO1_PHYSICAL_PIN) && defined(OTA_QSPI_IO1_ARDUINO_PIN)
|
||||
#error "QSPI IO1 must use either a physical or Arduino pin override"
|
||||
#elif !defined(OTA_QSPI_IO1_PHYSICAL_PIN) && !defined(OTA_QSPI_IO1_ARDUINO_PIN)
|
||||
#define OTA_QSPI_IO1_ARDUINO_PIN PIN_QSPI_IO1
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SCK_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_SCK_PHYSICAL_PIN) < 48, "invalid physical QSPI SCK pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_SCK_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI SCK pin");
|
||||
#endif
|
||||
#if defined(OTA_QSPI_CS_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_CS_PHYSICAL_PIN) < 48, "invalid physical QSPI CS pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_CS_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI CS pin");
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO0_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO0_PHYSICAL_PIN) < 48, "invalid physical QSPI IO0 pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_IO0_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO0 pin");
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO1_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO1_PHYSICAL_PIN) < 48, "invalid physical QSPI IO1 pin");
|
||||
#else
|
||||
static_assert((uint32_t)(OTA_QSPI_IO1_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO1 pin");
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_IO2_NOT_CONNECTED)
|
||||
#if defined(OTA_QSPI_IO2_ARDUINO_PIN) || defined(OTA_QSPI_IO2_PHYSICAL_PIN)
|
||||
#error "QSPI IO2 cannot be both connected and disconnected"
|
||||
#endif
|
||||
#elif defined(OTA_QSPI_IO2_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO2_PHYSICAL_PIN) < 48, "invalid physical QSPI IO2 pin");
|
||||
#elif defined(OTA_QSPI_IO2_ARDUINO_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO2_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO2 pin");
|
||||
#else
|
||||
#define OTA_QSPI_IO2_ARDUINO_PIN PIN_QSPI_IO2
|
||||
static_assert((uint32_t)(OTA_QSPI_IO2_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO2 pin");
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_IO3_NOT_CONNECTED)
|
||||
#if defined(OTA_QSPI_IO3_ARDUINO_PIN) || defined(OTA_QSPI_IO3_PHYSICAL_PIN)
|
||||
#error "QSPI IO3 cannot be both connected and disconnected"
|
||||
#endif
|
||||
#elif defined(OTA_QSPI_IO3_PHYSICAL_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO3_PHYSICAL_PIN) < 48, "invalid physical QSPI IO3 pin");
|
||||
#elif defined(OTA_QSPI_IO3_ARDUINO_PIN)
|
||||
static_assert((uint32_t)(OTA_QSPI_IO3_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO3 pin");
|
||||
#else
|
||||
#define OTA_QSPI_IO3_ARDUINO_PIN PIN_QSPI_IO3
|
||||
static_assert((uint32_t)(OTA_QSPI_IO3_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO3 pin");
|
||||
#endif
|
||||
|
||||
static uint8_t arduino_to_physical(uint32_t arduino_pin) {
|
||||
return (uint8_t)g_ADigitalPinMap[arduino_pin];
|
||||
}
|
||||
|
||||
static uint8_t qspi_sck_pin() {
|
||||
#ifdef OTA_QSPI_SCK_PHYSICAL_PIN
|
||||
return OTA_QSPI_SCK_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_SCK_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_cs_pin() {
|
||||
#ifdef OTA_QSPI_CS_PHYSICAL_PIN
|
||||
return OTA_QSPI_CS_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_CS_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io0_pin() {
|
||||
#ifdef OTA_QSPI_IO0_PHYSICAL_PIN
|
||||
return OTA_QSPI_IO0_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO0_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io1_pin() {
|
||||
#ifdef OTA_QSPI_IO1_PHYSICAL_PIN
|
||||
return OTA_QSPI_IO1_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO1_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io2_pin() {
|
||||
#ifdef OTA_QSPI_IO2_NOT_CONNECTED
|
||||
return NRF_QSPI_PIN_NOT_CONNECTED;
|
||||
#elif defined(OTA_QSPI_IO2_PHYSICAL_PIN)
|
||||
return OTA_QSPI_IO2_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO2_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t qspi_io3_pin() {
|
||||
#ifdef OTA_QSPI_IO3_NOT_CONNECTED
|
||||
return NRF_QSPI_PIN_NOT_CONNECTED;
|
||||
#elif defined(OTA_QSPI_IO3_PHYSICAL_PIN)
|
||||
return OTA_QSPI_IO3_PHYSICAL_PIN;
|
||||
#else
|
||||
return arduino_to_physical(OTA_QSPI_IO3_ARDUINO_PIN);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OtaStoreQspiNrf52::OtaStoreQspiNrf52() {
|
||||
resetSession();
|
||||
}
|
||||
|
||||
OtaStoreQspiNrf52::~OtaStoreQspiNrf52() {
|
||||
releaseFlash();
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::fail(const char *message) {
|
||||
_io_ok = false;
|
||||
strncpy(_error, message ? message : "QSPI error", sizeof(_error) - 1);
|
||||
_error[sizeof(_error) - 1] = 0;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::resetSession() {
|
||||
_total = 0;
|
||||
_io_ok = true;
|
||||
_meta_dirty = false;
|
||||
_data_dirty = false;
|
||||
_data_page_index = INVALID_PAGE;
|
||||
memset(_meta_page, 0xFF, sizeof(_meta_page));
|
||||
memset(_data_page, 0xFF, sizeof(_data_page));
|
||||
memset(_known_pages, 0, sizeof(_known_pages));
|
||||
_error[0] = 0;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::pageKnown(uint32_t page) const {
|
||||
return page < MAX_PAGES && (_known_pages[page >> 3] & (1u << (page & 7))) != 0;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::setPageKnown(uint32_t page) {
|
||||
if (page < MAX_PAGES) _known_pages[page >> 3] |= (uint8_t)(1u << (page & 7));
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::waitReady(uint32_t timeout_ms) {
|
||||
uint32_t started = millis();
|
||||
while (!nrf_qspi_event_check(NRF_QSPI, NRF_QSPI_EVENT_READY)) {
|
||||
if ((uint32_t)(millis() - started) >= timeout_ms) return false;
|
||||
delay(1);
|
||||
}
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx) {
|
||||
nrf_qspi_cinstr_conf_t config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.opcode = opcode;
|
||||
config.length = (nrf_qspi_cinstr_len_t)length;
|
||||
config.io2_level = true;
|
||||
config.io3_level = true;
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_cinstr_transfer_start(NRF_QSPI, &config);
|
||||
if (!waitReady(1000)) return false;
|
||||
if (rx) nrf_qspi_cinstrdata_get(NRF_QSPI, config.length, rx);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::ensureFlash() {
|
||||
if (_qspi_ready) return true;
|
||||
_error[0] = 0;
|
||||
_io_ok = true;
|
||||
|
||||
#if defined(OTA_QSPI_POWER_PIN)
|
||||
pinMode(OTA_QSPI_POWER_PIN, OUTPUT);
|
||||
digitalWrite(OTA_QSPI_POWER_PIN, HIGH);
|
||||
delay(2);
|
||||
#elif defined(PIN_FLASH_EN)
|
||||
pinMode(PIN_FLASH_EN, OUTPUT);
|
||||
digitalWrite(PIN_FLASH_EN, HIGH);
|
||||
delay(2);
|
||||
#elif defined(QSPI_FLASH_EN)
|
||||
pinMode(QSPI_FLASH_EN, OUTPUT);
|
||||
digitalWrite(QSPI_FLASH_EN, HIGH);
|
||||
delay(2);
|
||||
#endif
|
||||
|
||||
NVIC_DisableIRQ(QSPI_IRQn);
|
||||
NVIC_ClearPendingIRQ(QSPI_IRQn);
|
||||
nrf_qspi_int_disable(NRF_QSPI, 0xFFFFFFFFUL);
|
||||
|
||||
nrf_qspi_pins_t pins;
|
||||
pins.sck_pin = qspi_sck_pin();
|
||||
pins.csn_pin = qspi_cs_pin();
|
||||
pins.io0_pin = qspi_io0_pin();
|
||||
pins.io1_pin = qspi_io1_pin();
|
||||
pins.io2_pin = qspi_io2_pin();
|
||||
pins.io3_pin = qspi_io3_pin();
|
||||
nrf_qspi_pins_set(NRF_QSPI, &pins);
|
||||
|
||||
nrf_qspi_prot_conf_t protocol;
|
||||
protocol.readoc = NRF_QSPI_READOC_FASTREAD;
|
||||
protocol.writeoc = NRF_QSPI_WRITEOC_PP;
|
||||
protocol.addrmode = NRF_QSPI_ADDRMODE_24BIT;
|
||||
protocol.dpmconfig = false;
|
||||
nrf_qspi_ifconfig0_set(NRF_QSPI, &protocol);
|
||||
|
||||
nrf_qspi_phy_conf_t physical;
|
||||
physical.sck_delay = 5;
|
||||
physical.dpmen = false;
|
||||
physical.spi_mode = NRF_QSPI_MODE_0;
|
||||
#ifdef OTA_QSPI_SCK_FREQUENCY
|
||||
physical.sck_freq = static_cast<nrf_qspi_frequency_t>(OTA_QSPI_SCK_FREQUENCY);
|
||||
#else
|
||||
physical.sck_freq = NRF_QSPI_FREQ_32MDIV2;
|
||||
#endif
|
||||
nrf_qspi_ifconfig1_set(NRF_QSPI, &physical);
|
||||
|
||||
nrf_qspi_enable(NRF_QSPI);
|
||||
_qspi_active = true;
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ACTIVATE);
|
||||
if (!waitReady(1000)) {
|
||||
releaseFlash();
|
||||
fail("QSPI activate timed out");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Release from deep power-down, then identify capacity from the JEDEC byte.
|
||||
if (!customInstruction(0xAB, NRF_QSPI_CINSTR_LEN_1B)) {
|
||||
releaseFlash();
|
||||
fail("QSPI wake failed");
|
||||
return false;
|
||||
}
|
||||
_qspi_awake = true;
|
||||
delayMicroseconds(50);
|
||||
alignas(4) uint8_t jedec[4] = { 0, 0, 0, 0 };
|
||||
if (!customInstruction(0x9F, NRF_QSPI_CINSTR_LEN_4B, jedec) || jedec[0] == 0 || jedec[0] == 0xFF ||
|
||||
jedec[2] < 20 || jedec[2] > 24) {
|
||||
releaseFlash();
|
||||
fail("QSPI JEDEC ID/capacity unsupported");
|
||||
return false;
|
||||
}
|
||||
#ifdef OTA_QSPI_EXPECTED_JEDEC_ID
|
||||
const uint32_t jedec_id = ((uint32_t)jedec[0] << 16) | ((uint32_t)jedec[1] << 8) | jedec[2];
|
||||
if (jedec_id != (uint32_t)OTA_QSPI_EXPECTED_JEDEC_ID) {
|
||||
releaseFlash();
|
||||
fail("QSPI JEDEC ID does not match target");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
_flash_size = 1UL << jedec[2];
|
||||
if (_flash_size > MAX_FLASH) {
|
||||
releaseFlash();
|
||||
fail("QSPI flash exceeds 24-bit staging limit");
|
||||
return false;
|
||||
}
|
||||
#ifdef OTA_QSPI_EXPECTED_SIZE
|
||||
if (_flash_size != (uint32_t)OTA_QSPI_EXPECTED_SIZE) {
|
||||
releaseFlash();
|
||||
fail("QSPI capacity does not match target");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
_qspi_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::releaseFlash() {
|
||||
if (_qspi_awake) {
|
||||
// The repeater can remain idle for hours after a capacity/status probe or
|
||||
// a completed checkpoint. Put the NOR into deep power-down before
|
||||
// releasing the nRF QSPI peripheral instead of leaving both active for
|
||||
// the rest of the boot. ensureFlash() issues 0xAB on the next operation.
|
||||
(void)customInstruction(0xB9, NRF_QSPI_CINSTR_LEN_1B);
|
||||
delayMicroseconds(5);
|
||||
}
|
||||
// Match nrfx_qspi_uninit(): DEACTIVATE does not require a READY wait before
|
||||
// disabling the peripheral. Trigger it after every successful ENABLE, even
|
||||
// when wake or JEDEC identification failed before _qspi_ready was set.
|
||||
if (_qspi_active) {
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_DEACTIVATE);
|
||||
nrf_qspi_disable(NRF_QSPI);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
}
|
||||
_qspi_active = false;
|
||||
_qspi_awake = false;
|
||||
_qspi_ready = false;
|
||||
#if defined(OTA_QSPI_POWER_PIN)
|
||||
digitalWrite(OTA_QSPI_POWER_PIN, LOW);
|
||||
#elif defined(PIN_FLASH_EN)
|
||||
digitalWrite(PIN_FLASH_EN, LOW);
|
||||
#elif defined(QSPI_FLASH_EN)
|
||||
digitalWrite(QSPI_FLASH_EN, LOW);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::dmaReadAligned(uint32_t address, uint32_t length) {
|
||||
// nRF52840 QSPI EasyDMA requires a word-aligned RAM buffer and a transfer
|
||||
// count that is a multiple of four. Keep the external address aligned too,
|
||||
// so every supported SDK/peripheral revision receives the same safe shape.
|
||||
if (!length || length > sizeof(_bounce) || ((address | length) & 3u) != 0 ||
|
||||
(uint64_t)address + length > _flash_size) {
|
||||
fail("QSPI aligned read invalid");
|
||||
return false;
|
||||
}
|
||||
nrf_qspi_read_buffer_set(NRF_QSPI, _bounce, length, address);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_READSTART);
|
||||
if (!waitReady(5000)) {
|
||||
fail("QSPI read timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::dmaWriteAligned(uint32_t address, uint32_t length) {
|
||||
if (!length || length > sizeof(_bounce) || ((address | length) & 3u) != 0 ||
|
||||
(address & (PROGRAM - 1)) + length > PROGRAM ||
|
||||
(uint64_t)address + length > _flash_size) {
|
||||
fail("QSPI aligned program invalid");
|
||||
return false;
|
||||
}
|
||||
nrf_qspi_write_buffer_set(NRF_QSPI, _bounce, length, address);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_WRITESTART);
|
||||
if (!waitReady(10000)) {
|
||||
fail("QSPI program timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::rawRead(uint32_t address, void *data, uint32_t length) {
|
||||
if (length && !data) {
|
||||
fail("QSPI read buffer missing");
|
||||
return false;
|
||||
}
|
||||
if (!length) return true;
|
||||
if (!ensureFlash() || (uint64_t)address + length > _flash_size) {
|
||||
if (_io_ok) fail("QSPI read outside flash");
|
||||
return false;
|
||||
}
|
||||
uint8_t *out = static_cast<uint8_t *>(data);
|
||||
while (length) {
|
||||
const uint32_t aligned_address = address & ~3u;
|
||||
const uint32_t prefix = address - aligned_address;
|
||||
uint32_t n = length;
|
||||
if (n > sizeof(_bounce) - prefix) n = sizeof(_bounce) - prefix;
|
||||
const uint32_t dma_length = (prefix + n + 3u) & ~3u;
|
||||
if (!dmaReadAligned(aligned_address, dma_length)) return false;
|
||||
memcpy(out, _bounce + prefix, n);
|
||||
address += n;
|
||||
out += n;
|
||||
length -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::rawWrite(uint32_t address, const void *data, uint32_t length) {
|
||||
if (length && !data) {
|
||||
fail("QSPI program buffer missing");
|
||||
return false;
|
||||
}
|
||||
if (!length) return true;
|
||||
if (!ensureFlash() || (uint64_t)address + length > _flash_size) {
|
||||
if (_io_ok) fail("QSPI program outside flash");
|
||||
return false;
|
||||
}
|
||||
const uint8_t *in = static_cast<const uint8_t *>(data);
|
||||
while (length) {
|
||||
const uint32_t aligned_address = address & ~3u;
|
||||
const uint32_t prefix = address - aligned_address;
|
||||
const uint32_t page_room = PROGRAM - (aligned_address & (PROGRAM - 1));
|
||||
uint32_t n = length;
|
||||
if (n > page_room - prefix) n = page_room - prefix;
|
||||
const uint32_t dma_length = (prefix + n + 3u) & ~3u;
|
||||
|
||||
// Read-modify-program the aligned EasyDMA window. NOR flash cannot change
|
||||
// a programmed zero back to one without erasing the 4 KiB sector, so
|
||||
// reject such a request instead of silently corrupting the container.
|
||||
if (!dmaReadAligned(aligned_address, dma_length)) return false;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const uint8_t old_value = _bounce[prefix + i];
|
||||
const uint8_t new_value = in[i];
|
||||
if ((old_value & new_value) != new_value) {
|
||||
fail("QSPI program requires erase");
|
||||
return false;
|
||||
}
|
||||
_bounce[prefix + i] = new_value;
|
||||
}
|
||||
if (!dmaWriteAligned(aligned_address, dma_length)) return false;
|
||||
address += n;
|
||||
in += n;
|
||||
length -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::rawErasePage(uint32_t address) {
|
||||
if (!ensureFlash() || (address & (PAGE - 1)) != 0 || address > _flash_size - PAGE) return false;
|
||||
nrf_qspi_erase_ptr_set(NRF_QSPI, address, NRF_QSPI_ERASE_LEN_4KB);
|
||||
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
|
||||
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ERASESTART);
|
||||
if (!waitReady(30000)) {
|
||||
fail("QSPI erase timed out");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::flushPage(uint32_t page, const uint8_t *data) {
|
||||
if (!_io_ok || page >= MAX_PAGES || (uint64_t)(page + 1) * PAGE > _flash_size ||
|
||||
!rawErasePage(page * PAGE) || !rawWrite(page * PAGE, data, PAGE)) {
|
||||
if (_io_ok) fail("QSPI page write failed");
|
||||
return false;
|
||||
}
|
||||
alignas(4) uint8_t verify[PROGRAM];
|
||||
for (uint32_t off = 0; off < PAGE; off += sizeof(verify)) {
|
||||
if (!rawRead(page * PAGE + off, verify, sizeof(verify)) ||
|
||||
memcmp(verify, data + off, sizeof(verify)) != 0) {
|
||||
fail("QSPI page verify failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setPageKnown(page);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::flushMeta() {
|
||||
if (!_meta_dirty) return _io_ok;
|
||||
if (!flushPage(0, _meta_page)) return false;
|
||||
_meta_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::flushData() {
|
||||
if (_data_page_index == INVALID_PAGE || !_data_dirty) return _io_ok;
|
||||
if (!flushPage(_data_page_index, _data_page)) return false;
|
||||
_data_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::useDataPage(uint32_t page) {
|
||||
if (page == 0 || page >= MAX_PAGES) return false;
|
||||
if (_data_page_index == page) return true;
|
||||
if (!flushData()) return false;
|
||||
if (pageKnown(page)) {
|
||||
if (!rawRead(page * PAGE, _data_page, PAGE)) {
|
||||
fail("QSPI page load failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
memset(_data_page, 0xFF, PAGE);
|
||||
setPageKnown(page);
|
||||
}
|
||||
_data_page_index = page;
|
||||
_data_dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t OtaStoreQspiNrf52::capacity() const {
|
||||
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
|
||||
uint32_t result = self->ensureFlash() ? self->_flash_size : 0;
|
||||
self->releaseFlash();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t, uint32_t payload_size) {
|
||||
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) {
|
||||
fail("image exceeds nRF52 application region");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
bool result = ensureFlash();
|
||||
releaseFlash();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::begin(uint32_t total_size) {
|
||||
if (!ensureFlash() || total_size < 13 || total_size > _flash_size) {
|
||||
if (_io_ok) fail("QSPI lacks space for update");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Immediately invalidate an older raw container. Page zero is erased and
|
||||
// replaced with the new header at the first checkpoint/finalize.
|
||||
uint8_t zero[4] = { 0, 0, 0, 0 };
|
||||
uint8_t check[sizeof(zero)];
|
||||
if (!rawWrite(0, zero, sizeof(zero)) || !rawRead(0, check, sizeof(check)) ||
|
||||
memcmp(check, zero, sizeof(zero)) != 0) {
|
||||
fail("QSPI old-container invalidation failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
resetSession();
|
||||
_total = total_size;
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::set_meta_size(uint32_t meta_bytes) {
|
||||
return _total >= 13 && meta_bytes <= PAGE;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::write(uint32_t offset, const uint8_t *data, uint32_t len) {
|
||||
if (!_io_ok || !data || (uint64_t)offset + len > _total) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
while (len) {
|
||||
uint32_t page = offset / PAGE;
|
||||
uint32_t in_page = offset & (PAGE - 1);
|
||||
uint32_t n = PAGE - in_page;
|
||||
if (n > len) n = len;
|
||||
if (page == 0) {
|
||||
memcpy(_meta_page + in_page, data, n);
|
||||
_meta_dirty = true;
|
||||
} else {
|
||||
if (!useDataPage(page)) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
memcpy(_data_page + in_page, data, n);
|
||||
_data_dirty = true;
|
||||
}
|
||||
offset += n;
|
||||
data += n;
|
||||
len -= n;
|
||||
}
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::read(uint32_t offset, uint8_t *buf, uint32_t len) const {
|
||||
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
|
||||
if (!_io_ok || !buf || (uint64_t)offset + len > _total) {
|
||||
self->releaseFlash();
|
||||
return false;
|
||||
}
|
||||
while (len) {
|
||||
uint32_t page = offset / PAGE;
|
||||
uint32_t in_page = offset & (PAGE - 1);
|
||||
uint32_t n = PAGE - in_page;
|
||||
if (n > len) n = len;
|
||||
if (page == 0) {
|
||||
memcpy(buf, _meta_page + in_page, n);
|
||||
} else if (page == _data_page_index) {
|
||||
memcpy(buf, _data_page + in_page, n);
|
||||
} else if (pageKnown(page)) {
|
||||
if (!self->rawRead(offset, buf, n)) {
|
||||
self->releaseFlash();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
memset(buf, 0xFF, n);
|
||||
}
|
||||
offset += n;
|
||||
buf += n;
|
||||
len -= n;
|
||||
}
|
||||
self->releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::finalize() {
|
||||
if (!_total || !_io_ok) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
// Persist payload first, then the leaf-progress metadata that declares it.
|
||||
bool result = flushData() && flushMeta();
|
||||
releaseFlash();
|
||||
return result;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::checkpoint() {
|
||||
if (!_total || !_io_ok) {
|
||||
releaseFlash();
|
||||
return;
|
||||
}
|
||||
if (flushData()) flushMeta();
|
||||
releaseFlash();
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::reopen() {
|
||||
resetSession();
|
||||
if (!ensureFlash()) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint8_t header[8];
|
||||
if (!rawRead(0, header, sizeof(header)) || memcmp(header, MOTA_MAGIC, 4) != 0) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint32_t total = rd_u32le(header + 4);
|
||||
if (total < 13 || total > _flash_size) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint8_t trailer[5];
|
||||
if (!rawRead(total - sizeof(trailer), trailer, sizeof(trailer)) ||
|
||||
memcmp(trailer, MOTA_TRAILER, sizeof(trailer)) != 0) {
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
if (!rawRead(0, _meta_page, PAGE)) {
|
||||
fail("QSPI metadata load failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
_total = total;
|
||||
uint32_t pages = (total + PAGE - 1) / PAGE;
|
||||
for (uint32_t page = 0; page < pages; page++)
|
||||
setPageKnown(page);
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::clear() {
|
||||
bool ok = ensureFlash();
|
||||
uint8_t zero[4] = { 0, 0, 0, 0 };
|
||||
uint8_t check[sizeof(zero)];
|
||||
if (ok) {
|
||||
ok = rawWrite(0, zero, sizeof(zero)) && rawRead(0, check, sizeof(check)) &&
|
||||
memcmp(check, zero, sizeof(zero)) == 0;
|
||||
}
|
||||
if (ok && _total >= 8 + MOTA_OFF_APPROVAL + sizeof(zero)) {
|
||||
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
|
||||
ok = rawWrite(approval, zero, sizeof(zero)) && rawRead(approval, check, sizeof(check)) &&
|
||||
memcmp(check, zero, sizeof(zero)) == 0;
|
||||
}
|
||||
char saved_error[sizeof(_error)];
|
||||
strncpy(saved_error, _error, sizeof(saved_error));
|
||||
saved_error[sizeof(saved_error) - 1] = 0;
|
||||
resetSession();
|
||||
if (!ok) fail(saved_error[0] ? saved_error : "QSPI container invalidation failed");
|
||||
releaseFlash();
|
||||
}
|
||||
|
||||
bool OtaStoreQspiNrf52::approve_for_bootloader() {
|
||||
if (!finalize()) return false;
|
||||
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
|
||||
if (approval + sizeof(APPROVAL_YES) > _total || !rawWrite(approval, APPROVAL_YES, sizeof(APPROVAL_YES))) {
|
||||
fail("QSPI approval write failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
uint8_t check[sizeof(APPROVAL_YES)];
|
||||
if (!rawRead(approval, check, sizeof(check)) || memcmp(check, APPROVAL_YES, sizeof(check)) != 0) {
|
||||
fail("QSPI approval verify failed");
|
||||
releaseFlash();
|
||||
return false;
|
||||
}
|
||||
memcpy(_meta_page + approval, APPROVAL_YES, sizeof(APPROVAL_YES));
|
||||
releaseFlash();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(OTA_QSPI_STORE) && defined(QSPIFLASH)
|
||||
#error "OTA_QSPI_STORE raw staging cannot share a QSPI chip with QSPIFLASH"
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(ETHERNET_ENABLED)
|
||||
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with Ethernet"
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(OTA_SD_STORE)
|
||||
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with SD staging"
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(RAK_3401)
|
||||
#error "RAK3401's RAK13302 radio already owns the WisBlock SPI bus/chip-select"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
|
||||
#include "OtaStore.h"
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Persistent raw-QSPI staging for nRF52840 repeater builds. The complete .mota
|
||||
// lives at external-flash offset zero, leaving the whole internal application
|
||||
// region available to the bootloader as an in-place workspace. This store must
|
||||
// never be enabled on a build that mounts the same chip as a filesystem.
|
||||
class OtaStoreQspiNrf52 : public OtaStore {
|
||||
static const uint32_t PAGE = 4096;
|
||||
static const uint32_t PROGRAM = 256;
|
||||
static const uint32_t MAX_FLASH = 16UL * 1024 * 1024;
|
||||
static const uint32_t MAX_PAGES = MAX_FLASH / PAGE;
|
||||
static const uint32_t INVALID_PAGE = 0xFFFFFFFFUL;
|
||||
|
||||
uint32_t _total = 0;
|
||||
uint32_t _flash_size = 0;
|
||||
bool _qspi_active = false;
|
||||
bool _qspi_awake = false;
|
||||
bool _qspi_ready = false;
|
||||
bool _io_ok = true;
|
||||
bool _meta_dirty = false;
|
||||
bool _data_dirty = false;
|
||||
uint32_t _data_page_index = INVALID_PAGE;
|
||||
char _error[80] = { 0 };
|
||||
|
||||
alignas(4) uint8_t _meta_page[PAGE];
|
||||
alignas(4) uint8_t _data_page[PAGE];
|
||||
alignas(4) uint8_t _bounce[PROGRAM];
|
||||
uint8_t _known_pages[MAX_PAGES / 8];
|
||||
|
||||
void fail(const char *message);
|
||||
bool ensureFlash();
|
||||
void releaseFlash();
|
||||
bool waitReady(uint32_t timeout_ms);
|
||||
bool customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx = nullptr);
|
||||
bool dmaReadAligned(uint32_t address, uint32_t length);
|
||||
bool dmaWriteAligned(uint32_t address, uint32_t length);
|
||||
bool rawRead(uint32_t address, void *data, uint32_t length);
|
||||
bool rawWrite(uint32_t address, const void *data, uint32_t length);
|
||||
bool rawErasePage(uint32_t address);
|
||||
bool flushPage(uint32_t page, const uint8_t *data);
|
||||
bool flushMeta();
|
||||
bool flushData();
|
||||
bool useDataPage(uint32_t page);
|
||||
void resetSession();
|
||||
bool pageKnown(uint32_t page) const;
|
||||
void setPageKnown(uint32_t page);
|
||||
|
||||
public:
|
||||
OtaStoreQspiNrf52();
|
||||
~OtaStoreQspiNrf52() 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;
|
||||
uint32_t capacity() const override;
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override;
|
||||
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;
|
||||
|
||||
// Sets APRV only after all app-side verification gates have passed.
|
||||
bool approve_for_bootloader();
|
||||
const char *last_error() const { return _error; }
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
#endif
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.
|
||||
// 552 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
|
||||
// 566 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env 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;
|
||||
@@ -153,6 +153,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x21519537, "Heltec_v3_sensor" },
|
||||
{ 0x382bb181, "Heltec_v3_terminal_chat" },
|
||||
{ 0x5a2980e6, "heltec_v4_3_companion_radio_ble_femoff" },
|
||||
{ 0x90581c83, "heltec_v4_3_companion_radio_ble_ps_femoff" },
|
||||
{ 0x356df17c, "heltec_v4_3_companion_radio_usb_femoff" },
|
||||
{ 0x99a084fc, "heltec_v4_3_companion_radio_wifi_femoff" },
|
||||
{ 0x64c9dfc9, "heltec_v4_3_companion_radio_wifi_mqtt_femoff" },
|
||||
@@ -160,16 +161,23 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x2c13b8e9, "heltec_v4_3_tft_companion_radio_ble_femoff" },
|
||||
{ 0xb016979a, "heltec_v4_3_tft_companion_radio_usb_femoff" },
|
||||
{ 0xb49b6b55, "heltec_v4_3_tft_companion_radio_wifi_femoff" },
|
||||
{ 0x01d8f124, "heltec_v4_companion_radio_ble" },
|
||||
{ 0x86fb85f0, "heltec_v4_companion_radio_ble_femon" },
|
||||
{ 0xd33b0986, "heltec_v4_companion_radio_ble_ps_femon" },
|
||||
{ 0x1a4d0096, "heltec_v4_companion_radio_usb" },
|
||||
{ 0xa8f5940f, "heltec_v4_companion_radio_usb_femon" },
|
||||
{ 0x1cf5e14c, "heltec_v4_companion_radio_usb_ps_femoff" },
|
||||
{ 0x2707d3d1, "heltec_v4_companion_radio_usb_ps_femon" },
|
||||
{ 0x86ac3b1b, "heltec_v4_companion_radio_wifi_femon" },
|
||||
{ 0x7af848da, "heltec_v4_companion_radio_wifi_mqtt_femon" },
|
||||
{ 0xd522a14f, "heltec_v4_expansionkit_repeater" },
|
||||
{ 0x76657d76, "heltec_v4_expansionkit_repeater_observer_mqtt" },
|
||||
{ 0x21ee03bd, "heltec_v4_expansionkit_room_server_observer_mqtt" },
|
||||
{ 0x5977d941, "heltec_v4_expansionkit_tft_companion_radio_ble_femon" },
|
||||
{ 0xe2a1907b, "heltec_v4_expansionkit_tft_companion_radio_ble_ps" },
|
||||
{ 0xf75feb3e, "heltec_v4_kiss_modem" },
|
||||
{ 0xbc9cdc70, "heltec_v4_r8_companion_radio_ble" },
|
||||
{ 0x19e31fc6, "heltec_v4_r8_companion_radio_ble_ps" },
|
||||
{ 0x36c78d86, "heltec_v4_r8_companion_radio_usb" },
|
||||
{ 0xdf722326, "heltec_v4_r8_companion_radio_wifi" },
|
||||
{ 0xfa901c87, "heltec_v4_r8_kiss_modem" },
|
||||
@@ -411,6 +419,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x76f3c984, "RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors" },
|
||||
{ 0x41e33a97, "RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors" },
|
||||
{ 0x29a0da19, "RAK_4631_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x29bc38dc, "RAK_4631_repeater_rak15001_slot_c_lora_ota" },
|
||||
{ 0xf1d3c5a8, "RAK_4631_terminal_chat" },
|
||||
{ 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" },
|
||||
{ 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" },
|
||||
@@ -442,7 +451,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x0ba4453d, "Station_G3_ESP32_kiss_modem" },
|
||||
{ 0xf8d1958f, "Station_G3_ESP32_logging_repeater" },
|
||||
{ 0x3c49caf8, "Station_G3_ESP32_repeater" },
|
||||
{ 0x58689ef9, "Station_G3_ESP32_repeater_observer_mqtt" },
|
||||
{ 0x91878dd8, "Station_G3_ESP32_room_server" },
|
||||
{ 0x72a93c45, "Station_G3_ESP32_room_server_observer_mqtt" },
|
||||
{ 0x4dbf035e, "t1000e_companion_radio_ble" },
|
||||
{ 0xff4102d6, "t1000e_companion_radio_usb" },
|
||||
{ 0x2f9a9185, "t1000e_kiss_modem" },
|
||||
@@ -517,7 +528,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0xa5f4a562, "ThinkNode_M7_companion_radio_wifi" },
|
||||
{ 0xef83222b, "ThinkNode_M7_kiss_modem" },
|
||||
{ 0xc36ec867, "ThinkNode_M7_repeater" },
|
||||
{ 0xb932238e, "ThinkNode_M7_repeater_observer_mqtt" },
|
||||
{ 0x92f5bbea, "ThinkNode_M7_room_server" },
|
||||
{ 0x26d91ab2, "ThinkNode_M7_room_server_observer_mqtt" },
|
||||
{ 0x708ac4dc, "ThinkNode_M9_companion_radio_ble_" },
|
||||
{ 0xdc51bf13, "ThinkNode_M9_companion_radio_usb_" },
|
||||
{ 0xc27f8304, "ThinkNode_M9_companion_radio_wifi_" },
|
||||
@@ -530,6 +543,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
|
||||
{ 0x475a2a6a, "WioTrackerL1_companion_radio_ble" },
|
||||
{ 0xf4175d2c, "WioTrackerL1_companion_radio_usb" },
|
||||
{ 0x3d831006, "WioTrackerL1_kiss_modem" },
|
||||
{ 0x556eb1bc, "WioTrackerL1_repeater" },
|
||||
{ 0xb67370b3, "WioTrackerL1_repeater_lora_ota_no_external_sensors" },
|
||||
{ 0x0c689b90, "WioTrackerL1_room_server" },
|
||||
{ 0x91eac424, "WioTrackerL1Eink_companion_radio_ble" },
|
||||
|
||||
Reference in new issue
Block a user