Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore

---

### Packet stats - Packet counters: Received, Sent
### Packet Stats - Packet counters: Received, Sent
**Usage:** `stats-packets`

**Serial Only:** Yes

---

### Advert Limiter Stats - Limit, Remaining, Denied, Load Average, Limit Hit Count and Last Limit Hit Age (Repeater Only)
**Usage:** `stats-advert-ratelimit`

**Serial Only:** No

---

## Logging

### Begin capture of rx log to node storage
Expand Down Expand Up @@ -440,6 +447,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 <state>`

**Parameters:**
- `state`: `on`|`off`

**Default:** `on`

---

#### View or change this node's advert path hash size
**Usage:**
- `get path.hash.mode`
Expand Down
12 changes: 11 additions & 1 deletion examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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(150, 3, 9) // 150s window, 3x burst, floor 9
#if defined(WITH_RS232_BRIDGE)
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
#endif
Expand Down Expand Up @@ -1163,6 +1167,7 @@ void MyMesh::clearStats() {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
advert_limiter.clearStats();
}

void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
Expand Down Expand Up @@ -1251,6 +1256,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else if (memcmp(command, "stats-advert-ratelimit", 22) == 0 && (command[22] == 0 || command[22] == ' ')) {
AdaptiveRateLimiterStats stats = advert_limiter.stats(rtc_clock.getCurrentTime());
sprintf(reply, "{\"limit\":%u,\"remaining\":%u,\"denied\":%u,\"load_avg\":%u,\"limit_reached\":%u,\"last_limit_reached_ago\":%lu}",
(unsigned)stats.limit, (unsigned)stats.remaining, (unsigned)stats.denied,
(unsigned)stats.load_avg, (unsigned)stats.limit_reached, (unsigned long)stats.last_limit_reached_ago);
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
Expand Down
1 change: 1 addition & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
135 changes: 124 additions & 11 deletions examples/simple_repeater/RateLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,134 @@
#include <stdint.h>

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;
}
};
};

struct AdaptiveRateLimiterStats {
uint8_t limit;
uint8_t remaining;
uint8_t denied;
uint8_t load_avg;
uint16_t limit_reached;
uint32_t last_limit_reached_ago;
};

class AdaptiveRateLimiter {
// EWMA of recent per-window advert counts; cap each window at max(floor, ewma * burst).
enum {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding some documentation explaining the approach uses Exponentially Weighted Moving Average (EWMA). It might be good to have unit tests for this class to verify its behavior.

Copy link
Copy Markdown
Contributor Author

@ViezeVingertjes ViezeVingertjes May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit tests

We do unit tests these days?! haha.
No, all jokes aside, that should be easy to add in this case so will definetely do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are added.

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 _window_start;
uint16_t _window_secs;
uint8_t _window_count;
uint8_t _window_limit;
uint8_t _load_avg;
uint8_t _burst_multiplier;
uint8_t _min_limit;
uint8_t _denied;
uint16_t _limit_reached;
uint32_t _last_limit_reached_at;

static uint8_t clampU8(uint16_t v) { return v > 255 ? 255 : (uint8_t)v; }

uint8_t nextEwma() const {
uint8_t cap = clampU8((uint16_t)_load_avg + EWMA_GROWTH_CAP);
uint8_t effective = (_window_count > _load_avg) ? cap : _window_count;
uint16_t next = (uint16_t)_load_avg * EWMA_SMOOTHING + effective;

return (uint8_t)(next / EWMA_TOTAL_WEIGHT);
}

uint8_t computeLimit() const {
uint8_t clamped = clampU8((uint16_t)_load_avg * _burst_multiplier);
return clamped > _min_limit ? clamped : _min_limit;
}

void advanceWindow(uint32_t now) {
if (now - _window_start < _window_secs)
return;

uint32_t elapsed = (_window_secs == 0) ? 1 : (now - _window_start) / _window_secs;

if (elapsed > EWMA_TOTAL_WEIGHT * 8)
elapsed = EWMA_TOTAL_WEIGHT * 8;

_load_avg = nextEwma();
_window_limit = computeLimit();

while (elapsed > 1 && _load_avg > 0) {
_window_count = 0;
_load_avg = nextEwma();
_window_limit = computeLimit();

elapsed--;
}

_window_start = now;
_window_count = 0;
_denied = 0;
}

public:
AdaptiveRateLimiter(uint16_t window_secs, uint8_t burst_multiplier, uint8_t min_limit)
: _window_start(0), _window_secs(window_secs), _window_count(0), _window_limit(min_limit), _load_avg(min_limit),
_burst_multiplier(burst_multiplier), _min_limit(min_limit), _denied(0), _limit_reached(0), _last_limit_reached_at(0) {}

bool allow(uint32_t now) {
advanceWindow(now);

if (_window_count >= _window_limit) {
if (_denied < 255) _denied++;
return false;
}

_window_count++;

if (_window_count >= _window_limit) {
if (_limit_reached < 65535) _limit_reached++;
_last_limit_reached_at = now;
}

return true;
}

void clearStats() {
_denied = 0;
_limit_reached = 0;
_last_limit_reached_at = 0;
}

AdaptiveRateLimiterStats stats(uint32_t now) {
advanceWindow(now);
uint8_t remaining = (_window_count < _window_limit) ? (_window_limit - _window_count) : 0;
uint32_t last_limit_reached_ago = (_last_limit_reached_at == 0) ? 0 : (now - _last_limit_reached_at);
return { _window_limit, remaining, _denied, _load_avg, _limit_reached, last_limit_reached_ago };
}
};
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ platform = native
build_flags = -std=c++17
-I src
-I test/mocks
-I examples/simple_repeater
test_build_src = yes
build_src_filter =
-<*>
Expand Down
12 changes: 10 additions & 2 deletions src/helpers/CommonCLI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/helpers/CommonCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading