From 5b5b29c52314c598b7f5fa02c42519f4c3d6b5aa Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 25 Aug 2026 05:50:26 -0500 Subject: [PATCH] fix(position): halve the stationary/fixed-position broadcast floor to 6h The 12h floor introduced with traffic management was too aggressive: a fixed_position or stationary node goes quiet for half a day after its boot broadcast, so anything that missed that one packet - a node that joined later, or one that restarted - shows it with no position until the next refresh. Drop the floor to 6h, and drop the traffic-management identical-position dedup window from 11h to 5h with it. The two are a pair: the dedup window was deliberately sized just under the broadcast floor so a stationary node's periodic refresh clears its neighbours' window instead of being dropped as a duplicate. Leaving it at 11h would have made the extra broadcast pure airtime - aired, then discarded by every receiver - so the mesh would still have seen a 12h refresh. Role caps are unchanged and still bind: tracker 1h, lost-and-found 15m. Both remain shorter than the new 5h default, so those exceptions apply exactly as before. Co-Authored-By: Claude Opus 5 --- src/mesh/Default.h | 12 ++++++++---- src/mesh/NodeDB.cpp | 2 +- src/modules/PositionModule.cpp | 2 +- src/modules/TrafficManagementModule.cpp | 2 +- test/test_traffic_management/test_main.cpp | 18 +++++++++--------- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/mesh/Default.h b/src/mesh/Default.h index e5e8b8ab192..4f6c000f843 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -21,7 +21,9 @@ #define default_broadcast_smart_minimum_interval_secs 5 * 60 // Floor for our own position broadcasts when stationary (unchanged beyond the broadcast // precision) or fixed_position: identical positions get deduped by traffic management anyway. -#define default_position_stationary_broadcast_secs (12 * 60 * 60) +// Held one hour above default_traffic_mgmt_position_min_interval_secs so this refresh clears +// the receivers' dedup window instead of being dropped as a duplicate. +#define default_position_stationary_broadcast_secs (6 * 60 * 60) #define min_default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define min_default_broadcast_smart_minimum_interval_secs 5 * 60 #define default_wait_bluetooth_secs IF_ROUTER(1, 60) @@ -39,9 +41,11 @@ enum class TrafficType { POSITION, TELEMETRY }; // Traffic management defaults -#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m) -#define default_traffic_mgmt_position_min_interval_secs (11 * 60 * 60) // 11 hours between identical positions -// Role cap: tracker-role origins may refresh a duplicate position this often (vs the 11h default). +#define default_traffic_mgmt_position_precision_bits 19 // ~90m grid cells (±45m) +// Kept below default_position_stationary_broadcast_secs so a stationary node's periodic refresh +// is not deduped away by its neighbours. +#define default_traffic_mgmt_position_min_interval_secs (5 * 60 * 60) // 5 hours between identical positions +// Role cap: tracker-role origins may refresh a duplicate position this often (vs the 5h default). #define default_traffic_mgmt_tracker_position_min_interval_secs (60 * 60) // 1 hour // Role cap: lost-and-found origins may refresh a duplicate position this often, so a lost // device updates frequently without flooding. (Quantised to the dedup tick: ~2 ticks.) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 41d570f3e13..3e916e0f9c7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1185,7 +1185,7 @@ static void installTrafficManagementDefaults(meshtastic_LocalModuleConfig &mc) mc.has_traffic_management = true; mc.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero; #if HAS_TRAFFIC_MANAGEMENT - // Position dedup ships enabled at the 11-hour default window on all supported targets. + // Position dedup ships enabled at the 5-hour default window on all supported targets. // STM32WL is excluded at compile time (HAS_TRAFFIC_MANAGEMENT=0 in mesh-pb-constants.h). // Set position_min_interval_secs=0 at runtime to disable dedup. mc.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 9ee985b1564..618e4f2f064 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -540,7 +540,7 @@ int32_t PositionModule::runOnce() bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); - // Hold to the 12h floor when fixed_position (every role: pinning yourself forfeits the + // Hold to the 6h floor when fixed_position (every role: pinning yourself forfeits the // exception) or when stationary. A real move still goes out early via smart-broadcast below. // Not-fixed exceptions: lost-and-found broadcasts freely; trackers judge movement at their // own (unclamped) precision rather than the on-wire one (useConfiguredPrecision). diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index 0fdd8c72226..f049720fb7b 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -1362,7 +1362,7 @@ bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p, const int32_t lat_truncated = truncateCoordinate(pos->latitude_i, precision); const int32_t lon_truncated = truncateCoordinate(pos->longitude_i, precision); const uint8_t fingerprint = computePositionFingerprint(lat_truncated, lon_truncated, precision); - // Drop gate uses the RAW configured interval: 0 means "dedup disabled". The 12 h default + // Drop gate uses the RAW configured interval: 0 means "dedup disabled". The 5 h default // is only for TTL sizing - feeding it here would silently defeat that contract. uint32_t minIntervalMs = secsToMs(moduleConfig.traffic_management.position_min_interval_secs); diff --git a/test/test_traffic_management/test_main.cpp b/test/test_traffic_management/test_main.cpp index d7d947d5a04..10af9cbe37b 100644 --- a/test/test_traffic_management/test_main.cpp +++ b/test/test_traffic_management/test_main.cpp @@ -2806,12 +2806,12 @@ static void test_tm_nextHop_keptAliveAcrossMaintenanceSweep(void) /** * Verify TRACKER role caps the dedup window at 1 hour. - * A duplicate position that would normally be blocked for 11 h (default) must + * A duplicate position that would normally be blocked for 5 h (default) must * be forwarded once the 1-hour tracker cap expires. */ static void test_tm_trackerRole_capsDedupWindowAtOneHour(void) { - // Operator interval is 11 h - longer than the tracker cap. + // Operator interval is 5 h - longer than the tracker cap. moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs; installWellKnownPrimaryChannelWithPrecision(16); @@ -2872,11 +2872,11 @@ static void test_tm_takTrackerRole_capsDedupWindowAtOneHour(void) * hot and warm NodeDB stores - the TMM unified cache is the third fallback. The * role is cached on the entry while NodeDB still knows the node; once NodeDB * forgets it (getNodeRole → CLIENT), the cached role must keep the 1-hour cap - * applied instead of reverting to the 11-hour default interval. + * applied instead of reverting to the 5-hour default interval. */ static void test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole(void) { - // Operator interval is 11 h - longer than the tracker cap. + // Operator interval is 5 h - longer than the tracker cap. moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs; installWellKnownPrimaryChannelWithPrecision(16); @@ -2896,8 +2896,8 @@ static void test_tm_trackerRole_survivesNodeDbEvictionViaCachedRole(void) mockNodeDB->clearCachedNode(); ProcessMessage r2 = module.handleReceived(dup); // within 1-hour cap - still drop - // Advance past the tracker cap (3600 s) but stay well under the 11-hour default. - // Without the cached-role fallback this would still be inside the 11-hour window + // Advance past the tracker cap (3600 s) but stay well under the 5-hour default. + // Without the cached-role fallback this would still be inside the 5-hour window // (CLIENT → no exception) and wrongly drop; with it, the 1-hour cap lets it pass. TrafficManagementModule::s_testNowMs += (default_traffic_mgmt_tracker_position_min_interval_secs * 1000UL) + 1; ProcessMessage r3 = module.handleReceived(afterCap); @@ -2935,7 +2935,7 @@ static void test_tm_roleChange_viaNodeInfo_dropsTrackerException(void) meshtastic_MeshPacket info = makeNodeInfoPacketWithRole(kRemoteNode, meshtastic_Config_DeviceConfig_Role_CLIENT); module.handleReceived(info); - // Past the 1-hour tracker cap but within the 11-hour CLIENT interval. With the stale + // Past the 1-hour tracker cap but within the 5-hour CLIENT interval. With the stale // TRACKER role this would pass; after the demotion it must drop (full interval applies). TrafficManagementModule::s_testNowMs += (default_traffic_mgmt_tracker_position_min_interval_secs * 1000UL) + 1; meshtastic_MeshPacket afterCap = makePositionPacket(kRemoteNode, 374221234, -1220845678); @@ -3042,12 +3042,12 @@ static void test_tm_trackerRole_doesNotLengthenShorterOperatorInterval(void) /** * Verify LOST_AND_FOUND role caps duplicate-position dedup at ~15 min (2 pos-ticks), - * not the old one-tick fast-announce. A configured 11-hour interval is shortened to the + * not the old one-tick fast-announce. A configured 5-hour interval is shortened to the * 15-min cap; a duplicate one tick later still drops, but one past the 2-tick cap passes. */ static void test_tm_lostAndFoundRole_capsDedupAtFifteenMinutes(void) { - // Long interval that would normally suppress duplicates for 11 h. + // Long interval that would normally suppress duplicates for 5 h. moduleConfig.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs; installWellKnownPrimaryChannelWithPrecision(16);