Skip to content

Commit c51c016

Browse files
NomDeTomclaude
andauthored
Traffic Management module: dedup, rate limiting, role-aware policing (#10706)
Adds the Traffic Management module (TMM) plus the NodeDB/warm-store and next-hop foundations it builds on: - Unified per-node cache (flat array, 8-bit relative ticks) shared by all features; role-aware throttles for tracker / lost-and-found. - Position deduplication: drop unchanged position rebroadcasts within a configurable interval; precision driven off the channel ceiling (clamped to the public-key max on well-known channels). Enabled by default at 11h. - Per-node rate limiting and unknown-packet filtering (config-driven; a non-zero companion field enables each feature -- no bool toggles). - NodeInfo direct response from cache with role-based hop clamps. - Persistent next-hop overflow store: confirmed hops have no TTL, are seeded from NodeInfoLite at boot, and survive hot-store eviction. - Three-tier sender-role resolution (hot NodeInfoLite -> warm store -> TMM cache). Role is cached write-time (seeded on first track, refreshed from NodeInfo), pins its cache entry like a next-hop hint, and is evicted last. - Warm store caches device role + protected category across reboot/eviction. - PositionModule stationary floor for tracker / lost-and-found. - PSRAM gating for warm/satellite/TMM cache sizes; STM32WL excluded. Protobufs: TrafficManagementConfig trimmed to the five uint32 fields actually used; submodule repointed to protobufs develop. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d8d92b7 commit c51c016

26 files changed

Lines changed: 1185 additions & 609 deletions

protobufs

src/mesh/Channels.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,24 @@ bool Channels::usesPublicKey(ChannelIndex chIndex)
440440
return (psk.size == sizeof(defaultpsk) && memcmp(psk.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0);
441441
}
442442

443+
bool Channels::isWellKnownChannel(ChannelIndex chIndex)
444+
{
445+
const auto &ch = getByIndex(chIndex);
446+
// Absent (unencrypted) or single-byte PSK — all the well-known key indexes
447+
if (ch.settings.psk.size > 1)
448+
return false;
449+
450+
const char *name = getName(chIndex);
451+
for (int p = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; p <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; p++) {
452+
const char *presetName =
453+
DisplayFormatters::getModemPresetDisplayName(static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(p), false, true);
454+
// Presets without a display name fall through to "Invalid" — never a match
455+
if (strcmp(presetName, "Invalid") != 0 && strcmp(name, presetName) == 0)
456+
return true;
457+
}
458+
return false;
459+
}
460+
443461
bool Channels::hasDefaultChannel()
444462
{
445463
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel

src/mesh/Channels.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ class Channels
8888

8989
// Returns true if this channel's effective key is publicly decryptable (open or well-known/default PSK).
9090
bool usesPublicKey(ChannelIndex chIndex);
91+
// Returns true if the channel is "well known": its PSK is absent or a
92+
// single-byte well-known key index, AND its name is any modem-preset
93+
// display name (e.g. a channel named "LongFast" counts even while the
94+
// radio runs MediumFast). Broader than isDefaultChannel, which only
95+
// matches the current preset's name and PSK byte 1.
96+
bool isWellKnownChannel(ChannelIndex chIndex);
9197

9298
// Returns true if we can be reached via a channel with the default settings given a region and modem preset
9399
bool hasDefaultChannel();

src/mesh/Default.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
#define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60)
1919
#define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60)
2020
#define default_broadcast_smart_minimum_interval_secs 5 * 60
21+
// Floor for our own position broadcasts when stationary (unchanged beyond the broadcast
22+
// precision) or fixed_position: identical positions get deduped by traffic management anyway.
23+
#define default_position_stationary_broadcast_secs (12 * 60 * 60)
2124
#define min_default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60)
2225
#define min_default_broadcast_smart_minimum_interval_secs 5 * 60
2326
#define default_wait_bluetooth_secs IF_ROUTER(1, 60)
@@ -34,8 +37,14 @@
3437
enum class TrafficType { POSITION, TELEMETRY };
3538

3639
// Traffic management defaults
37-
#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m)
38-
#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions
40+
#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m)
41+
#define default_traffic_mgmt_position_min_interval_secs (11 * 60 * 60) // 11 hours between identical positions
42+
// Role cap: tracker-role origins may refresh a duplicate position this often (vs the 11h default).
43+
#define default_traffic_mgmt_tracker_position_min_interval_secs (60 * 60) // 1 hour
44+
// Role cap: lost-and-found origins may refresh a duplicate position this often, so a lost
45+
// device updates frequently without flooding. (Quantised to the dedup tick: ~2 ticks.)
46+
// Unlike before, lost-and-found is NOT exempt from the relayed precision clamp.
47+
#define default_traffic_mgmt_lost_and_found_position_min_interval_secs (15 * 60) // 15 minutes
3948

4049
// Hop scaling defaults
4150
#define default_hop_scaling_min_target_nodes 40 // walk threshold: first hop reaching this cumulative count

src/mesh/NodeDB.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,8 +1155,12 @@ static void installTrafficManagementDefaults(meshtastic_LocalModuleConfig &mc)
11551155
{
11561156
mc.has_traffic_management = true;
11571157
mc.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero;
1158-
mc.traffic_management.enabled = true;
1159-
mc.traffic_management.position_dedup_enabled = true;
1158+
#if HAS_TRAFFIC_MANAGEMENT
1159+
// Position dedup ships enabled at the 11-hour default window on all supported targets.
1160+
// STM32WL is excluded at compile time (HAS_TRAFFIC_MANAGEMENT=0 in mesh-pb-constants.h).
1161+
// Set position_min_interval_secs=0 at runtime to disable dedup.
1162+
mc.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
1163+
#endif
11601164
}
11611165

11621166
void NodeDB::installDefaultModuleConfig()
@@ -3450,6 +3454,20 @@ bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
34503454
return false;
34513455
}
34523456

3457+
meshtastic_Config_DeviceConfig_Role NodeDB::getNodeRole(NodeNum n)
3458+
{
3459+
const meshtastic_NodeInfoLite *info = getMeshNode(n);
3460+
if (nodeInfoLiteHasUser(info))
3461+
return info->role;
3462+
#if WARM_NODE_COUNT > 0
3463+
// Hot-store miss: fall back to the role the warm tier cached at eviction.
3464+
uint8_t role = 0, prot = 0;
3465+
if (warmStore.lookupMeta(n, role, prot))
3466+
return static_cast<meshtastic_Config_DeviceConfig_Role>(role);
3467+
#endif
3468+
return meshtastic_Config_DeviceConfig_Role_CLIENT;
3469+
}
3470+
34533471
/// Find a node in our DB, create an empty NodeInfo if missing
34543472
meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
34553473
{
@@ -3672,6 +3690,8 @@ bool NodeDB::createNewIdentity()
36723690
myNodeInfo.my_node_num = newNodeNum;
36733691

36743692
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
3693+
if (!info)
3694+
return false;
36753695
TypeConversions::CopyUserToNodeInfoLite(info, owner);
36763696

36773697
return true;

src/mesh/NodeDB.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ class NodeDB
341341
/// tier. Returns false if we don't know a key for n.
342342
bool copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out);
343343

344+
/// Resolve a node's device role — hot store (with user) first, then the role
345+
/// cached in the warm tier, else CLIENT. Lets role-aware policy keep firing for
346+
/// nodes that have aged out of the hot store.
347+
meshtastic_Config_DeviceConfig_Role getNodeRole(NodeNum n);
348+
344349
/// last_heard of a hot-store node, or 0 if absent. Plain scan of meshNodes
345350
/// with no allocation side effects (unlike getOrCreateMeshNode).
346351
uint32_t hotNodeLastHeard(NodeNum n) const;

src/mesh/PositionPrecision.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
2828
return precision;
2929
}
3030

31-
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
31+
int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
3232
{
33+
if (precision == 0 || precision >= 32)
34+
return coordinate;
35+
3336
uint32_t coordinateBits = static_cast<uint32_t>(coordinate);
3437
uint32_t truncated = coordinateBits & (UINT32_MAX << (32 - precision));
3538

@@ -39,6 +42,11 @@ static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
3942
return static_cast<int32_t>(truncated);
4043
}
4144

45+
int32_t truncateCoordinate(int32_t coordinate, uint8_t precision)
46+
{
47+
return truncateCoordinate(coordinate, static_cast<uint32_t>(precision));
48+
}
49+
4250
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision)
4351
{
4452
if (precision == 0) {

src/mesh/PositionPrecision.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
1515

1616
// Configured precision, clamped to MAX_POSITION_PRECISION_PUBLIC_KEY when the channel's effective key is publicly decryptable.
1717
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
18+
19+
// Truncate a single latitude_i/longitude_i to `precision` significant bits, centered in the
20+
// resulting grid cell (stable under GPS jitter). precision 0 or >=32 returns the value unchanged.
21+
// The return is the coordinate (int32_t); the uint8_t overload only narrows the precision arg.
22+
int32_t truncateCoordinate(int32_t coordinate, uint32_t precision);
23+
int32_t truncateCoordinate(int32_t coordinate, uint8_t precision);
1824
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
1925
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
2026
bool applyPositionPrecisionForChannel(meshtastic_MeshPacket &packet, uint8_t channelIndex);

src/mesh/Router.cpp

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,12 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
100100
return true;
101101
}
102102

103-
#if HAS_TRAFFIC_MANAGEMENT
104-
// When router_preserve_hops is enabled, preserve hops for decoded packets that are not
105-
// position or telemetry (those have their own exhaust_hop controls).
106-
if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled &&
107-
moduleConfig.traffic_management.router_preserve_hops && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
108-
p->decoded.portnum != meshtastic_PortNum_POSITION_APP && p->decoded.portnum != meshtastic_PortNum_TELEMETRY_APP) {
109-
LOG_DEBUG("Router hop preserved: port=%d from=0x%08x (traffic_management)", p->decoded.portnum, getFrom(p));
110-
if (trafficManagementModule) {
111-
trafficManagementModule->recordRouterHopPreserved();
112-
}
113-
return false;
114-
}
115-
#endif
103+
// router_preserve_hops: not suitable right now — removed from config until
104+
// the right heuristics for when to preserve vs. exhaust hops are established.
105+
// #if HAS_TRAFFIC_MANAGEMENT
106+
// if (moduleConfig.has_traffic_management &&
107+
// moduleConfig.traffic_management.router_preserve_hops && ...) { ... }
108+
// #endif
116109

117110
// For subsequent hops, preserve hop_limit only when the previous relay is UNAMBIGUOUSLY a favorite
118111
// router. The relay_node byte is just the last byte of a 32-bit node number, so on a dense mesh it

src/mesh/generated/meshtastic/deviceonly.pb.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
452452
/* Maximum encoded size of messages (where known) */
453453
/* meshtastic_NodeDatabase_size depends on runtime parameters */
454454
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
455-
#define meshtastic_BackupPreferences_size 2432
455+
#define meshtastic_BackupPreferences_size 2410
456456
#define meshtastic_ChannelFile_size 718
457457
#define meshtastic_DeviceState_size 1944
458458
#define meshtastic_NodeEnvironmentEntry_size 170

0 commit comments

Comments
 (0)