From 9a8609097825980ea74e72409a345f6ea89cef44 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 8 May 2026 17:04:47 -0700 Subject: [PATCH 01/25] Update direct retry CR controls --- docs/cli_commands.md | 8 +-- examples/simple_repeater/MyMesh.cpp | 34 +++++++++--- src/helpers/CommonCLI.cpp | 84 ++++++++++++++++++++--------- src/helpers/CommonCLI.h | 2 + 4 files changed, 92 insertions(+), 36 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f9dcc16a..ab6af502 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -559,18 +559,18 @@ 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 ,,,` +- `set direct.retry.cr ,,,` +- `set direct.retry.cr off` **Parameters:** - `cr4_min`: SNR in dB where retry packets use `CR4` - `cr5_min`: SNR in dB where retry packets use `CR5` +- `cr7_min`: SNR in dB where retry packets use `CR7` - `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`. +**Note:** DM retry packets use the next-hop SNR from a recent repeater table entry to pick a local transmit coding rate; if no recent entry is available, retry packets use `CR5`. 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. Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e5c81d..89bd6020 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -744,7 +744,8 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u time_label = "echo_ms"; } - MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)", + uint8_t log_cr = (packet->tx_cr >= 4 && packet->tx_cr <= 8) ? packet->tx_cr : active_cr; + MESH_DEBUG_PRINTLN("%s direct retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, cr=%u, %s=%lu)", getLogDateTime(), event, (unsigned int)retry_attempt, @@ -755,6 +756,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u next_hop, snr_pkt_text, snr_table_text, + (unsigned int)log_cr, time_label, (unsigned long)delay_millis); @@ -762,7 +764,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, %s=%lu)\n", + f.printf(": DIRECT RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%s, next_hop=%s, pkt=%s, tbl=%s, cr=%u, %s=%lu)\n", event, (unsigned int)retry_attempt, (uint32_t)packet->getPayloadType(), @@ -772,6 +774,7 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u next_hop, snr_pkt_text, snr_table_text, + (unsigned int)log_cr, time_label, (unsigned long)delay_millis); f.close(); @@ -847,6 +850,12 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return true; } uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (_prefs.direct_retry_cr4_snr_x4 == 0 + && _prefs.direct_retry_cr5_snr_x4 == 0 + && _prefs.direct_retry_cr7_snr_x4 == 0 + && _prefs.direct_retry_cr8_snr_x4 == 0) { + return 0; + } if (snr_x4 >= _prefs.direct_retry_cr4_snr_x4) { return 4; } @@ -856,11 +865,13 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) { return 8; } + if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) { + return 7; + } 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; @@ -873,6 +884,7 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: + case PAYLOAD_TYPE_TRACE: case PAYLOAD_TYPE_MULTIPART: break; default: @@ -885,13 +897,20 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* return; } - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); - if (recent == NULL) { + if (_prefs.direct_retry_cr4_snr_x4 == 0 + && _prefs.direct_retry_cr5_snr_x4 == 0 + && _prefs.direct_retry_cr7_snr_x4 == 0 + && _prefs.direct_retry_cr8_snr_x4 == 0) { return; } - uint8_t retry_cr = getDirectRetryCodingRateForSNR(recent->snr_x4); - if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != active_cr) { + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); + uint8_t retry_cr = (recent != NULL) ? getDirectRetryCodingRateForSNR(recent->snr_x4) : 5; + if (retry_cr == 4 && retry_attempt > 3) { + retry_cr = 5; + } + + if (retry_cr >= 4 && retry_cr <= 8 && retry_cr != 6 && retry_cr != active_cr) { retry->tx_cr = retry_cr; } } @@ -1294,6 +1313,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _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_cr7_snr_x4 = DIRECT_RETRY_CR7_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; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 42b41f29..87126629 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -92,29 +92,53 @@ static float directRetryCrX4ToDb(int8_t snr_x4) { 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_cr7_snr_x4 = DIRECT_RETRY_CR7_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 setDirectRetryCrOff(NodePrefs* prefs) { + prefs->direct_retry_cr4_snr_x4 = 0; + prefs->direct_retry_cr5_snr_x4 = 0; + prefs->direct_retry_cr7_snr_x4 = 0; + prefs->direct_retry_cr8_snr_x4 = 0; +} + +static bool directRetryCrThresholdsAreOff(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr7_snr_x4, int8_t cr8_snr_x4) { + return cr4_snr_x4 == 0 && cr5_snr_x4 == 0 && cr7_snr_x4 == 0 && cr8_snr_x4 == 0; +} + +static bool directRetryCrThresholdsAreValid(int8_t cr4_snr_x4, int8_t cr5_snr_x4, int8_t cr7_snr_x4, int8_t cr8_snr_x4) { + return directRetryCrThresholdsAreOff(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4) + || ((cr4_snr_x4 != 0 || cr5_snr_x4 != 0 || cr7_snr_x4 != 0 || cr8_snr_x4 != 0) + && cr4_snr_x4 >= cr5_snr_x4 + && cr5_snr_x4 >= cr7_snr_x4 + && cr7_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_cr7_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]; + if (directRetryCrThresholdsAreOff(prefs->direct_retry_cr4_snr_x4, + prefs->direct_retry_cr5_snr_x4, + prefs->direct_retry_cr7_snr_x4, + prefs->direct_retry_cr8_snr_x4)) { + strcpy(reply, "> off"); + return; + } + + char cr4[12], cr5[12], cr7[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(cr7, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr7_snr_x4))); strcpy(cr8, StrHelper::ftoa(directRetryCrX4ToDb(prefs->direct_retry_cr8_snr_x4))); - sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr8, cr8); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); } static uint16_t directRetryPresetStepDefault(uint8_t preset) { @@ -198,35 +222,36 @@ static bool parseDirectRetryCrThresholds(char* value, NodePrefs* prefs) { if (value == NULL || prefs == NULL) { return false; } + if (strcmp(value, "off") == 0) { + setDirectRetryCrOff(prefs); + return true; + } - const char* parts[4]; - int num = mesh::Utils::parseTextParts(value, parts, 4); - if (num != 3 && num != 4) { + const char* parts[5]; + int num = mesh::Utils::parseTextParts(value, parts, 5); + if (num != 4) { return false; } int8_t cr4_snr_x4; int8_t cr5_snr_x4; + int8_t cr7_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)) { + || !parseDirectRetryCrDb(parts[2], cr7_snr_x4) + || !parseDirectRetryCrDb(parts[3], 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)) { + if (directRetryCrThresholdsAreOff(cr4_snr_x4, cr5_snr_x4, cr7_snr_x4, cr8_snr_x4) + || !directRetryCrThresholdsAreValid(cr4_snr_x4, cr5_snr_x4, cr7_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_cr7_snr_x4 = cr7_snr_x4; prefs->direct_retry_cr8_snr_x4 = cr8_snr_x4; return true; } @@ -323,8 +348,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { 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_cr7_snr_x4, + sizeof(_prefs->direct_retry_cr7_snr_x4)); // 302 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, - sizeof(_prefs->direct_retry_cr8_snr_x4)); // 302 + sizeof(_prefs->direct_retry_cr8_snr_x4)); // 303 // 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) @@ -332,7 +359,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: 303 + // next: 304 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -386,8 +413,14 @@ 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) + if (retry_cr_read == sizeof(_prefs->direct_retry_cr4_snr_x4) + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr7_snr_x4)) { + _prefs->direct_retry_cr8_snr_x4 = _prefs->direct_retry_cr7_snr_x4; + sanitizeDirectRetryCrThresholds(_prefs); + } else if (retry_cr_read != sizeof(_prefs->direct_retry_cr4_snr_x4) + + sizeof(_prefs->direct_retry_cr5_snr_x4) + + sizeof(_prefs->direct_retry_cr7_snr_x4) + sizeof(_prefs->direct_retry_cr8_snr_x4)) { setDirectRetryCrDefaults(_prefs); } else { @@ -467,8 +500,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 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.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 302 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 303 + // next: 304 file.close(); } @@ -954,7 +988,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re savePrefs(); formatDirectRetryCrThresholds(_prefs, reply); } else { - strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; @@ -1516,7 +1550,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep savePrefs(); formatDirectRetryCrThresholds(_prefs, reply); } else { - strcpy(reply, "Error, expected cr4,cr5,cr8 or cr4,cr5,low,low"); + strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 43e8e6d7..92401b38 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -40,6 +40,7 @@ #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_CR7_MIN_SNR_X4_DEFAULT 10 // 2.5 dB and up => CR7 #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 @@ -96,6 +97,7 @@ struct NodePrefs { // persisted to file uint16_t direct_retry_step_ms; int8_t direct_retry_cr4_snr_x4; int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; }; From 2af7b75d70dd64031398352e1d503e058059aa6b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 21 Apr 2026 16:24:00 -0700 Subject: [PATCH 02/25] For packets with a path set; auto try again if no echo was heard --- docs/cli_commands.md | 28 +++ examples/simple_repeater/MyMesh.cpp | 90 +++++++++- examples/simple_repeater/MyMesh.h | 7 + src/Dispatcher.cpp | 8 +- src/Dispatcher.h | 2 + src/Mesh.cpp | 270 +++++++++++++++++++++++++++- src/Mesh.h | 41 +++++ src/helpers/CommonCLI.cpp | 55 +++++- src/helpers/CommonCLI.h | 4 +- src/helpers/SimpleMeshTables.h | 174 +++++++++++++++--- 10 files changed, 643 insertions(+), 36 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6b4f6157..c6a7fac0 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -405,6 +405,34 @@ --- +#### View or change whether direct retries can fall back to the recently-heard repeater list +**Usage:** +- `get direct.retry.heard` +- `set direct.retry.heard ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `off` + +**Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. + +--- + +#### View or change the SNR margin used for direct retry eligibility +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) + +**Default:** `5` + +**Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6d957cc0..f268679c 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -40,6 +40,9 @@ #ifndef TXT_ACK_DELAY #define TXT_ACK_DELAY 200 #endif +#ifndef HALO_DIRECT_RETRY_DELAY_MIN + #define HALO_DIRECT_RETRY_DELAY_MIN 200 +#endif #define FIRMWARE_VER_LEVEL 2 @@ -60,6 +63,20 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0 && neighbours[i].id.isHashMatch(hash, hash_len)) { + return &neighbours[i]; + } + } +#else + (void)hash; + (void)hash_len; +#endif + return NULL; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -383,6 +400,25 @@ File MyMesh::openAppend(const char *fname) { #endif } +static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 }; +static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 }; +static uint8_t max_loop_strict[] = { 0, /* 1-byte */ 1, /* 2-byte */ 1, /* 3-byte */ 1 }; +// SF5..SF12 receive floors, scaled by 4 so we can keep the retry gate in int8_t quarter-dB units. +static const int8_t direct_retry_floor_x4[] = { -10, -20, -30, -40, -50, -60, -70, -80 }; + +bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) { + uint8_t hash_size = packet->getPathHashSize(); + uint8_t hash_count = packet->getPathHashCount(); + uint8_t n = 0; + const uint8_t* path = packet->path; + while (hash_count > 0) { // count how many times this node is already in the path + if (self_id.isHashMatch(path, hash_size)) n++; + hash_count--; + path += hash_size; + } + return n >= max_counters[hash_size]; +} + bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; @@ -487,6 +523,44 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } +int8_t MyMesh::getDirectRetryMinSNRX4() const { + // Use the live SF so `tempradio` changes immediately affect the retry threshold. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + return (int8_t)constrain(threshold, -128, 127); +} +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + if (_prefs.disable_fwd) { + return false; + } + + int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); + // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. + if (neighbour != NULL && neighbour->snr >= min_snr_x4) { + return true; + } + + if (!_prefs.direct_retry_recent_enabled) { + return false; + } + + // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + return recent != NULL && recent->snr_x4 >= min_snr_x4; +} +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. + float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); + if (kbps <= 0.0f) { + return HALO_DIRECT_RETRY_DELAY_MIN; + } + + // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. + uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; + uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); + return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); +} bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) @@ -771,7 +845,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.airtime_factor = 1.0; // one half _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f - _prefs.direct_tx_delay_factor = 0.2f; // was zero + _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 + _prefs.direct_retry_recent_enabled = 0; + _prefs.direct_retry_snr_margin_db = 5; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -801,6 +877,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; } void MyMesh::begin(FILESYSTEM *fs) { @@ -819,6 +898,9 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; radio_set_tx_power(_prefs.tx_power_dbm); updateAdvertTimer(); @@ -1196,12 +1278,18 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr); + active_bw = pending_bw; + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0d5cd28a..31528829 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -106,8 +106,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; + float active_bw; // live BW, including temporary radio overrides uint8_t pending_sf; + uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; + uint8_t active_cr; // live CR, including temporary radio overrides int matching_peer_indexes[MAX_CLIENTS]; #if defined(WITH_RS232_BRIDGE) RS232Bridge bridge; @@ -115,6 +118,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif + const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); 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); @@ -141,6 +146,8 @@ protected: uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; + bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; + uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 0a154985..d9dd00a3 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -68,7 +68,8 @@ void Dispatcher::loop() { next_tx_time = futureMillis(t * getAirtimeBudgetFactor()); _radio->onSendFinished(); - logTx(outbound, 2 + outbound->path_len + outbound->payload_len); + logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendComplete(outbound); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -80,7 +81,8 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); - logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len); + logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); + onSendFail(outbound); releasePacket(outbound); // return to pool outbound = NULL; @@ -330,4 +332,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 25a41d82..29460061 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -151,6 +151,8 @@ protected: virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging virtual void logTx(Packet* packet, int len) { } virtual void logTxFail(Packet* packet, int len) { } + virtual void onSendComplete(Packet* packet) { } + virtual void onSendFail(Packet* packet) { } virtual const char* getLogDateTime() { return ""; } virtual float getAirtimeBudgetFactor() const; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0548c907..082b6bb4 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -4,11 +4,32 @@ namespace mesh { void Mesh::begin() { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + _direct_retries[i].packet = NULL; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].retry_at = 0; + _direct_retries[i].retry_delay = 0; + _direct_retries[i].priority = 0; + _direct_retries[i].progress_marker = 0; + _direct_retries[i].expect_path_growth = false; + _direct_retries[i].queued = false; + _direct_retries[i].active = false; + } Dispatcher::begin(); } void Mesh::loop() { Dispatcher::loop(); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + if (!isDirectRetryQueued(_direct_retries[i].packet)) { + clearDirectRetrySlot(i); + } + } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -22,10 +43,25 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) { return 0; // by default, no delay } +bool Mesh::allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + return false; +} +uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { + // Keep the base fallback aligned with the repeater's minimum retry wait. + return 200; +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } +void Mesh::onSendComplete(Packet* packet) { + armDirectRetryOnSendComplete(packet); +} + +void Mesh::onSendFail(Packet* packet) { + clearPendingDirectRetryOnSendFail(packet); +} + uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } @@ -44,6 +80,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { return ACTION_RELEASE; } + if (pkt->isRouteDirect()) { + cancelDirectRetryOnEcho(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -63,6 +103,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 5); return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable? } } @@ -103,6 +144,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { removeSelfFromPath(pkt); uint32_t d = getDirectRetransmitDelay(pkt); + maybeScheduleDirectRetry(pkt, 0); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } } @@ -379,6 +421,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { memcpy(a1->path, packet->path, a1->path_len = packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a1, 0); sendPacket(a1, 0, delay_millis); } extra--; @@ -389,11 +432,225 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { memcpy(a2->path, packet->path, a2->path_len = packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; + maybeScheduleDirectRetry(a2, 0); sendPacket(a2, 0, delay_millis); } } } +void Mesh::clearDirectRetrySlot(int idx) { + _direct_retries[idx].packet = NULL; + _direct_retries[idx].trigger_packet = NULL; + _direct_retries[idx].retry_at = 0; + _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].priority = 0; + _direct_retries[idx].progress_marker = 0; + _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].queued = false; + _direct_retries[idx].active = false; +} + +bool Mesh::isDirectRetryQueued(const Packet* packet) const { + for (int i = 0; i < _mgr->getOutboundTotal(); i++) { + if (_mgr->getOutboundByIdx(i) == packet) { + return true; + } + } + return false; +} + +void Mesh::calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const { + uint8_t type = packet->getPayloadType(); + Utils::sha256(dest_key, MAX_HASH_SIZE, &type, 1, packet->payload, packet->payload_len); +} + +bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { + uint8_t recv_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, recv_key); + + bool cleared = false; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active || memcmp(recv_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) != 0) { + continue; + } + + bool is_echo = _direct_retries[i].expect_path_growth + ? packet->path_len > _direct_retries[i].progress_marker + : packet->getPathHashCount() < _direct_retries[i].progress_marker; + if (!is_echo) { + continue; + } + + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; + } + } + clearDirectRetrySlot(i); + } else { + clearDirectRetrySlot(i); + } + cleared = true; + } + + return cleared; +} + +void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet != packet) { + continue; + } + + // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + + // Start the echo wait only after the initial direct transmission actually completed. + sendPacket(retry, _direct_retries[i].priority, _direct_retries[i].retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].trigger_packet = NULL; + _direct_retries[i].queued = true; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + } else { + clearDirectRetrySlot(i); + } + } +} + +void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].queued) { + if (_direct_retries[i].packet == packet) { + // The queued retry itself failed; Dispatcher will release it after this hook. + clearDirectRetrySlot(i); + } + continue; + } + + if (_direct_retries[i].trigger_packet == packet) { + clearDirectRetrySlot(i); + } + } +} + +bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const { + switch (packet->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: + if (packet->getPathHashCount() <= 1) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_MULTIPART: + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + return false; + } + next_hop_hash = packet->path; + next_hop_hash_len = packet->getPathHashSize(); + progress_marker = packet->getPathHashCount(); + expect_path_growth = false; + return true; + + case PAYLOAD_TYPE_TRACE: { + if (packet->payload_len < 9) { + return false; + } + + uint8_t hash_size = 1 << (packet->payload[8] & 0x03); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t offset = packet->path_len * hash_size; + if (offset + hash_size > route_bytes) { + return false; + } + if (offset + (2 * hash_size) > route_bytes) { + return false; // no downstream repeater means there will be no forward echo to overhear. + } + + next_hop_hash = &packet->payload[9 + offset]; + next_hop_hash_len = hash_size; + progress_marker = packet->path_len; + expect_path_growth = true; + return true; + } + + default: + return false; + } +} + +void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { + const uint8_t* next_hop_hash; + uint8_t next_hop_hash_len; + uint8_t progress_marker; + bool expect_path_growth; + if (!getDirectRetryTarget(packet, next_hop_hash, next_hop_hash_len, progress_marker, expect_path_growth) + || !allowDirectRetry(packet, next_hop_hash, next_hop_hash_len)) { + return; + } + + int slot_idx = -1; + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (!_direct_retries[i].active) { + slot_idx = i; + break; + } + } + if (slot_idx < 0) { + return; + } + + // Only store retry metadata here; allocate the retry packet after the initial TX really completes. + uint32_t retry_delay = getDirectRetryEchoDelay(packet); + calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); + _direct_retries[slot_idx].packet = NULL; + _direct_retries[slot_idx].trigger_packet = const_cast(packet); + _direct_retries[slot_idx].retry_at = 0; + _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].priority = priority; + _direct_retries[slot_idx].progress_marker = progress_marker; + _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].queued = false; + _direct_retries[slot_idx].active = true; +} + Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; @@ -634,7 +891,7 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { packet->header |= ROUTE_TYPE_FLOOD; packet->path_len = 0; - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -659,7 +916,7 @@ void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_m packet->transport_codes[1] = transport_codes[1]; packet->path_len = 0; - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -692,7 +949,8 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin pri = 0; } } - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us + maybeScheduleDirectRetry(packet, pri); sendPacket(packet, pri, delay_millis); } @@ -702,7 +960,7 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) { packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } @@ -715,9 +973,9 @@ void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay packet->path_len = 0; // path_len of zero means Zero Hop - _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us + _tables->markSent(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, 0, delay_millis); } -} \ No newline at end of file +} diff --git a/src/Mesh.h b/src/Mesh.h index 00f7ed00..874336b0 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,6 +4,10 @@ namespace mesh { +#ifndef MAX_DIRECT_RETRY_SLOTS + #define MAX_DIRECT_RETRY_SLOTS 6 +#endif + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -16,6 +20,7 @@ public: class MeshTables { public: virtual bool hasSeen(const Packet* packet) = 0; + virtual void markSent(const Packet* packet) = 0; virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; @@ -24,17 +29,42 @@ public: * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { + struct DirectRetryEntry { + Packet* packet; + Packet* trigger_packet; + unsigned long retry_at; + uint32_t retry_delay; + uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t priority; + uint8_t progress_marker; + bool expect_path_growth; + bool queued; + bool active; + }; + RTCClock* _rtc; RNG* _rng; MeshTables* _tables; + DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); + void clearDirectRetrySlot(int idx); + bool isDirectRetryQueued(const Packet* packet) const; + void calculateDirectRetryKey(const Packet* packet, uint8_t* dest_key) const; + bool cancelDirectRetryOnEcho(const Packet* packet); + void armDirectRetryOnSendComplete(const Packet* packet); + void clearPendingDirectRetryOnSendFail(const Packet* packet); + bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, + uint8_t& progress_marker, bool& expect_path_growth) const; + void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); protected: DispatcherAction onRecvPacket(Packet* pkt) override; + void onSendComplete(Packet* packet) override; + void onSendFail(Packet* packet) override; virtual uint32_t getCADFailRetryDelay() const override; @@ -65,6 +95,17 @@ protected: */ virtual uint32_t getDirectRetransmitDelay(const Packet* packet); + /** + * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. + * Sub-classes can use neighbour tables or other link-quality data to opt in selectively. + */ + virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; + + /** + * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. + */ + virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 10ab8669..0f954efc 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,17 @@ #include "AdvertDataHelpers.h" #include +#ifndef BRIDGE_MAX_BAUD +#define BRIDGE_MAX_BAUD 115200 +#endif + +// These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 +#define DIRECT_RETRY_RECENT_DEFAULT 0 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -56,7 +67,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.read(pad, 4); // 108 + file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic[0], sizeof(_prefs->direct_retry_prefs_magic)); // 110 file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -95,6 +108,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); + // Old firmware left offset 108..111 undefined, so require the marker before using the new retry prefs. + if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 + || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { + _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + } else { + _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -140,7 +162,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.write(pad, 4); // 108 + file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled)); // 108 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_db, sizeof(_prefs->direct_retry_snr_margin_db)); // 109 + // Persist a marker so later loads can distinguish real values from legacy garbage in this reserved slot. + uint8_t retry_magic[2] = { DIRECT_RETRY_PREFS_MAGIC_0, DIRECT_RETRY_PREFS_MAGIC_1 }; + file.write(retry_magic, sizeof(retry_magic)); // 110 file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 @@ -316,6 +342,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "direct.retry.heard", 18) == 0) { + sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -535,6 +565,27 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "Error, cannot be negative"); } + } else if (memcmp(config, "direct.retry.heard ", 19) == 0) { + if (memcmp(&config[19], "on", 2) == 0) { + _prefs->direct_retry_recent_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[19], "off", 3) == 0) { + _prefs->direct_retry_recent_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + int db = atoi(&config[20]); + if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { + _prefs->direct_retry_snr_margin_db = (uint8_t)db; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8661d1e6..d0f26bfe 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -27,7 +27,9 @@ struct NodePrefs { // persisted to file float tx_delay_factor; char guest_password[16]; float direct_tx_delay_factor; - uint32_t guard; + uint8_t direct_retry_recent_enabled; + uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; uint8_t allow_read_only; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2f8af52a..217fd5a0 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,13 +8,103 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 +#define MAX_RECENT_REPEATERS 64 +#define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { +public: + struct RecentRepeaterInfo { + // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint8_t prefix[MAX_ROUTE_HASH_BYTES]; + uint8_t prefix_len; + int8_t snr_x4; + }; + +private: uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; int _next_idx; uint32_t _acks[MAX_PACKET_ACKS]; int _next_ack_idx; uint32_t _direct_dups, _flood_dups; + RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; + int _next_recent_repeater_idx; + + bool hasSeenAck(uint32_t ack) const { + for (int i = 0; i < MAX_PACKET_ACKS; i++) { + if (ack == _acks[i]) { + return true; + } + } + return false; + } + + void storeAck(uint32_t ack) { + _acks[_next_ack_idx] = ack; + _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; + } + + bool hasSeenHash(const uint8_t* hash) const { + const uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + return true; + } + } + return false; + } + + void storeHash(const uint8_t* hash) { + memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; + } + + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { + memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->getPayloadType() == PAYLOAD_TYPE_CONTROL + && packet->isRouteDirect() + && packet->getPathHashCount() == 0 + && packet->payload_len >= 6 + MAX_ROUTE_HASH_BYTES + && (packet->payload[0] & 0xF0) == 0x90) { + memcpy(prefix, &packet->payload[6], MAX_ROUTE_HASH_BYTES); + prefix_len = MAX_ROUTE_HASH_BYTES; + return true; + } + + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + return false; + } + + void recordRecentRepeater(const mesh::Packet* packet) { + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { + return; + } + + // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. + RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = packet->_snr; + _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + } public: SimpleMeshTables() { @@ -23,6 +113,8 @@ public: memset(_acks, 0, sizeof(_acks)); _next_ack_idx = 0; _direct_dups = _flood_dups = 0; + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; } #ifdef ESP32 @@ -31,12 +123,16 @@ public: f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.read((uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.read((uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); f.write((const uint8_t *) &_acks[0], sizeof(_acks)); f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + f.write((const uint8_t *) &_recent_repeaters[0], sizeof(_recent_repeaters)); + f.write((const uint8_t *) &_next_recent_repeater_idx, sizeof(_next_recent_repeater_idx)); } #endif @@ -44,28 +140,8 @@ public: if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; memcpy(&ack, packet->payload, 4); - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; - } - } - - _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table - return false; - } - uint8_t hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(hash); - - const uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + if (hasSeenAck(ack)) { if (packet->isRouteDirect()) { _direct_dups++; // keep some stats } else { @@ -73,13 +149,46 @@ public: } return true; } + + storeAck(ack); + return false; } - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + + if (hasSeenHash(hash)) { + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; + } + return true; + } + + storeHash(hash); + recordRecentRepeater(packet); return false; } + void markSent(const mesh::Packet* packet) override { + // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + if (!hasSeenAck(ack)) { + storeAck(ack); + } + return; + } + + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + if (!hasSeenHash(hash)) { + storeHash(hash); + } + } + void clear(const mesh::Packet* packet) override { if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { uint32_t ack; @@ -107,5 +216,24 @@ public: uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { + if (hash == NULL || hash_len == 0) { + return NULL; + } + + // Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len < hash_len || info->prefix_len == 0) { + continue; + } + if (memcmp(info->prefix, hash, hash_len) == 0) { + return info; + } + } + return NULL; + } + void resetStats() { _direct_dups = _flood_dups = 0; } }; From dbc4dda0ca67a84f2bfccbd950e0608527f8e0ab Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 23 Apr 2026 16:25:51 -0700 Subject: [PATCH 03/25] Retry 3 times with a 200ms,300ms,400ms backoff. --- docs/cli_commands.md | 13 +++++ examples/simple_repeater/MyMesh.cpp | 85 ++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 2 + src/Dispatcher.h | 2 + src/Mesh.cpp | 55 +++++++++++++++++- src/Mesh.h | 6 ++ src/helpers/SimpleMeshTables.h | 86 +++++++++++++++++++++++++---- 7 files changed, 236 insertions(+), 13 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c6a7fac0..9fa175a2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -98,6 +98,19 @@ --- +### Get or set recent repeater fallback prefix/SNR +**Usage:** +- `recent.repeater` +- `recent.repeater ` + +**Parameters:** +- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `snr_db`: SNR in dB (supports decimals; stored at x4 precision) + +**Note:** `set` is rejected when the prefix already exists in neighbors. + +--- + ## Statistics ### Clear Stats diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f268679c..9d9e7d03 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -77,6 +77,15 @@ const NeighbourInfo* MyMesh::findNeighbourByHash(const uint8_t* hash, uint8_t ha return NULL; } +bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx) { + if (ctx == NULL || prefix == NULL || prefix_len == 0) { + return true; + } + + const MyMesh* self = (const MyMesh*) ctx; + return self->findNeighbourByHash(prefix, prefix_len) == NULL; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -510,6 +519,34 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { } } +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) { + if (packet == NULL) { + return; + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + getLogDateTime(), + event, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned long)delay_millis); + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + event, + (uint32_t)packet->getPayloadType(), + packet->isRouteDirect() ? "D" : "F", + (uint32_t)packet->payload_len, + (unsigned long)delay_millis); + f.close(); + } + } +} + int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); @@ -880,6 +917,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; + + ((SimpleMeshTables *)getTables())->setRecentRepeaterAllowFilter(&MyMesh::allowRecentRepeaterPrefixStore, this); } void MyMesh::begin(FILESYSTEM *fs) { @@ -901,6 +940,7 @@ void MyMesh::begin(FILESYSTEM *fs) { active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); radio_set_tx_power(_prefs.tx_power_dbm); updateAdvertTimer(); @@ -1250,6 +1290,48 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } + } else if (memcmp(command, "recent.repeater", 15) == 0) { + const char* sub = command + 15; + while (*sub == ' ') sub++; + auto* tables = (SimpleMeshTables*)getTables(); + if (*sub == 0) { + const auto* info = tables->getLatestRecentRepeater(); + if (info == NULL) { + strcpy(reply, "> none"); + } else { + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + } else { + char* params = (char*) sub; + char* arg_snr = strchr(params, ' '); + if (arg_snr == NULL) { + strcpy(reply, "Err - usage: recent.repeater "); + } else { + *arg_snr++ = 0; + while (*arg_snr == ' ') arg_snr++; + if (*arg_snr == 0) { + strcpy(reply, "Err - usage: recent.repeater "); + } else { + int hex_len = strlen(params); + int prefix_len = hex_len / 2; + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { + strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + } else { + float snr_db = strtof(arg_snr, nullptr); + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); + snr_x4 = constrain(snr_x4, -128, 127); + if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - prefix is already in neighbors"); + } + } + } + } + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } @@ -1293,6 +1375,9 @@ void MyMesh::loop() { MESH_DEBUG_PRINTLN("Radio params restored"); } + // Keep recent-prefix learning aligned with the live retry SNR gate. + ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); + // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { acl.save(_fs); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 31528829..4b5d7b1f 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 const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); @@ -148,6 +149,7 @@ protected: uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 29460061..970e1975 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -161,6 +161,8 @@ protected: virtual uint32_t getCADFailMaxDuration() const; 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; } + const Packet* getOutboundInFlight() const { return outbound; } public: void begin(); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 082b6bb4..b4710b8c 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,12 +3,16 @@ namespace mesh { +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; +static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; + void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { _direct_retries[i].packet = NULL; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].retry_at = 0; _direct_retries[i].retry_delay = 0; + _direct_retries[i].retry_attempts_sent = 0; _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; @@ -27,6 +31,9 @@ void Mesh::loop() { } if (!isDirectRetryQueued(_direct_retries[i].packet)) { + if (_direct_retries[i].packet == getOutboundInFlight()) { + continue; // currently transmitting; keep slot until onSendComplete/onSendFail emits event + } clearDirectRetrySlot(i); } } @@ -443,6 +450,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_at = 0; _direct_retries[idx].retry_delay = 0; + _direct_retries[idx].retry_attempts_sent = 0; _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; @@ -491,8 +499,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { break; } } + onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0); + onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); + onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); } cleared = true; @@ -510,7 +522,35 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. - clearDirectRetrySlot(i); + onDirectRetryEvent("resent", packet, 0); + _direct_retries[i].retry_attempts_sent++; + if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + onDirectRetryEvent("failure", packet, 0); + clearDirectRetrySlot(i); + continue; + } + + Packet* retry = obtainNewPacket(); + if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, 0); + onDirectRetryEvent("failure", packet, 0); + clearDirectRetrySlot(i); + continue; + } + + *retry = *packet; + uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + sendPacket(retry, _direct_retries[i].priority, retry_delay); + if (isDirectRetryQueued(retry)) { + _direct_retries[i].packet = retry; + _direct_retries[i].retry_delay = retry_delay; + _direct_retries[i].retry_at = futureMillis(retry_delay); + onDirectRetryEvent("queued", retry, retry_delay); + } else { + onDirectRetryEvent("dropped_queue_full", retry, retry_delay); + onDirectRetryEvent("failure", retry, 0); + clearDirectRetrySlot(i); + } } continue; } @@ -522,6 +562,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // Allocate the retry packet only after TX-complete so busy repeaters do not reserve pool slots early. Packet* retry = obtainNewPacket(); if (retry == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, _direct_retries[i].retry_delay); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; } @@ -535,7 +577,10 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay); } else { + onDirectRetryEvent("dropped_queue_full", retry, _direct_retries[i].retry_delay); + onDirectRetryEvent("failure", retry, 0); clearDirectRetrySlot(i); } } @@ -550,12 +595,16 @@ void Mesh::clearPendingDirectRetryOnSendFail(const Packet* packet) { if (_direct_retries[i].queued) { if (_direct_retries[i].packet == packet) { // The queued retry itself failed; Dispatcher will release it after this hook. + onDirectRetryEvent("dropped_send_fail", packet, 0); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); } continue; } if (_direct_retries[i].trigger_packet == packet) { + onDirectRetryEvent("dropped_send_fail", packet, 0); + onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); } } @@ -638,17 +687,19 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = getDirectRetryEchoDelay(packet); + uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); _direct_retries[slot_idx].retry_at = 0; _direct_retries[slot_idx].retry_delay = retry_delay; + _direct_retries[slot_idx].retry_attempts_sent = 0; _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; + onDirectRetryEvent("armed", packet, retry_delay); } Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { diff --git a/src/Mesh.h b/src/Mesh.h index 874336b0..89b6bed4 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -34,6 +34,7 @@ class Mesh : public Dispatcher { Packet* trigger_packet; unsigned long retry_at; uint32_t retry_delay; + uint8_t retry_attempts_sent; uint8_t retry_key[MAX_HASH_SIZE]; uint8_t priority; uint8_t progress_marker; @@ -111,6 +112,11 @@ protected: */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \brief Optional hook for logging direct-retry lifecycle events. + */ + virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis) { } + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 217fd5a0..f5da272b 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -13,6 +13,8 @@ class SimpleMeshTables : public mesh::MeshTables { public: + typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx); + struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. uint8_t prefix[MAX_ROUTE_HASH_BYTES]; @@ -28,6 +30,9 @@ private: uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; int _next_recent_repeater_idx; + int8_t _recent_repeater_min_snr_x4; + RecentRepeaterAllowFn _recent_repeater_allow_fn; + void* _recent_repeater_allow_ctx; bool hasSeenAck(uint32_t ack) const { for (int i = 0; i < MAX_PACKET_ACKS; i++) { @@ -58,6 +63,11 @@ private: _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; } + bool prefixesOverlap(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) const { + uint8_t n = a_len < b_len ? a_len : b_len; + return n > 0 && memcmp(a, b, n) == 0; + } + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { @@ -96,14 +106,10 @@ private: if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { return; } - - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; - memset(slot.prefix, 0, sizeof(slot.prefix)); - memcpy(slot.prefix, prefix, prefix_len); - slot.prefix_len = prefix_len; - slot.snr_x4 = packet->_snr; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + if (packet->_snr < _recent_repeater_min_snr_x4) { + return; + } + setRecentRepeater(prefix, prefix_len, packet->_snr); } public: @@ -115,6 +121,9 @@ public: _direct_dups = _flood_dups = 0; memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); _next_recent_repeater_idx = 0; + _recent_repeater_min_snr_x4 = -128; + _recent_repeater_allow_fn = NULL; + _recent_repeater_allow_ctx = NULL; } #ifdef ESP32 @@ -216,19 +225,74 @@ public: uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) { + _recent_repeater_min_snr_x4 = min_snr_x4; + } + void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) { + _recent_repeater_allow_fn = fn; + _recent_repeater_allow_ctx = ctx; + } + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + return false; + } + + // Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + existing.snr_x4 = snr_x4; + return true; + } + + // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. + RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + memset(slot.prefix, 0, sizeof(slot.prefix)); + memcpy(slot.prefix, prefix, prefix_len); + slot.prefix_len = prefix_len; + slot.snr_x4 = snr_x4; + _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + return true; + } + const RecentRepeaterInfo* getLatestRecentRepeater() const { + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len > 0) { + return info; + } + } + return NULL; + } + const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { if (hash == NULL || hash_len == 0) { return NULL; } - // Search newest-to-oldest so the retry gate prefers the freshest SNR sample for a prefix. + // Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; const RecentRepeaterInfo* info = &_recent_repeaters[idx]; - if (info->prefix_len < hash_len || info->prefix_len == 0) { + if (info->prefix_len == 0) { continue; } - if (memcmp(info->prefix, hash, hash_len) == 0) { + if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { return info; } } From b125d0a0d1e9d4cef975ee091f193bf32a7ee2ab Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 00:08:10 -0700 Subject: [PATCH 04/25] Max retries is now a var that can be set between 1 to 15 --- docs/cli_commands.md | 39 +++++++- examples/simple_repeater/MyMesh.cpp | 136 ++++++++++++++++++++++++---- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 30 ++++-- src/Mesh.h | 10 ++ src/helpers/CommonCLI.cpp | 55 ++++++++++- src/helpers/CommonCLI.h | 3 + src/helpers/SimpleMeshTables.h | 45 +++++++++ 8 files changed, 287 insertions(+), 32 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9fa175a2..bbd28693 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -100,14 +100,21 @@ ### Get or set recent repeater fallback prefix/SNR **Usage:** -- `recent.repeater` -- `recent.repeater ` +- `get recent.repeater` +- `get recent.repeater all` +- `get recent.repeater first ` +- `get recent.repeater last ` +- `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) +- `count`: number of entries to print -**Note:** `set` is rejected when the prefix already exists in neighbors. +**Notes:** +- `set` is rejected when the prefix already exists in neighbors. +- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. +- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. --- @@ -446,6 +453,32 @@ --- +#### View or change the number of direct retry attempts +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `value`: Retry attempts after initial TX (`1`-`15`) + +**Default:** `3` + +--- + +#### View or change the base direct retry wait (milliseconds) +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `value`: Base wait in milliseconds (`10`-`5000`) + +**Default:** `200` + +**Note:** The actual first retry wait is `base + computed_echo_wait_from_live_phy`. + +--- + #### [Experimental] View or change the processing delay for received traffic **Usage:** - `get rxdelay` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 9d9e7d03..3e8985e8 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -587,16 +587,21 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho return recent != NULL && recent->snr_x4 >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); // Approximate LoRa line rate in kilobits/sec from the live radio params the repeater is using now. float kbps = (((float) active_sf) * active_bw * ((float) active_cr)) / ((float) (1UL << active_sf)); if (kbps <= 0.0f) { - return HALO_DIRECT_RETRY_DELAY_MIN; + return base_wait_millis; } // Wait roughly long enough for our transmission, the next hop's receive/forward window, and its echo back. uint32_t bits = ((uint32_t) packet->getRawLength()) * 8; uint32_t scaled_wait_millis = (uint32_t) ((((float) bits) * 4.0f) / kbps); - return max((uint32_t) HALO_DIRECT_RETRY_DELAY_MIN, scaled_wait_millis); + return base_wait_millis + scaled_wait_millis; +} +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); } bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { @@ -885,6 +890,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 _prefs.direct_retry_recent_enabled = 0; _prefs.direct_retry_snr_margin_db = 5; + _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; @@ -1290,29 +1297,36 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } - } else if (memcmp(command, "recent.repeater", 15) == 0) { - const char* sub = command + 15; - while (*sub == ' ') sub++; - auto* tables = (SimpleMeshTables*)getTables(); - if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } + } else if (strncmp(command, "get recent.repeater", 19) == 0 + || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "recent.repeater", 15) == 0) { + bool is_get = false; + bool is_set = false; + const char* sub = command; + + if (strncmp(command, "get recent.repeater", 19) == 0) { + is_get = true; + sub = command + 19; + } else if (strncmp(command, "set recent.repeater", 19) == 0) { + is_set = true; + sub = command + 19; } else { + sub = command + 15; // legacy command format + } + while (*sub == ' ') sub++; + + auto* tables = (SimpleMeshTables*)getTables(); + + if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { int hex_len = strlen(params); int prefix_len = hex_len / 2; @@ -1331,6 +1345,94 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + } else if (*sub == 0) { + const auto* info = tables->getLatestRecentRepeater(); + if (info == NULL) { + strcpy(reply, "> none"); + } else { + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + } else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + int total = tables->getRecentRepeaterCount(); + if (total <= 0) { + strcpy(reply, "> none"); + } else { + bool newest_first = false; + int limit = total; + const char* mode = "all"; + if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + while (*nstr == ' ') nstr++; + if (*nstr == 0) { + strcpy(reply, "Err - usage: get recent.repeater first|last "); + return; + } + char* end_ptr = NULL; + long parsed = strtol(nstr, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + limit = (int)parsed; + if (sub[0] == 'l') { + newest_first = true; + mode = "last"; + } else { + mode = "first"; + } + } + if (limit > total) { + limit = total; + } + + if (sender_timestamp == 0) { + Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + } + sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + } else { + // Remote CLI replies are packet-bound, so include as many rows as fit. + int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + bool truncated = false; + if (written < 0) { + reply[0] = 0; + written = 0; + } + for (int i = 0; i < limit; i++) { + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + if (info == NULL) { + continue; + } + if (written >= 154) { + truncated = true; + break; + } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; + mesh::Utils::toHex(hex, info->prefix, info->prefix_len); + int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + if (n < 0 || n >= (160 - written)) { + truncated = true; + break; + } + written += n; + } + if (truncated && written < 156) { + snprintf(reply + written, 160 - written, "\n..."); + } + } + } + } else { + strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 4b5d7b1f..3ecbfc81 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -149,6 +149,7 @@ protected: uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override; uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override; + uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis) override; int getInterferenceThreshold() const override { diff --git a/src/Mesh.cpp b/src/Mesh.cpp index b4710b8c..be2c81f1 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,8 +3,8 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS = 3; -static const uint32_t DIRECT_RETRY_BACKOFF_MS[DIRECT_RETRY_MAX_ATTEMPTS] = { 200, 300, 400 }; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { @@ -57,6 +57,14 @@ uint32_t Mesh::getDirectRetryEchoDelay(const Packet* packet) const { // Keep the base fallback aligned with the repeater's minimum retry wait. return 200; } +uint8_t Mesh::getDirectRetryMaxAttempts(const Packet* packet) const { + return DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT; +} +uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const { + uint32_t base = getDirectRetryEchoDelay(packet); + // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. + return base + ((uint32_t)attempt_idx * 100UL); +} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } @@ -524,7 +532,13 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { // The retry packet itself just finished transmitting; Dispatcher will release it after this hook. onDirectRetryEvent("resent", packet, 0); _direct_retries[i].retry_attempts_sent++; - if (_direct_retries[i].retry_attempts_sent >= DIRECT_RETRY_MAX_ATTEMPTS) { + uint8_t max_attempts = getDirectRetryMaxAttempts(packet); + if (max_attempts < 1) { + max_attempts = 1; + } else if (max_attempts > DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX) { + max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; + } + if (_direct_retries[i].retry_attempts_sent >= max_attempts) { onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; @@ -539,7 +553,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { } *retry = *packet; - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[_direct_retries[i].retry_attempts_sent]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, _direct_retries[i].retry_attempts_sent); sendPacket(retry, _direct_retries[i].priority, retry_delay); if (isDirectRetryQueued(retry)) { _direct_retries[i].packet = retry; @@ -619,7 +633,9 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h case PAYLOAD_TYPE_RESPONSE: case PAYLOAD_TYPE_TXT_MSG: case PAYLOAD_TYPE_ANON_REQ: - if (packet->getPathHashCount() <= 1) { + // Allow retries even when only one downstream hop remains so fixed direct paths + // (e.g. remote admin/login over 2-hop chains) use the same retry policy. + if (packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -629,7 +645,7 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h return true; case PAYLOAD_TYPE_MULTIPART: - if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() <= 1) { + if (packet->payload_len < 1 || (packet->payload[0] & 0x0F) != PAYLOAD_TYPE_ACK || packet->getPathHashCount() == 0) { return false; } next_hop_hash = packet->path; @@ -687,7 +703,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { } // Only store retry metadata here; allocate the retry packet after the initial TX really completes. - uint32_t retry_delay = DIRECT_RETRY_BACKOFF_MS[0]; + uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); diff --git a/src/Mesh.h b/src/Mesh.h index 89b6bed4..dd58d754 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -107,6 +107,16 @@ protected: */ virtual uint32_t getDirectRetryEchoDelay(const Packet* packet) const; + /** + * \returns maximum number of retry transmissions after the initial direct TX. + */ + virtual uint8_t getDirectRetryMaxAttempts(const Packet* packet) const; + + /** + * \returns delay before a specific retry attempt, where attempt_idx=0 is the first retry. + */ + virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) const; + /** * \returns number of extra (Direct) ACK transmissions wanted. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 0f954efc..bae389ba 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -14,6 +14,14 @@ #define DIRECT_RETRY_RECENT_DEFAULT 0 #define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 #define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 +#define DIRECT_RETRY_TIMING_MAGIC_1 0x54 +#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_MIN 1 +#define DIRECT_RETRY_COUNT_MAX 15 +#define DIRECT_RETRY_BASE_MS_DEFAULT 200 +#define DIRECT_RETRY_BASE_MS_MIN 10 +#define DIRECT_RETRY_BASE_MS_MAX 5000 // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -92,9 +100,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 - file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - // 290 + file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 + file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 290 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 291 + file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 293 + // next: 295 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -117,6 +128,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 + || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { + _prefs->direct_retry_attempts = DIRECT_RETRY_COUNT_DEFAULT; + _prefs->direct_retry_base_ms = DIRECT_RETRY_BASE_MS_DEFAULT; + } else { + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -190,8 +209,12 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - // 290 + file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 290 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 291 + uint8_t retry_timing_magic[2] = { DIRECT_RETRY_TIMING_MAGIC_0, DIRECT_RETRY_TIMING_MAGIC_1 }; + file.write(retry_timing_magic, sizeof(retry_timing_magic)); // 293 + // next: 295 file.close(); } @@ -346,6 +369,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); } else if (memcmp(config, "owner.info", 10) == 0) { *reply++ = '>'; *reply++ = ' '; @@ -586,6 +613,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { sprintf(reply, "Error, min 0 and max %d", DIRECT_RETRY_SNR_MARGIN_DB_MAX); } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int count = atoi(&config[19]); + if (count >= DIRECT_RETRY_COUNT_MIN && count <= DIRECT_RETRY_COUNT_MAX) { + _prefs->direct_retry_attempts = (uint8_t)count; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_COUNT_MIN, DIRECT_RETRY_COUNT_MAX); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int delay_ms = atoi(&config[18]); + if (delay_ms >= DIRECT_RETRY_BASE_MS_MIN && delay_ms <= DIRECT_RETRY_BASE_MS_MAX) { + _prefs->direct_retry_base_ms = (uint16_t)delay_ms; + savePrefs(); + strcpy(reply, "OK"); + } else { + sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_BASE_MS_MIN, DIRECT_RETRY_BASE_MS_MAX); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index d0f26bfe..ab8e058a 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -54,6 +54,9 @@ struct NodePrefs { // persisted to file uint32_t discovery_mod_timestamp; float adc_multiplier; char owner_info[120]; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint8_t direct_retry_timing_magic[2]; }; class CommonCLICallbacks { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index f5da272b..705869ad 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -279,6 +279,51 @@ public: } return NULL; } + int getRecentRepeaterCount() const { + int count = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].prefix_len > 0) { + count++; + } + } + return count; + } + const RecentRepeaterInfo* getRecentRepeaterNewestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } + const RecentRepeaterInfo* getRecentRepeaterOldestByIdx(int idx_wanted) const { + if (idx_wanted < 0) { + return NULL; + } + int idx_seen = 0; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + if (info->prefix_len == 0) { + continue; + } + if (idx_seen == idx_wanted) { + return info; + } + idx_seen++; + } + return NULL; + } const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { if (hash == NULL || hash_len == 0) { From d028c69d46d9807495173b6e78a7b7cddc4ab53e Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 14:41:07 -0700 Subject: [PATCH 05/25] Fix issue with packet prefixes getting added to the table. --- docs/cli_commands.md | 6 +- examples/simple_repeater/MyMesh.cpp | 93 ++++++++++++++++++++++------- src/helpers/SimpleMeshTables.h | 24 ++++---- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bbd28693..14d68a06 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -102,19 +102,23 @@ **Usage:** - `get recent.repeater` - `get recent.repeater all` +- `get recent.repeater all ` - `get recent.repeater first ` +- `get recent.repeater first ` - `get recent.repeater last ` +- `get recent.repeater last ` - `set recent.repeater ` **Parameters:** - `prefix_hex`: 1-3 bytes of next-hop prefix (hex) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) - `count`: number of entries to print +- `offset`: zero-based row offset into the selected order **Notes:** - `set` is rejected when the prefix already exists in neighbors. - `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Remote CLI replies include rows too, but may truncate when the packet payload limit is reached. +- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 3e8985e8..bf160b22 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1317,7 +1317,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply auto* tables = (SimpleMeshTables*)getTables(); - if (is_set || (!is_get && *sub != 0 && strcmp(sub, "all") != 0 && strncmp(sub, "first ", 6) != 0 && strncmp(sub, "last ", 5) != 0)) { + bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); + bool is_first = (strncmp(sub, "first ", 6) == 0); + bool is_last = (strncmp(sub, "last ", 5) == 0); + + if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { @@ -1354,62 +1358,111 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply mesh::Utils::toHex(hex, info->prefix, info->prefix_len); sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); } - } else if (strcmp(sub, "all") == 0 || strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { + } else if (is_all || is_first || is_last) { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { bool newest_first = false; int limit = total; + int offset = 0; const char* mode = "all"; - if (strncmp(sub, "first ", 6) == 0 || strncmp(sub, "last ", 5) == 0) { - const char* nstr = sub + (sub[0] == 'f' ? 6 : 5); + + if (is_first || is_last) { + const char* nstr = sub + (is_first ? 6 : 5); while (*nstr == ' ') nstr++; if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last "); + strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); return; } + char* end_ptr = NULL; - long parsed = strtol(nstr, &end_ptr, 10); + long parsed_count = strtol(nstr, &end_ptr, 10); while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || *end_ptr != 0 || parsed <= 0) { + if (end_ptr == NULL || parsed_count <= 0) { strcpy(reply, "Err - count must be > 0"); return; } - limit = (int)parsed; - if (sub[0] == 'l') { + + if (*end_ptr != 0) { + char* end_ptr2 = NULL; + long parsed_offset = strtol(end_ptr, &end_ptr2, 10); + while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; + if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { + strcpy(reply, "Err - offset must be >= 0"); + return; + } + offset = (int)parsed_offset; + } + + limit = (int)parsed_count; + if (is_last) { newest_first = true; mode = "last"; } else { mode = "first"; } + } else if (strncmp(sub, "all ", 4) == 0) { + const char* arg = sub + 4; + while (*arg == ' ') arg++; + if (*arg == 0) { + strcpy(reply, "Err - usage: get recent.repeater all "); + return; + } + + char* end_ptr = NULL; + long parsed_a = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || parsed_a <= 0) { + strcpy(reply, "Err - count must be > 0"); + return; + } + + char* end_ptr2 = NULL; + long parsed_b = strtol(end_ptr, &end_ptr2, 10); + while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; + if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { + strcpy(reply, "Err - usage: get recent.repeater all "); + return; + } + limit = (int)parsed_a; + offset = (int)parsed_b; } - if (limit > total) { - limit = total; + + if (offset >= total) { + sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); + return; + } + + int available = total - offset; + if (limit > available) { + limit = available; } if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d):\n", mode, limit, total); + Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); for (int i = 0; i < limit; i++) { - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + int idx = offset + i; + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); if (info == NULL) { continue; } char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); } - sprintf(reply, "> showing %d/%d (%s)", limit, total, mode); + sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); } else { // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> showing %d/%d (%s)", limit, total, mode); + int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } for (int i = 0; i < limit; i++) { - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(i) : tables->getRecentRepeaterOldestByIdx(i); + int idx = offset + i; + const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); if (info == NULL) { continue; } @@ -1419,7 +1472,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", i + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1427,12 +1480,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n..."); + snprintf(reply + written, 160 - written, "\n... use offset"); } } } } else { - strcpy(reply, "Err - usage: get recent.repeater [all|first |last ]"); + strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 705869ad..539bd5b6 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -70,6 +70,19 @@ private: bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. + // For flood traffic, the last path entry is the repeater we directly heard. + if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; + memcpy(prefix, last_hop, prefix_len); + return true; + } + + // If there is no flood path to inspect, fall back to payload-derived identities. if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) { memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES); prefix_len = MAX_ROUTE_HASH_BYTES; @@ -86,17 +99,6 @@ private: return true; } - if (packet->isRouteFlood() && packet->getPathHashCount() > 0) { - prefix_len = packet->getPathHashSize(); - if (prefix_len > MAX_ROUTE_HASH_BYTES) { - prefix_len = MAX_ROUTE_HASH_BYTES; - } - - const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()]; - memcpy(prefix, last_hop, prefix_len); - return true; - } - return false; } From d1d50967cbc99c0204dc7b7cf4428ad77b276a9e Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 24 Apr 2026 15:19:48 -0700 Subject: [PATCH 06/25] Round up the SNR vs replacement to get a weighted average. --- src/helpers/SimpleMeshTables.h | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 539bd5b6..effea219 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -68,6 +68,21 @@ private: return n > 0 && memcmp(a, b, n) == 0; } + int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; + int16_t avg = sum / 2; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive odd sums. + if (sum > 0 && (sum & 1)) { + avg++; + } + if (avg > 127) { + avg = 127; + } else if (avg < -128) { + avg = -128; + } + return (int8_t)avg; + } + bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { // Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID. // For flood traffic, the last path entry is the repeater we directly heard. @@ -258,7 +273,7 @@ public: memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = snr_x4; + existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); return true; } From 399939f41babbe7cacbdae95c33daa9fd0f95d14 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 27 Apr 2026 16:33:39 -0700 Subject: [PATCH 07/25] Refine direct retry SNR handling and recent repeater controls --- docs/cli_commands.md | 28 +- examples/simple_repeater/MyMesh.cpp | 448 +++++++++++++++++++--------- examples/simple_repeater/MyMesh.h | 1 + src/Mesh.cpp | 15 +- src/helpers/CommonCLI.cpp | 28 +- src/helpers/CommonCLI.h | 2 +- src/helpers/SimpleMeshTables.h | 194 ++++++++++-- 7 files changed, 538 insertions(+), 178 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 14d68a06..78f8626d 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -101,24 +101,20 @@ ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` -- `get recent.repeater all` -- `get recent.repeater all ` -- `get recent.repeater first ` -- `get recent.repeater first ` -- `get recent.repeater last ` -- `get recent.repeater last ` -- `set recent.repeater ` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` **Parameters:** -- `prefix_hex`: 1-3 bytes of next-hop prefix (hex) +- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) - `snr_db`: SNR in dB (supports decimals; stored at x4 precision) -- `count`: number of entries to print -- `offset`: zero-based row offset into the selected order +- `page`: 1-based page number **Notes:** - `set` is rejected when the prefix already exists in neighbors. -- `all` prints oldest to newest; `first` prints the oldest N; `last` prints the newest N. -- Over LoRa remote CLI, replies are packet-size limited; use `offset` to page through all rows. +- Rows are shown newest-first. +- Serial CLI prints all rows (no paging). +- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. --- @@ -437,7 +433,7 @@ **Parameters:** - `state`: `on`|`off` -**Default:** `off` +**Default:** `on` **Note:** When enabled, a repeater can use recently-heard non-duplicate repeater prefixes as a fallback for direct retry eligibility when no suitable neighbor entry is available. @@ -449,9 +445,9 @@ - `set direct.retry.margin ` **Parameters:** -- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, default `5`) +- `value`: Margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `2.5`) -**Default:** `5` +**Default:** `2.5` **Note:** The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. @@ -465,7 +461,7 @@ **Parameters:** - `value`: Retry attempts after initial TX (`1`-`15`) -**Default:** `3` +**Default:** `15` --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index bf160b22..10be2212 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -86,6 +86,64 @@ bool MyMesh::allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefi return self->findNeighbourByHash(prefix, prefix_len) == NULL; } +static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = 0; + if (info == NULL) { + return; + } + + uint8_t prefix_len = info->prefix_len; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len > 0) { + mesh::Utils::toHex(out, info->prefix, prefix_len); + } + + size_t used = strlen(out); + const size_t target_len = MAX_ROUTE_HASH_BYTES * 2; + while (used < target_len && used + 1 < out_len) { + out[used++] = ' '; + } + out[used] = 0; +} + +static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + + const char* snr_text = StrHelper::ftoa(((float)snr_x4) / 4.0f); + if (snr_text[0] == '-') { + snprintf(out, out_len, "%s", snr_text); + } else { + snprintf(out, out_len, " %s", snr_text); + } +} + +static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { + uint8_t code = flags & 0x03; + uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation + uint8_t size_linear = (uint8_t)(code + 1U); // packed-size interpretation (1..4) + + bool pow2_ok = size_pow2 > 0 && (route_bytes % size_pow2) == 0; + bool linear_ok = size_linear > 0 && (route_bytes % size_linear) == 0; + + if (pow2_ok && !linear_ok) { + return size_pow2; + } + if (linear_ok && !pow2_ok) { + return size_linear; + } + if (pow2_ok) { + return size_pow2; + } + return size_linear; +} + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -430,7 +488,31 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + auto* tables = (SimpleMeshTables *)getTables(); + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint16_t offset = (uint16_t)packet->path_len * (uint16_t)hash_size; + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4 = direct_retry_floor_x4[sf - 5] + 40; // fixed +10 dB above SF floor + + // A successful TRACE forward reveals the downstream next-hop hash. Seed/update the recent table immediately. + if (hash_size > 0 && offset + (2U * hash_size) <= route_bytes) { + uint8_t prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + const uint8_t* next_hop_prefix = &packet->payload[9 + offset + hash_size]; + const auto* existing = tables->findRecentRepeaterByHash(next_hop_prefix, prefix_len); + // This point only proves we can forward TO next_hop; packet->_snr is upstream RX and not a + // trustworthy metric for next_hop. Seed with existing table value or fallback only. + int8_t trace_snr_x4 = (existing != NULL) ? existing->snr_x4 : (int8_t)constrain(fallback_snr_x4, -128, 127); + tables->setRecentRepeater(next_hop_prefix, prefix_len, trace_snr_x4, false, true); + } + } + + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); return false; @@ -524,23 +606,102 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u return; } - MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, delay=%lu)", + uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; + uint8_t prefix_len = 0; + bool has_prefix = extractDirectRetryPrefix(packet, prefix, prefix_len); + auto* tables = (SimpleMeshTables *)getTables(); + const auto* existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; + char next_hop_hex[(MAX_ROUTE_HASH_BYTES * 2) + 1] = {0}; + if (has_prefix && prefix_len > 0) { + mesh::Utils::toHex(next_hop_hex, prefix, prefix_len); + } + const char* next_hop = (has_prefix && prefix_len > 0) ? next_hop_hex : "unknown"; + // Direct-retry events are TX-side and usually have no trustworthy RX SNR. + // Cap event SNR at fixed SF floor + 10 dB so trace-start retries can't inflate table SNR. + uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); + int16_t fallback_snr_x4_raw = direct_retry_floor_x4[sf - 5] + 40; + int8_t fallback_snr_x4 = (int8_t)constrain(fallback_snr_x4_raw, -128, 127); + bool is_success_event = (strcmp(event, "good") == 0 || strcmp(event, "canceled_echo") == 0); + int8_t retry_event_snr_x4; + const char* snr_src; + if (is_success_event && packet->_snr != 0) { + // On success, Mesh.cpp injects echo RX SNR for TRACE retries. + retry_event_snr_x4 = packet->_snr; + snr_src = "packet"; + } else if (existing != NULL) { + retry_event_snr_x4 = existing->snr_x4; + snr_src = "table"; + } else { + retry_event_snr_x4 = fallback_snr_x4; + snr_src = "fallback"; + } + char snr_used_text[12]; + char snr_pkt_text[12]; + char snr_table_text[12]; + snprintf(snr_used_text, sizeof(snr_used_text), "%s", StrHelper::ftoa(((float)retry_event_snr_x4) / 4.0f)); + snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); + if (existing != NULL) { + snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)existing->snr_x4) / 4.0f)); + } else { + snprintf(snr_table_text, sizeof(snr_table_text), "na"); + } + + if (has_prefix && is_success_event) { + // Refresh SNR only on successful echo/progress events, not on queued/resent bookkeeping. + tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); + } + + if (strcmp(event, "resent") == 0) { + if (has_prefix) { + // Retry stats should be visible even when the prefix was never learned into recent.repeater. + tables->incrementRecentRepeaterRetryCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + } else if (strcmp(event, "failed_all_tries") == 0) { + if (has_prefix) { + // A failed_all_tries event means all retry attempts for this packet failed. + // Count failures by retry-attempts so fail% reflects failed retries, not just failed sessions. + uint8_t give_up_retries = getDirectRetryMaxAttempts(packet); + uint8_t failed_retries = give_up_retries; + if (failed_retries < 1) { + failed_retries = 1; + } + for (uint8_t i = 0; i < failed_retries; i++) { + tables->incrementRecentRepeaterFailCount(prefix, prefix_len, true, retry_event_snr_x4, true); + } + if (failed_retries >= give_up_retries && give_up_retries > 0) { + // If all configured retry attempts still fail, slightly degrade stored path quality. + tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); + } + } + } + + MESH_DEBUG_PRINTLN("%s direct retry %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)", getLogDateTime(), event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); if (_logging) { File f = openAppend(PACKET_LOG_FILE); if (f) { f.print(getLogDateTime()); - f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, delay=%lu)\n", + f.printf(": DIRECT RETRY %s (type=%d, route=%s, payload_len=%d, next_hop=%s, snr=%s, snr_src=%s, pkt_snr=%s, table_snr=%s, delay=%lu)\n", event, (uint32_t)packet->getPayloadType(), packet->isRouteDirect() ? "D" : "F", (uint32_t)packet->payload_len, + next_hop, + snr_used_text, + snr_src, + snr_pkt_text, + snr_table_text, (unsigned long)delay_millis); f.close(); } @@ -563,28 +724,59 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { int8_t MyMesh::getDirectRetryMinSNRX4() const { // Use the live SF so `tempradio` changes immediately affect the retry threshold. uint8_t sf = constrain(active_sf, (uint8_t)5, (uint8_t)12); - int16_t threshold = direct_retry_floor_x4[sf - 5] + ((int16_t)_prefs.direct_retry_snr_margin_db * 4); + int16_t threshold = direct_retry_floor_x4[sf - 5] + (int16_t)_prefs.direct_retry_snr_margin_db; return (int8_t)constrain(threshold, -128, 127); } +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || prefix == NULL) { + return false; + } + + // TRACE direct routes encode repeater hashes in payload; packet->path carries SNR trail bytes. + if (packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE && packet->payload_len >= 9) { + uint8_t route_bytes = packet->payload_len - 9; + uint8_t hash_size = decodeTraceHashSize(packet->payload[8], route_bytes); + uint8_t offset = packet->path_len * hash_size; + if (hash_size > 0 && offset + hash_size <= route_bytes) { + prefix_len = hash_size; + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + memcpy(prefix, &packet->payload[9 + offset], prefix_len); + return true; + } + } + + if (packet->isRouteDirect() && packet->getPathHashCount() > 0) { + prefix_len = packet->getPathHashSize(); + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + if (prefix_len == 0) { + return false; + } + memcpy(prefix, packet->path, prefix_len); + return true; + } + + return false; +} bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { if (_prefs.disable_fwd) { return false; } int8_t min_snr_x4 = getDirectRetryMinSNRX4(); + if (_prefs.direct_retry_recent_enabled) { + // Prefer the 64-entry recent-prefix cache first, then fall back to neighbours. + const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); + if (recent != NULL && recent->snr_x4 >= min_snr_x4) { + return true; + } + } + const NeighbourInfo* neighbour = findNeighbourByHash(next_hop_hash, next_hop_hash_len); - // Prefer the explicit neighbor table first; it is the strongest signal that this hop is still reachable. - if (neighbour != NULL && neighbour->snr >= min_snr_x4) { - return true; - } - - if (!_prefs.direct_retry_recent_enabled) { - return false; - } - - // If no neighbor entry exists, fall back to the recent-heard repeater cache keyed by the same path prefix. - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); - return recent != NULL && recent->snr_x4 >= min_snr_x4; + return neighbour != NULL && neighbour->snr >= min_snr_x4; } uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { uint32_t base_wait_millis = constrain((uint32_t)_prefs.direct_retry_base_ms, (uint32_t)10, (uint32_t)5000); @@ -888,9 +1080,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 - _prefs.direct_retry_recent_enabled = 0; - _prefs.direct_retry_snr_margin_db = 5; - _prefs.direct_retry_attempts = 3; + _prefs.direct_retry_recent_enabled = 1; + _prefs.direct_retry_snr_margin_db = 10; // 2.5 dB stored in x4 units + _prefs.direct_retry_attempts = 15; _prefs.direct_retry_base_ms = 200; StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; @@ -1299,9 +1491,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 + || strncmp(command, "clear recent.repeater", 21) == 0 || strncmp(command, "recent.repeater", 15) == 0) { bool is_get = false; bool is_set = false; + bool is_clear = false; const char* sub = command; if (strncmp(command, "get recent.repeater", 19) == 0) { @@ -1310,38 +1504,55 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else if (strncmp(command, "set recent.repeater", 19) == 0) { is_set = true; sub = command + 19; + } else if (strncmp(command, "clear recent.repeater", 21) == 0) { + is_clear = true; + sub = command + 21; } else { sub = command + 15; // legacy command format } while (*sub == ' ') sub++; auto* tables = (SimpleMeshTables*)getTables(); + if (!is_get && !is_set && !is_clear && strncmp(sub, "clear", 5) == 0 && (sub[5] == 0 || sub[5] == ' ')) { + is_clear = true; + sub += 5; + while (*sub == ' ') sub++; + } - bool is_all = (strcmp(sub, "all") == 0 || strncmp(sub, "all ", 4) == 0); - bool is_first = (strncmp(sub, "first ", 6) == 0); - bool is_last = (strncmp(sub, "last ", 5) == 0); - - if (is_set || (!is_get && *sub != 0 && !is_all && !is_first && !is_last)) { + if (is_clear) { + if (*sub != 0) { + strcpy(reply, "Err - usage: clear recent.repeater"); + } else { + tables->clearRecentRepeaters(); + strcpy(reply, "OK"); + } + } else if (is_set) { char* params = (char*) sub; char* arg_snr = strchr(params, ' '); if (arg_snr == NULL) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { *arg_snr++ = 0; while (*arg_snr == ' ') arg_snr++; if (*arg_snr == 0) { - strcpy(reply, "Err - usage: set recent.repeater "); + strcpy(reply, "Err - usage: set recent.repeater "); } else { - int hex_len = strlen(params); - int prefix_len = hex_len / 2; uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; - if ((hex_len % 2) != 0 || prefix_len <= 0 || prefix_len > MAX_ROUTE_HASH_BYTES || !mesh::Utils::fromHex(prefix, prefix_len, params)) { - strcpy(reply, "Err - prefix must be 1-3 bytes hex"); + int hex_len = strlen(params); + if (hex_len != (MAX_ROUTE_HASH_BYTES * 2) || !mesh::Utils::fromHex(prefix, MAX_ROUTE_HASH_BYTES, params)) { + strcpy(reply, "Err - prefix must be exactly 3 bytes hex (6 chars)"); } else { - float snr_db = strtof(arg_snr, nullptr); + char* end_snr = NULL; + float snr_db = strtof(arg_snr, &end_snr); + while (end_snr != NULL && *end_snr == ' ') end_snr++; + if (end_snr == arg_snr || (end_snr != NULL && *end_snr != 0)) { + strcpy(reply, "Err - snr must be numeric"); + return; + } + int snr_x4 = (int)(snr_db * 4.0f + (snr_db >= 0.0f ? 0.5f : -0.5f)); snr_x4 = constrain(snr_x4, -128, 127); - if (tables->setRecentRepeater(prefix, (uint8_t)prefix_len, (int8_t)snr_x4)) { + if (tables->setRecentRepeater(prefix, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { strcpy(reply, "OK"); } else { strcpy(reply, "Err - prefix is already in neighbors"); @@ -1349,120 +1560,82 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } - } else if (*sub == 0) { - const auto* info = tables->getLatestRecentRepeater(); - if (info == NULL) { - strcpy(reply, "> none"); - } else { - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - sprintf(reply, "> %s,%s", hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); - } - } else if (is_all || is_first || is_last) { + } else { int total = tables->getRecentRepeaterCount(); if (total <= 0) { strcpy(reply, "> none"); } else { - bool newest_first = false; - int limit = total; - int offset = 0; - const char* mode = "all"; - - if (is_first || is_last) { - const char* nstr = sub + (is_first ? 6 : 5); - while (*nstr == ' ') nstr++; - if (*nstr == 0) { - strcpy(reply, "Err - usage: get recent.repeater first|last [offset]"); - return; - } - - char* end_ptr = NULL; - long parsed_count = strtol(nstr, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_count <= 0) { - strcpy(reply, "Err - count must be > 0"); - return; - } - - if (*end_ptr != 0) { - char* end_ptr2 = NULL; - long parsed_offset = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_offset < 0) { - strcpy(reply, "Err - offset must be >= 0"); - return; - } - offset = (int)parsed_offset; - } - - limit = (int)parsed_count; - if (is_last) { - newest_first = true; - mode = "last"; - } else { - mode = "first"; - } - } else if (strncmp(sub, "all ", 4) == 0) { - const char* arg = sub + 4; - while (*arg == ' ') arg++; - if (*arg == 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; - } - - char* end_ptr = NULL; - long parsed_a = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || parsed_a <= 0) { - strcpy(reply, "Err - count must be > 0"); - return; - } - - char* end_ptr2 = NULL; - long parsed_b = strtol(end_ptr, &end_ptr2, 10); - while (end_ptr2 != NULL && *end_ptr2 == ' ') end_ptr2++; - if (end_ptr2 == NULL || *end_ptr2 != 0 || parsed_b < 0) { - strcpy(reply, "Err - usage: get recent.repeater all "); - return; - } - limit = (int)parsed_a; - offset = (int)parsed_b; - } - - if (offset >= total) { - sprintf(reply, "> none (%s off=%d/%d)", mode, offset, total); - return; - } - - int available = total - offset; - if (limit > available) { - limit = available; - } - if (sender_timestamp == 0) { - Serial.printf("Recent repeater table (%s %d/%d, off=%d):\n", mode, limit, total, offset); - for (int i = 0; i < limit; i++) { - int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); + // Serial CLI: print all entries (no paging). + Serial.printf("Recent repeater table (newest first, total=%d):\n", total); + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); if (info == NULL) { continue; } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - Serial.printf("%02d: %s,%s\n", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + uint32_t fail_pct_x10 = 0; + if (info->retry_count > 0) { + fail_pct_x10 = (((uint32_t)info->fail_count * 1000UL) + (info->retry_count / 2U)) / (uint32_t)info->retry_count; + } + Serial.printf("%03d: %s,%s,fp=%lu.%01lu%%,r=%u,f=%u%s\n", + i + 1, + hex, + snr_text, + (unsigned long)(fail_pct_x10 / 10U), + (unsigned long)(fail_pct_x10 % 10U), + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); } - sprintf(reply, "> %s off=%d n=%d/%d", mode, offset, limit, total); + sprintf(reply, "> n=%d/%d", total, total); } else { - // Remote CLI replies are packet-bound, so include as many rows as fit. - int written = snprintf(reply, 160, "> %s off=%d n=%d/%d", mode, offset, limit, total); + // Remote CLI: page by fixed size to fit packet-limited reply payload. + long page_num = 1; + const long page_size = 4; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + + int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { reply[0] = 0; written = 0; } + for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = newest_first ? tables->getRecentRepeaterNewestByIdx(idx) : tables->getRecentRepeaterOldestByIdx(idx); + const auto* info = tables->getRecentRepeaterNewestByIdx(idx); if (info == NULL) { continue; } @@ -1470,9 +1643,20 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply truncated = true; break; } + char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - mesh::Utils::toHex(hex, info->prefix, info->prefix_len); - int n = snprintf(reply + written, 160 - written, "\n%02d:%s,%s", idx + 1, hex, StrHelper::ftoa(((float)info->snr_x4) / 4.0f)); + formatRecentRepeaterPrefix(info, hex, sizeof(hex)); + char snr_text[12]; + formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); + int n = snprintf(reply + written, + 160 - written, + "\n%03d:%s,%s,r=%u,f=%u%s", + idx + 1, + hex, + snr_text, + (uint32_t)info->retry_count, + (uint32_t)info->fail_count, + info->snr_locked ? ",l" : ""); if (n < 0 || n >= (160 - written)) { truncated = true; break; @@ -1480,12 +1664,10 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply written += n; } if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n... use offset"); + snprintf(reply + written, 160 - written, "\n... next page"); } } } - } else { - strcpy(reply, "Err - usage: get recent.repeater [all|all |first [offset]|last [offset]]"); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 3ecbfc81..d873a6b4 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 const NeighbourInfo* findNeighbourByHash(const uint8_t* hash, uint8_t hash_len) const; + bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const; static bool allowRecentRepeaterPrefixStore(const uint8_t* prefix, uint8_t prefix_len, void* ctx); int8_t getDirectRetryMinSNRX4() const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); diff --git a/src/Mesh.cpp b/src/Mesh.cpp index be2c81f1..bc34c5ee 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -3,7 +3,7 @@ namespace mesh { -static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 3; +static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; void Mesh::begin() { @@ -498,6 +498,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } if (_direct_retries[i].queued) { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].packet->_snr = packet->_snr; + } for (int j = 0; j < _mgr->getOutboundTotal(); j++) { if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { Packet* pending = _mgr->removeOutboundByIdx(j); @@ -511,6 +517,12 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { + if (_direct_retries[i].expect_path_growth + && _direct_retries[i].trigger_packet != NULL + && _direct_retries[i].progress_marker < packet->path_len) { + // For retry-good quality, use the received echo packet SNR (return-link quality). + _direct_retries[i].trigger_packet->_snr = packet->_snr; + } onDirectRetryEvent("canceled_echo", _direct_retries[i].trigger_packet, 0); onDirectRetryEvent("good", _direct_retries[i].trigger_packet, 0); clearDirectRetrySlot(i); @@ -539,6 +551,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { + onDirectRetryEvent("failed_all_tries", packet, 0); onDirectRetryEvent("failure", packet, 0); clearDirectRetrySlot(i); continue; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index bae389ba..03d83308 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -11,12 +11,13 @@ // These bytes used to be reserved/unused in persisted prefs, so keep a marker before trusting them. #define DIRECT_RETRY_PREFS_MAGIC_0 0xD4 #define DIRECT_RETRY_PREFS_MAGIC_1 0x52 -#define DIRECT_RETRY_RECENT_DEFAULT 0 -#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT 5 -#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_RECENT_DEFAULT 1 +#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 10 +#define DIRECT_RETRY_SNR_MARGIN_DB_MAX 40 +#define DIRECT_RETRY_SNR_MARGIN_X4_MAX (DIRECT_RETRY_SNR_MARGIN_DB_MAX * 4) #define DIRECT_RETRY_TIMING_MAGIC_0 0xD5 #define DIRECT_RETRY_TIMING_MAGIC_1 0x54 -#define DIRECT_RETRY_COUNT_DEFAULT 3 +#define DIRECT_RETRY_COUNT_DEFAULT 15 #define DIRECT_RETRY_COUNT_MIN 1 #define DIRECT_RETRY_COUNT_MAX 15 #define DIRECT_RETRY_BASE_MS_DEFAULT 200 @@ -33,6 +34,15 @@ static uint32_t _atoi(const char* sp) { return n; } +static uint8_t directRetryMarginDbToX4(float margin_db) { + int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); + return (uint8_t)constrain(scaled_x4, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); +} + +static float directRetryMarginX4ToDb(uint8_t margin_x4) { + return ((float)margin_x4) / 4.0f; +} + static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -123,10 +133,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (_prefs->direct_retry_prefs_magic[0] != DIRECT_RETRY_PREFS_MAGIC_0 || _prefs->direct_retry_prefs_magic[1] != DIRECT_RETRY_PREFS_MAGIC_1) { _prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT; - _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT; + _prefs->direct_retry_snr_margin_db = DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4; } else { _prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1); - _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_DB_MAX); + _prefs->direct_retry_snr_margin_db = constrain(_prefs->direct_retry_snr_margin_db, 0, DIRECT_RETRY_SNR_MARGIN_X4_MAX); } if (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 || _prefs->direct_retry_timing_magic[1] != DIRECT_RETRY_TIMING_MAGIC_1) { @@ -368,7 +378,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(config, "direct.retry.heard", 18) == 0) { sprintf(reply, "> %s", _prefs->direct_retry_recent_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_snr_margin_db); + sprintf(reply, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(_prefs->direct_retry_snr_margin_db))); } else if (memcmp(config, "direct.retry.count", 18) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); } else if (memcmp(config, "direct.retry.base", 17) == 0) { @@ -605,9 +615,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "Error, must be on or off"); } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { - int db = atoi(&config[20]); + float db = atof(&config[20]); if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { - _prefs->direct_retry_snr_margin_db = (uint8_t)db; + _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); savePrefs(); strcpy(reply, "OK"); } else { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index ab8e058a..6d49fec8 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -28,7 +28,7 @@ struct NodePrefs { // persisted to file char guest_password[16]; float direct_tx_delay_factor; uint8_t direct_retry_recent_enabled; - uint8_t direct_retry_snr_margin_db; + uint8_t direct_retry_snr_margin_db; // stored in quarter-dB units (x4) uint8_t direct_retry_prefs_magic[2]; uint8_t sf; uint8_t cr; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index effea219..ac6d01b5 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,7 +8,14 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 -#define MAX_RECENT_REPEATERS 64 +#ifndef MAX_RECENT_REPEATERS + // Two defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. + #if defined(ESP32) + #define MAX_RECENT_REPEATERS 512 + #else + #define MAX_RECENT_REPEATERS 64 + #endif +#endif #define MAX_ROUTE_HASH_BYTES 3 class SimpleMeshTables : public mesh::MeshTables { @@ -17,9 +24,12 @@ public: struct RecentRepeaterInfo { // Just enough identity to match a next-hop path prefix plus the SNR that heard it. + uint16_t retry_count; + uint16_t fail_count; uint8_t prefix[MAX_ROUTE_HASH_BYTES]; uint8_t prefix_len; int8_t snr_x4; + uint8_t snr_locked; }; private: @@ -68,19 +78,20 @@ private: return n > 0 && memcmp(a, b, n) == 0; } - int8_t avgSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { - int16_t sum = (int16_t)curr_snr_x4 + (int16_t)new_snr_x4; - int16_t avg = sum / 2; // truncates toward zero - // "Round up" means ceil(), which only differs from truncation for positive odd sums. - if (sum > 0 && (sum & 1)) { - avg++; + int8_t weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const { + // Keep existing SNR heavier than a single new sample: 75% existing + 25% new. + int16_t weighted_sum = ((int16_t)curr_snr_x4 * 3) + (int16_t)new_snr_x4; + int16_t blended = weighted_sum / 4; // truncates toward zero + // "Round up" means ceil(), which only differs from truncation for positive remainders. + if (weighted_sum > 0 && (weighted_sum % 4) != 0) { + blended++; } - if (avg > 127) { - avg = 127; - } else if (avg < -128) { - avg = -128; + if (blended > 127) { + blended = 127; + } else if (blended < -128) { + blended = -128; } - return (int8_t)avg; + return (int8_t)blended; } bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { @@ -249,7 +260,8 @@ public: _recent_repeater_allow_fn = fn; _recent_repeater_allow_ctx = ctx; } - bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4, bool snr_locked = false, + bool bypass_allow_filter = false) { if (prefix == NULL || prefix_len == 0) { return false; } @@ -258,7 +270,8 @@ public: prefix_len = MAX_ROUTE_HASH_BYTES; } - if (_recent_repeater_allow_fn != NULL && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { + if (!bypass_allow_filter && _recent_repeater_allow_fn != NULL + && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { return false; } @@ -273,19 +286,160 @@ public: memcpy(existing.prefix, prefix, prefix_len); existing.prefix_len = prefix_len; } - existing.snr_x4 = avgSnrX4RoundUp(existing.snr_x4, snr_x4); + if (snr_locked) { + existing.snr_x4 = snr_x4; + existing.snr_locked = 1; + } else if (!existing.snr_locked) { + existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); + } return true; } - // Ring buffer is enough here; retry fallback only needs a recent prefix->SNR observation. - RecentRepeaterInfo& slot = _recent_repeaters[_next_recent_repeater_idx]; + int slot_idx = -1; + // Prefer empty slots first while preserving newest-order iteration. + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; + if (_recent_repeaters[idx].prefix_len == 0) { + slot_idx = idx; + break; + } + } + if (slot_idx < 0) { + // Table is full: evict the weakest observed SNR entry. + slot_idx = 0; + int8_t min_snr_x4 = _recent_repeaters[0].snr_x4; + for (int i = 1; i < MAX_RECENT_REPEATERS; i++) { + if (_recent_repeaters[i].snr_x4 < min_snr_x4) { + min_snr_x4 = _recent_repeaters[i].snr_x4; + slot_idx = i; + } + } + } + + RecentRepeaterInfo& slot = _recent_repeaters[slot_idx]; memset(slot.prefix, 0, sizeof(slot.prefix)); memcpy(slot.prefix, prefix, prefix_len); slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; - _next_recent_repeater_idx = (_next_recent_repeater_idx + 1) % MAX_RECENT_REPEATERS; + slot.retry_count = 0; + slot.fail_count = 0; + slot.snr_locked = snr_locked ? 1 : 0; + _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } + bool incrementRecentRepeaterRetryCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.retry_count < 0xFFFF) { + existing.retry_count++; + } + return true; + } + return false; + } + bool incrementRecentRepeaterFailCount(const uint8_t* prefix, uint8_t prefix_len, + bool create_if_missing = false, int8_t seed_snr_x4 = 0, + bool bypass_allow_filter = false) { + if (prefix == NULL || prefix_len == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + + if (!create_if_missing || !setRecentRepeater(prefix, prefix_len, seed_snr_x4, false, bypass_allow_filter)) { + return false; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (existing.fail_count < 0xFFFF) { + existing.fail_count++; + } + return true; + } + return false; + } + bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) { + if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) { + return false; + } + if (prefix_len > MAX_ROUTE_HASH_BYTES) { + prefix_len = MAX_ROUTE_HASH_BYTES; + } + + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + RecentRepeaterInfo& existing = _recent_repeaters[i]; + if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + continue; + } + if (prefix_len > existing.prefix_len) { + memset(existing.prefix, 0, sizeof(existing.prefix)); + memcpy(existing.prefix, prefix, prefix_len); + existing.prefix_len = prefix_len; + } + if (!existing.snr_locked) { + int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; + if (lowered < -128) { + lowered = -128; + } + existing.snr_x4 = (int8_t)lowered; + } + return true; + } + return false; + } const RecentRepeaterInfo* getLatestRecentRepeater() const { for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; @@ -360,6 +514,10 @@ public: } return NULL; } + void clearRecentRepeaters() { + memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); + _next_recent_repeater_idx = 0; + } void resetStats() { _direct_dups = _flood_dups = 0; } }; From c20e57385a6b9912663553040153732fe31b5703 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 7 May 2026 16:51:49 -0700 Subject: [PATCH 08/25] Fix direct retry diagnostics --- docs/cli_commands.md | 10 ++- examples/simple_repeater/MyMesh.cpp | 133 ++++++++++++++++++++-------- src/Mesh.cpp | 80 +++++++++++++---- src/Mesh.h | 1 + 4 files changed, 162 insertions(+), 62 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 78f8626d..12f49de2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -111,10 +111,12 @@ - `page`: 1-based page number **Notes:** -- `set` is rejected when the prefix already exists in neighbors. -- Rows are shown newest-first. -- Serial CLI prints all rows (no paging). -- Over LoRa remote CLI, page size is fixed at `4` rows; choose page with `get recent.repeater `. +- `set` stores or updates the prefix in the recent repeater table. +- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. +- A full direct retry failure lowers the stored SNR by `0.25 dB`. +- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. +- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. +- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 10be2212..34b9bb57 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,6 @@ #include "MyMesh.h" #include +#include /* ------------------------------ Config -------------------------------- */ @@ -124,6 +125,45 @@ static void formatRecentRepeaterSnrX4(int8_t snr_x4, char* out, size_t out_len) } } +static int buildSortedRecentRepeaterView(SimpleMeshTables* tables, + const SimpleMeshTables::RecentRepeaterInfo** out, + int out_cap) { + if (tables == NULL || out == NULL || out_cap <= 0) { + return 0; + } + + int total = tables->getRecentRepeaterCount(); + if (total > out_cap) { + total = out_cap; + } + + int count = 0; + for (int i = 0; i < total; i++) { + const auto* info = tables->getRecentRepeaterNewestByIdx(i); + if (info != NULL) { + out[count++] = info; + } + } + + std::stable_sort(out, out + count, [](const SimpleMeshTables::RecentRepeaterInfo* a, + const SimpleMeshTables::RecentRepeaterInfo* b) { + uint8_t a_len = a->prefix_len; + uint8_t b_len = b->prefix_len; + if (a_len > MAX_ROUTE_HASH_BYTES) a_len = MAX_ROUTE_HASH_BYTES; + if (b_len > MAX_ROUTE_HASH_BYTES) b_len = MAX_ROUTE_HASH_BYTES; + + if (a_len != b_len) { + return a_len > b_len; + } + if (a->snr_x4 != b->snr_x4) { + return a->snr_x4 > b->snr_x4; + } + return false; + }); + + return count; +} + static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; uint8_t size_pow2 = (uint8_t)(1U << code); // legacy TRACE interpretation @@ -605,6 +645,9 @@ void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, u if (packet == NULL) { return; } + if (strcmp(event, "failure") == 0) { + return; + } uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; uint8_t prefix_len = 0; @@ -1561,15 +1604,58 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } else { - int total = tables->getRecentRepeaterCount(); + const long page_size = sender_timestamp == 0 ? 128 : 7; + long page_num = 1; + const char* arg = sub; + + if (strncmp(arg, "page ", 5) == 0) { + arg += 5; + while (*arg == ' ') arg++; + } + + if (*arg != 0) { + char* end_ptr = NULL; + page_num = strtol(arg, &end_ptr, 10); + while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; + if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { + strcpy(reply, "Err - usage: get recent.repeater [page]"); + return; + } + } + + size_t sorted_size = sizeof(SimpleMeshTables::RecentRepeaterInfo*) * MAX_RECENT_REPEATERS; + const SimpleMeshTables::RecentRepeaterInfo** sorted_recent = + (const SimpleMeshTables::RecentRepeaterInfo**)malloc(sorted_size); + if (sorted_recent == NULL) { + strcpy(reply, "Err - unable to allocate recent repeater view"); + return; + } + + int total = buildSortedRecentRepeaterView(tables, sorted_recent, MAX_RECENT_REPEATERS); if (total <= 0) { strcpy(reply, "> none"); } else { + int total_pages = (total + (int)page_size - 1) / (int)page_size; + if (page_num > total_pages) { + sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); + free(sorted_recent); + return; + } + + int offset = ((int)page_num - 1) * (int)page_size; + int limit = total - offset; + if (limit > (int)page_size) { + limit = (int)page_size; + } + if (sender_timestamp == 0) { - // Serial CLI: print all entries (no paging). - Serial.printf("Recent repeater table (newest first, total=%d):\n", total); - for (int i = 0; i < total; i++) { - const auto* info = tables->getRecentRepeaterNewestByIdx(i); + Serial.printf("Recent repeater table (3-byte,2-byte,1-byte; SNR desc, page=%ld/%d, n=%d/%d):\n", + page_num, + total_pages, + limit, + total); + for (int i = 0; i < limit; i++) { + const auto* info = sorted_recent[offset + i]; if (info == NULL) { continue; } @@ -1592,40 +1678,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply (uint32_t)info->fail_count, info->snr_locked ? ",l" : ""); } - sprintf(reply, "> n=%d/%d", total, total); + sprintf(reply, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); } else { - // Remote CLI: page by fixed size to fit packet-limited reply payload. - long page_num = 1; - const long page_size = 4; - const char* arg = sub; - - if (strncmp(arg, "page ", 5) == 0) { - arg += 5; - while (*arg == ' ') arg++; - } - - if (*arg != 0) { - char* end_ptr = NULL; - page_num = strtol(arg, &end_ptr, 10); - while (end_ptr != NULL && *end_ptr == ' ') end_ptr++; - if (end_ptr == NULL || page_num <= 0 || (end_ptr != NULL && *end_ptr != 0)) { - strcpy(reply, "Err - usage: get recent.repeater [page]"); - return; - } - } - - int total_pages = (total + (int)page_size - 1) / (int)page_size; - if (page_num > total_pages) { - sprintf(reply, "> none (page=%ld/%d)", page_num, total_pages); - return; - } - - int offset = ((int)page_num - 1) * (int)page_size; - int limit = total - offset; - if (limit > (int)page_size) { - limit = (int)page_size; - } - int written = snprintf(reply, 160, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); bool truncated = false; if (written < 0) { @@ -1635,7 +1689,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply for (int i = 0; i < limit; i++) { int idx = offset + i; - const auto* info = tables->getRecentRepeaterNewestByIdx(idx); + const auto* info = sorted_recent[idx]; if (info == NULL) { continue; } @@ -1668,6 +1722,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } } } + free(sorted_recent); } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands diff --git a/src/Mesh.cpp b/src/Mesh.cpp index bc34c5ee..a84a1fda 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -16,6 +16,7 @@ void Mesh::begin() { _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].queued = false; _direct_retries[i].active = false; } @@ -26,7 +27,25 @@ void Mesh::loop() { Dispatcher::loop(); for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { - if (!_direct_retries[i].active || !_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { + if (!_direct_retries[i].active) { + continue; + } + + if (_direct_retries[i].waiting_final_echo) { + if (!millisHasNowPassed(_direct_retries[i].retry_at)) { + continue; + } + + uint32_t elapsed_millis = _direct_retries[i].retry_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); + onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + if (!_direct_retries[i].queued || !millisHasNowPassed(_direct_retries[i].retry_at)) { continue; } @@ -454,6 +473,9 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { } void Mesh::clearDirectRetrySlot(int idx) { + if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { + releasePacket(_direct_retries[idx].packet); + } _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_at = 0; @@ -462,6 +484,7 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; + _direct_retries[idx].waiting_final_echo = false; _direct_retries[idx].queued = false; _direct_retries[idx].active = false; } @@ -497,24 +520,30 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { continue; } - if (_direct_retries[i].queued) { - if (_direct_retries[i].expect_path_growth - && _direct_retries[i].packet != NULL - && _direct_retries[i].progress_marker < packet->path_len) { - // For retry-good quality, use the received echo packet SNR (return-link quality). - _direct_retries[i].packet->_snr = packet->_snr; + int8_t echo_snr_x4 = packet->_snr; + if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { + if (_direct_retries[i].packet != NULL) { + // Success quality comes from the received downstream echo, not the original upstream RX. + _direct_retries[i].packet->_snr = echo_snr_x4; } - for (int j = 0; j < _mgr->getOutboundTotal(); j++) { - if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { - Packet* pending = _mgr->removeOutboundByIdx(j); - if (pending) { - releasePacket(pending); + uint32_t echo_millis = _direct_retries[i].echo_wait_started_at == 0 + ? 0 + : (uint32_t)(_ms->getMillis() - _direct_retries[i].echo_wait_started_at); + uint8_t retry_attempt = _direct_retries[i].waiting_final_echo + ? _direct_retries[i].retry_attempts_sent + : _direct_retries[i].retry_attempts_sent + 1; + onDirectRetryEvent("good", _direct_retries[i].packet, echo_millis, retry_attempt); + if (_direct_retries[i].queued) { + for (int j = 0; j < _mgr->getOutboundTotal(); j++) { + if (_mgr->getOutboundByIdx(j) == _direct_retries[i].packet) { + Packet* pending = _mgr->removeOutboundByIdx(j); + if (pending) { + releasePacket(pending); + } + break; } - break; } } - onDirectRetryEvent("canceled_echo", _direct_retries[i].packet, 0); - onDirectRetryEvent("good", _direct_retries[i].packet, 0); clearDirectRetrySlot(i); } else { if (_direct_retries[i].expect_path_growth @@ -551,9 +580,19 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - onDirectRetryEvent("failed_all_tries", packet, 0); - onDirectRetryEvent("failure", packet, 0); - clearDirectRetrySlot(i); + Packet* final_wait = obtainNewPacket(); + if (final_wait == NULL) { + onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + clearDirectRetrySlot(i); + continue; + } + + *final_wait = *packet; + _direct_retries[i].packet = final_wait; + _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); + _direct_retries[i].waiting_final_echo = true; + _direct_retries[i].queued = false; continue; } @@ -572,7 +611,8 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].retry_delay = retry_delay; _direct_retries[i].retry_at = futureMillis(retry_delay); - onDirectRetryEvent("queued", retry, retry_delay); + _direct_retries[i].waiting_final_echo = false; + onDirectRetryEvent("queued", retry, retry_delay, _direct_retries[i].retry_attempts_sent + 1); } else { onDirectRetryEvent("dropped_queue_full", retry, retry_delay); onDirectRetryEvent("failure", retry, 0); @@ -603,6 +643,7 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { _direct_retries[i].packet = retry; _direct_retries[i].trigger_packet = NULL; _direct_retries[i].queued = true; + _direct_retries[i].waiting_final_echo = false; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); onDirectRetryEvent("queued", retry, _direct_retries[i].retry_delay); } else { @@ -726,6 +767,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; + _direct_retries[slot_idx].waiting_final_echo = false; _direct_retries[slot_idx].queued = false; _direct_retries[slot_idx].active = true; onDirectRetryEvent("armed", packet, retry_delay); diff --git a/src/Mesh.h b/src/Mesh.h index dd58d754..85848875 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -39,6 +39,7 @@ class Mesh : public Dispatcher { uint8_t priority; uint8_t progress_marker; bool expect_path_growth; + bool waiting_final_echo; bool queued; bool active; }; From 29dc93b184ae2ce7a2926b1e3c1b5e45c985976c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 15:56:13 -0700 Subject: [PATCH 09/25] Document Halo direct retry settings only --- docs/halo_keymind_settings.md | 361 ---------------------------------- docs/halo_settings.md | 136 +++++++++++++ 2 files changed, 136 insertions(+), 361 deletions(-) delete mode 100644 docs/halo_keymind_settings.md create mode 100644 docs/halo_settings.md diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md deleted file mode 100644 index ca2c0803..00000000 --- a/docs/halo_keymind_settings.md +++ /dev/null @@ -1,361 +0,0 @@ -# Halo and Keymind Branch Settings - -This file covers only CLI settings and helper commands added by the Halo or -Keymind branches. Use `docs/cli_commands.md` for the general MeshCore CLI. - -## Quick Start - -```text -set retry.preset rooftop -set direct.retry.heard on -set flood.retry.advert off -set flood.retry.bridge off -set flood.retry.prefixes none -set flood.retry.ignore none -``` - -Then verify: - -```text -get retry.preset -get direct.retry.heard -get flood.retry.advert -get flood.retry.prefixes -get flood.retry.ignore -``` - -Use prefixes from the analyzer or neighbors list or `get recent.repeater` after the repeater has been online for a few hours. - -## Common Examples - -Disable retrying advert packets: - -```text -set flood.retry.advert off -get flood.retry.advert -``` - -Ignore a repeater as a successful flood retry echo: -Use this if you have a car repeater and a house repeater; have the house ignore the car. - -```text -set flood.retry.ignore 71CE82,C7618C -get flood.retry.ignore -``` - -Only accept specific downstream relays as flood retry success: -You're in a hole and need to hit a mountain top repeater to get out; keep trying till one you see one of these send out your packet. - -```text -set flood.retry.prefixes A58296,860CCA,425E5C -get flood.retry.prefixes -``` - -Bridge two groups of repeaters: - -```text -set flood.retry.bridge on -set flood.retry.bucket 1 71CE82,C7618C -set flood.retry.bucket 2 BEEBB0,425E5C -get flood.retry.bucket.1 -get flood.retry.bucket.2 -``` - -Return to simple non-bridge flood retry: - -```text -set flood.retry.bridge off -set flood.retry.prefixes none -set flood.retry.ignore none -``` - -## Added Settings - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `battery.alert` | Sends opt-in low-battery warnings to `#repeaters`. | `get battery.alert`, `set battery.alert on/off` | `set battery.alert on` | -| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` | -| `battery.alert.critical` | Critical threshold percentage. Critical warnings repeat more often. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` | -| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | -| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | -| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | - -## Other Keymind Commands - -| Command | What it does | How to use | Example | -| --- | --- | --- | --- | -| `send text.flood` | Sends a `#repeaters` flood text message formatted as `: `. | `send text.flood ` | `send text.flood checking ridge link` | - -## Battery Alerts - -Battery alerts are off by default. When enabled, the repeater checks once per -minute and sends a flood text warning to `#repeaters` when voltage is above -`1 V` and the estimated battery percent is below `battery.alert.low`. - -Warnings repeat every `24` hours, or every `12` hours when the estimate is -below `battery.alert.critical`. - -Defaults: - -| Setting | Default | -| --- | ---: | -| `battery.alert` | `off` | -| `battery.alert.low` | `20` | -| `battery.alert.critical` | `10` | - -Example: - -```text -set battery.alert.low 20 -set battery.alert.critical 10 -set battery.alert on -get battery.alert -``` - -## Recent Repeater Table - -Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. -Bridge buckets also use this table: a configured bucket prefix is active only -when it was heard within the last hour. - -Show learned rows: - -```text -get recent.repeater -get recent.repeater 2 -get recent.repeater page 3 -``` - -Seed or correct a prefix: - -```text -set recent.repeater A1B2C3 8.5 -``` - -Clear learned and manually seeded rows: - -```text -clear recent.repeater -``` - -Rows are sorted by prefix width, then SNR. A full direct retry failure lowers -the matching row by `0.25 dB`. - -Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to -`7` rows. - -## Direct Path Overrides - -`outpath` and `altpath` apply to the current remote client ACL entry. They need -remote client context, so they are not useful from the local serial CLI. - -Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` -hex characters, and all hops in one path must use the same width. - -```text -get outpath -set outpath A1B2C3,D4E5F6 -set outpath direct -set outpath clear -set outpath flood - -get altpath -set altpath A1B2C3,D4E5F6 -set altpath clear -``` - -`set outpath direct` sets a zero-hop direct route for a client reachable without -repeaters. `set outpath clear` forgets the override and lets normal path -discovery fill it again. `set outpath flood` forces replies to use flood packets -until the client logs in again. `altpath` sends a duplicate reply over a second -direct route; clearing it returns replies to a single route. - -## Direct Retry Settings - -Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `retry.preset` | Applies shared direct and flood retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | -| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | -| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | -| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | -| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | -| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | -| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | - -The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. -SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, -`2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no -recent repeater table entry is available, retry packets use `CR5`. Use -`set direct.retry.cr off` to disable adaptive coding-rate overrides. If -adaptive selection chooses `CR4`, retries after the third attempt use -`CR5`. - -Preset details: - -| Preset | Base | Count | Step | SNR gate | -| --- | ---: | ---: | ---: | --- | -| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | -| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | -| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | - -Example for a quiet fixed repeater: - -```text -set retry.preset rooftop -set direct.retry.heard on -set direct.retry.margin 5 -``` - -Example for a moving or weak-link node: - -```text -set retry.preset mobile -set direct.retry.margin 0 -``` - -## Flood And Advert Settings - -Flood retry applies to flood-routed packets. A queued retry is canceled when a qualifying downstream echo is heard. - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `flood.retry.count` | Maximum flood retry attempts after initial TX. `0` disables flood retry. | `get flood.retry.count`, `set flood.retry.count <0-15>` | `set flood.retry.count 7` | -| `flood.retry.path` | Maximum path hash count eligible for flood retry, or `off` to disable the gate. | `get flood.retry.path`, `set flood.retry.path <0-63/off>` | `set flood.retry.path 1` | -| `flood.retry.advert` | Allows or blocks retry for node advert packets (`type=4`). Default is `off`. | `get flood.retry.advert`, `set flood.retry.advert on/off` | `set flood.retry.advert off` | -| `flood.retry.prefixes` | Target prefixes. If set, only matching downstream echoes cancel a retry. | `get flood.retry.prefixes`, `set flood.retry.prefixes ` | `set flood.retry.prefixes BEEBB0,425E5C` | -| `flood.retry.ignore` | Ignored prefixes. In non-bridge retry, ignored last-hop echoes do not cancel retry. | `get flood.retry.ignore`, `set flood.retry.ignore ` | `set flood.retry.ignore 71CE82,C7618C` | -| `flood.retry.bridge` | Enables bucket-based bridge retry logic. | `get flood.retry.bridge`, `set flood.retry.bridge on/off` | `set flood.retry.bridge on` | -| `flood.retry.bucket.` | Shows one bridge bucket. Buckets are numbered `1`-`6`. | `get flood.retry.bucket.` | `get flood.retry.bucket.1` | -| `flood.retry.bucket` | Sets bridge bucket prefixes. | `set flood.retry.bucket <1-6> ` | `set flood.retry.bucket 1 71CE82,C7618C` | - -The shared retry preset sets these flood defaults: - -| Preset | Retry count | Path gate | -| --- | ---: | ---: | -| `infra` | `1` | `1` | -| `rooftop` | `3` | `2` | -| `mobile` | `3` | `1` | - -Example for path-gated retry: - -```text -set retry.preset rooftop -set flood.retry.path 1 -set flood.retry.advert off -set flood.retry.ignore 71CE82,C7618C -``` - -## North South Buckets - -Buckets describe groups of repeaters on different sides of this relay. Bucket -numbers do not have built-in meanings; this example uses bucket `1` for North -and bucket `2` for South. - -```text - North bucket 1 - +-----------------------+ - | A1B2C3 D4E5F6 | - | North A North B | - +-----------+-----------+ - | - v - +-----------+ - | This node | - +-----------+ - ^ - | - +-----------+-----------+ - | 71CE82 C7618C | - | South A South B | - +-----------------------+ - South bucket 2 -``` - -Configure the buckets: - -```text -set flood.retry.bridge on -set flood.retry.bucket 1 A1B2C3,D4E5F6 -set flood.retry.bucket 2 71CE82,C7618C -set flood.retry.ignore none -``` - -Packet heard from the North: - -```text - heard source - | - v - +--------------+ retry targets - | North bucket | -----> South bucket - | bucket 1 | -----> Other fresh/unbucketed relays - +--------------+ -``` - -Packet heard from the South: - -```text - heard source - | - v - +--------------+ retry targets - | South bucket | -----> North bucket - | bucket 2 | -----> Other fresh/unbucketed relays - +--------------+ -``` - -Packet heard from an unbucketed or pathless source: - -```text - heard source - | - v - +--------------+ retry targets - | Other bucket | -----> North bucket - | implicit | -----> South bucket - +--------------+ -``` - -Bridge retry stays eligible until every target bucket has been heard or -`flood.retry.count` is exhausted. A configured bucket is a target only when at -least one of its prefixes is fresh in `recent.repeater`. Prefixes in -`flood.retry.ignore` never count as bucket hits. - -## Troubleshooting - -If advert packets are still retrying: - -```text -get flood.retry.advert -set flood.retry.advert off -``` - -If ignored prefixes still appear in `flood retry good` logs: - -```text -get flood.retry.ignore -set flood.retry.ignore -``` - -The ignored prefix must match the last hop shown as `heard=`. For example, this log needs `C7618C` in the ignore list: - -```text -flood retry good (... path=7773D0>C7618C, heard=C7618C ...) -``` - -If retries are too aggressive: - -```text -set flood.retry.count 1 -set flood.retry.path 1 -set direct.retry.count 4 -``` - -If retries are too sparse: - -```text -set flood.retry.count 7 -set flood.retry.path 2 -``` diff --git a/docs/halo_settings.md b/docs/halo_settings.md new file mode 100644 index 00000000..9324487d --- /dev/null +++ b/docs/halo_settings.md @@ -0,0 +1,136 @@ +# Halo Direct Message Retry Settings + +This file covers only CLI settings and helper commands added for Halo direct-message retry behavior. Use `docs/cli_commands.md` for the general MeshCore CLI. + +Halo retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. + +## Quick Start + +```text +set retry.preset rooftop +set direct.retry.heard on +get retry.preset +get direct.retry.heard +get direct.retry.count +get direct.retry.base +get direct.retry.step +``` + +Use prefixes from the analyzer, neighbors list, or `get recent.repeater` after the repeater has been online for a few hours. + +## Added Halo Settings + +| Setting | What it does | How to use | Example | +| --- | --- | --- | --- | +| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | +| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | +| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | +| `retry.preset` | Applies direct retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | +| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | +| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | +| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | +| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | +| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | +| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | + +## Recent Repeater Table + +Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. + +Show learned rows: + +```text +get recent.repeater +get recent.repeater 2 +get recent.repeater page 3 +``` + +Seed or correct a prefix: + +```text +set recent.repeater A1B2C3 8.5 +``` + +Clear learned and manually seeded rows: + +```text +clear recent.repeater +``` + +Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by `0.25 dB`. + +Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to `7` rows. + +## Direct Path Overrides + +`outpath` and `altpath` apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI. + +Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` hex characters, and all hops in one path must use the same width. + +```text +get outpath +set outpath A1B2C3,D4E5F6 +set outpath direct +set outpath clear +set outpath flood + +get altpath +set altpath A1B2C3,D4E5F6 +set altpath clear +``` + +`set outpath direct` sets a zero-hop direct route for a client reachable without repeaters. `set outpath clear` forgets the override and lets normal path discovery fill it again. `set outpath flood` forces replies to use flood packets until the client logs in again. `altpath` sends a duplicate reply over a second direct route; clearing it returns replies to a single route. + +## Direct Retry Details + +The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no recent repeater table entry is available, retry packets use `CR5`. + +Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. + +Preset details: + +| Preset | Base | Count | Step | SNR gate | +| --- | ---: | ---: | ---: | --- | +| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | +| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | +| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | + +Example for a quiet fixed repeater: + +```text +set retry.preset rooftop +set direct.retry.heard on +set direct.retry.margin 5 +``` + +Example for a moving or weak-link node: + +```text +set retry.preset mobile +set direct.retry.margin 0 +``` + +## Troubleshooting + +If direct retries are too aggressive: + +```text +set direct.retry.count 4 +set direct.retry.margin 10 +``` + +If direct retries are too sparse: + +```text +set direct.retry.count 15 +set direct.retry.margin 0 +``` + +If direct retry is skipping a path you expect it to retry: + +```text +get direct.retry.heard +get recent.repeater +``` + +Either disable the heard gate with `set direct.retry.heard off`, or seed the next-hop prefix with `set recent.repeater `. From 17dc51dbcea17c7d210b5819b24db7fb92af414b Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 15:58:37 -0700 Subject: [PATCH 10/25] Remove build and variant changes from Halo retry branch --- boards/nrf52840_s140_v6.ld | 15 +---- boards/nrf52840_s140_v6_extrafs.ld | 15 +---- boards/nrf52840_s140_v7.ld | 15 +---- boards/nrf52840_s140_v7_extrafs.ld | 12 +--- build.sh | 7 +-- variants/gat562_30s_mesh_kit/platformio.ini | 3 +- variants/gat562_mesh_evb_pro/platformio.ini | 2 +- .../gat562_mesh_tracker_pro/platformio.ini | 3 +- variants/gat562_mesh_watch13/platformio.ini | 2 +- variants/heltec_ct62/platformio.ini | 4 -- variants/heltec_e213/HeltecE213Board.cpp | 27 +++++++++ variants/heltec_e213/HeltecE213Board.h | 3 + variants/heltec_e290/HeltecE290Board.cpp | 27 +++++++++ variants/heltec_e290/HeltecE290Board.h | 3 + variants/heltec_t096/LoRaFEMControl.h | 2 +- variants/heltec_t096/platformio.ini | 10 +--- variants/heltec_t114/platformio.ini | 2 +- variants/heltec_t190/HeltecT190Board.cpp | 27 +++++++++ variants/heltec_t190/HeltecT190Board.h | 3 + variants/heltec_tracker/platformio.ini | 4 -- .../HeltecTrackerV2Board.cpp | 31 ++++++++-- .../heltec_tracker_v2/HeltecTrackerV2Board.h | 2 + variants/heltec_tracker_v2/LoRaFEMControl.h | 2 +- variants/heltec_tracker_v2/platformio.ini | 4 -- variants/heltec_v2/HeltecV2Board.h | 25 +++++++++ variants/heltec_v2/platformio.ini | 4 -- variants/heltec_v3/HeltecV3Board.h | 29 ++++++++++ variants/heltec_v3/platformio.ini | 8 --- variants/heltec_v4/HeltecV4Board.h | 2 + variants/heltec_v4/LoRaFEMControl.h | 2 +- variants/heltec_v4/platformio.ini | 18 ------ variants/heltec_wireless_paper/platformio.ini | 4 -- variants/lilygo_t3s3/platformio.ini | 4 -- variants/lilygo_tbeam_1w/platformio.ini | 5 -- variants/lilygo_tbeam_SX1262/platformio.ini | 7 --- variants/lilygo_tbeam_SX1276/platformio.ini | 4 -- .../platformio.ini | 4 -- variants/lilygo_tdeck/TDeckBoard.h | 24 ++++++++ variants/lilygo_tlora_v2_1/platformio.ini | 6 +- variants/promicro/platformio.ini | 1 - variants/rak3112/RAK3112Board.h | 29 ++++++++++ variants/rak3401/platformio.ini | 3 +- variants/rak4631/platformio.ini | 3 +- variants/rak_wismesh_tag/platformio.ini | 1 - variants/sensecap_solar/platformio.ini | 2 +- variants/station_g2/StationG2Board.h | 24 ++++++++ variants/station_g2/platformio.ini | 5 ++ variants/station_g2/target.cpp | 15 ----- variants/station_g2/target.h | 3 - variants/thinknode_m2/ThinknodeM2Board.cpp | 56 +++++++++++-------- variants/thinknode_m2/ThinknodeM2Board.h | 4 ++ variants/thinknode_m5/ThinknodeM5Board.cpp | 8 +++ variants/thinknode_m5/ThinknodeM5Board.h | 3 + variants/xiao_c3/XiaoC3Board.h | 32 +++++++++++ variants/xiao_c3/platformio.ini | 5 -- variants/xiao_c6/platformio.ini | 1 - variants/xiao_nrf52/platformio.ini | 2 +- variants/xiao_s3/platformio.ini | 4 -- variants/xiao_s3_wio/platformio.ini | 4 -- 59 files changed, 362 insertions(+), 214 deletions(-) diff --git a/boards/nrf52840_s140_v6.ld b/boards/nrf52840_s140_v6.ld index d0c7d1dc..6dad975b 100644 --- a/boards/nrf52840_s140_v6.ld +++ b/boards/nrf52840_s140_v6.ld @@ -7,9 +7,6 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000 - /* To keep data in RAM across resets */ - PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 - /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -17,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -44,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld index bd454747..35261067 100644 --- a/boards/nrf52840_s140_v6_extrafs.ld +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -7,9 +7,6 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000 - /* To keep data in RAM across resets */ - PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 - /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -17,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -44,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7.ld b/boards/nrf52840_s140_v7.ld index 2333238f..6aaeb403 100644 --- a/boards/nrf52840_s140_v7.ld +++ b/boards/nrf52840_s140_v7.ld @@ -7,9 +7,6 @@ MEMORY { FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000 - /* To keep data in RAM across resets */ - PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 - /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -17,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -44,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7_extrafs.ld b/boards/nrf52840_s140_v7_extrafs.ld index 48348188..5956183a 100644 --- a/boards/nrf52840_s140_v7_extrafs.ld +++ b/boards/nrf52840_s140_v7_extrafs.ld @@ -14,19 +14,11 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 + RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 } SECTIONS { - . = ALIGN(4); - .persistent (NOLOAD) : - { - KEEP(*(.persistent_magic)) - KEEP(*(.persistent_data)) - . = ALIGN(4); - } > PERSISTENT_RAM - . = ALIGN(4); .svc_data : { @@ -41,6 +33,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} +} INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/build.sh b/build.sh index 006eae96..313c4c47 100755 --- a/build.sh +++ b/build.sh @@ -134,8 +134,7 @@ build_firmware() { # set firmware version string # e.g: v1.0.0-abcdef - # FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}" + FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" # craft filename # e.g: RAK_4631_Repeater-v1.0.0-SHA @@ -153,8 +152,8 @@ build_firmware() { # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}-upgrade.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-freshInstall-merged.bin 2>/dev/null || true + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true fi # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index aa3915a4..2baac256 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -2,13 +2,12 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index d7de585a..b3e89417 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 78ec7d01..af153b8f 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -2,11 +2,10 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index f457424f..f3510b74 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index e8becc7e..0179d965 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -130,10 +130,6 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_ct62_companion_radio_ble_ps] -extends = env:Heltec_ct62_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_ct62_sensor] extends = Heltec_ct62 build_flags = diff --git a/variants/heltec_e213/HeltecE213Board.cpp b/variants/heltec_e213/HeltecE213Board.cpp index 88737c4d..af115318 100644 --- a/variants/heltec_e213/HeltecE213Board.cpp +++ b/variants/heltec_e213/HeltecE213Board.cpp @@ -20,6 +20,33 @@ void HeltecE213Board::begin() { } } + void HeltecE213Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecE213Board::powerOff() { + enterDeepSleep(0); + } + uint16_t HeltecE213Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e213/HeltecE213Board.h b/variants/heltec_e213/HeltecE213Board.h index fadc038f..2192c141 100644 --- a/variants/heltec_e213/HeltecE213Board.h +++ b/variants/heltec_e213/HeltecE213Board.h @@ -3,6 +3,7 @@ #include #include #include +#include class HeltecE213Board : public ESP32Board { @@ -12,6 +13,8 @@ public: HeltecE213Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; }; diff --git a/variants/heltec_e290/HeltecE290Board.cpp b/variants/heltec_e290/HeltecE290Board.cpp index 96ec59c9..3994a206 100644 --- a/variants/heltec_e290/HeltecE290Board.cpp +++ b/variants/heltec_e290/HeltecE290Board.cpp @@ -20,6 +20,33 @@ void HeltecE290Board::begin() { } } + void HeltecE290Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecE290Board::powerOff() { + enterDeepSleep(0); + } + uint16_t HeltecE290Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_e290/HeltecE290Board.h b/variants/heltec_e290/HeltecE290Board.h index f287227c..645ec348 100644 --- a/variants/heltec_e290/HeltecE290Board.h +++ b/variants/heltec_e290/HeltecE290Board.h @@ -3,6 +3,7 @@ #include #include #include +#include class HeltecE290Board : public ESP32Board { @@ -12,6 +13,8 @@ public: HeltecE290Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_t096/LoRaFEMControl.h b/variants/heltec_t096/LoRaFEMControl.h index a3b5c4ed..0ce60fff 100644 --- a/variants/heltec_t096/LoRaFEMControl.h +++ b/variants/heltec_t096/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = true; + bool lna_enabled = false; bool lna_can_control = false; }; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 5f17436a..e820bf58 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -120,7 +120,7 @@ build_src_filter = ${Heltec_t096.build_src_filter} lib_deps = ${Heltec_t096.lib_deps} -[env:Heltec_t096_companion_radio_ble_femon] +[env:Heltec_t096_companion_radio_ble] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 @@ -143,12 +143,6 @@ lib_deps = ${Heltec_t096.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_t096_companion_radio_ble_femoff] -extends = env:Heltec_t096_companion_radio_ble_femon -build_flags = - ${env:Heltec_t096_companion_radio_ble_femon.build_flags} - -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off - [env:Heltec_t096_companion_radio_usb] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index e808d6c2..135babb1 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/heltec_t190/HeltecT190Board.cpp b/variants/heltec_t190/HeltecT190Board.cpp index 0a16b52b..4f35be40 100644 --- a/variants/heltec_t190/HeltecT190Board.cpp +++ b/variants/heltec_t190/HeltecT190Board.cpp @@ -20,6 +20,33 @@ void HeltecT190Board::begin() { } } + void HeltecT190Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecT190Board::powerOff() { + enterDeepSleep(0); + } + uint16_t HeltecT190Board::getBattMilliVolts() { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, HIGH); diff --git a/variants/heltec_t190/HeltecT190Board.h b/variants/heltec_t190/HeltecT190Board.h index 557c070e..bc38c1e0 100644 --- a/variants/heltec_t190/HeltecT190Board.h +++ b/variants/heltec_t190/HeltecT190Board.h @@ -3,6 +3,7 @@ #include #include #include +#include class HeltecT190Board : public ESP32Board { @@ -12,6 +13,8 @@ public: HeltecT190Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { } void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 2cd0cea6..69293d70 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -99,10 +99,6 @@ lib_deps = ${Heltec_tracker_base.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_Wireless_Tracker_companion_radio_ble_ps] -extends = env:Heltec_Wireless_Tracker_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_Wireless_Tracker_repeater] extends = Heltec_tracker_base build_flags = diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 99b1cdfe..f182c905 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -35,12 +35,33 @@ void HeltecTrackerV2Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecTrackerV2Board::powerOff() { - // Turn off PA - digitalWrite(P_LORA_PA_POWER, LOW); - rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + void HeltecTrackerV2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - ESP32Board::powerOff(); + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + loRaFEMControl.setRxModeEnableWhenMCUSleep();//It also needs to be enabled in receive mode + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void HeltecTrackerV2Board::powerOff() { + enterDeepSleep(0); } uint16_t HeltecTrackerV2Board::getBattMilliVolts() { diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h index 2bd6a025..ccbecc7a 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.h +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "LoRaFEMControl.h" class HeltecTrackerV2Board : public ESP32Board { @@ -16,6 +17,7 @@ public: void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/heltec_tracker_v2/LoRaFEMControl.h b/variants/heltec_tracker_v2/LoRaFEMControl.h index a3b5c4ed..0ce60fff 100644 --- a/variants/heltec_tracker_v2/LoRaFEMControl.h +++ b/variants/heltec_tracker_v2/LoRaFEMControl.h @@ -17,6 +17,6 @@ class LoRaFEMControl bool isLNAEnabled(void) const { return lna_enabled; } private: - bool lna_enabled = true; + bool lna_enabled = false; bool lna_can_control = false; }; diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d914ce6a..d57c2113 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -176,10 +176,6 @@ lib_deps = ${Heltec_tracker_v2.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_tracker_v2_companion_radio_ble_ps] -extends = env:heltec_tracker_v2_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:heltec_tracker_v2_companion_radio_wifi] extends = Heltec_tracker_v2 build_flags = diff --git a/variants/heltec_v2/HeltecV2Board.h b/variants/heltec_v2/HeltecV2Board.h index 9b08fe94..fe800890 100644 --- a/variants/heltec_v2/HeltecV2Board.h +++ b/variants/heltec_v2/HeltecV2Board.h @@ -7,6 +7,8 @@ #define PIN_VBAT_READ 37 #define PIN_LED_BUILTIN 25 +#include + class HeltecV2Board : public ESP32Board { public: void begin() { @@ -24,6 +26,29 @@ public: } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_0, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint16_t getBattMilliVolts() override { analogReadResolution(10); diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index e9cf56f0..ba4f8694 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -172,10 +172,6 @@ lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_v2_companion_radio_ble_ps] -extends = env:Heltec_v2_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_v2_companion_radio_wifi] extends = Heltec_lora32_v2 build_flags = diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index 7e7abe31..ba22a7f2 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -17,6 +17,8 @@ #define PIN_ADC_CTRL_ACTIVE LOW #define PIN_ADC_CTRL_INACTIVE HIGH +#include + class HeltecV3Board : public ESP32Board { private: bool adc_active_state; @@ -50,6 +52,33 @@ public: } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(10); digitalWrite(PIN_ADC_CTRL, adc_active_state); diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 69e4ed05..a70a93a5 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -179,10 +179,6 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_v3_companion_radio_ble_ps] -extends = env:Heltec_v3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_v3_companion_radio_wifi] extends = Heltec_lora32_v3 build_flags = @@ -324,10 +320,6 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Heltec_WSL3_companion_radio_ble_ps] -extends = env:Heltec_WSL3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_WSL3_companion_radio_usb] extends = Heltec_lora32_v3 build_flags = diff --git a/variants/heltec_v4/HeltecV4Board.h b/variants/heltec_v4/HeltecV4Board.h index 55166bb3..fc37b9f6 100644 --- a/variants/heltec_v4/HeltecV4Board.h +++ b/variants/heltec_v4/HeltecV4Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "LoRaFEMControl.h" #ifndef ADC_MULTIPLIER @@ -22,6 +23,7 @@ public: void begin(); void onBeforeTransmit(void) override; void onAfterTransmit(void) override; + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); void powerOff() override; bool setLoRaFemLnaEnabled(bool enable) override; bool canControlLoRaFemLna() const override; diff --git a/variants/heltec_v4/LoRaFEMControl.h b/variants/heltec_v4/LoRaFEMControl.h index d84ebe9c..d312762d 100644 --- a/variants/heltec_v4/LoRaFEMControl.h +++ b/variants/heltec_v4/LoRaFEMControl.h @@ -24,7 +24,7 @@ class LoRaFEMControl LoRaFEMType getFEMType(void) const { return fem_type; } private: LoRaFEMType fem_type=OTHER_FEM_TYPES; - bool lna_enabled=true; + bool lna_enabled=false; bool lna_can_control=false; }; diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index caa71a33..fabf3827 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -222,16 +222,6 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_ble_ps_femon] -extends = env:heltec_v4_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - -[env:heltec_v4_3_companion_radio_ble_ps_femoff] -extends = env:heltec_v4_companion_radio_ble_ps_femon -build_flags = - ${env:heltec_v4_companion_radio_ble_ps_femon.build_flags} - -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off - [env:heltec_v4_companion_radio_wifi] extends = heltec_v4_oled build_flags = @@ -396,14 +386,6 @@ lib_deps = ${heltec_v4_tft.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_expansionkit_tft_companion_radio_ble_ps] -extends = env:heltec_v4_tft_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 -build_flags = - ${env:heltec_v4_tft_companion_radio_ble.build_flags} - -D ENV_PIN_SDA=4 - -D ENV_PIN_SCL=3 - [env:heltec_v4_tft_companion_radio_wifi] extends = heltec_v4_tft build_flags = diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index e7a107a5..48723d16 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -64,10 +64,6 @@ lib_deps = densaugeo/base64 @ ~1.4.0 bakercp/CRC32 @ ^2.0.0 -[env:Heltec_Wireless_Paper_companion_radio_ble_ps] -extends = env:Heltec_Wireless_Paper_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Heltec_Wireless_Paper_companion_radio_usb] extends = Heltec_Wireless_Paper_base build_flags = diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index e7f9f11f..54990117 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -174,10 +174,6 @@ lib_deps = ${LilyGo_T3S3_sx1262.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:LilyGo_T3S3_sx1262_companion_radio_ble_ps] -extends = env:LilyGo_T3S3_sx1262_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:LilyGo_T3S3_sx1262_kiss_modem] extends = LilyGo_T3S3_sx1262 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 4516e4cd..c7a59552 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -146,11 +146,6 @@ lib_deps = ${LilyGo_TBeam_1W.lib_deps} densaugeo/base64 @ ~1.4.0 -; === LILYGO T-Beam 1W Companion Radio PS (BLE PS) === -[env:LilyGo_TBeam_1W_companion_radio_ble_ps] -extends = env:LilyGo_TBeam_1W_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - ; === LILYGO T-Beam 1W Companion Radio (WiFi) === [env:LilyGo_TBeam_1W_companion_radio_wifi] extends = LilyGo_TBeam_1W diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 5bc0efdd..62ac09f8 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -65,13 +65,6 @@ lib_deps = ${LilyGo_TBeam_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Tbeam_SX1262_companion_radio_ble_ps] -extends = env:Tbeam_SX1262_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 -build_flags = - ${env:Tbeam_SX1262_companion_radio_ble.build_flags} - -D MAX_CONTACTS=150 - [env:Tbeam_SX1262_repeater] extends = LilyGo_TBeam_SX1262 build_flags = diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index edf84e21..cb25903c 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -61,10 +61,6 @@ lib_deps = ${LilyGo_TBeam_SX1276.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Tbeam_SX1276_companion_radio_ble_ps] -extends = env:Tbeam_SX1276_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Tbeam_SX1276_repeater] extends = LilyGo_TBeam_SX1276 build_flags = diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index b930a25b..02946156 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -142,10 +142,6 @@ lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps] -extends = env:T_Beam_S3_Supreme_SX1262_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:T_Beam_S3_Supreme_SX1262_companion_radio_wifi] extends = T_Beam_S3_Supreme_SX1262 build_flags = diff --git a/variants/lilygo_tdeck/TDeckBoard.h b/variants/lilygo_tdeck/TDeckBoard.h index e2844360..7ed007af 100644 --- a/variants/lilygo_tdeck/TDeckBoard.h +++ b/variants/lilygo_tdeck/TDeckBoard.h @@ -3,6 +3,7 @@ #include #include #include "helpers/ESP32Board.h" +#include #define PIN_VBAT_READ 4 #define BATTERY_SAMPLES 8 @@ -22,6 +23,29 @@ public: } #endif + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint16_t getBattMilliVolts() { #if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER) analogReadResolution(12); diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index bb173524..36731668 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -92,7 +92,7 @@ extends = LilyGo_TLora_V2_1_1_6 build_flags = ${LilyGo_TLora_V2_1_1_6.build_flags} -I examples/companion_radio/ui-new - -D MAX_CONTACTS=100 + -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=128 @@ -108,10 +108,6 @@ lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps] -extends = env:LilyGo_TLora_V2_1_1_6_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:LilyGo_TLora_V2_1_1_6_room_server] extends = LilyGo_TLora_V2_1_1_6 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 1e1a3274..5415e158 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -1,7 +1,6 @@ [Promicro] extends = nrf52_base board = promicro_nrf52840 -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} -I variants/promicro -D PROMICRO diff --git a/variants/rak3112/RAK3112Board.h b/variants/rak3112/RAK3112Board.h index 704162b8..8ba3197c 100644 --- a/variants/rak3112/RAK3112Board.h +++ b/variants/rak3112/RAK3112Board.h @@ -16,6 +16,8 @@ #define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000) #define BATTERY_SAMPLES 8 +#include + class RAK3112Board : public ESP32Board { private: bool adc_active_state; @@ -49,6 +51,33 @@ public: } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 5eeb6a9d..20a8a548 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -2,12 +2,11 @@ extends = nrf52_base board = rak3401 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 8f72999a..2bbba314 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -2,7 +2,6 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld extra_scripts = ${nrf52_base.extra_scripts} post:variants/rak4631/fix_bsec_lib.py build_flags = ${nrf52_base.build_flags} @@ -10,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_GPS_TX=PIN_SERIAL1_RX diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index b596c9a6..e9cddb74 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -2,7 +2,6 @@ extends = nrf52_base board = rak4631 board_check = true -board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak_wismesh_tag diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index c9f7ed39..effef38c 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=12 diff --git a/variants/station_g2/StationG2Board.h b/variants/station_g2/StationG2Board.h index d1989ee0..a905682c 100644 --- a/variants/station_g2/StationG2Board.h +++ b/variants/station_g2/StationG2Board.h @@ -2,6 +2,7 @@ #include #include +#include class StationG2Board : public ESP32Board { public: @@ -20,6 +21,29 @@ public: } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + uint16_t getBattMilliVolts() override { return 0; } diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 87e77152..6432b523 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -238,3 +238,8 @@ build_src_filter = ${Station_G2.build_src_filter} lib_deps = ${Station_G2.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:Station_G2_kiss_modem] +extends = Station_G2 +build_src_filter = ${Station_G2.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/station_g2/target.cpp b/variants/station_g2/target.cpp index 40fa8005..8018c20e 100644 --- a/variants/station_g2/target.cpp +++ b/variants/station_g2/target.cpp @@ -40,21 +40,6 @@ bool radio_init() { #endif } -uint32_t radio_get_rng_seed() { - return radio.random(0x7FFFFFFF); -} - -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { - radio.setFrequency(freq); - radio.setSpreadingFactor(sf); - radio.setBandwidth(bw); - radio.setCodingRate(cr); -} - -void radio_set_tx_power(int8_t dbm) { - radio.setOutputPower(dbm); -} - mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity diff --git a/variants/station_g2/target.h b/variants/station_g2/target.h index 9a361025..b4eadbba 100644 --- a/variants/station_g2/target.h +++ b/variants/station_g2/target.h @@ -24,8 +24,5 @@ extern EnvironmentSensorManager sensors; #endif bool radio_init(); -uint32_t radio_get_rng_seed(); -void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); -void radio_set_tx_power(int8_t dbm); mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m2/ThinknodeM2Board.cpp b/variants/thinknode_m2/ThinknodeM2Board.cpp index 8d68006d..05965103 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.cpp +++ b/variants/thinknode_m2/ThinknodeM2Board.cpp @@ -1,30 +1,40 @@ #include "ThinknodeM2Board.h" + + void ThinknodeM2Board::begin() { - pinMode(PIN_VEXT_EN, OUTPUT); - digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle - delay(20); // allow power rail to discharge - digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on - delay(120); // give display time to bias on cold boot - ESP32Board::begin(); - pinMode(PIN_STATUS_LED, OUTPUT); // init power led -} - -uint16_t ThinknodeM2Board::getBattMilliVolts() { - analogReadResolution(12); - analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); - - uint32_t mv = 0; - for (int i = 0; i < 8; ++i) { - mv += analogReadMilliVolts(PIN_VBAT_READ); - delayMicroseconds(200); + pinMode(PIN_VEXT_EN, OUTPUT); + digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // force power cycle + delay(20); // allow power rail to discharge + digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // turn backlight back on + delay(120); // give display time to bias on cold boot + ESP32Board::begin(); + pinMode(PIN_STATUS_LED, OUTPUT); // init power led } - mv /= 8; - analogReadResolution(10); - return static_cast(mv * ADC_MULTIPLIER); + void ThinknodeM2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_deep_sleep_start(); + } + + void ThinknodeM2Board::powerOff() { + enterDeepSleep(0); + } + + uint16_t ThinknodeM2Board::getBattMilliVolts() { + analogReadResolution(12); + analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); + + uint32_t mv = 0; + for (int i = 0; i < 8; ++i) { + mv += analogReadMilliVolts(PIN_VBAT_READ); + delayMicroseconds(200); + } + mv /= 8; + + analogReadResolution(10); + return static_cast(mv * ADC_MULTIPLIER ); } -const char *ThinknodeM2Board::getManufacturerName() const { - return "Elecrow ThinkNode M2"; -} + const char* ThinknodeM2Board::getManufacturerName() const { + return "Elecrow ThinkNode M2"; + } diff --git a/variants/thinknode_m2/ThinknodeM2Board.h b/variants/thinknode_m2/ThinknodeM2Board.h index 02556777..8011fae6 100644 --- a/variants/thinknode_m2/ThinknodeM2Board.h +++ b/variants/thinknode_m2/ThinknodeM2Board.h @@ -3,11 +3,15 @@ #include #include #include +#include class ThinknodeM2Board : public ESP32Board { public: + void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/thinknode_m5/ThinknodeM5Board.cpp b/variants/thinknode_m5/ThinknodeM5Board.cpp index 2cb138e6..c4de538c 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.cpp +++ b/variants/thinknode_m5/ThinknodeM5Board.cpp @@ -19,6 +19,14 @@ void ThinknodeM5Board::begin() { ESP32Board::begin(); } + void ThinknodeM5Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_deep_sleep_start(); + } + + void ThinknodeM5Board::powerOff() { + enterDeepSleep(0); + } + uint16_t ThinknodeM5Board::getBattMilliVolts() { analogReadResolution(12); analogSetPinAttenuation(PIN_VBAT_READ, ADC_11db); diff --git a/variants/thinknode_m5/ThinknodeM5Board.h b/variants/thinknode_m5/ThinknodeM5Board.h index 57ff0d00..3c120027 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.h +++ b/variants/thinknode_m5/ThinknodeM5Board.h @@ -3,6 +3,7 @@ #include #include #include +#include #include extern PCA9557 expander; @@ -12,6 +13,8 @@ class ThinknodeM5Board : public ESP32Board { public: void begin(); + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; diff --git a/variants/xiao_c3/XiaoC3Board.h b/variants/xiao_c3/XiaoC3Board.h index c5700c68..6ea1c15f 100644 --- a/variants/xiao_c3/XiaoC3Board.h +++ b/variants/xiao_c3/XiaoC3Board.h @@ -3,6 +3,7 @@ #include #include +#include #include class XiaoC3Board : public ESP32Board { @@ -39,6 +40,37 @@ public: #endif } + void enterDeepSleep(uint32_t secs, int8_t wake_pin = -1) { + gpio_set_direction(gpio_num_t(P_LORA_DIO_1), GPIO_MODE_INPUT); + if (wake_pin >= 0) { + gpio_set_direction((gpio_num_t)wake_pin, GPIO_MODE_INPUT); + } + + //hold disable, isolate and power domain config functions may be unnecessary + //gpio_deep_sleep_hold_dis(); + //esp_sleep_config_gpio_isolate(); + gpio_deep_sleep_hold_en(); + +#if defined(LORA_TX_BOOST_PIN) + gpio_hold_en((gpio_num_t) LORA_TX_BOOST_PIN); + gpio_deep_sleep_hold_en(); +#endif + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + if (wake_pin >= 0) { + esp_deep_sleep_enable_gpio_wakeup((1 << P_LORA_DIO_1) | (1 << wake_pin), ESP_GPIO_WAKEUP_GPIO_HIGH); + } else { + esp_deep_sleep_enable_gpio_wakeup(1 << P_LORA_DIO_1, ESP_GPIO_WAKEUP_GPIO_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + #if defined(LORA_TX_BOOST_PIN) || defined(P_LORA_TX_LED) void onBeforeTransmit() override { #if defined(P_LORA_TX_LED) diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index fa70d8a0..c0e8458d 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -90,11 +90,6 @@ lib_deps = ${esp32_ota.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Xiao_C3_companion_radio_ble_ps] -extends = env:Xiao_C3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 -board_build.partitions = min_spiffs.csv ; get around 4mb flash limit - [env:Xiao_C3_companion_radio_usb] extends = Xiao_esp32_C3 build_src_filter = ${Xiao_esp32_C3.build_src_filter} diff --git a/variants/xiao_c6/platformio.ini b/variants/xiao_c6/platformio.ini index 0d8c2a79..9f504b8e 100644 --- a/variants/xiao_c6/platformio.ini +++ b/variants/xiao_c6/platformio.ini @@ -1,7 +1,6 @@ [Xiao_C6] extends = esp32c6_base board = esp32-c6-devkitm-1 -board_build.flash_mode = dio board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32c6_base.build_flags} diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index f4d1b93e..a0854336 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index b59e69ae..22464e7d 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -114,10 +114,6 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 -[env:Xiao_S3_companion_radio_ble_ps] -extends = env:Xiao_S3_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Xiao_S3_companion_radio_usb] extends = Xiao_S3 build_flags = diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 41420acf..db8c5a94 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -173,10 +173,6 @@ lib_deps = densaugeo/base64 @ ~1.4.0 adafruit/Adafruit SSD1306 @ ^2.5.13 -[env:Xiao_S3_WIO_companion_radio_ble_ps] -extends = env:Xiao_S3_WIO_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 - [env:Xiao_S3_WIO_companion_radio_serial] extends = Xiao_S3_WIO build_flags = From 2acd61ec84988bbbba006d8fc7ae747f852c368f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 16:02:22 -0700 Subject: [PATCH 11/25] Remove PowerSaving changes from Halo retry branch --- .github/workflows/stale-bot.yml | 32 ----- SECURITY.md | 57 -------- build-iotthinks.sh | 135 ------------------ examples/companion_radio/DataStore.cpp | 2 - examples/companion_radio/MyMesh.cpp | 29 ---- examples/companion_radio/MyMesh.h | 2 +- examples/companion_radio/NodePrefs.h | 3 +- examples/companion_radio/main.cpp | 40 ------ examples/simple_room_server/MyMesh.cpp | 8 -- examples/simple_room_server/MyMesh.h | 3 - examples/simple_room_server/main.cpp | 13 -- src/MeshCore.h | 4 + src/helpers/ArduinoHelpers.cpp | 6 - src/helpers/ArduinoHelpers.h | 42 +----- src/helpers/ArduinoSerialInterface.cpp | 4 - src/helpers/ArduinoSerialInterface.h | 3 +- src/helpers/BaseChatMesh.h | 10 +- src/helpers/BaseSerialInterface.h | 1 - src/helpers/ESP32Board.h | 106 ++------------ src/helpers/MeshadventurerBoard.h | 30 ++++ src/helpers/NRF52Board.cpp | 28 ++-- src/helpers/NRF52Board.h | 4 +- src/helpers/SensorManager.h | 2 - src/helpers/esp32/SerialBLEInterface.cpp | 4 - src/helpers/esp32/SerialBLEInterface.h | 1 - src/helpers/esp32/SerialWifiInterface.cpp | 4 - src/helpers/esp32/SerialWifiInterface.h | 1 - src/helpers/esp32/TBeamBoard.h | 24 ++++ src/helpers/nrf52/SerialBLEInterface.cpp | 4 - src/helpers/nrf52/SerialBLEInterface.h | 1 - .../sensors/EnvironmentSensorManager.cpp | 16 --- .../sensors/EnvironmentSensorManager.h | 1 - src/helpers/ui/ST7735Display.cpp | 9 -- 33 files changed, 93 insertions(+), 536 deletions(-) delete mode 100644 .github/workflows/stale-bot.yml delete mode 100644 SECURITY.md delete mode 100644 build-iotthinks.sh delete mode 100644 src/helpers/ArduinoHelpers.cpp diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml deleted file mode 100644 index ec166587..00000000 --- a/.github/workflows/stale-bot.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: 'Run Stale Bot' -on: - schedule: - - cron: '30 1 * * *' # daily at 1:30am - workflow_dispatch: {} - -permissions: - actions: write - issues: write - pull-requests: write - -jobs: - close-issues: - # only run on main repo, not forks - if: github.repository == 'meshcore-dev/MeshCore' - runs-on: ubuntu-latest - steps: - - name: Close Stale Issues - uses: actions/stale@v10 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # auto close issues - days-before-issue-stale: 60 - days-before-issue-close: 7 - exempt-issue-labels: "keep-open" - stale-issue-label: "stale" - stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days." - close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." - # don't auto close prs - days-before-pr-stale: -1 - days-before-pr-close: -1 - \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index a4b2207d..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,57 +0,0 @@ -# Security Policy - -## Supported Versions - -Security fixes are applied to the latest release only. We do not backport -fixes to older versions. - -| Version | Supported | -|---------|-----------| -| 1.15+ | ✅ | -| <1.15 | ❌ | - -## Reporting a Vulnerability - -**Please do not report security vulnerabilities through public GitHub issues.** - -Use GitHub's private vulnerability reporting instead: -1. Go to the **Security** tab of this repository -2. Click **Report a vulnerability** -3. Fill in the details and submit - -### What to include - -A useful report tells us: -- Which component or file is affected -- What an attacker can do (impact) and under what conditions -- A minimal reproduction case or proof-of-concept if you have one -- Whether you believe it is remotely exploitable - -You do not need a working exploit to report. An incomplete report is better -than no report. - -## What to expect - -This is a volunteer-maintained open-source project. We will do our best to -respond in a reasonable timeframe, but cannot commit to specific deadlines. - -We ask that you give us a fair opportunity to investigate and address the -issue before any public disclosure. If you have not heard back after -**90 days**, feel free to follow up or proceed with disclosure at your -discretion. - -## Scope - -In scope: -- Remote code execution, memory corruption, or denial-of-service via crafted - radio packets -- Authentication or encryption bypasses -- Vulnerabilities in the packet routing or path handling logic - -Out of scope: -- Physical access attacks (e.g., JTAG, UART extraction of keys) -- Regulatory compliance (duty cycle, frequency restrictions) -- Jamming or other physical-layer radio interference -- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those - upstream -- "Best practice" suggestions without a demonstrated attack path diff --git a/build-iotthinks.sh b/build-iotthinks.sh deleted file mode 100644 index 613789d0..00000000 --- a/build-iotthinks.sh +++ /dev/null @@ -1,135 +0,0 @@ -# sh ./build-repeaters-iotthinks.sh -export FIRMWARE_VERSION="PowerSaving16" - -############# Repeaters ############# -# Commonly-used boards -## ESP32 - 17 boards -sh build.sh build-firmware \ -Heltec_v3_repeater \ -Heltec_WSL3_repeater \ -heltec_v4_repeater \ -Station_G2_repeater \ -T_Beam_S3_Supreme_SX1262_repeater \ -Tbeam_SX1262_repeater \ -LilyGo_T3S3_sx1262_repeater \ -Xiao_S3_WIO_repeater \ -Xiao_C3_repeater \ -Xiao_C6_repeater_ \ -Heltec_E290_repeater \ -Heltec_Wireless_Tracker_repeater \ -LilyGo_TBeam_1W_repeater \ -Xiao_S3_repeater \ -heltec_tracker_v2_repeater \ -Heltec_Wireless_Paper_repeater \ -Heltec_ct62_repeater - -## NRF52 - 17 boards -sh build.sh build-firmware \ -RAK_4631_repeater \ -Heltec_t114_repeater \ -Xiao_nrf52_repeater \ -Heltec_mesh_solar_repeater \ -ProMicro_repeater \ -SenseCap_Solar_repeater \ -t1000e_repeater \ -LilyGo_T-Echo_repeater \ -WioTrackerL1_repeater \ -RAK_3401_repeater \ -RAK_WisMesh_Tag_repeater \ -GAT562_30S_Mesh_Kit_repeater \ -GAT562_Mesh_Tracker_Pro_repeater \ -ikoka_nano_nrf_22dbm_repeater \ -ikoka_nano_nrf_30dbm_repeater \ -ikoka_nano_nrf_33dbm_repeater \ -ThinkNode_M1_repeater \ -Heltec_t096_repeater - -## ESP32, SX1276 - 3 boards -sh build.sh build-firmware \ -Heltec_v2_repeater \ -LilyGo_TLora_V2_1_1_6_repeater \ -Tbeam_SX1276_repeater - -############# Room Server ############# -# ESP32 - 7 boards -sh build.sh build-firmware \ -Heltec_v3_room_server \ -heltec_v4_room_server \ -LilyGo_TBeam_1W_room_server \ -Heltec_WSL3_room_server \ -Xiao_S3_room_server \ -heltec_tracker_v2_room_server \ -Heltec_Wireless_Paper_room_server - -# NRF52 - 6 boards -sh build.sh build-firmware \ -RAK_4631_room_server \ -Heltec_t114_room_server \ -Xiao_nrf52_room_server \ -t1000e_room_server \ -WioTrackerL1_room_server \ -RAK_3401_room_server \ -Heltec_t096_room_server - -############# Companions BLE ############# -# NRF52 - 12 boards -sh build.sh build-firmware \ -RAK_4631_companion_radio_ble \ -Heltec_t114_companion_radio_ble \ -Xiao_nrf52_companion_radio_ble \ -t1000e_companion_radio_ble \ -LilyGo_T-Echo_companion_radio_ble \ -WioTrackerL1_companion_radio_ble \ -RAK_3401_companion_radio_ble \ -RAK_WisMesh_Tag_companion_radio_ble \ -SenseCap_Solar_companion_radio_ble \ -ThinkNode_M1_companion_radio_ble \ -Heltec_t096_companion_radio_ble \ -Heltec_t096_companion_radio_ble_femoff - -############# Companions BLE PS ############# -# ESP32 - 18 boards -sh build.sh build-firmware \ -Heltec_v3_companion_radio_ble_ps \ -heltec_v4_companion_radio_ble_ps \ -heltec_v4_3_companion_radio_ble_ps_femoff \ -Xiao_C3_companion_radio_ble_ps \ -Xiao_S3_companion_radio_ble_ps \ -Xiao_S3_WIO_companion_radio_ble_ps \ -Heltec_v2_companion_radio_ble_ps \ -LilyGo_TBeam_1W_companion_radio_ble_ps \ -Heltec_WSL3_companion_radio_ble_ps \ -Heltec_Wireless_Tracker_companion_radio_ble_ps \ -heltec_tracker_v2_companion_radio_ble_ps \ -Heltec_Wireless_Paper_companion_radio_ble_ps \ -LilyGo_TLora_V2_1_1_6_companion_radio_ble_ps \ -Heltec_ct62_companion_radio_ble_ps \ -T_Beam_S3_Supreme_SX1262_companion_radio_ble_ps \ -Tbeam_SX1262_companion_radio_ble_ps \ -heltec_v4_expansionkit_tft_companion_radio_ble_ps \ -LilyGo_T3S3_sx1262_companion_radio_ble_ps - -# Not working -Tbeam_SX1276_companion_radio_ble_ps \ - -############# Companions USB ############# -sh build.sh build-firmware \ -Heltec_t096_companion_radio_usb - -############# Sample builds ############# -# 14 boards -sh build.sh build-firmware \ -Heltec_v3_repeater \ -heltec_v4_repeater \ -Xiao_C3_repeater \ -Xiao_C6_repeater_ \ -RAK_4631_repeater \ -Heltec_t096_repeater \ -Heltec_v3_companion_radio_ble_ps \ -heltec_v4_companion_radio_ble_ps \ -heltec_v4_3_companion_radio_ble_ps_femoff \ -Xiao_C3_companion_radio_ble_ps \ -Xiao_C6_companion_radio_ble_ \ -RAK_4631_companion_radio_ble \ -Heltec_t096_companion_radio_ble \ -Heltec_t096_companion_radio_ble_femoff diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index fdb924ad..bf2f36c3 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -233,7 +233,6 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.read((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } @@ -274,7 +273,6 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5d1adb46..5fb9bf9d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -62,8 +62,6 @@ #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 #define CMD_GET_DEFAULT_FLOOD_SCOPE 64 #define CMD_SEND_RAW_PACKET 65 -#define CMD_GET_RADIO_FEM_RXGAIN 66 -#define CMD_SET_RADIO_FEM_RXGAIN 67 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -891,7 +889,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_boosted_gain = 1; // enabled by default #endif #endif - _prefs.radio_fem_rxgain = 1; } void MyMesh::begin(bool has_display) { @@ -941,7 +938,6 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours - _prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1); #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -971,7 +967,6 @@ void MyMesh::begin(bool has_display) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); - board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } @@ -1830,30 +1825,6 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } - } else if (cmd_frame[0] == CMD_GET_RADIO_FEM_RXGAIN) { - if (!board.canControlLoRaFemLna()) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else { - out_frame[0] = RESP_CODE_OK; - uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0; - memcpy(&out_frame[1], &value, 1); - _serial->writeFrame(out_frame, 2); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) { - uint8_t value = cmd_frame[1]; - if (!board.canControlLoRaFemLna()) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else if (value <= 1) { - _prefs.radio_fem_rxgain = value; - if (board.setLoRaFemLnaEnabled(value != 0)) { - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 0d687cfd..f4190f30 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -166,7 +166,7 @@ protected: public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } - + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ecb117bd..48c381ce 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,10 +29,9 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) - uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; -}; +}; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index f10cb170..ef9b6bfc 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,11 +2,6 @@ #include #include "MyMesh.h" -#ifdef ESP32_PLATFORM -#include "esp_pm.h" -#include "esp_bt.h" -#endif - // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -245,37 +240,6 @@ void setup() { #endif board.onBootComplete(); - -#ifdef ESP32_PLATFORM -#if !CONFIG_IDF_TARGET_ESP32C6 - // Enable BLE sleep - esp_err_t errBLESleep = esp_bt_sleep_enable(); - if (errBLESleep == ESP_OK) { - Serial.println("Bluetooth sleep enabled successfully"); - } else { - Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep)); - } -#endif - -#if CONFIG_IDF_TARGET_ESP32C3 - esp_pm_config_esp32c3_t pm_config; -#elif CONFIG_IDF_TARGET_ESP32S3 - esp_pm_config_esp32s3_t pm_config; -#elif CONFIG_IDF_TARGET_ESP32 - esp_pm_config_esp32_t pm_config; -#elif CONFIG_IDF_TARGET_ESP32C6 - esp_pm_config_t pm_config; -#endif - - // Configure Power Management - pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true }; - esp_err_t errPM = esp_pm_configure(&pm_config); - if (errPM == ESP_OK) { - Serial.println("Power Management configured successfully"); - } else { - Serial.printf("Power Management failed to configure: %d\r\n", errPM); - } -#endif } void loop() { @@ -289,10 +253,6 @@ void loop() { if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible -#elif defined(ESP32_PLATFORM) - if (!serial_interface.isReadBusy() && !serial_interface.isWriteBusy()) { // BLE is not busy - vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep - } #endif } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index c311c941..12d0b0c3 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1031,11 +1031,3 @@ void MyMesh::loop() { uptime_millis += now - last_millis; last_millis = now; } - -// To check if there is pending work -bool MyMesh::hasPendingWork() const { -#if defined(WITH_BRIDGE) - if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep -#endif - return _mgr->getOutboundTotal() > 0; -} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 380e54da..e9e53ec9 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -225,7 +225,4 @@ public: void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); - - // To check if there is pending work - bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index ad8aa914..a3798b21 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -18,9 +18,6 @@ void halt() { static char command[MAX_POST_TEXT_LEN+1]; -// For power saving -unsigned long POWERSAVING_FIRSTSLEEP_SECS = 120; // The first sleep (if enabled) from boot - void setup() { Serial.begin(115200); delay(1000); @@ -118,14 +115,4 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); - - if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { -#if defined(NRF52_PLATFORM) - board.sleep(0); // nrf ignores seconds param, sleeps whenever possible -#else - if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep - board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet - } -#endif - } } diff --git a/src/MeshCore.h b/src/MeshCore.h index 846b8f05..89e60b1f 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -53,6 +53,10 @@ public: virtual void onAfterTransmit() { } virtual void reboot() = 0; virtual void powerOff() { /* no op */ } + // Called by example setup() functions to signal that boot is complete. + // Boards may override to stop a boot-indicator LED sequence or similar. + // Default no-op: boards that don't care need not implement anything. + virtual void onBootComplete() { /* no op */ } virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x) virtual void sleep(uint32_t secs) { /* no op */ } virtual uint32_t getGpio() { return 0; } diff --git a/src/helpers/ArduinoHelpers.cpp b/src/helpers/ArduinoHelpers.cpp deleted file mode 100644 index feb77a79..00000000 --- a/src/helpers/ArduinoHelpers.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -extern "C" { - __attribute__((section(".persistent_magic"))) uint32_t persistent_magic; - __attribute__((section(".persistent_data"))) uint32_t persistent_time; -} \ No newline at end of file diff --git a/src/helpers/ArduinoHelpers.h b/src/helpers/ArduinoHelpers.h index 9b50b98c..97596daa 100644 --- a/src/helpers/ArduinoHelpers.h +++ b/src/helpers/ArduinoHelpers.h @@ -3,57 +3,19 @@ #include #include -#ifdef NRF52_PLATFORM -#define CLOCK_MAGIC_NUM 0xAA55CC33 -#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 - -extern uint32_t persistent_magic; -extern uint32_t persistent_time; -#endif - class VolatileRTCClock : public mesh::RTCClock { uint32_t base_time; uint64_t accumulator; unsigned long prev_millis; - public: - VolatileRTCClock() { -#ifdef NRF52_PLATFORM - if (persistent_magic == CLOCK_MAGIC_NUM && persistent_time >= RTC_TIME_MIN) { - base_time = persistent_time; - } else { - base_time = RTC_TIME_MIN; - } -#else - base_time = 1715770351; -#endif - - accumulator = 0; - prev_millis = millis(); - } - + VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); } // 15 May 2024, 8:50pm uint32_t getCurrentTime() override { return base_time + accumulator/1000; } - - void setCurrentTime(uint32_t time) override { - base_time = time; - accumulator = 0; - prev_millis = millis(); - -#ifdef NRF52_PLATFORM - persistent_magic = CLOCK_MAGIC_NUM; - persistent_time = time; -#endif - } + void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); } void tick() override { unsigned long now = millis(); accumulator += (now - prev_millis); prev_millis = now; - -#ifdef NRF52_PLATFORM - persistent_magic = CLOCK_MAGIC_NUM; - persistent_time = getCurrentTime(); -#endif } }; diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index 6b443974..a01fa586 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,10 +17,6 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } -bool ArduinoSerialInterface::isReadBusy() const { - return false; -} - bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index 4fa2b75d..c4086353 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,8 +28,7 @@ public: bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; -}; +}; \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index c6027468..d9878547 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -38,6 +38,8 @@ public: #define MAX_CONTACTS 32 #endif +#define MAX_ANON_CONTACTS 8 + #ifndef MAX_CONNECTIONS #define MAX_CONNECTIONS 16 #endif @@ -59,9 +61,9 @@ class BaseChatMesh : public mesh::Mesh { friend class ContactsIterator; - ContactInfo contacts[MAX_CONTACTS]; + ContactInfo contacts[MAX_CONTACTS+MAX_ANON_CONTACTS]; int num_contacts; - int sort_array[MAX_CONTACTS]; + int sort_array[MAX_CONTACTS+MAX_ANON_CONTACTS]; int matching_peer_indexes[MAX_SEARCH_RESULTS]; unsigned long txt_send_timeout; #ifdef MAX_GROUP_CHANNELS @@ -73,7 +75,7 @@ class BaseChatMesh : public mesh::Mesh { ConnectionInfo connections[MAX_CONNECTIONS]; mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack); - void sendAckTo(const ContactInfo& dest, uint32_t ack_hash); + void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4); protected: BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) @@ -97,7 +99,7 @@ protected: num_contacts = MAX_ANON_CONTACTS; // seed the first contacts for anon requests } void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp); - ContactInfo* allocateContactSlot(); // helper to find slot for new contact + ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact // 'UI' concepts, for sub-classes to implement virtual bool isAutoAddEnabled() const { return true; } diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index 8ff110eb..e9a3f2ab 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -15,7 +15,6 @@ public: virtual bool isConnected() const = 0; - virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 48730fdd..a4cbf2a9 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -14,7 +14,6 @@ #include #include "soc/rtc.h" #include "esp_system.h" -#include class ESP32Board : public mesh::MainBoard { protected: @@ -25,7 +24,7 @@ protected: public: void begin() { // for future use, sub-classes SHOULD call this from their begin() - startup_reason = BD_STARTUP_NORMAL; + startup_reason = BD_STARTUP_NORMAL; #ifdef ESP32_CPU_FREQ setCpuFrequencyMhz(ESP32_CPU_FREQ); @@ -48,7 +47,7 @@ public: #endif #else Wire.begin(); - #endif + #endif } // Temperature from ESP32 MCU @@ -63,31 +62,6 @@ public: return raw / 4; } - void powerOff() override { - enterDeepSleep(0); // Do not wakeup - } - - void enterDeepSleep(uint32_t secs) { - // Clear stale wakeup sources to avoid ghost wakeup - // This is required when Power Management and automatic lightsleep are enabled - esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000ULL); - } - - // Keep LoRa inactive during deepsleep - digitalWrite(P_LORA_NSS, HIGH); -#if CONFIG_IDF_TARGET_ESP32C3 - gpio_hold_en((gpio_num_t)P_LORA_NSS); -#else - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); -#endif - - // Finally set ESP32 into deepsleep - esp_deep_sleep_start(); // CPU halts here and never returns! - } - uint32_t getIRQGpio() override { return P_LORA_DIO_1; // default for SX1262 } @@ -99,15 +73,8 @@ public: return; } - #if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT - if (Serial) { - delay(1); - return; - } - #endif - // Set GPIO wakeup - gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); + gpio_num_t wakeupPin = (gpio_num_t)getIRQGpio(); // Configure timer wakeup if (secs > 0) { @@ -188,68 +155,21 @@ public: void setInhibitSleep(bool inhibit) { inhibit_sleep = inhibit; } - - uint32_t getResetReason() const override { - return esp_reset_reason(); - } - - // https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html - const char *getResetReasonString(uint32_t reason) { - switch (reason) { - case ESP_RST_UNKNOWN: - return "Unknown or first boot"; - case ESP_RST_POWERON: - return "Power-on reset"; - case ESP_RST_EXT: - return "External reset"; - case ESP_RST_SW: - return "Software reset"; - case ESP_RST_PANIC: - return "Panic / exception reset"; - case ESP_RST_INT_WDT: - return "Interrupt watchdog reset"; - case ESP_RST_TASK_WDT: - return "Task watchdog reset"; - case ESP_RST_WDT: - return "Other watchdog reset"; - case ESP_RST_DEEPSLEEP: - return "Wake from deep sleep"; - case ESP_RST_BROWNOUT: - return "Brownout (low voltage)"; - case ESP_RST_SDIO: - return "SDIO reset"; - default: - static char buf[40]; - snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason); - return buf; - } - } }; -static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; -static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; -#define RTC_BACKUP_MAGIC 0xAA55CC33 -#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 - class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_DEEPSLEEP) { - return; // ESP-IDF preserves system time across deep sleep - } - // All other resets (power-on, crash, WDT, brownout) lose system time. - // Restore from RTC backup if valid, otherwise use hardcoded seed. - struct timeval tv; - if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { - tv.tv_sec = _rtc_backup_time; - } else { - tv.tv_sec = 1772323200; // 1 Mar 2026 - } + if (reason == ESP_RST_POWERON) { + // start with some date/time in the recent past + struct timeval tv; + tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm tv.tv_usec = 0; settimeofday(&tv, NULL); } + } uint32_t getCurrentTime() override { time_t _now; time(&_now); @@ -260,16 +180,6 @@ public: tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); - _rtc_backup_time = time; - _rtc_backup_magic = RTC_BACKUP_MAGIC; - } - void tick() override { - time_t now; - time(&now); - if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { - _rtc_backup_time = (uint32_t)now; - _rtc_backup_magic = RTC_BACKUP_MAGIC; - } } }; diff --git a/src/helpers/MeshadventurerBoard.h b/src/helpers/MeshadventurerBoard.h index 0325161d..65e11102 100644 --- a/src/helpers/MeshadventurerBoard.h +++ b/src/helpers/MeshadventurerBoard.h @@ -15,6 +15,8 @@ #include "ESP32Board.h" +#include + class MeshadventurerBoard : public ESP32Board { public: @@ -33,6 +35,34 @@ public: } } + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + void powerOff() override { + // TODO: re-enable this when there is a definite wake-up source pin: + // enterDeepSleep(0); + } + uint16_t getBattMilliVolts() override { analogReadResolution(12); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 2c8753d4..17265f04 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -66,20 +66,6 @@ void NRF52Board::initPowerMgr() { } } -bool NRF52Board::isExternalPowered() { - // Check if SoftDevice is enabled before using its API - uint8_t sd_enabled = 0; - sd_softdevice_is_enabled(&sd_enabled); - - if (sd_enabled) { - uint32_t usb_status; - sd_power_usbregstatus_get(&usb_status); - return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; - } else { - return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; - } -} - const char* NRF52Board::getResetReasonString(uint32_t reason) { if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin"; if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog"; @@ -251,6 +237,20 @@ void NRF52BoardDCDC::begin() { } } +bool NRF52Board::isExternalPowered() { + // Check if SoftDevice is enabled before using its API + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + + if (sd_enabled) { + uint32_t usb_status; + sd_power_usbregstatus_get(&usb_status); + return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; + } else { + return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0; + } +} + void NRF52Board::sleep(uint32_t secs) { // Clear FPU interrupt flags to avoid insomnia // see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 96f67dc9..17065cf4 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -53,9 +53,9 @@ public: virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual void sleep(uint32_t secs) override; + bool isExternalPowered() override; #ifdef NRF52_POWER_MANAGEMENT - bool isExternalPowered() override; uint16_t getBootVoltage() override { return boot_voltage_mv; } virtual uint32_t getResetReason() const override { return reset_reason; } uint8_t getShutdownReason() const override { return shutdown_reason; } @@ -67,7 +67,7 @@ public: /* * The NRF52 has an internal DC/DC regulator that allows increased efficiency * compared to the LDO regulator. For being able to use it, the module/board - * needs to have the required inductors and and capacitors populated. If the + * needs to have the required inductors and capacitors populated. If the * hardware requirements are met, this subclass can be used to enable the DC/DC * regulator. */ diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index d4aa63b7..89a174c2 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -2,7 +2,6 @@ #include #include "sensors/LocationProvider.h" -#include #define TELEM_PERM_BASE 0x01 // 'base' permission includes battery #define TELEM_PERM_LOCATION 0x02 @@ -16,7 +15,6 @@ public: double node_altitude; // altitude in meters SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } - virtual bool i2c_probe(TwoWire& wire, uint8_t addr) { return false; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 50e1501e..dcfa0e1e 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -182,10 +182,6 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 -bool SerialBLEInterface::isReadBusy() const { - return (recv_queue_len > 0); -} - bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 19e024b0..965e90fd 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -76,7 +76,6 @@ public: bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index bdecb1a9..462e3ecc 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -39,10 +39,6 @@ size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } -bool SerialWifiInterface::isReadBusy() const { - return false; -} - bool SerialWifiInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 1ff1d83d..19291497 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -52,7 +52,6 @@ public: bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 543226d6..98bd16bf 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -86,6 +86,7 @@ #include #include "XPowersLib.h" #include "helpers/ESP32Board.h" +#include class TBeamBoard : public ESP32Board { XPowersLibInterface *PMU = NULL; @@ -130,6 +131,29 @@ public: } #endif + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! +} + uint16_t getBattMilliVolts(){ return PMU->getBattVoltage(); } diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index a846e744..75a4e3b0 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -401,10 +401,6 @@ bool SerialBLEInterface::isConnected() const { return _isDeviceConnected && Bluefruit.connected() > 0; } -bool SerialBLEInterface::isReadBusy() const { - return (recv_queue_len > 0); -} - bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index d3cc5055..de103054 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -66,7 +66,6 @@ public: void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; - bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index ea57677f..73842d9e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -15,7 +15,6 @@ #if ENV_INCLUDE_BME680_BSEC #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 -#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -41,7 +40,6 @@ static uint32_t bsec_last_save_ms = 0; #ifdef ENV_INCLUDE_BME680 #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 -#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -65,7 +63,6 @@ static Adafruit_AHTX0 AHTX0; #if ENV_INCLUDE_BME280 #ifndef TELEM_BME280_ADDRESS #define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address -#define TELEM_BME280_ADDRESS_2 0x77 #endif #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -75,7 +72,6 @@ static Adafruit_BME280 BME280; #if ENV_INCLUDE_BMP280 #ifndef TELEM_BMP280_ADDRESS #define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address -#define TELEM_BMP280_ADDRESS_2 0x77 #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -561,27 +557,15 @@ static const SensorDef SENSOR_TABLE[] = { #endif #ifdef ENV_INCLUDE_BME680 { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, - #ifdef TELEM_BME680_ADDRESS_2 - { TELEM_BME680_ADDRESS_2, "BME680", init_bme680, query_bme680 }, - #endif #endif #if ENV_INCLUDE_BME680_BSEC { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, - #ifdef TELEM_BME680_ADDRESS_2 - { TELEM_BME680_ADDRESS_2, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, - #endif #endif #if ENV_INCLUDE_BME280 { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, - #ifdef TELEM_BME280_ADDRESS_2 - { TELEM_BME280_ADDRESS_2, "BME280", init_bme280, query_bme280 }, - #endif #endif #if ENV_INCLUDE_BMP280 { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, - #ifdef TELEM_BMP280_ADDRESS_2 - { TELEM_BMP280_ADDRESS_2, "BMP280", init_bmp280, query_bmp280 }, - #endif #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index b91d2dc3..29147c89 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -41,7 +41,6 @@ public: #else EnvironmentSensorManager(){}; #endif - bool i2c_probe(TwoWire& wire, uint8_t addr) override; bool begin() override; bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; #if ENV_INCLUDE_GPS || defined(ENV_INCLUDE_BME680_BSEC) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 8983c911..a6087dd8 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -63,15 +63,6 @@ void ST7735Display::turnOff() { #else digitalWrite(PIN_TFT_LEDA_CTL, LOW); #endif - - // Prevent back-powering to save 2.8 mA - pinMode(PIN_TFT_CS, INPUT); - pinMode(PIN_TFT_DC, INPUT); - pinMode(PIN_TFT_SDA, INPUT); - pinMode(PIN_TFT_SCL, INPUT); - pinMode(PIN_TFT_RST, INPUT); - pinMode(PIN_TFT_LEDA_CTL, INPUT); - _isOn = false; if (_peripher_power) _peripher_power->release(); From 7d8bb1d319488bf64139d93206b01abfed5ea401 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 16:05:23 -0700 Subject: [PATCH 12/25] Keep Halo branch focused on direct retry --- docs/cli_commands.md | 490 ++++++++---------------------- examples/simple_repeater/MyMesh.h | 39 --- examples/simple_repeater/main.cpp | 5 - src/Mesh.cpp | 308 ------------------- src/Mesh.h | 68 ----- 5 files changed, 127 insertions(+), 783 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d9a225e8..5c5ee670 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -28,25 +28,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `reboot` -**Note:** No reply is sent. - ---- - -### Power-off the node -**Usage:** -- `poweroff`, or -- `shutdown` - -**Note:** No reply is sent. - --- ### Reset the clock and reboot **Usage:** - `clkreboot` -**Note:** No reply is sent. - --- ### Sync the clock with the remote device @@ -128,66 +115,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -### Send flood text to `#repeaters` channel - -**Usage:** -- `send text.flood ` - -**Notes:** -- Sends a `PAYLOAD_TYPE_GRP_TXT` flood message using the built-in `#repeaters` channel key. -- Message format is `: `. - ---- - -### View or change automatic low-battery alerts to `#repeaters` - -**Usage:** -- `get battery.alert` -- `set battery.alert ` -- `get battery.alert.low` -- `set battery.alert.low ` -- `get battery.alert.critical` -- `set battery.alert.critical ` - -**Parameters:** -- `state`: `on` (enable) or `off` (disable) -- `percent`: Battery percentage threshold - -**Default:** `off` - -**Default thresholds:** `20` for `battery.alert.low`, `10` for `battery.alert.critical` - -**Notes:** -- When enabled, sends a `#repeaters` flood text warning if voltage is above `1 V` and the battery estimate is below `battery.alert.low`. -- Warnings repeat every `24` hours, or every `12` hours below `battery.alert.critical`. -- `battery.alert.critical` must be lower than `battery.alert.low`. - ---- - -### Get or set recent repeater prefix/SNR -**Usage:** -- `get recent.repeater` -- `get recent.repeater ` -- `get recent.repeater page ` -- `set recent.repeater ` - -**Parameters:** -- `prefix_hex_6`: Exactly 3 bytes of next-hop prefix in hex (6 chars) -- `snr_db`: SNR in dB (supports decimals; stored at x4 precision) -- `page`: 1-based page number - -**Notes:** -- `set` stores or updates the prefix in the recent repeater table. -- Output rows are `prefix,snr` with optional `,l` for locked manual entries. -- Rows are sorted by prefix width (3-byte, 2-byte, 1-byte), then SNR descending. -- A full direct retry failure lowers the stored SNR by `0.25 dB`. -- If a full failure has no row yet, it first seeds the row at the active retry cutoff + `2.5 dB`, then applies the `0.25 dB` penalty. -- Serial CLI page size is fixed at `128` rows; choose page with `get recent.repeater `. -- Over LoRa remote CLI, page size is fixed at `7` rows; choose page with `get recent.repeater `. -- Repeaters can use adjacent entries in this table to short-circuit non-TRACE direct packets when this node appears later in the direct path. - ---- - ## Statistics ### Clear Stats @@ -292,20 +219,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the boosted receive gain mode -**Usage:** -- `get radio.rxgain` -- `set radio.rxgain ` - -**Parameters:** -- `state`: `on`|`off` - -**Default:** `off` - -**Note:** Only available on SX1262 and SX1268 based boards. - ---- - #### Change the radio parameters for a set duration **Usage:** - `tempradio ,,,,` @@ -336,7 +249,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change this node's rx boosted gain mode (SX12xx only, v1.14.1+) +#### View or change this node's rx boosted gain mode (SX12xx and LR1110, v1.14.1+) **Usage:** - `get radio.rxgain` - `set radio.rxgain ` @@ -469,7 +382,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** `|` characters are translated to newlines -**Note:** Requires firmware 1.12.+ +**Note:** Requires firmware 1.12+ --- @@ -518,18 +431,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or set reboot interval (Repeater and room server) -**Usage:** -- `get reboot.interval` -- `set reboot.interval ` - -**Parameters:** -- `hours`: 0-255. 0 is disabled - -**Default:** `0` (disabled) - ---- - ### Routing #### View or change this node's repeat flag @@ -560,7 +461,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14 -**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. +**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. --- @@ -578,7 +479,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `off` -**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 +**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 **Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. @@ -594,6 +495,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `0.5` +**Note:** When multiple nearby repeaters all hear the same flood packet, each waits a random amount of time before retransmitting to avoid simultaneous collisions. This factor scales the size of that random window. Higher values reduce collision risk at the cost of added latency. `0` disables the window entirely. + --- #### View or change the retransmit delay factor for direct traffic @@ -604,116 +507,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Direct transmit delay factor (0-2) -**Default:** `0.3` +**Default:** `0.2` -**Note:** Direct retry waits include the same airtime-based randomized delay calculation as direct retransmits, so this factor also controls retry echo windows. - ---- - -#### View or change whether direct retries use the recent repeater SNR gate -**Usage:** -- `get direct.retry.heard` -- `set direct.retry.heard ` - -**Parameters:** -- `state`: `on`|`off` - -**Default:** `on` - -**Note:** When enabled, the recent repeater table is the only direct retry eligibility gate. Prefixes missing from the table are assumed reachable; prefixes in the table below the active SNR gate are blocked. Neighbor data is not used. - ---- - -#### View or change adaptive coding rate for direct retry packets -**Usage:** -- `get direct.retry.cr` -- `set direct.retry.cr ,,,` -- `set direct.retry.cr off` - -**Parameters:** -- `cr4_min`: SNR in dB where retry packets use `CR4` -- `cr5_min`: SNR in dB where retry packets use `CR5` -- `cr7_min`: SNR in dB where retry packets use `CR7` -- `cr8_max`: SNR in dB where retry packets use `CR8` - -**Default:** `10.0,7.5,2.5,2.5` - -**Note:** DM retry packets use the next-hop SNR from a recent repeater table entry to pick a local transmit coding rate; if no recent entry is available, retry packets use `CR5`. 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. Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. - ---- - -#### View or change the SNR margin used for direct retry eligibility -**Usage:** -- `get direct.retry.margin` -- `set direct.retry.margin ` - -**Parameters:** -- `value`: Rooftop preset margin in dB above the SF-specific receive floor (minimum `0`, maximum `40`, quarter-dB precision, default `5.0`) - -**Default:** `5.0` - -**Note:** `get direct.retry.margin` returns the active preset's effective margin. The retry gate uses the active SF floor of `SF5=-2.5`, `SF6=-5`, `SF7=-7.5`, `SF8=-10`, `SF9=-12.5`, `SF10=-15`, `SF11=-17.5`, `SF12=-20`, then adds this margin. - ---- - -#### View or change the retry preset -**Usage:** -- `get retry.preset` -- `set retry.preset ` - -**Parameters:** -- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` - -**Default:** `rooftop` (`1`) - -**Presets:** -- `infra` (`0`): `275 ms` direct base wait, `4` direct retries, `150 ms` added per direct retry, SNR gate is SF floor + `15 dB`; flood retry defaults to `1` retry and path gate `1` -- `rooftop` (`1`): `175 ms` direct base wait, `15` direct retries, `100 ms` added per direct retry, SNR gate is SF floor + `5 dB`; flood retry defaults to `3` retries and path gate `2` -- `mobile` (`2`): `175 ms` direct base wait, `15` direct retries, `50 ms` added per direct retry, SNR gate is the SF floor; flood retry defaults to `3` retries and path gate `1` - -**Note:** Selecting a preset copies those values into the direct retry settings and resets flood retry defaults. You can refine `direct.retry.margin`, `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, `flood.retry.count`, or `flood.retry.path` afterward. Retry delay is `direct.txdelay` jitter + base wait + packet-length airtime wait + per-attempt step. - ---- - -#### View or change the number of direct retry attempts -**Usage:** -- `get direct.retry.count` -- `set direct.retry.count ` - -**Parameters:** -- `value`: Maximum retry attempts after initial TX (`1`-`15`) - -**Default:** `15` - -**Note:** The effective value is capped by total direct path length: paths of `3` hops or less use at most `8` retries, `4` hops use at most `12`, and `5+` hops use at most `15`. A queued resend is canceled early when the next-hop echo is heard. - ---- - -#### View or change the base direct retry wait (milliseconds) -**Usage:** -- `get direct.retry.base` -- `set direct.retry.base ` - -**Parameters:** -- `value`: Base wait in milliseconds (`10`-`5000`) - -**Default:** `175` - -**Note:** The configured base is added to packet-length airtime and `direct.txdelay` jitter. Preset defaults are already reduced to account for the added `direct.txdelay` component. - ---- - -#### View or change the direct retry per-attempt add time (milliseconds) -**Usage:** -- `get direct.retry.step` -- `set direct.retry.step ` - -**Parameters:** -- `value`: Milliseconds added per retry attempt (`0`-`5000`) - -**Default:** `100` - -**Note:** This controls the linear add after the first retry wait. For example, `base=300` and `step=150` adds `0`, `150`, `300`, ... ms across retry attempts. +**Note:** Same collision-avoidance random window as `txdelay`, but applied to direct (non-flood, routed) traffic. The default is lower because direct packets are addressed to a specific next hop, so far fewer nodes compete to retransmit them. --- @@ -727,6 +523,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `0.0` +**Note:** When enabled, repeaters that received a flood packet with a weak signal are held in a delay queue before processing, while those that received it with a strong signal process it immediately. This gives strong-signal paths forwarding priority. By the time weak-signal nodes process their copy, the packet may have already propagated and will be suppressed as a duplicate, reducing redundant retransmissions. + --- #### View or change the duty cycle limit @@ -854,114 +652,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or change the number of flood retry attempts +#### Limit the number of hops for an unscoped flood message **Usage:** -- `get flood.retry.count` -- `set flood.retry.count ` +- `get flood.max.unscoped` +- `set flood.max.unscoped ` **Parameters:** -- `value`: Maximum retry attempts after initial flood TX (`0`-`15`) +- `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `3` for `rooftop` and `mobile`, `1` for `infra` +**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is. -**Note:** `0` disables flood retry. +**Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. ---- - -#### View or change the flood retry path gate -**Usage:** -- `get flood.retry.path` -- `set flood.retry.path ` - -**Parameters:** -- `value`: Maximum flood path length eligible for retry (`0`-`63`), or `off` to disable the gate - -**Default:** `2` for `rooftop`, `1` for `infra` and `mobile` - -**Note:** Prefixes in `flood.retry.ignore` do not count toward this path length. - ---- - -#### View or change whether advert packets are flood-retried -**Usage:** -- `get flood.retry.advert` -- `set flood.retry.advert ` - -**Parameters:** -- `state`: `on` or `off` - -**Default:** `off` - -**Note:** When this is `off`, node advert packets (`PAYLOAD_TYPE_ADVERT`, type `4`) are not queued for flood retry. - ---- - -#### View or change flood retry target prefixes -**Usage:** -- `get flood.retry.prefixes` -- `set flood.retry.prefixes ` - -**Parameters:** -- `prefixes`: Comma-separated 3-byte hex prefixes, such as `A1B2C3,D4E5F6`; use `none` or `off` to clear - -**Default:** `none` - -**Note:** Prefixes are stored as 3 bytes. Flood retry skips packets whose path already contains a matching target prefix. When prefixes are configured, only a downstream echo from one of those target prefixes cancels a queued retry; when no prefixes are configured, any downstream echo cancels it. Matching works with 3-byte, 2-byte, or 1-byte flood paths by comparing the matching leading bytes. - ---- - -#### View or change flood retry bridge mode -**Usage:** -- `get flood.retry.bridge` -- `set flood.retry.bridge ` - -**Parameters:** -- `state`: `on` or `off` - -**Default:** `off` - -**Note:** Bridge mode uses bucket definitions instead of the single `flood.retry.prefixes` target list. It also has an implicit unconfigured catch-all bucket. If a flood comes from one fresh configured bucket, retry continues until every other fresh configured bucket plus the catch-all bucket has been heard or `flood.retry.count` is exhausted. If a flood comes from an unconfigured or pathless source, retry targets every fresh configured bucket. This means one configured bucket bridges between that bucket and everything else. Prefixes in `flood.retry.ignore` never count as heard bridge targets. - ---- - -#### View or change flood retry bridge buckets -**Usage:** -- `get flood.retry.bucket.` -- `set flood.retry.bucket ` - -**Parameters:** -- `bucket`: Bucket number (`1`-`6`) -- `prefixes`: Up to 17 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear - -**Default:** all buckets empty - -**Note:** Prefixes are stored as 3 bytes but match 3-byte, 2-byte, and 1-byte flood paths by comparing leading bytes. Bucket prefixes are included in bridge retry logic only if they were heard in the recent repeater table within the last hour. - ---- - -#### View or change flood retry ignored prefixes -**Usage:** -- `get flood.retry.ignore` -- `set flood.retry.ignore ` - -**Parameters:** -- `prefixes`: Up to 8 comma-separated 3-byte hex prefixes, such as `AABBCC,223344`; use `none` or `off` to clear - -**Default:** empty - -**Note:** In non-bridge retry, an echo whose last hop matches an ignored prefix does not cancel a queued retry as successful. In bridge mode, ignored prefixes do not count as a heard bridge bucket or as the implicit catch-all bucket when bridge retry decides whether every target has repeated the flood. - ---- - -#### Limit the number of hops for an advert flood message -**Usage:** -- `get flood.max.advert` -- `set flood.max.advert ` - -**Parameters:** -- `value`: Maximum flood hop count (0-64) for an advert packet - -**Default:** `8` --- @@ -991,32 +693,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or set direct path overrides for the current remote client -**Usage:** -- `get outpath` -- `set outpath ` -- `set outpath direct` -- `set outpath clear` -- `set outpath flood` -- `get altpath` -- `set altpath ` -- `set altpath clear` - -**Parameters:** -- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. - -**Notes:** -- These commands require remote client context; they target the caller's ACL entry. -- The path hash size is inferred from the hop hash width. -- `outpath` overrides the primary direct route used for replies to the caller. -- `direct` sets a zero-hop direct route for a caller reachable without repeaters. -- `clear` forgets the current direct path and allows normal path discovery to repopulate it. -- `flood` forces replies to use flood packets until the client logs in again. -- `altpath` is an optional second direct route used for duplicate response attempts. -- `set altpath clear` removes the duplicate route so only one reply is sent. - ---- - #### View or change this room server's 'read-only' flag **Usage:** - `get allow.read.only` @@ -1104,27 +780,6 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- -#### View or set the direct path override for the current remote client -**Usage:** -- `get outpath` -- `set outpath ` -- `set outpath direct` -- `set outpath clear` -- `set outpath flood` - -**Parameters:** -- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. - -**Notes:** -- These commands require remote client context (they target the caller's ACL entry). -- The path hash size is inferred from the hop hash width. -- `outpath` overrides the primary direct route used for replies to the caller. -- `direct` sets a zero-hop direct route for a caller reachable without repeaters. -- `clear` forgets the current direct path and allows normal path discovery to repopulate it. -- `flood` forces replies to use flood packets until the client logs in again. - ---- - #### Create a new region **Usage:** - `region put [parent_name]` @@ -1135,6 +790,47 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### Define region hierarchy (single line) +**Usage:** +- `region def [ ...]` + +**Parameters (tokens):** Space-separated. A logical **cursor** starts at the wildcard `*`. + +- **`name`** — Create `name` as a child of the current cursor (equivalent to `region put name` with the cursor as parent). Cursor moves to `name`. +- **`name|jump`** *(or `name,jump`)* — Create `name` as a child of the current cursor, then move the cursor to `jump` (must already exist on the node, or have been created earlier in this command). `jump` is **not** the parent of `name`; use this form to pop back up and start another branch. + +**Behavior:** Each created region defaults to flood-allowed (same as `region put`). The reply is the resulting region tree (same format as bare `region`); review it before running `region save` to persist. On error, the reply is `Err - ...` and any regions placed before the failure remain on the node, just like a partial chain of `region put`. + +**Existing regions:** `region def` does not clear the existing tree — if a name already exists, its parent is updated to the current cursor; otherwise a new region is created. To start from scratch, `region remove` the unwanted regions first. + +**Limits:** Repeater serial accepts one line up to **160 characters**. For larger trees, split across multiple `region def` commands; the cursor resets to `*` between commands, so lead the next command with `child|ancestor` to reposition. Each token splits at most once on `|` — `region def a|b|c|d` is not a flat-list shorthand; see the flat-list example below. + +**Example — linear chain** (each token becomes a child of the previous): +``` +region def a b c d e +region save +``` + +**Example — branched tree** (equivalent to `region put a`, `region put b a`, `region put c b`, `region put d c`, `region put e b`, `region put f e`): +``` +region def a b c d|b e f +region save +``` + +**Example — error and partial state:** +``` +region def a b c|nope d +``` +The reply is `Err - unknown jump: nope`. `a`, `b`, and `c` were placed before the failure; `d` was not. Run `region` to inspect, then re-run with a corrected jump or repair with `region remove` / `region put`. + +**Example — flat list** (each region a child of `*`). Use `|*` after each token to pop the cursor back to the root before the next token: +``` +region def a|* b|* c|* d|* e|* f +region save +``` + +--- + #### Remove a region **Usage:** - `region remove ` @@ -1155,7 +851,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `filter`: `allowed`|`denied` -**Note:** Requires firmware 1.12.+ +**Note:** Requires firmware 1.12+ --- @@ -1312,7 +1008,7 @@ region save --- -#### View or change thevalue of a sensor +#### View or change the value of a sensor **Usage:** - `sensor get ` - `sensor set ` @@ -1432,3 +1128,71 @@ region save **Note:** Returns an error on boards without power management support. --- + +--- + +## Halo Direct Retry Commands + +These commands are available on the Halo direct-message retry branch. See `docs/halo_settings.md` for operating guidance and examples. + +### View or change the retry preset +**Usage:** +- `get retry.preset` +- `set retry.preset ` + +**Parameters:** +- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` + +--- + +### View or change whether direct retries use the recent repeater SNR gate +**Usage:** +- `get direct.retry.heard` +- `set direct.retry.heard ` + +**Parameters:** +- `state`: `on`|`off` + +--- + +### View or change direct retry timing and count +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` +- `get direct.retry.count` +- `set direct.retry.count <1-15>` +- `get direct.retry.base` +- `set direct.retry.base <10-5000>` +- `get direct.retry.step` +- `set direct.retry.step <0-5000>` + +--- + +### View or change adaptive coding rate for direct retry packets +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr ,,,` +- `set direct.retry.cr off` + +--- + +### Get or set recent repeater prefix/SNR +**Usage:** +- `get recent.repeater` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` +- `clear recent.repeater` + +--- + +### View or change direct reply path overrides +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath direct` +- `set outpath clear` +- `set outpath flood` +- `get altpath` +- `set altpath ` +- `set altpath clear` diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 36c66c3f..bb39c077 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -85,9 +85,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t last_millis; uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; - unsigned long next_battery_alert_check; - unsigned long last_battery_alert_sent; - bool battery_alert_sent; bool _logging; NodePrefs _prefs; ClientACL acl; @@ -102,15 +99,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionEntry* recv_pkt_region; TransportKey default_scope; RateLimiter discover_limiter, anon_limiter; - struct FloodRetryBridgeState { - uint8_t key[MAX_HASH_SIZE]; - uint8_t source_bucket; - uint8_t target_mask; - uint8_t heard_mask; - uint8_t progress_marker; - bool active; - }; - mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS]; uint32_t pending_discover_tag; unsigned long pending_discover_until; bool region_load_active; @@ -140,25 +128,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t getDirectRetryPreset() const; uint8_t getDirectRetryConfiguredMaxAttempts() const; uint32_t getDirectRetryAttemptStepMillis() const; - bool hasFloodRetryPrefixes() const; - bool floodRetryPrefixMatches(const mesh::Packet* packet) const; - bool floodRetryLastHopMatches(const mesh::Packet* packet) const; - bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const; - uint8_t floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops = 0xFF) const; - bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const; - int floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, - bool include_other) const; - int floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop, - uint8_t progress_marker) const; - int floodRetrySourceBucket(const mesh::Packet* packet) const; - uint8_t floodRetryBridgeTargetMask(uint8_t source_bucket) const; - uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket, - uint8_t progress_marker) const; - FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const; - void clearFloodRetryBridgeState(const mesh::Packet* packet); - void refreshFloodRetryHeardRecent(const mesh::Packet* packet); - void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const; - bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); 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); @@ -166,8 +135,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); - bool sendRepeatersFloodText(const char* text); - void checkBatteryAlert(); File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); @@ -196,12 +163,6 @@ protected: uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; - bool allowFloodRetry(const mesh::Packet* packet) const override; - void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; - bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override; - uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override; - uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override; - bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index b9b1ab4d..03a89dd4 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -159,9 +159,4 @@ void loop() { } #endif } - - if (the_mesh.getNodePrefs()->reboot_interval > 0 && - the_mesh.millisHasNowPassed(the_mesh.getNodePrefs()->reboot_interval * 3600000)) { - board.reboot(); - } } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 4dccd45a..a9f7aac0 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -5,8 +5,6 @@ namespace mesh { static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_DEFAULT = 15; static const uint8_t DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; -static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT = 3; -static const uint8_t FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX = 15; static uint8_t decodeTraceHashSize(uint8_t flags, uint8_t route_bytes) { uint8_t code = flags & 0x03; @@ -44,19 +42,6 @@ void Mesh::begin() { _direct_retries[i].queued = false; _direct_retries[i].active = false; } - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - _flood_retries[i].packet = NULL; - _flood_retries[i].trigger_packet = NULL; - _flood_retries[i].retry_started_at = 0; - _flood_retries[i].retry_at = 0; - _flood_retries[i].retry_delay = 0; - _flood_retries[i].retry_attempts_sent = 0; - _flood_retries[i].priority = 0; - _flood_retries[i].progress_marker = 0; - _flood_retries[i].waiting_final_echo = false; - _flood_retries[i].queued = false; - _flood_retries[i].active = false; - } Dispatcher::begin(); } @@ -93,37 +78,6 @@ void Mesh::loop() { clearDirectRetrySlot(i); } } - - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - continue; - } - - if (_flood_retries[i].waiting_final_echo) { - if (!millisHasNowPassed(_flood_retries[i].retry_at)) { - continue; - } - - uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 - ? 0 - : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - onFloodRetryEvent("failed_all_tries", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - onFloodRetryEvent("failure", _flood_retries[i].packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - clearFloodRetrySlot(i); - continue; - } - - if (!_flood_retries[i].queued || !millisHasNowPassed(_flood_retries[i].retry_at)) { - continue; - } - - if (!isFloodRetryQueued(_flood_retries[i].packet)) { - if (_flood_retries[i].packet == getOutboundInFlight()) { - continue; - } - clearFloodRetrySlot(i); - } - } } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -152,39 +106,16 @@ uint32_t Mesh::getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_ // Keep the historical linear spacing while allowing the base wait to vary by platform/profile. return base + ((uint32_t)attempt_idx * 100UL); } -bool Mesh::allowFloodRetry(const Packet* packet) const { - return true; -} -bool Mesh::hasFloodRetryTargetPrefix(const Packet* packet) const { - return false; -} -uint8_t Mesh::getFloodRetryMaxPathLength(const Packet* packet) const { - return 2; -} -uint8_t Mesh::getFloodRetryMaxAttempts(const Packet* packet) const { - return FLOOD_RETRY_MAX_ATTEMPTS_DEFAULT; -} -uint32_t Mesh::getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx) { - if (packet == NULL) { - return _radio->getEstAirtimeFor(MAX_TRANS_UNIT); - } - - uint32_t max_packet_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); - uint32_t packet_airtime = _radio->getEstAirtimeFor(packet->getRawLength()); - return max_packet_airtime + (20UL * packet_airtime); -} uint8_t Mesh::getExtraAckTransmitCount() const { return 0; } void Mesh::onSendComplete(Packet* packet) { armDirectRetryOnSendComplete(packet); - armFloodRetryOnSendComplete(packet); } void Mesh::onSendFail(Packet* packet) { clearPendingDirectRetryOnSendFail(packet); - clearPendingFloodRetryOnSendFail(packet); } uint32_t Mesh::getCADFailRetryDelay() const { @@ -202,8 +133,6 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteDirect()) { cancelDirectRetryOnEcho(pkt); - } else if (pkt->isRouteFlood()) { - cancelFloodRetryOnEcho(pkt); } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { @@ -523,7 +452,6 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { uint32_t d = getRetransmitDelay(packet); uint8_t priority = packet->getPathHashCount(); - maybeScheduleFloodRetry(packet, priority); // as this propagates outwards, give it lower and lower priority return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away } @@ -952,242 +880,6 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].active = true; } -void Mesh::clearFloodRetrySlot(int idx) { - if (_flood_retries[idx].waiting_final_echo && _flood_retries[idx].packet != NULL) { - releasePacket(_flood_retries[idx].packet); - } - _flood_retries[idx].packet = NULL; - _flood_retries[idx].trigger_packet = NULL; - _flood_retries[idx].retry_started_at = 0; - _flood_retries[idx].retry_at = 0; - _flood_retries[idx].retry_delay = 0; - _flood_retries[idx].retry_attempts_sent = 0; - _flood_retries[idx].priority = 0; - _flood_retries[idx].progress_marker = 0; - _flood_retries[idx].waiting_final_echo = false; - _flood_retries[idx].queued = false; - _flood_retries[idx].active = false; -} - -bool Mesh::isFloodRetryQueued(const Packet* packet) const { - for (int i = 0; i < _mgr->getOutboundTotal(); i++) { - if (_mgr->getOutboundByIdx(i) == packet) { - return true; - } - } - return false; -} - -bool Mesh::isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const { - return packet->isRouteFlood() && packet->getPathHashCount() > progress_marker; -} - -bool Mesh::cancelFloodRetryOnEcho(const Packet* packet) { - uint8_t recv_key[MAX_HASH_SIZE]; - packet->calculatePacketHash(recv_key); - - bool cleared = false; - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active || memcmp(recv_key, _flood_retries[i].retry_key, MAX_HASH_SIZE) != 0) { - continue; - } - if (!isFloodRetryEchoTarget(packet, _flood_retries[i].progress_marker)) { - continue; - } - - uint32_t echo_millis = _flood_retries[i].retry_started_at == 0 - ? 0 - : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - uint8_t retry_attempt = _flood_retries[i].waiting_final_echo - ? _flood_retries[i].retry_attempts_sent - : _flood_retries[i].retry_attempts_sent + 1; - onFloodRetryEvent("good", packet, echo_millis, retry_attempt); - - if (_flood_retries[i].queued) { - for (int j = 0; j < _mgr->getOutboundTotal(); j++) { - if (_mgr->getOutboundByIdx(j) == _flood_retries[i].packet) { - Packet* pending = _mgr->removeOutboundByIdx(j); - if (pending) { - releasePacket(pending); - } - break; - } - } - } - clearFloodRetrySlot(i); - cleared = true; - } - - return cleared; -} - -void Mesh::armFloodRetryOnSendComplete(const Packet* packet) { - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - continue; - } - - if (_flood_retries[i].queued) { - if (_flood_retries[i].packet != packet) { - continue; - } - - uint32_t elapsed_millis = _flood_retries[i].retry_started_at == 0 - ? 0 - : (uint32_t)(_ms->getMillis() - _flood_retries[i].retry_started_at); - onFloodRetryEvent("resent", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - _flood_retries[i].retry_attempts_sent++; - - uint8_t max_attempts = getFloodRetryMaxAttempts(packet); - if (max_attempts < 1) { - max_attempts = 1; - } else if (max_attempts > FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX) { - max_attempts = FLOOD_RETRY_MAX_ATTEMPTS_HARD_MAX; - } - if (_flood_retries[i].retry_attempts_sent >= max_attempts) { - Packet* final_wait = obtainNewPacket(); - if (final_wait == NULL) { - onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent); - clearFloodRetrySlot(i); - continue; - } - - *final_wait = *packet; - _flood_retries[i].packet = final_wait; - _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); - _flood_retries[i].waiting_final_echo = true; - _flood_retries[i].queued = false; - continue; - } - - Packet* retry = obtainNewPacket(); - if (retry == NULL) { - onFloodRetryEvent("dropped_no_packet", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - onFloodRetryEvent("failure", packet, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - clearFloodRetrySlot(i); - continue; - } - - *retry = *packet; - uint32_t retry_delay = getFloodRetryAttemptDelay(packet, _flood_retries[i].retry_attempts_sent); - if (queueOutboundPacket(retry, _flood_retries[i].priority, retry_delay)) { - _flood_retries[i].packet = retry; - _flood_retries[i].retry_delay = retry_delay; - _flood_retries[i].retry_at = futureMillis(retry_delay); - _flood_retries[i].retry_started_at = _ms->getMillis(); - _flood_retries[i].waiting_final_echo = false; - onFloodRetryEvent("queued", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); - } else { - onFloodRetryEvent("dropped_queue_full", retry, retry_delay, _flood_retries[i].retry_attempts_sent + 1); - onFloodRetryEvent("failure", retry, elapsed_millis, _flood_retries[i].retry_attempts_sent + 1); - releasePacket(retry); - clearFloodRetrySlot(i); - } - continue; - } - - if (_flood_retries[i].trigger_packet != packet) { - continue; - } - - Packet* retry = obtainNewPacket(); - if (retry == NULL) { - onFloodRetryEvent("dropped_no_packet", packet, _flood_retries[i].retry_delay, 1); - onFloodRetryEvent("failure", packet, 0, 1); - clearFloodRetrySlot(i); - continue; - } - - *retry = *packet; - if (queueOutboundPacket(retry, _flood_retries[i].priority, _flood_retries[i].retry_delay)) { - unsigned long now = _ms->getMillis(); - _flood_retries[i].packet = retry; - _flood_retries[i].trigger_packet = NULL; - _flood_retries[i].queued = true; - _flood_retries[i].waiting_final_echo = false; - _flood_retries[i].retry_at = futureMillis(_flood_retries[i].retry_delay); - _flood_retries[i].retry_started_at = now; - onFloodRetryEvent("queued", retry, _flood_retries[i].retry_delay, 1); - } else { - onFloodRetryEvent("dropped_queue_full", retry, _flood_retries[i].retry_delay, 1); - onFloodRetryEvent("failure", retry, 0, 1); - releasePacket(retry); - clearFloodRetrySlot(i); - } - } -} - -void Mesh::clearPendingFloodRetryOnSendFail(const Packet* packet) { - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - continue; - } - - if (_flood_retries[i].queued) { - if (_flood_retries[i].packet == packet) { - onFloodRetryEvent("dropped_send_fail", packet, 0, _flood_retries[i].retry_attempts_sent + 1); - onFloodRetryEvent("failure", packet, 0, _flood_retries[i].retry_attempts_sent + 1); - clearFloodRetrySlot(i); - } - continue; - } - - if (_flood_retries[i].trigger_packet == packet) { - onFloodRetryEvent("dropped_send_fail", packet, 0, 1); - onFloodRetryEvent("failure", packet, 0, 1); - clearFloodRetrySlot(i); - } - } -} - -void Mesh::maybeScheduleFloodRetry(const Packet* packet, uint8_t priority) { - if (packet == NULL || !packet->isRouteFlood() || hasFloodRetryTargetPrefix(packet)) { - return; - } - - uint8_t max_path_len = getFloodRetryMaxPathLength(packet); - if (max_path_len != FLOOD_RETRY_PATH_GATE_DISABLED && packet->getPathHashCount() > max_path_len) { - return; - } - - uint8_t max_attempts = getFloodRetryMaxAttempts(packet); - if (max_attempts == 0) { - return; - } - - int slot_idx = -1; - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (!_flood_retries[i].active) { - slot_idx = i; - break; - } - } - if (slot_idx < 0) { - onFloodRetryEvent("dropped_no_slot", packet, 0, 0); - onFloodRetryEvent("failure", packet, 0, 0); - return; - } - - if (!allowFloodRetry(packet)) { - return; - } - - uint32_t retry_delay = getFloodRetryAttemptDelay(packet, 0); - packet->calculatePacketHash(_flood_retries[slot_idx].retry_key); - _flood_retries[slot_idx].packet = NULL; - _flood_retries[slot_idx].trigger_packet = const_cast(packet); - _flood_retries[slot_idx].retry_started_at = 0; - _flood_retries[slot_idx].retry_at = 0; - _flood_retries[slot_idx].retry_delay = retry_delay; - _flood_retries[slot_idx].retry_attempts_sent = 0; - _flood_retries[slot_idx].priority = priority; - _flood_retries[slot_idx].progress_marker = packet->getPathHashCount(); - _flood_retries[slot_idx].waiting_final_echo = false; - _flood_retries[slot_idx].queued = false; - _flood_retries[slot_idx].active = true; -} - Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) { if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL; diff --git a/src/Mesh.h b/src/Mesh.h index 6ae26642..314db237 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -8,14 +8,6 @@ namespace mesh { #define MAX_DIRECT_RETRY_SLOTS 6 #endif -#ifndef MAX_FLOOD_RETRY_SLOTS - #define MAX_FLOOD_RETRY_SLOTS 6 -#endif - -#ifndef FLOOD_RETRY_PATH_GATE_DISABLED - #define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF -#endif - class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; @@ -54,26 +46,7 @@ class Mesh : public Dispatcher { bool active; }; - struct FloodRetryEntry { - Packet* packet; - Packet* trigger_packet; - unsigned long retry_started_at; - unsigned long retry_at; - uint32_t retry_delay; - uint8_t retry_attempts_sent; - uint8_t retry_key[MAX_HASH_SIZE]; - uint8_t priority; - uint8_t progress_marker; - bool waiting_final_echo; - bool queued; - bool active; - }; - - RTCClock* _rtc; - RNG* _rng; - MeshTables* _tables; DirectRetryEntry _direct_retries[MAX_DIRECT_RETRY_SLOTS]; - FloodRetryEntry _flood_retries[MAX_FLOOD_RETRY_SLOTS]; void removePathPrefix(Packet* packet, uint8_t prefix_count); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); @@ -87,12 +60,6 @@ class Mesh : public Dispatcher { uint8_t& progress_marker, bool& expect_path_growth) const; bool canDecodeDirectPayloadForSelf(const Packet* packet); void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); - void clearFloodRetrySlot(int idx); - bool isFloodRetryQueued(const Packet* packet) const; - bool cancelFloodRetryOnEcho(const Packet* packet); - void armFloodRetryOnSendComplete(const Packet* packet); - void clearPendingFloodRetryOnSendFail(const Packet* packet); - void maybeScheduleFloodRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); @@ -156,41 +123,6 @@ protected: */ virtual uint32_t getDirectRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); - /** - * \brief Decide whether a FLOOD packet should retry when no downstream echo is overheard. - */ - virtual bool allowFloodRetry(const Packet* packet) const; - - /** - * \brief Return true when this FLOOD packet already carries an application-defined target prefix. - */ - virtual bool hasFloodRetryTargetPrefix(const Packet* packet) const; - - /** - * \returns maximum flood path hash count eligible for retry, or FLOOD_RETRY_PATH_GATE_DISABLED. - */ - virtual uint8_t getFloodRetryMaxPathLength(const Packet* packet) const; - - /** - * \returns maximum number of FLOOD retry transmissions after the initial TX. - */ - virtual uint8_t getFloodRetryMaxAttempts(const Packet* packet) const; - - /** - * \brief Return true when a received FLOOD echo is enough to cancel a pending retry. - */ - virtual bool isFloodRetryEchoTarget(const Packet* packet, uint8_t progress_marker) const; - - /** - * \returns delay before a specific flood retry attempt, where attempt_idx=0 is the first retry. - */ - virtual uint32_t getFloodRetryAttemptDelay(const Packet* packet, uint8_t attempt_idx); - - /** - * \brief Optional hook for logging flood-retry lifecycle events. - */ - virtual void onFloodRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } - /** * \returns number of extra (Direct) ACK transmissions wanted. */ From 765ec1447badc7b6efdbfeaa19a7449361f08bbb Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 17:24:22 -0700 Subject: [PATCH 13/25] Add direct retry controls --- docs/cli_commands.md | 68 ------- docs/halo_settings.md | 136 -------------- examples/simple_repeater/MyMesh.cpp | 226 +++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 24 +-- examples/simple_repeater/main.cpp | 6 +- src/Mesh.cpp | 132 +++++-------- src/Mesh.h | 19 +- src/helpers/ClientACL.cpp | 13 +- src/helpers/ClientACL.h | 5 +- src/helpers/CommonCLI.cpp | 277 +++++++++++++++++++++++++++- src/helpers/CommonCLI.h | 51 +++++ src/helpers/SimpleMeshTables.h | 108 +++-------- 12 files changed, 649 insertions(+), 416 deletions(-) delete mode 100644 docs/halo_settings.md diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 5c5ee670..c06f5e12 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1128,71 +1128,3 @@ region save **Note:** Returns an error on boards without power management support. --- - ---- - -## Halo Direct Retry Commands - -These commands are available on the Halo direct-message retry branch. See `docs/halo_settings.md` for operating guidance and examples. - -### View or change the retry preset -**Usage:** -- `get retry.preset` -- `set retry.preset ` - -**Parameters:** -- `value`: `infra`|`rooftop`|`mobile` or `0`|`1`|`2` - ---- - -### View or change whether direct retries use the recent repeater SNR gate -**Usage:** -- `get direct.retry.heard` -- `set direct.retry.heard ` - -**Parameters:** -- `state`: `on`|`off` - ---- - -### View or change direct retry timing and count -**Usage:** -- `get direct.retry.margin` -- `set direct.retry.margin ` -- `get direct.retry.count` -- `set direct.retry.count <1-15>` -- `get direct.retry.base` -- `set direct.retry.base <10-5000>` -- `get direct.retry.step` -- `set direct.retry.step <0-5000>` - ---- - -### View or change adaptive coding rate for direct retry packets -**Usage:** -- `get direct.retry.cr` -- `set direct.retry.cr ,,,` -- `set direct.retry.cr off` - ---- - -### Get or set recent repeater prefix/SNR -**Usage:** -- `get recent.repeater` -- `get recent.repeater ` -- `get recent.repeater page ` -- `set recent.repeater ` -- `clear recent.repeater` - ---- - -### View or change direct reply path overrides -**Usage:** -- `get outpath` -- `set outpath ` -- `set outpath direct` -- `set outpath clear` -- `set outpath flood` -- `get altpath` -- `set altpath ` -- `set altpath clear` diff --git a/docs/halo_settings.md b/docs/halo_settings.md deleted file mode 100644 index 9324487d..00000000 --- a/docs/halo_settings.md +++ /dev/null @@ -1,136 +0,0 @@ -# Halo Direct Message Retry Settings - -This file covers only CLI settings and helper commands added for Halo direct-message retry behavior. Use `docs/cli_commands.md` for the general MeshCore CLI. - -Halo retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. - -## Quick Start - -```text -set retry.preset rooftop -set direct.retry.heard on -get retry.preset -get direct.retry.heard -get direct.retry.count -get direct.retry.base -get direct.retry.step -``` - -Use prefixes from the analyzer, neighbors list, or `get recent.repeater` after the repeater has been online for a few hours. - -## Added Halo Settings - -| Setting | What it does | How to use | Example | -| --- | --- | --- | --- | -| `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | -| `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | -| `altpath` | Optional second direct route used for duplicate response attempts to the current remote client. | `get altpath`, `set altpath `, `set altpath clear` | `set altpath A1B2C3,D4E5F6` | -| `retry.preset` | Applies direct retry defaults. Values: `infra`, `rooftop`, `mobile` or `0`, `1`, `2`. | `get retry.preset`, `set retry.preset ` | `set retry.preset rooftop` | -| `direct.retry.heard` | Uses the recent repeater table as the direct retry eligibility gate. | `get direct.retry.heard`, `set direct.retry.heard on/off` | `set direct.retry.heard on` | -| `direct.retry.margin` | SNR margin in dB above the SF-specific receive floor. | `get direct.retry.margin`, `set direct.retry.margin <0-40>` | `set direct.retry.margin 5` | -| `direct.retry.count` | Maximum direct retry attempts after initial TX. | `get direct.retry.count`, `set direct.retry.count <1-15>` | `set direct.retry.count 15` | -| `direct.retry.base` | Base wait in milliseconds before retry. | `get direct.retry.base`, `set direct.retry.base <10-5000>` | `set direct.retry.base 175` | -| `direct.retry.step` | Milliseconds added per retry attempt. | `get direct.retry.step`, `set direct.retry.step <0-5000>` | `set direct.retry.step 100` | -| `direct.retry.cr` | Adaptive coding-rate thresholds for direct retry packets. Uses `CR4`, `CR5`, `CR7`, or `CR8`; `CR6` is never selected. | `get direct.retry.cr`, `set direct.retry.cr ,,,`, `set direct.retry.cr off` | `set direct.retry.cr 10.0,7.5,2.5,0` | - -## Recent Repeater Table - -Direct retry uses the recent repeater table when `direct.retry.heard` is `on`. - -Show learned rows: - -```text -get recent.repeater -get recent.repeater 2 -get recent.repeater page 3 -``` - -Seed or correct a prefix: - -```text -set recent.repeater A1B2C3 8.5 -``` - -Clear learned and manually seeded rows: - -```text -clear recent.repeater -``` - -Rows are sorted by prefix width, then SNR. A full direct retry failure lowers the matching row by `0.25 dB`. - -Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to `7` rows. - -## Direct Path Overrides - -`outpath` and `altpath` apply to the current remote client ACL entry. They need remote client context, so they are not useful from the local serial CLI. - -Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` hex characters, and all hops in one path must use the same width. - -```text -get outpath -set outpath A1B2C3,D4E5F6 -set outpath direct -set outpath clear -set outpath flood - -get altpath -set altpath A1B2C3,D4E5F6 -set altpath clear -``` - -`set outpath direct` sets a zero-hop direct route for a client reachable without repeaters. `set outpath clear` forgets the override and lets normal path discovery fill it again. `set outpath flood` forces replies to use flood packets until the client logs in again. `altpath` sends a duplicate reply over a second direct route; clearing it returns replies to a single route. - -## Direct Retry Details - -The default adaptive coding-rate profile is `10.0,7.5,2.5,2.5`. SNR `10.0 dB` and up uses `CR4`, `7.5 dB` and up uses `CR5`, `2.5 dB` and down uses `CR8`, and the middle band uses `CR7`. If no recent repeater table entry is available, retry packets use `CR5`. - -Use `set direct.retry.cr off` to disable adaptive coding-rate overrides. If adaptive selection chooses `CR4`, retries after the third attempt use `CR5`. - -Preset details: - -| Preset | Base | Count | Step | SNR gate | -| --- | ---: | ---: | ---: | --- | -| `infra` | `275 ms` | `4` | `150 ms` | SF floor + `15 dB` | -| `rooftop` | `175 ms` | `15` | `100 ms` | SF floor + `5 dB` | -| `mobile` | `175 ms` | `15` | `50 ms` | SF floor | - -Example for a quiet fixed repeater: - -```text -set retry.preset rooftop -set direct.retry.heard on -set direct.retry.margin 5 -``` - -Example for a moving or weak-link node: - -```text -set retry.preset mobile -set direct.retry.margin 0 -``` - -## Troubleshooting - -If direct retries are too aggressive: - -```text -set direct.retry.count 4 -set direct.retry.margin 10 -``` - -If direct retries are too sparse: - -```text -set direct.retry.count 15 -set direct.retry.margin 0 -``` - -If direct retry is skipping a path you expect it to retry: - -```text -get direct.retry.heard -get recent.repeater -``` - -Either disable the heard gate with `set direct.retry.heard off`, or seed the next-hop prefix with `set recent.repeater `. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 5cc3a9a1..45f81ed2 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -549,6 +549,208 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { return getRNG()->nextInt(0, 5*t + 1); } +bool MyMesh::extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const { + if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0) { + return false; + } + prefix_len = packet->getPathHashSize(); + memcpy(prefix, packet->path, prefix_len); + return true; +} + +int8_t MyMesh::getDirectRetryMinSNRX4() const { + switch (active_sf) { + case 7: return -30; + case 8: return -40; + case 9: return -50; + case 10: return -60; + case 11: return -70; + case 12: return -80; + default: return -60; + } +} + +uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { + if (!_prefs.direct_retry_cr_enabled) return 0; + 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_cr7_snr_x4) return 7; + return 8; +} + +uint8_t MyMesh::getDirectRetryPreset() const { + return _prefs.retry_preset; +} + +uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { + return constrain(_prefs.direct_retry_attempts, 1, 15); +} + +uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { + return _prefs.direct_retry_step_ms; +} + +bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { + (void)packet; + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return true; + } + const SimpleMeshTables* tables = static_cast(getTables()); + const SimpleMeshTables::RecentRepeaterInfo* repeater = tables != NULL + ? tables->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len) + : NULL; + + if (repeater == NULL) { + // Retry unknown repeaters too. If they fail, onDirectRetryFailed() seeds the + // recent-repeater table below the +2.00 dB starting point. + return true; + } + int16_t retry_floor_x4 = (int16_t)getDirectRetryMinSNRX4() + (int16_t)_prefs.direct_retry_snr_margin_x4; + return (int16_t)repeater->snr_x4 >= retry_floor_x4; +} + +void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { + (void)retry_attempt; + int8_t snr_x4 = 8; // unknown repeaters start at +2.00 dB + const SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + uint8_t prefix[MAX_HASH_SIZE]; + uint8_t prefix_len = 0; + if (extractDirectRetryPrefix(original, prefix, prefix_len)) { + const SimpleMeshTables::RecentRepeaterInfo* repeater = tables->findRecentRepeaterByHash(prefix, prefix_len); + if (repeater != NULL) { + snr_x4 = repeater->snr_x4; + } + } + } + + retry->tx_cr = getDirectRetryCodingRateForSNR(snr_x4); +} + +bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) { + (void)packet; + return false; +} + +uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { + (void)packet; + return 200; +} + +uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { + (void)packet; + return getDirectRetryConfiguredMaxAttempts(); +} + +uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) { + (void)packet; + return _prefs.direct_retry_base_ms + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis()); +} + +void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("direct retry %s attempt=%u delay=%lu type=%u route=%s", + event ? event : "?", + (uint32_t)retry_attempt, + (unsigned long)delay_millis, + packet ? (uint32_t)packet->getPayloadType() : 0, + packet && packet->isRouteDirect() ? "D" : "F"); +#endif + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": direct retry %s attempt=%u delay=%lu type=%u route=%s\n", + event ? event : "?", + (uint32_t)retry_attempt, + (unsigned long)delay_millis, + packet ? (uint32_t)packet->getPayloadType() : 0, + packet && packet->isRouteDirect() ? "D" : "F"); + f.close(); + } + } +} + +void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return; + } + + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) { + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 7, false, true); + } + } +} + +void MyMesh::onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { + if (next_hop_hash == NULL || next_hop_hash_len == 0) { + return; + } + + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, snr_x4, false, true); + } +} + +static void formatLocalSnrX4(char* dest, size_t dest_len, int16_t snr_x4) { + int16_t v = snr_x4; + const char* sign = ""; + if (v < 0) { + sign = "-"; + v = -v; + } + snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); +} + +void MyMesh::formatRecentRepeatersReply(char *reply, int page) { + const SimpleMeshTables* tables = static_cast(getTables()); + if (tables == NULL) { + strcpy(reply, "Error: unsupported"); + return; + } + int count = tables->getRecentRepeaterCount(); + if (count <= 0) { + strcpy(reply, "> -none-"); + return; + } + + const int page_size = 4; + int pages = (count + page_size - 1) / page_size; + if (page < 1) page = 1; + if (page > pages) page = pages; + + int len = snprintf(reply, 160, "> %d/%d ", page, pages); + int start = (page - 1) * page_size; + for (int i = 0; i < page_size && len < 150; i++) { + const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterNewestByIdx(start + i); + if (info == NULL) break; + char prefix[MAX_ROUTE_HASH_BYTES * 2 + 1]; + char snr[12]; + mesh::Utils::toHex(prefix, info->prefix, info->prefix_len); + prefix[info->prefix_len * 2] = 0; + formatLocalSnrX4(snr, sizeof(snr), info->snr_x4); + len += snprintf(&reply[len], 160 - len, "%s%s,%s", + i == 0 ? "" : " ", + prefix, + snr); + } +} + +bool MyMesh::setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + SimpleMeshTables* tables = static_cast(getTables()); + return tables != NULL && tables->setRecentRepeater(prefix, prefix_len, snr_x4, false, true); +} + +void MyMesh::clearRecentRepeaters() { + SimpleMeshTables* tables = static_cast(getTables()); + if (tables != NULL) { + tables->clearRecentRepeaters(); + } +} + bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { @@ -865,6 +1067,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; + active_bw = 0.0f; + active_sf = 0; + active_cr = 0; _logging = false; region_load_active = false; @@ -894,6 +1099,18 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') + _prefs.retry_preset = RETRY_PRESET_ROOFTOP; + _prefs.direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + _prefs.direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + _prefs.direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + _prefs.direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + _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_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; + _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + _prefs.direct_retry_cr_enabled = 1; + _prefs.direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; + _prefs.direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -962,6 +1179,9 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); @@ -1289,12 +1509,18 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr); + active_bw = pending_bw; + active_sf = pending_sf; + active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + active_bw = _prefs.bw; + active_sf = _prefs.sf; + active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index bb39c077..055b4b83 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -163,6 +163,8 @@ protected: uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override; uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override; void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override; + void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override; + void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override; int getInterferenceThreshold() const override { return _prefs.interference_threshold; @@ -194,20 +196,6 @@ protected: void onControlDataRecv(mesh::Packet* packet) override; void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); - mesh::Packet* createPacketCopy(const mesh::Packet* packet, const char* caller); - mesh::Packet* createAltPathCopy(const mesh::Packet* packet, - const uint8_t* primary_path, uint8_t primary_path_len, - const uint8_t* alt_path, uint8_t alt_path_len); - void sendFloodReplyWithAltPath(mesh::Packet* packet, - const uint8_t* direct_path, uint8_t direct_path_len, - const uint8_t* alt_path, uint8_t alt_path_len, - unsigned long delay_millis, uint8_t path_hash_size); - void sendDirectWithAltPath(mesh::Packet* packet, - const uint8_t* path, uint8_t path_len, - const uint8_t* alt_path, uint8_t alt_path_len, - uint32_t delay_millis); - void sendFloodScopedWithSelfPath(const TransportKey& scope, mesh::Packet* pkt, - uint32_t delay_millis, uint8_t path_hash_size); public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); @@ -248,6 +236,9 @@ public: void formatStatsReply(char *reply) override; void formatRadioStatsReply(char *reply) override; void formatPacketStatsReply(char *reply) override; + void formatRecentRepeatersReply(char *reply, int page) override; + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override; + void clearRecentRepeaters() override; void startRegionsLoad() override; bool saveRegions() override; void onDefaultRegionChanged(const RegionEntry* r) override; @@ -257,10 +248,7 @@ public: void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); - void handleCommand(uint32_t sender_timestamp, char* command, char* reply) { - handleCommand(sender_timestamp, NULL, command, reply); - } + void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 03a89dd4..2ce056f5 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -53,7 +53,7 @@ void setup() { halt(); } - fast_rng.begin(radio_get_rng_seed()); + fast_rng.begin(radio_driver.getRngSeed()); FILESYSTEM* fs; #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -99,6 +99,8 @@ void setup() { #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); #endif + + board.onBootComplete(); } void loop() { @@ -120,7 +122,7 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index a9f7aac0..0bac461b 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -35,6 +35,7 @@ void Mesh::begin() { _direct_retries[i].retry_at = 0; _direct_retries[i].retry_delay = 0; _direct_retries[i].retry_attempts_sent = 0; + _direct_retries[i].next_hop_hash_len = 0; _direct_retries[i].priority = 0; _direct_retries[i].progress_marker = 0; _direct_retries[i].expect_path_growth = false; @@ -63,6 +64,7 @@ void Mesh::loop() { : (uint32_t)(_ms->getMillis() - _direct_retries[i].retry_started_at); onDirectRetryEvent("failed_all_tries", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); onDirectRetryEvent("failure", _direct_retries[i].packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); + onDirectRetryFailed(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len); clearDirectRetrySlot(i); continue; } @@ -183,12 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (canDecodeDirectPayloadForSelf(pkt)) { - // Some path sources include the final node hash, and some packets are - // heard before all planned hops are consumed. Only stop forwarding once - // this node proves it can decrypt the payload. - removePathPrefix(pkt, pkt->getPathHashCount()); - } else if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { if (allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); @@ -479,13 +476,10 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { if (!packet->isMarkedDoNotRetransmit()) { - uint32_t crc; - memcpy(&crc, packet->payload, 4); - uint8_t extra = getExtraAckTransmitCount(); while (extra > 0) { delay_millis += getDirectRetransmitDelay(packet) + 300; - auto a1 = createMultiAck(crc, extra); + auto a1 = createMultiAck(packet->payload, packet->payload_len, extra); if (a1) { a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; @@ -496,7 +490,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { extra--; } - auto a2 = createAck(crc); + auto a2 = createAck(packet->payload, packet->payload_len); if (a2) { a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; @@ -508,9 +502,6 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { } void Mesh::clearDirectRetrySlot(int idx) { - if (_direct_retries[idx].waiting_final_echo && _direct_retries[idx].packet != NULL) { - releasePacket(_direct_retries[idx].packet); - } _direct_retries[idx].packet = NULL; _direct_retries[idx].trigger_packet = NULL; _direct_retries[idx].retry_started_at = 0; @@ -518,6 +509,8 @@ void Mesh::clearDirectRetrySlot(int idx) { _direct_retries[idx].retry_at = 0; _direct_retries[idx].retry_delay = 0; _direct_retries[idx].retry_attempts_sent = 0; + memset(_direct_retries[idx].next_hop_hash, 0, sizeof(_direct_retries[idx].next_hop_hash)); + _direct_retries[idx].next_hop_hash_len = 0; _direct_retries[idx].priority = 0; _direct_retries[idx].progress_marker = 0; _direct_retries[idx].expect_path_growth = false; @@ -558,6 +551,7 @@ bool Mesh::cancelDirectRetryOnEcho(const Packet* packet) { } int8_t echo_snr_x4 = packet->_snr; + onDirectRetrySucceeded(_direct_retries[i].next_hop_hash, _direct_retries[i].next_hop_hash_len, echo_snr_x4); if (_direct_retries[i].queued || _direct_retries[i].waiting_final_echo) { if (_direct_retries[i].packet != NULL) { // Success quality comes from the received downstream echo, not the original upstream RX. @@ -620,16 +614,9 @@ void Mesh::armDirectRetryOnSendComplete(const Packet* packet) { max_attempts = DIRECT_RETRY_MAX_ATTEMPTS_HARD_MAX; } if (_direct_retries[i].retry_attempts_sent >= max_attempts) { - Packet* final_wait = obtainNewPacket(); - if (final_wait == NULL) { - onDirectRetryEvent("dropped_no_packet", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - onDirectRetryEvent("failure", packet, elapsed_millis, _direct_retries[i].retry_attempts_sent); - clearDirectRetrySlot(i); - continue; - } - - *final_wait = *packet; - _direct_retries[i].packet = final_wait; + // Dispatcher releases the retry packet after this hook. Keep only retry metadata + // for the final echo window so pool exhaustion cannot force a premature failure. + _direct_retries[i].packet = NULL; _direct_retries[i].retry_at = futureMillis(_direct_retries[i].retry_delay); _direct_retries[i].waiting_final_echo = true; _direct_retries[i].queued = false; @@ -783,62 +770,6 @@ bool Mesh::getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_h } } -bool Mesh::canDecodeDirectPayloadForSelf(const Packet* packet) { - if (packet == NULL || !packet->isRouteDirect() || packet->getPathHashCount() == 0 || packet->payload_len < 1) { - return false; - } - - switch (packet->getPayloadType()) { - case PAYLOAD_TYPE_PATH: - case PAYLOAD_TYPE_REQ: - case PAYLOAD_TYPE_RESPONSE: - case PAYLOAD_TYPE_TXT_MSG: { - if (packet->payload_len < 2) { - return false; - } - - int i = 0; - uint8_t dest_hash = packet->payload[i++]; - uint8_t src_hash = packet->payload[i++]; - if (i + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { - return false; - } - - int num = searchPeersByHash(&src_hash); - for (int j = 0; j < num; j++) { - uint8_t secret[PUB_KEY_SIZE]; - getPeerSharedSecret(secret, j); - - uint8_t data[MAX_PACKET_PAYLOAD]; - if (Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0) { - return true; - } - } - return false; - } - - case PAYLOAD_TYPE_ANON_REQ: { - int i = 0; - uint8_t dest_hash = packet->payload[i++]; - if (i + PUB_KEY_SIZE + CIPHER_MAC_SIZE >= packet->payload_len || !self_id.isHashMatch(&dest_hash)) { - return false; - } - - Identity sender(&packet->payload[i]); - i += PUB_KEY_SIZE; - - uint8_t secret[PUB_KEY_SIZE]; - self_id.calcSharedSecret(secret, sender); - - uint8_t data[MAX_PACKET_PAYLOAD]; - return Utils::MACThenDecrypt(secret, data, &packet->payload[i], packet->payload_len - i) > 0; - } - - default: - return false; - } -} - void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { const uint8_t* next_hop_hash; uint8_t next_hop_hash_len; @@ -849,6 +780,18 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { return; } + uint8_t retry_key[MAX_HASH_SIZE]; + calculateDirectRetryKey(packet, retry_key); + + for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { + if (_direct_retries[i].active + && memcmp(retry_key, _direct_retries[i].retry_key, MAX_HASH_SIZE) == 0 + && _direct_retries[i].progress_marker == progress_marker + && _direct_retries[i].expect_path_growth == expect_path_growth) { + return; + } + } + int slot_idx = -1; for (int i = 0; i < MAX_DIRECT_RETRY_SLOTS; i++) { if (!_direct_retries[i].active) { @@ -864,7 +807,7 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { // Only store retry metadata here; allocate the retry packet after the initial TX really completes. uint32_t retry_delay = getDirectRetryAttemptDelay(packet, 0); - calculateDirectRetryKey(packet, _direct_retries[slot_idx].retry_key); + memcpy(_direct_retries[slot_idx].retry_key, retry_key, MAX_HASH_SIZE); _direct_retries[slot_idx].packet = NULL; _direct_retries[slot_idx].trigger_packet = const_cast(packet); _direct_retries[slot_idx].retry_started_at = 0; @@ -872,6 +815,9 @@ void Mesh::maybeScheduleDirectRetry(const Packet* packet, uint8_t priority) { _direct_retries[slot_idx].retry_at = 0; _direct_retries[slot_idx].retry_delay = retry_delay; _direct_retries[slot_idx].retry_attempts_sent = 0; + memset(_direct_retries[slot_idx].next_hop_hash, 0, sizeof(_direct_retries[slot_idx].next_hop_hash)); + memcpy(_direct_retries[slot_idx].next_hop_hash, next_hop_hash, next_hop_hash_len); + _direct_retries[slot_idx].next_hop_hash_len = next_hop_hash_len; _direct_retries[slot_idx].priority = priority; _direct_retries[slot_idx].progress_marker = progress_marker; _direct_retries[slot_idx].expect_path_growth = expect_path_growth; @@ -1036,7 +982,9 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con return packet; } -Packet* Mesh::createAck(uint32_t ack_crc) { +Packet* Mesh::createAck(const uint8_t* ack_hash, uint8_t ack_len) { + if (ack_len > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime()); @@ -1044,13 +992,19 @@ Packet* Mesh::createAck(uint32_t ack_crc) { } packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later - memcpy(packet->payload, &ack_crc, 4); - packet->payload_len = 4; + memcpy(packet->payload, ack_hash, ack_len); + packet->payload_len = ack_len; return packet; } -Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { +Packet* Mesh::createAck(uint32_t ack_crc) { + return createAck((const uint8_t*)&ack_crc, 4); +} + +Packet* Mesh::createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining) { + if (ack_len + 1 > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime()); @@ -1059,12 +1013,16 @@ Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK; - memcpy(&packet->payload[1], &ack_crc, 4); - packet->payload_len = 5; + memcpy(&packet->payload[1], ack_hash, ack_len); + packet->payload_len = ack_len + 1; return packet; } +Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) { + return createMultiAck((const uint8_t*)&ack_crc, 4, remaining); +} + Packet* Mesh::createRawData(const uint8_t* data, size_t len) { if (len > sizeof(Packet::payload)) return NULL; // invalid arg diff --git a/src/Mesh.h b/src/Mesh.h index 314db237..d1b66f62 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -29,6 +29,10 @@ public: * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { + RNG* _rng; + RTCClock* _rtc; + MeshTables* _tables; + struct DirectRetryEntry { Packet* packet; Packet* trigger_packet; @@ -38,6 +42,8 @@ class Mesh : public Dispatcher { uint32_t retry_delay; uint8_t retry_attempts_sent; uint8_t retry_key[MAX_HASH_SIZE]; + uint8_t next_hop_hash[MAX_HASH_SIZE]; + uint8_t next_hop_hash_len; uint8_t priority; uint8_t progress_marker; bool expect_path_growth; @@ -58,7 +64,6 @@ class Mesh : public Dispatcher { void clearPendingDirectRetryOnSendFail(const Packet* packet); bool getDirectRetryTarget(const Packet* packet, const uint8_t*& next_hop_hash, uint8_t& next_hop_hash_len, uint8_t& progress_marker, bool& expect_path_growth) const; - bool canDecodeDirectPayloadForSelf(const Packet* packet); void maybeScheduleDirectRetry(const Packet* packet, uint8_t priority); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); @@ -133,6 +138,16 @@ protected: */ virtual void onDirectRetryEvent(const char* event, const Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { } + /** + * \brief Optional hook for link-quality feedback when all direct-retry attempts fail. + */ + virtual void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) { } + + /** + * \brief Optional hook for link-quality feedback when a direct-retry echo is heard. + */ + virtual void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) { } + /** * \brief Optional hook to set local-only transmit options on a retry packet before it is queued. */ @@ -253,7 +268,9 @@ public: Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len); Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len); Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len); + Packet* createAck(const uint8_t* ack_hash, uint8_t ack_len); Packet* createAck(uint32_t ack_crc); + Packet* createMultiAck(const uint8_t* ack_hash, uint8_t ack_len, uint8_t remaining); Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining); Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1d880823..12823827 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,7 +1,5 @@ #include "ClientACL.h" -static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1; - static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _fs->remove(filename); @@ -30,7 +28,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { uint8_t unused[2]; memset(&c, 0, sizeof(c)); - c.alt_path_len = OUT_PATH_UNKNOWN; bool success = (file.read(pub_key, 32) == 32); success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); @@ -39,10 +36,6 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); success = success && (file.read(c.out_path, 64) == 64); success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below - if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) { - success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1); - success = success && (file.read(c.alt_path, 64) == 64); - } if (!success) break; // EOF @@ -64,8 +57,7 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { File file = openWrite(_fs, "/s_contacts"); if (file) { uint8_t unused[2]; - unused[0] = CONTACT_RECORD_VERSION_ALT_PATH; - unused[1] = 0; + memset(unused, 0, sizeof(unused)); for (int i = 0; i < num_clients; i++) { auto c = &clients[i]; @@ -78,8 +70,6 @@ void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); - success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1); - success = success && (file.write(c->alt_path, 64) == 64); if (!success) break; // write failed } @@ -125,7 +115,6 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { c->permissions = init_perms; c->id = id; c->out_path_len = OUT_PATH_UNKNOWN; - c->alt_path_len = OUT_PATH_UNKNOWN; return c; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 356574de..b758f706 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,16 +10,13 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_FORCE_FLOOD 0xFE -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; uint8_t permissions; uint8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; - uint8_t alt_path_len; - uint8_t alt_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 82e53743..687b7f7a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -27,6 +27,113 @@ static bool isValidName(const char *n) { return true; } +static bool looksNumeric(const char* s) { + if (s == NULL) return false; + while (*s == ' ') s++; + if (*s == '-' || *s == '+') s++; + bool saw_digit = false; + while (*s) { + if (*s >= '0' && *s <= '9') { + saw_digit = true; + } else if (*s != '.') { + break; + } + s++; + } + return saw_digit; +} + +static int16_t parseSnrDbX4(const char* s) { + float db = atof(s); + return (int16_t)(db * 4.0f + (db >= 0.0f ? 0.5f : -0.5f)); +} + +static void formatSnrDbX4(char* dest, size_t dest_len, int16_t snr_x4) { + int16_t v = snr_x4; + const char* sign = ""; + if (v < 0) { + sign = "-"; + v = -v; + } + snprintf(dest, dest_len, "%s%d.%02d", sign, v / 4, (v % 4) * 25); +} + +static const char* retryPresetName(uint8_t preset) { + switch (preset) { + case RETRY_PRESET_INFRA: return "infra"; + case RETRY_PRESET_ROOFTOP: return "rooftop"; + case RETRY_PRESET_MOBILE: return "mobile"; + default: return "custom"; + } +} + +static void markDirectRetryPrefsValid(NodePrefs* prefs) { + prefs->direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; + prefs->direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; +} + +static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) { + prefs->retry_preset = preset; + if (preset == RETRY_PRESET_INFRA) { + prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_INFRA_MARGIN_X4; + } else if (preset == RETRY_PRESET_MOBILE) { + prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_MOBILE_MARGIN_X4; + } else { + prefs->retry_preset = RETRY_PRESET_ROOFTOP; + prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; + prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; + prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; + prefs->direct_retry_snr_margin_x4 = DIRECT_RETRY_ROOFTOP_MARGIN_X4; + } + markDirectRetryPrefsValid(prefs); +} + +static void setDefaultDirectRetryPrefs(NodePrefs* prefs) { + applyDirectRetryPreset(prefs, RETRY_PRESET_ROOFTOP); + prefs->direct_retry_cr_enabled = 1; + 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_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; + prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + markDirectRetryPrefsValid(prefs); +} + +static bool directRetryPrefsValid(const NodePrefs* prefs) { + return prefs->direct_retry_prefs_magic[0] == DIRECT_RETRY_PREFS_MAGIC_0 + && prefs->direct_retry_prefs_magic[1] == DIRECT_RETRY_PREFS_MAGIC_1; +} + +static bool parseRetryPreset(const char* s, uint8_t& preset) { + if (strcmp(s, "infra") == 0 || strcmp(s, "0") == 0) { + preset = RETRY_PRESET_INFRA; + return true; + } + if (strcmp(s, "rooftop") == 0 || strcmp(s, "1") == 0) { + preset = RETRY_PRESET_ROOFTOP; + return true; + } + if (strcmp(s, "mobile") == 0 || strcmp(s, "2") == 0) { + preset = RETRY_PRESET_MOBILE; + return true; + } + return false; +} + +static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) { + size_t hex_len = strlen(text); + if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) { + return false; + } + prefix_len = hex_len / 2; + return mesh::Utils::fromHex(prefix, prefix_len, text); +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -93,7 +200,18 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 + file.read((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 + file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 + file.read((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 + file.read((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 + file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 + file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 + file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 + file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 + file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 + // next: 310 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -125,6 +243,17 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean + if (!directRetryPrefsValid(_prefs)) { + setDefaultDirectRetryPrefs(_prefs); + } + if (_prefs->retry_preset > RETRY_PRESET_MOBILE && _prefs->retry_preset != RETRY_PRESET_CUSTOM) { + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + } + _prefs->direct_retry_attempts = constrain(_prefs->direct_retry_attempts, 1, 15); + _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000); + _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000); + _prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160); + _prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1); file.close(); } @@ -190,7 +319,19 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + markDirectRetryPrefsValid(_prefs); + file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 295 + file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 296 + file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 297 + file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 299 + file.write((uint8_t *)&_prefs->direct_retry_snr_margin_x4, sizeof(_prefs->direct_retry_snr_margin_x4)); // 301 + file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 303 + file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 + file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 + file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 + file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 + file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 + // next: 310 file.close(); } @@ -301,6 +442,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); + } else if (memcmp(command, "clear recent.repeater", 21) == 0 && (command[21] == 0 || command[21] == ' ')) { + _callbacks->clearRecentRepeaters(); + strcpy(reply, "OK"); } else if (memcmp(command, "get ", 4) == 0) { handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { @@ -680,6 +824,104 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, must be 0-2"); } + } else if (memcmp(config, "retry.preset ", 13) == 0) { + uint8_t preset; + if (parseRetryPreset(&config[13], preset)) { + applyDirectRetryPreset(_prefs, preset); + savePrefs(); + sprintf(reply, "OK - %s", retryPresetName(_prefs->retry_preset)); + } else { + strcpy(reply, "Error, must be infra, rooftop, or mobile"); + } + } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { + if (!looksNumeric(&config[20])) { + strcpy(reply, "Error, must be 0-40 dB"); + } else { + int16_t margin_x4 = parseSnrDbX4(&config[20]); + if (margin_x4 >= 0 && margin_x4 <= 160) { + _prefs->direct_retry_snr_margin_x4 = (uint16_t)margin_x4; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-40 dB"); + } + } + } else if (memcmp(config, "direct.retry.count ", 19) == 0) { + int attempts = _atoi(&config[19]); + if (attempts >= 1 && attempts <= 15) { + _prefs->direct_retry_attempts = (uint8_t)attempts; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 1-15"); + } + } else if (memcmp(config, "direct.retry.base ", 18) == 0) { + int base_ms = _atoi(&config[18]); + if (base_ms >= 10 && base_ms <= 5000) { + _prefs->direct_retry_base_ms = (uint16_t)base_ms; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 10-5000 ms"); + } + } else if (memcmp(config, "direct.retry.step ", 18) == 0) { + int step_ms = _atoi(&config[18]); + if (step_ms >= 0 && step_ms <= 5000) { + _prefs->direct_retry_step_ms = (uint16_t)step_ms; + _prefs->retry_preset = RETRY_PRESET_CUSTOM; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0-5000 ms"); + } + } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { + if (memcmp(&config[16], "off", 3) == 0) { + _prefs->direct_retry_cr_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(tmp, &config[16]); + const char *parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4, ','); + if (num == 4 && looksNumeric(parts[0]) && looksNumeric(parts[1]) && looksNumeric(parts[2]) && looksNumeric(parts[3])) { + int16_t cr4 = parseSnrDbX4(parts[0]); + int16_t cr5 = parseSnrDbX4(parts[1]); + int16_t cr7 = parseSnrDbX4(parts[2]); + int16_t cr8 = parseSnrDbX4(parts[3]); + if (cr4 >= -128 && cr4 <= 127 && cr5 >= -128 && cr5 <= 127 && cr7 >= -128 && cr7 <= 127 && cr8 >= -128 && cr8 <= 127) { + _prefs->direct_retry_cr4_snr_x4 = (int8_t)cr4; + _prefs->direct_retry_cr5_snr_x4 = (int8_t)cr5; + _prefs->direct_retry_cr7_snr_x4 = (int8_t)cr7; + _prefs->direct_retry_cr8_snr_x4 = (int8_t)cr8; + _prefs->direct_retry_cr_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); + } + } else { + strcpy(reply, "Error, use CR4,CR5,CR7,CR8 SNRs or off"); + } + } + } else if (memcmp(config, "recent.repeater ", 16) == 0) { + strcpy(tmp, &config[16]); + const char *parts[2]; + int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); + uint8_t prefix[MAX_HASH_SIZE]; + uint8_t prefix_len = 0; + int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0; + if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) { + strcpy(reply, "Error, use: set recent.repeater "); + } else if (snr_x4 < -128 || snr_x4 > 127) { + strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); + } else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, table rejected prefix"); + } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; char *dp = _prefs->owner_info; @@ -862,6 +1104,37 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); } else if (memcmp(config, "direct.txdelay", 14) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); + } else if (memcmp(config, "retry.preset", 12) == 0) { + sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); + } else if (memcmp(config, "direct.retry.margin", 19) == 0) { + char margin[12]; + formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); + sprintf(reply, "> %s", margin); + } else if (memcmp(config, "direct.retry.count", 18) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_attempts); + } else if (memcmp(config, "direct.retry.base", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_base_ms); + } else if (memcmp(config, "direct.retry.step", 17) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->direct_retry_step_ms); + } else if (memcmp(config, "direct.retry.cr", 15) == 0) { + if (!_prefs->direct_retry_cr_enabled) { + strcpy(reply, "> off"); + } else { + char cr4[12], cr5[12], cr7[12], cr8[12]; + formatSnrDbX4(cr4, sizeof(cr4), _prefs->direct_retry_cr4_snr_x4); + formatSnrDbX4(cr5, sizeof(cr5), _prefs->direct_retry_cr5_snr_x4); + formatSnrDbX4(cr7, sizeof(cr7), _prefs->direct_retry_cr7_snr_x4); + formatSnrDbX4(cr8, sizeof(cr8), _prefs->direct_retry_cr8_snr_x4); + sprintf(reply, "> %s,%s,%s,%s", cr4, cr5, cr7, cr8); + } + } else if (memcmp(config, "recent.repeater", 15) == 0) { + int page = 1; + const char* cursor = &config[15]; + while (*cursor == ' ') cursor++; + if (memcmp(cursor, "page ", 5) == 0) cursor += 5; + if (*cursor) page = _atoi(cursor); + if (page < 1) page = 1; + _callbacks->formatRecentRepeatersReply(reply, page); } else if (memcmp(config, "owner.info", 10) == 0) { auto start = reply; *reply++ = '>'; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 10cb00c7..7fa71405 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,34 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 +#define RETRY_PRESET_INFRA 0 +#define RETRY_PRESET_ROOFTOP 1 +#define RETRY_PRESET_MOBILE 2 +#define RETRY_PRESET_CUSTOM 0xFF + +#define DIRECT_RETRY_INFRA_BASE_MS 275 +#define DIRECT_RETRY_INFRA_COUNT 4 +#define DIRECT_RETRY_INFRA_STEP_MS 150 +#define DIRECT_RETRY_INFRA_MARGIN_X4 60 + +#define DIRECT_RETRY_ROOFTOP_BASE_MS 175 +#define DIRECT_RETRY_ROOFTOP_COUNT 15 +#define DIRECT_RETRY_ROOFTOP_STEP_MS 100 +#define DIRECT_RETRY_ROOFTOP_MARGIN_X4 20 + +#define DIRECT_RETRY_MOBILE_BASE_MS 175 +#define DIRECT_RETRY_MOBILE_COUNT 15 +#define DIRECT_RETRY_MOBILE_STEP_MS 50 +#define DIRECT_RETRY_MOBILE_MARGIN_X4 0 + +#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 +#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 +#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10 +#define DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT 10 + +#define DIRECT_RETRY_PREFS_MAGIC_0 0xD1 +#define DIRECT_RETRY_PREFS_MAGIC_1 0x52 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -65,6 +93,17 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + uint8_t retry_preset; + uint8_t direct_retry_attempts; + uint16_t direct_retry_base_ms; + uint16_t direct_retry_step_ms; + uint16_t direct_retry_snr_margin_x4; + int8_t direct_retry_cr4_snr_x4; + int8_t direct_retry_cr5_snr_x4; + int8_t direct_retry_cr7_snr_x4; + int8_t direct_retry_cr8_snr_x4; + uint8_t direct_retry_cr_enabled; + uint8_t direct_retry_prefs_magic[2]; }; class CommonCLICallbacks { @@ -88,6 +127,18 @@ public: virtual void formatStatsReply(char *reply) = 0; virtual void formatRadioStatsReply(char *reply) = 0; virtual void formatPacketStatsReply(char *reply) = 0; + virtual void formatRecentRepeatersReply(char *reply, int page) { + (void)page; + if (reply != NULL) reply[0] = 0; + } + virtual bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { + (void)prefix; + (void)prefix_len; + (void)snr_x4; + return false; + } + virtual void clearRecentRepeaters() { + } virtual mesh::LocalIdentity& getSelfId() = 0; virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2d2125dd..f1d52733 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -9,8 +9,7 @@ #include #endif -#define MAX_PACKET_HASHES 128 -#define MAX_PACKET_ACKS 64 +#define MAX_PACKET_HASHES (128+32) #ifndef MAX_RECENT_REPEATERS // Platform defaults. Can be overridden with -D MAX_RECENT_REPEATERS=. #if defined(ESP32) || defined(ESP32_PLATFORM) @@ -39,8 +38,6 @@ public: private: uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; int _next_idx; - uint32_t _acks[MAX_PACKET_ACKS]; - int _next_ack_idx; uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; int _next_recent_repeater_idx; @@ -48,20 +45,6 @@ private: RecentRepeaterAllowFn _recent_repeater_allow_fn; void* _recent_repeater_allow_ctx; - bool hasSeenAck(uint32_t ack) const { - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - return true; - } - } - return false; - } - - void storeAck(uint32_t ack) { - _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; - } - bool hasSeenHash(const uint8_t* hash) const { const uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { @@ -148,8 +131,6 @@ public: SimpleMeshTables() { memset(_hashes, 0, sizeof(_hashes)); _next_idx = 0; - memset(_acks, 0, sizeof(_acks)); - _next_ack_idx = 0; _direct_dups = _flood_dups = 0; memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); _next_recent_repeater_idx = 0; @@ -162,8 +143,6 @@ public: void restoreFrom(File f) { f.read(_hashes, sizeof(_hashes)); f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); - f.read((uint8_t *) &_acks[0], sizeof(_acks)); - f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); // Recent repeater entries are intentionally not restored across boots. // This avoids struct-layout migration issues and keeps stale path quality // stats from persisting indefinitely. @@ -173,31 +152,10 @@ public: void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); - f.write((const uint8_t *) &_acks[0], sizeof(_acks)); - f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); } #endif bool hasSeen(const mesh::Packet* packet) override { - if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { - recordRecentRepeater(packet); - - uint32_t ack; - memcpy(&ack, packet->payload, 4); - - if (hasSeenAck(ack)) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; - } - - storeAck(ack); - return false; - } - uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); @@ -217,15 +175,6 @@ public: void markSent(const mesh::Packet* packet) override { // Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves. - if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack; - memcpy(&ack, packet->payload, 4); - if (!hasSeenAck(ack)) { - storeAck(ack); - } - return; - } - uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); if (!hasSeenHash(hash)) { @@ -234,25 +183,14 @@ public: } void clear(const mesh::Packet* packet) override { - if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { - uint32_t ack; - memcpy(&ack, packet->payload, 4); - for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { - _acks[i] = 0; - break; - } - } - } else { - uint8_t hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(hash); + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); - uint8_t* sp = _hashes; - for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - memset(sp, 0, MAX_HASH_SIZE); - break; - } + uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + memset(sp, 0, MAX_HASH_SIZE); + break; } } } @@ -282,17 +220,13 @@ public: return false; } - // Keep one slot for overlapping prefixes so 1/2/3-byte paths share the same entry. + // Keep exact prefixes distinct so a 1-byte path prefix does not collapse + // independent 2/3-byte repeaters that share the same first byte. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { continue; } - if (prefix_len > existing.prefix_len) { - memset(existing.prefix, 0, sizeof(existing.prefix)); - memcpy(existing.prefix, prefix, prefix_len); - existing.prefix_len = prefix_len; - } if (snr_locked) { existing.snr_x4 = snr_x4; existing.snr_locked = 1; @@ -352,14 +286,9 @@ public: for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { RecentRepeaterInfo& existing = _recent_repeaters[i]; - if (existing.prefix_len == 0 || !prefixesOverlap(existing.prefix, existing.prefix_len, prefix, prefix_len)) { + if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { continue; } - if (prefix_len > existing.prefix_len) { - memset(existing.prefix, 0, sizeof(existing.prefix)); - memcpy(existing.prefix, prefix, prefix_len); - existing.prefix_len = prefix_len; - } if (!existing.snr_locked) { int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; if (lowered < -128) { @@ -432,18 +361,25 @@ public: return NULL; } - // Search newest-to-oldest and allow 1/2/3-byte prefixes to overlap-match. + // Prefer exact matches. If none exists, fall back to the newest longest + // overlapping prefix so coarse learned prefixes can still inform CR. + const RecentRepeaterInfo* best = NULL; for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; const RecentRepeaterInfo* info = &_recent_repeaters[idx]; if (info->prefix_len == 0) { continue; } - if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { + if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) { return info; } + if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { + if (best == NULL || info->prefix_len > best->prefix_len) { + best = info; + } + } } - return NULL; + return best; } void clearRecentRepeaters() { memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); From cb5e2e7c3d49ef921a63ce446918958ed46ff12d Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 17:42:53 -0700 Subject: [PATCH 14/25] Document direct retry CLI --- docs/cli_commands.md | 224 ++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.cpp | 7 +- src/helpers/CommonCLI.cpp | 30 +++- src/helpers/CommonCLI.h | 1 + 4 files changed, 255 insertions(+), 7 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c06f5e12..2d31a18b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -950,6 +950,230 @@ region save - Enables flooding for all child regions automatically - Useful for global networks with specific regional rules +--- +### Direct Retry + +Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages and TRACE packets. It does not change ACK handling. + +#### View or change direct retry state +**Usage:** +- `get direct.retry` +- `set direct.retry ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `on` + +**Examples:** +``` +get direct.retry +set direct.retry on +set direct.retry off +``` + +--- + +#### View or apply a direct retry preset +**Usage:** +- `get retry.preset` +- `set retry.preset ` + +**Parameters:** +- `preset`: `infra`|`rooftop`|`mobile` + +**Notes:** +- `infra`: fewer, slower retries for stable fixed infrastructure. +- `rooftop`: default long retry window for weak rooftop links. +- `mobile`: long retry count with shorter spacing for moving or changing links. +- Changing `direct.retry.count`, `direct.retry.base`, `direct.retry.step`, or `direct.retry.margin` makes the preset report as `custom`. + +**Examples:** +``` +get retry.preset +set retry.preset infra +set retry.preset rooftop +set retry.preset mobile +``` + +--- + +#### View or change direct retry count +**Usage:** +- `get direct.retry.count` +- `set direct.retry.count ` + +**Parameters:** +- `count`: Maximum retry attempts after the original send, from `1` to `15`. + +**Default:** `15` with the `rooftop` preset + +**Examples:** +``` +get direct.retry.count +set direct.retry.count 1 +set direct.retry.count 4 +set direct.retry.count 15 +``` + +--- + +#### View or change direct retry base delay +**Usage:** +- `get direct.retry.base` +- `set direct.retry.base ` + +**Parameters:** +- `ms`: First retry wait in milliseconds, from `10` to `5000`. + +**Default:** `175` with the `rooftop` preset + +**Explanation:** +- The first retry waits `base` milliseconds after the failed echo window. +- Larger values reduce channel pressure and give slow repeaters more time. +- Smaller values recover faster but create tighter retry bursts. + +**Examples:** +``` +get direct.retry.base +set direct.retry.base 175 +set direct.retry.base 275 +set direct.retry.base 500 +``` + +--- + +#### View or change direct retry step delay +**Usage:** +- `get direct.retry.step` +- `set direct.retry.step ` + +**Parameters:** +- `ms`: Extra milliseconds added for each subsequent retry, from `0` to `5000`. + +**Default:** `100` with the `rooftop` preset + +**Explanation:** +- Retry delay is `base + attempt_index * step`. +- With `base=175` and `step=100`, retries wait about `175`, `275`, `375`, `475` ms, and so on. +- `step=0` keeps every retry at the same delay. +- Larger steps spread retries over time and are safer on busy channels. + +**Examples:** +``` +get direct.retry.step +set direct.retry.step 0 +set direct.retry.step 50 +set direct.retry.step 100 +set direct.retry.step 250 +``` + +--- + +#### View or change direct retry SNR margin +**Usage:** +- `get direct.retry.margin` +- `set direct.retry.margin ` + +**Parameters:** +- `snr_db`: Extra SNR margin above the SF receive floor, from `0` to `40`. + +**Default:** `5.00` with the `rooftop` preset + +**Notes:** +- Unknown repeaters are still retried. +- Known repeaters below the receive floor plus this margin are skipped. +- Failed attempts lower the recent repeater SNR estimate by `0.25 dB`. + +**Examples:** +``` +get direct.retry.margin +set direct.retry.margin 0 +set direct.retry.margin 2.5 +set direct.retry.margin 5 +set direct.retry.margin 10 +``` + +--- + +#### View or change adaptive direct retry coding rate +**Usage:** +- `get direct.retry.cr` +- `set direct.retry.cr off` +- `set direct.retry.cr ,,,` + +**Parameters:** +- `cr4_min`: Minimum SNR in dB to retry at CR4. +- `cr5_min`: Minimum SNR in dB to retry at CR5. +- `cr7_min`: Minimum SNR in dB to retry at CR7. +- `cr8_max`: Maximum SNR in dB that forces CR8. + +**Default:** `10.00,7.50,2.50,2.50` + +**Explanation:** +- Higher SNR uses faster coding rates. +- Lower SNR uses more robust coding rates. +- CR6 is intentionally skipped. +- `off` disables per-packet retry CR overrides and uses the current radio CR. +- Unknown repeaters start at `+2.00 dB` for adaptive CR selection. +- A failed unknown repeater is seeded at `+1.75 dB`. +- Each later failure lowers the SNR estimate by `0.25 dB`. + +**Examples:** +``` +get direct.retry.cr +set direct.retry.cr off +set direct.retry.cr 10.0,7.5,2.5,2.5 +set direct.retry.cr 12.0,8.0,4.0,1.0 +set direct.retry.cr 8.0,5.0,1.5,0 +set direct.retry.cr 6.0,3.0,0,-2.0 +set direct.retry.cr 20.0,12.0,6.0,2.0 +set direct.retry.cr 4.0,2.0,0,-4.0 +``` + +**Example profiles:** +- Conservative weak-link profile: +``` +set direct.retry.cr 12.0,8.0,4.0,1.0 +``` +- Balanced rooftop profile: +``` +set direct.retry.cr 10.0,7.5,2.5,2.5 +``` +- Faster strong-link profile: +``` +set direct.retry.cr 6.0,3.0,0,-2.0 +``` +- Very cautious noisy-link profile: +``` +set direct.retry.cr 20.0,12.0,6.0,2.0 +``` + +--- + +#### View, seed, or clear the recent repeater table +**Usage:** +- `get recent.repeater` +- `get recent.repeater ` +- `get recent.repeater page ` +- `set recent.repeater ` +- `clear recent.repeater` + +**Parameters:** +- `prefix`: Repeater path-hash prefix as hex. +- `snr_db`: SNR in dB. +- `page`: 1-based result page. + +**Examples:** +``` +get recent.repeater +get recent.repeater 2 +get recent.repeater page 3 +set recent.repeater A1B2C3 8.5 +set recent.repeater 71CE82 -3.25 +clear recent.repeater +``` + --- ### GPS (When GPS support is compiled in) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 45f81ed2..f8946e64 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -575,7 +575,8 @@ 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_cr7_snr_x4) return 7; - return 8; + if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) return 8; + return 7; } uint8_t MyMesh::getDirectRetryPreset() const { @@ -592,6 +593,9 @@ uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const { (void)packet; + if (!_prefs.direct_retry_enabled) { + return false; + } if (next_hop_hash == NULL || next_hop_hash_len == 0) { return true; } @@ -1108,6 +1112,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + _prefs.direct_retry_enabled = 1; _prefs.direct_retry_cr_enabled = 1; _prefs.direct_retry_prefs_magic[0] = DIRECT_RETRY_PREFS_MAGIC_0; _prefs.direct_retry_prefs_magic[1] = DIRECT_RETRY_PREFS_MAGIC_1; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 687b7f7a..4dfbbf6d 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -101,6 +101,7 @@ static void setDefaultDirectRetryPrefs(NodePrefs* prefs) { prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; + prefs->direct_retry_enabled = 1; markDirectRetryPrefsValid(prefs); } @@ -209,9 +210,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 - file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 - file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 - // next: 310 + file.read((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 + file.read((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 + file.read((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 + // next: 311 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -253,6 +255,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->direct_retry_base_ms = constrain(_prefs->direct_retry_base_ms, 10, 5000); _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, 0, 5000); _prefs->direct_retry_snr_margin_x4 = constrain(_prefs->direct_retry_snr_margin_x4, 0, 160); + _prefs->direct_retry_enabled = constrain(_prefs->direct_retry_enabled, 0, 1); _prefs->direct_retry_cr_enabled = constrain(_prefs->direct_retry_cr_enabled, 0, 1); file.close(); @@ -329,9 +332,10 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 304 file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 305 file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 306 - file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 307 - file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 308 - // next: 310 + file.write((uint8_t *)&_prefs->direct_retry_enabled, sizeof(_prefs->direct_retry_enabled)); // 307 + file.write((uint8_t *)&_prefs->direct_retry_cr_enabled, sizeof(_prefs->direct_retry_cr_enabled)); // 308 + file.write((uint8_t *)&_prefs->direct_retry_prefs_magic, sizeof(_prefs->direct_retry_prefs_magic)); // 309 + // next: 311 file.close(); } @@ -833,6 +837,18 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, must be infra, rooftop, or mobile"); } + } else if (memcmp(config, "direct.retry ", 13) == 0) { + if (memcmp(&config[13], "on", 2) == 0) { + _prefs->direct_retry_enabled = 1; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(&config[13], "off", 3) == 0) { + _prefs->direct_retry_enabled = 0; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be on or off"); + } } else if (memcmp(config, "direct.retry.margin ", 20) == 0) { if (!looksNumeric(&config[20])) { strcpy(reply, "Error, must be 0-40 dB"); @@ -1106,6 +1122,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->direct_tx_delay_factor)); } else if (memcmp(config, "retry.preset", 12) == 0) { sprintf(reply, "> %s", retryPresetName(_prefs->retry_preset)); + } else if (memcmp(config, "direct.retry", 12) == 0 && (config[12] == 0 || config[12] == ' ')) { + sprintf(reply, "> %s", _prefs->direct_retry_enabled ? "on" : "off"); } else if (memcmp(config, "direct.retry.margin", 19) == 0) { char margin[12]; formatSnrDbX4(margin, sizeof(margin), _prefs->direct_retry_snr_margin_x4); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 7fa71405..76a7cf46 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -102,6 +102,7 @@ struct NodePrefs { // persisted to file int8_t direct_retry_cr5_snr_x4; int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; + uint8_t direct_retry_enabled; uint8_t direct_retry_cr_enabled; uint8_t direct_retry_prefs_magic[2]; }; From 21c5e2701bf771f0fd5de638a57edf3104dbbd1c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 18:06:58 -0700 Subject: [PATCH 15/25] Tune unknown retry SNR --- docs/cli_commands.md | 13 +++++++++++-- examples/simple_repeater/MyMesh.cpp | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 2d31a18b..6adcc4c7 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1115,8 +1115,8 @@ set direct.retry.margin 10 - Lower SNR uses more robust coding rates. - CR6 is intentionally skipped. - `off` disables per-packet retry CR overrides and uses the current radio CR. -- Unknown repeaters start at `+2.00 dB` for adaptive CR selection. -- A failed unknown repeater is seeded at `+1.75 dB`. +- Unknown repeaters start at `+3.00 dB` for adaptive CR selection. +- A failed unknown repeater is seeded at `+2.75 dB`. - Each later failure lowers the SNR estimate by `0.25 dB`. **Examples:** @@ -1164,6 +1164,15 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 - `snr_db`: SNR in dB. - `page`: 1-based result page. +**SNR details:** +- Recent repeater SNR is stored internally in quarter-dB units. +- Heard repeater samples update an existing table entry with a weighted blend: `75%` existing SNR and `25%` new heard SNR, rounded up. +- Direct retry success also feeds the heard echo SNR back into the same weighted table. +- Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by `0.25 dB`. +- Unknown repeaters start at `+3.00 dB` for adaptive CR selection. +- If an unknown repeater fails, it is seeded into the table at `+2.75 dB`. +- `set recent.repeater ` seeds a missing prefix or adds another weighted sample for an existing prefix. + **Examples:** ``` get recent.repeater diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f8946e64..05c308bc 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -606,7 +606,7 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho if (repeater == NULL) { // Retry unknown repeaters too. If they fail, onDirectRetryFailed() seeds the - // recent-repeater table below the +2.00 dB starting point. + // recent-repeater table below the +3.00 dB starting point. return true; } int16_t retry_floor_x4 = (int16_t)getDirectRetryMinSNRX4() + (int16_t)_prefs.direct_retry_snr_margin_x4; @@ -615,7 +615,7 @@ bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_ho void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) { (void)retry_attempt; - int8_t snr_x4 = 8; // unknown repeaters start at +2.00 dB + int8_t snr_x4 = 12; // unknown repeaters start at +3.00 dB const SimpleMeshTables* tables = static_cast(getTables()); if (tables != NULL) { uint8_t prefix[MAX_HASH_SIZE]; @@ -683,7 +683,7 @@ void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_ SimpleMeshTables* tables = static_cast(getTables()); if (tables != NULL) { if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) { - tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 7, false, true); + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 11, false, true); } } } From e35886780c9d3ce6a11c38d561e2e94abb6361ac Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 18:10:40 -0700 Subject: [PATCH 16/25] Simplify recent repeater paging --- docs/cli_commands.md | 2 -- src/helpers/CommonCLI.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6adcc4c7..50a14b77 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1155,7 +1155,6 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 **Usage:** - `get recent.repeater` - `get recent.repeater ` -- `get recent.repeater page ` - `set recent.repeater ` - `clear recent.repeater` @@ -1177,7 +1176,6 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 ``` get recent.repeater get recent.repeater 2 -get recent.repeater page 3 set recent.repeater A1B2C3 8.5 set recent.repeater 71CE82 -3.25 clear recent.repeater diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 4dfbbf6d..78cd45b9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1149,7 +1149,6 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep int page = 1; const char* cursor = &config[15]; while (*cursor == ' ') cursor++; - if (memcmp(cursor, "page ", 5) == 0) cursor += 5; if (*cursor) page = _atoi(cursor); if (page < 1) page = 1; _callbacks->formatRecentRepeatersReply(reply, page); From 4ed9415866d635cb6cbfcbb9d14076276b4bc245 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 15 Jun 2026 18:15:00 -0700 Subject: [PATCH 17/25] Clean up Mesh comments --- src/Mesh.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index d1b66f62..4511b394 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -190,7 +190,6 @@ protected: /** * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) - * NOTE: these can be received multiple times (per sender), via differen routes * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned * \param secret the pre-calculated shared-secret (handy for sending response packet) * \returns true, if path was accepted and that reciprocal path should be sent @@ -213,7 +212,6 @@ protected: /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) - * NOTE: these can be received multiple times (per sender), via differen routes */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } @@ -295,12 +293,10 @@ public: void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); /** - * \brief send a locally-generated Packet to just neigbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** - * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); From 7d2ff381207c3b84ee083ebc0fe45bfb653c5151 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:15:52 -0700 Subject: [PATCH 18/25] Trim direct retry cleanup --- examples/simple_repeater/MyMesh.cpp | 13 ------------- examples/simple_repeater/MyMesh.h | 3 --- src/Mesh.cpp | 5 ++--- src/Mesh.h | 10 ++++++++-- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 05c308bc..4261edd9 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -579,10 +579,6 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { return 7; } -uint8_t MyMesh::getDirectRetryPreset() const { - return _prefs.retry_preset; -} - uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { return constrain(_prefs.direct_retry_attempts, 1, 15); } @@ -631,11 +627,6 @@ void MyMesh::configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* retry->tx_cr = getDirectRetryCodingRateForSNR(snr_x4); } -bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) { - (void)packet; - return false; -} - uint32_t MyMesh::getDirectRetryEchoDelay(const mesh::Packet* packet) const { (void)packet; return 200; @@ -1071,7 +1062,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; - active_bw = 0.0f; active_sf = 0; active_cr = 0; _logging = false; @@ -1184,7 +1174,6 @@ void MyMesh::begin(FILESYSTEM *fs) { #endif radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; radio_driver.setTxPower(_prefs.tx_power_dbm); @@ -1514,7 +1503,6 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr); - active_bw = pending_bw; active_sf = pending_sf; active_cr = pending_cr; MESH_DEBUG_PRINTLN("Temp radio params"); @@ -1523,7 +1511,6 @@ void MyMesh::loop() { if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - active_bw = _prefs.bw; active_sf = _prefs.sf; active_cr = _prefs.cr; MESH_DEBUG_PRINTLN("Radio params restored"); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 055b4b83..fddbed3e 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -110,7 +110,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long set_radio_at, revert_radio_at; float pending_freq; float pending_bw; - float active_bw; // live BW, including temporary radio overrides uint8_t pending_sf; uint8_t active_sf; // live SF, including temporary radio overrides uint8_t pending_cr; @@ -125,7 +124,6 @@ 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; void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); @@ -157,7 +155,6 @@ protected: 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; - bool maybeShortCircuitDirect(mesh::Packet* packet) 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; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0bac461b..a4b2750d 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -185,7 +185,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) || maybeShortCircuitDirect(pkt)) { + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize())) { if (allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); @@ -448,9 +448,8 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) { packet->setPathHashCount(n + 1); uint32_t d = getRetransmitDelay(packet); - uint8_t priority = packet->getPathHashCount(); // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(priority, d); // give priority to closer sources, than ones further away + return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away } return ACTION_RELEASE; } diff --git a/src/Mesh.h b/src/Mesh.h index 4511b394..523c204b 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -111,8 +111,6 @@ protected: /** * \brief Allow subclasses to rewrite a non-TRACE DIRECT packet path when this node can safely skip ahead. */ - virtual bool maybeShortCircuitDirect(Packet* packet) { return false; } - /** * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. */ @@ -190,6 +188,8 @@ protected: /** * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) + * NOTE: these can be received multiple times (per sender), via different routes + * NOTE: these can be received multiple times (per sender), via differen routes * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned * \param secret the pre-calculated shared-secret (handy for sending response packet) * \returns true, if path was accepted and that reciprocal path should be sent @@ -212,6 +212,8 @@ protected: /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) + * NOTE: these can be received multiple times (per sender), via different routes + * NOTE: these can be received multiple times (per sender), via differen routes */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } @@ -293,10 +295,14 @@ public: void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0); /** + * \brief send a locally-generated Packet to just neighbor nodes (zero hops) + * \brief send a locally-generated Packet to just neigbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** + * \brief send a locally-generated Packet to just neighbor nodes (zero hops), with specific transport codes + * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); From f58cdef8a82d50c704af970f7d5553c83022e7f4 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:21:51 -0700 Subject: [PATCH 19/25] Revert Mesh comment noise --- src/Mesh.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mesh.h b/src/Mesh.h index 523c204b..81842aa1 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -82,7 +82,7 @@ protected: /** * \brief Called _before_ the packet is dispatched to the on..Recv() methods. - * \returns true, if given packet should be NOT be processed. + * \returns true, if given packet should NOT be processed. */ virtual bool filterRecvFloodPacket(Packet* packet) { return false; } From 4a9d6bae52f228ad7c4af90ba356c19ce5447c09 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:25:04 -0700 Subject: [PATCH 20/25] Remove duplicate Mesh comments --- src/Mesh.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index 81842aa1..04c8b84b 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -189,7 +189,6 @@ protected: /** * \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded) * NOTE: these can be received multiple times (per sender), via different routes - * NOTE: these can be received multiple times (per sender), via differen routes * \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned * \param secret the pre-calculated shared-secret (handy for sending response packet) * \returns true, if path was accepted and that reciprocal path should be sent @@ -213,7 +212,6 @@ protected: /** * \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded) * NOTE: these can be received multiple times (per sender), via different routes - * NOTE: these can be received multiple times (per sender), via differen routes */ virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { } @@ -296,13 +294,11 @@ public: /** * \brief send a locally-generated Packet to just neighbor nodes (zero hops) - * \brief send a locally-generated Packet to just neigbor nodes (zero hops) */ void sendZeroHop(Packet* packet, uint32_t delay_millis=0); /** * \brief send a locally-generated Packet to just neighbor nodes (zero hops), with specific transport codes - * \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes * \param transport_codes array of 2 codes to attach to packet */ void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); From 1f14f744af90244e6160f764d490bc1f6df28810 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:28:46 -0700 Subject: [PATCH 21/25] Clean Mesh header comments --- src/Mesh.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Mesh.h b/src/Mesh.h index 04c8b84b..b342aed2 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -29,8 +29,8 @@ public: * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. */ class Mesh : public Dispatcher { - RNG* _rng; RTCClock* _rtc; + RNG* _rng; MeshTables* _tables; struct DirectRetryEntry { @@ -104,13 +104,10 @@ protected: /** * \brief Decide whether a DIRECT packet should get one delayed retry if the next hop echo is not overheard. - * Sub-classes can use neighbour tables or other link-quality data to opt in selectively. + * Sub-classes can use recent repeater or other link-quality data to opt in selectively. */ virtual bool allowDirectRetry(const Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const; - /** - * \brief Allow subclasses to rewrite a non-TRACE DIRECT packet path when this node can safely skip ahead. - */ /** * \returns milliseconds to wait for the next-hop echo before queueing one retry of the DIRECT packet. */ From 9c510ab7a65db218fca16b101c60914d628f42fd Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 13:30:49 -0700 Subject: [PATCH 22/25] Add Packet EOF newline --- src/Packet.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Packet.cpp b/src/Packet.cpp index 3aab6349..a542a5a2 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -87,3 +87,4 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { } } + From 8d46ed4f21b6b7abc113bb102024635057d00fe1 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 16 Jun 2026 16:25:04 -0700 Subject: [PATCH 23/25] Enable retry CR radio overrides --- docs/cli_commands.md | 8 +++--- src/Dispatcher.cpp | 23 +++++++++++++--- src/Dispatcher.h | 6 ++++- src/helpers/CommonCLI.cpp | 28 ++++++++++++++++---- src/helpers/radiolib/CustomLLCC68Wrapper.h | 4 +++ src/helpers/radiolib/CustomLR1110Wrapper.h | 4 +++ src/helpers/radiolib/CustomSTM32WLxWrapper.h | 4 +++ src/helpers/radiolib/CustomSX1262Wrapper.h | 4 +++ src/helpers/radiolib/CustomSX1268Wrapper.h | 4 +++ src/helpers/radiolib/CustomSX1276Wrapper.h | 4 +++ src/helpers/radiolib/RadioLibWrappers.h | 2 ++ 11 files changed, 78 insertions(+), 13 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 50a14b77..9c816cf0 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1155,12 +1155,12 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 **Usage:** - `get recent.repeater` - `get recent.repeater ` -- `set recent.repeater ` +- `set recent.repeater [snr_db]` - `clear recent.repeater` **Parameters:** - `prefix`: Repeater path-hash prefix as hex. -- `snr_db`: SNR in dB. +- `snr_db`: Optional SNR in dB. If omitted or invalid, defaults to `3.0`. - `page`: 1-based result page. **SNR details:** @@ -1170,7 +1170,8 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 - Direct retry failure is not weighted: each final echo-timeout failure lowers that repeater's SNR by `0.25 dB`. - Unknown repeaters start at `+3.00 dB` for adaptive CR selection. - If an unknown repeater fails, it is seeded into the table at `+2.75 dB`. -- `set recent.repeater ` seeds a missing prefix or adds another weighted sample for an existing prefix. +- `set recent.repeater [snr_db]` seeds a missing prefix or adds another weighted sample for an existing prefix. +- Successful `set recent.repeater` replies include the stored prefix and SNR, for example `OK - set A1B2C3 at 3.0 SNR`. **Examples:** ``` @@ -1178,6 +1179,7 @@ get recent.repeater get recent.repeater 2 set recent.repeater A1B2C3 8.5 set recent.repeater 71CE82 -3.25 +set recent.repeater A1B2C3 clear recent.repeater ``` diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index b870be9e..63112f85 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -52,11 +52,15 @@ void Dispatcher::updateTxBudget() { } } -void Dispatcher::restoreOutboundCodingRate() { +void Dispatcher::restoreOutboundTxOverrides() { if (outbound_restore_cr != 0) { _radio->setCodingRate(outbound_restore_cr); outbound_restore_cr = 0; } + if (outbound_restore_preamble_len != 0) { + _radio->setPreambleLength(outbound_restore_preamble_len); + outbound_restore_preamble_len = 0; + } } int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { @@ -113,7 +117,7 @@ void Dispatcher::loop() { } _radio->onSendFinished(); - restoreOutboundCodingRate(); + restoreOutboundTxOverrides(); logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendComplete(outbound); if (outbound->isRouteFlood()) { @@ -127,7 +131,7 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); - restoreOutboundCodingRate(); + restoreOutboundTxOverrides(); logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); onSendFail(outbound); @@ -342,6 +346,7 @@ void Dispatcher::checkSend() { uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_restore_cr = 0; + outbound_restore_preamble_len = 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) { @@ -352,12 +357,22 @@ void Dispatcher::checkSend() { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): WARN: failed to set packet CR%d", getLogDateTime(), (uint32_t)outbound->tx_cr); } } + bool has_direct_path = outbound->getPathHashCount() > 0 + || (outbound->getPayloadType() == PAYLOAD_TYPE_TRACE && outbound->payload_len > 9); + if (outbound->isRouteDirect() && has_direct_path + && (outbound->tx_cr == 4 || outbound->tx_cr == 5)) { + uint16_t default_preamble_len = _radio->getDefaultPreambleLength(); + if (default_preamble_len > 16 && _radio->setPreambleLength(16)) { + outbound_restore_preamble_len = default_preamble_len; + 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()); - restoreOutboundCodingRate(); + restoreOutboundTxOverrides(); logTxFail(outbound, outbound->getRawLength()); onSendFail(outbound); diff --git a/src/Dispatcher.h b/src/Dispatcher.h index e6daae4b..cb86bb4b 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -51,6 +51,8 @@ public: * \returns true if the radio accepted the coding rate. */ virtual bool setCodingRate(uint8_t cr) { return false; } + virtual uint16_t getDefaultPreambleLength() const { return 0; } + virtual bool setPreambleLength(uint16_t len) { return false; } /** * \returns true if the previous 'startSendRaw()' completed successfully. @@ -124,6 +126,7 @@ typedef uint32_t DispatcherAction; class Dispatcher { Packet* outbound; // current outbound packet unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time; + uint16_t outbound_restore_preamble_len; uint8_t outbound_restore_cr; unsigned long next_tx_time; unsigned long cad_busy_start; @@ -137,7 +140,7 @@ class Dispatcher { unsigned long duty_cycle_window_ms; void processRecvPacket(Packet* pkt); - void restoreOutboundCodingRate(); + void restoreOutboundTxOverrides(); void updateTxBudget(); protected: @@ -150,6 +153,7 @@ protected: : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; + outbound_restore_preamble_len = 0; outbound_restore_cr = 0; total_air_time = rx_air_time = 0; next_tx_time = ms.getMillis(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 78cd45b9..4c179223 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -9,6 +9,8 @@ #define BRIDGE_MAX_BAUD 115200 #endif +#define RECENT_REPEATER_PREFIX_MAX_BYTES 3 + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -128,13 +130,21 @@ static bool parseRetryPreset(const char* s, uint8_t& preset) { static bool parseHashPrefix(const char* text, uint8_t* prefix, uint8_t& prefix_len) { size_t hex_len = strlen(text); - if (hex_len == 0 || (hex_len & 1) || hex_len > MAX_HASH_SIZE * 2) { + if (hex_len == 0 || (hex_len & 1) || hex_len > RECENT_REPEATER_PREFIX_MAX_BYTES * 2) { return false; } prefix_len = hex_len / 2; return mesh::Utils::fromHex(prefix, prefix_len, text); } +static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) { + formatSnrDbX4(dest, dest_len, snr_x4); + size_t len = strlen(dest); + if (len > 3 && dest[len - 1] == '0') { + dest[len - 1] = 0; + } +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -928,13 +938,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' '); uint8_t prefix[MAX_HASH_SIZE]; uint8_t prefix_len = 0; - int16_t snr_x4 = num > 1 && looksNumeric(parts[1]) ? parseSnrDbX4(parts[1]) : 0; - if (num != 2 || !parseHashPrefix(parts[0], prefix, prefix_len)) { - strcpy(reply, "Error, use: set recent.repeater "); + int16_t snr_x4 = 12; // default to +3.0 dB when omitted or invalid + if (num > 1 && looksNumeric(parts[1])) { + snr_x4 = parseSnrDbX4(parts[1]); + } + if (num < 1 || !parseHashPrefix(parts[0], prefix, prefix_len)) { + strcpy(reply, "Error, use: set recent.repeater [snr_db]"); } else if (snr_x4 < -128 || snr_x4 > 127) { strcpy(reply, "Error, SNR must fit -32.00..31.75 dB"); } else if (_callbacks->setRecentRepeater(prefix, prefix_len, (int8_t)snr_x4)) { - strcpy(reply, "OK"); + char prefix_hex[RECENT_REPEATER_PREFIX_MAX_BYTES * 2 + 1]; + char snr[12]; + mesh::Utils::toHex(prefix_hex, prefix, prefix_len); + prefix_hex[prefix_len * 2] = 0; + formatSnrDbX4Short(snr, sizeof(snr), snr_x4); + sprintf(reply, "OK - set %s at %s SNR", prefix_hex, snr); } else { strcpy(reply, "Error, table rejected prefix"); } diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 8861f76d..620ef864 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -16,6 +16,10 @@ public: updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomLLCC68 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomLLCC68 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 13efd25b..4d30f515 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -16,6 +16,10 @@ public: updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomLR1110 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 97bf6820..45e275a3 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -17,6 +17,10 @@ public: updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSTM32WLx *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSTM32WLx *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index cc7bb223..66f7adbf 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -20,6 +20,10 @@ public: updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1262 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1262 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 9ddea78f..b87e1cd6 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -20,6 +20,10 @@ public: updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1268 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1268 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 9d75ce12..cb073f91 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -19,6 +19,10 @@ public: updatePreamble(sf); } + bool setCodingRate(uint8_t cr) override { + return ((CustomSX1276 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE; + } + bool isReceivingPacket() override { return ((CustomSX1276 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9943bcab..07740234 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -40,6 +40,8 @@ public: } virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0; + uint16_t getDefaultPreambleLength() const override { return preambleLengthForSF(_preamble_sf); } + bool setPreambleLength(uint16_t len) override { return _radio->setPreambleLength(len) == RADIOLIB_ERR_NONE; } uint32_t getRngSeed(); void setTxPower(int8_t dbm); From 19cfc43269e0fddb01e7573fc98786ee2a8f0b38 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Wed, 17 Jun 2026 16:32:04 -0700 Subject: [PATCH 24/25] Trim recent repeater API and document retry defaults --- docs/cli_commands.md | 8 ++ examples/simple_repeater/MyMesh.cpp | 8 +- src/helpers/SimpleMeshTables.h | 158 +++++++++------------------- 3 files changed, 64 insertions(+), 110 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9c816cf0..6657097e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -965,6 +965,9 @@ Direct retry resends direct-routed packets when the downstream echo is not heard **Default:** `on` +**Notes:** +- New installs and older preference files without direct retry settings default to `on` with the `rooftop` preset. + **Examples:** ``` get direct.retry @@ -1115,6 +1118,7 @@ set direct.retry.margin 10 - Lower SNR uses more robust coding rates. - CR6 is intentionally skipped. - `off` disables per-packet retry CR overrides and uses the current radio CR. +- Direct path retry packets sent at CR4 or CR5 temporarily use a shorter 16-symbol preamble, then restore the radio's default preamble. - Unknown repeaters start at `+3.00 dB` for adaptive CR selection. - A failed unknown repeater is seeded at `+2.75 dB`. - Each later failure lowers the SNR estimate by `0.25 dB`. @@ -1163,6 +1167,10 @@ set direct.retry.cr 20.0,12.0,6.0,2.0 - `snr_db`: Optional SNR in dB. If omitted or invalid, defaults to `3.0`. - `page`: 1-based result page. +**Output order:** +- `get recent.repeater` lists 3-byte prefixes first, then 2-byte prefixes, then 1-byte prefixes. +- Within each prefix length, entries are sorted from highest SNR to lowest SNR. + **SNR details:** - Recent repeater SNR is stored internally in quarter-dB units. - Heard repeater samples update an existing table entry with a weighted blend: `75%` existing SNR and `25%` new heard SNR, rounded up. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4261edd9..4765e7af 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -674,7 +674,7 @@ void MyMesh::onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_ SimpleMeshTables* tables = static_cast(getTables()); if (tables != NULL) { if (!tables->decrementRecentRepeaterSnrX4(next_hop_hash, next_hop_hash_len, 1)) { - tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 11, false, true); + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, 11); } } } @@ -686,7 +686,7 @@ void MyMesh::onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_h SimpleMeshTables* tables = static_cast(getTables()); if (tables != NULL) { - tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, snr_x4, false, true); + tables->setRecentRepeater(next_hop_hash, next_hop_hash_len, snr_x4); } } @@ -720,7 +720,7 @@ void MyMesh::formatRecentRepeatersReply(char *reply, int page) { int len = snprintf(reply, 160, "> %d/%d ", page, pages); int start = (page - 1) * page_size; for (int i = 0; i < page_size && len < 150; i++) { - const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterNewestByIdx(start + i); + const SimpleMeshTables::RecentRepeaterInfo* info = tables->getRecentRepeaterBySortedIdx(start + i); if (info == NULL) break; char prefix[MAX_ROUTE_HASH_BYTES * 2 + 1]; char snr[12]; @@ -736,7 +736,7 @@ void MyMesh::formatRecentRepeatersReply(char *reply, int page) { bool MyMesh::setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { SimpleMeshTables* tables = static_cast(getTables()); - return tables != NULL && tables->setRecentRepeater(prefix, prefix_len, snr_x4, false, true); + return tables != NULL && tables->setRecentRepeater(prefix, prefix_len, snr_x4); } void MyMesh::clearRecentRepeaters() { diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index f1d52733..fb717bfc 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -1,9 +1,6 @@ #pragma once #include -#if ARDUINO - #include -#endif #ifdef ESP32 #include @@ -24,15 +21,11 @@ class SimpleMeshTables : public mesh::MeshTables { public: - typedef bool (*RecentRepeaterAllowFn)(const uint8_t* prefix, uint8_t prefix_len, void* ctx); - struct RecentRepeaterInfo { // Identity and link quality for a next-hop path prefix. uint8_t prefix[MAX_ROUTE_HASH_BYTES]; uint8_t prefix_len; int8_t snr_x4; - uint8_t snr_locked; - uint32_t last_heard_millis; }; private: @@ -40,10 +33,6 @@ private: int _next_idx; uint32_t _direct_dups, _flood_dups; RecentRepeaterInfo _recent_repeaters[MAX_RECENT_REPEATERS]; - int _next_recent_repeater_idx; - int8_t _recent_repeater_min_snr_x4; - RecentRepeaterAllowFn _recent_repeater_allow_fn; - void* _recent_repeater_allow_ctx; bool hasSeenHash(const uint8_t* hash) const { const uint8_t* sp = _hashes; @@ -115,15 +104,27 @@ private: return false; } + bool recentRepeaterComesBefore(const RecentRepeaterInfo& a, int a_idx, + const RecentRepeaterInfo& b, int b_idx) const { + if (a.prefix_len != b.prefix_len) { + return a.prefix_len > b.prefix_len; // 3-byte prefixes, then 2-byte, then 1-byte. + } + if (a.snr_x4 != b.snr_x4) { + return a.snr_x4 > b.snr_x4; // Highest SNR first within each prefix length. + } + int cmp = memcmp(a.prefix, b.prefix, a.prefix_len); + if (cmp != 0) { + return cmp < 0; + } + return a_idx < b_idx; + } + void recordRecentRepeater(const mesh::Packet* packet) { uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; uint8_t prefix_len = 0; if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) { return; } - if (packet->_snr < _recent_repeater_min_snr_x4) { - return; - } setRecentRepeater(prefix, prefix_len, packet->_snr); } @@ -133,10 +134,6 @@ public: _next_idx = 0; _direct_dups = _flood_dups = 0; memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); - _next_recent_repeater_idx = 0; - _recent_repeater_min_snr_x4 = -128; - _recent_repeater_allow_fn = NULL; - _recent_repeater_allow_ctx = NULL; } #ifdef ESP32 @@ -147,7 +144,6 @@ public: // This avoids struct-layout migration issues and keeps stale path quality // stats from persisting indefinitely. memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); - _next_recent_repeater_idx = 0; } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); @@ -198,15 +194,7 @@ public: uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } - void setRecentRepeaterMinSNRX4(int8_t min_snr_x4) { - _recent_repeater_min_snr_x4 = min_snr_x4; - } - void setRecentRepeaterAllowFilter(RecentRepeaterAllowFn fn, void* ctx) { - _recent_repeater_allow_fn = fn; - _recent_repeater_allow_ctx = ctx; - } - bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4, bool snr_locked = false, - bool bypass_allow_filter = false) { + bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) { if (prefix == NULL || prefix_len == 0) { return false; } @@ -215,11 +203,6 @@ public: prefix_len = MAX_ROUTE_HASH_BYTES; } - if (!bypass_allow_filter && _recent_repeater_allow_fn != NULL - && !_recent_repeater_allow_fn(prefix, prefix_len, _recent_repeater_allow_ctx)) { - return false; - } - // Keep exact prefixes distinct so a 1-byte path prefix does not collapse // independent 2/3-byte repeaters that share the same first byte. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { @@ -227,26 +210,14 @@ public: if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { continue; } - if (snr_locked) { - existing.snr_x4 = snr_x4; - existing.snr_locked = 1; - } else if (!existing.snr_locked) { - existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); - } -#if ARDUINO - existing.last_heard_millis = millis(); -#else - existing.last_heard_millis = 0; -#endif + existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4); return true; } int slot_idx = -1; - // Prefer empty slots first while preserving newest-order iteration. for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; - if (_recent_repeaters[idx].prefix_len == 0) { - slot_idx = idx; + if (_recent_repeaters[i].prefix_len == 0) { + slot_idx = i; break; } } @@ -267,13 +238,6 @@ public: memcpy(slot.prefix, prefix, prefix_len); slot.prefix_len = prefix_len; slot.snr_x4 = snr_x4; - slot.snr_locked = snr_locked ? 1 : 0; -#if ARDUINO - slot.last_heard_millis = millis(); -#else - slot.last_heard_millis = 0; -#endif - _next_recent_repeater_idx = (slot_idx + 1) % MAX_RECENT_REPEATERS; return true; } bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) { @@ -289,27 +253,15 @@ public: if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) { continue; } - if (!existing.snr_locked) { - int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; - if (lowered < -128) { - lowered = -128; - } - existing.snr_x4 = (int8_t)lowered; + int16_t lowered = (int16_t)existing.snr_x4 - (int16_t)amount_x4; + if (lowered < -128) { + lowered = -128; } + existing.snr_x4 = (int8_t)lowered; return true; } return false; } - const RecentRepeaterInfo* getLatestRecentRepeater() const { - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; - const RecentRepeaterInfo* info = &_recent_repeaters[idx]; - if (info->prefix_len > 0) { - return info; - } - } - return NULL; - } int getRecentRepeaterCount() const { int count = 0; for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { @@ -319,41 +271,36 @@ public: } return count; } - const RecentRepeaterInfo* getRecentRepeaterNewestByIdx(int idx_wanted) const { + const RecentRepeaterInfo* getRecentRepeaterBySortedIdx(int idx_wanted) const { if (idx_wanted < 0) { return NULL; } - int idx_seen = 0; - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; - const RecentRepeaterInfo* info = &_recent_repeaters[idx]; - if (info->prefix_len == 0) { - continue; + + const RecentRepeaterInfo* last = NULL; + int last_idx = -1; + for (int rank = 0; rank <= idx_wanted; rank++) { + const RecentRepeaterInfo* best = NULL; + int best_idx = -1; + for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { + const RecentRepeaterInfo* info = &_recent_repeaters[i]; + if (info->prefix_len == 0) { + continue; + } + if (last != NULL && !recentRepeaterComesBefore(*last, last_idx, *info, i)) { + continue; + } + if (best == NULL || recentRepeaterComesBefore(*info, i, *best, best_idx)) { + best = info; + best_idx = i; + } } - if (idx_seen == idx_wanted) { - return info; + if (best == NULL) { + return NULL; } - idx_seen++; + last = best; + last_idx = best_idx; } - return NULL; - } - const RecentRepeaterInfo* getRecentRepeaterOldestByIdx(int idx_wanted) const { - if (idx_wanted < 0) { - return NULL; - } - int idx_seen = 0; - for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - int idx = (_next_recent_repeater_idx + i) % MAX_RECENT_REPEATERS; - const RecentRepeaterInfo* info = &_recent_repeaters[idx]; - if (info->prefix_len == 0) { - continue; - } - if (idx_seen == idx_wanted) { - return info; - } - idx_seen++; - } - return NULL; + return last; } const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const { @@ -361,12 +308,11 @@ public: return NULL; } - // Prefer exact matches. If none exists, fall back to the newest longest - // overlapping prefix so coarse learned prefixes can still inform CR. + // Prefer exact matches. If none exists, fall back to the longest overlapping + // prefix, using highest SNR to break ties. const RecentRepeaterInfo* best = NULL; for (int i = 0; i < MAX_RECENT_REPEATERS; i++) { - int idx = (_next_recent_repeater_idx - 1 - i + MAX_RECENT_REPEATERS) % MAX_RECENT_REPEATERS; - const RecentRepeaterInfo* info = &_recent_repeaters[idx]; + const RecentRepeaterInfo* info = &_recent_repeaters[i]; if (info->prefix_len == 0) { continue; } @@ -374,7 +320,8 @@ public: return info; } if (prefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) { - if (best == NULL || info->prefix_len > best->prefix_len) { + if (best == NULL || info->prefix_len > best->prefix_len + || (info->prefix_len == best->prefix_len && info->snr_x4 > best->snr_x4)) { best = info; } } @@ -383,7 +330,6 @@ public: } void clearRecentRepeaters() { memset(_recent_repeaters, 0, sizeof(_recent_repeaters)); - _next_recent_repeater_idx = 0; } void resetStats() { _direct_dups = _flood_dups = 0; } From 6b8135aa36154e6d544dbd820b437c2700d08759 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Wed, 17 Jun 2026 16:44:58 -0700 Subject: [PATCH 25/25] Fix direct retry review nits --- docs/cli_commands.md | 2 +- examples/simple_repeater/MyMesh.cpp | 2 +- src/Dispatcher.cpp | 6 +++++- src/Packet.cpp | 1 - 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6657097e..cba67c7e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -953,7 +953,7 @@ region save --- ### Direct Retry -Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages and TRACE packets. It does not change ACK handling. +Direct retry resends direct-routed packets when the downstream echo is not heard. It applies to direct messages, ACK packets, multipart packets carrying ACK payloads, and TRACE packets. #### View or change direct retry state **Usage:** diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4765e7af..2afaae2e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -574,8 +574,8 @@ uint8_t MyMesh::getDirectRetryCodingRateForSNR(int8_t snr_x4) const { if (!_prefs.direct_retry_cr_enabled) return 0; 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_cr7_snr_x4) return 7; if (snr_x4 <= _prefs.direct_retry_cr8_snr_x4) return 8; + if (snr_x4 >= _prefs.direct_retry_cr7_snr_x4) return 7; return 7; } diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 63112f85..aee81a04 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -348,19 +348,23 @@ void Dispatcher::checkSend() { outbound_restore_cr = 0; outbound_restore_preamble_len = 0; uint8_t default_cr = getDefaultTxCodingRate(); + bool using_low_retry_cr = false; 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; + using_low_retry_cr = outbound->tx_cr == 4 || outbound->tx_cr == 5; 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); } + } else if (outbound->tx_cr == 4 || outbound->tx_cr == 5) { + using_low_retry_cr = outbound->tx_cr == default_cr; } bool has_direct_path = outbound->getPathHashCount() > 0 || (outbound->getPayloadType() == PAYLOAD_TYPE_TRACE && outbound->payload_len > 9); if (outbound->isRouteDirect() && has_direct_path - && (outbound->tx_cr == 4 || outbound->tx_cr == 5)) { + && using_low_retry_cr) { uint16_t default_preamble_len = _radio->getDefaultPreambleLength(); if (default_preamble_len > 16 && _radio->setPreambleLength(16)) { outbound_restore_preamble_len = default_preamble_len; diff --git a/src/Packet.cpp b/src/Packet.cpp index a542a5a2..3aab6349 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -87,4 +87,3 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { } } -