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:
Jakub
2026-06-18 23:51:01 +02:00
co-authored by Claude Sonnet 4.6
parent e213ff6efb
commit d790f7e5cd
7 changed files with 59 additions and 2 deletions
@@ -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();