Procházet zdrojové kódy

Merge branch 'dev'

# Conflicts:
#	docs/payloads.md
Scott Powell před 9 měsíci
rodič
revize
9405e8bee3
100 změnil soubory, kde provedl 3957 přidání a 626 odebrání
  1. 39 0
      boards/ESP32-S3-WROOM-1-N4.json
  2. 40 0
      boards/heltec_tracker_v2.json
  3. 72 0
      boards/rak4631.json
  4. 40 7
      build.sh
  5. 31 0
      create-uf2.py
  6. 4 0
      docs/packet_structure.md
  7. 44 12
      docs/payloads.md
  8. 5 33
      examples/companion_radio/DataStore.cpp
  9. 70 2
      examples/companion_radio/MyMesh.cpp
  10. 11 3
      examples/companion_radio/MyMesh.h
  11. 1 0
      examples/companion_radio/main.cpp
  12. 80 15
      examples/companion_radio/ui-new/UITask.cpp
  13. 5 0
      examples/companion_radio/ui-new/UITask.h
  14. 258 22
      examples/simple_repeater/MyMesh.cpp
  15. 56 13
      examples/simple_repeater/MyMesh.h
  16. 23 0
      examples/simple_repeater/RateLimiter.h
  17. 4 1
      examples/simple_repeater/main.cpp
  18. 41 11
      examples/simple_room_server/MyMesh.cpp
  19. 14 2
      examples/simple_room_server/MyMesh.h
  20. 1 0
      examples/simple_room_server/main.cpp
  21. 2 1
      examples/simple_secure_chat/main.cpp
  22. 66 7
      examples/simple_sensor/SensorMesh.cpp
  23. 12 2
      examples/simple_sensor/SensorMesh.h
  24. 1 0
      examples/simple_sensor/main.cpp
  25. 3 3
      library.json
  26. 10 2
      platformio.ini
  27. 67 3
      src/Mesh.cpp
  28. 24 0
      src/Mesh.h
  29. 11 0
      src/MeshCore.h
  30. 1 0
      src/Packet.h
  31. 15 3
      src/helpers/AbstractBridge.h
  32. 12 4
      src/helpers/ArduinoHelpers.h
  33. 4 0
      src/helpers/AutoDiscoverRTCClock.h
  34. 31 15
      src/helpers/BaseChatMesh.cpp
  35. 3 0
      src/helpers/BaseChatMesh.h
  36. 313 55
      src/helpers/CommonCLI.cpp
  37. 59 25
      src/helpers/CommonCLI.h
  38. 237 0
      src/helpers/RegionMap.cpp
  39. 52 0
      src/helpers/RegionMap.h
  40. 13 0
      src/helpers/SensorManager.h
  41. 54 0
      src/helpers/StatsFormatHelper.h
  42. 92 0
      src/helpers/TransportKeyStore.cpp
  43. 31 0
      src/helpers/TransportKeyStore.h
  44. 27 0
      src/helpers/TxtDataHelpers.cpp
  45. 2 0
      src/helpers/TxtDataHelpers.h
  46. 13 1
      src/helpers/bridges/BridgeBase.cpp
  47. 17 9
      src/helpers/bridges/BridgeBase.h
  48. 62 39
      src/helpers/bridges/ESPNowBridge.cpp
  49. 17 16
      src/helpers/bridges/ESPNowBridge.h
  50. 64 60
      src/helpers/bridges/RS232Bridge.cpp
  51. 9 2
      src/helpers/bridges/RS232Bridge.h
  52. 14 56
      src/helpers/radiolib/CustomLR1110.h
  53. 123 18
      src/helpers/sensors/EnvironmentSensorManager.cpp
  54. 3 0
      src/helpers/sensors/EnvironmentSensorManager.h
  55. 5 4
      src/helpers/sensors/LocationProvider.h
  56. 17 3
      src/helpers/sensors/MicroNMEALocationProvider.h
  57. 9 0
      src/helpers/ui/DisplayDriver.h
  58. 125 0
      src/helpers/ui/LGFXDisplay.cpp
  59. 39 0
      src/helpers/ui/LGFXDisplay.h
  60. 2 2
      src/helpers/ui/MomentaryButton.cpp
  61. 1 1
      src/helpers/ui/MomentaryButton.h
  62. 8 1
      src/helpers/ui/ST7735Display.cpp
  63. 20 5
      src/helpers/ui/ST7789LCDDisplay.cpp
  64. 5 5
      variants/ebyte_eora_s3/platformio.ini
  65. 4 2
      variants/generic-e22/platformio.ini
  66. 2 2
      variants/generic_espnow/platformio.ini
  67. 6 5
      variants/heltec_ct62/platformio.ini
  68. 12 11
      variants/heltec_e213/platformio.ini
  69. 12 11
      variants/heltec_e290/platformio.ini
  70. 0 39
      variants/heltec_t114/T114Board.cpp
  71. 0 5
      variants/heltec_t114/T114Board.h
  72. 1 0
      variants/heltec_t114/platformio.ini
  73. 1 2
      variants/heltec_t114/target.cpp
  74. 1 0
      variants/heltec_t114/target.h
  75. 12 11
      variants/heltec_t190/platformio.ini
  76. 5 4
      variants/heltec_tracker/platformio.ini
  77. 88 0
      variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp
  78. 23 0
      variants/heltec_tracker_v2/HeltecTrackerV2Board.h
  79. 60 0
      variants/heltec_tracker_v2/pins_arduino.h
  80. 218 0
      variants/heltec_tracker_v2/platformio.ini
  81. 60 0
      variants/heltec_tracker_v2/target.cpp
  82. 30 0
      variants/heltec_tracker_v2/target.h
  83. 2 12
      variants/heltec_v2/HeltecV2Board.h
  84. 36 5
      variants/heltec_v2/platformio.ini
  85. 1 1
      variants/heltec_v2/target.h
  86. 3 15
      variants/heltec_v3/HeltecV3Board.h
  87. 39 12
      variants/heltec_v3/platformio.ini
  88. 1 1
      variants/heltec_v3/target.h
  89. 9 8
      variants/heltec_v4/platformio.ini
  90. 4 3
      variants/heltec_wireless_paper/platformio.ini
  91. 5 10
      variants/ikoka_nano_nrf/IkokaNanoNRFBoard.cpp
  92. 12 4
      variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h
  93. 312 0
      variants/ikoka_nano_nrf/platformio.ini
  94. 44 0
      variants/ikoka_nano_nrf/target.cpp
  95. 28 0
      variants/ikoka_nano_nrf/target.h
  96. 86 0
      variants/ikoka_nano_nrf/variant.cpp
  97. 149 0
      variants/ikoka_nano_nrf/variant.h
  98. 94 0
      variants/ikoka_stick_nrf/IkokaStickNRFBoard.cpp
  99. 60 0
      variants/ikoka_stick_nrf/IkokaStickNRFBoard.h
  100. 3 0
      variants/ikoka_stick_nrf/platformio.ini

+ 39 - 0
boards/ESP32-S3-WROOM-1-N4.json

@@ -0,0 +1,39 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "esp32s3_out.ld"
+    },
+    "core": "esp32",
+    "extra_flags": [
+      "-D ARDUINO_USB_CDC_ON_BOOT=0",
+      "-D ARDUINO_USB_MSC_ON_BOOT=0",
+      "-D ARDUINO_USB_DFU_ON_BOOT=0",
+      "-D ARDUINO_USB_MODE=0",
+      "-D ARDUINO_RUNNING_CORE=1",
+      "-D ARDUINO_EVENT_RUNNING_CORE=1"
+    ],
+    "f_cpu": "240000000L",
+    "f_flash": "80000000L",
+    "flash_mode": "qio",
+    "hwids": [["0x303A", "0x1001"]],
+    "mcu": "esp32s3",
+    "variant": "ESP32-S3-WROOM-1-N4"
+  },
+  "connectivity": ["wifi", "bluetooth"],
+  "debug": {
+    "default_tool": "esp-builtin",
+    "onboard_tools": ["esp-builtin"],
+    "openocd_target": "esp32s3.cfg"
+  },
+  "frameworks": ["arduino", "espidf"],
+  "name": "ESP32-S3-WROOM-1-N4 (4 MB Flash, No PSRAM)",
+  "upload": {
+    "flash_size": "4MB",
+    "maximum_ram_size": 524288,
+    "maximum_size": 4194304,
+    "require_upload_port": true,
+    "speed": 921600
+  },
+  "url": "https://www.espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf",
+  "vendor": "Espressif"
+}

+ 40 - 0
boards/heltec_tracker_v2.json

@@ -0,0 +1,40 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "esp32s3_out.ld",
+      "partitions": "default_8MB.csv"
+      },
+    "core": "esp32",
+    "extra_flags": [
+      "-DARDUINO_USB_CDC_ON_BOOT=1",
+      "-DARDUINO_USB_MODE=0",
+      "-DARDUINO_RUNNING_CORE=1",
+      "-DARDUINO_EVENT_RUNNING_CORE=1"
+    ],
+    "f_cpu": "240000000L",
+    "f_flash": "80000000L",
+    "flash_mode": "qio",
+    "hwids": [["0x303A", "0x1001"]],
+    "mcu": "esp32s3",
+    "variant": "heltec_tracker_v2"
+  },
+  "connectivity": ["wifi", "bluetooth", "lora"],
+  "debug": {
+    "default_tool": "esp-builtin",
+    "onboard_tools": ["esp-builtin"],
+    "openocd_target": "esp32s3.cfg"
+  },
+  "frameworks": ["arduino", "espidf"],
+  "name": "heltec_tracker v2",
+  "upload": {
+    "flash_size": "8MB",
+    "maximum_ram_size": 327680,
+    "maximum_size": 8388608,
+    "use_1200bps_touch": true,
+    "wait_for_upload_port": true,
+    "require_upload_port": true,
+    "speed": 921600
+  },
+  "url": "https://heltec.org/",
+  "vendor": "heltec"
+}

+ 72 - 0
boards/rak4631.json

@@ -0,0 +1,72 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "nrf52840_s140_v6.ld"
+    },
+    "core": "nRF5",
+    "cpu": "cortex-m4",
+    "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
+    "f_cpu": "64000000L",
+    "hwids": [
+      [
+        "0x239A",
+        "0x8029"
+      ],
+      [
+        "0x239A",
+        "0x0029"
+      ],
+      [
+        "0x239A",
+        "0x002A"
+      ],
+      [
+        "0x239A",
+        "0x802A"
+      ]
+    ],
+    "usb_product": "WisCore RAK4631 Board",
+    "mcu": "nrf52840",
+    "variant": "WisCore_RAK4631_Board",
+    "bsp": {
+      "name": "adafruit"
+    },
+    "softdevice": {
+      "sd_flags": "-DS140",
+      "sd_name": "s140",
+      "sd_version": "6.1.1",
+      "sd_fwid": "0x00B6"
+    },
+    "bootloader": {
+      "settings_addr": "0xFF000"
+    }
+  },
+  "connectivity": [
+    "bluetooth"
+  ],
+  "debug": {
+    "jlink_device": "nRF52840_xxAA",
+    "svd_path": "nrf52840.svd"
+  },
+  "frameworks": [
+    "arduino"
+  ],
+  "name": "WisCore RAK4631 Board",
+  "upload": {
+    "maximum_ram_size": 248832,
+    "maximum_size": 815104,
+    "speed": 115200,
+    "protocol": "nrfutil",
+    "protocols": [
+      "jlink",
+      "nrfjprog",
+      "nrfutil",
+      "stlink"
+    ],
+    "use_1200bps_touch": true,
+    "require_upload_port": true,
+    "wait_for_upload_port": true
+  },
+  "url": "https://www.rakwireless.com",
+  "vendor": "RAKwireless"
+}

+ 40 - 7
build.sh

@@ -1,12 +1,45 @@
 #!/usr/bin/env bash
 
-# usage
-# sh build.sh build-firmware RAK_4631_Repeater
-# sh build.sh build-firmwares
-# sh build.sh build-matching-firmwares RAK_4631
-# sh build.sh build-companion-firmwares
-# sh build.sh build-repeater-firmwares
-# sh build.sh build-room-server-firmwares
+global_usage() {
+  cat - <<EOF
+Usage:
+sh build.sh <command> [target]
+
+Commands:
+  help|usage|-h|--help: Shows this message.
+  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>.
+  build-companion-firmwares: Build all companion firmwares for all build targets.
+  build-repeater-firmwares: Build all repeater firmwares for all build targets.
+  build-room-server-firmwares: Build all chat room server firmwares for all build targets.
+
+Examples:
+Build firmware for the "RAK_4631_repeater" device target
+$ sh build.sh build-firmware RAK_4631_repeater
+
+Build all firmwares for device targets containing the string "RAK_4631"
+$ sh build.sh build-matching-firmwares <build-match-spec>
+
+Build all companion firmwares
+$ sh build.sh build-companion-firmwares
+
+Build all repeater firmwares
+$ sh build.sh build-repeater-firmwares
+
+Build all chat room server firmwares
+$ sh build.sh build-room-server-firmwares
+EOF
+}
+
+# Catch cries for help before doing anything else.
+case $1 in
+  help|usage|-h|--help)
+    global_usage
+    exit 1
+    ;;
+esac
+
 
 # get a list of pio env names that start with "env:"
 get_pio_envs() {

+ 31 - 0
create-uf2.py

@@ -0,0 +1,31 @@
+#!/usr/bin/python3
+
+# Adds PlatformIO post-processing to convert hex files to uf2 files
+
+import os
+
+Import("env")
+
+firmware_hex = "${BUILD_DIR}/${PROGNAME}.hex"
+uf2_file = os.environ.get("UF2_FILE_PATH", "${BUILD_DIR}/${PROGNAME}.uf2")
+
+def create_uf2_action(source, target, env):
+    uf2_cmd = " ".join(
+        [
+            '"$PYTHONEXE"',
+            '"$PROJECT_DIR/bin/uf2conv/uf2conv.py"',
+            '-f', '0xADA52840',
+            '-c', firmware_hex,
+            '-o', uf2_file,
+        ]
+    )
+    env.Execute(uf2_cmd)
+
+env.AddCustomTarget(
+    name="create_uf2",
+    dependencies=firmware_hex,
+    actions=create_uf2_action,
+    title="Create UF2 file",
+    description="Use uf2conv to convert hex binary into uf2",
+    always_build=True,
+)

+ 4 - 0
docs/packet_structure.md

@@ -44,6 +44,10 @@ bit 0 means the lowest bit (1s place)
 | `0x08` | `PAYLOAD_TYPE_PATH`       | Returned path.                                |
 | `0x09` | `PAYLOAD_TYPE_TRACE`      | trace a path, collecting SNI for each hop.    |
 | `0x0A` | `PAYLOAD_TYPE_MULTIPART`  | packet is part of a sequence of packets.      |
+| `0x0B` | `PAYLOAD_TYPE_CONTROL`    | control packet data (unencrypted)             |
+| `0x0C` | .                         | reserved                                      |
+| `0x0D` | .                         | reserved                                      |
+| `0x0E` | .                         | reserved                                      |
 | `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption). |
 
 ## Payload Version Values

+ 44 - 12
docs/payloads.md

@@ -11,6 +11,7 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified
 * Group text message (unverified).
 * Group datagram (unverified).
 * Multi-part packet
+* Control data packet
 * Custom packet (raw bytes, custom encryption).
 
 This document defines the structure of each of these payload types.
@@ -57,7 +58,7 @@ Appdata Flags
 
 # Acknowledgement
 
-An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement will be sent in the "extra" payload (see [Returned Path](#returned-path)) and not as a discrete acknowledgement. CLI commands do not require an acknowledgement, neither discrete nor extra.
+An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate ackowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra.
 
 | Field    | Size (bytes) | Description                                                |
 |----------|--------------|------------------------------------------------------------|
@@ -140,13 +141,13 @@ Request data about sensors on the node, including battery level.
 
 ## Plain text message
 
-| Field           | Size (bytes)    | Description                                                  |
-|-----------------|-----------------|--------------------------------------------------------------|
-| timestamp       | 4               | send time (unix timestamp)                                   |
-| flags + attempt | 1               | upper six bits are flags (see below), lower two bits are attempt number (0..3) |
-| message         | rest of payload | the message content, see next table                          |
+| Field              | Size (bytes)    | Description                                                  |
+|--------------------|-----------------|--------------------------------------------------------------|
+| timestamp          | 4               | send time (unix timestamp)                                   |
+| txt_type + attempt | 1               | upper six bits are txt_type (see below), lower two bits are attempt number (0..3) |
+| message            | rest of payload | the message content, see next table                          |
 
-Flags
+txt_type
 
 | Value  | Description               | Message content                                            |
 |--------|---------------------------|------------------------------------------------------------|
@@ -163,13 +164,20 @@ Flags
 | cipher MAC       | 2               | MAC for encrypted data in next field      |
 | ciphertext       | rest of payload | encrypted message, see below for details  |
 
-Plaintext message
+## Room server login
 
 | Field          | Size (bytes)    | Description                                                                   |
 |----------------|-----------------|-------------------------------------------------------------------------------|
-| timestamp      | 4               | send time (unix timestamp)                                                    |
-| sync timestamp | 4               | NOTE: room server only! - sender's "sync messages SINCE x" timestamp |
-| password       | rest of message | password for repeater/room                                                    |
+| timestamp      | 4               | sender time (unix timestamp)                                                  |
+| sync timestamp | 4               | sender's "sync messages SINCE x" timestamp                                    |
+| password       | rest of message | password for room                                                             |
+
+## Repeater/Sensor login
+
+| Field          | Size (bytes)    | Description                                                                   |
+|----------------|-----------------|-------------------------------------------------------------------------------|
+| timestamp      | 4               | sender time (unix timestamp)                                                  |
+| password       | rest of message | password for repeater/sensor                                                  |
 
 # Group text message / datagram
 
@@ -182,7 +190,31 @@ Plaintext message
 The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `<sender name>: <message body>` (eg., `user123: I'm on my way`).
 
 
-TODO: describe what datagram looks like
+# Control data
+
+| Field        | Size (bytes)    | Description                                |
+|--------------|-----------------|--------------------------------------------|
+| flags        | 1               | upper 4 bits is sub_type                   |
+| data         | rest of payload | typically unencrypted data                 |
+
+## DISCOVER_REQ (sub_type)
+
+| Field        | Size (bytes)    | Description                                  |
+|--------------|-----------------|----------------------------------------------|
+| flags        | 1               | 0x8 (upper 4 bits), prefix_only (lowest bit) |
+| type_filter  | 1               | bit for each ADV_TYPE_*                      |
+| tag          | 4               | randomly generate by sender                  |
+| since        | 4               | (optional) epoch timestamp (0 by default)    |
+
+## DISCOVER_RESP (sub_type)
+
+| Field        | Size (bytes)    | Description                                |
+|--------------|-----------------|--------------------------------------------|
+| flags        | 1               | 0x9 (upper 4 bits), node_type (lower 4)    |
+| snr          | 1               | signed, SNR*4                              |
+| tag          | 4               | reflected back from DISCOVER_REQ           |
+| pubkey       | 8 or 32         | node's ID (or prefix)                      |
+
 
 # Custom packet
 

+ 5 - 33
examples/companion_radio/DataStore.cpp

@@ -197,11 +197,7 @@ void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon)
 }
 
 void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
-#if defined(RP2040_PLATFORM)
-  File file = _fs->open(filename, "r");
-#else
-  File file = _fs->open(filename);
-#endif
+  File file = openRead(_fs, filename);
   if (file) {
     uint8_t pad[8];
 
@@ -262,16 +258,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
 }
 
 void DataStore::loadContacts(DataStoreHost* host) {
-#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
-  if (_getContactsChannelsFS()->exists("/contacts3")) {
-    File file = _getContactsChannelsFS()->open("/contacts3");
-#elif defined(RP2040_PLATFORM)
-    if (_fs->exists("/contacts3")) {
-    File file = _fs->open("/contacts3", "r");
-#else
-    if (_fs->exists("/contacts3")) {
-      File file = _fs->open("/contacts3", "r", false);
-#endif
+File file = openRead(_getContactsChannelsFS(), "/contacts3");
     if (file) {
       bool full = false;
       while (!full) {
@@ -299,7 +286,6 @@ void DataStore::loadContacts(DataStoreHost* host) {
       }
       file.close();
     }
-  }
 }
 
 void DataStore::saveContacts(DataStoreHost* host) {
@@ -332,16 +318,7 @@ void DataStore::saveContacts(DataStoreHost* host) {
 }
 
 void DataStore::loadChannels(DataStoreHost* host) {
-#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
-  if (_getContactsChannelsFS()->exists("/channels2")) {
-    File file = _getContactsChannelsFS()->open("/channels2");
-#elif defined(RP2040_PLATFORM)
-  if (_fs->exists("/channels2")) {
-    File file = _fs->open("/channels2", "r");
-#else
-  if (_fs->exists("/channels2")) {
-    File file = _fs->open("/channels2", "r", false);
-#endif
+    File file = openRead(_getContactsChannelsFS(), "/channels2");
     if (file) {
       bool full = false;
       uint8_t channel_idx = 0;
@@ -363,7 +340,6 @@ void DataStore::loadChannels(DataStoreHost* host) {
       }
       file.close();
     }
-  }
 }
 
 void DataStore::saveChannels(DataStoreHost* host) {
@@ -520,7 +496,7 @@ void DataStore::migrateToSecondaryFS() {
 }
 
 uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
-  File file = _getContactsChannelsFS()->open("/adv_blobs");
+  File file = openRead(_getContactsChannelsFS(), "/adv_blobs");
   uint8_t len = 0;  // 0 = not found
   if (file) {
     BlobRec tmp;
@@ -583,11 +559,7 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b
   sprintf(path, "/bl/%s", fname);
 
   if (_fs->exists(path)) {
-#if defined(RP2040_PLATFORM)
-    File f = _fs->open(path, "r");
-#else
-    File f = _fs->open(path);
-#endif
+    File f = openRead(_fs, path);
     if (f) {
       int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
       f.close();

+ 70 - 2
examples/companion_radio/MyMesh.cpp

@@ -50,6 +50,8 @@
 #define CMD_SEND_BINARY_REQ           50
 #define CMD_FACTORY_RESET             51
 #define CMD_SEND_PATH_DISCOVERY_REQ   52
+#define CMD_SET_FLOOD_SCOPE           54   // v8+
+#define CMD_SEND_CONTROL_DATA         55   // v8+
 
 #define RESP_CODE_OK                  0
 #define RESP_CODE_ERR                 1
@@ -99,6 +101,7 @@
 #define PUSH_CODE_TELEMETRY_RESPONSE    0x8B
 #define PUSH_CODE_BINARY_RESPONSE       0x8C
 #define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
+#define PUSH_CODE_CONTROL_DATA          0x8E   // v8+
 
 #define ERR_CODE_UNSUPPORTED_CMD        1
 #define ERR_CODE_NOT_FOUND              2
@@ -378,6 +381,35 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
 #endif
 }
 
+bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
+  // REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
+  //    if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
+  return false;
+}
+
+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);
+  } 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);
+  }
+}
+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);
+  } 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);
+  }
+}
+
 void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
                            const char *text) {
   markConnectionActive(from); // in case this is from a server, and we have a connection
@@ -596,6 +628,26 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
   return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len);
 }
 
+void MyMesh::onControlDataRecv(mesh::Packet *packet) {
+  if (packet->payload_len + 4 > sizeof(out_frame)) {
+    MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
+    return;
+  }
+  int i = 0;
+  out_frame[i++] = PUSH_CODE_CONTROL_DATA;
+  out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
+  out_frame[i++] = (int8_t)(_radio->getLastRSSI());
+  out_frame[i++] = packet->path_len;
+  memcpy(&out_frame[i], packet->payload, packet->payload_len);
+  i += packet->payload_len;
+
+  if (_serial->isConnected()) {
+    _serial->writeFrame(out_frame, i);
+  } else {
+    MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
+  }
+}
+
 void MyMesh::onRawDataRecv(mesh::Packet *packet) {
   if (packet->payload_len + 4 > sizeof(out_frame)) {
     MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
@@ -663,6 +715,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
   sign_data = NULL;
   dirty_contacts_expiry = 0;
   memset(advert_paths, 0, sizeof(advert_paths));
+  memset(send_scope.key, 0, sizeof(send_scope.key));
 
   // defaults
   memset(&_prefs, 0, sizeof(_prefs));
@@ -706,8 +759,8 @@ void MyMesh::begin(bool has_display) {
   _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
   _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
   _prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f);
-  _prefs.bw = constrain(_prefs.bw, 62.5f, 500.0f);
-  _prefs.sf = constrain(_prefs.sf, 7, 12);
+  _prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
+  _prefs.sf = constrain(_prefs.sf, 5, 12);
   _prefs.cr = constrain(_prefs.cr, 5, 8);
   _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER);
 
@@ -1485,6 +1538,21 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
       writeErrFrame(ERR_CODE_FILE_IO_ERROR);
     }
+  } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) {
+    if (len >= 2 + 16) {
+      memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key));  // set curr scope TransportKey
+    } else {
+      memset(send_scope.key, 0, sizeof(send_scope.key));  // set scope to null
+    }
+    writeOKFrame();
+  } else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) {
+    auto resp = createControlData(&cmd_frame[1], len - 1);
+    if (resp) {
+      sendZeroHop(resp);
+      writeOKFrame();
+    } else {
+      writeErrFrame(ERR_CODE_TABLE_FULL);
+    }
   } else {
     writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

+ 11 - 3
examples/companion_radio/MyMesh.h

@@ -5,14 +5,14 @@
 #include "AbstractUITask.h"
 
 /*------------ Frame Protocol --------------*/
-#define FIRMWARE_VER_CODE 7
+#define FIRMWARE_VER_CODE 8
 
 #ifndef FIRMWARE_BUILD_DATE
-#define FIRMWARE_BUILD_DATE "2 Oct 2025"
+#define FIRMWARE_BUILD_DATE "13 Nov 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-#define FIRMWARE_VERSION "v1.9.1"
+#define FIRMWARE_VERSION "v1.10.0"
 #endif
 
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -68,6 +68,7 @@
 #endif
 
 #include <helpers/BaseChatMesh.h>
+#include <helpers/TransportKeyStore.h>
 
 /* -------------------------------------------------------------------------------------- */
 
@@ -106,6 +107,10 @@ protected:
   int getInterferenceThreshold() const override;
   int calcRxDelay(float score, uint32_t air_time) const override;
   uint8_t getExtraAckTransmitCount() const override;
+  bool filterRecvFloodPacket(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;
 
   void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
   bool isAutoAddEnabled() const override;
@@ -128,6 +133,7 @@ protected:
   uint8_t onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
                            uint8_t len, uint8_t *reply) override;
   void onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) override;
+  void onControlDataRecv(mesh::Packet *packet) override;
   void onRawDataRecv(mesh::Packet *packet) override;
   void onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
                    const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) override;
@@ -191,6 +197,8 @@ private:
   uint32_t sign_data_len;
   unsigned long dirty_contacts_expiry;
 
+  TransportKey send_scope;
+
   uint8_t cmd_frame[MAX_FRAME_SIZE + 1];
   uint8_t out_frame[MAX_FRAME_SIZE + 1];
   CayenneLPP telemetry;

+ 1 - 0
examples/companion_radio/main.cpp

@@ -227,4 +227,5 @@ void loop() {
 #ifdef DISPLAY_CLASS
   ui_task.loop();
 #endif
+  rtc_clock.tick();
 }

+ 80 - 15
examples/companion_radio/ui-new/UITask.cpp

@@ -20,7 +20,11 @@
   #define UI_RECENT_LIST_SIZE 4
 #endif
 
-#define PRESS_LABEL "long press"
+#if UI_HAS_JOYSTICK
+  #define PRESS_LABEL "press Enter"
+#else
+  #define PRESS_LABEL "long press"
+#endif
 
 #include "icons.h"
 
@@ -75,6 +79,9 @@ class HomeScreen : public UIScreen {
     RADIO,
     BLUETOOTH,
     ADVERT,
+#if ENV_INCLUDE_GPS == 1
+    GPS,
+#endif
 #if UI_SENSORS_PAGE == 1
     SENSORS,
 #endif
@@ -170,7 +177,7 @@ public:
 
     // curr page indicator
     int y = 14;
-    int x = display.width() / 2 - 25;
+    int x = display.width() / 2 - 5 * (HomePage::Count-1);
     for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) {
       if (i == _page) {
         display.fillRect(x-1, y-1, 3, 3);
@@ -250,6 +257,34 @@ public:
       display.setColor(DisplayDriver::GREEN);
       display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
       display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL);
+#if ENV_INCLUDE_GPS == 1
+    } else if (_page == HomePage::GPS) {
+      LocationProvider* nmea = sensors.getLocationProvider();
+      int y = 18;
+      display.drawTextLeftAlign(0, y, _task->getGPSState() ? "gps on" : "gps off");
+      if (nmea == NULL) {
+        y = y + 12;
+        display.drawTextLeftAlign(0, y, "Can't access GPS");
+      } else {
+        char buf[50];
+        strcpy(buf, nmea->isValid()?"fix":"no fix");
+        display.drawTextRightAlign(display.width()-1, y, buf);
+        y = y + 12;
+        display.drawTextLeftAlign(0, y, "sat");
+        sprintf(buf, "%d", nmea->satellitesCount());
+        display.drawTextRightAlign(display.width()-1, y, buf);
+        y = y + 12;
+        display.drawTextLeftAlign(0, y, "pos");
+        sprintf(buf, "%.4f %.4f", 
+          nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
+        display.drawTextRightAlign(display.width()-1, y, buf);
+        y = y + 12;
+        display.drawTextLeftAlign(0, y, "alt");
+        sprintf(buf, "%.2f", nmea->getAltitude()/1000.);
+        display.drawTextRightAlign(display.width()-1, y, buf);
+        y = y + 12;
+      }
+#endif
 #if UI_SENSORS_PAGE == 1
     } else if (_page == HomePage::SENSORS) {
       int y = 18;
@@ -329,7 +364,7 @@ public:
         display.drawTextCentered(display.width() / 2, 34, "hibernating...");
       } else {
         display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32);
-        display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate: " PRESS_LABEL);
+        display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
       }
     }
     return 5000;   // next render after 5000 ms
@@ -364,6 +399,12 @@ public:
       }
       return true;
     }
+#if ENV_INCLUDE_GPS == 1
+    if (c == KEY_ENTER && _page == HomePage::GPS) {
+      _task->toggleGPS();
+      return true;
+    }
+#endif
 #if UI_SENSORS_PAGE == 1
     if (c == KEY_ENTER && _page == HomePage::SENSORS) {
       _task->toggleGPS();
@@ -623,19 +664,13 @@ bool UITask::isButtonPressed() const {
 
 void UITask::loop() {
   char c = 0;
-#if defined(PIN_USER_BTN)
+#if UI_HAS_JOYSTICK
   int ev = user_btn.check();
   if (ev == BUTTON_EVENT_CLICK) {
-    c = checkDisplayOn(KEY_NEXT);
+    c = checkDisplayOn(KEY_ENTER);
   } else if (ev == BUTTON_EVENT_LONG_PRESS) {
-    c = handleLongPress(KEY_ENTER);
-  } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
-    c = handleDoubleClick(KEY_PREV);
-  } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
-    c = handleTripleClick(KEY_SELECT);
+    c = handleLongPress(KEY_ENTER);  // REVISIT: could be mapped to different key code
   }
-#endif
-#if defined(WIO_TRACKER_L1)
   ev = joystick_left.check();
   if (ev == BUTTON_EVENT_CLICK) {
     c = checkDisplayOn(KEY_LEFT);
@@ -648,9 +683,12 @@ void UITask::loop() {
   } else if (ev == BUTTON_EVENT_LONG_PRESS) {
     c = handleLongPress(KEY_RIGHT);
   }
-#endif
-#if defined(PIN_USER_BTN_ANA)
-  ev = analog_btn.check();
+  ev = back_btn.check();
+  if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
+    c = handleTripleClick(KEY_SELECT);
+  }
+#elif defined(PIN_USER_BTN)
+  int ev = user_btn.check();
   if (ev == BUTTON_EVENT_CLICK) {
     c = checkDisplayOn(KEY_NEXT);
   } else if (ev == BUTTON_EVENT_LONG_PRESS) {
@@ -661,6 +699,21 @@ void UITask::loop() {
     c = handleTripleClick(KEY_SELECT);
   }
 #endif
+#if defined(PIN_USER_BTN_ANA)
+  if (abs(millis() - _analogue_pin_read_millis) > 10) {
+    ev = analog_btn.check();
+    if (ev == BUTTON_EVENT_CLICK) {
+      c = checkDisplayOn(KEY_NEXT);
+    } else if (ev == BUTTON_EVENT_LONG_PRESS) {
+      c = handleLongPress(KEY_ENTER);
+    } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
+      c = handleDoubleClick(KEY_PREV);
+    } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
+      c = handleTripleClick(KEY_SELECT);
+    }
+    _analogue_pin_read_millis = millis();
+  }
+#endif
 #if defined(DISP_BACKLIGHT) && defined(BACKLIGHT_BTN)
   if (millis() > next_backlight_btn_check) {
     bool touch_state = digitalRead(PIN_BUTTON2);
@@ -773,6 +826,18 @@ char UITask::handleTripleClick(char c) {
   return c;
 }
 
+bool UITask::getGPSState() {
+  if (_sensors != NULL) {
+    int num = _sensors->getNumSettings();
+    for (int i = 0; i < num; i++) {
+      if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
+        return !strcmp(_sensors->getSettingValue(i), "1");
+      }
+    }
+  } 
+  return false;
+}
+
 void UITask::toggleGPS() {
     if (_sensors != NULL) {
     // toggle GPS on/off

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

@@ -40,6 +40,10 @@ class UITask : public AbstractUITask {
   int last_led_increment = 0;
 #endif
 
+#ifdef PIN_USER_BTN_ANA
+  unsigned long _analogue_pin_read_millis = millis();
+#endif
+
   UIScreen* splash;
   UIScreen* home;
   UIScreen* msg_preview;
@@ -71,6 +75,7 @@ public:
   bool isButtonPressed() const;
 
   void toggleBuzzer();
+  bool getGPSState();
   void toggleGPS();
 
 

+ 258 - 22
examples/simple_repeater/MyMesh.cpp

@@ -114,6 +114,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr
     MESH_DEBUG_PRINTLN("Login success!");
     client->last_timestamp = sender_timestamp;
     client->last_activity = getRTCClock()->getCurrentTime();
+    client->permissions &= ~0x03;
     client->permissions |= perms;
     memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
 
@@ -148,7 +149,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
     stats.n_packets_recv = radio_driver.getPacketsRecv();
     stats.n_packets_sent = radio_driver.getPacketsSent();
     stats.total_air_time_secs = getTotalAirTime() / 1000;
-    stats.total_up_time_secs = _ms->getMillis() / 1000;
+    stats.total_up_time_secs = uptime_millis / 1000;
     stats.n_sent_flood = getNumSentFlood();
     stats.n_sent_direct = getNumSentDirect();
     stats.n_recv_flood = getNumRecvFlood();
@@ -169,7 +170,10 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
     telemetry.reset();
     telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
     // query other sensors -- target specific
-    sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry);
+    if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
+      perm_mask = 0x00;  // just base telemetry allowed
+    }
+    sensors.querySensors(perm_mask, telemetry);
 
     uint8_t tlen = telemetry.getSize();
     memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
@@ -287,11 +291,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
 
 mesh::Packet *MyMesh::createSelfAdvert() {
   uint8_t app_data[MAX_ADVERT_DATA_SIZE];
-  uint8_t app_data_len;
-  {
-    AdvertDataBuilder builder(ADV_TYPE_REPEATER, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
-    app_data_len = builder.encodeTo(app_data);
-  }
+  uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_REPEATER, app_data);
 
   return createAdvert(self_id, app_data, app_data_len);
 }
@@ -309,6 +309,10 @@ 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() && recv_pkt_region == NULL) {
+    MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
+    return false;
+  }
   return true;
 }
 
@@ -331,6 +335,12 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
 }
 
 void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
+#ifdef WITH_BRIDGE
+  if (_prefs.bridge_pkt_src == 1) {
+    bridge.sendPacket(pkt);
+  }
+#endif
+
   if (_logging) {
     File f = openAppend(PACKET_LOG_FILE);
     if (f) {
@@ -352,8 +362,11 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
 
 void MyMesh::logTx(mesh::Packet *pkt, int len) {
 #ifdef WITH_BRIDGE
-  bridge.onPacketTransmitted(pkt);
+  if (_prefs.bridge_pkt_src == 0) {
+    bridge.sendPacket(pkt);
+  }
 #endif
+
   if (_logging) {
     File f = openAppend(PACKET_LOG_FILE);
     if (f) {
@@ -391,11 +404,28 @@ 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);
-  return getRNG()->nextInt(0, 6) * t;
+  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);
-  return getRNG()->nextInt(0, 6) * t;
+  return getRNG()->nextInt(0, 5*t + 1);
+}
+
+bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
+  // just try to determine region for packet (apply later in allowPacketForward())
+  if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
+    recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
+  } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
+    if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
+      recv_pkt_region = NULL;
+    } else {
+      recv_pkt_region =  &region_map.getWildcard();
+    }
+  } else {
+    recv_pkt_region = NULL;
+  }
+  // do normal processing
+  return false;
 }
 
 void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const mesh::Identity &sender,
@@ -406,7 +436,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
     memcpy(&timestamp, data, 4);
 
     data[len] = 0;  // ensure null terminator
-    uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
+    uint8_t reply_len;
+    if (data[4] == 0 || data[4] >= ' ') {   // is password, ie. a login request
+      reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
+    //} else if (data[4] == ANON_REQ_TYPE_*) {   // future type codes
+      // TODO
+    } else {
+      reply_len = 0;  // unknown request type
+    }
 
     if (reply_len == 0) return;   // invalid request
 
@@ -442,12 +479,19 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) {
   }
 }
 
+static bool isShare(const mesh::Packet *packet) {
+  if (packet->hasTransportCodes()) {
+    return packet->transport_codes[0] == 0 && packet->transport_codes[1] == 0;  // codes { 0, 0 } means 'send to nowhere'
+  }
+  return false;
+}
+
 void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp,
                           const uint8_t *app_data, size_t app_data_len) {
   mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl
 
-  // if this a zero hop advert, add it to neighbours
-  if (packet->path_len == 0) {
+  // if this a zero hop advert (and not via 'Share'), add it to neighbours
+  if (packet->path_len == 0 && !isShare(packet)) {
     AdvertDataParser parser(app_data, app_data_len);
     if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters
       putNeighbour(id, timestamp, packet->getSNR());
@@ -577,20 +621,57 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
   return false;
 }
 
+#define CTL_TYPE_NODE_DISCOVER_REQ   0x80
+#define CTL_TYPE_NODE_DISCOVER_RESP  0x90
+
+void MyMesh::onControlDataRecv(mesh::Packet* packet) {
+  uint8_t type = packet->payload[0] & 0xF0;    // just test upper 4 bits
+  if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) {
+    int i = 1;
+    uint8_t  filter = packet->payload[i++];
+    uint32_t tag;
+    memcpy(&tag, &packet->payload[i], 4); i += 4;
+    uint32_t since;
+    if (packet->payload_len >= i+4) {   // optional since field
+      memcpy(&since, &packet->payload[i], 4); i += 4;
+    } else {
+      since = 0;
+    }
+
+    if ((filter & (1 << ADV_TYPE_REPEATER)) != 0 && _prefs.discovery_mod_timestamp >= since) {
+      bool prefix_only = packet->payload[0] & 1;
+      uint8_t data[6 + PUB_KEY_SIZE];
+      data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_REPEATER;   // low 4-bits for node type
+      data[1] = packet->_snr;   // let sender know the inbound SNR ( x 4)
+      memcpy(&data[2], &tag, 4);     // include tag from request, for client to match to
+      memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
+      auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
+      if (resp) {
+        sendZeroHop(resp, getRetransmitDelay(resp)*4);  // apply random delay (widened x4), as multiple nodes can respond to this
+      }
+    }
+  }
+}
+
 MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
                mesh::RTCClock &rtc, mesh::MeshTables &tables)
     : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
+      _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store),
+      discover_limiter(4, 120)  // max 4 every 2 minutes
 #if defined(WITH_RS232_BRIDGE)
-      , bridge(WITH_RS232_BRIDGE, _mgr, &rtc)
-#elif defined(WITH_ESPNOW_BRIDGE)
-      , bridge(_mgr, &rtc)
+      , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
+#endif
+#if defined(WITH_ESPNOW_BRIDGE)
+      , bridge(&_prefs, _mgr, &rtc)
 #endif
 {
+  last_millis = 0;
+  uptime_millis = 0;
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
   set_radio_at = revert_radio_at = 0;
   _logging = false;
+  region_load_active = false;
 
 #if MAX_NEIGHBOURS
   memset(neighbours, 0, sizeof(neighbours));
@@ -601,6 +682,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
   StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
   _prefs.node_lat = ADVERT_LAT;
   _prefs.node_lon = ADVERT_LON;
@@ -614,6 +696,20 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.flood_advert_interval = 12; // 12 hours
   _prefs.flood_max = 64;
   _prefs.interference_threshold = 0; // disabled
+
+  // bridge defaults
+  _prefs.bridge_enabled = 1;    // enabled
+  _prefs.bridge_delay   = 500;  // milliseconds
+  _prefs.bridge_pkt_src = 0;    // logTx
+  _prefs.bridge_baud = 115200;  // baud rate
+  _prefs.bridge_channel = 1;    // channel 1
+
+  StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret));
+
+  // GPS defaults
+  _prefs.gps_enabled = 0;
+  _prefs.gps_interval = 0;
+  _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
 }
 
 void MyMesh::begin(FILESYSTEM *fs) {
@@ -621,11 +717,14 @@ void MyMesh::begin(FILESYSTEM *fs) {
   _fs = fs;
   // load persisted prefs
   _cli.loadPrefs(_fs);
-
   acl.load(_fs);
+  // TODO: key_store.begin();
+  region_map.load(_fs);
 
-#ifdef WITH_BRIDGE
-  bridge.begin();
+#if defined(WITH_BRIDGE)
+  if (_prefs.bridge_enabled) {
+    bridge.begin();
+  }
 #endif
 
   radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -633,6 +732,10 @@ void MyMesh::begin(FILESYSTEM *fs) {
 
   updateAdvertTimer();
   updateFloodAdvertTimer();
+
+#if ENV_INCLUDE_GPS == 1
+  applyGpsPrefs();
+#endif
 }
 
 void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
@@ -758,6 +861,19 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) {
 #endif
 }
 
+void MyMesh::formatStatsReply(char *reply) {
+  StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
+}
+
+void MyMesh::formatRadioStatsReply(char *reply) {
+  StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
+}
+
+void MyMesh::formatPacketStatsReply(char *reply) {
+  StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), 
+                                       getNumRecvFlood(), getNumRecvDirect());
+}
+
 void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
   self_id = new_id;
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -779,8 +895,41 @@ void MyMesh::clearStats() {
 }
 
 void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
-  while (*command == ' ')
-    command++; // skip leading spaces
+  if (region_load_active) {
+    if (StrHelper::isBlank(command)) {  // empty/blank line, signal to terminate 'load' operation
+      region_map = temp_map;  // copy over the temp instance as new current map
+      region_load_active = false;
+
+      sprintf(reply, "OK - loaded %d regions", region_map.getCount());
+    } else {
+      char *np = command;
+      while (*np == ' ') np++;   // skip indent
+      int indent = np - command;
+
+      char *ep = np;
+      while (RegionMap::is_name_char(*ep)) ep++;
+      if (*ep) { *ep++ = 0; }  // set null terminator for end of name
+
+      while (*ep && *ep != 'F') ep++;  // look for (optional) flags
+
+      if (indent > 0 && indent < 8 && strlen(np) > 0) {
+        auto parent = load_stack[indent - 1];
+        if (parent) {
+          auto old = region_map.findByName(np);
+          auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0);  // carry-over the current ID (if name already exists)
+          if (nw) {
+            nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD);   // carry-over flags from curr
+
+            load_stack[indent] = nw;  // keep pointers to parent regions, to resolve parent_id's
+          }
+        }
+      }
+      reply[0] = 0;
+    }
+    return;
+  }
+
+  while (*command == ' ') command++; // skip leading spaces
 
   if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
     memcpy(reply, command, 3);                    // reflect the prefix back
@@ -822,6 +971,88 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
       Serial.printf("\n");
     }
     reply[0] = 0;
+  } else if (memcmp(command, "region", 6) == 0) {
+    reply[0] = 0;
+
+    const char* parts[4];
+    int n = mesh::Utils::parseTextParts(command, parts, 4, ' ');
+    if (n == 1 && sender_timestamp == 0) {
+      region_map.exportTo(Serial);
+    } else if (n >= 2 && strcmp(parts[1], "load") == 0) {
+      temp_map.resetFrom(region_map);   // rebuild regions in a temp instance
+      memset(load_stack, 0, sizeof(load_stack));
+      load_stack[0] = &temp_map.getWildcard();
+      region_load_active = true;
+    } else if (n >= 2 && strcmp(parts[1], "save") == 0) {
+      _prefs.discovery_mod_timestamp = rtc_clock.getCurrentTime();   // this node is now 'modified' (for discovery info)
+      savePrefs();
+      bool success = region_map.save(_fs);
+      strcpy(reply, success ? "OK" : "Err - save failed");
+    } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) {
+      auto region = region_map.findByNamePrefix(parts[2]);
+      if (region) {
+        region->flags &= ~REGION_DENY_FLOOD;
+        strcpy(reply, "OK");
+      } else {
+        strcpy(reply, "Err - unknown region");
+      }
+    } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) {
+      auto region = region_map.findByNamePrefix(parts[2]);
+      if (region) {
+        region->flags |= REGION_DENY_FLOOD;
+        strcpy(reply, "OK");
+      } else {
+        strcpy(reply, "Err - unknown region");
+      }
+    } else if (n >= 3 && strcmp(parts[1], "get") == 0) {
+      auto region = region_map.findByNamePrefix(parts[2]);
+      if (region) {
+        auto parent = region_map.findById(region->parent);
+        if (parent && parent->id != 0) {
+          sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
+        } else {
+          sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F");
+        }
+      } else {
+        strcpy(reply, "Err - unknown region");
+      }
+    } else if (n >= 3 && strcmp(parts[1], "home") == 0) {
+      auto home = region_map.findByNamePrefix(parts[2]);
+      if (home) {
+        region_map.setHomeRegion(home);
+        sprintf(reply, " home is now %s", home->name);
+      } else {
+        strcpy(reply, "Err - unknown region");
+      }
+    } else if (n == 2 && strcmp(parts[1], "home") == 0) {
+      auto home = region_map.getHomeRegion();
+      sprintf(reply, " home is %s", home ? home->name : "*");
+    } else if (n >= 3 && strcmp(parts[1], "put") == 0) {
+      auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : &region_map.getWildcard();
+      if (parent == NULL) {
+        strcpy(reply, "Err - unknown parent");
+      } else {
+        auto region = region_map.putRegion(parts[2], parent->id);
+        if (region == NULL) {
+          strcpy(reply, "Err - unable to put");
+        } else {
+          strcpy(reply, "OK");
+        }
+      }
+    } else if (n >= 3 && strcmp(parts[1], "remove") == 0) {
+      auto region = region_map.findByName(parts[2]);
+      if (region) {
+        if (region_map.removeRegion(*region)) {
+          strcpy(reply, "OK");
+        } else {
+          strcpy(reply, "Err - not empty");
+        }
+      } else {
+        strcpy(reply, "Err - not found");
+      }
+    } else {
+      strcpy(reply, "Err - ??");
+    }
   } else{
     _cli.handleCommand(sender_timestamp, command, reply);  // common CLI commands
   }
@@ -864,4 +1095,9 @@ void MyMesh::loop() {
     acl.save(_fs);
     dirty_contacts_expiry = 0;
   }
+
+  // update uptime
+  uint32_t now = millis();
+  uptime_millis += now - last_millis;
+  last_millis = now;
 }

+ 56 - 13
examples/simple_repeater/MyMesh.h

@@ -2,7 +2,8 @@
 
 #include <Arduino.h>
 #include <Mesh.h>
-#include <helpers/CommonCLI.h>
+#include <RTClib.h>
+#include <target.h>
 
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
   #include <InternalFileSystem.h>
@@ -12,16 +13,6 @@
   #include <SPIFFS.h>
 #endif
 
-#include <helpers/ArduinoHelpers.h>
-#include <helpers/StaticPoolPacketManager.h>
-#include <helpers/SimpleMeshTables.h>
-#include <helpers/IdentityStore.h>
-#include <helpers/AdvertDataHelpers.h>
-#include <helpers/TxtDataHelpers.h>
-#include <helpers/ClientACL.h>
-#include <RTClib.h>
-#include <target.h>
-
 #ifdef WITH_RS232_BRIDGE
 #include "helpers/bridges/RS232Bridge.h"
 #define WITH_BRIDGE
@@ -32,6 +23,18 @@
 #define WITH_BRIDGE
 #endif
 
+#include <helpers/AdvertDataHelpers.h>
+#include <helpers/ArduinoHelpers.h>
+#include <helpers/ClientACL.h>
+#include <helpers/CommonCLI.h>
+#include <helpers/IdentityStore.h>
+#include <helpers/SimpleMeshTables.h>
+#include <helpers/StaticPoolPacketManager.h>
+#include <helpers/StatsFormatHelper.h>
+#include <helpers/TxtDataHelpers.h>
+#include <helpers/RegionMap.h>
+#include "RateLimiter.h"
+
 #ifdef WITH_BRIDGE
 extern AbstractBridge* bridge;
 #endif
@@ -65,11 +68,11 @@ struct NeighbourInfo {
 };
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Oct 2025"
+  #define FIRMWARE_BUILD_DATE   "13 Nov 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.9.1"
+  #define FIRMWARE_VERSION   "v1.10.0"
 #endif
 
 #define FIRMWARE_ROLE "repeater"
@@ -78,12 +81,20 @@ struct NeighbourInfo {
 
 class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   FILESYSTEM* _fs;
+  uint32_t last_millis;
+  uint64_t uptime_millis;
   unsigned long next_local_advert, next_flood_advert;
   bool _logging;
   NodePrefs _prefs;
   CommonCLI _cli;
   uint8_t reply_data[MAX_PACKET_PAYLOAD];
   ClientACL  acl;
+  TransportKeyStore key_store;
+  RegionMap region_map, temp_map;
+  RegionEntry* load_stack[8];
+  RegionEntry* recv_pkt_region;
+  RateLimiter discover_limiter;
+  bool region_load_active;
   unsigned long dirty_contacts_expiry;
 #if MAX_NEIGHBOURS
   NeighbourInfo neighbours[MAX_NEIGHBOURS];
@@ -135,12 +146,21 @@ protected:
     return _prefs.multi_acks;
   }
 
+#if ENV_INCLUDE_GPS == 1
+  void applyGpsPrefs() {
+    sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
+  }
+#endif
+
+  bool filterRecvFloodPacket(mesh::Packet* pkt) override;
+
   void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
   int searchPeersByHash(const uint8_t* hash) override;
   void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
   void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
   void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
   bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
+  void onControlDataRecv(mesh::Packet* packet) override;
 
 public:
   MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
@@ -175,6 +195,9 @@ public:
   void setTxPower(uint8_t power_dbm) override;
   void formatNeighborsReply(char *reply) override;
   void removeNeighbor(const uint8_t* pubkey, int key_len) override;
+  void formatStatsReply(char *reply) override;
+  void formatRadioStatsReply(char *reply) override;
+  void formatPacketStatsReply(char *reply) override;
 
   mesh::LocalIdentity& getSelfId() override { return self_id; }
 
@@ -182,4 +205,24 @@ public:
   void clearStats() override;
   void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
   void loop();
+
+#if defined(WITH_BRIDGE)
+  void setBridgeState(bool enable) override {
+    if (enable == bridge.isRunning()) return;
+    if (enable)
+    {
+      bridge.begin();
+    }
+    else 
+    {
+      bridge.end();
+    }
+  }
+
+  void restartBridge() override {
+    if (!bridge.isRunning()) return;
+    bridge.end();
+    bridge.begin();
+  }
+#endif
 };

+ 23 - 0
examples/simple_repeater/RateLimiter.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <stdint.h>
+
+class RateLimiter {
+  uint32_t _start_timestamp;
+  uint32_t _secs;
+  uint16_t _maximum, _count;
+
+public:
+  RateLimiter(uint16_t maximum, uint32_t secs): _maximum(maximum), _secs(secs), _start_timestamp(0), _count(0) { }
+
+  bool allow(uint32_t now) {
+    if (now < _start_timestamp + _secs) {
+      _count++;
+      if (_count > _maximum) return false;   // deny
+    } else {   // time window now expired
+      _start_timestamp = now;
+      _count = 1;
+    }
+    return true;
+  }
+};

+ 4 - 1
examples/simple_repeater/main.cpp

@@ -91,14 +91,16 @@ void loop() {
     if (c != '\n') {
       command[len++] = c;
       command[len] = 0;
+      Serial.print(c);
     }
-    Serial.print(c);
+    if (c == '\r') break;
   }
   if (len == sizeof(command)-1) {  // command buffer full
     command[sizeof(command)-1] = '\r';
   }
 
   if (len > 0 && command[len - 1] == '\r') {  // received complete line
+    Serial.print('\n');
     command[len - 1] = 0;  // replace newline with C string null terminator
     char reply[160];
     the_mesh.handleCommand(0, command, reply);  // NOTE: there is no sender_timestamp via serial!
@@ -114,4 +116,5 @@ void loop() {
 #ifdef DISPLAY_CLASS
   ui_task.loop();
 #endif
+  rtc_clock.tick();
 }

+ 41 - 11
examples/simple_room_server/MyMesh.cpp

@@ -114,11 +114,7 @@ bool MyMesh::processAck(const uint8_t *data) {
 
 mesh::Packet *MyMesh::createSelfAdvert() {
   uint8_t app_data[MAX_ADVERT_DATA_SIZE];
-  uint8_t app_data_len;
-  {
-    AdvertDataBuilder builder(ADV_TYPE_ROOM, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
-    app_data_len = builder.encodeTo(app_data);
-  }
+  uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_ROOM, app_data);
 
   return createAdvert(self_id, app_data, app_data_len);
 }
@@ -148,7 +144,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
     stats.n_packets_recv = radio_driver.getPacketsRecv();
     stats.n_packets_sent = radio_driver.getPacketsSent();
     stats.total_air_time_secs = getTotalAirTime() / 1000;
-    stats.total_up_time_secs = _ms->getMillis() / 1000;
+    stats.total_up_time_secs = uptime_millis / 1000;
     stats.n_sent_flood = getNumSentFlood();
     stats.n_sent_direct = getNumSentDirect();
     stats.n_recv_flood = getNumRecvFlood();
@@ -169,7 +165,10 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
     telemetry.reset();
     telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
     // query other sensors -- target specific
-    sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry);
+    if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {
+      perm_mask = 0x00;  // just base telemetry allowed
+    }
+    sensors.querySensors(perm_mask, telemetry);
 
     uint8_t tlen = telemetry.getSize();
     memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
@@ -266,11 +265,11 @@ 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);
-  return getRNG()->nextInt(0, 6) * t;
+  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);
-  return getRNG()->nextInt(0, 6) * t;
+  return getRNG()->nextInt(0, 5*t + 1);
 }
 
 bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
@@ -290,7 +289,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
     data[len] = 0;                                        // ensure null terminator
 
     ClientInfo* client = NULL;
-    if (data[8] == 0 && !_prefs.allow_read_only) {   // blank password, just check if sender is in ACL
+    if (data[8] == 0) {   // blank password, just check if sender is in ACL
       client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
       if (client == NULL) {
       #if MESH_DEBUG
@@ -326,6 +325,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
       client->extra.room.push_failures = 0;
 
       client->last_activity = getRTCClock()->getCurrentTime();
+      client->permissions &= ~0x03;
       client->permissions |= perm;
       memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
 
@@ -583,7 +583,9 @@ void MyMesh::onAckRecv(mesh::Packet *packet, uint32_t ack_crc) {
 MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
                mesh::RTCClock &rtc, mesh::MeshTables &tables)
     : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
+      _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) {
+  last_millis = 0;
+  uptime_millis = 0;
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
   _logging = false;
@@ -594,6 +596,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.airtime_factor = 1.0;   // one half
   _prefs.rx_delay_base = 0.0f;   // off by default, was 10.0
   _prefs.tx_delay_factor = 0.5f; // was 0.25f;
+  _prefs.direct_tx_delay_factor = 0.2f; // was zero
   StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
   _prefs.node_lat = ADVERT_LAT;
   _prefs.node_lon = ADVERT_LON;
@@ -612,6 +615,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
 #endif
 
+  // GPS defaults
+  _prefs.gps_enabled = 0;
+  _prefs.gps_interval = 0;
+  _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
+
   next_post_idx = 0;
   next_client_idx = 0;
   next_push = 0;
@@ -632,6 +640,10 @@ void MyMesh::begin(FILESYSTEM *fs) {
 
   updateAdvertTimer();
   updateFloodAdvertTimer();
+
+#if ENV_INCLUDE_GPS == 1
+  applyGpsPrefs();
+#endif
 }
 
 void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
@@ -721,6 +733,19 @@ void MyMesh::clearStats() {
   ((SimpleMeshTables *)getTables())->resetStats();
 }
 
+void MyMesh::formatStatsReply(char *reply) {
+  StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
+}
+
+void MyMesh::formatRadioStatsReply(char *reply) {
+  StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
+}
+
+void MyMesh::formatPacketStatsReply(char *reply) {
+  StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), 
+                                       getNumRecvFlood(), getNumRecvDirect());
+}
+
 void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
   while (*command == ' ')
     command++; // skip leading spaces
@@ -852,4 +877,9 @@ void MyMesh::loop() {
   }
 
   // TODO: periodically check for OLD/inactive entries in known_clients[], and evict
+
+  // update uptime
+  uint32_t now = millis();
+  uptime_millis += now - last_millis;
+  last_millis = now;
 }

+ 14 - 2
examples/simple_room_server/MyMesh.h

@@ -18,6 +18,7 @@
 #include <helpers/AdvertDataHelpers.h>
 #include <helpers/TxtDataHelpers.h>
 #include <helpers/CommonCLI.h>
+#include <helpers/StatsFormatHelper.h>
 #include <helpers/ClientACL.h>
 #include <RTClib.h>
 #include <target.h>
@@ -25,11 +26,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Oct 2025"
+  #define FIRMWARE_BUILD_DATE   "13 Nov 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.9.1"
+  #define FIRMWARE_VERSION   "v1.10.0"
 #endif
 
 #ifndef LORA_FREQ
@@ -88,6 +89,8 @@ struct PostInfo {
 
 class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   FILESYSTEM* _fs;
+  uint32_t last_millis;
+  uint64_t uptime_millis;
   unsigned long next_local_advert, next_flood_advert;
   bool _logging;
   NodePrefs _prefs;
@@ -149,6 +152,12 @@ protected:
   bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
   void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
 
+#if ENV_INCLUDE_GPS == 1
+  void applyGpsPrefs() {
+    sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
+  }
+#endif
+
 public:
   MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
 
@@ -184,6 +193,9 @@ public:
   void formatNeighborsReply(char *reply) override {
     strcpy(reply, "not supported");
   }
+  void formatStatsReply(char *reply) override;
+  void formatRadioStatsReply(char *reply) override;
+  void formatPacketStatsReply(char *reply) override;
 
   mesh::LocalIdentity& getSelfId() override { return self_id; }
 

+ 1 - 0
examples/simple_room_server/main.cpp

@@ -110,4 +110,5 @@ void loop() {
 #ifdef DISPLAY_CLASS
   ui_task.loop();
 #endif
+  rtc_clock.tick();
 }

+ 2 - 1
examples/simple_secure_chat/main.cpp

@@ -548,7 +548,7 @@ public:
 
 StdRNG fast_rng;
 SimpleMeshTables tables;
-MyMesh the_mesh(radio_driver, fast_rng, *new VolatileRTCClock(), tables); // TODO: test with 'rtc_clock' in target.cpp
+MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables);
 
 void halt() {
   while (1) ;
@@ -587,4 +587,5 @@ void setup() {
 
 void loop() {
   the_mesh.loop();
+  rtc_clock.tick();
 }

+ 66 - 7
examples/simple_sensor/SensorMesh.cpp

@@ -239,11 +239,7 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
 
 mesh::Packet* SensorMesh::createSelfAdvert() {
   uint8_t app_data[MAX_ADVERT_DATA_SIZE];
-  uint8_t app_data_len;
-  {
-    AdvertDataBuilder builder(ADV_TYPE_SENSOR, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
-    app_data_len = builder.encodeTo(app_data);
-  }
+  uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_SENSOR, app_data);
 
   return createAdvert(self_id, app_data, app_data_len);
 }
@@ -453,7 +449,14 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con
     memcpy(&timestamp, data, 4);
 
     data[len] = 0;  // ensure null terminator
-    uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
+    uint8_t reply_len;
+    if (data[4] == 0 || data[4] >= ' ') {   // is password, ie. a login request
+      reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
+    //} else if (data[4] == ANON_REQ_TYPE_*) {   // future type codes
+      // TODO
+    } else {
+      reply_len = 0;  // unknown request type
+    }
 
     if (reply_len == 0) return;   // invalid request
 
@@ -614,6 +617,39 @@ bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t
   return false;
 }
 
+#define CTL_TYPE_NODE_DISCOVER_REQ   0x80
+#define CTL_TYPE_NODE_DISCOVER_RESP  0x90
+
+void SensorMesh::onControlDataRecv(mesh::Packet* packet) {
+  uint8_t type = packet->payload[0] & 0xF0;    // just test upper 4 bits
+  if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6) {
+    // TODO: apply rate limiting to these!
+    int i = 1;
+    uint8_t  filter = packet->payload[i++];
+    uint32_t tag;
+    memcpy(&tag, &packet->payload[i], 4); i += 4;
+    uint32_t since;
+    if (packet->payload_len >= i+4) {   // optional since field
+      memcpy(&since, &packet->payload[i], 4); i += 4;
+    } else {
+      since = 0;
+    }
+
+    if ((filter & (1 << ADV_TYPE_SENSOR)) != 0 && _prefs.discovery_mod_timestamp >= since) {
+      bool prefix_only = packet->payload[0] & 1;
+      uint8_t data[6 + PUB_KEY_SIZE];
+      data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_SENSOR;   // low 4-bits for node type
+      data[1] = packet->_snr;   // let sender know the inbound SNR ( x 4)
+      memcpy(&data[2], &tag, 4);     // include tag from request, for client to match to
+      memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
+      auto resp = createControlData(data,  prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
+      if (resp) {
+        sendZeroHop(resp, getRetransmitDelay(resp)*4);  // apply random delay (widened x4), as multiple nodes can respond to this
+      }
+    }
+  }
+}
+
 bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
   int i = matching_peer_indexes[sender_idx];
   if (i < 0 || i >= acl.getNumClients()) {
@@ -655,7 +691,7 @@ void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
 
 SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
      : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
-      _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
+      _cli(board, rtc, sensors, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4)
 {
   next_local_advert = next_flood_advert = 0;
   dirty_contacts_expiry = 0;
@@ -668,6 +704,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
   _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
   StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
   _prefs.node_lat = ADVERT_LAT;
   _prefs.node_lon = ADVERT_LON;
@@ -682,6 +719,11 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
   _prefs.disable_fwd = true;
   _prefs.flood_max = 64;
   _prefs.interference_threshold = 0;  // disabled
+
+  // GPS defaults
+  _prefs.gps_enabled = 0;
+  _prefs.gps_interval = 0;
+  _prefs.advert_loc_policy = ADVERT_LOC_PREFS;
 }
 
 void SensorMesh::begin(FILESYSTEM* fs) {
@@ -697,6 +739,10 @@ void SensorMesh::begin(FILESYSTEM* fs) {
 
   updateAdvertTimer();
   updateFloodAdvertTimer();
+
+#if ENV_INCLUDE_GPS == 1
+  applyGpsPrefs();
+#endif
 }
 
 bool SensorMesh::formatFileSystem() {
@@ -764,6 +810,19 @@ void SensorMesh::setTxPower(uint8_t power_dbm) {
   radio_set_tx_power(power_dbm);
 }
 
+void SensorMesh::formatStatsReply(char *reply) {
+  StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
+}
+
+void SensorMesh::formatRadioStatsReply(char *reply) {
+  StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
+}
+
+void SensorMesh::formatPacketStatsReply(char *reply) {
+  StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), 
+                                       getNumRecvFlood(), getNumRecvDirect());
+}
+
 float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
   auto buf = telemetry.getBuffer();
   uint8_t size = telemetry.getSize();

+ 12 - 2
examples/simple_sensor/SensorMesh.h

@@ -20,6 +20,7 @@
 #include <helpers/AdvertDataHelpers.h>
 #include <helpers/TxtDataHelpers.h>
 #include <helpers/CommonCLI.h>
+#include <helpers/StatsFormatHelper.h>
 #include <helpers/ClientACL.h>
 #include <RTClib.h>
 #include <target.h>
@@ -32,11 +33,11 @@
 #define PERM_RECV_ALERTS_HI    (1 << 7)   // high priority alerts
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Oct 2025"
+  #define FIRMWARE_BUILD_DATE   "13 Nov 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.9.1"
+  #define FIRMWARE_VERSION   "v1.10.0"
 #endif
 
 #define FIRMWARE_ROLE "sensor"
@@ -69,6 +70,9 @@ public:
   void formatNeighborsReply(char *reply) override {
     strcpy(reply, "not supported");
   }
+  void formatStatsReply(char *reply) override;
+  void formatRadioStatsReply(char *reply) override;
+  void formatPacketStatsReply(char *reply) override;
   mesh::LocalIdentity& getSelfId() override { return self_id; }
   void saveIdentity(const mesh::LocalIdentity& new_id) override;
   void clearStats() override { }
@@ -121,6 +125,7 @@ protected:
   void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
   void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
   bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
+  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, uint flags, size_t len);
   void sendAckTo(const ClientInfo& dest, uint32_t ack_hash);
@@ -149,4 +154,9 @@ private:
 
   void sendAlert(const ClientInfo* c, Trigger* t);
 
+  #if ENV_INCLUDE_GPS == 1
+  void applyGpsPrefs() {
+    sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
+  }
+#endif
 };

+ 1 - 0
examples/simple_sensor/main.cpp

@@ -144,4 +144,5 @@ void loop() {
 #ifdef DISPLAY_CLASS
   ui_task.loop();
 #endif
+  rtc_clock.tick();
 }

+ 3 - 3
library.json

@@ -1,14 +1,14 @@
 {
     "name": "MeshCore",
-    "version" : "1.8.0",
+    "version" : "1.10.0",
     "dependencies": {
         "SPI": "*",
         "Wire": "*",
-        "jgromes/RadioLib": "^7.1.2",
+        "jgromes/RadioLib": "^7.3.0",
         "rweather/Crypto": "^0.4.0",
         "adafruit/RTClib": "^2.1.3",
         "melopero/Melopero RV3028": "^1.1.0",
-        "electroniccats/CayenneLPP": "1.4.0"
+        "electroniccats/CayenneLPP": "1.6.1"
     },
     "build": {
         "extraScript": "build_as_lib.py"

+ 10 - 2
platformio.ini

@@ -18,11 +18,11 @@ monitor_speed = 115200
 lib_deps =
   SPI
   Wire
-  jgromes/RadioLib @ ^7.1.2
+  jgromes/RadioLib @ ^7.3.0
   rweather/Crypto @ ^0.4.0
   adafruit/RTClib @ ^2.1.3
   melopero/Melopero RV3028 @ ^1.1.0
-  electroniccats/CayenneLPP @ 1.4.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
@@ -67,6 +67,7 @@ lib_deps =
   file://arch/esp32/AsyncElegantOTA
 
 ; esp32c6 uses arduino framework 3.x
+; WARNING: experimental. pioarduino on esp32c6 needs work - it's not considered stable and has issues.
 [esp32c6_base]
 extends = esp32_base
 platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip
@@ -76,6 +77,9 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
 [nrf52_base]
 extends = arduino_base
 platform = nordicnrf52
+platform_packages =
+  framework-arduinoadafruitnrf52 @ 1.10700.0
+extra_scripts = create-uf2.py
 build_flags = ${arduino_base.build_flags}
   -D NRF52_PLATFORM
   -D LFS_NO_ASSERT=1
@@ -124,6 +128,8 @@ build_flags =
   -D ENV_INCLUDE_INA260=1
   -D ENV_INCLUDE_MLX90614=1
   -D ENV_INCLUDE_VL53L0X=1
+  -D ENV_INCLUDE_BME680=1
+  -D ENV_INCLUDE_BMP085=1
 lib_deps =
   adafruit/Adafruit INA3221 Library @ ^1.0.1
   adafruit/Adafruit INA219 @ ^1.2.3
@@ -138,3 +144,5 @@ lib_deps =
   adafruit/Adafruit MLX90614 Library @ ^2.1.5
   adafruit/Adafruit_VL53L0X @ ^1.2.4
   stevemarple/MicroNMEA @ ^2.0.6
+  adafruit/Adafruit BME680 Library @ ^2.0.4
+  adafruit/Adafruit BMP085 Library @ ^1.2.4

+ 67 - 3
src/Mesh.cpp

@@ -68,6 +68,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
     return ACTION_RELEASE;
   }
 
+  if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
+    if (pkt->path_len == 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 (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
       if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
@@ -90,6 +98,8 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
     return ACTION_RELEASE;   // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
   }
 
+  if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE;
+
   DispatcherAction action = ACTION_RELEASE;
 
   switch (pkt->getPayloadType()) {
@@ -201,9 +211,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
       if (i + 2 >= pkt->payload_len) {
         MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
       } else if (!_tables->hasSeen(pkt)) {
-        // scan channels DB, for all matching hashes of 'channel_hash' (max 2 matches supported ATM)
-        GroupChannel channels[2];
-        int num = searchChannelsByHash(&channel_hash, channels, 2);
+        // scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM)
+        GroupChannel channels[4];
+        int num = searchChannelsByHash(&channel_hash, channels, 4);
         // for each matching channel, try to decrypt data
         for (int j = 0; j < num; j++) {
           // decrypt, checking MAC is valid
@@ -587,6 +597,22 @@ Packet* Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) {
   return packet;
 }
 
+Packet* Mesh::createControlData(const uint8_t* data, size_t len) {
+  if (len > sizeof(Packet::payload)) return NULL;  // invalid arg
+
+  Packet* packet = obtainNewPacket();
+  if (packet == NULL) {
+    MESH_DEBUG_PRINTLN("%s Mesh::createControlData(): error, packet pool empty", getLogDateTime());
+    return NULL;
+  }
+  packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT);  // ROUTE_TYPE_* set later
+
+  memcpy(packet->payload, data, len);
+  packet->payload_len = len;
+
+  return packet;
+}
+
 void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
   if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
     MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
@@ -610,6 +636,31 @@ 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) {
+  if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
+    MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", 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;
+
+  _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
+
+  uint8_t pri;
+  if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
+    pri = 2;
+  } else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
+    pri = 3;   // de-prioritie these
+  } else {
+    pri = 1;
+  }
+  sendPacket(packet, pri, delay_millis);
+}
+
 void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) {
   packet->header &= ~PH_ROUTE_MASK;
   packet->header |= ROUTE_TYPE_DIRECT;
@@ -645,4 +696,17 @@ void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) {
   sendPacket(packet, 0, delay_millis);
 }
 
+void Mesh::sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) {
+  packet->header &= ~PH_ROUTE_MASK;
+  packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT;
+  packet->transport_codes[0] = transport_codes[0];
+  packet->transport_codes[1] = transport_codes[1];
+
+  packet->path_len = 0;  // path_len of zero means Zero Hop
+
+  _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
+
+  sendPacket(packet, 0, delay_millis);
+}
+
 }

+ 24 - 0
src/Mesh.h

@@ -43,6 +43,12 @@ protected:
    */
   DispatcherAction routeRecvPacket(Packet* packet);
 
+  /**
+   * \brief    Called _before_ the packet is dispatched to the on..Recv() methods.
+   * \returns  true, if given packet should be NOT be processed.
+   */
+  virtual bool filterRecvFloodPacket(Packet* packet) { return false; }
+
   /**
    * \brief  Check whether this packet should be forwarded (re-transmitted) or not.
    *     Is sub-classes responsibility to make sure given packet is only transmitted ONCE (by this node)
@@ -128,6 +134,11 @@ protected:
   */
   virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { }
 
+  /**
+   * \brief  A control packet has been received.
+  */
+  virtual void onControlDataRecv(Packet* packet) { }
+
   /**
    * \brief  A packet with PAYLOAD_TYPE_RAW_CUSTOM has been received.
   */
@@ -180,12 +191,19 @@ public:
   Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
   Packet* createRawData(const uint8_t* data, size_t len);
   Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0);
+  Packet* createControlData(const uint8_t* data, size_t len);
 
   /**
    * \brief  send a locally-generated Packet with flood routing
   */
   void sendFlood(Packet* packet, uint32_t delay_millis=0);
 
+  /**
+   * \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);
+
   /**
    * \brief  send a locally-generated Packet with Direct routing
   */
@@ -196,6 +214,12 @@ public:
   */
   void sendZeroHop(Packet* packet, uint32_t delay_millis=0);
 
+  /**
+   * \brief  send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes
+   * \param transport_codes   array of 2 codes to attach to packet
+  */
+  void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0);
+
 };
 
 }

+ 11 - 0
src/MeshCore.h

@@ -28,6 +28,12 @@
   #define MESH_DEBUG_PRINTLN(...) {}
 #endif
 
+#if BRIDGE_DEBUG && ARDUINO
+#define BRIDGE_DEBUG_PRINTLN(F, ...) Serial.printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__)
+#else
+#define BRIDGE_DEBUG_PRINTLN(...) {}
+#endif
+
 namespace mesh {
 
 #define  BD_STARTUP_NORMAL     0  // getStartupReason() codes
@@ -66,6 +72,11 @@ public:
   */
   virtual void setCurrentTime(uint32_t time) = 0;
 
+  /**
+   * override in classes that need to periodically update internal state
+   */
+  virtual void tick() { /* no op */}
+
   uint32_t getCurrentTimeUnique() {
     uint32_t t = getCurrentTime();
     if (t <= last_unique) {

+ 1 - 0
src/Packet.h

@@ -27,6 +27,7 @@ namespace mesh {
 #define PAYLOAD_TYPE_PATH        0x08    // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
 #define PAYLOAD_TYPE_TRACE       0x09    // trace a path, collecting SNI for each hop
 #define PAYLOAD_TYPE_MULTIPART   0x0A    // packet is one of a set of packets
+#define PAYLOAD_TYPE_CONTROL     0x0B    // a control/discovery packet
 //...
 #define PAYLOAD_TYPE_RAW_CUSTOM   0x0F    // custom packet as raw bytes, for applications with custom encryption, payloads, etc
 

+ 15 - 3
src/helpers/AbstractBridge.h

@@ -11,6 +11,18 @@ public:
    */
   virtual void begin() = 0;
 
+  /**
+   * @brief Stops the bridge.
+   */
+  virtual void end() = 0;
+
+  /**
+   * @brief Gets the current state of the bridge.
+   *
+   * @return true if the bridge is initialized and running, false otherwise.
+   */
+  virtual bool isRunning() const = 0;
+
   /**
    * @brief A method to be called on every main loop iteration.
    *        Used for tasks like checking for incoming data.
@@ -20,14 +32,14 @@ public:
   /**
    * @brief A callback that is triggered when the mesh transmits a packet.
    *        The bridge can use this to forward the packet.
-   * 
+   *
    * @param packet The packet that was transmitted.
    */
-  virtual void onPacketTransmitted(mesh::Packet* packet) = 0;
+  virtual void sendPacket(mesh::Packet* packet) = 0;
 
   /**
    * @brief Processes a received packet from the bridge's medium.
-   * 
+   *
    * @param packet The packet that was received.
    */
   virtual void onPacketReceived(mesh::Packet* packet) = 0;

+ 12 - 4
src/helpers/ArduinoHelpers.h

@@ -4,11 +4,19 @@
 #include <Arduino.h>
 
 class VolatileRTCClock : public mesh::RTCClock {
-  long millis_offset;
+  uint32_t base_time;
+  uint64_t accumulator;
+  unsigned long prev_millis;
 public:
-  VolatileRTCClock() { millis_offset = 1715770351; } // 15 May 2024, 8:50pm
-  uint32_t getCurrentTime() override { return (millis()/1000 + millis_offset); }
-  void setCurrentTime(uint32_t time) override { millis_offset = time - millis()/1000; }
+  VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); }   // 15 May 2024, 8:50pm
+  uint32_t getCurrentTime() override { return base_time + accumulator/1000; }
+  void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); }
+
+  void tick() override {
+    unsigned long now = millis();
+    accumulator += (now - prev_millis);
+    prev_millis = now;
+  }
 };
 
 class ArduinoMillis : public mesh::MillisecondClock {

+ 4 - 0
src/helpers/AutoDiscoverRTCClock.h

@@ -14,4 +14,8 @@ public:
   void begin(TwoWire& wire);
   uint32_t getCurrentTime() override;
   void setCurrentTime(uint32_t time) override;
+
+  void tick() override {
+    _fallback->tick();   // is typically VolatileRTCClock, which now needs tick()
+  }
 };

+ 31 - 15
src/helpers/BaseChatMesh.cpp

@@ -9,6 +9,13 @@
   #define TXT_ACK_DELAY     200
 #endif
 
+void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
+  sendFlood(pkt, delay_millis);
+}
+void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
+  sendFlood(pkt, delay_millis);
+}
+
 mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
   uint8_t app_data[MAX_ADVERT_DATA_SIZE];
   uint8_t app_data_len;
@@ -34,7 +41,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) {
     mesh::Packet* ack = createAck(ack_hash);
-    if (ack) sendFlood(ack, TXT_ACK_DELAY);
+    if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
   } else {
     uint32_t d = TXT_ACK_DELAY;
     if (getExtraAckTransmitCount() > 0) {
@@ -68,9 +75,16 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
   }
 
   // save a copy of raw advert packet (to support "Share..." function)
-  int plen = packet->writeTo(temp_buf);
+  int plen;
+  {
+    uint8_t save = packet->header;
+    packet->header &= ~PH_ROUTE_MASK;
+    packet->header |= ROUTE_TYPE_FLOOD;   // make sure transport codes are NOT saved
+    plen = packet->writeTo(temp_buf);
+    packet->header = save;
+  }
   putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
-  
+
   bool is_new = false;
   if (from == NULL) {
     if (!isAutoAddEnabled()) {
@@ -168,7 +182,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
         // 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) sendFloodScoped(from, path, TXT_ACK_DELAY);
       } else {
         sendAckTo(from, ack_hash);
       }
@@ -179,7 +193,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
       if (packet->isRouteFlood()) {
         // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
         mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
-        if (path) sendFlood(path);
+        if (path) sendFloodScoped(from, path);
       }
     } else if (flags == TXT_TYPE_SIGNED_PLAIN) {
       if (timestamp > from.sync_since) {  // make sure 'sync_since' is up-to-date
@@ -195,7 +209,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
         // 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) sendFloodScoped(from, path, TXT_ACK_DELAY);
       } else {
         sendAckTo(from, ack_hash);
       }
@@ -211,14 +225,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
         // 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, temp_buf, reply_len);
-        if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+        if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY);
       } 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
             sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
           } else {
-            sendFlood(reply, SERVER_RESPONSE_DELAY);
+            sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
           }
         }
       }
@@ -339,7 +353,7 @@ int  BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
 
   int rc;
   if (recipient.out_path_len < 0) {
-    sendFlood(pkt);
+    sendFloodScoped(recipient, pkt);
     txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
     rc = MSG_SEND_SENT_FLOOD;
   } else {
@@ -365,7 +379,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) {
-    sendFlood(pkt);
+    sendFloodScoped(recipient, pkt);
     txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
     rc = MSG_SEND_SENT_FLOOD;
   } else {
@@ -391,7 +405,7 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan
 
   auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len);
   if (pkt) {
-    sendFlood(pkt);
+    sendFloodScoped(channel, pkt);
     return true;
   }
   return false;
@@ -405,7 +419,9 @@ bool BaseChatMesh::shareContactZeroHop(const ContactInfo& contact) {
   if (packet == NULL) return false;  // no Packets available
 
   packet->readFrom(temp_buf, plen);  // restore Packet from 'blob'
-  sendZeroHop(packet);
+  uint16_t codes[2];
+  codes[0] = codes[1] = 0;   // { 0, 0 } means 'send this nowhere'
+  sendZeroHop(packet, codes);
   return true;  // success
 }
 
@@ -451,7 +467,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) {
-      sendFlood(pkt);
+      sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
     } else {
@@ -478,7 +494,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) {
-      sendFlood(pkt);
+      sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
     } else {
@@ -505,7 +521,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) {
-      sendFlood(pkt);
+      sendFloodScoped(recipient, pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
       return MSG_SEND_SENT_FLOOD;
     } else {

+ 3 - 0
src/helpers/BaseChatMesh.h

@@ -107,6 +107,9 @@ protected:
   virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
   virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
 
+  virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0);
+  virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0);
+
   // storage concepts, for sub-classes to override/implement
   virtual int  getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; }  // not implemented
   virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; }

+ 313 - 55
src/helpers/CommonCLI.cpp

@@ -1,6 +1,7 @@
 #include <Arduino.h>
 #include "CommonCLI.h"
 #include "TxtDataHelpers.h"
+#include "AdvertDataHelpers.h"
 #include <RTClib.h>
 
 // Believe it or not, this std C function is busted on some platforms!
@@ -32,32 +33,44 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
   if (file) {
     uint8_t pad[8];
 
-    file.read((uint8_t *) &_prefs->airtime_factor, sizeof(_prefs->airtime_factor));  // 0
-    file.read((uint8_t *) &_prefs->node_name, sizeof(_prefs->node_name));  // 4
-    file.read(pad, 4);   // 36
-    file.read((uint8_t *) &_prefs->node_lat, sizeof(_prefs->node_lat));  // 40
-    file.read((uint8_t *) &_prefs->node_lon, sizeof(_prefs->node_lon));  // 48
-    file.read((uint8_t *) &_prefs->password[0], sizeof(_prefs->password));  // 56
-    file.read((uint8_t *) &_prefs->freq, sizeof(_prefs->freq));   // 72
-    file.read((uint8_t *) &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm));  // 76
-    file.read((uint8_t *) &_prefs->disable_fwd, sizeof(_prefs->disable_fwd));  // 77
-    file.read((uint8_t *) &_prefs->advert_interval, sizeof(_prefs->advert_interval));  // 78
-    file.read((uint8_t *) pad, 1);  // 79  was 'unused'
-    file.read((uint8_t *) &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base));  // 80
-    file.read((uint8_t *) &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor));  // 84
-    file.read((uint8_t *) &_prefs->guest_password[0], sizeof(_prefs->guest_password));  // 88
-    file.read((uint8_t *) &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor));  // 104
-    file.read(pad, 4);   // 108
-    file.read((uint8_t *) &_prefs->sf, sizeof(_prefs->sf));  // 112
-    file.read((uint8_t *) &_prefs->cr, sizeof(_prefs->cr));  // 113
-    file.read((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only));  // 114
-    file.read((uint8_t *) &_prefs->multi_acks, sizeof(_prefs->multi_acks));  // 115
-    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(pad, 3);   // 121
-    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
+    file.read((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor));    // 0
+    file.read((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name));              // 4
+    file.read(pad, 4);                                                                // 36
+    file.read((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat));                // 40
+    file.read((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon));                // 48
+    file.read((uint8_t *)&_prefs->password[0], sizeof(_prefs->password));             // 56
+    file.read((uint8_t *)&_prefs->freq, sizeof(_prefs->freq));                        // 72
+    file.read((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm));        // 76
+    file.read((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd));          // 77
+    file.read((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval));  // 78
+    file.read((uint8_t *)pad, 1);                                                     // 79  was 'unused'
+    file.read((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base));      // 80
+    file.read((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor));  // 84
+    file.read((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
+    file.read((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
+    file.read(pad, 4);                                                                             // 108
+    file.read((uint8_t *)&_prefs->sf, sizeof(_prefs->sf));                                         // 112
+    file.read((uint8_t *)&_prefs->cr, sizeof(_prefs->cr));                                         // 113
+    file.read((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only));               // 114
+    file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks));                         // 115
+    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(pad, 3);                                                                             // 121
+    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
+    file.read((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled));                 // 127
+    file.read((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay));                     // 128
+    file.read((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src));                 // 130
+    file.read((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud));                       // 131
+    file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel));                 // 135
+    file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret));                   // 136
+    file.read(pad, 4);                                                                             // 152
+    file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled));                       // 156
+    file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval));                     // 157
+    file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy));          // 161
+    file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
+    // 166
 
     // sanitise bad pref values
     _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -65,12 +78,22 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     _prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0, 2.0f);
     _prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 9.0f);
     _prefs->freq = constrain(_prefs->freq, 400.0f, 2500.0f);
-    _prefs->bw = constrain(_prefs->bw, 62.5f, 500.0f);
-    _prefs->sf = constrain(_prefs->sf, 7, 12);
+    _prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
+    _prefs->sf = constrain(_prefs->sf, 5, 12);
     _prefs->cr = constrain(_prefs->cr, 5, 8);
     _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30);
     _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1);
 
+    // sanitise bad bridge pref values
+    _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
+    _prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000);
+    _prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
+    _prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, 115200);
+    _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
+
+    _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
+    _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
+
     file.close();
   }
 }
@@ -88,32 +111,44 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
     uint8_t pad[8];
     memset(pad, 0, sizeof(pad));
 
-    file.write((uint8_t *) &_prefs->airtime_factor, sizeof(_prefs->airtime_factor));  // 0
-    file.write((uint8_t *) &_prefs->node_name, sizeof(_prefs->node_name));  // 4
-    file.write(pad, 4);   // 36
-    file.write((uint8_t *) &_prefs->node_lat, sizeof(_prefs->node_lat));  // 40
-    file.write((uint8_t *) &_prefs->node_lon, sizeof(_prefs->node_lon));  // 48
-    file.write((uint8_t *) &_prefs->password[0], sizeof(_prefs->password));  // 56
-    file.write((uint8_t *) &_prefs->freq, sizeof(_prefs->freq));   // 72
-    file.write((uint8_t *) &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm));  // 76
-    file.write((uint8_t *) &_prefs->disable_fwd, sizeof(_prefs->disable_fwd));  // 77
-    file.write((uint8_t *) &_prefs->advert_interval, sizeof(_prefs->advert_interval));  // 78
-    file.write((uint8_t *) pad, 1);  // 79  was 'unused'
-    file.write((uint8_t *) &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base));  // 80
-    file.write((uint8_t *) &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor));  // 84
-    file.write((uint8_t *) &_prefs->guest_password[0], sizeof(_prefs->guest_password));  // 88
-    file.write((uint8_t *) &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor));  // 104
-    file.write(pad, 4);   // 108
-    file.write((uint8_t *) &_prefs->sf, sizeof(_prefs->sf));  // 112
-    file.write((uint8_t *) &_prefs->cr, sizeof(_prefs->cr));  // 113
-    file.write((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only));  // 114
-    file.write((uint8_t *) &_prefs->multi_acks, sizeof(_prefs->multi_acks));  // 115
-    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(pad, 3);   // 121
-    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
+    file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor));    // 0
+    file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name));              // 4
+    file.write(pad, 4);                                                                // 36
+    file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat));                // 40
+    file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon));                // 48
+    file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password));             // 56
+    file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq));                        // 72
+    file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm));        // 76
+    file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd));          // 77
+    file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval));  // 78
+    file.write((uint8_t *)pad, 1);                                                     // 79  was 'unused'
+    file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base));      // 80
+    file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor));  // 84
+    file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
+    file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
+    file.write(pad, 4);                                                                             // 108
+    file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf));                                         // 112
+    file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr));                                         // 113
+    file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only));               // 114
+    file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks));                         // 115
+    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(pad, 3);                                                                             // 121
+    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
+    file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled));                 // 127
+    file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay));                     // 128
+    file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src));                 // 130
+    file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud));                       // 131
+    file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel));                 // 135
+    file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret));                   // 136
+    file.write(pad, 4);                                                                             // 152
+    file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled));                       // 156
+    file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval));                     // 157
+    file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy));           // 161
+    file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
+    // 166
 
     file.close();
   }
@@ -128,6 +163,19 @@ void CommonCLI::savePrefs() {
   _callbacks->savePrefs();
 }
 
+uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
+  if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) {
+    AdvertDataBuilder builder(node_type, _prefs->node_name);
+    return builder.encodeTo(app_data);
+  } else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) {
+    AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon);
+    return builder.encodeTo(app_data);
+  } else {
+    AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon);
+    return builder.encodeTo(app_data);
+  }
+}
+
 void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
     if (memcmp(command, "reboot", 6) == 0) {
       _board->reboot();  // doesn't return
@@ -199,6 +247,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
     } else if (memcmp(command, "clear stats", 11) == 0) {
       _callbacks->clearStats();
       strcpy(reply, "(OK - stats reset)");
+    /*
+     * GET commands
+     */
     } else if (memcmp(command, "get ", 4) == 0) {
       const char* config = &command[4];
       if (memcmp(config, "af", 2) == 0) {
@@ -252,9 +303,40 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         mesh::Utils::toHex(&reply[2], _callbacks->getSelfId().pub_key, PUB_KEY_SIZE);
       } else if (memcmp(config, "role", 4) == 0) {
         sprintf(reply, "> %s", _callbacks->getRole());
+      } else if (memcmp(config, "bridge.type", 11) == 0) {
+        sprintf(reply, "> %s",
+#ifdef WITH_RS232_BRIDGE
+                "rs232"
+#elif WITH_ESPNOW_BRIDGE
+                "espnow"
+#else
+                "none"
+#endif
+        );
+#ifdef WITH_BRIDGE
+      } else if (memcmp(config, "bridge.enabled", 14) == 0) {
+        sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
+      } else if (memcmp(config, "bridge.delay", 12) == 0) {
+        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
+      } else if (memcmp(config, "bridge.source", 13) == 0) {
+        sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "logRx" : "logTx");
+#endif
+#ifdef WITH_RS232_BRIDGE
+      } else if (memcmp(config, "bridge.baud", 11) == 0) {
+        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
+#endif
+#ifdef WITH_ESPNOW_BRIDGE
+      } else if (memcmp(config, "bridge.channel", 14) == 0) {
+        sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
+      } else if (memcmp(config, "bridge.secret", 13) == 0) {
+        sprintf(reply, "> %s", _prefs->bridge_secret);
+#endif
       } else {
         sprintf(reply, "??: %s", config);
       }
+    /*
+     * SET commands
+     */
     } else if (memcmp(command, "set ", 4) == 0) {
       const char* config = &command[4];
       if (memcmp(config, "af ", 3) == 0) {
@@ -301,7 +383,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         StrHelper::strncpy(_prefs->guest_password, &config[15], sizeof(_prefs->guest_password));
         savePrefs();
         strcpy(reply, "OK");
-      } else if (sender_timestamp == 0 && memcmp(config, "prv.key ", 8) == 0) {  // from serial command line only
+      } else if (sender_timestamp == 0 &&
+                 memcmp(config, "prv.key ", 8) == 0) { // from serial command line only
         uint8_t prv_key[PRV_KEY_SIZE];
         bool success = mesh::Utils::fromHex(prv_key, PRV_KEY_SIZE, &config[8]);
         if (success) {
@@ -391,6 +474,55 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         _prefs->freq = atof(&config[5]);
         savePrefs();
         strcpy(reply, "OK - reboot to apply");
+#ifdef WITH_BRIDGE
+      } else if (memcmp(config, "bridge.enabled ", 15) == 0) {
+        _prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
+        _callbacks->setBridgeState(_prefs->bridge_enabled);
+        savePrefs();
+        strcpy(reply, "OK");
+      } else if (memcmp(config, "bridge.delay ", 13) == 0) {
+        int delay = _atoi(&config[13]);
+        if (delay >= 0 && delay <= 10000) {
+          _prefs->bridge_delay = (uint16_t)delay;
+          savePrefs();
+          strcpy(reply, "OK");
+        } else {
+          strcpy(reply, "Error: delay must be between 0-10000 ms");
+        }
+      } else if (memcmp(config, "bridge.source ", 14) == 0) {
+        _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0;
+        savePrefs();
+        strcpy(reply, "OK");
+#endif
+#ifdef WITH_RS232_BRIDGE
+      } else if (memcmp(config, "bridge.baud ", 12) == 0) {
+        uint32_t baud = atoi(&config[12]);
+        if (baud >= 9600 && baud <= 115200) {
+          _prefs->bridge_baud = (uint32_t)baud;
+          _callbacks->restartBridge();
+          savePrefs();
+          strcpy(reply, "OK");
+        } else {
+          strcpy(reply, "Error: baud rate must be between 9600-115200");
+        }
+#endif
+#ifdef WITH_ESPNOW_BRIDGE
+      } else if (memcmp(config, "bridge.channel ", 15) == 0) {
+        int ch = atoi(&config[15]);
+        if (ch > 0 && ch < 15) {
+          _prefs->bridge_channel = (uint8_t)ch;
+          _callbacks->restartBridge();
+          savePrefs();
+          strcpy(reply, "OK");
+        } else {
+          strcpy(reply, "Error: channel must be between 1-14");
+        }
+      } else if (memcmp(config, "bridge.secret ", 14) == 0) {
+        StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
+        _callbacks->restartBridge();
+        savePrefs();
+        strcpy(reply, "OK");
+#endif
       } else {
         sprintf(reply, "unknown config: %s", config);
       }
@@ -401,6 +533,126 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate());
     } else if (memcmp(command, "board", 5) == 0) {
       sprintf(reply, "%s", _board->getManufacturerName());
+    } else if (memcmp(command, "sensor get ", 11) == 0) {
+      const char* key = command + 11;
+      const char* val = _sensors->getSettingByKey(key);
+      if (val != NULL) {
+        sprintf(reply, "> %s", val);
+      } else {
+        strcpy(reply, "null");
+      }
+    } else if (memcmp(command, "sensor set ", 11) == 0) {
+      strcpy(tmp, &command[11]);
+      const char *parts[2]; 
+      int num = mesh::Utils::parseTextParts(tmp, parts, 2, ' ');
+      const char *key = (num > 0) ? parts[0] : "";
+      const char *value = (num > 1) ? parts[1] : "null";
+      if (_sensors->setSettingValue(key, value)) {
+        strcpy(reply, "ok");
+      } else {
+        strcpy(reply, "can't find custom var");
+      }
+    } else if (memcmp(command, "sensor list", 11) == 0) {
+      char* dp = reply;
+      int start = 0;
+      int end = _sensors->getNumSettings();
+      if (strlen(command) > 11) {
+        start = _atoi(command+12);
+      }
+      if (start >= end) {
+        strcpy(reply, "no custom var");
+      } else {
+        sprintf(dp, "%d vars\n", end);
+        dp = strchr(dp, 0);
+        int i;
+        for (i = start; i < end && (dp-reply < 134); i++) {
+          sprintf(dp, "%s=%s\n", 
+            _sensors->getSettingName(i),
+            _sensors->getSettingValue(i));
+          dp = strchr(dp, 0);
+        }
+        if (i < end) {
+          sprintf(dp, "... next:%d", i);
+        } else {
+          *(dp-1) = 0; // remove last CR
+        }
+      }
+#if ENV_INCLUDE_GPS == 1
+    } else if (memcmp(command, "gps on", 6) == 0) {
+      if (_sensors->setSettingValue("gps", "1")) {
+        _prefs->gps_enabled = 1;
+        savePrefs();
+        strcpy(reply, "ok");
+      } else {
+        strcpy(reply, "gps toggle not found");
+      }
+    } else if (memcmp(command, "gps off", 7) == 0) {
+      if (_sensors->setSettingValue("gps", "0")) {
+        _prefs->gps_enabled = 0;
+        savePrefs();
+        strcpy(reply, "ok");
+      } else {
+        strcpy(reply, "gps toggle not found");
+      }
+    } else if (memcmp(command, "gps sync", 8) == 0) {
+      LocationProvider * l = _sensors->getLocationProvider();
+      if (l != NULL) {
+        l->syncTime();
+      }
+    } else if (memcmp(command, "gps setloc", 10) == 0) {
+      _prefs->node_lat = _sensors->node_lat;
+      _prefs->node_lon = _sensors->node_lon;
+      savePrefs();
+      strcpy(reply, "ok");
+    } else if (memcmp(command, "gps advert", 10) == 0) {
+      if (strlen(command) == 10) {
+        switch (_prefs->advert_loc_policy) {
+          case ADVERT_LOC_NONE:
+            strcpy(reply, "> none");
+            break;
+          case ADVERT_LOC_PREFS:
+            strcpy(reply, "> prefs");
+            break;
+          case ADVERT_LOC_SHARE:
+            strcpy(reply, "> share");
+            break;
+          default:
+            strcpy(reply, "error");
+        }
+      } else if (memcmp(command+11, "none", 4) == 0) {
+        _prefs->advert_loc_policy = ADVERT_LOC_NONE;
+        savePrefs();
+        strcpy(reply, "ok");
+      } else if (memcmp(command+11, "share", 5) == 0) {
+        _prefs->advert_loc_policy = ADVERT_LOC_SHARE;
+        savePrefs();
+        strcpy(reply, "ok");
+      } else if (memcmp(command+11, "prefs", 4) == 0) {
+        _prefs->advert_loc_policy = ADVERT_LOC_PREFS;
+        savePrefs();
+        strcpy(reply, "ok");
+      } else {
+        strcpy(reply, "error");
+      }
+    } else if (memcmp(command, "gps", 3) == 0) {
+      LocationProvider * l = _sensors->getLocationProvider();
+      if (l != NULL) {
+        bool enabled = l->isEnabled(); // is EN pin on ?
+        bool fix = l->isValid();       // has fix ?
+        int sats = l->satellitesCount();
+        bool active = !strcmp(_sensors->getSettingByKey("gps"), "1");
+        if (enabled) {
+          sprintf(reply, "on, %s, %s, %d sats",
+            active?"active":"deactivated", 
+            fix?"fix":"no fix", 
+            sats);
+        } else {
+          strcpy(reply, "off");
+        }
+      } else {
+        strcpy(reply, "Can't find GPS");
+      }
+#endif
     } else if (memcmp(command, "log start", 9) == 0) {
       _callbacks->setLoggingOn(true);
       strcpy(reply, "   logging on");
@@ -413,6 +665,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
     } else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) {
       _callbacks->dumpLogFile();
       strcpy(reply, "   EOF");
+    } else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) {
+      _callbacks->formatPacketStatsReply(reply);
+    } else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 11) == 0 && (command[11] == 0 || command[11] == ' ')) {
+      _callbacks->formatRadioStatsReply(reply);
+    } else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
+      _callbacks->formatStatsReply(reply);
     } else {
       strcpy(reply, "Unknown command");
     }

+ 59 - 25
src/helpers/CommonCLI.h

@@ -2,30 +2,51 @@
 
 #include "Mesh.h"
 #include <helpers/IdentityStore.h>
+#include <helpers/SensorManager.h>
 
-struct NodePrefs {  // persisted to file
-    float airtime_factor;
-    char node_name[32];
-    double node_lat, node_lon;
-    char password[16];
-    float freq;
-    uint8_t tx_power_dbm;
-    uint8_t disable_fwd;
-    uint8_t advert_interval;   // minutes / 2
-    uint8_t flood_advert_interval;   // hours
-    float rx_delay_base;
-    float tx_delay_factor;
-    char guest_password[16];
-    float direct_tx_delay_factor;
-    uint32_t guard;
-    uint8_t sf;
-    uint8_t cr;
-    uint8_t allow_read_only;
-    uint8_t multi_acks;
-    float bw;
-    uint8_t flood_max;
-    uint8_t interference_threshold;
-    uint8_t agc_reset_interval;   // secs / 4
+#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
+#define WITH_BRIDGE
+#endif
+
+#define ADVERT_LOC_NONE       0
+#define ADVERT_LOC_SHARE      1
+#define ADVERT_LOC_PREFS      2
+
+struct NodePrefs { // persisted to file
+  float airtime_factor;
+  char node_name[32];
+  double node_lat, node_lon;
+  char password[16];
+  float freq;
+  uint8_t tx_power_dbm;
+  uint8_t disable_fwd;
+  uint8_t advert_interval;       // minutes / 2
+  uint8_t flood_advert_interval; // hours
+  float rx_delay_base;
+  float tx_delay_factor;
+  char guest_password[16];
+  float direct_tx_delay_factor;
+  uint32_t guard;
+  uint8_t sf;
+  uint8_t cr;
+  uint8_t allow_read_only;
+  uint8_t multi_acks;
+  float bw;
+  uint8_t flood_max;
+  uint8_t interference_threshold;
+  uint8_t agc_reset_interval; // secs / 4
+  // Bridge settings
+  uint8_t bridge_enabled; // boolean
+  uint16_t bridge_delay;  // milliseconds (default 500 ms)
+  uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx)
+  uint32_t bridge_baud;   // 9600, 19200, 38400, 57600, 115200 (default 115200)
+  uint8_t bridge_channel; // 1-14 (ESP-NOW only)
+  char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only)
+  // Gps settings
+  uint8_t gps_enabled;
+  uint32_t gps_interval; // in seconds
+  uint8_t advert_loc_policy;
+  uint32_t discovery_mod_timestamp;
 };
 
 class CommonCLICallbacks {
@@ -46,10 +67,21 @@ public:
   virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
     // no op by default
   };
+  virtual void formatStatsReply(char *reply) = 0;
+  virtual void formatRadioStatsReply(char *reply) = 0;
+  virtual void formatPacketStatsReply(char *reply) = 0;
   virtual mesh::LocalIdentity& getSelfId() = 0;
   virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
   virtual void clearStats() = 0;
   virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
+
+  virtual void setBridgeState(bool enable) {
+    // no op by default
+  };
+
+  virtual void restartBridge() {
+    // no op by default
+  };
 };
 
 class CommonCLI {
@@ -57,6 +89,7 @@ class CommonCLI {
   NodePrefs* _prefs;
   CommonCLICallbacks* _callbacks;
   mesh::MainBoard* _board;
+  SensorManager* _sensors;
   char tmp[PRV_KEY_SIZE*2 + 4];
 
   mesh::RTCClock* getRTCClock() { return _rtc; }
@@ -64,10 +97,11 @@ class CommonCLI {
   void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
 
 public:
-  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, NodePrefs* prefs, CommonCLICallbacks* callbacks)
-      : _board(&board), _rtc(&rtc), _prefs(prefs), _callbacks(callbacks) { }
+  CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, NodePrefs* prefs, CommonCLICallbacks* callbacks)
+      : _board(&board), _rtc(&rtc), _sensors(&sensors), _prefs(prefs), _callbacks(callbacks) { }
 
   void loadPrefs(FILESYSTEM* _fs);
   void savePrefs(FILESYSTEM* _fs);
   void handleCommand(uint32_t sender_timestamp, const char* command, char* reply);
+  uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
 };

+ 237 - 0
src/helpers/RegionMap.cpp

@@ -0,0 +1,237 @@
+#include "RegionMap.h"
+#include <helpers/TxtDataHelpers.h>
+#include <SHA256.h>
+
+RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
+  next_id = 1; num_regions = 0; home_id = 0;
+  wildcard.id = wildcard.parent = 0;
+  wildcard.flags = 0;  // default behaviour, allow flood and direct
+  strcpy(wildcard.name, "*");
+}
+
+bool RegionMap::is_name_char(char c) {
+  return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '#';
+}
+
+static File openWrite(FILESYSTEM* _fs, const char* filename) {
+  #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
+    _fs->remove(filename);
+    return _fs->open(filename, FILE_O_WRITE);
+  #elif defined(RP2040_PLATFORM)
+    return _fs->open(filename, "w");
+  #else
+    return _fs->open(filename, "w", true);
+  #endif
+}
+
+bool RegionMap::load(FILESYSTEM* _fs) {
+  if (_fs->exists("/regions2")) {
+  #if defined(RP2040_PLATFORM)
+    File file = _fs->open("/regions2", "r");
+  #else
+    File file = _fs->open("/regions2");
+  #endif
+
+    if (file) {
+      uint8_t pad[128];
+
+      num_regions = 0; next_id = 1; home_id = 0;
+
+      bool success = file.read(pad, 5) == 5;  // reserved header
+      success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
+      success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
+      success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
+
+      if (success) {
+        while (num_regions < MAX_REGION_ENTRIES) {
+          auto r = &regions[num_regions];
+
+          success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
+          success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
+          success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
+          success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
+          success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
+
+          if (!success) break; // EOF
+
+          if (r->id >= next_id) {    // make sure next_id is valid
+            next_id = r->id + 1;
+          }
+          num_regions++;
+        }
+      }
+      file.close();
+      return true;
+    }
+  }
+  return false;  // failed
+}
+
+bool RegionMap::save(FILESYSTEM* _fs) {
+  File file = openWrite(_fs, "/regions2");
+  if (file) {
+    uint8_t pad[128];
+    memset(pad, 0, sizeof(pad));
+
+    bool success = file.write(pad, 5) == 5;  // reserved header
+    success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
+    success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
+    success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
+
+    if (success) {
+      for (int i = 0; i < num_regions; i++) {
+        auto r = &regions[i];
+
+        success = file.write((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
+        success = success && file.write((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
+        success = success && file.write((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
+        success = success && file.write((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
+        success = success && file.write(pad, sizeof(pad)) == sizeof(pad);
+        if (!success) break; // write failed
+      }
+    }
+    file.close();
+    return true;
+  }
+  return false;  // failed
+}
+
+RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) {
+  const char* sp = name;  // check for illegal name chars
+  while (*sp) {
+    if (!is_name_char(*sp)) return NULL;   // error
+    sp++;
+  }
+
+  auto region = findByName(name);
+  if (region) {
+    if (region->id == parent_id) return NULL;   // ERROR: invalid parent!
+
+    region->parent = parent_id;   // re-parent / move this region in the hierarchy
+  } else {
+    if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL;  // full!
+
+    region = &regions[num_regions++];   // alloc new RegionEntry
+    region->flags = REGION_DENY_FLOOD;     // DENY by default
+    region->id = id == 0 ? next_id++ : id;
+    StrHelper::strncpy(region->name, name, sizeof(region->name));
+    region->parent = parent_id;
+  }
+  return region;
+}
+
+RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
+  for (int i = 0; i < num_regions; i++) {
+    auto region = &regions[i];
+    if ((region->flags & mask) == 0) {   // does region allow this? (per 'mask' param)
+      TransportKey keys[4];
+      int num;
+      if (region->name[0] == '#') {   // auto hashtag region
+        _store->getAutoKeyFor(region->id, region->name, keys[0]);
+        num = 1;
+      } else {
+        num = _store->loadKeysFor(region->id, keys, 4);
+      }
+      for (int j = 0; j < num; j++) {
+        uint16_t code = keys[j].calcTransportCode(packet);
+        if (packet->transport_codes[0] == code) {   // a match!!
+          return region;
+        }
+      }
+    }
+  }
+  return NULL;  // no matches
+}
+
+RegionEntry* RegionMap::findByName(const char* name) {
+  if (strcmp(name, "*") == 0) return &wildcard;
+
+  for (int i = 0; i < num_regions; i++) {
+    auto region = &regions[i];
+    if (strcmp(name, region->name) == 0) return region;
+  }
+  return NULL;  // not found
+}
+
+RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
+  if (strcmp(prefix, "*") == 0) return &wildcard;
+
+  RegionEntry* partial = NULL;
+  for (int i = 0; i < num_regions; i++) {
+    auto region = &regions[i];
+    if (strcmp(prefix, region->name) == 0) return region;  // is a complete match, preference this one
+    if (memcmp(prefix, region->name, strlen(prefix)) == 0) {
+      partial = region;
+    }
+  }
+  return partial;
+}
+
+RegionEntry* RegionMap::findById(uint16_t id) {
+  if (id == 0) return &wildcard;   // special root Region
+
+  for (int i = 0; i < num_regions; i++) {
+    auto region = &regions[i];
+    if (region->id == id) return region;
+  }
+  return NULL;  // not found
+}
+
+RegionEntry* RegionMap::getHomeRegion() {
+  return findById(home_id);
+}
+
+void RegionMap::setHomeRegion(const RegionEntry* home) {
+  home_id = home ? home->id : 0;
+}
+
+bool RegionMap::removeRegion(const RegionEntry& region) {
+  if (region.id == 0) return false;  // failed (cannot remove the wildcard Region)
+
+  int i;     // first check region has no child regions
+  for (i = 0; i < num_regions; i++) {
+    if (regions[i].parent == region.id) return false;   // failed (must remove child Regions first)
+  }
+
+  i = 0;
+  while (i < num_regions) {
+    if (region.id == regions[i].id) break;
+    i++;
+  }
+  if (i >= num_regions) return false;  // failed (not found)
+
+  num_regions--;    // remove from regions array
+  while (i < num_regions) {
+    regions[i] = regions[i + 1];
+    i++;
+  }
+  return true;  // success
+}
+
+bool RegionMap::clear() {
+  num_regions = 0;
+  return true;  // success
+}
+
+void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& out) const {
+  for (int i = 0; i < indent; i++) {
+    out.print(' ');
+  }
+
+  if (parent->flags & REGION_DENY_FLOOD) {
+    out.printf("%s%s\n", parent->name, parent->id == home_id ? "^" : "");
+  } else {
+    out.printf("%s%s F\n", parent->name, parent->id == home_id ? "^" : "");
+  }
+
+  for (int i = 0; i < num_regions; i++) {
+    auto r = &regions[i];
+    if (r->parent == parent->id) {
+      printChildRegions(indent + 1, r, out);
+    }
+  }
+}
+
+void RegionMap::exportTo(Stream& out) const {
+  printChildRegions(0, &wildcard, out);   // recursive
+}

+ 52 - 0
src/helpers/RegionMap.h

@@ -0,0 +1,52 @@
+#pragma once
+
+#include <Arduino.h>   // needed for PlatformIO
+#include <Packet.h>
+#include "TransportKeyStore.h"
+
+#ifndef MAX_REGION_ENTRIES
+  #define MAX_REGION_ENTRIES  32
+#endif
+
+#define REGION_DENY_FLOOD   0x01
+#define REGION_DENY_DIRECT  0x02   // reserved for future
+
+struct RegionEntry {
+  uint16_t id;
+  uint16_t parent;
+  uint8_t flags;
+  char name[31];
+};
+
+class RegionMap {
+  TransportKeyStore* _store;
+  uint16_t next_id, home_id;
+  uint16_t num_regions;
+  RegionEntry regions[MAX_REGION_ENTRIES];
+  RegionEntry wildcard;
+
+  void printChildRegions(int indent, const RegionEntry* parent, Stream& out) const;
+
+public:
+  RegionMap(TransportKeyStore& store);
+
+  static bool is_name_char(char c);
+
+  bool load(FILESYSTEM* _fs);
+  bool save(FILESYSTEM* _fs);
+
+  RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
+  RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
+  RegionEntry& getWildcard() { return wildcard; }
+  RegionEntry* findByName(const char* name);
+  RegionEntry* findByNamePrefix(const char* prefix);
+  RegionEntry* findById(uint16_t id);
+  RegionEntry* getHomeRegion();   // NOTE: can be NULL
+  void setHomeRegion(const RegionEntry* home);
+  bool removeRegion(const RegionEntry& region);
+  bool clear();
+  void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
+  int getCount() const { return num_regions; }
+
+  void exportTo(Stream& out) const;
+};

+ 13 - 0
src/helpers/SensorManager.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include <CayenneLPP.h>
+#include "sensors/LocationProvider.h"
 
 #define TELEM_PERM_BASE         0x01   // 'base' permission includes battery
 #define TELEM_PERM_LOCATION     0x02
@@ -21,4 +22,16 @@ public:
   virtual const char* getSettingName(int i) const { return NULL; }
   virtual const char* getSettingValue(int i) const { return NULL; }
   virtual bool setSettingValue(const char* name, const char* value) { return false; }
+  virtual LocationProvider* getLocationProvider() { return NULL; }
+
+  // Helper functions to manage setting by keys (useful in many places ...)
+  const char* getSettingByKey(const char* key) {
+    int num = getNumSettings();
+    for (int i = 0; i < num; i++) {
+      if (strcmp(getSettingName(i), key) == 0) {
+        return getSettingValue(i);
+      }
+    }
+    return NULL;
+  }
 };

+ 54 - 0
src/helpers/StatsFormatHelper.h

@@ -0,0 +1,54 @@
+#pragma once
+
+#include "Mesh.h"
+
+class StatsFormatHelper {
+public:
+  static void formatCoreStats(char* reply, 
+                             mesh::MainBoard& board, 
+                             mesh::MillisecondClock& ms, 
+                             uint16_t err_flags,
+                             mesh::PacketManager* mgr) {
+    sprintf(reply, 
+      "{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}",
+      board.getBattMilliVolts(),
+      ms.getMillis() / 1000,
+      err_flags,
+      mgr->getOutboundCount(0xFFFFFFFF)
+    );
+  }
+
+  template<typename RadioDriverType>
+  static void formatRadioStats(char* reply,
+                              mesh::Radio* radio,
+                              RadioDriverType& driver,
+                              uint32_t total_air_time_ms,
+                              uint32_t total_rx_air_time_ms) {
+    sprintf(reply, 
+      "{\"noise_floor\":%d,\"last_rssi\":%d,\"last_snr\":%.2f,\"tx_air_secs\":%u,\"rx_air_secs\":%u}",
+      (int16_t)radio->getNoiseFloor(),
+      (int16_t)driver.getLastRSSI(),
+      driver.getLastSNR(),
+      total_air_time_ms / 1000,
+      total_rx_air_time_ms / 1000
+    );
+  }
+
+  template<typename RadioDriverType>
+  static void formatPacketStats(char* reply,
+                               RadioDriverType& driver,
+                               uint32_t n_sent_flood,
+                               uint32_t n_sent_direct,
+                               uint32_t n_recv_flood,
+                               uint32_t n_recv_direct) {
+    sprintf(reply, 
+      "{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u}",
+      driver.getPacketsRecv(),
+      driver.getPacketsSent(),
+      n_sent_flood,
+      n_sent_direct,
+      n_recv_flood,
+      n_recv_direct
+    );
+  }
+};

+ 92 - 0
src/helpers/TransportKeyStore.cpp

@@ -0,0 +1,92 @@
+#include "TransportKeyStore.h"
+#include <SHA256.h>
+
+uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const {
+  uint16_t code;
+  SHA256 sha;
+  sha.resetHMAC(key, sizeof(key));
+  uint8_t type = packet->getPayloadType();
+  sha.update(&type, 1);
+  sha.update(packet->payload, packet->payload_len);
+  sha.finalizeHMAC(key, sizeof(key), &code, 2);
+  if (code == 0) {     // reserve codes 0000 and FFFF
+    code++;
+  } else if (code == 0xFFFF) {
+    code--;
+  }
+  return code;
+}
+
+bool TransportKey::isNull() const {
+  for (int i = 0; i < sizeof(key); i++) {
+    if (key[i]) return false;
+  }
+  return true;  // key is all zeroes
+}
+
+void TransportKeyStore::putCache(uint16_t id, const TransportKey& key) {
+  if (num_cache < MAX_TKS_ENTRIES) {
+    cache_ids[num_cache] = id;
+    cache_keys[num_cache] = key;
+    num_cache++;
+  } else {
+    // TODO: evict oldest cache entry
+  }
+}
+
+void TransportKeyStore::getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest) {
+  for (int i = 0; i < num_cache; i++) {  // first, check cache
+    if (cache_ids[i] == id) {   // cache hit!
+      dest = cache_keys[i];
+      return;
+    }
+  }
+  // calc key for publicly-known hashtag region name
+  SHA256 sha;
+  sha.update(name, strlen(name));
+  sha.finalize(&dest.key, sizeof(dest.key));
+
+  putCache(id, dest);
+}
+
+int TransportKeyStore::loadKeysFor(uint16_t id, TransportKey keys[], int max_num) {
+  int n = 0;
+  for (int i = 0; i < num_cache && n < max_num; i++) {  // first, check cache
+    if (cache_ids[i] == id) {
+      keys[n++] = cache_keys[i];
+    }
+  }
+  if (n > 0) return n;   // cache hit!
+
+  // TODO:  retrieve from difficult-to-copy keystore
+
+  // store in cache (if room)
+  for (int i = 0; i < n; i++) {
+    putCache(id, keys[i]);
+  }
+  return n;
+}
+
+bool TransportKeyStore::saveKeysFor(uint16_t id, const TransportKey keys[], int num) {
+  invalidateCache();
+
+  // TODO: update hardware keystore
+
+  return false;  // failed
+}
+
+bool TransportKeyStore::removeKeys(uint16_t id) {
+  invalidateCache();
+
+  // TODO: remove from hardware keystore
+
+  return false;  // failed
+}
+
+bool TransportKeyStore::clear() {
+  invalidateCache();
+
+  // TODO: clear hardware keystore
+
+  return false;  // failed
+}

+ 31 - 0
src/helpers/TransportKeyStore.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <Arduino.h>   // needed for PlatformIO
+#include <Packet.h>
+#include <helpers/IdentityStore.h>
+
+struct TransportKey {
+  uint8_t key[16];
+
+  uint16_t calcTransportCode(const mesh::Packet* packet) const;
+  bool isNull() const;
+};
+
+#define MAX_TKS_ENTRIES   16
+
+class TransportKeyStore {
+  uint16_t     cache_ids[MAX_TKS_ENTRIES];
+  TransportKey cache_keys[MAX_TKS_ENTRIES];
+  int num_cache;
+
+  void putCache(uint16_t id, const TransportKey& key);
+  void invalidateCache() { num_cache = 0; }
+
+public:
+  TransportKeyStore() { num_cache = 0; }
+  void getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest);
+  int loadKeysFor(uint16_t id, TransportKey keys[], int max_num);
+  bool saveKeysFor(uint16_t id, const TransportKey keys[], int num);
+  bool removeKeys(uint16_t id);
+  bool clear();
+};

+ 27 - 0
src/helpers/TxtDataHelpers.cpp

@@ -19,6 +19,13 @@ void StrHelper::strzcpy(char* dest, const char* src, size_t buf_sz) {
   }
 }
 
+bool StrHelper::isBlank(const char* str) {
+  while (*str) {
+    if (*str++ != ' ') return false;
+  }
+  return true;
+}
+
 #include <Arduino.h>
 
 union int32_Float_t 
@@ -132,3 +139,23 @@ const char* StrHelper::ftoa(float f) {
   }
   return tmp;
 }
+
+uint32_t StrHelper::fromHex(const char* src) {
+  uint32_t n = 0;
+  while (*src) {
+    if (*src >= '0' && *src <= '9') {
+      n <<= 4;
+      n |= (*src - '0');
+    } else if (*src >= 'A' && *src <= 'F') {
+      n <<= 4;
+      n |= (*src - 'A' + 10);
+    } else if (*src >= 'a' && *src <= 'f') {
+      n <<= 4;
+      n |= (*src - 'a' + 10);
+    } else {
+      break;  // non-hex char encountered, stop parsing
+    }
+    src++;
+  }
+  return n;
+}

+ 2 - 0
src/helpers/TxtDataHelpers.h

@@ -12,4 +12,6 @@ public:
   static void strncpy(char* dest, const char* src, size_t buf_sz);
   static void strzcpy(char* dest, const char* src, size_t buf_sz);   // pads with trailing nulls
   static const char* ftoa(float f);
+  static bool isBlank(const char* str);
+  static uint32_t fromHex(const char* src);
 };

+ 13 - 1
src/helpers/bridges/BridgeBase.cpp

@@ -2,6 +2,10 @@
 
 #include <Arduino.h>
 
+bool BridgeBase::isRunning() const {
+  return _initialized;
+}
+
 const char *BridgeBase::getLogDateTime() {
   static char tmp[32];
   uint32_t now = _rtc->getCurrentTime();
@@ -28,8 +32,16 @@ bool BridgeBase::validateChecksum(const uint8_t *data, size_t len, uint16_t rece
 }
 
 void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
+  // Guard against uninitialized state
+  if (_initialized == false) {
+    BRIDGE_DEBUG_PRINTLN("RX packet received before initialization\n");
+    _mgr->free(packet);
+    return;
+  }
+
   if (!_seen_packets.hasSeen(packet)) {
-    _mgr->queueInbound(packet, millis() + BRIDGE_DELAY);
+    // bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
+    _mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
   } else {
     _mgr->free(packet);
   }

+ 17 - 9
src/helpers/bridges/BridgeBase.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "helpers/AbstractBridge.h"
+#include "helpers/CommonCLI.h"
 #include "helpers/SimpleMeshTables.h"
 
 #include <RTClib.h>
@@ -21,6 +22,13 @@ class BridgeBase : public AbstractBridge {
 public:
   virtual ~BridgeBase() = default;
 
+  /**
+   * @brief Gets the current state of the bridge.
+   *
+   * @return true if the bridge is initialized and running, false otherwise.
+   */
+  bool isRunning() const override;
+
   /**
    * @brief Common magic number used by all bridge implementations for packet identification
    *
@@ -41,31 +49,31 @@ public:
   static constexpr uint16_t BRIDGE_LENGTH_SIZE = sizeof(uint16_t);
   static constexpr uint16_t BRIDGE_CHECKSUM_SIZE = sizeof(uint16_t);
 
-  /**
-   * @brief Default delay in milliseconds for scheduling inbound packet processing
-   *
-   * It provides a buffer to prevent immediate processing conflicts in the mesh network.
-   * Used in handleReceivedPacket() as: millis() + BRIDGE_DELAY
-   */
-  static constexpr uint16_t BRIDGE_DELAY = 500; // TODO: maybe too high ?
-
 protected:
+  /** Tracks bridge state */
+  bool _initialized = false;
+
   /** Packet manager for allocating and queuing mesh packets */
   mesh::PacketManager *_mgr;
 
   /** RTC clock for timestamping debug messages */
   mesh::RTCClock *_rtc;
 
+  /** Node preferences for configuration settings */
+  NodePrefs *_prefs;
+
   /** Tracks seen packets to prevent loops in broadcast communications */
   SimpleMeshTables _seen_packets;
 
   /**
    * @brief Constructs a BridgeBase instance
    *
+   * @param prefs Node preferences for configuration settings
    * @param mgr PacketManager for allocating and queuing packets
    * @param rtc RTCClock for timestamping debug messages
    */
-  BridgeBase(mesh::PacketManager *mgr, mesh::RTCClock *rtc) : _mgr(mgr), _rtc(rtc) {}
+  BridgeBase(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
+      : _prefs(prefs), _mgr(mgr), _rtc(rtc) {}
 
   /**
    * @brief Gets formatted date/time string for logging

+ 62 - 39
src/helpers/bridges/ESPNowBridge.cpp

@@ -21,18 +21,26 @@ void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) {
   }
 }
 
-ESPNowBridge::ESPNowBridge(mesh::PacketManager *mgr, mesh::RTCClock *rtc)
-    : BridgeBase(mgr, rtc), _rx_buffer_pos(0) {
+ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
+    : BridgeBase(prefs, mgr, rtc), _rx_buffer_pos(0) {
   _instance = this;
 }
 
 void ESPNowBridge::begin() {
+  BRIDGE_DEBUG_PRINTLN("Initializing...\n");
+
   // Initialize WiFi in station mode
   WiFi.mode(WIFI_STA);
+  
+  // Set wifi channel
+  if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
+    BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
+    return;
+  }
 
   // Initialize ESP-NOW
   if (esp_now_init() != ESP_OK) {
-    Serial.printf("%s: ESPNOW BRIDGE: Error initializing ESP-NOW\n", getLogDateTime());
+    BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
     return;
   }
 
@@ -44,13 +52,41 @@ void ESPNowBridge::begin() {
   esp_now_peer_info_t peerInfo = {};
   memset(&peerInfo, 0, sizeof(peerInfo));
   memset(peerInfo.peer_addr, 0xFF, ESP_NOW_ETH_ALEN); // Broadcast address
-  peerInfo.channel = 0;
+  peerInfo.channel = _prefs->bridge_channel;
   peerInfo.encrypt = false;
 
   if (esp_now_add_peer(&peerInfo) != ESP_OK) {
-    Serial.printf("%s: ESPNOW BRIDGE: Failed to add broadcast peer\n", getLogDateTime());
+    BRIDGE_DEBUG_PRINTLN("Failed to add broadcast peer\n");
     return;
   }
+
+  // Update bridge state
+  _initialized = true;
+}
+
+void ESPNowBridge::end() {
+  BRIDGE_DEBUG_PRINTLN("Stopping...\n");
+
+  // Remove broadcast peer
+  uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
+  if (esp_now_del_peer(broadcastAddress) != ESP_OK) {
+    BRIDGE_DEBUG_PRINTLN("Error removing broadcast peer\n");
+  }
+
+  // Unregister callbacks
+  esp_now_register_recv_cb(nullptr);
+  esp_now_register_send_cb(nullptr);
+
+  // Deinitialize ESP-NOW
+  if (esp_now_deinit() != ESP_OK) {
+    BRIDGE_DEBUG_PRINTLN("Error deinitializing ESP-NOW\n");
+  }
+
+  // Turn off WiFi
+  WiFi.mode(WIFI_OFF);
+
+  // Update bridge state
+  _initialized = false;
 }
 
 void ESPNowBridge::loop() {
@@ -58,35 +94,29 @@ void ESPNowBridge::loop() {
 }
 
 void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
-  size_t keyLen = strlen(_secret);
+  size_t keyLen = strlen(_prefs->bridge_secret);
   for (size_t i = 0; i < len; i++) {
-    data[i] ^= _secret[i % keyLen];
+    data[i] ^= _prefs->bridge_secret[i % keyLen];
   }
 }
 
 void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len) {
   // Ignore packets that are too small to contain header + checksum
   if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: ESPNOW BRIDGE: RX packet too small, len=%d\n", getLogDateTime(), len);
-#endif
+    BRIDGE_DEBUG_PRINTLN("RX packet too small, len=%d\n", len);
     return;
   }
 
   // Validate total packet size
   if (len > MAX_ESPNOW_PACKET_SIZE) {
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: ESPNOW BRIDGE: RX packet too large, len=%d\n", getLogDateTime(), len);
-#endif
+    BRIDGE_DEBUG_PRINTLN("RX packet too large, len=%d\n", len);
     return;
   }
 
   // Check packet header magic
   uint16_t received_magic = (data[0] << 8) | data[1];
   if (received_magic != BRIDGE_PACKET_MAGIC) {
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: ESPNOW BRIDGE: RX invalid magic 0x%04X\n", getLogDateTime(), received_magic);
-#endif
+    BRIDGE_DEBUG_PRINTLN("RX invalid magic 0x%04X\n", received_magic);
     return;
   }
 
@@ -104,16 +134,11 @@ void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t l
 
   if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
     // Failed to decrypt - likely from a different network
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: ESPNOW BRIDGE: RX checksum mismatch, rcv=0x%04X\n", getLogDateTime(),
-                  received_checksum);
-#endif
+    BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04X\n", received_checksum);
     return;
   }
 
-#if MESH_PACKET_LOGGING
-  Serial.printf("%s: ESPNOW BRIDGE: RX, payload_len=%d\n", getLogDateTime(), payloadLen);
-#endif
+  BRIDGE_DEBUG_PRINTLN("RX, payload_len=%d\n", payloadLen);
 
   // Create mesh packet
   mesh::Packet *pkt = _instance->_mgr->allocNew();
@@ -130,31 +155,27 @@ void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t sta
   // Could add transmission error handling here if needed
 }
 
-void ESPNowBridge::onPacketReceived(mesh::Packet *packet) {
-  handleReceivedPacket(packet);
-}
+void ESPNowBridge::sendPacket(mesh::Packet *packet) {
+  // Guard against uninitialized state
+  if (_initialized == false) {
+    return;
+  }
 
-void ESPNowBridge::onPacketTransmitted(mesh::Packet *packet) {
   // First validate the packet pointer
   if (!packet) {
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: ESPNOW BRIDGE: TX invalid packet pointer\n", getLogDateTime());
-#endif
+    BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
     return;
   }
 
   if (!_seen_packets.hasSeen(packet)) {
-
     // Create a temporary buffer just for size calculation and reuse for actual writing
     uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
     uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
 
     // Check if packet fits within our maximum payload size
     if (meshPacketLen > MAX_PAYLOAD_SIZE) {
-#if MESH_PACKET_LOGGING
-      Serial.printf("%s: ESPNOW BRIDGE: TX packet too large (payload=%d, max=%d)\n", getLogDateTime(),
-                    meshPacketLen, MAX_PAYLOAD_SIZE);
-#endif
+      BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", meshPacketLen,
+                           MAX_PAYLOAD_SIZE);
       return;
     }
 
@@ -183,14 +204,16 @@ void ESPNowBridge::onPacketTransmitted(mesh::Packet *packet) {
     uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
     esp_err_t result = esp_now_send(broadcastAddress, buffer, totalPacketSize);
 
-#if MESH_PACKET_LOGGING
     if (result == ESP_OK) {
-      Serial.printf("%s: ESPNOW BRIDGE: TX, len=%d\n", getLogDateTime(), meshPacketLen);
+      BRIDGE_DEBUG_PRINTLN("TX, len=%d\n", meshPacketLen);
     } else {
-      Serial.printf("%s: ESPNOW BRIDGE: TX FAILED!\n", getLogDateTime());
+      BRIDGE_DEBUG_PRINTLN("TX FAILED!\n");
     }
-#endif
   }
 }
 
+void ESPNowBridge::onPacketReceived(mesh::Packet *packet) {
+  handleReceivedPacket(packet);
+}
+
 #endif

+ 17 - 16
src/helpers/bridges/ESPNowBridge.h

@@ -6,10 +6,6 @@
 
 #ifdef WITH_ESPNOW_BRIDGE
 
-#ifndef WITH_ESPNOW_BRIDGE_SECRET
-#error WITH_ESPNOW_BRIDGE_SECRET must be defined to use ESPNowBridge
-#endif
-
 /**
  * @brief Bridge implementation using ESP-NOW protocol for packet transport
  *
@@ -36,11 +32,11 @@
  *
  * Configuration:
  * - Define WITH_ESPNOW_BRIDGE to enable this bridge
- * - Define WITH_ESPNOW_BRIDGE_SECRET with a string to set the network encryption key
+ * - Define _prefs->bridge_secret with a string to set the network encryption key
  *
  * Network Isolation:
  * Multiple independent mesh networks can coexist by using different
- * WITH_ESPNOW_BRIDGE_SECRET values. Packets encrypted with a different key will
+ * _prefs->bridge_secret values. Packets encrypted with a different key will
  * fail the checksum validation and be discarded.
  */
 class ESPNowBridge : public BridgeBase {
@@ -73,17 +69,11 @@ private:
   /** Current position in receive buffer */
   size_t _rx_buffer_pos;
 
-  /**
-   * Network encryption key from build define
-   * Must be defined with WITH_ESPNOW_BRIDGE_SECRET
-   * Used for XOR encryption to isolate different mesh networks
-   */
-  const char *_secret = WITH_ESPNOW_BRIDGE_SECRET;
-
   /**
    * Performs XOR encryption/decryption of data
+   * Used to isolate different mesh networks
    *
-   * Uses WITH_ESPNOW_BRIDGE_SECRET as the key in a simple XOR operation.
+   * Uses _prefs->bridge_secret as the key in a simple XOR operation.
    * The same operation is used for both encryption and decryption.
    * While not cryptographically secure, it provides basic network isolation.
    *
@@ -115,10 +105,11 @@ public:
   /**
    * Constructs an ESPNowBridge instance
    *
+   * @param prefs Node preferences for configuration settings
    * @param mgr PacketManager for allocating and queuing packets
    * @param rtc RTCClock for timestamping debug messages
    */
-  ESPNowBridge(mesh::PacketManager *mgr, mesh::RTCClock *rtc);
+  ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
 
   /**
    * Initializes the ESP-NOW bridge
@@ -130,6 +121,16 @@ public:
    */
   void begin() override;
 
+  /**
+   * Stops the ESP-NOW bridge
+   *
+   * - Removes broadcast peer
+   * - Unregisters callbacks
+   * - Deinitializes ESP-NOW protocol
+   * - Turns off WiFi to release radio resources
+   */
+  void end() override;
+
   /**
    * Main loop handler
    * ESP-NOW is callback-based, so this is currently empty
@@ -150,7 +151,7 @@ public:
    *
    * @param packet The mesh packet to transmit
    */
-  void onPacketTransmitted(mesh::Packet *packet) override;
+  void sendPacket(mesh::Packet *packet) override;
 };
 
 #endif

+ 64 - 60
src/helpers/bridges/RS232Bridge.cpp

@@ -4,10 +4,11 @@
 
 #ifdef WITH_RS232_BRIDGE
 
-RS232Bridge::RS232Bridge(Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
-    : BridgeBase(mgr, rtc), _serial(&serial) {}
+RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
+    : BridgeBase(prefs, mgr, rtc), _serial(&serial) {}
 
 void RS232Bridge::begin() {
+  BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
 #if !defined(WITH_RS232_BRIDGE_RX) || !defined(WITH_RS232_BRIDGE_TX)
 #error "WITH_RS232_BRIDGE_RX and WITH_RS232_BRIDGE_TX must be defined"
 #endif
@@ -25,53 +26,26 @@ void RS232Bridge::begin() {
 #else
 #error RS232Bridge was not tested on the current platform
 #endif
-  ((HardwareSerial *)_serial)->begin(115200);
-}
-
-void RS232Bridge::onPacketTransmitted(mesh::Packet *packet) {
-  // First validate the packet pointer
-  if (!packet) {
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: RS232 BRIDGE: TX invalid packet pointer\n", getLogDateTime());
-#endif
-    return;
-  }
-
-  if (!_seen_packets.hasSeen(packet)) {
-
-    uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
-    uint16_t len = packet->writeTo(buffer + 4);
-
-    // Check if packet fits within our maximum payload size
-    if (len > (MAX_TRANS_UNIT + 1)) {
-#if MESH_PACKET_LOGGING
-      Serial.printf("%s: RS232 BRIDGE: TX packet too large (payload=%d, max=%d)\n", getLogDateTime(), len,
-                    MAX_TRANS_UNIT + 1);
-#endif
-      return;
-    }
-
-    // Build packet header
-    buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; // Magic high byte
-    buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;        // Magic low byte
-    buffer[2] = (len >> 8) & 0xFF;                 // Length high byte
-    buffer[3] = len & 0xFF;                        // Length low byte
+  ((HardwareSerial *)_serial)->begin(_prefs->bridge_baud);
 
-    // Calculate checksum over the payload
-    uint16_t checksum = fletcher16(buffer + 4, len);
-    buffer[4 + len] = (checksum >> 8) & 0xFF; // Checksum high byte
-    buffer[5 + len] = checksum & 0xFF;        // Checksum low byte
+  // Update bridge state
+  _initialized = true;
+}
 
-    // Send complete packet
-    _serial->write(buffer, len + SERIAL_OVERHEAD);
+void RS232Bridge::end() {
+  BRIDGE_DEBUG_PRINTLN("Stopping...\n");
+  ((HardwareSerial *)_serial)->end();
 
-#if MESH_PACKET_LOGGING
-    Serial.printf("%s: RS232 BRIDGE: TX, len=%d crc=0x%04x\n", getLogDateTime(), len, checksum);
-#endif
-  }
+  // Update bridge state
+  _initialized = false;
 }
 
 void RS232Bridge::loop() {
+  // Guard against uninitialized state
+  if (_initialized == false) {
+    return;
+  }
+
   while (_serial->available()) {
     uint8_t b = _serial->read();
 
@@ -97,9 +71,7 @@ void RS232Bridge::loop() {
 
         // Validate length field
         if (len > (MAX_TRANS_UNIT + 1)) {
-#if MESH_PACKET_LOGGING
-          Serial.printf("%s: RS232 BRIDGE: RX invalid length %d, resetting\n", getLogDateTime(), len);
-#endif
+          BRIDGE_DEBUG_PRINTLN("RX invalid length %d, resetting\n", len);
           _rx_buffer_pos = 0; // Invalid length, reset
           continue;
         }
@@ -108,30 +80,20 @@ void RS232Bridge::loop() {
           uint16_t received_checksum = (_rx_buffer[4 + len] << 8) | _rx_buffer[5 + len];
 
           if (validateChecksum(_rx_buffer + 4, len, received_checksum)) {
-#if MESH_PACKET_LOGGING
-            Serial.printf("%s: RS232 BRIDGE: RX, len=%d crc=0x%04x\n", getLogDateTime(), len,
-                          received_checksum);
-#endif
+            BRIDGE_DEBUG_PRINTLN("RX, len=%d crc=0x%04x\n", len, received_checksum);
             mesh::Packet *pkt = _mgr->allocNew();
             if (pkt) {
               if (pkt->readFrom(_rx_buffer + 4, len)) {
                 onPacketReceived(pkt);
               } else {
-#if MESH_PACKET_LOGGING
-                Serial.printf("%s: RS232 BRIDGE: RX failed to parse packet\n", getLogDateTime());
-#endif
+                BRIDGE_DEBUG_PRINTLN("RX failed to parse packet\n");
                 _mgr->free(pkt);
               }
             } else {
-#if MESH_PACKET_LOGGING
-              Serial.printf("%s: RS232 BRIDGE: RX failed to allocate packet\n", getLogDateTime());
-#endif
+              BRIDGE_DEBUG_PRINTLN("RX failed to allocate packet\n");
             }
           } else {
-#if MESH_PACKET_LOGGING
-            Serial.printf("%s: RS232 BRIDGE: RX checksum mismatch, rcv=0x%04x\n", getLogDateTime(),
-                          received_checksum);
-#endif
+            BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04x\n", received_checksum);
           }
           _rx_buffer_pos = 0; // Reset for next packet
         }
@@ -140,6 +102,48 @@ void RS232Bridge::loop() {
   }
 }
 
+void RS232Bridge::sendPacket(mesh::Packet *packet) {
+  // Guard against uninitialized state
+  if (_initialized == false) {
+    return;
+  }
+
+  // First validate the packet pointer
+  if (!packet) {
+    BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
+    return;
+  }
+
+  if (!_seen_packets.hasSeen(packet)) {
+
+    uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
+    uint16_t len = packet->writeTo(buffer + 4);
+
+    // Check if packet fits within our maximum payload size
+    if (len > (MAX_TRANS_UNIT + 1)) {
+      BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", len,
+                           MAX_TRANS_UNIT + 1);
+      return;
+    }
+
+    // Build packet header
+    buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; // Magic high byte
+    buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;        // Magic low byte
+    buffer[2] = (len >> 8) & 0xFF;                 // Length high byte
+    buffer[3] = len & 0xFF;                        // Length low byte
+
+    // Calculate checksum over the payload
+    uint16_t checksum = fletcher16(buffer + 4, len);
+    buffer[4 + len] = (checksum >> 8) & 0xFF; // Checksum high byte
+    buffer[5 + len] = checksum & 0xFF;        // Checksum low byte
+
+    // Send complete packet
+    _serial->write(buffer, len + SERIAL_OVERHEAD);
+
+    BRIDGE_DEBUG_PRINTLN("TX, len=%d crc=0x%04x\n", len, checksum);
+  }
+}
+
 void RS232Bridge::onPacketReceived(mesh::Packet *packet) {
   handleReceivedPacket(packet);
 }

+ 9 - 2
src/helpers/bridges/RS232Bridge.h

@@ -49,11 +49,12 @@ public:
   /**
    * @brief Constructs an RS232Bridge instance
    *
+   * @param prefs Node preferences for configuration settings
    * @param serial The hardware serial port to use
    * @param mgr PacketManager for allocating and queuing packets
    * @param rtc RTCClock for timestamping debug messages
    */
-  RS232Bridge(Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
+  RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
 
   /**
    * Initializes the RS232 bridge
@@ -65,6 +66,12 @@ public:
    */
   void begin() override;
 
+  /**
+   * Stops the RS232 bridge
+   *
+   */
+  void end() override;
+
   /**
    * @brief Main loop handler for processing incoming serial data
    *
@@ -90,7 +97,7 @@ public:
    *
    * @param packet The mesh packet to transmit
    */
-  void onPacketTransmitted(mesh::Packet *packet) override;
+  void sendPacket(mesh::Packet *packet) override;
 
   /**
    * @brief Called when a complete valid packet has been received from serial

+ 14 - 56
src/helpers/radiolib/CustomLR1110.h

@@ -1,70 +1,28 @@
 #pragma once
 
 #include <RadioLib.h>
-
-#define LR1110_IRQ_HAS_PREAMBLE                     0b0000000100  //  4     4     valid LoRa header received
-#define LR1110_IRQ_HEADER_VALID                     0b0000010000  //  4     4     valid LoRa header received
+#include "MeshCore.h"
 
 class CustomLR1110 : public LR1110 {
   public:
     CustomLR1110(Module *mod) : LR1110(mod) { }
 
-    RadioLibTime_t getTimeOnAir(size_t len) override {
-  // calculate number of symbols
-  float N_symbol = 0;
-  if(this->codingRate <= RADIOLIB_LR11X0_LORA_CR_4_8_SHORT) {
-    // legacy coding rate - nice and simple
-    // get SF coefficients
-    float coeff1 = 0;
-    int16_t coeff2 = 0;
-    int16_t coeff3 = 0;
-    if(this->spreadingFactor < 7) {
-      // SF5, SF6
-      coeff1 = 6.25;
-      coeff2 = 4*this->spreadingFactor;
-      coeff3 = 4*this->spreadingFactor;
-    } else if(this->spreadingFactor < 11) {
-      // SF7. SF8, SF9, SF10
-      coeff1 = 4.25;
-      coeff2 = 4*this->spreadingFactor + 8;
-      coeff3 = 4*this->spreadingFactor;
-    } else {
-      // SF11, SF12
-      coeff1 = 4.25;
-      coeff2 = 4*this->spreadingFactor + 8;
-      coeff3 = 4*(this->spreadingFactor - 2);
-    }
-
-    // get CRC length
-    int16_t N_bitCRC = 16;
-    if(this->crcTypeLoRa == RADIOLIB_LR11X0_LORA_CRC_DISABLED) {
-      N_bitCRC = 0;
-    }
-
-    // get header length
-    int16_t N_symbolHeader = 20;
-    if(this->headerType == RADIOLIB_LR11X0_LORA_HEADER_IMPLICIT) {
-      N_symbolHeader = 0;
+    size_t getPacketLength(bool update) override {
+      size_t len = LR1110::getPacketLength(update);
+      if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
+        // we've just recieved a corrupted packet
+        // this may have triggered a bug causing subsequent packets to be shifted
+        // call standby() to return radio to known-good state
+        // recvRaw will call startReceive() to restart rx
+        MESH_DEBUG_PRINTLN("LR1110: got header err, calling standby()");
+        standby();
+      }
+      return len;
     }
-
-    // calculate number of LoRa preamble symbols - NO! Lora preamble is already in symbols
-    // uint32_t N_symbolPreamble = (this->preambleLengthLoRa & 0x0F) * (uint32_t(1) << ((this->preambleLengthLoRa & 0xF0) >> 4));
-
-    // calculate the number of symbols - nope
-    // N_symbol = (float)N_symbolPreamble + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4);
-    // calculate the number of symbols - using only preamblelora because it's already in symbols
-    N_symbol = (float)preambleLengthLoRa + coeff1 + 8.0f + ceilf((float)RADIOLIB_MAX((int16_t)(8 * len + N_bitCRC - coeff2 + N_symbolHeader), (int16_t)0) / (float)coeff3) * (float)(this->codingRate + 4);
-  } else {
-    // long interleaving - not needed for this modem
-  }
-
-  // get time-on-air in us
-  return(((uint32_t(1) << this->spreadingFactor) / this->bandwidthKhz) * N_symbol * 1000.0f);
-}
-
+    
     bool isReceiving() {
       uint16_t irq = getIrqStatus();
-      bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE));
+      bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
       return detected;
     }
 };

+ 123 - 18
src/helpers/sensors/EnvironmentSensorManager.cpp

@@ -6,6 +6,21 @@
 #define TELEM_WIRE &Wire  // Use default I2C bus for Environment Sensors
 #endif
 
+#ifdef ENV_INCLUDE_BME680
+#ifndef TELEM_BME680_ADDRESS
+#define TELEM_BME680_ADDRESS 0x76
+#endif
+#define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25)
+#include <Adafruit_BME680.h>
+static Adafruit_BME680 BME680;
+#endif
+
+#ifdef ENV_INCLUDE_BMP085
+#define TELEM_BMP085_SEALEVELPRESSURE_HPA (1013.25)
+#include <Adafruit_BMP085.h>
+static Adafruit_BMP085 BMP085;
+#endif
+
 #if ENV_INCLUDE_AHTX0
 #define TELEM_AHTX_ADDRESS      0x38      // AHT10, AHT20 temperature and humidity sensor I2C address
 #include <Adafruit_AHTX0.h>
@@ -96,6 +111,41 @@ static bool serialGPSFlag = false;
 #define TELEM_RAK12500_ADDRESS   0x42     //RAK12500 Ublox GPS via i2c
 #include <SparkFun_u-blox_GNSS_Arduino_Library.h>
 static SFE_UBLOX_GNSS ublox_GNSS;
+
+class RAK12500LocationProvider : public LocationProvider {
+  long _lat = 0;
+  long _lng = 0;
+  long _alt = 0;
+  int _sats = 0;
+  long _epoch = 0;
+  bool _fix = false;
+public:
+  long getLatitude() override { return _lat; }
+  long getLongitude() override { return _lng; }
+  long getAltitude() override { return _alt; }
+  long satellitesCount() override { return _sats; }
+  bool isValid() override { return _fix; }
+  long getTimestamp() override { return _epoch; }
+  void sendSentence(const char * sentence) override { }
+  void reset() override { }
+  void begin() override { }
+  void stop() override { }
+  void loop() override {
+    if (ublox_GNSS.getGnssFixOk(8)) {
+      _fix = true;
+      _lat = ublox_GNSS.getLatitude(2) / 10;
+      _lng = ublox_GNSS.getLongitude(2) / 10;
+      _alt = ublox_GNSS.getAltitude(2);
+      _sats = ublox_GNSS.getSIV(2);
+    } else {
+      _fix = false;
+    }
+    _epoch = ublox_GNSS.getUnixEpoch(2);
+  }
+  bool isEnabled() override { return true; }
+};
+
+static RAK12500LocationProvider RAK12500_provider;
 #endif
 
 bool EnvironmentSensorManager::begin() {
@@ -251,6 +301,28 @@ bool EnvironmentSensorManager::begin() {
   }
   #endif
 
+  #if ENV_INCLUDE_BME680
+  if (BME680.begin(TELEM_BME680_ADDRESS, TELEM_WIRE)) {
+    MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
+    BME680_initialized = true;
+  } else {
+    BME680_initialized = false;
+    MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
+  }
+  #endif
+
+  #if ENV_INCLUDE_BMP085
+  // First argument is  MODE (aka oversampling)
+  // choose ULTRALOWPOWER
+  if (BMP085.begin(0, TELEM_WIRE)) {
+    MESH_DEBUG_PRINTLN("Found sensor BMP085");
+    BMP085_initialized = true;
+  } else {
+    BMP085_initialized = false;
+    MESH_DEBUG_PRINTLN("BMP085 was not found at I2C address %02X", 0x77);
+  }
+  #endif
+
   return true;
 }
 
@@ -380,6 +452,27 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     }
     #endif
 
+    #if ENV_INCLUDE_BME680
+    if (BME680_initialized) {
+      if (BME680.performReading()) {
+        telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
+        telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
+        telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
+        telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
+        telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
+        next_available_channel++;
+      }
+    }
+    #endif
+
+    #if ENV_INCLUDE_BMP085
+    if (BMP085_initialized) {
+        telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
+        telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP085.readPressure() / 100);
+        telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
+    }
+    #endif
+
   }
 
   return true;
@@ -387,27 +480,34 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
 
 
 int EnvironmentSensorManager::getNumSettings() const {
+  int settings = 0;
   #if ENV_INCLUDE_GPS
-    return gps_detected ? 1 : 0;  // only show GPS setting if GPS is detected
-  #else
-    return 0;
+    if (gps_detected) settings++;  // only show GPS setting if GPS is detected
   #endif
+  return settings;
 }
 
 const char* EnvironmentSensorManager::getSettingName(int i) const {
+  int settings = 0;
   #if ENV_INCLUDE_GPS
-    return (gps_detected && i == 0) ? "gps" : NULL;
-  #else
-    return NULL;
+    if (gps_detected && i == settings++) {
+      return "gps";
+    }
   #endif
+  // convenient way to add params (needed for some tests)
+//  if (i == settings++) return "param.2";
+  return NULL;
 }
 
 const char* EnvironmentSensorManager::getSettingValue(int i) const {
+  int settings = 0;
   #if ENV_INCLUDE_GPS
-  if (gps_detected && i == 0) {
-    return gps_active ? "1" : "0";
-  }
+    if (gps_detected && i == settings++) {
+      return gps_active ? "1" : "0";
+    }
   #endif
+  // convenient way to add params ...
+//  if (i == settings++) return "2";
   return NULL;
 }
 
@@ -514,12 +614,22 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
   //Try to init RAK12500 on I2C
   if (ublox_GNSS.begin(Wire) == true){
     MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i",ioPin);
-    ublox_GNSS.setI2COutput(COM_TYPE_NMEA);
+    ublox_GNSS.setI2COutput(COM_TYPE_UBX);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GPS);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GALILEO);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GLONASS);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_SBAS);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_BEIDOU);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_IMES);
+    ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_QZSS);
+    ublox_GNSS.setMeasurementRate(1000);
     ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
     gpsResetPin = ioPin;
     i2cGPSFlag = true;
     gps_active = true;
     gps_detected = true;
+
+    _location = &RAK12500_provider;
     return true;
   }
   else if(Serial1){
@@ -571,19 +681,13 @@ void EnvironmentSensorManager::stop_gps() {
 void EnvironmentSensorManager::loop() {
   static long next_gps_update = 0;
 
+  #if ENV_INCLUDE_GPS
   _location->loop();
 
   if (millis() > next_gps_update) {
     if(gps_active){
     #ifdef RAK_WISBLOCK_GPS
-    if(i2cGPSFlag){
-      node_lat = ((double)ublox_GNSS.getLatitude())/10000000.;
-      node_lon = ((double)ublox_GNSS.getLongitude())/10000000.;
-      MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
-      node_altitude = ((double)ublox_GNSS.getAltitude()) / 1000.0;
-      MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
-    }
-    else if (serialGPSFlag && _location->isValid()) {
+    if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) {
       node_lat = ((double)_location->getLatitude())/1000000.;
       node_lon = ((double)_location->getLongitude())/1000000.;
       MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
@@ -602,5 +706,6 @@ void EnvironmentSensorManager::loop() {
     }
     next_gps_update = millis() + 1000;
   }
+  #endif
 }
 #endif

+ 3 - 0
src/helpers/sensors/EnvironmentSensorManager.h

@@ -20,6 +20,8 @@ protected:
   bool MLX90614_initialized = false;
   bool VL53L0X_initialized = false;
   bool SHT4X_initialized = false;
+  bool BME680_initialized = false;
+  bool BMP085_initialized = false;
 
   bool gps_detected = false;
   bool gps_active = false;
@@ -39,6 +41,7 @@ protected:
 public:
   #if ENV_INCLUDE_GPS
   EnvironmentSensorManager(LocationProvider &location): _location(&location){};
+  LocationProvider* getLocationProvider() { return _location; }
   #else
   EnvironmentSensorManager(){};
   #endif

+ 5 - 4
src/helpers/sensors/LocationProvider.h

@@ -17,8 +17,9 @@ public:
     virtual bool isValid() = 0;
     virtual long getTimestamp() = 0;
     virtual void sendSentence(const char * sentence);
-    virtual void reset();
-    virtual void begin();
-    virtual void stop();
-    virtual void loop();
+    virtual void reset() = 0;
+    virtual void begin() = 0;
+    virtual void stop() = 0;
+    virtual void loop() = 0;
+    virtual bool isEnabled() = 0;
 };

+ 17 - 3
src/helpers/sensors/MicroNMEALocationProvider.h

@@ -3,6 +3,7 @@
 #include "LocationProvider.h"
 #include <MicroNMEA.h>
 #include <RTClib.h>
+#include <helpers/RefCountedDigitalPin.h>
 
 #ifndef GPS_EN
     #ifdef PIN_GPS_EN
@@ -37,14 +38,15 @@ class MicroNMEALocationProvider : public LocationProvider {
     MicroNMEA nmea;
     mesh::RTCClock* _clock;
     Stream* _gps_serial;
+    RefCountedDigitalPin* _peripher_power;
     int _pin_reset;
     int _pin_en;
     long next_check = 0;
     long time_valid = 0;
 
 public :
-    MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN) :
-    _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock) {
+    MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
+    _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) {
         if (_pin_reset != -1) {
             pinMode(_pin_reset, OUTPUT);
             digitalWrite(_pin_reset, GPS_RESET_FORCE);
@@ -56,6 +58,7 @@ public :
     }
 
     void begin() override {
+        if (_peripher_power) _peripher_power->claim();
         if (_pin_en != -1) {
             digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE);
         }
@@ -75,7 +78,18 @@ public :
     void stop() override {
         if (_pin_en != -1) {
             digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
-        }        
+        }
+        if (_peripher_power) _peripher_power->release();  
+    }
+
+    bool isEnabled() override {
+        // directly read the enable pin if present as gps can be
+        // activated/deactivated outside of here ...
+        if (_pin_en != -1) {
+            return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE;
+        } else {
+            return true; // no enable so must be active
+        }
     }
 
     void syncTime() override { nmea.clear(); LocationProvider::syncTime(); }

+ 9 - 0
src/helpers/ui/DisplayDriver.h

@@ -32,6 +32,15 @@ public:
     setCursor(mid_x - w/2, y);
     print(str);
   }
+  virtual void drawTextRightAlign(int x_anch, int y, const char* str) {
+    int w = getTextWidth(str);
+    setCursor(x_anch - w, y);
+    print(str);
+  }
+  virtual void drawTextLeftAlign(int x_anch, int y, const char* str) {
+    setCursor(x_anch, y);
+    print(str);
+  }
   
   // convert UTF-8 characters to displayable block characters for compatibility
   virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {

+ 125 - 0
src/helpers/ui/LGFXDisplay.cpp

@@ -0,0 +1,125 @@
+#include "LGFXDisplay.h"
+
+bool LGFXDisplay::begin() {
+  turnOn();
+  display->init();
+  display->setRotation(1);
+  display->setBrightness(64);
+  display->setColorDepth(8);
+  display->setTextColor(TFT_WHITE);
+
+  buffer.setColorDepth(8);
+  buffer.setPsram(true);
+  buffer.createSprite(width(), height());
+
+  return true;
+}
+
+void LGFXDisplay::turnOn() {
+//  display->wakeup();
+  if (!_isOn) {
+    display->wakeup();
+  }
+  _isOn = true;
+}
+
+void LGFXDisplay::turnOff() {
+  if (_isOn) {
+    display->sleep();
+  }
+  _isOn = false;
+}
+
+void LGFXDisplay::clear() {
+//  display->clearDisplay();
+  buffer.clearDisplay();
+}
+
+void LGFXDisplay::startFrame(Color bkg) {
+//  display->startWrite();
+//  display->getScanLine();
+  buffer.clearDisplay();
+  buffer.setTextColor(TFT_WHITE);
+}
+
+void LGFXDisplay::setTextSize(int sz) {
+  buffer.setTextSize(sz);
+}
+
+void LGFXDisplay::setColor(Color c) {
+  // _color = (c != 0) ? ILI9342_WHITE : ILI9342_BLACK;
+  switch (c) {
+    case DARK:
+      _color = TFT_BLACK;
+      break;
+    case LIGHT:
+      _color = TFT_WHITE;
+      break;
+    case RED:
+      _color = TFT_RED;
+      break;
+    case GREEN:
+      _color = TFT_GREEN;
+      break;
+    case BLUE:
+      _color = TFT_BLUE;
+      break;
+    case YELLOW:
+      _color = TFT_YELLOW;
+      break;
+    case ORANGE:
+      _color = TFT_ORANGE;
+      break;
+    default:
+      _color = TFT_WHITE;
+  }
+  buffer.setTextColor(_color);
+}
+
+void LGFXDisplay::setCursor(int x, int y) {
+  buffer.setCursor(x, y);
+}
+
+void LGFXDisplay::print(const char* str) {
+  buffer.println(str);
+//  Serial.println(str);
+}
+
+void LGFXDisplay::fillRect(int x, int y, int w, int h) {
+  buffer.fillRect(x, y, w, h, _color);
+}
+
+void LGFXDisplay::drawRect(int x, int y, int w, int h) {
+  buffer.drawRect(x, y, w, h, _color);
+}
+
+void LGFXDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
+  buffer.drawBitmap(x, y, bits, w, h, _color);
+}
+
+uint16_t LGFXDisplay::getTextWidth(const char* str) {
+  return buffer.textWidth(str);
+}
+
+void LGFXDisplay::endFrame() {
+  display->startWrite();
+  if (UI_ZOOM != 1) {
+    buffer.pushRotateZoom(display, display->width()/2, display->height()/2 , 0, UI_ZOOM, UI_ZOOM);
+  } else {
+    buffer.pushSprite(display, 0, 0);
+  }
+  display->endWrite();
+}
+
+bool LGFXDisplay::getTouch(int *x, int *y) {
+  lgfx::v1::touch_point_t point;
+  display->getTouch(&point);
+  if (UI_ZOOM != 1) {
+    *x = point.x / UI_ZOOM;
+    *y = point.y / UI_ZOOM;
+  } else {
+    *x = point.x;
+    *y = point.y;
+  }
+  return (*x >= 0) && (*y >= 0);
+}

+ 39 - 0
src/helpers/ui/LGFXDisplay.h

@@ -0,0 +1,39 @@
+#pragma once
+
+#include <helpers/ui/DisplayDriver.h>
+
+#define LGFX_USE_V1
+#include <LovyanGFX.hpp>
+
+#ifndef UI_ZOOM
+  #define UI_ZOOM 1
+#endif
+
+class LGFXDisplay : public DisplayDriver {
+protected:
+  LGFX_Device* display;
+  LGFX_Sprite buffer;
+
+  bool _isOn = false;
+  int _color = TFT_WHITE;
+
+public:
+  LGFXDisplay(int w, int h, LGFX_Device &disp)
+    : DisplayDriver(w/UI_ZOOM, h/UI_ZOOM), display(&disp) {}
+  bool begin();
+  bool isOn() override { return _isOn; }
+  void turnOn() override;
+  void turnOff() override;
+  void clear() override;
+  void startFrame(Color bkg = DARK) override;
+  void setTextSize(int sz) override;
+  void setColor(Color c) override;
+  void setCursor(int x, int y) override;
+  void print(const char* str) override;
+  void fillRect(int x, int y, int w, int h) override;
+  void drawRect(int x, int y, int w, int h) override;
+  void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
+  uint16_t getTextWidth(const char* str) override;
+  void endFrame() override;
+  virtual bool getTouch(int *x, int *y);
+};

+ 2 - 2
src/helpers/ui/MomentaryButton.cpp

@@ -2,7 +2,7 @@
 
 #define MULTI_CLICK_WINDOW_MS  280
 
-MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup) { 
+MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse, bool pulldownup, bool multiclick) { 
   _pin = pin;
   _reverse = reverse;
   _pull = pulldownup;
@@ -13,7 +13,7 @@ MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse
   _threshold = 0;
   _click_count = 0;
   _last_click_time = 0;
-  _multi_click_window = MULTI_CLICK_WINDOW_MS;
+  _multi_click_window = multiclick ? MULTI_CLICK_WINDOW_MS : 0;
   _pending_click = false;
 }
 

+ 1 - 1
src/helpers/ui/MomentaryButton.h

@@ -23,7 +23,7 @@ class MomentaryButton {
   bool isPressed(int level) const;
 
 public:
-  MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false);
+  MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
   MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
   void begin();
   int check(bool repeat_click=false);  // returns one of BUTTON_EVENT_*

+ 8 - 1
src/helpers/ui/ST7735Display.cpp

@@ -24,14 +24,21 @@ bool ST7735Display::begin() {
     digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
     digitalWrite(PIN_TFT_RST, HIGH);
 
+#if defined(HELTEC_TRACKER_V2)
+    display.initR(INITR_MINI160x80);
+    display.setRotation(DISPLAY_ROTATION);
+    uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR
+    display.sendCommand(ST77XX_MADCTL, &madctl, 1);
+#else
     display.initR(INITR_MINI160x80_PLUGIN);
     display.setRotation(DISPLAY_ROTATION);
+#endif
     display.setSPISpeed(40000000);
     display.fillScreen(ST77XX_BLACK);
     display.setTextColor(ST77XX_WHITE);
     display.setTextSize(2); 
     display.cp437(true);         // Use full 256 char 'Code Page 437' font
-  
+    
     _isOn = true;
   }
   return true;

+ 20 - 5
src/helpers/ui/ST7789LCDDisplay.cpp

@@ -39,7 +39,7 @@ bool ST7789LCDDisplay::begin() {
 
     display.fillScreen(ST77XX_BLACK);
     display.setTextColor(ST77XX_WHITE);
-    display.setTextSize(2); 
+    display.setTextSize(2 * DISPLAY_SCALE_X); 
     display.cp437(true); // Use full 256 char 'Code Page 437' font
   
     _isOn = true;
@@ -70,12 +70,12 @@ void ST7789LCDDisplay::clear() {
 void ST7789LCDDisplay::startFrame(Color bkg) {
   display.fillScreen(ST77XX_BLACK);
   display.setTextColor(ST77XX_WHITE);
-  display.setTextSize(1); // This one affects size of Please wait... message
+  display.setTextSize(1 * DISPLAY_SCALE_X); // This one affects size of Please wait... message
   display.cp437(true); // Use full 256 char 'Code Page 437' font
 }
 
 void ST7789LCDDisplay::setTextSize(int sz) {
-  display.setTextSize(sz);
+  display.setTextSize(sz * DISPLAY_SCALE_X);
 }
 
 void ST7789LCDDisplay::setColor(Color c) {
@@ -125,7 +125,22 @@ void ST7789LCDDisplay::drawRect(int x, int y, int w, int h) {
 }
 
 void ST7789LCDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
-  display.drawBitmap(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y, bits, w, h, _color);
+  uint8_t byteWidth = (w + 7) / 8;
+
+  for (int j = 0; j < h; j++) {
+    for (int i = 0; i < w; i++) {
+      uint8_t byte = bits[j * byteWidth + i / 8];
+      bool pixelOn = byte & (0x80 >> (i & 7));
+
+      if (pixelOn) {
+        for (int dy = 0; dy < DISPLAY_SCALE_X; dy++) {
+          for (int dx = 0; dx < DISPLAY_SCALE_X; dx++) {
+            display.drawPixel(x * DISPLAY_SCALE_X + i * DISPLAY_SCALE_X + dx, y * DISPLAY_SCALE_Y + j * DISPLAY_SCALE_X + dy, _color);
+          }
+        }
+      }
+    }
+  }
 }
 
 uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
@@ -138,4 +153,4 @@ uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
 
 void ST7789LCDDisplay::endFrame() {
   // display.display();
-}
+}

+ 5 - 5
variants/ebyte_eora_s3/platformio.ini

@@ -64,7 +64,7 @@ lib_deps =
 extends = Ebyte_EoRa-S3
 build_flags =
   ${Ebyte_EoRa-S3.build_flags}
-  -D MAX_CONTACTS=300
+  -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
@@ -99,8 +99,8 @@ build_flags =
   ${Ebyte_EoRa-S3.build_flags}
   -I examples/companion_radio/ui-new
   -D DISPLAY_CLASS=SSD1306Display
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -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 = ${Ebyte_EoRa-S3.build_src_filter}
@@ -118,8 +118,8 @@ build_flags =
   ${Ebyte_EoRa-S3.build_flags}
   -I examples/companion_radio/ui-new
   -D DISPLAY_CLASS=SSD1306Display
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456
   -D BLE_DEBUG_LOGGING=1
   -D OFFLINE_QUEUE_SIZE=256

+ 4 - 2
variants/generic-e22/platformio.ini

@@ -65,6 +65,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; lib_deps =
@@ -87,7 +88,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 lib_deps =
@@ -132,6 +133,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; lib_deps =
@@ -154,7 +156,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 lib_deps =

+ 2 - 2
variants/generic_espnow/platformio.ini

@@ -26,7 +26,7 @@ build_src_filter = ${esp32_base.build_src_filter}
 extends = Generic_ESPNOW
 build_flags =
   ${Generic_ESPNOW.build_flags}
-  -D MAX_CONTACTS=300
+  -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=1
 build_src_filter = ${Generic_ESPNOW.build_src_filter}
   +<../examples/simple_secure_chat/main.cpp>
@@ -54,7 +54,7 @@ lib_deps =
 extends = Generic_ESPNOW
 build_flags =
   ${Generic_ESPNOW.build_flags}
-  -D MAX_CONTACTS=300
+  -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=8
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1

+ 6 - 5
variants/heltec_ct62/platformio.ini

@@ -61,6 +61,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_ct62.build_src_filter}
@@ -80,7 +81,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_ct62.build_src_filter}
@@ -96,8 +97,8 @@ build_flags =
   ${Heltec_ct62.build_flags}
 ;  -D ARDUINO_USB_MODE=1
 ;  -D ARDUINO_USB_CDC_ON_BOOT=1
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D OFFLINE_QUEUE_SIZE=256
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
@@ -114,8 +115,8 @@ build_flags =
   ${Heltec_ct62.build_flags}
 ;  -D ARDUINO_USB_MODE=1
 ;  -D ARDUINO_USB_CDC_ON_BOOT=1
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D OFFLINE_QUEUE_SIZE=256
   -D BLE_PIN_CODE=123456
 ;  -D MESH_PACKET_LOGGING=1

+ 12 - 11
variants/heltec_e213/platformio.ini

@@ -40,13 +40,13 @@ lib_deps =
   ${esp32_base.lib_deps}
   https://github.com/Quency-D/heltec-eink-modules/archive/563dd41fd850a1bc3039b8723da4f3a20fe1c800.zip
 
-[env:Heltec_E213_companion_radio_ble]
+[env:Heltec_E213_companion_radio_ble_]
 extends = Heltec_E213_base
 build_flags =
   ${Heltec_E213_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=E213Display
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D BLE_DEBUG_LOGGING=1
@@ -60,13 +60,13 @@ lib_deps =
   ${Heltec_E213_base.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:Heltec_E213_companion_radio_usb]
+[env:Heltec_E213_companion_radio_usb_]
 extends = Heltec_E213_base
 build_flags =
   ${Heltec_E213_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=E213Display
   -D OFFLINE_QUEUE_SIZE=256
 build_src_filter = ${Heltec_E213_base.build_src_filter}
@@ -78,7 +78,7 @@ lib_deps =
   ${Heltec_E213_base.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:Heltec_E213_repeater]
+[env:Heltec_E213_repeater_]
 extends = Heltec_E213_base
 build_flags =
   ${Heltec_E213_base.build_flags}
@@ -95,7 +95,7 @@ lib_deps =
   ${Heltec_E213_base.lib_deps}
   ${esp32_ota.lib_deps}
 
-; [env:Heltec_E213_repeater_bridge_rs232]
+; [env:Heltec_E213_repeater_bridge_rs232_]
 ; extends = Heltec_E213_base
 ; build_flags =
 ;   ${Heltec_E213_base.build_flags}
@@ -108,6 +108,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_E213_base.build_src_filter}
@@ -118,7 +119,7 @@ lib_deps =
 ;   ${Heltec_E213_base.lib_deps}
 ;   ${esp32_ota.lib_deps}
 
-[env:Heltec_E213_repeater_bridge_espnow]
+[env:Heltec_E213_repeater_bridge_espnow_]
 extends = Heltec_E213_base
 build_flags =
   ${Heltec_E213_base.build_flags}
@@ -129,7 +130,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_E213_base.build_src_filter}
@@ -140,7 +141,7 @@ lib_deps =
   ${Heltec_E213_base.lib_deps}
   ${esp32_ota.lib_deps}
 
-[env:Heltec_E213_room_server]
+[env:Heltec_E213_room_server_]
 extends = Heltec_E213_base
 build_flags =
   ${Heltec_E213_base.build_flags}

+ 12 - 11
variants/heltec_e290/platformio.ini

@@ -34,13 +34,13 @@ lib_deps =
   ${esp32_base.lib_deps}
   https://github.com/Quency-D/heltec-eink-modules/archive/563dd41fd850a1bc3039b8723da4f3a20fe1c800.zip
 
-[env:Heltec_E290_companion_radio_ble]
+[env:Heltec_E290_companion_ble_]
 extends = Heltec_E290_base
 build_flags =
   ${Heltec_E290_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=E290Display
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D BLE_DEBUG_LOGGING=1
@@ -54,13 +54,13 @@ lib_deps =
   ${Heltec_E290_base.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:Heltec_E290_companion_radio_usb]
+[env:Heltec_E290_companion_usb_]
 extends = Heltec_E290_base
 build_flags =
   ${Heltec_E290_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=E290Display
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D BLE_DEBUG_LOGGING=1
@@ -74,7 +74,7 @@ lib_deps =
   ${Heltec_E290_base.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:Heltec_E290_repeater]
+[env:Heltec_E290_repeater_]
 extends = Heltec_E290_base
 build_flags =
   ${Heltec_E290_base.build_flags}
@@ -91,7 +91,7 @@ lib_deps =
   ${Heltec_E290_base.lib_deps}
   ${esp32_ota.lib_deps}
 
-; [env:Heltec_E290_repeater_bridge_rs232]
+; [env:Heltec_E290_repeater_bridge_rs232_]
 ; extends = Heltec_E290_base
 ; build_flags =
 ;   ${Heltec_E290_base.build_flags}
@@ -104,6 +104,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_E290_base.build_src_filter}
@@ -114,7 +115,7 @@ lib_deps =
 ;   ${Heltec_E290_base.lib_deps}
 ;   ${esp32_ota.lib_deps}
 
-[env:Heltec_E290_repeater_bridge_espnow]
+[env:Heltec_E290_repeater_bridge_espnow_]
 extends = Heltec_E290_base
 build_flags =
   ${Heltec_E290_base.build_flags}
@@ -125,7 +126,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_E290_base.build_src_filter}
@@ -136,7 +137,7 @@ lib_deps =
   ${Heltec_E290_base.lib_deps}
   ${esp32_ota.lib_deps}
 
-[env:Heltec_E290_room_server]
+[env:Heltec_E290_room_server_]
 extends = Heltec_E290_base
 build_flags =
   ${Heltec_E290_base.build_flags}

+ 0 - 39
variants/heltec_t114/T114Board.cpp

@@ -24,45 +24,6 @@ void T114Board::begin() {
 
   pinMode(PIN_VBAT_READ, INPUT);
 
-  // Enable SoftDevice low-power mode
-  sd_power_mode_set(NRF_POWER_MODE_LOWPWR);
-
-  // Enable DC/DC converter for better efficiency (REG1 stage)
-  NRF_POWER->DCDCEN = 1;
-
-  // Power down unused communication peripherals
-  // UART1 - Not used on T114
-  NRF_UARTE1->ENABLE = 0;
-
-  // SPIM2/SPIS2 - Not used (SPI is on SPIM0)
-  NRF_SPIM2->ENABLE = 0;
-  NRF_SPIS2->ENABLE = 0;
-
-  // TWI1 (I2C1) - Not used (I2C is on TWI0)
-  NRF_TWIM1->ENABLE = 0;
-  NRF_TWIS1->ENABLE = 0;
-
-  // PWM modules - Not used for standard T114 functions
-  NRF_PWM1->ENABLE = 0;
-  NRF_PWM2->ENABLE = 0;
-  NRF_PWM3->ENABLE = 0;
-
-  // PDM (Digital Microphone Interface) - Not used
-  NRF_PDM->ENABLE = 0;
-
-  // I2S - Not used
-  NRF_I2S->ENABLE = 0;
-
-  // QSPI - Not used (no external flash)
-  NRF_QSPI->ENABLE = 0;
-
-  // Disable unused analog peripherals
-  // SAADC channels - only keep what's needed for battery monitoring
-  NRF_SAADC->ENABLE = 0; // Re-enable only when needed for measurements
-
-  // COMP - Comparator not used
-  NRF_COMP->ENABLE = 0;
-
 #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
   Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
 #endif

+ 0 - 5
variants/heltec_t114/T114Board.h

@@ -27,9 +27,6 @@ public:
 
   uint16_t getBattMilliVolts() override {
     int adcvalue = 0;
-
-    NRF_SAADC->ENABLE = 1;
-
     analogReadResolution(12);
     analogReference(AR_INTERNAL_3_0);
     pinMode(PIN_BAT_CTL, OUTPUT);          // battery adc can be read only ctrl pin 6 set to high
@@ -39,8 +36,6 @@ public:
     adcvalue = analogRead(PIN_VBAT_READ);
     digitalWrite(6, 0);
 
-    NRF_SAADC->ENABLE = 0;
-
     return (uint16_t)((float)adcvalue * MV_LSB * 4.9);
   }
 

+ 1 - 0
variants/heltec_t114/platformio.ini

@@ -170,6 +170,7 @@ build_flags =
   -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456
+  -D ENV_INCLUDE_GPS=1   ; enable the GPS page in UI
 ;  -D BLE_DEBUG_LOGGING=1
   -D OFFLINE_QUEUE_SIZE=256
 ;  -D MESH_PACKET_LOGGING=1

+ 1 - 2
variants/heltec_t114/target.cpp

@@ -74,11 +74,10 @@ bool T114SensorManager::begin() {
 
   if (gps_detected) {
     MESH_DEBUG_PRINTLN("GPS detected");
-    digitalWrite(GPS_EN, LOW);  // Power off GPS until the setting is changed
   } else {
     MESH_DEBUG_PRINTLN("No GPS detected");
-    digitalWrite(GPS_EN, LOW);
   }
+  digitalWrite(GPS_EN, LOW);  // Power off GPS until the setting is changed
 
   return true;
 }

+ 1 - 0
variants/heltec_t114/target.h

@@ -30,6 +30,7 @@ public:
   bool begin() override;
   bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
   void loop() override;
+  LocationProvider* getLocationProvider() override { return gps_detected ? _location : NULL; }
   int getNumSettings() const override;
   const char* getSettingName(int i) const override;
   const char* getSettingValue(int i) const override;

+ 12 - 11
variants/heltec_t190/platformio.ini

@@ -47,13 +47,13 @@ lib_deps =
   ${esp32_base.lib_deps}
   adafruit/Adafruit GFX Library @ ^1.12.1
 
-[env:Heltec_T190_companion_radio_ble]
+[env:Heltec_T190_companion_radio_ble_]
 extends = Heltec_T190_base
 build_flags =
   ${Heltec_T190_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D BLE_DEBUG_LOGGING=1
   -D OFFLINE_QUEUE_SIZE=256
@@ -65,13 +65,13 @@ lib_deps =
   ${Heltec_T190_base.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:Heltec_T190_companion_radio_usb]
+[env:Heltec_T190_companion_radio_usb_]
 extends = Heltec_T190_base
 build_flags =
   ${Heltec_T190_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D OFFLINE_QUEUE_SIZE=256
 build_src_filter = ${Heltec_T190_base.build_src_filter}
   +<helpers/esp32/*.cpp>
@@ -81,7 +81,7 @@ lib_deps =
   ${Heltec_T190_base.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:Heltec_T190_repeater]
+[env:Heltec_T190_repeater_]
 extends = Heltec_T190_base
 build_flags =
   ${Heltec_T190_base.build_flags}
@@ -96,7 +96,7 @@ lib_deps =
   ${Heltec_T190_base.lib_deps}
   ${esp32_ota.lib_deps}
 
-; [env:Heltec_T190_repeater_bridge_rs232]
+; [env:Heltec_T190_repeater_bridge_rs232_]
 ; extends = Heltec_T190_base
 ; build_flags =
 ;   ${Heltec_T190_base.build_flags}
@@ -108,6 +108,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_T190_base.build_src_filter}
@@ -117,7 +118,7 @@ lib_deps =
 ;   ${Heltec_T190_base.lib_deps}
 ;   ${esp32_ota.lib_deps}
 
-[env:Heltec_T190_repeater_bridge_espnow]
+[env:Heltec_T190_repeater_bridge_espnow_]
 extends = Heltec_T190_base
 build_flags =
   ${Heltec_T190_base.build_flags}
@@ -127,7 +128,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_T190_base.build_src_filter}
@@ -137,7 +138,7 @@ lib_deps =
   ${Heltec_T190_base.lib_deps}
   ${esp32_ota.lib_deps}
 
-[env:Heltec_T190_room_server]
+[env:Heltec_T190_room_server_]
 extends = Heltec_T190_base
 build_flags =
   ${Heltec_T190_base.build_flags}

+ 5 - 4
variants/heltec_tracker/platformio.ini

@@ -5,6 +5,7 @@ build_flags =
   ${esp32_base.build_flags}
   -I variants/heltec_tracker
   -D HELTEC_LORA_V3
+  -D ARDUINO_USB_CDC_ON_BOOT=1      ; need for Serial
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
   -D LORA_TX_POWER=22
@@ -40,11 +41,10 @@ build_flags =
   ${Heltec_tracker_base.build_flags}
   -I src/helpers/ui
   -I examples/companion_radio/ui-new
-  -D ARDUINO_USB_CDC_ON_BOOT=1      ; need for Serial
   -D DISPLAY_ROTATION=1
   -D DISPLAY_CLASS=ST7735Display
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -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
@@ -94,6 +94,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_tracker_base.build_src_filter}
@@ -116,7 +117,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_tracker_base.build_src_filter}

+ 88 - 0
variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp

@@ -0,0 +1,88 @@
+#include "HeltecTrackerV2Board.h"
+
+void HeltecTrackerV2Board::begin() {
+    ESP32Board::begin();
+
+    pinMode(PIN_ADC_CTRL, OUTPUT);
+    digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive
+
+    pinMode(P_LORA_PA_POWER, OUTPUT);
+    digitalWrite(P_LORA_PA_POWER,HIGH);
+
+    rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN);
+    pinMode(P_LORA_PA_EN, OUTPUT);
+    digitalWrite(P_LORA_PA_EN,HIGH);
+    pinMode(P_LORA_PA_TX_EN, OUTPUT);
+    digitalWrite(P_LORA_PA_TX_EN,LOW);
+
+    periph_power.begin();
+
+    esp_reset_reason_t reason = esp_reset_reason();
+    if (reason == ESP_RST_DEEPSLEEP) {
+      long wakeup_source = esp_sleep_get_ext1_wakeup_status();
+      if (wakeup_source & (1 << P_LORA_DIO_1)) {  // received a LoRa packet (while in deep sleep)
+        startup_reason = BD_STARTUP_RX_PACKET;
+      }
+
+      rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
+      rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
+    }
+  }
+
+  void HeltecTrackerV2Board::onBeforeTransmit(void) {
+    digitalWrite(P_LORA_TX_LED, HIGH);   // turn TX LED on
+    digitalWrite(P_LORA_PA_TX_EN,HIGH);
+  }
+
+  void HeltecTrackerV2Board::onAfterTransmit(void) {
+    digitalWrite(P_LORA_TX_LED, LOW);   // turn TX LED off
+    digitalWrite(P_LORA_PA_TX_EN,LOW);
+  }
+
+  void HeltecTrackerV2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
+    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
+
+    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
+    rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
+    rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
+
+    rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
+
+    rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); //It also needs to be enabled in receive mode
+
+    if (pin_wake_btn < 0) {
+      esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);  // wake up on: recv LoRa packet
+    } else {
+      esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);  // wake up on: recv LoRa packet OR wake btn
+    }
+
+    if (secs > 0) {
+      esp_sleep_enable_timer_wakeup(secs * 1000000);
+    }
+
+    // Finally set ESP32 into sleep
+    esp_deep_sleep_start();   // CPU halts here and never returns!
+  }
+
+  void HeltecTrackerV2Board::powerOff()  {
+    enterDeepSleep(0);
+  }
+
+  uint16_t HeltecTrackerV2Board::getBattMilliVolts()  {
+    analogReadResolution(10);
+    digitalWrite(PIN_ADC_CTRL, HIGH);
+    delay(10);
+    uint32_t raw = 0;
+    for (int i = 0; i < 8; i++) {
+      raw += analogRead(PIN_VBAT_READ);
+    }
+    raw = raw / 8;
+
+    digitalWrite(PIN_ADC_CTRL, LOW);
+
+    return (5.42 * (3.3 / 1024.0) * raw) * 1000;
+  }
+
+  const char* HeltecTrackerV2Board::getManufacturerName() const {
+    return "Heltec Tracker V2";
+  }

+ 23 - 0
variants/heltec_tracker_v2/HeltecTrackerV2Board.h

@@ -0,0 +1,23 @@
+#pragma once
+
+#include <Arduino.h>
+#include <helpers/RefCountedDigitalPin.h>
+#include <helpers/ESP32Board.h>
+#include <driver/rtc_io.h>
+
+class HeltecTrackerV2Board : public ESP32Board {
+
+public:
+  RefCountedDigitalPin periph_power;
+
+  HeltecTrackerV2Board() : periph_power(PIN_VEXT_EN,PIN_VEXT_EN_ACTIVE) { }
+
+  void begin();
+  void onBeforeTransmit(void) override;
+  void onAfterTransmit(void) override;
+  void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
+  void powerOff() override;
+  uint16_t getBattMilliVolts() override;
+  const char* getManufacturerName() const override ;
+
+};

+ 60 - 0
variants/heltec_tracker_v2/pins_arduino.h

@@ -0,0 +1,60 @@
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include <stdint.h>
+
+static const uint8_t LED_BUILTIN = 18;
+#define BUILTIN_LED  LED_BUILTIN // backward compatibility
+#define LED_BUILTIN LED_BUILTIN  // allow testing #ifdef LED_BUILTIN
+
+static const uint8_t TX = 43;
+static const uint8_t RX = 44;
+
+static const uint8_t SDA = 5;
+static const uint8_t SCL = 6;
+
+static const uint8_t SS    = 8;
+static const uint8_t MOSI  = 10;
+static const uint8_t MISO  = 11;
+static const uint8_t SCK   = 9;
+
+static const uint8_t A0 = 1;
+static const uint8_t A1 = 2;
+static const uint8_t A2 = 3;
+static const uint8_t A3 = 4;
+static const uint8_t A4 = 5;
+static const uint8_t A5 = 6;
+static const uint8_t A6 = 7;
+static const uint8_t A7 = 8;
+static const uint8_t A8 = 9;
+static const uint8_t A9 = 10;
+static const uint8_t A10 = 11;
+static const uint8_t A11 = 12;
+static const uint8_t A12 = 13;
+static const uint8_t A13 = 14;
+static const uint8_t A14 = 15;
+static const uint8_t A15 = 16;
+static const uint8_t A16 = 17;
+static const uint8_t A17 = 18;
+static const uint8_t A18 = 19;
+static const uint8_t A19 = 20;
+
+static const uint8_t T1 = 1;
+static const uint8_t T2 = 2;
+static const uint8_t T3 = 3;
+static const uint8_t T4 = 4;
+static const uint8_t T5 = 5;
+static const uint8_t T6 = 6;
+static const uint8_t T7 = 7;
+static const uint8_t T8 = 8;
+static const uint8_t T9 = 9;
+static const uint8_t T10 = 10;
+static const uint8_t T11 = 11;
+static const uint8_t T12 = 12;
+static const uint8_t T13 = 13;
+static const uint8_t T14 = 14;
+
+static const uint8_t Vext = 3;
+static const uint8_t LED  = 18;
+
+#endif /* Pins_Arduino_h */

+ 218 - 0
variants/heltec_tracker_v2/platformio.ini

@@ -0,0 +1,218 @@
+[Heltec_tracker_v2]
+extends = esp32_base
+board = heltec_tracker_v2
+build_flags =
+  ${esp32_base.build_flags}
+  ${sensor_base.build_flags}
+  -I variants/heltec_tracker_v2
+  -D HELTEC_TRACKER_V2
+  -D ESP32_CPU_FREQ=160
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D P_LORA_TX_LED=18
+  -D P_LORA_DIO_1=14
+  -D P_LORA_NSS=8
+  -D P_LORA_RESET=12
+  -D P_LORA_BUSY=13
+  -D P_LORA_SCLK=9
+  -D P_LORA_MISO=11
+  -D P_LORA_MOSI=10
+  -D P_LORA_PA_POWER=7 ;power en
+  -D P_LORA_PA_EN=4
+  -D P_LORA_PA_TX_EN=46  ;enable tx
+  -D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
+  -D MAX_LORA_TX_POWER=22 ;Max SX1262 output
+  -D SX126X_DIO2_AS_RF_SWITCH=true
+  -D SX126X_DIO3_TCXO_VOLTAGE=1.8
+  -D SX126X_CURRENT_LIMIT=140
+  -D SX126X_RX_BOOSTED_GAIN=1
+  -D PIN_BOARD_SDA=5
+  -D PIN_BOARD_SCL=6
+  -D PIN_USER_BTN=0
+  -D PIN_TFT_SDA=42   ; SDIN
+  -D PIN_TFT_SCL=41   ; SCLK
+  -D PIN_TFT_DC=40    ; RS (register select)
+  -D PIN_TFT_RST=39   ; RES
+  -D PIN_TFT_CS=38
+  -D USE_PIN_TFT=1
+  -D PIN_VEXT_EN=3   ; Vext is connected to VDD which is also connected to OLED & GPS
+  -D PIN_VEXT_EN_ACTIVE=HIGH
+  -D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground)
+  -D DISPLAY_ROTATION=1
+  -D PIN_GPS_RX=34
+  -D PIN_GPS_TX=33
+  -D PIN_GPS_RESET=35
+  -D PIN_GPS_RESET_ACTIVE=LOW
+  -D GPS_BAUD_RATE=115200
+  -D ENV_INCLUDE_GPS=1
+  -D PIN_ADC_CTRL=2
+  -D PIN_VBAT_READ=1
+build_src_filter = ${esp32_base.build_src_filter}
+  +<../variants/heltec_tracker_v2>
+  +<helpers/sensors>
+lib_deps =
+  ${esp32_base.lib_deps}
+  ${sensor_base.lib_deps}
+  adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
+
+[env:heltec_tracker_v2_repeater]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -D DISPLAY_CLASS=ST7735Display
+  -D ADVERT_NAME='"Heltec Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/ui/ST7735Display.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  ${esp32_ota.lib_deps}
+  bakercp/CRC32 @ ^2.0.0
+
+[env:heltec_tracker_v2_repeater_bridge_espnow]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -D DISPLAY_CLASS=ST7735Display
+  -D ADVERT_NAME='"ESPNow Bridge"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+  -D WITH_ESPNOW_BRIDGE=1
+;  -D BRIDGE_DEBUG=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/bridges/ESPNowBridge.cpp>
+  +<helpers/ui/ST7735Display.cpp>
+  +<../examples/simple_repeater>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:heltec_tracker_v2_room_server]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -D DISPLAY_CLASS=ST7735Display
+  -D ADVERT_NAME='"Heltec Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/ui/ST7735Display.cpp>
+  +<../examples/simple_room_server>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:heltec_tracker_v2_terminal_chat]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<../examples/simple_secure_chat/main.cpp>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_tracker_v2_companion_radio_usb]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=ST7735Display
+; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
+; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/ui/ST7735Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_tracker_v2_companion_radio_ble]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=ST7735Display
+  -D BLE_PIN_CODE=123456   ; dynamic, random PIN
+  -D AUTO_SHUTDOWN_MILLIVOLTS=3400
+  -D BLE_DEBUG_LOGGING=1
+  -D OFFLINE_QUEUE_SIZE=256
+  ; -D BLE_DEBUG_LOGGING=1
+  ; -D MESH_PACKET_LOGGING=1
+  ; -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/ui/ST7735Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_tracker_v2_companion_radio_wifi]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D DISPLAY_CLASS=ST7735Display
+  -D WIFI_DEBUG_LOGGING=1
+  -D WIFI_SSID='"myssid"'
+  -D WIFI_PWD='"mypwd"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/ui/ST7735Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:heltec_tracker_v2_sensor]
+extends = Heltec_tracker_v2
+build_flags =
+  ${Heltec_tracker_v2.build_flags}
+  -D ADVERT_NAME='"Heltec Tracker V2 Sensor"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ENV_PIN_SDA=3
+  -D ENV_PIN_SCL=4
+  -D DISPLAY_CLASS=ST7735Display
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_tracker_v2.build_src_filter}
+  +<helpers/ui/ST7735Display.cpp>
+  +<../examples/simple_sensor>
+lib_deps =
+  ${Heltec_tracker_v2.lib_deps}
+  ${esp32_ota.lib_deps}

+ 60 - 0
variants/heltec_tracker_v2/target.cpp

@@ -0,0 +1,60 @@
+#include <Arduino.h>
+#include "target.h"
+
+HeltecTrackerV2Board board;
+
+#if defined(P_LORA_SCLK)
+  static SPIClass spi;
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
+#else
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
+#endif
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+ESP32RTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+
+#if ENV_INCLUDE_GPS
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+  MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, NULL, GPS_RESET, GPS_EN, &board.periph_power);
+  EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
+#else
+  EnvironmentSensorManager sensors;
+#endif
+
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display(&board.periph_power);   // peripheral power pin is shared
+  MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
+#endif
+
+bool radio_init() {
+  fallback_clock.begin();
+  rtc_clock.begin(Wire);
+  
+#if defined(P_LORA_SCLK)
+  return radio.std_init(&spi);
+#else
+  return radio.std_init();
+#endif
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 30 - 0
variants/heltec_tracker_v2/target.h

@@ -0,0 +1,30 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <HeltecTrackerV2Board.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/AutoDiscoverRTCClock.h>
+#include <helpers/SensorManager.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+#ifdef DISPLAY_CLASS
+  #include <helpers/ui/ST7735Display.h>
+  #include <helpers/ui/MomentaryButton.h>
+#endif
+
+extern HeltecTrackerV2Board board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern EnvironmentSensorManager sensors;
+
+#ifdef DISPLAY_CLASS
+  extern DISPLAY_CLASS display;
+  extern MomentaryButton user_btn;
+#endif
+
+bool radio_init();
+uint32_t radio_get_rng_seed();
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
+void radio_set_tx_power(uint8_t dbm);
+mesh::LocalIdentity radio_new_identity();

+ 2 - 12
src/helpers/HeltecV2Board.h → variants/heltec_v2/HeltecV2Board.h

@@ -1,22 +1,12 @@
 #pragma once
 
 #include <Arduino.h>
-
-// LoRa radio module pins for Heltec V2
-#define  P_LORA_DIO_1   26    // DIO0
-#define  P_LORA_NSS     18
-#define  P_LORA_RESET   RADIOLIB_NC  // 14
-#define  P_LORA_BUSY    RADIOLIB_NC
-#define  P_LORA_SCLK     5
-#define  P_LORA_MISO    19
-#define  P_LORA_MOSI    27
+#include <helpers/ESP32Board.h>
 
 // built-ins
 #define  PIN_VBAT_READ   37
 #define  PIN_LED_BUILTIN 25
 
-#include "ESP32Board.h"
-
 #include <driver/rtc_io.h>
 
 class HeltecV2Board : public ESP32Board {
@@ -39,7 +29,7 @@ public:
   void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
     esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 
-    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep 
+    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
     rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
     rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
 

+ 36 - 5
variants/heltec_v2/platformio.ini

@@ -7,13 +7,20 @@ build_flags =
   -D HELTEC_LORA_V2
   -D RADIO_CLASS=CustomSX1276
   -D WRAPPER_CLASS=CustomSX1276Wrapper
+  -D P_LORA_DIO_1=26
+  -D P_LORA_NSS=18
+  -D P_LORA_RESET=RADIOLIB_NC
+  -D P_LORA_BUSY=RADIOLIB_NC
+  -D P_LORA_SCLK=5
+  -D P_LORA_MISO=19
+  -D P_LORA_MOSI=27
+  -D P_LORA_TX_LED=25
   -D SX127X_CURRENT_LIMIT=120
   -D LORA_TX_POWER=20
   -D PIN_BOARD_SDA=4
   -D PIN_BOARD_SCL=15
   -D PIN_USER_BTN=0
   -D PIN_OLED_RESET=16
-  -D P_LORA_TX_LED=25
 build_src_filter = ${esp32_base.build_src_filter}
   +<../variants/heltec_v2>
 lib_deps =
@@ -53,6 +60,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_lora32_v2.build_src_filter}
@@ -75,7 +83,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v2.build_src_filter}
@@ -111,7 +119,7 @@ lib_deps =
 extends = Heltec_lora32_v2
 build_flags =
   ${Heltec_lora32_v2.build_flags}
-  -D MAX_CONTACTS=170
+  -D MAX_CONTACTS=160
   -D MAX_GROUP_CHANNELS=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
@@ -127,7 +135,7 @@ build_flags =
   ${Heltec_lora32_v2.build_flags}
   -I examples/companion_radio/ui-new
   -D DISPLAY_CLASS=SSD1306Display
-  -D MAX_CONTACTS=170
+  -D MAX_CONTACTS=160
   -D MAX_GROUP_CHANNELS=8
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
@@ -147,7 +155,7 @@ build_flags =
   ${Heltec_lora32_v2.build_flags}
   -I examples/companion_radio/ui-new
   -D DISPLAY_CLASS=SSD1306Display
-  -D MAX_CONTACTS=170
+  -D MAX_CONTACTS=160
   -D MAX_GROUP_CHANNELS=8
   -D BLE_PIN_CODE=123456
   -D BLE_DEBUG_LOGGING=1
@@ -163,3 +171,26 @@ build_src_filter = ${Heltec_lora32_v2.build_src_filter}
 lib_deps =
   ${Heltec_lora32_v2.lib_deps}
   densaugeo/base64 @ ~1.4.0
+
+[env:Heltec_v2_companion_radio_wifi]
+extends = Heltec_lora32_v2
+build_flags =
+  ${Heltec_lora32_v2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D DISPLAY_CLASS=SSD1306Display
+  -D MAX_CONTACTS=160
+  -D MAX_GROUP_CHANNELS=8
+  -D WIFI_DEBUG_LOGGING=1
+  -D WIFI_SSID='"myssid"'
+  -D WIFI_PWD='"mypwd"'
+; -D MESH_PACKET_LOGGING=1
+; -D MESH_DEBUG=1
+build_src_filter = ${Heltec_lora32_v2.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Heltec_lora32_v2.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 1 - 1
variants/heltec_v2/target.h

@@ -3,7 +3,7 @@
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
 #include <helpers/radiolib/RadioLibWrappers.h>
-#include <helpers/HeltecV2Board.h>
+#include <HeltecV2Board.h>
 #include <helpers/radiolib/CustomSX1276Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>

+ 3 - 15
src/helpers/HeltecV3Board.h → variants/heltec_v3/HeltecV3Board.h

@@ -2,16 +2,7 @@
 
 #include <Arduino.h>
 #include <helpers/RefCountedDigitalPin.h>
-
-// LoRa radio module pins for Heltec V3
-// Also for Heltec Wireless Tracker/Paper
-#define  P_LORA_DIO_1   14
-#define  P_LORA_NSS      8
-#define  P_LORA_RESET   RADIOLIB_NC
-#define  P_LORA_BUSY    13
-#define  P_LORA_SCLK     9
-#define  P_LORA_MISO    11
-#define  P_LORA_MOSI    10
+#include <helpers/ESP32Board.h>
 
 // built-ins
 #ifndef PIN_VBAT_READ              // set in platformio.ini for boards like Heltec Wireless Paper (20)
@@ -22,9 +13,6 @@
 #endif
 #define  PIN_ADC_CTRL_ACTIVE    LOW
 #define  PIN_ADC_CTRL_INACTIVE  HIGH
-//#define  PIN_LED_BUILTIN 35
-
-#include "ESP32Board.h"
 
 #include <driver/rtc_io.h>
 
@@ -43,7 +31,7 @@ public:
     // Auto-detect correct ADC_CTRL pin polarity (different for boards >3.2)
     pinMode(PIN_ADC_CTRL, INPUT);
     adc_active_state = !digitalRead(PIN_ADC_CTRL);
-    
+
     pinMode(PIN_ADC_CTRL, OUTPUT);
     digitalWrite(PIN_ADC_CTRL, !adc_active_state); // Initially inactive
 
@@ -64,7 +52,7 @@ public:
   void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
     esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 
-    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep 
+    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
     rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
     rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
 

+ 39 - 12
variants/heltec_v3/platformio.ini

@@ -7,6 +7,13 @@ build_flags =
   -I variants/heltec_v3
   -D HELTEC_LORA_V3
   -D ESP32_CPU_FREQ=80
+  -D P_LORA_DIO_1=14
+  -D P_LORA_NSS=8
+  -D P_LORA_RESET=RADIOLIB_NC
+  -D P_LORA_BUSY=13
+  -D P_LORA_SCLK=9
+  -D P_LORA_MISO=11
+  -D P_LORA_MOSI=10
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
   -D LORA_TX_POWER=22
@@ -62,6 +69,7 @@ build_flags =
   -D WITH_RS232_BRIDGE=Serial2
   -D WITH_RS232_BRIDGE_RX=5
   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
@@ -83,7 +91,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
@@ -117,7 +125,7 @@ lib_deps =
 extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
-  -D MAX_CONTACTS=300
+  -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
@@ -132,8 +140,8 @@ extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
@@ -151,8 +159,8 @@ extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D AUTO_SHUTDOWN_MILLIVOLTS=3400
@@ -175,8 +183,8 @@ extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'
@@ -243,6 +251,7 @@ build_flags =
   -D WITH_RS232_BRIDGE=Serial2
   -D WITH_RS232_BRIDGE_RX=5
   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
@@ -263,7 +272,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
@@ -295,8 +304,8 @@ lib_deps =
 extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D BLE_PIN_CODE=123456
   -D BLE_DEBUG_LOGGING=1
   -D OFFLINE_QUEUE_SIZE=256
@@ -314,7 +323,7 @@ extends = Heltec_lora32_v3
 build_flags =
   ${Heltec_lora32_v3.build_flags}
   -D MAX_CONTACTS=140
-  -D MAX_GROUP_CHANNELS=8
+  -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 = ${Heltec_lora32_v3.build_src_filter}
@@ -323,6 +332,24 @@ lib_deps =
   ${Heltec_lora32_v3.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
+[env:Heltec_WSL3_companion_radio_wifi]
+extends = Heltec_lora32_v3
+build_flags =
+  ${Heltec_lora32_v3.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D WIFI_DEBUG_LOGGING=1
+  -D WIFI_SSID='"myssid"'
+  -D WIFI_PWD='"mypwd"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+lib_deps =
+  ${Heltec_lora32_v3.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
 [env:Heltec_WSL3_sensor]
 extends = Heltec_lora32_v3
 build_flags =

+ 1 - 1
variants/heltec_v3/target.h

@@ -3,7 +3,7 @@
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
 #include <helpers/radiolib/RadioLibWrappers.h>
-#include <helpers/HeltecV3Board.h>
+#include <HeltecV3Board.h>
 #include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>

+ 9 - 8
variants/heltec_v4/platformio.ini

@@ -26,6 +26,7 @@ build_flags =
   -D PIN_VEXT_EN=36
   -D PIN_VEXT_EN_ACTIVE=HIGH
   -D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm.
+  -D MAX_LORA_TX_POWER=22 ; Max SX1262 output
   -D SX126X_DIO2_AS_RF_SWITCH=true
   -D SX126X_DIO3_TCXO_VOLTAGE=1.8
   -D SX126X_CURRENT_LIMIT=140
@@ -77,7 +78,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v4.build_src_filter}
@@ -111,7 +112,7 @@ lib_deps =
 extends = Heltec_lora32_v4
 build_flags =
   ${Heltec_lora32_v4.build_flags}
-  -D MAX_CONTACTS=300
+  -D MAX_CONTACTS=350
   -D MAX_GROUP_CHANNELS=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
@@ -126,8 +127,8 @@ extends = Heltec_lora32_v4
 build_flags =
   ${Heltec_lora32_v4.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
@@ -145,8 +146,8 @@ extends = Heltec_lora32_v4
 build_flags =
   ${Heltec_lora32_v4.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D AUTO_SHUTDOWN_MILLIVOLTS=3400
@@ -169,8 +170,8 @@ extends = Heltec_lora32_v4
 build_flags =
   ${Heltec_lora32_v4.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=SSD1306Display
   -D WIFI_DEBUG_LOGGING=1
   -D WIFI_SSID='"myssid"'

+ 4 - 3
variants/heltec_wireless_paper/platformio.ini

@@ -38,8 +38,8 @@ extends = Heltec_Wireless_Paper_base
 build_flags =
   ${Heltec_Wireless_Paper_base.build_flags}
   -I examples/companion_radio/ui-new
-  -D MAX_CONTACTS=300
-  -D MAX_GROUP_CHANNELS=8
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
   -D DISPLAY_CLASS=E213Display
   -D BLE_PIN_CODE=123456   ; dynamic, random PIN
   -D BLE_DEBUG_LOGGING=1
@@ -83,6 +83,7 @@ lib_deps =
 ;   -D WITH_RS232_BRIDGE=Serial2
 ;   -D WITH_RS232_BRIDGE_RX=5
 ;   -D WITH_RS232_BRIDGE_TX=6
+;  -D BRIDGE_DEBUG=1
 ; ;  -D MESH_PACKET_LOGGING=1
 ; ;  -D MESH_DEBUG=1
 ; build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter}
@@ -104,7 +105,7 @@ build_flags =
   -D ADMIN_PASSWORD='"password"'
   -D MAX_NEIGHBOURS=50
   -D WITH_ESPNOW_BRIDGE=1
-  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D BRIDGE_DEBUG=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter}

+ 5 - 10
variants/ikoka_stick_nrf/ikoka_stick_nrf_board.cpp → variants/ikoka_nano_nrf/IkokaNanoNRFBoard.cpp

@@ -1,28 +1,26 @@
 #ifdef XIAO_NRF52
 
 #include <Arduino.h>
-#include "ikoka_stick_nrf_board.h"
+#include "IkokaNanoNRFBoard.h"
 
 #include <bluefruit.h>
 #include <Wire.h>
 
 static BLEDfu bledfu;
 
-static void connect_callback(uint16_t conn_handle)
-{
+static void connect_callback(uint16_t conn_handle) {
   (void)conn_handle;
   MESH_DEBUG_PRINTLN("BLE client connected");
 }
 
-static void disconnect_callback(uint16_t conn_handle, uint8_t reason)
-{
+static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
   (void)conn_handle;
   (void)reason;
 
   MESH_DEBUG_PRINTLN("BLE client disconnected");
 }
 
-void ikoka_stick_nrf_board::begin() {
+void IkokaNanoNRFBoard::begin() {
   // for future use, sub-classes SHOULD call this from their begin()
   startup_reason = BD_STARTUP_NORMAL;
 
@@ -50,7 +48,7 @@ void ikoka_stick_nrf_board::begin() {
   delay(10);   // give sx1262 some time to power up
 }
 
-bool ikoka_stick_nrf_board::startOTAUpdate(const char* id, char reply[]) {
+bool IkokaNanoNRFBoard::startOTAUpdate(const char *id, char reply[]) {
   // Config the peripheral connection with maximum bandwidth
   // more SRAM required by SoftDevice
   // Note: All config***() function must be called before begin()
@@ -91,9 +89,6 @@ bool ikoka_stick_nrf_board::startOTAUpdate(const char* id, char reply[]) {
 
   strcpy(reply, "OK - started");
   return true;
-
-
-  return false;
 }
 
 #endif

+ 12 - 4
variants/ikoka_stick_nrf/ikoka_stick_nrf_board.h → variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h

@@ -5,7 +5,7 @@
 
 #ifdef XIAO_NRF52
 
-class ikoka_stick_nrf_board : public mesh::MainBoard {
+class IkokaNanoNRFBoard : public mesh::MainBoard {
 protected:
   uint8_t startup_reason;
 
@@ -16,9 +16,17 @@ public:
 #if defined(P_LORA_TX_LED)
   void onBeforeTransmit() override {
     digitalWrite(P_LORA_TX_LED, LOW);   // turn TX LED on
+    #if defined(LED_BLUE)
+       // turn off that annoying blue LED before transmitting
+       digitalWrite(LED_BLUE, HIGH);
+    #endif
   }
   void onAfterTransmit() override {
     digitalWrite(P_LORA_TX_LED, HIGH);   // turn TX LED off
+    #if defined(LED_BLUE)
+       // do it after transmitting too, just in case
+       digitalWrite(LED_BLUE, HIGH);
+    #endif
   }
 #endif
 
@@ -38,15 +46,15 @@ public:
     return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
   }
 
-  const char* getManufacturerName() const override {
-    return "Ikoka Stick (Xiao-nrf52)";
+  const char *getManufacturerName() const override {
+    return MANUFACTURER_STRING;
   }
 
   void reboot() override {
     NVIC_SystemReset();
   }
 
-  bool startOTAUpdate(const char* id, char reply[]) override;
+  bool startOTAUpdate(const char *id, char reply[]) override;
 };
 
 #endif

+ 312 - 0
variants/ikoka_nano_nrf/platformio.ini

@@ -0,0 +1,312 @@
+[nrf52840_xiao]
+extends = nrf52_base
+platform_packages =
+  toolchain-gccarmnoneeabi@~1.100301.0
+  framework-arduinoadafruitnrf52
+board = seeed-xiao-afruitnrf52-nrf52840
+board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52_base.build_flags}
+  -D NRF52_PLATFORM -D XIAO_NRF52
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
+lib_ignore =
+  BluetoothOTA
+  lvgl
+  lib5b4
+lib_deps =
+  ${nrf52_base.lib_deps}
+  rweather/Crypto @ ^0.4.0
+  adafruit/Adafruit INA3221 Library @ ^1.0.1
+  adafruit/Adafruit INA219 @ ^1.2.3
+  adafruit/Adafruit AHTX0 @ ^2.0.5
+  adafruit/Adafruit BME280 Library @ ^2.3.0
+  adafruit/Adafruit SSD1306 @ ^2.5.13
+
+[ikoka_nano_nrf_baseboard]
+extends = nrf52840_xiao
+;board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52840_xiao.build_flags}
+  -D P_LORA_TX_LED=11
+  -I variants/ikoka_nano_nrf
+  -I src/helpers/nrf52
+  -D DISPLAY_CLASS=NullDisplayDriver
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D P_LORA_DIO_1=D1
+;  -D P_LORA_BUSY=D3
+  -D P_LORA_BUSY=D2 ; specific to ikoka nano variant.
+;  -D P_LORA_RESET=D2
+  -D P_LORA_RESET=D3 ; specific to ikoka nano variant.
+;  -D P_LORA_NSS=D4
+  -D P_LORA_NSS=D0 ; specific to ikoka nano variant.
+;  -D SX126X_RXEN=D5
+  -D SX126X_RXEN=D7
+  -D SX126X_TXEN=RADIOLIB_NC
+  -D SX126X_DIO2_AS_RF_SWITCH=1
+  -D SX126X_DIO3_TCXO_VOLTAGE=1.8
+  -D SX126X_CURRENT_LIMIT=140
+  -D SX126X_RX_BOOSTED_GAIN=1
+  -D PIN_WIRE_SCL=5 ; specific to ikoka nano variant.
+  -D PIN_WIRE_SDA=4 ; specific to ikoka nano variant.
+  -D ENV_INCLUDE_AHTX0=1
+  -D ENV_INCLUDE_BME280=1
+  -D ENV_INCLUDE_INA3221=1
+  -D ENV_INCLUDE_INA219=1
+debug_tool = jlink
+upload_protocol = nrfutil
+
+
+;;; abstracted hardware variants
+
+[ikoka_nano_nrf_e22_22dbm]
+extends = ikoka_nano_nrf_baseboard
+; No PA in this model, full 22dBm
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D MANUFACTURER_STRING='"Ikoka Nano-E22-22dBm (Xiao_nrf52)"'
+  -D LORA_TX_POWER=22
+build_src_filter = ${nrf52840_xiao.build_src_filter}
+  +<helpers/*.cpp>
+  +<helpers/sensors>
+  +<helpers/ui/NullDisplayDriver.cpp>
+  +<../variants/ikoka_nano_nrf>
+
+[ikoka_nano_nrf_e22_30dbm]
+extends = ikoka_nano_nrf_baseboard
+; limit txpower to 20dBm on E22-900M30S. Anything higher will
+; cause distortion in the PA output. 20dBm in -> 30dBm out
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D MANUFACTURER_STRING='"Ikoka Nano-E22-30dBm (Xiao_nrf52)"'
+  -D LORA_TX_POWER=20
+build_src_filter = ${nrf52840_xiao.build_src_filter}
+  +<helpers/*.cpp>
+  +<helpers/sensors>
+  +<helpers/ui/NullDisplayDriver.cpp>
+  +<../variants/ikoka_nano_nrf>
+
+[ikoka_nano_nrf_e22_33dbm]
+extends = ikoka_nano_nrf_baseboard
+; limit txpower to 9dBm on E22-900M33S to avoid hardware damage
+; to the rf amplifier frontend. 9dBm in -> 33dBm out
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D MANUFACTURER_STRING='"Ikoka Nano-E22-33dBm (Xiao_nrf52)"'
+  -D LORA_TX_POWER=9
+build_src_filter = ${nrf52840_xiao.build_src_filter}
+  +<helpers/*.cpp>
+  +<helpers/sensors>
+  +<helpers/ui/NullDisplayDriver.cpp>
+  +<../variants/ikoka_nano_nrf>
+
+;;; abstracted firmware roles
+
+[ikoka_nano_nrf_companion_radio_ble]
+extends = ikoka_nano_nrf_baseboard
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D BLE_PIN_CODE=123456
+  -D OFFLINE_QUEUE_SIZE=256
+  -I examples/companion_radio/ui-new
+;  -D BLE_DEBUG_LOGGING=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${ikoka_nano_nrf_baseboard.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[ikoka_nano_nrf_companion_radio_usb]
+extends = ikoka_nano_nrf_baseboard
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -I examples/companion_radio/ui-new
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${ikoka_nano_nrf_baseboard.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[ikoka_nano_nrf_repeater]
+extends = ikoka_nano_nrf_baseboard
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D ADVERT_NAME='"Ikoka Nano Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=50
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+  +<../examples/simple_repeater/*.cpp>
+
+[ikoka_nano_nrf_room_server]
+extends = ikoka_nano_nrf_baseboard
+build_flags =
+  ${ikoka_nano_nrf_baseboard.build_flags}
+  -D ADVERT_NAME='"Ikoka Nano Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ikoka_nano_nrf_baseboard.build_src_filter}
+  +<../examples/simple_room_server/*.cpp>
+
+;;; hardware + firmware variants
+
+;;;    22dBm EBYTE E22-900M22 variants
+
+[env:ikoka_nano_nrf_22dbm_companion_radio_usb]
+extends =
+   ikoka_nano_nrf_e22_22dbm
+   ikoka_nano_nrf_companion_radio_usb
+build_flags =
+   ${ikoka_nano_nrf_companion_radio_usb.build_flags}
+   ${ikoka_nano_nrf_e22_22dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_companion_radio_usb.build_src_filter}
+   ${ikoka_nano_nrf_e22_22dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_22dbm_companion_radio_ble]
+extends =
+   ikoka_nano_nrf_e22_22dbm
+   ikoka_nano_nrf_companion_radio_ble
+build_flags =
+   ${ikoka_nano_nrf_companion_radio_ble.build_flags}
+   ${ikoka_nano_nrf_e22_22dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_companion_radio_ble.build_src_filter}
+   ${ikoka_nano_nrf_e22_22dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_22dbm_repeater]
+extends =
+   ikoka_nano_nrf_e22_22dbm
+   ikoka_nano_nrf_repeater
+build_flags =
+   ${ikoka_nano_nrf_repeater.build_flags}
+   ${ikoka_nano_nrf_e22_22dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_repeater.build_src_filter}
+   ${ikoka_nano_nrf_e22_22dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_22dbm_room_server]
+extends =
+   ikoka_nano_nrf_e22_22dbm
+   ikoka_nano_nrf_room_server
+build_flags =
+   ${ikoka_nano_nrf_room_server.build_flags}
+   ${ikoka_nano_nrf_e22_22dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_room_server.build_src_filter}
+   ${ikoka_nano_nrf_e22_22dbm.build_src_filter}
+
+
+;;;    30dBm EBYTE E22-900M30 variants
+
+[env:ikoka_nano_nrf_30dbm_companion_radio_usb]
+extends =
+   ikoka_nano_nrf_e22_30dbm
+   ikoka_nano_nrf_companion_radio_usb
+build_flags =
+   ${ikoka_nano_nrf_companion_radio_usb.build_flags}
+   ${ikoka_nano_nrf_e22_30dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_companion_radio_usb.build_src_filter}
+   ${ikoka_nano_nrf_e22_30dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_30dbm_companion_radio_ble]
+extends =
+   ikoka_nano_nrf_e22_30dbm
+   ikoka_nano_nrf_companion_radio_ble
+build_flags =
+   ${ikoka_nano_nrf_companion_radio_ble.build_flags}
+   ${ikoka_nano_nrf_e22_30dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_companion_radio_ble.build_src_filter}
+   ${ikoka_nano_nrf_e22_30dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_30dbm_repeater]
+extends =
+   ikoka_nano_nrf_e22_30dbm
+   ikoka_nano_nrf_repeater
+build_flags =
+   ${ikoka_nano_nrf_repeater.build_flags}
+   ${ikoka_nano_nrf_e22_30dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_repeater.build_src_filter}
+   ${ikoka_nano_nrf_e22_30dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_30dbm_room_server]
+extends =
+   ikoka_nano_nrf_e22_30dbm
+   ikoka_nano_nrf_room_server
+build_flags =
+   ${ikoka_nano_nrf_room_server.build_flags}
+   ${ikoka_nano_nrf_e22_30dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_room_server.build_src_filter}
+   ${ikoka_nano_nrf_e22_30dbm.build_src_filter}
+
+
+;;;    33dBm EBYTE E22-900M33 variants
+
+[env:ikoka_nano_nrf_33dbm_companion_radio_usb]
+extends =
+   ikoka_nano_nrf_e22_33dbm
+   ikoka_nano_nrf_companion_radio_usb
+build_flags =
+   ${ikoka_nano_nrf_companion_radio_usb.build_flags}
+   ${ikoka_nano_nrf_e22_33dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_companion_radio_usb.build_src_filter}
+   ${ikoka_nano_nrf_e22_33dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_33dbm_companion_radio_ble]
+extends =
+   ikoka_nano_nrf_e22_33dbm
+   ikoka_nano_nrf_companion_radio_ble
+build_flags =
+   ${ikoka_nano_nrf_companion_radio_ble.build_flags}
+   ${ikoka_nano_nrf_e22_33dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_companion_radio_ble.build_src_filter}
+   ${ikoka_nano_nrf_e22_33dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_33dbm_repeater]
+extends =
+   ikoka_nano_nrf_e22_33dbm
+   ikoka_nano_nrf_repeater
+build_flags =
+   ${ikoka_nano_nrf_repeater.build_flags}
+   ${ikoka_nano_nrf_e22_33dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_repeater.build_src_filter}
+   ${ikoka_nano_nrf_e22_33dbm.build_src_filter}
+
+[env:ikoka_nano_nrf_33dbm_room_server]
+extends =
+   ikoka_nano_nrf_e22_33dbm
+   ikoka_nano_nrf_room_server
+build_flags =
+   ${ikoka_nano_nrf_room_server.build_flags}
+   ${ikoka_nano_nrf_e22_33dbm.build_flags}
+build_src_filter =
+   ${ikoka_nano_nrf_room_server.build_src_filter}
+   ${ikoka_nano_nrf_e22_33dbm.build_src_filter}

+ 44 - 0
variants/ikoka_nano_nrf/target.cpp

@@ -0,0 +1,44 @@
+#include <Arduino.h>
+#include "target.h"
+#include <helpers/ArduinoHelpers.h>
+
+IkokaNanoNRFBoard board;
+
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display;
+  // MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
+#endif
+
+RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+VolatileRTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+EnvironmentSensorManager sensors;
+
+bool radio_init() {
+    rtc_clock.begin(Wire);
+  
+    return radio.std_init(&SPI);
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 28 - 0
variants/ikoka_nano_nrf/target.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <IkokaNanoNRFBoard.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/AutoDiscoverRTCClock.h>
+#include <helpers/ArduinoHelpers.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+
+#ifdef DISPLAY_CLASS
+  #include <helpers/ui/NullDisplayDriver.h>
+  #include <helpers/ui/MomentaryButton.h>
+  extern DISPLAY_CLASS display;
+  // extern MomentaryButton user_btn;
+#endif
+
+extern IkokaNanoNRFBoard board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern EnvironmentSensorManager sensors;
+
+bool radio_init();
+uint32_t radio_get_rng_seed();
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
+void radio_set_tx_power(uint8_t dbm);
+mesh::LocalIdentity radio_new_identity();

+ 86 - 0
variants/ikoka_nano_nrf/variant.cpp

@@ -0,0 +1,86 @@
+#include "variant.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+#include "nrf.h"
+
+const uint32_t g_ADigitalPinMap[] =
+{
+    // D0 .. D10
+     2, // D0  is P0.02 (A0)
+     3, // D1  is P0.03 (A1)
+    28, // D2  is P0.28 (A2)
+    29, // D3  is P0.29 (A3)
+     4, // D4  is P0.04 (A4,SDA)
+     5, // D5  is P0.05 (A5,SCL)
+    43, // D6  is P1.11 (TX)
+    44, // D7  is P1.12 (RX)
+    45, // D8  is P1.13 (SCK)
+    46, // D9  is P1.14 (MISO)
+    47, // D10 is P1.15 (MOSI)
+
+    // LEDs
+    26, // D11 is P0.26 (LED RED)
+     6, // D12 is P0.06 (LED BLUE)
+    30, // D13 is P0.30 (LED GREEN)
+    14, // D14 is P0.14 (READ_BAT)
+
+    // LSM6DS3TR
+    40, // D15 is P1.08 (6D_PWR)
+    27, // D16 is P0.27 (6D_I2C_SCL)
+     7, // D17 is P0.07 (6D_I2C_SDA)
+    11, // D18 is P0.11 (6D_INT1)
+
+    // MIC
+    42, // D19 is P1.10 (MIC_PWR)
+    32, // D20 is P1.00 (PDM_CLK)
+    16, // D21 is P0.16 (PDM_DATA)
+
+    // BQ25100
+    13, // D22 is P0.13 (HICHG)
+    17, // D23 is P0.17 (~CHG)
+
+    //
+    21, // D24 is P0.21 (QSPI_SCK)
+    25, // D25 is P0.25 (QSPI_CSN)
+    20, // D26 is P0.20 (QSPI_SIO_0 DI)
+    24, // D27 is P0.24 (QSPI_SIO_1 DO)
+    22, // D28 is P0.22 (QSPI_SIO_2 WP)
+    23, // D29 is P0.23 (QSPI_SIO_3 HOLD)
+
+    // NFC
+     9, // D30 is P0.09 (NFC1)
+    10, // D31 is P0.10 (NFC2)
+
+    // VBAT
+    31, // D32 is P0.31 (VBAT)
+};
+
+void initVariant()
+{
+    // Disable reading of the BAT voltage.
+    // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+    pinMode(VBAT_ENABLE, OUTPUT);
+    //digitalWrite(VBAT_ENABLE, HIGH); 
+    // This was taken from Seeed github butis not coherent with the doc, 
+    // VBAT_ENABLE should be kept to LOW to protect P0.14, (1500/500)*(4.2-3.3)+3.3 = 3.9V > 3.6V
+    // This induces a 3mA current in the resistors :( but it's better than burning the nrf
+    digitalWrite(VBAT_ENABLE, LOW);
+
+    // Low charging current (50mA)
+    // https://wiki.seeedstudio.com/XIAO_BLE#battery-charging-current
+    //pinMode(PIN_CHARGING_CURRENT, INPUT);
+
+    // High charging current (100mA)
+    pinMode(PIN_CHARGING_CURRENT, OUTPUT);
+    digitalWrite(PIN_CHARGING_CURRENT, LOW);
+
+    pinMode(PIN_QSPI_CS, OUTPUT);
+    digitalWrite(PIN_QSPI_CS, HIGH);
+
+    pinMode(LED_RED, OUTPUT);
+    digitalWrite(LED_RED, HIGH);
+    pinMode(LED_GREEN, OUTPUT);
+    digitalWrite(LED_GREEN, HIGH);
+    pinMode(LED_BLUE, OUTPUT);
+    digitalWrite(LED_BLUE, HIGH);
+}

+ 149 - 0
variants/ikoka_nano_nrf/variant.h

@@ -0,0 +1,149 @@
+#ifndef _IKOKA_NANO_NRF_H_
+#define _IKOKA_NANO_NRF_H_
+
+/** Master clock frequency */
+#define VARIANT_MCK       (64000000ul)
+
+#define USE_LFXO      // Board uses 32khz crystal for LF
+//#define USE_LFRC    // Board uses RC for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif // __cplusplus
+
+#define PINS_COUNT              (33)
+#define NUM_DIGITAL_PINS        (33)
+#define NUM_ANALOG_INPUTS       (8)
+#define NUM_ANALOG_OUTPUTS      (0)
+
+// LEDs
+#define PIN_LED                 (LED_RED)
+#define LED_PWR                 (PINS_COUNT)
+#define PIN_NEOPIXEL            (PINS_COUNT)
+#define NEOPIXEL_NUM            (0)
+
+#define LED_BUILTIN             (PIN_LED)
+
+#define LED_RED                 (11)
+#define LED_GREEN               (13)
+#define LED_BLUE                (12)
+
+#define LED_STATE_ON            (0)     // State when LED is litted
+
+// Buttons
+// #define PIN_BUTTON1             (PINS_COUNT)
+
+// Digital PINs
+static const uint8_t D0  = 0 ;
+static const uint8_t D1  = 1 ;
+static const uint8_t D2  = 2 ;
+static const uint8_t D3  = 3 ;
+static const uint8_t D4  = 4 ;
+static const uint8_t D5  = 5 ;
+static const uint8_t D6  = 6 ;
+static const uint8_t D7  = 7 ;
+static const uint8_t D8  = 8 ;
+static const uint8_t D9  = 9 ;
+static const uint8_t D10 = 10;
+
+#define VBAT_ENABLE             (14)    // Output LOW to enable reading of the BAT voltage.
+                                        // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+
+#define PIN_CHARGING_CURRENT    (22)    // Battery Charging current
+                                        // https://wiki.seeedstudio.com/XIAO_BLE#battery-charging-current
+
+// Analog pins
+#define PIN_A0                  (0)
+#define PIN_A1                  (1)
+#define PIN_A2                  (2)
+#define PIN_A3                  (3)
+#define PIN_A4                  (4)
+#define PIN_A5                  (5)
+#define PIN_VBAT                (32)    // Read the BAT voltage.
+                                        // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+
+#define BAT_NOT_CHARGING        (23)    // LOW when charging
+
+#define AREF_VOLTAGE            (3.0)
+#define ADC_MULTIPLIER          (3.0F) // 1M, 512k divider bridge
+
+static const uint8_t A0  = PIN_A0;
+static const uint8_t A1  = PIN_A1;
+static const uint8_t A2  = PIN_A2;
+static const uint8_t A3  = PIN_A3;
+static const uint8_t A4  = PIN_A4;
+static const uint8_t A5  = PIN_A5;
+
+#define ADC_RESOLUTION          (12)
+
+// Other pins
+#define PIN_NFC1                (30)
+#define PIN_NFC2                (31)
+
+// Serial interfaces
+#define PIN_SERIAL1_RX          (7)
+#define PIN_SERIAL1_TX          (6)
+
+// SPI Interfaces
+#define SPI_INTERFACES_COUNT    (2)
+
+#define PIN_SPI_MISO            (9)
+#define PIN_SPI_MOSI            (10)
+#define PIN_SPI_SCK             (8)
+
+#define PIN_SPI1_MISO           (25)
+#define PIN_SPI1_MOSI           (26)
+#define PIN_SPI1_SCK            (29)
+
+// Lora SPI is on SPI0
+#define  P_LORA_SCLK            PIN_SPI_SCK
+#define  P_LORA_MISO            PIN_SPI_MISO
+#define  P_LORA_MOSI            PIN_SPI_MOSI
+
+// Wire Interfaces
+#define WIRE_INTERFACES_COUNT   (1)
+
+// #define PIN_WIRE_SDA            (17) // 4 and 5 are used for the sx1262 !
+// #define PIN_WIRE_SCL            (16) // use WIRE1_SDA
+
+static const uint8_t SDA = PIN_WIRE_SDA;
+static const uint8_t SCL = PIN_WIRE_SCL;
+
+//#define PIN_WIRE1_SDA           (17)
+//#define PIN_WIRE1_SCL           (16)
+#define PIN_LSM6DS3TR_C_POWER   (15)
+#define PIN_LSM6DS3TR_C_INT1    (18)
+
+// PDM Interfaces
+#define PIN_PDM_PWR	            (19)
+#define PIN_PDM_CLK	            (20)
+#define PIN_PDM_DIN	            (21)
+
+// QSPI Pins
+#define PIN_QSPI_SCK            (24)
+#define PIN_QSPI_CS             (25)
+#define PIN_QSPI_IO0            (26)
+#define PIN_QSPI_IO1            (27)
+#define PIN_QSPI_IO2            (28)
+#define PIN_QSPI_IO3            (29)
+
+// On-board QSPI Flash
+#define EXTERNAL_FLASH_DEVICES  (P25Q16H)
+#define EXTERNAL_FLASH_USE_QSPI
+
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif

+ 94 - 0
variants/ikoka_stick_nrf/IkokaStickNRFBoard.cpp

@@ -0,0 +1,94 @@
+#ifdef XIAO_NRF52
+
+#include <Arduino.h>
+#include "IkokaStickNRFBoard.h"
+
+#include <bluefruit.h>
+#include <Wire.h>
+
+static BLEDfu bledfu;
+
+static void connect_callback(uint16_t conn_handle) {
+  (void)conn_handle;
+  MESH_DEBUG_PRINTLN("BLE client connected");
+}
+
+static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
+  (void)conn_handle;
+  (void)reason;
+
+  MESH_DEBUG_PRINTLN("BLE client disconnected");
+}
+
+void IkokaStickNRFBoard::begin() {
+  // for future use, sub-classes SHOULD call this from their begin()
+  startup_reason = BD_STARTUP_NORMAL;
+
+  pinMode(PIN_VBAT, INPUT);
+  pinMode(VBAT_ENABLE, OUTPUT);
+  digitalWrite(VBAT_ENABLE, HIGH);
+
+#ifdef PIN_USER_BTN
+  pinMode(PIN_USER_BTN, INPUT_PULLUP);
+#endif
+
+#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
+  Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
+#endif
+
+  Wire.begin();
+
+#ifdef P_LORA_TX_LED
+  pinMode(P_LORA_TX_LED, OUTPUT);
+  digitalWrite(P_LORA_TX_LED, HIGH);
+#endif
+
+//  pinMode(SX126X_POWER_EN, OUTPUT);
+//  digitalWrite(SX126X_POWER_EN, HIGH);
+  delay(10);   // give sx1262 some time to power up
+}
+
+bool IkokaStickNRFBoard::startOTAUpdate(const char *id, char reply[]) {
+  // Config the peripheral connection with maximum bandwidth
+  // more SRAM required by SoftDevice
+  // Note: All config***() function must be called before begin()
+  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
+  Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
+
+  Bluefruit.begin(1, 0);
+  // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
+  Bluefruit.setTxPower(4);
+  // Set the BLE device name
+  Bluefruit.setName("XIAO_NRF52_OTA");
+
+  Bluefruit.Periph.setConnectCallback(connect_callback);
+  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
+
+  // To be consistent OTA DFU should be added first if it exists
+  bledfu.begin();
+
+  // Set up and start advertising
+  // Advertising packet
+  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
+  Bluefruit.Advertising.addTxPower();
+  Bluefruit.Advertising.addName();
+
+  /* Start Advertising
+    - Enable auto advertising if disconnected
+    - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
+    - Timeout for fast mode is 30 seconds
+    - Start(timeout) with timeout = 0 will advertise forever (until connected)
+
+    For recommended advertising interval
+    https://developer.apple.com/library/content/qa/qa1931/_index.html
+  */
+  Bluefruit.Advertising.restartOnDisconnect(true);
+  Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
+  Bluefruit.Advertising.setFastTimeout(30);   // number of seconds in fast mode
+  Bluefruit.Advertising.start(0);             // 0 = Don't stop advertising after n seconds
+
+  strcpy(reply, "OK - started");
+  return true;
+}
+
+#endif

+ 60 - 0
variants/ikoka_stick_nrf/IkokaStickNRFBoard.h

@@ -0,0 +1,60 @@
+#pragma once
+
+#include <MeshCore.h>
+#include <Arduino.h>
+
+#ifdef XIAO_NRF52
+
+class IkokaStickNRFBoard : public mesh::MainBoard {
+protected:
+  uint8_t startup_reason;
+
+public:
+  void begin();
+  uint8_t getStartupReason() const override { return startup_reason; }
+
+#if defined(P_LORA_TX_LED)
+  void onBeforeTransmit() override {
+    digitalWrite(P_LORA_TX_LED, LOW);   // turn TX LED on
+    #if defined(LED_BLUE)
+       // turn off that annoying blue LED before transmitting
+       digitalWrite(LED_BLUE, HIGH);
+    #endif
+  }
+  void onAfterTransmit() override {
+    digitalWrite(P_LORA_TX_LED, HIGH);   // turn TX LED off
+    #if defined(LED_BLUE)
+       // do it after transmitting too, just in case
+       digitalWrite(LED_BLUE, HIGH);
+    #endif
+  }
+#endif
+
+  uint16_t getBattMilliVolts() override {
+    // Please read befor going further ;)
+    // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+
+    // We can't drive VBAT_ENABLE to HIGH as long 
+    // as we don't know wether we are charging or not ...
+    // this is a 3mA loss (4/1500)
+    digitalWrite(VBAT_ENABLE, LOW);
+    int adcvalue = 0;
+    analogReadResolution(12);
+    analogReference(AR_INTERNAL_3_0);  
+    delay(10);
+    adcvalue = analogRead(PIN_VBAT);
+    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
+  }
+
+  const char *getManufacturerName() const override {
+    return MANUFACTURER_STRING;
+  }
+
+  void reboot() override {
+    NVIC_SystemReset();
+  }
+
+  bool startOTAUpdate(const char *id, char reply[]) override;
+};
+
+#endif

+ 3 - 0
variants/ikoka_stick_nrf/platformio.ini

@@ -61,6 +61,7 @@ extends = ikoka_stick_nrf_baseboard
 ; No PA in this model, full 22dBm
 build_flags =
   ${ikoka_stick_nrf_baseboard.build_flags}
+  -D MANUFACTURER_STRING='"Ikoka Stick-E22-22dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=22
 build_src_filter = ${nrf52840_xiao.build_src_filter}
   +<helpers/*.cpp>
@@ -75,6 +76,7 @@ extends = ikoka_stick_nrf_baseboard
 ; cause distortion in the PA output. 20dBm in -> 30dBm out
 build_flags =
   ${ikoka_stick_nrf_baseboard.build_flags}
+  -D MANUFACTURER_STRING='"Ikoka Stick-E22-30dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=20
 build_src_filter = ${nrf52840_xiao.build_src_filter}
   +<helpers/*.cpp>
@@ -89,6 +91,7 @@ extends = ikoka_stick_nrf_baseboard
 ; to the rf amplifier frontend. 9dBm in -> 33dBm out
 build_flags =
   ${ikoka_stick_nrf_baseboard.build_flags}
+  -D MANUFACTURER_STRING='"Ikoka Stick-E22-33dBm (Xiao_nrf52)"'
   -D LORA_TX_POWER=9
 build_src_filter = ${nrf52840_xiao.build_src_filter}
   +<helpers/*.cpp>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů