diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 2c7f1d9..a57eb4a 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -1856,7 +1856,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) #endif static const uint8_t fw_build[12] = FIRMWARE_BUILD_DATE; static const uint8_t model[40] = CONFIG_ZEPHCORE_BOARD_NAME; - static const uint8_t version[20] = "v1.13.0-zephyr"; + static const uint8_t version[20] = "v1.14.0-zephyr"; uint8_t rsp[82]; rsp[0] = PACKET_DEVICE_INFO; rsp[1] = 10; // FIRMWARE_VER_CODE - v10 = path_hash_mode support diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 661a779..072c05e 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -396,10 +396,41 @@ mesh::Packet* RepeaterMesh::createSelfAdvert() { return createAdvert(self_id, app_data, app_data_len); } +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 }; + +bool RepeaterMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) { + uint8_t hash_size = packet->getPathHashSize(); + uint8_t hash_count = packet->getPathHashCount(); + uint8_t n = 0; + const uint8_t* path = packet->path; + while (hash_count > 0) { + if (self_id.isHashMatch(path, hash_size)) n++; + hash_count--; + path += hash_size; + } + return n >= max_counters[hash_size]; +} + bool RepeaterMesh::allowPacketForward(const mesh::Packet* packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == nullptr) return false; + if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) { + const uint8_t* maximums; + if (_prefs.loop_detect == LOOP_DETECT_MINIMAL) { + maximums = max_loop_minimal; + } else if (_prefs.loop_detect == LOOP_DETECT_MODERATE) { + maximums = max_loop_moderate; + } else { + maximums = max_loop_strict; + } + if (isLooped(packet, maximums)) { + MESH_DEBUG_PRINTLN("allowPacketForward: FLOOD packet loop detected!"); + return false; + } + } return true; } diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index 6f1a3bb..fc602f6 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -25,7 +25,7 @@ #include "RepeaterDataStore.h" #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.13.0-zephyr" + #define FIRMWARE_VERSION "v1.14.0-zephyr" #endif #ifndef FIRMWARE_BUILD_DATE @@ -121,6 +121,7 @@ protected: } bool allowPacketForward(const mesh::Packet* packet) override; + bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); const char* getLogDateTime() override; void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 87a21f6..cb3de2e 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -90,7 +90,8 @@ void CommonCLI::loadPrefs(const char* path) { ok = ok && prefs_read(&file, &_prefs->bw, sizeof(_prefs->bw)); // 116 ok = ok && prefs_read(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 ok = ok && prefs_read(&file, &_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 - ok = ok && prefs_read(&file, pad, 2); // 122 + ok = ok && prefs_read(&file, &_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 + ok = ok && prefs_read(&file, pad, 1); // 123 ok = ok && prefs_read(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 ok = ok && prefs_read(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 ok = ok && prefs_read(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 @@ -186,7 +187,8 @@ void CommonCLI::savePrefs(const char* path) { fs_write(&file, &_prefs->bw, sizeof(_prefs->bw)); fs_write(&file, &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); fs_write(&file, &_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); - fs_write(&file, pad, 2); + fs_write(&file, &_prefs->loop_detect, sizeof(_prefs->loop_detect)); + fs_write(&file, pad, 1); fs_write(&file, &_prefs->flood_max, sizeof(_prefs->flood_max)); fs_write(&file, &_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); fs_write(&file, &_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); @@ -302,6 +304,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "clkreboot", 9) == 0) { getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm _board->reboot(); + } else if (memcmp(command, "advert.zerohop", 14) == 0) { + _callbacks->sendSelfAdvertisement(1500, false); // 0-hop (direct) advert + strcpy(reply, "OK - zerohop advert sent"); } else if (memcmp(command, "advert", 6) == 0) { _callbacks->sendSelfAdvertisement(1500, true); strcpy(reply, "OK - Advert sent"); @@ -430,6 +435,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch *reply = 0; } else if (memcmp(config, "path.hash.mode", 14) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %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] == ' ')) { snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { @@ -618,6 +633,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } 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) { int val = atoi(&config[3]); #ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 68f0192..1b2c06c 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -23,6 +23,11 @@ #define ADVERT_LOC_SHARE 1 #define ADVERT_LOC_PREFS 2 +#define LOOP_DETECT_OFF 0 +#define LOOP_DETECT_MINIMAL 1 +#define LOOP_DETECT_MODERATE 2 +#define LOOP_DETECT_STRICT 3 + struct NodePrefs { /* ---- Common fields (both roles) ---- */ float airtime_factor; @@ -70,6 +75,7 @@ struct NodePrefs { uint8_t client_repeat; // 1 = offgrid mode (forward packets), 0 = companion only uint8_t path_hash_mode; // which path mode to use when sending (0-2) uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + uint8_t loop_detect; // LOOP_DETECT_OFF/MINIMAL/MODERATE/STRICT }; /* Default prefs — MUST match LoRaConfig.h defaults for radio interop.