Browse Source

Merge branch 'dev'

Scott Powell 1 year ago
parent
commit
c1041af5a1
79 changed files with 812 additions and 783 deletions
  1. 8 6
      docs/packet_structure.md
  2. 26 24
      docs/payloads.md
  3. 6 4
      examples/companion_radio/DataStore.cpp
  4. 8 1
      examples/companion_radio/MyMesh.cpp
  5. 3 2
      examples/companion_radio/MyMesh.h
  6. 1 1
      examples/companion_radio/NodePrefs.h
  7. 4 0
      examples/companion_radio/UITask.cpp
  8. 35 3
      examples/simple_repeater/main.cpp
  9. 47 9
      examples/simple_room_server/main.cpp
  10. 93 33
      examples/simple_sensor/SensorMesh.cpp
  11. 20 13
      examples/simple_sensor/SensorMesh.h
  12. 1 0
      platformio.ini
  13. 120 14
      src/Mesh.cpp
  14. 11 0
      src/Mesh.h
  15. 1 0
      src/Packet.h
  16. 19 16
      src/helpers/BaseChatMesh.cpp
  17. 1 0
      src/helpers/BaseChatMesh.h
  18. 25 3
      src/helpers/CommonCLI.cpp
  19. 2 1
      src/helpers/CommonCLI.h
  20. 0 0
      src/helpers/radiolib/CustomLLCC68.h
  21. 0 0
      src/helpers/radiolib/CustomLLCC68Wrapper.h
  22. 0 0
      src/helpers/radiolib/CustomLR1110.h
  23. 0 0
      src/helpers/radiolib/CustomLR1110Wrapper.h
  24. 0 0
      src/helpers/radiolib/CustomSTM32WLx.h
  25. 0 0
      src/helpers/radiolib/CustomSTM32WLxWrapper.h
  26. 0 0
      src/helpers/radiolib/CustomSX1262.h
  27. 0 0
      src/helpers/radiolib/CustomSX1262Wrapper.h
  28. 0 0
      src/helpers/radiolib/CustomSX1268.h
  29. 0 0
      src/helpers/radiolib/CustomSX1268Wrapper.h
  30. 0 0
      src/helpers/radiolib/CustomSX1276.h
  31. 0 0
      src/helpers/radiolib/CustomSX1276Wrapper.h
  32. 3 3
      src/helpers/radiolib/RadioLibWrappers.cpp
  33. 0 0
      src/helpers/radiolib/RadioLibWrappers.h
  34. 181 7
      src/helpers/sensors/EnvironmentSensorManager.cpp
  35. 6 0
      src/helpers/sensors/EnvironmentSensorManager.h
  36. 3 3
      variants/generic-e22/target.h
  37. 2 35
      variants/heltec_ct62/target.cpp
  38. 2 2
      variants/heltec_ct62/target.h
  39. 2 2
      variants/heltec_tracker/target.h
  40. 2 2
      variants/heltec_v2/target.h
  41. 24 1
      variants/heltec_v3/platformio.ini
  42. 2 2
      variants/heltec_v3/target.h
  43. 2 2
      variants/heltec_wireless_paper/target.h
  44. 2 2
      variants/lilygo_t3s3/target.h
  45. 2 2
      variants/lilygo_t3s3_sx1276/target.h
  46. 2 2
      variants/lilygo_tbeam_SX1262/target.h
  47. 2 2
      variants/lilygo_tbeam_SX1276/target.h
  48. 2 2
      variants/lilygo_tbeam_supreme_SX1262/target.h
  49. 2 2
      variants/lilygo_tlora_c6/target.h
  50. 2 2
      variants/lilygo_tlora_v2_1/target.h
  51. 3 3
      variants/meshadventurer/target.h
  52. 2 2
      variants/minewsemi_me25ls01/target.h
  53. 3 4
      variants/nano_g2_ultra/target.h
  54. 2 2
      variants/picow/target.h
  55. 2 4
      variants/promicro/target.h
  56. 8 0
      variants/rak3x72/platformio.ini
  57. 3 3
      variants/rak3x72/target.h
  58. 5 8
      variants/rak4631/RAK4631Board.h
  59. 21 51
      variants/rak4631/platformio.ini
  60. 8 392
      variants/rak4631/target.cpp
  61. 6 69
      variants/rak4631/target.h
  62. 35 4
      variants/sensecap_solar/platformio.ini
  63. 2 2
      variants/sensecap_solar/target.h
  64. 2 2
      variants/station_g2/target.h
  65. 2 2
      variants/t1000-e/target.h
  66. 2 2
      variants/t114/target.h
  67. 2 2
      variants/techo/target.h
  68. 3 3
      variants/tenstar_c3/target.h
  69. 2 2
      variants/thinknode_m1/target.h
  70. 2 2
      variants/waveshare_rp2040_lora/target.h
  71. 3 3
      variants/wio-e5-dev/target.h
  72. 4 4
      variants/wio-e5-mini/target.h
  73. 2 2
      variants/wio-tracker-l1/target.h
  74. 3 1
      variants/wio-tracker-l1/variant.h
  75. 3 3
      variants/xiao_c3/target.h
  76. 2 2
      variants/xiao_c6/target.h
  77. 2 2
      variants/xiao_nrf52/target.h
  78. 2 2
      variants/xiao_rp2040/target.h
  79. 2 2
      variants/xiao_s3_wio/target.h

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

+ 8 - 1
examples/companion_radio/MyMesh.cpp

@@ -210,6 +210,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;
@@ -719,7 +723,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+
@@ -1050,6 +1054,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();

+ 3 - 2
examples/companion_radio/MyMesh.h

@@ -10,11 +10,11 @@
 #define FIRMWARE_VER_CODE 7
 
 #ifndef FIRMWARE_BUILD_DATE
-#define FIRMWARE_BUILD_DATE "15 Jul 2025"
+#define FIRMWARE_BUILD_DATE "24 Jul 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-#define FIRMWARE_VERSION "v1.7.3"
+#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;

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

+ 35 - 3
examples/simple_repeater/main.cpp

@@ -22,11 +22,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "15 Jul 2025"
+  #define FIRMWARE_BUILD_DATE   "24 Jul 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.7.3"
+  #define FIRMWARE_VERSION   "v1.7.4"
 #endif
 
 #ifndef LORA_FREQ
@@ -120,7 +120,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;
@@ -134,6 +134,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;
@@ -339,6 +344,9 @@ 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, 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)
@@ -555,6 +563,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
@@ -606,6 +615,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();
@@ -731,6 +750,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

+ 47 - 9
examples/simple_room_server/main.cpp

@@ -22,11 +22,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "15 Jul 2025"
+  #define FIRMWARE_BUILD_DATE   "24 Jul 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.7.3"
+  #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++) {
@@ -424,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;
@@ -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));
@@ -768,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();
@@ -912,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

+ 93 - 33
examples/simple_sensor/SensorMesh.cpp

@@ -243,7 +243,7 @@ static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t mult
 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 && (perms & PERM_GET_TELEMETRY) != 0) {
+  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
@@ -254,7 +254,7 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
     memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
     return 4 + tlen;  // reply_len
   }
-  if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_GET_OTHER_STATS) != 0) {
+  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);
@@ -288,13 +288,14 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint
     }
     return ofs;
   }
-  if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_LEVEL3) {
+  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;
       }
@@ -315,7 +316,14 @@ mesh::Packet* SensorMesh::createSelfAdvert() {
   return createAdvert(self_id, app_data, app_data_len);
 }
 
-ContactInfo* SensorMesh::putContact(const mesh::Identity& id) {
+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++) {
@@ -333,22 +341,35 @@ ContactInfo* SensorMesh::putContact(const mesh::Identity& id) {
     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;
 }
 
-void SensorMesh::applyContactPermissions(const uint8_t* pubkey, uint8_t perms) {
-  mesh::Identity id(pubkey);
-  auto c = putContact(id);
-
+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
-    memset(c, 0, sizeof(*c));
+    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) {
@@ -434,32 +455,43 @@ int SensorMesh::getAGCResetInterval() const {
 }
 
 uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
-  if (strcmp((char *) data, _prefs.password) != 0) {  // check for valid password
-  #if MESH_DEBUG
-    MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
-  #endif
-    return 0;
-  }
+  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;
+    }
 
-  auto client = putContact(sender);  // 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
-  }
+    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_LEVEL3 | PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO;  // initially opt-in to receive alerts (can opt out)
-  memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
+    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);
+    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] = 1;  // 1 = is admin
+  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
 
@@ -484,16 +516,20 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
   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 || sp - hex != PUB_KEY_SIZE*2) {
-      strcpy(reply, "Err - bad pubkey len");
+    if (sp == NULL) {
+      strcpy(reply, "Err - bad params");
     } else {
       *sp++ = 0;   // replace space with null terminator
 
       uint8_t pubkey[PUB_KEY_SIZE];
-      if (mesh::Utils::fromHex(pubkey, PUB_KEY_SIZE, hex)) {
+      int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
+      if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
         uint8_t perms = atoi(sp);
-        applyContactPermissions(pubkey, perms);
-        strcpy(reply, "OK");
+        if (applyContactPermissions(pubkey, hex_len / 2, perms)) {
+          strcpy(reply, "OK");
+        } else {
+          strcpy(reply, "Err - invalid params");
+        }
       } else {
         strcpy(reply, "Err - bad pubkey");
       }
@@ -502,6 +538,7 @@ void SensorMesh::handleCommand(uint32_t sender_timestamp, char* command, char* r
     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);
@@ -569,7 +606,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i
     memcpy(&timestamp, data, 4);
 
     if (timestamp > from.last_timestamp) {  // prevent replay attacks
-      uint8_t reply_len = handleRequest(from.isAdmin() ? 0xFFFF : from.permissions, timestamp, data[4], &data[5], len - 5);
+      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;
@@ -685,6 +722,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
   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));
@@ -735,6 +773,16 @@ bool SensorMesh::formatFileSystem() {
 #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) {
@@ -810,6 +858,18 @@ void SensorMesh::loop() {
     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();

+ 20 - 13
examples/simple_sensor/SensorMesh.h

@@ -25,14 +25,14 @@
 
 #define PERM_ACL_ROLE_MASK     3   // lower 2 bits
 #define PERM_ACL_GUEST         0
-#define PERM_ACL_LEVEL1        1
-#define PERM_ACL_LEVEL2        2
-#define PERM_ACL_LEVEL3        3   // admin
-
-#define PERM_GET_TELEMETRY     (1 << 2)
-#define PERM_GET_OTHER_STATS   (1 << 3)
-#define PERM_RESERVED1         (1 << 4)
-#define PERM_RESERVED2         (1 << 5)
+#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
 
@@ -45,15 +45,15 @@ struct ContactInfo {
   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_LEVEL3; }
+  bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
 };
 
 #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
 
 #define FIRMWARE_ROLE "sensor"
@@ -90,6 +90,7 @@ public:
   }
   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);
 
@@ -154,14 +155,20 @@ private:
   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* putContact(const mesh::Identity& id);
-  void applyContactPermissions(const uint8_t* pubkey, uint8_t perms);
+  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);
 

+ 1 - 0
platformio.ini

@@ -46,6 +46,7 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
 build_src_filter =
   +<*.cpp>
   +<helpers/*.cpp>
+  +<helpers/radiolib/*.cpp>
 
 ; ----------------- ESP32 ---------------------
 

+ 120 - 14
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.
   }
@@ -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
 

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

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

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

+ 25 - 3
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
@@ -131,7 +132,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
     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();
@@ -164,6 +165,21 @@ 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));
@@ -180,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) {
@@ -235,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();

+ 2 - 1
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 {

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


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


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


+ 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


+ 181 - 7
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>
@@ -47,13 +53,43 @@ static Adafruit_INA3221 INA3221;
 static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
 #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 +99,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;
@@ -105,7 +141,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 +156,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 +165,26 @@ bool EnvironmentSensorManager::begin() {
   }
   #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;
 }
 
@@ -162,7 +218,7 @@ 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.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
       telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
     }
     #endif
@@ -209,6 +265,25 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
     }
     #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 +371,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 +461,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 +481,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

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

@@ -15,6 +15,8 @@ protected:
   bool INA219_initialized = false;
   bool SHTC3_initialized = false;
   bool LPS22HB_initialized = false;
+  bool MLX90614_initialized = false;
+  bool VL53L0X_initialized = false;
 
   bool gps_detected = false;
   bool gps_active = false;
@@ -24,6 +26,10 @@ protected:
   void start_gps();
   void stop_gps();
   void initBasicGPS();
+  #ifdef RAK_BOARD
+  void rakGPSInit();
+  bool gpsIsAwake(uint8_t ioPin);
+  #endif
   #endif
 
 

+ 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

+ 24 - 1
variants/heltec_v3/platformio.ini

@@ -147,6 +147,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 =
@@ -214,4 +238,3 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter}
 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>

+ 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

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

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

+ 2 - 2
variants/minewsemi_me25ls01/target.h

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

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

+ 2 - 4
variants/promicro/target.h

@@ -2,10 +2,9 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <PromicroBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
-#include <helpers/CustomLLCC68Wrapper.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}

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

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

+ 21 - 51
variants/rak4631/platformio.ini

@@ -6,20 +6,41 @@ board_check = true
 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
+  -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
 build_src_filter = ${nrf52_base.build_src_filter}
   +<../variants/rak4631>
+  +<helpers/sensors>
 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
+  sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27
 
 [env:RAK_4631_Repeater]
 extends = rak4631
@@ -37,30 +58,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 =
@@ -117,33 +114,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

+ 6 - 69
variants/rak4631/target.h

@@ -2,84 +2,21 @@
 
 #define RADIOLIB_STATIC_ONLY 1
 #include <RadioLib.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <RAK4631Board.h>
-#include <helpers/CustomSX1262Wrapper.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();

+ 35 - 4
variants/sensecap_solar/platformio.ini

@@ -32,13 +32,11 @@ build_flags = ${nrf52_base.build_flags}
 build_src_filter = ${nrf52_base.build_src_filter}
   +<helpers/*.cpp>
   +<helpers/sensors>
-  +<helpers/nrf52/SenseCapSolarBoard.cpp>
-  +<../variants/SenseCap_Solar>
+  +<../variants/sensecap_solar>
 debug_tool = jlink
 upload_protocol = nrfutil
 lib_deps =
   ${nrf52_base.lib_deps}
-  rweather/Crypto @ ^0.4.0
   adafruit/Adafruit INA3221 Library @ ^1.0.1
   adafruit/Adafruit INA219 @ ^1.2.3
   adafruit/Adafruit AHTX0 @ ^2.0.5
@@ -72,4 +70,37 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${SenseCap_Solar.build_src_filter}
-  +<../examples/simple_room_server/main.cpp>
+  +<../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

+ 2 - 2
variants/sensecap_solar/target.h

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

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

+ 2 - 2
variants/techo/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/TechoBoard.h>
-#include <helpers/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
 #include <helpers/AutoDiscoverRTCClock.h>
 #include <helpers/SensorManager.h>
 #include <helpers/sensors/LocationProvider.h>

+ 3 - 3
variants/tenstar_c3/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/XiaoC3Board.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 - 2
variants/thinknode_m1/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/ThinkNodeM1Board.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/waveshare_rp2040_lora/target.h

@@ -4,8 +4,8 @@
 
 #include <RadioLib.h>
 #include <helpers/AutoDiscoverRTCClock.h>
-#include <helpers/CustomSX1262Wrapper.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/SensorManager.h>
 #include <helpers/rp2040/WaveshareBoard.h>
 

+ 3 - 3
variants/wio-e5-dev/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>
 
@@ -16,7 +16,7 @@ public:
 
     // Just returns ADC value for now to test adc
     uint16_t getBattMilliVolts() override {
-        uint32_t raw = analogRead(PIN_A3);            
+        uint32_t raw = analogRead(PIN_A3);
         return raw;
     }
 };

+ 4 - 4
variants/wio-e5-mini/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>
 #ifdef DISPLAY_CLASS
@@ -37,14 +37,14 @@ public:
         uint32_t raw = 0;
         for (int i=0; i<8;i++) {
             raw += analogRead(PIN_A3);
-        }            
+        }
         return ((double)raw) * 1.73 * 5 * 1000 / 8 / 4096;
     }
 };
 
 class WIOE5SensorManager : public SensorManager {
     BME280I2C bme;
-    bool has_bme = false;   
+    bool has_bme = false;
 
 public:
     WIOE5SensorManager() {}

+ 2 - 2
variants/wio-tracker-l1/target.h

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

+ 3 - 1
variants/wio-tracker-l1/variant.h

@@ -30,7 +30,7 @@
 #define PIN_BUTTON3             (26) // Joystick Down
 #define PIN_BUTTON4             (27) // Joystick Left
 #define PIN_BUTTON5             (28) // Joystick Right
-#define PIN_BUTTON6             (28) // Joystick Press
+#define PIN_BUTTON6             (29) // Joystick Press
 #define PIN_USER_BTN            PIN_BUTTON1
 #define JOYSTICK_UP             PIN_BUTTON2
 #define JOYSTICK_DOWN           PIN_BUTTON3
@@ -70,6 +70,8 @@
 #define  P_LORA_NSS             (4)
 #define  SX126X_RXEN            (5)
 #define  SX126X_TXEN            RADIOLIB_NC
+#define  SX126X_DIO2_AS_RF_SWITCH true
+#define  SX126X_DIO3_TCXO_VOLTAGE (1.8f)
 
 // Wire Interfaces
 #define WIRE_INTERFACES_COUNT   (2)

+ 3 - 3
variants/xiao_c3/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/XiaoC3Board.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 - 2
variants/xiao_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/xiao_nrf52/target.h

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

+ 2 - 2
variants/xiao_rp2040/target.h

@@ -4,8 +4,8 @@
 
 #include <RadioLib.h>
 #include <helpers/AutoDiscoverRTCClock.h>
-#include <helpers/CustomSX1262Wrapper.h>
-#include <helpers/RadioLibWrappers.h>
+#include <helpers/radiolib/CustomSX1262Wrapper.h>
+#include <helpers/radiolib/RadioLibWrappers.h>
 #include <helpers/SensorManager.h>
 #include <helpers/rp2040/XiaoRP2040Board.h>
 

+ 2 - 2
variants/xiao_s3_wio/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