1224 lines
62 KiB
C++
1224 lines
62 KiB
C++
#include "OtaCli.h"
|
|
#include "OtaContext.h"
|
|
#include "FolderMotaStore.h" // `ota pull <id> folder` destination (set_mid on the connected folder store)
|
|
#include "OtaVerify.h"
|
|
#include "OtaSelf.h"
|
|
#include "OtaTargets.h" // ota_target_env_name(): human-readable name for a target_id (no string on the wire)
|
|
#if defined(NRF52_PLATFORM)
|
|
#include "OtaBlInfo.h" // installed bootloader application/update capability views
|
|
#endif
|
|
#include "Utils.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <Arduino.h> // millis() for session-age display (device-only command surface)
|
|
|
|
namespace mesh {
|
|
namespace ota {
|
|
|
|
static uint32_t parse_u32(const char* s) {
|
|
uint32_t n = 0;
|
|
while (*s == ' ') s++;
|
|
while (*s >= '0' && *s <= '9') n = n * 10 + (uint32_t)(*s++ - '0');
|
|
return n;
|
|
}
|
|
|
|
static bool parse_page(const char* s, uint16_t& page) {
|
|
while (*s == ' ') s++;
|
|
if (*s == 0) { page = 1; return true; }
|
|
uint32_t n = 0;
|
|
const char* p = s;
|
|
while (*p >= '0' && *p <= '9') {
|
|
n = n * 10 + (uint32_t)(*p++ - '0');
|
|
if (n > 255) return false;
|
|
}
|
|
while (*p == ' ') p++;
|
|
if (*p != 0 || n == 0) return false;
|
|
page = (uint16_t)n;
|
|
return true;
|
|
}
|
|
|
|
static char fstate_char(OtaManager::FetchState s) {
|
|
switch (s) {
|
|
case OtaManager::IDLE: return 'I';
|
|
case OtaManager::WANT_MANIFEST: return 'W';
|
|
case OtaManager::WANT_LEAVES: return 'L';
|
|
case OtaManager::VERIFYING_STAGED: return 'V';
|
|
case OtaManager::FETCHING: return 'F';
|
|
case OtaManager::COMPLETE: return 'C';
|
|
case OtaManager::PAUSED: return 'P';
|
|
default: return 'X';
|
|
}
|
|
}
|
|
|
|
// For users, the only distinction that matters is application full/delta vs the separately gated
|
|
// bootloader package. Which application delta codec is an implementation detail.
|
|
static const char* codec_kind(uint8_t flags, uint8_t c) {
|
|
return (flags & MFLAG_BOOTLOADER) ? "bootloader" : c == CODEC_FULL ? "full" : "delta";
|
|
}
|
|
|
|
static bool parse_hex_exact(const char* text, uint8_t* out, size_t bytes) {
|
|
if (!text || strlen(text) != bytes * 2u) return false;
|
|
for (size_t i = 0; i < bytes * 2u; i++) {
|
|
const char c = text[i];
|
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
|
|
return false;
|
|
}
|
|
return mesh::Utils::fromHex(out, bytes, text);
|
|
}
|
|
|
|
static bool staged_manifest(OtaContext& c, MotaManifest& m) {
|
|
uint8_t hdr[8], manifest[MOTA_MFL];
|
|
const uint32_t total = c.fetch_store.staged_size();
|
|
return total >= 8u + MOTA_MFL + 5u && c.fetch_store.read(0, hdr, sizeof(hdr)) &&
|
|
memcmp(hdr, MOTA_MAGIC, sizeof(MOTA_MAGIC)) == 0 && rd_u32le(hdr + 4) == total &&
|
|
c.fetch_store.read(8, manifest, sizeof(manifest)) &&
|
|
mota_parse_manifest(manifest, sizeof(manifest), m);
|
|
}
|
|
|
|
// A plain-language word for the fetch state (shown in `ota status`).
|
|
static const char* state_word(OtaManager::FetchState s) {
|
|
switch (s) {
|
|
case OtaManager::IDLE: return "idle";
|
|
case OtaManager::WANT_MANIFEST: return "starting";
|
|
case OtaManager::WANT_LEAVES: return "validating seed";
|
|
case OtaManager::VERIFYING_STAGED: return "verifying staged blocks";
|
|
case OtaManager::FETCHING: return "downloading";
|
|
case OtaManager::COMPLETE: return "ready to install";
|
|
case OtaManager::FAILED: return "failed";
|
|
case OtaManager::PAUSED: return "paused (folder link lost - reconnect to resume)";
|
|
default: return "?";
|
|
}
|
|
}
|
|
|
|
// A compact one-word fetch state for the dense `ota stats` line (state_word's phrases are too long).
|
|
static const char* state_short(OtaManager::FetchState s) {
|
|
switch (s) {
|
|
case OtaManager::WANT_MANIFEST: return "manifest";
|
|
case OtaManager::WANT_LEAVES: return "leaves";
|
|
case OtaManager::VERIFYING_STAGED: return "verify";
|
|
case OtaManager::FETCHING: return "dl";
|
|
case OtaManager::COMPLETE: return "done";
|
|
case OtaManager::FAILED: return "failed";
|
|
case OtaManager::PAUSED: return "paused";
|
|
default: return "idle";
|
|
}
|
|
}
|
|
|
|
static const char* fetch_error_word(OtaManager::FetchError error) {
|
|
switch (error) {
|
|
case OtaManager::FETCH_ERROR_MANIFEST: return "invalid manifest";
|
|
case OtaManager::FETCH_ERROR_HASH_ALGO: return "unsupported hash";
|
|
case OtaManager::FETCH_ERROR_VERSION: return "not a newer version";
|
|
case OtaManager::FETCH_ERROR_CODEC: return "unsupported codec";
|
|
case OtaManager::FETCH_ERROR_GEOMETRY: return "invalid geometry";
|
|
case OtaManager::FETCH_ERROR_TOO_LARGE: return "image too large";
|
|
case OtaManager::FETCH_ERROR_STORAGE: return "storage error";
|
|
case OtaManager::FETCH_ERROR_INTEGRITY: return "integrity check";
|
|
case OtaManager::FETCH_ERROR_MANIFEST_TIMEOUT: return "manifest timeout";
|
|
case OtaManager::FETCH_ERROR_LEAVES_TIMEOUT: return "leaves timeout";
|
|
default: return "none";
|
|
}
|
|
}
|
|
|
|
// Render the packed fw_version as "v1.2.3" (or "v1.2.3.4" when a prerelease byte is set).
|
|
static void ver_str(char* out, size_t cap, uint32_t v) {
|
|
FwVersion fw = FwVersion::unpack(v);
|
|
if (fw.prerelease) snprintf(out, cap, "v%u.%u.%u.%u", fw.major, fw.minor, fw.patch, fw.prerelease);
|
|
else snprintf(out, cap, "v%u.%u.%u", fw.major, fw.minor, fw.patch);
|
|
}
|
|
|
|
// Match the first word of `a` against any of the '|'-separated names (so commands have intuitive aliases
|
|
// and short forms); on a match, point `*rest` at the argument text. Keeps the dispatch table readable.
|
|
static bool is_cmd(const char* a, const char* names, const char** rest) {
|
|
size_t tlen = 0; while (a[tlen] && a[tlen] != ' ') tlen++;
|
|
for (const char* s = names; *s; ) {
|
|
const char* d = s; while (*d && *d != '|') d++;
|
|
if ((size_t)(d - s) == tlen && tlen && strncmp(a, s, tlen) == 0) {
|
|
const char* r = a + tlen; while (*r == ' ') r++;
|
|
if (rest) *rest = r;
|
|
return true;
|
|
}
|
|
s = (*d == '|') ? d + 1 : d;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The everyday OTA surface is BitTorrent-shaped: `ota` shows what you're holding (your running firmware
|
|
// as a full mOTA + your one fetch session), `ota neighbors` shows the mOTAs heard around you, `ota pull`
|
|
// starts fetching one, `ota drop` frees the session. The raw primitives (manual content load, low-level
|
|
// apply steps) live under `ota dev ...` so they don't clutter the everyday surface. Every reply fits one
|
|
// packet so it works as remote-admin over LoRa.
|
|
static bool handle_dev(const char* d, char* reply, OtaContext& c);
|
|
|
|
bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board) {
|
|
const char* a = command + 3;
|
|
if (*a != 0 && *a != ' ') return false;
|
|
while (*a == ' ') a++;
|
|
OtaContext& c = ota_ctx();
|
|
const char* rest = a;
|
|
|
|
// ---- raw / internal primitives, tucked under `ota dev ...` ----
|
|
if (is_cmd(a, "dev", &rest)) {
|
|
return handle_dev(rest, reply, c);
|
|
}
|
|
|
|
// ---- help: list the commands in plain words (aliases in parentheses) ----
|
|
if (is_cmd(a, "help|?|h", &rest)) {
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
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) && \
|
|
!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,
|
|
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | "
|
|
"qspi | folder | config | key");
|
|
#else
|
|
strcpy(reply,
|
|
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | qspi | "
|
|
"folder | config | key");
|
|
#endif
|
|
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
strcpy(reply,
|
|
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | "
|
|
"folder | cache | config | key");
|
|
#else
|
|
strcpy(reply,
|
|
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
|
|
"cache | config | key");
|
|
#endif
|
|
#else
|
|
snprintf(reply, 160,
|
|
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
|
|
"cache | config | key. `ota ls [page]`; folder [validate].");
|
|
#endif
|
|
|
|
// ---- inventory dashboard: running fw (self), the one fetch session, serving state ----
|
|
} else if (*a == 0 || is_cmd(a, "status|st", &rest)) {
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
uint8_t dig[4]; c.manager.servedDigest(dig);
|
|
char dighx[9]; mesh::Utils::toHex(dighx, dig, 4);
|
|
snprintf(reply, 160,
|
|
"OTA seeder | install:disabled | target:00000000 | maxblk:%u | folder:%s | "
|
|
"serving:%u dg=%s (source only)",
|
|
(unsigned)ota_max_block_capability(),
|
|
c.folder_active ? c.folder_dest_info : "not connected",
|
|
(unsigned)c.manager.servedCount(), dighx);
|
|
#else
|
|
SelfFwInfo fi; bool s = ota_self_firmware(fi);
|
|
char selfhx[9]; if (s && fi.valid) mesh::Utils::toHex(selfhx, fi.body_hash, 4); else strcpy(selfhx, "?");
|
|
OtaManager::FetchState fs = c.manager.fetchState();
|
|
char dl[80];
|
|
if (fs == OtaManager::IDLE) {
|
|
strcpy(dl, "no download");
|
|
} else {
|
|
char midhx[9]; mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
|
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
|
|
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
|
|
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
|
|
if (fs == OtaManager::FAILED)
|
|
snprintf(dl, sizeof dl, "download: failed (%s) %u/%u id=%s",
|
|
fetch_error_word(c.manager.fetchError()), have, tot, midhx);
|
|
else
|
|
snprintf(dl, sizeof dl, "%sdownload: %s %u/%u (%u%%) id=%s %us",
|
|
c.manager.fetched_is_bootloader() ? "bootloader " : "",
|
|
state_word(fs), have, tot, pct, midhx, age);
|
|
}
|
|
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
|
|
#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).
|
|
const OtaBlCaps& bl = c.bootloaderAppCaps();
|
|
#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
|
|
const char* bl_state = bl.present ? "apply" : "NONE";
|
|
#endif
|
|
const uint8_t last_rc = ota_bootloader_last_rc();
|
|
const char* rc_name = ota_nrf52_boot_update_result(last_rc) ? "blup" : "blrc";
|
|
// Keep the fixed-width parser/capability fields ahead of the variable download text and optional
|
|
// human target name. hw is bounded to 32 bytes, so target, maxblk, and 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 | maxblk:%u | 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(), (unsigned)ota_max_block_capability(),
|
|
bl_state, rc_name, last_rc, dl,
|
|
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
|
(unsigned)c.allow.count(), tenv ? tenv : "?");
|
|
#else
|
|
// target and maxblk precede the variable download text so remote-admin truncation cannot hide the
|
|
// package-geometry preflight fields.
|
|
snprintf(reply, 160,
|
|
"OTA | this fw %s (%uK) hw=%s | target:%08X | maxblk:%u | %s | "
|
|
"serving:%s (%u) | keys:%u | env:%s",
|
|
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw,
|
|
(unsigned)c.manager.target(), (unsigned)ota_max_block_capability(), dl,
|
|
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
|
(unsigned)c.allow.count(), tenv ? tenv : "?");
|
|
#endif
|
|
#endif
|
|
|
|
// ---- admin OTA stats: crypto identities (our fw's content-id + body_hash), serving set, live fetch,
|
|
// policy - one dense line. The remote-admin CLI path is admin-gated, so this is admin-only over the
|
|
// mesh (send it from the app's repeater command screen, or the WiFi/serial OTA console).
|
|
} else if (is_cmd(a, "stats", &rest)) {
|
|
// our running fw's merkle content-id (mid) comes from the self serve entry; the EndF body_hash (fw
|
|
// identity, matched against a delta base) is separate - surface BOTH (only body_hash showed before).
|
|
const OtaManager::ServeEntry* self = nullptr;
|
|
for (uint8_t i = 0; i < c.manager.servedCount(); i++) {
|
|
const OtaManager::ServeEntry* e = c.manager.servedEntry(i);
|
|
if (e && e->is_self) { self = e; break; }
|
|
}
|
|
SelfFwInfo fi; bool sok = ota_self_firmware(fi);
|
|
char midhx[9], bodyhx[9], verbuf[20], dighx[9];
|
|
if (self) mesh::Utils::toHex(midhx, self->mid, 4); else strcpy(midhx, "?");
|
|
if (sok && fi.valid) mesh::Utils::toHex(bodyhx, fi.body_hash, 4); else strcpy(bodyhx, "?");
|
|
if (self) ver_str(verbuf, sizeof verbuf, self->fw_version); else strcpy(verbuf, "v?");
|
|
uint8_t dig[4]; c.manager.servedDigest(dig); mesh::Utils::toHex(dighx, dig, 4);
|
|
OtaManager::FetchState fs = c.manager.fetchState();
|
|
char fbuf[72];
|
|
if (fs == OtaManager::IDLE) {
|
|
strcpy(fbuf, "fetch idle");
|
|
} else {
|
|
char fmid[9]; mesh::Utils::toHex(fmid, c.manager.fetchManifestId(), 4);
|
|
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
|
|
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
|
|
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
|
|
if (fs == OtaManager::FAILED)
|
|
snprintf(fbuf, sizeof fbuf, "fetch failed:%s %u/%u id=%s",
|
|
fetch_error_word(c.manager.fetchError()), have, tot, fmid);
|
|
else
|
|
snprintf(fbuf, sizeof fbuf, "fetch %s %u/%u %u%% id=%s %us",
|
|
state_short(fs), have, tot, pct, fmid, age);
|
|
}
|
|
uint8_t af = c.manager.autofetch();
|
|
snprintf(reply, 160, "OTA | fw %s id=%s body=%s %ub %uK | serv %u dg=%s | %s | af=%s hops=%u",
|
|
verbuf, midhx, bodyhx, (unsigned)(self ? self->have_count : 0),
|
|
(unsigned)((sok ? fi.image_len : 0) / 1024), (unsigned)c.manager.servedCount(), dighx, fbuf,
|
|
af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off",
|
|
(unsigned)c.manager.max_hops());
|
|
|
|
// ---- what's available around me (catalogued from beacons + OTA_HAVE), best/most-recent first ----
|
|
} else if (is_cmd(a, "neighbors|nbrs|updates|ls|n", &rest)) {
|
|
// Kick a fresh round of catalog queries (async - rows arrive over the next seconds); render what we
|
|
// have now in plain words. The reply buffer is 160 B (serial / one LoRa packet for remote-admin), so
|
|
// the retained protocol catalog is rendered two rows at a time.
|
|
uint16_t page;
|
|
if (!parse_page(rest, page)) { strcpy(reply, "ERR usage: ota ls [page]"); return true; }
|
|
c.manager.queryAll();
|
|
const int CAP = 160;
|
|
static const uint8_t PAGE_SIZE = 2;
|
|
uint16_t count = c.manager.catalogCount();
|
|
uint16_t pages = count ? (uint16_t)((count + PAGE_SIZE - 1) / PAGE_SIZE) : 1;
|
|
if (page > pages) {
|
|
snprintf(reply, CAP, "ERR update page %u out of range (1-%u)", (unsigned)page, (unsigned)pages);
|
|
return true;
|
|
}
|
|
int n = snprintf(reply, CAP, "Updates %u/%u (%u src; refreshing):",
|
|
(unsigned)page, (unsigned)pages, (unsigned)c.manager.sourceCount());
|
|
OtaManager::FetchState fs = c.manager.fetchState();
|
|
const uint8_t* cur = (fs != OtaManager::IDLE) ? c.manager.fetchManifestId() : nullptr;
|
|
uint32_t myt = c.manager.target(); // effective target (EndF identity if present, else build flag)
|
|
#if defined(NRF52_PLATFORM)
|
|
const OtaBlCaps& list_bl = c.bootloaderAppCaps();
|
|
#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
|
defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
const OtaBlCaps& list_bl_update = c.bootloaderUpdateCaps();
|
|
#endif
|
|
#endif
|
|
#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
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
SelfFwInfo list_sd_self;
|
|
const bool list_sd_headroom = ota_self_firmware(list_sd_self) &&
|
|
ota_bootloader_scratch_headroom_valid(
|
|
list_sd_self.valid, mota_nrf52_app_base(), list_sd_self.image_len,
|
|
OTA_BOOT_SCRATCH_START) &&
|
|
ota_bootloader_live_bank_preserves_scratch(
|
|
mota_nrf52_app_base(), list_sd_self.image_len,
|
|
OTA_BOOT_SCRATCH_START);
|
|
#endif
|
|
uint32_t now = millis(); int shown = 0;
|
|
uint16_t first = (uint16_t)(page - 1) * PAGE_SIZE;
|
|
uint16_t last = first + PAGE_SIZE; if (last > count) last = count;
|
|
for (uint16_t i = first; i < last; i++) {
|
|
const OtaManager::CatRow* h = c.manager.catalogRow(i);
|
|
if (!h || CAP - n < 24) break;
|
|
bool on = cur && memcmp(cur, h->mid, 4) == 0;
|
|
// Tag the active session by real fetch state (not always "downloading" - COMPLETE is ready).
|
|
const char* tag = "";
|
|
if (on) {
|
|
if (fs == OtaManager::COMPLETE) tag = " [ready]";
|
|
else if (fs == OtaManager::PAUSED) tag = " [paused]";
|
|
else if (fs == OtaManager::FAILED) tag = " [failed]";
|
|
else tag = " [downloading]"; // WANT_*/FETCHING
|
|
}
|
|
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 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_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
|
defined(OTA_SD_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 == ota_bootloader_target_id(bid) &&
|
|
ota_bootloader_self_update_caps_valid(list_bl_update);
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
installable = installable && list_sd_headroom;
|
|
#endif
|
|
#endif
|
|
fit = installable ? "yours" : "bootloader unsupported";
|
|
} else if (myt && h->target_id == myt) {
|
|
bool installable = c.manager.codecOk(h->codec);
|
|
#if defined(NRF52_PLATFORM)
|
|
installable = installable && list_bl.present && list_bl.apply_abi >= MOTA_FORMAT_VER
|
|
&& 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_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
|
|
else fit = "same target";
|
|
} else if (env) fit = env;
|
|
else if (h->target_id == 0) fit = "?";
|
|
else { snprintf(hwbuf, sizeof hwbuf, "hw %08X", (unsigned)h->target_id); fit = hwbuf; }
|
|
char midhx[9]; mesh::Utils::toHex(midhx, h->mid, 4);
|
|
n += snprintf(reply + n, CAP - n, "\n %u) %s %s %s [%s] %un %us%s",
|
|
(unsigned)(i + 1), midhx, ver, codec_kind(h->flags, h->codec), fit,
|
|
(unsigned)h->n_seeders, (unsigned)age, tag);
|
|
shown++;
|
|
}
|
|
if (shown == 0) strcpy(reply, "No updates seen yet - re-run `ota ls` in a few seconds (just asked around).");
|
|
|
|
// ---- 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).
|
|
char selstr[24]; int i = 0;
|
|
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, 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
|
|
// values retain the legacy index form; `#N` is the unambiguous explicit index spelling.
|
|
size_t selector_len = strlen(selstr);
|
|
bool explicit_index = selstr[0] == '#';
|
|
const char* index_text = explicit_index ? selstr + 1 : selstr;
|
|
bool decimal = *index_text != 0;
|
|
uint16_t index = 0;
|
|
for (const char* x = index_text; *x && decimal; x++) {
|
|
if (*x < '0' || *x > '9') decimal = false;
|
|
else if (index > 255 / 10 || (index == 255 / 10 && (uint8_t)(*x - '0') > 255 % 10)) decimal = false;
|
|
else index = (uint16_t)(index * 10 + (uint8_t)(*x - '0'));
|
|
}
|
|
bool use_index = explicit_index || (selector_len != 8 && decimal);
|
|
if (use_index && decimal) {
|
|
if (index >= 1 && index <= c.manager.catalogCount()) sel = c.manager.catalogRow((uint8_t)(index - 1));
|
|
} else if (!explicit_index && selector_len == 8 && mesh::Utils::fromHex(mid, 4, selstr)) {
|
|
for (uint8_t k = 0; k < c.manager.catalogCount(); k++)
|
|
if (memcmp(c.manager.catalogRow(k)->mid, mid, 4) == 0) { sel = c.manager.catalogRow(k); break; }
|
|
}
|
|
if (!sel) { strcpy(reply, "ERR no such update (copy its eight-digit ID from `ota ls`)"); return true; }
|
|
// destination is MANDATORY: with none given, show the choices (flash always; folder iff a link is up).
|
|
if (*dst == 0) {
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
if (c.folder_dest)
|
|
snprintf(reply, 160, "choose a destination: `ota pull %s folder` (folder: %s)",
|
|
selstr, c.folder_dest_info);
|
|
else
|
|
strcpy(reply, "ERR no folder connected (run motatool serve --tcp)");
|
|
#else
|
|
if (c.folder_dest)
|
|
snprintf(reply, 160, "choose a destination: `ota pull %s flash` | `ota pull %s folder` (folder: %s)",
|
|
selstr, selstr, c.folder_dest_info);
|
|
else
|
|
snprintf(reply, 160, "choose a destination: `ota pull %s flash` (folder: none connected - motatool serve)",
|
|
selstr);
|
|
#endif
|
|
return true;
|
|
}
|
|
if (c.apply_pending) { strcpy(reply, "ERR busy applying"); return true; }
|
|
// Parse exact destination/options. Prefix matches used to accept typos such as "flashgarbage" and an
|
|
// ignored third token, which is especially unsafe for the explicit no-EndF rescue acknowledgement.
|
|
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) {
|
|
snprintf(reply, 160, "ERR %s", pull_usage);
|
|
return true;
|
|
}
|
|
bool to_flash = strcmp(destination, "flash") == 0;
|
|
bool to_folder = strcmp(destination, "folder") == 0;
|
|
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)) {
|
|
snprintf(reply, 160, "ERR %s", pull_usage);
|
|
return true;
|
|
}
|
|
|
|
uint8_t selmid[4]; uint32_t seltgt = sel->target_id; uint8_t selcodec = sel->codec;
|
|
const uint8_t selflags = sel->flags;
|
|
const bool selboot = (selflags & MFLAG_BOOTLOADER) != 0;
|
|
memcpy(selmid, sel->mid, 4); // sel may move when catalog traffic arrives
|
|
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
c.stopSdCacheFetch(); // explicit operator work owns the receive slot
|
|
#endif
|
|
OtaManager::FetchState current = c.manager.fetchState();
|
|
if (current != OtaManager::IDLE && current != OtaManager::FAILED) {
|
|
snprintf(reply, 160, "ERR OTA slot is %s; use `ota cancel` before replacing it", state_word(current));
|
|
return true;
|
|
}
|
|
|
|
OtaStore* store = nullptr; const char* dname = nullptr;
|
|
if (to_flash) {
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
strcpy(reply, "ERR seeder-only build cannot stage or install firmware; use `folder`");
|
|
return true;
|
|
#else
|
|
if (selboot) {
|
|
#if defined(NRF52_PLATFORM) && \
|
|
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
|
defined(OTA_SD_BOOTLOADER_UPDATE))
|
|
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
|
const OtaBlCaps& bl = c.bootloaderUpdateCaps();
|
|
if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) ||
|
|
selcodec != CODEC_FULL) {
|
|
strcpy(reply, "ERR malformed bootloader catalog row; capture it to folder for inspection");
|
|
return true;
|
|
}
|
|
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 (!ota_bootloader_self_update_caps_valid(bl)) {
|
|
strcpy(reply, "ERR installed bootloader lacks safe LoRa bootloader-update support");
|
|
return true;
|
|
}
|
|
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
|
SelfFwInfo sd_self;
|
|
if (!ota_self_firmware(sd_self) ||
|
|
!ota_bootloader_scratch_headroom_valid(
|
|
sd_self.valid, mota_nrf52_app_base(), sd_self.image_len,
|
|
OTA_BOOT_SCRATCH_START) ||
|
|
!ota_bootloader_live_bank_preserves_scratch(
|
|
mota_nrf52_app_base(), sd_self.image_len,
|
|
OTA_BOOT_SCRATCH_START)) {
|
|
strcpy(reply, "ERR running firmware/settings do not preserve E0000 scratch; use local DFU/SWD");
|
|
return true;
|
|
}
|
|
#endif
|
|
#else
|
|
strcpy(reply, "ERR this build cannot install bootloader packages; use folder capture or USB DFU");
|
|
return true;
|
|
#endif
|
|
} else if (!c.manager.codecOk(selcodec)) {
|
|
snprintf(reply, 160,
|
|
"ERR %s codec %u cannot be installed by this build; use `ota pull %s folder` to capture it",
|
|
codec_kind(selflags, selcodec), (unsigned)selcodec, selstr);
|
|
return true;
|
|
}
|
|
#if defined(NRF52_PLATFORM)
|
|
const OtaBlCaps& bl = selboot ? c.bootloaderUpdateCaps()
|
|
: c.bootloaderAppCaps();
|
|
if (!bl.present) {
|
|
strcpy(reply, "ERR bootloader has no mOTA apply support; update it over USB first");
|
|
return true;
|
|
}
|
|
const uint8_t need_abi = selboot ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER;
|
|
if (bl.apply_abi < need_abi || selcodec >= 16 || !(bl.codec_mask & (1u << selcodec))) {
|
|
snprintf(reply, 160, "ERR bootloader cannot apply mOTA ABI %u codec %u (has abi=%u codecs=0x%x)",
|
|
need_abi, (unsigned)selcodec, bl.apply_abi, bl.codec_mask);
|
|
return true;
|
|
}
|
|
#if defined(OTA_SD_STORE)
|
|
if (!(bl.storage_flags & OTA_BL_STORAGE_SD)) {
|
|
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) && \
|
|
!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>`",
|
|
selstr);
|
|
return true;
|
|
}
|
|
if (!has_endf && seltgt != c.manager.target()) {
|
|
strcpy(reply, "ERR rescue requires an update for this exact target");
|
|
return true;
|
|
}
|
|
if (has_endf && rescue) {
|
|
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");
|
|
return true;
|
|
}
|
|
#endif
|
|
store = &c.fetch_store; dname = rescue ? "flash+rescue" : "flash";
|
|
#endif
|
|
} else if (to_folder) {
|
|
if (!c.folder_dest) { strcpy(reply, "ERR no folder connected (run motatool serve --tcp/--serial)"); return true; }
|
|
c.folder_dest->set_mid(selmid); store = c.folder_dest; dname = validate ? "folder+validate" : "folder";
|
|
}
|
|
c.manager.reset_session();
|
|
c.fetch_to_folder = to_folder;
|
|
c.manager.set_fetch_store(store); // stage this pull to the chosen destination
|
|
OtaManager::PullResult result = to_folder
|
|
? c.manager.pull_archive(selmid, seltgt, validate)
|
|
: c.manager.pull(selmid, seltgt, false);
|
|
char midhx[9]; mesh::Utils::toHex(midhx, selmid, 4);
|
|
if (result != OtaManager::PULL_STARTED && result != OtaManager::PULL_RESUMED) {
|
|
c.manager.reset_session();
|
|
c.fetch_to_folder = false;
|
|
c.manager.set_fetch_store(&c.fetch_store);
|
|
const char* why = result == OtaManager::PULL_NO_STORE ? "no destination store"
|
|
: result == OtaManager::PULL_BUSY ? "receive slot busy" : "invalid manifest id";
|
|
snprintf(reply, 160, "ERR pull did not start: %s", why);
|
|
return true;
|
|
}
|
|
snprintf(reply, 160, "OK %s mid=%s -> %s (primary traffic)",
|
|
result == OtaManager::PULL_RESUMED ? "resuming" : "pulling", midhx, dname);
|
|
|
|
// ---- discard the current session (e.g. a stalled old fetch) to free the slot ----
|
|
} else if (is_cmd(a, "drop|cancel|stop", &rest)) {
|
|
if (c.apply_pending) { strcpy(reply, "ERR update is armed; reboot is pending"); return true; }
|
|
OtaManager::FetchState fs = c.manager.fetchState();
|
|
const bool was_folder = c.fetch_to_folder;
|
|
bool was_sd_archive = false;
|
|
char midhx[9]; strcpy(midhx, "-");
|
|
if (fs != OtaManager::IDLE) mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
was_sd_archive = c.sdCacheFetching();
|
|
c.stopSdCacheFetch();
|
|
#endif
|
|
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
|
|
if (was_folder && c.folder_dest) c.folder_dest->clear();
|
|
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) && !defined(OTA_QSPI_STORE)
|
|
c.manager.set_accept_full(false);
|
|
#endif
|
|
const bool discarded = !was_folder && !was_sd_archive &&
|
|
c.fetch_store.discard();
|
|
// Fetch cancellation and serving are independent. In particular, a
|
|
// manual ESP32 serve view can point into serve_buf, so leave the manager
|
|
// view and its caller-owned buffer intact until `ota dev clear` (which
|
|
// detaches the view before releasing the buffer).
|
|
c.session_started_ms = 0;
|
|
if (was_folder) {
|
|
snprintf(reply, 160,
|
|
"OK dropped folder session (was %c mid=%s); host file left untouched",
|
|
fstate_char(fs), midhx);
|
|
} else if (was_sd_archive) {
|
|
snprintf(reply, 160,
|
|
"OK dropped SD archive session (was %c mid=%s); partial retained for resume",
|
|
fstate_char(fs), midhx);
|
|
} else if (!discarded) {
|
|
snprintf(reply, 160,
|
|
"ERR dropped live session (was %c mid=%s), but persistent OTA slot invalidation failed",
|
|
fstate_char(fs), midhx);
|
|
} else {
|
|
snprintf(reply, 160,
|
|
"OK dropped session (was %c mid=%s); OTA receive slot confirmed clear",
|
|
fstate_char(fs), midhx);
|
|
}
|
|
|
|
// ---- broadcast our tiny beacon so peers discover us. If not already serving, set up flash-backed
|
|
// self-serve first (so we're a real, fetchable source of our own running firmware). ----
|
|
} else if (is_cmd(a, "announce|adv", &rest)) {
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
c.manager.announce();
|
|
snprintf(reply, 160, "OK beacon sent (serving=%u host mOTA)",
|
|
(unsigned)c.manager.servedCount());
|
|
#else
|
|
if (!c.serving) c.serving = ota_serve_self(c, 0);
|
|
c.manager.announce();
|
|
sprintf(reply, "OK beacon sent (serving=%s)", c.serving ? "self fw" : "nothing");
|
|
#endif
|
|
|
|
// ---- raw-QSPI staging diagnostics (read-only probe; preserves a latched fetch failure) ----
|
|
} else if (is_cmd(a, "qspi|storage", &rest)) {
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
|
// This probe only reads JEDEC/SR1. capacity() deliberately preserves a
|
|
// latched fetch failure so asking for diagnostics cannot erase its cause.
|
|
uint32_t qspi_capacity = c.fetch_store.capacity();
|
|
const char *error = c.fetch_store.last_error();
|
|
snprintf(reply, 160, "QSPI jedec=%06lX size=%luK sr1=%02X stage=%s%s%s",
|
|
(unsigned long)c.fetch_store.jedec_id(),
|
|
(unsigned long)(qspi_capacity / 1024), c.fetch_store.status1(),
|
|
c.fetch_store.last_stage(), error[0] ? " error=" : "", error);
|
|
#else
|
|
strcpy(reply, "ERR this build does not use a QSPI OTA store");
|
|
#endif
|
|
|
|
// ---- running firmware identity (compare against a delta's base_hash) ----
|
|
} else if (is_cmd(a, "self|id", &rest)) {
|
|
SelfFwInfo fi;
|
|
if (!ota_self_firmware(fi) || !fi.valid) { strcpy(reply, "ERR no EndF (firmware lacks the trailer?)"); return true; }
|
|
char hx[17]; mesh::Utils::toHex(hx, fi.body_hash, 8);
|
|
// maxblk is before the optional bootloader diagnostics so it survives the 160-byte remote reply cap.
|
|
int n = snprintf(reply, 160, "self body=%u image=%u base_hash=%s | maxblk:%u",
|
|
(unsigned)fi.body_len, (unsigned)fi.image_len, hx,
|
|
(unsigned)ota_max_block_capability());
|
|
#if defined(NRF52_PLATFORM)
|
|
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store.
|
|
const OtaBlCaps& bl = c.bootloaderAppCaps(); // cached (flash scanned once)
|
|
#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
|
|
snprintf(reply + n, 160 - n, " | bootloader: NO SD mota-apply support (install will refuse)");
|
|
#else
|
|
if (bl.present) {
|
|
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling(bl);
|
|
snprintf(reply + n, 160 - n,
|
|
" | bootloader: apply OK (abi=%u codecs=0x%x stage=%05X)",
|
|
bl.apply_abi, bl.codec_mask, (unsigned)stage_ceiling);
|
|
}
|
|
else snprintf(reply + n, 160 - n, " | bootloader: NO mota-apply support (delta install will refuse)");
|
|
#endif
|
|
#endif
|
|
|
|
} else if (is_cmd(a, "rescue", &rest)) {
|
|
// Recovery-only nRF52 handoff for firmware whose normal EndF validation is broken. This command is
|
|
// 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_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;
|
|
}
|
|
const char* hash_text = nullptr;
|
|
uint8_t operator_base_hash[8];
|
|
if (!is_cmd(rest, "install", &hash_text) ||
|
|
!mesh::Utils::fromHex(operator_base_hash, sizeof(operator_base_hash), hash_text)) {
|
|
strcpy(reply, "ERR usage: ota rescue install <hash16>");
|
|
return true;
|
|
}
|
|
if (c.manager.fetchState() != OtaManager::COMPLETE || c.fetch_store.staged_size() == 0) {
|
|
sprintf(reply, "ERR no complete update fetched (fetch=%c %u/%u)",
|
|
fstate_char(c.manager.fetchState()), (unsigned)c.manager.blocksHave(),
|
|
(unsigned)c.manager.blocksTotal());
|
|
return true;
|
|
}
|
|
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_SEEDER_ONLY) && \
|
|
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
|
defined(OTA_SD_BOOTLOADER_UPDATE))
|
|
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
|
const OtaBlCaps& bl = c.bootloaderUpdateCaps();
|
|
if (*rest == 0 || strcmp(rest, "status") == 0) {
|
|
if (!bid.present || !bid.crc_ok) {
|
|
strcpy(reply, "Bootloader update unavailable: installed embedded manifest/CRC is invalid");
|
|
return true;
|
|
}
|
|
if (!ota_bootloader_self_update_caps_valid(bl)) {
|
|
strcpy(reply, "Bootloader update unavailable: installed bootloader supports application OTA only");
|
|
return true;
|
|
}
|
|
MotaManifest staged;
|
|
char midhx[9] = "-", hashhx[17] = "-";
|
|
const bool ready = c.manager.fetchState() == OtaManager::COMPLETE &&
|
|
staged_manifest(c, staged) && staged.is_bootloader();
|
|
if (ready) {
|
|
mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
|
mesh::Utils::toHex(hashhx, staged.image_hash, 8);
|
|
}
|
|
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;
|
|
}
|
|
const char* confirm = nullptr;
|
|
if (!is_cmd(rest, "install", &confirm)) {
|
|
strcpy(reply, "ERR usage: ota bootloader install <MID8> <HASH16>"); return true;
|
|
}
|
|
char midtxt[9] = {0}, hashtxt[17] = {0}, extra[2] = {0};
|
|
if (sscanf(confirm, "%8s %16s %1s", midtxt, hashtxt, extra) != 2) {
|
|
strcpy(reply, "ERR usage: ota bootloader install <MID8> <HASH16>"); return true;
|
|
}
|
|
uint8_t operator_mid[4], operator_hash8[8];
|
|
if (!parse_hex_exact(midtxt, operator_mid, sizeof(operator_mid)) ||
|
|
!parse_hex_exact(hashtxt, operator_hash8, sizeof(operator_hash8))) {
|
|
strcpy(reply, "ERR MID must be 8 hex and HASH must be 16 hex characters"); return true;
|
|
}
|
|
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 install storage"); return true;
|
|
}
|
|
char m2[100];
|
|
const bool ok = c.apply_fetched_bootloader(operator_mid, operator_hash8, m2);
|
|
snprintf(reply, 160, "%s | %s", ok ? "OK" : "ERR", m2);
|
|
#else
|
|
strcpy(reply, "ERR this build cannot update its bootloader over LoRa");
|
|
#endif
|
|
|
|
} else if (is_cmd(a, "install|apply|applydelta", &rest)) {
|
|
// Apply the fetched update. Destructive (reflashes + reboots) and GATED, not interactive (no "type
|
|
// yes" round-trip - unreliable over LoRa): refuse unless the fetch is COMPLETE, then the apply path
|
|
// validates in order (payload hash -> built-for-this-firmware -> signature/trust) and returns the
|
|
// FIRST failing gate, so the operator knows exactly why it refused; it proceeds only if all pass.
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
strcpy(reply, "ERR seeder-only build cannot install firmware via LoRa");
|
|
return true;
|
|
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
if (c.sdCacheFetching()) {
|
|
strcpy(reply, "ERR SD archive capture is active; use `ota cancel` before installing");
|
|
return true;
|
|
}
|
|
#endif
|
|
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;
|
|
}
|
|
if (c.manager.fetchState() != OtaManager::COMPLETE || c.fetch_store.staged_size() == 0) {
|
|
sprintf(reply, "ERR no complete update fetched (fetch=%c %u/%u)",
|
|
fstate_char(c.manager.fetchState()), (unsigned)c.manager.blocksHave(),
|
|
(unsigned)c.manager.blocksTotal());
|
|
return true;
|
|
}
|
|
// On success the slot is armed but NOT yet rebooted - defer so this reply reaches the operator first;
|
|
// the mesh loop reboots once it has been transmitted (same path used by auto-install).
|
|
char m2[100];
|
|
bool ok = c.apply_fetched(m2);
|
|
sprintf(reply, "%s | %s", ok ? "OK" : "ERR", m2);
|
|
|
|
// ---- external folder relay: advertise + serve `.mota` from a host daemon over the seeder UART, so the
|
|
// node hosts MANY images (any architecture) it doesn't hold in flash. Trustless (fetchers verify). --
|
|
} else if (is_cmd(a, "folder|fold", &rest)) {
|
|
const char* p = rest;
|
|
if (strcmp(p, "on") == 0) {
|
|
#if defined(OTA_FOLDER_SERIAL)
|
|
#if !defined(OTA_SEEDER_ONLY)
|
|
if (!c.serving) c.serving = ota_serve_self(c, 0); // keep serving our own fw alongside the folder
|
|
#endif
|
|
char m2[120]; c.attach_folder(m2, sizeof(m2)); c.manager.announce();
|
|
strncpy(reply, m2, 159); reply[159] = 0;
|
|
#else
|
|
strcpy(reply, "ERR not built with OTA_FOLDER_SERIAL (set the seeder UART in platformio.ini)");
|
|
#endif
|
|
} else if (strcmp(p, "off") == 0) {
|
|
c.detach_folder(); c.manager.announce();
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
strcpy(reply, "OK folder detached (serving nothing)");
|
|
#else
|
|
strcpy(reply, "OK folder detached (still serving own fw)");
|
|
#endif
|
|
} else if (*p == 0) { // status + list served entries (* = our own fw)
|
|
uint16_t offered = 0, advertised = 0;
|
|
bool have_stats = c.folderSourceStats(offered, advertised);
|
|
int n = have_stats
|
|
? snprintf(reply, 159, "folder=%s host=%u/%u serving=%u:", c.folder_active ? "on" : "off",
|
|
(unsigned)advertised, (unsigned)offered, (unsigned)c.manager.servedCount())
|
|
: snprintf(reply, 159, "folder=%s serving=%u:", c.folder_active ? "on" : "off",
|
|
(unsigned)c.manager.servedCount());
|
|
for (uint8_t i = 0; i < c.manager.servedCount() && n < 148; i++) {
|
|
const OtaManager::ServeEntry* e = c.manager.servedEntry(i);
|
|
if (!e) break;
|
|
char midhx[9]; mesh::Utils::toHex(midhx, e->mid, 4);
|
|
n += snprintf(reply + n, 159 - n, " %s%s/%08X", e->is_self ? "*" : "", midhx, (unsigned)e->target_id);
|
|
}
|
|
} else {
|
|
strcpy(reply, "ERR usage: ota folder [on|off]");
|
|
}
|
|
|
|
// ---- SD OTA archive: default-on capture of every advertised mOTA, retained and served after reboot. ----
|
|
} else if (is_cmd(a, "cache|archive|seed", &rest)) {
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
const char* p = rest;
|
|
if (strcmp(p, "on") == 0 || strcmp(p, "off") == 0) {
|
|
bool enabled = p[1] == 'n';
|
|
if (!c.setSdCacheEnabled(enabled)) {
|
|
snprintf(reply, 160, "ERR SD OTA archive setting failed: %s", c.sd_cache.last_error());
|
|
} else {
|
|
snprintf(reply, 160, "OK SD OTA archive capture %s (saved on card; cached files still seed)",
|
|
enabled ? "on" : "off");
|
|
}
|
|
} else if (*p != 0) {
|
|
strcpy(reply, "ERR usage: ota cache [on|off]");
|
|
} else if (!c.ensureSdCache()) {
|
|
snprintf(reply, 160, "SD OTA archive unavailable: %s", c.sd_cache.last_error());
|
|
} else {
|
|
OtaManager::FetchState fs = c.manager.fetchState();
|
|
if (c.sdCacheFetching()) {
|
|
char midhx[9]; mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
|
unsigned have = (unsigned)c.manager.blocksHave(), total = (unsigned)c.manager.blocksTotal();
|
|
snprintf(reply, 160, "SD OTA archive: capture=%s cached=%u, saving %s %u/%u",
|
|
c.sd_cache.autoCaptureEnabled() ? "on" : "off",
|
|
(unsigned)c.sd_cache.capturedCount(), midhx, have, total);
|
|
} else {
|
|
snprintf(reply, 160, "SD OTA archive: capture=%s cached=%u, %s; `ota cache off` stops new saves",
|
|
c.sd_cache.autoCaptureEnabled() ? "on" : "off",
|
|
(unsigned)c.sd_cache.capturedCount(), state_short(fs));
|
|
}
|
|
}
|
|
#else
|
|
strcpy(reply, "ERR this target has no SD-backed OTA archive");
|
|
#endif
|
|
|
|
// ---- policy config (persisted via NodePrefs). conservative defaults: autofetch/autoinstall off ----
|
|
} else if (is_cmd(a, "config|cfg|set", &rest)) {
|
|
const char* p = rest;
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
if (strncmp(p, "cache ", 6) == 0 || strncmp(p, "sdseed ", 7) == 0) {
|
|
const char* v = p + (p[0] == 'c' ? 6 : 7);
|
|
bool enabled;
|
|
if (strcmp(v, "on") == 0) enabled = true;
|
|
else if (strcmp(v, "off") == 0) enabled = false;
|
|
else { strcpy(reply, "ERR usage: ota config cache <on|off>"); return true; }
|
|
if (!c.setSdCacheEnabled(enabled))
|
|
snprintf(reply, 160, "ERR SD OTA archive setting failed: %s", c.sd_cache.last_error());
|
|
else
|
|
snprintf(reply, 160, "OK SD OTA archive capture %s (saved on card)", enabled ? "on" : "off");
|
|
} else
|
|
#endif
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
if (strncmp(p, "autofetch ", 10) == 0 || strncmp(p, "autoinstall ", 12) == 0) {
|
|
strcpy(reply, "ERR seeder-only build keeps autofetch and autoinstall off");
|
|
} else
|
|
#endif
|
|
if (strncmp(p, "autofetch ", 10) == 0) {
|
|
const char* v = p + 10;
|
|
uint8_t pol = strncmp(v, "any", 3) == 0 ? OtaManager::AUTOFETCH_ANY
|
|
: strncmp(v, "signed", 6) == 0 ? OtaManager::AUTOFETCH_SIGNED
|
|
: strncmp(v, "off", 3) == 0 ? OtaManager::AUTOFETCH_OFF : 0xFF;
|
|
if (pol == 0xFF) { strcpy(reply, "ERR usage: ota config autofetch <off|any|signed>"); return true; }
|
|
c.manager.set_autofetch(pol); c.config_dirty = true; strcpy(reply, "OK autofetch updated (saved)");
|
|
} else if (strncmp(p, "autoinstall ", 12) == 0) {
|
|
const char* v = p + 12;
|
|
uint8_t pol = strncmp(v, "trusted", 7) == 0 ? OtaContext::AUTOINSTALL_TRUSTED
|
|
: strncmp(v, "off", 3) == 0 ? OtaContext::AUTOINSTALL_OFF : 0xFF;
|
|
if (pol == 0xFF) { strcpy(reply, "ERR usage: ota config autoinstall <off|trusted>"); return true; }
|
|
c.autoinstall = pol; c.config_dirty = true; strcpy(reply, "OK autoinstall updated (saved)");
|
|
} else if (strncmp(p, "checkpoint ", 11) == 0) { // resume checkpoint cadence (blocks; 0=never)
|
|
long n = atol(p + 11);
|
|
if (n < 0 || n > 4096) { strcpy(reply, "ERR usage: ota config checkpoint <0..4096> (blocks; 0=never)"); return true; }
|
|
c.manager.set_checkpoint_blocks((uint16_t)n); c.config_dirty = true;
|
|
sprintf(reply, "OK checkpoint every %ld blocks (saved)%s", n, n == 0 ? " - periodic resume disabled" : "");
|
|
} else if (strncmp(p, "advert ", 7) == 0) { // beacon re-advertise cadence (minutes; 0=disable)
|
|
long m = atol(p + 7);
|
|
if (m < 0 || m > 10080) { strcpy(reply, "ERR usage: ota config advert <0..10080> (minutes; 0=disable)"); return true; }
|
|
c.manager.set_advert_mins((uint16_t)m); c.config_dirty = true;
|
|
sprintf(reply, "OK re-advertise every %ld min (saved)%s", m, m == 0 ? " - periodic advert disabled" : "");
|
|
} else if (strncmp(p, "hops ", 5) == 0) { // OTA flood reach in hops (0 = direct only)
|
|
long h = atol(p + 5);
|
|
if (h < 0 || h > 8) { strcpy(reply, "ERR usage: ota config hops <0..8> (hops; 0 = direct only)"); return true; }
|
|
c.manager.set_max_hops((uint8_t)h); c.config_dirty = true;
|
|
sprintf(reply, "OK OTA reach = %ld hop%s (saved)%s", h, h == 1 ? "" : "s", h == 0 ? " - direct only" : "");
|
|
} else { // show current policy
|
|
uint8_t af = c.manager.autofetch();
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
bool cache_ready = c.ensureSdCache();
|
|
sprintf(reply, "ota config: cache=%s/%u autofetch=%s autoinstall=%s checkpoint=%u advert=%umin hops=%u keys=%u",
|
|
cache_ready ? (c.sd_cache.autoCaptureEnabled() ? "on" : "off") : "unavailable",
|
|
cache_ready ? (unsigned)c.sd_cache.capturedCount() : 0,
|
|
af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off",
|
|
c.autoinstall == OtaContext::AUTOINSTALL_TRUSTED ? "trusted" : "off",
|
|
(unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(),
|
|
(unsigned)c.manager.max_hops(), (unsigned)c.allow.count());
|
|
#else
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
sprintf(reply, "ota config: mode=seeder-only autofetch=off autoinstall=off checkpoint=%u advert=%umin hops=%u",
|
|
(unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(),
|
|
(unsigned)c.manager.max_hops());
|
|
#else
|
|
sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u advert=%umin hops=%u keys=%u (persisted)",
|
|
af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off",
|
|
c.autoinstall == OtaContext::AUTOINSTALL_TRUSTED ? "trusted" : "off",
|
|
(unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(),
|
|
(unsigned)c.manager.max_hops(), (unsigned)c.allow.count());
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
// ---- trusted signer allowlist (security config; persisted): `ota key add|rm <hex>` / `ota key` lists ----
|
|
} else if (is_cmd(a, "key|keys", &rest)) {
|
|
const char* p = rest;
|
|
if (strncmp(p, "add ", 4) == 0) {
|
|
uint8_t pub[32];
|
|
if (mesh::Utils::fromHex(pub, 32, p + 4) && c.allow.add(pub)) { c.config_dirty = true; strcpy(reply, "OK key added (saved)"); }
|
|
else strcpy(reply, "ERR key");
|
|
} else if (strncmp(p, "rm ", 3) == 0 || strncmp(p, "remove ", 7) == 0) {
|
|
uint8_t pub[32]; const char* h = p + (p[0] == 'r' && p[1] == 'm' ? 3 : 7);
|
|
if (mesh::Utils::fromHex(pub, 32, h) && c.allow.remove(pub)) { c.config_dirty = true; strcpy(reply, "OK removed (saved)"); }
|
|
else strcpy(reply, "ERR");
|
|
} else { // bare `ota key` (or `key list`) -> show them
|
|
int n = snprintf(reply, 160, "trusted signer keys (%u):", (unsigned)c.allow.count());
|
|
for (uint8_t i = 0; i < c.allow.count() && n < 140; i++) {
|
|
char hx[17]; mesh::Utils::toHex(hx, c.allow.get(i), 8);
|
|
n += snprintf(reply + n, 160 - n, " %s", hx);
|
|
}
|
|
if (c.allow.count() == 0) strcpy(reply, "no trusted signer keys yet (add one with `ota key add <hex>`)");
|
|
}
|
|
|
|
} else {
|
|
strcpy(reply, "Unknown OTA command. Type `ota help`.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Raw / internal primitives (manual content load + low-level apply steps), under `ota dev ...`.
|
|
static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
|
#if defined(OTA_SEEDER_ONLY)
|
|
(void)d;
|
|
(void)c;
|
|
strcpy(reply, "ERR ota dev staging/apply is disabled on this seeder-only build");
|
|
#else
|
|
if (strncmp(d, "stage ", 6) == 0) {
|
|
uint32_t sz = parse_u32(d + 6);
|
|
if (sz == 0 || sz > OTA_SERVE_BUF_SIZE) { sprintf(reply, "ERR size 1..%u", OTA_SERVE_BUF_SIZE); }
|
|
else if (!c.ensureServeBuffer()) { strcpy(reply, "ERR stage OOM"); }
|
|
else { c.manager.clear_primary(); c.serving = false;
|
|
memset(c.serve_buf, 0xFF, sz); c.serve_expected = sz;
|
|
sprintf(reply, "OK stage %u bytes", (unsigned)sz); }
|
|
|
|
} else if (strncmp(d, "recv ", 5) == 0) {
|
|
const char* p = d + 5; uint32_t off = parse_u32(p);
|
|
const char* hex = strchr(p, ' ');
|
|
if (!hex) { strcpy(reply, "ERR usage: ota dev recv <off> <hex>"); return true; }
|
|
hex++;
|
|
int blen = (int)strlen(hex) / 2;
|
|
uint8_t tmp[80];
|
|
if (blen <= 0 || blen > (int)sizeof(tmp) || !mesh::Utils::fromHex(tmp, blen, hex)) strcpy(reply, "ERR hex");
|
|
else if (!c.serve_buf || off + blen > c.serve_expected) strcpy(reply, "ERR off>size (stage first)");
|
|
else { memcpy(c.serve_buf + off, tmp, blen); sprintf(reply, "OK %d@%u", blen, (unsigned)off); }
|
|
|
|
} else if (strncmp(d, "serve self", 10) == 0) { // host our own running firmware, served from flash
|
|
if (ota_serve_self(c, 0)) {
|
|
c.serving = true;
|
|
char midhx[9]; mesh::Utils::toHex(midhx, c.serve_self_manifest + 20, 4);
|
|
uint32_t img = (uint32_t)c.serve_self_manifest[11] | ((uint32_t)c.serve_self_manifest[12] << 8)
|
|
| ((uint32_t)c.serve_self_manifest[13] << 16) | ((uint32_t)c.serve_self_manifest[14] << 24);
|
|
sprintf(reply, "OK serving self fw mid=%s (%u B, flash-backed) - peers can pull it", midhx, (unsigned)img);
|
|
} else strcpy(reply, "ERR serve self (no EndF / image too big / OOM)");
|
|
} else if (strncmp(d, "serve", 5) == 0) {
|
|
if (!c.serve_buf || c.serve_expected == 0) {
|
|
strcpy(reply, "ERR nothing staged");
|
|
return true;
|
|
}
|
|
c.serving = c.manager.serve(c.serve_buf, c.serve_expected);
|
|
if (!c.serving) { strcpy(reply, "ERR serve (bad .mota)"); return true; }
|
|
VerifyResult r = ota_verify(c.serve_buf, c.serve_expected, c.allow);
|
|
sprintf(reply, "OK serving | root=%d payload=%d img=%d sig=%d trust=%d",
|
|
r.root_ok, r.payload_ok, r.image_ok, r.sig_ok, r.trusted);
|
|
|
|
} else if (is_cmd(d, "resume", &d)) { // explicit re-adopt of a known/active staged MID (test/debug)
|
|
uint8_t mid[4];
|
|
if (*d) {
|
|
if (!parse_hex_exact(d, mid, sizeof(mid))) {
|
|
strcpy(reply, "ERR usage: ota dev resume [mid8]");
|
|
return true;
|
|
}
|
|
} else {
|
|
// With no argument, reuse only an active/requested session MID. After a reboot the operator must
|
|
// name the persisted MID explicitly; nullptr is reserved for policy-governed automatic adoption.
|
|
if (c.manager.fetchState() == OtaManager::IDLE) {
|
|
strcpy(reply, "ERR usage: ota dev resume <mid8>");
|
|
return true;
|
|
}
|
|
memcpy(mid, c.manager.fetchManifestId(), sizeof(mid));
|
|
}
|
|
bool ok = c.manager.resumeStagedExplicit(mid, 0);
|
|
sprintf(reply, "%s resume: sess=%c %u/%u", ok ? "OK" : "ERR", fstate_char(c.manager.fetchState()),
|
|
(unsigned)c.manager.blocksHave(), (unsigned)c.manager.blocksTotal());
|
|
|
|
} else if (strncmp(d, "announce", 8) == 0) {
|
|
if (!c.serving) { strcpy(reply, "ERR not serving (ota dev serve first)"); return true; }
|
|
c.manager.announce();
|
|
strcpy(reply, "OK announced");
|
|
|
|
} else if (strncmp(d, "verify", 6) == 0) {
|
|
if (c.fetch_to_folder && c.manager.fetchState() == OtaManager::COMPLETE) {
|
|
strcpy(reply, "ERR completed fetch is in the host folder, not local verification storage");
|
|
return true;
|
|
}
|
|
#if defined(NRF52_PLATFORM) && \
|
|
(defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE) || defined(OTA_FLASH_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",
|
|
r.parsed, r.root_ok, r.payload_ok, r.image_ok, r.is_signed, r.sig_ok, r.trusted,
|
|
r.integrity_ok(), r.auto_appliable());
|
|
return true;
|
|
}
|
|
#endif
|
|
const uint8_t* buf; uint32_t len;
|
|
#if defined(NRF52_PLATFORM) && \
|
|
(defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE) || defined(OTA_FLASH_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(); }
|
|
else { buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0; }
|
|
#endif
|
|
if (len == 0 || !buf) { strcpy(reply, "ERR nothing to verify (flash-staged: applydelta verifies)"); return true; }
|
|
VerifyResult r = ota_verify(buf, len, c.allow);
|
|
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
|
|
r.parsed, r.root_ok, r.payload_ok, r.image_ok, r.is_signed, r.sig_ok, r.trusted,
|
|
r.integrity_ok(), r.auto_appliable());
|
|
|
|
} else if (strncmp(d, "want ", 5) == 0) {
|
|
const char* p = d + 5; while (*p == ' ') p++;
|
|
if (strncmp(p, "auto", 4) == 0) { c.manager.want(0); c.manager.want_mid(nullptr); strcpy(reply, "OK auto (own target only)"); }
|
|
else { uint32_t t = (uint32_t)strtoul(p, nullptr, 16); c.manager.want(t); c.manager.want_mid(nullptr);
|
|
sprintf(reply, "OK cross-target: will fetch %08X (you ensure HW compatible)", (unsigned)t); }
|
|
|
|
} else if (strncmp(d, "apply", 5) == 0) {
|
|
const char* sub = d + 5; while (*sub == ' ') sub++;
|
|
if (strncmp(sub, "slot", 4) == 0) {
|
|
uint32_t addr = 0, size = 0;
|
|
if (ota_apply_slot_info(&addr, &size)) sprintf(reply, "inactive slot addr=0x%X size=%u", (unsigned)addr, (unsigned)size);
|
|
else strcpy(reply, "ERR no A/B slot (apply unsupported on this build)");
|
|
} else if (strncmp(sub, "manifest", 8) == 0) {
|
|
if (c.serve_buf && ota_apply_set_manifest(c.serve_buf, c.serve_expected, c.allow, c.apply_st))
|
|
sprintf(reply, "manifest ok img=%u sig=%d trust=%d", (unsigned)c.apply_st.image_size, c.apply_st.sig_ok, c.apply_st.trusted);
|
|
else strcpy(reply, "ERR manifest parse / not full-image / unsupported");
|
|
} else if (strncmp(sub, "verify", 6) == 0) {
|
|
bool ok = ota_apply_verify_slot(c.apply_st);
|
|
sprintf(reply, "slot image_hash %s (size=%u)", ok ? "MATCH" : "MISMATCH", (unsigned)c.apply_st.image_size);
|
|
} else if (strncmp(sub, "commit", 6) == 0) {
|
|
if (!c.apply_st.slot_ok) { strcpy(reply, "ERR run 'ota dev apply verify' first (slot must match)"); return true; }
|
|
if (c.apply_pending) { strcpy(reply, "ERR update is already armed; reboot is pending"); return true; }
|
|
if (!ota_apply_arm()) { strcpy(reply, "ERR commit failed (no A/B slot?)"); return true; }
|
|
// Use the same deferred path as ordinary installs: it lets this reply
|
|
// drain, then Mesh invokes prepareForOtaReboot() before the reset.
|
|
c.bootloader_apply_pending = false;
|
|
c.apply_at = 0;
|
|
c.apply_hard = 0;
|
|
c.apply_pending = true;
|
|
strcpy(reply, "OK slot armed; guarded reboot pending");
|
|
} else {
|
|
strcpy(reply, "ERR ota dev apply (slot|manifest|verify|commit)");
|
|
}
|
|
|
|
} else if (strncmp(d, "clear", 5) == 0) {
|
|
if (c.apply_pending) {
|
|
strcpy(reply, "ERR update is armed; reboot is pending");
|
|
return true;
|
|
}
|
|
const bool was_folder = c.fetch_to_folder;
|
|
bool was_sd_archive = false;
|
|
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
|
was_sd_archive = c.sdCacheFetching();
|
|
c.stopSdCacheFetch();
|
|
#endif
|
|
c.manager.clear_primary();
|
|
c.serve_expected = 0; c.serving = false; c.releaseServeBuffer();
|
|
c.manager.reset_session();
|
|
if (was_folder && c.folder_dest) c.folder_dest->clear();
|
|
c.fetch_to_folder = false;
|
|
c.manager.set_fetch_store(&c.fetch_store);
|
|
if (was_folder) {
|
|
strcpy(reply, "OK cleared folder session; host file left untouched");
|
|
} else if (was_sd_archive) {
|
|
strcpy(reply, "OK cleared SD archive session; partial retained for resume");
|
|
} else {
|
|
const bool discarded = c.fetch_store.discard();
|
|
strcpy(reply, discarded ? "OK OTA receive slot confirmed clear"
|
|
: "ERR RAM state cleared; persistent OTA slot invalidation failed");
|
|
}
|
|
|
|
} else {
|
|
strcpy(reply, "ota dev: stage|recv|serve|announce|resume [mid8]|verify|want|apply|clear");
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
} // namespace ota
|
|
} // namespace mesh
|