Files
HaloKeymind/src/helpers/ota/OtaStoreFlashEsp32.cpp
T

350 lines
14 KiB
C++

#include "OtaStoreFlashEsp32.h"
#if defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
#include "OtaDebug.h"
#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)
#include "esp_ota_ops.h" // esp_ota_get_next_update_partition (the inactive A/B slot)
namespace mesh {
namespace ota {
OtaStoreFlashEsp32::~OtaStoreFlashEsp32() { free(_meta); }
bool OtaStoreFlashEsp32::acquire() {
if (!_part) {
_part = esp_ota_get_next_update_partition(nullptr);
_psize = _part ? _part->size : 0;
}
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), reopen (resume), and discard validation so all three
// derive identical geometry.
bool OtaStoreFlashEsp32::layout() {
MotaEsp32StageLayout planned;
if (!mota_esp32_stage_layout(
_psize, SEC, OTA_ESP32_META_CAP, _full, _image_size,
_meta_bytes, _pay_size, planned)) {
return false;
}
set_layout(planned);
return true;
}
// Choose placement from the parsed manifest and refuse anything that won't fit, BEFORE any block is
// staged. (See the header for the delta-vs-full layout rationale.)
bool OtaStoreFlashEsp32::plan_layout(bool is_full, uint32_t image_size,
uint32_t payload_off, uint32_t payload_size,
bool is_bootloader) {
if (is_bootloader) return false;
if (!acquire()) return false;
_full = is_full; _image_size = image_size; _meta_bytes = payload_off; _pay_size = payload_size;
bool ok = layout();
OTA_DBG("OTA esp32: plan %s total=%u image=%u meta=%u write_start=%u meta_part=%u ok=%d\n",
is_full ? "FULL" : "DELTA", (unsigned)_total, (unsigned)image_size, (unsigned)_meta_bytes,
(unsigned)_write_start, (unsigned)_meta_part, (int)ok);
return ok;
}
bool OtaStoreFlashEsp32::begin(uint32_t total_size) {
if (!_part || _total == 0 || total_size != _total) return false; // plan_layout must have run + agreed
free(_meta);
_meta = (uint8_t*)malloc(_meta_flush); // header+manifest+leaves(+full trailer)
if (!_meta) { _total = 0; return false; }
memset(_meta, 0xFF, _meta_flush);
memset(_trailer, 0xFF, sizeof(_trailer));
_pay_open = false; _pay_sec = 0; _pay_max_sec = 0; _flushed = false;
_io_ok = true;
return true;
}
void OtaStoreFlashEsp32::clear() {
free(_meta); _meta = nullptr;
_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)));
if (L < _meta_span) return _meta + L;
return nullptr; // payload -> sliding sector / flash
}
const uint8_t* OtaStoreFlashEsp32::meta_slot_c(uint32_t L) const {
if (in_trailer(L)) return _full ? (_meta + _meta_bytes + (L - (_total - 5)))
: (_trailer + (L - (_total - 5)));
if (L < _meta_span) return _meta + L;
return nullptr;
}
// Bytes from `pos` that stay in one region, and (for payload) one flash sector.
uint32_t OtaStoreFlashEsp32::run(uint32_t pos, uint32_t remain) const {
if (pos >= _total - 5) return remain; // trailer (<=5, one buffer)
if (pos < _meta_span) { uint32_t c = _meta_span - pos; return remain < c ? remain : c; }
uint32_t poff = pay_part(pos);
uint32_t to_sec = SEC - (poff % SEC);
uint32_t to_end = (_total - 5) - pos; // don't cross into the trailer
uint32_t c = remain;
if (to_sec < c) c = to_sec;
if (to_end < c) c = to_end;
return c;
}
void OtaStoreFlashEsp32::flush_sector(uint32_t slot_off, const uint8_t* buf, uint32_t n) {
if (!_io_ok) return;
if (esp_partition_erase_range(_part, slot_off, n) != ESP_OK) { _io_ok = false; return; }
if (esp_partition_write(_part, slot_off, buf, n) != ESP_OK) { _io_ok = false; return; }
OTA_DBG("OTA esp32: flushed %u B @ slot+%u\n", (unsigned)n, (unsigned)slot_off);
}
void OtaStoreFlashEsp32::flush_pay() {
if (_pay_open) { flush_sector((uint32_t)_pay_sec * SEC, _pay, SEC); _pay_open = false; }
}
void OtaStoreFlashEsp32::open_pay(uint32_t sec) {
if (_pay_open) flush_pay();
if (sec < _pay_max_sec) {
// revisiting an already-flushed sector (out-of-order block) -> read it back so the gaps we don't
// touch are preserved (they were programmed as 0xFF or earlier block data); we erase+reprogram on flush.
if (esp_partition_read(_part, (size_t)sec * SEC, _pay, SEC) != ESP_OK) _io_ok = false;
} else {
memset(_pay, 0xFF, SEC); // fresh sector
_pay_max_sec = sec;
}
_pay_sec = sec; _pay_open = true;
}
bool OtaStoreFlashEsp32::write(uint32_t offset, const uint8_t* d, uint32_t len) {
if ((uint64_t)offset + len > _total || !_io_ok) return false;
for (uint32_t pos = offset, end = offset + len; pos < end; ) {
uint32_t n = run(pos, end - pos);
if (uint8_t* dst = meta_slot(pos)) { // meta / leaves / trailer -> pinned RAM
memcpy(dst, d, n);
} else { // payload -> sliding sector
uint32_t poff = pay_part(pos);
uint32_t sec = poff / SEC;
if (!_pay_open || sec != _pay_sec) open_pay(sec);
memcpy(_pay + (poff % SEC), d, n);
}
pos += n; d += n;
if (!_io_ok) return false;
}
return true;
}
bool OtaStoreFlashEsp32::read(uint32_t offset, uint8_t* buf, uint32_t len) const {
if ((uint64_t)offset + len > _total) return false;
for (uint32_t pos = offset, end = offset + len; pos < end; ) {
uint32_t n = run(pos, end - pos);
if (const uint8_t* src = meta_slot_c(pos)) {
memcpy(buf, src, n);
} else {
uint32_t poff = pay_part(pos);
if (_pay_open && poff / SEC == _pay_sec) memcpy(buf, _pay + (poff % SEC), n); // still in RAM
else if (esp_partition_read(_part, poff, buf, n) != ESP_OK) return false; // flushed -> flash
}
pos += n; buf += n;
}
return true;
}
bool OtaStoreFlashEsp32::finalize() {
if (_flushed) return _io_ok;
if (_total == 0 || !_io_ok) return false;
if (!_full) {
// delta: drop the 5-byte trailer into the sliding payload sector(s) so it flushes with them (the
// trailer sits right after the payload; this also covers the rare case it spills into a fresh sector).
uint32_t tpoff = _write_start + (_total - 5);
for (uint32_t off = 0; off < 5; ) {
uint32_t sec = (tpoff + off) / SEC;
if (!_pay_open || sec != _pay_sec) open_pay(sec);
uint32_t in = SEC - ((tpoff + off) % SEC); if (in > 5 - off) in = 5 - off;
memcpy(_pay + ((tpoff + off) % SEC), _trailer + off, in);
off += in;
}
}
flush_pay(); // last payload sector(s) (+ delta trailer)
flush_sector(_meta_part, _meta, _meta_flush); // meta (+ trailer for full)
_flushed = _io_ok;
OTA_DBG("OTA esp32: finalize %s io_ok=%d\n", _full ? "FULL" : "DELTA", (int)_io_ok);
return _io_ok;
}
// Persist mid-transfer progress so a reboot can resume. Flush the open payload sector (KEEP it buffered so
// continued in-order writes aren't lost), then the meta region (leaves). Payload-before-leaves keeps it
// consistent: every block whose leaf is now in flash also has its payload in flash. Infrequent.
void OtaStoreFlashEsp32::checkpoint() {
if (_total == 0 || _flushed || !_io_ok) return;
if (_pay_open) flush_sector((uint32_t)_pay_sec * SEC, _pay, SEC); // flush but leave _pay/_pay_open intact
flush_sector(_meta_part, _meta, _meta_flush); // header + manifest + leaves (+full trailer)
}
// Re-attach to a container already staged in the slot (after a reboot), WITHOUT erasing. The header
// (MOTA_MAGIC + total) sits at a sector boundary (meta_part for full, write_start for delta - both
// sector-aligned), so scan sector starts from the bottom up. A candidate is accepted only if: magic +
// plausible total, the manifest parses, the container geometry is self-consistent with the total, AND the
// recomputed placement lands the meta exactly where we found the magic. Any miss -> false (fetch fresh),
// so a stray/stale match can only cost a restart, never a corrupt adopt.
bool OtaStoreFlashEsp32::reopen() {
if (!acquire() || _psize < SEC) return false;
// 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 = 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;
}
return false;
}
} // namespace ota
} // namespace mesh
#endif