浏览代码

Merge branch 'dev' into remove_set_setting_by_key

Florent 9 月之前
父节点
当前提交
90e26129ee
共有 96 个文件被更改,包括 3051 次插入433 次删除
  1. 39 0
      boards/ESP32-S3-WROOM-1-N4.json
  2. 72 0
      boards/rak4631.json
  3. 40 7
      build.sh
  4. 4 0
      docs/packet_structure.md
  5. 44 12
      docs/payloads.md
  6. 5 33
      examples/companion_radio/DataStore.cpp
  7. 70 2
      examples/companion_radio/MyMesh.cpp
  8. 9 1
      examples/companion_radio/MyMesh.h
  9. 1 0
      examples/companion_radio/main.cpp
  10. 16 9
      examples/companion_radio/ui-new/UITask.cpp
  11. 4 0
      examples/companion_radio/ui-new/UITask.h
  12. 216 10
      examples/simple_repeater/MyMesh.cpp
  13. 17 0
      examples/simple_repeater/MyMesh.h
  14. 23 0
      examples/simple_repeater/RateLimiter.h
  15. 4 1
      examples/simple_repeater/main.cpp
  16. 25 4
      examples/simple_room_server/MyMesh.cpp
  17. 6 0
      examples/simple_room_server/MyMesh.h
  18. 1 0
      examples/simple_room_server/main.cpp
  19. 2 1
      examples/simple_secure_chat/main.cpp
  20. 23 2
      examples/simple_sensor/SensorMesh.cpp
  21. 4 0
      examples/simple_sensor/SensorMesh.h
  22. 1 0
      examples/simple_sensor/main.cpp
  23. 6 1
      platformio.ini
  24. 67 3
      src/Mesh.cpp
  25. 24 0
      src/Mesh.h
  26. 5 0
      src/MeshCore.h
  27. 1 0
      src/Packet.h
  28. 12 4
      src/helpers/ArduinoHelpers.h
  29. 4 0
      src/helpers/AutoDiscoverRTCClock.h
  30. 31 15
      src/helpers/BaseChatMesh.cpp
  31. 3 0
      src/helpers/BaseChatMesh.h
  32. 25 17
      src/helpers/CommonCLI.cpp
  33. 8 3
      src/helpers/CommonCLI.h
  34. 237 0
      src/helpers/RegionMap.cpp
  35. 52 0
      src/helpers/RegionMap.h
  36. 54 0
      src/helpers/StatsFormatHelper.h
  37. 92 0
      src/helpers/TransportKeyStore.cpp
  38. 31 0
      src/helpers/TransportKeyStore.h
  39. 7 0
      src/helpers/TxtDataHelpers.cpp
  40. 1 0
      src/helpers/TxtDataHelpers.h
  41. 9 120
      src/helpers/radiolib/CustomLR1110.h
  42. 26 0
      src/helpers/sensors/EnvironmentSensorManager.cpp
  43. 1 0
      src/helpers/sensors/EnvironmentSensorManager.h
  44. 125 0
      src/helpers/ui/LGFXDisplay.cpp
  45. 39 0
      src/helpers/ui/LGFXDisplay.h
  46. 6 6
      variants/heltec_e213/platformio.ini
  47. 6 6
      variants/heltec_e290/platformio.ini
  48. 0 39
      variants/heltec_t114/T114Board.cpp
  49. 0 5
      variants/heltec_t114/T114Board.h
  50. 1 0
      variants/heltec_t114/platformio.ini
  51. 1 2
      variants/heltec_t114/target.cpp
  52. 1 0
      variants/heltec_t114/target.h
  53. 6 6
      variants/heltec_t190/platformio.ini
  54. 2 12
      variants/heltec_v2/HeltecV2Board.h
  55. 34 4
      variants/heltec_v2/platformio.ini
  56. 1 1
      variants/heltec_v2/target.h
  57. 3 15
      variants/heltec_v3/HeltecV3Board.h
  58. 7 0
      variants/heltec_v3/platformio.ini
  59. 1 1
      variants/heltec_v3/target.h
  60. 3 3
      variants/lilygo_tlora_c6/platformio.ini
  61. 2 2
      variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h
  62. 4 18
      variants/lilygo_tlora_v2_1/platformio.ini
  63. 1 2
      variants/lilygo_tlora_v2_1/target.h
  64. 1 2
      variants/rak4631/platformio.ini
  65. 49 0
      variants/rak4631/variant.cpp
  66. 172 0
      variants/rak4631/variant.h
  67. 2 2
      variants/rak_wismesh_tag/platformio.ini
  68. 128 0
      variants/sensecap_indicator-espnow/SCIndicatorDisplay.h
  69. 50 0
      variants/sensecap_indicator-espnow/platformio.ini
  70. 56 0
      variants/sensecap_indicator-espnow/target.cpp
  71. 29 0
      variants/sensecap_indicator-espnow/target.h
  72. 1 16
      variants/station_g2/StationG2Board.h
  73. 33 3
      variants/station_g2/platformio.ini
  74. 8 1
      variants/station_g2/target.cpp
  75. 3 3
      variants/station_g2/target.h
  76. 1 0
      variants/t1000-e/platformio.ini
  77. 32 0
      variants/thinknode_m2/ThinknodeM2Board.cpp
  78. 18 0
      variants/thinknode_m2/ThinknodeM2Board.h
  79. 28 0
      variants/thinknode_m2/pins_arduino.h
  80. 171 0
      variants/thinknode_m2/platformio.ini
  81. 57 0
      variants/thinknode_m2/target.cpp
  82. 32 0
      variants/thinknode_m2/target.h
  83. 15 0
      variants/thinknode_m2/variant.h
  84. 1 1
      variants/wio-tracker-l1/target.cpp
  85. 75 0
      variants/wio_wm1110/WioWM1110Board.cpp
  86. 58 0
      variants/wio_wm1110/WioWM1110Board.h
  87. 86 0
      variants/wio_wm1110/platformio.ini
  88. 92 0
      variants/wio_wm1110/target.cpp
  89. 21 0
      variants/wio_wm1110/target.h
  90. 92 0
      variants/wio_wm1110/variant.cpp
  91. 145 0
      variants/wio_wm1110/variant.h
  92. 10 10
      variants/xiao_c6/platformio.ini
  93. 4 26
      variants/xiao_rp2040/target.cpp
  94. 13 0
      variants/xiao_s3_wio/XiaoS3WIOBoard.h
  95. 1 1
      variants/xiao_s3_wio/target.cpp
  96. 2 1
      variants/xiao_s3_wio/target.h

+ 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"
+}

+ 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() {

+ 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 ackowledgement. 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]);

+ 9 - 1
examples/companion_radio/MyMesh.h

@@ -5,7 +5,7 @@
 #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"
@@ -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();
 }

+ 16 - 9
examples/companion_radio/ui-new/UITask.cpp

@@ -683,6 +683,10 @@ void UITask::loop() {
   } else if (ev == BUTTON_EVENT_LONG_PRESS) {
     c = handleLongPress(KEY_RIGHT);
   }
+  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) {
@@ -696,15 +700,18 @@ void UITask::loop() {
   }
 #endif
 #if defined(PIN_USER_BTN_ANA)
-  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);
+  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)

+ 4 - 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;

+ 216 - 10
examples/simple_repeater/MyMesh.cpp

@@ -149,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();
@@ -306,6 +306,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;
 }
 
@@ -397,11 +401,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,
@@ -412,7 +433,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
 
@@ -448,12 +476,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());
@@ -583,10 +618,43 @@ 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(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
 #endif
@@ -594,10 +662,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
       , 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));
@@ -608,6 +679,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;
@@ -642,8 +714,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
   _fs = fs;
   // load persisted prefs
   _cli.loadPrefs(_fs);
-
   acl.load(_fs);
+  // TODO: key_store.begin();
+  region_map.load(_fs);
 
 #if defined(WITH_BRIDGE)
   if (_prefs.bridge_enabled) {
@@ -785,6 +858,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)
@@ -806,8 +892,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
@@ -849,6 +968,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
   }
@@ -891,4 +1092,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;
 }

+ 17 - 0
examples/simple_repeater/MyMesh.h

@@ -30,7 +30,10 @@
 #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;
@@ -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];
@@ -141,12 +152,15 @@ protected:
   }
 #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);
@@ -181,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; }
 

+ 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();
 }

+ 25 - 4
examples/simple_room_server/MyMesh.cpp

@@ -144,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();
@@ -262,11 +262,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) {
@@ -580,7 +580,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;
@@ -591,6 +593,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;
@@ -727,6 +730,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
@@ -858,4 +874,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;
 }

+ 6 - 0
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>
@@ -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;
@@ -190,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();
 }

+ 23 - 2
examples/simple_sensor/SensorMesh.cpp

@@ -449,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
 
@@ -651,7 +658,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;
@@ -664,6 +671,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;
@@ -769,6 +777,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();

+ 4 - 0
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>
@@ -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 { }

+ 1 - 0
examples/simple_sensor/main.cpp

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

+ 6 - 1
platformio.ini

@@ -22,7 +22,7 @@ lib_deps =
   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,8 @@ 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
@@ -126,6 +129,7 @@ build_flags =
   -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
@@ -141,3 +145,4 @@ lib_deps =
   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);
+
 };
 
 }

+ 5 - 0
src/MeshCore.h

@@ -72,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
 

+ 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; }

+ 25 - 17
src/helpers/CommonCLI.cpp

@@ -69,7 +69,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     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
-    // 162
+    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);
@@ -77,8 +78,8 @@ 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);
@@ -146,7 +147,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
     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
-    // 162
+    file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
+    // 166
 
     file.close();
   }
@@ -166,7 +168,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
     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);
+    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);
@@ -533,7 +535,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       sprintf(reply, "%s", _board->getManufacturerName());
     } else if (memcmp(command, "sensor get ", 11) == 0) {
       const char* key = command + 11;
-      const char* val = sensors.getSettingByKey(key);
+      const char* val = _sensors->getSettingByKey(key);
       if (val != NULL) {
         sprintf(reply, "> %s", val);
       } else {
@@ -545,7 +547,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       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)) {
+      if (_sensors->setSettingValue(key, value)) {
         strcpy(reply, "ok");
       } else {
         strcpy(reply, "can't find custom var");
@@ -553,7 +555,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
     } else if (memcmp(command, "sensor list", 11) == 0) {
       char* dp = reply;
       int start = 0;
-      int end = sensors.getNumSettings();
+      int end = _sensors->getNumSettings();
       if (strlen(command) > 11) {
         start = _atoi(command+12);
       }
@@ -565,8 +567,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         int i;
         for (i = start; i < end && (dp-reply < 134); i++) {
           sprintf(dp, "%s=%s\n", 
-            sensors.getSettingName(i),
-            sensors.getSettingValue(i));
+            _sensors->getSettingName(i),
+            _sensors->getSettingValue(i));
           dp = strchr(dp, 0);
         }
         if (i < end) {
@@ -577,7 +579,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       }
 #if ENV_INCLUDE_GPS == 1
     } else if (memcmp(command, "gps on", 6) == 0) {
-      if (sensors.setSettingValue("gps", "1")) {
+      if (_sensors->setSettingValue("gps", "1")) {
         _prefs->gps_enabled = 1;
         savePrefs();
         strcpy(reply, "ok");
@@ -585,7 +587,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         strcpy(reply, "gps toggle not found");
       }
     } else if (memcmp(command, "gps off", 7) == 0) {
-      if (sensors.setSettingValue("gps", "0")) {
+      if (_sensors->setSettingValue("gps", "0")) {
         _prefs->gps_enabled = 0;
         savePrefs();
         strcpy(reply, "ok");
@@ -593,13 +595,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         strcpy(reply, "gps toggle not found");
       }
     } else if (memcmp(command, "gps sync", 8) == 0) {
-      LocationProvider * l = sensors.getLocationProvider();
+      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;
+      _prefs->node_lat = _sensors->node_lat;
+      _prefs->node_lon = _sensors->node_lon;
       savePrefs();
       strcpy(reply, "ok");
     } else if (memcmp(command, "gps advert", 10) == 0) {
@@ -633,12 +635,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         strcpy(reply, "error");
       }
     } else if (memcmp(command, "gps", 3) == 0) {
-      LocationProvider * l = sensors.getLocationProvider();
+      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");
+        bool active = !strcmp(_sensors->getSettingByKey("gps"), "1");
         if (enabled) {
           sprintf(reply, "on, %s, %s, %d sats",
             active?"active":"deactivated", 
@@ -663,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");
     }

+ 8 - 3
src/helpers/CommonCLI.h

@@ -2,7 +2,7 @@
 
 #include "Mesh.h"
 #include <helpers/IdentityStore.h>
-#include <target.h>
+#include <helpers/SensorManager.h>
 
 #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
 #define WITH_BRIDGE
@@ -46,6 +46,7 @@ struct NodePrefs { // persisted to file
   uint8_t gps_enabled;
   uint32_t gps_interval; // in seconds
   uint8_t advert_loc_policy;
+  uint32_t discovery_mod_timestamp;
 };
 
 class CommonCLICallbacks {
@@ -66,6 +67,9 @@ 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;
@@ -85,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; }
@@ -92,8 +97,8 @@ 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);

+ 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;
+};

+ 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();
+};

+ 7 - 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 

+ 1 - 0
src/helpers/TxtDataHelpers.h

@@ -12,4 +12,5 @@ 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);
 };

+ 9 - 120
src/helpers/radiolib/CustomLR1110.h

@@ -3,137 +3,26 @@
 #include <RadioLib.h>
 #include "MeshCore.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
-
 class CustomLR1110 : public LR1110 {
   public:
     CustomLR1110(Module *mod) : LR1110(mod) { }
 
-    uint8_t shiftCount = 0;
-
-    int16_t standby() override {
-      // tx resets the shift, standby is called on tx completion
-      // this might not actually be what resets it, but it seems to work
-      // more investigation needed
-      this->shiftCount = 0;
-      return LR1110::standby();
-    }
-
     size_t getPacketLength(bool update) override {
       size_t len = LR1110::getPacketLength(update);
-      if (len == 0) {
-        uint32_t irq = getIrqStatus();
-        if (irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
-          MESH_DEBUG_PRINTLN("LR1110: got header err, assuming shift");
-          this->shiftCount += 4; // uint8 will loop around to 0 at 256, perfect as rx buffer is 256 bytes
-        } else {
-          MESH_DEBUG_PRINTLN("LR1110: got zero-length packet without header err irq");
-        }
+      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;
     }
-
-    int16_t readData(uint8_t *data, size_t len) override {
-      // check active modem
-      uint8_t modem = RADIOLIB_LR11X0_PACKET_TYPE_NONE;
-      int16_t state = getPacketType(&modem);
-      RADIOLIB_ASSERT(state);
-      if((modem != RADIOLIB_LR11X0_PACKET_TYPE_LORA) && 
-        (modem != RADIOLIB_LR11X0_PACKET_TYPE_GFSK)) {
-        return(RADIOLIB_ERR_WRONG_MODEM);
-      }
-
-      // check integrity CRC
-      uint32_t irq = getIrqStatus();
-      int16_t crcState = RADIOLIB_ERR_NONE;
-      // Report CRC mismatch when there's a payload CRC error, or a header error and no valid header (to avoid false alarm from previous packet)
-      if((irq & RADIOLIB_LR11X0_IRQ_CRC_ERR) || ((irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR) && !(irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID))) {
-        crcState = RADIOLIB_ERR_CRC_MISMATCH;
-      }
-
-      // get packet length
-      // the offset is needed since LR11x0 seems to move the buffer base by 4 bytes on every packet
-      uint8_t offset = 0;
-      size_t length = LR1110::getPacketLength(true, &offset);
-      if((len != 0) && (len < length)) {
-        // user requested less data than we got, only return what was requested
-        length = len;
-      }
-
-      // read packet data
-      state = readBuffer8(data, length, (uint8_t)(offset + this->shiftCount));  // add shiftCount to offset - only change from radiolib
-      RADIOLIB_ASSERT(state);
-
-      // clear the Rx buffer
-      state = clearRxBuffer();
-      RADIOLIB_ASSERT(state);
-
-      // clear interrupt flags
-      state = clearIrqState(RADIOLIB_LR11X0_IRQ_ALL);
-
-      // check if CRC failed - this is done after reading data to give user the option to keep them
-      RADIOLIB_ASSERT(crcState);
-
-      return(state);
-    }
-
-    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;
-    }
-
-    // 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;
     }
 };

+ 26 - 0
src/helpers/sensors/EnvironmentSensorManager.cpp

@@ -15,6 +15,12 @@
 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>
@@ -305,6 +311,18 @@ bool EnvironmentSensorManager::begin() {
   }
   #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;
 }
 
@@ -447,6 +465,14 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     }
     #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;

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

@@ -21,6 +21,7 @@ protected:
   bool VL53L0X_initialized = false;
   bool SHT4X_initialized = false;
   bool BME680_initialized = false;
+  bool BMP085_initialized = false;
 
   bool gps_detected = false;
   bool gps_active = false;

+ 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);
+};

+ 6 - 6
variants/heltec_e213/platformio.ini

@@ -40,7 +40,7 @@ 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}
@@ -60,7 +60,7 @@ 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}
@@ -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}
@@ -119,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}
@@ -141,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}

+ 6 - 6
variants/heltec_e290/platformio.ini

@@ -34,7 +34,7 @@ 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}
@@ -54,7 +54,7 @@ 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}
@@ -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}
@@ -115,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}
@@ -137,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;

+ 6 - 6
variants/heltec_t190/platformio.ini

@@ -47,7 +47,7 @@ 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}
@@ -65,7 +65,7 @@ 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}
@@ -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}
@@ -118,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}
@@ -138,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}

+ 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);
 

+ 34 - 4
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 =
@@ -112,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
@@ -128,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
@@ -148,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
@@ -164,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);
 

+ 7 - 0
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

+ 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>

+ 3 - 3
variants/lilygo_tlora_c6/platformio.ini

@@ -30,7 +30,7 @@ build_flags =
 build_src_filter = ${esp32c6_base.build_src_filter}
   +<../variants/lilygo_tlora_c6>
 
-[env:LilyGo_Tlora_C6_repeater]
+[env:LilyGo_Tlora_C6_repeater_]
 extends = tlora_c6
 build_src_filter = ${tlora_c6.build_src_filter}
   +<../examples/simple_repeater/*.cpp>
@@ -47,7 +47,7 @@ lib_deps =
   ${tlora_c6.lib_deps}
 ;  ${esp32_ota.lib_deps}
 
-[env:LilyGo_Tlora_C6_room_server]
+[env:LilyGo_Tlora_C6_room_server_]
 extends = tlora_c6
 build_src_filter = ${tlora_c6.build_src_filter}
   +<../examples/simple_room_server>
@@ -64,7 +64,7 @@ lib_deps =
   ${tlora_c6.lib_deps}
 ;  ${esp32_ota.lib_deps}
 
-[env:LilyGo_Tlora_C6_companion_radio_ble]
+[env:LilyGo_Tlora_C6_companion_radio_ble_]
 extends = tlora_c6
 build_flags = ${tlora_c6.build_flags}
   -D MAX_CONTACTS=300

+ 2 - 2
src/helpers/LilyGoTLoraBoard.h → variants/lilygo_tlora_v2_1/LilyGoTLoraBoard.h

@@ -1,7 +1,7 @@
 #pragma once
 
 #include <Arduino.h>
-#include "ESP32Board.h"
+#include <helpers/ESP32Board.h>
 
 // LILYGO T-LoRa V2.1-1.6 board with SX1276
 class LilyGoTLoraBoard : public ESP32Board {
@@ -9,7 +9,7 @@ public:
   const char* getManufacturerName() const override {
     return "LILYGO T-LoRa V2.1-1.6";
   }
-  
+
   uint16_t getBattMilliVolts() override {
     analogReadResolution(12);
 

+ 4 - 18
variants/lilygo_tlora_v2_1/platformio.ini

@@ -6,6 +6,8 @@ build_type = release  ; Set build type to release
 board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
 build_flags =
   ${esp32_base.build_flags}
+  ${sensor_base.build_flags}
+  -UENV_INCLUDE_GPS
   -I variants/lilygo_tlora_v2_1
   -Os -ffunction-sections -fdata-sections  ; Optimize for size
   -D LILYGO_TLORA  ; LILYGO T-LoRa V2.1-1.6 ESP32 with SX1276
@@ -27,28 +29,18 @@ build_flags =
   -D WRAPPER_CLASS=CustomSX1276Wrapper
   -D SX127X_CURRENT_LIMIT=120
   -D LORA_TX_POWER=20
-  -D ENV_INCLUDE_AHTX0=1
-  -D ENV_INCLUDE_BME280=1
-  -D ENV_INCLUDE_BMP280=1
-  -D ENV_INCLUDE_INA3221=1
-  -D ENV_INCLUDE_INA219=1
 build_src_filter = ${esp32_base.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
   +<../variants/lilygo_tlora_v2_1>
   +<helpers/sensors>
 lib_deps =
   ${esp32_base.lib_deps}
-  adafruit/Adafruit SSD1306 @ ^2.5.13
-  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 BMP280 Library @ ^2.6.8
+  ${sensor_base.lib_deps}
 
 ; === LILYGO T-LoRa V2.1-1.6 with SX1276 environments ===
 [env:LilyGo_TLora_V2_1_1_6_repeater]
 extends = LilyGo_TLora_V2_1_1_6
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 build_flags =
   ${LilyGo_TLora_V2_1_1_6.build_flags}
@@ -73,7 +65,6 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
   ${LilyGo_TLora_V2_1_1_6.lib_deps}
@@ -89,7 +80,6 @@ build_flags =
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
@@ -111,7 +101,6 @@ build_flags =
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
@@ -149,7 +138,6 @@ build_flags =
   -D WIFI_DEBUG_LOGGING=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
   +<../examples/companion_radio/*.cpp>
   +<../examples/companion_radio/ui-new/*.cpp>
@@ -163,7 +151,6 @@ lib_deps =
 [env:LilyGo_TLora_V2_1_1_6_repeater_bridge_rs232]
 extends = LilyGo_TLora_V2_1_1_6
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/bridges/RS232Bridge.cpp>
   +<../examples/simple_repeater>
 build_flags =
@@ -187,7 +174,6 @@ lib_deps =
 [env:LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow]
 extends = LilyGo_TLora_V2_1_1_6
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/bridges/ESPNowBridge.cpp>
   +<../examples/simple_repeater>
 build_flags =

+ 1 - 2
variants/lilygo_tlora_v2_1/target.h

@@ -3,10 +3,9 @@
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
 #include <helpers/radiolib/RadioLibWrappers.h>
-#include <helpers/LilyGoTLoraBoard.h>
+#include <LilyGoTLoraBoard.h>
 #include <helpers/radiolib/CustomSX1276Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
-#include <helpers/SensorManager.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>
 #ifdef DISPLAY_CLASS
   #include <helpers/ui/SSD1306Display.h>

+ 1 - 2
variants/rak4631/platformio.ini

@@ -1,7 +1,6 @@
 [rak4631]
 extends = nrf52_base
-platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak
-board = wiscore_rak4631
+board = rak4631
 board_check = true
 build_flags = ${nrf52_base.build_flags}
   ${sensor_base.build_flags}

+ 49 - 0
variants/rak4631/variant.cpp

@@ -0,0 +1,49 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "variant.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+#include "nrf.h"
+
+const uint32_t g_ADigitalPinMap[] =
+{
+  // P0
+  0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ,
+  8 , 9 , 10, 11, 12, 13, 14, 15,
+  16, 17, 18, 19, 20, 21, 22, 23,
+  24, 25, 26, 27, 28, 29, 30, 31,
+
+  // P1
+  32, 33, 34, 35, 36, 37, 38, 39,
+  40, 41, 42, 43, 44, 45, 46, 47
+};
+
+
+void initVariant()
+{
+  // LED1 & LED2
+  pinMode(PIN_LED1, OUTPUT);
+  ledOff(PIN_LED1);
+
+  pinMode(PIN_LED2, OUTPUT);
+  ledOff(PIN_LED2);;
+}
+

+ 172 - 0
variants/rak4631/variant.h

@@ -0,0 +1,172 @@
+/*
+  Copyright (c) 2014-2015 Arduino LLC.  All right reserved.
+  Copyright (c) 2016 Sandeep Mistry All right reserved.
+  Copyright (c) 2018, Adafruit Industries (adafruit.com)
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Lesser General Public
+  License as published by the Free Software Foundation; either
+  version 2.1 of the License, or (at your option) any later version.
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  See the GNU Lesser General Public License for more details.
+  You should have received a copy of the GNU Lesser General Public
+  License along with this library; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef _VARIANT_RAK4630_
+#define _VARIANT_RAK4630_
+
+#define RAK4630
+
+/** 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
+
+	/*
+ * WisBlock Base GPIO definitions
+ */
+	static const uint8_t WB_IO1 = 17;	   // SLOT_A SLOT_B
+	static const uint8_t WB_IO2 = 34;	   // SLOT_A SLOT_B
+	static const uint8_t WB_IO3 = 21;	   // SLOT_C
+	static const uint8_t WB_IO4 = 4;	   // SLOT_C
+	static const uint8_t WB_IO5 = 9;	   // SLOT_D
+	static const uint8_t WB_IO6 = 10;	   // SLOT_D
+	static const uint8_t WB_SW1 = 33;	   // IO_SLOT
+	static const uint8_t WB_A0 = 5;		   // IO_SLOT
+	static const uint8_t WB_A1 = 31;	   // IO_SLOT
+	static const uint8_t WB_I2C1_SDA = 13; // SENSOR_SLOT IO_SLOT
+	static const uint8_t WB_I2C1_SCL = 14; // SENSOR_SLOT IO_SLOT
+	static const uint8_t WB_I2C2_SDA = 24; // IO_SLOT
+	static const uint8_t WB_I2C2_SCL = 25; // IO_SLOT
+	static const uint8_t WB_SPI_CS = 26;   // IO_SLOT
+	static const uint8_t WB_SPI_CLK = 3;   // IO_SLOT
+	static const uint8_t WB_SPI_MISO = 29; // IO_SLOT
+	static const uint8_t WB_SPI_MOSI = 30; // IO_SLOT
+
+// Number of pins defined in PinDescription array
+#define PINS_COUNT (48)
+#define NUM_DIGITAL_PINS (48)
+#define NUM_ANALOG_INPUTS (6)
+#define NUM_ANALOG_OUTPUTS (0)
+
+// LEDs
+#define PIN_LED1 (35)
+#define PIN_LED2 (36)
+
+#define LED_BUILTIN PIN_LED1
+#define LED_CONN PIN_LED2
+
+#define LED_GREEN PIN_LED1
+#define LED_BLUE PIN_LED2
+
+#define LED_STATE_ON 1 // State when LED is litted
+
+/*
+ * Buttons
+ */
+// RAK4631 has no buttons
+
+/*
+ * Analog pins
+ */
+#define PIN_A0 (5)	//(3)
+#define PIN_A1 (31) //(4)
+#define PIN_A2 (28)
+#define PIN_A3 (29)
+#define PIN_A4 (30)
+#define PIN_A5 (31)
+#define PIN_A6 (0xff)
+#define PIN_A7 (0xff)
+
+	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;
+	static const uint8_t A6 = PIN_A6;
+	static const uint8_t A7 = PIN_A7;
+#define ADC_RESOLUTION 14
+
+// Other pins
+#define PIN_AREF (2)
+#define PIN_NFC1 (9)
+#define PIN_NFC2 (10)
+
+	static const uint8_t AREF = PIN_AREF;
+
+/*
+ * Serial interfaces
+ */
+// TXD1 RXD1 on Base Board
+#define PIN_SERIAL1_RX (15)
+#define PIN_SERIAL1_TX (16)
+
+// TXD0 RXD0 on Base Board
+#define PIN_SERIAL2_RX (19)
+#define PIN_SERIAL2_TX (20)
+
+/*
+ * SPI Interfaces
+ */
+#define SPI_INTERFACES_COUNT 1
+
+#define PIN_SPI_MISO (29)
+#define PIN_SPI_MOSI (30)
+#define PIN_SPI_SCK (3)
+
+	static const uint8_t SS = 26;
+	static const uint8_t MOSI = PIN_SPI_MOSI;
+	static const uint8_t MISO = PIN_SPI_MISO;
+	static const uint8_t SCK = PIN_SPI_SCK;
+
+/*
+ * Wire Interfaces
+ */
+#define WIRE_INTERFACES_COUNT 2
+
+#define PIN_WIRE_SDA (13)
+#define PIN_WIRE_SCL (14)
+
+#define PIN_WIRE1_SDA (24)
+#define PIN_WIRE1_SCL (25)
+
+	// QSPI Pins
+	// QSPI occupied by GPIO's
+	#define PIN_QSPI_SCK 3	// 19
+	#define PIN_QSPI_CS 26	// 17
+	#define PIN_QSPI_IO0 30 // 20
+	#define PIN_QSPI_IO1 29 // 21
+	#define PIN_QSPI_IO2 28 // 22
+	#define PIN_QSPI_IO3 2	// 23
+
+	// On-board QSPI Flash
+	// No onboard flash
+	#define EXTERNAL_FLASH_DEVICES IS25LP080D
+	#define EXTERNAL_FLASH_USE_QSPI
+
+#ifdef __cplusplus
+}
+#endif
+
+/*----------------------------------------------------------------------------
+ *        Arduino objects - C++ only
+ *----------------------------------------------------------------------------*/
+
+#endif

+ 2 - 2
variants/rak_wismesh_tag/platformio.ini

@@ -1,7 +1,6 @@
 [rak_wismesh_tag]
 extends = nrf52_base
-platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak
-board = wiscore_rak4631
+board = rak4631
 board_check = true
 build_flags = ${nrf52_base.build_flags}
   ${sensor_base.build_flags}
@@ -95,6 +94,7 @@ build_flags =
   -D BLE_PIN_CODE=123456
   -D BLE_DEBUG_LOGGING=1
   -D OFFLINE_QUEUE_SIZE=256
+  -D PIN_GPS_EN=34
 ;  -D MESH_PACKET_LOGGING=1
   -D MESH_DEBUG=1
 build_src_filter = ${rak_wismesh_tag.build_src_filter}

+ 128 - 0
variants/sensecap_indicator-espnow/SCIndicatorDisplay.h

@@ -0,0 +1,128 @@
+#pragma once
+
+#include <helpers/ui/LGFXDisplay.h>
+
+#define LGFX_USE_V1
+#include <LovyanGFX.hpp>
+
+#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
+#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
+
+class LGFX : public lgfx::LGFX_Device
+{
+  lgfx::Panel_ST7701 _panel_instance;
+  lgfx::Bus_RGB _bus_instance;
+  lgfx::Light_PWM _light_instance;
+  lgfx::Touch_FT5x06 _touch_instance;
+
+public:
+  const uint16_t screenWidth = 480;
+  const uint16_t screenHeight = 480;
+
+  bool hasButton(void) { return true; }
+
+  LGFX(void)
+  {
+    {
+        auto cfg = _panel_instance.config();
+        cfg.memory_width = 480;
+        cfg.memory_height = 480;
+        cfg.panel_width = screenWidth;
+        cfg.panel_height = screenHeight;
+        cfg.offset_x = 0;
+        cfg.offset_y = 0;
+        cfg.offset_rotation = 1;
+        _panel_instance.config(cfg);
+    }
+
+    {
+        auto cfg = _panel_instance.config_detail();
+        cfg.pin_cs = 4 | IO_EXPANDER;
+        cfg.pin_sclk = 41;
+        cfg.pin_mosi = 48;
+        cfg.use_psram = 1;
+        _panel_instance.config_detail(cfg);
+    }
+
+    {
+        auto cfg = _bus_instance.config();
+        cfg.panel = &_panel_instance;
+
+        cfg.freq_write = 8000000;
+        cfg.pin_henable = 18;
+
+        cfg.pin_pclk = 21;
+        cfg.pclk_active_neg = 0;
+        cfg.pclk_idle_high = 0;
+        cfg.de_idle_high = 1;
+
+        cfg.pin_hsync = 16;
+        cfg.hsync_polarity = 0;
+        cfg.hsync_front_porch = 10;
+        cfg.hsync_pulse_width = 8;
+        cfg.hsync_back_porch = 50;
+
+        cfg.pin_vsync = 17;
+        cfg.vsync_polarity = 0;
+        cfg.vsync_front_porch = 10;
+        cfg.vsync_pulse_width = 8;
+        cfg.vsync_back_porch = 20;
+
+        cfg.pin_d0 = 15;
+        cfg.pin_d1 = 14;
+        cfg.pin_d2 = 13;
+        cfg.pin_d3 = 12;
+        cfg.pin_d4 = 11;
+        cfg.pin_d5 = 10;
+        cfg.pin_d6 = 9;
+        cfg.pin_d7 = 8;
+        cfg.pin_d8 = 7;
+        cfg.pin_d9 = 6;
+        cfg.pin_d10 = 5;
+        cfg.pin_d11 = 4;
+        cfg.pin_d12 = 3;
+        cfg.pin_d13 = 2;
+        cfg.pin_d14 = 1;
+        cfg.pin_d15 = 0;
+
+        _bus_instance.config(cfg);
+    }
+    _panel_instance.setBus(&_bus_instance);
+
+    {
+        auto cfg = _light_instance.config();
+        cfg.pin_bl = 45;
+        _light_instance.config(cfg);
+    }
+    _panel_instance.light(&_light_instance);
+
+    {
+        auto cfg = _touch_instance.config();
+        cfg.pin_cs = GPIO_NUM_NC;
+        cfg.x_min = 0;
+        cfg.x_max = 479;
+        cfg.y_min = 0;
+        cfg.y_max = 479;
+        cfg.pin_int = GPIO_NUM_NC;
+        cfg.pin_rst = GPIO_NUM_NC;
+        cfg.bus_shared = true;
+        cfg.offset_rotation = 0;
+
+        cfg.i2c_port = 0;
+        cfg.i2c_addr = 0x48;
+        cfg.pin_sda = 39;
+        cfg.pin_scl = 40;
+        cfg.freq = 400000;
+        _touch_instance.config(cfg);
+        _panel_instance.setTouch(&_touch_instance);
+    }
+
+    setPanel(&_panel_instance);
+  }
+};
+
+class SCIndicatorDisplay : public LGFXDisplay {
+  LGFX disp;
+public:
+  SCIndicatorDisplay() : LGFXDisplay(480, 480, disp) {}
+};

+ 50 - 0
variants/sensecap_indicator-espnow/platformio.ini

@@ -0,0 +1,50 @@
+[SenseCapIndicator-ESPNow]
+extends = esp32_base
+board = esp32-s3-devkitc-1
+board_build.arduino.memory_type = qio_opi
+board_build.flash_mode = qio
+board_build.psram_type = opi
+board_upload.flash_size = 8MB
+board_upload.maximum_size = 8388608
+board_build.partitions = default.csv
+build_flags =
+  ${esp32_base.build_flags}
+  -D PIN_BOARD_SDA=39
+  -D PIN_BOARD_SCL=40
+  -D DISPLAY_CLASS=SCIndicatorDisplay
+  -D DISPLAY_LINES=21
+  -D LINE_LENGTH=53
+  -D DISABLE_WIFI_OTA=1
+  -D IO_EXPANDER=0x40
+  -D IO_EXPANDER_IRQ=42
+  -D UI_ZOOM=3.5
+  -D UI_RECENT_LIST_SIZE=9
+  -D UI_SENSORS_PAGE=1
+  -D PIN_USER_BTN=38
+  -D HAS_TOUCH
+  -I variants/sensecap_indicator-espnow
+build_src_filter = ${esp32_base.build_src_filter}
+  +<../variants/sensecap_indicator-espnow/*.cpp>
+  +<helpers/esp32/ESPNOWRadio.cpp>
+  +<helpers/ui/LGFXDisplay.cpp>
+  +<helpers/sensors/*>
+lib_deps=${esp32_base.lib_deps}
+  adafruit/Adafruit BusIO @ ^1.17.2
+  lovyan03/LovyanGFX @ ^1.2.7
+
+[env:SenseCapIndicator-ESPNow_comp_radio_usb]
+extends =SenseCapIndicator-ESPNow
+build_flags =
+  ${SenseCapIndicator-ESPNow.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=300
+  -D MAX_GROUP_CHANNELS=8
+; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
+; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
+; NOTE: DO NOT ENABLE -->  -D ESPNOW_DEBUG_LOGGING=1
+build_src_filter = ${SenseCapIndicator-ESPNow.build_src_filter}
+  +<../examples/companion_radio/ui-new/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+lib_deps =
+  ${SenseCapIndicator-ESPNow.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 56 - 0
variants/sensecap_indicator-espnow/target.cpp

@@ -0,0 +1,56 @@
+#include <Arduino.h>
+#include "target.h"
+#include <helpers/ArduinoHelpers.h>
+
+ESP32Board board;
+
+ESPNOWRadio radio_driver;
+
+ESP32RTCClock rtc_clock;
+#if defined(ENV_INCLUDE_GPS)
+MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, (mesh::RTCClock*)&rtc_clock);
+EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
+#else
+EnvironmentSensorManager sensors = EnvironmentSensorManager();
+#endif
+
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display;
+  #ifdef PIN_USER_BTN
+  MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
+  #endif
+#endif
+
+bool radio_init() {
+  rtc_clock.begin();
+
+  radio_driver.init();
+
+  return true;  // success
+}
+
+uint32_t radio_get_rng_seed() {
+  return millis() + radio_driver.intID();  // TODO: where to get some entropy?
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  // no-op
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio_driver.setTxPower(dbm);
+}
+
+// NOTE: as we are using the WiFi radio, the ESP_IDF will have enabled hardware RNG:
+//    https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/random.html
+class ESP_RNG : public mesh::RNG {
+public:
+  void random(uint8_t* dest, size_t sz) override {
+    esp_fill_random(dest, sz);
+  }
+};
+
+mesh::LocalIdentity radio_new_identity() {
+  ESP_RNG rng;
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 29 - 0
variants/sensecap_indicator-espnow/target.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <helpers/ESP32Board.h>
+#include <helpers/esp32/ESPNOWRadio.h>
+#include <helpers/SensorManager.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+#ifdef ENV_INCLUDE_GPS
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+#endif
+#ifdef DISPLAY_CLASS
+  #include "SCIndicatorDisplay.h"
+  #include <helpers/ui/MomentaryButton.h>
+#endif
+
+extern ESP32Board board;
+extern ESPNOWRadio radio_driver;
+extern ESP32RTCClock 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();

+ 1 - 16
src/helpers/StationG2Board.h → variants/station_g2/StationG2Board.h

@@ -1,22 +1,7 @@
 #pragma once
 
 #include <Arduino.h>
-
-// LoRa radio module pins for Station G2
-#define  P_LORA_DIO_1   48
-#define  P_LORA_NSS     11
-#define  P_LORA_RESET   21
-#define  P_LORA_BUSY    47
-#define  P_LORA_SCLK    12
-#define  P_LORA_MISO    14
-#define  P_LORA_MOSI    13
-
-// built-ins
-//#define  PIN_LED_BUILTIN 35
-//#define  PIN_VEXT_EN     36
-
-#include "ESP32Board.h"
-
+#include <helpers/ESP32Board.h>
 #include <driver/rtc_io.h>
 
 class StationG2Board : public ESP32Board {

+ 33 - 3
variants/station_g2/platformio.ini

@@ -3,28 +3,40 @@ extends = esp32_base
 board = station-g2
 build_flags =
   ${esp32_base.build_flags}
+  ${sensor_base.build_flags}
   -I variants/station_g2
+  -I src/helpers/ui
   -D STATION_G2
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D P_LORA_DIO_1=48
+  -D P_LORA_NSS=11
+  -D P_LORA_RESET=21
+  -D P_LORA_BUSY=47
+  -D P_LORA_SCLK=12
+  -D P_LORA_MISO=14
+  -D P_LORA_MOSI=13
   -D LORA_TX_POWER=19
 ;  -D P_LORA_TX_LED=35
   -D PIN_BOARD_SDA=5
   -D PIN_BOARD_SCL=6
   -D PIN_USER_BTN=38
+  -D PIN_GPS_RX=7
+  -D PIN_GPS_TX=15
   -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 - DO NOT ENABLE THIS!
 ;  https://wiki.uniteng.com/en/meshtastic/station-g2#impact-of-lora-node-dense-areashigh-noise-environments-on-rf-performance
-  -I src/helpers/ui
   -D DISPLAY_CLASS=SH1106Display
 build_src_filter = ${esp32_base.build_src_filter}
   +<../variants/station_g2>
+  +<helpers/sensors>
   +<helpers/ui/SH1106Display.cpp>
   +<helpers/ui/MomentaryButton.cpp>
 lib_deps =
   ${esp32_base.lib_deps}
+  ${sensor_base.lib_deps}
   adafruit/Adafruit SH110X @ ~2.1.13
   adafruit/Adafruit GFX Library @ ^1.12.1
 
@@ -172,7 +184,6 @@ extends = Station_G2
 build_flags =
   ${Station_G2.build_flags}
   -I examples/companion_radio/ui-new
-  -D DISPLAY_CLASS=SH1106Display
   -D MAX_CONTACTS=300
   -D MAX_GROUP_CHANNELS=8
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
@@ -190,7 +201,6 @@ extends = Station_G2
 build_flags =
   ${Station_G2.build_flags}
   -I examples/companion_radio/ui-new
-  -D DISPLAY_CLASS=SH1106Display
   -D MAX_CONTACTS=300
   -D MAX_GROUP_CHANNELS=8
   -D BLE_PIN_CODE=123456
@@ -205,3 +215,23 @@ build_src_filter = ${Station_G2.build_src_filter}
 lib_deps =
   ${Station_G2.lib_deps}
   densaugeo/base64 @ ~1.4.0
+
+[env:Station_G2_companion_radio_wifi]
+extends = Station_G2
+build_flags =
+  ${Station_G2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=300
+  -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 = ${Station_G2.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${Station_G2.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 8 - 1
variants/station_g2/target.cpp

@@ -14,7 +14,14 @@ WRAPPER_CLASS radio_driver(radio, board);
 
 ESP32RTCClock fallback_clock;
 AutoDiscoverRTCClock rtc_clock(fallback_clock);
-SensorManager sensors;
+
+#if ENV_INCLUDE_GPS
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+  MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
+  EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
+#else
+  EnvironmentSensorManager sensors;
+#endif
 
 #ifdef DISPLAY_CLASS
   DISPLAY_CLASS display;

+ 3 - 3
variants/station_g2/target.h

@@ -3,10 +3,10 @@
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
 #include <helpers/radiolib/RadioLibWrappers.h>
-#include <helpers/StationG2Board.h>
+#include <StationG2Board.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/SH1106Display.h>
@@ -16,7 +16,7 @@
 extern StationG2Board board;
 extern WRAPPER_CLASS radio_driver;
 extern AutoDiscoverRTCClock rtc_clock;
-extern SensorManager sensors;
+extern EnvironmentSensorManager sensors;
 
 #ifdef DISPLAY_CLASS
   extern DISPLAY_CLASS display;

+ 1 - 0
variants/t1000-e/platformio.ini

@@ -26,6 +26,7 @@ build_flags = ${nrf52_base.build_flags}
   -D P_LORA_RESET=42 ; P1.10
   -D LR11X0_DIO_AS_RF_SWITCH=true
   -D LR11X0_DIO3_TCXO_VOLTAGE=1.6
+  -D ENV_INCLUDE_GPS=1
 build_src_filter = ${nrf52_base.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/nrf52/T1000eBoard.cpp>

+ 32 - 0
variants/thinknode_m2/ThinknodeM2Board.cpp

@@ -0,0 +1,32 @@
+#include "ThinknodeM2Board.h"
+
+
+
+void ThinknodeM2Board::begin() {
+    ESP32Board::begin();
+    pinMode(PIN_VEXT_EN, OUTPUT); // init display
+    digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // pin needs to be high
+    delay(10); 
+    digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE); // need to do this twice. do not know why..
+    pinMode(PIN_STATUS_LED, OUTPUT); // init power led    
+  }
+
+  void ThinknodeM2Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) {
+    esp_deep_sleep_start();
+  }
+
+  void ThinknodeM2Board::powerOff()  {
+    enterDeepSleep(0);
+  }
+
+  uint16_t ThinknodeM2Board::getBattMilliVolts()  {
+    analogReadResolution(12);
+    delay(10);
+    float volts = (analogRead(PIN_VBAT_READ) * ADC_MULTIPLIER * AREF_VOLTAGE) / 4096;
+    analogReadResolution(10);
+    return volts * 1000;
+  }
+
+  const char* ThinknodeM2Board::getManufacturerName() const {
+    return "Elecrow ThinkNode M2";
+  }

+ 18 - 0
variants/thinknode_m2/ThinknodeM2Board.h

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

+ 28 - 0
variants/thinknode_m2/pins_arduino.h

@@ -0,0 +1,28 @@
+// Need this file for ESP32-S3
+// No need to modify this file, changes to pins imported from variant.h
+// Most is similar to https://github.com/espressif/arduino-esp32/blob/master/variants/esp32s3/pins_arduino.h
+
+#ifndef Pins_Arduino_h
+#define Pins_Arduino_h
+
+#include <stdint.h>
+#include <variant.h>
+
+#define USB_VID 0x303a
+#define USB_PID 0x1001
+
+// Serial
+static const uint8_t TX = GPS_TX;
+static const uint8_t RX = GPS_RX;
+
+// Default SPI will be mapped to Radio
+static const uint8_t SS = P_LORA_NSS;
+static const uint8_t SCK = P_LORA_SCLK;
+static const uint8_t MOSI = P_LORA_MISO;
+static const uint8_t MISO = P_LORA_MOSI;
+
+// The default Wire will be mapped to PMU and RTC
+static const uint8_t SCL = PIN_BOARD_SCL;
+static const uint8_t SDA = PIN_BOARD_SDA;
+
+#endif /* Pins_Arduino_h */

+ 171 - 0
variants/thinknode_m2/platformio.ini

@@ -0,0 +1,171 @@
+[ThinkNode_M2]
+extends = esp32_base
+board = ESP32-S3-WROOM-1-N4
+build_flags = ${esp32_base.build_flags}
+  -I variants/thinknode_m2
+  -D THINKNODE_M2
+  -D GPS_RX=44
+  -D GPS_TX=43
+  -D PIN_VEXT_EN=46
+  -D PIN_BUZZER=5
+  -D PIN_VEXT_EN_ACTIVE=HIGH
+  -D PIN_BOARD_SCL=15
+  -D PIN_BOARD_SDA=16
+  -D P_LORA_DIO_1=3
+  -D P_LORA_NSS=10
+  -D P_LORA_RESET=21   ; RADIOLIB_NC
+  -D P_LORA_BUSY=14   ;  DIO2 = 38
+  -D P_LORA_SCLK=12
+  -D P_LORA_MISO=13
+  -D P_LORA_MOSI=11
+  -D PIN_USER_BTN=47
+  -D PIN_STATUS_LED=6
+  -D PIN_LED=6
+  -D SX126X_DIO2_AS_RF_SWITCH=true
+  -D SX126X_DIO3_TCXO_VOLTAGE=3.3
+  -D SX126X_CURRENT_LIMIT=140
+  -D DISPLAY_CLASS=SH1106Display
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D LORA_TX_POWER=22
+  -D SX126X_RX_BOOSTED_GAIN=1
+  -D MESH_DEBUG=1
+build_src_filter = ${esp32_base.build_src_filter}
+  +<helpers/ui/SH1106Display.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<helpers/ui/buzzer.cpp>
+  +<../variants/thinknode_m2>
+lib_deps = ${esp32_base.lib_deps}
+  adafruit/Adafruit SH110X @ ~2.1.13
+  adafruit/Adafruit GFX Library @ ^1.12.1
+
+[env:ThinkNode_M2_Repeater]
+extends = ThinkNode_M2
+build_src_filter = ${ThinkNode_M2.build_src_filter}
+  +<../examples/simple_repeater/*.cpp>
+build_flags =
+  ${ThinkNode_M2.build_flags}
+  -D ADVERT_NAME='"Thinknode M2 Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=8
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+lib_deps =
+  ${ThinkNode_M2.lib_deps}
+  ${esp32_ota.lib_deps}
+
+; [env:ThinkNode_M2_Repeater_bridge_rs232]
+; extends = ThinkNode_M2
+; build_src_filter = ${ThinkNode_M2.build_src_filter}
+;   +<helpers/bridges/RS232Bridge.cpp>
+;   +<../examples/simple_repeater/*.cpp>
+; build_flags =
+;   ${ThinkNode_M2.build_flags}
+;   -D ADVERT_NAME='"RS232 Bridge"'
+;   -D ADVERT_LAT=0.0
+;   -D ADVERT_LON=0.0
+;   -D ADMIN_PASSWORD='"password"'
+;   -D MAX_NEIGHBOURS=8
+;   -D WITH_RS232_BRIDGE=Serial2
+;   -D WITH_RS232_BRIDGE_RX=5
+;   -D WITH_RS232_BRIDGE_TX=6
+; ;  -D MESH_PACKET_LOGGING=1
+; ;  -D MESH_DEBUG=1
+; lib_deps =
+;   ${ThinkNode_M2.lib_deps}
+;   ${esp32_ota.lib_deps}
+
+[env:ThinkNode_M2_Repeater_bridge_espnow]
+extends = ThinkNode_M2
+build_src_filter = ${ThinkNode_M2.build_src_filter}
+  +<helpers/bridges/ESPNowBridge.cpp>
+  +<../examples/simple_repeater/*.cpp>
+build_flags =
+  ${ThinkNode_M2.build_flags}
+  -D ADVERT_NAME='"ESPNow Bridge"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=8
+  -D WITH_ESPNOW_BRIDGE=1
+  -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+lib_deps =
+  ${ThinkNode_M2.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:ThinkNode_M2_room_server]
+extends = ThinkNode_M2
+build_src_filter = ${ThinkNode_M2.build_src_filter}
+  +<../examples/simple_room_server>
+build_flags =
+  ${ThinkNode_M2.build_flags}
+  -D ADVERT_NAME='"Thinknode M2 Room Server"'
+  -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
+lib_deps =
+  ${ThinkNode_M2.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:ThinkNode_M2_terminal_chat]
+extends = ThinkNode_M2
+build_flags =
+  ${ThinkNode_M2.build_flags}
+  -D MAX_CONTACTS=300
+  -D MAX_GROUP_CHANNELS=8
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ThinkNode_M2.build_src_filter}
+  +<../examples/simple_secure_chat/main.cpp>
+lib_deps =
+  ${ThinkNode_M2.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:ThinkNode_M2_companion_radio_ble]
+extends = ThinkNode_M2
+build_flags =
+  ${ThinkNode_M2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=300
+  -D MAX_GROUP_CHANNELS=8
+  -D BLE_PIN_CODE=123456
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D BLE_DEBUG_LOGGING=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ThinkNode_M2.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+
+lib_deps =
+  ${ThinkNode_M2.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:ThinkNode_M2_companion_radio_serial]
+extends = ThinkNode_M2
+build_flags =
+  ${ThinkNode_M2.build_flags}
+  -I examples/companion_radio/ui-new
+  -D MAX_CONTACTS=300
+  -D MAX_GROUP_CHANNELS=8
+  -D SERIAL_TX=D6
+  -D SERIAL_RX=D7
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${ThinkNode_M2.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<helpers/ui/MomentaryButton.cpp>
+  +<../examples/companion_radio/*.cpp>
+  +<../examples/companion_radio/ui-new/*.cpp>
+lib_deps =
+  ${ThinkNode_M2.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 57 - 0
variants/thinknode_m2/target.cpp

@@ -0,0 +1,57 @@
+#include <Arduino.h>
+#include "target.h"
+
+ThinknodeM2Board 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);
+SensorManager sensors;
+
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display;
+  MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
+#endif
+
+bool radio_init() {
+  fallback_clock.begin();
+  rtc_clock.begin(Wire);
+  pinMode(21, INPUT);
+  pinMode(48, OUTPUT);
+  #if defined(P_LORA_SCLK)
+  spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
+  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
+}
+

+ 32 - 0
variants/thinknode_m2/target.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+//#include <helpers/ESP32Board.h>
+#include <ThinknodeM2Board.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/AutoDiscoverRTCClock.h>
+#include <helpers/SensorManager.h>
+#ifdef DISPLAY_CLASS
+  #include <helpers/ui/SH1106Display.h>
+  #include <helpers/ui/MomentaryButton.h>
+#endif
+
+extern ThinknodeM2Board board;
+extern WRAPPER_CLASS radio_driver;
+extern AutoDiscoverRTCClock rtc_clock;
+extern SensorManager 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();
+
+ 

+ 15 - 0
variants/thinknode_m2/variant.h

@@ -0,0 +1,15 @@
+#define I2C_SCL 15
+#define I2C_SDA 16
+#define  PIN_VBAT_READ    17
+#define AREF_VOLTAGE            (3.0)
+#define ADC_MULTIPLIER (1.548F)
+#define PIN_BUZZER 5
+#define PIN_VEXT_EN_ACTIVE HIGH
+#define PIN_VEXT_EN 46
+#define PIN_USER_BTN 47
+#define PIN_LED 6
+#define PIN_STATUS_LED 6
+#define PIN_PWRBTN 4
+
+
+

+ 1 - 1
variants/wio-tracker-l1/target.cpp

@@ -24,7 +24,7 @@ EnvironmentSensorManager sensors = EnvironmentSensorManager();
   MomentaryButton user_btn(PIN_USER_BTN, 1000, true, false, false);
   MomentaryButton joystick_left(JOYSTICK_LEFT, 1000, true, false, false);
   MomentaryButton joystick_right(JOYSTICK_RIGHT, 1000, true, false, false);
-  MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, false);
+  MomentaryButton back_btn(PIN_BACK_BTN, 1000, true, false, true);
 #endif
 
 bool radio_init() {

+ 75 - 0
variants/wio_wm1110/WioWM1110Board.cpp

@@ -0,0 +1,75 @@
+#ifdef WIO_WM1110
+
+#include <Arduino.h>
+#include <Wire.h>
+#include <bluefruit.h>
+
+#include "WioWM1110Board.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 WioWM1110Board::begin() {
+  startup_reason = BD_STARTUP_NORMAL;
+
+  sd_power_mode_set(NRF_POWER_MODE_LOWPWR);
+  NRF_POWER->DCDCEN = 1;
+
+  pinMode(BATTERY_PIN, INPUT);
+  pinMode(LED_GREEN, OUTPUT);
+  pinMode(LED_RED, OUTPUT);
+  pinMode(SENSOR_POWER_PIN, OUTPUT);
+
+  digitalWrite(LED_GREEN, HIGH);
+  digitalWrite(LED_RED, LOW);
+  digitalWrite(SENSOR_POWER_PIN, LOW);
+
+  Serial1.begin(115200);
+
+#if defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL)
+  Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
+#endif
+
+  Wire.begin();
+
+  delay(10);
+}
+
+bool WioWM1110Board::startOTAUpdate(const char *id, char reply[]) {
+  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
+  Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
+
+  Bluefruit.begin(1, 0);
+  Bluefruit.setTxPower(4);
+  Bluefruit.setName("WM1110_OTA");
+
+  Bluefruit.Periph.setConnectCallback(connect_callback);
+  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
+
+  bledfu.begin();
+
+  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
+  Bluefruit.Advertising.addTxPower();
+  Bluefruit.Advertising.addName();
+  Bluefruit.Advertising.restartOnDisconnect(true);
+  Bluefruit.Advertising.setInterval(32, 244);
+  Bluefruit.Advertising.setFastTimeout(30);
+  Bluefruit.Advertising.start(0);
+
+  strcpy(reply, "OK - started");
+  return true;
+}
+
+#endif
+

+ 58 - 0
variants/wio_wm1110/WioWM1110Board.h

@@ -0,0 +1,58 @@
+#pragma once
+
+#include <MeshCore.h>
+#include <Arduino.h>
+
+#ifdef WIO_WM1110
+
+#ifdef Serial
+  #undef Serial
+#endif
+#define Serial Serial1
+
+class WioWM1110Board : public mesh::MainBoard {
+protected:
+  uint8_t startup_reason;
+
+public:
+  void begin();
+  uint8_t getStartupReason() const override { return startup_reason; }
+
+#if defined(LED_GREEN)
+  void onBeforeTransmit() override {
+    digitalWrite(LED_RED, HIGH);
+  }
+  void onAfterTransmit() override {
+    digitalWrite(LED_RED, LOW);
+  }
+#endif
+
+  uint16_t getBattMilliVolts() override {
+    int adcvalue = 0;
+    analogReadResolution(12);
+    analogReference(AR_INTERNAL_3_0);
+    delay(10);
+    adcvalue = analogRead(BATTERY_PIN);
+    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE * 1000.0) / 4096.0;
+  }
+
+  const char* getManufacturerName() const override {
+    return "Seeed Wio WM1110";
+  }
+
+  void reboot() override {
+    NVIC_SystemReset();
+  }
+
+  bool startOTAUpdate(const char* id, char reply[]) override;
+
+  void enableSensorPower(bool enable) {
+    digitalWrite(SENSOR_POWER_PIN, enable ? HIGH : LOW);
+    if (enable) {
+      delay(100);
+    }
+  }
+};
+
+#endif
+

+ 86 - 0
variants/wio_wm1110/platformio.ini

@@ -0,0 +1,86 @@
+[wio_wm1110]
+extends = nrf52_base
+board = seeed-xiao-afruitnrf52-nrf52840
+board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52_base.build_flags}
+  ${sensor_base.build_flags}
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
+  -I variants/wio_wm1110
+  -D NRF52_PLATFORM
+  -D WIO_WM1110
+;  -D MESH_DEBUG=1
+  -D RADIO_CLASS=CustomLR1110
+  -D WRAPPER_CLASS=CustomLR1110Wrapper
+  -D LORA_TX_POWER=22
+  -D RX_BOOSTED_GAIN=true
+  -D P_LORA_DIO_1=40
+  -D P_LORA_RESET=42
+  -D P_LORA_BUSY=43
+  -D P_LORA_NSS=44
+  -D P_LORA_SCLK=45
+  -D P_LORA_MOSI=46
+  -D P_LORA_MISO=47
+  -D LR11X0_DIO_AS_RF_SWITCH=true
+  -D LR11X0_DIO3_TCXO_VOLTAGE=1.8
+  -D RF_SWITCH_TABLE
+  -D ENV_INCLUDE_GPS=0
+build_src_filter = ${nrf52_base.build_src_filter}
+  +<helpers/*.cpp>
+  +<helpers/sensors>
+  +<../variants/wio_wm1110>
+debug_tool = jlink
+upload_protocol = jlink
+lib_deps = ${nrf52_base.lib_deps}
+  ${sensor_base.lib_deps}
+  adafruit/Adafruit LIS3DH @ ^1.2.4
+  adafruit/Adafruit SHT4x Library @ ^1.0.4
+
+[env:wio_wm1110_repeater]
+extends = wio_wm1110
+build_flags =
+  ${wio_wm1110.build_flags}
+  -D ADVERT_NAME='"WM1110 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 = ${wio_wm1110.build_src_filter}
+  +<../examples/simple_repeater/*.cpp>
+
+[env:wio_wm1110_room_server]
+extends = wio_wm1110
+build_flags =
+  ${wio_wm1110.build_flags}
+  -D ADVERT_NAME='"WM1110 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 = ${wio_wm1110.build_src_filter}
+  +<../examples/simple_room_server/*.cpp>
+
+[env:wio_wm1110_companion_radio_ble]
+extends = wio_wm1110
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
+board_upload.maximum_size = 708608
+build_flags =
+  ${wio_wm1110.build_flags}
+  -D MAX_CONTACTS=350
+  -D MAX_GROUP_CHANNELS=40
+  -D BLE_PIN_CODE=123456
+  -D QSPIFLASH=1
+  -D OFFLINE_QUEUE_SIZE=256
+;  -D BLE_DEBUG_LOGGING=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${wio_wm1110.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/*.cpp>
+lib_deps =
+  ${wio_wm1110.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 92 - 0
variants/wio_wm1110/target.cpp

@@ -0,0 +1,92 @@
+#include <Arduino.h>
+#include "target.h"
+#include <helpers/ArduinoHelpers.h>
+
+WioWM1110Board board;
+
+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 rtc_clock;
+EnvironmentSensorManager sensors;
+
+#ifndef LORA_CR
+  #define LORA_CR      5
+#endif
+
+#ifdef RF_SWITCH_TABLE
+static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = {
+  RADIOLIB_LR11X0_DIO5,
+  RADIOLIB_LR11X0_DIO6,
+  RADIOLIB_LR11X0_DIO7,
+  RADIOLIB_LR11X0_DIO8, 
+  RADIOLIB_NC
+};
+
+static const Module::RfSwitchMode_t rfswitch_table[] = {
+  // mode                 DIO5  DIO6  DIO7  DIO8
+  { LR11x0::MODE_STBY,   {LOW,  LOW,  LOW,  LOW  }},  
+  { LR11x0::MODE_RX,     {HIGH, LOW,  LOW,  HIGH }},
+  { LR11x0::MODE_TX,     {HIGH, HIGH, LOW,  HIGH }},
+  { LR11x0::MODE_TX_HP,  {LOW,  HIGH, LOW,  HIGH }},
+  { LR11x0::MODE_TX_HF,  {LOW,  LOW,  LOW,  LOW  }}, 
+  { LR11x0::MODE_GNSS,   {LOW,  LOW,  HIGH, LOW  }},
+  { LR11x0::MODE_WIFI,   {LOW,  LOW,  LOW,  LOW  }},  
+  END_OF_MODE_TABLE,
+};
+#endif
+
+bool radio_init() {
+  board.enableSensorPower(true);
+  
+#ifdef LR11X0_DIO3_TCXO_VOLTAGE
+  float tcxo = LR11X0_DIO3_TCXO_VOLTAGE;
+#else
+  float tcxo = 1.8f;
+#endif
+
+  SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
+  SPI.begin();
+  
+  int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
+  if (status != RADIOLIB_ERR_NONE) {
+    Serial.print("ERROR: radio init failed: ");
+    Serial.println(status);
+    return false;  // fail
+  }
+  
+  radio.setCRC(2);
+  radio.explicitHeader();
+
+#ifdef RF_SWITCH_TABLE
+  radio.setRfSwitchTable(rfswitch_dios, rfswitch_table);
+#endif
+
+#ifdef RX_BOOSTED_GAIN
+  radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
+#endif
+
+  return true;  // success
+}
+
+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
+}
+

+ 21 - 0
variants/wio_wm1110/target.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include "WioWM1110Board.h"
+#include <helpers/radiolib/CustomLR1110Wrapper.h>
+#include <helpers/ArduinoHelpers.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+
+extern WioWM1110Board board;
+extern WRAPPER_CLASS radio_driver;
+extern VolatileRTCClock 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();
+

+ 92 - 0
variants/wio_wm1110/variant.cpp

@@ -0,0 +1,92 @@
+/*
+ * variant.cpp - Seeed Wio WM1110 Dev Board
+ * Pin mapping for nRF52840
+ */
+
+#include "variant.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+
+const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] =
+{
+  0,  // P0.00
+  1,  // P0.01
+  2,  // P0.02, AIN0, SENSOR_AIN_0
+  3,  // P0.03, AIN1, SENSOR_AIN_1
+  4,  // P0.04, AIN2, SENSOR_AIN_2
+  5,  // P0.05, AIN3, SENSOR_AIN_3
+  6,  // P0.06, PIN_SERIAL2_RX, SENSOR_RXD
+  7,  // P0.07, SENSOR_POWER_PIN
+  8,  // P0.08, PIN_SERIAL2_TX, SENSOR_TXD
+  9,  // P0.09
+  10, // P0.10
+  11, // P0.11, LIS3DH_INT_PIN_1, SENSOR_INT_1
+  12, // P0.12, LIS3DH_INT_PIN_2, SENSOR_INT_2
+  13, // P0.13, LED_GREEN, USER_LED_G
+  14, // P0.14, LED_RED, USER_LED_R
+  15, // P0.15
+  16, // P0.16
+  17, // P0.17
+  18, // P0.18
+  19, // P0.19
+  20, // P0.20
+  21, // P0.21
+  22, // P0.22, PIN_SERIAL1_RX, DEBUG_RX_PIN
+  23, // P0.23
+  24, // P0.24, PIN_SERIAL1_TX, DEBUG_TX_PIN
+  25, // P0.25
+  26, // P0.26, PIN_WIRE_SCL, SENSOR_SCL
+  27, // P0.27, PIN_WIRE_SDA, SENSOR_SDA
+  28, // P0.28, AIN4, SENSOR_AIN_4
+  29, // P0.29, AIN5, SENSOR_AIN_5
+  30, // P0.30, AIN6, SENSOR_AIN_6
+  31, // P0.31, AIN7, SENSOR_AIN_7, BATTERY_PIN
+  32, // P1.00
+  33, // P1.01
+  34, // P1.02
+  35, // P1.03
+  36, // P1.04
+  37, // P1.05, LR1110_GNSS_ANT_PIN
+  38, // P1.06
+  39, // P1.07
+  40, // P1.08, LORA_DIO_1, LR1110_IRQ_PIN
+  41, // P1.09
+  42, // P1.10, LORA_RESET, LR1110_NRESET_PIN
+  43, // P1.11, LORA_BUSY, LR1110_BUSY_PIN
+  44, // P1.12, PIN_SPI_NSS, LR1110_SPI_NSS_PIN
+  45, // P1.13, PIN_SPI_SCK, LR1110_SPI_SCK_PIN
+  46, // P1.14, PIN_SPI_MOSI, LR1110_SPI_MOSI_PIN
+  47, // P1.15, PIN_SPI_MISO, LR1110_SPI_MISO_PIN
+  255,  // NRFX_SPIM_PIN_NOT_USED
+};
+
+void initVariant()
+{
+  // All pins output HIGH by default.
+  // https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/blob/fab7d30a997a1dfeef9d1d59bfb549adda73815a/cores/nRF5/wiring.c#L65-L69
+
+  // Set analog input pins
+  pinMode(BATTERY_PIN, INPUT);
+  pinMode(SENSOR_AIN_0, INPUT);
+  pinMode(SENSOR_AIN_1, INPUT);
+  pinMode(SENSOR_AIN_2, INPUT);
+  pinMode(SENSOR_AIN_3, INPUT);
+  pinMode(SENSOR_AIN_4, INPUT);
+  pinMode(SENSOR_AIN_5, INPUT);
+  pinMode(SENSOR_AIN_6, INPUT);
+  
+  // Sensor interrupts as inputs
+  pinMode(LIS3DH_INT_PIN_1, INPUT);
+  pinMode(LIS3DH_INT_PIN_2, INPUT);
+
+  // Set output pins
+  pinMode(LED_GREEN, OUTPUT);
+  pinMode(LED_RED, OUTPUT);
+  pinMode(SENSOR_POWER_PIN, OUTPUT);
+
+  // Initialize outputs to safe states
+  digitalWrite(LED_GREEN, HIGH);  // Power indicator LED on
+  digitalWrite(LED_RED, LOW);
+  digitalWrite(SENSOR_POWER_PIN, LOW);  // Sensors powered off initially
+}
+

+ 145 - 0
variants/wio_wm1110/variant.h

@@ -0,0 +1,145 @@
+/*
+ * variant.h - Seeed Wio WM1110 Dev Board
+ * nRF52840 + LR1110 (LoRa + GNSS + WiFi Scanner)
+ */
+
+#pragma once
+
+#include "WVariant.h"
+
+////////////////////////////////////////////////////////////////////////////////
+// Low frequency clock source
+
+#define USE_LFXO    // 32.768 kHz crystal oscillator
+#define VARIANT_MCK (64000000ul)
+
+////////////////////////////////////////////////////////////////////////////////
+// Power
+
+#define BATTERY_PIN             (31)            // AIN7
+#define BATTERY_IMMUTABLE
+#define ADC_MULTIPLIER          (2.0F)
+
+#define ADC_RESOLUTION          (14)
+#define BATTERY_SENSE_RES       (12)
+
+#define AREF_VOLTAGE            (3.0)
+
+////////////////////////////////////////////////////////////////////////////////
+// Number of pins
+
+#define PINS_COUNT              (48)
+#define NUM_DIGITAL_PINS        (48)
+#define NUM_ANALOG_INPUTS       (8)
+#define NUM_ANALOG_OUTPUTS      (0)
+
+////////////////////////////////////////////////////////////////////////////////
+// UART pin definition
+
+#define PIN_SERIAL1_RX          (22)
+#define PIN_SERIAL1_TX          (24)
+
+#define PIN_SERIAL2_RX          (6)
+#define PIN_SERIAL2_TX          (8)
+
+////////////////////////////////////////////////////////////////////////////////
+// I2C pin definition
+
+#define HAS_WIRE                (1)
+#define WIRE_INTERFACES_COUNT   (1)
+
+#define PIN_WIRE_SDA            (27)
+#define PIN_WIRE_SCL            (26)
+#define I2C_NO_RESCAN
+
+#define SENSOR_POWER_PIN        (7)
+
+#define HAS_LIS3DH              (1)
+#define LIS3DH_INT_PIN_1        (11)
+#define LIS3DH_INT_PIN_2        (12)
+
+#define HAS_SHT41               (1)
+
+////////////////////////////////////////////////////////////////////////////////
+// SPI pin definition
+
+#define SPI_INTERFACES_COUNT    (1)
+
+#define PIN_SPI_MISO            (47)
+#define PIN_SPI_MOSI            (46)
+#define PIN_SPI_SCK             (45)
+#define PIN_SPI_NSS             (44)
+
+////////////////////////////////////////////////////////////////////////////////
+// Builtin LEDs
+
+#define LED_BUILTIN             (13)
+#define LED_GREEN               (13)
+#define LED_RED                 (14)
+#define LED_BLUE                LED_RED
+#define LED_PIN                 LED_GREEN
+
+#define LED_STATE_ON            HIGH
+
+////////////////////////////////////////////////////////////////////////////////
+// Builtin buttons
+
+#define PIN_BUTTON1             (-1)
+#define BUTTON_PIN              PIN_BUTTON1
+
+////////////////////////////////////////////////////////////////////////////////
+// LR1110 LoRa Radio + GNSS + WiFi
+
+#define LORA_DIO_1              (40)            // P1.8 - LR1110_IRQ_PIN
+#define LORA_NSS                (PIN_SPI_NSS)   // P1.12
+#define LORA_RESET              (42)            // P1.10 - LR1110_NRESET_PIN
+#define LORA_BUSY               (43)            // P1.11 - LR1110_BUSY_PIN
+#define LORA_SCLK               (PIN_SPI_SCK)   // P1.13
+#define LORA_MISO               (PIN_SPI_MISO)  // P1.15
+#define LORA_MOSI               (PIN_SPI_MOSI)  // P1.14
+#define LORA_CS                 PIN_SPI_NSS     // P1.12
+
+// LR1110 specific settings
+#define LR11X0_DIO_AS_RF_SWITCH    true
+#define LR11X0_DIO3_TCXO_VOLTAGE   1.8
+#define LR1110_GNSS_ANT_PIN        (37)         // P1.5
+
+// Pin aliases for LR1110 driver compatibility
+#define LR1110_IRQ_PIN          LORA_DIO_1
+#define LR1110_NRESET_PIN       LORA_RESET
+#define LR1110_BUSY_PIN         LORA_BUSY
+#define LR1110_SPI_NSS_PIN      LORA_CS
+#define LR1110_SPI_SCK_PIN      LORA_SCLK
+#define LR1110_SPI_MOSI_PIN     LORA_MOSI
+#define LR1110_SPI_MISO_PIN     LORA_MISO
+
+////////////////////////////////////////////////////////////////////////////////
+// Analog Input Pins
+
+#define SENSOR_AIN_0            (2)
+#define SENSOR_AIN_1            (3)
+#define SENSOR_AIN_2            (4)
+#define SENSOR_AIN_3            (5)
+#define SENSOR_AIN_4            (28)
+#define SENSOR_AIN_5            (29)
+#define SENSOR_AIN_6            (30)
+#define SENSOR_AIN_7            (31)
+
+static const uint8_t A0  = SENSOR_AIN_0;
+static const uint8_t A1  = SENSOR_AIN_1;
+static const uint8_t A2  = SENSOR_AIN_2;
+static const uint8_t A3  = SENSOR_AIN_3;
+static const uint8_t A4  = SENSOR_AIN_4;
+static const uint8_t A5  = SENSOR_AIN_5;
+static const uint8_t A6  = SENSOR_AIN_6;
+static const uint8_t A7  = SENSOR_AIN_7;
+
+////////////////////////////////////////////////////////////////////////////////
+// GPS/GNSS
+
+#define HAS_GPS                 0
+#define PIN_GPS_TX              (-1)
+#define PIN_GPS_RX              (-1)
+#define GPS_EN                  (-1)
+#define GPS_RESET               (-1)
+

+ 10 - 10
variants/xiao_c6/platformio.ini

@@ -30,7 +30,7 @@ build_src_filter = ${esp32c6_base.build_src_filter}
   +<../variants/xiao_c6>
   +<XiaoC6Board.cpp>
 
-[env:Xiao_C6_repeater]
+[env:Xiao_C6_repeater_]
 extends = Xiao_C6
 build_src_filter = ${Xiao_C6.build_src_filter}
   +<../examples/simple_repeater/*.cpp>
@@ -47,7 +47,7 @@ lib_deps =
   ${Xiao_C6.lib_deps}
 ;  ${esp32_ota.lib_deps}
 
-[env:Xiao_C6_companion_radio_ble]
+[env:Xiao_C6_companion_radio_ble_]
 extends = Xiao_C6
 build_flags = ${Xiao_C6.build_flags}
   -D MAX_CONTACTS=300
@@ -90,10 +90,10 @@ build_flags =
   -D SX126X_RX_BOOSTED_GAIN=1
   -D USE_XIAO_ESP32C6_EXTERNAL_ANTENNA=1
 
-[env:Meshimi_repeater]
+[env:Meshimi_repeater_]
 extends = Meshimi
 build_src_filter = ${Meshimi.build_src_filter}
-                   +<../examples/simple_repeater/*.cpp>
+  +<../examples/simple_repeater/*.cpp>
 build_flags =
   ${Meshimi.build_flags}
   -D ADVERT_NAME='"Meshimi Repeater"'
@@ -104,7 +104,7 @@ build_flags =
 lib_deps =
   ${Meshimi.lib_deps}
 
-[env:Meshimi_companion_radio_ble]
+[env:Meshimi_companion_radio_ble_]
 extends = Meshimi
 build_flags = ${Meshimi.build_flags}
   -D MAX_CONTACTS=300
@@ -115,9 +115,9 @@ build_flags = ${Meshimi.build_flags}
   -D ENABLE_PRIVATE_KEY_IMPORT=1
   -D ENABLE_PRIVATE_KEY_EXPORT=1
 build_src_filter = ${Meshimi.build_src_filter}
-                   +<helpers/esp32/*.cpp>
-                   -<helpers/esp32/ESPNOWRadio.cpp>
-                   +<../examples/companion_radio/*.cpp>
+  +<helpers/esp32/*.cpp>
+  -<helpers/esp32/ESPNOWRadio.cpp>
+  +<../examples/companion_radio/*.cpp>
 lib_deps =
   ${Meshimi.lib_deps}
   densaugeo/base64 @ ~1.4.0
@@ -147,7 +147,7 @@ build_flags =
   -USX126X_DIO2_AS_RF_SWITCH
   -USX126X_DIO3_TCXO_VOLTAGE
 
-[env:WHY2025_badge_repeater]
+[env:WHY2025_badge_repeater_]
 extends = WHY2025_badge
 build_src_filter = ${WHY2025_badge.build_src_filter}
   +<../examples/simple_repeater/*.cpp>
@@ -164,7 +164,7 @@ lib_deps =
   ${WHY2025_badge.lib_deps}
 ;  ${esp32_ota.lib_deps}
 
-[env:WHY2025_badge_companion_radio_ble]
+[env:WHY2025_badge_companion_radio_ble_]
 extends = WHY2025_badge
 build_flags = ${WHY2025_badge.build_flags}
   -D MAX_CONTACTS=300

+ 4 - 26
variants/xiao_rp2040/target.cpp

@@ -20,34 +20,12 @@ SensorManager sensors;
 bool radio_init() {
   rtc_clock.begin(Wire);
 
-#ifdef SX126X_DIO3_TCXO_VOLTAGE
-  float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
+#if defined(P_LORA_SCLK)
+  spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
+  return radio.std_init(&spi);
 #else
-  float tcxo = 1.6f;
+  return radio.std_init();
 #endif
-  int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo);
-
-  if (status != RADIOLIB_ERR_NONE) {
-    Serial.print("ERROR: radio init failed: ");
-    Serial.println(status);
-    return false; // fail
-  }
-
-  radio.setCRC(1);
-
-#ifdef SX126X_CURRENT_LIMIT
-  radio.setCurrentLimit(SX126X_CURRENT_LIMIT);
-#endif
-
-#ifdef SX126X_DIO2_AS_RF_SWITCH
-  radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
-#endif
-
-#ifdef SX126X_RX_BOOSTED_GAIN
-  radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
-#endif
-
-  return true; // success
 }
 
 uint32_t radio_get_rng_seed() {

+ 13 - 0
variants/xiao_s3_wio/XiaoS3WIOBoard.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <Arduino.h>
+#include <helpers/ESP32Board.h>
+
+class XiaoS3WIOBoard : public ESP32Board {
+public:
+  XiaoS3WIOBoard() { }
+
+  const char* getManufacturerName() const override {
+    return "Xiao S3 WIO";
+  }
+};

+ 1 - 1
variants/xiao_s3_wio/target.cpp

@@ -1,7 +1,7 @@
 #include <Arduino.h>
 #include "target.h"
 
-ESP32Board board;
+XiaoS3WIOBoard board;
 
 #if defined(P_LORA_SCLK)
   static SPIClass spi;

+ 2 - 1
variants/xiao_s3_wio/target.h

@@ -11,8 +11,9 @@
   #include <helpers/ui/SSD1306Display.h>
   #include <helpers/ui/MomentaryButton.h>
 #endif
+#include "XiaoS3WIOBoard.h"
 
-extern ESP32Board board;
+extern XiaoS3WIOBoard board;
 extern WRAPPER_CLASS radio_driver;
 extern AutoDiscoverRTCClock rtc_clock;
 extern SensorManager sensors;