* 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>
133 lines
6.0 KiB
C++
133 lines
6.0 KiB
C++
// Migration from the legacy (pre-v25) NodeDatabase shape to the slim header +
|
|
// satellite maps layout. Lives in its own translation unit so the bulk
|
|
// fixed-shape decode + field-by-field copy doesn't clutter NodeDB.cpp.
|
|
//
|
|
// Caller (NodeDB::loadFromDisk) decides what to do with the result:
|
|
// - true -> persist via saveNodeDatabaseToDisk()
|
|
// - false -> reset via installDefaultNodeDatabase()
|
|
//
|
|
// This file (and the deviceonly_legacy proto) can be removed once
|
|
// DEVICESTATE_MIN_VER advances past 24.
|
|
|
|
#include "NodeDB.h"
|
|
#include "concurrency/LockGuard.h"
|
|
#include "configuration.h"
|
|
#include "mesh-pb-constants.h"
|
|
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
|
#include "meshUtils.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
|
|
bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
|
{
|
|
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
|
|
if (ostream) {
|
|
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite_Legacy> *>(iter->pData);
|
|
for (auto item : *vec) {
|
|
if (!pb_encode_tag_for_field(ostream, iter))
|
|
return false;
|
|
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_Legacy_fields, &item))
|
|
return false;
|
|
}
|
|
}
|
|
if (istream) {
|
|
meshtastic_NodeInfoLite_Legacy node;
|
|
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite_Legacy> *>(iter->pData);
|
|
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_Legacy_fields, &node))
|
|
vec->push_back(node);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool NodeDB::migrateLegacyNodeDatabase()
|
|
{
|
|
LOG_WARN("NodeDatabase v%u: migrating to v%u", nodeDatabase.version, DEVICESTATE_CUR_VER);
|
|
|
|
// _init_zero brace-inits the embedded std::vector via its explicit
|
|
// (size_type, allocator) ctor, so default-construct instead.
|
|
meshtastic_NodeDatabase_Legacy legacyDb{};
|
|
legacyDb.version = 0;
|
|
meshtastic_NodeDatabase_Legacy legacyEmpty{};
|
|
legacyEmpty.version = DEVICESTATE_CUR_VER;
|
|
size_t legacyEmptyEncoded = 0;
|
|
pb_get_encoded_size(&legacyEmptyEncoded, meshtastic_NodeDatabase_Legacy_fields, &legacyEmpty);
|
|
const size_t legacyBufSize = legacyEmptyEncoded + (MAX_NUM_NODES * meshtastic_NodeInfoLite_Legacy_size);
|
|
auto legacyState = loadProto(nodeDatabaseFileName, legacyBufSize, sizeof(meshtastic_NodeDatabase_Legacy),
|
|
&meshtastic_NodeDatabase_Legacy_msg, &legacyDb);
|
|
if (legacyState != LoadFileResult::LOAD_SUCCESS) {
|
|
LOG_ERROR("Failed to load NodeDatabase via legacy descriptor; installing default");
|
|
return false;
|
|
}
|
|
|
|
nodeDatabase.nodes.clear();
|
|
const size_t maxToMigrate = std::min<size_t>(legacyDb.nodes.size(), MAX_NUM_NODES);
|
|
nodeDatabase.nodes.reserve(maxToMigrate);
|
|
size_t posCount = 0, telCount = 0;
|
|
{
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
for (size_t i = 0; i < maxToMigrate; ++i) {
|
|
const auto &legacy = legacyDb.nodes[i];
|
|
meshtastic_NodeInfoLite slim = meshtastic_NodeInfoLite_init_default;
|
|
slim.num = legacy.num;
|
|
slim.snr = legacy.snr;
|
|
slim.last_heard = legacy.last_heard;
|
|
slim.channel = legacy.channel;
|
|
slim.has_hops_away = legacy.has_hops_away;
|
|
slim.hops_away = legacy.hops_away;
|
|
slim.next_hop = legacy.next_hop;
|
|
slim.bitfield = legacy.bitfield;
|
|
if (legacy.via_mqtt)
|
|
slim.bitfield |= NODEINFO_BITFIELD_VIA_MQTT_MASK;
|
|
if (legacy.is_favorite)
|
|
slim.bitfield |= NODEINFO_BITFIELD_IS_FAVORITE_MASK;
|
|
if (legacy.is_ignored)
|
|
slim.bitfield |= NODEINFO_BITFIELD_IS_IGNORED_MASK;
|
|
if (legacy.has_user) {
|
|
slim.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
|
|
strncpy(slim.long_name, legacy.user.long_name, sizeof(slim.long_name));
|
|
slim.long_name[sizeof(slim.long_name) - 1] = '\0';
|
|
sanitizeUtf8(slim.long_name, sizeof(slim.long_name)); // replace bad bytes so nanopb encode never fails
|
|
strncpy(slim.short_name, legacy.user.short_name, sizeof(slim.short_name));
|
|
slim.short_name[sizeof(slim.short_name) - 1] = '\0';
|
|
sanitizeUtf8(slim.short_name, sizeof(slim.short_name)); // same — v24 names may contain non-UTF-8 bytes
|
|
slim.hw_model = legacy.user.hw_model;
|
|
slim.role = legacy.user.role;
|
|
if (legacy.user.is_licensed)
|
|
slim.bitfield |= NODEINFO_BITFIELD_IS_LICENSED_MASK;
|
|
slim.public_key.size = legacy.user.public_key.size;
|
|
memcpy(slim.public_key.bytes, legacy.user.public_key.bytes, sizeof(slim.public_key.bytes));
|
|
if (legacy.user.has_is_unmessagable) {
|
|
slim.bitfield |= NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK;
|
|
if (legacy.user.is_unmessagable)
|
|
slim.bitfield |= NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK;
|
|
}
|
|
// macaddr deprecated since 1.2.11 - dropped from slim header.
|
|
}
|
|
nodeDatabase.nodes.push_back(slim);
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
if (legacy.has_position)
|
|
nodePositions[legacy.num] = legacy.position;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
if (legacy.has_device_metrics)
|
|
nodeTelemetry[legacy.num] = legacy.device_metrics;
|
|
#endif
|
|
}
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
posCount = nodePositions.size();
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
telCount = nodeTelemetry.size();
|
|
#endif
|
|
}
|
|
nodeDatabase.version = DEVICESTATE_CUR_VER;
|
|
meshNodes = &nodeDatabase.nodes;
|
|
numMeshNodes = nodeDatabase.nodes.size();
|
|
LOG_INFO("Migrated %u nodes from legacy -> v%u (positions: %u, telemetry: %u)", (unsigned)numMeshNodes, DEVICESTATE_CUR_VER,
|
|
(unsigned)posCount, (unsigned)telCount);
|
|
return true;
|
|
}
|