From 0fef83d4344d07e6e3910c48423e15670baff54d Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:54:48 -0700 Subject: [PATCH] Add configurable event mode hop limit (#11275) * feat: resolve event mode hop limit * feat: bake event mode hop limit * fix: honor event mode hop cap in routing * docs: expose event mode hop limit preference * fix: enforce event hop defaults across routing * docs: clarify event hop override behavior * refactor: simplify event mode hop preference * fix: cap equal event hop limit --- src/mesh/Default.cpp | 2 +- src/mesh/Default.h | 16 +++++++- src/mesh/NextHopRouter.cpp | 22 ++++++++--- src/mesh/NodeDB.cpp | 4 ++ src/modules/RoutingModule.cpp | 13 ++++--- test/test_default/test_main.cpp | 32 ++++++++++++++++ test/test_nexthop_routing/test_main.cpp | 49 +++++++++++++++++++++++++ userPrefs.jsonc | 1 + 8 files changed, 126 insertions(+), 13 deletions(-) diff --git a/src/mesh/Default.cpp b/src/mesh/Default.cpp index c8cea9d89..95e4132bf 100644 --- a/src/mesh/Default.cpp +++ b/src/mesh/Default.cpp @@ -92,7 +92,7 @@ uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minV uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured) { #if USERPREFS_EVENT_MODE - return (configured > HOP_RELIABLE) ? HOP_RELIABLE : config.lora.hop_limit; + return (configured >= eventModeHopLimit) ? eventModeHopLimit : config.lora.hop_limit; #else return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit; #endif diff --git a/src/mesh/Default.h b/src/mesh/Default.h index b38a68185..e5e8b8ab1 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -5,6 +5,7 @@ #include #include #include +#include #define ONE_DAY 24 * 60 * 60 #define ONE_MINUTE_MS 60 * 1000 #define THIRTY_SECONDS_MS 30 * 1000 @@ -75,7 +76,20 @@ enum class TrafficType { POSITION, TELEMETRY }; class Default { +#if USERPREFS_EVENT_MODE && defined(USERPREFS_EVENT_MODE_HOP_LIMIT) + static constexpr auto eventModeHopLimitSetting = USERPREFS_EVENT_MODE_HOP_LIMIT; +#else + static constexpr auto eventModeHopLimitSetting = HOP_RELIABLE; +#endif + using EventModeHopLimitType = typename std::remove_cv::type; + static_assert(std::is_integral::value && !std::is_same::value && + eventModeHopLimitSetting >= 0 && eventModeHopLimitSetting <= HOP_MAX, + "USERPREFS_EVENT_MODE_HOP_LIMIT must be an integer between 0 and 7"); + public: + static constexpr uint8_t eventModeHopLimit = static_cast(eventModeHopLimitSetting); + static constexpr uint8_t eventModeRelayHopLimit = eventModeHopLimit > 0 ? eventModeHopLimit - 1 : 0; + static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval); static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval); static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue); @@ -129,4 +143,4 @@ class Default return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor } } -}; \ No newline at end of file +}; diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 029648e2d..0a64a1f1a 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -1,4 +1,5 @@ #include "NextHopRouter.h" +#include "Default.h" #include "MeshTypes.h" #include "meshUtils.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE @@ -9,6 +10,18 @@ #endif #include "NodeDB.h" +#if USERPREFS_EVENT_MODE +static void capEventRelayHops(meshtastic_MeshPacket *packet) +{ + if (packet->hop_limit <= Default::eventModeRelayHopLimit) + return; + + const uint8_t reduction = packet->hop_limit - Default::eventModeRelayHopLimit; + packet->hop_start = reduction <= packet->hop_start ? packet->hop_start - reduction : 0; + packet->hop_limit = Default::eventModeRelayHopLimit; +} +#endif + NextHopRouter::NextHopRouter() {} bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p) @@ -26,6 +39,9 @@ bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p) if (!relay) return false; relay->hop_limit--; +#if USERPREFS_EVENT_MODE + capEventRelayHops(relay); +#endif relay->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // The interface declines some packets (NODENUM_BROADCAST_NO_LORA) with ERRNO_SHOULD_RELEASE, // which leaves the copy ours to free. Dropping it here would leak a pool slot per opaque frame. @@ -211,11 +227,7 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); } #if USERPREFS_EVENT_MODE - if (tosend->hop_limit > 2) { - // if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away. - tosend->hop_start -= (tosend->hop_limit - 2); - tosend->hop_limit = 2; - } + capEventRelayHops(tosend); #endif ErrorCode res = diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index c7d139aaa..3c742e7ca 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -927,7 +927,11 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY; #endif +#if USERPREFS_EVENT_MODE + config.lora.hop_limit = Default::eventModeHopLimit; +#else config.lora.hop_limit = HOP_RELIABLE; +#endif #ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT; #else diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 1ce7c4502..3d0812e5e 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -64,19 +64,20 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { const int8_t hopsUsed = getHopsAway(mp); + const uint8_t responseHopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); if (hopsUsed >= 0) { - if (hopsUsed > (int32_t)(config.lora.hop_limit)) { -// In event mode, we never want to send packets with more than our default 3 hops. -#if !(EVENTMODE) // This falls through to the default. + if (hopsUsed > static_cast(responseHopLimit)) { +// In event mode, never exceed the configured event hop limit. +#if !USERPREFS_EVENT_MODE // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops #endif } else if (mp.hop_start == 0) { return 0; // The requesting node wanted 0 hops, so the response also uses a direct/local path. - } else if ((uint8_t)(hopsUsed + 2) < config.lora.hop_limit) { + } else if (static_cast(hopsUsed + 2) < responseHopLimit) { return hopsUsed + 2; // Use only the amount of hops needed with some margin as the way back may be different } } - return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit + return responseHopLimit; } meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, @@ -93,4 +94,4 @@ RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_RO // LocalOnly requires either the from or to to be a known node // knownOnly specifically requires the from to be a known node. encryptedOk = true; -} \ No newline at end of file +} diff --git a/test/test_default/test_main.cpp b/test/test_default/test_main.cpp index 674475b79..ca9e48b89 100644 --- a/test/test_default/test_main.cpp +++ b/test/test_default/test_main.cpp @@ -3,6 +3,8 @@ #include "MeshRadio.h" #include "TestUtil.h" #include "meshUtils.h" +#include "modules/RoutingModule.h" +#include #include // Helper to compute expected ms using same logic as Default::congestionScalingCoefficient @@ -181,6 +183,32 @@ void test_scaled_overflow_saturates() TEST_ASSERT_EQUAL_UINT32(static_cast(INT32_MAX), res); } +void test_configured_or_default_hop_limit() +{ + config.lora.hop_limit = HOP_MAX; + const uint8_t result = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + +#if USERPREFS_EVENT_MODE + TEST_ASSERT_EQUAL_UINT8(Default::eventModeHopLimit, result); + TEST_ASSERT_EQUAL_UINT8(Default::eventModeHopLimit, Default::getConfiguredOrDefaultHopLimit(Default::eventModeHopLimit)); +#else + TEST_ASSERT_EQUAL_UINT8(HOP_MAX, result); +#endif +} + +#if USERPREFS_EVENT_MODE +void test_event_mode_caps_optimized_response() +{ + config.lora.hop_limit = HOP_MAX; + meshtastic_MeshPacket request = meshtastic_MeshPacket_init_zero; + request.hop_start = HOP_MAX; + request.hop_limit = HOP_MAX - 4; + + RoutingModule module; + TEST_ASSERT_EQUAL_UINT8(std::min(6, Default::eventModeHopLimit), module.getHopLimitForResponse(request)); +} +#endif + void setup() { // Small delay to match other test mains @@ -201,6 +229,10 @@ void setup() RUN_TEST(test_ms_default_clamps); RUN_TEST(test_ms_result_is_int32_safe); RUN_TEST(test_scaled_overflow_saturates); + RUN_TEST(test_configured_or_default_hop_limit); +#if USERPREFS_EVENT_MODE + RUN_TEST(test_event_mode_caps_optimized_response); +#endif exit(UNITY_END()); } diff --git a/test/test_nexthop_routing/test_main.cpp b/test/test_nexthop_routing/test_main.cpp index c7a27bf5c..7d3dd9eec 100644 --- a/test/test_nexthop_routing/test_main.cpp +++ b/test/test_nexthop_routing/test_main.cpp @@ -13,6 +13,7 @@ #include "configuration.h" #include "gps/RTC.h" +#include "mesh/Default.h" #include "mesh/NextHopRouter.h" #include "mesh/NodeDB.h" #include "mesh/RadioInterface.h" @@ -89,6 +90,7 @@ class NextHopRouterTestShim : public NextHopRouter using NextHopRouter::noteRouteLearned; using NextHopRouter::noteRouteSuccess; using NextHopRouter::perhapsRebroadcast; + using NextHopRouter::relayOpaquePacket; using Router::shouldDecrementHopLimit; // protected in Router void resetRouteHealthForTest() @@ -108,6 +110,8 @@ class MockRadioInterface : public RadioInterface ErrorCode send(meshtastic_MeshPacket *p) override { sendCount++; + lastHopLimit = p->hop_limit; + lastHopStart = p->hop_start; if (declineAll || p->to == NODENUM_BROADCAST_NO_LORA) return ERRNO_SHOULD_RELEASE; @@ -124,6 +128,8 @@ class MockRadioInterface : public RadioInterface int sendCount = 0; bool declineAll = false; + uint8_t lastHopLimit = 0; + uint8_t lastHopStart = 0; }; static MockNodeDB *mockNodeDB = nullptr; @@ -498,6 +504,46 @@ void test_rebroadcast_declined_send_releases_packet(void) TEST_ASSERT_EQUAL_MESSAGE(1, mockIface->sendCount, "the copy must have reached the mock radio"); } +#if USERPREFS_EVENT_MODE +void test_event_mode_hop_behavior(void) +{ + MockRadioInterface *mockIface = installMockIface(); + meshtastic_MeshPacket p = makeRebroadcastCandidate(NODENUM_BROADCAST); + p.from = kLocalNode; + p.hop_start = 0; + p.hop_limit = HOP_MAX; + + TEST_ASSERT_EQUAL(ERRNO_OK, shim->send(packetPool.allocCopy(p))); + TEST_ASSERT_EQUAL_UINT8(HOP_MAX, mockIface->lastHopLimit); + TEST_ASSERT_EQUAL_UINT8(HOP_MAX, mockIface->lastHopStart); + + p = makeRebroadcastCandidate(NODENUM_BROADCAST); + p.hop_start = HOP_MAX; + p.hop_limit = HOP_MAX; + + TEST_ASSERT_TRUE(shim->perhapsRebroadcast(&p)); + TEST_ASSERT_EQUAL_UINT8(Default::eventModeRelayHopLimit, mockIface->lastHopLimit); + TEST_ASSERT_EQUAL_UINT8(static_cast(Default::eventModeRelayHopLimit + 1), mockIface->lastHopStart); + + config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; + p = makeRebroadcastCandidate(NODENUM_BROADCAST); + p.hop_start = HOP_MAX; + p.hop_limit = HOP_MAX; + + TEST_ASSERT_TRUE(shim->relayOpaquePacket(&p)); + TEST_ASSERT_EQUAL_UINT8(Default::eventModeRelayHopLimit, mockIface->lastHopLimit); + TEST_ASSERT_EQUAL_UINT8(static_cast(Default::eventModeRelayHopLimit + 1), mockIface->lastHopStart); + + p = makeRebroadcastCandidate(NODENUM_BROADCAST); + p.hop_start = 0; + p.hop_limit = HOP_MAX; + + TEST_ASSERT_TRUE(shim->relayOpaquePacket(&p)); + TEST_ASSERT_EQUAL_UINT8(Default::eventModeRelayHopLimit, mockIface->lastHopLimit); + TEST_ASSERT_EQUAL_UINT8(0, mockIface->lastHopStart); +} +#endif + // =========================================================================== void setup() @@ -552,6 +598,9 @@ void setup() RUN_TEST(test_rebroadcast_normal_broadcast_is_relayed); RUN_TEST(test_rebroadcast_no_lora_broadcast_is_not_relayed); RUN_TEST(test_rebroadcast_declined_send_releases_packet); +#if USERPREFS_EVENT_MODE + RUN_TEST(test_event_mode_hop_behavior); +#endif exit(UNITY_END()); } diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 639ac6e28..fc18c0097 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -24,6 +24,7 @@ // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, and LOST AND FOUND roles are restricted. // "USERPREFS_EVENT_MODE": "1", + // "USERPREFS_EVENT_MODE_HOP_LIMIT": "3", // Event-mode default and firmware-generated/relay hop cap (0-7; default 3) // "USERPREFS_TMM_APPLY_TO_PRIVATE_CHANNELS": "1", // Extend TMM position dedup and precision clamping to private/custom-key channels (default: well-known channels only) // "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN", // "USERPREFS_FIXED_BLUETOOTH": "121212",