diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 85a5c6f7..baec7e90 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -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; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index ee98d86a..a033274f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index afc39671..46a8bf15 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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 diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index f963c988..08d57c36 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -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(); diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 375debe0..59b8ca55 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -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) { diff --git a/src/Dispatcher.h b/src/Dispatcher.h index de26ee91..557216be 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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) }; } diff --git a/src/Mesh.h b/src/Mesh.h index a81d7a7c..fd1ea663 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -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. */