Преглед изворни кода

Merge branch 'dev' into 2026/remote-lna

João Brázio пре 5 месеци
родитељ
комит
64a16d7ad2
45 измењених фајлова са 665 додато и 255 уклоњено
  1. 43 0
      .github/workflows/pr-build-check.yml
  2. 1 1
      boards/t_beams3_supreme.json
  3. 51 19
      build.sh
  4. 1 1
      docs/cli_commands.md
  5. 6 4
      examples/companion_radio/DataStore.cpp
  6. 83 23
      examples/companion_radio/MyMesh.cpp
  7. 7 3
      examples/companion_radio/MyMesh.h
  8. 2 0
      examples/companion_radio/NodePrefs.h
  9. 8 0
      examples/companion_radio/ui-new/UITask.cpp
  10. 8 0
      examples/companion_radio/ui-new/UITask.h
  11. 4 0
      examples/companion_radio/ui-new/icons.h
  12. 94 26
      examples/simple_repeater/MyMesh.cpp
  13. 6 2
      examples/simple_repeater/MyMesh.h
  14. 23 21
      examples/simple_room_server/MyMesh.cpp
  15. 2 2
      examples/simple_room_server/MyMesh.h
  16. 3 2
      examples/simple_secure_chat/main.cpp
  17. 23 21
      examples/simple_sensor/SensorMesh.cpp
  18. 3 3
      examples/simple_sensor/SensorMesh.h
  19. 4 3
      platformio.ini
  20. 51 43
      src/Dispatcher.cpp
  21. 1 0
      src/Dispatcher.h
  22. 4 0
      src/Identity.h
  23. 39 35
      src/Mesh.cpp
  24. 2 2
      src/Mesh.h
  25. 1 0
      src/MeshCore.h
  26. 31 4
      src/Packet.cpp
  27. 10 0
      src/Packet.h
  28. 14 14
      src/helpers/BaseChatMesh.cpp
  29. 1 1
      src/helpers/ClientACL.cpp
  30. 3 1
      src/helpers/ClientACL.h
  31. 28 2
      src/helpers/CommonCLI.cpp
  32. 2 2
      src/helpers/CommonCLI.h
  33. 3 1
      src/helpers/ContactInfo.h
  34. 19 0
      src/helpers/NRF52Board.cpp
  35. 1 0
      src/helpers/NRF52Board.h
  36. 13 7
      src/helpers/StaticPoolPacketManager.cpp
  37. 1 1
      src/helpers/StaticPoolPacketManager.h
  38. 1 0
      src/helpers/esp32/SerialBLEInterface.cpp
  39. 25 0
      variants/heltec_tracker/platformio.ini
  40. 1 0
      variants/heltec_tracker_v2/platformio.ini
  41. 27 1
      variants/lilygo_tbeam_supreme_SX1262/platformio.ini
  42. 3 3
      variants/lilygo_tlora_v2_1/platformio.ini
  43. 3 1
      variants/m5stack_unit_c6l/platformio.ini
  44. 7 6
      variants/rak3112/platformio.ini
  45. 2 0
      variants/wio-tracker-l1/platformio.ini

+ 43 - 0
.github/workflows/pr-build-check.yml

@@ -0,0 +1,43 @@
+name: PR Build Check
+
+on:
+  pull_request:
+    branches: [main, dev]
+    paths:
+      - 'src/**'
+      - 'examples/**'
+      - 'variants/**'
+      - 'platformio.ini'
+      - '.github/workflows/pr-build-check.yml'
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        environment:
+          # ESP32-S3 (most common platform)
+          - Heltec_v3_companion_radio_ble
+          - Heltec_v3_repeater
+          - Heltec_v3_room_server
+          # nRF52
+          - RAK_4631_companion_radio_ble
+          - RAK_4631_repeater
+          - RAK_4631_room_server
+          # RP2040
+          - PicoW_repeater
+          # STM32
+          - wio-e5-mini_repeater
+          # ESP32-C6
+          - LilyGo_Tlora_C6_repeater_
+
+    steps:
+      - name: Clone Repo
+        uses: actions/checkout@v4
+
+      - name: Setup Build Environment
+        uses: ./.github/actions/setup-build-environment
+
+      - name: Build ${{ matrix.environment }}
+        run: pio run -e ${{ matrix.environment }}

+ 1 - 1
boards/t_beams3_supreme.json

@@ -41,7 +41,7 @@
     "name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)",
     "upload": {
       "flash_size": "8MB",
-      "maximum_ram_size": 327680,
+      "maximum_ram_size": 8388608,
       "maximum_size": 8388608,
       "require_upload_port": true,
       "speed": 460800

+ 51 - 19
build.sh

@@ -7,6 +7,7 @@ sh build.sh <command> [target]
 
 Commands:
   help|usage|-h|--help: Shows this message.
+  list|-l: List firmwares available to build.
   build-firmware <target>: Build the firmware for the given build target.
   build-firmwares: Build all firmwares for all targets.
   build-matching-firmwares <build-match-spec>: Build all firmwares for build targets containing the string given for <build-match-spec>.
@@ -46,19 +47,25 @@ $ sh build.sh build-firmware RAK_4631_repeater
 EOF
 }
 
+# get a list of pio env names that start with "env:"
+get_pio_envs() {
+  pio project config | grep 'env:' | sed 's/env://'
+}
+
 # Catch cries for help before doing anything else.
 case $1 in
   help|usage|-h|--help)
     global_usage
     exit 1
     ;;
+  list|-l)
+    get_pio_envs
+    exit 0
+    ;;
 esac
 
-
-# get a list of pio env names that start with "env:"
-get_pio_envs() {
-  echo $(pio project config | grep 'env:' | sed 's/env://')
-}
+# cache project config json for use in get_platform_for_env()
+PIO_CONFIG_JSON=$(pio project config --json-output)
 
 # $1 should be the string to find (case insensitive)
 get_pio_envs_containing_string() {
@@ -82,6 +89,25 @@ get_pio_envs_ending_with_string() {
   done
 }
 
+# get platform flag for a given environment
+# $1 should be the environment name
+get_platform_for_env() {
+  local env_name=$1
+  echo "$PIO_CONFIG_JSON" | python3 -c "
+import sys, json, re
+data = json.load(sys.stdin)
+for section, options in data:
+    if section == 'env:$env_name':
+        for key, value in options:
+            if key == 'build_flags':
+                for flag in value:
+                    match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag)
+                    if match:
+                        print(match.group(1))
+                        sys.exit(0)
+"
+}
+
 # disable all debug logging flags if DISABLE_DEBUG=1 is set
 disable_debug_flags() {
   if [ "$DISABLE_DEBUG" == "1" ]; then
@@ -91,6 +117,8 @@ disable_debug_flags() {
 
 # build firmware for the provided pio env in $1
 build_firmware() {
+  # get env platform for post build actions
+  ENV_PLATFORM=($(get_platform_for_env $1))
 
   # get git commit sha
   COMMIT_HASH=$(git rev-parse --short HEAD)
@@ -121,27 +149,31 @@ build_firmware() {
   # build firmware target
   pio run -e $1
 
-  # build merge-bin for esp32 fresh install
-  if [ -f .pio/build/$1/firmware.bin ]; then
+  # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin)
+  if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then
     pio run -t mergebin -e $1
+    cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
+    cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true
   fi
 
-  # build .uf2 for nrf52 boards
-  if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then
+  # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2)
+  if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then
     python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
+    cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
+    cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
   fi
 
-  # copy .bin, .uf2, and .zip to out folder
-  # e.g: Heltec_v3_room_server-v1.0.0-SHA.bin
-  # e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2
-
-  # copy .bin for esp32 boards
-  cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
-  cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true
+  # for stm32, copy .bin and .hex to out folder
+  if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then
+    cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
+    cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true
+  fi
 
-  # copy .zip and .uf2 of nrf52 boards
-  cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
-  cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
+  # for rp2040, copy .bin and .uf2 to out folder
+  if [ "$ENV_PLATFORM" == "RP2040_PLATFORM" ]; then
+    cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true
+    cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
+  fi
 
 }
 

+ 1 - 1
docs/cli_commands.md

@@ -642,7 +642,7 @@
 **Usage:** 
 - `region`
 
-**Serial Only:** Yes
+**Serial Only:** For firmware older than 1.12.0
 
 ---
 

+ 6 - 4
examples/companion_radio/DataStore.cpp

@@ -212,7 +212,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
     file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq));                               // 56
     file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf));                                   // 60
     file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr));                                   // 61
-    file.read(pad, 1);                                                                     // 62
+    file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat));             // 62
     file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
     file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw));                                   // 64
     file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm));               // 68
@@ -222,7 +222,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
     file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base));             // 72
     file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy));     // 76
     file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks));                   // 77
-    file.read(pad, 2);                                                                     // 78
+    file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode));           // 78
+    file.read(pad, 1);                                                                     // 79
     file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin));                         // 80
     file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));               // 84
     file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));                 // 85
@@ -248,7 +249,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
     file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq));                               // 56
     file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf));                                   // 60
     file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr));                                   // 61
-    file.write(pad, 1);                                                                     // 62
+    file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat));             // 62
     file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
     file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw));                                   // 64
     file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm));               // 68
@@ -258,7 +259,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
     file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base));             // 72
     file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy));     // 76
     file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks));                   // 77
-    file.write(pad, 2);                                                                     // 78
+    file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode));           // 78
+    file.write(pad, 1);                                                                     // 79
     file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin));                         // 80
     file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));               // 84
     file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));                 // 85

+ 83 - 23
examples/companion_radio/MyMesh.cpp

@@ -56,6 +56,8 @@
 #define CMD_SEND_ANON_REQ             57
 #define CMD_SET_AUTOADD_CONFIG        58
 #define CMD_GET_AUTOADD_CONFIG        59
+#define CMD_GET_ALLOWED_REPEAT_FREQ   60
+#define CMD_SET_PATH_HASH_MODE        61
 
 // Stats sub-types for CMD_GET_STATS
 #define STATS_TYPE_CORE               0
@@ -88,6 +90,7 @@
 #define RESP_CODE_TUNING_PARAMS       23
 #define RESP_CODE_STATS               24   // v8+, second byte is stats type
 #define RESP_CODE_AUTOADD_CONFIG      25
+#define RESP_ALLOWED_REPEAT_FREQ      26
 
 #define SEND_TIMEOUT_BASE_MILLIS        500
 #define FLOOD_SEND_TIMEOUT_FACTOR       16.0f
@@ -255,6 +258,15 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
   return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
 }
 
+uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
+  return getRNG()->nextInt(0, 5*t + 1);
+}
+uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
+  return getRNG()->nextInt(0, 5*t + 1);
+}
+
 uint8_t MyMesh::getExtraAckTransmitCount() const {
   return _prefs.multi_acks;
 }
@@ -338,7 +350,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
   }
 
   // add inbound-path to mem cache
-  if (path && path_len <= sizeof(AdvertPath::path)) {  // check path is valid
+  if (path && mesh::Packet::isValidPathLen(path_len)) {  // check path is valid
     AdvertPath* p = advert_paths;
     uint32_t oldest = 0xFFFFFFFF;
     for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) {   // check if already in table, otherwise evict oldest
@@ -355,8 +367,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
     memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
     strcpy(p->name, contact.name);
     p->recv_timestamp = getRTCClock()->getCurrentTime();
-    p->path_len = path_len;
-    memcpy(p->path, path, p->path_len);
+    p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
   }
 
   if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
@@ -455,26 +466,30 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
   return false;
 }
 
+bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
+  return _prefs.client_repeat != 0;
+}
+
 void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
   // TODO: dynamic send_scope, depending on recipient and current 'home' Region
   if (send_scope.isNull()) {
-    sendFlood(pkt, delay_millis);
+    sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
   } else {
     uint16_t codes[2];
     codes[0] = send_scope.calcTransportCode(pkt);
     codes[1] = 0;  // REVISIT: set to 'home' Region, for sender/return region?
-    sendFlood(pkt, codes, delay_millis);
+    sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
   }
 }
 void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
   // TODO: have per-channel send_scope
   if (send_scope.isNull()) {
-    sendFlood(pkt, delay_millis);
+    sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
   } else {
     uint16_t codes[2];
     codes[0] = send_scope.calcTransportCode(pkt);
     codes[1] = 0;  // REVISIT: set to 'home' Region, for sender/return region?
-    sendFlood(pkt, codes, delay_millis);
+    sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
   }
 }
 
@@ -671,7 +686,7 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
     if (tag == pending_discovery) {  // check for matching response tag)
       pending_discovery = 0;
 
-      if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
+      if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) {
         MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
       } else {
         int i = 0;
@@ -680,11 +695,9 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
         memcpy(&out_frame[i], contact.id.pub_key, 6);
         i += 6; // pub_key_prefix
         out_frame[i++] = out_path_len;
-        memcpy(&out_frame[i], out_path, out_path_len);
-        i += out_path_len;
+        i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len);
         out_frame[i++] = in_path_len;
-        memcpy(&out_frame[i], in_path, in_path_len);
-        i += in_path_len;
+        i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len);
         // NOTE: telemetry data in 'extra' is discarded at present
 
         _serial->writeFrame(out_frame, i);
@@ -770,9 +783,10 @@ uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
   return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
 }
 uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const {
+  uint8_t path_hash_count = path_len & 63;
   return SEND_TIMEOUT_BASE_MILLIS +
          ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) *
-          (path_len + 1));
+          (path_hash_count + 1));
 }
 
 void MyMesh::onSendTimeout() {}
@@ -893,6 +907,24 @@ uint32_t MyMesh::getBLEPin() {
   return _active_ble_pin;
 }
 
+struct FreqRange {
+  uint32_t lower_freq, upper_freq;
+};
+
+static FreqRange repeat_freq_ranges[] = {
+  { 433000, 433000 },
+  { 869000, 869000 },
+  { 918000, 918000 }
+};
+
+bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
+  for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) {
+    auto r = &repeat_freq_ranges[i];
+    if (f >= r->lower_freq && f <= r->upper_freq) return true;
+  }
+  return false;
+}
+
 void MyMesh::startInterface(BaseSerialInterface &serial) {
   _serial = &serial;
   serial.enable();
@@ -916,6 +948,8 @@ void MyMesh::handleCmdFrame(size_t len) {
     i += 40;
     StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
     i += 20;
+    out_frame[i++] = _prefs.client_repeat;   // v9+
+    out_frame[i++] = _prefs.path_hash_mode;  // v10+
     _serial->writeFrame(out_frame, i);
   } else if (cmd_frame[0] == CMD_APP_START &&
              len >= 8) { // sent when app establishes connection, respond with node ID
@@ -1093,7 +1127,8 @@ void MyMesh::handleCmdFrame(size_t len) {
     }
     if (pkt) {
       if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
-        sendFlood(pkt);
+        unsigned long delay_millis = 0;
+        sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
       } else {
         sendZeroHop(pkt);
       }
@@ -1105,7 +1140,7 @@ void MyMesh::handleCmdFrame(size_t len) {
     uint8_t *pub_key = &cmd_frame[1];
     ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
     if (recipient) {
-      recipient->out_path_len = -1;
+      recipient->out_path_len = OUT_PATH_UNKNOWN;
       // recipient->lastmod = ??   shouldn't be needed, app already has this version of contact
       dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
       writeOKFrame();
@@ -1220,13 +1255,20 @@ void MyMesh::handleCmdFrame(size_t len) {
     i += 4;
     uint8_t sf = cmd_frame[i++];
     uint8_t cr = cmd_frame[i++];
+    uint8_t repeat = 0;  // default - false
+    if (len > i) {
+      repeat = cmd_frame[i++];   // FIRMWARE_VER_CODE  9+
+    }
 
-    if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
+    if (repeat && !isValidClientRepeatFreq(freq)) {
+      writeErrFrame(ERR_CODE_ILLEGAL_ARG);
+    } else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
         bw <= 500000) {
       _prefs.sf = sf;
       _prefs.cr = cr;
       _prefs.freq = (float)freq / 1000.0;
       _prefs.bw = (float)bw / 1000.0;
+      _prefs.client_repeat = repeat;
       savePrefs();
 
       radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -1283,6 +1325,14 @@ void MyMesh::handleCmdFrame(size_t len) {
     }
     savePrefs();
     writeOKFrame();
+  } else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) {
+    if (cmd_frame[2] >= 3) {
+      writeErrFrame(ERR_CODE_ILLEGAL_ARG);
+    } else {
+      _prefs.path_hash_mode = cmd_frame[2];
+      savePrefs();
+      writeOKFrame();
+    }
   } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
     if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
       saveContacts();
@@ -1420,7 +1470,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       memset(&req_data[2], 0, 3);  // reserved
       getRNG()->random(&req_data[5], 4);   // random blob to help make packet-hash unique
       auto save = recipient->out_path_len;    // temporarily force sendRequest() to flood
-      recipient->out_path_len = -1;
+      recipient->out_path_len = OUT_PATH_UNKNOWN;
       int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
       recipient->out_path_len = save;
       if (result == MSG_SEND_FAILED) {
@@ -1580,7 +1630,7 @@ void MyMesh::handleCmdFrame(size_t len) {
         sendDirect(pkt, &cmd_frame[10], path_len);
 
         uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
-        uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len);
+        uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz);
 
         out_frame[0] = RESP_CODE_SENT;
         out_frame[1] = 0;
@@ -1657,11 +1707,12 @@ void MyMesh::handleCmdFrame(size_t len) {
       }
     }
     if (found) {
-      out_frame[0] = RESP_CODE_ADVERT_PATH;
-      memcpy(&out_frame[1], &found->recv_timestamp, 4);
-      out_frame[5] = found->path_len;
-      memcpy(&out_frame[6], found->path, found->path_len);
-      _serial->writeFrame(out_frame, 6 + found->path_len);
+      int i = 0;
+      out_frame[i++] = RESP_CODE_ADVERT_PATH;
+      memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4;
+      out_frame[i++] = found->path_len;
+      i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len);
+      _serial->writeFrame(out_frame, i);
     } else {
       writeErrFrame(ERR_CODE_NOT_FOUND);
     }
@@ -1753,6 +1804,15 @@ void MyMesh::handleCmdFrame(size_t len) {
     out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
     out_frame[i++] = _prefs.autoadd_config;
     _serial->writeFrame(out_frame, i);
+  } else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) {
+    int i = 0;
+    out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ;
+    for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) {
+      auto r = &repeat_freq_ranges[k];
+      memcpy(&out_frame[i], &r->lower_freq, 4); i += 4;
+      memcpy(&out_frame[i], &r->upper_freq, 4); i += 4;
+    }
+    _serial->writeFrame(out_frame, i);
   } else {
     writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

+ 7 - 3
examples/companion_radio/MyMesh.h

@@ -5,14 +5,14 @@
 #include "AbstractUITask.h"
 
 /*------------ Frame Protocol --------------*/
-#define FIRMWARE_VER_CODE 8
+#define FIRMWARE_VER_CODE 10
 
 #ifndef FIRMWARE_BUILD_DATE
-#define FIRMWARE_BUILD_DATE "29 Jan 2026"
+#define FIRMWARE_BUILD_DATE "15 Feb 2026"
 #endif
 
 #ifndef FIRMWARE_VERSION
-#define FIRMWARE_VERSION "v1.12.0"
+#define FIRMWARE_VERSION "v1.13.0"
 #endif
 
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -106,8 +106,11 @@ protected:
   float getAirtimeBudgetFactor() const override;
   int getInterferenceThreshold() 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;
   uint8_t getExtraAckTransmitCount() const override;
   bool filterRecvFloodPacket(mesh::Packet* packet) override;
+  bool allowPacketForward(const mesh::Packet* packet) override;
 
   void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
   void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;
@@ -176,6 +179,7 @@ private:
 
   void checkCLIRescueCmd();
   void checkSerialInterface();
+  bool isValidClientRepeatFreq(uint32_t f) const;
 
   // helpers, short-cuts
   void saveChannels() { _store->saveChannels(this); }

+ 2 - 0
examples/companion_radio/NodePrefs.h

@@ -29,4 +29,6 @@ struct NodePrefs {  // persisted to file
   uint32_t gps_interval;     // GPS read interval in seconds
   uint8_t autoadd_config;    // bitmask for auto-add contacts config
   uint8_t sx126x_rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
+  uint8_t client_repeat;
+  uint8_t path_hash_mode;    // which path mode to use when sending
 };

+ 8 - 0
examples/companion_radio/ui-new/UITask.cpp

@@ -131,6 +131,14 @@ class HomeScreen : public UIScreen {
     // fill the battery based on the percentage
     int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
     display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
+
+    // show muted icon if buzzer is muted
+#ifdef PIN_BUZZER
+    if (_task->isBuzzerQuiet()) {
+      display.setColor(DisplayDriver::RED);
+      display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8);
+    }
+#endif
   }
 
   CayenneLPP sensors_lpp;

+ 8 - 0
examples/companion_radio/ui-new/UITask.h

@@ -78,6 +78,14 @@ public:
   bool hasDisplay() const { return _display != NULL; }
   bool isButtonPressed() const;
 
+  bool isBuzzerQuiet() { 
+#ifdef PIN_BUZZER
+    return buzzer.isQuiet();
+#else
+    return true;
+#endif
+  }
+
   void toggleBuzzer();
   bool getGPSState();
   void toggleGPS();

+ 4 - 0
examples/companion_radio/ui-new/icons.h

@@ -115,4 +115,8 @@ static const uint8_t advert_icon[] = {
 0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
 0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t muted_icon[] = {
+  0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
 };

+ 94 - 26
examples/simple_repeater/MyMesh.cpp

@@ -129,7 +129,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
   }
 
   if (is_flood) {
-    client->out_path_len = -1;  // need to rediscover out_path
+    client->out_path_len = OUT_PATH_UNKNOWN;  // need to rediscover out_path
   }
 
   uint32_t now = getRTCClock()->getCurrentTimeUnique();
@@ -147,9 +147,12 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
 uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
   if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
     // request data has: {reply-path-len}{reply-path}
-    reply_path_len = *data++ & 0x3F;
-    memcpy(reply_path, data, reply_path_len);
-    // data += reply_path_len;
+    reply_path_len = *data & 63;
+    reply_path_hash_size = (*data >> 6) + 1;
+    data++;
+
+    memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size);
+    // data += (uint8_t)reply_path_len * reply_path_hash_size;
 
     memcpy(reply_data, &sender_timestamp, 4);   // prefix with sender_timestamp, like a tag
     uint32_t now = getRTCClock()->getCurrentTime();
@@ -163,9 +166,12 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send
 uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
   if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
     // request data has: {reply-path-len}{reply-path}
-    reply_path_len = *data++ & 0x3F;
-    memcpy(reply_path, data, reply_path_len);
-    // data += reply_path_len;
+    reply_path_len = *data & 63;
+    reply_path_hash_size = (*data >> 6) + 1;
+    data++;
+
+    memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size);
+    // data += (uint8_t)reply_path_len * reply_path_hash_size;
 
     memcpy(reply_data, &sender_timestamp, 4);   // prefix with sender_timestamp, like a tag
     uint32_t now = getRTCClock()->getCurrentTime();
@@ -180,9 +186,12 @@ uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender
 uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) {
   if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
     // request data has: {reply-path-len}{reply-path}
-    reply_path_len = *data++ & 0x3F;
-    memcpy(reply_path, data, reply_path_len);
-    // data += reply_path_len;
+    reply_path_len = *data & 63;
+    reply_path_hash_size = (*data >> 6) + 1;
+    data++;
+
+    memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size);
+    // data += (uint8_t)reply_path_len * reply_path_hash_size;
 
     memcpy(reply_data, &sender_timestamp, 4);   // prefix with sender_timestamp, like a tag
     uint32_t now = getRTCClock()->getCurrentTime();
@@ -292,6 +301,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
 
       // create copy of neighbours list, skipping empty entries so we can sort it separately from main list
       int16_t neighbours_count = 0;
+#if MAX_NEIGHBOURS
       NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
       for (int i = 0; i < MAX_NEIGHBOURS; i++) {
         auto neighbour = &neighbours[i];
@@ -327,6 +337,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
           return a->snr < b->snr; // asc
         });
       }
+#endif
 
       // build results buffer
       int results_count = 0;
@@ -341,6 +352,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
           break;
         }
 
+#if MAX_NEIGHBOURS
         // add next neighbour to results
         auto neighbour = sorted_neighbours[index + offset];
         uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
@@ -348,6 +360,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
         memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4;
         memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1;
         results_count++;
+#endif
 
       }
 
@@ -385,7 +398,7 @@ File MyMesh::openAppend(const char *fname) {
 
 bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
   if (_prefs.disable_fwd) return false;
-  if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
+  if (packet->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;
@@ -480,11 +493,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
 }
 
 uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
   return getRNG()->nextInt(0, 5*t + 1);
 }
 uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
   return getRNG()->nextInt(0, 5*t + 1);
 }
 
@@ -534,13 +547,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
       mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
                                             PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
-      if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+      if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else if (reply_path_len < 0) {
       mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
-      if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
+      if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else {
       mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
-      if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
+      uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63);
+      if (reply) sendDirect(reply, reply_path,  path_len, SERVER_RESPONSE_DELAY);
     }
   }
 }
@@ -609,15 +623,15 @@ 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) sendFlood(path, SERVER_RESPONSE_DELAY);
+        if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
       } else {
         mesh::Packet *reply =
             createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
         if (reply) {
-          if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
+          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 {
-            sendFlood(reply, SERVER_RESPONSE_DELAY);
+            sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           }
         }
       }
@@ -647,8 +661,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
 
         mesh::Packet *ack = createAck(ack_hash);
         if (ack) {
-          if (client->out_path_len < 0) {
-            sendFlood(ack, TXT_ACK_DELAY);
+          if (client->out_path_len == OUT_PATH_UNKNOWN) {
+            sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
           } else {
             sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
           }
@@ -675,8 +689,8 @@ 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 (client->out_path_len < 0) {
-            sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
+          if (client->out_path_len == OUT_PATH_UNKNOWN) {
+            sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
           } else {
             sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
           }
@@ -697,7 +711,8 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
     MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
     auto client = acl.getClientByIdx(i);
 
-    memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
+    // store a copy of path, for sendDirect()
+    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);
@@ -738,6 +753,47 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) {
         sendZeroHop(resp, getRetransmitDelay(resp)*4);  // apply random delay (widened x4), as multiple nodes can respond to this
       }
     }
+  } else if (type == CTL_TYPE_NODE_DISCOVER_RESP && packet->payload_len >= 6) {
+    uint8_t node_type = packet->payload[0] & 0x0F;
+    if (node_type != ADV_TYPE_REPEATER) {
+      return;
+    }
+    if (packet->payload_len < 6 + PUB_KEY_SIZE) {
+      MESH_DEBUG_PRINTLN("onControlDataRecv: DISCOVER_RESP pubkey too short: %d", (uint32_t)packet->payload_len);
+      return;
+    }
+
+    if (pending_discover_tag == 0 || millisHasNowPassed(pending_discover_until)) {
+      pending_discover_tag = 0;
+      return;
+    }
+    uint32_t tag;
+    memcpy(&tag, &packet->payload[2], 4);
+    if (tag != pending_discover_tag) {
+      return;
+    }
+
+    mesh::Identity id(&packet->payload[6]);
+    if (id.matches(self_id)) {
+      return;
+    }
+    putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR());
+  }
+}
+
+void MyMesh::sendNodeDiscoverReq() {
+  uint8_t data[10];
+  data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // prefix_only=0
+  data[1] = (1 << ADV_TYPE_REPEATER);
+  getRNG()->random(&data[2], 4); // tag
+  memcpy(&pending_discover_tag, &data[2], 4);
+  pending_discover_until = futureMillis(60000);
+  uint32_t since = 0;
+  memcpy(&data[6], &since, 4);
+
+  auto pkt = createControlData(data, sizeof(data));
+  if (pkt) {
+    sendZeroHop(pkt);
   }
 }
 
@@ -771,7 +827,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.airtime_factor = 1.0;   // one half
   _prefs.rx_delay_base = 0.0f;   // turn off by default, was 10.0;
   _prefs.tx_delay_factor = 0.5f; // was 0.25f
-  _prefs.direct_tx_delay_factor = 0.2f; // was zero
+  _prefs.direct_tx_delay_factor = 0.3f; // was 0.2
   StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
   _prefs.node_lat = ADVERT_LAT;
   _prefs.node_lon = ADVERT_LON;
@@ -809,6 +865,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.sx126x_rx_boosted_gain = 1; // enabled by default;
 #endif
 #endif
+
+  pending_discover_tag = 0;
+  pending_discover_until = 0;
 }
 
 void MyMesh::begin(FILESYSTEM *fs) {
@@ -872,7 +931,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet *pkt = createSelfAdvert();
   if (pkt) {
     if (flood) {
-      sendFlood(pkt, delay_millis);
+      sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
     } else {
       sendZeroHop(pkt, delay_millis);
     }
@@ -1182,6 +1241,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
     } else {
       strcpy(reply, "Err - ??");
     }
+  } else if (memcmp(command, "discover.neighbors", 18) == 0) {
+    const char* sub = command + 18;
+    while (*sub == ' ') sub++;
+    if (*sub != 0) {
+      strcpy(reply, "Err - discover.neighbors has no options");
+    } else {
+      sendNodeDiscoverReq();
+      strcpy(reply, "OK - Discover sent");
+    }
   } else{
     _cli.handleCommand(sender_timestamp, command, reply);  // common CLI commands
   }

+ 6 - 2
examples/simple_repeater/MyMesh.h

@@ -69,11 +69,11 @@ struct NeighbourInfo {
 };
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "29 Jan 2026"
+  #define FIRMWARE_BUILD_DATE   "15 Feb 2026"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.12.0"
+  #define FIRMWARE_VERSION   "v1.13.0"
 #endif
 
 #define FIRMWARE_ROLE "repeater"
@@ -92,11 +92,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
   uint8_t reply_path[MAX_PATH_SIZE];
   int8_t  reply_path_len;
+  uint8_t reply_path_hash_size;
   TransportKeyStore key_store;
   RegionMap region_map, temp_map;
   RegionEntry* load_stack[8];
   RegionEntry* recv_pkt_region;
   RateLimiter discover_limiter, anon_limiter;
+  uint32_t pending_discover_tag;
+  unsigned long pending_discover_until;
   bool region_load_active;
   unsigned long dirty_contacts_expiry;
 #if MAX_NEIGHBOURS
@@ -116,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
 #endif
 
   void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
+  void sendNodeDiscoverReq();
   uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
   uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
   uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);

+ 23 - 21
examples/simple_room_server/MyMesh.cpp

@@ -73,13 +73,15 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
 
   auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len);
   if (reply) {
-    if (client->out_path_len < 0) {
-      sendFlood(reply);
+    if (client->out_path_len == OUT_PATH_UNKNOWN) {
+      unsigned long delay_millis = 0;
+      sendFlood(reply, delay_millis, _prefs.path_hash_mode + 1);
       client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
     } else {
       sendDirect(reply, client->out_path, client->out_path_len);
-      client->extra.room.ack_timeout =
-          futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1));
+
+      uint8_t path_hash_count = client->out_path_len & 63;
+      client->extra.room.ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (path_hash_count + 1));
     }
     _num_post_pushes++; // stats
   } else {
@@ -264,17 +266,17 @@ const char *MyMesh::getLogDateTime() {
 }
 
 uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
   return getRNG()->nextInt(0, 5*t + 1);
 }
 uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
   return getRNG()->nextInt(0, 5*t + 1);
 }
 
 bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
   if (_prefs.disable_fwd) return false;
-  if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
+  if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
   return true;
 }
 
@@ -333,7 +335,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
     }
 
     if (packet->isRouteFlood()) {
-      client->out_path_len = -1;  // need to rediscover out_path
+      client->out_path_len = OUT_PATH_UNKNOWN;  // need to rediscover out_path
     }
 
     uint32_t now = getRTCClock()->getCurrentTimeUnique();
@@ -353,14 +355,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
       mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
                                             PAYLOAD_TYPE_RESPONSE, reply_data, 13);
-      if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+      if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else {
       mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
       if (reply) {
-        if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
+        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 {
-          sendFlood(reply, SERVER_RESPONSE_DELAY);
+          sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
         }
       }
     }
@@ -448,9 +450,9 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
 
       uint32_t delay_millis;
       if (send_ack) {
-        if (client->out_path_len < 0) {
+        if (client->out_path_len == OUT_PATH_UNKNOWN) {
           mesh::Packet *ack = createAck(ack_hash);
-          if (ack) sendFlood(ack, TXT_ACK_DELAY);
+          if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize());
           delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
         } else {
           uint32_t d = TXT_ACK_DELAY;
@@ -482,8 +484,8 @@ 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 (client->out_path_len < 0) {
-            sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY);
+          if (client->out_path_len == OUT_PATH_UNKNOWN) {
+            sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           } else {
             sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY);
           }
@@ -521,7 +523,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
         // if client sends too quickly, evict()
 
         // RULE: only send keep_alive response DIRECT!
-        if (client->out_path_len >= 0) {
+        if (client->out_path_len != OUT_PATH_UNKNOWN) {
           uint32_t ack_hash; // calc ACK to prove to sender that we got request
           mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE);
 
@@ -538,14 +540,14 @@ 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) sendFlood(path, SERVER_RESPONSE_DELAY);
+            if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           } else {
             mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
             if (reply) {
-              if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
+              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 {
-                sendFlood(reply, SERVER_RESPONSE_DELAY);
+                sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
               }
             }
           }
@@ -563,7 +565,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
   if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
     MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
     auto client = acl.getClientByIdx(i);
-    memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
+    client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); // store a copy of path, for sendDirect()
     client->last_activity = getRTCClock()->getCurrentTime();
   } else {
     MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
@@ -679,7 +681,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet *pkt = createSelfAdvert();
   if (pkt) {
     if (flood) {
-      sendFlood(pkt, delay_millis);
+      sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
     } else {
       sendZeroHop(pkt, delay_millis);
     }

+ 2 - 2
examples/simple_room_server/MyMesh.h

@@ -26,11 +26,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "29 Jan 2026"
+  #define FIRMWARE_BUILD_DATE   "15 Feb 2026"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.12.0"
+  #define FIRMWARE_VERSION   "v1.13.0"
 #endif
 
 #ifndef LORA_FREQ

+ 3 - 2
examples/simple_secure_chat/main.cpp

@@ -213,7 +213,7 @@ protected:
   }
 
   void onContactPathUpdated(const ContactInfo& contact) override {
-    Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (int32_t) contact.out_path_len);
+    Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (uint32_t) contact.out_path_len);
     saveContacts();
   }
 
@@ -266,8 +266,9 @@ protected:
     return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
   }
   uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override {
+    uint8_t path_hash_count = path_len & 63;
     return SEND_TIMEOUT_BASE_MILLIS + 
-         ( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1));
+         ( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_hash_count + 1));
   }
 
   void onSendTimeout() override {

+ 23 - 21
examples/simple_sensor/SensorMesh.cpp

@@ -258,10 +258,11 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
 
   auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
   if (pkt) {
-    if (c->out_path_len >= 0) {  // we have an out_path, so send DIRECT
+    if (c->out_path_len != OUT_PATH_UNKNOWN) {  // we have an out_path, so send DIRECT
       sendDirect(pkt, c->out_path, c->out_path_len);
     } else {
-      sendFlood(pkt);
+      unsigned long delay_millis = 0;
+      sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
     }
   }
   t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
@@ -302,7 +303,7 @@ float SensorMesh::getAirtimeBudgetFactor() const {
 
 bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
   if (_prefs.disable_fwd) return false;
-  if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
+  if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
   return true;
 }
 
@@ -312,11 +313,11 @@ int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
 }
 
 uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor);
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
   return getRNG()->nextInt(0, 6)*t;
 }
 uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
   return getRNG()->nextInt(0, 6)*t;
 }
 int SensorMesh::getInterferenceThreshold() const {
@@ -360,7 +361,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t*
   }
 
   if (is_flood) {
-    client->out_path_len = -1;  // need to rediscover out_path
+    client->out_path_len = OUT_PATH_UNKNOWN;  // need to rediscover out_path
   }
 
   uint32_t now = getRTCClock()->getCurrentTimeUnique();
@@ -468,10 +469,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
       // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
       mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
                                             PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
-      if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+      if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     } else {
       mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
-      if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
+      if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
     }
   }
 }
@@ -496,10 +497,10 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
   }
 }
 
-void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) {
-  if (dest.out_path_len < 0) {
+void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
+  if (dest.out_path_len == OUT_PATH_UNKNOWN) {
     mesh::Packet* ack = createAck(ack_hash);
-    if (ack) sendFlood(ack, TXT_ACK_DELAY);
+    if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size);
   } else {
     uint32_t d = TXT_ACK_DELAY;
     if (getExtraAckTransmitCount() > 0) {
@@ -537,14 +538,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
         // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
         mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len,
                                               PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
-        if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+        if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
       } else {
         mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len);
         if (reply) {
-          if (from->out_path_len >= 0) {  // we have an out_path, so send DIRECT
+          if (from->out_path_len != OUT_PATH_UNKNOWN) {  // we have an out_path, so send DIRECT
             sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY);
           } else {
-            sendFlood(reply, SERVER_RESPONSE_DELAY);
+            sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
           }
         }
       }
@@ -567,9 +568,9 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
             // 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);
-            if (path) sendFlood(path, TXT_ACK_DELAY);
+            if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize());
           } else {
-            sendAckTo(*from, ack_hash);
+            sendAckTo(*from, ack_hash, packet->getPathHashSize());
           }
         }
       } else if (flags == TXT_TYPE_CLI_DATA) {  
@@ -596,8 +597,8 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
 
           auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
           if (reply) {
-            if (from->out_path_len < 0) {
-              sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
+            if (from->out_path_len == OUT_PATH_UNKNOWN) {
+              sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
             } else {
               sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
             }
@@ -666,7 +667,7 @@ bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint
   MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
   // NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
   // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
-  memcpy(from->out_path, path, from->out_path_len = path_len);  // store a copy of path, for sendDirect()
+  from->out_path_len = mesh::Packet::copyPath(from->out_path, path, path_len);  // store a copy of path, for sendDirect()
   from->last_activity = getRTCClock()->getCurrentTime();
 
   // REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
@@ -791,7 +792,7 @@ void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
   mesh::Packet* pkt = createSelfAdvert();
   if (pkt) {
     if (flood) {
-      sendFlood(pkt, delay_millis);
+      sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
     } else {
       sendZeroHop(pkt, delay_millis);
     }
@@ -868,7 +869,8 @@ void SensorMesh::loop() {
 
   if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
     mesh::Packet* pkt = createSelfAdvert();
-    if (pkt) sendFlood(pkt);
+    unsigned long delay_millis = 0;
+    if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
 
     updateFloodAdvertTimer();   // schedule next flood advert
     updateAdvertTimer();   // also schedule local advert (so they don't overlap)

+ 3 - 3
examples/simple_sensor/SensorMesh.h

@@ -33,11 +33,11 @@
 #define PERM_RECV_ALERTS_HI    (1 << 7)   // high priority alerts
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "29 Jan 2026"
+  #define FIRMWARE_BUILD_DATE   "15 Feb 2026"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.12.0"
+  #define FIRMWARE_VERSION   "v1.13.0"
 #endif
 
 #define FIRMWARE_ROLE "sensor"
@@ -128,7 +128,7 @@ protected:
   void onControlDataRecv(mesh::Packet* packet) override;
   void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
   virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len);
-  void sendAckTo(const ClientInfo& dest, uint32_t ack_hash);
+  void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1);
 private:
   FILESYSTEM* _fs;
   unsigned long next_local_advert, next_flood_advert;

+ 4 - 3
platformio.ini

@@ -24,9 +24,9 @@ lib_deps =
   melopero/Melopero RV3028 @ ^1.1.0
   electroniccats/CayenneLPP @ 1.6.1
 build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
-  -D LORA_FREQ=869.525
-  -D LORA_BW=250
-  -D LORA_SF=11
+  -D LORA_FREQ=869.618
+  -D LORA_BW=62.5
+  -D LORA_SF=8
   -D ENABLE_ADVERT_ON_BOOT=1
   -D ENABLE_PRIVATE_KEY_IMPORT=1   ; NOTE: comment these out for more secure firmware
   -D ENABLE_PRIVATE_KEY_EXPORT=1
@@ -59,6 +59,7 @@ platform = platformio/espressif32@6.11.0
 monitor_filters = esp32_exception_decoder
 extra_scripts = merge-bin.py
 build_flags = ${arduino_base.build_flags}
+  -D ESP32_PLATFORM
 ;  -D ESP32_CPU_FREQ=80          ; change it to your need
 build_src_filter = ${arduino_base.build_src_filter}
 

+ 51 - 43
src/Dispatcher.cpp

@@ -68,7 +68,7 @@ void Dispatcher::loop() {
       next_tx_time = futureMillis(t * getAirtimeBudgetFactor());
 
       _radio->onSendFinished();
-      logTx(outbound, 2 + outbound->path_len + outbound->payload_len);
+      logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
       if (outbound->isRouteFlood()) {
         n_sent_flood++;
       } else {
@@ -80,7 +80,7 @@ void Dispatcher::loop() {
       MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());
 
       _radio->onSendFinished();
-      logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len);
+      logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
 
       releasePacket(outbound);  // return to pool
       outbound = NULL;
@@ -108,6 +108,48 @@ void Dispatcher::loop() {
   checkSend();
 }
 
+bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
+  int i = 0;
+
+  pkt->header = raw[i++];
+  if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
+    MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime());
+    return false;
+  }
+
+  if (pkt->hasTransportCodes()) {
+    memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
+    memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
+  } else {
+    pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
+  }
+
+  pkt->path_len = raw[i++];
+  uint8_t path_mode = pkt->path_len >> 6;  // upper 2 bits (legacy firmware: 00)
+  if (path_mode == 3) {   // Reserved for future
+    MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported path mode: 3", getLogDateTime());
+    return false;
+  }
+
+  uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize();
+  if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) {
+    MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len);
+    return false;
+  }
+
+  memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len;
+
+  pkt->payload_len = len - i;  // payload is remainder
+  if (pkt->payload_len > sizeof(pkt->payload)) {
+    MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len);
+    return false;
+  }
+
+  memcpy(pkt->payload, &raw[i], pkt->payload_len);
+
+  return true;  // success
+}
+
 void Dispatcher::checkRecv() {
   Packet* pkt;
   float score;
@@ -122,45 +164,14 @@ void Dispatcher::checkRecv() {
       if (pkt == NULL) {
         MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime());
       } else {
-        int i = 0;
-#ifdef NODE_ID
-        uint8_t sender_id = raw[i++];
-        if (sender_id == NODE_ID - 1 || sender_id == NODE_ID + 1) {  // simulate that NODE_ID can only hear NODE_ID-1 or NODE_ID+1, eg. 3 can't hear 1
-        } else {
-          _mgr->free(pkt);  // put back into pool
-          return;
-        }
-#endif
-
-        pkt->header = raw[i++];
-        if (pkt->hasTransportCodes()) {
-          memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
-          memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
+        if (tryParsePacket(pkt, raw, len)) {
+          pkt->_snr = _radio->getLastSNR() * 4.0f;
+          score = _radio->packetScore(_radio->getLastSNR(), len);
+          air_time = _radio->getEstAirtimeFor(len);
+          rx_air_time += air_time;
         } else {
-          pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
-        }
-        pkt->path_len = raw[i++];
-
-        if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) {
-          MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len);
           _mgr->free(pkt);  // put back into pool
           pkt = NULL;
-        } else {
-          memcpy(pkt->path, &raw[i], pkt->path_len); i += pkt->path_len;
-
-          pkt->payload_len = len - i;  // payload is remainder
-          if (pkt->payload_len > sizeof(pkt->payload)) {
-            MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len);
-            _mgr->free(pkt);  // put back into pool
-            pkt = NULL;  
-          } else {
-            memcpy(pkt->payload, &raw[i], pkt->payload_len);
-
-            pkt->_snr = _radio->getLastSNR() * 4.0f;
-            score = _radio->packetScore(_radio->getLastSNR(), len);
-            air_time = _radio->getEstAirtimeFor(len);
-            rx_air_time += air_time;
-          }
         }
       }
     } else {
@@ -249,16 +260,13 @@ void Dispatcher::checkSend() {
     int len = 0;
     uint8_t raw[MAX_TRANS_UNIT];
 
-#ifdef NODE_ID
-    raw[len++] = NODE_ID;
-#endif
     raw[len++] = outbound->header;
     if (outbound->hasTransportCodes()) {
       memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
       memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
     }
     raw[len++] = outbound->path_len;
-    memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len;
+    len += Packet::writePath(&raw[len], outbound->path, outbound->path_len);
 
     if (len + outbound->payload_len > MAX_TRANS_UNIT) {
       MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len);
@@ -312,7 +320,7 @@ void Dispatcher::releasePacket(Packet* packet) {
 }
 
 void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
-  if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) {
+  if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
     MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
     _mgr->free(packet);
   } else {

+ 1 - 0
src/Dispatcher.h

@@ -184,6 +184,7 @@ public:
   unsigned long futureMillis(int millis_from_now) const;
 
 private:
+  bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
   void checkRecv();
   void checkSend();
 };

+ 4 - 0
src/Identity.h

@@ -20,6 +20,10 @@ public:
     memcpy(dest, pub_key, PATH_HASH_SIZE);    // hash is just prefix of pub_key
     return PATH_HASH_SIZE;
   }
+  int copyHashTo(uint8_t* dest, uint8_t len) const { 
+    memcpy(dest, pub_key, len);    // hash is just prefix of pub_key
+    return len;
+  }
   bool isHashMatch(const uint8_t* hash) const {
     return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0;
   }

+ 39 - 35
src/Mesh.cpp

@@ -39,11 +39,6 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int
 }
 
 DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
-  if (pkt->getPayloadVer() > PAYLOAD_VER_1) {  // not supported in this firmware version
-    MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unsupported packet version", getLogDateTime());
-    return ACTION_RELEASE;
-  }
-
   if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) {
     if (pkt->path_len < MAX_PATH_SIZE) {
       uint8_t i = 0;
@@ -70,14 +65,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
   }
 
   if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
-    if (pkt->path_len == 0) {
+    if (pkt->getPathHashCount() == 0) {
       onControlDataRecv(pkt);
     }
     // just zero-hop control packets allowed (for this subset of payloads)
     return ACTION_RELEASE;
   }
 
-  if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
+  if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) {
     // check for 'early received' ACK
     if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
       int i = 0;
@@ -88,7 +83,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
       }
     }
 
-    if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
+    if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) {
       if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
         return forwardMultipartDirect(pkt);
       } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
@@ -158,7 +153,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
               if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
                 int k = 0;
                 uint8_t path_len = data[k++];
-                uint8_t* path = &data[k]; k += path_len;
+                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;
                 uint8_t extra_type = data[k++] & 0x0F;   // upper 4 bits reserved for future use
                 uint8_t* extra = &data[k];
                 uint8_t extra_len = len - k;   // remainder of packet (may be padded with zeroes!)
@@ -293,8 +290,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
         if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {    // a multipart ACK
           Packet tmp;
           tmp.header = pkt->header;
-          tmp.path_len = pkt->path_len;
-          memcpy(tmp.path, pkt->path, pkt->path_len);
+          tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len);
           tmp.payload_len = pkt->payload_len - 1;
           memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
 
@@ -321,27 +317,25 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
 
 void Mesh::removeSelfFromPath(Packet* pkt) {
   // remove our hash from 'path'
-  pkt->path_len -= PATH_HASH_SIZE;
-#if 0
-  memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
-#elif PATH_HASH_SIZE == 1
-  for (int k = 0; k < pkt->path_len; k++) {  // shuffle bytes by 1
-    pkt->path[k] = pkt->path[k + 1];
+  pkt->setPathHashCount(pkt->getPathHashCount() - 1);  // decrement the count
+
+  uint8_t sz = pkt->getPathHashSize();
+  for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) {  // shuffle path by 1 'entry'
+    memcpy(&pkt->path[k], &pkt->path[k + sz], sz);
   }
-#else
-  #error "need path remove impl"
-#endif
 }
 
 DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
+  uint8_t n = packet->getPathHashCount();
   if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
-    && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
+    && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) {
     // append this node's hash to 'path'
-    packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]);
+    self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize());
+    packet->setPathHashCount(n + 1);
 
     uint32_t d = getRetransmitDelay(packet);
     // as this propagates outwards, give it lower and lower priority
-    return ACTION_RETRANSMIT_DELAYED(packet->path_len, d);   // give priority to closer sources, than ones further away
+    return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d);   // give priority to closer sources, than ones further away
   }
   return ACTION_RELEASE;
 }
@@ -353,8 +347,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
   if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {    // a multipart ACK
     Packet tmp;
     tmp.header = pkt->header;
-    tmp.path_len = pkt->path_len;
-    memcpy(tmp.path, pkt->path, pkt->path_len);
+    tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len);
     tmp.payload_len = pkt->payload_len - 1;
     memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
 
@@ -376,7 +369,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
       delay_millis += getDirectRetransmitDelay(packet) + 300;
       auto a1 = createMultiAck(crc, extra);
       if (a1) {
-        memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
+        a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
         a1->header &= ~PH_ROUTE_MASK;
         a1->header |= ROUTE_TYPE_DIRECT;
         sendPacket(a1, 0, delay_millis);
@@ -386,7 +379,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
 
     auto a2 = createAck(crc);
     if (a2) {
-      memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
+      a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
       a2->header &= ~PH_ROUTE_MASK;
       a2->header |= ROUTE_TYPE_DIRECT;
       sendPacket(a2, 0, delay_millis);
@@ -439,7 +432,10 @@ Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, cons
 }
 
 Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) {
-  if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL;  // too long!!
+  uint8_t path_hash_size = (path_len >> 6) + 1;
+  uint8_t path_hash_count = path_len & 63;
+
+  if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return NULL;  // too long!!
 
   Packet* packet = obtainNewPacket();
   if (packet == NULL) {
@@ -457,7 +453,7 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret,
     uint8_t data[MAX_PACKET_PAYLOAD];
 
     data[data_len++] = path_len;
-    memcpy(&data[data_len], path, path_len); data_len += path_len;
+    memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size;
     if (extra_len > 0) {
       data[data_len++] = extra_type;
       memcpy(&data[data_len], extra, extra_len); data_len += extra_len;
@@ -624,15 +620,19 @@ Packet* Mesh::createControlData(const uint8_t* data, size_t len) {
   return packet;
 }
 
-void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
+void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
   if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
     MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
     return;
   }
+  if (path_hash_size == 0 || path_hash_size > 3) {
+    MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
+    return;
+  }
 
   packet->header &= ~PH_ROUTE_MASK;
   packet->header |= ROUTE_TYPE_FLOOD;
-  packet->path_len = 0;
+  packet->setPathHashSizeAndCount(path_hash_size, 0);
 
   _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
 
@@ -647,17 +647,21 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
   sendPacket(packet, pri, delay_millis);
 }
 
-void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) {
+void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
   if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
     MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
     return;
   }
+  if (path_hash_size == 0 || path_hash_size > 3) {
+    MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
+    return;
+  }
 
   packet->header &= ~PH_ROUTE_MASK;
   packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD;
   packet->transport_codes[0] = transport_codes[0];
   packet->transport_codes[1] = transport_codes[1];
-  packet->path_len = 0;
+  packet->setPathHashSizeAndCount(path_hash_size, 0);
 
   _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
 
@@ -679,13 +683,13 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin
   uint8_t pri;
   if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {   // TRACE packets are different
     // for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's)
-    memcpy(&packet->payload[packet->payload_len], path, path_len);
+    memcpy(&packet->payload[packet->payload_len], path, path_len);  // NOTE: path_len here can be > 64, and NOT in the new scheme
     packet->payload_len += path_len;
 
     packet->path_len = 0;
     pri = 5;   // maybe make this configurable
   } else {
-    memcpy(packet->path, path, packet->path_len = path_len);
+    packet->path_len = Packet::copyPath(packet->path, path, path_len);
     if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
       pri = 1;   // slightly less priority
     } else {

+ 2 - 2
src/Mesh.h

@@ -196,13 +196,13 @@ public:
   /**
    * \brief  send a locally-generated Packet with flood routing
   */
-  void sendFlood(Packet* packet, uint32_t delay_millis=0);
+  void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1);
 
   /**
    * \brief  send a locally-generated Packet with flood routing
    * \param transport_codes   array of 2 codes to attach to packet
   */
-  void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0);
+  void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1);
 
   /**
    * \brief  send a locally-generated Packet with Direct routing

+ 1 - 0
src/MeshCore.h

@@ -55,6 +55,7 @@ public:
   virtual uint32_t getGpio() { return 0; }
   virtual void setGpio(uint32_t values) {}
   virtual uint8_t getStartupReason() const = 0;
+  virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
   virtual bool startOTAUpdate(const char* id, char reply[]) { return false; }   // not supported
 
   // Power management interface (boards with power management override these)

+ 31 - 4
src/Packet.cpp

@@ -10,8 +10,32 @@ Packet::Packet() {
   payload_len = 0;
 }
 
+bool Packet::isValidPathLen(uint8_t path_len) {
+  uint8_t hash_count = path_len & 63;
+  uint8_t hash_size = (path_len >> 6) + 1;
+  if (hash_size == 4) return false;  // Reserved for future
+  return hash_count*hash_size <= MAX_PATH_SIZE;
+}
+
+size_t Packet::writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) {
+  uint8_t hash_count = path_len & 63;
+  uint8_t hash_size = (path_len >> 6) + 1;
+  size_t len = hash_count*hash_size;
+  if (len > MAX_PATH_SIZE) {
+    MESH_DEBUG_PRINTLN("Packet::copyPath, invalid path_len=%d", (uint32_t)path_len);
+    return 0;   // Error
+  }
+  memcpy(dest, src, len);
+  return len;
+}
+
+uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) {
+  writePath(dest, src, path_len);
+  return path_len;
+}
+
 int Packet::getRawLength() const {
-  return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0);
+  return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0);
 }
 
 void Packet::calculatePacketHash(uint8_t* hash) const {
@@ -33,7 +57,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const {
     memcpy(&dest[i], &transport_codes[1], 2); i += 2;
   }
   dest[i++] = path_len;
-  memcpy(&dest[i], path, path_len); i += path_len;
+  i += writePath(&dest[i], path, path_len);
   memcpy(&dest[i], payload, payload_len); i += payload_len;
   return i;
 }
@@ -48,8 +72,11 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
     transport_codes[0] = transport_codes[1] = 0;
   }
   path_len = src[i++];
-  if (path_len > sizeof(path)) return false;   // bad encoding
-  memcpy(path, &src[i], path_len); i += path_len;
+  if (!isValidPathLen(path_len)) return false;   // bad encoding
+
+  uint8_t bl = getPathByteLen();
+  memcpy(path, &src[i], bl); i += bl;
+
   if (i >= len) return false;   // bad encoding
   payload_len = len - i;
   if (payload_len > sizeof(payload)) return false;  // bad encoding

+ 10 - 0
src/Packet.h

@@ -76,6 +76,16 @@ public:
    */
   uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; }
 
+  uint8_t getPathHashSize() const { return (path_len >> 6) + 1; }
+  uint8_t getPathHashCount() const { return path_len & 63; }
+  uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); }
+  void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; }
+  void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); }
+
+  static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len);  // returns path_len
+  static size_t writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len);  // returns byte length written
+  static bool isValidPathLen(uint8_t path_len);
+
   void markDoNotRetransmit() { header = 0xFF; }
   bool isMarkedDoNotRetransmit() const { return header == 0xFF; }
 

+ 14 - 14
src/helpers/BaseChatMesh.cpp

@@ -39,7 +39,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
 }
 
 void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
-  if (dest.out_path_len < 0) {
+  if (dest.out_path_len == OUT_PATH_UNKNOWN) {
     mesh::Packet* ack = createAck(ack_hash);
     if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
   } else {
@@ -92,7 +92,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot() {
 void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
   memset(&ci, 0, sizeof(ci));
   ci.id = id;
-  ci.out_path_len = -1;  // initially out_path is unknown
+  ci.out_path_len = OUT_PATH_UNKNOWN;
   StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
   ci.type = parser.getType();
   if (parser.hasLatLon()) {
@@ -263,7 +263,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
       } else {
         mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
         if (reply) {
-          if (from.out_path_len >= 0) {  // we have an out_path, so send DIRECT
+          if (from.out_path_len != OUT_PATH_UNKNOWN) {  // we have an out_path, so send DIRECT
             sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
           } else {
             sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
@@ -273,7 +273,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
     }
   } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) {
     onContactResponse(from, data, len);
-    if (packet->isRouteFlood() && from.out_path_len >= 0) {
+    if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) {
       // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?)
       handleReturnPathRetry(from, packet->path, packet->path_len);
     }
@@ -295,7 +295,7 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui
 bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
   // NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
   // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
-  memcpy(from.out_path, out_path, from.out_path_len = out_path_len);  // store a copy of path, for sendDirect()
+  from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len);  // store a copy of path, for sendDirect()
   from.lastmod = getRTCClock()->getCurrentTime();
 
   onContactPathUpdated(from);
@@ -317,7 +317,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
     txt_send_timeout = 0;   // matched one we're waiting for, cancel timeout timer
     packet->markDoNotRetransmit();   // ACK was for this node, so don't retransmit
 
-    if (packet->isRouteFlood() && from->out_path_len >= 0) {
+    if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
       // we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?)
       handleReturnPathRetry(*from, packet->path, packet->path_len);
     }
@@ -386,7 +386,7 @@ int  BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
   uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
 
   int rc;
-  if (recipient.out_path_len < 0) {
+  if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
     sendFloodScoped(recipient, pkt);
     txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
     rc = MSG_SEND_SENT_FLOOD;
@@ -412,7 +412,7 @@ int  BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest
 
   uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
   int rc;
-  if (recipient.out_path_len < 0) {
+  if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
     sendFloodScoped(recipient, pkt);
     txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
     rc = MSG_SEND_SENT_FLOOD;
@@ -500,7 +500,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
   }
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
-    if (recipient.out_path_len < 0) {
+    if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
       sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
@@ -525,7 +525,7 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data,
   }
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
-    if (recipient.out_path_len < 0) {
+    if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
       sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
@@ -552,7 +552,7 @@ int  BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
   }
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
-    if (recipient.out_path_len < 0) {
+    if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
       sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
@@ -579,7 +579,7 @@ int  BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
   }
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
-    if (recipient.out_path_len < 0) {
+    if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
       sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
@@ -683,7 +683,7 @@ void BaseChatMesh::checkConnections() {
         MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!");
         continue;
       }
-      if (contact->out_path_len < 0) {
+      if (contact->out_path_len == OUT_PATH_UNKNOWN) {
         MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!");
         continue;
       }
@@ -710,7 +710,7 @@ void BaseChatMesh::checkConnections() {
 }
 
 void BaseChatMesh::resetPathTo(ContactInfo& recipient) {
-  recipient.out_path_len = -1;
+  recipient.out_path_len = OUT_PATH_UNKNOWN;
 }
 
 static ContactInfo* table;  // pass via global :-(

+ 1 - 1
src/helpers/ClientACL.cpp

@@ -114,7 +114,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
   memset(c, 0, sizeof(*c));
   c->permissions = init_perms;
   c->id = id;
-  c->out_path_len = -1;  // initially out_path is unknown
+  c->out_path_len = OUT_PATH_UNKNOWN;
   return c;
 }
 

+ 3 - 1
src/helpers/ClientACL.h

@@ -10,10 +10,12 @@
 #define PERM_ACL_READ_WRITE    2
 #define PERM_ACL_ADMIN         3
 
+#define OUT_PATH_UNKNOWN  0xFF
+
 struct ClientInfo {
   mesh::Identity id;
   uint8_t permissions;
-  int8_t out_path_len;
+  uint8_t out_path_len;
   uint8_t out_path[MAX_PATH_SIZE];
   uint8_t shared_secret[PUB_KEY_SIZE];
   uint32_t last_timestamp;   // by THEIR clock  (transient)

+ 28 - 2
src/helpers/CommonCLI.cpp

@@ -64,7 +64,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw));                                         // 116
     file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval));         // 120
     file.read((uint8_t *)&_prefs->sx126x_rx_boosted_gain, sizeof(_prefs->sx126x_rx_boosted_gain)); // 121
-    file.read(pad, 2); // 122 : 2 byte unused
+    file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode));                 // 122
+    file.read(pad, 1); // 123 : 1 byte unused
     file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max));                           // 124
     file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval));   // 125
     file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -96,6 +97,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30);
     _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
 
     // sanitise bad bridge pref values
     _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
@@ -152,7 +154,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
     file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw));                                         // 116
     file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval));         // 120
     file.write((uint8_t *)&_prefs->sx126x_rx_boosted_gain, sizeof(_prefs->sx126x_rx_boosted_gain)); // 121
-    file.write(pad, 2); // 122 : 2 byte unused
+    file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode));                 // 122
+    file.write(pad, 1); // 123 : 1 byte unused
     file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max));                           // 124
     file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval));   // 125
     file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
@@ -334,6 +337,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
           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, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
         sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
       } else if (memcmp(config, "freq", 4) == 0) {
@@ -371,6 +376,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       } 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) {
@@ -561,6 +577,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         *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, "tx ", 3) == 0) {
         _prefs->tx_power_dbm = atoi(&config[3]);
         savePrefs();

+ 2 - 2
src/helpers/CommonCLI.h

@@ -52,8 +52,8 @@ struct NodePrefs { // persisted to file
   uint32_t discovery_mod_timestamp;
   float adc_multiplier;
   char owner_info[120];
-  // Power settings
-  uint8_t sx126x_rx_boosted_gain;
+  uint8_t sx126x_rx_boosted_gain; // power settings
+  uint8_t path_hash_mode;   // which path mode to use when sending
 };
 
 class CommonCLICallbacks {

+ 3 - 1
src/helpers/ContactInfo.h

@@ -3,12 +3,14 @@
 #include <Arduino.h>
 #include <Mesh.h>
 
+#define OUT_PATH_UNKNOWN   0xFF
+
 struct ContactInfo {
   mesh::Identity id;
   char name[32];
   uint8_t type;   // on of ADV_TYPE_*
   uint8_t flags;
-  int8_t out_path_len;
+  uint8_t out_path_len;
   mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated
   uint8_t out_path[MAX_PATH_SIZE];
   uint32_t last_advert_timestamp;   // by THEIR clock

+ 19 - 0
src/helpers/NRF52Board.cpp

@@ -297,6 +297,25 @@ float NRF52Board::getMCUTemperature() {
   return temp * 0.25f; // Convert to *C
 }
 
+bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
+    static const char BOOTLOADER_MARKER[] = "UF2 Bootloader ";
+    const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B
+
+    for (uint32_t i = 0; i < 0x3000 - (sizeof(BOOTLOADER_MARKER) - 1); i++) {
+        if (memcmp(&flash[i], BOOTLOADER_MARKER, sizeof(BOOTLOADER_MARKER) - 1) == 0) {
+            const char* ver = (const char*)&flash[i + sizeof(BOOTLOADER_MARKER) - 1];
+            size_t len = 0;
+            while (len < max_len - 1 && ver[len] != '\0' && ver[len] != ' ' && ver[len] != '\n' && ver[len] != '\r') {
+                out[len] = ver[len];
+                len++;
+            }
+            out[len] = '\0';
+            return len > 0; // bootloader string is non-empty
+        }
+    }
+    return false;
+}
+
 bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
   // Config the peripheral connection with maximum bandwidth
   // more SRAM required by SoftDevice

+ 1 - 0
src/helpers/NRF52Board.h

@@ -50,6 +50,7 @@ public:
   virtual uint8_t getStartupReason() const override { return startup_reason; }
   virtual float getMCUTemperature() override;
   virtual void reboot() override { NVIC_SystemReset(); }
+  virtual bool getBootloaderVersion(char* version, size_t max_len) override;
   virtual bool startOTAUpdate(const char *id, char reply[]) override;
   virtual void sleep(uint32_t secs) override;
 

+ 13 - 7
src/helpers/StaticPoolPacketManager.cpp

@@ -11,7 +11,7 @@ PacketQueue::PacketQueue(int max_entries) {
 int PacketQueue::countBefore(uint32_t now) const {
   int n = 0;
   for (int j = 0; j < _num; j++) {
-    if (_schedule_table[j] > now) continue;   // scheduled for future... ignore for now
+    if ((int32_t)(_schedule_table[j] - now) > 0) continue;   // scheduled for future... ignore for now
     n++;
   }
   return n;
@@ -21,7 +21,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) {
   uint8_t min_pri = 0xFF;
   int best_idx = -1;
   for (int j = 0; j < _num; j++) {
-    if (_schedule_table[j] > now) continue;   // scheduled for future... ignore for now
+    if ((int32_t)(_schedule_table[j] - now) > 0) continue;   // scheduled for future... ignore for now
     if (_pri_table[j] < min_pri) {  // select most important priority amongst non-future entries
       min_pri = _pri_table[j];
       best_idx = j;
@@ -55,15 +55,15 @@ mesh::Packet* PacketQueue::removeByIdx(int i) {
   return item;
 }
 
-void PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
+bool PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
   if (_num == _size) {
-    // TODO: log "FATAL: queue is full!"
-    return;
+    return false;
   }
   _table[_num] = packet;
   _pri_table[_num] = priority;
   _schedule_table[_num] = scheduled_for;
   _num++;
+  return true;
 }
 
 StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_size), send_queue(pool_size), rx_queue(pool_size) {
@@ -82,7 +82,10 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) {
 }
 
 void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
-  send_queue.add(packet, priority, scheduled_for);
+  if (!send_queue.add(packet, priority, scheduled_for)) {
+    MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
+    free(packet);
+  }
 }
 
 mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) {
@@ -106,7 +109,10 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) {
 }
 
 void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) {
-  rx_queue.add(packet, 0, scheduled_for);
+  if (!rx_queue.add(packet, 0, scheduled_for)) {
+    MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet");
+    free(packet);
+  }
 }
 mesh::Packet* StaticPoolPacketManager::getNextInbound(uint32_t now) {
   return rx_queue.get(now);

+ 1 - 1
src/helpers/StaticPoolPacketManager.h

@@ -11,7 +11,7 @@ class PacketQueue {
 public:
   PacketQueue(int max_entries);
   mesh::Packet* get(uint32_t now);
-  void add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
+  bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
   int count() const { return _num; }
   int countBefore(uint32_t now) const;
   mesh::Packet* itemAt(int i) const { return _table[i]; }

+ 1 - 0
src/helpers/esp32/SerialBLEInterface.cpp

@@ -1,4 +1,5 @@
 #include "SerialBLEInterface.h"
+#include "esp_mac.h"
 
 // See the following for generating UUIDs:
 // https://www.uuidgenerator.net/

+ 25 - 0
variants/heltec_tracker/platformio.ini

@@ -44,6 +44,31 @@ lib_deps =
   stevemarple/MicroNMEA @ ^2.0.6
   adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
 
+[env:Heltec_Wireless_Tracker_companion_radio_usb]
+extends = Heltec_tracker_base
+build_flags =
+  ${Heltec_tracker_base.build_flags}
+  -I src/helpers/ui
+  -I examples/companion_radio/ui-new
+  -D DISPLAY_ROTATION=1
+  -D DISPLAY_CLASS=ST7735Display
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+; -D BLE_PIN_CODE=123456 ; HWT will use display for pin
+; -D OFFLINE_QUEUE_SIZE=256
+;  -D BLE_DEBUG_LOGGING=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_base.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+  +<helpers/ui/ST7735Display.cpp>
+lib_deps =
+  ${Heltec_tracker_base.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
 [env:Heltec_Wireless_Tracker_companion_radio_ble]
 extends = Heltec_tracker_base
 build_flags =

+ 1 - 0
variants/heltec_tracker_v2/platformio.ini

@@ -27,6 +27,7 @@ build_flags =
   -D SX126X_DIO3_TCXO_VOLTAGE=1.8
   -D SX126X_CURRENT_LIMIT=140
   -D SX126X_RX_BOOSTED_GAIN=1
+  -D SX126X_REGISTER_PATCH=1
   -D PIN_BOARD_SDA=5
   -D PIN_BOARD_SCL=6
   -D PIN_USER_BTN=0

+ 27 - 1
variants/lilygo_tbeam_supreme_SX1262/platformio.ini

@@ -27,7 +27,9 @@ build_src_filter = ${esp32_base.build_src_filter}
   +<helpers/ui/SH1106Display.cpp>
   +<helpers/esp32/TBeamBoard.cpp>
   +<helpers/sensors>
-board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
+board_build.partitions = default_8MB.csv
+board_upload.flash_size = 8MB
+board_upload.maximum_size = 8388608
 lib_deps =
   ${esp32_base.lib_deps}
   lewisxhe/XPowersLib @ ^0.2.7
@@ -132,3 +134,27 @@ build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter}
 lib_deps =
   ${T_Beam_S3_Supreme_SX1262.lib_deps}
   densaugeo/base64 @ ~1.4.0
+
+[env:T_Beam_S3_Supreme_SX1262_companion_radio_wifi]
+extends = T_Beam_S3_Supreme_SX1262
+build_flags =
+  ${T_Beam_S3_Supreme_SX1262.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D OFFLINE_QUEUE_SIZE=256
+  -D WIFI_SSID='"WIFI_SSID"'
+  -D WIFI_PWD='"Password"'
+;  -D WIFI_DEBUG_LOGGING=1
+;  -D MESH_PACKET_LOGGING=8
+;  -D MESH_DEBUG=1
+;  -D ARDUHAL_LOG_LEVEL=4
+;  -D CORE_DEBUG_LEVEL=4
+build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${T_Beam_S3_Supreme_SX1262.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 3 - 3
variants/lilygo_tlora_v2_1/platformio.ini

@@ -18,7 +18,7 @@ build_flags =
   -D P_LORA_SCLK=5      ; SPI clock
   -D P_LORA_MISO=19     ; SPI MISO
   -D P_LORA_MOSI=27     ; SPI MOSI
-  -D P_LORA_TX_LED=2    ; LED pin for TX indication
+  -D P_LORA_TX_LED=25    ; LED pin for TX indication
   -D PIN_BOARD_SDA=21
   -D PIN_BOARD_SCL=22
   -D PIN_VBAT_READ=35   ; Battery voltage reading (analog pin)
@@ -65,7 +65,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<../examples/simple_repeater>
+  +<../examples/simple_secure_chat/main.cpp>
 lib_deps =
   ${LilyGo_TLora_V2_1_1_6.lib_deps}
   densaugeo/base64 @ ~1.4.0
@@ -191,4 +191,4 @@ build_flags =
 ;  -D CORE_DEBUG_LEVEL=3
 lib_deps =
   ${LilyGo_TLora_V2_1_1_6.lib_deps}
-  ${esp32_ota.lib_deps}
+  ${esp32_ota.lib_deps}

+ 3 - 1
variants/m5stack_unit_c6l/platformio.ini

@@ -5,7 +5,7 @@ board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
 build_flags =
   ${esp32c6_base.build_flags}
   ${sensor_base.build_flags}
-  -I variants/M5Stack_Unit_C6L
+  -I variants/m5stack_unit_c6l
   -D P_LORA_TX_LED=15
   -D P_LORA_SCLK=20
   -D P_LORA_MISO=22
@@ -95,6 +95,8 @@ build_flags = ${M5Stack_Unit_C6L.build_flags}
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D OFFLINE_QUEUE_SIZE=256
+  -D ARDUINO_USB_CDC_ON_BOOT=1
+  -D ARDUINO_USB_MODE=1
 build_src_filter = ${M5Stack_Unit_C6L.build_src_filter}
   +<helpers/esp32/*.cpp>
   -<helpers/esp32/ESPNOWRadio.cpp>

+ 7 - 6
variants/rak3112/platformio.ini

@@ -7,6 +7,7 @@ build_flags =
   -I variants/rak3112
   -D RAK_3112=1
   -D ESP32_CPU_FREQ=80
+  -D ARDUINO_USB_CDC_ON_BOOT=1
   -D P_LORA_DIO_1=47
   -D P_LORA_NSS=7
   -D P_LORA_RESET=8
@@ -132,14 +133,14 @@ lib_deps =
 extends = rak3112
 build_flags =
   ${rak3112.build_flags}
-  -I examples/companion_radio/ui-new
+  -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${rak3112.build_src_filter}
   +<../examples/companion_radio/*.cpp>
-  +<../examples/companion_radio/ui-new/*.cpp>
+  +<../examples/companion_radio/ui-orig/*.cpp>
 lib_deps =
   ${rak3112.lib_deps}
   densaugeo/base64 @ ~1.4.0
@@ -148,7 +149,7 @@ lib_deps =
 extends = rak3112
 build_flags =
   ${rak3112.build_flags}
-  -I examples/companion_radio/ui-new
+  -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
@@ -160,7 +161,7 @@ build_flags =
 build_src_filter = ${rak3112.build_src_filter}
   +<helpers/esp32/*.cpp>
   +<../examples/companion_radio/*.cpp>
-  +<../examples/companion_radio/ui-new/*.cpp>
+  +<../examples/companion_radio/ui-orig/*.cpp>
 lib_deps =
   ${rak3112.lib_deps}
   densaugeo/base64 @ ~1.4.0
@@ -169,7 +170,7 @@ lib_deps =
 extends = rak3112
 build_flags =
   ${rak3112.build_flags}
-  -I examples/companion_radio/ui-new
+  -I examples/companion_radio/ui-orig
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D WIFI_DEBUG_LOGGING=1
@@ -181,7 +182,7 @@ build_flags =
 build_src_filter = ${rak3112.build_src_filter}
   +<helpers/esp32/*.cpp>
   +<../examples/companion_radio/*.cpp>
-  +<../examples/companion_radio/ui-new/*.cpp>
+  +<../examples/companion_radio/ui-orig/*.cpp>
 lib_deps =
   ${rak3112.lib_deps}
   densaugeo/base64 @ ~1.4.0

+ 2 - 0
variants/wio-tracker-l1/platformio.ini

@@ -97,6 +97,8 @@ build_flags = ${WioTrackerL1.build_flags}
   -D PIN_BUZZER=12
   -D QSPIFLASH=1
   -D ADVERT_NAME='"@@MAC"'
+  -D ENV_PIN_SDA=PIN_WIRE1_SDA
+  -D ENV_PIN_SCL=PIN_WIRE1_SCL
   ; -D MESH_PACKET_LOGGING=1
   ; -D MESH_DEBUG=1
 build_src_filter = ${WioTrackerL1.build_src_filter}