From c430354124911889b9268201c564d7ff804f081e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 4 Mar 2026 23:05:40 +0100 Subject: [PATCH 1/2] Add SNR-driven dynamic coding rate selection for repeater retransmits When a repeater forwards a direct-routed pkt, it looks up the next-hop neighbor's SNR, computes the link margin against the SF-specific demodulation floor, then determines coding rate (RT) based on SNR. Weaker links get higher CR. We apply CR on a per-packet basis and restore to default configuration after TX. Margin thresholds: <3 dB - CR 4/8 3-6 dB - CR 4/7 6-10 dB - CR 4/6 >=10 dB - CR 4/5 Flood repeats adhere to configured CR, as do forwards to unknown neighbors. --- examples/simple_repeater/MyMesh.cpp | 38 ++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 2 ++ src/Dispatcher.cpp | 16 ++++++++- src/Dispatcher.h | 7 ++++ src/Mesh.cpp | 3 +- src/Mesh.h | 8 +++++ src/Packet.cpp | 2 ++ src/Packet.h | 1 + src/helpers/radiolib/CustomLLCC68Wrapper.h | 2 ++ src/helpers/radiolib/CustomLR1110Wrapper.h | 2 ++ src/helpers/radiolib/CustomSTM32WLxWrapper.h | 2 ++ src/helpers/radiolib/CustomSX1262Wrapper.h | 2 ++ src/helpers/radiolib/CustomSX1268Wrapper.h | 2 ++ src/helpers/radiolib/CustomSX1276Wrapper.h | 2 ++ 14 files changed, 87 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e8894927..6eac134aec 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,6 @@ #include "MyMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -87,6 +88,42 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn #endif } +int8_t MyMesh::findNeighbourSNR(const uint8_t* hash, uint8_t hash_size) { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp != 0 && neighbours[i].id.isHashMatch(hash, hash_size)) { + return neighbours[i].snr; + } + } +#endif + return INT8_MAX; +} + +// Approximate SNR demod floor per SF (same as RadioLibWrappers.cpp) +static float cr_snr_thresholds[] = { + -7.5f, // SF7 + -10.0f, // SF8 + -12.5f, // SF9 + -15.0f, // SF10 + -17.5f, // SF11 + -20.0f // SF12 +}; + +uint8_t MyMesh::selectCodingRateForPeer(const uint8_t* hash, uint8_t hash_size) { + int8_t snr4 = findNeighbourSNR(hash, hash_size); + if (snr4 == INT8_MAX) return 0; // unknown neighbor, use default + + float snr = snr4 / 4.0f; + float threshold = (_prefs.sf >= 7 && _prefs.sf <= 12) + ? cr_snr_thresholds[_prefs.sf - 7] : -15.0f; + float margin = snr - threshold; + + if (margin < 3.0f) return 8; + if (margin < 6.0f) return 7; + if (margin < 10.0f) return 6; + return 5; // good margin, use lightest CR +} + uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { ClientInfo* client = NULL; if (data[0] == 0) { // blank password, just check if sender is in ACL @@ -918,6 +955,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); + setDefaultCR(_prefs.cr); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 88729ea729..0a167f5e3d 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + int8_t findNeighbourSNR(const uint8_t* hash, uint8_t hash_size); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); @@ -155,6 +156,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getExtraAckTransmitCount() const override { return _prefs.multi_acks; } + uint8_t selectCodingRateForPeer(const uint8_t* hash, uint8_t hash_size) override; #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..8ff881f477 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -105,6 +105,9 @@ void Dispatcher::loop() { } _radio->onSendFinished(); + if (outbound->_tx_cr != 0 && outbound->_tx_cr != _default_cr) { + _radio->setCodingRate(_default_cr); + } logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); if (outbound->isRouteFlood()) { n_sent_flood++; @@ -117,6 +120,9 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); + if (outbound->_tx_cr != 0 && outbound->_tx_cr != _default_cr) { + _radio->setCodingRate(_default_cr); + } logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); releasePacket(outbound); // return to pool @@ -323,14 +329,21 @@ void Dispatcher::checkSend() { } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; + if (outbound->_tx_cr != 0 && outbound->_tx_cr != _default_cr) { + _radio->setCodingRate(outbound->_tx_cr); + } + uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); + if (outbound->_tx_cr != 0 && outbound->_tx_cr != _default_cr) { + _radio->setCodingRate(_default_cr); + } logTxFail(outbound, outbound->getRawLength()); - + releasePacket(outbound); // return to pool outbound = NULL; return; @@ -359,6 +372,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->_tx_cr = 0; } return pkt; } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..480651ac5e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -74,6 +74,8 @@ class Radio { */ virtual bool isReceiving() { return false; } + virtual void setCodingRate(uint8_t cr) { } + virtual float getLastRSSI() const { return 0; } virtual float getLastSNR() const { return 0; } }; @@ -136,6 +138,8 @@ class Dispatcher { MillisecondClock* _ms; uint16_t _err_flags; + uint8_t _default_cr; + Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr) : _radio(&radio), _ms(&ms), _mgr(&mgr) { @@ -150,6 +154,7 @@ class Dispatcher { tx_budget_ms = 0; last_budget_update = 0; duty_cycle_window_ms = 3600000; + _default_cr = 5; } virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; @@ -184,6 +189,8 @@ class Dispatcher { uint32_t getNumSentDirect() const { return n_sent_direct; } uint32_t getNumRecvFlood() const { return n_recv_flood; } uint32_t getNumRecvDirect() const { return n_recv_direct; } + void setDefaultCR(uint8_t cr) { _default_cr = cr; } + void resetStats() { n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0; _err_flags = 0; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 57fee14036..38dd3d8157 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -96,9 +96,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (!_tables->hasSeen(pkt)) { removeSelfFromPath(pkt); + pkt->_tx_cr = selectCodingRateForPeer(pkt->path, pkt->getPathHashSize()); uint32_t d = getDirectRetransmitDelay(pkt); - return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority + return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } } return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard. diff --git a/src/Mesh.h b/src/Mesh.h index f9f8786320..14c06bc02d 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -70,6 +70,14 @@ class Mesh : public Dispatcher { */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \brief Select a coding rate for the next-hop peer based on link quality. + * \param hash the hash of the next-hop peer (from path[0..hash_size-1]) + * \param hash_size bytes per hash entry + * \returns 0 = use node default CR, 5-8 = override CR for this packet + */ + virtual uint8_t selectCodingRateForPeer(const uint8_t* hash, uint8_t hash_size) { return 0; } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/Packet.cpp b/src/Packet.cpp index aad3e2f48e..43f8c3a023 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -8,6 +8,8 @@ Packet::Packet() { header = 0; path_len = 0; payload_len = 0; + _snr = 0; + _tx_cr = 0; } bool Packet::isValidPathLen(uint8_t path_len) { diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..e6d510bbb1 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ class Packet { uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + uint8_t _tx_cr; // 0 = use node default, 5-8 = override CR for this TX (not serialized to wire) /** * \brief calculate the hash of payload + type diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index fc0975cf65..3eb286d9da 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -16,6 +16,8 @@ class CustomLLCC68Wrapper : public RadioLibWrapper { float getLastRSSI() const override { return ((CustomLLCC68 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomLLCC68 *)_radio)->getSNR(); } + void setCodingRate(uint8_t cr) override { ((CustomLLCC68 *)_radio)->setCodingRate(cr); } + float packetScore(float snr, int packet_len) override { int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 42d364408c..f9b1bb0c5a 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -22,6 +22,8 @@ class CustomLR1110Wrapper : public RadioLibWrapper { _radio->setPreambleLength(16); // overcomes weird issues with small and big pkts } + void setCodingRate(uint8_t cr) override { ((CustomLR1110 *)_radio)->setCodingRate(cr); } + float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index e3e5202949..6cf84e3d02 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -17,6 +17,8 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper { float getLastRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); } + void setCodingRate(uint8_t cr) override { ((CustomSTM32WLx *)_radio)->setCodingRate(cr); } + float packetScore(float snr, int packet_len) override { int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 6499deb296..711a949669 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -20,6 +20,8 @@ class CustomSX1262Wrapper : public RadioLibWrapper { float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); } + void setCodingRate(uint8_t cr) override { ((CustomSX1262 *)_radio)->setCodingRate(cr); } + float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1262 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 54c37ee8aa..79db066bd6 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -20,6 +20,8 @@ class CustomSX1268Wrapper : public RadioLibWrapper { float getLastRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); } + void setCodingRate(uint8_t cr) override { ((CustomSX1268 *)_radio)->setCodingRate(cr); } + float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1268 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 5cde72f750..05fcf44d4e 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -19,6 +19,8 @@ class CustomSX1276Wrapper : public RadioLibWrapper { float getLastRSSI() const override { return ((CustomSX1276 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomSX1276 *)_radio)->getSNR(); } + void setCodingRate(uint8_t cr) override { ((CustomSX1276 *)_radio)->setCodingRate(cr); } + float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1276 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); From bf7a82b7f652eb55e9bc7a3c2c6c461a7cd943a6 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 5 Mar 2026 04:46:38 +0100 Subject: [PATCH 2/2] make it a const --- examples/simple_repeater/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6eac134aec..d05eac76a5 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -100,7 +100,7 @@ int8_t MyMesh::findNeighbourSNR(const uint8_t* hash, uint8_t hash_size) { } // Approximate SNR demod floor per SF (same as RadioLibWrappers.cpp) -static float cr_snr_thresholds[] = { +static const float cr_snr_thresholds[] = { -7.5f, // SF7 -10.0f, // SF8 -12.5f, // SF9