Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e03469e
Harden TMM NodeInfo direct-response: staleness, key hygiene, throttle
claude Jul 16, 2026
f3017e5
Add TODO(T1-T9) markers for code-review findings on NodeInfo hardening
claude Jul 16, 2026
09812be
Address code-review cleanups T6, T7, T8 in NodeInfo direct-response
claude Jul 16, 2026
a66409c
Throttle the NodeDB fallback direct-response path (T3)
claude Jul 17, 2026
7898b85
Make NodeInfo staleness/throttle wrap- and sentinel-safe (T4, T9)
claude Jul 17, 2026
02cc39e
Stamp PSRAM throttle entry by captured index, not a rescan (T5)
claude Jul 17, 2026
288fc6f
Document accepted per-target throttle tradeoffs (T1, T2)
claude Jul 17, 2026
0a1a86d
Track signed-provenance of cached NodeInfo public keys
claude Jul 17, 2026
3a90f65
Draw public keys from the TMM NodeInfo cache as a last resort
claude Jul 17, 2026
54ff850
Retain keyed NodeInfo entries as a pubkey pool; trust-tier eviction
claude Jul 17, 2026
0485bef
Test signed-provenance flag and copyPublicKey key-pool source
claude Jul 17, 2026
00e0e4a
Inherit signer provenance from NodeDB when caching a re-found node
claude Jul 17, 2026
24568f4
Lock NodeInfoPayloadEntry packing with a size assert
claude Jul 17, 2026
8716284
Pack NodeInfo cache booleans into 1-bit fields
claude Jul 17, 2026
7c0dc20
Gate NodeInfo replay on signer-proven provenance (compile-time, defau…
claude Jul 17, 2026
b1e3bb2
Rehydrate re-admitted node names from the TMM NodeInfo cache
claude Jul 17, 2026
9aafee1
Drop non-portable NodeInfoPayloadEntry size assert (fixes pico build)
claude Jul 17, 2026
42a15da
Run the NodeInfo cache paths in native tests (TMM_HAS_NODEINFO_CACHE)
claude Jul 17, 2026
00f95d7
Pin NodeInfo cache keys against the warm tier, not just the hot store
claude Jul 17, 2026
d209890
Convert NodeInfo cache clocks to uint8 tick counters
claude Jul 17, 2026
40d5069
Keep the NodeInfo cache a superset of NodeDB (seed + keep-alive)
claude Jul 17, 2026
a891d8e
Write-through NodeDB identity commits into the NodeInfo cache
claude Jul 17, 2026
4586f8d
Purge TrafficManagement caches on explicit node removal
claude Jul 17, 2026
b89b6ee
Document the tmm-fix-2 / tmm-fix-superset reconciliation decisions
claude Jul 17, 2026
1f861e0
Reconcile retention: no timed eviction, obsTick LRU, hourly seeding
claude Jul 17, 2026
293f5cd
Reconcile hooks: key-commit write-through, purgeAll, key-matched signer
claude Jul 17, 2026
0068dd7
Merge the two branches' test suites for the reconciled design
claude Jul 17, 2026
b0b695d
Adapt cherry-picked and seeding tests to the reconciled semantics
claude Jul 17, 2026
01df57b
trunk fml
NomDeTom Jul 17, 2026
02e323f
Doc tidyup
NomDeTom Jul 17, 2026
d002146
TrafficManagement: key NodeInfo-cache maintenance to its own guard
NomDeTom Jul 17, 2026
ad36dba
TrafficManagement: forward throttled NodeInfo requests instead of con…
NomDeTom Jul 17, 2026
f408c34
TrafficManagement: consolidate NodeInfo-cache #if sprawl into one region
NomDeTom Jul 17, 2026
61a6533
Router: resolve sender key only for PKI-decrypt candidates
NomDeTom Jul 17, 2026
a46c21f
TrafficManagement: refresh NodeInfo membership hourly, not per-sweep
NomDeTom Jul 17, 2026
6b8dfc4
NodeDB: centralize bare-key commits in commitRemoteKey()
NomDeTom Jul 17, 2026
03ff445
docs: tick wrap-safety analysis, updated cadences, CodeRabbit doc fixes
NomDeTom Jul 17, 2026
505921b
TrafficManagement: adapt tests to forwarded throttles and hourly memb…
NomDeTom Jul 17, 2026
8997a6c
expand test coverage
NomDeTom Jul 18, 2026
5514997
nitpicks and docs update
NomDeTom Jul 18, 2026
e5f0cca
more comment fixes
NomDeTom Jul 18, 2026
9dffc61
nitpicks
NomDeTom Jul 18, 2026
c374da1
test: make TMM fixture cleanup abort-safe in tearDown()
NomDeTom Jul 18, 2026
135551f
docs tidy
NomDeTom Jul 20, 2026
04ff850
Throttle NodeInfo direct responses
caveman99 Jul 20, 2026
c37d25e
test: reconcile TMM direct-response tests with #11104 throttle
NomDeTom Jul 20, 2026
6d5c3aa
TMM: unify direct-response throttles into per-sender + per-target RAM…
NomDeTom Jul 20, 2026
43f7261
whats up, doc?
NomDeTom Jul 20, 2026
63daf74
trimming the comments
NomDeTom Jul 20, 2026
a95cc26
test: bump native-suite-count to 38 after upstream rebase
NomDeTom Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions docs/node_info_stores.md

Large diffs are not rendered by default.

193 changes: 193 additions & 0 deletions docs/traffic_management_module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# The Traffic Management Module (TMM)

TMM is an optional module that shapes **transit** traffic on busy meshes. Large networks get
noisy fast - repeated position packets, bursty senders, and unknown/undecryptable frames all
burn limited airtime and power - and TMM filters or answers that traffic before it is
rebroadcast. On supported targets it **ships enabled** (`has_traffic_management` defaults to
true) with position dedup running at its 11 h default; the other features each default off, so
the module is on out of the box but opt-in per feature. It was introduced in
[meshtastic/firmware#9358](https://github.com/meshtastic/firmware/pull/9358).

This document covers the module's behaviour, with a deep dive on the two TMM-specific
NodeInfo features - **direct-serve** (answering NodeInfo requests on another node's behalf)
and the **throttling** that bounds it. The identity/traffic-state stores those features read
from are documented separately in [node_info_stores.md](node_info_stores.md); this file owns
the direct-serve and throttle behaviour, that file owns the stores.

Sources of truth: `src/modules/TrafficManagementModule.{h,cpp}`, defaults in
`src/mesh/Default.h`.

---

## How it runs

- **Enablement is three-gated.** Compile-time `HAS_TRAFFIC_MANAGEMENT` (with the
`MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT` build exclusion), then the runtime
`moduleConfig.has_traffic_management` presence flag. While the runtime gate is off, the
packet path, the maintenance sweep, the NodeDB write-through hooks, and the cache accessors
all no-op - content, maintenance, and reads are keyed to the same condition.
- **It runs before `RoutingModule`** in `callModules()`. Returning `STOP` from
`handleReceived()` fully consumes a packet, so it is never rebroadcast; `CONTINUE` lets it
proceed through normal relay handling.
- **State is cheap.** Per-node traffic-shaping counters live in a flat 10-byte
`UnifiedCacheEntry` array (position fingerprint, rate/unknown counters, modular tick
stamps, a next-hop hint, and a 4-bit role fallback) - see
[node_info_stores.md §3](node_info_stores.md). Direct-serve additionally reads the PSRAM
NodeInfo payload cache (or the NodeDB fallback when that cache is absent).

## What it does

| Feature | Default | In one line |
| ------------------------ | -------------- | -------------------------------------------------------------- |
| Position dedup | on, 11 h | Suppresses a stationary sender's repeated position broadcasts. |
| Per-sender rate limit | off | Caps how many transit packets one sender may spend per window. |
| Unknown-packet filter | off | Drops a sender's undecryptable traffic past a threshold. |
| NodeInfo direct response | off | Answers a NodeInfo request on the target's behalf (see below). |
| Position precision clamp | channel-driven | Truncates relayed position to the channel's precision. |

Config lives under `moduleConfig.traffic_management`; the per-feature sections below give the
exact fields, defaults, and behaviour. NodeInfo direct response has its own deep-dive sections
after these.

### Position dedup

`position_min_interval_secs` (default 11 h; `0` disables). Drops a duplicate position from the
same sender inside the interval, where "duplicate" means the same fingerprint on the channel's
`position_precision` grid (firmware default 19-bit, ~90 m cells). Role caps only ever _shorten_
the interval: **tracker / TAK tracker → 1 h**, **lost-and-found → 15 min**.

### Per-sender rate limit

`rate_limit_window_secs` + `rate_limit_max_packets` (default off; either `0` disables). Drops a
sender's transit packets once it exceeds the budget within the window.

### Unknown-packet filter

`unknown_packet_threshold` (default `0` = off). Drops undecryptable traffic from a sender once it
passes the threshold within a ~5 min window.

### NodeInfo direct response

`nodeinfo_direct_response_max_hops` (default `0` = off). When set, a neighbour that already
holds the target's identity answers a unicast NodeInfo request on its behalf, saving the full
round trip. This is TMM's most security-sensitive feature; the serve gates and the throttle
that bounds it are covered in the two dedicated sections below.

### Position precision clamp

Driven by the channel's `position_precision` ceiling (else the 19-bit firmware default).
`alterReceived()` truncates relayed position coordinates to that precision.

### Shelved

Present in the config surface but currently no-ops in the module, deferred until the right
heuristics are settled: hop exhaustion for position/telemetry (`exhaust_hop_position` /
`exhaust_hop_telemetry`) and `router_preserve_hops`. `alterReceived()` leaves rebroadcast hop
handling untouched.

---

## NodeInfo direct response (direct-serve)

Normally a unicast NodeInfo request travels all the way to the target and the reply travels
all the way back. On a large mesh that is several hops of airtime per lookup. When
`nodeinfo_direct_response_max_hops > 0`, a neighbour that already holds the target's identity
answers **on the target's behalf** with a spoofed reply, cutting the round trip to one hop.

**Data source.** The reply payload comes from the TMM NodeInfo payload cache (PSRAM-backed;
full cached `User` plus provenance metadata) or, on builds without that cache, from the
NodeDB fallback. Both are described in [node_info_stores.md §4](node_info_stores.md); this
feature is a _consumer_ of them.

**Decision pipeline** (`shouldRespondToNodeInfo()`), in order - any failure returns `false`
and the request is left to propagate normally:

1. **Eligibility** (checked by the caller): `nodeinfo_direct_response_max_hops > 0`,
`NODEINFO_APP` portnum, `want_response`, and the packet is unicast, not to us, not from us.
2. **Hop clamp** (`isMinHopsFromRequestor()`): respond only when the requester is within the
role-clamped hop ceiling - **routers up to 3 hops** (`kRouterDefaultMaxHops`, may be
lowered by config), **clients direct-only, 0 hops** (`kClientDefaultMaxHops`).
3. **Identity lookup**: NodeInfo cache hit (cache path) or NodeDB fallback (fallback path).
4. **Staleness gate (6 h)**: never vouch for a node not genuinely _heard_ within the serve
window. Only a real observed frame stamps the recency bit - seeding and write-through are
knowledge, not observation, so a silent node can never look alive to this path.
5. **Signer-provenance gate** (`TMM_NODEINFO_REPLAY_SIGNED_GATE`, default on): vouch only for
an identity whose key is signer-proven (XEdDSA-verified, directly or inherited from
NodeDB). A trust-on-first-use identity is left for the genuine node - or another
cache-holder that _has_ proof - to answer. Bypassed when PKI is compiled out.
6. **Throttle** (`directResponseAllowed()`): see the next section.

**The spoofed reply.** On success TMM emits a NodeInfo reply with `from` set to the _target_
(so the requester sees a valid answer), `to` the requester, `hop_limit = 0` (one hop only),
`request_id` the original packet id, and the OK_TO_MQTT bit set from local
`config.lora.config_ok_to_mqtt` policy. The requester's own identity claim in the request is
**not** written back to NodeDB - a unicast NodeInfo is unsigned, so treating it as an
identity update would be unauthenticated. `nodeinfo_cache_hits` counts only replies actually
sent.

---

## Throttling direct responses

A direct reply is addressed to the requesting packet's `from` and spoofs the requested
target - and **both fields are unauthenticated header data**. Without a bound, an attacker
crafts requests carrying a victim's address as `from`, and every neighbour holding the target
transmits at the victim: a reflector-amplification primitive. The throttle is the security
core of this feature, checked immediately before a reply would go out so requests declined for
other reasons never consume the budget.

**Three bounds**, all keyed off `clockMs()` and evaluated under `cacheLock`:

| Bound | Window | Bounds |
| ------------------------------------------------ | ------ | ------------------------------------------------ |
| Per requester (`kDirectResponsePerRequesterMs`) | 60 s | how much any single node can be made to receive |
| Per target (`kDirectResponsePerTargetMs`) | 60 s | how often we vouch for the same identity |
| Global airtime floor (`kDirectResponseGlobalMs`) | 1 s | total spoofed TX, regardless of key distribution |

**Mechanism.** The two per-key bounds are fixed **8-slot LRU tables in internal RAM**
(`directRequesterSeen`, `directTargetSeen`) - _not_ the PSRAM NodeInfo cache - so they behave
identically with and without PSRAM, on the cache path and the NodeDB-fallback path alike.
Timestamps are full `uint32` milliseconds compared by wrap-safe subtraction, so there is no
tick clock and no maintenance sweep to keep them honest. `directResponseAllowed(requester,
target, now)` resolves a slot in _both_ tables before stamping either - so a reply one axis
throttles never consumes the other axis's budget - then records the send on all three bounds.
The global floor is a single stamp, checked first as the cheap common case.

**When a table fills.** For an unseen key with no free slot, `directResponseSlot()` evicts the
**least-recently-used** entry (smallest last-reply time) and admits the new key. The LRU
victim is by construction the entry closest to expiring anyway, so eviction is the
lowest-cost choice. An attacker who cycles more than 8 distinct requesters or targets - easy,
since both are unauthenticated - evicts entries and defeats _per-key_ throttling for the
cycled keys; that is expected, and why the **global 1 s floor is the hard backstop**. It is a
single stamp, cannot fill, and caps total spoofed replies at ~1/s no matter what. Per-key
throttling degrades gracefully to the floor under pressure.

**Throttled is not dropped.** A throttled request returns `false`, which lets
`handleReceived()` `CONTINUE`: the request forwards toward the genuine target (which can
answer itself) rather than being black-holed. A requester whose first reply was lost on a
noisy link would otherwise get silence for the whole window; repeats of the same packet id
are already absorbed by the router's duplicate detection.

**Evolution.** The original design split throttling by path: a per-entry `respTick` stamp in
each NodeInfo cache slot (cache path, 30 s, swept for wrap-safety) plus a single module-global
stamp for the NodeDB fallback (30 s, neither per-requester nor per-target). Those two routes
were unified into the symmetric per-requester + per-target RAM tables above, aligned to a
single 60 s window, so both axes hold with and without PSRAM and the cache entry no longer
carries throttle state.

---

## Configuration

All tunables live under `moduleConfig.traffic_management`; the whole module is gated by the
`has_traffic_management` presence flag, and each per-feature section above lists its own
field(s) and default. Two related sets of knobs are **firmware constants, not config**: the
role-based position caps `default_traffic_mgmt_tracker_position_min_interval_secs` (1 h) and
`default_traffic_mgmt_lost_and_found_position_min_interval_secs` (15 min), and the direct-serve
throttle windows (the `kDirectResponse*Ms` constants).

## See also

- [node_info_stores.md](node_info_stores.md) - the NodeDB hot store, warm tier, TMM NodeInfo
payload cache, and unified cache that the direct-serve path reads from, plus their trust,
provenance, and anti-entropy model.
128 changes: 127 additions & 1 deletion src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
#if HAS_VARIABLE_HOPS
#include "modules/HopScalingModule.h"
#endif
#if HAS_TRAFFIC_MANAGEMENT
#include "modules/TrafficManagementModule.h"
#endif
#include "xmodem.h"
#include <ErriezCRC32.h>
#include <algorithm>
Expand Down Expand Up @@ -767,6 +770,12 @@ bool NodeDB::factoryReset(bool eraseBleBonds)
warmStore.clear();
warmStore.saveIfDirty();
#endif
#if HAS_TRAFFIC_MANAGEMENT
// Factory reset forgets everything; TMM's RAM caches must not survive to resurrect
// identities (the device usually reboots after this, but don't rely on it).
if (trafficManagementModule)
trafficManagementModule->purgeAll();
#endif

// second, install default state (this will deal with the duplicate mac address issue)
installDefaultNodeDatabase();
Expand Down Expand Up @@ -1597,6 +1606,11 @@ void NodeDB::resetNodes(bool keepFavorites)
#if WARM_NODE_COUNT > 0
warmStore.clear(); // warm entries are never favorites; a DB reset clears them too
#endif
#if HAS_TRAFFIC_MANAGEMENT
// A user-initiated DB reset forgets everything; TMM's caches must not resurrect it.
if (trafficManagementModule)
trafficManagementModule->purgeAll();
#endif

devicestate.has_rx_waypoint = false;
saveNodeDatabaseToDisk();
Expand Down Expand Up @@ -1629,6 +1643,12 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
// Explicit user removal: don't let the warm tier resurrect the node
warmStore.remove(nodeNum);
#endif
#if HAS_TRAFFIC_MANAGEMENT
// Explicit removal is full removal: the TrafficManagement caches (unified slot +
// NodeInfo identity cache) must not keep serving or resurrect the node either.
if (trafficManagementModule)
trafficManagementModule->purgeNode(nodeNum);
#endif

LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
saveNodeDatabaseToDisk();
Expand Down Expand Up @@ -3404,6 +3424,20 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
}
}

#if HAS_TRAFFIC_MANAGEMENT
// Write-through: every accepted remote-identity commit lands here (NodeInfoModule,
// MeshService, and TMM's requester learning all funnel through updateUser; the two
// key-write sites that bypass it call onNodeKeyCommitted instead), so TMM's NodeInfo
// cache reflects the commit immediately rather than at the next reconcile pass. Runs on
// acceptance, not on `changed`: an identical update still proves the identity is
// current. `p` is the post-hygiene payload; signerKnown transfers only key-matched
// verified-signer status (isVerifiedSignerForKey semantics), never a bare node flag.
if (nodeId != getNodeNum() && trafficManagementModule) {
const bool signerKnown = p.public_key.size == 32 && isVerifiedSignerForKey(nodeId, p.public_key.bytes);
trafficManagementModule->onNodeIdentityCommitted(nodeId, p, signerKnown);
}
#endif

return changed;
}

Expand Down Expand Up @@ -3714,7 +3748,7 @@ uint32_t NodeDB::hotNodeLastHeard(NodeNum n) const
return 0;
}

bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
bool NodeDB::copyPublicKeyAuthoritative(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
{
const meshtastic_NodeInfoLite *info = getMeshNode(n);
if (info && info->public_key.size == 32) {
Expand All @@ -3730,6 +3764,81 @@ bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
return false;
}

bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
{
if (copyPublicKeyAuthoritative(n, out))
return true;
#if HAS_TRAFFIC_MANAGEMENT
// Last resort: a key the TrafficManagement NodeInfo cache learned from an observed frame
// for a node no longer in either NodeDB tier. This extends the pool of peers we can
// encrypt to. Keys here may be trust-on-first-use (see copyPublicKey's signerProven), the
// same first-contact trust NodeDB itself applies via updateUser().
if (trafficManagementModule && trafficManagementModule->copyPublicKey(n, out.bytes)) {
out.size = 32;
return true;
}
#endif
return false;
}

bool NodeDB::isVerifiedSignerForKey(NodeNum n, const uint8_t *key32)
{
if (!key32)
return false;
// Hot store is authoritative when present; a node lives in the hot XOR warm tier, so if the
// hot store holds it the warm tier does not, and we decide entirely from the hot entry.
const meshtastic_NodeInfoLite *info = getMeshNode(n);
if (info)
return info->public_key.size == 32 && nodeInfoLiteHasXeddsaSigned(info) && memcmp(info->public_key.bytes, key32, 32) == 0;
#if WARM_NODE_COUNT > 0
uint8_t warmKey[32];
if (warmStore.copyKey(n, warmKey) && memcmp(warmKey, key32, 32) == 0)
return warmStore.isVerifiedSigner(n);
#endif
return false;
}

bool NodeDB::isKnownXeddsaSigner(NodeNum n)
{
// A node lives in the hot XOR warm tier, so the hot verdict is final when present.
const meshtastic_NodeInfoLite *info = getMeshNode(n);
if (info)
return nodeInfoLiteHasXeddsaSigned(info);
#if WARM_NODE_COUNT > 0
return warmStore.isVerifiedSigner(n);
#else
return false;
#endif
}

void NodeDB::commitRemoteKey(NodeNum n, const uint8_t key32[32], KeyCommitTrust trust)
{
if (!key32 || n == 0)
return;
// Local copy first: callers may pass the node's own key bytes back in (e.g. manual
// verification re-committing an already-stored key), and memcpy forbids overlap.
uint8_t key[32];
memcpy(key, key32, 32);

meshtastic_NodeInfoLite *info = getOrCreateMeshNode(n);
if (!info)
return;
// Unconditional overwrite - deliberately NOT updateUser()'s "don't replace a known key" pin.
// That pin protects against unauthenticated NodeInfo broadcasts; the only callers here are
// possession/authority-proven (ManuallyVerified = user confirmed the key; AdminChannelProven =
// decrypted via the admin key with p->from bound into the AEAD nonce), i.e. exactly the paths
// meant to establish or rotate a key. Keep new call sites to that same trust bar.
memcpy(info->public_key.bytes, key, 32);
info->public_key.size = 32;

#if HAS_TRAFFIC_MANAGEMENT
// Write-through, mirroring updateUser()'s identity hook: without it the TrafficManagement
// NodeInfo cache diverges until the next hourly reconcile.
if (trafficManagementModule)
trafficManagementModule->onNodeKeyCommitted(n, key, trust == KeyCommitTrust::ManuallyVerified);
#endif
}

meshtastic_Config_DeviceConfig_Role NodeDB::getNodeRole(NodeNum n)
{
const meshtastic_NodeInfoLite *info = getMeshNode(n);
Expand Down Expand Up @@ -3827,6 +3936,23 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
}
LOG_MIGRATION("Rehydrated node 0x%08x from warm tier (key=%d)", n, lite->public_key.size == 32);
}
#endif
#if HAS_TRAFFIC_MANAGEMENT
// Name rehydration: the warm tier keeps a node's key but not its name, so a re-admitted
// long-tail node is nameless until its next NodeInfo. The TrafficManagement NodeInfo
// cache is much larger and often still holds the full User. Restore it - but only when
// its cached key matches the key we just restored from warm, so a name never attaches to
// a different identity than the one we encrypt to. No-op without the TMM NodeInfo cache
// or when no key is present (key-matched by design). CopyUserToNodeInfoLite sets only the
// user-related bits, so the warm-restored signer bit survives.
if (lite->public_key.size == 32 && !nodeInfoLiteHasUser(lite) && trafficManagementModule) {
meshtastic_User tmmUser = meshtastic_User_init_zero;
if (trafficManagementModule->copyUser(n, tmmUser) && tmmUser.public_key.size == 32 &&
memcmp(tmmUser.public_key.bytes, lite->public_key.bytes, 32) == 0) {
TypeConversions::CopyUserToNodeInfoLite(lite, tmmUser);
LOG_INFO("Rehydrated node 0x%08x identity from TMM NodeInfo cache", n);
}
}
#endif
LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap());
}
Expand Down
Loading
Loading