Skip to content

Commit 2ecaf71

Browse files
NomDeTomclaude
andcommitted
TrafficManagement: flat unified cache + persistent next-hop overflow store
Reworks the TrafficManagementModule cache layer (policing behaviour unchanged from upstream) and adds a routing-hint overflow store: - Flatten the ring: replace the cuckoo-hashed unified cache and the bucketed PSRAM NodeInfo index with plain flat arrays + linear scan (same idiom as WarmNodeStore). At LoRa packet rates an O(n) scan of the cache is negligible, and it removes a large amount of hashing/displacement complexity. The cache entry is 11 B; timestamps use a uniform +1 presence-offset so a 0 byte always means "empty" across every sub-store. Adds rebaseEpoch() so cached state survives the ~19 h relative-timestamp horizon instead of being flushed. - Next-hop overflow cache: setNextHop/getNextHopHint store a confirmed last-byte relay for a destination, written only from NextHopRouter's ACK-confirmed decision (and mirrored from TraceRoute). NextHopRouter::getNextHop falls back to this cache when the hot NodeDB has no hint, so DMs/relays to long-tail nodes keep routing after the node ages out of NodeInfoLite. - Persistence: preloadNextHopsFromNodeDB warm-starts the cache from persisted NodeInfoLite hints on first maintenance pass; next_hop entries are kept alive across the maintenance sweep (no TTL) and never clobbered by a stale preload. All packet-policing logic (rate limit, position dedup, unknown-packet drop, NodeInfo direct response, hop exhaustion) is the existing upstream behaviour, untouched. HAS_TRAFFIC_MANAGEMENT defaults on so the module is compiled in. (see note). Tests: upstream policing suite now actually runs (adds the MeshTypes.h include that gates HAS_TRAFFIC_MANAGEMENT) plus 4 next-hop tests. Role-aware throttles, politeness, precision clamp, port-interval and mesh-radius gating — and the rate-limit >255 saturation fix — are deferred to the advanced-TMM branch. Note: default dedup movement grid moves to ~91m, which also means 1.5km required to end up with the same signature position - coarser and therefore further than before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7c1e1c3 commit 2ecaf71

8 files changed

Lines changed: 616 additions & 685 deletions

File tree

src/mesh/Default.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
enum class TrafficType { POSITION, TELEMETRY };
3535

3636
// Traffic management defaults
37-
#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells
37+
#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m)
3838
#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions
3939

4040
// Hop scaling defaults

src/mesh/NextHopRouter.cpp

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,27 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
9898
// destination
9999
if (p->from != 0) {
100100
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
101-
if (origTx) {
102-
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
103-
// directly from the destination
104-
// Single lookup for both relayer checks on the same (request_id, to) pair
105-
bool wasAlreadyRelayer = false;
106-
bool weWereSoleRelayer = false;
107-
bool weWereRelayer = false;
108-
checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer,
109-
&weWereSoleRelayer);
110-
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
111-
if (origTx->next_hop != p->relay_node) { // Not already set
112-
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
113-
p->relay_node, wasAlreadyRelayer, weWereSoleRelayer);
114-
origTx->next_hop = p->relay_node;
115-
}
101+
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
102+
// directly from the destination. checkRelayers is read-only on PacketHistory and O(1), so we run it even
103+
// when origTx is absent — that lets us still capture the confirmed hop into the TMM overflow cache below.
104+
// Single lookup for both relayer checks on the same (request_id, to) pair
105+
bool wasAlreadyRelayer = false;
106+
bool weWereSoleRelayer = false;
107+
bool weWereRelayer = false;
108+
checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer,
109+
&weWereSoleRelayer);
110+
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
111+
if (origTx && origTx->next_hop != p->relay_node) { // Not already set
112+
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
113+
p->relay_node, wasAlreadyRelayer, weWereSoleRelayer);
114+
origTx->next_hop = p->relay_node;
116115
}
116+
#if HAS_TRAFFIC_MANAGEMENT
117+
// Mirror the confirmed hop into the TMM overflow cache so it survives even when the source
118+
// isn't (or is no longer) in the hot NodeDB. Same bidirectional confirmation gate as above.
119+
if (trafficManagementModule)
120+
trafficManagementModule->setNextHop(p->from, p->relay_node);
121+
#endif
117122
}
118123
}
119124
if (!isToUs(p)) {
@@ -194,6 +199,7 @@ std::optional<uint8_t> NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
194199
if (isBroadcast(to))
195200
return std::nullopt;
196201

202+
// Hot store first: a direct array hit on the live NodeDB entry.
197203
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to);
198204
if (node && node->next_hop) {
199205
// We are careful not to return the relay node as the next hop
@@ -203,6 +209,19 @@ std::optional<uint8_t> NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
203209
} else
204210
LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop);
205211
}
212+
213+
#if HAS_TRAFFIC_MANAGEMENT
214+
// Fallback: TMM overflow cache holds confirmed hops for nodes that have aged
215+
// out of the hot store. Same byte source/confidence as NodeInfoLite.next_hop.
216+
if (trafficManagementModule) {
217+
uint8_t hint = trafficManagementModule->getNextHopHint(to);
218+
if (hint && hint != relay_node) {
219+
// LOG_DEBUG("Next hop for 0x%x is 0x%x (TMM cache)", to, hint);
220+
return hint;
221+
}
222+
}
223+
#endif
224+
206225
return std::nullopt;
207226
}
208227

src/mesh/NodeDB.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,20 @@ void NodeDB::initConfigIntervals()
11221122
#endif
11231123
}
11241124

1125+
// Always-on traffic management defaults. Only booleans are written; every
1126+
// numeric field stays 0 and resolves to its default_traffic_mgmt_* macro at
1127+
// use (e.g. position dedup precision/interval), so fork-wide tuning changes
1128+
// take effect without another migration. Rate limiting and the features that
1129+
// exhaust or reshape relayed traffic (exhaust_hop_*, drop_unknown_enabled,
1130+
// nodeinfo_direct_response) stay opt-in.
1131+
static void installTrafficManagementDefaults(meshtastic_LocalModuleConfig &mc)
1132+
{
1133+
mc.has_traffic_management = true;
1134+
mc.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero;
1135+
mc.traffic_management.enabled = true;
1136+
mc.traffic_management.position_dedup_enabled = true;
1137+
}
1138+
11251139
void NodeDB::installDefaultModuleConfig()
11261140
{
11271141
LOG_INFO("Install default ModuleConfig");
@@ -1225,6 +1239,8 @@ void NodeDB::installDefaultModuleConfig()
12251239
moduleConfig.has_neighbor_info = true;
12261240
moduleConfig.neighbor_info.enabled = false;
12271241

1242+
installTrafficManagementDefaults(moduleConfig);
1243+
12281244
moduleConfig.has_detection_sensor = true;
12291245
moduleConfig.detection_sensor.enabled = false;
12301246
moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH;
@@ -2101,6 +2117,16 @@ void NodeDB::loadFromDisk()
21012117
}
21022118
}
21032119

2120+
// Always-on traffic management: a device that has NEVER configured TMM
2121+
// (has_traffic_management false — AdminModule always sets the has_ flag on
2122+
// write, even when disabling) gets the fork defaults. Explicitly configured
2123+
// devices keep their exact settings.
2124+
if (!moduleConfig.has_traffic_management) {
2125+
LOG_INFO("Traffic management never configured, installing always-on defaults");
2126+
installTrafficManagementDefaults(moduleConfig);
2127+
saveToDisk(SEGMENT_MODULECONFIG);
2128+
}
2129+
21042130
state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg,
21052131
&channelFile);
21062132
if (state != LoadFileResult::LOAD_SUCCESS) {

src/mesh/mesh-pb-constants.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ static_assert(sizeof(meshtastic_NodeInfoLite) <= 130, "NodeInfoLite size increas
137137
// Traffic Management module configuration
138138
// Enable per-variant by defining HAS_TRAFFIC_MANAGEMENT=1 in variant.h
139139
#ifndef HAS_TRAFFIC_MANAGEMENT
140-
#define HAS_TRAFFIC_MANAGEMENT 0
140+
#define HAS_TRAFFIC_MANAGEMENT 1
141141
#endif
142142

143143
// HopScalingModule - variable hop module: dynamically adjusts broadcast hop_limit based on mesh density

src/modules/TraceRouteModule.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
#include "meshUtils.h"
99
#include <vector>
1010

11+
#if HAS_TRAFFIC_MANAGEMENT
12+
#include "modules/TrafficManagementModule.h"
13+
#endif
14+
1115
extern graphics::Screen *screen;
1216

1317
TraceRouteModule *traceRouteModule;
@@ -323,6 +327,14 @@ void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte)
323327
LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte);
324328
node->next_hop = nextHopByte;
325329
}
330+
331+
#if HAS_TRAFFIC_MANAGEMENT
332+
// Mirror into the TMM overflow cache. Traceroute is the highest-confidence
333+
// source (full known route), and this captures the target even when it isn't
334+
// in the hot NodeDB — same rationale as the ACK-confirmed path in NextHopRouter.
335+
if (trafficManagementModule)
336+
trafficManagementModule->setNextHop(target, nextHopByte);
337+
#endif
326338
}
327339

328340
void TraceRouteModule::processUpgradedPacket(const meshtastic_MeshPacket &mp)

0 commit comments

Comments
 (0)