Browse Source

Merge remote-tracking branch 'origin/dev' into jbrazio/2025_3f11ad35

João Brázio 1 year ago
parent
commit
85273a6dc6
100 changed files with 3845 additions and 1025 deletions
  1. 59 0
      boards/minewsemi_me25ls01.json
  2. 61 0
      boards/seeed-wio-tracker-l1.json
  3. 60 0
      boards/seeed_sensecap_solar.json
  4. 8 6
      docs/packet_structure.md
  5. 26 24
      docs/payloads.md
  6. 6 4
      examples/companion_radio/DataStore.cpp
  7. 87 17
      examples/companion_radio/MyMesh.cpp
  8. 7 5
      examples/companion_radio/MyMesh.h
  9. 1 1
      examples/companion_radio/NodePrefs.h
  10. 4 0
      examples/companion_radio/UITask.cpp
  11. 53 11
      examples/simple_repeater/main.cpp
  12. 65 17
      examples/simple_room_server/main.cpp
  13. 926 0
      examples/simple_sensor/SensorMesh.cpp
  14. 175 0
      examples/simple_sensor/SensorMesh.h
  15. 45 0
      examples/simple_sensor/TimeSeriesData.cpp
  16. 29 0
      examples/simple_sensor/TimeSeriesData.h
  17. 114 0
      examples/simple_sensor/UITask.cpp
  18. 19 0
      examples/simple_sensor/UITask.h
  19. 147 0
      examples/simple_sensor/main.cpp
  20. 15 8
      platformio.ini
  21. 121 15
      src/Mesh.cpp
  22. 13 2
      src/Mesh.h
  23. 1 0
      src/Packet.h
  24. 2 1
      src/helpers/AdvertDataHelpers.h
  25. 74 38
      src/helpers/BaseChatMesh.cpp
  26. 2 0
      src/helpers/BaseChatMesh.h
  27. 27 16
      src/helpers/CommonCLI.cpp
  28. 3 5
      src/helpers/CommonCLI.h
  29. 0 46
      src/helpers/CustomLR1110.h
  30. 61 46
      src/helpers/nrf52/SerialBLEInterface.cpp
  31. 6 7
      src/helpers/nrf52/SerialBLEInterface.h
  32. 0 0
      src/helpers/radiolib/CustomLLCC68.h
  33. 0 0
      src/helpers/radiolib/CustomLLCC68Wrapper.h
  34. 70 0
      src/helpers/radiolib/CustomLR1110.h
  35. 0 0
      src/helpers/radiolib/CustomLR1110Wrapper.h
  36. 0 0
      src/helpers/radiolib/CustomSTM32WLx.h
  37. 0 0
      src/helpers/radiolib/CustomSTM32WLxWrapper.h
  38. 0 0
      src/helpers/radiolib/CustomSX1262.h
  39. 0 0
      src/helpers/radiolib/CustomSX1262Wrapper.h
  40. 0 0
      src/helpers/radiolib/CustomSX1268.h
  41. 0 0
      src/helpers/radiolib/CustomSX1268Wrapper.h
  42. 0 0
      src/helpers/radiolib/CustomSX1276.h
  43. 0 0
      src/helpers/radiolib/CustomSX1276Wrapper.h
  44. 3 3
      src/helpers/radiolib/RadioLibWrappers.cpp
  45. 0 0
      src/helpers/radiolib/RadioLibWrappers.h
  46. 241 9
      src/helpers/sensors/EnvironmentSensorManager.cpp
  47. 8 0
      src/helpers/sensors/EnvironmentSensorManager.h
  48. 7 0
      src/helpers/ui/buzzer.cpp
  49. 3 3
      variants/generic-e22/target.h
  50. 2 35
      variants/heltec_ct62/target.cpp
  51. 2 2
      variants/heltec_ct62/target.h
  52. 2 2
      variants/heltec_tracker/target.h
  53. 2 2
      variants/heltec_v2/target.h
  54. 40 0
      variants/heltec_v3/platformio.ini
  55. 2 2
      variants/heltec_v3/target.h
  56. 11 4
      variants/heltec_wireless_paper/target.cpp
  57. 2 2
      variants/heltec_wireless_paper/target.h
  58. 2 2
      variants/lilygo_t3s3/target.h
  59. 2 2
      variants/lilygo_t3s3_sx1276/target.h
  60. 18 1
      variants/lilygo_tbeam_SX1262/platformio.ini
  61. 2 2
      variants/lilygo_tbeam_SX1262/target.h
  62. 2 2
      variants/lilygo_tbeam_SX1276/target.h
  63. 2 2
      variants/lilygo_tbeam_supreme_SX1262/target.h
  64. 2 2
      variants/lilygo_tlora_c6/target.h
  65. 2 2
      variants/lilygo_tlora_v2_1/target.h
  66. 5 38
      variants/meshadventurer/target.cpp
  67. 3 3
      variants/meshadventurer/target.h
  68. 91 0
      variants/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp
  69. 88 0
      variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h
  70. 24 0
      variants/minewsemi_me25ls01/NullDisplayDriver.h
  71. 164 0
      variants/minewsemi_me25ls01/platformio.ini
  72. 98 0
      variants/minewsemi_me25ls01/target.cpp
  73. 29 0
      variants/minewsemi_me25ls01/target.h
  74. 70 0
      variants/minewsemi_me25ls01/variant.cpp
  75. 94 0
      variants/minewsemi_me25ls01/variant.h
  76. 3 4
      variants/nano_g2_ultra/target.h
  77. 2 2
      variants/picow/target.h
  78. 0 0
      variants/promicro/PromicroBoard.cpp
  79. 0 0
      variants/promicro/PromicroBoard.h
  80. 13 88
      variants/promicro/platformio.ini
  81. 3 5
      variants/promicro/target.h
  82. 8 0
      variants/rak3x72/platformio.ini
  83. 1 1
      variants/rak3x72/target.cpp
  84. 3 3
      variants/rak3x72/target.h
  85. 0 0
      variants/rak4631/RAK4631Board.cpp
  86. 5 8
      variants/rak4631/RAK4631Board.h
  87. 29 56
      variants/rak4631/platformio.ini
  88. 8 392
      variants/rak4631/target.cpp
  89. 7 70
      variants/rak4631/target.h
  90. 81 0
      variants/sensecap_solar/SenseCapSolarBoard.cpp
  91. 42 0
      variants/sensecap_solar/SenseCapSolarBoard.h
  92. 106 0
      variants/sensecap_solar/platformio.ini
  93. 39 0
      variants/sensecap_solar/target.cpp
  94. 21 0
      variants/sensecap_solar/target.h
  95. 71 0
      variants/sensecap_solar/variant.cpp
  96. 85 0
      variants/sensecap_solar/variant.h
  97. 2 2
      variants/station_g2/target.h
  98. 2 1
      variants/t1000-e/target.cpp
  99. 2 2
      variants/t1000-e/target.h
  100. 2 2
      variants/t114/target.h

+ 59 - 0
boards/minewsemi_me25ls01.json

@@ -0,0 +1,59 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "nrf52840_s140_v7.ld"
+    },
+    "core": "nRF5",
+    "cpu": "cortex-m4",
+    "extra_flags": "-DARDUINO_WIO_WM1110 -DNRF52840_XXAA",
+    "f_cpu": "64000000L",
+    "hwids": [
+      ["0x239A", "0x8029"],
+      ["0x239A", "0x0029"],
+      ["0x239A", "0x002A"],
+      ["0x239A", "0x802A"]
+    ],
+    "usb_product": "me25ls01-BOOT",
+    "mcu": "nrf52840",
+    "variant": "minewsemi_me25ls01",
+    "bsp": {
+      "name": "adafruit"
+    },
+    "softdevice": {
+      "sd_flags": "-DS140",
+      "sd_name": "s140",
+      "sd_version": "7.3.0",
+      "sd_fwid": "0x0123"
+    },
+    "bootloader": {
+      "settings_addr": "0xFF000"
+    }
+  },
+  "connectivity": ["bluetooth"],
+  "debug": {
+    "jlink_device": "nRF52840_xxAA",
+    "svd_path": "nrf52840.svd",
+    "openocd_target": "nrf52.cfg"
+  },
+  "frameworks": ["arduino"],
+  "name": "Minewsemi ME25LS01",
+  "upload": {
+    "maximum_ram_size": 248832,
+    "maximum_size": 815104,
+    "speed": 115200,
+    "protocol": "nrfutil",
+    "protocols": [
+      "jlink",
+      "nrfjprog",
+      "nrfutil",
+      "stlink",
+      "cmsis-dap",
+      "blackmagic"
+    ],
+    "use_1200bps_touch": true,
+    "require_upload_port": true,
+    "wait_for_upload_port": true
+  },
+  "url": "https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01",
+  "vendor": "MINEWSEMI"
+}

+ 61 - 0
boards/seeed-wio-tracker-l1.json

@@ -0,0 +1,61 @@
+{
+  "build": {
+    "arduino": {
+    "ldscript": "nrf52840_s140_v7.ld"
+    },
+    "core": "nRF5",
+    "cpu": "cortex-m4",
+    "extra_flags": "-DARDUINO_SEEED_WIO_TRACKER_L1 -DNRF52840_XXAA -DSEEED_WIO_TRACKER_L1 ",
+    "f_cpu": "64000000L",
+    "hwids": [
+      [ "0x2886", "0x1667" ],
+      [ "0x2886", "0x1668" ]
+    ],
+    "mcu": "nrf52840",
+    "variant": "Seeed_Wio_Tracker_L1",
+    "softdevice": {
+      "sd_flags": "-DS140",
+      "sd_name": "s140",
+      "sd_version": "7.3.0",
+      "sd_fwid": "0x0123"
+    },
+    "bsp": {
+      "name": "adafruit"
+    },
+    "bootloader": {
+      "settings_addr": "0xFF000"
+    },
+    "usb_product": "Seeed Wio Tracker L1"
+  },
+  "connectivity": [
+    "bluetooth"
+  ],
+  "debug": {
+    "jlink_device": "nRF52840_xxAA",
+    "openocd_target": "nrf52.cfg",
+    "svd_path": "nrf52840.svd"
+  },
+  "frameworks": [
+    "arduino"
+  ],
+  "name": "Seeed Wio Tracker L1",
+  "upload": {
+    "maximum_ram_size": 248832,
+    "maximum_size": 815104,
+    "protocol": "nrfutil",
+    "speed": 115200,
+    "protocols": [
+      "jlink",
+      "nrfjprog",
+      "nrfutil",
+      "cmsis-dap",
+      "sam-ba",
+      "blackmagic"
+    ],
+    "use_1200bps_touch": true,
+    "require_upload_port": true,
+    "wait_for_upload_port": true
+  },
+  "url": "https://wiki.seeedstudio.com/wio_tracker_l1_node/",
+  "vendor": "Seeed Studio"
+}

+ 60 - 0
boards/seeed_sensecap_solar.json

@@ -0,0 +1,60 @@
+{
+  "build": {
+    "arduino": {
+      "ldscript": "nrf52840_s140_v7.ld"
+    },
+    "core": "nRF5",
+    "cpu": "cortex-m4",
+    "extra_flags": "-DARDUINO_Seeed_XIAO_nRF52840 -DNRF52840_XXAA -DSEEED_XIAO_NRF52840 ",
+    "f_cpu": "64000000L",
+    "hwids": [
+      [ "0x2886", "0x0059" ]
+    ],
+    "mcu": "nrf52840",
+    "variant": "Seeed_XIAO_nRF52840",
+    "softdevice": {
+      "sd_flags": "-DS140",
+      "sd_name": "s140",
+      "sd_version": "7.3.0",
+      "sd_fwid": "0x0123"
+    },
+    "bsp": {
+      "name": "adafruit"
+    },
+    "bootloader": {
+      "settings_addr": "0xFF000"
+    },
+    "usb_product": "XIAO nRF52840"
+  },
+  "connectivity": [
+    "bluetooth"
+  ],
+  "debug": {
+    "jlink_device": "nRF52840_xxAA",
+    "openocd_target": "nrf52.cfg",
+    "svd_path": "nrf52840.svd"
+  },
+  "frameworks": [
+    "arduino"
+  ],
+  "name": "Seeed Studio XIAO nRF52840",
+  "upload": {
+    "maximum_ram_size": 248832,
+    "maximum_size": 815104,
+    "protocol": "nrfutil",
+    "speed": 115200,
+    "protocols": [
+      "jlink",
+      "nrfjprog",
+      "nrfutil",
+      "cmsis-dap",
+      "sam-ba",
+      "blackmagic"
+    ],
+    "use_1200bps_touch": true,
+    "require_upload_port": true,
+    "wait_for_upload_port": true
+  },
+  "url": "https://wiki.seeedstudio.com/meshtastic_solar_node/",
+  "vendor": "Seeed Studio"
+}

+ 8 - 6
docs/packet_structure.md

@@ -1,11 +1,12 @@
 # Packet Structure
 
-| Field    | Size (bytes)                     | Description                                               |
-|----------|----------------------------------|-----------------------------------------------------------|
-| header   | 1                                | Contains routing type, payload type, and payload version. |
-| path_len | 1                                | Length of the path field in bytes.                        |
-| path     | up to 64 (`MAX_PATH_SIZE`)       | Stores the routing path if applicable.                    |
-| payload  | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted.                        |
+| Field           | Size (bytes)                     | Description                                               |
+|-----------------|----------------------------------|-----------------------------------------------------------|
+| header          | 1                                | Contains routing type, payload type, and payload version. |
+| transport_codes | 4 (optional)                     | 2x 16-bit transport codes (if ROUTE_TYPE_TRANSPORT_*)     |
+| path_len        | 1                                | Length of the path field in bytes.                        |
+| path            | up to 64 (`MAX_PATH_SIZE`)       | Stores the routing path if applicable.                    |
+| payload         | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted.                        |
 
 Note: see the [payloads doc](./payloads.md) for more information about the content of payload.
 
@@ -42,6 +43,7 @@ bit 0 means the lowest bit (1s place)
 | `0x07` | `PAYLOAD_TYPE_ANON_REQ`   | Anonymous request.                            |
 | `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.      |
 | `0x0F` | `PAYLOAD_TYPE_RAW_CUSTOM` | Custom packet (raw bytes, custom encryption). |
 
 ## Payload Version Values

+ 26 - 24
docs/payloads.md

@@ -10,13 +10,16 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified
 * Anonymous request.
 * Group text message (unverified).
 * Group datagram (unverified).
+* Multi-part packet
 * Custom packet (raw bytes, custom encryption).
 
-This document defines the structure of each of these payload types
+This document defines the structure of each of these payload types.
+
+NOTE: all 16 and 32-bit integer fields are Little Endian.
 
 ## Important concepts:
 
-* Node/channel hash: the first byte of the node or channel's public key
+* Node hash: the first byte of the node's public key
 
 # Node advertisement
 This kind of payload notifies receivers that a node exists, and gives information about the node
@@ -33,10 +36,10 @@ Appdata
 | Field         | Size (bytes)    | Description                                           |
 |---------------|-----------------|-------------------------------------------------------|
 | flags         | 1               | specifies which of the fields are present, see below  |
-| latitude      | 4               | decimal latitude multiplied by 1000000, integer       |
-| longitude     | 4               | decimal longitude multiplied by 1000000, integer      |
-| feature 1     | 2               | reserved for future use                               |
-| feature 2     | 2               | reserved for future use                               |
+| latitude      | 4 (optional)    | decimal latitude multiplied by 1000000, integer       |
+| longitude     | 4 (optional)    | decimal longitude multiplied by 1000000, integer      |
+| feature 1     | 2  (optional)   | reserved for future use                               |
+| feature 2     | 2  (optional)   | reserved for future use                               |
 | name          | rest of appdata | name of the node                                      |
 
 Appdata Flags
@@ -46,6 +49,7 @@ Appdata Flags
 | `0x01` | is chat node   | advert is for a chat node             |
 | `0x02` | is repeater    | advert is for a repeater              |
 | `0x03` | is room server | advert is for a room server           |
+| `0x04` | is sensor      | advert is for a sensor server         |
 | `0x10` | has location   | appdata contains lat/long information |
 | `0x20` | has feature 1  | Reserved for future use.              |
 | `0x40` | has feature 2  | Reserved for future use.              |
@@ -92,13 +96,15 @@ Returned path messages provide a description of the route a packet took from the
 
 Request type
 
-| Value  | Name               | Description                           |
-|--------|--------------------|---------------------------------------|
-| `0x01` | get status         | get status of repeater or room server |
-| `0x02` | keepalive          | TODO |
-| `0x03` | get telemetry data | TODO |
+| Value  | Name                 | Description                           |
+|--------|----------------------|---------------------------------------|
+| `0x01` | get stats            | get stats of repeater or room server  |
+| `0x02` | keepalive            | (deprecated) |
+| `0x03` | get telemetry data   | TODO |
+| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span |
+| `0x05` | get access list      | get node's approved access list       |
 
-### Get status
+### Get stats
 
 Gets information about the node, possibly including the following:
 
@@ -121,10 +127,6 @@ Gets information about the node, possibly including the following:
 * Number posted (?)
 * Number of post pushes (?)
 
-### Keepalive
-
-No-op request.
-
 ### Get telemetry data
 
 Request data about sensors on the node, including battery level.
@@ -138,11 +140,11 @@ Request data about sensors on the node, including battery level.
 
 ## Plain text message
 
-| Field        | Size (bytes)    | Description                                                  |
-|--------------|-----------------|--------------------------------------------------------------|
-| timestamp    | 4               | send time (unix timestamp)                                   |
-| flags + TODO | 1               | first six bits are flags (see below), last two bits are TODO |
-| message      | rest of payload | the message content, see next table                          |
+| 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                          |
 
 Flags
 
@@ -150,7 +152,7 @@ Flags
 |--------|---------------------------|------------------------------------------------------------|
 | `0x00` | plain text message        | the plain text of the message                              |
 | `0x01` | CLI command               | the command text of the message                            |
-| `0x02` | signed plain text message | two bytes of sender prefix, followed by plain text message |
+| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
 
 # Anonymous request
 
@@ -166,14 +168,14 @@ Plaintext message
 | Field          | Size (bytes)    | Description                                                                   |
 |----------------|-----------------|-------------------------------------------------------------------------------|
 | timestamp      | 4               | send time (unix timestamp)                                                    |
-| sync timestamp | 4               | for room server, otherwise absent: sender's "sync messages SINCE x" timestamp |
+| sync timestamp | 4               | NOTE: room server only! - sender's "sync messages SINCE x" timestamp |
 | password       | rest of message | password for repeater/room                                                    |
 
 # Group text message / datagram
 
 | Field        | Size (bytes)    | Description                                |
 |--------------|-----------------|--------------------------------------------|
-| channel hash | 1               | the first byte of the channel's public key | 
+| channel hash | 1               | first byte of SHA256 of channel's shared key  |
 | cipher MAC   | 2               | MAC for encrypted data in next field       |
 | ciphertext   | rest of payload | encrypted message, see below for details   |
 

+ 6 - 4
examples/companion_radio/DataStore.cpp

@@ -154,7 +154,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
     file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq));                               // 56
     file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf));                                   // 60
     file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr));                                   // 61
-    file.read((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1));                     // 62
+    file.read(pad, 1);                                                                     // 62
     file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
     file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw));                                   // 64
     file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm));               // 68
@@ -163,7 +163,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
     file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env));   // 71
     file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base));             // 72
     file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy));     // 76
-    file.read(pad, 3);                                                                     // 77
+    file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks));                   // 77
+    file.read(pad, 2);                                                                     // 78
     file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin));                         // 80
 
     file.close();
@@ -184,7 +185,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
     file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq));                               // 56
     file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf));                                   // 60
     file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr));                                   // 61
-    file.write((uint8_t *)&_prefs.reserved1, sizeof(_prefs.reserved1));                     // 62
+    file.write(pad, 1);                                                                     // 62
     file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
     file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw));                                   // 64
     file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm));               // 68
@@ -193,7 +194,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
     file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env));   // 71
     file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base));             // 72
     file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy));     // 76
-    file.write(pad, 3);                                                                     // 77
+    file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks));                   // 77
+    file.write(pad, 2);                                                                     // 78
     file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin));                         // 80
 
     file.close();

+ 87 - 17
examples/companion_radio/MyMesh.cpp

@@ -45,6 +45,10 @@
 #define CMD_GET_CUSTOM_VARS           40
 #define CMD_SET_CUSTOM_VAR            41
 #define CMD_GET_ADVERT_PATH           42
+#define CMD_GET_TUNING_PARAMS         43
+// NOTE: CMD range 44..49 parked, potentially for WiFi operations
+#define CMD_SEND_BINARY_REQ           50
+#define CMD_FACTORY_RESET             51
 
 #define RESP_CODE_OK                  0
 #define RESP_CODE_ERR                 1
@@ -69,6 +73,7 @@
 #define RESP_CODE_SIGNATURE           20
 #define RESP_CODE_CUSTOM_VARS         21
 #define RESP_CODE_ADVERT_PATH         22
+#define RESP_CODE_TUNING_PARAMS       23
 
 #define SEND_TIMEOUT_BASE_MILLIS        500
 #define FLOOD_SEND_TIMEOUT_FACTOR       16.0f
@@ -91,6 +96,7 @@
 #define PUSH_CODE_TRACE_DATA            0x89
 #define PUSH_CODE_NEW_ADVERT            0x8A
 #define PUSH_CODE_TELEMETRY_RESPONSE    0x8B
+#define PUSH_CODE_BINARY_RESPONSE       0x8C
 
 #define ERR_CODE_UNSUPPORTED_CMD        1
 #define ERR_CODE_NOT_FOUND              2
@@ -146,7 +152,7 @@ void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
   _serial->writeFrame(out_frame, i);
 }
 
-void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len) {
+void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) {
   int i = 0;
   uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT
   memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE);
@@ -165,6 +171,9 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, const uint8_t *frame,
     i += 4;
     memcpy(&contact.gps_lon, &frame[i], 4);
     i += 4;
+    if (i + 4 >= len) {
+      memcpy(&last_mod, &frame[i], 4);
+    }
   }
 }
 
@@ -204,6 +213,10 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
   return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
 }
 
+uint8_t MyMesh::getExtraAckTransmitCount() const {
+  return _prefs.multi_acks;
+}
+
 void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
   if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) {
     int i = 0;
@@ -319,13 +332,17 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
     uint8_t frame[1];
     frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
     _serial->writeFrame(frame, 1);
-  } else {
-#ifdef DISPLAY_CLASS
-    ui_task.soundBuzzer(UIEventType::contactMessage);
-#endif
   }
+
 #ifdef DISPLAY_CLASS
-  ui_task.newMsg(path_len, from.name, text, offline_queue_len);
+  // we only want to show text messages on display, not cli data
+  bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN;
+  if (should_display) {
+    ui_task.newMsg(path_len, from.name, text, offline_queue_len);
+    if (!_serial->isConnected()) {
+      ui_task.soundBuzzer(UIEventType::contactMessage);
+    }
+  }
 #endif
 }
 
@@ -462,6 +479,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
       i += 6; // pub_key_prefix
       memcpy(&out_frame[i], &tag, 4);
       i += 4; // NEW: include server timestamp
+      out_frame[i++] = data[7]; // NEW (v7): ACL permissions
     } else {
       out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
       out_frame[i++] = 0; // reserved
@@ -484,7 +502,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
     memcpy(&out_frame[i], &data[4], len - 4);
     i += (len - 4);
     _serial->writeFrame(out_frame, i);
-  } else if (len > 4 && tag == pending_telemetry) { // check for telemetry response
+  } else if (len > 4 && tag == pending_telemetry) {  // check for matching response tag
     pending_telemetry = 0;
 
     int i = 0;
@@ -495,6 +513,17 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
     memcpy(&out_frame[i], &data[4], len - 4);
     i += (len - 4);
     _serial->writeFrame(out_frame, i);
+  } else if (len > 4 && tag == pending_req) {  // check for matching response tag
+    pending_req = 0;
+
+    int i = 0;
+    out_frame[i++] = PUSH_CODE_BINARY_RESPONSE;
+    out_frame[i++] = 0; // reserved
+    memcpy(&out_frame[i], &tag, 4);   // app needs to match this to RESP_CODE_SENT.tag
+    i += 4;
+    memcpy(&out_frame[i], &data[4], len - 4);
+    i += (len - 4);
+    _serial->writeFrame(out_frame, i);
   }
 }
 
@@ -560,7 +589,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
   _cli_rescue = false;
   offline_queue_len = 0;
   app_target_ver = 0;
-  pending_login = pending_status = pending_telemetry = 0;
+  pending_login = pending_status = pending_telemetry = pending_req = 0;
   next_ack_idx = 0;
   sign_data = NULL;
   dirty_contacts_expiry = 0;
@@ -697,7 +726,7 @@ void MyMesh::handleCmdFrame(size_t len) {
     i += 4;
     memcpy(&out_frame[i], &lon, 4);
     i += 4;
-    out_frame[i++] = 0; // reserved
+    out_frame[i++] = _prefs.multi_acks; // new v7+
     out_frame[i++] = _prefs.advert_loc_policy;
     out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) |
                      (_prefs.telemetry_mode_base); // v5+
@@ -870,15 +899,16 @@ void MyMesh::handleCmdFrame(size_t len) {
   } else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) {
     uint8_t *pub_key = &cmd_frame[1];
     ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
+    uint32_t last_mod = getRTCClock()->getCurrentTime();  // fallback value if not present in cmd_frame
     if (recipient) {
-      updateContactFromFrame(*recipient, cmd_frame, len);
-      // recipient->lastmod = ??   shouldn't be needed, app already has this version of contact
+      updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
+      recipient->lastmod = last_mod;
       dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
       writeOKFrame();
     } else {
       ContactInfo contact;
-      updateContactFromFrame(contact, cmd_frame, len);
-      contact.lastmod = getRTCClock()->getCurrentTime();
+      updateContactFromFrame(contact, last_mod, cmd_frame, len);
+      contact.lastmod = last_mod;
       contact.sync_since = 0;
       if (addContact(contact)) {
         dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
@@ -1012,6 +1042,13 @@ void MyMesh::handleCmdFrame(size_t len) {
     _prefs.airtime_factor = ((float)af) / 1000.0f;
     savePrefs();
     writeOKFrame();
+  } else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) {
+    uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000;
+    int i = 0;
+    out_frame[i++] = RESP_CODE_TUNING_PARAMS;
+    memcpy(&out_frame[i], &rx, 4); i += 4;
+    memcpy(&out_frame[i], &af, 4); i += 4;
+    _serial->writeFrame(out_frame, i);
   } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) {
     _prefs.manual_add_contacts = cmd_frame[1];
     if (len >= 3) {
@@ -1021,6 +1058,9 @@ void MyMesh::handleCmdFrame(size_t len) {
 
       if (len >= 4) {
         _prefs.advert_loc_policy = cmd_frame[3];
+        if (len >= 5) {
+          _prefs.multi_acks = cmd_frame[4];
+        }
       }
     }
     savePrefs();
@@ -1090,7 +1130,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       if (result == MSG_SEND_FAILED) {
         writeErrFrame(ERR_CODE_TABLE_FULL);
       } else {
-        pending_telemetry = pending_status = 0;
+        pending_req = pending_telemetry = pending_status = 0;
         memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse()
         out_frame[0] = RESP_CODE_SENT;
         out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1110,7 +1150,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       if (result == MSG_SEND_FAILED) {
         writeErrFrame(ERR_CODE_TABLE_FULL);
       } else {
-        pending_telemetry = pending_login = 0;
+        pending_req = pending_telemetry = pending_login = 0;
         // FUTURE:  pending_status = tag;  // match this in onContactResponse()
         memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme
         out_frame[0] = RESP_CODE_SENT;
@@ -1122,7 +1162,7 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
       writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
     }
-  } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) {
+  } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) {  // can deprecate, in favour of CMD_SEND_BINARY_REQ
     uint8_t *pub_key = &cmd_frame[4];
     ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
     if (recipient) {
@@ -1131,7 +1171,7 @@ void MyMesh::handleCmdFrame(size_t len) {
       if (result == MSG_SEND_FAILED) {
         writeErrFrame(ERR_CODE_TABLE_FULL);
       } else {
-        pending_status = pending_login = 0;
+        pending_status = pending_login = pending_req = 0;
         pending_telemetry = tag; // match this in onContactResponse()
         out_frame[0] = RESP_CODE_SENT;
         out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
@@ -1157,6 +1197,27 @@ void MyMesh::handleCmdFrame(size_t len) {
     memcpy(&out_frame[i], telemetry.getBuffer(), tlen);
     i += tlen;
     _serial->writeFrame(out_frame, i);
+  } else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) {
+    uint8_t *pub_key = &cmd_frame[1];
+    ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
+    if (recipient) {
+      uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE];
+      uint32_t tag, est_timeout;
+      int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout);
+      if (result == MSG_SEND_FAILED) {
+        writeErrFrame(ERR_CODE_TABLE_FULL);
+      } else {
+        pending_status = pending_login = pending_telemetry = 0;
+        pending_req = tag; // match this in onContactResponse()
+        out_frame[0] = RESP_CODE_SENT;
+        out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0;
+        memcpy(&out_frame[2], &tag, 4);
+        memcpy(&out_frame[6], &est_timeout, 4);
+        _serial->writeFrame(out_frame, 10);
+      }
+    } else {
+      writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found
+    }
   } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) {
     uint8_t *pub_key = &cmd_frame[1];
     if (hasConnectionTo(pub_key)) {
@@ -1312,6 +1373,15 @@ void MyMesh::handleCmdFrame(size_t len) {
     } else {
       writeErrFrame(ERR_CODE_NOT_FOUND);
     }
+  } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) {
+    bool success = _store->formatFileSystem();
+    if (success) {
+      writeOKFrame();
+      delay(1000);
+      board.reboot();  // doesn't return
+    } else {
+      writeErrFrame(ERR_CODE_FILE_IO_ERROR);
+    }
   } else {
     writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
     MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

+ 7 - 5
examples/companion_radio/MyMesh.h

@@ -7,14 +7,14 @@
 #endif
 
 /*------------ Frame Protocol --------------*/
-#define FIRMWARE_VER_CODE 6
+#define FIRMWARE_VER_CODE 7
 
 #ifndef FIRMWARE_BUILD_DATE
-#define FIRMWARE_BUILD_DATE "2 Jul 2025"
+#define FIRMWARE_BUILD_DATE "24 Jul 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-#define FIRMWARE_VERSION "v1.7.2"
+#define FIRMWARE_VERSION "v1.7.4"
 #endif
 
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -97,6 +97,7 @@ protected:
   float getAirtimeBudgetFactor() const override;
   int getInterferenceThreshold() const override;
   int calcRxDelay(float score, uint32_t air_time) const override;
+  uint8_t getExtraAckTransmitCount() const override;
 
   void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
   bool isAutoAddEnabled() const override;
@@ -137,7 +138,7 @@ private:
   void writeErrFrame(uint8_t err_code);
   void writeDisabledFrame();
   void writeContactRespFrame(uint8_t code, const ContactInfo &contact);
-  void updateContactFromFrame(ContactInfo &contact, const uint8_t *frame, int len);
+  void updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
   void addToOfflineQueue(const uint8_t frame[], int len);
   int getFromOfflineQueue(uint8_t frame[]);
   int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) override { 
@@ -160,7 +161,8 @@ private:
   NodePrefs _prefs;
   uint32_t pending_login;
   uint32_t pending_status;
-  uint32_t pending_telemetry;
+  uint32_t pending_telemetry;   // pending _TELEMETRY_REQ
+  uint32_t pending_req;   // pending _BINARY_REQ
   BaseSerialInterface *_serial;
 
   ContactsIterator _iter;

+ 1 - 1
examples/companion_radio/NodePrefs.h

@@ -14,7 +14,7 @@ struct NodePrefs {  // persisted to file
   float freq;
   uint8_t sf;
   uint8_t cr;
-  uint8_t reserved1;
+  uint8_t multi_acks;
   uint8_t manual_add_contacts;
   float bw;
   uint8_t tx_power_dbm;

+ 4 - 0
examples/companion_radio/UITask.cpp

@@ -408,8 +408,12 @@ void UITask::handleButtonQuadruplePress() {
       if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
         if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
           _sensors->setSettingValue("gps", "0");
+          soundBuzzer(UIEventType::ack);
+          sprintf(_alert, "GPS: Disabled");
         } else {
           _sensors->setSettingValue("gps", "1");
+          soundBuzzer(UIEventType::ack);
+          sprintf(_alert, "GPS: Enabled");
         }
         break;
       }

+ 53 - 11
examples/simple_repeater/main.cpp

@@ -22,11 +22,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Jul 2025"
+  #define FIRMWARE_BUILD_DATE   "24 Jul 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.7.2"
+  #define FIRMWARE_VERSION   "v1.7.4"
 #endif
 
 #ifndef LORA_FREQ
@@ -124,7 +124,7 @@ struct NeighbourInfo {
   int8_t snr; // multiplied by 4, user should divide to get float value
 };
 
-#define CLI_REPLY_DELAY_MILLIS  1000
+#define CLI_REPLY_DELAY_MILLIS  600
 
 class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   FILESYSTEM* _fs;
@@ -138,6 +138,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   NeighbourInfo neighbours[MAX_NEIGHBOURS];
 #endif
   CayenneLPP telemetry;
+  unsigned long set_radio_at, revert_radio_at;
+  float pending_freq;
+  float pending_bw;
+  uint8_t pending_sf;
+  uint8_t pending_cr;
 
   ClientInfo* putClient(const mesh::Identity& id) {
     uint32_t min_time = 0xFFFFFFFF;
@@ -153,7 +158,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
     oldest->id = id;
     oldest->out_path_len = -1;  // initially out_path is unknown
     oldest->last_timestamp = 0;
-    self_id.calcSharedSecret(oldest->secret, id);   // calc ECDH shared secret
     return oldest;
   }
 
@@ -432,9 +436,12 @@ protected:
   int getAGCResetInterval() const override {
     return ((int)_prefs.agc_reset_interval) * 4000;   // milliseconds
   }
+  uint8_t getExtraAckTransmitCount() const override {
+    return _prefs.multi_acks;
+  }
 
-  void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
-    if (type == PAYLOAD_TYPE_ANON_REQ) {  // received an initial request by a possible admin client (unknown at this stage)
+  void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override {
+    if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) {  // received an initial request by a possible admin client (unknown at this stage)
       uint32_t timestamp;
       memcpy(&timestamp, data, 4);
 
@@ -461,6 +468,7 @@ protected:
       client->last_timestamp = timestamp;
       client->last_activity = getRTCClock()->getCurrentTime();
       client->is_admin = is_admin;
+      memcpy(client->secret, secret, PUB_KEY_SIZE);
 
       uint32_t now = getRTCClock()->getCurrentTimeUnique();
       memcpy(reply_data, &now, 4);   // response packets always prefixed with timestamp
@@ -592,12 +600,12 @@ protected:
         }
 
         uint8_t temp[166];
-        const char *command = (const char *) &data[5];
+        char *command = (char *) &data[5];
         char *reply = (char *) &temp[5];
         if (is_retry) {
           *reply = 0;
         } else {
-          _cli.handleCommand(sender_timestamp, command, reply);
+          handleCommand(sender_timestamp, command, reply);
         }
         int text_len = strlen(reply);
         if (text_len > 0) {
@@ -647,6 +655,7 @@ public:
   {
     memset(known_clients, 0, sizeof(known_clients));
     next_local_advert = next_flood_advert = 0;
+    set_radio_at = revert_radio_at = 0;
     _logging = false;
 
 #if MAX_NEIGHBOURS
@@ -673,8 +682,6 @@ public:
     _prefs.interference_threshold = 0;  // disabled
   }
 
-  CommonCLI* getCLI() { return &_cli; }
-
   void begin(FILESYSTEM* fs) {
     mesh::Mesh::begin();
     _fs = fs;
@@ -700,6 +707,16 @@ public:
     _cli.savePrefs(_fs);
   }
 
+  void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override {
+    set_radio_at = futureMillis(2000);   // give CLI reply some time to be sent back, before applying temp radio params
+    pending_freq = freq;
+    pending_bw = bw;
+    pending_sf = sf;
+    pending_cr = cr;
+
+    revert_radio_at = futureMillis(2000 + timeout_mins*60*1000);   // schedule when to revert radio params
+  }
+
   bool formatFileSystem() override {
 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
     return InternalFS.format();
@@ -798,6 +815,18 @@ public:
     ((SimpleMeshTables *)getTables())->resetStats();
   }
 
+  void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
+    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
+      reply += 3;
+      command += 3;
+    }
+
+    _cli.handleCommand(sender_timestamp, command, reply);  // common CLI commands
+  }
+
   void loop() {
 #ifdef BRIDGE_OVER_SERIAL
     serialBridgeReceivePkt();
@@ -817,6 +846,19 @@ public:
 
       updateAdvertTimer();   // schedule next local advert
     }
+
+    if (set_radio_at && millisHasNowPassed(set_radio_at)) {   // apply pending (temporary) radio params
+      set_radio_at = 0;  // clear timer
+      radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
+      MESH_DEBUG_PRINTLN("Temp radio params");
+    }
+
+    if (revert_radio_at && millisHasNowPassed(revert_radio_at)) {   // revert radio params to orig
+      revert_radio_at = 0;  // clear timer
+      radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
+      MESH_DEBUG_PRINTLN("Radio params restored");
+    }
+
   #ifdef DISPLAY_CLASS
     ui_task.loop();
   #endif
@@ -930,7 +972,7 @@ void loop() {
   if (len > 0 && command[len - 1] == '\r') {  // received complete line
     command[len - 1] = 0;  // replace newline with C string null terminator
     char reply[160];
-    the_mesh.getCLI()->handleCommand(0, command, reply);  // NOTE: there is no sender_timestamp via serial!
+    the_mesh.handleCommand(0, command, reply);  // NOTE: there is no sender_timestamp via serial!
     if (reply[0]) {
       Serial.print("  -> "); Serial.println(reply);
     }

+ 65 - 17
examples/simple_room_server/main.cpp

@@ -22,11 +22,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "2 Jul 2025"
+  #define FIRMWARE_BUILD_DATE   "24 Jul 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.7.2"
+  #define FIRMWARE_VERSION   "v1.7.4"
 #endif
 
 #ifndef LORA_FREQ
@@ -165,6 +165,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
   int next_post_idx;
   PostInfo posts[MAX_UNSYNCED_POSTS];   // cyclic queue
   CayenneLPP telemetry;
+  unsigned long set_radio_at, revert_radio_at;
+  float pending_freq;
+  float pending_bw;
+  uint8_t pending_sf;
+  uint8_t pending_cr;
 
   ClientInfo* putClient(const mesh::Identity& id) {
     for (int i = 0; i < num_clients; i++) {
@@ -188,7 +193,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
     newClient->id = id;
     newClient->out_path_len = -1;  // initially out_path is unknown
     newClient->last_timestamp = 0;
-    self_id.calcSharedSecret(newClient->secret, id);   // calc ECDH shared secret
     return newClient;
   }
 
@@ -334,7 +338,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
     }
     return 0;  // unknown command
   }
-   
+
 protected:
   float getAirtimeBudgetFactor() const override {
     return _prefs.airtime_factor;
@@ -425,6 +429,9 @@ protected:
   int getAGCResetInterval() const override {
     return ((int)_prefs.agc_reset_interval) * 4000;   // milliseconds
   }
+  uint8_t getExtraAckTransmitCount() const override {
+    return _prefs.multi_acks;
+  }
 
   bool allowPacketForward(const mesh::Packet* packet) override {
     if (_prefs.disable_fwd) return false;
@@ -432,8 +439,8 @@ protected:
     return true;
   }
 
-  void onAnonDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Identity& sender, uint8_t* data, size_t len) override {
-    if (type == PAYLOAD_TYPE_ANON_REQ) {  // received an initial request by a possible admin client (unknown at this stage)
+  void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override {
+    if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) {  // received an initial request by a possible admin client (unknown at this stage)
       uint32_t sender_timestamp, sender_sync_since;
       memcpy(&sender_timestamp, data, 4);
       memcpy(&sender_sync_since, &data[4], 4);  // sender's "sync messags SINCE x" timestamp
@@ -465,6 +472,7 @@ protected:
       client->sync_since = sender_sync_since;
       client->pending_ack = 0;
       client->push_failures = 0;
+      memcpy(client->secret, secret, PUB_KEY_SIZE);
 
       uint32_t now = getRTCClock()->getCurrentTime();
       client->last_activity = now;
@@ -555,7 +563,7 @@ protected:
             if (is_retry) {
               temp[5] = 0;  // no reply
             } else {
-              _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]);
+              handleCommand(sender_timestamp, (char *) &data[5], (char *) &temp[5]);
               temp[4] = (TXT_TYPE_CLI_DATA << 2);  // attempt and flags,  (NOTE: legacy was: TXT_TYPE_PLAIN)
             }
             send_ack = false;
@@ -578,15 +586,22 @@ protected:
 
         uint32_t delay_millis;
         if (send_ack) {
-          mesh::Packet* ack = createAck(ack_hash);
-          if (ack) {
-            if (client->out_path_len < 0) {
-              sendFlood(ack, TXT_ACK_DELAY);
-            } else {
-              sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
+          if (client->out_path_len < 0) {
+            mesh::Packet* ack = createAck(ack_hash);
+            if (ack) sendFlood(ack, TXT_ACK_DELAY);
+            delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
+          } else {
+            uint32_t d = TXT_ACK_DELAY;
+            if (getExtraAckTransmitCount() > 0) {
+              mesh::Packet* a1 = createMultiAck(ack_hash, 1);
+              if (a1) sendDirect(a1, client->out_path, client->out_path_len, d);
+              d += 300;
             }
+
+            mesh::Packet* a2 = createAck(ack_hash);
+            if (a2) sendDirect(a2, client->out_path, client->out_path_len, d);
+            delay_millis = d + REPLY_DELAY_MILLIS;
           }
-          delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS;
         } else {
           delay_millis = 0;
         }
@@ -711,6 +726,7 @@ public:
   {
     next_local_advert = next_flood_advert = 0;
     _logging = false;
+    set_radio_at = revert_radio_at = 0;
 
     // defaults
     memset(&_prefs, 0, sizeof(_prefs));
@@ -743,8 +759,6 @@ public:
     _num_posted = _num_post_pushes = 0;
   }
 
-  CommonCLI* getCLI() { return &_cli; }
-
   void begin(FILESYSTEM* fs) {
     mesh::Mesh::begin();
     _fs = fs;
@@ -770,6 +784,16 @@ public:
     _cli.savePrefs(_fs);
   }
 
+  void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override {
+    set_radio_at = futureMillis(2000);   // give CLI reply some time to be sent back, before applying temp radio params
+    pending_freq = freq;
+    pending_bw = bw;
+    pending_sf = sf;
+    pending_cr = cr;
+
+    revert_radio_at = futureMillis(2000 + timeout_mins*60*1000);   // schedule when to revert radio params
+  }
+
   bool formatFileSystem() override {
     #if defined(NRF52_PLATFORM)
       return InternalFS.format();
@@ -845,6 +869,18 @@ public:
     ((SimpleMeshTables *)getTables())->resetStats();
   }
 
+  void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
+    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
+      reply += 3;
+      command += 3;
+    }
+
+    _cli.handleCommand(sender_timestamp, command, reply);  // common CLI commands
+  }
+
   void loop() {
     mesh::Mesh::loop();
 
@@ -902,6 +938,18 @@ public:
       updateAdvertTimer();   // schedule next local advert
     }
 
+    if (set_radio_at && millisHasNowPassed(set_radio_at)) {   // apply pending (temporary) radio params
+      set_radio_at = 0;  // clear timer
+      radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
+      MESH_DEBUG_PRINTLN("Temp radio params");
+    }
+
+    if (revert_radio_at && millisHasNowPassed(revert_radio_at)) {   // revert radio params to orig
+      revert_radio_at = 0;  // clear timer
+      radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
+      MESH_DEBUG_PRINTLN("Radio params restored");
+    }
+
   #ifdef DISPLAY_CLASS
     ui_task.loop();
   #endif
@@ -998,7 +1046,7 @@ void loop() {
   if (len > 0 && command[len - 1] == '\r') {  // received complete line
     command[len - 1] = 0;  // replace newline with C string null terminator
     char reply[160];
-    the_mesh.getCLI()->handleCommand(0, command, reply);  // NOTE: there is no sender_timestamp via serial!
+    the_mesh.handleCommand(0, command, reply);  // NOTE: there is no sender_timestamp via serial!
     if (reply[0]) {
       Serial.print("  -> "); Serial.println(reply);
     }

+ 926 - 0
examples/simple_sensor/SensorMesh.cpp

@@ -0,0 +1,926 @@
+#include "SensorMesh.h"
+
+/* ------------------------------ Config -------------------------------- */
+
+#ifndef LORA_FREQ
+  #define LORA_FREQ   915.0
+#endif
+#ifndef LORA_BW
+  #define LORA_BW     250
+#endif
+#ifndef LORA_SF
+  #define LORA_SF     10
+#endif
+#ifndef LORA_CR
+  #define LORA_CR      5
+#endif
+#ifndef LORA_TX_POWER
+  #define LORA_TX_POWER  20
+#endif
+
+#ifndef ADVERT_NAME
+  #define  ADVERT_NAME   "sensor"
+#endif
+#ifndef ADVERT_LAT
+  #define  ADVERT_LAT  0.0
+#endif
+#ifndef ADVERT_LON
+  #define  ADVERT_LON  0.0
+#endif
+
+#ifndef ADMIN_PASSWORD
+  #define  ADMIN_PASSWORD  "password"
+#endif
+
+#ifndef SERVER_RESPONSE_DELAY
+  #define SERVER_RESPONSE_DELAY   300
+#endif
+
+#ifndef TXT_ACK_DELAY
+  #define TXT_ACK_DELAY     200
+#endif
+
+#ifndef SENSOR_READ_INTERVAL_SECS
+  #define SENSOR_READ_INTERVAL_SECS  60
+#endif
+
+/* ------------------------------ Code -------------------------------- */
+
+#define REQ_TYPE_LOGIN               0x00
+#define REQ_TYPE_GET_STATUS          0x01
+#define REQ_TYPE_KEEP_ALIVE          0x02
+#define REQ_TYPE_GET_TELEMETRY_DATA  0x03
+#define REQ_TYPE_GET_AVG_MIN_MAX     0x04
+#define REQ_TYPE_GET_ACCESS_LIST     0x05
+
+#define RESP_SERVER_LOGIN_OK      0   // response to ANON_REQ
+
+#define CLI_REPLY_DELAY_MILLIS  1000
+
+#define LAZY_CONTACTS_WRITE_DELAY       5000
+
+#define ALERT_ACK_EXPIRY_MILLIS         8000   // wait 8 secs for ACKs to alert messages
+
+static File openAppend(FILESYSTEM* _fs, const char* fname) {
+  #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
+    return _fs->open(fname, FILE_O_WRITE);
+  #elif defined(RP2040_PLATFORM)
+    return _fs->open(fname, "a");
+  #else
+    return _fs->open(fname, "a", true);
+  #endif
+}
+
+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
+}
+
+void SensorMesh::loadContacts() {
+  num_contacts = 0;
+  if (_fs->exists("/s_contacts")) {
+  #if defined(RP2040_PLATFORM)
+    File file = _fs->open("/s_contacts", "r");
+  #else
+    File file = _fs->open("/s_contacts");
+  #endif
+    if (file) {
+      bool full = false;
+      while (!full) {
+        ContactInfo c;
+        uint8_t pub_key[32];
+        uint8_t unused[6];
+
+        bool success = (file.read(pub_key, 32) == 32);
+        success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
+        success = success && (file.read(unused, 6) == 6);
+        success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
+        success = success && (file.read(c.out_path, 64) == 64);
+        success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
+        c.last_timestamp = 0;  // transient
+        c.last_activity = 0;
+
+        if (!success) break; // EOF
+
+        c.id = mesh::Identity(pub_key);
+        if (num_contacts < MAX_CONTACTS) {
+          contacts[num_contacts++] = c;
+        } else {
+          full = true;
+        }
+      }
+      file.close();
+    }
+  }
+}
+
+void SensorMesh::saveContacts() {
+  File file = openWrite(_fs, "/s_contacts");
+  if (file) {
+    uint8_t unused[5];
+    memset(unused, 0, sizeof(unused));
+
+    for (int i = 0; i < num_contacts; i++) {
+      auto c = &contacts[i];
+      if (c->permissions == 0) continue;    // skip deleted entries
+
+      bool success = (file.write(c->id.pub_key, 32) == 32);
+      success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
+      success = success && (file.write(unused, 6) == 6);
+      success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
+      success = success && (file.write(c->out_path, 64) == 64);
+      success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
+
+      if (!success) break; // write failed
+    }
+    file.close();
+  }
+}
+
+static uint8_t getDataSize(uint8_t type) {
+    switch (type) {
+      case LPP_GPS:
+        return 9;
+      case LPP_POLYLINE:
+        return 8;  // TODO: this is MINIMIUM
+      case LPP_GYROMETER:
+      case LPP_ACCELEROMETER:
+        return 6;
+      case LPP_GENERIC_SENSOR:
+      case LPP_FREQUENCY:
+      case LPP_DISTANCE:
+      case LPP_ENERGY:
+      case LPP_UNIXTIME:
+        return 4;
+      case LPP_COLOUR:
+        return 3;
+      case LPP_ANALOG_INPUT:
+      case LPP_ANALOG_OUTPUT:
+      case LPP_LUMINOSITY:
+      case LPP_TEMPERATURE:
+      case LPP_CONCENTRATION:
+      case LPP_BAROMETRIC_PRESSURE:
+      case LPP_RELATIVE_HUMIDITY:
+      case LPP_ALTITUDE:
+      case LPP_VOLTAGE:
+      case LPP_CURRENT:
+      case LPP_DIRECTION:
+      case LPP_POWER:
+        return 2;
+    }
+    return 1;
+}
+
+static uint32_t getMultiplier(uint8_t type) {
+    switch (type) {
+      case LPP_CURRENT:
+      case LPP_DISTANCE:
+      case LPP_ENERGY:
+        return 1000;
+      case LPP_VOLTAGE:
+      case LPP_ANALOG_INPUT:
+      case LPP_ANALOG_OUTPUT:
+        return 100;
+      case LPP_TEMPERATURE:
+      case LPP_BAROMETRIC_PRESSURE:
+      case LPP_RELATIVE_HUMIDITY:
+        return 10;
+    }
+    return 1;
+}
+
+static bool isSigned(uint8_t type) {
+  return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
+      type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
+}
+
+static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
+  uint32_t value = 0;
+  for (uint8_t i = 0; i < size; i++) {
+    value = (value << 8) + buffer[i];
+  }
+
+  int sign = 1;
+  if (is_signed) {
+    uint32_t bit = 1ul << ((size * 8) - 1);
+    if ((value & bit) == bit) {
+      value = (bit << 1) - value;
+      sign = -1;
+    }
+  }
+  return sign * ((float) value / multiplier);
+}
+
+static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
+  // check sign
+  bool sign = value < 0;
+  if (sign) value = -value;
+
+  // get value to store
+  uint32_t v = value * multiplier;
+
+  // format an uint32_t as if it was an int32_t
+  if (is_signed & sign) {
+    uint32_t mask = (1 << (size * 8)) - 1;
+    v = v & mask;
+    if (sign) v = mask - v + 1;
+  }
+
+  // add bytes (MSB first)
+  for (uint8_t i=1; i<=size; i++) {
+    dest[size - i] = (v & 0xFF);
+    v >>= 8;
+  }
+  return size;
+}
+
+uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
+  memcpy(reply_data, &sender_timestamp, 4);   // reflect sender_timestamp back in response packet (kind of like a 'tag')
+
+  if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) {  // allow all
+    telemetry.reset();
+    telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
+    // query other sensors -- target specific
+    sensors.querySensors(0xFF, telemetry);  // allow all telemetry permissions for admin or guest
+    // TODO: let requester know permissions they have:  telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
+
+    uint8_t tlen = telemetry.getSize();
+    memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
+    return 4 + tlen;  // reply_len
+  }
+  if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
+    uint32_t start_secs_ago, end_secs_ago;
+    memcpy(&start_secs_ago, &payload[0], 4);
+    memcpy(&end_secs_ago, &payload[4], 4);
+    uint8_t res1 = payload[8];   // reserved for future  (extra query params)
+    uint8_t res2 = payload[9];
+
+    MinMaxAvg data[8];
+    int n;
+    if (res1 == 0 && res2 == 0) {
+      n = querySeriesData(start_secs_ago, end_secs_ago, data, 8);
+    } else {
+      n = 0;
+    }
+
+    uint8_t ofs = 4;
+    {
+      uint32_t now = getRTCClock()->getCurrentTime();
+      memcpy(&reply_data[ofs], &now, 4); ofs += 4;
+    }
+
+    for (int i = 0; i < n; i++) {
+      auto d = &data[i];
+      reply_data[ofs++] = d->_channel;
+      reply_data[ofs++] = d->_lpp_type;
+      uint8_t sz = getDataSize(d->_lpp_type);
+      uint32_t mult = getMultiplier(d->_lpp_type);
+      bool is_signed = isSigned(d->_lpp_type);
+      ofs += putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
+      ofs += putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
+      ofs += putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
+    }
+    return ofs;
+  }
+  if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
+    uint8_t res1 = payload[0];   // reserved for future  (extra query params)
+    uint8_t res2 = payload[1];
+    if (res1 == 0 && res2 == 0) {
+      uint8_t ofs = 4;
+      for (int i = 0; i < num_contacts && ofs + 7 <= sizeof(reply_data) - 4; i++) {
+        auto c = &contacts[i];
+        if (c->permissions == 0) continue;  // skip deleted entries
+        memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6;  // just 6-byte pub_key prefix
+        reply_data[ofs++] = c->permissions;
+      }
+      return ofs;
+    }
+  }
+  return 0;  // unknown command
+}
+
+mesh::Packet* SensorMesh::createSelfAdvert() {
+  uint8_t app_data[MAX_ADVERT_DATA_SIZE];
+  uint8_t app_data_len;
+  {
+    AdvertDataBuilder builder(ADV_TYPE_SENSOR, _prefs.node_name, _prefs.node_lat, _prefs.node_lon);
+    app_data_len = builder.encodeTo(app_data);
+  }
+
+  return createAdvert(self_id, app_data, app_data_len);
+}
+
+ContactInfo* SensorMesh::getContact(const uint8_t* pubkey, int key_len) {
+  for (int i = 0; i < num_contacts; i++) {
+    if (memcmp(pubkey, contacts[i].id.pub_key, key_len) == 0) return &contacts[i];  // already known
+  }
+  return NULL;  // not found
+}
+
+ContactInfo* SensorMesh::putContact(const mesh::Identity& id, uint8_t init_perms) {
+  uint32_t min_time = 0xFFFFFFFF;
+  ContactInfo* oldest = &contacts[MAX_CONTACTS - 1];
+  for (int i = 0; i < num_contacts; i++) {
+    if (id.matches(contacts[i].id)) return &contacts[i];  // already known
+    if (!contacts[i].isAdmin() && contacts[i].last_activity < min_time) {
+      oldest = &contacts[i];
+      min_time = oldest->last_activity;
+    }
+  }
+
+  ContactInfo* c;
+  if (num_contacts < MAX_CONTACTS) {
+    c = &contacts[num_contacts++];
+  } else {
+    c = oldest;  // evict least active contact
+  }
+  memset(c, 0, sizeof(*c));
+  c->permissions = init_perms;
+  c->id = id;
+  c->out_path_len = -1;  // initially out_path is unknown
+  return c;
+}
+
+bool SensorMesh::applyContactPermissions(const uint8_t* pubkey, int key_len, uint8_t perms) {
+  ContactInfo* c;
+  if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) {  // guest role is not persisted in contacts
+    c = getContact(pubkey, key_len);
+    if (c == NULL) return false;   // partial pubkey not found
+
+    num_contacts--;   // delete from contacts[]
+    int i = c - contacts;
+    while (i < num_contacts) {
+      contacts[i] = contacts[i + 1];
+      i++;
+    }
+  } else {
+    if (key_len < PUB_KEY_SIZE) return false;   // need complete pubkey when adding/modifying
+
+    mesh::Identity id(pubkey);
+    c = putContact(id, 0);
+
+    c->permissions = perms;  // update their permissions
+    self_id.calcSharedSecret(c->shared_secret, pubkey);
+  }
+  dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);   // trigger saveContacts()
+  return true;
+}
+
+void SensorMesh::sendAlert(ContactInfo* c, Trigger* t) {
+  int text_len = strlen(t->text);
+
+  uint8_t data[MAX_PACKET_PAYLOAD];
+  memcpy(data, &t->timestamp, 4);
+  data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt;  // attempt and flags
+  memcpy(&data[5], t->text, text_len);
+
+  // calc expected ACK reply
+  mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
+  t->attempt++;
+
+  auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
+  if (pkt) {
+    if (c->out_path_len >= 0) {  // we have an out_path, so send DIRECT
+      sendDirect(pkt, c->out_path, c->out_path_len);
+    } else {
+      sendFlood(pkt);
+    }
+  }
+  t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
+}
+
+void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) {
+  if (condition) {
+    if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) {
+      StrHelper::strncpy(t.text, text, sizeof(t.text));
+      t.pri = pri;
+      t.send_expiry = 0;  // signal that initial send is needed
+      t.attempt = 4;
+      t.curr_contact_idx = -1;  // start iterating thru contacts[]
+
+      alert_tasks[num_alert_tasks++] = &t;  // add to queue
+    }
+  } else {
+    if (t.isTriggered()) {
+      t.text[0] = 0;
+      // remove 't' from alert queue
+      int i = 0;
+      while (i < num_alert_tasks && alert_tasks[i] != &t) i++;
+
+      if (i < num_alert_tasks) {  // found,  now delete from array
+        num_alert_tasks--;
+        while (i < num_alert_tasks) {
+          alert_tasks[i] = alert_tasks[i + 1];
+          i++;
+        }
+      }
+    }
+  }
+}
+
+float SensorMesh::getAirtimeBudgetFactor() const {
+  return _prefs.airtime_factor;
+}
+
+bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
+  if (_prefs.disable_fwd) return false;
+  if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false;
+  return true;
+}
+
+int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
+  if (_prefs.rx_delay_base <= 0.0f) return 0;
+  return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
+}
+
+uint32_t SensorMesh::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;
+}
+uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
+  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
+  return getRNG()->nextInt(0, 6)*t;
+}
+int SensorMesh::getInterferenceThreshold() const {
+  return _prefs.interference_threshold;
+}
+int SensorMesh::getAGCResetInterval() const {
+  return ((int)_prefs.agc_reset_interval) * 4000;   // milliseconds
+}
+
+uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
+  ContactInfo* client;
+  if (data[0] == 0) {   // blank password, just check if sender is in ACL
+    client = getContact(sender.pub_key, PUB_KEY_SIZE);
+    if (client == NULL) {
+    #if MESH_DEBUG
+      MESH_DEBUG_PRINTLN("Login, sender not in ACL");
+    #endif
+      return 0;
+    }
+  } else {
+    if (strcmp((char *) data, _prefs.password) != 0) {  // check for valid admin password
+    #if MESH_DEBUG
+      MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
+    #endif
+      return 0;
+    }
+
+    client = putContact(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO);  // add to contacts (if not already known)
+    if (sender_timestamp <= client->last_timestamp) {
+      MESH_DEBUG_PRINTLN("Possible login replay attack!");
+      return 0;  // FATAL: client table is full -OR- replay attack
+    }
+
+    MESH_DEBUG_PRINTLN("Login success!");
+    client->last_timestamp = sender_timestamp;
+    client->last_activity = getRTCClock()->getCurrentTime();
+    client->permissions |= PERM_ACL_ADMIN;
+    memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
+
+    dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
+  }
+
+  uint32_t now = getRTCClock()->getCurrentTimeUnique();
+  memcpy(reply_data, &now, 4);   // response packets always prefixed with timestamp
+  reply_data[4] = RESP_SERVER_LOGIN_OK;
+  reply_data[5] = 0;  // NEW: recommended keep-alive interval (secs / 16)
+  reply_data[6] = client->isAdmin() ? 1 : 0;
+  reply_data[7] = client->permissions;
+  getRNG()->random(&reply_data[8], 4);   // random blob to help packet-hash uniqueness
+
+  return 12;  // reply length
+}
+
+void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
+  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
+    reply += 3;
+    command += 3;
+  }
+
+  // first, see if this is a custom-handled CLI command (ie. in main.cpp)
+  if (handleCustomCommand(sender_timestamp, command, reply)) {
+    return;   // command has been handled
+  }
+
+  // handle sensor-specific CLI commands
+  if (memcmp(command, "setperm ", 8) == 0) {   // format:  setperm {pubkey-hex} {permissions-int8}
+    char* hex = &command[8];
+    char* sp = strchr(hex, ' ');   // look for separator char
+    if (sp == NULL) {
+      strcpy(reply, "Err - bad params");
+    } else {
+      *sp++ = 0;   // replace space with null terminator
+
+      uint8_t pubkey[PUB_KEY_SIZE];
+      int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
+      if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
+        uint8_t perms = atoi(sp);
+        if (applyContactPermissions(pubkey, hex_len / 2, perms)) {
+          strcpy(reply, "OK");
+        } else {
+          strcpy(reply, "Err - invalid params");
+        }
+      } else {
+        strcpy(reply, "Err - bad pubkey");
+      }
+    }
+  } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
+    Serial.println("ACL:");
+    for (int i = 0; i < num_contacts; i++) {
+      auto c = &contacts[i];
+      if (c->permissions == 0) continue;  // skip deleted entries
+
+      Serial.printf("%02X ", c->permissions);
+      mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
+      Serial.printf("\n");
+    }
+    reply[0] = 0;
+  } else {
+    _cli.handleCommand(sender_timestamp, command, reply);  // common CLI commands
+  }
+}
+
+void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) {
+  if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) {  // received an initial request by a possible admin client (unknown at this stage)
+    uint32_t timestamp;
+    memcpy(&timestamp, data, 4);
+
+    data[len] = 0;  // ensure null terminator
+    uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
+
+    if (reply_len == 0) return;   // invalid request
+
+    if (packet->isRouteFlood()) {
+      // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
+      mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
+                                            PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
+      if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+    } else {
+      mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
+      if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
+    }
+  }
+}
+
+int SensorMesh::searchPeersByHash(const uint8_t* hash) {
+  int n = 0;
+  for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
+    if (contacts[i].id.isHashMatch(hash)) {
+      matching_peer_indexes[n++] = i;  // store the INDEXES of matching contacts (for subsequent 'peer' methods)
+    }
+  }
+  return n;
+}
+
+void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
+  int i = matching_peer_indexes[peer_idx];
+  if (i >= 0 && i < num_contacts) {
+    // lookup pre-calculated shared_secret
+    memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
+  } else {
+    MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
+  }
+}
+
+void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
+  int i = matching_peer_indexes[sender_idx];
+  if (i < 0 || i >= num_contacts) {
+    MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
+    return;
+  }
+
+  ContactInfo& from = contacts[i];
+
+  if (type == PAYLOAD_TYPE_REQ) {  // request (from a known contact)
+    uint32_t timestamp;
+    memcpy(&timestamp, data, 4);
+
+    if (timestamp > from.last_timestamp) {  // prevent replay attacks
+      uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFF : from.permissions, timestamp, data[4], &data[5], len - 5);
+      if (reply_len == 0) return;  // invalid command
+
+      from.last_timestamp = timestamp;
+      from.last_activity = getRTCClock()->getCurrentTime();
+
+      if (packet->isRouteFlood()) {
+        // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
+        mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
+                                              PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
+        if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
+      } else {
+        mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, reply_data, reply_len);
+        if (reply) {
+          if (from.out_path_len >= 0) {  // we have an out_path, so send DIRECT
+            sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
+          } else {
+            sendFlood(reply, SERVER_RESPONSE_DELAY);
+          }
+        }
+      }
+    } else {
+      MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
+    }
+  } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from.isAdmin()) {   // a CLI command
+    uint32_t sender_timestamp;
+    memcpy(&sender_timestamp, data, 4);  // timestamp (by sender's RTC clock - which could be wrong)
+    uint flags = (data[4] >> 2);   // message attempt number, and other flags
+
+    if (!(flags == TXT_TYPE_CLI_DATA)) {
+      MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
+    } else if (sender_timestamp > from.last_timestamp) {  // prevent replay attacks
+      from.last_timestamp = sender_timestamp;
+      from.last_activity = getRTCClock()->getCurrentTime();
+
+      // len can be > original length, but 'text' will be padded with zeroes
+      data[len] = 0; // need to make a C string again, with null terminator
+
+      uint8_t temp[166];
+      char *command = (char *) &data[5];
+      char *reply = (char *) &temp[5];
+      handleCommand(sender_timestamp, command, reply);
+
+      int text_len = strlen(reply);
+      if (text_len > 0) {
+        uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
+        if (timestamp == sender_timestamp) {
+          // WORKAROUND: the two timestamps need to be different, in the CLI view
+          timestamp++;
+        }
+        memcpy(temp, &timestamp, 4);   // mostly an extra blob to help make packet_hash unique
+        temp[4] = (TXT_TYPE_CLI_DATA << 2);
+
+        auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from.id, secret, temp, 5 + text_len);
+        if (reply) {
+          if (from.out_path_len < 0) {
+            sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
+          } else {
+            sendDirect(reply, from.out_path, from.out_path_len, CLI_REPLY_DELAY_MILLIS);
+          }
+        }
+      }
+    } else {
+      MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
+    }
+  }
+}
+
+bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
+  int i = matching_peer_indexes[sender_idx];
+  if (i < 0 || i >= num_contacts) {
+    MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
+    return false;
+  }
+
+  ContactInfo& from = contacts[i];
+
+  MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
+  // NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
+  // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
+  memcpy(from.out_path, path, from.out_path_len = path_len);  // store a copy of path, for sendDirect()
+  from.last_activity = getRTCClock()->getCurrentTime();
+
+  // REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
+  if (from.isAdmin()) {
+    // only do saveContacts() (of this out_path change) if this is an admin
+    dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
+  }
+
+  // NOTE: no reciprocal path send!!
+  return false;
+}
+
+void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
+  if (num_alert_tasks > 0) {
+    auto t = alert_tasks[0];   // check current alert task
+    for (int i = 0; i < t->attempt; i++) {
+      if (ack_crc == t->expected_acks[i]) {   // matching ACK!
+        t->attempt = 4;  // signal to move to next contact
+        t->send_expiry = 0;
+        packet->markDoNotRetransmit();   // ACK was for this node, so don't retransmit
+        return;
+      }
+    }
+  }
+}
+
+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)
+{
+  num_contacts = 0;
+  next_local_advert = next_flood_advert = 0;
+  dirty_contacts_expiry = 0;
+  last_read_time = 0;
+  num_alert_tasks = 0;
+  set_radio_at = revert_radio_at = 0;
+
+  // defaults
+  memset(&_prefs, 0, sizeof(_prefs));
+  _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
+  StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
+  _prefs.node_lat = ADVERT_LAT;
+  _prefs.node_lon = ADVERT_LON;
+  StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
+  _prefs.freq = LORA_FREQ;
+  _prefs.sf = LORA_SF;
+  _prefs.bw = LORA_BW;
+  _prefs.cr = LORA_CR;
+  _prefs.tx_power_dbm = LORA_TX_POWER;
+  _prefs.advert_interval = 1;  // default to 2 minutes for NEW installs
+  _prefs.flood_advert_interval = 0;   // disabled
+  _prefs.disable_fwd = true;
+  _prefs.flood_max = 64;
+  _prefs.interference_threshold = 0;  // disabled
+}
+
+void SensorMesh::begin(FILESYSTEM* fs) {
+  mesh::Mesh::begin();
+  _fs = fs;
+  // load persisted prefs
+  _cli.loadPrefs(_fs);
+
+  loadContacts();
+
+  radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
+  radio_set_tx_power(_prefs.tx_power_dbm);
+
+  updateAdvertTimer();
+  updateFloodAdvertTimer();
+}
+
+bool SensorMesh::formatFileSystem() {
+#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
+    return InternalFS.format();
+#elif defined(RP2040_PLATFORM)
+    return LittleFS.format();
+#elif defined(ESP32)
+    return SPIFFS.format();
+#else
+    #error "need to implement file system erase"
+    return false;
+#endif
+}
+
+void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
+  set_radio_at = futureMillis(2000);   // give CLI reply some time to be sent back, before applying temp radio params
+  pending_freq = freq;
+  pending_bw = bw;
+  pending_sf = sf;
+  pending_cr = cr;
+
+  revert_radio_at = futureMillis(2000 + timeout_mins*60*1000);   // schedule when to revert radio params
+}
+
+void SensorMesh::sendSelfAdvertisement(int delay_millis) {
+  mesh::Packet* pkt = createSelfAdvert();
+  if (pkt) {
+    sendFlood(pkt, delay_millis);
+  } else {
+    MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
+  }
+}
+
+void SensorMesh::updateAdvertTimer() {
+  if (_prefs.advert_interval > 0) {  // schedule local advert timer
+    next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000);
+  } else {
+    next_local_advert = 0;  // stop the timer
+  }
+}
+void SensorMesh::updateFloodAdvertTimer() {
+  if (_prefs.flood_advert_interval > 0) {  // schedule flood advert timer
+    next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
+  } else {
+    next_flood_advert = 0;  // stop the timer
+  }
+}
+
+void SensorMesh::setTxPower(uint8_t power_dbm) {
+  radio_set_tx_power(power_dbm);
+}
+
+float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
+  auto buf = telemetry.getBuffer();
+  uint8_t size = telemetry.getSize();
+  uint8_t i = 0;
+
+  while (i + 2 < size) {
+    // Get channel #
+    uint8_t ch = buf[i++];
+    // Get data type
+    uint8_t t = buf[i++];
+    uint8_t sz = getDataSize(t);
+
+    if (ch == channel && t == type) {
+      return getFloat(&buf[i], sz, getMultiplier(t), isSigned(t));
+    }
+    i += sz;  // skip
+  }
+  return 0.0f;   // not found
+}
+
+bool  SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
+  if (channel == TELEM_CHANNEL_SELF) {
+    lat = sensors.node_lat;
+    lon = sensors.node_lon;
+    alt = sensors.node_altitude;
+    return true;
+  }
+  // REVISIT: custom GPS channels??
+  return false;
+}
+
+void SensorMesh::loop() {
+  mesh::Mesh::loop();
+
+  if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
+    mesh::Packet* pkt = createSelfAdvert();
+    if (pkt) sendFlood(pkt);
+
+    updateFloodAdvertTimer();   // schedule next flood advert
+    updateAdvertTimer();   // also schedule local advert (so they don't overlap)
+  } else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
+    mesh::Packet* pkt = createSelfAdvert();
+    if (pkt) sendZeroHop(pkt);
+
+    updateAdvertTimer();   // schedule next local advert
+  }
+
+  if (set_radio_at && millisHasNowPassed(set_radio_at)) {   // apply pending (temporary) radio params
+    set_radio_at = 0;  // clear timer
+    radio_set_params(pending_freq, pending_bw, pending_sf, pending_cr);
+    MESH_DEBUG_PRINTLN("Temp radio params");
+  }
+
+  if (revert_radio_at && millisHasNowPassed(revert_radio_at)) {   // revert radio params to orig
+    revert_radio_at = 0;  // clear timer
+    radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
+    MESH_DEBUG_PRINTLN("Radio params restored");
+  }
+
+  uint32_t curr = getRTCClock()->getCurrentTime();
+  if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) {
+    telemetry.reset();
+    telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
+    // query other sensors -- target specific
+    sensors.querySensors(0xFF, telemetry);  // allow all telemetry permissions
+
+    onSensorDataRead();
+
+    last_read_time = curr;
+  }
+
+  // check the alert send queue
+  if (num_alert_tasks > 0) {
+    auto t = alert_tasks[0];   // process head of queue
+
+    if (millisHasNowPassed(t->send_expiry)) {  // next send needed?
+      if (t->attempt >= 4) {  // max attempts reached, try next contact
+        t->curr_contact_idx++;
+        if (t->curr_contact_idx >= num_contacts) {  // no more contacts to try?
+          num_alert_tasks--;   // remove t from queue
+          for (int i = 0; i < num_alert_tasks; i++) {
+            alert_tasks[i] = alert_tasks[i + 1];
+          }
+        } else {
+          auto c = &contacts[t->curr_contact_idx];
+          uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
+
+          if (c->permissions & pri_mask) {   // contact wants alert
+            // reset attempts
+            t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0;   // Low pri alerts, start at attempt #3 (ie. only make ONE attempt)
+            t->timestamp = getRTCClock()->getCurrentTimeUnique();   // need unique timestamp per contact
+
+            sendAlert(c, t);  // NOTE: modifies attempt, expected_acks[] and send_expiry
+          } else {
+            // next contact tested in next ::loop()
+          }
+        }
+      } else if (t->curr_contact_idx < num_contacts) {
+        auto c = &contacts[t->curr_contact_idx];   // send next attempt
+        sendAlert(c, t);  // NOTE: modifies attempt, expected_acks[] and send_expiry
+      } else {
+        // contact list has likely been modified while waiting for alert ACK, cancel this task
+        t->attempt = 4;   // next ::loop() will remove t from queue
+      }
+    }
+  }
+
+  // is there are pending dirty contacts write needed?
+  if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
+    saveContacts();
+    dirty_contacts_expiry = 0;
+  }
+}

+ 175 - 0
examples/simple_sensor/SensorMesh.h

@@ -0,0 +1,175 @@
+#pragma once
+
+#include <Arduino.h>   // needed for PlatformIO
+#include <Mesh.h>
+
+#include "TimeSeriesData.h"
+
+#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
+#include <InternalFileSystem.h>
+#elif defined(RP2040_PLATFORM)
+#include <LittleFS.h>
+#elif defined(ESP32)
+#include <SPIFFS.h>
+#endif
+
+#include <helpers/ArduinoHelpers.h>
+#include <helpers/StaticPoolPacketManager.h>
+#include <helpers/SimpleMeshTables.h>
+#include <helpers/IdentityStore.h>
+#include <helpers/AdvertDataHelpers.h>
+#include <helpers/TxtDataHelpers.h>
+#include <helpers/CommonCLI.h>
+#include <RTClib.h>
+#include <target.h>
+
+#define PERM_ACL_ROLE_MASK     3   // lower 2 bits
+#define PERM_ACL_GUEST         0
+#define PERM_ACL_READ_ONLY     1
+#define PERM_ACL_READ_WRITE    2
+#define PERM_ACL_ADMIN         3
+
+#define PERM_RESERVED1         (1 << 2)
+#define PERM_RESERVED2         (1 << 3)
+#define PERM_RESERVED3         (1 << 4)
+#define PERM_RESERVED4         (1 << 5)
+#define PERM_RECV_ALERTS_LO    (1 << 6)   // low priority alerts
+#define PERM_RECV_ALERTS_HI    (1 << 7)   // high priority alerts
+
+struct ContactInfo {
+  mesh::Identity id;
+  uint8_t permissions;
+  int8_t out_path_len;
+  uint8_t out_path[MAX_PATH_SIZE];
+  uint8_t shared_secret[PUB_KEY_SIZE];
+  uint32_t last_timestamp;   // by THEIR clock  (transient)
+  uint32_t last_activity;    // by OUR clock    (transient)
+
+  bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
+};
+
+#ifndef FIRMWARE_BUILD_DATE
+  #define FIRMWARE_BUILD_DATE   "24 Jul 2025"
+#endif
+
+#ifndef FIRMWARE_VERSION
+  #define FIRMWARE_VERSION   "v1.7.4"
+#endif
+
+#define FIRMWARE_ROLE "sensor"
+
+#define MAX_CONTACTS           20
+
+#define MAX_SEARCH_RESULTS      8
+#define MAX_CONCURRENT_ALERTS   4
+
+class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
+public:
+  SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
+  void begin(FILESYSTEM* fs);
+  void loop();
+  void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
+
+  // CommonCLI callbacks
+  const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
+  const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
+  const char* getRole() override { return FIRMWARE_ROLE; }
+  const char* getNodeName() { return _prefs.node_name; }
+  NodePrefs* getNodePrefs() { return &_prefs; }
+  void savePrefs() override { _cli.savePrefs(_fs); }
+  bool formatFileSystem() override;
+  void sendSelfAdvertisement(int delay_millis) override;
+  void updateAdvertTimer() override;
+  void updateFloodAdvertTimer() override;
+  void setLoggingOn(bool enable) override {  }
+  void eraseLogFile() override { }
+  void dumpLogFile() override { }
+  void setTxPower(uint8_t power_dbm) override;
+  void formatNeighborsReply(char *reply) override {
+    strcpy(reply, "not supported");
+  }
+  const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; }
+  void clearStats() override { }
+  void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
+
+  float getTelemValue(uint8_t channel, uint8_t type);
+
+protected:
+  // current telemetry data queries
+  float getVoltage(uint8_t channel) { return getTelemValue(channel, LPP_VOLTAGE); }
+  float getCurrent(uint8_t channel) { return getTelemValue(channel, LPP_CURRENT); }
+  float getPower(uint8_t channel) { return getTelemValue(channel, LPP_POWER); }
+  float getTemperature(uint8_t channel) { return getTelemValue(channel, LPP_TEMPERATURE); }
+  float getRelativeHumidity(uint8_t channel) { return getTelemValue(channel, LPP_RELATIVE_HUMIDITY); }
+  float getBarometricPressure(uint8_t channel) { return getTelemValue(channel, LPP_BAROMETRIC_PRESSURE); }
+  float getAltitude(uint8_t channel) { return getTelemValue(channel, LPP_ALTITUDE); }
+  bool  getGPS(uint8_t channel, float& lat, float& lon, float& alt);
+
+  // alerts
+  enum AlertPriority { LOW_PRI_ALERT, HIGH_PRI_ALERT };
+
+  struct Trigger {
+    uint32_t timestamp;
+    AlertPriority pri;
+    uint32_t expected_acks[4];
+    int8_t   curr_contact_idx;
+    uint8_t  attempt;
+    unsigned long send_expiry;
+    char text[MAX_PACKET_PAYLOAD];
+
+    Trigger() { text[0] = 0; }
+    bool isTriggered() const { return text[0] != 0; }
+  };
+  void alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text);
+
+  virtual void onSensorDataRead() = 0;   // for app to implement
+  virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0;  // for app to implement
+  virtual bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) { return false; }
+
+  // Mesh overrides
+  float getAirtimeBudgetFactor() const override;
+  bool allowPacketForward(const mesh::Packet* packet) override;
+  int calcRxDelay(float score, uint32_t air_time) const override;
+  uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
+  uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
+  int getInterferenceThreshold() const override;
+  int getAGCResetInterval() const override;
+  void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
+  int searchPeersByHash(const uint8_t* hash) override;
+  void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
+  void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
+  bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
+  void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
+
+private:
+  FILESYSTEM* _fs;
+  unsigned long next_local_advert, next_flood_advert;
+  NodePrefs _prefs;
+  CommonCLI _cli;
+  uint8_t reply_data[MAX_PACKET_PAYLOAD];
+  ContactInfo contacts[MAX_CONTACTS];
+  int num_contacts;
+  unsigned long dirty_contacts_expiry;
+  CayenneLPP telemetry;
+  uint32_t last_read_time;
+  int matching_peer_indexes[MAX_SEARCH_RESULTS];
+  int num_alert_tasks;
+  Trigger* alert_tasks[MAX_CONCURRENT_ALERTS];
+  unsigned long set_radio_at, revert_radio_at;
+  float pending_freq;
+  float pending_bw;
+  uint8_t pending_sf;
+  uint8_t pending_cr;
+
+  void loadContacts();
+  void saveContacts();
+  uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
+  uint8_t handleRequest(uint8_t perms, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len);
+  mesh::Packet* createSelfAdvert();
+  ContactInfo* getContact(const uint8_t* pubkey, int key_len);
+  ContactInfo* putContact(const mesh::Identity& id, uint8_t init_perms);
+  bool applyContactPermissions(const uint8_t* pubkey, int key_len, uint8_t perms);
+
+  void sendAlert(ContactInfo* c, Trigger* t);
+
+};

+ 45 - 0
examples/simple_sensor/TimeSeriesData.cpp

@@ -0,0 +1,45 @@
+#include "TimeSeriesData.h"
+
+void TimeSeriesData::recordData(mesh::RTCClock* clock, float value) {
+  uint32_t now = clock->getCurrentTime();
+  if (now >= last_timestamp + interval_secs) {
+    last_timestamp = now;
+
+    data[next] = value;   // append to cycle table
+    next = (next + 1) % num_slots;
+  }
+}
+
+void TimeSeriesData::calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const {
+  int i = next, n = num_slots;
+  uint32_t ago = clock->getCurrentTime() - last_timestamp;
+  int num_values = 0;
+  float total = 0.0f;
+
+  dest->_channel = channel;
+  dest->_lpp_type = lpp_type;
+
+  // start at most recet recording, back-track through to oldest
+  while (n > 0) {
+    n--;
+    i = (i + num_slots - 1) % num_slots;  // go back by one
+    if (ago >= end_secs_ago && ago < start_secs_ago) {   // filter by the desired time range
+      float v = data[i];
+      num_values++;
+      total += v;
+      if (num_values == 1) {
+        dest->_max = dest->_min = v;
+      } else {
+        if (v < dest->_min) dest->_min = v;
+        if (v > dest->_max) dest->_max = v;
+      }
+    }
+    ago += interval_secs;
+  }
+  // calc average
+  if (num_values > 0) {
+    dest->_avg = total / num_values;
+  } else {
+    dest->_max = dest->_min = dest->_avg = NAN;
+  }
+}

+ 29 - 0
examples/simple_sensor/TimeSeriesData.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <Arduino.h>
+#include <Mesh.h>
+
+struct MinMaxAvg {
+  float _min, _max, _avg;
+  uint8_t _lpp_type, _channel;
+};
+
+class TimeSeriesData {
+  float* data;
+  int num_slots, next;
+  uint32_t last_timestamp;
+  uint32_t interval_secs;
+
+public:
+  TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) { 
+    memset(data, 0, sizeof(float)*num);
+  }
+  TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) {
+    data = new float[num];
+    memset(data, 0, sizeof(float)*num);
+  }
+
+  void recordData(mesh::RTCClock* clock, float value);
+  void calcMinMaxAvg(mesh::RTCClock* clock, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) const;
+};
+

+ 114 - 0
examples/simple_sensor/UITask.cpp

@@ -0,0 +1,114 @@
+#include "UITask.h"
+#include <Arduino.h>
+#include <helpers/CommonCLI.h>
+
+#define AUTO_OFF_MILLIS      20000  // 20 seconds
+#define BOOT_SCREEN_MILLIS   4000   // 4 seconds
+
+// 'meshcore', 128x13px
+static const uint8_t meshcore_logo [] PROGMEM = {
+    0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 
+    0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, 
+    0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, 
+    0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, 
+    0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, 
+    0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, 
+    0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, 
+    0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, 
+    0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, 
+    0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, 
+    0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, 
+    0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, 
+    0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, 
+};
+
+void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
+  _prevBtnState = HIGH;
+  _auto_off = millis() + AUTO_OFF_MILLIS;
+  _node_prefs = node_prefs;
+  _display->turnOn();
+
+  // strip off dash and commit hash by changing dash to null terminator
+  // e.g: v1.2.3-abcdef -> v1.2.3
+  char *version = strdup(firmware_version);
+  char *dash = strchr(version, '-');
+  if(dash){
+    *dash = 0;
+  }
+
+  // v1.2.3 (1 Jan 2025)
+  sprintf(_version_info, "%s (%s)", version, build_date);
+}
+
+void UITask::renderCurrScreen() {
+  char tmp[80];
+  if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
+    // meshcore logo
+    _display->setColor(DisplayDriver::BLUE);
+    int logoWidth = 128;
+    _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
+
+    // version info
+    _display->setColor(DisplayDriver::LIGHT);
+    _display->setTextSize(1);
+    uint16_t versionWidth = _display->getTextWidth(_version_info);
+    _display->setCursor((_display->width() - versionWidth) / 2, 22);
+    _display->print(_version_info);
+
+    // node type
+    const char* node_type = "< Sensor >";
+    uint16_t typeWidth = _display->getTextWidth(node_type);
+    _display->setCursor((_display->width() - typeWidth) / 2, 35);
+    _display->print(node_type);
+  } else {  // home screen
+    // node name
+    _display->setCursor(0, 0);
+    _display->setTextSize(1);
+    _display->setColor(DisplayDriver::GREEN);
+    _display->print(_node_prefs->node_name);
+
+    // freq / sf
+    _display->setCursor(0, 20);
+    _display->setColor(DisplayDriver::YELLOW);
+    sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
+    _display->print(tmp);
+
+    // bw / cr
+    _display->setCursor(0, 30);
+    sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
+    _display->print(tmp);
+  }
+}
+
+void UITask::loop() {
+#ifdef PIN_USER_BTN
+  if (millis() >= _next_read) {
+    int btnState = digitalRead(PIN_USER_BTN);
+    if (btnState != _prevBtnState) {
+      if (btnState == LOW) {  // pressed?
+        if (_display->isOn()) {
+          // TODO: any action ?
+        } else {
+          _display->turnOn();
+        }
+        _auto_off = millis() + AUTO_OFF_MILLIS;   // extend auto-off timer
+      }
+      _prevBtnState = btnState;
+    }
+    _next_read = millis() + 200;  // 5 reads per second
+  }
+#endif
+
+  if (_display->isOn()) {
+    if (millis() >= _next_refresh) {
+      _display->startFrame();
+      renderCurrScreen();
+      _display->endFrame();
+
+      _next_refresh = millis() + 1000;   // refresh every second
+    }
+    if (millis() > _auto_off) {
+      _display->turnOff();
+    }
+  }
+}

+ 19 - 0
examples/simple_sensor/UITask.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <helpers/ui/DisplayDriver.h>
+#include <helpers/CommonCLI.h>
+
+class UITask {
+  DisplayDriver* _display;
+  unsigned long _next_read, _next_refresh, _auto_off;
+  int _prevBtnState;
+  NodePrefs* _node_prefs;
+  char _version_info[32];
+
+  void renderCurrScreen();
+public:
+  UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
+  void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
+
+  void loop();
+};

+ 147 - 0
examples/simple_sensor/main.cpp

@@ -0,0 +1,147 @@
+#include "SensorMesh.h"
+
+#ifdef DISPLAY_CLASS
+  #include "UITask.h"
+  static UITask ui_task(display);
+#endif
+
+class MyMesh : public SensorMesh {
+public:
+  MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
+     : SensorMesh(board, radio, ms, rng, rtc, tables), 
+       battery_data(12*24, 5*60)    // 24 hours worth of battery data, every 5 minutes
+  {
+  }
+
+protected:
+  /* ========================== custom logic here ========================== */
+  Trigger low_batt, critical_batt;
+  TimeSeriesData  battery_data;
+
+  void onSensorDataRead() override {
+    float batt_voltage = getVoltage(TELEM_CHANNEL_SELF);
+
+    battery_data.recordData(getRTCClock(), batt_voltage);   // record battery
+    alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!");
+    alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low");
+  }
+
+  int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override {
+    battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE);
+    return 1;
+  }
+
+  bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override {
+    if (strcmp(command, "magic") == 0) {    // example 'custom' command handling
+      strcpy(reply, "**Magic now done**");
+      return true;   // handled
+    }
+    return false;  // not handled
+  }
+  /* ======================================================================= */
+};
+
+StdRNG fast_rng;
+SimpleMeshTables tables;
+
+MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
+
+void halt() {
+  while (1) ;
+}
+
+static char command[120];
+
+void setup() {
+  Serial.begin(115200);
+  delay(1000);
+
+  board.begin();
+
+#ifdef DISPLAY_CLASS
+  if (display.begin()) {
+    display.startFrame();
+    display.print("Please wait...");
+    display.endFrame();
+  }
+#endif
+
+  if (!radio_init()) { halt(); }
+
+  fast_rng.begin(radio_get_rng_seed());
+
+  FILESYSTEM* fs;
+#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
+  InternalFS.begin();
+  fs = &InternalFS;
+  IdentityStore store(InternalFS, "");
+#elif defined(ESP32)
+  SPIFFS.begin(true);
+  fs = &SPIFFS;
+  IdentityStore store(SPIFFS, "/identity");
+#elif defined(RP2040_PLATFORM)
+  LittleFS.begin();
+  fs = &LittleFS;
+  IdentityStore store(LittleFS, "/identity");
+  store.begin();
+#else
+  #error "need to define filesystem"
+#endif
+  if (!store.load("_main", the_mesh.self_id)) {
+    MESH_DEBUG_PRINTLN("Generating new keypair");
+    the_mesh.self_id = radio_new_identity();   // create new random identity
+    int count = 0;
+    while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) {  // reserved id hashes
+      the_mesh.self_id = radio_new_identity(); count++;
+    }
+    store.save("_main", the_mesh.self_id);
+  }
+
+  Serial.print("Sensor ID: ");
+  mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println();
+
+  command[0] = 0;
+
+  sensors.begin();
+
+  the_mesh.begin(fs);
+
+#ifdef DISPLAY_CLASS
+  ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
+#endif
+
+  // send out initial Advertisement to the mesh
+  the_mesh.sendSelfAdvertisement(16000);
+}
+
+void loop() {
+  int len = strlen(command);
+  while (Serial.available() && len < sizeof(command)-1) {
+    char c = Serial.read();
+    if (c != '\n') {
+      command[len++] = c;
+      command[len] = 0;
+    }
+    Serial.print(c);
+  }
+  if (len == sizeof(command)-1) {  // command buffer full
+    command[sizeof(command)-1] = '\r';
+  }
+
+  if (len > 0 && command[len - 1] == '\r') {  // received complete line
+    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!
+    if (reply[0]) {
+      Serial.print("  -> "); Serial.println(reply);
+    }
+
+    command[0] = 0;  // reset command buffer
+  }
+
+  the_mesh.loop();
+  sensors.loop();
+#ifdef DISPLAY_CLASS
+  ui_task.loop();
+#endif
+}

+ 15 - 8
platformio.ini

@@ -29,9 +29,24 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
   -D LORA_SF=11
   -D ENABLE_PRIVATE_KEY_IMPORT=1   ; NOTE: comment these out for more secure firmware
   -D ENABLE_PRIVATE_KEY_EXPORT=1
+  -D RADIOLIB_EXCLUDE_CC1101=1
+  -D RADIOLIB_EXCLUDE_RF69=1
+  -D RADIOLIB_EXCLUDE_SX1231=1
+  -D RADIOLIB_EXCLUDE_SI443X=1
+  -D RADIOLIB_EXCLUDE_RFM2X=1
+  -D RADIOLIB_EXCLUDE_SX128X=1
+  -D RADIOLIB_EXCLUDE_AFSK=1
+  -D RADIOLIB_EXCLUDE_AX25=1
+  -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1
+  -D RADIOLIB_EXCLUDE_MORSE=1
+  -D RADIOLIB_EXCLUDE_APRS=1
+  -D RADIOLIB_EXCLUDE_BELL=1
+  -D RADIOLIB_EXCLUDE_RTTY=1
+  -D RADIOLIB_EXCLUDE_SSTV=1
 build_src_filter =
   +<*.cpp>
   +<helpers/*.cpp>
+  +<helpers/radiolib/*.cpp>
 
 ; ----------------- ESP32 ---------------------
 
@@ -63,14 +78,6 @@ build_flags = ${arduino_base.build_flags}
   -D NRF52_PLATFORM
   -D LFS_NO_ASSERT=1
 
-[nrf52840_base]
-extends = nrf52_base
-build_flags = ${nrf52_base.build_flags}
-lib_deps =
-  ${nrf52_base.lib_deps}
-  rweather/Crypto @ ^0.4.0
-  https://github.com/adafruit/Adafruit_nRF52_Arduino
-
 ; ----------------- RP2040 ---------------------
 
 [rp2040_base]

+ 121 - 15
src/Mesh.cpp

@@ -22,6 +22,9 @@ uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) {
 uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) {
   return 0;  // by default, no delay
 }
+uint8_t Mesh::getExtraAckTransmitCount() const {
+  return 0;
+}
 
 uint32_t Mesh::getCADFailRetryDelay() const {
   return _rng->nextInt(1, 4)*120;
@@ -67,22 +70,22 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
 
   if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
     if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
-      if (_tables->hasSeen(pkt)) return ACTION_RELEASE;  // don't retransmit!
-
-      // remove our hash from 'path', then re-broadcast
-      pkt->path_len -= PATH_HASH_SIZE;
-  #if 0
-      memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
-  #elif PATH_HASH_SIZE == 1
-      for (int k = 0; k < pkt->path_len; k++) {  // shuffle bytes by 1
-        pkt->path[k] = pkt->path[k + 1];
+      if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
+        return forwardMultipartDirect(pkt);
+      } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
+        if (!_tables->hasSeen(pkt)) {  // don't retransmit!
+          removeSelfFromPath(pkt);
+          routeDirectRecvAcks(pkt, 0);
+        }
+        return ACTION_RELEASE;
       }
-  #else
-      #error "need path remove impl"
-  #endif
 
-      uint32_t d = getDirectRetransmitDelay(pkt);
-      return ACTION_RETRANSMIT_DELAYED(0, d);  // Routed traffic is HIGHEST priority 
+      if (!_tables->hasSeen(pkt)) {
+        removeSelfFromPath(pkt);
+
+        uint32_t d = getDirectRetransmitDelay(pkt);
+        return ACTION_RETRANSMIT_DELAYED(0, d);  // Routed traffic is HIGHEST priority 
+      }
     }
     return ACTION_RELEASE;   // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
   }
@@ -181,7 +184,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
           uint8_t data[MAX_PACKET_PAYLOAD];
           int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
           if (len > 0) {  // success!
-            onAnonDataRecv(pkt, pkt->getPayloadType(), sender, data, len);
+            onAnonDataRecv(pkt, secret, sender, data, len);
             pkt->markDoNotRetransmit();
           }
         }
@@ -261,6 +264,32 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
       }
       break;
     }
+    case PAYLOAD_TYPE_MULTIPART:
+      if (pkt->payload_len > 2) {
+        uint8_t remaining = pkt->payload[0] >> 4;  // num of packets in this multipart sequence still to be sent
+        uint8_t type = pkt->payload[0] & 0x0F;
+
+        if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {    // a multipart ACK
+          Packet tmp;
+          tmp.header = pkt->header;
+          tmp.path_len = pkt->path_len;
+          memcpy(tmp.path, pkt->path, pkt->path_len);
+          tmp.payload_len = pkt->payload_len - 1;
+          memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
+
+          if (!_tables->hasSeen(&tmp)) {
+            uint32_t ack_crc;
+            memcpy(&ack_crc, tmp.payload, 4);
+
+            onAckRecv(&tmp, ack_crc);
+            //action = routeRecvPacket(&tmp);  // NOTE: currently not needed, as multipart ACKs not sent Flood
+          }
+        } else {
+          // FUTURE: other multipart types??
+        }
+      }
+      break;
+
     default:
       MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header);
       // Don't flood route unknown packet types!   action = routeRecvPacket(pkt);
@@ -269,6 +298,20 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
   return action;
 }
 
+void Mesh::removeSelfFromPath(Packet* pkt) {
+  // remove our hash from 'path'
+  pkt->path_len -= PATH_HASH_SIZE;
+#if 0
+  memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
+#elif PATH_HASH_SIZE == 1
+  for (int k = 0; k < pkt->path_len; k++) {  // shuffle bytes by 1
+    pkt->path[k] = pkt->path[k + 1];
+  }
+#else
+  #error "need path remove impl"
+#endif
+}
+
 DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
   if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
     && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
@@ -282,6 +325,54 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
   return ACTION_RELEASE;
 }
 
+DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
+  uint8_t remaining = pkt->payload[0] >> 4;  // num of packets in this multipart sequence still to be sent
+  uint8_t type = pkt->payload[0] & 0x0F;
+
+  if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {    // a multipart ACK
+    Packet tmp;
+    tmp.header = pkt->header;
+    tmp.path_len = pkt->path_len;
+    memcpy(tmp.path, pkt->path, pkt->path_len);
+    tmp.payload_len = pkt->payload_len - 1;
+    memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
+
+    if (!_tables->hasSeen(&tmp)) {   // don't retransmit!
+      removeSelfFromPath(&tmp);
+      routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300);  // expect multipart ACKs 300ms apart (x2)
+    }
+  }
+  return ACTION_RELEASE;
+}
+
+void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
+  if (!packet->isMarkedDoNotRetransmit()) {
+    uint32_t crc;
+    memcpy(&crc, packet->payload, 4);
+
+    uint8_t extra = getExtraAckTransmitCount();
+    while (extra > 0) {
+      delay_millis += getDirectRetransmitDelay(packet) + 300;
+      auto a1 = createMultiAck(crc, extra);
+      if (a1) {
+        memcpy(a1->path, packet->path, a1->path_len = packet->path_len);
+        a1->header &= ~PH_ROUTE_MASK;
+        a1->header |= ROUTE_TYPE_DIRECT;
+        sendPacket(a1, 0, delay_millis);
+      }
+      extra--;
+    }
+
+    auto a2 = createAck(crc);
+    if (a2) {
+      memcpy(a2->path, packet->path, a2->path_len = packet->path_len);
+      a2->header &= ~PH_ROUTE_MASK;
+      a2->header |= ROUTE_TYPE_DIRECT;
+      sendPacket(a2, 0, delay_millis);
+    }
+  }
+}
+
 Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
   if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
 
@@ -449,6 +540,21 @@ Packet* Mesh::createAck(uint32_t ack_crc) {
   return packet;
 }
 
+Packet* Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining) {
+  Packet* packet = obtainNewPacket();
+  if (packet == NULL) {
+    MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
+    return NULL;
+  }
+  packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT);  // ROUTE_TYPE_* set later
+
+  packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
+  memcpy(&packet->payload[1], &ack_crc, 4);
+  packet->payload_len = 5;
+
+  return packet;
+}
+
 Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
   if (len > sizeof(Packet::payload)) return NULL;  // invalid arg
 

+ 13 - 2
src/Mesh.h

@@ -28,6 +28,11 @@ class Mesh : public Dispatcher {
   RNG* _rng;
   MeshTables* _tables;
 
+  void removeSelfFromPath(Packet* packet);
+  void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
+  //void routeRecvAcks(Packet* packet, uint32_t delay_millis);
+  DispatcherAction forwardMultipartDirect(Packet* pkt);
+
 protected:
   DispatcherAction onRecvPacket(Packet* pkt) override;
 
@@ -54,6 +59,11 @@ protected:
    */
   virtual uint32_t getDirectRetransmitDelay(const Packet* packet);
 
+  /**
+   * \returns  number of extra (Direct) ACK transmissions wanted.
+   */
+  virtual uint8_t getExtraAckTransmitCount() const;
+
   /**
    * \brief  Perform search of local DB of peers/contacts.
    * \returns  Number of peers with matching hash
@@ -107,10 +117,10 @@ protected:
   /**
    * \brief  A (now decrypted) data packet has been received.
    *         NOTE: these can be received multiple times (per sender/contents), via different routes
-   * \param  type  one of: PAYLOAD_TYPE_ANON_REQ
+   * \param  secret  ECDH shared secret
    * \param  sender  public key provided by sender
   */
-  virtual void onAnonDataRecv(Packet* packet, uint8_t type, const Identity& sender, uint8_t* data, size_t len) { }
+  virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret, const Identity& sender, uint8_t* data, size_t len) { }
 
   /**
    * \brief  A path TO 'sender' has been received. (also with optional 'extra' data encoded)
@@ -165,6 +175,7 @@ public:
   Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
   Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
   Packet* createAck(uint32_t ack_crc);
+  Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
   Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
   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);

+ 1 - 0
src/Packet.h

@@ -26,6 +26,7 @@ namespace mesh {
 #define PAYLOAD_TYPE_ANON_REQ    0x07    // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
 #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_RAW_CUSTOM   0x0F    // custom packet as raw bytes, for applications with custom encryption, payloads, etc
 

+ 2 - 1
src/helpers/AdvertDataHelpers.h

@@ -8,7 +8,8 @@
 #define ADV_TYPE_CHAT         1
 #define ADV_TYPE_REPEATER     2
 #define ADV_TYPE_ROOM         3
-//FUTURE: 4..15
+#define ADV_TYPE_SENSOR       4
+//FUTURE: 5..15
 
 #define ADV_LATLON_MASK       0x10
 #define ADV_FEAT1_MASK        0x20   // FUTURE

+ 74 - 38
src/helpers/BaseChatMesh.cpp

@@ -31,6 +31,23 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
   return createAdvert(self_id, app_data, app_data_len);
 }
 
+void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
+  if (dest.out_path_len < 0) {
+    mesh::Packet* ack = createAck(ack_hash);
+    if (ack) sendFlood(ack, TXT_ACK_DELAY);
+  } else {
+    uint32_t d = TXT_ACK_DELAY;
+    if (getExtraAckTransmitCount() > 0) {
+      mesh::Packet* a1 = createMultiAck(ack_hash, 1);
+      if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
+      d += 300;
+    }
+
+    mesh::Packet* a2 = createAck(ack_hash);
+    if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
+  }
+}
+
 void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
   AdvertDataParser parser(app_data, app_data_len);
   if (!(parser.isValid() && parser.hasName())) {
@@ -152,14 +169,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
                                                 PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
         if (path) sendFlood(path, TXT_ACK_DELAY);
       } else {
-        mesh::Packet* ack = createAck(ack_hash);
-        if (ack) {
-          if (from.out_path_len < 0) {
-            sendFlood(ack, TXT_ACK_DELAY);
-          } else {
-            sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
-          }
-        }
+        sendAckTo(from, ack_hash);
       }
     } else if (flags == TXT_TYPE_CLI_DATA) {
       onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]);  // let UI know
@@ -185,14 +195,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
                                                 PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
         if (path) sendFlood(path, TXT_ACK_DELAY);
       } else {
-        mesh::Packet* ack = createAck(ack_hash);
-        if (ack) {
-          if (from.out_path_len < 0) {
-            sendFlood(ack, TXT_ACK_DELAY);
-          } else {
-            sendDirect(ack, from.out_path, from.out_path_len, TXT_ACK_DELAY);
-          }
-        }
+        sendAckTo(from, ack_hash);
       }
     } else {
       MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags);
@@ -403,22 +406,52 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) {
 }
 
 int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
-  int tlen;
-  uint8_t temp[24];
-  uint32_t now = getRTCClock()->getCurrentTimeUnique();
-  memcpy(temp, &now, 4);   // mostly an extra blob to help make packet_hash unique
-  if (recipient.type == ADV_TYPE_ROOM) {
-    memcpy(&temp[4], &recipient.sync_since, 4);
-    int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
-    memcpy(&temp[8], password, len);
-    tlen = 8 + len;
-  } else {
-    int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
-    memcpy(&temp[4], password, len);
-    tlen = 4 + len;
+  mesh::Packet* pkt;
+  {
+    int tlen;
+    uint8_t temp[24];
+    uint32_t now = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &now, 4);   // mostly an extra blob to help make packet_hash unique
+    if (recipient.type == ADV_TYPE_ROOM) {
+      memcpy(&temp[4], &recipient.sync_since, 4);
+      int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
+      memcpy(&temp[8], password, len);
+      tlen = 8 + len;
+    } else {
+      int len = strlen(password); if (len > 15) len = 15;  // max 15 chars currently
+      memcpy(&temp[4], password, len);
+      tlen = 4 + len;
+    }
+
+    pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
+  }
+  if (pkt) {
+    uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
+    if (recipient.out_path_len < 0) {
+      sendFlood(pkt);
+      est_timeout = calcFloodTimeoutMillisFor(t);
+      return MSG_SEND_SENT_FLOOD;
+    } else {
+      sendDirect(pkt, recipient.out_path, recipient.out_path_len);
+      est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
+      return MSG_SEND_SENT_DIRECT;
+    }
   }
+  return MSG_SEND_FAILED;
+}
+
+int  BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
+  if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
+
+  mesh::Packet* pkt;
+  {
+    uint8_t temp[MAX_PACKET_PAYLOAD];
+    tag = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &tag, 4);   // mostly an extra blob to help make packet_hash unique
+    memcpy(&temp[4], req_data, data_len);
 
-  auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
+    pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, 4 + data_len);
+  }
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     if (recipient.out_path_len < 0) {
@@ -435,14 +468,17 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
 }
 
 int  BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
-  uint8_t temp[13];
-  tag = getRTCClock()->getCurrentTimeUnique();
-  memcpy(temp, &tag, 4);   // mostly an extra blob to help make packet_hash unique
-  temp[4] = req_type;
-  memset(&temp[5], 0, 4);  // reserved (possibly for 'since' param)
-  getRNG()->random(&temp[9], 4);   // random blob to help make packet-hash unique
-
-  auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
+  mesh::Packet* pkt;
+  {
+    uint8_t temp[13];
+    tag = getRTCClock()->getCurrentTimeUnique();
+    memcpy(temp, &tag, 4);   // mostly an extra blob to help make packet_hash unique
+    temp[4] = req_type;
+    memset(&temp[5], 0, 4);  // reserved (possibly for 'since' param)
+    getRNG()->random(&temp[9], 4);   // random blob to help make packet-hash unique
+
+    pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
+  }
   if (pkt) {
     uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     if (recipient.out_path_len < 0) {

+ 2 - 0
src/helpers/BaseChatMesh.h

@@ -72,6 +72,7 @@ class BaseChatMesh : public mesh::Mesh {
   ConnectionInfo connections[MAX_CONNECTIONS];
 
   mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
+  void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
 
 protected:
   BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
@@ -134,6 +135,7 @@ public:
   bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
   int  sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
   int  sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
+  int  sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
   bool shareContactZeroHop(const ContactInfo& contact);
   uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
   bool importContact(const uint8_t src_buf[], uint8_t len);

+ 27 - 16
src/helpers/CommonCLI.cpp

@@ -51,7 +51,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     file.read((uint8_t *) &_prefs->sf, sizeof(_prefs->sf));  // 112
     file.read((uint8_t *) &_prefs->cr, sizeof(_prefs->cr));  // 113
     file.read((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only));  // 114
-    file.read((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2));  // 115
+    file.read((uint8_t *) &_prefs->multi_acks, sizeof(_prefs->multi_acks));  // 115
     file.read((uint8_t *) &_prefs->bw, sizeof(_prefs->bw));  // 116
     file.read((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval));  // 120
     file.read(pad, 3);   // 121
@@ -69,6 +69,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     _prefs->sf = constrain(_prefs->sf, 7, 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);
 
     file.close();
   }
@@ -106,7 +107,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
     file.write((uint8_t *) &_prefs->sf, sizeof(_prefs->sf));  // 112
     file.write((uint8_t *) &_prefs->cr, sizeof(_prefs->cr));  // 113
     file.write((uint8_t *) &_prefs->allow_read_only, sizeof(_prefs->allow_read_only));  // 114
-    file.write((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2));  // 115
+    file.write((uint8_t *) &_prefs->multi_acks, sizeof(_prefs->multi_acks));  // 115
     file.write((uint8_t *) &_prefs->bw, sizeof(_prefs->bw));  // 116
     file.write((uint8_t *) &_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval));  // 120
     file.write(pad, 3);   // 121
@@ -120,25 +121,18 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
 
 #define MIN_LOCAL_ADVERT_INTERVAL   60
 
-void CommonCLI::checkAdvertInterval() {
+void CommonCLI::savePrefs() {
   if (_prefs->advert_interval * 2 < MIN_LOCAL_ADVERT_INTERVAL) {
     _prefs->advert_interval = 0;  // turn it off, now that device has been manually configured
   }
+  _callbacks->savePrefs();
 }
 
 void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
-    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
-      reply += 3;
-      command += 3;
-    }
-
     if (memcmp(command, "reboot", 6) == 0) {
       _board->reboot();  // doesn't return
     } else if (memcmp(command, "advert", 6) == 0) {
-      _callbacks->sendSelfAdvertisement(400);
+      _callbacks->sendSelfAdvertisement(1500);  // longer delay, give CLI response time to be sent first
       strcpy(reply, "OK - Advert sent");
     } else if (memcmp(command, "clock sync", 10) == 0) {
       uint32_t curr = getRTCClock()->getCurrentTime();
@@ -171,10 +165,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
       }
     } else if (memcmp(command, "neighbors", 9) == 0) {
       _callbacks->formatNeighborsReply(reply);
+    } else if (memcmp(command, "tempradio ", 10) == 0) {
+      strcpy(tmp, &command[10]);
+      const char *parts[5];
+      int num = mesh::Utils::parseTextParts(tmp, parts, 5);
+      float freq  = num > 0 ? atof(parts[0]) : 0.0f;
+      float bw    = num > 1 ? atof(parts[1]) : 0.0f;
+      uint8_t sf  = num > 2 ? atoi(parts[2]) : 0;
+      uint8_t cr  = num > 3 ? atoi(parts[3]) : 0;
+      int temp_timeout_mins  = num > 4 ? atoi(parts[4]) : 0;
+      if (freq >= 300.0f && freq <= 2500.0f && sf >= 7 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7.0f && bw <= 500.0f && temp_timeout_mins > 0) {
+        _callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins);
+        sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins);
+      } else {
+        strcpy(reply, "Error, invalid params");
+      }
     } else if (memcmp(command, "password ", 9) == 0) {
       // change admin password
       StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password));
-      checkAdvertInterval();
       savePrefs();
       sprintf(reply, "password now: %s", _prefs->password);   // echo back just to let admin know for sure!!
     } else if (memcmp(command, "clear stats", 11) == 0) {
@@ -188,6 +196,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold);
       } else if (memcmp(config, "agc.reset.interval", 18) == 0) {
         sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4);
+      } else if (memcmp(config, "multi.acks", 10) == 0) {
+        sprintf(reply, "> %d", (uint32_t) _prefs->multi_acks);
       } else if (memcmp(config, "allow.read.only", 15) == 0) {
         sprintf(reply, "> %s", _prefs->allow_read_only ? "on" : "off");
       } else if (memcmp(config, "flood.advert.interval", 21) == 0) {
@@ -243,6 +253,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         _prefs->agc_reset_interval = atoi(&config[19]) / 4;
         savePrefs();
         strcpy(reply, "OK");
+      } else if (memcmp(config, "multi.acks ", 11) == 0) {
+        _prefs->multi_acks = atoi(&config[11]);
+        savePrefs();
+        strcpy(reply, "OK");
       } else if (memcmp(config, "allow.read.only ", 16) == 0) {
         _prefs->allow_read_only = memcmp(&config[16], "on", 2) == 0;
         savePrefs();
@@ -273,7 +287,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         strcpy(reply, "OK");
       } else if (memcmp(config, "name ", 5) == 0) {
         StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name));
-        checkAdvertInterval();
         savePrefs();
         strcpy(reply, "OK");
       } else if (memcmp(config, "repeat ", 7) == 0) {
@@ -300,12 +313,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         }
       } else if (memcmp(config, "lat ", 4) == 0) {
         _prefs->node_lat = atof(&config[4]);
-        checkAdvertInterval();
         savePrefs();
         strcpy(reply, "OK");
       } else if (memcmp(config, "lon ", 4) == 0) {
         _prefs->node_lon = atof(&config[4]);
-        checkAdvertInterval();
         savePrefs();
         strcpy(reply, "OK");
       } else if (memcmp(config, "rxdelay ", 8) == 0) {

+ 3 - 5
src/helpers/CommonCLI.h

@@ -21,7 +21,7 @@ struct NodePrefs {  // persisted to file
     uint8_t sf;
     uint8_t cr;
     uint8_t allow_read_only;
-    uint8_t reserved2;
+    uint8_t multi_acks;
     float bw;
     uint8_t flood_max;
     uint8_t interference_threshold;
@@ -45,6 +45,7 @@ public:
   virtual void formatNeighborsReply(char *reply) = 0;
   virtual const uint8_t* getSelfIdPubKey() = 0;
   virtual void clearStats() = 0;
+  virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
 };
 
 class CommonCLI {
@@ -55,10 +56,7 @@ class CommonCLI {
   char tmp[80];
 
   mesh::RTCClock* getRTCClock() { return _rtc; }
-  void savePrefs() { _callbacks->savePrefs(); }
-
-  void checkAdvertInterval();
-
+  void savePrefs();
   void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
 
 public:

+ 0 - 46
src/helpers/CustomLR1110.h

@@ -1,46 +0,0 @@
-#pragma once
-
-#include <RadioLib.h>
-
-#define LR1110_IRQ_HAS_PREAMBLE                     0b0000000100  //  4     4     valid LoRa header received
-#define LR1110_IRQ_HEADER_VALID                     0b0000010000  //  4     4     valid LoRa header received
-
-class CustomLR1110 : public LR1110 {
-  public:
-    CustomLR1110(Module *mod) : LR1110(mod) { }
-
-    RadioLibTime_t getTimeOnAir(size_t len) override {
-      uint32_t symbolLength_us = ((uint32_t)(1000 * 10) << this->spreadingFactor) / (this->bandwidthKhz * 10) ;
-      uint8_t sfCoeff1_x4 = 17; // (4.25 * 4)
-      uint8_t sfCoeff2 = 8;
-      if(this->spreadingFactor == 5 || this->spreadingFactor == 6) {
-        sfCoeff1_x4 = 25; // 6.25 * 4
-        sfCoeff2 = 0;
-      }
-      uint8_t sfDivisor = 4*this->spreadingFactor;
-      if(symbolLength_us >= 16000) {
-        sfDivisor = 4*(this->spreadingFactor - 2);
-      }
-      const int8_t bitsPerCrc = 16;
-      const int8_t N_symbol_header = this->headerType == RADIOLIB_SX126X_LORA_HEADER_EXPLICIT ? 20 : 0;
-
-      // numerator of equation in section 6.1.4 of SX1268 datasheet v1.1 (might not actually be bitcount, but it has len * 8)
-      int16_t bitCount = (int16_t) 8 * len + this->crcTypeLoRa * bitsPerCrc - 4 * this->spreadingFactor  + sfCoeff2 + N_symbol_header;
-      if(bitCount < 0) {
-        bitCount = 0;
-      }
-      // add (sfDivisor) - 1 to the numerator to give integer CEIL(...)
-      uint16_t nPreCodedSymbols = (bitCount + (sfDivisor - 1)) / (sfDivisor);
-
-      // preamble can be 65k, therefore nSymbol_x4 needs to be 32 bit
-      uint32_t nSymbol_x4 = (this->preambleLengthLoRa + 8) * 4 + sfCoeff1_x4 + nPreCodedSymbols * (this->codingRate + 4) * 4;
-
-      return((symbolLength_us * nSymbol_x4) / 4);
-    }
-
-    bool isReceiving() {
-      uint16_t irq = getIrqStatus();
-      bool detected = ((irq & LR1110_IRQ_HEADER_VALID) || (irq & LR1110_IRQ_HAS_PREAMBLE));
-      return detected;
-    }
-};

+ 61 - 46
src/helpers/nrf52/SerialBLEInterface.cpp

@@ -1,6 +1,27 @@
 #include "SerialBLEInterface.h"
 
+static SerialBLEInterface* instance;
+
+void SerialBLEInterface::onConnect(uint16_t connection_handle) {
+  BLE_DEBUG_PRINTLN("SerialBLEInterface: connected");
+  if(instance){
+    instance->_isDeviceConnected = true;
+    // no need to stop advertising on connect, as the ble stack does this automatically
+  }
+}
+
+void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
+  BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected reason=%d", reason);
+  if(instance){
+    instance->_isDeviceConnected = false;
+    instance->startAdv();
+  }
+}
+
 void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
+
+  instance = this;
+
   char charpin[20];
   sprintf(charpin, "%d", pin_code);
 
@@ -13,11 +34,31 @@ void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) {
   Bluefruit.Security.setMITM(true);
   Bluefruit.Security.setPIN(charpin);
 
+  Bluefruit.Periph.setConnectCallback(onConnect);
+  Bluefruit.Periph.setDisconnectCallback(onDisconnect);
+
   // To be consistent OTA DFU should be added first if it exists
   //bledfu.begin();
+
+  // Configure and start the BLE Uart service
+  bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
+  bleuart.begin();
+  
 }
 
 void SerialBLEInterface::startAdv() {
+
+  BLE_DEBUG_PRINTLN("SerialBLEInterface: starting advertising");
+  
+  // clean restart if already advertising
+  if(Bluefruit.Advertising.isRunning()){
+    BLE_DEBUG_PRINTLN("SerialBLEInterface: already advertising, stopping to allow clean restart");
+    Bluefruit.Advertising.stop();
+  }
+
+  Bluefruit.Advertising.clearData(); // clear advertising data
+  Bluefruit.ScanResponse.clearData(); // clear scan response data
+  
   // Advertising packet
   Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
   Bluefruit.Advertising.addTxPower();
@@ -38,10 +79,25 @@ void SerialBLEInterface::startAdv() {
    * For recommended advertising interval
    * https://developer.apple.com/library/content/qa/qa1931/_index.html   
    */
-  Bluefruit.Advertising.restartOnDisconnect(true);
+  Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect
   Bluefruit.Advertising.setInterval(32, 244);    // in unit of 0.625 ms
   Bluefruit.Advertising.setFastTimeout(30);      // number of seconds in fast mode
-  Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds 
+  Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds
+
+}
+
+void SerialBLEInterface::stopAdv() {
+
+  BLE_DEBUG_PRINTLN("SerialBLEInterface: stopping advertising");
+  
+  // we only want to stop advertising if it's running, otherwise an invalid state error is logged by ble stack
+  if(!Bluefruit.Advertising.isRunning()){
+    return;
+  }
+
+  // stop advertising
+  Bluefruit.Advertising.stop();
+
 }
 
 // ---------- public methods
@@ -52,25 +108,14 @@ void SerialBLEInterface::enable() {
   _isEnabled = true;
   clearBuffers();
 
-  // Configure and start the BLE Uart service
-  bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
-  bleuart.begin();
-
   // Start advertising
   startAdv();
-
-  checkAdvRestart = false;
 }
 
 void SerialBLEInterface::disable() {
   _isEnabled = false;
-
   BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
-
-  Bluefruit.Advertising.stop();
-
-  oldDeviceConnected = deviceConnected = false;
-  checkAdvRestart = false;
+  stopAdv();
 }
 
 size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
@@ -79,7 +124,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
     return 0;
   }
 
-  if (deviceConnected && len > 0) {
+  if (_isDeviceConnected && len > 0) {
     if (send_queue_len >= FRAME_QUEUE_SIZE) {
       BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
       return 0;
@@ -115,44 +160,14 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
   } else {
     int len = bleuart.available();
     if (len > 0) {
-      deviceConnected = true; // should probably use the callback to monitor cx 
       bleuart.readBytes(dest, len);
       BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
       return len;
     }
   }
-
-  if (Bluefruit.connected() == 0)  deviceConnected = false;
-
-  if (deviceConnected != oldDeviceConnected) {
-    if (!deviceConnected) {    // disconnecting
-      clearBuffers();
-
-      BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
-      delay(500); // give the bluetooth stack the chance to get things ready
-
-      checkAdvRestart = true;
-    } else {
-      BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising");
-      BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting...");
-      // connecting
-      // do stuff here on connecting
-      Bluefruit.Advertising.stop();
-      checkAdvRestart = false;
-    }
-    oldDeviceConnected = deviceConnected;
-  }
-
-  if (checkAdvRestart) {
-    if (Bluefruit.connected() == 0) {
-      BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising");
-      startAdv();
-    }
-    checkAdvRestart = false;
-  }
   return 0;
 }
 
 bool SerialBLEInterface::isConnected() const {
-  return deviceConnected;  //pServer != NULL && pServer->getConnectedCount() > 0;
+  return _isDeviceConnected;
 }

+ 6 - 7
src/helpers/nrf52/SerialBLEInterface.h

@@ -5,10 +5,8 @@
 
 class SerialBLEInterface : public BaseSerialInterface {
   BLEUart bleuart;
-  bool deviceConnected;
-  bool oldDeviceConnected;
-  bool checkAdvRestart;
   bool _isEnabled;
+  bool _isDeviceConnected;
   unsigned long _last_write;
 
   struct Frame {
@@ -21,18 +19,19 @@ class SerialBLEInterface : public BaseSerialInterface {
   Frame send_queue[FRAME_QUEUE_SIZE];
 
   void clearBuffers() { send_queue_len = 0; }
-  void startAdv();
+  static void onConnect(uint16_t connection_handle);
+  static void onDisconnect(uint16_t connection_handle, uint8_t reason);
 
 public:
   SerialBLEInterface() {
-    deviceConnected = false;
-    oldDeviceConnected = false;
-    checkAdvRestart = false;
     _isEnabled = false;
+    _isDeviceConnected = false;
     _last_write = 0;
     send_queue_len = 0;
   }
 
+  void startAdv();
+  void stopAdv();
   void begin(const char* device_name, uint32_t pin_code);
 
   // BaseSerialInterface methods

+ 0 - 0
src/helpers/CustomLLCC68.h → src/helpers/radiolib/CustomLLCC68.h


+ 0 - 0
src/helpers/CustomLLCC68Wrapper.h → src/helpers/radiolib/CustomLLCC68Wrapper.h


+ 70 - 0
src/helpers/radiolib/CustomLR1110.h

@@ -0,0 +1,70 @@
+#pragma once
+
+#include <RadioLib.h>
+
+#define LR1110_IRQ_HAS_PREAMBLE                     0b0000000100  //  4     4     valid LoRa header received
+#define LR1110_IRQ_HEADER_VALID                     0b0000010000  //  4     4     valid LoRa header received
+
+class CustomLR1110 : public LR1110 {
+  public:
+    CustomLR1110(Module *mod) : LR1110(mod) { }
+
+    RadioLibTime_t getTimeOnAir(size_t len) override {
+  // calculate number of symbols
+  float N_symbol = 0;
+  if(this->codingRate <= RADIOLIB_LR11X0_LORA_CR_4_8_SHORT) {
+    // legacy coding rate - nice and simple
+    // get SF coefficients
+    float coeff1 = 0;
+    int16_t coeff2 = 0;
+    int16_t coeff3 = 0;
+    if(this->spreadingFactor < 7) {
+      // SF5, SF6
+      coeff1 = 6.25;
+      coeff2 = 4*this->spreadingFactor;
+      coeff3 = 4*this->spreadingFactor;
+    } else if(this->spreadingFactor < 11) {
+      // SF7. SF8, SF9, SF10
+      coeff1 = 4.25;
+      coeff2 = 4*this->spreadingFactor + 8;
+      coeff3 = 4*this->spreadingFactor;
+    } else {
+      // SF11, SF12
+      coeff1 = 4.25;
+      coeff2 = 4*this->spreadingFactor + 8;
+      coeff3 = 4*(this->spreadingFactor - 2);
+    }
+
+    // get CRC length
+    int16_t N_bitCRC = 16;
+    if(this->crcTypeLoRa == RADIOLIB_LR11X0_LORA_CRC_DISABLED) {
+      N_bitCRC = 0;
+    }
+
+    // get header length
+    int16_t N_symbolHeader = 20;
+    if(this->headerType == RADIOLIB_LR11X0_LORA_HEADER_IMPLICIT) {
+      N_symbolHeader = 0;
+    }
+
+    // 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));
+      return detected;
+    }
+};

+ 0 - 0
src/helpers/CustomLR1110Wrapper.h → src/helpers/radiolib/CustomLR1110Wrapper.h


+ 0 - 0
src/helpers/CustomSTM32WLx.h → src/helpers/radiolib/CustomSTM32WLx.h


+ 0 - 0
src/helpers/CustomSTM32WLxWrapper.h → src/helpers/radiolib/CustomSTM32WLxWrapper.h


+ 0 - 0
src/helpers/CustomSX1262.h → src/helpers/radiolib/CustomSX1262.h


+ 0 - 0
src/helpers/CustomSX1262Wrapper.h → src/helpers/radiolib/CustomSX1262Wrapper.h


+ 0 - 0
src/helpers/CustomSX1268.h → src/helpers/radiolib/CustomSX1268.h


+ 0 - 0
src/helpers/CustomSX1268Wrapper.h → src/helpers/radiolib/CustomSX1268Wrapper.h


+ 0 - 0
src/helpers/CustomSX1276.h → src/helpers/radiolib/CustomSX1276.h


+ 0 - 0
src/helpers/CustomSX1276Wrapper.h → src/helpers/radiolib/CustomSX1276Wrapper.h


+ 3 - 3
src/helpers/RadioLibWrappers.cpp → src/helpers/radiolib/RadioLibWrappers.cpp

@@ -96,8 +96,9 @@ bool RadioLibWrapper::isInRecvMode() const {
 }
 
 int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
+  int len = 0;
   if (state & STATE_INT_READY) {
-    int len = _radio->getPacketLength();
+    len = _radio->getPacketLength();
     if (len > 0) {
       if (len > sz) { len = sz; }
       int err = _radio->readData(bytes, len);
@@ -110,7 +111,6 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
       }
     }
     state = STATE_IDLE;   // need another startReceive()
-    return len;
   }
 
   if (state != STATE_RX) {
@@ -121,7 +121,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
       MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
     }
   }
-  return 0;
+  return len;
 }
 
 uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {

+ 0 - 0
src/helpers/RadioLibWrappers.h → src/helpers/radiolib/RadioLibWrappers.h


+ 241 - 9
src/helpers/sensors/EnvironmentSensorManager.cpp

@@ -1,5 +1,11 @@
 #include "EnvironmentSensorManager.h"
 
+#if ENV_PIN_SDA && ENV_PIN_SCL
+#define TELEM_WIRE &Wire1  // Use Wire1 as the I2C bus for Environment Sensors
+#else
+#define TELEM_WIRE &Wire  // Use default I2C bus for Environment Sensors
+#endif
+
 #if ENV_INCLUDE_AHTX0
 #define TELEM_AHTX_ADDRESS      0x38      // AHT10, AHT20 temperature and humidity sensor I2C address
 #include <Adafruit_AHTX0.h>
@@ -29,6 +35,12 @@ static Adafruit_BMP280 BMP280;
 static Adafruit_SHTC3 SHTC3;
 #endif
 
+#if ENV_INCLUDE_SHT4X
+#define TELEM_SHT4X_ADDRESS 0x44  //0x44 - 0x46
+#include <SensirionI2cSht4x.h>
+static SensirionI2cSht4x SHT4X;
+#endif
+
 #if ENV_INCLUDE_LPS22HB
 #include <Arduino_LPS22HB.h>
 #endif
@@ -47,13 +59,49 @@ static Adafruit_INA3221 INA3221;
 static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
 #endif
 
+#if ENV_INCLUDE_INA260
+#define TELEM_INA260_ADDRESS    0x41      // INA260 single channel current sensor I2C address
+#include <Adafruit_INA260.h>
+static Adafruit_INA260 INA260;
+#endif
+
+#if ENV_INCLUDE_MLX90614
+#define TELEM_MLX90614_ADDRESS 0x5A      // MLX90614 IR temperature sensor I2C address
+#include <Adafruit_MLX90614.h>
+static Adafruit_MLX90614 MLX90614;
+#endif
+
+#if ENV_INCLUDE_VL53L0X
+#define TELEM_VL53L0X_ADDRESS 0x29      // VL53L0X time-of-flight distance sensor I2C address
+#include <Adafruit_VL53L0X.h>
+static Adafruit_VL53L0X VL53L0X;
+#endif
+
+#if ENV_INCLUDE_GPS && RAK_BOARD
+static uint32_t gpsResetPin = 0;
+static bool i2cGPSFlag = false;
+static bool serialGPSFlag = false;
+#define TELEM_RAK12500_ADDRESS   0x42     //RAK12500 Ublox GPS via i2c
+#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
+static SFE_UBLOX_GNSS ublox_GNSS;
+#endif
+
 bool EnvironmentSensorManager::begin() {
   #if ENV_INCLUDE_GPS
+  #if RAK_BOARD
+  rakGPSInit();   //probe base board/sockets for GPS
+  #else
   initBasicGPS();
   #endif
+  #endif
+
+  #if ENV_PIN_SDA && ENV_PIN_SCL
+  Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
+  MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
+  #endif
 
   #if ENV_INCLUDE_AHTX0
-  if (AHTX0.begin(&Wire, 0, TELEM_AHTX_ADDRESS)) {
+  if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
     MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
     AHTX0_initialized = true;
   } else {
@@ -63,7 +111,7 @@ bool EnvironmentSensorManager::begin() {
   #endif
 
   #if ENV_INCLUDE_BME280
-  if (BME280.begin(TELEM_BME280_ADDRESS, &Wire)) {
+  if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
     MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
     MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
     BME280_initialized = true;
@@ -94,6 +142,21 @@ bool EnvironmentSensorManager::begin() {
   }
   #endif
 
+
+  #if ENV_INCLUDE_SHT4X
+  SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
+  uint32_t serialNumber = 0;
+  int16_t sht4x_error;
+  sht4x_error = SHT4X.serialNumber(serialNumber);
+  if (sht4x_error == 0) {
+    MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
+    SHT4X_initialized = true;
+  } else {
+    SHT4X_initialized = false;
+    MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
+  }
+  #endif
+
   #if ENV_INCLUDE_LPS22HB
   if (BARO.begin()) {
     MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
@@ -105,7 +168,7 @@ bool EnvironmentSensorManager::begin() {
   #endif
 
   #if ENV_INCLUDE_INA3221
-  if (INA3221.begin(TELEM_INA3221_ADDRESS, &Wire)) {
+  if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
     MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
     MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
 
@@ -120,7 +183,7 @@ bool EnvironmentSensorManager::begin() {
   #endif
 
   #if ENV_INCLUDE_INA219
-  if (INA219.begin(&Wire)) {
+  if (INA219.begin(TELEM_WIRE)) {
     MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
     INA219_initialized = true;
   } else {
@@ -129,6 +192,36 @@ bool EnvironmentSensorManager::begin() {
   }
   #endif
 
+  #if ENV_INCLUDE_INA260
+  if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
+    MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
+    INA260_initialized = true;
+  } else {
+    INA260_initialized = false;
+    MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
+  }
+  #endif
+
+  #if ENV_INCLUDE_MLX90614
+  if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
+    MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
+    MLX90614_initialized = true;
+  } else {
+    MLX90614_initialized = false;
+    MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS);
+  }
+  #endif
+
+  #if ENV_INCLUDE_VL53L0X
+  if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
+    MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
+    VL53L0X_initialized = true;
+  } else {
+    VL53L0X_initialized = false;
+    MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS);
+  }
+  #endif
+
   return true;
 }
 
@@ -154,7 +247,7 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     if (BME280_initialized) {
       telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
       telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
-      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure());
+      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
       telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
     }
     #endif
@@ -162,8 +255,8 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     #if ENV_INCLUDE_BMP280
     if (BMP280_initialized) {
       telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
-      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure());
-      telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
+      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
+      telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
     }
     #endif
 
@@ -177,6 +270,18 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     }
     #endif
 
+    #if ENV_INCLUDE_SHT4X
+    if (SHT4X_initialized) {
+      float sht4x_humidity, sht4x_temperature;
+      int16_t sht4x_error;
+      sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
+      if (sht4x_error == 0) {
+        telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
+        telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
+      }
+    }
+    #endif
+
     #if ENV_INCLUDE_LPS22HB
     if (LPS22HB_initialized) {
       telemetry.addTemperature(TELEM_CHANNEL_SELF, BARO.readTemperature());
@@ -209,6 +314,34 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     }
     #endif
 
+    #if ENV_INCLUDE_INA260
+    if (INA260_initialized) {
+      telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
+      telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
+      telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
+      next_available_channel++;
+    }
+    #endif
+
+    #if ENV_INCLUDE_MLX90614
+    if (MLX90614_initialized) {
+      telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
+      telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
+    }
+    #endif
+
+    #if ENV_INCLUDE_VL53L0X
+    if (VL53L0X_initialized) {
+      VL53L0X_RangingMeasurementData_t measure;
+      VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
+      if (measure.RangeStatus != 4) { // phase failures
+        telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
+      } else {
+        telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
+      }
+    }
+    #endif
+
   }
 
   return true;
@@ -296,8 +429,85 @@ void EnvironmentSensorManager::initBasicGPS() {
   gps_active = false; //Set GPS visibility off until setting is changed
 }
 
+#ifdef RAK_BOARD
+void EnvironmentSensorManager::rakGPSInit(){
+
+  Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
+
+  #ifdef GPS_BAUD_RATE
+  Serial1.begin(GPS_BAUD_RATE);
+  #else
+  Serial1.begin(9600);
+  #endif
+
+  //search for the correct IO standby pin depending on socket used
+  if(gpsIsAwake(WB_IO2)){
+  //  MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
+  //  MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
+  }
+  else if(gpsIsAwake(WB_IO4)){
+  //  MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
+  //  MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
+  }
+  else if(gpsIsAwake(WB_IO5)){
+  //  MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
+  //  MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
+  }
+  else{
+    MESH_DEBUG_PRINTLN("No GPS found");
+    gps_active = false;
+    gps_detected = false;
+    return;
+  }
+
+  #ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented
+  //Now that GPS is found and set up, set to sleep for initial state
+  stop_gps();
+  #endif
+}
+
+bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
+
+  //set initial waking state
+  pinMode(ioPin,OUTPUT);
+  digitalWrite(ioPin,LOW);
+  delay(500);
+  digitalWrite(ioPin,HIGH);
+  delay(500);
+
+  //Try to init RAK12500 on I2C
+  if (ublox_GNSS.begin(Wire) == true){
+    MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i",ioPin);
+    ublox_GNSS.setI2COutput(COM_TYPE_NMEA);
+    ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
+    gpsResetPin = ioPin;
+    i2cGPSFlag = true;
+    gps_active = true;
+    gps_detected = true;
+    return true;
+  }
+  else if(Serial1){
+    MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
+    if(PIN_GPS_EN){
+      gpsResetPin = PIN_GPS_EN;
+    }
+    serialGPSFlag = true;
+    gps_active = true;
+    gps_detected = true;
+    return true;
+  }
+  MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
+  return false;
+}
+#endif
+
 void EnvironmentSensorManager::start_gps() {
   gps_active = true;
+  #ifdef RAK_BOARD
+    pinMode(gpsResetPin, OUTPUT);
+    digitalWrite(gpsResetPin, HIGH);
+    return;
+  #endif
   #ifdef PIN_GPS_EN
     pinMode(PIN_GPS_EN, OUTPUT);
     digitalWrite(PIN_GPS_EN, HIGH);
@@ -309,6 +519,11 @@ void EnvironmentSensorManager::start_gps() {
 
 void EnvironmentSensorManager::stop_gps() {
   gps_active = false;
+  #ifdef RAK_BOARD
+    pinMode(gpsResetPin, OUTPUT);
+    digitalWrite(gpsResetPin, LOW);
+    return;
+  #endif
   #ifdef PIN_GPS_EN
     pinMode(PIN_GPS_EN, OUTPUT);
     digitalWrite(PIN_GPS_EN, LOW);
@@ -324,12 +539,29 @@ void EnvironmentSensorManager::loop() {
   _location->loop();
 
   if (millis() > next_gps_update) {
-    if (gps_active && _location->isValid()) {
+    if(gps_active){
+    #ifndef RAK_BOARD
+    if (_location->isValid()) {
       node_lat = ((double)_location->getLatitude())/1000000.;
       node_lon = ((double)_location->getLongitude())/1000000.;
       MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
     }
+    #else
+    if(i2cGPSFlag){
+      node_lat = ((double)ublox_GNSS.getLatitude())/10000000.;
+      node_lon = ((double)ublox_GNSS.getLongitude())/10000000.;
+      MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
+    }
+    else if (serialGPSFlag && _location->isValid()) {
+      node_lat = ((double)_location->getLatitude())/1000000.;
+      node_lon = ((double)_location->getLongitude())/1000000.;
+      MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
+    }
+    //else
+    //MESH_DEBUG_PRINTLN("No valid GPS data");
+    #endif
+    }
     next_gps_update = millis() + 1000;
   }
 }
-#endif
+#endif

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

@@ -13,8 +13,12 @@ protected:
   bool BMP280_initialized = false;
   bool INA3221_initialized = false;
   bool INA219_initialized = false;
+  bool INA260_initialized = false;
   bool SHTC3_initialized = false;
   bool LPS22HB_initialized = false;
+  bool MLX90614_initialized = false;
+  bool VL53L0X_initialized = false;
+  bool SHT4X_initialized = false;
 
   bool gps_detected = false;
   bool gps_active = false;
@@ -24,6 +28,10 @@ protected:
   void start_gps();
   void stop_gps();
   void initBasicGPS();
+  #ifdef RAK_BOARD
+  void rakGPSInit();
+  bool gpsIsAwake(uint8_t ioPin);
+  #endif
   #endif
 
 

+ 7 - 0
src/helpers/ui/buzzer.cpp

@@ -46,6 +46,13 @@ void genericBuzzer::shutdown() {
 
 void genericBuzzer::quiet(bool buzzer_state) {
     _is_quiet = buzzer_state;
+#ifdef PIN_BUZZER_EN
+    if (_is_quiet) {
+      digitalWrite(PIN_BUZZER_EN, LOW);
+    } else {
+      digitalWrite(PIN_BUZZER_EN, HIGH);
+    }
+#endif
 }
 
 bool genericBuzzer::isQuiet() {

+ 3 - 3
variants/generic-e22/target.h

@@ -2,10 +2,10 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/ESP32Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
-#include <helpers/CustomSX1268Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1268Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 

+ 2 - 35
variants/heltec_ct62/target.cpp

@@ -10,43 +10,10 @@ ESP32RTCClock fallback_clock;
 AutoDiscoverRTCClock rtc_clock(fallback_clock);
 SensorManager sensors;
 
-#ifndef LORA_CR
-  #define LORA_CR      5
-#endif
-
 bool radio_init() {
   fallback_clock.begin();
-  rtc_clock.begin(Wire);
-  
-#ifdef SX126X_DIO3_TCXO_VOLTAGE
-  float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
-#else
-  float tcxo = 1.6f;
-#endif
-
-#if defined(P_LORA_SCLK)
-  SPI.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
-#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
+  rtc_clock.begin(Wire);  
+  return radio.std_init(&SPI);
 }
 
 uint32_t radio_get_rng_seed() {

+ 2 - 2
variants/heltec_ct62/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include "HT-CT62Board.h"
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 

+ 2 - 2
variants/heltec_tracker/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/HeltecV3Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/LocationProvider.h>

+ 2 - 2
variants/heltec_v2/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/HeltecV2Board.h>
-#include <helpers/CustomSX1276Wrapper.h>
+#include <helpers/radiolib/CustomSX1276Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #ifdef DISPLAY_CLASS

+ 40 - 0
variants/heltec_v3/platformio.ini

@@ -169,6 +169,30 @@ lib_deps =
   ${Heltec_lora32_v3.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
+[env:Heltec_v3_sensor]
+extends = Heltec_lora32_v3
+build_flags =
+  ${Heltec_lora32_v3.build_flags}
+  -D ADVERT_NAME='"Heltec v3 Sensor"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ENV_PIN_SDA=33
+  -D ENV_PIN_SCL=34
+  -D ENV_INCLUDE_MLX90614=1
+  -D ENV_INCLUDE_VL53L0X=1
+  -D DISPLAY_CLASS=SSD1306Display
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_sensor>
+lib_deps =
+  ${Heltec_lora32_v3.lib_deps}
+  ${esp32_ota.lib_deps}
+  adafruit/Adafruit MLX90614 Library @ ^2.1.5
+  adafruit/Adafruit_VL53L0X @ ^1.2.4
+
 [env:Heltec_WSL3_repeater]
 extends = Heltec_lora32_v3
 build_flags =
@@ -220,3 +244,19 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
 lib_deps =
   ${Heltec_lora32_v3.lib_deps}
   densaugeo/base64 @ ~1.4.0
+
+[env:Heltec_WSL3_sensor]
+extends = Heltec_lora32_v3
+build_flags =
+  ${Heltec_lora32_v3.build_flags}
+  -D ADVERT_NAME='"Heltec Sensor"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+  +<../examples/simple_sensor>
+lib_deps =
+  ${Heltec_lora32_v3.lib_deps}
+  ${esp32_ota.lib_deps}

+ 2 - 2
variants/heltec_v3/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/HeltecV3Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>

+ 11 - 4
variants/heltec_wireless_paper/target.cpp

@@ -1,11 +1,14 @@
 #include "target.h"
-
 #include <Arduino.h>
 
 HeltecV3Board board;
 
-static SPIClass spi;
-RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
+#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);
 
@@ -21,7 +24,11 @@ DISPLAY_CLASS display;
 bool radio_init() {
   fallback_clock.begin();
   rtc_clock.begin(Wire);
+#if defined(P_LORA_SCLK)
   return radio.std_init(&spi);
+#else
+  return radio.std_init();
+#endif
 }
 
 uint32_t radio_get_rng_seed() {
@@ -42,4 +49,4 @@ void radio_set_tx_power(uint8_t dbm) {
 mesh::LocalIdentity radio_new_identity() {
   RadioNoiseListener rng(radio);
   return mesh::LocalIdentity(&rng); // create new random identity
-}
+}

+ 2 - 2
variants/heltec_wireless_paper/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/HeltecV3Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #ifdef DISPLAY_CLASS

+ 2 - 2
variants/lilygo_t3s3/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/ESP32Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #ifdef DISPLAY_CLASS

+ 2 - 2
variants/lilygo_t3s3_sx1276/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/ESP32Board.h>
-#include <helpers/CustomSX1276Wrapper.h>
+#include <helpers/radiolib/CustomSX1276Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #ifdef DISPLAY_CLASS

+ 18 - 1
variants/lilygo_tbeam_SX1262/platformio.ini

@@ -69,4 +69,21 @@ build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter}
   +<../examples/simple_repeater>
 lib_deps =
   ${LilyGo_TBeam_SX1262.lib_deps}
-  ${esp32_ota.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:Tbeam_SX1262_room_server]
+extends = LilyGo_TBeam_SX1262
+build_flags =
+  ${LilyGo_TBeam_SX1262.build_flags}
+  -D ADVERT_NAME='"Tbeam SX1262 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 = ${LilyGo_TBeam_SX1262.build_src_filter}
+  +<../examples/simple_room_server>
+lib_deps =
+  ${LilyGo_TBeam_SX1262.lib_deps}
+  ${esp32_ota.lib_deps}

+ 2 - 2
variants/lilygo_tbeam_SX1262/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/esp32/TBeamBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>
 #ifdef DISPLAY_CLASS

+ 2 - 2
variants/lilygo_tbeam_SX1276/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 //#include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/esp32/TBeamBoard.h>
-#include <helpers/CustomSX1276Wrapper.h>
+#include <helpers/radiolib/CustomSX1276Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>
 #ifdef DISPLAY_CLASS

+ 2 - 2
variants/lilygo_tbeam_supreme_SX1262/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/esp32/TBeamBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>
 

+ 2 - 2
variants/lilygo_tlora_c6/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/ESP32Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 

+ 2 - 2
variants/lilygo_tlora_v2_1/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/LilyGoTLoraBoard.h>
-#include <helpers/CustomSX1276Wrapper.h>
+#include <helpers/radiolib/CustomSX1276Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/EnvironmentSensorManager.h>

+ 5 - 38
variants/meshadventurer/target.cpp

@@ -5,13 +5,8 @@
 
 MeshadventurerBoard 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
-
+static SPIClass spi;
+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);
 
 ESP32RTCClock fallback_clock;
@@ -30,40 +25,12 @@ MASensorManager sensors = MASensorManager(nmea);
 bool radio_init() {
   fallback_clock.begin();
   rtc_clock.begin(Wire);
-  
-#ifdef SX126X_DIO3_TCXO_VOLTAGE
-  float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
-#else
-  float tcxo = 1.6f;
-#endif
 
 #if defined(P_LORA_SCLK)
-  spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
-#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);
-  
-#if defined(SX126X_RXEN) && defined(SX126X_TXEN)
-  radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
-#endif
-
-#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);
+  return radio.std_init(&spi);
+#else
+  return radio.std_init();
 #endif
-
-  return true;  // success
 }
 
 uint32_t radio_get_rng_seed() {

+ 3 - 3
variants/meshadventurer/target.h

@@ -2,10 +2,10 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/MeshadventurerBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
-#include <helpers/CustomSX1268Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1268Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/LocationProvider.h>

+ 91 - 0
variants/minewsemi_me25ls01/MinewsemiME25LS01Board.cpp

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

+ 88 - 0
variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h

@@ -0,0 +1,88 @@
+#pragma once
+
+#include <MeshCore.h>
+#include <Arduino.h>
+
+// LoRa and SPI pins
+
+#define  P_LORA_DIO_1   (32 + 12)  // P1.12
+#define  P_LORA_NSS     (32 + 13)  // P1.13
+#define  P_LORA_RESET   (32 + 11)  // P1.11
+#define  P_LORA_BUSY    (32 + 10)  // P1.10
+#define  P_LORA_SCLK    (32 + 15)  // P1.15
+#define  P_LORA_MISO    (0 + 29)   // P0.29
+#define  P_LORA_MOSI    (0 + 2)    // P0.2
+ 
+#define LR11X0_DIO_AS_RF_SWITCH  true
+#define LR11X0_DIO3_TCXO_VOLTAGE   1.6
+
+#define  PIN_VBAT_READ BATTERY_PIN
+#define  ADC_MULTIPLIER   (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking
+
+
+class MinewsemiME25LS01Board : public mesh::MainBoard {
+protected:
+  uint8_t startup_reason;
+  uint8_t btn_prev_state;
+
+public:
+  void begin();
+
+#define BATTERY_SAMPLES 8
+
+  uint16_t getBattMilliVolts() override {
+    analogReadResolution(12);
+
+    uint32_t raw = 0;
+    for (int i = 0; i < BATTERY_SAMPLES; i++) {
+      raw += analogRead(PIN_VBAT_READ);
+    }
+    raw = raw / BATTERY_SAMPLES;
+    return (ADC_MULTIPLIER * raw);
+  }
+
+  uint8_t getStartupReason() const override { return startup_reason; }
+
+  const char* getManufacturerName() const override {
+    return "Minewsemi";
+  }
+
+  void powerOff() override {
+    #ifdef HAS_GPS
+        digitalWrite(GPS_VRTC_EN, LOW);
+        digitalWrite(GPS_RESET, LOW);
+        digitalWrite(GPS_SLEEP_INT, LOW);
+        digitalWrite(GPS_RTC_INT, LOW);
+        pinMode(GPS_RESETB, OUTPUT);
+        digitalWrite(GPS_RESETB, LOW);
+    #endif
+    
+    #ifdef BUZZER_EN
+        digitalWrite(BUZZER_EN, LOW);
+    #endif
+    
+    #ifdef LED_PIN
+    digitalWrite(LED_PIN, LOW);
+    #endif
+    #ifdef BUTTON_PIN
+    nrf_gpio_cfg_sense_input(digitalPinToInterrupt(BUTTON_PIN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_HIGH);
+    #endif
+    sd_power_system_off();
+  }
+
+  #if defined(P_LORA_TX_LED)
+  void onBeforeTransmit() override {
+    digitalWrite(P_LORA_TX_LED, HIGH);// turn TX LED on
+  }
+  void onAfterTransmit() override {
+    digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off
+  }
+#endif
+
+
+  void reboot() override {
+    NVIC_SystemReset();
+  }
+
+  bool startOTAUpdate(const char* id, char reply[]) override;
+};

+ 24 - 0
variants/minewsemi_me25ls01/NullDisplayDriver.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include <helpers/ui/DisplayDriver.h>
+
+class NullDisplayDriver : public DisplayDriver {
+public:
+  NullDisplayDriver() : DisplayDriver(128, 64) { }
+  bool begin() { return false; }   // not present
+
+  bool isOn() override { return false; }
+  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 { return 0; }
+  void endFrame() { }
+};

+ 164 - 0
variants/minewsemi_me25ls01/platformio.ini

@@ -0,0 +1,164 @@
+; ----------------- NRF52 me25ls01---------------------
+[nrf52840_me25ls01]
+extends = nrf52_base
+platform_packages = framework-arduinoadafruitnrf52
+build_flags = ${nrf52_base.build_flags}
+  -I src/helpers/nrf52
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
+lib_ignore =
+  BluetoothOTA
+  lib5b4
+lib_deps =
+  ${nrf52_base.lib_deps}
+  rweather/Crypto @ ^0.4.0
+
+[me25ls01]
+extends = nrf52840_me25ls01
+board = minewsemi_me25ls01
+board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52840_me25ls01.build_flags}
+  -I variants/minewsemi_me25ls01
+  -D me25ls01
+  -D PIN_USER_BTN=27
+  -D USER_BTN_PRESSED=HIGH
+  -D PIN_STATUS_LED=39
+  -D P_LORA_TX_LED=22
+  -D RADIO_CLASS=CustomLR1110
+  -D WRAPPER_CLASS=CustomLR1110Wrapper
+  -D LORA_TX_POWER=22
+  -D ENV_INCLUDE_GPS=0
+  -D ENV_INCLUDE_AHTX0=1
+  -D ENV_INCLUDE_BME280=1
+  -D ENV_INCLUDE_INA3221=1
+  -D ENV_INCLUDE_INA219=1
+build_src_filter = ${nrf52840_me25ls01.build_src_filter}
+  +<helpers/*.cpp>
+  +<../variants/minewsemi_me25ls01>
+  +<helpers/sensors>
+debug_tool = jlink
+upload_protocol = nrfutil
+lib_deps = ${nrf52840_me25ls01.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+  stevemarple/MicroNMEA @ ^2.0.6
+  end2endzone/NonBlockingRTTTL@^1.3.0
+  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  
+
+[env:Minewsemi_me25ls01_companion_radio_ble]
+extends = me25ls01
+build_flags = ${me25ls01.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+  -D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+  -D MESH_PACKET_LOGGING=1
+  -D MESH_DEBUG=1
+  -D OFFLINE_QUEUE_SIZE=256
+  -D RX_BOOSTED_GAIN=true
+  -D RF_SWITCH_TABLE
+  -D DISPLAY_CLASS=NullDisplayDriver
+  ;-D PIN_BUZZER=25
+  ;-D PIN_BUZZER_EN=37      
+build_src_filter = ${me25ls01.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/*.cpp>
+lib_deps = ${me25ls01.lib_deps}
+  adafruit/RTClib @ ^2.1.3
+ 
+
+[env:Minewsemi_me25ls01_repeater]
+extends = me25ls01
+build_flags = ${me25ls01.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+  -D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+  -D MESH_PACKET_LOGGING=1
+  -D MESH_DEBUG=1
+  -D OFFLINE_QUEUE_SIZE=256
+  -D RX_BOOSTED_GAIN=true
+  -D RF_SWITCH_TABLE
+  -D ADVERT_NAME='"ME25LS01 Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D MAX_NEIGHBOURS=8
+  -D DISPLAY_CLASS=NullDisplayDriver
+build_src_filter = ${me25ls01.build_src_filter}
+  +<../examples/simple_repeater>
+lib_deps = ${me25ls01.lib_deps}
+  adafruit/RTClib @ ^2.1.3
+
+
+
+[env:Minewsemi_me25ls01_room_server]
+extends = me25ls01
+build_flags = ${me25ls01.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+;  -D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+  -D OFFLINE_QUEUE_SIZE=256
+  -D RX_BOOSTED_GAIN=true
+  -D RF_SWITCH_TABLE
+  -D ADVERT_NAME='"ME25LS01 Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+  -D MAX_NEIGHBOURS=8
+  -D DISPLAY_CLASS=NullDisplayDriver
+build_src_filter = ${me25ls01.build_src_filter}
+  +<../examples/simple_room_server>
+lib_deps = ${me25ls01.lib_deps}
+  adafruit/RTClib @ ^2.1.3
+
+[env:Minewsemi_me25ls01_terminal_chat]
+extends = me25ls01
+build_flags = ${me25ls01.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+  -D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+  -D MESH_PACKET_LOGGING=1
+  -D MESH_DEBUG=1
+  -D OFFLINE_QUEUE_SIZE=256
+  -D RX_BOOSTED_GAIN=true
+  -D RF_SWITCH_TABLE
+  -D ADVERT_NAME='"ME25LS01 Chat"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+  -D MAX_NEIGHBOURS=8
+  -D DISPLAY_CLASS=NullDisplayDriver
+build_src_filter = ${me25ls01.build_src_filter}
+  +<../examples/simple_secure_chat/main.cpp>
+lib_deps = ${me25ls01.lib_deps}
+  adafruit/RTClib @ ^2.1.3
+
+[env:Minewsemi_me25ls01_companion_radio_usb]
+extends = me25ls01
+build_flags = ${me25ls01.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+  ;-D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+  -D MESH_PACKET_LOGGING=1
+  -D MESH_DEBUG=1
+  -D OFFLINE_QUEUE_SIZE=256
+  -D RX_BOOSTED_GAIN=true
+  -D RF_SWITCH_TABLE
+  -D DISPLAY_CLASS=NullDisplayDriver
+build_src_filter = ${me25ls01.build_src_filter}
+  +<helpers/nrf52/*.cpp>
+  +<../examples/companion_radio>
+lib_deps = ${me25ls01.lib_deps}
+  adafruit/RTClib @ ^2.1.3
+

+ 98 - 0
variants/minewsemi_me25ls01/target.cpp

@@ -0,0 +1,98 @@
+#include <Arduino.h>
+#include "target.h"
+
+MinewsemiME25LS01Board 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;
+extern EnvironmentSensorManager sensors;
+#if ENV_INCLUDE_GPS
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+  MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
+  EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
+#else
+  EnvironmentSensorManager sensors;
+#endif
+
+#ifdef DISPLAY_CLASS
+  NullDisplayDriver display;
+#endif
+
+#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() {
+  //rtc_clock.begin(Wire);
+  
+#ifdef LR11X0_DIO3_TCXO_VOLTAGE
+  float tcxo = LR11X0_DIO3_TCXO_VOLTAGE;
+#else
+  float tcxo = 1.6f;
+#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(1);
+
+#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
+}

+ 29 - 0
variants/minewsemi_me25ls01/target.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#define RADIOLIB_STATIC_ONLY 1
+#include <RadioLib.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <MinewsemiME25LS01Board.h>
+#include <helpers/radiolib/CustomLR1110Wrapper.h>
+#include <helpers/ArduinoHelpers.h>
+#include <helpers/SensorManager.h>
+#include <helpers/sensors/LocationProvider.h>
+#include <helpers/sensors/EnvironmentSensorManager.h>
+#ifdef DISPLAY_CLASS
+  #include "NullDisplayDriver.h"
+#endif
+
+#ifdef DISPLAY_CLASS
+  extern NullDisplayDriver display;
+#endif
+
+extern MinewsemiME25LS01Board 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();

+ 70 - 0
variants/minewsemi_me25ls01/variant.cpp

@@ -0,0 +1,70 @@
+#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
+  3,  // P0.03
+  4,  // P0.04
+  5,  // P0.05
+  6,  // P0.06
+  7,  // P0.07
+  8,  // P0.08
+  9,  // P0.09
+  10, // P0.10
+  11, // P0.11
+  12, // P0.12
+  13, // P0.13, PIN_SERIAL1_TX
+  14, // P0.14, PIN_SERIAL1_RX
+  15, // P0.15, PIN_SERIAL2_RX
+  16, // P0.16, PIN_WIRE_SCL
+  17, // P0.17, PIN_SERIAL2_TX
+  18, // P0.18
+  19, // P0.19
+  20, // P0.20
+  21, // P0.21, PIN_WIRE_SDA
+  22, // P0.22
+  23, // P0.23
+  24, // P0.24, 
+  25, // P0.25, 
+  26, // P0.26, 
+  27, // P0.27, 
+  28, // P0.28
+  29, // P0.29, 
+  30, // P0.30
+  31, // P0.31, BATTERY_PIN
+  32, // P1.00
+  33, // P1.01, LORA_DIO_1
+  34, // P1.02
+  35, // P1.03, 
+  36, // P1.04
+  37, // P1.05, LR1110_EN
+  38, // P1.06, 
+  39, // P1.07, 
+  40, // P1.08, PIN_SPI_MISO
+  41, // P1.09, PIN_SPI_MOSI
+  42, // P1.10, LORA_RESET
+  43, // P1.11, GPS_EN
+  44, // P1.12, GPS_SLEEP_INT
+  45, // P1.13
+  46, // P1.14, GPS_RESETB
+  47, // P1.15, PIN_GPS_RESET
+  255,  // NRFX_SPIM_PIN_NOT_USED
+};
+
+void initVariant()
+{
+  pinMode(BATTERY_PIN, INPUT);
+  pinMode(PIN_BUTTON1, INPUT);
+
+  // pinMode(PIN_3V3_EN, OUTPUT);
+  // pinMode(PIN_3V3_ACC_EN, OUTPUT);
+  pinMode(LED_PIN, OUTPUT);
+  pinMode(P_LORA_TX_LED, OUTPUT);
+
+  digitalWrite(LED_PIN, HIGH);
+  digitalWrite(P_LORA_TX_LED, LOW);
+}

+ 94 - 0
variants/minewsemi_me25ls01/variant.h

@@ -0,0 +1,94 @@
+#pragma once
+
+#include "WVariant.h"
+
+// Low frequency clock source
+#define USE_LFXO    // 32.768 kHz crystal oscillator
+#define VARIANT_MCK (64000000ul)
+// #define USE_LFRC    // 32.768 kHz RC oscillator
+
+// Power
+#define BATTERY_PIN             (31)
+#define BATTERY_IMMUTABLE
+#define ADC_MULTIPLIER          (2.0F)
+
+#define ADC_RESOLUTION          (14)
+#define BATTERY_SENSE_RES       (12)
+
+// Number of pins
+#define PINS_COUNT              (48)
+#define NUM_DIGITAL_PINS        (48)
+#define NUM_ANALOG_INPUTS       (6)
+#define NUM_ANALOG_OUTPUTS      (0)
+
+// UART pin definition
+#define PIN_SERIAL1_RX          (14)             // P0.14
+#define PIN_SERIAL1_TX          (13)             // P0.13
+
+#define PIN_SERIAL2_RX          (15)             // P0.15
+#define PIN_SERIAL2_TX          (17)             // P0.17
+
+// I2C pin definition
+#define HAS_WIRE                (1)     
+#define WIRE_INTERFACES_COUNT   (1)     
+
+#define PIN_WIRE_SDA            (21)             // P0.21
+#define PIN_WIRE_SCL            (16)             // P0.16
+#define I2C_NO_RESCAN
+
+// SPI pin definition
+#define SPI_INTERFACES_COUNT    (1)
+
+#define PIN_SPI_MISO            (0 + 29)            // P0.29
+#define PIN_SPI_MOSI            (0 + 2)             // P0.2
+#define PIN_SPI_SCK             (32 + 15)           // P1.15
+#define PIN_SPI_NSS             (32 + 13)           // P1.13
+
+// Builtin LEDs
+#define LED_BUILTIN             (-1)
+#define LED_RED                (32 + 5)            // P1.5
+#define LED_BLUE               (32 + 7)            // P1.7 
+#define LED_PIN                 LED_BLUE
+#define P_LORA_TX_LED           LED_RED
+
+#define LED_STATE_ON            HIGH
+
+// Builtin buttons
+
+#define PIN_BUTTON1             (0 + 27)             // P0.6
+#define BUTTON_PIN              PIN_BUTTON1
+
+// LR1110
+#define LORA_DIO_1              (32 + 12)           // P1.12
+#define LORA_DIO_2              (32 + 10)           // P1.10 
+#define LORA_NSS                (PIN_SPI_NSS)       // P1.13
+#define LORA_RESET              (32 + 11)           // P1.11 
+#define LORA_BUSY               (32 + 10)           // P1.10
+#define LORA_SCLK               (PIN_SPI_SCK)       // P1.15
+#define LORA_MISO               (PIN_SPI_MISO)      // P0.29
+#define LORA_MOSI               (PIN_SPI_MOSI)      // P0.2
+#define LORA_CS                 PIN_SPI_NSS         // P1.13
+ 
+#define LR11X0_DIO_AS_RF_SWITCH    true
+#define LR11X0_DIO3_TCXO_VOLTAGE   1.6
+
+#define LR1110_IRQ_PIN LORA_DIO_1
+#define LR1110_NRESET_PIN LORA_RESET
+#define LR1110_BUSY_PIN LORA_DIO_2
+#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
+
+// GPS
+#define HAS_GPS                 0
+#define GPS_RX_PIN              PIN_SERIAL1_RX
+#define GPS_TX_PIN              PIN_SERIAL1_TX
+
+#define GPS_EN                  (-1)            // P1.11
+#define GPS_RESET               (-1)            // P1.15
+
+#define GPS_VRTC_EN             (-1)             // P0.8
+#define GPS_SLEEP_INT           (-1)            // P1.12
+#define GPS_RTC_INT             (-1)            // P0.15
+#define GPS_RESETB              (-1)            // P1.14

+ 3 - 4
variants/nano_g2_ultra/target.h

@@ -3,8 +3,8 @@
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
 #include "nano-g2.h"
-#include <helpers/RadioLibWrappers.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #ifdef DISPLAY_CLASS
@@ -12,8 +12,7 @@
 #endif
 #include <helpers/sensors/LocationProvider.h>
 
-class NanoG2UltraSensorManager : public SensorManager
-{
+class NanoG2UltraSensorManager : public SensorManager {
   bool gps_active = false;
   LocationProvider *_location;
 

+ 2 - 2
variants/picow/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/rp2040/PicoWBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 

+ 0 - 0
src/helpers/nrf52/PromicroBoard.cpp → variants/promicro/PromicroBoard.cpp


+ 0 - 0
src/helpers/nrf52/PromicroBoard.h → variants/promicro/PromicroBoard.h


+ 13 - 88
variants/promicro/platformio.ini

@@ -23,7 +23,6 @@ build_flags = ${nrf52_base.build_flags}
   -D ENV_INCLUDE_INA3221=1
   -D ENV_INCLUDE_INA219=1
 build_src_filter = ${nrf52_base.build_src_filter}
-  +<helpers/nrf52/PromicroBoard.cpp>
   +<helpers/sensors>
   +<../variants/promicro>
 lib_deps= ${nrf52_base.lib_deps}
@@ -117,93 +116,19 @@ lib_deps = ${Faketec.lib_deps}
   adafruit/RTClib @ ^2.1.3
   densaugeo/base64 @ ~1.4.0
 
-[ProMicroLLCC68]
-extends = nrf52_base
-board = promicro_nrf52840
-build_flags = ${nrf52_base.build_flags}
-  -I variants/promicro
-  -D PROMICROLLCC68
-  -D RADIO_CLASS=CustomLLCC68
-  -D WRAPPER_CLASS=CustomLLCC68Wrapper
-  -D LORA_TX_POWER=22
-  -D SX126X_CURRENT_LIMIT=140
-  -D SX126X_RX_BOOSTED_GAIN=1
-build_src_filter =
-  ${nrf52_base.build_src_filter}
-  +<helpers/nrf52/PromicroBoard.cpp>
-  +<helpers/sensors>
-  +<../variants/promicro>
-lib_deps= ${nrf52_base.lib_deps}
-  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
-
-[env:ProMicroLLCC68_Repeater]
-extends = ProMicroLLCC68
-build_src_filter = ${ProMicroLLCC68.build_src_filter}
-  +<../examples/simple_repeater/main.cpp>
-build_flags = ${ProMicroLLCC68.build_flags}
-  -D ADVERT_NAME='"ProMicroLLCC68 Repeater"'
-  -D ADMIN_PASSWORD='"password"'
-  -D MAX_NEIGHBOURS=8
-;  -D MESH_PACKET_LOGGING=1
-;  -D MESH_DEBUG=1
-lib_deps = ${ProMicroLLCC68.lib_deps}
-  adafruit/RTClib @ ^2.1.3
-
-[env:ProMicroLLCC68_room_server]
-extends = ProMicroLLCC68
-build_src_filter = ${ProMicroLLCC68.build_src_filter}
-  +<../examples/simple_room_server/main.cpp>
-build_flags = ${ProMicroLLCC68.build_flags}
-  -D ADVERT_NAME='"ProMicroLLCC68 Room"'
+[env:Faketec_sensor]
+extends = Faketec
+build_flags =
+  ${Faketec.build_flags}
+  -D ADVERT_NAME='"Faketec Sensor"'
+  -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 = ${ProMicroLLCC68.lib_deps}
-  adafruit/RTClib @ ^2.1.3
-
-[env:ProMicroLLCC68_terminal_chat]
-extends = ProMicroLLCC68
-build_flags = ${ProMicroLLCC68.build_flags}
-  -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=1
-;  -D MESH_PACKET_LOGGING=1
-;  -D MESH_DEBUG=1
-build_src_filter = ${ProMicroLLCC68.build_src_filter}
-  +<../examples/simple_secure_chat/main.cpp>
-lib_deps = ${ProMicroLLCC68.lib_deps}
-  densaugeo/base64 @ ~1.4.0
-  adafruit/RTClib @ ^2.1.3
-
-[env:ProMicroLLCC68_companion_radio_usb]
-extends = ProMicroLLCC68
-build_flags = ${ProMicroLLCC68.build_flags}
-  -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=8
-; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
-; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
-build_src_filter = ${ProMicroLLCC68.build_src_filter}
-  +<../examples/companion_radio>
-lib_deps = ${ProMicroLLCC68.lib_deps}
-  adafruit/RTClib @ ^2.1.3
-  densaugeo/base64 @ ~1.4.0
-
-[env:ProMicroLLCC68_companion_radio_ble]
-extends = ProMicroLLCC68
-build_flags = ${ProMicroLLCC68.build_flags}
-  -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=8
-  -D BLE_PIN_CODE=123456
-  -D BLE_DEBUG_LOGGING=1
-  -D OFFLINE_QUEUE_SIZE=256
+  -D DISPLAY_CLASS=SSD1306Display
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
-build_src_filter = ${ProMicroLLCC68.build_src_filter}
-  +<helpers/nrf52/SerialBLEInterface.cpp>
-  +<../examples/companion_radio>
-lib_deps = ${ProMicroLLCC68.lib_deps}
-  adafruit/RTClib @ ^2.1.3
-  densaugeo/base64 @ ~1.4.0
+build_src_filter = ${Faketec.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<../examples/simple_sensor>
+lib_deps =
+  ${Faketec.lib_deps}

+ 3 - 5
variants/promicro/target.h

@@ -2,10 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
-#include <helpers/nrf52/PromicroBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
-#include <helpers/CustomLLCC68Wrapper.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <PromicroBoard.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #ifdef DISPLAY_CLASS
   #include <helpers/ui/SSD1306Display.h>
@@ -27,4 +26,3 @@ 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();
-

+ 8 - 0
variants/rak3x72/platformio.ini

@@ -21,6 +21,14 @@ build_flags = ${rak3x72.build_flags}
 build_src_filter = ${rak3x72.build_src_filter}
   +<../examples/simple_repeater/main.cpp>
 
+[env:rak3x72-sensor]
+extends = rak3x72
+build_flags = ${rak3x72.build_flags}
+  -D ADVERT_NAME='"RAK3x72 Sensor"'
+  -D ADMIN_PASSWORD='"password"'
+build_src_filter = ${rak3x72.build_src_filter}
+  +<../examples/simple_sensor>
+
 [env:rak3x72_companion_radio_usb]
 extends = rak3x72
 build_flags = ${rak3x72.build_flags}

+ 1 - 1
variants/rak3x72/target.cpp

@@ -38,7 +38,7 @@ bool radio_init() {
 
   radio.setRfSwitchTable(rfswitch_pins, rfswitch_table);
 
-  int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, STM32WL_TCXO_VOLTAGE, 0); 
+  int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, STM32WL_TCXO_VOLTAGE, 0); 
 
   if (status != RADIOLIB_ERR_NONE) {
     Serial.print("ERROR: radio init failed: ");

+ 3 - 3
variants/rak3x72/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/stm32/STM32Board.h>
-#include <helpers/CustomSTM32WLxWrapper.h>
+#include <helpers/radiolib/CustomSTM32WLxWrapper.h>
 #include <helpers/ArduinoHelpers.h>
 #include <helpers/SensorManager.h>
 
@@ -22,7 +22,7 @@ public:
         uint32_t raw = 0;
         for (int i=0; i<8;i++) {
             raw += analogRead(PIN_VBAT_READ);
-        }            
+        }
         return ((double)raw) * ADC_MULTIPLIER / 8 / 4096;
     }
 };

+ 0 - 0
src/helpers/nrf52/RAK4631Board.cpp → variants/rak4631/RAK4631Board.cpp


+ 5 - 8
src/helpers/nrf52/RAK4631Board.h → variants/rak4631/RAK4631Board.h

@@ -13,14 +13,11 @@
 #define  P_LORA_MOSI    44
 #define  SX126X_POWER_EN  37
 
-#define P_GPS_SDA       13  //GPS SDA pin (output option)
-#define P_GPS_SCL       14  //GPS SCL pin (output option)
-#define P_GPS_TX        16  //GPS TX pin
-#define P_GPS_RX        15  //GPS RX pin
-#define P_GPS_STANDBY_A 34  //GPS Reset/Standby pin (IO2 for socket A)
-#define P_GPS_STANDBY_C 4   //GPS Reset/Standby pin (IO4 for socket C)
-#define P_GPS_STANDBY_F 9   //GPS Reset/Standby pin (IO5 for socket F)
-#define P_GPS_1PPS      17  //GPS PPS pin
+//#define PIN_GPS_SDA       13  //GPS SDA pin (output option)
+//#define PIN_GPS_SCL       14  //GPS SCL pin (output option)
+//#define PIN_GPS_TX        16  //GPS TX pin
+//#define PIN_GPS_RX        15  //GPS RX pin
+#define PIN_GPS_1PPS      17  //GPS PPS pin
 #define GPS_BAUD_RATE   9600
 #define GPS_ADDRESS   0x42  //i2c address for GPS
  

+ 29 - 56
variants/rak4631/platformio.ini

@@ -1,26 +1,50 @@
 [rak4631]
-extends = nrf52840_base
+extends = nrf52_base
 platform = https://github.com/maxgerhardt/platform-nordicnrf52.git#rak
 board = wiscore_rak4631
 board_check = true
-build_flags = ${nrf52840_base.build_flags}
+build_flags = ${nrf52_base.build_flags}
   -I variants/rak4631
   -D RAK_4631
+  -D RAK_BOARD
   -D PIN_BOARD_SCL=14
   -D PIN_BOARD_SDA=13
+  -D PIN_GPS_TX=16
+  -D PIN_GPS_RX=15
+  -D PIN_GPS_EN=-1
   -D PIN_OLED_RESET=-1
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
   -D LORA_TX_POWER=22
   -D SX126X_CURRENT_LIMIT=140
   -D SX126X_RX_BOOSTED_GAIN=1
-build_src_filter = ${nrf52840_base.build_src_filter}
-  +<helpers/nrf52/RAK4631Board.cpp>
+  -D ENV_INCLUDE_GPS=1
+  -D ENV_INCLUDE_AHTX0=1
+  -D ENV_INCLUDE_BME280=1
+  -D ENV_INCLUDE_BMP280=1
+  -D ENV_INCLUDE_SHTC3=1
+  -D ENV_INCLUDE_LPS22HB=1
+  -D ENV_INCLUDE_INA3221=1
+  -D ENV_INCLUDE_INA219=1
+  -D ENV_INCLUDE_INA260=1
+  -D ENV_INCLUDE_SHT4X=1
+build_src_filter = ${nrf52_base.build_src_filter}
   +<../variants/rak4631>
+  +<helpers/sensors>
 lib_deps =
-  ${nrf52840_base.lib_deps}
+  ${nrf52_base.lib_deps}
   adafruit/Adafruit SSD1306 @ ^2.5.13
   stevemarple/MicroNMEA @ ^2.0.6
+  arduino-libraries/Arduino_LPS22HB@^1.0.2
+  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
+  adafruit/Adafruit SHTC3 Library @ ^1.0.1
+  adafruit/Adafruit INA260 Library @ ^1.5.3
+  sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27
+  sensirion/Sensirion I2C SHT4x @ ^1.1.2
 
 [env:RAK_4631_Repeater]
 extends = rak4631
@@ -38,30 +62,6 @@ build_src_filter = ${rak4631.build_src_filter}
   +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 
-[env:RAK_4631_GPS_Repeater]
-extends = rak4631
-build_flags =
-  ${rak4631.build_flags}
-  -D DISPLAY_CLASS=SSD1306Display
-  -D ADVERT_NAME='"RAK4631 GPS Repeater"'
-  -D ADVERT_LAT=0.0
-  -D ADVERT_LON=0.0
-  -D ADMIN_PASSWORD='"password"'
-  -D MAX_NEIGHBOURS=8
-  -D FORCE_GPS_ALIVE=1
-  -D ENV_INCLUDE_GPS=1
-  -D ENV_INCLUDE_BME680=1
-;  -D MESH_PACKET_LOGGING=1
-;  -D MESH_DEBUG=1
-build_src_filter = ${rak4631.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
-  +<../examples/simple_repeater>
-lib_deps = 
-  ${rak4631.lib_deps}
-  sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27
-  https://github.com/boschsensortec/Bosch-BSEC2-Library
-  https://github.com/boschsensortec/Bosch-BME68x-Library
-
 [env:RAK_4631_room_server]
 extends = rak4631
 build_flags =
@@ -118,33 +118,6 @@ lib_deps =
   ${rak4631.lib_deps}
   densaugeo/base64 @ ~1.4.0
 
-[env:RAK_4631_GPS_companion_radio_ble]
-extends = rak4631
-build_flags =
-  ${rak4631.build_flags}
-  -D PIN_USER_BTN=9
-  -D PIN_USER_BTN_ANA=31
-  -D DISPLAY_CLASS=SSD1306Display
-  -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=8
-  -D BLE_PIN_CODE=123456
-  -D BLE_DEBUG_LOGGING=1
-  -D OFFLINE_QUEUE_SIZE=256
-  -D ENV_INCLUDE_GPS=1
-  -D ENV_INCLUDE_BME680=1
-;  -D MESH_PACKET_LOGGING=1
-;  -D MESH_DEBUG=1
-build_src_filter = ${rak4631.build_src_filter}
-  +<helpers/ui/SSD1306Display.cpp>
-  +<helpers/nrf52/SerialBLEInterface.cpp>
-  +<../examples/companion_radio>
-lib_deps =
-  ${rak4631.lib_deps}
-  sparkfun/SparkFun u-blox GNSS Arduino Library @ ^2.2.27
-  https://github.com/boschsensortec/Bosch-BSEC2-Library
-  https://github.com/boschsensortec/Bosch-BME68x-Library
-  densaugeo/base64 @ ~1.4.0
-
 [env:RAK_4631_terminal_chat]
 extends = rak4631
 build_flags =

+ 8 - 392
variants/rak4631/target.cpp

@@ -1,10 +1,13 @@
 #include <Arduino.h>
 #include "target.h"
 #include <helpers/ArduinoHelpers.h>
-#include <helpers/sensors/MicroNMEALocationProvider.h>
 
 RAK4631Board board;
 
+#ifdef DISPLAY_CLASS
+  DISPLAY_CLASS display;
+#endif
+
 RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
 
 WRAPPER_CLASS radio_driver(radio, board);
@@ -13,80 +16,11 @@ VolatileRTCClock fallback_clock;
 AutoDiscoverRTCClock rtc_clock(fallback_clock);
 
 #if ENV_INCLUDE_GPS
-MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Wire);
-RAK4631SensorManager sensors = RAK4631SensorManager(nmea);
+  #include <helpers/sensors/MicroNMEALocationProvider.h>
+  MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1);
+  EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
 #else
-RAK4631SensorManager sensors;
-#endif
-
-#if ENV_INCLUDE_BME680
-#ifndef TELEM_BME680_ADDRESS
-#define TELEM_BME680_ADDRESS    0x76      // BME680 environmental sensor I2C address
-#endif
-#include <bsec2.h>
-static Bsec2 BME680;
-static float rawPressure = 0;
-static float rawTemperature = 0;
-static float compTemperature = 0;
-static float rawHumidity = 0;
-static float compHumidity = 0;
-static float readIAQ = 0;
-static float readStaticIAQ = 0;
-static float readCO2 = 0;
-#endif
-
-#ifdef DISPLAY_CLASS
-  DISPLAY_CLASS display;
-#endif
-
-#ifdef MESH_DEBUG
-uint32_t deviceOnline = 0x00;
-static void scanDevices(TwoWire *w)
-{
-    uint8_t err, addr;
-    int nDevices = 0;
-    uint32_t start = 0;
-
-    Serial.println("Scanning I2C for Devices");
-    for (addr = 1; addr < 127; addr++) {
-        start = millis();
-        w->beginTransmission(addr); delay(2);
-        err = w->endTransmission();
-        if (err == 0) {
-            nDevices++;
-            switch (addr) {
-            case 0x42:
-                Serial.println("\tFound RAK12500 GPS Sensor");
-                deviceOnline |= RAK12500_ONLINE;
-                break;
-            case 0x76:
-                Serial.println("\tFound RAK1906 Environment Sensor");
-                deviceOnline |= BME680_ONLINE;
-                break;
-            default:
-                Serial.print("\tI2C device found at address 0x");
-                if (addr < 16) {
-                    Serial.print("0");
-                }
-                Serial.print(addr, HEX);
-                Serial.println(" !");
-                break;
-            }
-
-        } else if (err == 4) {
-            Serial.print("Unknow error at address 0x");
-            if (addr < 16) {
-                Serial.print("0");
-            }
-            Serial.println(addr, HEX);
-        }
-    }
-    if (nDevices == 0)
-        Serial.println("No I2C devices found\n");
-
-    Serial.println("Scan for devices is complete.");
-    Serial.println("\n");
-}
+  EnvironmentSensorManager sensors;
 #endif
 
 bool radio_init() {
@@ -109,324 +43,6 @@ void radio_set_tx_power(uint8_t dbm) {
   radio.setOutputPower(dbm);
 }
 
-#if ENV_INCLUDE_GPS
-void RAK4631SensorManager::start_gps()
-{
-  //function currently not used
-  gps_active = true;
-  pinMode(disStandbyPin, OUTPUT);
-  digitalWrite(disStandbyPin, 1);
-  MESH_DEBUG_PRINTLN("GPS should be on now");
-}
-
-void RAK4631SensorManager::stop_gps()
-{
-  //function currently not used
-  gps_active = false;
-  pinMode(disStandbyPin, OUTPUT);
-  digitalWrite(disStandbyPin, 0);
-  MESH_DEBUG_PRINTLN("GPS should be off now");
-}
-
-void RAK4631SensorManager::sleep_gps() {
-  gps_active = false;
-  ublox_GNSS.powerSaveMode();
-  MESH_DEBUG_PRINTLN("GPS should be sleeping now");
-}
-
-void RAK4631SensorManager::wake_gps() {
-  gps_active = true;
-  ublox_GNSS.powerSaveMode(false);
-  MESH_DEBUG_PRINTLN("GPS should be waking now");
-}
-
-bool RAK4631SensorManager::gpsIsAwake(uint32_t ioPin){
-  
-  int pinInitialState = 0;
-
-  //set initial waking state
-  pinMode(ioPin,OUTPUT);
-  digitalWrite(ioPin,0);
-  delay(1000);
-  digitalWrite(ioPin,1);
-  delay(1000);
-
-  if (ublox_GNSS.begin(Wire) == true){
-    MESH_DEBUG_PRINTLN("GPS init correctly and GPS is turned on");
-    ublox_GNSS.setI2COutput(COM_TYPE_NMEA);
-    ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
-    disStandbyPin = ioPin;
-    gps_active = true;
-    gps_detected = true;
-    return true;
-  }
-  else
-  MESH_DEBUG_PRINTLN("GPS failed to init on this IO pin... try the next");
-  //digitalWrite(ioPin,pinInitialState); //reset the IO pin to initial state
-  return false;
-}
-#endif
-
-#if ENV_INCLUDE_BME680
-static void checkBMEStatus(Bsec2 bsec) {
-  if (bsec.status < BSEC_OK)
-  {
-    MESH_DEBUG_PRINTLN("BSEC error code : %f", float(bsec.status));
-  }
-  else if (bsec.status > BSEC_OK)
-  {
-    MESH_DEBUG_PRINTLN("BSEC warning code : %f", float(bsec.status));
-  }
-
-  if (bsec.sensor.status < BME68X_OK)
-  {
-    MESH_DEBUG_PRINTLN("BME68X error code : %f", bsec.sensor.status);
-  }
-  else if (bsec.sensor.status > BME68X_OK)
-  {
-    MESH_DEBUG_PRINTLN("BME68X warning code : %f", bsec.sensor.status);
-  }
-}
-
-static void newDataCallback(const bme68xData data, const bsecOutputs outputs, Bsec2 bsec) {
-    if (!outputs.nOutputs) {
-      MESH_DEBUG_PRINTLN("No new data to report out");
-      return;
-    }
-
-    MESH_DEBUG_PRINTLN("BSEC outputs:\n\tTime stamp = %f", (int) (outputs.output[0].time_stamp / INT64_C(1000000)));
-    for (uint8_t i = 0; i < outputs.nOutputs; i++) {
-        const bsecData output  = outputs.output[i];
-        switch (output.sensor_id)
-        {
-          case BSEC_OUTPUT_IAQ:
-              readIAQ = output.signal;
-              MESH_DEBUG_PRINTLN("\tIAQ = %f", output.signal);
-              MESH_DEBUG_PRINTLN("\tIAQ accuracy = %f", output.accuracy);
-              break;
-          case BSEC_OUTPUT_RAW_TEMPERATURE:
-              rawTemperature = output.signal;
-              MESH_DEBUG_PRINTLN("\tTemperature = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_RAW_PRESSURE:
-              rawPressure = output.signal;
-              MESH_DEBUG_PRINTLN("\tPressure = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_RAW_HUMIDITY:
-              rawHumidity = output.signal;
-              MESH_DEBUG_PRINTLN("\tHumidity = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_RAW_GAS:
-              MESH_DEBUG_PRINTLN("\tGas resistance = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_STABILIZATION_STATUS:
-              MESH_DEBUG_PRINTLN("\tStabilization status = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_RUN_IN_STATUS:
-              MESH_DEBUG_PRINTLN("\tRun in status = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
-              compTemperature = output.signal;
-              MESH_DEBUG_PRINTLN("\tCompensated temperature = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
-              compHumidity = output.signal;
-              MESH_DEBUG_PRINTLN("\tCompensated humidity = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_STATIC_IAQ:
-              readStaticIAQ = output.signal;
-              MESH_DEBUG_PRINTLN("\tStatic IAQ = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_CO2_EQUIVALENT:
-              readCO2 = output.signal;
-              MESH_DEBUG_PRINTLN("\tCO2 Equivalent = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
-              MESH_DEBUG_PRINTLN("\tbVOC equivalent = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_GAS_PERCENTAGE:
-              MESH_DEBUG_PRINTLN("\tGas percentage = %f", output.signal);
-              break;
-          case BSEC_OUTPUT_COMPENSATED_GAS:
-              MESH_DEBUG_PRINTLN("\tCompensated gas = %f", output.signal);
-              break;
-          default:
-              break;
-        }
-    }
-}
-#endif
-
-bool RAK4631SensorManager::begin() {
-  
-  #ifdef MESH_DEBUG
-  scanDevices(&Wire);
-  #endif
-
-  #if ENV_INCLUDE_GPS
-    //search for the correct IO standby pin depending on socket used
-    if(gpsIsAwake(P_GPS_STANDBY_A)){
-      MESH_DEBUG_PRINTLN("GPS is on socket A");
-    }
-    else if(gpsIsAwake(P_GPS_STANDBY_C)){
-      MESH_DEBUG_PRINTLN("GPS is on socket C"); 
-    }
-    else if(gpsIsAwake(P_GPS_STANDBY_F)){
-      MESH_DEBUG_PRINTLN("GPS is on socket F"); 
-    }
-    else{
-      MESH_DEBUG_PRINTLN("Error: No GPS found on sockets A, C or F"); 
-      gps_active = false;
-      gps_detected = false;
-      return false;
-    }
-
-    #ifndef FORCE_GPS_ALIVE
-    //Now that GPS is found and set up, set to sleep for initial state
-    stop_gps();
-    #endif
-  #endif
-
-  #if ENV_INCLUDE_BME680
-
-    bsecSensor sensorList[5] = {
-      BSEC_OUTPUT_IAQ,
-    //  BSEC_OUTPUT_RAW_TEMPERATURE,
-      BSEC_OUTPUT_RAW_PRESSURE,
-    //  BSEC_OUTPUT_RAW_HUMIDITY,
-    //  BSEC_OUTPUT_RAW_GAS,
-    //  BSEC_OUTPUT_STABILIZATION_STATUS,
-    //  BSEC_OUTPUT_RUN_IN_STATUS,
-      BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
-      BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
-      BSEC_OUTPUT_STATIC_IAQ,
-    //  BSEC_OUTPUT_CO2_EQUIVALENT,
-    //  BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,
-    //  BSEC_OUTPUT_GAS_PERCENTAGE,
-    //  BSEC_OUTPUT_COMPENSATED_GAS
-    };
-
-    if(!BME680.begin(TELEM_BME680_ADDRESS, Wire)){
-      checkBMEStatus(BME680);
-      bme680_present = false;
-      bme680_active = false;
-      return false;
-    }
-    
-    MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
-    bme680_present = true;
-    bme680_active = true;
-
-    if (SAMPLING_RATE == BSEC_SAMPLE_RATE_ULP)
-	  {
-		  BME680.setTemperatureOffset(BSEC_SAMPLE_RATE_ULP);
-	  }
-	  else if (SAMPLING_RATE == BSEC_SAMPLE_RATE_LP)
-	  {
-		  BME680.setTemperatureOffset(TEMP_OFFSET_LP);
-	  }
-
-    if (!BME680.updateSubscription(sensorList, ARRAY_LEN(sensorList), SAMPLING_RATE))
-    {
-      checkBMEStatus(BME680);
-    }
-
-    BME680.attachCallback(newDataCallback);
-
-  #endif
-}
-
-bool RAK4631SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
-  #ifdef ENV_INCLUDE_GPS
-  if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {   // does requester have permission?
-    telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude);
-  }
-  #endif
-
-  if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
-
-    #if ENV_INCLUDE_BME680
-    if (bme680_active) {
-      telemetry.addTemperature(TELEM_CHANNEL_SELF, compTemperature);
-      telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, compHumidity);
-      telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, rawPressure);
-      telemetry.addTemperature(TELEM_CHANNEL_SELF+1, readIAQ);
-      telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF+1, readStaticIAQ);
-    }
-    #endif
-  }
-  return true;
-}
-
-void RAK4631SensorManager::loop() {
-  static long next_update = 0;
-  
-  #ifdef ENV_INCLUDE_GPS
-  _nmea->loop();
-  #endif
-
-  if (millis() > next_update) {
-    
-    #ifdef ENV_INCLUDE_GPS
-    if(gps_active){
-      node_lat = (double)ublox_GNSS.getLatitude()/10000000.;
-      node_lon = (double)ublox_GNSS.getLongitude()/10000000.;
-      node_altitude = (double)ublox_GNSS.getAltitude()/1000.;
-      MESH_DEBUG_PRINT("lat %f lon %f alt %f\r\n", node_lat, node_lon, node_altitude);
-    }
-    #endif
-
-    #ifdef ENV_INCLUDE_BME680
-    if(bme680_active){
-      if (!BME680.run()){
-        checkBMEStatus(BME680);
-      }
-    }
-    #endif
-    next_update = millis() + 1000;
-  }
-
-}
-
-int RAK4631SensorManager::getNumSettings() const { 
-  #if ENV_INCLUDE_GPS
-    return gps_detected ? 1 : 0;  // only show GPS setting if GPS is detected
-  #else
-    return 0;
-  #endif
-}
-
-const char* RAK4631SensorManager::getSettingName(int i) const {
-  #if ENV_INCLUDE_GPS
-    return (gps_detected && i == 0) ? "gps" : NULL;
-  #else  
-    return NULL;
-  #endif
-}
-
-const char* RAK4631SensorManager::getSettingValue(int i) const {
-  #if ENV_INCLUDE_GPS
-  if (gps_detected && i == 0) {
-    return gps_active ? "1" : "0";
-  }
-  #endif
-  return NULL;
-}
-
-bool RAK4631SensorManager::setSettingValue(const char* name, const char* value) {
-  #if ENV_INCLUDE_GPS
-  if (gps_detected && strcmp(name, "gps") == 0) {
-    if (strcmp(value, "0") == 0) {
-      stop_gps();
-    } else {
-      start_gps();
-    }
-    return true;
-  }
-  #endif
-  return false;  // not supported
-}
-
 mesh::LocalIdentity radio_new_identity() {
   RadioNoiseListener rng(radio);
   return mesh::LocalIdentity(&rng);  // create new random identity

+ 7 - 70
variants/rak4631/target.h

@@ -2,84 +2,21 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
-#include <helpers/nrf52/RAK4631Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
+#include <RAK4631Board.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
-#include <helpers/SensorManager.h>
-#if ENV_INCLUDE_GPS
-  #include <helpers/sensors/LocationProvider.h>
-  #include <SparkFun_u-blox_GNSS_Arduino_Library.h>
-#endif
+#include <helpers/sensors/EnvironmentSensorManager.h>
+
 #ifdef DISPLAY_CLASS
   #include <helpers/ui/SSD1306Display.h>
+  extern DISPLAY_CLASS display;
 #endif
 
-#define _BV(x)   (1 << x)
-
-class RAK4631SensorManager: public SensorManager {
-  #if ENV_INCLUDE_GPS
-    bool gps_active = false;
-    bool gps_detected = false;
-    LocationProvider * _nmea;
-    SFE_UBLOX_GNSS ublox_GNSS;
-    uint32_t disStandbyPin = 0;
-  
-    void start_gps();
-    void stop_gps();
-    void sleep_gps();
-    void wake_gps();
-    bool gpsIsAwake(uint32_t ioPin);
-  #endif
-
-  #if ENV_INCLUDE_BME680
-    bool bme680_active = false;
-    bool bme680_present = false;
-    #define SAMPLING_RATE		BSEC_SAMPLE_RATE_ULP
-  #endif
-
-  public:
-  #if ENV_INCLUDE_GPS
-    RAK4631SensorManager(LocationProvider &nmea): _nmea(&nmea) { }
-  #else
-    RAK4631SensorManager() { }
-  #endif
-
-  void loop() override;
-  bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
-  int getNumSettings() const override;
-  const char* getSettingName(int i) const override;
-  const char* getSettingValue(int i) const override;
-  bool setSettingValue(const char* name, const char* value) override;
-  bool begin() override;
-};
-
 extern RAK4631Board board;
 extern WRAPPER_CLASS radio_driver;
 extern AutoDiscoverRTCClock rtc_clock;
-extern RAK4631SensorManager sensors;
-
-#ifdef DISPLAY_CLASS
-  extern DISPLAY_CLASS display;
-#endif
-
-enum {
-  POWERMANAGE_ONLINE  = _BV(0),
-  DISPLAY_ONLINE      = _BV(1),
-  RADIO_ONLINE        = _BV(2),
-  GPS_ONLINE          = _BV(3),
-  PSRAM_ONLINE        = _BV(4),
-  SDCARD_ONLINE       = _BV(5),
-  AXDL345_ONLINE      = _BV(6),
-  BME280_ONLINE       = _BV(7),
-  BMP280_ONLINE       = _BV(8),
-  BME680_ONLINE       = _BV(9),
-  QMC6310_ONLINE      = _BV(10),
-  QMI8658_ONLINE      = _BV(11),
-  PCF8563_ONLINE      = _BV(12),
-  OSC32768_ONLINE     = _BV(13),
-  RAK12500_ONLINE     = _BV(14),
-};
+extern EnvironmentSensorManager sensors;
 
 bool radio_init();
 uint32_t radio_get_rng_seed();

+ 81 - 0
variants/sensecap_solar/SenseCapSolarBoard.cpp

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

+ 42 - 0
variants/sensecap_solar/SenseCapSolarBoard.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <MeshCore.h>
+#include <Arduino.h>
+
+class SenseCapSolarBoard : public mesh::MainBoard {
+protected:
+  uint8_t startup_reason;
+
+public:
+  void begin();
+  uint8_t getStartupReason() const override { return startup_reason; }
+
+#if defined(P_LORA_TX_LED)
+  void onBeforeTransmit() override {
+    digitalWrite(P_LORA_TX_LED, HIGH);   // turn TX LED on
+  }
+  void onAfterTransmit() override {
+    digitalWrite(P_LORA_TX_LED, LOW);   // turn TX LED off
+  }
+#endif
+
+  uint16_t getBattMilliVolts() override {
+    digitalWrite(VBAT_ENABLE, LOW);
+    int adcvalue = 0;
+    analogReadResolution(12);
+    analogReference(AR_INTERNAL_3_0);
+    delay(10);
+    adcvalue = analogRead(BATTERY_PIN);
+    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
+  }
+
+  const char* getManufacturerName() const override {
+    return "Seeed SenseCap Solar";
+  }
+
+  void reboot() override {
+    NVIC_SystemReset();
+  }
+
+  bool startOTAUpdate(const char* id, char reply[]) override;
+};

+ 106 - 0
variants/sensecap_solar/platformio.ini

@@ -0,0 +1,106 @@
+[SenseCap_Solar]
+extends = nrf52_base
+board = seeed_sensecap_solar
+board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52_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/sensecap_solar
+  -I src/helpers/nrf52
+  -D NRF52_PLATFORM=1
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D P_LORA_TX_LED=12
+  -D P_LORA_DIO_1=1
+  -D P_LORA_RESET=2
+  -D P_LORA_BUSY=3
+  -D P_LORA_NSS=4
+  -D LORA_TX_POWER=22
+  -D SX126X_RXEN=5
+  -D SX126X_TXEN=RADIOLIB_NC
+  -D SX126X_DIO2_AS_RF_SWITCH=1
+  -D SX126X_DIO3_TCXO_VOLTAGE=1.8
+  -D SX126X_CURRENT_LIMIT=140
+  -D SX126X_RX_BOOSTED_GAIN=1
+  -D ENV_INCLUDE_AHTX0=1
+  -D ENV_INCLUDE_BME280=1
+  -D ENV_INCLUDE_BMP280=1
+  -D ENV_INCLUDE_SHTC3=1
+  -D ENV_INCLUDE_LPS22HB=1
+  -D ENV_INCLUDE_INA3221=1
+  -D ENV_INCLUDE_INA219=1
+build_src_filter = ${nrf52_base.build_src_filter}
+  +<helpers/*.cpp>
+  +<helpers/sensors>
+  +<../variants/sensecap_solar>
+debug_tool = jlink
+upload_protocol = nrfutil
+lib_deps =
+  ${nrf52_base.lib_deps}
+  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
+  adafruit/Adafruit SHTC3 Library @ ^1.0.1
+  arduino-libraries/Arduino_LPS22HB @ ^1.0.2
+
+[env:SenseCap_Solar_repeater]
+extends = SenseCap_Solar
+build_flags =
+  ${SenseCap_Solar.build_flags}
+  -D ADVERT_NAME='"SenseCap_Solar 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
+build_src_filter = ${SenseCap_Solar.build_src_filter}
+  +<../examples/simple_repeater/main.cpp>
+
+[env:SenseCap_Solar_room_server]
+extends = SenseCap_Solar
+build_flags =
+  ${SenseCap_Solar.build_flags}
+  -D ADVERT_NAME='"SenseCap_Solar Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${SenseCap_Solar.build_src_filter}
+  +<../examples/simple_room_server/main.cpp>
+
+[env:SenseCap_Solar_companion_radio_ble]
+extends = SenseCap_Solar
+build_flags =
+  ${SenseCap_Solar.build_flags}
+  -D MAX_CONTACTS=100
+  -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 = ${SenseCap_Solar.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio>
+lib_deps =
+  ${SenseCap_Solar.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:SenseCap_Solar_companion_radio_usb]
+extends = SenseCap_Solar
+build_flags =
+  ${SenseCap_Solar.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${SenseCap_Solar.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio>
+lib_deps =
+  ${SenseCap_Solar.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 39 - 0
variants/sensecap_solar/target.cpp

@@ -0,0 +1,39 @@
+#include <Arduino.h>
+#include "target.h"
+#include <helpers/ArduinoHelpers.h>
+
+SenseCapSolarBoard 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 fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+EnvironmentSensorManager sensors;
+
+bool radio_init() {
+    rtc_clock.begin(Wire);
+
+    return radio.std_init(&SPI);
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 21 - 0
variants/sensecap_solar/target.h

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

+ 71 - 0
variants/sensecap_solar/variant.cpp

@@ -0,0 +1,71 @@
+#include "variant.h"
+#include "wiring_constants.h"
+#include "wiring_digital.h"
+#include "nrf.h"
+
+const uint32_t g_ADigitalPinMap[] = {
+    // D0 .. D10 - Peripheral control pins
+    2,  // D0  P0.02 (A0)    GNSS_WAKEUP
+    3,  // D1  P0.03 (A1)    LORA_DIO1
+    28, // D2  P0.28 (A2)    LORA_RESET
+    29, // D3  P0.29 (A3)    LORA_BUSY
+    4,  // D4  P0.04 (A4/SDA) LORA_CS
+    5,  // D5  P0.05 (A5/SCL) LORA_SW
+    43, // D6  P1.11 (UART_TX) GNSS_TX
+    44, // D7  P1.12 (UART_RX) GNSS_RX
+    45, // D8  P1.13 (SPI_SCK) LORA_SCK
+    46, // D9  P1.14 (SPI_MISO) LORA_MISO
+    47, // D10 P1.15 (SPI_MOSI) LORA_MOSI
+
+    // D11-D12 - LED outputs
+    15, // D11 P0.15 User LED
+    19, // D12 P0.19 Breathing LED
+
+    // D13 - User input
+    33, // D13 P1.01 User Button
+
+    // D14-D15 - Grove/NFC interface
+    9,  // D14 P0.09 NFC1/GROVE_D1
+    10, // D15 P0.10 NFC2/GROVE_D0
+
+    // D16 - Power management
+    // 31, // D16 P0.31 VBAT_ADC (Battery voltage)
+    31, // D16 P0.31 VBAT_ADC (Battery voltage)
+    // D17 - GNSS control
+    35, // D17 P1.03 GNSS_RESET
+
+    37, // D18 P1.05 GNSS_ENABLE
+    14, // D19 P0.14 BAT_READ
+    39, // D20 P1.07 USER_BUTTON
+
+    //
+    21, // D21 P0.21 (QSPI_SCK)
+    25, // D22 P0.25 (QSPI_CSN)
+    20, // D23 P0.20 (QSPI_SIO_0 DI)
+    24, // D24 P0.24 (QSPI_SIO_1 DO)
+    22, // D25 P0.22 (QSPI_SIO_2 WP)
+    23, // D26 P0.23 (QSPI_SIO_3 HOLD)
+};
+
+void initVariant() {
+    pinMode(GPS_EN, OUTPUT);
+    digitalWrite(GPS_EN, LOW);
+
+    pinMode(BATTERY_PIN, INPUT);
+    pinMode(VBAT_ENABLE, OUTPUT);
+    digitalWrite(VBAT_ENABLE, LOW);
+
+    pinMode(PIN_QSPI_CS, OUTPUT);
+    digitalWrite(PIN_QSPI_CS, HIGH);
+
+    pinMode(LED_GREEN, OUTPUT);
+    digitalWrite(LED_GREEN, LOW);
+
+    pinMode(LED_BLUE, OUTPUT);
+    digitalWrite(LED_BLUE, LOW);
+
+    /* disable gps until we actually support it.
+    pinMode(GPS_EN, OUTPUT);
+    digitalWrite(GPS_EN, HIGH);
+    */
+}

+ 85 - 0
variants/sensecap_solar/variant.h

@@ -0,0 +1,85 @@
+#ifndef _SEEED_SENSECAP_SOLAR_H_
+#define _SEEED_SENSECAP_SOLAR_H_
+
+/** Master clock frequency */
+#define VARIANT_MCK             (64000000ul)
+
+#define USE_LFXO                // Board uses 32khz crystal for LF
+
+/*----------------------------------------------------------------------------
+ *        Headers
+ *----------------------------------------------------------------------------*/
+
+#include "WVariant.h"
+
+#define PINS_COUNT              (33)
+#define NUM_DIGITAL_PINS        (33)
+#define NUM_ANALOG_INPUTS       (8)
+#define NUM_ANALOG_OUTPUTS      (0)
+
+// LEDs
+#define PIN_LED                 (12)
+#define LED_PWR                 (PINS_COUNT)
+
+#define LED_BUILTIN             (PIN_LED)
+
+#define LED_RED                 (PINS_COUNT)
+#define LED_GREEN               (12)
+#define LED_BLUE                (11)
+
+#define LED_STATE_ON            (1)     // State when LED is litted
+
+// Buttons
+#define PIN_BUTTON1             (13)
+#define PIN_BUTTON2             (20)
+
+#define VBAT_ENABLE             (19)    // Output LOW to enable reading of the BAT voltage.
+
+// Analog pins
+#define BATTERY_PIN             (16)    // Read the BAT voltage.
+#define AREF_VOLTAGE            (3.0F)
+#define ADC_MULTIPLIER          (3.0F) // 1M, 512k divider bridge
+#define ADC_RESOLUTION          (12)
+
+// Serial interfaces
+#define PIN_SERIAL1_RX          (7)
+#define PIN_SERIAL1_TX          (6)
+
+// SPI Interfaces
+#define SPI_INTERFACES_COUNT    (1)
+
+#define PIN_SPI_MISO            (9)
+#define PIN_SPI_MOSI            (10)
+#define PIN_SPI_SCK             (8)
+
+// Lora SPI is on SPI0
+#define  P_LORA_SCLK            PIN_SPI_SCK
+#define  P_LORA_MISO            PIN_SPI_MISO
+#define  P_LORA_MOSI            PIN_SPI_MOSI
+
+// Wire Interfaces
+#define WIRE_INTERFACES_COUNT   (1)
+
+#define PIN_WIRE_SDA            (14)
+#define PIN_WIRE_SCL            (15)
+
+// GPS L76KB
+#define GPS_BAUDRATE            9600
+#define GPS_THREAD_INTERVAL     50
+#define PIN_GPS_TX              PIN_SERIAL1_RX
+#define PIN_GPS_RX              PIN_SERIAL1_TX
+#define PIN_GPS_STANDBY         (0)
+#define GPS_EN                  (18)
+
+// QSPI Pins
+#define PIN_QSPI_SCK            (21)
+#define PIN_QSPI_CS             (22)
+#define PIN_QSPI_IO0            (23)
+#define PIN_QSPI_IO1            (24)
+#define PIN_QSPI_IO2            (25)
+#define PIN_QSPI_IO3            (26)
+
+#define EXTERNAL_FLASH_DEVICES P25Q16H
+#define EXTERNAL_FLASH_USE_QSPI
+
+#endif

+ 2 - 2
variants/station_g2/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/StationG2Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 

+ 2 - 1
variants/t1000-e/target.cpp

@@ -61,7 +61,8 @@ bool radio_init() {
     return false;  // fail
   }
   
-  radio.setCRC(1);
+  radio.setCRC(2);
+  radio.explicitHeader();
 
 #ifdef RF_SWITCH_TABLE
   radio.setRfSwitchTable(rfswitch_dios, rfswitch_table);

+ 2 - 2
variants/t1000-e/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include "T1000eBoard.h"
-#include <helpers/CustomLR1110Wrapper.h>
+#include <helpers/radiolib/CustomLR1110Wrapper.h>
 #include <helpers/ArduinoHelpers.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/LocationProvider.h>

+ 2 - 2
variants/t114/target.h

@@ -2,9 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/nrf52/T114Board.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/LocationProvider.h>

Some files were not shown because too many files changed in this diff