649 lines
22 KiB
C++
649 lines
22 KiB
C++
#include "WarmNodeStore.h"
|
||
|
||
#if WARM_NODE_COUNT > 0
|
||
|
||
#include "FSCommon.h"
|
||
#include "SPILock.h"
|
||
#include "SafeFile.h"
|
||
#include "configuration.h"
|
||
#include "memory/MemAudit.h"
|
||
#include "power/PowerHAL.h"
|
||
#include <ErriezCRC32.h>
|
||
#include <vector>
|
||
|
||
#if defined(NRF52840_XXAA)
|
||
#include "flash/flash_nrf5x.h"
|
||
#define WARM_RING_MAGIC 0x334E5257u // "WRN3" - v3: last_heard low bits carry role + protected + signer
|
||
#define WARM_RING_MAGIC_V2 0x324E5257u // "WRN2" - v2: role + protected only; bit 6 was still timestamp.
|
||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" - v1: last_heard was a plain timestamp.
|
||
// Older pages are still read on upgrade: v1 keeps identity + key but discards last_heard,
|
||
// v2 keeps the word but clears bit 6, which was still part of the timestamp then.
|
||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
||
#define WARM_RING_TOMBSTONE 0xFFFFFFFFu
|
||
#else
|
||
// warm.dat layout: this header followed by count packed WarmNodeEntry records.
|
||
struct WarmStoreHeader {
|
||
uint32_t magic; // WARM_STORE_MAGIC
|
||
uint32_t reserved; // 0; kept so the header stays 16 B
|
||
uint16_t count; // entries persisted
|
||
uint16_t entrySize; // sizeof(WarmNodeEntry), format guard
|
||
uint32_t crc; // crc32 over count * entrySize bytes
|
||
};
|
||
static_assert(sizeof(WarmStoreHeader) == 16, "header layout is part of the persistence format");
|
||
|
||
#define WARM_STORE_MAGIC 0x334D5257u // "WRM3" - v3: last_heard low bits carry role + protected + signer
|
||
#define WARM_STORE_MAGIC_V2 \
|
||
0x324D5257u // "WRM2" - v2: role + protected only; bit 6 was still timestamp. On upgrade
|
||
// we clear the signer bit, then rewrite as v3.
|
||
#define WARM_STORE_MAGIC_V1 \
|
||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
||
// identity + key but discard last_heard, then rewrite as v3.
|
||
|
||
#ifdef FSCom
|
||
static const char *warmFileName = "/prefs/warm.dat";
|
||
#endif
|
||
#endif // NRF52840_XXAA
|
||
|
||
static inline bool keyIsSet(const uint8_t key[32])
|
||
{
|
||
for (int i = 0; i < 32; i++)
|
||
if (key[i])
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
WarmNodeStore::WarmNodeStore()
|
||
{
|
||
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
||
entries = static_cast<WarmNodeEntry *>(ps_calloc(WARM_NODE_COUNT, sizeof(WarmNodeEntry)));
|
||
if (!entries) {
|
||
LOG_WARN("WarmStore: PSRAM alloc failed, using heap");
|
||
entries = static_cast<WarmNodeEntry *>(calloc(WARM_NODE_COUNT, sizeof(WarmNodeEntry)));
|
||
}
|
||
#else
|
||
entries = static_cast<WarmNodeEntry *>(calloc(WARM_NODE_COUNT, sizeof(WarmNodeEntry)));
|
||
#endif
|
||
memaudit::set("warm", entries ? WARM_NODE_COUNT * sizeof(WarmNodeEntry) : 0);
|
||
#if defined(NRF52840_XXAA)
|
||
memset(pageOf, kNoPage, sizeof(pageOf));
|
||
#endif
|
||
}
|
||
|
||
WarmNodeStore::~WarmNodeStore()
|
||
{
|
||
free(entries); // always malloc-family (calloc / ps_calloc)
|
||
entries = nullptr;
|
||
memaudit::set("warm", 0);
|
||
}
|
||
|
||
WarmNodeEntry *WarmNodeStore::find(NodeNum num) const
|
||
{
|
||
if (!entries || !num)
|
||
return nullptr;
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||
if (entries[i].num == num)
|
||
return &entries[i];
|
||
return nullptr;
|
||
}
|
||
|
||
// Slot placement with the keyed-first admission policy. Shared by absorb()
|
||
// and the ring replay, so the policy is applied identically in both paths.
|
||
WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8_t *key32)
|
||
{
|
||
if (!entries || !num)
|
||
return nullptr;
|
||
|
||
const bool candidateKeyed = key32 && keyIsSet(key32);
|
||
|
||
WarmNodeEntry *slot = find(num);
|
||
const bool sameNode = slot != nullptr;
|
||
if (!slot) {
|
||
// Pick a victim: any empty slot, else the oldest keyless entry, else
|
||
// (only for keyed candidates) the oldest keyed entry.
|
||
WarmNodeEntry *oldestKeyless = nullptr, *oldestKeyed = nullptr;
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++) {
|
||
WarmNodeEntry &e = entries[i];
|
||
if (!e.num) {
|
||
slot = &e;
|
||
break;
|
||
}
|
||
// Compare on the time bits only - the low metadata bits (role/protected) must
|
||
// not perturb LRU victim selection.
|
||
if (keyIsSet(e.public_key)) {
|
||
if (!oldestKeyed || warmTimeOf(e) < warmTimeOf(*oldestKeyed))
|
||
oldestKeyed = &e;
|
||
} else {
|
||
if (!oldestKeyless || warmTimeOf(e) < warmTimeOf(*oldestKeyless))
|
||
oldestKeyless = &e;
|
||
}
|
||
}
|
||
if (!slot)
|
||
slot = oldestKeyless ? oldestKeyless : (candidateKeyed ? oldestKeyed : nullptr);
|
||
if (!slot)
|
||
return nullptr; // store full of keyed entries and the candidate has no key
|
||
}
|
||
|
||
slot->num = num;
|
||
slot->last_heard = lastHeard;
|
||
if (candidateKeyed)
|
||
memcpy(slot->public_key, key32, 32);
|
||
else if (!sameNode)
|
||
// Repurposing a victim slot for a different node: clear its stale key.
|
||
// A keyless refresh of a node already here keeps the key we learned.
|
||
memset(slot->public_key, 0, 32);
|
||
return slot;
|
||
}
|
||
|
||
bool WarmNodeStore::absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32, uint8_t role, uint8_t protectedCat, bool signer)
|
||
{
|
||
// Pack role + protected category + signer into the low bits of last_heard. place() and
|
||
// ring replay store the raw word verbatim, so the metadata round-trips through flash.
|
||
const uint32_t packed = warmPackLastHeard(lastHeard, role, protectedCat, signer);
|
||
const WarmNodeEntry *slot = place(num, packed, key32);
|
||
if (!slot)
|
||
return false;
|
||
persistEntry(*slot);
|
||
LOG_MIGRATION("WarmStore absorb 0x%08x key=%d last_heard=%u role=%u prot=%u signer=%u (now %u/%u)", (unsigned)num,
|
||
keyIsSet(slot->public_key) ? 1 : 0, (unsigned)warmTimeOf(*slot), (unsigned)role, (unsigned)protectedCat,
|
||
signer ? 1u : 0u, (unsigned)count(), (unsigned)capacity());
|
||
return true;
|
||
}
|
||
|
||
bool WarmNodeStore::lookupMeta(NodeNum num, uint8_t &role, uint8_t &protectedCat) const
|
||
{
|
||
const WarmNodeEntry *e = find(num);
|
||
if (!e)
|
||
return false;
|
||
role = warmRoleOf(*e);
|
||
protectedCat = warmProtOf(*e);
|
||
return true;
|
||
}
|
||
|
||
bool WarmNodeStore::take(NodeNum num, WarmNodeEntry &out)
|
||
{
|
||
WarmNodeEntry *e = find(num);
|
||
if (!e)
|
||
return false;
|
||
out = *e;
|
||
const int idx = static_cast<int>(e - entries);
|
||
memset(e, 0, sizeof(*e));
|
||
persistRemove(num, idx);
|
||
LOG_MIGRATION("WarmStore take(rehydrate) 0x%08x key=%d (now %u/%u)", (unsigned)num, keyIsSet(out.public_key) ? 1 : 0,
|
||
(unsigned)count(), (unsigned)capacity());
|
||
return true;
|
||
}
|
||
|
||
#if MESHTASTIC_NODEDB_MIGRATION_VERBOSE
|
||
void WarmNodeStore::dumpToLog(const char *reason) const
|
||
{
|
||
if (!entries) {
|
||
LOG_MIGRATION("WarmStore dump (%s): backend not allocated", reason);
|
||
return;
|
||
}
|
||
LOG_MIGRATION("WarmStore dump (%s): %u live / %u cap ==>", reason, (unsigned)count(), (unsigned)capacity());
|
||
unsigned shown = 0;
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++) {
|
||
const WarmNodeEntry &e = entries[i];
|
||
if (e.num == 0)
|
||
continue;
|
||
LOG_MIGRATION(" warm[%3u] 0x%08x last_heard=%u key=%d", (unsigned)i, (unsigned)e.num, (unsigned)e.last_heard,
|
||
keyIsSet(e.public_key) ? 1 : 0);
|
||
shown++;
|
||
}
|
||
LOG_MIGRATION("WarmStore dump (%s): <== end (%u entries)", reason, shown);
|
||
}
|
||
#endif // MESHTASTIC_NODEDB_MIGRATION_VERBOSE
|
||
|
||
bool WarmNodeStore::copyKey(NodeNum num, uint8_t out[32]) const
|
||
{
|
||
const WarmNodeEntry *e = find(num);
|
||
if (!e || !keyIsSet(e->public_key))
|
||
return false;
|
||
memcpy(out, e->public_key, 32);
|
||
return true;
|
||
}
|
||
|
||
bool WarmNodeStore::contains(NodeNum num) const
|
||
{
|
||
return find(num) != nullptr;
|
||
}
|
||
|
||
void WarmNodeStore::remove(NodeNum num)
|
||
{
|
||
WarmNodeEntry *e = find(num);
|
||
if (e) {
|
||
const int idx = static_cast<int>(e - entries);
|
||
memset(e, 0, sizeof(*e));
|
||
persistRemove(num, idx);
|
||
}
|
||
}
|
||
|
||
void WarmNodeStore::clear()
|
||
{
|
||
if (!entries)
|
||
return;
|
||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||
#if defined(NRF52840_XXAA)
|
||
memset(pageOf, kNoPage, sizeof(pageOf));
|
||
#endif
|
||
persistClear();
|
||
}
|
||
|
||
size_t WarmNodeStore::count() const
|
||
{
|
||
size_t n = 0;
|
||
if (entries)
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||
if (entries[i].num)
|
||
n++;
|
||
return n;
|
||
}
|
||
|
||
bool WarmNodeStore::saveIfDirty()
|
||
{
|
||
if (!dirty)
|
||
return true;
|
||
bool ok = save();
|
||
if (ok)
|
||
dirty = false;
|
||
return ok;
|
||
}
|
||
|
||
#if defined(NRF52840_XXAA)
|
||
|
||
// Raw-flash record-ring backend (nRF52840).
|
||
// 3 × 4 KB pages below LittleFS. Mutations append 40 B records (entry snapshot,
|
||
// or tombstone with last_heard == 0xFFFFFFFF) via the shared flash_nrf5x page
|
||
// cache; saveIfDirty() is the durability point. A full page reclaims the oldest
|
||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||
// the page cache is shared with InternalFS/LittleFS.
|
||
|
||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, WarmFormat *fmt) const
|
||
{
|
||
flash_nrf5x_read(&h, WARM_FLASH_PAGE_ADDR(page), sizeof(h));
|
||
if (h.seq == 0xFFFFFFFFu)
|
||
return false; // erased page
|
||
if (h.magic == WARM_RING_MAGIC) {
|
||
if (fmt)
|
||
*fmt = WarmFormat::Current;
|
||
return true;
|
||
}
|
||
if (h.magic == WARM_RING_MAGIC_V2) {
|
||
if (fmt)
|
||
*fmt = WarmFormat::V2; // replay it, but clear the signer bit (see WARM_RING_MAGIC_V2)
|
||
return true;
|
||
}
|
||
if (h.magic == WARM_RING_MAGIC_V1) {
|
||
if (fmt)
|
||
*fmt = WarmFormat::V1; // replay it, but discard last_heard (see WARM_RING_MAGIC_V1)
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Caller holds spiLock.
|
||
void WarmNodeStore::ringOpenPage(uint8_t page)
|
||
{
|
||
// Drop any cached state for the page before the real erase, so a later
|
||
// cache flush can't resurrect stale bytes.
|
||
flash_nrf5x_flush();
|
||
flash_nrf5x_erase(WARM_FLASH_PAGE_ADDR(page));
|
||
WarmPageHeader h;
|
||
h.magic = WARM_RING_MAGIC;
|
||
h.seq = nextSeq++;
|
||
flash_nrf5x_write(WARM_FLASH_PAGE_ADDR(page), &h, sizeof(h));
|
||
activePage = page;
|
||
writeSlot = 0;
|
||
}
|
||
|
||
// Caller holds spiLock. May recurse once via ringAppend if the stranded set
|
||
// fills the fresh page exactly - bounded by WARM_NODE_COUNT <= 2*kRecordsPerPage.
|
||
void WarmNodeStore::ringRotate()
|
||
{
|
||
uint8_t target = 0;
|
||
if (activePage != kNoPage) {
|
||
// Lowest-seq valid page, preferring erased pages; never the active one
|
||
uint32_t bestSeq = 0;
|
||
bool found = false;
|
||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
||
if (p == activePage)
|
||
continue;
|
||
WarmPageHeader h;
|
||
if (!ringReadHeader(p, h)) {
|
||
target = p; // erased/invalid page: free real estate, take it
|
||
found = true;
|
||
break;
|
||
}
|
||
if (!found || static_cast<int32_t>(h.seq - bestSeq) < 0) {
|
||
target = p;
|
||
bestSeq = h.seq;
|
||
found = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Capture live entries stranded in the page we're about to erase
|
||
int stranded[WARM_NODE_COUNT] = {};
|
||
int nStranded = 0;
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++) {
|
||
if (entries[i].num && pageOf[i] == target)
|
||
stranded[nStranded++] = static_cast<int>(i);
|
||
if (pageOf[i] == target)
|
||
pageOf[i] = kNoPage;
|
||
}
|
||
|
||
ringOpenPage(target);
|
||
|
||
for (int k = 0; k < nStranded; k++)
|
||
ringAppend(entries[stranded[k]], stranded[k]);
|
||
}
|
||
|
||
// Caller holds spiLock.
|
||
void WarmNodeStore::ringAppend(const WarmNodeEntry &rec, int storeSlot)
|
||
{
|
||
if (activePage == kNoPage || writeSlot >= kRecordsPerPage)
|
||
ringRotate();
|
||
const uint32_t addr =
|
||
WARM_FLASH_PAGE_ADDR(activePage) + sizeof(WarmPageHeader) + static_cast<uint32_t>(writeSlot) * sizeof(WarmNodeEntry);
|
||
flash_nrf5x_write(addr, &rec, sizeof(rec));
|
||
writeSlot++;
|
||
if (storeSlot >= 0)
|
||
pageOf[storeSlot] = activePage;
|
||
dirty = true;
|
||
}
|
||
|
||
void WarmNodeStore::persistEntry(const WarmNodeEntry &e)
|
||
{
|
||
concurrency::LockGuard g(spiLock);
|
||
ringAppend(e, static_cast<int>(&e - entries));
|
||
}
|
||
|
||
void WarmNodeStore::persistRemove(NodeNum num, int storeSlot)
|
||
{
|
||
if (storeSlot >= 0 && storeSlot < static_cast<int>(WARM_NODE_COUNT))
|
||
pageOf[storeSlot] = 0xFF;
|
||
WarmNodeEntry tomb;
|
||
memset(&tomb, 0, sizeof(tomb));
|
||
tomb.num = num;
|
||
tomb.last_heard = WARM_RING_TOMBSTONE;
|
||
concurrency::LockGuard g(spiLock);
|
||
ringAppend(tomb, -1);
|
||
}
|
||
|
||
void WarmNodeStore::persistClear()
|
||
{
|
||
concurrency::LockGuard g(spiLock);
|
||
flash_nrf5x_flush();
|
||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++)
|
||
flash_nrf5x_erase(WARM_FLASH_PAGE_ADDR(p));
|
||
activePage = 0xFF;
|
||
writeSlot = 0;
|
||
nextSeq = 1;
|
||
dirty = false; // the erased ring already reflects the empty store
|
||
}
|
||
|
||
void WarmNodeStore::load()
|
||
{
|
||
if (!entries)
|
||
return;
|
||
concurrency::LockGuard g(spiLock);
|
||
|
||
// Order valid pages by ascending seq so replay applies oldest first
|
||
uint8_t order[WARM_FLASH_PAGES] = {};
|
||
uint32_t seqs[WARM_FLASH_PAGES] = {};
|
||
WarmFormat fmtOf[WARM_FLASH_PAGES] = {}; // per-page on-flash format; older ones are normalised on replay
|
||
uint8_t nValid = 0;
|
||
uint8_t nCorrupt = 0;
|
||
for (uint8_t p = 0; p < WARM_FLASH_PAGES; p++) {
|
||
WarmPageHeader h;
|
||
WarmFormat fmt = WarmFormat::Current;
|
||
if (!ringReadHeader(p, h, &fmt)) {
|
||
// An erased page reads back all-ones; any other magic is a
|
||
// partially-written or bit-rotted header we're dropping, so flag it
|
||
// rather than silently treating the loss as a clean empty ring.
|
||
if (h.magic != 0xFFFFFFFFu)
|
||
nCorrupt++;
|
||
continue;
|
||
}
|
||
fmtOf[p] = fmt;
|
||
uint8_t pos = nValid;
|
||
while (pos > 0 && static_cast<int32_t>(h.seq - seqs[pos - 1]) < 0) {
|
||
order[pos] = order[pos - 1];
|
||
seqs[pos] = seqs[pos - 1];
|
||
pos--;
|
||
}
|
||
order[pos] = p;
|
||
seqs[pos] = h.seq;
|
||
nValid++;
|
||
}
|
||
|
||
if (nValid == 0) {
|
||
activePage = 0xFF;
|
||
writeSlot = 0;
|
||
nextSeq = 1;
|
||
if (nCorrupt)
|
||
LOG_WARN("WarmStore: ring unreadable (%u bad page(s)), empty", nCorrupt);
|
||
else
|
||
LOG_INFO("WarmStore: ring empty, starting fresh");
|
||
return;
|
||
}
|
||
|
||
uint32_t replayed = 0;
|
||
uint32_t migrated = 0;
|
||
for (uint8_t k = 0; k < nValid; k++) {
|
||
const uint8_t p = order[k];
|
||
const WarmFormat fmt = fmtOf[p];
|
||
uint16_t slot = 0;
|
||
for (; slot < kRecordsPerPage; slot++) {
|
||
WarmNodeEntry rec;
|
||
flash_nrf5x_read(&rec, WARM_FLASH_PAGE_ADDR(p) + sizeof(WarmPageHeader) + (uint32_t)slot * sizeof(rec), sizeof(rec));
|
||
if (rec.num == 0xFFFFFFFFu)
|
||
break; // erased space: end of this page's records (append-only)
|
||
if (rec.num == 0)
|
||
continue; // unexpected; skip defensively
|
||
replayed++;
|
||
if (rec.last_heard == WARM_RING_TOMBSTONE) {
|
||
WarmNodeEntry *e = find(rec.num);
|
||
if (e) {
|
||
pageOf[e - entries] = 0xFF;
|
||
memset(e, 0, sizeof(*e));
|
||
}
|
||
} else {
|
||
// Normalise older records: v1's timestamp would be misread as role/protected,
|
||
// and v2's bit 6 as a signer we never verified.
|
||
uint32_t lh = rec.last_heard;
|
||
if (fmt == WarmFormat::V1) {
|
||
lh = 0;
|
||
migrated++;
|
||
} else if (fmt == WarmFormat::V2) {
|
||
lh &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||
migrated++;
|
||
}
|
||
const WarmNodeEntry *e = place(rec.num, lh, rec.public_key);
|
||
if (e)
|
||
pageOf[e - entries] = p;
|
||
}
|
||
}
|
||
if (k == nValid - 1) { // newest page becomes the active head
|
||
activePage = p;
|
||
writeSlot = slot;
|
||
nextSeq = seqs[k] + 1;
|
||
// Rotate rather than append into an older-format page: a later load would
|
||
// normalise the new records to that page's format and drop metadata.
|
||
if (fmt != WarmFormat::Current)
|
||
writeSlot = kRecordsPerPage;
|
||
}
|
||
}
|
||
if (nCorrupt)
|
||
LOG_WARN("WarmStore: dropped %u corrupt ring page(s), some nodes lost", nCorrupt);
|
||
if (migrated)
|
||
LOG_INFO("WarmStore: migrated %u legacy record(s) to the current format", (unsigned)migrated);
|
||
LOG_INFO("WarmStore: replayed %u ring records -> %u live nodes (page %u, slot %u)", (unsigned)replayed, (unsigned)count(),
|
||
activePage, writeSlot);
|
||
}
|
||
|
||
bool WarmNodeStore::save()
|
||
{
|
||
if (!powerHAL_isPowerLevelSafe()) {
|
||
LOG_ERROR("Error: trying to save WarmStore on unsafe device power level.");
|
||
return false;
|
||
}
|
||
concurrency::LockGuard g(spiLock);
|
||
flash_nrf5x_flush();
|
||
return true;
|
||
}
|
||
|
||
#else // !NRF52840_XXAA --------------------
|
||
|
||
void WarmNodeStore::persistEntry(const WarmNodeEntry &e)
|
||
{
|
||
(void)e;
|
||
dirty = true;
|
||
}
|
||
|
||
void WarmNodeStore::persistRemove(NodeNum num, int storeSlot)
|
||
{
|
||
(void)num;
|
||
(void)storeSlot;
|
||
dirty = true;
|
||
}
|
||
|
||
void WarmNodeStore::persistClear()
|
||
{
|
||
dirty = true;
|
||
}
|
||
|
||
#ifdef FSCom
|
||
|
||
// ---- File persistence: /prefs/warm.dat snapshots ----------------------------
|
||
|
||
// Compact occupied slots to the front of `dst`; returns the count.
|
||
static uint16_t packEntries(const WarmNodeEntry *src, WarmNodeEntry *dst)
|
||
{
|
||
uint16_t n = 0;
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||
if (src[i].num)
|
||
dst[n++] = src[i];
|
||
return n;
|
||
}
|
||
|
||
void WarmNodeStore::load()
|
||
{
|
||
if (!entries)
|
||
return;
|
||
// Clear first - all failure paths below then correctly represent "empty",
|
||
// even if load() is called on an already-used instance.
|
||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||
concurrency::LockGuard g(spiLock);
|
||
auto f = FSCom.open(warmFileName, FILE_O_READ);
|
||
if (!f)
|
||
return;
|
||
WarmStoreHeader h;
|
||
if ((size_t)f.read((uint8_t *)&h, sizeof(h)) != sizeof(h)) {
|
||
f.close();
|
||
LOG_WARN("WarmStore: %s header read failed, starting empty", warmFileName);
|
||
return;
|
||
}
|
||
// Older snapshots are still accepted: same record size, fewer metadata bits in
|
||
// last_heard. Both are normalised to the current format below.
|
||
const bool known = h.magic == WARM_STORE_MAGIC || h.magic == WARM_STORE_MAGIC_V2 || h.magic == WARM_STORE_MAGIC_V1;
|
||
const WarmFormat fmt = h.magic == WARM_STORE_MAGIC_V1 ? WarmFormat::V1
|
||
: h.magic == WARM_STORE_MAGIC_V2 ? WarmFormat::V2
|
||
: WarmFormat::Current;
|
||
const bool legacy = fmt != WarmFormat::Current;
|
||
if (!known || h.entrySize != sizeof(WarmNodeEntry) || h.count > WARM_NODE_COUNT) {
|
||
f.close();
|
||
LOG_WARN("WarmStore: %s header invalid (magic=0x%08x entrySize=%u count=%u), starting empty", warmFileName, h.magic,
|
||
h.entrySize, h.count);
|
||
return;
|
||
}
|
||
if (h.count) {
|
||
const size_t len = (size_t)h.count * sizeof(WarmNodeEntry);
|
||
const bool readOk = (size_t)f.read((uint8_t *)entries, len) == len;
|
||
f.close();
|
||
if (!readOk) {
|
||
LOG_WARN("WarmStore: %s entries read failed, starting empty", warmFileName);
|
||
return;
|
||
}
|
||
// CRC covers the bytes as written (v1 still has the old last_heard), so check before migrating.
|
||
if (crc32Buffer(entries, len) != h.crc) {
|
||
LOG_WARN("WarmStore: %s CRC mismatch, starting empty", warmFileName);
|
||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||
return;
|
||
}
|
||
// Normalise older records, then mark dirty so save() rewrites in the current format:
|
||
// v1's timestamp would be misread as role/protected, v2's bit 6 as an unverified signer.
|
||
if (fmt == WarmFormat::V1) {
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||
if (entries[i].num)
|
||
entries[i].last_heard = 0;
|
||
dirty = true;
|
||
} else if (fmt == WarmFormat::V2) {
|
||
for (size_t i = 0; i < WARM_NODE_COUNT; i++)
|
||
if (entries[i].num)
|
||
entries[i].last_heard &= ~(WARM_SIGNER_MASK << WARM_SIGNER_SHIFT);
|
||
dirty = true;
|
||
}
|
||
} else {
|
||
f.close();
|
||
}
|
||
LOG_INFO("WarmStore: loaded %u warm nodes from %s%s", h.count, warmFileName,
|
||
legacy ? " (migrated from an older format)" : "");
|
||
}
|
||
|
||
bool WarmNodeStore::save()
|
||
{
|
||
if (!entries)
|
||
return false;
|
||
if (!powerHAL_isPowerLevelSafe()) {
|
||
LOG_ERROR("Error: trying to save WarmStore on unsafe device power level.");
|
||
return false;
|
||
}
|
||
|
||
std::vector<WarmNodeEntry> packed(WARM_NODE_COUNT);
|
||
WarmStoreHeader h;
|
||
h.magic = WARM_STORE_MAGIC;
|
||
h.reserved = 0;
|
||
h.count = packEntries(entries, packed.data());
|
||
h.entrySize = sizeof(WarmNodeEntry);
|
||
h.crc = crc32Buffer(packed.data(), h.count * sizeof(WarmNodeEntry));
|
||
|
||
// SafeFile already does its own spiLock in its constructor and close().
|
||
// Avoid nesting spiLocks, as this will hang until watchdog reset!
|
||
|
||
{
|
||
concurrency::LockGuard g(spiLock);
|
||
FSCom.mkdir("/prefs");
|
||
}
|
||
|
||
auto f = SafeFile(warmFileName, false);
|
||
|
||
{
|
||
concurrency::LockGuard g(spiLock);
|
||
f.write((const uint8_t *)&h, sizeof(h));
|
||
f.write((const uint8_t *)packed.data(), h.count * sizeof(WarmNodeEntry));
|
||
}
|
||
|
||
bool ok = f.close();
|
||
if (!ok)
|
||
LOG_ERROR("WarmStore: can't write %s", warmFileName);
|
||
else
|
||
LOG_DEBUG("WarmStore: saved %u warm nodes to %s", h.count, warmFileName);
|
||
return ok;
|
||
}
|
||
|
||
#else
|
||
|
||
void WarmNodeStore::load() {}
|
||
bool WarmNodeStore::save()
|
||
{
|
||
return true;
|
||
}
|
||
|
||
#endif // FSCom
|
||
#endif // NRF52840_XXAA
|
||
|
||
#endif // WARM_NODE_COUNT > 0
|