diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bf77872f..f9dcc16a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -556,6 +556,24 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change adaptive coding rate for direct retry packets +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr ,,` +- `set direct.retry.cr ,,,` + +**Parameters:** +- `cr4_min`: SNR in dB where retry packets use `CR4` +- `cr5_min`: SNR in dB where retry packets use `CR5` +- `cr8_max`: SNR in dB where retry packets use `CR8` +- `low`: optional repeated low boundary; both low values must match + +**Default:** `10.0,7.5,2.5,2.5` + +**Note:** DM retry packets with a recent repeater table entry use that entry's SNR to pick a local transmit coding rate. With the default, SNR `10.0 dB` and up uses `CR4`, SNR `7.5 dB` and up uses `CR5`, SNR `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. `CR6` is never selected. The shorter form `set direct.retry.cr 10.0,7.5,2.5` is equivalent to `set direct.retry.cr 10.0,7.5,2.5,2.5`. + +--- + #### View or change the SNR margin used for direct retry eligibility **Usage:** - `get direct.retry.margin` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f9a961b9..24e5c81d 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -846,6 +846,55 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } +uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) { + return 4; + } + if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) { + return 5; + } + if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) { + return 8; + } + return 7; +} +void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { + (void) original; + (void) retry_attempt; + + if (retry == NULL || !retry->isRouteDirect()) { + return; + } + + switch (retry->getPayloadType()) { + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_TXT_MSG: + case PAYLOAD_TYPE_ANON_REQ: + case PAYLOAD_TYPE_MULTIPART: + break; + default: + return; + } + + uint8_t prefix[MAX_ROUTE_HASH_BYTES]; + uint8_t prefix_len = 0; + if (!extractDirectRetryPrefix(retry, prefix, prefix_len)) { + return; + } + + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); + if (recent == NULL) { + return; + } + + uint8_t retry_cr = getDirectRetryCodingRateForSNR(recent->snr_x4); + if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != active_cr) { + retry->tx_cr = retry_cr; + } +} uint8_t MyMesh::getDirectRetryPreset() const { if (_prefs.direct_retry_preset <= DIRECT_RETRY_PRESET_MOBILE) { return _prefs.direct_retry_preset; @@ -1243,6 +1292,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; _prefs.direct_retry_preset = DIRECT_RETRY_PRESET_ROOFTOP; + _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 1a2f505c..a3a67676 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -124,6 +124,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; int8_t getDirectRetryMinSNRX4() const; + uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const; uint8_t getDirectRetryPreset() const; uint8_t getDirectRetryConfiguredMaxAttempts() const; uint32_t getDirectRetryAttemptStepMillis() const; @@ -154,7 +155,9 @@ protected: uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + uint8_t getDefaultTxCodingRate() const override { return active_cr; } bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index cccbd36c..4eb7eaf1 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -52,6 +52,13 @@ void Dispatcher::updateTxBudget() { } } +void Dispatcher::restoreOutboundCodingRate() { + if (outbound_restore_cr != 0) { + _radio->setCodingRate(outbound_restore_cr); + outbound_restore_cr = 0; + } +} + int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { return (int) ((pow(10, 0.85f - score) - 1.0) * air_time); } @@ -105,6 +112,7 @@ void Dispatcher::loop() { } _radio->onSendFinished(); + restoreOutboundCodingRate(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendComplete(outbound); if (outbound->isRouteFlood()) { @@ -118,6 +126,7 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); + restoreOutboundCodingRate(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendFail(outbound); @@ -150,6 +159,7 @@ void Dispatcher::loop() { bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { int i = 0; + pkt->tx_cr = 0; pkt->header = raw[i++]; if (pkt->getPayloadVer() > PAYLOAD_VER_1) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime()); @@ -326,11 +336,23 @@ void Dispatcher::checkSend() { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; + outbound_restore_cr = 0; + uint8_t default_cr = getDefaultTxCodingRate(); + if (outbound->tx_cr >= 4 && outbound->tx_cr <= 8 && default_cr >= 4 && default_cr <= 8 + && outbound->tx_cr != default_cr) { + if (_radio->setCodingRate(outbound->tx_cr)) { + outbound_restore_cr = default_cr; + max_airtime = _radio->getEstAirtimeFor(len)*3/2; + } else { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr); + } + } outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); + restoreOutboundCodingRate(); logTxFail(outbound, outbound->getRawLength()); releasePacket(outbound); // return to pool @@ -361,6 +383,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 90ee5cdb..2a910d8f 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -46,6 +46,12 @@ public: */ virtual bool startSendRaw(const uint8_t* bytes, int len) = 0; + /** + * \brief Sets LoRa coding rate for subsequent transmits/receives. + * \returns true if the radio accepted the coding rate. + */ + virtual bool setCodingRate(uint8_t cr) { return false; } + /** * \returns true if the previous 'startSendRaw()' completed successfully. */ @@ -116,6 +122,7 @@ typedef uint32_t DispatcherAction; class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint8_t outbound_restore_cr; unsigned long next_tx_time; unsigned long cad_busy_start; unsigned long radio_nonrx_start; @@ -128,6 +135,7 @@ class Dispatcher { unsigned long duty_cycle_window_ms; void processRecvPacket(Packet* pkt); + void restoreOutboundCodingRate(); void updateTxBudget(); protected: @@ -140,6 +148,7 @@ protected: : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; + outbound_restore_cr = 0; total_air_time = rx_air_time = 0; next_tx_time = ms.getMillis(); cad_busy_start = 0; @@ -167,6 +176,7 @@ protected: virtual int calcRxDelay(float score, uint32_t air_time) const; virtual uint32_t getCADFailRetryDelay() const; virtual uint32_t getCADFailMaxDuration() const; + virtual uint8_t getDefaultTxCodingRate() const { return 0; } virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 42bcf5b1..9228c88c 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -626,6 +626,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; + retry->tx_cr = 0; + uint8_t retry_attempt = _direct_retries[i].retry_attempts_sent + 1; + configureDirectRetryPacket(retry, packet, retry_attempt); uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); sendPacket(retry, _direct_retries[i].priority, retry_delay); if (isDirectRetryQueued(retry)) { @@ -633,10 +636,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); _direct_retries[i].waiting_final_echo = false; - onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("queued", retry, retry_delay, retry_attempt); } else { - onDirectRetryEvent("dropped_queue_full", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); - onDirectRetryEvent("failure", retry, elapsed_millis, _direct_retries[i].retry_attempts_sent + 1); + onDirectRetryEvent("dropped_queue_full", retry, retry_delay, retry_attempt); + onDirectRetryEvent("failure", retry, elapsed_millis, retry_attempt); clearDirectRetrySlot(i); } } @@ -657,6 +660,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; + retry->tx_cr = 0; + configureDirectRetryPacket(retry, packet, 1); // Start the echo wait only after the initial direct transmission actually completed. sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); diff --git a/src/Mesh.h b/src/Mesh.h index 91aec9a6..f2ce4538 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -130,6 +130,11 @@ protected: */ virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** + * \brief Optional hook to set local-only transmit options on a retry packet before it is queued. + */ + virtual void configureDirectRetryPacket(Packet* retry, const Packet* original, uint8_t retry_attempt) { } + /** * \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 aad3e2f4..3aab6349 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -8,6 +8,7 @@ Packet::Packet() { header = 0; path_len = 0; payload_len = 0; + tx_cr = 0; } bool Packet::isValidPathLen(uint8_t path_len) { @@ -64,6 +65,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { bool Packet::readFrom(const uint8_t src[], uint8_t len) { uint8_t i = 0; + tx_cr = 0; header = src[i++]; if (hasTransportCodes()) { memcpy(&transport_codes[0], &src[i], 2); i += 2; @@ -84,4 +86,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { return true; // success } -} \ No newline at end of file +} diff --git a/src/Packet.h b/src/Packet.h index 0886a06c..2943c037 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ public: uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + uint8_t tx_cr; // volatile local-only TX coding-rate override; not serialized /** * \brief calculate the hash of payload + type diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index dc206516..42b41f29 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -85,6 +85,38 @@ static uint8_t directRetryEffectiveMarginX4(const NodePrefs* prefs) { return constrain(prefs->direct_retry_snr_margin_db, (uint8_t)0, (uint8_t)DIRECT_RETRY_SNR_MARGIN_X4_MAX); } +static float directRetryCrX4ToDb(int8_t snr_x4) { + return ((float)snr_x4) / 4.0f; +} + +static void setDirectRetryCrDefaults(NodePrefs* prefs) { + prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; +} + +static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr8_snr_x4) { + return (cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr8_snr_x4 != 0) + && cr4_snr_x4 >= cr5_snr_x4 + && cr5_snr_x4 >= cr8_snr_x4; +} + +static void sanitizeDirectRetryCrThresholds(NodePrefs* prefs) { + if (!directRetryCrThresholdsAreValid(prefs->direct_retry_cr4_snr_x4, + prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr8_snr_x4)) { + setDirectRetryCrDefaults(prefs); + } +} + +static void formatDirectRetryCrThresholds(const NodePrefs* prefs, char* reply) { + char cr4[12], cr5[12], cr8[12]; + strcpy(cr4, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr4_snr_x4))); + strcpy(cr5, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr5_snr_x4))); + strcpy(cr8, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr8_snr_x4))); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr8, cr8); +} + static uint16_t directRetryPresetStepDefault(uint8_t preset) { switch (directRetryPresetOrDefault(preset)) { case DIRECT_RETRY_PRESET_INFRA: @@ -142,6 +174,63 @@ static bool parseDirectRetryPreset(const char* value, uint8_t& preset) { return false; } +static bool parseDirectRetryCrDb(const char* value, int8_t& snr_x4) { + if (value == NULL) { + return false; + } + + char* end = NULL; + float snr_db = strtof(value, &end); + while (end != NULL && *end == ' ') end++; + if (end == value || (end != NULL && *end != 0)) { + return false; + } + + int32_t scaled_x4 = (int32_t)((snr_db * 4.0f) + (snr_db >= 0.0f ? 0.5f : -0.5f)); + if (scaled_x4 < DIRECT_RETRY_CR_SNR_X4_MIN || scaled_x4 > DIRECT_RETRY_CR_SNR_X4_MAX) { + return false; + } + snr_x4 = (int8_t)scaled_x4; + return true; +} + +static bool parseDirectRetryCrThresholds(char* value, NodePrefs* prefs) { + if (value == NULL || prefs == NULL) { + return false; + } + + const char* parts[4]; + int num = mesh::Utils::parseTextParts(value, parts, 4); + if (num != 3 && num != 4) { + return false; + } + + int8_t cr4_snr_x4; + int8_t cr5_snr_x4; + int8_t cr8_snr_x4; + if (!parseDirectRetryCrDb(parts[0], cr4_snr_x4) + || !parseDirectRetryCrDb(parts[1], cr5_snr_x4) + || !parseDirectRetryCrDb(parts[num == 4 ? 3 : 2], cr8_snr_x4)) { + return false; + } + + if (num == 4) { + int8_t repeated_low_snr_x4; + if (!parseDirectRetryCrDb(parts[2], repeated_low_snr_x4) || repeated_low_snr_x4 != cr8_snr_x4) { + return false; + } + } + + if (!directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr8_snr_x4)) { + return false; + } + + prefs->direct_retry_cr4_snr_x4 = cr4_snr_x4; + prefs->direct_retry_cr5_snr_x4 = cr5_snr_x4; + prefs->direct_retry_cr8_snr_x4 = cr8_snr_x4; + return true; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -157,6 +246,8 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { loadPrefsInt(fs, "/node_prefs"); savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old + } else { + setDirectRetryCrDefaults(_prefs); } } @@ -227,6 +318,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 + size_t retry_cr_read = 0; + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, + sizeof(_prefs->direct_retry_cr4_snr_x4)); // 300 + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, + sizeof(_prefs->direct_retry_cr5_snr_x4)); // 301 + retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, + sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -234,7 +332,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1)) { _prefs->radio_fem_rxgain = constrain(legacy_retry_attempts_or_radio_fem_rxgain, 0, 1); } - // next: 298 + // next: 303 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -288,6 +386,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr8_snr_x4)) { + setDirectRetryCrDefaults(_prefs); + } else { + sanitizeDirectRetryCrThresholds(_prefs); + } file.close(); } @@ -360,7 +465,10 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 file.write((uint8_t *)&_prefs->direct_retry_preset, sizeof(_prefs->direct_retry_preset)); // 297 file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 - // next: 300 + file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 300 + file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 301 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 + // next: 303 file.close(); } @@ -532,6 +640,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); } else if (memcmp(config, "direct.retry.step", 17) == 0) { sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + formatDirectRetryCrThresholds(_prefs, reply); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -838,6 +948,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + StrHelper::strncpy(tmp, &config[16], sizeof(tmp)); + if (parseDirectRetryCrThresholds(tmp, _prefs)) { + savePrefs(); + formatDirectRetryCrThresholds(_prefs, reply); + } else { + strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1392,6 +1510,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + StrHelper::strncpy(tmp, &config[16], sizeof(tmp)); + if (parseDirectRetryCrThresholds(tmp, _prefs)) { + savePrefs(); + formatDirectRetryCrThresholds(_prefs, reply); + } else { + strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -1581,6 +1707,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); } else if (memcmp(config, "direct.retry.step", 17) == 0) { sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + formatDirectRetryCrThresholds(_prefs, reply); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ea30777a..43e8e6d7 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -38,6 +38,12 @@ #define DIRECT_RETRY_MOBILE_STEP_MS 50 #define DIRECT_RETRY_MOBILE_MARGIN_X4 0 +#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 // 10.0 dB and up => CR4 +#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 // 7.5 dB and up => CR5 +#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 // 2.5 dB and down => CR8 +#define DIRECT_RETRY_CR_SNR_X4_MIN -128 +#define DIRECT_RETRY_CR_SNR_X4_MAX 127 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -88,6 +94,9 @@ struct NodePrefs { // persisted to file uint8_t direct_retry_timing_magic[2]; uint8_t direct_retry_preset; uint16_t direct_retry_step_ms; + int8_t direct_retry_cr4_snr_x4; + int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr8_snr_x4; }; class CommonCLICallbacks { diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index fc0975cf..1b1ddcaa 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -20,6 +20,15 @@ public: int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomLLCC68 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomLLCC68Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 42d36440..b07e561d 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -25,6 +25,16 @@ public: float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomLR1110 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } + void setRxBoostedGainMode(bool en) override { ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en); } diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index e3e52029..d0aa2dae 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -21,6 +21,15 @@ public: int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSTM32WLx *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSTM32WLxWrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } }; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 6499deb2..72f6ba38 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -24,6 +24,15 @@ public: int sf = ((CustomSX1262 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSX1262 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } virtual void powerOff() override { ((CustomSX1262 *)_radio)->sleep(false); } diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 54c37ee8..50dfa9c2 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -24,6 +24,15 @@ public: int sf = ((CustomSX1268 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSX1268 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSX1268Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 5cde72f7..dd976306 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -23,4 +23,13 @@ public: int sf = ((CustomSX1276 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + bool setCodingRate(uint8_t cr) override { + idle(); + int err = ((CustomSX1276 *)_radio)->setCodingRate(cr); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("CustomSX1276Wrapper: error: setCodingRate(%d)=%d", (uint32_t)cr, err); + return false; + } + return true; + } };