mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(repeater): overhear suppression — cancel queued retransmit if a peer relays first
When enabled (Settings > Radio > Suppress dup), a received flood packet whose hash matches one still waiting in the outbound queue cancels that queued retransmit: another node already relayed it, so this node stays quiet. Cuts redundant airtime in dense meshes; pairs with Yield (longer delay = wider window to overhear a peer first). - Dispatcher: wantsOverhearSuppress() hook (default off) + suppressQueuedDuplicate() scan over the outbound queue by packet hash; onRetransmitCancelled() hook. - Mesh: overrides onRetransmitCancelled() to back out n_forwarded so the count reflects what actually goes on air. resetStats() already covered. - Flood-only and hash-matched (payload hash ignores path for non-TRACE, so our copy and the peer's relayed copy hash equal). Own sends can't be overheard before their first TX, so suppression only ever cancels forwards. - New repeat_suppress_dup pref, schema sentinel 0xC0DE000F, stray-byte clamp. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -343,12 +343,14 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops));
|
||||
rd(&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
||||
rd(&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
||||
rd(&_prefs.repeat_suppress_dup, sizeof(_prefs.repeat_suppress_dup));
|
||||
if (_prefs.repeat_skip_adverts > 1) _prefs.repeat_skip_adverts = 0;
|
||||
if (_prefs.repeat_max_hops > 64) _prefs.repeat_max_hops = 0;
|
||||
if (_prefs.repeat_delay_boost > 8) _prefs.repeat_delay_boost = 0;
|
||||
if (_prefs.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED &&
|
||||
(_prefs.repeat_min_snr < -30 || _prefs.repeat_min_snr > 20))
|
||||
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED;
|
||||
if (_prefs.repeat_suppress_dup > 1) _prefs.repeat_suppress_dup = 0;
|
||||
// → 0xC0DE000B: append bot_commands_enabled + quiet-hours. Older files leave
|
||||
// stray bytes here; clamp so upgraders fall back to off / no quiet hours.
|
||||
if (_prefs.bot_commands_enabled > 1) _prefs.bot_commands_enabled = 0;
|
||||
@@ -510,6 +512,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops));
|
||||
file.write((uint8_t *)&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
||||
file.write((uint8_t *)&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
||||
file.write((uint8_t *)&_prefs.repeat_suppress_dup, sizeof(_prefs.repeat_suppress_dup));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
|
||||
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
||||
|
||||
@@ -153,6 +153,9 @@ protected:
|
||||
bool filterRecvFloodPacket(mesh::Packet* packet) override;
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
bool isRepeatLooped(const mesh::Packet* packet) const;
|
||||
// Overhear suppression only makes sense while repeating; gated behind its own
|
||||
// opt-in pref (Settings > Radio > Suppress dup).
|
||||
bool wantsOverhearSuppress() const override { return _prefs.client_repeat && _prefs.repeat_suppress_dup; }
|
||||
|
||||
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis);
|
||||
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
|
||||
|
||||
@@ -164,17 +164,20 @@ struct NodePrefs { // persisted to file
|
||||
// fixed repeaters. Effective delay = base * (1 + repeat_delay_boost). 0 = off.
|
||||
// repeat_min_snr: drop a flood packet received below this SNR (dB), so marginal
|
||||
// fringe traffic isn't re-flooded. REPEAT_SNR_DISABLED (-128) = off.
|
||||
// repeat_suppress_dup: 1 = cancel a queued retransmit when the same flood is
|
||||
// overheard from another node first (less redundant airtime in dense mesh).
|
||||
uint8_t repeat_skip_adverts;
|
||||
uint8_t repeat_max_hops;
|
||||
uint8_t repeat_delay_boost;
|
||||
int8_t repeat_min_snr;
|
||||
static const int8_t REPEAT_SNR_DISABLED = -128;
|
||||
uint8_t repeat_suppress_dup;
|
||||
|
||||
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE000E;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE000F;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
@@ -63,6 +63,7 @@ class SettingsScreen : public UIScreen {
|
||||
RPT_MAX_HOPS,
|
||||
RPT_DELAY,
|
||||
RPT_MIN_SNR,
|
||||
RPT_SUPPRESS,
|
||||
POWER_SAVE,
|
||||
TX_APC,
|
||||
// System section
|
||||
@@ -290,7 +291,8 @@ class SettingsScreen : public UIScreen {
|
||||
}
|
||||
|
||||
bool isRepeaterSubItem(int item) const {
|
||||
return item == RPT_SKIP_ADV || item == RPT_MAX_HOPS || item == RPT_DELAY || item == RPT_MIN_SNR;
|
||||
return item == RPT_SKIP_ADV || item == RPT_MAX_HOPS || item == RPT_DELAY
|
||||
|| item == RPT_MIN_SNR || item == RPT_SUPPRESS;
|
||||
}
|
||||
|
||||
bool isHomePage(int item) const {
|
||||
@@ -648,6 +650,10 @@ class SettingsScreen : public UIScreen {
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RPT_SUPPRESS) {
|
||||
display.print(" Suppress dup");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->repeat_suppress_dup) ? "ON" : "OFF");
|
||||
} else if (item == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -1073,6 +1079,11 @@ public:
|
||||
_dirty = true; return true;
|
||||
}
|
||||
}
|
||||
if (_selected == RPT_SUPPRESS && p && (left || right || enter)) {
|
||||
p->repeat_suppress_dup ^= 1;
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||
p->rx_powersave ^= 1;
|
||||
_task->applyPowerSave();
|
||||
|
||||
@@ -240,6 +240,9 @@ void Dispatcher::checkRecv() {
|
||||
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
|
||||
|
||||
n_recv_by_type[pkt->getPayloadType()]++;
|
||||
// Overhear suppression: if we just heard a flood packet that we still have
|
||||
// queued to retransmit, another node beat us to it — drop our copy.
|
||||
if (pkt->isRouteFlood() && wantsOverhearSuppress()) suppressQueuedDuplicate(pkt);
|
||||
if (pkt->isRouteFlood()) {
|
||||
n_recv_flood++;
|
||||
|
||||
@@ -261,6 +264,27 @@ void Dispatcher::checkRecv() {
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::suppressQueuedDuplicate(Packet* pkt) {
|
||||
uint8_t h[MAX_HASH_SIZE];
|
||||
pkt->calculatePacketHash(h);
|
||||
// Scan the whole outbound queue (not just due entries). hasSeen() already
|
||||
// keeps at most one copy queued, so the first hash match is the only one.
|
||||
int n = _mgr->getOutboundTotal();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Packet* q = _mgr->getOutboundByIdx(i);
|
||||
if (q == NULL) continue;
|
||||
uint8_t qh[MAX_HASH_SIZE];
|
||||
q->calculatePacketHash(qh);
|
||||
if (memcmp(h, qh, MAX_HASH_SIZE) == 0) {
|
||||
_mgr->removeOutboundByIdx(i);
|
||||
onRetransmitCancelled(q);
|
||||
_mgr->free(q);
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher: overhear suppressed a queued retransmit", getLogDateTime());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatcher::processRecvPacket(Packet* pkt) {
|
||||
DispatcherAction action = onRecvPacket(pkt);
|
||||
if (action == ACTION_RELEASE) {
|
||||
|
||||
@@ -170,6 +170,14 @@ protected:
|
||||
virtual int getAGCResetInterval() const { return 0; } // disabled by default
|
||||
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
|
||||
|
||||
// When true, a received flood packet whose hash matches one still waiting in
|
||||
// the outbound queue cancels that queued retransmit (overhear suppression):
|
||||
// another node already relayed it, so this node stays quiet. Default off.
|
||||
virtual bool wantsOverhearSuppress() const { return false; }
|
||||
// Hook fired when such a queued retransmit is cancelled — lets a sub-class
|
||||
// keep its forward counter honest. Default no-op.
|
||||
virtual void onRetransmitCancelled(Packet* packet) { }
|
||||
|
||||
public:
|
||||
void begin();
|
||||
void loop();
|
||||
@@ -205,6 +213,7 @@ public:
|
||||
private:
|
||||
void checkRecv();
|
||||
void checkSend();
|
||||
void suppressQueuedDuplicate(Packet* pkt); // overhear cancel (see wantsOverhearSuppress)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ protected:
|
||||
*/
|
||||
virtual bool allowPacketForward(const Packet* packet);
|
||||
|
||||
// Keep the forward counter honest when an overhear cancels a queued retransmit:
|
||||
// the packet was counted at the ACTION_RETRANSMIT decision, so back it out here.
|
||||
void onRetransmitCancelled(Packet* packet) override { if (n_forwarded) n_forwarded--; }
|
||||
|
||||
/**
|
||||
* \returns number of milliseconds delay to apply to retransmitting the given packet.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user