diff --git a/docs/cli_commands.md b/docs/cli_commands.md index e70ba21725..3ab9153dc1 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -440,6 +440,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the advert rate limiter (Repeater Only) +**Usage:** +- `get advert.ratelimit` +- `set advert.ratelimit ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `on` + +--- + #### View or change this node's advert path hash size **Usage:** - `get path.hash.mode` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 53f642fdf6..14efff37a0 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -428,6 +428,9 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; + if (!_prefs.disable_advert_rate_limiter + && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT + && !advert_limiter.allow(rtc_clock.getCurrentTime())) return false; if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); @@ -848,7 +851,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _cli(board, rtc, sensors, region_map, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), discover_limiter(4, 120), // max 4 every 2 minutes - anon_limiter(4, 180) // max 4 every 3 minutes + anon_limiter(4, 180), // max 4 every 3 minutes + advert_limiter(300, 3, 5) #if defined(WITH_RS232_BRIDGE) , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) #endif diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e69..14479424aa 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -99,6 +99,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionEntry* recv_pkt_region; TransportKey default_scope; RateLimiter discover_limiter, anon_limiter; + AdaptiveRateLimiter advert_limiter; uint32_t pending_discover_tag; unsigned long pending_discover_until; bool region_load_active; diff --git a/examples/simple_repeater/RateLimiter.h b/examples/simple_repeater/RateLimiter.h index a6633c0a26..6304c4131e 100644 --- a/examples/simple_repeater/RateLimiter.h +++ b/examples/simple_repeater/RateLimiter.h @@ -3,21 +3,100 @@ #include class RateLimiter { - uint32_t _start_timestamp; - uint32_t _secs; - uint16_t _maximum, _count; + uint32_t _start; + uint16_t _secs; + uint16_t _maximum; + uint16_t _count; public: - RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { } + RateLimiter(uint16_t maximum, uint16_t secs) + : _start(0), _secs(secs), _maximum(maximum), _count(0) {} bool allow(uint32_t now) { - if (now < _start_timestamp + _secs) { - _count++; - if (_count > _maximum) return false; // deny - } else { // time window now expired - _start_timestamp = now; - _count = 1; + if (now - _start >= _secs) { + _start = now; + _count = 0; } + + if (_count >= _maximum) + return false; + + _count++; + + return true; + } +}; + +class AdaptiveRateLimiter { + enum { + EWMA_SMOOTHING = 3, + EWMA_TOTAL_WEIGHT = EWMA_SMOOTHING + 1, + EWMA_GROWTH_CAP = 4 + }; + + static_assert(EWMA_SMOOTHING >= 1 && EWMA_SMOOTHING <= 256, "EWMA_SMOOTHING must be 1-256"); + static_assert(EWMA_GROWTH_CAP >= 1, "EWMA_GROWTH_CAP must be at least 1"); + + uint32_t _start; + uint16_t _secs; + uint8_t _count; + uint8_t _limit; + uint8_t _ewma; + uint8_t _burst; + uint8_t _floor; + + static uint8_t clampU8(uint16_t v) { return v > 255 ? 255 : (uint8_t)v; } + + uint8_t nextEwma() const { + uint8_t cap = clampU8((uint16_t)_ewma + EWMA_GROWTH_CAP); + uint8_t effective = (_count > _ewma) ? cap : _count; + uint16_t next = (uint16_t)_ewma * EWMA_SMOOTHING + effective; + + return (uint8_t)(next / EWMA_TOTAL_WEIGHT); + } + + uint8_t computeLimit() const { + uint8_t clamped = clampU8((uint16_t)_ewma * _burst); + return clamped > _floor ? clamped : _floor; + } + + void advanceWindow(uint32_t now) { + if (now - _start < _secs) + return; + + uint32_t elapsed = (_secs == 0) ? 1 : (now - _start) / _secs; + + if (elapsed > EWMA_TOTAL_WEIGHT * 8) + elapsed = EWMA_TOTAL_WEIGHT * 8; + + _ewma = nextEwma(); + _limit = computeLimit(); + + while (elapsed > 1 && _ewma > 0) { + _count = 0; + _ewma = nextEwma(); + _limit = computeLimit(); + + elapsed--; + } + + _start = now; + _count = 0; + } + +public: + AdaptiveRateLimiter(uint16_t secs, uint8_t burst, uint8_t floor) + : _start(0), _secs(secs), _count(0), _limit(floor), _ewma(floor), + _burst(burst), _floor(floor) {} + + bool allow(uint32_t now) { + advanceWindow(now); + + if (_count >= _limit) + return false; + + _count++; + return true; } -}; \ No newline at end of file +}; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b71afc72e2..24cc7373d0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -89,7 +89,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->disable_advert_rate_limiter, sizeof(_prefs->disable_advert_rate_limiter)); // 291 + // next: 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -180,7 +181,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.write((uint8_t *)&_prefs->disable_advert_rate_limiter, sizeof(_prefs->disable_advert_rate_limiter)); // 291 + // next: 292 file.close(); } @@ -547,6 +549,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); + } else if (memcmp(config, "advert.ratelimit ", 17) == 0) { + _prefs->disable_advert_rate_limiter = memcmp(&config[17], "off", 3) == 0; + savePrefs(); + strcpy(reply, _prefs->disable_advert_rate_limiter ? "OK - advert rate limiter OFF" : "OK - advert rate limiter ON"); #if defined(USE_SX1262) || defined(USE_SX1268) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; @@ -765,6 +771,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", _prefs->node_name); } else if (memcmp(config, "repeat", 6) == 0) { sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on"); + } else if (memcmp(config, "advert.ratelimit", 16) == 0) { + sprintf(reply, "> %s", _prefs->disable_advert_rate_limiter ? "off" : "on"); } else if (memcmp(config, "lat", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ffdc7c6536..429cc06c9f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -61,6 +61,7 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t disable_advert_rate_limiter; }; class CommonCLICallbacks {