fix: harden LoRa OTA chain and storage
This commit is contained in:
1 parent
400aa710a2
commit
b40d2e6cb2
29 files changed
+7497
-528
No files matched your search
@@ -647,19 +647,45 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
} 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
|
||||
c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0;
|
||||
snprintf(reply, 160, "OK dropped session (was %c mid=%s); slot free for a new pull", fstate_char(fs), midhx);
|
||||
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). ----
|
||||
@@ -1141,13 +1167,27 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
}
|
||||
|
||||
} else if (strncmp(d, "clear", 5) == 0) {
|
||||
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.fetch_store.clear(); c.manager.reset_session(); c.fetch_to_folder = false;
|
||||
strcpy(reply, "OK cleared");
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// Physical placement of one ESP32 flash-store container. Keep this independent
|
||||
// of ESP-IDF so reopen/discard geometry can be tested on the native host.
|
||||
struct MotaEsp32StageLayout {
|
||||
uint32_t total = 0;
|
||||
uint32_t meta_span = 0;
|
||||
uint32_t meta_flush = 0;
|
||||
uint32_t write_start = 0;
|
||||
uint32_t meta_part = 0;
|
||||
uint32_t pay_log0 = 0;
|
||||
uint32_t pay_part0 = 0;
|
||||
};
|
||||
|
||||
inline bool mota_esp32_align_up(uint32_t value, uint32_t alignment,
|
||||
uint32_t& result) {
|
||||
if (alignment == 0 || value > UINT32_MAX - (alignment - 1u)) return false;
|
||||
result = ((value + alignment - 1u) / alignment) * alignment;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint32_t mota_esp32_align_down(uint32_t value,
|
||||
uint32_t alignment) {
|
||||
return alignment ? (value / alignment) * alignment : 0u;
|
||||
}
|
||||
|
||||
inline bool mota_esp32_stage_layout(
|
||||
uint32_t partition_size, uint32_t sector_size, uint32_t meta_capacity,
|
||||
bool is_full, uint32_t image_size, uint32_t meta_bytes,
|
||||
uint32_t payload_size, MotaEsp32StageLayout& out) {
|
||||
if (sector_size == 0 || partition_size < sector_size ||
|
||||
partition_size % sector_size != 0) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t total64 =
|
||||
(uint64_t)meta_bytes + (uint64_t)payload_size + 5u;
|
||||
if (total64 > UINT32_MAX) return false;
|
||||
|
||||
MotaEsp32StageLayout layout;
|
||||
layout.total = (uint32_t)total64;
|
||||
if (is_full) {
|
||||
// A FULL payload is the final application image, byte for byte. Reject a
|
||||
// malformed manifest before begin() can erase or stream anything, and
|
||||
// prove that the complete logical container fits the inactive partition.
|
||||
if (payload_size != image_size || layout.total > partition_size) {
|
||||
return false;
|
||||
}
|
||||
layout.meta_span = meta_bytes;
|
||||
if (meta_bytes > UINT32_MAX - 5u ||
|
||||
!mota_esp32_align_up(meta_bytes + 5u, sector_size,
|
||||
layout.meta_flush) ||
|
||||
layout.meta_flush > meta_capacity ||
|
||||
layout.meta_flush > partition_size) {
|
||||
return false;
|
||||
}
|
||||
layout.meta_part = mota_esp32_align_down(
|
||||
partition_size - layout.meta_flush, sector_size);
|
||||
layout.pay_log0 = meta_bytes;
|
||||
layout.pay_part0 = 0;
|
||||
layout.write_start = 0;
|
||||
if (image_size > layout.meta_part) return false;
|
||||
} else {
|
||||
if (!mota_esp32_align_up(meta_bytes, sector_size,
|
||||
layout.meta_span) ||
|
||||
layout.meta_span > meta_capacity || layout.total > partition_size) {
|
||||
return false;
|
||||
}
|
||||
layout.meta_flush = layout.meta_span;
|
||||
layout.write_start = mota_esp32_align_down(
|
||||
partition_size - layout.total, sector_size);
|
||||
layout.meta_part = layout.write_start;
|
||||
layout.pay_log0 = layout.meta_span;
|
||||
if (layout.write_start > UINT32_MAX - layout.meta_span) return false;
|
||||
layout.pay_part0 = layout.write_start + layout.meta_span;
|
||||
if (image_size > layout.write_start) return false;
|
||||
}
|
||||
out = layout;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The probe separates ordinary non-candidates from partition I/O failure:
|
||||
// true/false is I/O success, while `reopenable` says whether this sector holds
|
||||
// a header that the store's reopen path would adopt.
|
||||
typedef bool (*MotaEsp32ProbeStagedHeader)(void* context, uint32_t offset,
|
||||
bool& reopenable);
|
||||
typedef bool (*MotaEsp32InvalidateStagedHeader)(void* context,
|
||||
uint32_t offset);
|
||||
|
||||
inline bool mota_esp32_discard_staged_headers(
|
||||
uint32_t partition_size, uint32_t sector_size, void* context,
|
||||
MotaEsp32ProbeStagedHeader probe_header,
|
||||
MotaEsp32InvalidateStagedHeader invalidate_header,
|
||||
uint32_t* invalidated_count = nullptr) {
|
||||
if (invalidated_count) *invalidated_count = 0;
|
||||
if (sector_size == 0 || partition_size < sector_size ||
|
||||
partition_size % sector_size != 0 || !probe_header ||
|
||||
!invalidate_header) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
uint32_t count = 0;
|
||||
uint32_t offset = mota_esp32_align_down(
|
||||
partition_size - sector_size, sector_size);
|
||||
for (;;) {
|
||||
bool reopenable = false;
|
||||
if (!probe_header(context, offset, reopenable)) {
|
||||
ok = false;
|
||||
} else if (reopenable) {
|
||||
if (invalidate_header(context, offset)) {
|
||||
++count;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (offset < sector_size) break;
|
||||
offset -= sector_size;
|
||||
}
|
||||
if (invalidated_count) *invalidated_count = count;
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -47,6 +47,7 @@ static const uint32_t MOTA_NRF52_STAGE_CEILING_EXPANDED = MOTA_NRF52_APP_END;
|
||||
// explicit ceiling so it cannot accidentally cross an Internal ExtraFS.
|
||||
static const uint32_t MOTA_NRF52_FS_START = MOTA_NRF52_EXTRAFS_START;
|
||||
static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
|
||||
static const uint32_t MOTA_NRF52_CONTAINER_MIN_SIZE = 8u + 197u + 5u;
|
||||
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
|
||||
static const uint8_t GPREGRET_OTA_BOOTLOADER_UPDATE = 0x6Bu;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
|
||||
@@ -272,5 +273,71 @@ inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32
|
||||
mota_nrf52_layout_stage_ceiling(), out_start);
|
||||
}
|
||||
|
||||
// Durable internal-store cancellation has to work even when the live
|
||||
// OtaStore object is empty (for example, autofetch is off after a reboot). It
|
||||
// also has to invalidate every older bottom-aligned header: invalidating only
|
||||
// the highest container could expose a larger stale container below it on the
|
||||
// next reopen scan. Keep the page walk and layout recognition pure so native
|
||||
// tests can exercise fresh-object and multiple-header cases without nRF flash.
|
||||
typedef bool (*MotaNrf52ReadStagedHeader)(void* context, uint32_t address,
|
||||
uint32_t& total_size);
|
||||
typedef bool (*MotaNrf52InvalidateStagedHeader)(void* context,
|
||||
uint32_t address);
|
||||
|
||||
inline bool mota_nrf52_staged_header_matches_layout(
|
||||
uint32_t address, uint32_t total_size, uint32_t app_base,
|
||||
uint32_t app_end, uint32_t effective_stage_ceiling) {
|
||||
uint32_t planned = 0;
|
||||
if (mota_nrf52_stage_plan(total_size, app_base, app_end,
|
||||
effective_stage_ceiling, planned) &&
|
||||
planned == address) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A device that moved from the legacy D4000 ceiling to the expanded ED000
|
||||
// ceiling can still contain an older, otherwise reopenable legacy header.
|
||||
// The expanded layout has no filesystem in that range, so it is safe to
|
||||
// invalidate both geometries. A legacy-only build never scans above D4000.
|
||||
return effective_stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED &&
|
||||
mota_nrf52_stage_plan(total_size, app_base, app_end,
|
||||
MOTA_NRF52_STAGE_CEILING_LEGACY, planned) &&
|
||||
planned == address;
|
||||
}
|
||||
|
||||
inline bool mota_nrf52_discard_staged_headers(
|
||||
uint32_t app_base, uint32_t app_end, uint32_t effective_stage_ceiling,
|
||||
void* context, MotaNrf52ReadStagedHeader read_header,
|
||||
MotaNrf52InvalidateStagedHeader invalidate_header,
|
||||
uint32_t* invalidated_count = nullptr) {
|
||||
if (invalidated_count) *invalidated_count = 0;
|
||||
if (!read_header || !invalidate_header ||
|
||||
!mota_nrf52_layout_valid(app_base, effective_stage_ceiling) ||
|
||||
app_end < app_base || app_end > effective_stage_ceiling) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
uint32_t count = 0;
|
||||
uint32_t address = effective_stage_ceiling - MOTA_NRF52_FLASH_PAGE;
|
||||
while (address >= app_end) {
|
||||
uint32_t total_size = 0;
|
||||
if (read_header(context, address, total_size) &&
|
||||
total_size >= MOTA_NRF52_CONTAINER_MIN_SIZE &&
|
||||
mota_nrf52_staged_header_matches_layout(
|
||||
address, total_size, app_base, app_end,
|
||||
effective_stage_ceiling)) {
|
||||
if (invalidate_header(context, address)) {
|
||||
++count;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (address < MOTA_NRF52_FLASH_PAGE) break;
|
||||
address -= MOTA_NRF52_FLASH_PAGE;
|
||||
}
|
||||
if (invalidated_count) *invalidated_count = count;
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -24,6 +24,17 @@ public:
|
||||
virtual uint32_t staged_size() const = 0; // total_size from begin(), 0 if none
|
||||
virtual void clear() = 0;
|
||||
|
||||
// Durably discard a staged container. `clear()` resets the store object's
|
||||
// live session state and is intentionally cheap enough for begin()/retry
|
||||
// paths; a persistent backend may therefore leave its resumable bytes in
|
||||
// place. User-facing cancellation needs the stronger operation: after a
|
||||
// successful discard, a fresh store object must not be able to reopen the
|
||||
// old container. The conservative default clears only the live object and
|
||||
// returns false: a backend must opt in only when it can prove that its
|
||||
// durable copy is gone. A false result means cancellation must not be
|
||||
// reported as having cleared the persistent slot.
|
||||
virtual bool discard() { clear(); return false; }
|
||||
|
||||
// Optional: declare the size of the leading metadata (header + manifest + merkle leaves, i.e.
|
||||
// everything before the payload). A flash-backed store keeps that region - which is updated
|
||||
// throughout the transfer (a leaf is committed per block) - pinned in one RAM page, so it can
|
||||
@@ -90,9 +101,17 @@ public:
|
||||
uint32_t capacity() const override { return CAP; }
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override { _total = 0; }
|
||||
bool discard() override {
|
||||
_total = 0;
|
||||
const size_t marker_bytes =
|
||||
CAP < sizeof(MOTA_MAGIC) ? CAP : sizeof(MOTA_MAGIC);
|
||||
memset(_buf, 0, marker_bytes);
|
||||
return true;
|
||||
}
|
||||
// RAM doesn't survive a real reboot, but the buffer persists within a process - enough to exercise the
|
||||
// manager's resume path in native tests. Recover `total` from the stored header so read() bounds work.
|
||||
bool reopen() override {
|
||||
if (CAP < 8u) return false;
|
||||
if (memcmp(_buf, MOTA_MAGIC, 4) != 0) return false;
|
||||
uint32_t t = (uint32_t)_buf[4] | ((uint32_t)_buf[5] << 8) | ((uint32_t)_buf[6] << 16) | ((uint32_t)_buf[7] << 24);
|
||||
if (t < 13 || t > CAP) return false;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#if defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
|
||||
#include "OtaDebug.h"
|
||||
#include "OtaByteIO.h" // align_up / align_down (flash-sector geometry)
|
||||
#include "OtaByteIO.h" // rd_u32le (persisted header total)
|
||||
#include "MotaContainer.h" // mota_parse_manifest (reopen: rebuild geometry from the staged manifest)
|
||||
#include <string.h>
|
||||
#include <stdlib.h> // malloc/free (the meta buffer is sized per fetch)
|
||||
@@ -22,33 +22,29 @@ bool OtaStoreFlashEsp32::acquire() {
|
||||
return _part != nullptr;
|
||||
}
|
||||
|
||||
void OtaStoreFlashEsp32::set_layout(const MotaEsp32StageLayout& layout) {
|
||||
_total = layout.total;
|
||||
_meta_span = layout.meta_span;
|
||||
_meta_flush = layout.meta_flush;
|
||||
_write_start = layout.write_start;
|
||||
_meta_part = layout.meta_part;
|
||||
_pay_log0 = layout.pay_log0;
|
||||
_pay_part0 = layout.pay_part0;
|
||||
_trailer_part = _full ? _meta_part + _meta_bytes
|
||||
: _write_start + _total - 5u;
|
||||
}
|
||||
|
||||
// Compute the slot placement from _full/_image_size/_meta_bytes/_pay_size (already set). Returns false if
|
||||
// it won't fit. Shared by plan_layout (fresh fetch) and reopen (resume) so both derive identical geometry.
|
||||
// it won't fit. Shared by plan_layout (fresh fetch), reopen (resume), and discard validation so all three
|
||||
// derive identical geometry.
|
||||
bool OtaStoreFlashEsp32::layout() {
|
||||
uint32_t total = _meta_bytes + _pay_size + 5;
|
||||
if (_full) {
|
||||
// payload streams to slot offset 0 (it IS the image); header+manifest+leaves+trailer persist at the
|
||||
// bottom so the container survives a reboot (resume / re-serve).
|
||||
_meta_span = _meta_bytes; // routing boundary: [0,meta) -> RAM meta buffer
|
||||
_meta_flush = align_up(_meta_bytes + 5, SEC); // meta + 5-byte trailer, whole sectors
|
||||
if (_meta_flush > OTA_ESP32_META_CAP) return false;
|
||||
uint32_t bottom = align_down(_psize - _meta_flush, SEC);
|
||||
_meta_part = bottom;
|
||||
_pay_log0 = _meta_bytes; _pay_part0 = 0;
|
||||
_write_start = 0;
|
||||
if (_image_size > bottom) return false; // image would overrun the bottom meta region
|
||||
} else {
|
||||
// whole container staged bottom-aligned; the decoded image fills the slot from offset 0.
|
||||
_meta_span = align_up(_meta_bytes, SEC); // pin whole sectors covering meta (+ spillover payload)
|
||||
_meta_flush = _meta_span;
|
||||
if (_meta_flush > OTA_ESP32_META_CAP) return false;
|
||||
if (total > _psize) return false;
|
||||
_write_start = align_down(_psize - total, SEC);
|
||||
_meta_part = _write_start;
|
||||
_pay_log0 = _meta_span; _pay_part0 = _write_start + _meta_span;
|
||||
if (_image_size > _write_start) return false; // decoded output would overlap the staged container
|
||||
MotaEsp32StageLayout planned;
|
||||
if (!mota_esp32_stage_layout(
|
||||
_psize, SEC, OTA_ESP32_META_CAP, _full, _image_size,
|
||||
_meta_bytes, _pay_size, planned)) {
|
||||
return false;
|
||||
}
|
||||
_total = total;
|
||||
set_layout(planned);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -84,6 +80,120 @@ void OtaStoreFlashEsp32::clear() {
|
||||
_total = 0; _pay_open = false; _flushed = false; // _part kept (re-acquire is fine)
|
||||
}
|
||||
|
||||
OtaStoreFlashEsp32::CandidateProbe OtaStoreFlashEsp32::probe_candidate(
|
||||
uint32_t offset, StagedCandidate& candidate) const {
|
||||
if (!_part || _psize < SEC || offset > _psize - SEC) {
|
||||
return CandidateProbe::INVALID;
|
||||
}
|
||||
|
||||
uint8_t header[8];
|
||||
if (esp_partition_read(_part, offset, header, sizeof(header)) != ESP_OK) {
|
||||
return CandidateProbe::IO_ERROR;
|
||||
}
|
||||
if (memcmp(header, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0) {
|
||||
return CandidateProbe::INVALID;
|
||||
}
|
||||
|
||||
const uint32_t total = rd_u32le(header + sizeof(MOTA_MAGIC));
|
||||
if (total < 8u + MOTA_MFL + 5u || total > _psize) {
|
||||
return CandidateProbe::INVALID;
|
||||
}
|
||||
|
||||
uint8_t manifest_bytes[256];
|
||||
uint32_t manifest_read = total - 8u;
|
||||
if (manifest_read > sizeof(manifest_bytes)) {
|
||||
manifest_read = sizeof(manifest_bytes);
|
||||
}
|
||||
if (esp_partition_read(_part, offset + 8u, manifest_bytes,
|
||||
manifest_read) != ESP_OK) {
|
||||
return CandidateProbe::IO_ERROR;
|
||||
}
|
||||
|
||||
MotaManifest manifest;
|
||||
if (!mota_parse_manifest(manifest_bytes, manifest_read, manifest)) {
|
||||
return CandidateProbe::INVALID;
|
||||
}
|
||||
const uint32_t manifest_len =
|
||||
(uint32_t)(manifest.approval - manifest.manifest_start) +
|
||||
sizeof(APPROVAL_YES);
|
||||
const uint64_t payload_off64 =
|
||||
8u + (uint64_t)manifest_len + (uint64_t)manifest.block_count * 4u;
|
||||
if (payload_off64 > UINT32_MAX ||
|
||||
payload_off64 + (uint64_t)manifest.payload_size + 5u != total) {
|
||||
return CandidateProbe::INVALID;
|
||||
}
|
||||
|
||||
MotaEsp32StageLayout planned;
|
||||
if (!mota_esp32_stage_layout(
|
||||
_psize, SEC, OTA_ESP32_META_CAP, manifest.is_full(),
|
||||
manifest.image_size, (uint32_t)payload_off64,
|
||||
manifest.payload_size, planned) ||
|
||||
planned.meta_part != offset || planned.total != total) {
|
||||
return CandidateProbe::INVALID;
|
||||
}
|
||||
|
||||
candidate.layout = planned;
|
||||
candidate.full = manifest.is_full();
|
||||
candidate.image_size = manifest.image_size;
|
||||
candidate.meta_bytes = (uint32_t)payload_off64;
|
||||
candidate.pay_size = manifest.payload_size;
|
||||
return CandidateProbe::VALID;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashEsp32::probe_staged_header(
|
||||
void* context, uint32_t offset, bool& reopenable) {
|
||||
OtaStoreFlashEsp32* store = static_cast<OtaStoreFlashEsp32*>(context);
|
||||
StagedCandidate candidate;
|
||||
const CandidateProbe result = store->probe_candidate(offset, candidate);
|
||||
reopenable = result == CandidateProbe::VALID;
|
||||
return result != CandidateProbe::IO_ERROR;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashEsp32::invalidate_staged_header(
|
||||
void* context, uint32_t offset) {
|
||||
OtaStoreFlashEsp32* store = static_cast<OtaStoreFlashEsp32*>(context);
|
||||
if (!store->_part || store->_psize < SEC || offset > store->_psize - SEC ||
|
||||
esp_partition_read(store->_part, offset, store->_pay, SEC) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both markers are inside the already-validated first metadata sector.
|
||||
// One sector RMW avoids erasing a delta's adjacent metadata/payload twice.
|
||||
memset(store->_pay, 0, sizeof(MOTA_MAGIC));
|
||||
memset(store->_pay + 8u + MOTA_OFF_APPROVAL, 0,
|
||||
sizeof(APPROVAL_YES));
|
||||
if (esp_partition_erase_range(store->_part, offset, SEC) != ESP_OK ||
|
||||
esp_partition_write(store->_part, offset, store->_pay, SEC) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t check[sizeof(MOTA_MAGIC)];
|
||||
if (esp_partition_read(store->_part, offset, check, sizeof(check)) != ESP_OK ||
|
||||
memcmp(check, store->_pay, sizeof(check)) != 0 ||
|
||||
esp_partition_read(store->_part, offset + 8u + MOTA_OFF_APPROVAL,
|
||||
check, sizeof(check)) != ESP_OK ||
|
||||
memcmp(check, store->_pay + 8u + MOTA_OFF_APPROVAL,
|
||||
sizeof(check)) != 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashEsp32::discard() {
|
||||
bool ok = acquire();
|
||||
uint32_t invalidated = 0;
|
||||
if (ok) {
|
||||
ok = mota_esp32_discard_staged_headers(
|
||||
_psize, SEC, this, probe_staged_header,
|
||||
invalidate_staged_header, &invalidated);
|
||||
}
|
||||
OTA_DBG("OTA esp32: discard invalidated=%u ok=%d\n",
|
||||
(unsigned)invalidated, (int)ok);
|
||||
clear();
|
||||
_io_ok = ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint8_t* OtaStoreFlashEsp32::meta_slot(uint32_t L) {
|
||||
if (in_trailer(L)) return _full ? (_meta + _meta_bytes + (L - (_total - 5)))
|
||||
: (_trailer + (L - (_total - 5)));
|
||||
@@ -207,34 +317,26 @@ void OtaStoreFlashEsp32::checkpoint() {
|
||||
// so a stray/stale match can only cost a restart, never a corrupt adopt.
|
||||
bool OtaStoreFlashEsp32::reopen() {
|
||||
if (!acquire() || _psize < SEC) return false;
|
||||
uint8_t hb[8];
|
||||
// The meta/container is staged at the bottom of the slot. Large delta containers can begin well above
|
||||
// the final 512 KB, so scan the complete partition rather than silently losing resumability after reboot.
|
||||
for (uint32_t o = align_down(_psize - SEC, SEC); ; o -= SEC) {
|
||||
if (esp_partition_read(_part, o, hb, 8) == ESP_OK && memcmp(hb, MOTA_MAGIC, 4) == 0) {
|
||||
uint32_t total = rd_u32le(hb + 4);
|
||||
if (total >= 13 && total <= _psize) {
|
||||
uint8_t mbuf[256]; uint32_t mread = total - 8; if (mread > sizeof(mbuf)) mread = sizeof(mbuf);
|
||||
MotaManifest m;
|
||||
if (esp_partition_read(_part, o + 8, mbuf, mread) == ESP_OK && mota_parse_manifest(mbuf, mread, m)) {
|
||||
uint32_t mfl = (uint32_t)(m.approval - m.manifest_start) + 4;
|
||||
uint32_t payload_off = 8 + mfl + m.block_count * 4;
|
||||
if ((uint64_t)payload_off + m.payload_size + 5 == total) {
|
||||
_full = m.is_full(); _image_size = m.image_size; _meta_bytes = payload_off; _pay_size = m.payload_size;
|
||||
if (layout() && _meta_part == o) { // geometry agrees AND magic is where we'd place meta
|
||||
free(_meta); _meta = (uint8_t*)malloc(_meta_flush);
|
||||
if (!_meta) { _total = 0; return false; }
|
||||
if (esp_partition_read(_part, _meta_part, _meta, _meta_flush) != ESP_OK) {
|
||||
free(_meta); _meta = nullptr; _total = 0; return false; }
|
||||
memset(_trailer, 0xFF, sizeof(_trailer)); // delta trailer (re-written at finalize); full reads it from _meta
|
||||
_pay_open = false; _pay_sec = 0; _flushed = false; _io_ok = true;
|
||||
_pay_max_sec = (_pay_part0 + _pay_size + SEC) / SEC; // treat all payload sectors as seen -> RMW preserves committed blocks
|
||||
OTA_DBG("OTA esp32: reopen %s total=%u meta_part=%u\n", _full ? "FULL" : "DELTA", (unsigned)total, (unsigned)o);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t o = mota_esp32_align_down(_psize - SEC, SEC); ; o -= SEC) {
|
||||
StagedCandidate candidate;
|
||||
if (probe_candidate(o, candidate) == CandidateProbe::VALID) {
|
||||
_full = candidate.full;
|
||||
_image_size = candidate.image_size;
|
||||
_meta_bytes = candidate.meta_bytes;
|
||||
_pay_size = candidate.pay_size;
|
||||
set_layout(candidate.layout);
|
||||
free(_meta); _meta = (uint8_t*)malloc(_meta_flush);
|
||||
if (!_meta) { _total = 0; return false; }
|
||||
if (esp_partition_read(_part, _meta_part, _meta, _meta_flush) != ESP_OK) {
|
||||
free(_meta); _meta = nullptr; _total = 0; return false;
|
||||
}
|
||||
memset(_trailer, 0xFF, sizeof(_trailer)); // delta trailer (re-written at finalize); full reads it from _meta
|
||||
_pay_open = false; _pay_sec = 0; _flushed = false; _io_ok = true;
|
||||
_pay_max_sec = (_pay_part0 + _pay_size + SEC) / SEC; // treat all payload sectors as seen -> RMW preserves committed blocks
|
||||
OTA_DBG("OTA esp32: reopen %s total=%u meta_part=%u\n", _full ? "FULL" : "DELTA", (unsigned)_total, (unsigned)o);
|
||||
return true;
|
||||
}
|
||||
if (o == 0) break;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
|
||||
#include "OtaStore.h"
|
||||
#include "OtaFlashLayout_esp32.h"
|
||||
#include "esp_partition.h"
|
||||
|
||||
// Persistent flash-backed OtaStore for ESP32 (A/B). Stages the received `.mota` in the INACTIVE OTA
|
||||
@@ -41,6 +42,15 @@ namespace ota {
|
||||
class OtaStoreFlashEsp32 : public OtaStore {
|
||||
static const uint32_t SEC = 4096; // ESP32 NOR flash erase unit
|
||||
|
||||
enum class CandidateProbe : uint8_t { INVALID, VALID, IO_ERROR };
|
||||
struct StagedCandidate {
|
||||
MotaEsp32StageLayout layout;
|
||||
bool full = false;
|
||||
uint32_t image_size = 0;
|
||||
uint32_t meta_bytes = 0;
|
||||
uint32_t pay_size = 0;
|
||||
};
|
||||
|
||||
const esp_partition_t* _part = nullptr; // inactive OTA slot (acquired in plan_layout/begin)
|
||||
uint32_t _psize = 0; // slot size
|
||||
|
||||
@@ -62,7 +72,7 @@ class OtaStoreFlashEsp32 : public OtaStore {
|
||||
|
||||
// RX-safe staging buffers
|
||||
uint8_t* _meta = nullptr; // heap, sized per fetch: header+manifest+leaves(+full trailer)
|
||||
uint8_t _pay[SEC]; // one sliding payload sector (slot-sector aligned)
|
||||
alignas(4) uint8_t _pay[SEC]; // one sliding payload sector (slot-sector aligned)
|
||||
uint32_t _pay_sec = 0; // slot sector index currently in _pay (0 = none open)
|
||||
uint8_t _trailer[5];
|
||||
uint32_t _meta_flush = 0; // whole-sector byte count to program for the meta buffer
|
||||
@@ -73,6 +83,12 @@ class OtaStoreFlashEsp32 : public OtaStore {
|
||||
|
||||
bool acquire(); // resolve the inactive slot (idempotent)
|
||||
bool layout(); // compute placement from _full/_image_size/_meta_bytes/_pay_size
|
||||
void set_layout(const MotaEsp32StageLayout& layout);
|
||||
CandidateProbe probe_candidate(uint32_t offset,
|
||||
StagedCandidate& candidate) const;
|
||||
static bool probe_staged_header(void* context, uint32_t offset,
|
||||
bool& reopenable);
|
||||
static bool invalidate_staged_header(void* context, uint32_t offset);
|
||||
uint32_t pay_part(uint32_t L) const { return _pay_part0 + (L - _pay_log0); } // payload slot offset
|
||||
bool in_trailer(uint32_t L) const { return L >= _total - 5; }
|
||||
uint32_t run(uint32_t pos, uint32_t remain) const; // bytes from `pos` that stay in one region+sector
|
||||
@@ -92,6 +108,7 @@ public:
|
||||
uint32_t capacity() const override { return _psize; } // loose bound; plan_layout does the real check
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override;
|
||||
bool discard() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes < OTA_ESP32_META_CAP; }
|
||||
bool finalize() override;
|
||||
void checkpoint() override; // persist meta(leaves) + open payload sector so a reboot can resume
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
void OtaStoreFlashNrf52::reset_session() {
|
||||
_write_start = 0;
|
||||
_stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
_total = 0;
|
||||
_pay_idx = 0;
|
||||
_flushed = false;
|
||||
_io_ok = true;
|
||||
_planned_bootloader = false;
|
||||
_planned_total = 0;
|
||||
_planned_start = 0;
|
||||
}
|
||||
|
||||
// A valid EndF gives the exact live-image extent. Legacy app-only builds keep
|
||||
// the conservative rescue fallback. Shared internal bootloader-update builds
|
||||
// reject every package kind without EndF because their normal linker may place
|
||||
@@ -73,6 +85,59 @@ static bool flash_matches(uint32_t addr, const uint8_t* expected, uint32_t n) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::read_staged_header(
|
||||
void*, uint32_t address, uint32_t& total_size) {
|
||||
const uint8_t* header = (const uint8_t*)(uintptr_t)address;
|
||||
if (memcmp(header, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0) return false;
|
||||
total_size = rd_u32le(header + sizeof(MOTA_MAGIC));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::invalidate_staged_header(
|
||||
void* context, uint32_t address) {
|
||||
OtaStoreFlashNrf52* store = static_cast<OtaStoreFlashNrf52*>(context);
|
||||
const uint8_t* page = (const uint8_t*)(uintptr_t)address;
|
||||
memcpy(store->_meta_page, page, PG);
|
||||
|
||||
// Magic prevents application reopen; approval prevents an explicitly
|
||||
// triggered legacy bootloader from accepting a half-cleared container.
|
||||
// Rewrite the page once so both durable markers are consumed together.
|
||||
memset(store->_meta_page, 0, sizeof(MOTA_MAGIC));
|
||||
memset(store->_meta_page + 8u + MOTA_OFF_APPROVAL, 0,
|
||||
sizeof(APPROVAL_YES));
|
||||
if (flash_nrf5x_write(address, store->_meta_page, PG) < 0) {
|
||||
store->_io_ok = false;
|
||||
return false;
|
||||
}
|
||||
flash_nrf5x_flush();
|
||||
if (!flash_matches(address, store->_meta_page, PG)) {
|
||||
store->_io_ok = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::discard() {
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
||||
uint32_t app_end = 0;
|
||||
if (!protected_app_end(app_base, stage_ceiling, app_end)) {
|
||||
reset_session();
|
||||
_io_ok = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t invalidated = 0;
|
||||
const bool ok = mota_nrf52_discard_staged_headers(
|
||||
app_base, app_end, stage_ceiling, this, read_staged_header,
|
||||
invalidate_staged_header, &invalidated);
|
||||
OTA_DBG("OTA flash: discard invalidated=%u ok=%d\n",
|
||||
(unsigned)invalidated, (int)ok);
|
||||
reset_session();
|
||||
_io_ok = ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) {
|
||||
if (!_io_ok || _write_start > _stage_ceiling - PG ||
|
||||
page_idx > (_stage_ceiling - PG - _write_start) / PG) {
|
||||
@@ -133,7 +198,7 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
const bool planned_bootloader = _planned_bootloader;
|
||||
const uint32_t planned_total = _planned_total;
|
||||
const uint32_t planned_start = _planned_start;
|
||||
clear();
|
||||
reset_session();
|
||||
|
||||
// never collide with the running application image (its extent comes from its EndF trailer)
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
|
||||
@@ -57,6 +57,10 @@ class OtaStoreFlashNrf52 : public OtaStore {
|
||||
uint8_t* write_slot(uint32_t pos);
|
||||
bool flush_pay(); // commit _pay_page to flash (erase + program, one page)
|
||||
bool flush_page(uint32_t page_idx, const uint8_t* buf); // write + verify a full page
|
||||
void reset_session(); // RAM bookkeeping only; never touches staged flash
|
||||
static bool read_staged_header(void* context, uint32_t address,
|
||||
uint32_t& total_size);
|
||||
static bool invalidate_staged_header(void* context, uint32_t address);
|
||||
|
||||
public:
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off,
|
||||
@@ -66,11 +70,8 @@ public:
|
||||
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 {
|
||||
_stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
_total = 0; _pay_idx = 0; _flushed = false; _io_ok = true;
|
||||
_planned_bootloader = false; _planned_total = 0; _planned_start = 0;
|
||||
}
|
||||
void clear() override { reset_session(); }
|
||||
bool discard() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes <= PG; } // leaves must fit page 0
|
||||
bool finalize() override;
|
||||
void checkpoint() override; // persist page 0 (leaves) + the open payload page so a reboot can resume
|
||||
|
||||
@@ -864,7 +864,7 @@ bool OtaStoreQspiNrf52::reopen() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::clear() {
|
||||
bool OtaStoreQspiNrf52::discard() {
|
||||
bool ok = ensureFlash();
|
||||
uint8_t zero[4] = { 0, 0, 0, 0 };
|
||||
uint8_t check[sizeof(zero)];
|
||||
@@ -872,7 +872,11 @@ void OtaStoreQspiNrf52::clear() {
|
||||
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)) {
|
||||
// The raw QSPI store is dedicated to OTA, and APRV has a fixed manifest
|
||||
// offset. A fresh store object has _total == 0 even when an older approved
|
||||
// container remains in flash, so invalidate both gates from flash capacity
|
||||
// rather than trusting this session's RAM state.
|
||||
if (ok && _flash_size >= 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;
|
||||
@@ -883,8 +887,11 @@ void OtaStoreQspiNrf52::clear() {
|
||||
resetSession();
|
||||
if (!ok) fail(saved_error[0] ? saved_error : "QSPI container invalidation failed");
|
||||
releaseFlash();
|
||||
return ok;
|
||||
}
|
||||
|
||||
void OtaStoreQspiNrf52::clear() { (void)discard(); }
|
||||
|
||||
bool OtaStoreQspiNrf52::approve_for_bootloader() {
|
||||
if (!finalize()) return false;
|
||||
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
|
||||
|
||||
@@ -155,6 +155,7 @@ public:
|
||||
uint32_t capacity() const override;
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override;
|
||||
bool discard() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override;
|
||||
bool finalize() override;
|
||||
void checkpoint() override;
|
||||
|
||||
@@ -509,17 +509,32 @@ bool OtaStoreSdNrf52::reopen() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreSdNrf52::clear() {
|
||||
bool OtaStoreSdNrf52::discard() {
|
||||
_total = 0;
|
||||
_planned_bootloader = false;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
clear_sd_auth_record();
|
||||
if (!mount()) return;
|
||||
if (!mount()) return false;
|
||||
if (_file && *_file) _file->close();
|
||||
_sd->remove(PATH);
|
||||
if (_sd->exists(PATH) && !_sd->remove(PATH)) {
|
||||
fail("SD OTA file removal failed");
|
||||
return false;
|
||||
}
|
||||
if (_sd->exists(PATH)) {
|
||||
fail("SD OTA file still exists after removal");
|
||||
return false;
|
||||
}
|
||||
SdCard* const card = _sd->card();
|
||||
if (card && !card->syncDevice()) {
|
||||
fail("SD OTA removal sync failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OtaStoreSdNrf52::clear() { (void)discard(); }
|
||||
|
||||
bool OtaStoreSdNrf52::approve_for_bootloader(
|
||||
const uint8_t expected_boot_image_hash[32],
|
||||
const uint8_t authorized_container_hash[32]) {
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
uint32_t capacity() const override;
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override;
|
||||
bool discard() override;
|
||||
bool set_meta_size(uint32_t meta_bytes) override;
|
||||
bool finalize() override;
|
||||
void checkpoint() override;
|
||||
|
||||
@@ -172,6 +172,17 @@ static RAK12035_SoilMoisture RAK12035;
|
||||
#endif
|
||||
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
// Release capability manifests scan the linked firmware image rather than
|
||||
// trusting build flags. Give the actual WisBlock provider a unique marker and
|
||||
// keep an observable reference from begin() so LTO/section GC cannot discard
|
||||
// it. The marker lives inside this provider guard, so generic GPS CLI text
|
||||
// cannot produce a false positive.
|
||||
extern "C" {
|
||||
extern const char meshcore_capability_rak_wisblock_gps[];
|
||||
const char meshcore_capability_rak_wisblock_gps[] __attribute__((used)) =
|
||||
"meshcore.capability.rak_wisblock_gps.v1";
|
||||
}
|
||||
|
||||
// -1 = no enable pin; out-of-range values are no-ops in pinMode/digitalWrite,
|
||||
// while 0 would be a real GPIO (P0.00 = LFXO crystal on nRF52)
|
||||
static uint32_t gpsResetPin = -1;
|
||||
@@ -702,6 +713,10 @@ static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TA
|
||||
bool EnvironmentSensorManager::begin() {
|
||||
#if ENV_INCLUDE_GPS
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
// A volatile read anchors the externally linked marker even under LTO.
|
||||
const volatile char* capability_marker =
|
||||
meshcore_capability_rak_wisblock_gps;
|
||||
(void)*capability_marker;
|
||||
rakGPSInit();
|
||||
#else
|
||||
initBasicGPS();
|
||||
|
||||
Reference in new issue
Block a user