diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b8d7cb56..d9a225e8 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -780,6 +780,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### Enable or disable hardware Channel Activity Detection (CAD) +**Usage:** +- `get cad` +- `set cad ` + +**Description:** When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of `int.thresh` — either, both, or none may be active. + +**Parameters:** +- `on|off`: Enable or disable hardware CAD + +**Default:** `off` + +--- + #### View or change the AGC Reset Interval **Usage:** - `get agc.reset.interval` diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1dd5162b..5d1adb46 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -263,6 +263,9 @@ float MyMesh::getAirtimeBudgetFactor() const { int MyMesh::getInterferenceThreshold() const { return 0; // disabled for now, until currentRSSI() problem is resolved } +bool MyMesh::getCADEnabled() const { + return true; // hardware CAD before TX (no CLI toggle on companion; enabled by default) +} int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; @@ -856,7 +859,7 @@ void MyMesh::onSendTimeout() {} MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { _iter_started = false; _cli_rescue = false; offline_queue_len = 0; @@ -1547,6 +1550,7 @@ void MyMesh::handleCmdFrame(size_t len) { memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE); anon.out_path_len = 0; // default to zero-hop direct anon.type = ADV_TYPE_NONE; // unknown + anon.lastmod = getRTCClock()->getCurrentTime(); if (addContact(anon)) recipient = &anon; } @@ -2216,15 +2220,7 @@ void MyMesh::checkSerialInterface() { && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! ) { ContactInfo contact; - bool found = false; - while (_iter.hasNext(this, contact)) { - if (contact.type != ADV_TYPE_NONE) { - found = true; - break; - } - } - - if (found) { + if (_iter.hasNext(this, contact)) { if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter writeContactRespFrame(RESP_CODE_CONTACT, contact); if (contact.lastmod > _most_recent_lastmod) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 865a339e..0d687cfd 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -105,6 +105,7 @@ public: protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; + bool getCADEnabled() const override; int calcRxDelay(float score, uint32_t air_time) const override; uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a7bb8b84..5cc3a9a1 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,6 +1,5 @@ #include "MyMesh.h" #include -#include /* ------------------------------ Config -------------------------------- */ @@ -20,31 +19,6 @@ #define LORA_TX_POWER 20 #endif -#ifndef DEFAULT_RX_DELAY_BASE - #define DEFAULT_RX_DELAY_BASE 0.0f -#endif -#ifndef DEFAULT_LOOP_DETECT - #define DEFAULT_LOOP_DETECT LOOP_DETECT_OFF -#endif -#ifndef DEFAULT_POWERSAVING_ENABLED - #define DEFAULT_POWERSAVING_ENABLED 0 -#endif -#ifndef DEFAULT_AGC_RESET_INTERVAL - #define DEFAULT_AGC_RESET_INTERVAL 0 -#endif -#ifndef DEFAULT_ADVERT_INTERVAL - #define DEFAULT_ADVERT_INTERVAL 1 -#endif -#ifndef DEFAULT_FLOOD_ADVERT_INTERVAL - #define DEFAULT_FLOOD_ADVERT_INTERVAL 12 -#endif -#ifndef DEFAULT_MULTI_ACKS - #define DEFAULT_MULTI_ACKS 0 -#endif -#ifndef DEFAULT_PATH_HASH_MODE - #define DEFAULT_PATH_HASH_MODE 0 -#endif - #ifndef ADVERT_NAME #define ADVERT_NAME "repeater" #endif @@ -66,9 +40,6 @@ #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 @@ -88,119 +59,6 @@ #define CLI_REPLY_DELAY_MILLIS 600 #define LAZY_CONTACTS_WRITE_DELAY 5000 -#ifndef REPEATERS_CHANNEL_KEY_HEX - #define REPEATERS_CHANNEL_KEY_HEX "89db441e2814dccf0dbd2e8cc5f501a3" -#endif -#ifndef BATT_MIN_MILLIVOLTS - #define BATT_MIN_MILLIVOLTS 3000 -#endif -#ifndef BATT_MAX_MILLIVOLTS - #define BATT_MAX_MILLIVOLTS 4200 -#endif - -#define LOW_BATTERY_MIN_VALID_MV 1000 -#define LOW_BATTERY_WARN_PERCENT_DEFAULT 20 -#define LOW_BATTERY_CRITICAL_PERCENT_DEFAULT 10 -#define LOW_BATTERY_CHECK_INTERVAL (60UL * 1000UL) -#define LOW_BATTERY_WARN_INTERVAL (24UL * 60UL * 60UL * 1000UL) -#define LOW_BATTERY_CRITICAL_INTERVAL (12UL * 60UL * 60UL * 1000UL) - -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 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; // 3-byte first, 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 size - } - return false; // keep original newest-first order for ties - }); - - 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 - 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 @@ -541,8 +399,6 @@ File MyMesh::openAppend(const char *fname) { 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(); @@ -570,148 +426,13 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui } } -static bool pathsEqual(const uint8_t* a, uint8_t a_len, const uint8_t* b, uint8_t b_len) { - if (a == NULL || b == NULL || a_len != b_len || !mesh::Packet::isValidPathLen(a_len)) return false; - - uint8_t hash_count = a_len & 63; - uint8_t hash_size = (a_len >> 6) + 1; - return memcmp(a, b, hash_count * hash_size) == 0; -} - -static bool hasUsablePath(const uint8_t* path, uint8_t path_len) { - return path != NULL && mesh::Packet::isValidPathLen(path_len); -} - -static bool buildRepeatersChannel(mesh::GroupChannel& channel) { - const char* hex = REPEATERS_CHANNEL_KEY_HEX; - size_t hex_len = strlen(hex); - if (!(hex_len == 32 || hex_len == 64)) return false; - for (size_t i = 0; i < hex_len; i++) { - if (!mesh::Utils::isHexChar(hex[i])) return false; - } - - memset(channel.secret, 0, sizeof(channel.secret)); - size_t key_len = hex_len / 2; - if (!mesh::Utils::fromHex(channel.secret, key_len, hex)) return false; - - mesh::Utils::sha256(channel.hash, sizeof(channel.hash), channel.secret, key_len); - return true; -} - -static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) { - const int min_mv = BATT_MIN_MILLIVOLTS; - const int max_mv = BATT_MAX_MILLIVOLTS; - if (max_mv <= min_mv) return 100; - - int pct = (((int)batt_mv - min_mv) * 100) / (max_mv - min_mv); - if (pct < 0) return 0; - if (pct > 100) return 100; - return (uint8_t)pct; -} - -static bool parseBatteryAlertPercent(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { - if (value == NULL || *value == 0) { - return false; - } - - uint16_t parsed = 0; - while (*value) { - if (*value < '0' || *value > '9') { - return false; - } - parsed = (uint16_t)(parsed * 10 + (*value - '0')); - if (parsed > max_value) { - return false; - } - value++; - } - if (parsed < min_value) { - return false; - } - - result = (uint8_t)parsed; - return true; -} - -mesh::Packet* MyMesh::createPacketCopy(const mesh::Packet* packet, const char* caller) { - if (packet == NULL) return NULL; - - mesh::Packet* copy = obtainNewPacket(); - if (copy == NULL) { - MESH_DEBUG_PRINTLN("%s %s: error, packet pool empty", getLogDateTime(), caller); - return NULL; - } - *copy = *packet; - return copy; -} - -mesh::Packet* MyMesh::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) { - if (!hasUsablePath(alt_path, alt_path_len)) return NULL; - if (hasUsablePath(primary_path, primary_path_len) && pathsEqual(primary_path, primary_path_len, alt_path, alt_path_len)) { - return NULL; - } - return createPacketCopy(packet, "MyMesh::createAltPathCopy()"); -} - -void MyMesh::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) { - mesh::Packet* direct = hasUsablePath(direct_path, direct_path_len) - ? createPacketCopy(packet, "MyMesh::sendFloodReplyWithAltPath(direct)") - : NULL; - mesh::Packet* alt = createAltPathCopy(packet, direct_path, direct_path_len, alt_path, alt_path_len); - - sendFloodReply(packet, delay_millis, path_hash_size); - if (direct) sendDirect(direct, direct_path, direct_path_len, delay_millis); - if (alt) sendDirect(alt, alt_path, alt_path_len, delay_millis); -} - -void MyMesh::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) { - mesh::Packet* alt = createAltPathCopy(packet, path, path_len, alt_path, alt_path_len); - - sendDirect(packet, path, path_len, delay_millis); - if (alt) sendDirect(alt, alt_path, alt_path_len, delay_millis); -} - bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) 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()) { if (packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false; if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false; } - - 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; @@ -814,130 +535,6 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { } } -void MyMesh::onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { - if (packet == NULL) { - return; - } - if (strcmp(event, "failure") == 0) { - return; - } - - 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"; - char hop_text[24]; - 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 total_hops = (hash_size > 0) ? (route_bytes / hash_size) : 0; - if (total_hops > 0) { - snprintf(hop_text, sizeof(hop_text), "%u/%u", (unsigned int)packet->path_len, (unsigned int)total_hops); - } else { - snprintf(hop_text, sizeof(hop_text), "unknown"); - } - } else if (packet->isRouteDirect()) { - snprintf(hop_text, sizeof(hop_text), "remaining:%u", (unsigned int)packet->getPathHashCount()); - } else { - snprintf(hop_text, sizeof(hop_text), "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; - bool updates_quality = strcmp(event, "good") == 0; - int8_t retry_event_snr_x4; - if (is_success_event) { - // On success, Mesh.cpp injects echo RX SNR for the downstream retry target. - retry_event_snr_x4 = packet->_snr; - } else if (existing != NULL) { - retry_event_snr_x4 = existing->snr_x4; - } else { - retry_event_snr_x4 = fallback_snr_x4; - } - char snr_pkt_text[12]; - char snr_table_text[12]; - - if (has_prefix && updates_quality) { - // Refresh SNR only once per successful echo/progress event, not on queued/resent bookkeeping. - tables->setRecentRepeater(prefix, prefix_len, retry_event_snr_x4, false, true); - } - - if (strcmp(event, "failed_all_tries") == 0) { - if (has_prefix) { - if (existing == NULL) { - int16_t seed_snr_x4 = (int16_t)getDirectRetryMinSNRX4() + 10; // +2.5 dB over the active retry cutoff. - tables->setRecentRepeater(prefix, prefix_len, (int8_t)constrain(seed_snr_x4, -128, 127), false, true); - } - // SNR is stored in quarter-dB units, so 1 lowers quality by 0.25 dB. - tables->decrementRecentRepeaterSnrX4(prefix, prefix_len, 1); - } - } - - snprintf(snr_pkt_text, sizeof(snr_pkt_text), "%s", StrHelper::ftoa(((float)packet->_snr) / 4.0f)); - const auto* log_existing = has_prefix ? tables->findRecentRepeaterByHash(prefix, prefix_len) : NULL; - if (log_existing != NULL) { - snprintf(snr_table_text, sizeof(snr_table_text), "%s", StrHelper::ftoa(((float)log_existing->snr_x4) / 4.0f)); - } else { - snprintf(snr_table_text, sizeof(snr_table_text), "na"); - } - - const char* time_label = "time_ms"; - if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) { - time_label = "wait_ms"; - } else if (strcmp(event, "resent") == 0 || strcmp(event, "failed_all_tries") == 0 || strcmp(event, "failure") == 0) { - time_label = "elapsed_ms"; - } else if (strcmp(event, "good") == 0) { - time_label = "echo_ms"; - } - - 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, - (uint32_t)packet->getPayloadType(), - packet->isRouteDirect() ? "D" : "F", - (uint32_t)packet->payload_len, - hop_text, - next_hop, - snr_pkt_text, - snr_table_text, - (unsigned int)log_cr, - time_label, - (unsigned long)delay_millis); - - if (_logging) { - 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, cr=%u, %s=%lu)\n", - event, - (unsigned int)retry_attempt, - (uint32_t)packet->getPayloadType(), - packet->isRouteDirect() ? "D" : "F", - (uint32_t)packet->payload_len, - hop_text, - next_hop, - snr_pkt_text, - snr_table_text, - (unsigned int)log_cr, - time_label, - (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); @@ -951,776 +548,6 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + 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 margin_x4 = (int16_t)_prefs.direct_retry_snr_margin_db; - int16_t threshold = direct_retry_floor_x4[sf - 5] + margin_x4; - 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); - uint16_t offset = (uint16_t)packet->path_len * (uint16_t)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) { - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(next_hop_hash, next_hop_hash_len); - return recent == NULL || recent->snr_x4 >= min_snr_x4; - } - - return true; -} -bool MyMesh::maybeShortCircuitDirect(mesh::Packet* packet) { - if (packet == NULL || !packet->isRouteDirect() || packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - return false; - } - - uint8_t hash_count = packet->getPathHashCount(); - uint8_t hash_size = packet->getPathHashSize(); - if (hash_count < 2 || hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return false; - } - - // Normal direct forwarding handles the first path entry. Short-circuit only when we are later in the route. - int self_idx = -1; - for (uint8_t i = 1; i < hash_count; i++) { - if (self_id.isHashMatch(&packet->path[i * hash_size], hash_size)) { - self_idx = i; - break; - } - } - if (self_idx < 0) { - return false; - } - - const SimpleMeshTables* tables = (const SimpleMeshTables*)getTables(); - bool adjacent_recent = false; - if (self_idx > 0 - && tables->findRecentRepeaterByHash(&packet->path[(self_idx - 1) * hash_size], hash_size) != NULL) { - adjacent_recent = true; - } - if (!adjacent_recent && self_idx + 1 < hash_count - && tables->findRecentRepeaterByHash(&packet->path[(self_idx + 1) * hash_size], hash_size) != NULL) { - adjacent_recent = true; - } - if (!adjacent_recent) { - return false; - } - - uint8_t remaining = hash_count - (uint8_t)self_idx; - memmove(packet->path, &packet->path[self_idx * hash_size], remaining * hash_size); - packet->setPathHashCount(remaining); - - MESH_DEBUG_PRINTLN("%s direct short-circuit (type=%d, original_hop=%d, remaining_hops=%d)", - getLogDateTime(), - (uint32_t)packet->getPayloadType(), - self_idx + 1, - (uint32_t)remaining); - 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; - } - if (snr_x4 >= _prefs.direct_retry_cr5_snr_x4) { - return 5; - } - 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; - - if (retry == NULL || !retry->isRouteDirect()) { - return; - } - - switch (retry->getPayloadType()) { - case PAYLOAD_TYPE_ACK: - case PAYLOAD_TYPE_PATH: - case PAYLOAD_TYPE_REQ: - case PAYLOAD_TYPE_RESPONSE: - case PAYLOAD_TYPE_TXT_MSG: - case PAYLOAD_TYPE_ANON_REQ: - case PAYLOAD_TYPE_TRACE: - case PAYLOAD_TYPE_MULTIPART: - break; - default: - return; - } - - uint8_t prefix[MAX_ROUTE_HASH_BYTES]; - uint8_t prefix_len = 0; - if (!extractDirectRetryPrefix(retry, prefix, prefix_len)) { - return; - } - - 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; - } - - 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; - } -} -uint8_t MyMesh::getDirectRetryPreset() const { - if (_prefs.retry_preset <= RETRY_PRESET_MOBILE) { - return _prefs.retry_preset; - } - return RETRY_PRESET_ROOFTOP; -} -uint8_t MyMesh::getDirectRetryConfiguredMaxAttempts() const { - return constrain(_prefs.direct_retry_attempts, (uint8_t)1, (uint8_t)15); -} -uint32_t MyMesh::getDirectRetryAttemptStepMillis() const { - return constrain((uint32_t)_prefs.direct_retry_step_ms, (uint32_t)0, (uint32_t)5000); -} -bool MyMesh::hasFloodRetryPrefixes() const { - for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { - const uint8_t* configured = _prefs.flood_retry_prefixes[i]; - if (configured[0] != 0 || configured[1] != 0 || configured[2] != 0) { - return true; - } - } - return false; -} -bool MyMesh::floodRetryLastHopMatches(const mesh::Packet* packet) const { - if (packet == NULL || packet->getPathHashCount() == 0) { - return false; - } - - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return false; - } - - const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; - for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { - const uint8_t* configured = _prefs.flood_retry_prefixes[i]; - if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) - && memcmp(configured, heard_prefix, hash_size) == 0) { - return true; - } - } - - return false; -} -bool MyMesh::floodRetryPrefixMatches(const mesh::Packet* packet) const { - if (packet == NULL || packet->getPathHashCount() == 0) { - return false; - } - - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return false; - } - - const uint8_t* path = packet->path; - for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - for (int i = 0; i < FLOOD_RETRY_PREFIX_SLOTS; i++) { - const uint8_t* configured = _prefs.flood_retry_prefixes[i]; - if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) - && memcmp(configured, path, hash_size) == 0) { - return true; - } - } - path += hash_size; - } - - return false; -} -bool MyMesh::floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const { - if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) { - return false; - } - for (int i = 0; i < FLOOD_RETRY_IGNORE_PREFIXES; i++) { - const uint8_t* ignored = _prefs.flood_retry_ignore_prefixes[i]; - if ((ignored[0] != 0 || ignored[1] != 0 || ignored[2] != 0) - && memcmp(ignored, prefix, prefix_len) == 0) { - return true; - } - } - return false; -} -uint8_t MyMesh::floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops) const { - if (packet == NULL || !packet->isRouteFlood() || packet->getPathHashCount() == 0) { - return 0; - } - - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return packet->getPathHashCount(); - } - - uint8_t hop_count = packet->getPathHashCount(); - if (max_hops < hop_count) { - hop_count = max_hops; - } - - uint8_t effective_len = 0; - const uint8_t* path = packet->path; - for (uint8_t hop = 0; hop < hop_count; hop++) { - if (!floodRetryPrefixIgnored(path, hash_size)) { - effective_len++; - } - path += hash_size; - } - return effective_len; -} -bool MyMesh::floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const { - const auto* recent = ((const SimpleMeshTables *)getTables())->findRecentRepeaterByHash(prefix, prefix_len); - if (recent == NULL || recent->last_heard_millis == 0) { - return false; - } - return (uint32_t)(millis() - recent->last_heard_millis) <= 3600000UL; -} -static const uint8_t FLOOD_RETRY_BRIDGE_OTHER_BUCKET = FLOOD_RETRY_BRIDGE_BUCKETS; - -static uint8_t floodRetryBucketMask(uint8_t bucket) { - if (bucket >= 8) { - return 0; - } - return (uint8_t)(1U << bucket); -} -int MyMesh::floodRetryBucketForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh, - bool include_other) const { - if (prefix == NULL || prefix_len == 0 || prefix_len > MAX_ROUTE_HASH_BYTES) { - return -1; - } - if (floodRetryPrefixIgnored(prefix, prefix_len)) { - return -1; - } - if (require_fresh && !floodRetryPrefixFresh(prefix, prefix_len)) { - return -1; - } - for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) { - for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { - const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i]; - if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) - && memcmp(configured, prefix, prefix_len) == 0) { - return bucket; - } - } - } - if (include_other) { - return FLOOD_RETRY_BRIDGE_OTHER_BUCKET; - } - return -1; -} -int MyMesh::floodRetryBucketForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop, - uint8_t progress_marker) const { - return floodRetryBucketForPrefix(prefix, prefix_len, hop < progress_marker, true); -} -int MyMesh::floodRetrySourceBucket(const mesh::Packet* packet) const { - if (packet == NULL) { - return -1; - } - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return -1; - } - if (packet->getPathHashCount() < 2) { - return FLOOD_RETRY_BRIDGE_OTHER_BUCKET; - } - const uint8_t* source_prefix = &packet->path[(packet->getPathHashCount() - 2) * hash_size]; - return floodRetryBucketForPrefix(source_prefix, hash_size, true, true); -} -uint8_t MyMesh::floodRetryBridgeTargetMask(uint8_t source_bucket) const { - uint8_t mask = 0; - for (int bucket = 0; bucket < FLOOD_RETRY_BRIDGE_BUCKETS; bucket++) { - if (bucket == source_bucket) { - continue; - } - for (int i = 0; i < FLOOD_RETRY_BUCKET_PREFIXES; i++) { - const uint8_t* configured = _prefs.flood_retry_bridge_buckets[bucket][i]; - if ((configured[0] != 0 || configured[1] != 0 || configured[2] != 0) - && !floodRetryPrefixIgnored(configured, FLOOD_RETRY_PREFIX_LEN) - && floodRetryPrefixFresh(configured, FLOOD_RETRY_PREFIX_LEN)) { - mask |= floodRetryBucketMask((uint8_t)bucket); - break; - } - } - } - if (source_bucket != FLOOD_RETRY_BRIDGE_OTHER_BUCKET) { - mask |= floodRetryBucketMask(FLOOD_RETRY_BRIDGE_OTHER_BUCKET); - } - return mask; -} -uint8_t MyMesh::floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_bucket, - uint8_t progress_marker) const { - if (packet == NULL || packet->getPathHashCount() == 0) { - return 0; - } - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return 0; - } - - uint8_t mask = 0; - const uint8_t* path = packet->path; - for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - if (progress_marker > 0 && hop == progress_marker - 1) { - path += hash_size; - continue; - } - int bucket = floodRetryBucketForPathHop(path, hash_size, (uint8_t)hop, progress_marker); - if (bucket >= 0 && bucket != source_bucket) { - mask |= floodRetryBucketMask((uint8_t)bucket); - } - path += hash_size; - } - return mask; -} -MyMesh::FloodRetryBridgeState* MyMesh::floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const { - if (packet == NULL) { - return NULL; - } - - uint8_t key[MAX_HASH_SIZE]; - packet->calculatePacketHash(key); - FloodRetryBridgeState* free_slot = NULL; - for (int i = 0; i < MAX_FLOOD_RETRY_SLOTS; i++) { - if (flood_retry_bridge_states[i].active - && memcmp(flood_retry_bridge_states[i].key, key, MAX_HASH_SIZE) == 0) { - return &flood_retry_bridge_states[i]; - } - if (!flood_retry_bridge_states[i].active && free_slot == NULL) { - free_slot = &flood_retry_bridge_states[i]; - } - } - if (!create) { - return NULL; - } - if (free_slot == NULL) { - return NULL; - } - - int source_bucket = floodRetrySourceBucket(packet); - if (source_bucket < 0) { - return NULL; - } - - uint8_t target_mask = floodRetryBridgeTargetMask((uint8_t)source_bucket); - if (target_mask == 0) { - return NULL; - } - - uint8_t progress_marker = packet->getPathHashCount(); - uint8_t heard_mask = floodRetryBridgeHeardMask(packet, (uint8_t)source_bucket, progress_marker) & target_mask; - if ((heard_mask & target_mask) == target_mask) { - return NULL; - } - - memset(free_slot, 0, sizeof(*free_slot)); - memcpy(free_slot->key, key, sizeof(free_slot->key)); - free_slot->source_bucket = (uint8_t)source_bucket; - free_slot->target_mask = target_mask; - free_slot->heard_mask = heard_mask; - free_slot->progress_marker = progress_marker; - free_slot->active = true; - return free_slot; -} -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); - if (packet == NULL) { - return base_wait_millis; - } - - // 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 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 base_wait_millis + scaled_wait_millis; -} -bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const { - if (_prefs.disable_fwd || constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)15) == 0) { - return false; - } - if (packet != NULL && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && !_prefs.flood_retry_advert_enabled) { - return false; - } - if (!_prefs.flood_retry_bridge_enabled) { - return true; - } - FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, true); - if (state == NULL) { - return false; - } - if ((state->heard_mask & state->target_mask) == state->target_mask) { - state->active = false; - return false; - } - return true; -} -void MyMesh::clearFloodRetryBridgeState(const mesh::Packet* packet) { - FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); - if (state != NULL) { - state->active = false; - } -} -void MyMesh::refreshFloodRetryHeardRecent(const mesh::Packet* packet) { - if (packet == NULL || !packet->isRouteFlood() || packet->getPathHashCount() == 0) { - return; - } - - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return; - } - - auto* tables = (SimpleMeshTables*)getTables(); - const uint8_t* path = packet->path; - if (_prefs.flood_retry_bridge_enabled) { - FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); - if (state != NULL) { - for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - if (state->progress_marker > 0 && hop == state->progress_marker - 1) { - path += hash_size; - continue; - } - int bucket = floodRetryBucketForPathHop(path, hash_size, (uint8_t)hop, state->progress_marker); - uint8_t bucket_mask = bucket >= 0 ? floodRetryBucketMask((uint8_t)bucket) : 0; - if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & bucket_mask)) { - tables->setRecentRepeater(path, hash_size, packet->_snr, false, true); - } - path += hash_size; - } - return; - } - } - - const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; - tables->setRecentRepeater(heard_prefix, hash_size, packet->_snr, false, true); -} -void MyMesh::formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const { - if (dest == NULL || dest_len == 0) { - return; - } - dest[0] = 0; - - if (packet == NULL || packet->getPathHashCount() == 0) { - StrHelper::strncpy(dest, "-", dest_len); - return; - } - - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - StrHelper::strncpy(dest, "invalid", dest_len); - return; - } - - char* out = dest; - size_t remaining = dest_len; - const uint8_t* path = packet->path; - for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - size_t needed = (hop > 0 ? 1 : 0) + ((size_t)hash_size * 2) + 1; - if (remaining < needed) { - if (remaining > 4) { - strcpy(out, "..."); - } - return; - } - if (hop > 0) { - *out++ = '>'; - remaining--; - } - mesh::Utils::toHex(out, path, hash_size); - out += (size_t)hash_size * 2; - remaining -= (size_t)hash_size * 2; - path += hash_size; - } -} -bool MyMesh::formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const { - if (dest == NULL || dest_len == 0 || packet == NULL || packet->getPathHashCount() == 0) { - return false; - } - dest[0] = 0; - - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return false; - } - - char* out = dest; - size_t remaining = dest_len; - bool first = true; - - if (_prefs.flood_retry_bridge_enabled) { - FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); - if (state == NULL) { - return false; - } - const uint8_t* path = packet->path; - for (int hop = 0; hop < packet->getPathHashCount(); hop++) { - if (state->progress_marker > 0 && hop == state->progress_marker - 1) { - path += hash_size; - continue; - } - int bucket = floodRetryBucketForPathHop(path, hash_size, (uint8_t)hop, state->progress_marker); - uint8_t bucket_mask = bucket >= 0 ? floodRetryBucketMask((uint8_t)bucket) : 0; - if (bucket >= 0 && bucket != state->source_bucket && (state->target_mask & bucket_mask)) { - char bucket_label[8]; - if ((uint8_t)bucket == FLOOD_RETRY_BRIDGE_OTHER_BUCKET) { - strcpy(bucket_label, "other"); - } else { - snprintf(bucket_label, sizeof(bucket_label), "b%d", bucket + 1); - } - size_t needed = (first ? 0 : 1) + strlen(bucket_label) + 1 + ((size_t)hash_size * 2) + 1; - if (remaining < needed) { - if (remaining > 4) { - strcpy(out, "..."); - } - return dest[0] != 0; - } - if (!first) { - *out++ = ','; - remaining--; - } - int n = snprintf(out, remaining, "%s:", bucket_label); - if (n < 0 || (size_t)n >= remaining) { - return dest[0] != 0; - } - out += n; - remaining -= n; - mesh::Utils::toHex(out, path, hash_size); - out += (size_t)hash_size * 2; - remaining -= (size_t)hash_size * 2; - first = false; - } - path += hash_size; - } - return dest[0] != 0; - } - - const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; - if (remaining < ((size_t)hash_size * 2) + 1) { - return false; - } - mesh::Utils::toHex(out, heard_prefix, hash_size); - return true; -} -void MyMesh::onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) { - if (event == NULL || packet == NULL) { - return; - } - - bool clear_bridge_state = _prefs.flood_retry_bridge_enabled - && (strcmp(event, "good") == 0 || strcmp(event, "failure") == 0 || strcmp(event, "failed_all_tries") == 0 - || strncmp(event, "dropped_", 8) == 0); - - if (clear_bridge_state && strcmp(event, "failure") == 0) { - clearFloodRetryBridgeState(packet); - } - - if (strcmp(event, "failure") == 0) { - return; - } - - const char* time_label = "time_ms"; - if (strcmp(event, "queued") == 0 || strcmp(event, "dropped_queue_full") == 0) { - time_label = "wait_ms"; - } else if (strcmp(event, "resent") == 0 || strcmp(event, "failed_all_tries") == 0 - || strcmp(event, "failure") == 0 || strncmp(event, "dropped_", 8) == 0) { - time_label = "elapsed_ms"; - } else if (strcmp(event, "good") == 0) { - time_label = "echo_ms"; - } - - char path_log[208]; - char heard_log[96]; - char heard_suffix[112]; - formatFloodRetryPath(path_log, sizeof(path_log), packet); - heard_suffix[0] = 0; - if (strcmp(event, "good") == 0 && formatFloodRetryHeard(heard_log, sizeof(heard_log), packet)) { - refreshFloodRetryHeardRecent(packet); - snprintf(heard_suffix, sizeof(heard_suffix), ", heard=%s", heard_log); - } - - MESH_DEBUG_PRINTLN("%s flood retry %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, path=%s%s, %s=%lu)", - getLogDateTime(), - event, - (unsigned int)retry_attempt, - (uint32_t)packet->getPayloadType(), - packet->isRouteDirect() ? "D" : "F", - (uint32_t)packet->payload_len, - (unsigned int)packet->getPathHashCount(), - path_log, - heard_suffix, - time_label, - (unsigned long)delay_millis); - - if (_logging) { - File f = openAppend(PACKET_LOG_FILE); - if (f) { - f.print(getLogDateTime()); - f.printf(": FLOOD RETRY %s (retry=%u, type=%d, route=%s, payload_len=%d, hop=%u, path=%s%s, %s=%lu)\n", - event, - (unsigned int)retry_attempt, - (uint32_t)packet->getPayloadType(), - packet->isRouteDirect() ? "D" : "F", - (uint32_t)packet->payload_len, - (unsigned int)packet->getPathHashCount(), - path_log, - heard_suffix, - time_label, - (unsigned long)delay_millis); - f.close(); - } - } - - if (clear_bridge_state) { - clearFloodRetryBridgeState(packet); - } -} -bool MyMesh::hasFloodRetryTargetPrefix(const mesh::Packet* packet) const { - if (_prefs.flood_retry_bridge_enabled) { - return false; - } - return floodRetryPrefixMatches(packet); -} -uint8_t MyMesh::getFloodRetryMaxPathLength(const mesh::Packet* packet) const { - uint8_t gate = _prefs.flood_retry_path_gate; - if (gate == FLOOD_RETRY_PATH_GATE_DISABLED) { - return FLOOD_RETRY_PATH_GATE_DISABLED; - } - if (gate > 63) { - gate = 2; - } - - uint8_t raw_hops = packet != NULL ? packet->getPathHashCount() : 0; - uint8_t effective_hops = floodRetryEffectivePathLength(packet); - uint8_t ignored_hops = raw_hops > effective_hops ? raw_hops - effective_hops : 0; - uint16_t adjusted_gate = (uint16_t)gate + ignored_hops; - return adjusted_gate > 63 ? 63 : (uint8_t)adjusted_gate; -} -uint8_t MyMesh::getFloodRetryMaxAttempts(const mesh::Packet* packet) const { - if (_prefs.disable_fwd) { - return 0; - } - return constrain(_prefs.flood_retry_attempts, (uint8_t)0, (uint8_t)15); -} -bool MyMesh::isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const { - if (packet == NULL || !packet->isRouteFlood()) { - return false; - } - if (_prefs.flood_retry_bridge_enabled) { - FloodRetryBridgeState* state = floodRetryBridgeStateFor(packet, false); - if (state == NULL) { - return false; - } - state->heard_mask |= floodRetryBridgeHeardMask(packet, state->source_bucket, state->progress_marker) & state->target_mask; - return (state->heard_mask & state->target_mask) == state->target_mask; - } - if (packet->getPathHashCount() == 0) { - return false; - } - uint8_t hash_size = packet->getPathHashSize(); - if (hash_size == 0 || hash_size > MAX_ROUTE_HASH_BYTES) { - return false; - } - const uint8_t* heard_prefix = &packet->path[(packet->getPathHashCount() - 1) * hash_size]; - if (floodRetryPrefixIgnored(heard_prefix, hash_size)) { - return false; - } - if (hasFloodRetryPrefixes()) { - return floodRetryLastHopMatches(packet); - } - if (floodRetryEffectivePathLength(packet) <= floodRetryEffectivePathLength(packet, progress_marker)) { - return false; - } - return true; -} -uint8_t MyMesh::getDirectRetryMaxAttempts(const mesh::Packet* packet) const { - uint8_t configured_attempts = getDirectRetryConfiguredMaxAttempts(); - uint8_t total_hops = 0; - - if (packet != NULL) { - 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); - if (hash_size > 0) { - total_hops = (uint8_t)(route_bytes / hash_size); - } - } else { - total_hops = packet->getPathHashCount(); - } - } - - uint8_t path_cap = 15; - if (total_hops <= 3) { - path_cap = 8; - } else if (total_hops == 4) { - path_cap = 12; - } - - return configured_attempts < path_cap ? configured_attempts : path_cap; -} -uint32_t MyMesh::getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) { - uint32_t retry_delay = getDirectRetryEchoDelay(packet) + ((uint32_t)attempt_idx * getDirectRetryAttemptStepMillis()); - if (packet == NULL) { - return retry_delay; - } - return getDirectRetransmitDelay(packet) + retry_delay; -} bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) @@ -1812,7 +639,7 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32 mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl // if this a zero hop advert (and not via 'Share'), add it to neighbours - if (packet->path_len == 0 && !isShare(packet)) { + if (packet->getPathHashCount() == 0 && !isShare(packet)) { AdvertDataParser parser(app_data, app_data_len); if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters putNeighbour(id, timestamp, packet->getSNR()); @@ -1844,24 +671,13 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) { - if (hasUsablePath(client->out_path, client->out_path_len)) { - sendFloodReplyWithAltPath(path, client->out_path, client->out_path_len, - client->alt_path, client->alt_path_len, - SERVER_RESPONSE_DELAY, packet->getPathHashSize()); - } else { - sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); - } - } + if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - - if (hasUsablePath(client->out_path, client->out_path_len)) { // we have an out_path, so send DIRECT - sendDirectWithAltPath(reply, client->out_path, client->out_path_len, - client->alt_path, client->alt_path_len, SERVER_RESPONSE_DELAY); - + if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } @@ -1893,13 +709,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - - if (hasUsablePath(client->out_path, client->out_path_len)) { - sendDirectWithAltPath(ack, client->out_path, client->out_path_len, - client->alt_path, client->alt_path_len, TXT_ACK_DELAY); - - } else { + if (client->out_path_len == OUT_PATH_UNKNOWN) { sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); + } else { + sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); } } } @@ -1910,7 +723,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (is_retry) { *reply = 0; } else { - handleCommand(sender_timestamp, client, command, reply); + handleCommand(sender_timestamp, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -1924,13 +737,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - - if (hasUsablePath(client->out_path, client->out_path_len)) { - sendDirectWithAltPath(reply, client->out_path, client->out_path_len, - client->alt_path, client->alt_path_len, CLI_REPLY_DELAY_MILLIS); - - } else { + if (client->out_path_len == OUT_PATH_UNKNOWN) { sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); + } else { + sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); } } } @@ -1950,9 +760,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t auto client = acl.getClientByIdx(i); // store a copy of path, for sendDirect() - if (client->out_path_len != OUT_PATH_FORCE_FLOOD) { - client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); - } + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -2055,14 +863,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc last_millis = 0; uptime_millis = 0; next_local_advert = next_flood_advert = 0; - next_battery_alert_check = 0; - last_battery_alert_sent = 0; - battery_alert_sent = false; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; region_load_active = false; - memset(flood_retry_bridge_states, 0, sizeof(flood_retry_bridge_states)); #if MAX_NEIGHBOURS memset(neighbours, 0, sizeof(neighbours)); @@ -2071,26 +875,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc // defaults memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; - _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE; + _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 = 1; - _prefs.direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; - _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.retry_preset = RETRY_PRESET_ROOFTOP; - _prefs.flood_retry_attempts = 3; - _prefs.flood_retry_path_gate = 2; - _prefs.flood_retry_bridge_enabled = 0; - _prefs.flood_retry_advert_enabled = 0; - _prefs.battery_alert_enabled = 0; - _prefs.battery_alert_low_percent = LOW_BATTERY_WARN_PERCENT_DEFAULT; - _prefs.battery_alert_critical_percent = LOW_BATTERY_CRITICAL_PERCENT_DEFAULT; - _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; _prefs.node_lon = ADVERT_LON; @@ -2100,17 +887,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.bw = LORA_BW; _prefs.cr = LORA_CR; _prefs.tx_power_dbm = LORA_TX_POWER; - _prefs.advert_interval = DEFAULT_ADVERT_INTERVAL; - _prefs.flood_advert_interval = DEFAULT_FLOOD_ADVERT_INTERVAL; + _prefs.advert_interval = 1; // default to 2 minutes for NEW installs + _prefs.flood_advert_interval = 47; // 47 hours _prefs.flood_max = 64; _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled - _prefs.agc_reset_interval = DEFAULT_AGC_RESET_INTERVAL; - _prefs.multi_acks = DEFAULT_MULTI_ACKS; - _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; - _prefs.loop_detect = DEFAULT_LOOP_DETECT; - _prefs.powersaving_enabled = DEFAULT_POWERSAVING_ENABLED; + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') // bridge defaults _prefs.bridge_enabled = 1; // enabled @@ -2139,9 +922,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc pending_discover_tag = 0; pending_discover_until = 0; - active_bw = _prefs.bw; - active_sf = _prefs.sf; - active_cr = _prefs.cr; + + memset(default_scope.key, 0, sizeof(default_scope.key)); } void MyMesh::begin(FILESYSTEM *fs) { @@ -2149,15 +931,6 @@ void MyMesh::begin(FILESYSTEM *fs) { _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); -#ifdef CASCADIA_PROFILE - _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE; - _prefs.agc_reset_interval = DEFAULT_AGC_RESET_INTERVAL; - _prefs.advert_interval = DEFAULT_ADVERT_INTERVAL; - _prefs.flood_advert_interval = DEFAULT_FLOOD_ADVERT_INTERVAL; - _prefs.multi_acks = DEFAULT_MULTI_ACKS; - _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; - _prefs.loop_detect = DEFAULT_LOOP_DETECT; -#endif acl.load(_fs, self_id); // TODO: key_store.begin(); region_map.load(_fs); @@ -2188,12 +961,8 @@ 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; - ((SimpleMeshTables *)getTables())->setRecentRepeaterMinSNRX4(getDirectRetryMinSNRX4()); - radio_set_tx_power(_prefs.tx_power_dbm); + 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); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", @@ -2221,117 +990,6 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3 } } -void MyMesh::sendFloodScopedWithSelfPath(const TransportKey& scope, mesh::Packet* pkt, - uint32_t delay_millis, uint8_t path_hash_size) { - if (pkt == NULL) { - return; - } - if (path_hash_size == 0 || path_hash_size > MAX_ROUTE_HASH_BYTES) { - MESH_DEBUG_PRINTLN("%s MyMesh::sendFloodScopedWithSelfPath(): invalid path_hash_size", getLogDateTime()); - return; - } - - pkt->header &= ~PH_ROUTE_MASK; - if (scope.isNull()) { - pkt->header |= ROUTE_TYPE_FLOOD; - } else { - uint16_t codes[2]; - codes[0] = scope.calcTransportCode(pkt); - codes[1] = 0; - pkt->header |= ROUTE_TYPE_TRANSPORT_FLOOD; - pkt->transport_codes[0] = codes[0]; - pkt->transport_codes[1] = codes[1]; - } - - pkt->setPathHashSizeAndCount(path_hash_size, 1); - self_id.copyHashTo(pkt->path, path_hash_size); - getTables()->markSent(pkt); - sendPacket(pkt, 1, delay_millis); -} - -bool MyMesh::sendRepeatersFloodText(const char* text) { - if (text == NULL || *text == 0) return false; - - mesh::GroupChannel channel; - if (!buildRepeatersChannel(channel)) { - return false; - } - - uint8_t temp[MAX_PACKET_PAYLOAD]; - uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, ×tamp, 4); - temp[4] = (TXT_TYPE_PLAIN << 2); - - const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; - const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; - char sender_name[sizeof(_prefs.node_name)]; - StrHelper::strncpy(sender_name, _prefs.node_name, sizeof(sender_name)); - for (char* p = sender_name; *p; p++) { - if (*p == ':') { - *p = ';'; - } - } - int prefix_written = prefix_cap > 0 - ? snprintf((char*)&temp[5], prefix_cap, "%s: ", sender_name) - : -1; - if (prefix_written < 0) { - return false; - } - - size_t prefix_len = (size_t)prefix_written; - if (prefix_len >= prefix_cap) { - prefix_len = prefix_cap - 1; - } - - size_t text_len = strlen(text); - size_t max_text_len = max_data_len - 5 - prefix_len; - if (text_len > max_text_len) { - text_len = max_text_len; - } - memcpy(&temp[5 + prefix_len], text, text_len); - - auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); - if (pkt == NULL) { - return false; - } - - sendFloodScopedWithSelfPath(default_scope, pkt, 0, _prefs.path_hash_mode + 1); - return true; -} - -void MyMesh::checkBatteryAlert() { - if (!_prefs.battery_alert_enabled) { - battery_alert_sent = false; - return; - } - - if (next_battery_alert_check && !millisHasNowPassed(next_battery_alert_check)) { - return; - } - next_battery_alert_check = futureMillis(LOW_BATTERY_CHECK_INTERVAL); - - uint16_t batt_mv = board.getBattMilliVolts(); - uint8_t batt_pct = batteryPercentFromMilliVolts(batt_mv); - if (batt_mv <= LOW_BATTERY_MIN_VALID_MV || batt_pct >= _prefs.battery_alert_low_percent) { - battery_alert_sent = false; - return; - } - - unsigned long interval = batt_pct < _prefs.battery_alert_critical_percent - ? LOW_BATTERY_CRITICAL_INTERVAL - : LOW_BATTERY_WARN_INTERVAL; - if (battery_alert_sent && !millisHasNowPassed(last_battery_alert_sent + interval)) { - return; - } - - char text[96]; - snprintf(text, sizeof(text), "LOW BATTERY %u%% (%u mV)", (uint32_t)batt_pct, (uint32_t)batt_mv); - if (sendRepeatersFloodText(text)) { - battery_alert_sent = true; - last_battery_alert_sent = millis(); - } -} - void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) { set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params pending_freq = freq; @@ -2401,7 +1059,7 @@ void MyMesh::dumpLogFile() { } void MyMesh::setTxPower(int8_t power_dbm) { - radio_set_tx_power(power_dbm); + radio_driver.setTxPower(power_dbm); } #if defined(USE_SX1262) || defined(USE_SX1268) @@ -2516,114 +1174,7 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -static char* trimSpaces(char* s) { - while (*s == ' ') s++; - char* end = s + strlen(s); - while (end > s && end[-1] == ' ') end--; - *end = 0; - return s; -} - -static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len, const char*& err) { - if (raw == NULL || out_path == NULL) { - err = "Err - bad params"; - return false; - } - - char* spec = trimSpaces(raw); - if (*spec == 0) { - err = "Err - missing path"; - return false; - } - if (strcmp(spec, "clear") == 0 || strcmp(spec, "-") == 0 || strcmp(spec, "none") == 0) { - out_path_len = OUT_PATH_UNKNOWN; - return true; - } - if (strcmp(spec, "flood") == 0) { - out_path_len = OUT_PATH_FORCE_FLOOD; - return true; - } - if (strcmp(spec, "direct") == 0) { - out_path_len = 0; - return true; - } - - uint8_t hash_size = 0; - uint8_t hop_count = 0; - char* token = spec; - while (token && *token) { - char* comma = strchr(token, ','); - if (comma) *comma = 0; - token = trimSpaces(token); - - int hex_len = strlen(token); - if (!(hex_len == 2 || hex_len == 4 || hex_len == 6)) { - - err = "Err - each hop must be 1/2/3 bytes hex"; - - return false; - } - - uint8_t hop_hash_size = (uint8_t)(hex_len / 2); - if (hash_size == 0) { - hash_size = hop_hash_size; - } else if (hash_size != hop_hash_size) { - - err = "Err - mixed hash sizes in path"; - - return false; - } - - if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) { - - err = "Err - path too long"; - - return false; - } - if (!mesh::Utils::fromHex(&out_path[hop_count * hash_size], hash_size, token)) { - err = "Err - bad hex"; - return false; - } - - hop_count++; - token = comma ? comma + 1 : NULL; - } - - if (hash_size == 0 || hop_count == 0) { - err = "Err - missing path"; - return false; - } - out_path_len = ((hash_size - 1) << 6) | (hop_count & 63); - return true; -} - -static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, size_t out_len) { - if (path_len == OUT_PATH_FORCE_FLOOD) { - snprintf(out, out_len, "> flood"); - return; - } - if (path_len == OUT_PATH_UNKNOWN) { - snprintf(out, out_len, "> unknown"); - return; - } - if (!mesh::Packet::isValidPathLen(path_len)) { - snprintf(out, out_len, "> invalid"); - return; - } - if ((path_len & 63) == 0) { - snprintf(out, out_len, "> direct"); - return; - } - - uint8_t hash_size = (path_len >> 6) + 1; - uint8_t hop_count = path_len & 63; - uint8_t byte_len = hop_count * hash_size; - char hex[(MAX_PATH_SIZE * 2) + 1]; - mesh::Utils::toHex(hex, path, byte_len); - snprintf(out, out_len, "> hs=%u hops=%u hex=%s", (uint32_t)hash_size, (uint32_t)hop_count, hex); -} - -void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *command, char *reply) { +void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map @@ -2700,291 +1251,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * Serial.printf("\n"); } reply[0] = 0; - } else if (strcmp(command, "get outpath") == 0 - || strcmp(command, "set outpath") == 0 - - || strncmp(command, "set outpath ", 12) == 0 - || strcmp(command, "get altpath") == 0 - || strcmp(command, "set altpath") == 0 - || strncmp(command, "set altpath ", 12) == 0) { - bool is_get = strncmp(command, "get ", 4) == 0; - bool is_alt = strstr(command, "altpath") != NULL; - if (sender == NULL) { - strcpy(reply, "Err - command needs remote client context"); - } else if (is_get) { - formatPathReply(is_alt ? sender->alt_path : sender->out_path, - is_alt ? sender->alt_path_len : sender->out_path_len, - reply, 160); - } else { - char* spec = command + 11; // length of "set outpath"/"set altpath" - - if (*spec == ' ') spec++; - - uint8_t path[MAX_PATH_SIZE]; - uint8_t path_len = OUT_PATH_UNKNOWN; - const char* err = NULL; - if (!parsePathCommand(spec, path, path_len, err)) { - strcpy(reply, err ? err : "Err - invalid path"); - - } else if (is_alt && path_len == OUT_PATH_FORCE_FLOOD) { - strcpy(reply, "Err - bad params"); - } else { - if (is_alt) { - if (path_len == OUT_PATH_UNKNOWN) { - memset(sender->alt_path, 0, sizeof(sender->alt_path)); - sender->alt_path_len = OUT_PATH_UNKNOWN; - } else { - sender->alt_path_len = mesh::Packet::copyPath(sender->alt_path, path, path_len); - } - } else { - if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { - memset(sender->out_path, 0, sizeof(sender->out_path)); - sender->out_path_len = path_len; - } else { - sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); - } - } - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - formatPathReply(is_alt ? sender->alt_path : sender->out_path, - is_alt ? sender->alt_path_len : sender->out_path_len, - reply, 160); - } - } - } else if (strncmp(command, "send text.flood ", 16) == 0) { - char* text = trimSpaces(command + 16); - if (*text == 0) { - strcpy(reply, "Err - usage: send text.flood "); - } else if (sendRepeatersFloodText(text)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - unable to create packet"); - } - } else if (strcmp(command, "get battery.alert") == 0) { - sprintf(reply, "> %s", _prefs.battery_alert_enabled ? "on" : "off"); - } else if (strcmp(command, "get battery.alert.low") == 0) { - sprintf(reply, "> %u", (uint32_t)_prefs.battery_alert_low_percent); - } else if (strcmp(command, "get battery.alert.critical") == 0) { - sprintf(reply, "> %u", (uint32_t)_prefs.battery_alert_critical_percent); - } else if (strncmp(command, "set battery.alert ", 18) == 0) { - const char* value = command + 18; - if (strcmp(value, "on") == 0) { - _prefs.battery_alert_enabled = 1; - next_battery_alert_check = 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (strcmp(value, "off") == 0) { - _prefs.battery_alert_enabled = 0; - battery_alert_sent = false; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - usage: set battery.alert "); - } - } else if (strncmp(command, "set battery.alert.low ", 22) == 0) { - uint8_t percent; - if (!parseBatteryAlertPercent(command + 22, 1, 100, percent)) { - strcpy(reply, "Err - usage: set battery.alert.low <1-100>"); - } else if (percent <= _prefs.battery_alert_critical_percent) { - strcpy(reply, "Err - low must be greater than critical"); - } else { - _prefs.battery_alert_low_percent = percent; - next_battery_alert_check = 0; - savePrefs(); - strcpy(reply, "OK"); - } - } else if (strncmp(command, "set battery.alert.critical ", 27) == 0) { - uint8_t percent; - if (!parseBatteryAlertPercent(command + 27, 0, 99, percent)) { - strcpy(reply, "Err - usage: set battery.alert.critical <0-99>"); - } else if (percent >= _prefs.battery_alert_low_percent) { - strcpy(reply, "Err - critical must be less than low"); - } else { - _prefs.battery_alert_critical_percent = percent; - next_battery_alert_check = 0; - savePrefs(); - strcpy(reply, "OK"); - } - } 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) { - is_get = true; - sub = command + 19; - } 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++; - } - - 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 "); - } else { - *arg_snr++ = 0; - while (*arg_snr == ' ') arg_snr++; - if (*arg_snr == 0) { - strcpy(reply, "Err - usage: set recent.repeater "); - } else { - uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0}; - 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 { - 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, MAX_ROUTE_HASH_BYTES, (int8_t)snr_x4, true)) { - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - unable to store prefix"); - } - } - } - } - } else { - 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.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; - } - - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - formatRecentRepeaterPrefix(info, hex, sizeof(hex)); - char snr_text[12]; - formatRecentRepeaterSnrX4(info->snr_x4, snr_text, sizeof(snr_text)); - Serial.printf("%s,%s%s\n", - hex, - snr_text, - info->snr_locked ? ",l" : ""); - } - sprintf(reply, "> page=%ld/%d n=%d/%d", page_num, total_pages, limit, total); - } else { - 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 = sorted_recent[idx]; - if (info == NULL) { - continue; - } - if (written >= 154) { - truncated = true; - break; - } - - char hex[(MAX_ROUTE_HASH_BYTES * 2) + 1]; - 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%s,%s%s", - hex, - snr_text, - info->snr_locked ? ",l" : ""); - if (n < 0 || n >= (160 - written)) { - truncated = true; - break; - } - written += n; - } - if (truncated && written < 156) { - snprintf(reply + written, 160 - written, "\n... next page"); - } - } - } - free(sorted_recent); - } - } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; while (*sub == ' ') sub++; @@ -3005,7 +1271,6 @@ void MyMesh::loop() { #endif mesh::Mesh::loop(); - checkBatteryAlert(); if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet *pkt = createSelfAdvert(); @@ -3023,25 +1288,16 @@ 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; + radio_driver.setParams(pending_freq, pending_bw, pending_sf, 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; + radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); 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 23bb34f8..36c66c3f 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -206,6 +206,9 @@ protected: int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + bool getCADEnabled() const override { + return _prefs.cad_enabled; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index bbea97f5..c311c941 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -650,6 +650,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') #ifdef ROOM_PASSWORD StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); #endif diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 24c26418..380e54da 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -144,6 +144,9 @@ protected: int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + bool getCADEnabled() const override { + return _prefs.cad_enabled; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index d5066736..d93810ed 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -135,7 +135,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor { File file = _fs->open("/contacts", "w", true); #endif if (file) { - ContactsIterator iter; + ContactsIterator iter = startContactsIterator(); ContactInfo c; uint8_t unused = 0; uint32_t reserved = 0; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 879fcbf0..59c9aa09 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -323,6 +323,9 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { int SensorMesh::getInterferenceThreshold() const { return _prefs.interference_threshold; } +bool SensorMesh::getCADEnabled() const { + return _prefs.cad_enabled; +} int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } @@ -726,11 +729,13 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.disable_fwd = true; _prefs.flood_max = 64; _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') // GPS defaults _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.radio_fem_rxgain = 1; memset(default_scope.key, 0, sizeof(default_scope.key)); } @@ -766,6 +771,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index c9f135f6..1d65b877 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -120,6 +120,7 @@ protected: uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; int getInterferenceThreshold() const override; + bool getCADEnabled() const override; int getAGCResetInterval() const override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 85eae179..b870be9e 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -73,6 +73,7 @@ uint32_t Dispatcher::getCADFailMaxDuration() const { void Dispatcher::loop() { if (millisHasNowPassed(next_floor_calib_time)) { _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); + _radio->setCADEnabled(getCADEnabled()); next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL); } _radio->loop(); diff --git a/src/Dispatcher.h b/src/Dispatcher.h index f97a058c..e6daae4b 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -71,6 +71,8 @@ public: virtual void triggerNoiseFloorCalibrate(int threshold) { } + virtual void setCADEnabled(bool enable) { } + virtual void resetAGC() { } virtual bool isInRecvMode() const = 0; @@ -178,6 +180,7 @@ protected: virtual uint32_t getCADFailMaxDuration() const; virtual uint8_t getDefaultTxCodingRate() const { return 0; } virtual int getInterferenceThreshold() const { return 0; } // disabled by default + virtual bool getCADEnabled() const { return false; } // hardware CAD 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; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index dbdbbf24..4dccd45a 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -334,6 +334,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { int k = 0; uint8_t path_len = data[k++]; + if (!Packet::isValidPathLen(path_len)) { + MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, bad path_len: %u", getLogDateTime(), (uint32_t)path_len); + break; // reject bad encoding + } uint8_t hash_size = (path_len >> 6) + 1; uint8_t hash_count = path_len & 63; uint8_t* path = &data[k]; k += hash_size*hash_count; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 7ddc461d..972a97e9 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -38,19 +38,19 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl return createAdvert(self_id, app_data, app_data_len); } -void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { +void BaseChatMesh::sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len) { if (dest.out_path_len == OUT_PATH_UNKNOWN) { - mesh::Packet* ack = createAck(ack_hash); + mesh::Packet* ack = createAck(ack_hash, ack_len); if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { - mesh::Packet* a1 = createMultiAck(ack_hash, 1); + mesh::Packet* a1 = createMultiAck(ack_hash, ack_len, 1); if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d); d += 300; } - mesh::Packet* a2 = createAck(ack_hash); + mesh::Packet* a2 = createAck(ack_hash, ack_len); if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d); } } @@ -67,24 +67,38 @@ void BaseChatMesh::bootstrapRTCfromContacts() { } } -ContactInfo* BaseChatMesh::allocateContactSlot() { - if (num_contacts < MAX_CONTACTS) { - return &contacts[num_contacts++]; - } else if (shouldOverwriteWhenFull()) { - // Find oldest non-favourite contact by oldest lastmod timestamp - int oldest_idx = -1; - uint32_t oldest_lastmod = 0xFFFFFFFF; - for (int i = 0; i < num_contacts; i++) { - bool is_favourite = (contacts[i].flags & 0x01) != 0; - if (!is_favourite && contacts[i].lastmod < oldest_lastmod) { +ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) { + int oldest_idx = -1; + uint32_t oldest_lastmod = 0xFFFFFFFF; + if (transient_only) { + // only allocate from first N + for (int i = 0; i < MAX_ANON_CONTACTS; i++) { + if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) { oldest_lastmod = contacts[i].lastmod; oldest_idx = i; } } if (oldest_idx >= 0) { - onContactOverwrite(contacts[oldest_idx].id.pub_key); + // NOTE: do NOT call onContactOverwrite() return &contacts[oldest_idx]; } + } else { + if (num_contacts < MAX_ANON_CONTACTS+MAX_CONTACTS) { + return &contacts[num_contacts++]; + } else if (shouldOverwriteWhenFull()) { + // Find oldest non-favourite contact by oldest lastmod timestamp + for (int i = MAX_ANON_CONTACTS; i < num_contacts; i++) { + bool is_favourite = (contacts[i].flags & 0x01) != 0; + if (!is_favourite && contacts[i].lastmod < oldest_lastmod) { + oldest_lastmod = contacts[i].lastmod; + oldest_idx = i; + } + } + if (oldest_idx >= 0) { + onContactOverwrite(contacts[oldest_idx].id.pub_key); + return &contacts[oldest_idx]; + } + } } return NULL; // no space, no overwrite or all contacts are all favourites } @@ -132,6 +146,11 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, packet->header = save; } + if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ? + memset(from, 0, sizeof(*from)); // clear the anon/temp slot + from = NULL; // do normal 'add' flow + } + bool is_new = false; // true = not in contacts[], false = exists in contacts[] if (from == NULL) { if (!shouldAutoAddContactType(parser.getType())) { @@ -164,16 +183,17 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, from->sync_since = 0; from->shared_secret_valid = false; } + // update - putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen); - StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name)); - from->type = parser.getType(); - if (parser.hasLatLon()) { - from->gps_lat = parser.getIntLat(); - from->gps_lon = parser.getIntLon(); - } - from->last_advert_timestamp = timestamp; - from->lastmod = getRTCClock()->getCurrentTime(); + putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen); + StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name)); + from->type = parser.getType(); + if (parser.hasLatLon()) { + from->gps_lat = parser.getIntLat(); + from->gps_lon = parser.getIntLon(); + } + from->last_advert_timestamp = timestamp; + from->lastmod = getRTCClock()->getCurrentTime(); onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know } @@ -218,16 +238,20 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know - uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it - mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE); + int text_len = strlen((char *)&data[5]); + uint8_t ack_hash[6]; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it + mesh::Utils::sha256(ack_hash, 4, data, 5 + text_len, from.id.pub_key, PUB_KEY_SIZE); + // NEW: append (potential) extended attempt byte (to make packethash unique) + ack_hash[4] = data[5 + text_len + 1]; + getRNG()->random(&ack_hash[5], 1); // make 6th byte random if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); + PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 6); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { - sendAckTo(from, ack_hash); + sendAckTo(from, ack_hash, 6); } } else if (flags == TXT_TYPE_CLI_DATA) { onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know @@ -254,7 +278,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { - sendAckTo(from, ack_hash); + sendAckTo(from, (uint8_t *) &ack_hash); } } else { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags); @@ -825,7 +849,7 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre } bool BaseChatMesh::addContact(const ContactInfo& contact) { - ContactInfo* dest = allocateContactSlot(); + ContactInfo* dest = allocateContactSlot(contact.type == ADV_TYPE_NONE); if (dest) { *dest = contact; dest->shared_secret_valid = false; // mark shared_secret as needing calculation @@ -918,11 +942,11 @@ bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) { } ContactsIterator BaseChatMesh::startContactsIterator() { - return ContactsIterator(); + return ContactsIterator(MAX_ANON_CONTACTS); // start at offset, skip the anon entries } bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) { - if (next_idx >= mesh->getNumContacts()) return false; + if (next_idx >= mesh->getTotalContactSlots()) return false; dest = mesh->contacts[next_idx++]; return true; diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index b39e7363..c6027468 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -28,8 +28,9 @@ public: class BaseChatMesh; class ContactsIterator { - int next_idx = 0; + int next_idx; public: + ContactsIterator(int start) { next_idx = start; } bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest); }; @@ -77,8 +78,9 @@ class BaseChatMesh : public mesh::Mesh { protected: BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, mgr, tables) - { - num_contacts = 0; + { + resetContacts(); + #ifdef MAX_GROUP_CHANNELS memset(channels, 0, sizeof(channels)); num_channels = 0; @@ -89,7 +91,11 @@ protected: } void bootstrapRTCfromContacts(); - void resetContacts() { num_contacts = 0; } + + void resetContacts() { + memset(contacts, 0, sizeof(contacts[0])*MAX_ANON_CONTACTS); // set all to have type = ADV_TYPE_NONE(0) + 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 @@ -164,7 +170,8 @@ public: ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len); bool removeContact(ContactInfo& contact); bool addContact(const ContactInfo& contact); - int getNumContacts() const { return num_contacts; } + int getTotalContactSlots() const { return num_contacts; } + int getNumContacts() const { return num_contacts - MAX_ANON_CONTACTS; } // don't include the reserved slots at start bool getContactByIdx(uint32_t idx, ContactInfo& contact); ContactsIterator startContactsIterator(); ChannelDetails* addChannel(const char* name, const char* psk_base64); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 7cacb185..82e53743 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -2,40 +2,13 @@ #include "CommonCLI.h" #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" +#include "TxtDataHelpers.h" #include -#include -#define STR_HELPER(x) #x -#define STR(x) STR_HELPER(x) #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 1 -#define DIRECT_RETRY_SNR_MARGIN_DB_DEFAULT_X4 DIRECT_RETRY_ROOFTOP_MARGIN_X4 -#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 DIRECT_RETRY_ROOFTOP_COUNT -#define DIRECT_RETRY_COUNT_MIN 1 -#define DIRECT_RETRY_COUNT_MAX 15 -#define DIRECT_RETRY_BASE_MS_DEFAULT DIRECT_RETRY_ROOFTOP_BASE_MS -#define DIRECT_RETRY_BASE_MS_MIN 10 -#define DIRECT_RETRY_BASE_MS_MAX 5000 -#define DIRECT_RETRY_STEP_MS_DEFAULT DIRECT_RETRY_ROOFTOP_STEP_MS -#define DIRECT_RETRY_STEP_MS_MIN 0 -#define DIRECT_RETRY_STEP_MS_MAX 5000 -#define RETRY_PRESET_DEFAULT RETRY_PRESET_ROOFTOP -#define FLOOD_RETRY_PREFS_MAGIC_0 0xF4 -#define FLOOD_RETRY_PREFS_MAGIC_1 0x52 -#define FLOOD_RETRY_COUNT_MIN 0 -#define FLOOD_RETRY_COUNT_MAX 15 -#define FLOOD_RETRY_ADVERT_DEFAULT 0 - // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -46,406 +19,6 @@ static uint32_t _atoi(const char* sp) { return n; } -static bool parseUint8Strict(const char* value, uint8_t min_value, uint8_t max_value, uint8_t& result) { - if (value == NULL || *value == 0) { - return false; - } - - uint16_t parsed = 0; - const char* sp = value; - while (*sp) { - if (*sp < '0' || *sp > '9') { - return false; - } - parsed = (uint16_t)((parsed * 10) + (*sp - '0')); - if (parsed > max_value) { - return false; - } - sp++; - } - if (parsed < min_value) { - return false; - } - result = (uint8_t)parsed; - return true; -} - -static uint8_t directRetryMarginDbToX4(float margin_db) { - int32_t scaled_x4 = (int32_t)((margin_db * 4.0f) + 0.5f); // nearest 0.25 dB - 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 uint8_t retryPresetOrDefault(uint8_t preset) { - if (preset <= RETRY_PRESET_MOBILE) { - return preset; - } - return RETRY_PRESET_DEFAULT; -} - -static const char* retryPresetName(uint8_t preset) { - switch (retryPresetOrDefault(preset)) { - case RETRY_PRESET_INFRA: - return "infra"; - case RETRY_PRESET_MOBILE: - return "mobile"; - case RETRY_PRESET_ROOFTOP: - default: - return "rooftop"; - } -} - -static uint8_t directRetryEffectiveCount(const NodePrefs* prefs) { - return constrain(prefs->direct_retry_attempts, (uint8_t)DIRECT_RETRY_COUNT_MIN, (uint8_t)DIRECT_RETRY_COUNT_MAX); -} - -static uint16_t directRetryEffectiveBaseMs(const NodePrefs* prefs) { - return constrain(prefs->direct_retry_base_ms, (uint16_t)DIRECT_RETRY_BASE_MS_MIN, (uint16_t)DIRECT_RETRY_BASE_MS_MAX); -} - -static uint16_t directRetryEffectiveStepMs(const NodePrefs* prefs) { - return constrain(prefs->direct_retry_step_ms, (uint16_t)DIRECT_RETRY_STEP_MS_MIN, (uint16_t)DIRECT_RETRY_STEP_MS_MAX); -} - -static uint8_t directRetryEffectiveMarginX4(const NodePrefs* prefs) { - return constrain(prefs->direct_retry_snr_margin_db, (uint8_t)0, (uint8_t)DIRECT_RETRY_SNR_MARGIN_X4_MAX); -} - -static float directRetryCrX4ToDb(int8_t snr_x4) { - return ((float)snr_x4) / 4.0f; -} - -static void setDirectRetryCrDefaults(NodePrefs* prefs) { - prefs->direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; - prefs->direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; - prefs->direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; - prefs->direct_retry_cr8_snr_x4 = DIRECT_RETRY_CR8_MAX_SNR_X4_DEFAULT; -} - -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) { - 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, cr7, cr8); -} - -static uint16_t retryPresetStepDefault(uint8_t preset) { - switch (retryPresetOrDefault(preset)) { - case RETRY_PRESET_INFRA: - return DIRECT_RETRY_INFRA_STEP_MS; - case RETRY_PRESET_MOBILE: - return DIRECT_RETRY_MOBILE_STEP_MS; - case RETRY_PRESET_ROOFTOP: - default: - return DIRECT_RETRY_ROOFTOP_STEP_MS; - } -} - -static uint8_t floodRetryPresetCountDefault(uint8_t preset) { - switch (retryPresetOrDefault(preset)) { - case RETRY_PRESET_INFRA: - return 1; - case RETRY_PRESET_MOBILE: - case RETRY_PRESET_ROOFTOP: - default: - return 3; - } -} - -static uint8_t floodRetryPresetPathDefault(uint8_t preset) { - switch (retryPresetOrDefault(preset)) { - case RETRY_PRESET_INFRA: - return 1; - case RETRY_PRESET_MOBILE: - return 1; - case RETRY_PRESET_ROOFTOP: - default: - return 2; - } -} - -static void applyFloodRetryPreset(NodePrefs* prefs, uint8_t preset) { - prefs->flood_retry_attempts = floodRetryPresetCountDefault(preset); - prefs->flood_retry_path_gate = floodRetryPresetPathDefault(preset); -} - -static uint8_t floodRetryEffectiveCount(const NodePrefs* prefs) { - return constrain(prefs->flood_retry_attempts, (uint8_t)FLOOD_RETRY_COUNT_MIN, (uint8_t)FLOOD_RETRY_COUNT_MAX); -} - -static void applyRetryPreset(NodePrefs* prefs, uint8_t preset) { - prefs->retry_preset = retryPresetOrDefault(preset); - switch (prefs->retry_preset) { - case RETRY_PRESET_INFRA: - prefs->direct_retry_base_ms = DIRECT_RETRY_INFRA_BASE_MS; - prefs->direct_retry_attempts = DIRECT_RETRY_INFRA_COUNT; - prefs->direct_retry_step_ms = DIRECT_RETRY_INFRA_STEP_MS; - prefs->direct_retry_snr_margin_db = DIRECT_RETRY_INFRA_MARGIN_X4; - break; - case RETRY_PRESET_MOBILE: - prefs->direct_retry_base_ms = DIRECT_RETRY_MOBILE_BASE_MS; - prefs->direct_retry_attempts = DIRECT_RETRY_MOBILE_COUNT; - prefs->direct_retry_step_ms = DIRECT_RETRY_MOBILE_STEP_MS; - prefs->direct_retry_snr_margin_db = DIRECT_RETRY_MOBILE_MARGIN_X4; - break; - case RETRY_PRESET_ROOFTOP: - default: - prefs->direct_retry_base_ms = DIRECT_RETRY_ROOFTOP_BASE_MS; - prefs->direct_retry_attempts = DIRECT_RETRY_ROOFTOP_COUNT; - prefs->direct_retry_step_ms = DIRECT_RETRY_ROOFTOP_STEP_MS; - prefs->direct_retry_snr_margin_db = DIRECT_RETRY_ROOFTOP_MARGIN_X4; - break; - } - applyFloodRetryPreset(prefs, prefs->retry_preset); -} - -static void formatRetryPreset(char* reply, const NodePrefs* prefs) { - sprintf(reply, "> %d,%s", - (uint32_t)retryPresetOrDefault(prefs->retry_preset), - retryPresetName(prefs->retry_preset)); -} - -static bool parseFloodRetryPathGate(const char* value, uint8_t& path_gate) { - if (value == NULL) { - return false; - } - if (strcmp(value, "off") == 0 || strcmp(value, "disabled") == 0 || strcmp(value, "disable") == 0) { - path_gate = FLOOD_RETRY_PATH_GATE_DISABLED; - return true; - } - return parseUint8Strict(value, 0, 63, path_gate); -} - -static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) { - if (path_gate == FLOOD_RETRY_PATH_GATE_DISABLED) { - strcpy(dest, "off"); - } else { - sprintf(dest, "%u", (unsigned int)path_gate); - } -} - -static void formatFloodRetryPrefixList(char* dest, const uint8_t prefixes[][FLOOD_RETRY_PREFIX_LEN], - uint8_t max_prefixes) { - char* out = dest; - bool first = true; - for (int i = 0; i < max_prefixes; i++) { - const uint8_t* prefix = prefixes[i]; - if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { - continue; - } - if (!first) { - *out++ = ','; - } - mesh::Utils::toHex(out, prefix, FLOOD_RETRY_PREFIX_LEN); - out += FLOOD_RETRY_PREFIX_LEN * 2; - first = false; - } - *out = 0; -} - -static void formatFloodRetryPrefixes(char* dest, const NodePrefs* prefs) { - formatFloodRetryPrefixList(dest, prefs->flood_retry_prefixes, FLOOD_RETRY_PREFIX_SLOTS); -} - -static void formatFloodRetryBridgeBucket(char* dest, const NodePrefs* prefs, uint8_t bucket) { - if (bucket >= FLOOD_RETRY_BRIDGE_BUCKETS) { - dest[0] = 0; - return; - } - formatFloodRetryPrefixList(dest, prefs->flood_retry_bridge_buckets[bucket], FLOOD_RETRY_BUCKET_PREFIXES); -} - -static bool parseFloodRetryPrefixes(NodePrefs* prefs, const char* value) { - uint8_t parsed[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; - memset(parsed, 0, sizeof(parsed)); - if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { - memcpy(prefs->flood_retry_prefixes, parsed, sizeof(prefs->flood_retry_prefixes)); - return true; - } - - char tmp[FLOOD_RETRY_LIST_TEXT_MAX]; - StrHelper::strncpy(tmp, value, sizeof(tmp)); - const char* parts[FLOOD_RETRY_PREFIX_SLOTS + 1]; - int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_PREFIX_SLOTS + 1); - if (num > FLOOD_RETRY_PREFIX_SLOTS) { - return false; - } - for (int i = 0; i < num; i++) { - if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { - return false; - } - for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { - if (!mesh::Utils::isHexChar(parts[i][j])) { - return false; - } - } - if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i])) { - return false; - } - const uint8_t* prefix = parsed[i]; - if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == 0) { - return false; - } - } - memcpy(prefs->flood_retry_prefixes, parsed, sizeof(prefs->flood_retry_prefixes)); - return true; -} - -static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes, const char* value) { - if (max_prefixes > FLOOD_RETRY_LIST_PREFIXES) { - return false; - } - uint8_t parsed[FLOOD_RETRY_LIST_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; - memset(parsed, 0, sizeof(parsed)); - if (value == NULL || value[0] == 0 || strcmp(value, "none") == 0 || strcmp(value, "off") == 0) { - memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); - return true; - } - - char tmp[FLOOD_RETRY_LIST_TEXT_MAX]; - StrHelper::strncpy(tmp, value, sizeof(tmp)); - const char* parts[FLOOD_RETRY_LIST_PREFIXES + 1]; - int num = mesh::Utils::parseTextParts(tmp, parts, FLOOD_RETRY_LIST_PREFIXES + 1); - if (num > max_prefixes) { - return false; - } - for (int i = 0; i < num; i++) { - if (strlen(parts[i]) != FLOOD_RETRY_PREFIX_LEN * 2) { - return false; - } - for (int j = 0; j < FLOOD_RETRY_PREFIX_LEN * 2; j++) { - if (!mesh::Utils::isHexChar(parts[i][j])) { - return false; - } - } - if (!mesh::Utils::fromHex(parsed[i], FLOOD_RETRY_PREFIX_LEN, parts[i]) - || (parsed[i][0] == 0 && parsed[i][1] == 0 && parsed[i][2] == 0)) { - return false; - } - } - memcpy(dest, parsed, max_prefixes * FLOOD_RETRY_PREFIX_LEN); - return true; -} - -static bool parseRetryPreset(const char* value, uint8_t& preset) { - if (value == NULL) { - return false; - } - if (strcmp(value, "0") == 0 || strcmp(value, "infra") == 0 - || strcmp(value, "infa") == 0 || strcmp(value, "infrastructure") == 0) { - preset = RETRY_PRESET_INFRA; - return true; - } - if (strcmp(value, "1") == 0 || strcmp(value, "rooftop") == 0) { - preset = RETRY_PRESET_ROOFTOP; - return true; - } - if (strcmp(value, "2") == 0 || strcmp(value, "mobile") == 0) { - preset = RETRY_PRESET_MOBILE; - return true; - } - return false; -} - -static bool parseDirectRetryCrDb(const char* value, int8_t& snr_x4) { - if (value == NULL) { - return false; - } - - char* end = NULL; - float snr_db = strtof(value, &end); - while (end != NULL && *end == ' ') end++; - if (end == value || (end != NULL && *end != 0)) { - return false; - } - - int32_t scaled_x4 = (int32_t)((snr_db * 4.0f) + (snr_db >= 0.0f ? 0.5f : -0.5f)); - if (scaled_x4 < DIRECT_RETRY_CR_SNR_X4_MIN || scaled_x4 > DIRECT_RETRY_CR_SNR_X4_MAX) { - return false; - } - snr_x4 = (int8_t)scaled_x4; - return true; -} - -static bool parseDirectRetryCrThresholds(char* value, NodePrefs* prefs) { - if (value == NULL || prefs == NULL) { - return false; - } - if (strcmp(value, "off") == 0) { - setDirectRetryCrOff(prefs); - return true; - } - - 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[2], cr7_snr_x4) - || !parseDirectRetryCrDb(parts[3], cr8_snr_x4)) { - return false; - } - - 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; -} - static bool isValidName(const char *n) { while (*n) { if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; @@ -461,8 +34,6 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { loadPrefsInt(fs, "/node_prefs"); savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old - } else { - setDirectRetryCrDefaults(_prefs); } } @@ -490,9 +61,7 @@ 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((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(pad, 4); // 108 : 4 bytes unused 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 @@ -512,74 +81,19 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.read((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 - file.read(pad, 2); // 154 + file.read(pad, 3); // 153 file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 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 - file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - uint8_t legacy_retry_attempts_or_radio_fem_rxgain = _prefs->direct_retry_attempts; - size_t legacy_retry_attempts_read = file.read((uint8_t *)&legacy_retry_attempts_or_radio_fem_rxgain, - sizeof(legacy_retry_attempts_or_radio_fem_rxgain)); // 291 - if (legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain)) { - _prefs->direct_retry_attempts = legacy_retry_attempts_or_radio_fem_rxgain; - } - file.read((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 - file.read((uint8_t *)&_prefs->direct_retry_timing_magic[0], sizeof(_prefs->direct_retry_timing_magic)); // 294 - size_t radio_fem_rxgain_read = file.read((uint8_t *)&_prefs->radio_fem_rxgain, - sizeof(_prefs->radio_fem_rxgain)); // 296 - file.read((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 297 - size_t retry_step_read = file.read((uint8_t *)&_prefs->direct_retry_step_ms, - sizeof(_prefs->direct_retry_step_ms)); // 298 - size_t flood_retry_attempts_read = file.read((uint8_t *)&_prefs->flood_retry_attempts, - sizeof(_prefs->flood_retry_attempts)); // 300 - file.read((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 - file.read((uint8_t *)&_prefs->flood_retry_prefs_magic[0], sizeof(_prefs->flood_retry_prefs_magic)); // 302 - file.read((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 - file.read((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 - memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); - memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); - file.read((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 - size_t flood_retry_ignore_read = file.read((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], - sizeof(_prefs->flood_retry_ignore_prefixes)); // 635 - _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; - size_t flood_retry_advert_read = file.read((uint8_t *)&_prefs->flood_retry_advert_enabled, - sizeof(_prefs->flood_retry_advert_enabled)); // 659 - size_t retry_cr_read = 0; - retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, - sizeof(_prefs->direct_retry_cr4_snr_x4)); // 660 - retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, - sizeof(_prefs->direct_retry_cr5_snr_x4)); // 661 - retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, - sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 - retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, - sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 - _prefs->battery_alert_enabled = 0; - size_t battery_alert_read = file.read((uint8_t *)&_prefs->battery_alert_enabled, - sizeof(_prefs->battery_alert_enabled)); // 664 - _prefs->battery_alert_low_percent = 20; - _prefs->battery_alert_critical_percent = 10; - size_t battery_alert_low_read = file.read((uint8_t *)&_prefs->battery_alert_low_percent, - sizeof(_prefs->battery_alert_low_percent)); // 665 - size_t battery_alert_critical_read = file.read((uint8_t *)&_prefs->battery_alert_critical_percent, - sizeof(_prefs->battery_alert_critical_percent)); // 666 - _prefs->flood_max_unscoped = 64; - size_t flood_max_unscoped_read = file.read((uint8_t *)&_prefs->flood_max_unscoped, - sizeof(_prefs->flood_max_unscoped)); // 667 - _prefs->flood_max_advert = 8; - size_t flood_max_advert_read = file.read((uint8_t *)&_prefs->flood_max_advert, - sizeof(_prefs->flood_max_advert)); // 668 - // 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) - && (_prefs->direct_retry_timing_magic[0] != DIRECT_RETRY_TIMING_MAGIC_0 - || _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: 669 + file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 + file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 + 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 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -594,23 +108,6 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future - // 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_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_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) { - _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); @@ -620,7 +117,6 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14); _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1); - _prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); @@ -628,72 +124,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean - if (flood_max_unscoped_read != sizeof(_prefs->flood_max_unscoped)) { - _prefs->flood_max_unscoped = 64; - } else { - _prefs->flood_max_unscoped = constrain(_prefs->flood_max_unscoped, 0, 64); - } - if (flood_max_advert_read != sizeof(_prefs->flood_max_advert)) { - _prefs->flood_max_advert = 8; - } else { - _prefs->flood_max_advert = constrain(_prefs->flood_max_advert, 0, 64); - } - _prefs->retry_preset = retryPresetOrDefault(_prefs->retry_preset); - if (retry_step_read != sizeof(_prefs->direct_retry_step_ms)) { - _prefs->direct_retry_step_ms = retryPresetStepDefault(_prefs->retry_preset); - } else { - _prefs->direct_retry_step_ms = constrain(_prefs->direct_retry_step_ms, DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); - } - if (flood_retry_attempts_read != sizeof(_prefs->flood_retry_attempts) - || _prefs->flood_retry_prefs_magic[0] != FLOOD_RETRY_PREFS_MAGIC_0 - || _prefs->flood_retry_prefs_magic[1] != FLOOD_RETRY_PREFS_MAGIC_1) { - applyFloodRetryPreset(_prefs, _prefs->retry_preset); - memset(_prefs->flood_retry_prefixes, 0, sizeof(_prefs->flood_retry_prefixes)); - _prefs->flood_retry_bridge_enabled = 0; - memset(_prefs->flood_retry_bridge_buckets, 0, sizeof(_prefs->flood_retry_bridge_buckets)); - memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); - _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; - } else { - _prefs->flood_retry_attempts = constrain(_prefs->flood_retry_attempts, FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); - if (_prefs->flood_retry_path_gate > 63 && _prefs->flood_retry_path_gate != FLOOD_RETRY_PATH_GATE_DISABLED) { - _prefs->flood_retry_path_gate = floodRetryPresetPathDefault(_prefs->retry_preset); - } - _prefs->flood_retry_bridge_enabled = constrain(_prefs->flood_retry_bridge_enabled, 0, 1); - if (flood_retry_ignore_read != sizeof(_prefs->flood_retry_ignore_prefixes)) { - memset(_prefs->flood_retry_ignore_prefixes, 0, sizeof(_prefs->flood_retry_ignore_prefixes)); - } - if (flood_retry_advert_read != sizeof(_prefs->flood_retry_advert_enabled)) { - _prefs->flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; - } else { - _prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1); - } - } - 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 { - sanitizeDirectRetryCrThresholds(_prefs); - } - if (battery_alert_read != sizeof(_prefs->battery_alert_enabled)) { - _prefs->battery_alert_enabled = 0; - } else { - _prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1); - } - if (battery_alert_low_read != sizeof(_prefs->battery_alert_low_percent) - || battery_alert_critical_read != sizeof(_prefs->battery_alert_critical_percent) - || _prefs->battery_alert_low_percent < 1 - || _prefs->battery_alert_low_percent > 100 - || _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) { - _prefs->battery_alert_low_percent = 20; - _prefs->battery_alert_critical_percent = 10; - } + _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean file.close(); } @@ -727,11 +158,7 @@ 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((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(pad, 4); // 108 : 4 byte unused 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 @@ -751,41 +178,19 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.write((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 - file.write(pad, 2); // 154 + file.write(pad, 3); // 153 file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 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 - file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - file.write((uint8_t *)&_prefs->direct_retry_attempts, sizeof(_prefs->direct_retry_attempts)); // 291 - file.write((uint8_t *)&_prefs->direct_retry_base_ms, sizeof(_prefs->direct_retry_base_ms)); // 292 - 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)); // 294 - file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 296 - file.write((uint8_t *)&_prefs->retry_preset, sizeof(_prefs->retry_preset)); // 297 - file.write((uint8_t *)&_prefs->direct_retry_step_ms, sizeof(_prefs->direct_retry_step_ms)); // 298 - file.write((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 300 - file.write((uint8_t *)&_prefs->flood_retry_path_gate, sizeof(_prefs->flood_retry_path_gate)); // 301 - uint8_t flood_retry_magic[2] = { FLOOD_RETRY_PREFS_MAGIC_0, FLOOD_RETRY_PREFS_MAGIC_1 }; - file.write(flood_retry_magic, sizeof(flood_retry_magic)); // 302 - file.write((uint8_t *)&_prefs->flood_retry_prefixes[0][0], sizeof(_prefs->flood_retry_prefixes)); // 304 - file.write((uint8_t *)&_prefs->flood_retry_bridge_enabled, sizeof(_prefs->flood_retry_bridge_enabled)); // 328 - file.write((uint8_t *)&_prefs->flood_retry_bridge_buckets[0][0][0], sizeof(_prefs->flood_retry_bridge_buckets)); // 329 - file.write((uint8_t *)&_prefs->flood_retry_ignore_prefixes[0][0], sizeof(_prefs->flood_retry_ignore_prefixes)); // 635 - file.write((uint8_t *)&_prefs->flood_retry_advert_enabled, sizeof(_prefs->flood_retry_advert_enabled)); // 659 - file.write((uint8_t *)&_prefs->direct_retry_cr4_snr_x4, sizeof(_prefs->direct_retry_cr4_snr_x4)); // 660 - file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 661 - file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 - file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 - file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); // 664 - file.write((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent)); // 665 - file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent)); // 666 - file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 667 - file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 668 - // next: 669 + file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 + file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 + 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 file.close(); } @@ -847,7 +252,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "clock", 5) == 0) { uint32_t now = getRTCClock()->getCurrentTime(); DateTime dt = DateTime(now); - sprintf(reply, "%02d:%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), dt.year()); + sprintf(reply, "%02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year()); } else if (memcmp(command, "time ", 5) == 0) { // set time (to epoch seconds) uint32_t secs = _atoi(&command[5]); uint32_t curr = getRTCClock()->getCurrentTime(); @@ -891,596 +296,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); savePrefs(); - sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!! + sprintf(reply, "password now: "); + StrHelper::strncpy(&reply[14], _prefs->password, 160-15); // echo back just to let admin know for sure!! } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); } else if (memcmp(command, "get ", 4) == 0) { - const char* config = &command[4]; - if (memcmp(config, "af", 2) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); - } else if (memcmp(config, "int.thresh", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); - } else if (memcmp(config, "agc.reset.interval", 18) == 0) { - sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks", 10) == 0) { - sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks); - } else if (memcmp(config, "allow.read.only", 15) == 0) { - sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off"); - } else if (memcmp(config, "flood.advert.interval", 21) == 0) { - sprintf(reply, "> %d", ((uint32_t) _prefs->flood_advert_interval)); - } else if (memcmp(config, "advert.interval", 15) == 0) { - sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2); - } else if (memcmp(config, "guest.password", 14) == 0) { - sprintf(reply, "> %s", _prefs->guest_password); - } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only - uint8_t prv_key[PRV_KEY_SIZE]; - int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); - mesh::Utils::toHex(tmp, prv_key, len); - sprintf(reply, "> %s", tmp); - } else if (memcmp(config, "name", 4) == 0) { - sprintf(reply, "> %s", _prefs->node_name); - } else if (memcmp(config, "repeat", 6) == 0) { - sprintf(reply, "> %s", _prefs->disable_fwd ? "off" : "on"); - } else if (memcmp(config, "lat", 3) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); - } else if (memcmp(config, "lon", 3) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); -#if defined(USE_SX1262) || defined(USE_SX1268) - } else if (memcmp(config, "radio.rxgain", 12) == 0) { - sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); -#endif - } else if (memcmp(config, "radio", 5) == 0) { - char freq[16], bw[16]; - strcpy(freq, StrHelper::ftoa(_prefs->freq)); - strcpy(bw, StrHelper::ftoa3(_prefs->bw)); - sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)_prefs->sf, (uint32_t)_prefs->cr); - } else if (memcmp(config, "rxdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base)); - } else if (memcmp(config, "txdelay", 7) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor)); - } else if (memcmp(config, "flood.max", 9) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); - } else if (memcmp(config, "retry.preset", 12) == 0) { - formatRetryPreset(reply, _prefs); - } else if (memcmp(config, "flood.retry.count", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); - } else if (memcmp(config, "flood.retry.path", 16) == 0) { - char path_gate[8]; - formatFloodRetryPathGate(path_gate, _prefs->flood_retry_path_gate); - sprintf(reply, "> %s", path_gate); - } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { - formatFloodRetryPrefixes(tmp, _prefs); - sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { - formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); - sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else if (memcmp(config, "flood.retry.advert", 18) == 0) { - sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off"); - } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { - sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); - } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { - uint8_t bucket = atoi(&config[19]); - if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { - formatFloodRetryBridgeBucket(tmp, _prefs, bucket - 1); - sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else { - sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); - } - } 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, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); - } else if (memcmp(config, "direct.retry.count", 18) == 0) { - sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); - } else if (memcmp(config, "direct.retry.base", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); - } else if (memcmp(config, "direct.retry.step", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); - } else if (memcmp(config, "direct.retry.cr", 15) == 0) { - formatDirectRetryCrThresholds(_prefs, reply); - } else if (memcmp(config, "owner.info", 10) == 0) { - *reply++ = '>'; - *reply++ = ' '; - const char* sp = _prefs->owner_info; - while (*sp) { - *reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|' - sp++; - } - *reply = 0; // set null terminator - } else if (memcmp(config, "path.hash.mode", 14) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); - } else if (memcmp(config, "loop.detect", 11) == 0) { - if (_prefs->loop_detect == LOOP_DETECT_OFF) { - strcpy(reply, "> off"); - } else if (_prefs->loop_detect == LOOP_DETECT_MINIMAL) { - strcpy(reply, "> minimal"); - } else if (_prefs->loop_detect == LOOP_DETECT_MODERATE) { - strcpy(reply, "> moderate"); - } else { - strcpy(reply, "> strict"); - } - } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { - sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); - } else if (memcmp(config, "freq", 4) == 0) { - sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); - } else if (memcmp(config, "public.key", 10) == 0) { - strcpy(reply, "> "); - mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE); - } else if (memcmp(config, "role", 4) == 0) { - sprintf(reply, "> %s", _callbacks->getRole()); - } else if (memcmp(config, "bridge.type", 11) == 0) { - sprintf(reply, "> %s", -#ifdef WITH_RS232_BRIDGE - "rs232" -#elif WITH_ESPNOW_BRIDGE - "espnow" -#else - "none" -#endif - ); -#ifdef WITH_BRIDGE - } else if (memcmp(config, "bridge.enabled", 14) == 0) { - sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off"); - } else if (memcmp(config, "bridge.delay", 12) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay); - } else if (memcmp(config, "bridge.source", 13) == 0) { - sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx"); -#endif -#ifdef WITH_RS232_BRIDGE - } else if (memcmp(config, "bridge.baud", 11) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud); -#endif -#ifdef WITH_ESPNOW_BRIDGE - } else if (memcmp(config, "bridge.channel", 14) == 0) { - sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); - } else if (memcmp(config, "bridge.secret", 13) == 0) { - sprintf(reply, "> %s", _prefs->bridge_secret); -#endif - } else if (memcmp(config, "bootloader.ver", 14) == 0) { - #ifdef NRF52_PLATFORM - char ver[32]; - if (_board->getBootloaderVersion(ver, sizeof(ver))) { - sprintf(reply, "> %s", ver); - } else { - strcpy(reply, "> unknown"); - } - #else - strcpy(reply, "ERROR: unsupported"); - #endif - } else if (memcmp(config, "adc.multiplier", 14) == 0) { - float adc_mult = _board->getAdcMultiplier(); - if (adc_mult == 0.0f) { - strcpy(reply, "Error: unsupported by this board"); - } else { - sprintf(reply, "> %.3f", adc_mult); - } - // Power management commands - } else if (memcmp(config, "pwrmgt.support", 14) == 0) { -#ifdef NRF52_POWER_MANAGEMENT - strcpy(reply, "> supported"); -#else - strcpy(reply, "> unsupported"); -#endif - } else if (memcmp(config, "pwrmgt.source", 13) == 0) { -#ifdef NRF52_POWER_MANAGEMENT - strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery"); -#else - strcpy(reply, "ERROR: Power management not supported"); -#endif - } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) { -#ifdef NRF52_POWER_MANAGEMENT - sprintf(reply, "> Reset: %s; Shutdown: %s", - _board->getResetReasonString(_board->getResetReason()), - _board->getShutdownReasonString(_board->getShutdownReason())); -#else - strcpy(reply, "ERROR: Power management not supported"); -#endif - } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { -#ifdef NRF52_POWER_MANAGEMENT - sprintf(reply, "> %u mV", _board->getBootVoltage()); -#else - strcpy(reply, "ERROR: Power management not supported"); -#endif - } else { - sprintf(reply, "??: %s", config); - } - /* - * SET commands - */ + handleGetCmd(sender_timestamp, command, reply); } else if (memcmp(command, "set ", 4) == 0) { - const char* config = &command[4]; - if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "int.thresh ", 11) == 0) { - _prefs->interference_threshold = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { - _prefs->agc_reset_interval = atoi(&config[19]) / 4; - savePrefs(); - sprintf(reply, "OK - interval rounded to %d", ((uint32_t) _prefs->agc_reset_interval) * 4); - } else if (memcmp(config, "multi.acks ", 11) == 0) { - _prefs->multi_acks = atoi(&config[11]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "allow.read.only ", 16) == 0) { - _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "flood.advert.interval ", 22) == 0) { - int hours = _atoi(&config[22]); - if ((hours > 0 && hours < 3) || (hours > 168)) { - strcpy(reply, "Error: interval range is 3-168 hours"); - } else { - _prefs->flood_advert_interval = (uint8_t)(hours); - _callbacks->updateFloodAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "advert.interval ", 16) == 0) { - int mins = _atoi(&config[16]); - if ((mins > 0 && mins < MIN_LOCAL_ADVERT_INTERVAL) || (mins > 240)) { - sprintf(reply, "Error: interval range is %d-240 minutes", MIN_LOCAL_ADVERT_INTERVAL); - } else { - _prefs->advert_interval = (uint8_t)(mins / 2); - _callbacks->updateAdvertTimer(); - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "guest.password ", 15) == 0) { - StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "prv.key ", 8) == 0) { - uint8_t prv_key[PRV_KEY_SIZE]; - bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]); - // only allow rekey if key is valid - if (success && mesh::LocalIdentity::validatePrivateKey(prv_key)) { - mesh::LocalIdentity new_id; - new_id.readFrom(prv_key, PRV_KEY_SIZE); - _callbacks->saveIdentity(new_id); - strcpy(reply, "OK, reboot to apply! New pubkey: "); - mesh::Utils::toHex(&reply[33], new_id.pub_key, PUB_KEY_SIZE); - } else { - strcpy(reply, "Error, bad key"); - } - } else if (memcmp(config, "name ", 5) == 0) { - if (isValidName(&config[5])) { - StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, bad chars"); - } - } else if (memcmp(config, "repeat ", 7) == 0) { - _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; - savePrefs(); - strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); -#if defined(USE_SX1262) || defined(USE_SX1268) - } else if (memcmp(config, "radio.rxgain ", 13) == 0) { - _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; - strcpy(reply, "OK"); - savePrefs(); - _callbacks->setRxBoostedGain(_prefs->rx_boosted_gain); -#endif - } else if (memcmp(config, "radio ", 6) == 0) { - strcpy(tmp, &config[6]); - const char *parts[4]; - int num = mesh::Utils::parseTextParts(tmp, parts, 4); - float freq = num > 0 ? strtof(parts[0], nullptr) : 0.0f; - float bw = num > 1 ? strtof(parts[1], nullptr) : 0.0f; - uint8_t sf = num > 2 ? atoi(parts[2]) : 0; - uint8_t cr = num > 3 ? atoi(parts[3]) : 0; - if (freq >= 300.0f && freq <= 2500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f) { - _prefs->sf = sf; - _prefs->cr = cr; - _prefs->freq = freq; - _prefs->bw = bw; - _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error, invalid radio params"); - } - } else if (memcmp(config, "lat ", 4) == 0) { - _prefs->node_lat = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "lon ", 4) == 0) { - _prefs->node_lon = atof(&config[4]); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "rxdelay ", 8) == 0) { - float db = atof(&config[8]); - if (db >= 0) { - _prefs->rx_delay_base = db; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "txdelay ", 8) == 0) { - float f = atof(&config[8]); - if (f >= 0) { - _prefs->tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, cannot be negative"); - } - } else if (memcmp(config, "flood.max ", 10) == 0) { - uint8_t m = atoi(&config[10]); - if (m <= 64) { - _prefs->flood_max = m; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, max 64"); - } - } else if (memcmp(config, "retry.preset ", 13) == 0) { - uint8_t preset; - if (parseRetryPreset(&config[13], preset)) { - applyRetryPreset(_prefs, preset); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); - } - } else if (memcmp(config, "flood.retry.count ", 18) == 0) { - uint8_t count; - if (parseUint8Strict(&config[18], FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX, count)) { - _prefs->flood_retry_attempts = count; - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, min %d and max %d", FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); - } - } else if (memcmp(config, "flood.retry.path ", 17) == 0) { - uint8_t path_gate; - if (parseFloodRetryPathGate(&config[17], path_gate)) { - _prefs->flood_retry_path_gate = path_gate; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-63 or off"); - } - } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { - if (parseFloodRetryPrefixes(_prefs, &config[21])) { - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); - } - } else if (memcmp(config, "flood.retry.ignore ", 19) == 0) { - if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes, - FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) { - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", - (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); - } - } else if (memcmp(config, "flood.retry.advert ", 19) == 0) { - if (memcmp(&config[19], "on", 2) == 0) { - _prefs->flood_retry_advert_enabled = 1; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(&config[19], "off", 3) == 0) { - _prefs->flood_retry_advert_enabled = 0; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be on or off"); - } - } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { - if (memcmp(&config[19], "on", 2) == 0) { - _prefs->flood_retry_bridge_enabled = 1; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(&config[19], "off", 3) == 0) { - _prefs->flood_retry_bridge_enabled = 0; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be on or off"); - } - } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { - const char* params = &config[19]; - uint8_t bucket = atoi(params); - const char* list = strchr(params, ' '); - if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { - sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); - } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], - FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", - (unsigned int)FLOOD_RETRY_BUCKET_PREFIXES); - } - } else if (memcmp(config, "direct.txdelay ", 15) == 0) { - float f = atof(&config[15]); - if (f >= 0) { - _prefs->direct_tx_delay_factor = f; - savePrefs(); - strcpy(reply, "OK"); - } 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) { - float db = atof(&config[20]); - if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { - _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); - savePrefs(); - strcpy(reply, "OK"); - } 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, "direct.retry.step ", 18) == 0) { - int delay_ms = atoi(&config[18]); - if (delay_ms >= DIRECT_RETRY_STEP_MS_MIN && delay_ms <= DIRECT_RETRY_STEP_MS_MAX) { - _prefs->direct_retry_step_ms = (uint16_t)delay_ms; - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); - } - } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { - StrHelper::strncpy(tmp, &config[16], sizeof(tmp)); - if (parseDirectRetryCrThresholds(tmp, _prefs)) { - savePrefs(); - formatDirectRetryCrThresholds(_prefs, reply); - } else { - strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); - } - } else if (memcmp(config, "owner.info ", 11) == 0) { - config += 11; - char *dp = _prefs->owner_info; - while (*config && dp - _prefs->owner_info < sizeof(_prefs->owner_info)-1) { - *dp++ = (*config == '|') ? '\n' : *config; // translate '|' to newline chars - config++; - } - *dp = 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "path.hash.mode ", 15) == 0) { - config += 15; - uint8_t mode = atoi(config); - if (mode < 3) { - _prefs->path_hash_mode = mode; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0,1, or 2"); - } - } else if (memcmp(config, "loop.detect ", 12) == 0) { - config += 12; - uint8_t mode; - if (memcmp(config, "off", 3) == 0) { - mode = LOOP_DETECT_OFF; - } else if (memcmp(config, "minimal", 7) == 0) { - mode = LOOP_DETECT_MINIMAL; - } else if (memcmp(config, "moderate", 8) == 0) { - mode = LOOP_DETECT_MODERATE; - } else if (memcmp(config, "strict", 6) == 0) { - mode = LOOP_DETECT_STRICT; - } else { - mode = 0xFF; - strcpy(reply, "Error, must be: off, minimal, moderate, or strict"); - } - if (mode != 0xFF) { - _prefs->loop_detect = mode; - savePrefs(); - strcpy(reply, "OK"); - } - } else if (memcmp(config, "tx ", 3) == 0) { - _prefs->tx_power_dbm = atoi(&config[3]); - savePrefs(); - _callbacks->setTxPower(_prefs->tx_power_dbm); - strcpy(reply, "OK"); - } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { - _prefs->freq = atof(&config[5]); - savePrefs(); - strcpy(reply, "OK - reboot to apply"); -#ifdef WITH_BRIDGE - } else if (memcmp(config, "bridge.enabled ", 15) == 0) { - _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0; - _callbacks->setBridgeState(_prefs->bridge_enabled); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "bridge.delay ", 13) == 0) { - int delay = _atoi(&config[13]); - if (delay >= 0 && delay <= 10000) { - _prefs->bridge_delay = (uint16_t)delay; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: delay must be between 0-10000 ms"); - } - } else if (memcmp(config, "bridge.source ", 14) == 0) { - _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); -#endif -#ifdef WITH_RS232_BRIDGE - } else if (memcmp(config, "bridge.baud ", 12) == 0) { - uint32_t baud = atoi(&config[12]); - if (baud >= 9600 && baud <= BRIDGE_MAX_BAUD) { - _prefs->bridge_baud = (uint32_t)baud; - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD); - } -#endif -#ifdef WITH_ESPNOW_BRIDGE - } else if (memcmp(config, "bridge.channel ", 15) == 0) { - int ch = atoi(&config[15]); - if (ch > 0 && ch < 15) { - _prefs->bridge_channel = (uint8_t)ch; - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: channel must be between 1-14"); - } - } else if (memcmp(config, "bridge.secret ", 14) == 0) { - StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret)); - _callbacks->restartBridge(); - savePrefs(); - strcpy(reply, "OK"); -#endif - } else if (memcmp(config, "adc.multiplier ", 15) == 0) { - _prefs->adc_multiplier = atof(&config[15]); - if (_board->setAdcMultiplier(_prefs->adc_multiplier)) { - savePrefs(); - if (_prefs->adc_multiplier == 0.0f) { - strcpy(reply, "OK - using default board multiplier"); - } else { - sprintf(reply, "OK - multiplier set to %.3f", _prefs->adc_multiplier); - } - } else { - _prefs->adc_multiplier = 0.0f; - strcpy(reply, "Error: unsupported by this board"); - }; - } else { - sprintf(reply, "unknown config: %s", config); - } + handleSetCmd(sender_timestamp, command, reply); } else if (sender_timestamp == 0 && strcmp(command, "erase") == 0) { bool s = _callbacks->formatFileSystem(); sprintf(reply, "File system erase: %s", s ? "OK" : "Err"); @@ -1617,11 +441,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re #if defined(NRF52_PLATFORM) _prefs->powersaving_enabled = 1; savePrefs(); - strcpy(reply, "On - Immediate effect"); + strcpy(reply, "on - Immediate effect"); #elif defined(ESP32) && !defined(WITH_BRIDGE) _prefs->powersaving_enabled = 1; savePrefs(); - strcpy(reply, "On - After 2 minutes"); + strcpy(reply, "on - After 2 minutes"); #elif defined(WITH_BRIDGE) strcpy(reply, "Bridge not supported"); #else @@ -1630,43 +454,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "powersaving off", 15) == 0) { _prefs->powersaving_enabled = 0; savePrefs(); - strcpy(reply, "Off"); + strcpy(reply, "off"); } else if (memcmp(command, "powersaving", 11) == 0) { if (_prefs->powersaving_enabled) { - strcpy(reply, "On"); + strcpy(reply, "on"); } else { - strcpy(reply, "Off"); + strcpy(reply, "off"); } - } else if (memcmp(command, "sensor", 6) == 0) { - // I2C -#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL) - sprintf(reply, "I2C Wire1: SDA=%s,SCL=%s\r\n", STR(ENV_PIN_SDA), STR(ENV_PIN_SCL)); -#elif defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) - sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_BOARD_SDA), STR(PIN_BOARD_SCL)); -#elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) - sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_WIRE_SDA), STR(PIN_WIRE_SCL)); -#else - sprintf(reply, "I2C GPIOs not defined\r\n"); -#endif - - // GPS -#if defined(PIN_GPS_RX) && defined(PIN_GPS_TX) - sprintf(reply + strlen(reply), "GPS Serial: RX=%s, TX=%s", STR(PIN_GPS_RX), STR(PIN_GPS_TX)); -#ifdef ENV_INCLUDE_GPS> 0 - sprintf(reply + strlen(reply), ". Configured"); -#else - sprintf(reply + strlen(reply), ". Not configured"); -#endif -#else - sprintf(reply + strlen(reply), "GPS Serial not defined"); -#endif - } else if (memcmp(command, "powerlog", 8) == 0) { - sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason())); -#if defined(NRF52_PLATFORM) - sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s", - _board->getShutdownReasonString(_board->getShutdownReason())); - sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage()); -#endif } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); @@ -1712,6 +506,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->interference_threshold = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "cad ", 4) == 0) { + _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { _prefs->agc_reset_interval = atoi(&config[19]) / 4; savePrefs(); @@ -1773,7 +571,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->disable_fwd = memcmp(&config[7], "off", 3) == 0; savePrefs(); strcpy(reply, _prefs->disable_fwd ? "OK - repeat is now OFF" : "OK - repeat is now ON"); -#if defined(USE_SX1262) || defined(USE_SX1268) +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) } else if (memcmp(config, "radio.rxgain ", 13) == 0) { _prefs->rx_boosted_gain = memcmp(&config[13], "on", 2) == 0; strcpy(reply, "OK"); @@ -1830,21 +628,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "OK"); } else if (memcmp(config, "rxdelay ", 8) == 0) { float db = atof(&config[8]); - if (db >= 0) { + if (db >= 0 && db <= 20.0f) { _prefs->rx_delay_base = db; savePrefs(); strcpy(reply, "OK"); } else { - strcpy(reply, "Error, cannot be negative"); + strcpy(reply, "Error, must be 0-20"); } } else if (memcmp(config, "txdelay ", 8) == 0) { float f = atof(&config[8]); - if (f >= 0) { + if (f >= 0 && f <= 2.0f) { _prefs->tx_delay_factor = f; savePrefs(); strcpy(reply, "OK"); } else { - strcpy(reply, "Error, cannot be negative"); + strcpy(reply, "Error, must be 0-2"); } } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { uint8_t m = atoi(&config[19]); @@ -1854,7 +652,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "OK"); } else { strcpy(reply, "Error, max 64"); - } + } } else if (memcmp(config, "flood.max.advert ", 17) == 0) { uint8_t m = atoi(&config[17]); if (m <= 64) { @@ -1873,151 +671,14 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, max 64"); } - } else if (memcmp(config, "retry.preset ", 13) == 0) { - uint8_t preset; - if (parseRetryPreset(&config[13], preset)) { - applyRetryPreset(_prefs, preset); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be infra, rooftop, mobile, 0, 1, or 2"); - } - } else if (memcmp(config, "flood.retry.count ", 18) == 0) { - uint8_t count; - if (parseUint8Strict(&config[18], FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX, count)) { - _prefs->flood_retry_attempts = count; - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, min %d and max %d", FLOOD_RETRY_COUNT_MIN, FLOOD_RETRY_COUNT_MAX); - } - } else if (memcmp(config, "flood.retry.path ", 17) == 0) { - uint8_t path_gate; - if (parseFloodRetryPathGate(&config[17], path_gate)) { - _prefs->flood_retry_path_gate = path_gate; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be 0-63 or off"); - } - } else if (memcmp(config, "flood.retry.prefixes ", 21) == 0) { - if (parseFloodRetryPrefixes(_prefs, &config[21])) { - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, use comma-separated 3-byte hex prefixes"); - } - } else if (memcmp(config, "flood.retry.ignore ", 19) == 0) { - if (parseFloodRetryPrefixList(_prefs->flood_retry_ignore_prefixes, - FLOOD_RETRY_IGNORE_PREFIXES, &config[19])) { - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", - (unsigned int)FLOOD_RETRY_IGNORE_PREFIXES); - } - } else if (memcmp(config, "flood.retry.advert ", 19) == 0) { - if (memcmp(&config[19], "on", 2) == 0) { - _prefs->flood_retry_advert_enabled = 1; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(&config[19], "off", 3) == 0) { - _prefs->flood_retry_advert_enabled = 0; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be on or off"); - } - } else if (memcmp(config, "flood.retry.bridge ", 19) == 0) { - if (memcmp(&config[19], "on", 2) == 0) { - _prefs->flood_retry_bridge_enabled = 1; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(&config[19], "off", 3) == 0) { - _prefs->flood_retry_bridge_enabled = 0; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error, must be on or off"); - } - } else if (memcmp(config, "flood.retry.bucket ", 19) == 0) { - const char* params = &config[19]; - uint8_t bucket = atoi(params); - const char* list = strchr(params, ' '); - if (bucket < 1 || bucket > FLOOD_RETRY_BRIDGE_BUCKETS || list == NULL || *(list + 1) == 0) { - sprintf(reply, "Error, usage: set flood.retry.bucket <1-%d> ", FLOOD_RETRY_BRIDGE_BUCKETS); - } else if (parseFloodRetryPrefixList(_prefs->flood_retry_bridge_buckets[bucket - 1], - FLOOD_RETRY_BUCKET_PREFIXES, list + 1)) { - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, use up to %u comma-separated 3-byte hex prefixes", - (unsigned int)FLOOD_RETRY_BUCKET_PREFIXES); - } } else if (memcmp(config, "direct.txdelay ", 15) == 0) { float f = atof(&config[15]); - if (f >= 0) { + if (f >= 0 && f <= 2.0f) { _prefs->direct_tx_delay_factor = f; savePrefs(); strcpy(reply, "OK"); } 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) { - float db = atof(&config[20]); - if (db >= 0 && db <= DIRECT_RETRY_SNR_MARGIN_DB_MAX) { - _prefs->direct_retry_snr_margin_db = directRetryMarginDbToX4(db); - savePrefs(); - strcpy(reply, "OK"); - } 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, "direct.retry.step ", 18) == 0) { - int delay_ms = atoi(&config[18]); - if (delay_ms >= DIRECT_RETRY_STEP_MS_MIN && delay_ms <= DIRECT_RETRY_STEP_MS_MAX) { - _prefs->direct_retry_step_ms = (uint16_t)delay_ms; - savePrefs(); - strcpy(reply, "OK"); - } else { - sprintf(reply, "Error, min %d and max %d", DIRECT_RETRY_STEP_MS_MIN, DIRECT_RETRY_STEP_MS_MAX); - } - } else if (memcmp(config, "direct.retry.cr ", 16) == 0) { - StrHelper::strncpy(tmp, &config[16], sizeof(tmp)); - if (parseDirectRetryCrThresholds(tmp, _prefs)) { - savePrefs(); - formatDirectRetryCrThresholds(_prefs, reply); - } else { - strcpy(reply, "Error, expected off or cr4,cr5,cr7,cr8"); + strcpy(reply, "Error, must be 0-2"); } } else if (memcmp(config, "owner.info ", 11) == 0) { config += 11; @@ -2130,21 +791,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported"); }; - } else if (memcmp(config, "reboot.interval ", 16) == 0) { - int hours = _atoi(&config[16]); - if (hours == 0) { - _prefs->reboot_interval = 0; - savePrefs(); - strcpy(reply, "reboot.interval disabled"); - } else if (hours < 1 || 255 < hours) { - strcpy(reply, "Error: interval range is 1-255 hours"); - } else { - _prefs->reboot_interval = hours; - savePrefs(); - sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval); - } } else { strcpy(reply, "unknown config: "); + StrHelper::strncpy(&reply[16], config, 160-17); } } @@ -2159,6 +808,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); + } else if (memcmp(config, "cad", 3) == 0) { + sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks", 10) == 0) { @@ -2184,7 +835,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lat)); } else if (memcmp(config, "lon", 3) == 0) { sprintf(reply, "> %s", StrHelper::ftoa(_prefs->node_lon)); -#if defined(USE_SX1262) || defined(USE_SX1268) +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) } else if (memcmp(config, "radio.rxgain", 12) == 0) { sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off"); #endif @@ -2209,51 +860,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_unscoped); } else if (memcmp(config, "flood.max", 9) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->flood_max); - } else if (memcmp(config, "retry.preset", 12) == 0) { - formatRetryPreset(reply, _prefs); - } else if (memcmp(config, "flood.retry.count", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)floodRetryEffectiveCount(_prefs)); - } else if (memcmp(config, "flood.retry.path", 16) == 0) { - char path_gate[8]; - formatFloodRetryPathGate(path_gate, _prefs->flood_retry_path_gate); - sprintf(reply, "> %s", path_gate); - } else if (memcmp(config, "flood.retry.prefixes", 20) == 0) { - formatFloodRetryPrefixes(tmp, _prefs); - sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else if (memcmp(config, "flood.retry.ignore", 18) == 0) { - formatFloodRetryPrefixList(tmp, _prefs->flood_retry_ignore_prefixes, FLOOD_RETRY_IGNORE_PREFIXES); - sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else if (memcmp(config, "flood.retry.advert", 18) == 0) { - sprintf(reply, "> %s", _prefs->flood_retry_advert_enabled ? "on" : "off"); - } else if (memcmp(config, "flood.retry.bridge", 18) == 0) { - sprintf(reply, "> %s", _prefs->flood_retry_bridge_enabled ? "on" : "off"); - } else if (memcmp(config, "flood.retry.bucket.", 19) == 0) { - uint8_t bucket = atoi(&config[19]); - if (bucket >= 1 && bucket <= FLOOD_RETRY_BRIDGE_BUCKETS) { - formatFloodRetryBridgeBucket(tmp, _prefs, bucket - 1); - sprintf(reply, "> %s", tmp[0] ? tmp : "none"); - } else { - sprintf(reply, "Error, bucket 1-%d", FLOOD_RETRY_BRIDGE_BUCKETS); - } } 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, "> %s", StrHelper::ftoa(directRetryMarginX4ToDb(directRetryEffectiveMarginX4(_prefs)))); - } else if (memcmp(config, "direct.retry.count", 18) == 0) { - sprintf(reply, "> %d", (uint32_t)directRetryEffectiveCount(_prefs)); - } else if (memcmp(config, "direct.retry.base", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)directRetryEffectiveBaseMs(_prefs)); - } else if (memcmp(config, "direct.retry.step", 17) == 0) { - sprintf(reply, "> %d", (uint32_t)directRetryEffectiveStepMs(_prefs)); - } else if (memcmp(config, "direct.retry.cr", 15) == 0) { - formatDirectRetryCrThresholds(_prefs, reply); } else if (memcmp(config, "owner.info", 10) == 0) { + auto start = reply; *reply++ = '>'; *reply++ = ' '; const char* sp = _prefs->owner_info; - while (*sp) { + while (*sp && reply - start < 159) { *reply++ = (*sp == '\n') ? '|' : *sp; // translate newline back to orig '|' sp++; } @@ -2352,20 +966,81 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #else strcpy(reply, "ERROR: Power management not supported"); #endif - } else if (memcmp(config, "reboot.interval", 15) == 0) { - if (_prefs->reboot_interval == 0) { - strcpy(reply, "disabled"); - } else { - sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval); - } } else { sprintf(reply, "??: %s", config); } } +static char* skipSpaces(char* s) { + while (*s == ' ') s++; + return s; +} + +static void rtrimSpaces(char* s) { + char* e = s + strlen(s); + while (e > s && e[-1] == ' ') *--e = '\0'; +} + +static char* takeToken(char** cursor) { + char* p = skipSpaces(*cursor); + if (*p == '\0') { *cursor = p; return nullptr; } + char* tok = p; + while (*p && *p != ' ') p++; + if (*p) *p++ = '\0'; + *cursor = p; + return tok; +} + +static char* splitNameJump(char* tok) { + for (char* q = tok; *q; q++) { + if (*q == '|' || *q == ',') { + *q = '\0'; + char* jump = skipSpaces(q + 1); + rtrimSpaces(jump); + return jump; + } + } + return nullptr; +} + +static bool processRegionDefSegment(RegionMap* map, char* tok, RegionEntry** cursor, char* reply) { + char* jump = splitNameJump(tok); + char* name = skipSpaces(tok); + if (*name == '\0') { snprintf(reply, 160, "Err - empty name"); return false; } + if (jump && *jump == '\0') { snprintf(reply, 160, "Err - empty jump"); return false; } + + RegionEntry* r = map->putRegion(name, (*cursor)->id); + if (r == NULL) { snprintf(reply, 160, "Err - put failed: %s", name); return false; } + r->flags = 0; + + if (jump) { + RegionEntry* j = map->findByNamePrefix(jump); + if (j == NULL) { snprintf(reply, 160, "Err - unknown jump: %s", jump); return false; } + *cursor = j; + } else { + *cursor = r; + } + return true; +} + void CommonCLI::handleRegionCmd(char* command, char* reply) { reply[0] = 0; + // `region def`: must run before parseTextParts mutates the buffer + char* cmd = skipSpaces(command); + if (strncmp(cmd, "region def", 10) == 0 && (cmd[10] == ' ' || cmd[10] == '\0')) { + char* payload = skipSpaces(cmd + 10); + rtrimSpaces(payload); + if (*payload == '\0') { snprintf(reply, 160, "Err - empty def"); return; } + + RegionEntry* cursor = &_region_map->getWildcard(); + for (char* tok; (tok = takeToken(&payload)) != nullptr; ) { + if (!processRegionDefSegment(_region_map, tok, &cursor, reply)) return; + } + _region_map->exportTo(reply, 160); + return; + } + const char* parts[4]; int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); if (n == 1) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index aa442354..10cb00c7 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,57 +19,6 @@ #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 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 - -#ifndef FLOOD_RETRY_PREFIX_SLOTS - #define FLOOD_RETRY_PREFIX_SLOTS 8 -#endif -#ifndef FLOOD_RETRY_PREFIX_LEN - #define FLOOD_RETRY_PREFIX_LEN 3 -#endif -#ifndef FLOOD_RETRY_BRIDGE_BUCKETS - #define FLOOD_RETRY_BRIDGE_BUCKETS 6 -#endif -#ifndef FLOOD_RETRY_BUCKET_PREFIXES - #define FLOOD_RETRY_BUCKET_PREFIXES 17 -#endif -#ifndef FLOOD_RETRY_IGNORE_PREFIXES - #define FLOOD_RETRY_IGNORE_PREFIXES 8 -#endif -#ifndef FLOOD_RETRY_LIST_PREFIXES - #define FLOOD_RETRY_LIST_PREFIXES ((FLOOD_RETRY_IGNORE_PREFIXES > FLOOD_RETRY_BUCKET_PREFIXES) ? FLOOD_RETRY_IGNORE_PREFIXES : FLOOD_RETRY_BUCKET_PREFIXES) -#endif -#ifndef FLOOD_RETRY_LIST_TEXT_MAX - #define FLOOD_RETRY_LIST_TEXT_MAX (FLOOD_RETRY_LIST_PREFIXES * FLOOD_RETRY_PREFIX_LEN * 2 + FLOOD_RETRY_LIST_PREFIXES) -#endif -#ifndef COMMON_CLI_TMP_LEN - #define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4)) -#endif - -#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 - struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -84,9 +33,7 @@ struct NodePrefs { // persisted to file float tx_delay_factor; char guest_password[16]; float direct_tx_delay_factor; - uint8_t direct_retry_recent_enabled; - uint8_t direct_retry_snr_margin_db; // stored in quarter-dB units (x4) - uint8_t direct_retry_prefs_magic[2]; + uint32_t guard; uint8_t sf; uint8_t cr; uint8_t allow_read_only; @@ -106,7 +53,6 @@ struct NodePrefs { // persisted to file char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting uint8_t powersaving_enabled; // boolean - uint8_t reboot_interval; // hours, 0-255 (default 0=disable) // Gps settings uint8_t gps_enabled; uint32_t gps_interval; // in seconds @@ -118,26 +64,7 @@ struct NodePrefs { // persisted to file uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; - uint8_t direct_retry_attempts; - uint16_t direct_retry_base_ms; - uint8_t direct_retry_timing_magic[2]; - uint8_t retry_preset; - uint16_t direct_retry_step_ms; - uint8_t flood_retry_attempts; - uint8_t flood_retry_path_gate; - uint8_t flood_retry_prefs_magic[2]; - uint8_t flood_retry_prefixes[FLOOD_RETRY_PREFIX_SLOTS][FLOOD_RETRY_PREFIX_LEN]; - uint8_t flood_retry_bridge_enabled; - uint8_t flood_retry_bridge_buckets[FLOOD_RETRY_BRIDGE_BUCKETS][FLOOD_RETRY_BUCKET_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; - uint8_t flood_retry_ignore_prefixes[FLOOD_RETRY_IGNORE_PREFIXES][FLOOD_RETRY_PREFIX_LEN]; - uint8_t flood_retry_advert_enabled; - 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 battery_alert_enabled; - uint8_t battery_alert_low_percent; - uint8_t battery_alert_critical_percent; + uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) }; class CommonCLICallbacks { @@ -197,7 +124,7 @@ class CommonCLI { SensorManager* _sensors; RegionMap* _region_map; ClientACL* _acl; - char tmp[COMMON_CLI_TMP_LEN]; + char tmp[PRV_KEY_SIZE*2 + 4]; mesh::RTCClock* getRTCClock() { return _rtc; } void savePrefs(); diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b6519aef..5e72336c 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -36,6 +36,7 @@ void RadioLibWrapper::begin() { _noise_floor = 0; _threshold = 0; + _cad_enabled = false; // start average out some samples _num_floor_samples = 0; @@ -178,10 +179,26 @@ void RadioLibWrapper::onSendFinished() { state = STATE_IDLE; } +int16_t RadioLibWrapper::performChannelScan() { + return _radio->scanChannel(); +} + bool RadioLibWrapper::isChannelActive() { - return _threshold == 0 - ? false // interference check is disabled - : getCurrentRSSI() > _noise_floor + _threshold; + // int.thresh: RSSI-based interference detection (relative to noise floor) + if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + + // cad: hardware channel activity detection + if (_cad_enabled) { + int16_t result = performChannelScan(); + // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY + // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't + // try to read a non-existent packet and count a spurious recv error. + state = STATE_IDLE; + startRecv(); + if (result != RADIOLIB_CHANNEL_FREE) return true; + } + + return false; } float RadioLibWrapper::getLastRSSI() const { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index efd3e179..9943bcab 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -9,6 +9,7 @@ protected: mesh::MainBoard* _board; uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; + bool _cad_enabled; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; @@ -32,7 +33,7 @@ public: bool isInRecvMode() const override; bool isChannelActive(); - bool isReceiving() override { + bool isReceiving() override { if (isReceivingPacket()) return true; return isChannelActive(); @@ -46,9 +47,11 @@ public: virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } + virtual int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; + void setCADEnabled(bool enable) override { _cad_enabled = enable; } void resetAGC() override; void loop() override; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index c7f69722..54425145 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -126,10 +126,6 @@ const char* T096Board::getManufacturerName() const { } bool T096Board::setLoRaFemLnaEnabled(bool enable) { -#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) - enable = false; -#endif - if (!loRaFEMControl.isLnaCanControl()) { return false; } diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index ba3a7f1c..3978c51e 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -32,12 +32,33 @@ void HeltecV4Board::begin() { loRaFEMControl.setRxModeEnable(); } - void HeltecV4Board::powerOff() { - // Turn off PA - digitalWrite(P_LORA_PA_POWER, LOW); - rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + void HeltecV4Board::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 HeltecV4Board::powerOff() { + enterDeepSleep(0); } uint16_t HeltecV4Board::getBattMilliVolts() { @@ -64,10 +85,6 @@ void HeltecV4Board::begin() { } bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) { -#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) - enable = false; -#endif - if (!loRaFEMControl.isLnaCanControl()) { return false; }