Skip to content
Open
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
63 changes: 56 additions & 7 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,26 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[])

bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

could a max based on min(_prefs->flood_max, getEffectiveFloodMax(...)) be used, to adhere to a configured lower value?

Copy link
Copy Markdown
Contributor Author

@wbijen wbijen Mar 8, 2026

Choose a reason for hiding this comment

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

Lowest now wins, fixed and pushed! thanks.

It now has GROUP_FLOOD_MAX and ADVERT_FLOOD_MAX configured at 8. Do you think that value makes sense or you want me to set it to 64 so by default there is no change in behavior at quiet times?

if (_prefs.disable_fwd) 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");
return false;
if (packet->isRouteFlood()) {
if (recv_pkt_region == NULL) {
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
return false;
}
// per-type adaptive hop limits (groups and adverts use busy-driven ramp,
// but never exceed the operator-configured flood_max)
uint8_t hops = packet->getPathHashCount();
uint8_t limit = _prefs.flood_max;
uint8_t type = packet->getPayloadType();
if (type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA) {
uint8_t adaptive = getEffectiveFloodMax(busy_tracker.busy,
GROUP_FLOOD_MAX, GROUP_FLOOD_MID, GROUP_FLOOD_FLOOR);
limit = (adaptive < limit) ? adaptive : limit;
} else if (type == PAYLOAD_TYPE_ADVERT) {
uint8_t adaptive = getEffectiveFloodMax(busy_tracker.busy,
ADVERT_FLOOD_MAX, ADVERT_FLOOD_MID, ADVERT_FLOOD_FLOOR);
limit = (adaptive < limit) ? adaptive : limit;
}
if (hops >= limit) return false;
}
if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) {
const uint8_t* maximums;
Expand Down Expand Up @@ -525,7 +541,8 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {

uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
float scale = 1.0f + busy_tracker.busy * BUSY_TX_DELAY_SCALE;
return getRNG()->nextInt(0, (uint32_t)(5 * t * scale) + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
Expand Down Expand Up @@ -830,7 +847,7 @@ void MyMesh::sendNodeDiscoverReq() {

MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(PACKET_POOL_SIZE), tables),
_cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
discover_limiter(4, 120), // max 4 every 2 minutes
anon_limiter(4, 180) // max 4 every 3 minutes
Expand Down Expand Up @@ -908,6 +925,10 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif

busy_tracker.reset(millis(), getTotalAirTime(), getReceiveAirTime(),
getNumRecvFlood(),
((SimpleMeshTables *)getTables())->getNumFloodDups());

radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_set_tx_power(_prefs.tx_power_dbm);

Expand Down Expand Up @@ -1057,10 +1078,27 @@ void MyMesh::formatRadioStatsReply(char *reply) {
}

void MyMesh::formatPacketStatsReply(char *reply) {
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
getNumRecvFlood(), getNumRecvDirect());
}

void MyMesh::formatBusyStatsReply(char *reply) {
const char* label = busy_tracker.busy < BUSY_ONSET ? "low"
: busy_tracker.busy < 0.50f ? "medium"
: "high";
uint8_t grp_hops = getEffectiveFloodMax(busy_tracker.busy,
GROUP_FLOOD_MAX, GROUP_FLOOD_MID, GROUP_FLOOD_FLOOR);
uint8_t adv_hops = getEffectiveFloodMax(busy_tracker.busy,
ADVERT_FLOOD_MAX, ADVERT_FLOOD_MID, ADVERT_FLOOD_FLOOR);
sprintf(reply,
"busy: %.2f (%s)\n"
" air:%.2f q:%.2f dup:%.2f\n"
" grp_hops:%d/%d adv_hops:%d/%d",
busy_tracker.busy, label,
busy_tracker.airtime_ratio, busy_tracker.queue_ratio, busy_tracker.dup_ratio,
grp_hops, GROUP_FLOOD_MAX, adv_hops, ADVERT_FLOOD_MAX);
}

void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
Expand All @@ -1078,6 +1116,9 @@ void MyMesh::clearStats() {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
busy_tracker.reset(millis(), getTotalAirTime(), getReceiveAirTime(),
getNumRecvFlood(),
((SimpleMeshTables *)getTables())->getNumFloodDups());
}

void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
Expand Down Expand Up @@ -1267,6 +1308,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else if (sender_timestamp == 0 && memcmp(command, "stats-busy", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
formatBusyStatsReply(reply);
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
Expand Down Expand Up @@ -1314,6 +1357,12 @@ void MyMesh::loop() {
uint32_t now = millis();
uptime_millis += now - last_millis;
last_millis = now;

// update busy score
auto t = (SimpleMeshTables *)getTables();
busy_tracker.update(now, getTotalAirTime(), getReceiveAirTime(),
_mgr->getOutboundCount(now), PACKET_POOL_SIZE,
getNumRecvFlood(), t->getNumFloodDups());
}

// To check if there is pending work
Expand Down
10 changes: 10 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <helpers/StatsFormatHelper.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/RegionMap.h>
#include <helpers/BusyTracker.h>
#include "RateLimiter.h"

#ifdef WITH_BRIDGE
Expand Down Expand Up @@ -61,6 +62,10 @@ struct RepeaterStats {
#define MAX_CLIENTS 32
#endif

#ifndef PACKET_POOL_SIZE
#define PACKET_POOL_SIZE 32
#endif

struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
Expand Down Expand Up @@ -97,6 +102,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
BusyTracker busy_tracker;
RateLimiter discover_limiter, anon_limiter;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
Expand Down Expand Up @@ -185,6 +191,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
NodePrefs* getNodePrefs() {
return &_prefs;
}
BusyTracker* getBusyTracker() {
return &busy_tracker;
}

void savePrefs() override {
_cli.savePrefs(_fs);
Expand All @@ -209,6 +218,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void formatBusyStatsReply(char *reply);

mesh::LocalIdentity& getSelfId() override { return self_id; }

Expand Down
17 changes: 16 additions & 1 deletion examples/simple_repeater/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ static const uint8_t meshcore_logo [] PROGMEM = {
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};

void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
void UITask::begin(NodePrefs* node_prefs, BusyTracker* busy, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_prefs = node_prefs;
_busy_tracker = busy;
_display->turnOn();

// strip off dash and commit hash by changing dash to null terminator
Expand Down Expand Up @@ -77,6 +78,20 @@ void UITask::renderCurrScreen() {
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);

// busy score + effective hop limits
if (_busy_tracker) {
uint8_t grp = getEffectiveFloodMax(_busy_tracker->busy,
GROUP_FLOOD_MAX, GROUP_FLOOD_MID, GROUP_FLOOD_FLOOR);
uint8_t adv = getEffectiveFloodMax(_busy_tracker->busy,
ADVERT_FLOOD_MAX, ADVERT_FLOOD_MID, ADVERT_FLOOD_FLOOR);
_display->setCursor(0, 42);
_display->setColor(DisplayDriver::LIGHT);
sprintf(tmp, "GRP:%d/%d ADV:%d/%d B:%.0f%%",
grp, GROUP_FLOOD_MAX, adv, ADVERT_FLOOD_MAX,
_busy_tracker->busy * 100.0f);
_display->print(tmp);
}
}
}

Expand Down
8 changes: 5 additions & 3 deletions examples/simple_repeater/UITask.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@

#include <helpers/ui/DisplayDriver.h>
#include <helpers/CommonCLI.h>
#include <helpers/BusyTracker.h>

class UITask {
DisplayDriver* _display;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
BusyTracker* _busy_tracker;
char _version_info[32];

void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
UITask(DisplayDriver& display) : _display(&display), _busy_tracker(nullptr) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, BusyTracker* busy, const char* build_date, const char* firmware_version);

void loop();
};
};
2 changes: 1 addition & 1 deletion examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ void setup() {
the_mesh.begin(fs);

#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
ui_task.begin(the_mesh.getNodePrefs(), the_mesh.getBusyTracker(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif

// send out initial zero hop Advertisement to the mesh
Expand Down
150 changes: 150 additions & 0 deletions src/helpers/BusyTracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#pragma once

#include <stdint.h>

// --- Congestion control compile-time defines ---
// Override any of these via platformio.ini build_flags, e.g.:
// -D GROUP_FLOOD_MAX=16 -D BUSY_ONSET=0.20f

#ifndef BUSY_ONSET
#define BUSY_ONSET 0.15f // busy level below which no throttling occurs
#endif
#ifndef BUSY_WINDOW_MS
#define BUSY_WINDOW_MS 60000 // tumbling window for busy calculation (ms)
#endif

#ifndef GROUP_FLOOD_MAX
#define GROUP_FLOOD_MAX 8 // max group reach when quiet. 0 = no group forwarding.
#endif
#ifndef GROUP_FLOOD_MID
#define GROUP_FLOOD_MID 5 // knee point at moderate congestion
#endif
#ifndef GROUP_FLOOD_FLOOR
#define GROUP_FLOOD_FLOOR 2 // minimum group reach under extreme congestion
#endif

#ifndef ADVERT_FLOOD_MAX
#define ADVERT_FLOOD_MAX 8 // max advert reach when quiet. 0 = no advert forwarding.
#endif
#ifndef ADVERT_FLOOD_MID
#define ADVERT_FLOOD_MID 4 // knee point at moderate congestion
#endif
#ifndef ADVERT_FLOOD_FLOOR
#define ADVERT_FLOOD_FLOOR 1 // minimum advert reach under extreme congestion
#endif

// Busy score component weights (must sum to 1.0)
#ifndef BUSY_WEIGHT_AIRTIME
#define BUSY_WEIGHT_AIRTIME 0.5f // tx+rx airtime fraction of window
#endif
#ifndef BUSY_WEIGHT_QUEUE
#define BUSY_WEIGHT_QUEUE 0.3f // tx queue fill level
#endif
#ifndef BUSY_WEIGHT_DUP
#define BUSY_WEIGHT_DUP 0.2f // flood duplicate ratio
#endif

// TX delay scaling: delay multiplier = 1 + busy * BUSY_TX_DELAY_SCALE
// At busy=0: 1× (unchanged), at busy=1: (1+scale)× wider jitter window
#ifndef BUSY_TX_DELAY_SCALE
#define BUSY_TX_DELAY_SCALE 2.0f // busy=1 gives 3× wider retransmit window
#endif

/**
* \brief Piecewise-linear ramp: dead zone + two-segment curve through ceiling → mid → floor.
* Returns effective hop limit for a given busy score.
*/
inline uint8_t getEffectiveFloodMax(float busy, uint8_t ceiling, uint8_t mid, uint8_t floor) {
if (ceiling == 0) return 0; // type disabled
if (mid > ceiling) mid = ceiling; // guard against misconfigured overrides
if (floor > mid) floor = mid;
if (busy <= BUSY_ONSET) return ceiling;
if (busy >= 1.0f) return floor;

float t = (busy - BUSY_ONSET) / (1.0f - BUSY_ONSET); // normalize active zone to [0,1]
if (t <= 0.5f) {
float s = t / 0.5f; // [0,1] within first half
return mid + (uint8_t)((1.0f - s) * (ceiling - mid));
} else {
float s = (t - 0.5f) / 0.5f; // [0,1] within second half
return floor + (uint8_t)((1.0f - s) * (mid - floor));
}
}

/**
* \brief Lightweight busy score tracker. Computes busy ∈ [0,1] over a rolling window
* from airtime ratio, queue depth, and flood duplicate ratio.
* Call update() from loop(). Recomputes every BUSY_WINDOW_MS (tumbling window).
* All counters come from existing Dispatcher/SimpleMeshTables.
*/
struct BusyTracker {
float busy; // composite score [0,1]
float airtime_ratio; // tx+rx airtime / window
float queue_ratio; // queue depth / capacity
float dup_ratio; // flood dups / flood recv

// snapshot of counters at start of window
unsigned long window_start;
unsigned long prev_tx_air;
unsigned long prev_rx_air;
uint32_t prev_flood_recv;
uint32_t prev_flood_dups;

void reset(unsigned long now_ms,
unsigned long tx_air, unsigned long rx_air,
uint32_t flood_recv, uint32_t flood_dups) {
busy = 0;
airtime_ratio = queue_ratio = dup_ratio = 0;
window_start = now_ms;
prev_tx_air = tx_air;
prev_rx_air = rx_air;
prev_flood_recv = flood_recv;
prev_flood_dups = flood_dups;
}

void update(unsigned long now_ms,
unsigned long tx_air, unsigned long rx_air,
int queue_count, int queue_capacity,
uint32_t flood_recv, uint32_t flood_dups) {
unsigned long elapsed = now_ms - window_start;
if (elapsed < BUSY_WINDOW_MS) return; // not yet time

// airtime deltas — detect counter wrap (~49 days) and skip window
unsigned long delta_tx = tx_air - prev_tx_air;
unsigned long delta_rx = rx_air - prev_rx_air;
if (delta_tx > elapsed || delta_rx > elapsed) {
// counter wrapped — re-baseline and keep previous busy score
prev_tx_air = tx_air;
prev_rx_air = rx_air;
prev_flood_recv = flood_recv;
prev_flood_dups = flood_dups;
window_start = now_ms;
return;
}

// airtime component: fraction of window spent transmitting or receiving
airtime_ratio = (float)(delta_tx + delta_rx) / (float)elapsed;
if (airtime_ratio > 1.0f) airtime_ratio = 1.0f;

// queue component: instantaneous depth / capacity
queue_ratio = (queue_capacity > 0) ? (float)queue_count / (float)queue_capacity : 0;
if (queue_ratio > 1.0f) queue_ratio = 1.0f;

// duplicate component: flood dups / flood recv
uint32_t delta_recv = flood_recv - prev_flood_recv;
uint32_t delta_dups = flood_dups - prev_flood_dups;
dup_ratio = (delta_recv > 0) ? (float)delta_dups / (float)delta_recv : 0;
if (dup_ratio > 1.0f) dup_ratio = 1.0f;

// weighted composite
busy = airtime_ratio * BUSY_WEIGHT_AIRTIME + queue_ratio * BUSY_WEIGHT_QUEUE + dup_ratio * BUSY_WEIGHT_DUP;
if (busy > 1.0f) busy = 1.0f;

// slide window
prev_tx_air = tx_air;
prev_rx_air = rx_air;
prev_flood_recv = flood_recv;
prev_flood_dups = flood_dups;
window_start = now_ms;
}
};
Loading