Pr1 nodedb warmstore (#10705)
* NodeDB: 3-tier node store with persistent warm tier (long-tail identity retention)
Introduces a tiered NodeDB so the device retains identity (public key,
last_heard) for far more nodes than fit in the full-record hot store,
without growing heap or the persisted nodes.proto unboundedly.
- Hot store: full NodeInfoLite, MAX_NUM_NODES (120 on nRF52).
- Satellite maps: position/telemetry/environment/status capped at
MAX_SATELLITE_NODES (40 freshest); eviction via enforceSatelliteCaps /
evictSatelliteOverCap.
- Warm tier (WarmNodeStore): 40 B {num,last_heard,public_key} records for
evicted nodes so DMs to/from long-tail nodes keep encrypting/decrypting.
Persisted to /prefs/warm.dat, or on nRF52840 a dedicated 12 KB raw-flash
record-ring below LittleFS (3x4 KB pages; see linker scripts + the
nrf52_warm_region.py post-link guard).
NodeDB::getOrCreateMeshNode now demotes evicted nodes into the warm tier and
re-admits them (restoring key/last_heard). Router PKI decrypt/encode resolve
the peer key via NodeDB::copyPublicKey (hot store, then warm tier).
NodeInfoLite gains snr_q4 (sint32, Q4-encoded dB); the float snr is zeroed on
disk. NodeInfoLite grows 105 -> 112 B; backup 2432 -> 2468 B.
Note: the snr_q4 .proto change still needs to land in the protobufs submodule
(generated header is updated here; submodule pointer left at upstream).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* NodeDB: robust receive + retention for blocked (ignored) nodes
Hardens how ignored/favourite nodes are received over admin and retained,
closing paths where a block could be lost or accidentally cleared.
- Blocking keeps the node's public key (admin set_ignored_node and
addFromContact no longer zero it / drop the warm-tier key), so a blocked
peer stays a verifiable identity.
- set_ignored_node creates the node if absent, so a block by node ID sticks
even for a node we've never heard from (e.g. pushed by a remote admin) with
no NodeInfo or key.
- Eviction protection (favourite/ignored/manually-verified) now also applies to
the load-time hot-store migration and is never undone by cleanupMeshDB, which
previously purged ignored nodes that lacked user info.
- The hot-store migration leaves our own node (index 0) in place and prefers to
demote non-protected nodes, like the runtime eviction scan.
Caps the protected set (favourite + ignored + verified) at MAX_NUM_NODES-2 via
NodeDB::setProtectedFlag(), so at least two evictable slots always remain and
getOrCreateMeshNode can always make room — replacing the previous unconditional
append that could run off the end of the node vector when every node was
protected. A locally-set favourite/ignore that hits the cap reports back to the
phone via a ClientNotification.
Adds test_nodedb_blocked covering the migration, favourite/ignored eviction
protection, ignored-survives-cleanup, and the protected-node cap. The
maintenance methods stay private in production; the test reaches them through a
PIO_UNIT_TESTING-guarded friend shim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
# src/mesh/NodeDB.h
* fix copilot comments
* once again
* WarmNodeStore: fix cppcheck warnings (uninitvar, constVariablePointer)
Zero-initialise `stranded[]` and `seqs[]/order[]` VLAs so cppcheck can
verify there are no unguarded reads of uninitialised memory (the guards
exist but are not visible to static analysis). Mark two local pointers
`const` where the pointed-to entry is never mutated after assignment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* self-care added to assist 2.7 and 2.8 nodedb migration
* Tidy warm-store/self-care: comments, guards, log + flash cleanup
Style/cleanup pass over the branch (no behavior change except the noted
preprocessor simplifications, which are semantically identical):
- Comments: move function descriptions to the headers, cap in-function
comments at ~3-4 lines, drop leading-number step markers, label stacked
#endif blocks, de-decorate banner comments.
- dumpToLog: fully gate decl + definition + AdminModule call site behind
MESHTASTIC_NODEDB_MIGRATION_VERBOSE so it compiles out when disabled
(~1.2 KB when off).
- mesh-pb-constants: drop the dead nRF52832 WARM_NODE_COUNT branch and trim
the macro docs.
- WarmNodeStore: simplify the redundant `ARCH_NRF52 && NRF52840_XXAA` guards
to `NRF52840_XXAA`, add a kNoPage sentinel for the ring page state.
- Shorten the always-on LOG_WARN strings (~120 B flash).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* more tidying up, aligning with docs and undoing other-arch regressions
* Update protobufs (#19)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* made the migration pathway cleareer
* address copilot review
* fixed a copilot review on a downstream PR.
* Address Copilot review comments for PR #10705 (warmstore/nodedb)
- WarmNodeStore.h: default MIGRATION_VERBOSE to 0 (suppress info-level
chatter on production builds; opt in with =1)
- WarmNodeStore.cpp load(): move memset to top of function so all
failure paths (header-read fail, invalid header) leave entries clear
- WarmNodeStore.cpp save(): replace manual spiLock lock/unlock around
mkdir with LockGuard covering the full SafeFile sequence, matching
the lock discipline in load()
- Router.cpp: memcpy(&p->public_key.bytes, ...) -> memcpy(p->public_key.bytes,
...) — pass decayed uint8_t* rather than pointer-to-array
- AdminModule.cpp: check setProtectedFlag return for PKC auto-favorite;
log cap-refusal warning instead of unconditional "auto-favoriting"
- nrf52_warm_region.py: error message references both v6.ld and v7.ld
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* NodeDB: formatting cleanup (blank lines after preprocessor blocks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Lukewarm store
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
co-authored by
GitHub
Claude Opus 4.8
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Ben Meadors
parent
b311e01ce0
commit
79a7dcc46c
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshTypes.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
// Verbose tracing for the warm-store migration + NodeDB self-care. Per-event /
|
||||
// per-boot chatter routes through this so it can be silenced in one place (set
|
||||
// to 0) once they're proven; genuine LOG_WARN anomalies stay unconditional.
|
||||
#ifndef MESHTASTIC_NODEDB_MIGRATION_VERBOSE
|
||||
#define MESHTASTIC_NODEDB_MIGRATION_VERBOSE 0
|
||||
#endif
|
||||
#if MESHTASTIC_NODEDB_MIGRATION_VERBOSE
|
||||
#define LOG_MIGRATION(...) LOG_INFO(__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_MIGRATION(...) ((void)0)
|
||||
#endif
|
||||
|
||||
#if WARM_NODE_COUNT > 0
|
||||
|
||||
/**
|
||||
* Warm ("long-tail") node tier.
|
||||
*
|
||||
* Minimal identity record (NodeNum, last_heard, Curve25519 public key) for nodes
|
||||
* evicted from the hot NodeInfoLite store, so DMs to/from them keep encrypting —
|
||||
* the key is expensive to re-learn, the rest rebuilds from traffic in seconds.
|
||||
* Flat fixed array, linear scan (only on hot-store misses), LRU by last_heard
|
||||
* with keyed entries outranking keyless.
|
||||
*
|
||||
* Persistence: nRF52840 uses a 12 KB raw-flash record-ring below LittleFS
|
||||
* (append + replay + compact-on-rotate — see the backend in WarmNodeStore.cpp,
|
||||
* link-guarded by nrf52840_s140_v7.ld). Everywhere else: /prefs/warm.dat.
|
||||
*/
|
||||
struct WarmNodeEntry {
|
||||
NodeNum num; // 0 = empty slot
|
||||
uint32_t last_heard; // recency for LRU ordering
|
||||
uint8_t public_key[32]; // all-zero = no key (a real key is never all-zero)
|
||||
};
|
||||
static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B — persistence format depends on it");
|
||||
|
||||
// Gated on NRF52840_XXAA: the ring sits at 0xEA000
|
||||
// valid only on the 1 MB-flash nRF52840.
|
||||
#if defined(NRF52840_XXAA)
|
||||
#define WARM_FLASH_PAGE_SIZE 4096u
|
||||
#define WARM_FLASH_PAGES 3u
|
||||
#define WARM_FLASH_REGION_BASE (0xED000u - WARM_FLASH_PAGES * WARM_FLASH_PAGE_SIZE) // 0xEA000
|
||||
#define WARM_FLASH_PAGE_ADDR(i) (WARM_FLASH_REGION_BASE + (i)*WARM_FLASH_PAGE_SIZE)
|
||||
#endif
|
||||
|
||||
class WarmNodeStore
|
||||
{
|
||||
public:
|
||||
WarmNodeStore();
|
||||
~WarmNodeStore();
|
||||
WarmNodeStore(const WarmNodeStore &) = delete;
|
||||
WarmNodeStore &operator=(const WarmNodeStore &) = delete;
|
||||
|
||||
/// Remember an evicted hot node. Keyless candidates never displace keyed
|
||||
/// entries; otherwise the oldest (keyless-first) entry is replaced.
|
||||
/// @return true if the node was stored or updated
|
||||
bool absorb(NodeNum num, uint32_t lastHeard, const uint8_t *key32 /* may be NULL */);
|
||||
|
||||
/// Find and remove an entry (used when the node is re-admitted to the hot store).
|
||||
bool take(NodeNum num, WarmNodeEntry &out);
|
||||
|
||||
/// Copy the 32-byte public key for a node, if we have one.
|
||||
bool copyKey(NodeNum num, uint8_t out[32]) const;
|
||||
|
||||
bool contains(NodeNum num) const;
|
||||
void remove(NodeNum num);
|
||||
void clear();
|
||||
size_t count() const;
|
||||
size_t capacity() const { return entries ? WARM_NODE_COUNT : 0; }
|
||||
|
||||
#if MESHTASTIC_NODEDB_MIGRATION_VERBOSE
|
||||
/// Debug: dump every live warm entry (num / last_heard / has-key) to the
|
||||
/// console. Compiled out unless MESHTASTIC_NODEDB_MIGRATION_VERBOSE.
|
||||
void dumpToLog(const char *reason = "dump") const;
|
||||
#endif
|
||||
|
||||
/// Load persisted entries (called once at boot, after the node DB loads).
|
||||
void load();
|
||||
/// Durability point, piggybacked on the node-database save cadence. On the
|
||||
/// ring backend this flushes the shared flash page cache; on the file
|
||||
/// backend it writes the warm.dat snapshot.
|
||||
bool saveIfDirty();
|
||||
|
||||
private:
|
||||
WarmNodeEntry *entries = nullptr; // WARM_NODE_COUNT slots; PSRAM on ESP32 when available
|
||||
bool dirty = false;
|
||||
|
||||
WarmNodeEntry *find(NodeNum num) const;
|
||||
// Internal slot-placement shared by absorb() and ring replay: applies the
|
||||
// keyed-first admission policy without touching persistence.
|
||||
WarmNodeEntry *place(NodeNum num, uint32_t lastHeard, const uint8_t *key32);
|
||||
|
||||
// Persistence hooks called from the mutation paths. File backend: mark
|
||||
// dirty. Ring backend: append an upsert/tombstone record (+ mark dirty).
|
||||
void persistEntry(const WarmNodeEntry &e); // e must point into entries[]
|
||||
void persistRemove(NodeNum num, int storeSlot);
|
||||
void persistClear();
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
// nRF52840 raw-flash record-ring state.
|
||||
struct WarmPageHeader {
|
||||
uint32_t magic; // WARM_RING_MAGIC
|
||||
uint32_t seq; // page generation; 0xFFFFFFFF = erased/unused
|
||||
};
|
||||
static_assert(sizeof(WarmPageHeader) == 8, "page header is part of the flash format");
|
||||
static constexpr uint16_t kRecordsPerPage = (WARM_FLASH_PAGE_SIZE - sizeof(WarmPageHeader)) / sizeof(WarmNodeEntry); // 102
|
||||
static_assert(WARM_NODE_COUNT <= 2 * ((WARM_FLASH_PAGE_SIZE - 8) / 40), "live set must fit the ring with one page reclaimed");
|
||||
|
||||
static constexpr uint8_t kNoPage = 0xFF; // "no page" sentinel for activePage / pageOf[]
|
||||
|
||||
uint8_t activePage = kNoPage; // no page opened yet (fresh/erased ring)
|
||||
uint16_t writeSlot = 0; // next free record slot in the active page
|
||||
uint32_t nextSeq = 1; // seq for the next page opened
|
||||
uint8_t pageOf[WARM_NODE_COUNT]; // flash page holding each RAM slot's newest record; kNoPage = none
|
||||
|
||||
void ringAppend(const WarmNodeEntry &rec, int storeSlot /* -1 for tombstones */);
|
||||
void ringRotate(); // reclaim oldest page, compacting stranded live entries
|
||||
void ringOpenPage(uint8_t page); // erase + write header (seq = nextSeq++)
|
||||
bool ringReadHeader(uint8_t page, WarmPageHeader &h) const;
|
||||
#endif
|
||||
|
||||
bool save();
|
||||
};
|
||||
|
||||
#endif // WARM_NODE_COUNT > 0
|
||||
Reference in New Issue
Block a user