Răsfoiți Sursa

Merge branch 'dev'

Scott Powell 1 an în urmă
părinte
comite
933764546a
58 a modificat fișierele cu 1750 adăugiri și 131 ștergeri
  1. 61 0
      boards/seeed-xiao-afruitnrf52-nrf52840.json
  2. 51 0
      boards/t_beams3_supreme.json
  3. 32 7
      examples/companion_radio/UITask.cpp
  4. 3 2
      examples/companion_radio/UITask.h
  5. 41 15
      examples/companion_radio/main.cpp
  6. 13 5
      examples/simple_repeater/UITask.cpp
  7. 2 2
      examples/simple_repeater/UITask.h
  8. 3 3
      examples/simple_repeater/main.cpp
  9. 13 5
      examples/simple_room_server/UITask.cpp
  10. 2 2
      examples/simple_room_server/UITask.h
  11. 16 4
      examples/simple_room_server/main.cpp
  12. 1 1
      platformio.ini
  13. 12 2
      src/Dispatcher.cpp
  14. 1 1
      src/Mesh.cpp
  15. 14 0
      src/Packet.cpp
  16. 14 6
      src/Packet.h
  17. 13 2
      src/helpers/AutoDiscoverRTCClock.cpp
  18. 25 8
      src/helpers/BaseChatMesh.cpp
  19. 1 0
      src/helpers/BaseChatMesh.h
  20. 1 0
      src/helpers/CustomLR1110Wrapper.h
  21. 3 3
      src/helpers/TBeamBoard.h
  22. 106 0
      src/helpers/TBeamS3SupremeBoard.h
  23. 1 1
      src/helpers/nrf52/PromicroBoard.cpp
  24. 1 1
      src/helpers/nrf52/RAK4631Board.cpp
  25. 1 1
      src/helpers/nrf52/T1000eBoard.cpp
  26. 7 0
      src/helpers/nrf52/T1000eBoard.h
  27. 1 1
      src/helpers/nrf52/T114Board.cpp
  28. 0 13
      src/helpers/nrf52/TechoBoard.cpp
  29. 95 0
      src/helpers/nrf52/XiaoNrf52Board.cpp
  30. 75 0
      src/helpers/nrf52/XiaoNrf52Board.h
  31. 1 1
      src/helpers/ui/DisplayDriver.h
  32. 99 0
      src/helpers/ui/GxEPDDisplay.cpp
  33. 51 0
      src/helpers/ui/GxEPDDisplay.h
  34. 1 1
      src/helpers/ui/SSD1306Display.cpp
  35. 117 0
      src/helpers/ui/ST7789Display.cpp
  36. 33 0
      src/helpers/ui/ST7789Display.h
  37. 4 4
      variants/heltec_v2/platformio.ini
  38. 5 5
      variants/heltec_v3/platformio.ini
  39. 4 4
      variants/lilygo_t3s3/platformio.ini
  40. 3 3
      variants/lilygo_tbeam/platformio.ini
  41. 72 0
      variants/lilygo_tbeam_supreme_SX1262/platformio.ini
  42. 182 0
      variants/lilygo_tbeam_supreme_SX1262/target.cpp
  43. 17 0
      variants/lilygo_tbeam_supreme_SX1262/target.h
  44. 5 5
      variants/lilygo_tlora_v2_1/platformio.ini
  45. 9 6
      variants/rak4631/platformio.ini
  46. 2 2
      variants/t1000-e/variant.cpp
  47. 31 0
      variants/t114/platformio.ini
  48. 16 3
      variants/t114/variant.h
  49. 8 2
      variants/techo/platformio.ini
  50. 19 7
      variants/techo/variant.cpp
  51. 33 2
      variants/techo/variant.h
  52. 81 0
      variants/xiao_nrf52/platformio.ini
  53. 68 0
      variants/xiao_nrf52/target.cpp
  54. 18 0
      variants/xiao_nrf52/target.h
  55. 86 0
      variants/xiao_nrf52/variant.cpp
  56. 149 0
      variants/xiao_nrf52/variant.h
  57. 24 0
      variants/xiao_s3_wio/platformio.ini
  58. 3 1
      variants/xiao_s3_wio/target.cpp

+ 61 - 0
boards/seeed-xiao-afruitnrf52-nrf52840.json

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

+ 51 - 0
boards/t_beams3_supreme.json

@@ -0,0 +1,51 @@
+{
+    "build": {
+      "arduino":{
+        "ldscript": "esp32s3_out.ld",
+        "partitions": "default.csv",
+        "memory_type": "qio_qspi"
+      },
+      "core": "esp32",
+      "extra_flags": [
+        "-DARDUINO_USB_MODE=1",
+        "-DARDUINO_USB_CDC_ON_BOOT=1",
+        "-DARDUINO_RUNNING_CORE=1",
+        "-DARDUINO_EVENT_RUNNING_CORE=1"
+      ],
+      "f_cpu": "240000000L",
+      "f_flash": "80000000L",
+      "flash_mode": "qio",
+      "hwids": [
+        [
+          "0x303A",
+          "0x1001"
+        ]
+      ],
+      "mcu": "esp32s3",
+      "variant": "esp32s3"
+    },
+    "connectivity": [
+      "wifi"
+    ],
+    "debug": {
+      "default_tool": "esp-builtin",
+      "onboard_tools": [
+        "esp-builtin"
+      ],
+      "openocd_target": "esp32s3.cfg"
+    },
+    "frameworks": [
+      "arduino",
+      "espidf"
+    ],
+    "name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)",
+    "upload": {
+      "flash_size": "8MB",
+      "maximum_ram_size": 327680,
+      "maximum_size": 8388608,
+      "require_upload_port": true,
+      "speed": 460800
+    },
+    "url": "https://www.lilygo.cc/products/t-beamsupreme-m",
+    "vendor": "LilyGo"
+  }

+ 32 - 7
examples/companion_radio/UITask.cpp

@@ -25,16 +25,30 @@ static const uint8_t meshcore_logo [] PROGMEM = {
     0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, 
 };
 
-void UITask::begin(DisplayDriver* display, const char* node_name, const char* build_date, uint32_t pin_code) {
+void UITask::begin(DisplayDriver* display, const char* node_name, const char* build_date, const char* firmware_version, uint32_t pin_code) {
   _display = display;
   _auto_off = millis() + AUTO_OFF_MILLIS;
   clearMsgPreview();
   _node_name = node_name;
-  _build_date = build_date;
   _pin_code = pin_code;
   if (_display != NULL) {
     _display->turnOn();
   }
+
+  // strip off dash and commit hash by changing dash to null terminator
+  // e.g: v1.2.3-abcdef -> v1.2.3
+  char *version = strdup(firmware_version);
+  char *dash = strchr(version, '-');
+  if(dash){
+    *dash = 0;
+  }
+
+  #ifdef PIN_USER_BTN
+    pinMode(PIN_USER_BTN, INPUT);
+  #endif
+
+  // v1.2.3 (1 Jan 2025)
+  sprintf(_version_info, "%s (%s)", version, build_date);
 }
 
 void UITask::msgRead(int msgcount) {
@@ -47,6 +61,7 @@ void UITask::msgRead(int msgcount) {
 void UITask::clearMsgPreview() {
   _origin[0] = 0;
   _msg[0] = 0;
+  _need_refresh = true;
 }
 
 void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
@@ -62,6 +77,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
   if (_display != NULL) {
     if (!_display->isOn()) _display->turnOn();
     _auto_off = millis() + AUTO_OFF_MILLIS;  // extend the auto-off timer
+    _need_refresh = true;
   }
 }
 
@@ -73,38 +89,46 @@ void UITask::renderCurrScreen() {
     // render message preview
     _display->setCursor(0, 0);
     _display->setTextSize(1);
+    _display->setColor(DisplayDriver::GREEN);
     _display->print(_node_name);
 
     _display->setCursor(0, 12);
+    _display->setColor(DisplayDriver::YELLOW);
     _display->print(_origin);
     _display->setCursor(0, 24);
+    _display->setColor(DisplayDriver::LIGHT);
     _display->print(_msg);
 
-    _display->setCursor(100, 9);
+    _display->setCursor(_display->width() - 28, 9);
     _display->setTextSize(2);
+    _display->setColor(DisplayDriver::ORANGE);
     sprintf(tmp, "%d", _msgcount);
     _display->print(tmp);
   } else {
     // render 'home' screen
+    _display->setColor(DisplayDriver::BLUE);
     _display->drawXbm(0, 0, meshcore_logo, 128, 13);
     _display->setCursor(0, 20);
     _display->setTextSize(1);
-    _display->print(_node_name);
 
-    sprintf(tmp, "Build: %s", _build_date);
+    _display->setColor(DisplayDriver::LIGHT);
+    _display->print(_node_name);
+    
     _display->setCursor(0, 32);
-    _display->print(tmp);
+    _display->print(_version_info);
 
     if (_connected) {
       //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf);
       //_display->printf("bw   : %03.2f cr %d\n", _prefs.bw, _prefs.cr);
     } else if (_pin_code != 0) {
+      _display->setColor(DisplayDriver::RED);
       _display->setTextSize(2);
       _display->setCursor(0, 43);
       sprintf(tmp, "Pin:%d", _pin_code);
       _display->print(tmp);
     }
   }
+  _need_refresh = false;
 }
 
 void UITask::userLedHandler() {
@@ -148,6 +172,7 @@ void UITask::buttonHandler() {
             clearMsgPreview();
           } else {
             _display->turnOn();
+            _need_refresh = true;
           }
           _auto_off = cur_time + AUTO_OFF_MILLIS;   // extend auto-off timer
         }
@@ -173,7 +198,7 @@ void UITask::loop() {
   userLedHandler();
 
   if (_display != NULL && _display->isOn()) {
-    if (millis() >= _next_refresh) {
+    if (millis() >= _next_refresh && _need_refresh) {
       _display->startFrame();
       renderCurrScreen();
       _display->endFrame();

+ 3 - 2
examples/companion_radio/UITask.h

@@ -11,10 +11,11 @@ class UITask {
   bool _connected;
   uint32_t _pin_code;
   const char* _node_name;
-  const char* _build_date;
+  char _version_info[32];
   char _origin[62];
   char _msg[80];
   int _msgcount;
+  bool _need_refresh = true;
 
   void renderCurrScreen();
   void buttonHandler();
@@ -26,7 +27,7 @@ public:
       _next_refresh = 0; 
       _connected = false;
   }
-  void begin(DisplayDriver* display, const char* node_name, const char* build_date, uint32_t pin_code);
+  void begin(DisplayDriver* display, const char* node_name, const char* build_date, const char* firmware_version, uint32_t pin_code);
 
   void setHasConnection(bool connected) { _connected = connected; }
   bool hasDisplay() const { return _display != NULL; }

+ 41 - 15
examples/companion_radio/main.cpp

@@ -59,9 +59,14 @@
 
 #ifdef DISPLAY_CLASS
   #include "UITask.h"
-  #include <helpers/ui/SSD1306Display.h>
-
-  static DISPLAY_CLASS  display;
+  #ifdef ST7789
+    #include <helpers/ui/ST7789Display.h>
+  #elif defined(HAS_GxEPD)
+    #include <helpers/ui/GxEPDDisplay.h>
+  #else
+    #include <helpers/ui/SSD1306Display.h>
+  #endif
+  static DISPLAY_CLASS display;
   #define HAS_UI
 #endif
 
@@ -83,14 +88,14 @@ static uint32_t _atoi(const char* sp) {
 
 /*------------ Frame Protocol --------------*/
 
-#define FIRMWARE_VER_CODE    3
+#define FIRMWARE_VER_CODE    4
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "7 Apr 2025"
+  #define FIRMWARE_BUILD_DATE   "21 Apr 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.4.3"
+  #define FIRMWARE_VERSION   "v1.5.0"
 #endif
 
 #define CMD_APP_START              1
@@ -130,6 +135,7 @@ static uint32_t _atoi(const char* sp) {
 #define CMD_SIGN_FINISH           35
 #define CMD_SEND_TRACE_PATH       36
 #define CMD_SET_DEVICE_PIN        37
+#define CMD_SET_OTHER_PARAMS      38
 
 #define RESP_CODE_OK                0
 #define RESP_CODE_ERR               1
@@ -164,6 +170,7 @@ static uint32_t _atoi(const char* sp) {
 #define PUSH_CODE_STATUS_RESPONSE   0x87
 #define PUSH_CODE_LOG_RX_DATA       0x88
 #define PUSH_CODE_TRACE_DATA        0x89
+#define PUSH_CODE_NEW_ADVERT        0x8A
 
 #define ERR_CODE_UNSUPPORTED_CMD      1
 #define ERR_CODE_NOT_FOUND            2
@@ -184,7 +191,7 @@ struct NodePrefs {  // persisted to file
   uint8_t sf;
   uint8_t cr;
   uint8_t reserved1;
-  uint8_t reserved2;
+  uint8_t manual_add_contacts;
   float bw;
   uint8_t tx_power_dbm;
   uint8_t unused[3];
@@ -499,11 +506,19 @@ protected:
     }
   }
 
+  bool isAutoAddEnabled() const override {
+    return (_prefs.manual_add_contacts & 1) == 0;
+  }
+
   void onDiscoveredContact(ContactInfo& contact, bool is_new) override {
     if (_serial->isConnected()) {
-      out_frame[0] = PUSH_CODE_ADVERT;
-      memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
-      _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
+      if (!isAutoAddEnabled() && is_new) {
+        writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact);
+      } else {
+        out_frame[0] = PUSH_CODE_ADVERT;
+        memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
+        _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
+      }
     } else {
       soundBuzzer();
     }
@@ -663,6 +678,10 @@ protected:
   }
 
   void onRawDataRecv(mesh::Packet* packet) override {
+    if (packet->payload_len + 4 > sizeof(out_frame)) {
+      MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len);
+      return;
+    }
     int i = 0;
     out_frame[i++] = PUSH_CODE_RAW_DATA;
     out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
@@ -746,7 +765,7 @@ public:
       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((uint8_t *) &_prefs.reserved2, sizeof(_prefs.reserved2));  // 63
+      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
       file.read((uint8_t *) _prefs.unused, sizeof(_prefs.unused));  // 69
@@ -847,7 +866,7 @@ public:
       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((uint8_t *) &_prefs.reserved2, sizeof(_prefs.reserved2));  // 63
+      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
       file.write((uint8_t *) _prefs.unused, sizeof(_prefs.unused));  // 69
@@ -888,12 +907,15 @@ public:
       out_frame[i++] = MAX_LORA_TX_POWER;
       memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE;
 
-      int32_t lat, lon, alt = 0;
+      int32_t lat, lon;
       lat = (_prefs.node_lat * 1000000.0);
       lon = (_prefs.node_lon * 1000000.0);
       memcpy(&out_frame[i], &lat, 4); i += 4;
       memcpy(&out_frame[i], &lon, 4); i += 4;
-      memcpy(&out_frame[i], &alt, 4); i += 4;
+      out_frame[i++] = 0;  // reserved
+      out_frame[i++] = 0;  // reserved
+      out_frame[i++] = 0;  // reserved
+      out_frame[i++] = _prefs.manual_add_contacts;
 
       uint32_t freq = _prefs.freq * 1000;
       memcpy(&out_frame[i], &freq, 4); i += 4;
@@ -1177,6 +1199,10 @@ public:
       _prefs.airtime_factor = ((float)af) / 1000.0f;
       savePrefs();
       writeOKFrame();
+    } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) {
+      _prefs.manual_add_contacts = cmd_frame[1];
+      savePrefs();
+      writeOKFrame();
     } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
       board.reboot();
     } else if (cmd_frame[0] == CMD_GET_BATTERY_VOLTAGE) {
@@ -1505,7 +1531,7 @@ void setup() {
 #endif
 
 #ifdef HAS_UI
-  ui_task.begin(disp, the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, the_mesh.getBLEPin());
+  ui_task.begin(disp, the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION, the_mesh.getBLEPin());
 #endif
 }
 

+ 13 - 5
examples/simple_repeater/UITask.cpp

@@ -20,25 +20,33 @@ static const uint8_t meshcore_logo [] PROGMEM = {
     0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, 
 };
 
-void UITask::begin(const char* node_name, const char* build_date) {
+void UITask::begin(const char* node_name, const char* build_date, const char* firmware_version) {
   _prevBtnState = HIGH;
   _auto_off = millis() + AUTO_OFF_MILLIS;
   _node_name = node_name;
-  _build_date = build_date;
   _display->turnOn();
+
+  // strip off dash and commit hash by changing dash to null terminator
+  // e.g: v1.2.3-abcdef -> v1.2.3
+  char *version = strdup(firmware_version);
+  char *dash = strchr(version, '-');
+  if(dash){
+    *dash = 0;
+  }
+
+  // v1.2.3 (1 Jan 2025)
+  sprintf(_version_info, "%s (%s)", version, build_date);
 }
 
 void UITask::renderCurrScreen() {
-  char tmp[80];
   // render 'home' screen
   _display->drawXbm(0, 0, meshcore_logo, 128, 13);
   _display->setCursor(0, 20);
   _display->setTextSize(1);
   _display->print(_node_name);
 
-  sprintf(tmp, "Build: %s", _build_date);
   _display->setCursor(0, 32);
-  _display->print(tmp);
+  _display->print(_version_info);
   _display->setCursor(0, 43);
   _display->print("< Repeater >");
   //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf);

+ 2 - 2
examples/simple_repeater/UITask.h

@@ -7,12 +7,12 @@ class UITask {
   unsigned long _next_read, _next_refresh, _auto_off;
   int _prevBtnState;
   const char* _node_name;
-  const char* _build_date;
+  char _version_info[32];
 
   void renderCurrScreen();
 public:
   UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
-  void begin(const char* node_name, const char* build_date);
+  void begin(const char* node_name, const char* build_date, const char* firmware_version);
 
   void loop();
 };

+ 3 - 3
examples/simple_repeater/main.cpp

@@ -20,11 +20,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "7 Apr 2025"
+  #define FIRMWARE_BUILD_DATE   "21 Apr 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.4.3"
+  #define FIRMWARE_VERSION   "v1.5.0"
 #endif
 
 #ifndef LORA_FREQ
@@ -658,7 +658,7 @@ void setup() {
   the_mesh.begin(fs);
 
 #ifdef DISPLAY_CLASS
-  ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE);
+  ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
 #endif
 
   // send out initial Advertisement to the mesh

+ 13 - 5
examples/simple_room_server/UITask.cpp

@@ -20,25 +20,33 @@ static const uint8_t meshcore_logo [] PROGMEM = {
     0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, 
 };
 
-void UITask::begin(const char* node_name, const char* build_date) {
+void UITask::begin(const char* node_name, const char* build_date, const char* firmware_version) {
   _prevBtnState = HIGH;
   _auto_off = millis() + AUTO_OFF_MILLIS;
   _node_name = node_name;
-  _build_date = build_date;
   _display->turnOn();
+
+  // strip off dash and commit hash by changing dash to null terminator
+  // e.g: v1.2.3-abcdef -> v1.2.3
+  char *version = strdup(firmware_version);
+  char *dash = strchr(version, '-');
+  if(dash){
+    *dash = 0;
+  }
+
+  // v1.2.3 (1 Jan 2025)
+  sprintf(_version_info, "%s (%s)", version, build_date);
 }
 
 void UITask::renderCurrScreen() {
-  char tmp[80];
   // render 'home' screen
   _display->drawXbm(0, 0, meshcore_logo, 128, 13);
   _display->setCursor(0, 20);
   _display->setTextSize(1);
   _display->print(_node_name);
 
-  sprintf(tmp, "Build: %s", _build_date);
   _display->setCursor(0, 32);
-  _display->print(tmp);
+  _display->print(_version_info);
   _display->setCursor(0, 43);
   _display->print("< Room Server >");
   //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf);

+ 2 - 2
examples/simple_room_server/UITask.h

@@ -7,12 +7,12 @@ class UITask {
   unsigned long _next_read, _next_refresh, _auto_off;
   int _prevBtnState;
   const char* _node_name;
-  const char* _build_date;
+  char _version_info[32];
 
   void renderCurrScreen();
 public:
   UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
-  void begin(const char* node_name, const char* build_date);
+  void begin(const char* node_name, const char* build_date, const char* firmware_version);
 
   void loop();
 };

+ 16 - 4
examples/simple_room_server/main.cpp

@@ -20,11 +20,11 @@
 /* ------------------------------ Config -------------------------------- */
 
 #ifndef FIRMWARE_BUILD_DATE
-  #define FIRMWARE_BUILD_DATE   "7 Apr 2025"
+  #define FIRMWARE_BUILD_DATE   "21 Apr 2025"
 #endif
 
 #ifndef FIRMWARE_VERSION
-  #define FIRMWARE_VERSION   "v1.4.3"
+  #define FIRMWARE_VERSION   "v1.5.0"
 #endif
 
 #ifndef LORA_FREQ
@@ -232,6 +232,17 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
     }
   }
 
+  uint8_t getUnsyncedCount(ClientInfo* client) {
+    uint8_t count = 0;
+    for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) {
+      if (posts[k].post_timestamp > client->sync_since   // is new post for this Client?
+        && !posts[k].author.matches(client->id)) {    // don't push posts to the author
+        count++;
+      }
+    }
+    return count;
+  }
+
   bool processAck(const uint8_t *data) {
     for (int i = 0; i < num_clients; i++) {
       auto client = &known_clients[i];
@@ -398,7 +409,7 @@ protected:
       reply_data[4] = RESP_SERVER_LOGIN_OK;
       reply_data[5] = (CLIENT_KEEP_ALIVE_SECS >> 4);  // NEW: recommended keep-alive interval (secs / 16)
       reply_data[6] = (perm == RoomPermission::ADMIN ? 1 : (perm == RoomPermission::GUEST ? 0 : 2));
-      reply_data[7] = 0;  // FUTURE: reserved
+      reply_data[7] = getUnsyncedCount(client);  // NEW
       memcpy(&reply_data[8], "OK", 2);  // REVISIT: not really needed
 
       next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS);  // delay next push, give RESPONSE packet time to arrive first
@@ -572,6 +583,7 @@ protected:
 
             auto reply = createAck(ack_hash);
             if (reply) {
+              reply->payload[reply->payload_len++] = getUnsyncedCount(client);  // NEW: add unsynced counter to end of ACK packet
               sendDirect(reply, client->out_path, client->out_path_len);
             }
           }
@@ -887,7 +899,7 @@ void setup() {
   the_mesh.begin(fs);
 
 #ifdef DISPLAY_CLASS
-  ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE);
+  ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
 #endif
 
   // send out initial Advertisement to the mesh

+ 1 - 1
platformio.ini

@@ -59,4 +59,4 @@ build_flags = ${nrf52_base.build_flags}
 lib_deps =
   ${nrf52_base.lib_deps}
   rweather/Crypto @ ^0.4.0
-  https://github.com/adafruit/Adafruit_nRF52_Arduino
+  https://github.com/adafruit/Adafruit_nRF52_Arduino

+ 12 - 2
src/Dispatcher.cpp

@@ -101,6 +101,12 @@ void Dispatcher::checkRecv() {
 #endif
 
         pkt->header = raw[i++];
+        if (pkt->hasTransportCodes()) {
+          memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
+          memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
+        } else {
+          pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
+        }
         pkt->path_len = raw[i++];
 
         if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) {
@@ -132,7 +138,7 @@ void Dispatcher::checkRecv() {
     #if MESH_PACKET_LOGGING
     Serial.print(getLogDateTime());
     Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", 
-            2 + pkt->path_len + pkt->payload_len, pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
+            pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
             (int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000));
 
     static uint8_t packet_hash[MAX_HASH_SIZE];
@@ -147,7 +153,7 @@ void Dispatcher::checkRecv() {
       Serial.printf("\n");
     }
     #endif
-    logRx(pkt, 2 + pkt->path_len + pkt->payload_len, score);   // hook for custom logging
+    logRx(pkt, pkt->getRawLength(), score);   // hook for custom logging
 
     if (pkt->isRouteFlood()) {
       n_recv_flood++;
@@ -212,6 +218,10 @@ void Dispatcher::checkSend() {
     raw[len++] = NODE_ID;
 #endif
     raw[len++] = outbound->header;
+    if (outbound->hasTransportCodes()) {
+      memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
+      memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
+    }
     raw[len++] = outbound->path_len;
     memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len;
 

+ 1 - 1
src/Mesh.cpp

@@ -15,7 +15,7 @@ bool Mesh::allowPacketForward(const mesh::Packet* packet) {
   return false;  // by default, Transport NOT enabled
 }
 uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) { 
-  uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * 52 / 50) / 2;
+  uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2;
 
   return _rng->nextInt(0, 5)*t;
 }

+ 14 - 0
src/Packet.cpp

@@ -10,6 +10,10 @@ Packet::Packet() {
   payload_len = 0;
 }
 
+int Packet::getRawLength() const {
+  return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0);
+}
+
 void Packet::calculatePacketHash(uint8_t* hash) const {
   SHA256 sha;
   uint8_t t = getPayloadType();
@@ -24,6 +28,10 @@ void Packet::calculatePacketHash(uint8_t* hash) const {
 uint8_t Packet::writeTo(uint8_t dest[]) const {
   uint8_t i = 0;
   dest[i++] = header;
+  if (hasTransportCodes()) {
+    memcpy(&dest[i], &transport_codes[0], 2); i += 2;
+    memcpy(&dest[i], &transport_codes[1], 2); i += 2;
+  }
   dest[i++] = path_len;
   memcpy(&dest[i], path, path_len); i += path_len;
   memcpy(&dest[i], payload, payload_len); i += payload_len;
@@ -33,6 +41,12 @@ uint8_t Packet::writeTo(uint8_t dest[]) const {
 bool Packet::readFrom(const uint8_t src[], uint8_t len) {
   uint8_t i = 0;
   header = src[i++];
+  if (hasTransportCodes()) {
+    memcpy(&transport_codes[0], &src[i], 2); i += 2;
+    memcpy(&transport_codes[1], &src[i], 2); i += 2;
+  } else {
+    transport_codes[0] = transport_codes[1] = 0;
+  }
   path_len = src[i++];
   if (path_len > sizeof(path)) return false;   // bad encoding
   memcpy(path, &src[i], path_len); i += path_len;

+ 14 - 6
src/Packet.h

@@ -11,10 +11,10 @@ namespace mesh {
 #define PH_VER_SHIFT         6
 #define PH_VER_MASK       0x03   // 2-bits
 
-#define ROUTE_TYPE_RESERVED1     0x00    // FUTURE
-#define ROUTE_TYPE_FLOOD         0x01    // flood mode, needs 'path' to be built up (max 64 bytes)
-#define ROUTE_TYPE_DIRECT        0x02    // direct route, 'path' is supplied
-#define ROUTE_TYPE_RESERVED2     0x03    // FUTURE
+#define ROUTE_TYPE_TRANSPORT_FLOOD   0x00    // flood mode + transport codes
+#define ROUTE_TYPE_FLOOD             0x01    // flood mode, needs 'path' to be built up (max 64 bytes)
+#define ROUTE_TYPE_DIRECT            0x02    // direct route, 'path' is supplied
+#define ROUTE_TYPE_TRANSPORT_DIRECT  0x03    // direct route + transport codes
 
 #define PAYLOAD_TYPE_REQ         0x00    // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
 #define PAYLOAD_TYPE_RESPONSE    0x01    // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -43,6 +43,7 @@ public:
 
   uint8_t header;
   uint16_t payload_len, path_len;
+  uint16_t transport_codes[2];
   uint8_t path[MAX_PATH_SIZE];
   uint8_t payload[MAX_PACKET_PAYLOAD];
   int8_t _snr;
@@ -58,8 +59,10 @@ public:
    */
   uint8_t getRouteType() const { return header & PH_ROUTE_MASK; }
 
-  bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD; }
-  bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT; }
+  bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD; }
+  bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; }
+
+  bool hasTransportCodes() const { return getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; }
 
   /**
    * \returns  one of PAYLOAD_TYPE_ values
@@ -76,6 +79,11 @@ public:
 
   float getSNR() const { return ((float)_snr) / 4.0f; }
 
+  /**
+   * \returns  the encoded/wire format length of this packet
+   */
+  int getRawLength() const;
+
   /**
    * \brief  save entire packet as a blob
    * \param dest  (OUT) destination buffer (assumed to be MAX_MTU_SIZE)

+ 13 - 2
src/helpers/AutoDiscoverRTCClock.cpp

@@ -8,8 +8,12 @@ static bool ds3231_success = false;
 static Melopero_RV3028 rtc_rv3028;
 static bool rv3028_success = false;
 
+static RTC_PCF8563 rtc_8563;
+static bool rtc_8563_success = false;
+
 #define DS3231_ADDRESS   0x68
 #define RV3028_ADDRESS   0x52
+#define PCF8563_ADDRESS  0x51
 
 bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
   wire.beginTransmission(addr);
@@ -28,6 +32,9 @@ void AutoDiscoverRTCClock::begin(TwoWire& wire) {
 	rtc_rv3028.set24HourMode(); // Set the device to use the 24hour format (default) instead of the 12 hour format
     rv3028_success = true;
   }
+  if(i2c_probe(wire,PCF8563_ADDRESS)){
+    rtc_8563_success = rtc_8563.begin(&wire);
+  }
 }
 
 uint32_t AutoDiscoverRTCClock::getCurrentTime() {
@@ -44,6 +51,9 @@ uint32_t AutoDiscoverRTCClock::getCurrentTime() {
         rtc_rv3028.getSecond()
     ).unixtime();
   }
+  if(rtc_8563_success){
+    return rtc_8563.now().unixtime();
+  }
   return _fallback->getCurrentTime();
 }
 
@@ -52,9 +62,10 @@ void AutoDiscoverRTCClock::setCurrentTime(uint32_t time) {
     rtc_3231.adjust(DateTime(time));
   } else if (rv3028_success) {
     auto dt = DateTime(time);
-	uint8_t weekday = (dt.day() + (uint16_t)((2.6 * dt.month()) - 0.2) - (2 * (dt.year() / 100)) + dt.year() + (uint16_t)(dt.year() / 4) + (uint16_t)(dt.year() / 400)) % 7;
-	
+	  uint8_t weekday = (dt.day() + (uint16_t)((2.6 * dt.month()) - 0.2) - (2 * (dt.year() / 100)) + dt.year() + (uint16_t)(dt.year() / 4) + (uint16_t)(dt.year() / 400)) % 7;
     rtc_rv3028.setTime(dt.year(), dt.month(), weekday, dt.day(), dt.hour(), dt.minute(), dt.second());
+  } else if (rtc_8563_success) {
+    rtc_8563.adjust(DateTime(time));
   } else {
     _fallback->setCurrentTime(time);
   }

+ 25 - 8
src/helpers/BaseChatMesh.cpp

@@ -31,8 +31,29 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
     }
   }
 
+  // save a copy of raw advert packet (to support "Share..." function)
+  int plen = packet->writeTo(temp_buf);
+  putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
+  
   bool is_new = false;
   if (from == NULL) {
+    if (!isAutoAddEnabled()) {
+      ContactInfo ci;
+      memset(&ci, 0, sizeof(ci));
+      ci.id = id;
+      ci.out_path_len = -1;  // initially out_path is unknown
+      StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
+      ci.type = parser.getType();
+      if (parser.hasLatLon()) {
+        ci.gps_lat = parser.getIntLat();
+        ci.gps_lon = parser.getIntLon();
+      }
+      ci.last_advert_timestamp = timestamp;
+      ci.lastmod = getRTCClock()->getCurrentTime();
+      onDiscoveredContact(ci, true);       // let UI know
+      return;
+    }
+
     is_new = true;
     if (num_contacts < MAX_CONTACTS) {
       from = &contacts[num_contacts++];
@@ -50,10 +71,6 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
     }
   }
 
-  // save a copy of raw advert packet (to support "Share..." function)
-  int plen = packet->writeTo(temp_buf);
-  putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
-  
   // update
   StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
   from->type = parser.getType();
@@ -252,7 +269,7 @@ int  BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
   mesh::Packet* pkt = composeMsgPacket(recipient, timestamp, attempt, text, expected_ack);
   if (pkt == NULL) return MSG_SEND_FAILED;
 
-  uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
+  uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
 
   int rc;
   if (recipient.out_path_len < 0) {
@@ -279,7 +296,7 @@ int  BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest
   auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.shared_secret, temp, 5 + text_len);
   if (pkt == NULL) return MSG_SEND_FAILED;
 
-  uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
+  uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
   int rc;
   if (recipient.out_path_len < 0) {
     sendFlood(pkt);
@@ -362,7 +379,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
 
   auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.shared_secret, temp, tlen);
   if (pkt) {
-    uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
+    uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     if (recipient.out_path_len < 0) {
       sendFlood(pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);
@@ -386,7 +403,7 @@ int  BaseChatMesh::sendStatusRequest(const ContactInfo& recipient, uint32_t& est
 
   auto pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.shared_secret, temp, sizeof(temp));
   if (pkt) {
-    uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
+    uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
     if (recipient.out_path_len < 0) {
       sendFlood(pkt);
       est_timeout = calcFloodTimeoutMillisFor(t);

+ 1 - 0
src/helpers/BaseChatMesh.h

@@ -103,6 +103,7 @@ protected:
   }
 
   // 'UI' concepts, for sub-classes to implement
+  virtual bool isAutoAddEnabled() const { return true; }
   virtual void onDiscoveredContact(ContactInfo& contact, bool is_new) = 0;
   virtual bool processAck(const uint8_t *data) = 0;
   virtual void onContactPathUpdated(const ContactInfo& contact) = 0;

+ 1 - 0
src/helpers/CustomLR1110Wrapper.h

@@ -27,4 +27,5 @@ public:
 
   float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
   float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
+  int16_t setRxBoostedGainMode(bool en) { return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en); };
 };

+ 3 - 3
src/helpers/TBeamBoard.h

@@ -33,11 +33,11 @@ class TBeamBoard : public ESP32Board {
 public:
   void begin() {
     ESP32Board::begin();
-    
+
     power.setALDO2Voltage(3300);
     power.enableALDO2();
 
-    pinMode(38,INPUT_PULLUP);
+    pinMode(38, INPUT_PULLUP);
 
     esp_reset_reason_t reason = esp_reset_reason();
     if (reason == ESP_RST_DEEPSLEEP) {
@@ -75,7 +75,7 @@ public:
   }
 
   uint16_t getBattMilliVolts() override {
-    return 0;
+    return power.getBattVoltage();
   }
 
   const char* getManufacturerName() const override {

+ 106 - 0
src/helpers/TBeamS3SupremeBoard.h

@@ -0,0 +1,106 @@
+#pragma once
+
+#include "ESP32Board.h"
+#include <driver/rtc_io.h>
+#include <Wire.h>
+#include <Arduino.h>
+#include "XPowersLib.h"
+
+// LoRa radio module pins for TBeam S3 Supreme
+#define  P_LORA_DIO_1   1   //SX1262 IRQ pin
+#define  P_LORA_NSS     10  //SX1262 SS pin
+#define  P_LORA_RESET   5   //SX1262 Rest pin
+#define  P_LORA_BUSY    4   //SX1262 Busy pin
+#define  P_LORA_SCLK    12  //SX1262 SCLK pin
+#define  P_LORA_MISO    13  //SX1262 MISO pin
+#define  P_LORA_MOSI    11  //SX1262 MOSI pin
+
+#define PIN_BOARD_SDA 17  //SDA for OLED, BME280, and QMC6310U (0x1C)
+#define PIN_BOARD_SCL 18  //SCL for OLED, BME280, and QMC6310U (0x1C)
+
+#define PIN_BOARD_SDA1 42  //SDA for PMU and PFC8563 (RTC)
+#define PIN_BOARD_SCL1 41  //SCL for PMU and PFC8563 (RTC)
+#define PIN_PMU_IRQ 40     //IRQ pin for PMU
+
+#define PIN_USER_BTN 0
+
+#define P_BOARD_SPI_MOSI 35  //SPI for SD Card and QMI8653 (IMU)
+#define P_BOARD_SPI_MISO 37  //SPI for SD Card and QMI8653 (IMU)
+#define P_BOARD_SPI_SCK  36  //SPI for SD Card and QMI8653 (IMU)
+#define P_BPARD_SPI_CS   47  //SPI for SD Card and QMI8653 (IMU)
+#define P_BOARD_IMU_CS   34  //Pin for QMI8653 (IMU) CS
+
+#define P_BOARD_IMU_INT  33  //IMU Int pin
+#define P_BOARD_RTC_INT  14  //RTC Int pin
+
+#define P_GPS_RX    9   //GPS RX pin
+#define P_GPS_TX    8   //GPS TX pin
+#define P_GPS_WAKE  7   //GPS Wakeup pin
+#define P_GPS_1PPS  6   //GPS 1PPS pin
+
+//I2C Wire addresses
+#define I2C_BME280_ADD    0x76  //BME280 sensor I2C address on Wire
+#define I2C_OLED_ADD      0x3C  //SH1106 OLED I2C address on Wire
+#define I2C_QMC6310U_ADD  0x1C  //QMC6310U mag sensor I2C address on Wire
+
+//I2C Wire1 addresses
+#define I2C_RTC_ADD       0x51  //RTC I2C address on Wire1
+#define I2C_PMU_ADD       0x34  //AXP2101 I2C address on Wire1
+
+
+
+class TBeamS3SupremeBoard : public ESP32Board {
+
+public:
+  void begin() {
+    
+    bool power_init();
+    
+    ESP32Board::begin();
+
+    esp_reset_reason_t reason = esp_reset_reason();
+    if (reason == ESP_RST_DEEPSLEEP) {
+      long wakeup_source = esp_sleep_get_ext1_wakeup_status();
+      if (wakeup_source & (1 << P_LORA_DIO_1)) {  // received a LoRa packet (while in deep sleep)
+        startup_reason = BD_STARTUP_RX_PACKET;
+      }
+
+      rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
+      rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
+    }
+  }
+
+  void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
+    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
+
+    // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
+    rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
+    rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
+
+    rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
+
+    if (pin_wake_btn < 0) {
+      esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH);  // wake up on: recv LoRa packet
+    } else {
+      esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH);  // wake up on: recv LoRa packet OR wake btn
+    }
+
+    if (secs > 0) {
+      esp_sleep_enable_timer_wakeup(secs * 1000000);
+    }
+
+    // Finally set ESP32 into sleep
+    esp_deep_sleep_start();   // CPU halts here and never returns!
+  }
+
+  uint16_t getBattMilliVolts() override {
+
+    return 0;
+  }
+
+  uint16_t getBattPercent();
+
+  const char* getManufacturerName() const override {
+    return "LilyGo T-Beam S3 Supreme SX1262";
+  }
+};

+ 1 - 1
src/helpers/nrf52/PromicroBoard.cpp

@@ -14,7 +14,7 @@ void PromicroBoard::begin() {
     pinMode(PIN_VBAT_READ, INPUT);
 
     #ifdef BUTTON_PIN
-      pinMode(BUTTON_PIN, INPUT);
+      pinMode(BUTTON_PIN, INPUT_PULLUP);
     #endif
 
     #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)

+ 1 - 1
src/helpers/nrf52/RAK4631Board.cpp

@@ -27,7 +27,7 @@ void RAK4631Board::begin() {
 #endif
 
 #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
-  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL)
+  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
 #endif
 
   Wire.begin();

+ 1 - 1
src/helpers/nrf52/T1000eBoard.cpp

@@ -18,7 +18,7 @@ void T1000eBoard::begin() {
 #endif
 
 #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
-  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL)
+  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
 #endif
 
   Wire.begin();

+ 7 - 0
src/helpers/nrf52/T1000eBoard.h

@@ -29,9 +29,16 @@ public:
 
   uint16_t getBattMilliVolts() override {
   #ifdef BATTERY_PIN
+   #ifdef PIN_3V3_EN
+    digitalWrite(PIN_3V3_EN, HIGH);
+   #endif
     analogReference(AR_INTERNAL_3_0);
     analogReadResolution(12);
+    delay(10);
     float volts = (analogRead(BATTERY_PIN) * ADC_MULTIPLIER * AREF_VOLTAGE) / 4096;
+   #ifdef PIN_3V3_EN
+    digitalWrite(PIN_3V3_EN, LOW);
+   #endif
 
     analogReference(AR_DEFAULT);  // put back to default
     analogReadResolution(10);

+ 1 - 1
src/helpers/nrf52/T114Board.cpp

@@ -27,7 +27,7 @@ void T114Board::begin() {
   pinMode(PIN_VBAT_READ, INPUT);
 
 #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
-  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL)
+  Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL);
 #endif
 
   Wire.begin();

+ 0 - 13
src/helpers/nrf52/TechoBoard.cpp

@@ -24,19 +24,6 @@ void TechoBoard::begin() {
   // for future use, sub-classes SHOULD call this from their begin()
   startup_reason = BD_STARTUP_NORMAL;
 
-  delay(200);
-  pinMode(PIN_PWR_EN, OUTPUT);
-  digitalWrite(PIN_PWR_EN, HIGH);
-  pinMode(PIN_BUTTON1, INPUT_PULLUP);
-  pinMode(PIN_BUTTON2, INPUT_PULLUP);
-  pinMode(LED_RED, OUTPUT);
-  pinMode(LED_GREEN, OUTPUT);
-  pinMode(LED_BLUE, OUTPUT);
-  delay(200);
-
-  pinMode(PIN_TXCO, OUTPUT);
-  digitalWrite(PIN_TXCO, HIGH);
-
   Wire.begin();
 
   pinMode(SX126X_POWER_EN, OUTPUT);

+ 95 - 0
src/helpers/nrf52/XiaoNrf52Board.cpp

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

+ 75 - 0
src/helpers/nrf52/XiaoNrf52Board.h

@@ -0,0 +1,75 @@
+#pragma once
+
+#include <MeshCore.h>
+#include <Arduino.h>
+
+#ifdef XIAO_NRF52
+
+// LoRa radio module pins for Seeed Xiao-nrf52
+#ifdef SX1262_XIAO_S3_VARIANT
+  #define  P_LORA_DIO_1       D0
+  #define  P_LORA_BUSY        D1
+  #define  P_LORA_RESET       D2
+  #define  P_LORA_NSS         D3
+  #define  LORA_TX_BOOST_PIN  D4
+#else
+  #define  P_LORA_DIO_1       D1
+  #define  P_LORA_BUSY        D3
+  #define  P_LORA_RESET       D2
+  #define  P_LORA_NSS         D4
+  #define  LORA_TX_BOOST_PIN  D5
+#endif
+#define  P_LORA_SCLK        PIN_SPI_SCK
+#define  P_LORA_MISO        PIN_SPI_MISO
+#define  P_LORA_MOSI        PIN_SPI_MOSI
+//#define  SX126X_POWER_EN  37
+
+#define SX126X_DIO2_AS_RF_SWITCH  true
+#define SX126X_DIO3_TCXO_VOLTAGE   1.8
+
+
+class XiaoNrf52Board : public mesh::MainBoard {
+protected:
+  uint8_t startup_reason;
+
+public:
+  void begin();
+  uint8_t getStartupReason() const override { return startup_reason; }
+
+#if defined(P_LORA_TX_LED)
+  void onBeforeTransmit() override {
+    digitalWrite(P_LORA_TX_LED, LOW);   // turn TX LED on
+  }
+  void onAfterTransmit() override {
+    digitalWrite(P_LORA_TX_LED, HIGH);   // turn TX LED off
+  }
+#endif
+
+  uint16_t getBattMilliVolts() override {
+    // Please read befor going further ;)
+    // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+
+    // We can't drive VBAT_ENABLE to HIGH as long 
+    // as we don't know wether we are charging or not ...
+    // this is a 3mA loss (4/1500)
+    digitalWrite(VBAT_ENABLE, LOW);
+    int adcvalue = 0;
+    analogReadResolution(12);
+    analogReference(AR_INTERNAL_3_0);  
+    delay(10);
+    adcvalue = analogRead(PIN_VBAT);
+    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
+  }
+
+  const char* getManufacturerName() const override {
+    return "Seeed Xiao-nrf52";
+  }
+
+  void reboot() override {
+    NVIC_SystemReset();
+  }
+
+  bool startOTAUpdate(const char* id, char reply[]) override;
+};
+
+#endif

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

@@ -7,7 +7,7 @@ class DisplayDriver {
 protected:
   DisplayDriver(int w, int h) { _w = w; _h = h; }
 public:
-  enum Color { DARK, LIGHT };
+  enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light
 
   int width() const { return _w; }
   int height() const { return _h; }

+ 99 - 0
src/helpers/ui/GxEPDDisplay.cpp

@@ -0,0 +1,99 @@
+
+#include "GxEPDDisplay.h"
+
+bool GxEPDDisplay::begin() {
+  display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
+  SPI1.begin();
+  display.init(115200, true, 2, false);
+  display.setRotation(3);
+  #ifdef TECHO_ZOOM
+    display.setFont(&FreeMono9pt7b);
+  #endif
+  display.setPartialWindow(0, 0, display.width(), display.height());
+
+  display.fillScreen(GxEPD_WHITE);
+  display.display(true);
+  #if DISP_BACKLIGHT
+  pinMode(DISP_BACKLIGHT, OUTPUT);
+  #endif
+  _init = true;
+  return true;
+}
+
+void GxEPDDisplay::turnOn() {
+  if (!_init) begin();
+#if DISP_BACKLIGHT
+  digitalWrite(DISP_BACKLIGHT, HIGH);
+  _isOn = true;
+#endif
+}
+
+void GxEPDDisplay::turnOff() {
+#if DISP_BACKLIGHT
+  digitalWrite(DISP_BACKLIGHT, LOW);
+#endif
+  _isOn = false;
+}
+
+void GxEPDDisplay::clear() {
+  display.fillScreen(GxEPD_WHITE);
+  display.setTextColor(GxEPD_BLACK);
+}
+
+void GxEPDDisplay::startFrame(Color bkg) {
+  display.fillScreen(GxEPD_WHITE);
+}
+
+void GxEPDDisplay::setTextSize(int sz) {
+  display.setTextSize(sz);
+}
+
+void GxEPDDisplay::setColor(Color c) {
+  display.setTextColor(GxEPD_BLACK);
+}
+
+void GxEPDDisplay::setCursor(int x, int y) {
+#ifdef TECHO_ZOOM
+  x = x + (x >> 1);
+  y = y + (y >> 1);
+#endif
+  display.setCursor(x, (y+10));
+}
+
+void GxEPDDisplay::print(const char* str) {
+  display.print(str);
+}
+
+void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
+#ifdef TECHO_ZOOM
+  x = x + (x >> 1);
+  y = y + (y >> 1);
+  w = w + (w >> 1);
+  h = h + (h >> 1);
+#endif
+  display.fillRect(x, y, w, h, GxEPD_BLACK);
+}
+
+void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
+#ifdef TECHO_ZOOM
+  x = x + (x >> 1);
+  y = y + (y >> 1);
+  w = w + (w >> 1);
+  h = h + (h >> 1);
+#endif
+  display.drawRect(x, y, w, h, GxEPD_BLACK);
+}
+
+void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
+#ifdef TECHO_ZOOM
+  x = x + (x >> 1);
+  y = y + (y >> 1);
+  w = w + (w >> 1);
+  h = h + (h >> 1);
+#endif
+  display.drawBitmap(x*1.5, (y*1.5) + 10, bits, w, h, GxEPD_BLACK);
+}
+
+void GxEPDDisplay::endFrame() {
+  display.display(true);
+}

+ 51 - 0
src/helpers/ui/GxEPDDisplay.h

@@ -0,0 +1,51 @@
+#pragma once
+
+#include <SPI.h>
+#include <Wire.h>
+
+#define ENABLE_GxEPD2_GFX 0
+
+#include <GxEPD2_BW.h>
+#include <GxEPD2_3C.h>
+#include <GxEPD2_4C.h>
+#include <GxEPD2_7C.h>
+#include <Fonts/FreeMono9pt7b.h>
+
+#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
+#define GxEPD2_DRIVER_CLASS GxEPD2_150_BN  // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1
+
+#include <epd/GxEPD2_150_BN.h>  // 1.54" b/w 
+
+#include "DisplayDriver.h"
+
+//GxEPD2_BW<GxEPD2_150_BN, 200> display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)); // DEPG0150BN 200x200, SSD1681, TTGO T5 V2.4.1                 
+
+
+class GxEPDDisplay : public DisplayDriver {
+
+  GxEPD2_BW<GxEPD2_150_BN, 200> display;
+  bool _init = false;
+  bool _isOn = false;
+
+public:
+  // there is a margin in y...
+  GxEPDDisplay() : DisplayDriver(200, 200-10), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) { 
+
+  }
+
+  bool begin();
+
+  bool isOn() override {return _isOn;}; 
+  void turnOn() override;
+  void turnOff() override;
+  void clear() override;
+  void startFrame(Color bkg = DARK) override;
+  void setTextSize(int sz) override;
+  void setColor(Color c) override;
+  void setCursor(int x, int y) override;
+  void print(const char* str) override;
+  void fillRect(int x, int y, int w, int h) override;
+  void drawRect(int x, int y, int w, int h) override;
+  void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
+  void endFrame() override;
+};

+ 1 - 1
src/helpers/ui/SSD1306Display.cpp

@@ -38,7 +38,7 @@ void SSD1306Display::setTextSize(int sz) {
 }
 
 void SSD1306Display::setColor(Color c) {
-  _color = (c == LIGHT) ? SSD1306_WHITE : SSD1306_BLACK;
+  _color = (c != 0) ? SSD1306_WHITE : SSD1306_BLACK;
   display.setTextColor(_color);
 }
 

+ 117 - 0
src/helpers/ui/ST7789Display.cpp

@@ -0,0 +1,117 @@
+#ifdef ST7789
+
+#include "ST7789Display.h"
+
+bool ST7789Display::i2c_probe(TwoWire& wire, uint8_t addr) {
+  return true;
+/*
+  wire.beginTransmission(addr);
+  uint8_t error = wire.endTransmission();
+  return (error == 0);
+*/
+}
+
+bool ST7789Display::begin() {
+  if(!_isOn) {
+    pinMode(PIN_TFT_VDD_CTL, OUTPUT);
+    pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
+    digitalWrite(PIN_TFT_VDD_CTL, LOW);
+    digitalWrite(PIN_TFT_LEDA_CTL, LOW);
+    digitalWrite(PIN_TFT_RST, HIGH);
+
+    display.init(135, 240);
+    display.setRotation(2);
+    display.setSPISpeed(40000000);
+    display.fillScreen(ST77XX_BLACK);
+    display.setTextColor(ST77XX_WHITE);
+    display.setTextSize(2);
+    display.cp437(true);         // Use full 256 char 'Code Page 437' font
+  
+    _isOn = true;
+  }
+  return true;
+}
+
+void ST7789Display::turnOn() {
+  ST7789Display::begin();
+}
+
+void ST7789Display::turnOff() {
+  digitalWrite(PIN_TFT_VDD_CTL, HIGH);
+  digitalWrite(PIN_TFT_LEDA_CTL, HIGH);
+  digitalWrite(PIN_TFT_RST, LOW);
+  // digitalWrite(PIN_TFT_VDD_CTL, LOW);
+  // digitalWrite(PIN_TFT_LEDA_CTL, LOW);
+  _isOn = false;
+}
+
+void ST7789Display::clear() {
+  display.fillScreen(ST77XX_BLACK);
+}
+
+void ST7789Display::startFrame(Color bkg) {
+  display.fillScreen(0x00);
+  display.setTextColor(ST77XX_WHITE);
+  display.setTextSize(2);
+  display.cp437(true);         // Use full 256 char 'Code Page 437' font
+}
+
+void ST7789Display::setTextSize(int sz) {
+  display.setTextSize(sz);
+}
+
+void ST7789Display::setColor(Color c) {
+  switch (c) {
+    case DisplayDriver::DARK :
+      _color = ST77XX_BLACK;
+      break;
+    case DisplayDriver::LIGHT : 
+      _color = ST77XX_WHITE;
+      break;
+    case DisplayDriver::RED : 
+      _color = ST77XX_RED;
+      break;
+    case DisplayDriver::GREEN : 
+      _color = ST77XX_GREEN;
+      break;
+    case DisplayDriver::BLUE : 
+      _color = ST77XX_BLUE;
+      break;
+    case DisplayDriver::YELLOW : 
+      _color = ST77XX_YELLOW;
+      break;
+    case DisplayDriver::ORANGE : 
+      _color = ST77XX_ORANGE;
+      break;
+    default:
+      _color = ST77XX_WHITE;
+      break;
+  }
+  display.setTextColor(_color);
+}
+
+void ST7789Display::setCursor(int x, int y) {
+  display.setCursor(x, y);
+}
+
+void ST7789Display::print(const char* str) {
+  display.print(str);
+}
+
+void ST7789Display::fillRect(int x, int y, int w, int h) {
+  display.fillRect(x, y, w, h, _color);
+}
+
+void ST7789Display::drawRect(int x, int y, int w, int h) {
+  display.drawRect(x, y, w, h, _color);
+}
+
+void ST7789Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
+  display.drawBitmap(x, y, bits, w, h, _color);
+}
+
+void ST7789Display::endFrame() {
+  // display.display();
+}
+
+#endif

+ 33 - 0
src/helpers/ui/ST7789Display.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include "DisplayDriver.h"
+#include <Wire.h>
+#include <SPI.h>
+#include <Adafruit_GFX.h>
+#include <Adafruit_ST7789.h>
+
+class ST7789Display : public DisplayDriver {
+  Adafruit_ST7789 display;
+  bool _isOn;
+  uint16_t _color;
+
+  bool i2c_probe(TwoWire& wire, uint8_t addr);
+public:
+  ST7789Display() : DisplayDriver(135, 240), display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST) { _isOn = false; }
+//  ST7789Display() : DisplayDriver(135, 240), display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST) { _isOn = false; }
+  bool begin();
+
+  bool isOn() override { return _isOn; }
+  void turnOn() override;
+  void turnOff() override;
+  void clear() override;
+  void startFrame(Color bkg = DARK) override;
+  void setTextSize(int sz) override;
+  void setColor(Color c) override;
+  void setCursor(int x, int y) override;
+  void print(const char* str) override;
+  void fillRect(int x, int y, int w, int h) override;
+  void drawRect(int x, int y, int w, int h) override;
+  void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
+  void endFrame() override;
+};

+ 4 - 4
variants/heltec_v2/platformio.ini

@@ -32,7 +32,7 @@ build_flags =
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v2.build_src_filter}
   +<../examples/simple_repeater>
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
 lib_deps =
   ${Heltec_lora32_v2.lib_deps}
   ${esp32_ota.lib_deps}
@@ -50,7 +50,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v2.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_room_server>
 lib_deps =
   ${Heltec_lora32_v2.lib_deps}
@@ -81,7 +81,7 @@ build_flags =
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v2.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${Heltec_lora32_v2.lib_deps}
@@ -102,7 +102,7 @@ build_flags =
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v2.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${Heltec_lora32_v2.lib_deps}

+ 5 - 5
variants/heltec_v3/platformio.ini

@@ -34,7 +34,7 @@ build_flags =
   -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
   ${Heltec_lora32_v3.lib_deps}
@@ -53,7 +53,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_room_server>
 lib_deps =
   ${Heltec_lora32_v3.lib_deps}
@@ -85,7 +85,7 @@ build_flags =
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${Heltec_lora32_v3.lib_deps}
@@ -105,7 +105,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/esp32/*.cpp>
   +<../examples/companion_radio>
 lib_deps =
@@ -127,7 +127,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${Heltec_lora32_v3.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<helpers/esp32/*.cpp>
   +<../examples/companion_radio>
 lib_deps =

+ 4 - 4
variants/lilygo_t3s3/platformio.ini

@@ -45,7 +45,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
   ${LilyGo_T3S3_sx1262.lib_deps}
@@ -78,7 +78,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_room_server>
 lib_deps =
   ${LilyGo_T3S3_sx1262.lib_deps}
@@ -96,7 +96,7 @@ build_flags =
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${LilyGo_T3S3_sx1262.lib_deps}
@@ -117,7 +117,7 @@ build_flags =
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${LilyGo_T3S3_sx1262.lib_deps}

+ 3 - 3
variants/lilygo_tbeam/platformio.ini

@@ -27,7 +27,7 @@ build_flags =
   ${LilyGo_TBeam.build_flags}
   -D DISPLAY_CLASS=SSD1306Display
   -D MAX_CONTACTS=100
-  -D MAX_GROUP_CHANNELS=1
+  -D MAX_GROUP_CHANNELS=8
   -D BLE_PIN_CODE=123456
   -D BLE_DEBUG_LOGGING=1
   -D MESH_PACKET_LOGGING=1
@@ -39,7 +39,7 @@ build_flags =
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TBeam.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${LilyGo_TBeam.lib_deps}
@@ -57,7 +57,7 @@ build_flags =
   -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TBeam.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
   ${LilyGo_TBeam.lib_deps}

+ 72 - 0
variants/lilygo_tbeam_supreme_SX1262/platformio.ini

@@ -0,0 +1,72 @@
+[T_Beam_S3_Supreme_SX1262]
+extends = esp32_base
+board = t_beams3_supreme  ; LILYGO T-Beam Supreme ESP32S3 with SX1262
+build_flags =
+  ${esp32_base.build_flags}
+  -I variants/lilygo_tbeam_supreme_SX1262
+  -D LORA_TX_POWER=22
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  ;-D DISPLAY_CLASS=SSD1306Display      ;Needs to be modified for SH1106
+  -D SX126X_RX_BOOSTED_GAIN=1
+build_src_filter = ${esp32_base.build_src_filter}
+  +<../variants/lilygo_tbeam_supreme_SX1262>
+board_build.partitions = min_spiffs.csv ; get around 4mb flash limit
+lib_deps =
+  ${esp32_base.lib_deps}
+  lewisxhe/PCF8563_Library@^1.0.1
+  lewisxhe/XPowersLib @ ^0.2.7
+  ;adafruit/Adafruit SSD1306 @ ^2.5.13
+
+; === LILYGO T-Beam S3 Supreme with SX1262 environments ===
+[env:T_Beam_S3_Supreme_SX1262_repeater]
+extends = T_Beam_S3_Supreme_SX1262
+build_flags =
+  ${T_Beam_S3_Supreme_SX1262.build_flags}
+  -D ADVERT_NAME='"T-Beam S3 Supreme SX1262 Repeater"'
+  -D ADVERT_LAT=0
+  -D ADVERT_LON=0
+  -D ADMIN_PASSWORD='"password"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter}
+  +<../examples/simple_repeater>
+lib_deps =
+  ${T_Beam_S3_Supreme_SX1262.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:T_Beam_S3_Supreme_SX1262_room_server]
+extends = T_Beam_S3_Supreme_SX1262
+build_flags =
+  ${T_Beam_S3_Supreme_SX1262.build_flags}
+  -D ADVERT_NAME='"T_Beam_S3_Supreme_SX1262 Room"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+  -D ROOM_PASSWORD='"hello"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter}
+  +<../examples/simple_room_server>
+lib_deps =
+  ${T_Beam_S3_Supreme_SX1262.lib_deps}
+  ${esp32_ota.lib_deps}
+
+[env:T_Beam_S3_Supreme_SX1262_companion_radio_ble]
+extends = T_Beam_S3_Supreme_SX1262
+build_flags =
+  ${T_Beam_S3_Supreme_SX1262.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=1
+  -D BLE_PIN_CODE=123456
+  -D BLE_DEBUG_LOGGING=1
+;  -D ENABLE_PRIVATE_KEY_IMPORT=1
+;  -D ENABLE_PRIVATE_KEY_EXPORT=1
+  -D MESH_PACKET_LOGGING=8
+  -D MESH_DEBUG=1
+build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter}
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio>
+lib_deps =
+  ${T_Beam_S3_Supreme_SX1262.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 182 - 0
variants/lilygo_tbeam_supreme_SX1262/target.cpp

@@ -0,0 +1,182 @@
+#include <Arduino.h>
+#include "target.h"
+
+TBeamS3SupremeBoard board;
+
+// Using PMU AXP2102
+XPowersAXP2101 PMU;
+
+bool pmuIntFlag;
+
+#ifndef LORA_CR
+  #define LORA_CR      5
+#endif
+
+#if defined(P_LORA_SCLK)
+  static SPIClass spi;
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
+#else
+  RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
+#endif
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+ESP32RTCClock fallback_clock;
+AutoDiscoverRTCClock rtc_clock(fallback_clock);
+
+
+static void setPMUIntFlag(){
+  pmuIntFlag = true;
+}
+
+bool power_init() {
+  //Start up Wire1 with PMU address
+  //Serial.println("Starting Wire1 for PMU");
+  //Wire1.begin(I2C_PMU_ADD);
+  //Wire1.begin(PIN_BOARD_SDA1,PIN_BOARD_SCL1);
+  
+  //Set LED to indicate charge state
+  Serial.println("Setting charge led");
+  PMU.setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG);  
+  
+  //Set up PMU interrupts
+  Serial.println("Setting up PMU interrupts");
+  pinMode(PIN_PMU_IRQ,INPUT_PULLUP);
+  attachInterrupt(PIN_PMU_IRQ,setPMUIntFlag,FALLING);
+
+  //GPS
+  Serial.println("Setting and enabling a-ldo4 for GPS");
+  PMU.setALDO4Voltage(3300);  
+  PMU.enableALDO4();          //disable to save power
+  
+  //Lora
+  Serial.println("Setting and enabling a-ldo3 for LoRa");
+  PMU.setALDO3Voltage(3300);  
+  PMU.enableALDO3();   
+
+  //To avoid SPI bus issues during power up, reset OLED, sensor, and SD card supplies
+  Serial.println("Reset a-ldo1&2 and b-ldo1");
+  if(ESP_SLEEP_WAKEUP_UNDEFINED == esp_sleep_get_wakeup_cause()){
+    PMU.enableALDO1();
+    PMU.enableALDO2();
+    PMU.enableBLDO1();
+    delay(250);
+  }
+ 
+  //BME280 and OLED
+  Serial.println("Setting and enabling a-ldo1 for oled");
+  PMU.setALDO1Voltage(3300);
+  PMU.enableALDO1();
+
+  //QMC6310U 
+  Serial.println("Setting and enabling a-ldo2 for QMC");
+  PMU.setALDO2Voltage(3300);
+  PMU.enableALDO2();          //disable to save power
+
+  //SD card
+  Serial.println("Setting and enabling b-ldo2 for SD card");
+  PMU.setBLDO1Voltage(3300);
+  PMU.enableBLDO1();
+
+  //Out to header pins
+  Serial.println("Setting and enabling b-ldo2 for output to header");
+  PMU.setBLDO2Voltage(3300);
+  PMU.enableBLDO2();
+
+  Serial.println("Setting and enabling dcdc4 for output to header");
+  PMU.setDC4Voltage(XPOWERS_AXP2101_DCDC4_VOL2_MAX);    //1.8V
+  PMU.enableDC4();
+
+  Serial.println("Setting and enabling dcdc5 for output to header");
+  PMU.setDC5Voltage(3300);
+  PMU.enableDC5();
+
+  //Other power rails
+  Serial.println("Setting and enabling dcdc3 for ?");
+  PMU.setDC3Voltage(3300);    //doesn't go anywhere in the schematic??
+  PMU.enableDC3();
+
+  //Unused power rails
+  Serial.println("Disabling unused supplies dcdc2, dldo1 and dldo2");
+  PMU.disableDC2();
+  PMU.disableDLDO1();
+  PMU.disableDLDO2();  
+
+  //Set charge current to 300mA
+  Serial.println("Setting battery charge current limit and voltage");
+  PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_300MA);
+  PMU.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
+
+  //enable battery voltage measurement
+  Serial.println("Enabling battery measurement");
+  PMU.enableBattVoltageMeasure();
+
+  //Reset and re-enable PMU interrupts
+  Serial.println("Re-enable interrupts");
+  PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
+  PMU.clearIrqStatus();
+  PMU.enableIRQ(
+    XPOWERS_AXP2101_BAT_INSERT_IRQ    | XPOWERS_AXP2101_BAT_REMOVE_IRQ      |   //Battery interrupts
+    XPOWERS_AXP2101_VBUS_INSERT_IRQ   | XPOWERS_AXP2101_VBUS_REMOVE_IRQ     |   //VBUS interrupts
+    XPOWERS_AXP2101_PKEY_SHORT_IRQ    | XPOWERS_AXP2101_PKEY_LONG_IRQ       |   //Power Key interrupts
+    XPOWERS_AXP2101_BAT_CHG_DONE_IRQ  | XPOWERS_AXP2101_BAT_CHG_START_IRQ       //Charging interrupts
+  );
+
+  //Set the power key off press time
+  PMU.setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
+
+  return true;
+}
+
+bool radio_init() {
+  fallback_clock.begin();
+  Wire1.begin(PIN_BOARD_SDA1,PIN_BOARD_SCL1);
+  rtc_clock.begin(Wire1);
+  
+#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);
+  
+  return true;  // success
+}
+
+uint16_t getBattPercent() {
+  //Read the PMU fuel guage for battery %
+  uint16_t battPercent = PMU.getBatteryPercent();
+
+  return battPercent;
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 17 - 0
variants/lilygo_tbeam_supreme_SX1262/target.h

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

+ 5 - 5
variants/lilygo_tlora_v2_1/platformio.ini

@@ -34,7 +34,7 @@ lib_deps =
 [env:LilyGo_TLora_V2_1_1_6_Repeater]
 extends = LilyGo_TLora_V2_1_1_6
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 build_flags =
   ${LilyGo_TLora_V2_1_1_6.build_flags}
@@ -58,7 +58,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 lib_deps =
   ${LilyGo_TLora_V2_1_1_6.lib_deps}
@@ -75,7 +75,7 @@ build_flags =
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${LilyGo_TLora_V2_1_1_6.lib_deps}
@@ -95,7 +95,7 @@ build_flags =
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
   +<helpers/esp32/*.cpp>
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${LilyGo_TLora_V2_1_1_6.lib_deps}
@@ -104,7 +104,7 @@ lib_deps =
 [env:LilyGo_TLora_V2_1_1_6_room_server]
 extends = LilyGo_TLora_V2_1_1_6
 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_room_server>
 build_flags =
   ${LilyGo_TLora_V2_1_1_6.build_flags}

+ 9 - 6
variants/rak4631/platformio.ini

@@ -7,13 +7,16 @@ build_flags = ${nrf52840_base.build_flags}
   -I variants/rak4631
   -D RAK_4631
   -D PIN_USER_BTN=9
+  -D PIN_BOARD_SCL=14
+  -D PIN_BOARD_SDA=13
+  -D PIN_OLED_RESET=-1
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper
   -D LORA_TX_POWER=22
   -D SX126X_CURRENT_LIMIT=130
   -D SX126X_RX_BOOSTED_GAIN=1
 build_src_filter = ${nrf52840_base.build_src_filter}
-  +<helpers/nrf52/*.cpp>
+  +<helpers/nrf52/RAK4631Board.cpp>
   +<../variants/rak4631>
 lib_deps =
   ${nrf52840_base.lib_deps}
@@ -31,7 +34,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${rak4631.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_repeater>
 
 [env:RAK_4631_room_server]
@@ -47,7 +50,7 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${rak4631.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/simple_room_server>
 
 [env:RAK_4631_companion_radio_usb]
@@ -62,7 +65,7 @@ build_flags =
 ; NOTE: DO NOT ENABLE -->  -D MESH_PACKET_LOGGING=1
 ; NOTE: DO NOT ENABLE -->  -D MESH_DEBUG=1
 build_src_filter = ${rak4631.build_src_filter}
-  +<helpers/ui/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${rak4631.lib_deps}
@@ -82,8 +85,8 @@ build_flags =
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${rak4631.build_src_filter}
-  +<helpers/ui/*.cpp>
-  +<helpers/nrf52/*.cpp>
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/nrf52/SerialBLEInterface.cpp>
   +<../examples/companion_radio>
 lib_deps =
   ${rak4631.lib_deps}

+ 2 - 2
variants/t1000-e/variant.cpp

@@ -83,13 +83,13 @@ void initVariant()
   pinMode(GPS_RTC_INT, OUTPUT);
   pinMode(LED_PIN, OUTPUT);
 
-  digitalWrite(PIN_3V3_EN, HIGH);
+  digitalWrite(PIN_3V3_EN, LOW);
   digitalWrite(PIN_3V3_ACC_EN, LOW);
   digitalWrite(BUZZER_EN, LOW);
   digitalWrite(SENSOR_EN, LOW);
   digitalWrite(GPS_EN, LOW);
   digitalWrite(GPS_RESET, LOW);
-  digitalWrite(GPS_VRTC_EN, HIGH);
+  digitalWrite(GPS_VRTC_EN, LOW);
   digitalWrite(GPS_SLEEP_INT, HIGH);
   digitalWrite(GPS_RTC_INT, LOW);
   digitalWrite(LED_PIN, LOW);

+ 31 - 0
variants/t114/platformio.ini

@@ -74,3 +74,34 @@ build_src_filter = ${Heltec_t114.build_src_filter}
 lib_deps =
   ${Heltec_t114.lib_deps}
   densaugeo/base64 @ ~1.4.0
+
+[env:Heltec_t114_companion_radio_ble_screen]
+extends = env:Heltec_t114_companion_radio_ble
+build_flags = ${env:Heltec_t114_companion_radio_ble.build_flags}
+  -I src/helpers/ui
+  -D ST7789
+  -D DISPLAY_CLASS=ST7789Display
+build_src_filter = ${env:Heltec_t114_companion_radio_ble.build_src_filter}
+  +<../examples/companion_radio/UITask.cpp>
+  +<helpers/ui/ST7789Display.cpp>
+lib_deps = ${env:Heltec_t114_companion_radio_ble.lib_deps}
+  adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
+
+[env:Heltec_t114_companion_radio_usb]
+extends = Heltec_t114
+build_flags =
+  ${Heltec_t114.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+;  -D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+;  -D ENABLE_PRIVATE_KEY_IMPORT=1
+;  -D ENABLE_PRIVATE_KEY_EXPORT=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Heltec_t114.build_src_filter}
+  +<helpers/nrf52/*.cpp>
+  +<../examples/companion_radio/main.cpp>
+lib_deps =
+  ${Heltec_t114.lib_deps}
+  densaugeo/base64 @ ~1.4.0

+ 16 - 3
variants/t114/variant.h

@@ -80,11 +80,13 @@
 ////////////////////////////////////////////////////////////////////////////////
 // Builtin buttons
 
-#define PIN_BUTTON1             (5)
+#define PIN_BUTTON1             (42)
 #define BUTTON_PIN              PIN_BUTTON1
 
-#define PIN_BUTTON2             (11)
-#define BUTTON_PIN2             PIN_BUTTON2
+// #define PIN_BUTTON2             (11)
+// #define BUTTON_PIN2             PIN_BUTTON2
+
+#define PIN_USER_BTN            BUTTON_PIN
 
 #define EXTERNAL_FLASH_DEVICES MX25R1635F
 #define EXTERNAL_FLASH_USE_QSPI
@@ -108,3 +110,14 @@
 // Buzzer
 
 #define PIN_BUZZER              (46)
+
+
+////////////////////////////////////////////////////////////////////////////////
+// TFT
+#define PIN_TFT_SCL             (40)
+#define PIN_TFT_SDA             (41)
+#define PIN_TFT_RST             (2)
+#define PIN_TFT_VDD_CTL         (3)
+#define PIN_TFT_LEDA_CTL        (15)
+#define PIN_TFT_CS              (11)
+#define PIN_TFT_DC              (12)

+ 8 - 2
variants/techo/platformio.ini

@@ -56,17 +56,23 @@ build_flags =
 extends = LilyGo_Techo
 build_flags =
   ${LilyGo_Techo.build_flags}
+  -I src/helpers/ui
   -D MAX_CONTACTS=100
   -D MAX_GROUP_CHANNELS=8
   -D BLE_PIN_CODE=123456
   -D BLE_DEBUG_LOGGING=1
+  -D DISPLAY_CLASS=GxEPDDisplay
+  -D HAS_GxEPD
 ;  -D ENABLE_PRIVATE_KEY_IMPORT=1
 ;  -D ENABLE_PRIVATE_KEY_EXPORT=1
 ;  -D MESH_PACKET_LOGGING=1
 ;  -D MESH_DEBUG=1
 build_src_filter = ${LilyGo_Techo.build_src_filter}
-  +<helpers/nrf52/*.cpp>
-  +<../examples/companion_radio/main.cpp>
+  +<helpers/nrf52/TechoBoard.cpp>
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<helpers/ui/GxEPDDisplay.cpp>
+  +<../examples/companion_radio>
 lib_deps =
   ${LilyGo_Techo.lib_deps}
   densaugeo/base64 @ ~1.4.0
+  zinggjm/GxEPD2 @ 1.6.2

+ 19 - 7
variants/techo/variant.cpp

@@ -2,6 +2,10 @@
 #include "wiring_constants.h"
 #include "wiring_digital.h"
 
+const int MISO = PIN_SPI1_MISO;
+const int MOSI = PIN_SPI1_MOSI;
+const int SCK = PIN_SPI1_SCK;
+
 const uint32_t g_ADigitalPinMap[] = {
   0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
   14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
@@ -9,14 +13,22 @@ const uint32_t g_ADigitalPinMap[] = {
   40, 41, 42, 43, 44, 45, 46, 47
 };
 
-void initVariant()
-{
-  pinMode(LED_RED, OUTPUT);
-  ledOff(LED_RED);
+void initVariant() {
+  pinMode(PIN_PWR_EN, OUTPUT);
+  digitalWrite(PIN_PWR_EN, HIGH);
 
-  pinMode(LED_GREEN, OUTPUT);
-  ledOff(LED_GREEN);
+  pinMode(PIN_BUTTON1, INPUT_PULLUP);
+  pinMode(PIN_BUTTON2, INPUT_PULLUP);
 
+  pinMode(LED_RED, OUTPUT);
+  pinMode(LED_GREEN, OUTPUT);
   pinMode(LED_BLUE, OUTPUT);
-  ledOff(LED_BLUE);
+  digitalWrite(LED_BLUE, HIGH);
+
+  pinMode(PIN_TXCO, OUTPUT);
+  digitalWrite(PIN_TXCO, HIGH);
+
+  // shutdown gps
+  pinMode(PIN_GPS_STANDBY, OUTPUT);
+  digitalWrite(PIN_GPS_STANDBY, LOW);
 }

+ 33 - 2
variants/techo/variant.h

@@ -42,7 +42,6 @@
 
 #define PIN_SERIAL1_RX          (41) // GPS TX
 #define PIN_SERIAL1_TX          (40) // GPS RX
-
 ////////////////////////////////////////////////////////////////////////////////
 // I2C pin definition
 
@@ -66,6 +65,7 @@
 #define LED_GREEN               (33)
 #define LED_BLUE                (14)
 
+#define PIN_STATUS_LED          LED_GREEN
 #define LED_BUILTIN             LED_GREEN
 #define PIN_LED                 LED_BUILTIN
 #define LED_PIN                 LED_BUILTIN
@@ -79,6 +79,7 @@
 
 #define PIN_BUTTON1             (42)
 #define BUTTON_PIN              PIN_BUTTON1
+#define PIN_USER_BTN            BUTTON_PIN
 
 #define PIN_BUTTON2             (11)
 #define BUTTON_PIN2             PIN_BUTTON2
@@ -97,6 +98,36 @@
 #define SX126X_DIO2_AS_RF_SWITCH
 #define SX126X_DIO3_TCXO_VOLTAGE 1.8
 
-#define PIN_SPI1_MISO           (39)
+////////////////////////////////////////////////////////////////////////////////
+// SPI1
+
+#define PIN_SPI1_MISO           (38)
 #define PIN_SPI1_MOSI           (29)
 #define PIN_SPI1_SCK            (31)
+
+// GxEPD2 needs that for a panel that is not even used !
+extern const int MISO;
+extern const int MOSI;
+extern const int SCK;
+
+////////////////////////////////////////////////////////////////////////////////
+// Display
+
+#define DISP_MISO               (38)
+#define DISP_MOSI               (29)
+#define DISP_SCLK               (31)
+#define DISP_CS                 (30)
+#define DISP_DC                 (28)
+#define DISP_RST                (2)
+#define DISP_BUSY               (3)
+#define DISP_BACKLIGHT          (43)
+
+////////////////////////////////////////////////////////////////////////////////
+// GPS
+
+#define PIN_GPS_RX              (41)
+#define PIN_GPS_TX              (40)
+#define PIN_GPS_WAKEUP          (34)
+#define PIN_GPS_RESET           (37)
+#define PIN_GPS_PPS             (36)
+#define PIN_GPS_STANDBY         (34)

+ 81 - 0
variants/xiao_nrf52/platformio.ini

@@ -0,0 +1,81 @@
+[nrf52840_xiao]
+extends = nrf52_base
+platform_packages =
+  toolchain-gccarmnoneeabi@~1.100301.0
+  framework-arduinoadafruitnrf52
+board = seeed-xiao-afruitnrf52-nrf52840
+board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52_base.build_flags}
+  -D NRF52_PLATFORM -D XIAO_NRF52
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include
+  -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
+lib_ignore =
+  BluetoothOTA
+  lvgl
+  lib5b4
+lib_deps =
+  ${nrf52_base.lib_deps}
+  rweather/Crypto @ ^0.4.0
+
+[Xiao_nrf52]
+extends = nrf52840_xiao
+;board_build.ldscript = boards/nrf52840_s140_v7.ld
+build_flags = ${nrf52840_xiao.build_flags}
+  -D P_LORA_TX_LED=11
+  -I variants/xiao_nrf52
+  -I src/helpers/nrf52
+  -D RADIO_CLASS=CustomSX1262
+  -D WRAPPER_CLASS=CustomSX1262Wrapper
+  -D LORA_TX_POWER=22
+  -D SX126X_CURRENT_LIMIT=130
+  -D SX126X_RX_BOOSTED_GAIN=1
+build_src_filter = ${nrf52840_xiao.build_src_filter}
+  +<helpers/*.cpp>
+  +<helpers/nrf52/XiaoNrf52Board.cpp>
+  +<../variants/xiao_nrf52>
+debug_tool = jlink
+upload_protocol = nrfutil
+
+[env:Xiao_nrf52_companion_radio_ble]
+extends = Xiao_nrf52
+build_flags =
+  ${Xiao_nrf52.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+  -D BLE_PIN_CODE=123456
+;  -D BLE_DEBUG_LOGGING=1
+;  -D ENABLE_PRIVATE_KEY_IMPORT=1
+;  -D ENABLE_PRIVATE_KEY_EXPORT=1
+  -D MESH_PACKET_LOGGING=1
+  -D MESH_DEBUG=1
+build_src_filter = ${Xiao_nrf52.build_src_filter}
+  +<helpers/nrf52/SerialBLEInterface.cpp>
+  +<../examples/companion_radio/main.cpp>
+lib_deps =
+  ${Xiao_nrf52.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+
+[env:Xiao_nrf52_alt_pinout_companion_radio_ble]
+extends = env:Xiao_nrf52_companion_radio_ble
+build_flags = 
+  ${env:Xiao_nrf52_companion_radio_ble.build_flags}
+  -D SX1262_XIAO_S3_VARIANT
+
+[env:Xiao_nrf52_repeater]
+extends = Xiao_nrf52
+build_flags =
+  ${Xiao_nrf52.build_flags}
+  -D ADVERT_NAME='"Xiao_nrf52 Repeater"'
+  -D ADVERT_LAT=0.0
+  -D ADVERT_LON=0.0
+  -D ADMIN_PASSWORD='"password"'
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Xiao_nrf52.build_src_filter}
+  +<../examples/simple_repeater/main.cpp>
+
+[env:Xiao_nrf52_alt_pinout_repeater]
+extends = env:Xiao_nrf52_repeater
+build_flags = 
+  ${env:Xiao_nrf52_repeater.build_flags}
+  -D SX1262_XIAO_S3_VARIANT

+ 68 - 0
variants/xiao_nrf52/target.cpp

@@ -0,0 +1,68 @@
+#include <Arduino.h>
+#include "target.h"
+#include <helpers/ArduinoHelpers.h>
+
+XiaoNrf52Board board;
+
+RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
+
+WRAPPER_CLASS radio_driver(radio, board);
+
+VolatileRTCClock rtc_clock;
+
+#ifndef LORA_CR
+  #define LORA_CR      5
+#endif
+
+bool radio_init() {
+//  rtc_clock.begin(Wire);
+  
+#ifdef SX126X_DIO3_TCXO_VOLTAGE
+  float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
+#else
+  float tcxo = 1.6f;
+#endif
+
+  SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
+  SPI.begin();
+  int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo);
+  if (status != RADIOLIB_ERR_NONE) {
+    Serial.print("ERROR: radio init failed: ");
+    Serial.println(status);
+    return false;  // fail
+  }
+  
+  radio.setCRC(1);
+  
+#ifdef SX126X_CURRENT_LIMIT
+  radio.setCurrentLimit(SX126X_CURRENT_LIMIT);
+#endif
+#ifdef SX126X_DIO2_AS_RF_SWITCH
+  radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
+#endif
+#ifdef SX126X_RX_BOOSTED_GAIN
+  radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
+#endif
+
+  return true;  // success
+}
+
+uint32_t radio_get_rng_seed() {
+  return radio.random(0x7FFFFFFF);
+}
+
+void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
+  radio.setFrequency(freq);
+  radio.setSpreadingFactor(sf);
+  radio.setBandwidth(bw);
+  radio.setCodingRate(cr);
+}
+
+void radio_set_tx_power(uint8_t dbm) {
+  radio.setOutputPower(dbm);
+}
+
+mesh::LocalIdentity radio_new_identity() {
+  RadioNoiseListener rng(radio);
+  return mesh::LocalIdentity(&rng);  // create new random identity
+}

+ 18 - 0
variants/xiao_nrf52/target.h

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

+ 86 - 0
variants/xiao_nrf52/variant.cpp

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

+ 149 - 0
variants/xiao_nrf52/variant.h

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

+ 24 - 0
variants/xiao_s3_wio/platformio.ini

@@ -13,6 +13,8 @@ build_flags = ${esp32_base.build_flags}
   -D P_LORA_SCLK=7
   -D P_LORA_MISO=8
   -D P_LORA_MOSI=9
+  -D PIN_USER_BTN=21
+  -D PIN_STATUS_LED=48
   -D SX126X_DIO2_AS_RF_SWITCH=true
   -D SX126X_DIO3_TCXO_VOLTAGE=1.8
   -D SX126X_CURRENT_LIMIT=130
@@ -105,3 +107,25 @@ build_src_filter = ${Xiao_S3_WIO.build_src_filter}
 lib_deps =
   ${Xiao_S3_WIO.lib_deps}
   densaugeo/base64 @ ~1.4.0
+
+[env:Xiao_S3_WIO_expansion_companion_radio_ble]
+extends = Xiao_S3_WIO
+build_flags =
+  ${Xiao_S3_WIO.build_flags}
+  -D MAX_CONTACTS=100
+  -D MAX_GROUP_CHANNELS=8
+  -D BLE_PIN_CODE=123456
+  -D DISPLAY_CLASS=SSD1306Display
+;  -D BLE_DEBUG_LOGGING=1
+;  -D ENABLE_PRIVATE_KEY_IMPORT=1
+;  -D ENABLE_PRIVATE_KEY_EXPORT=1
+;  -D MESH_PACKET_LOGGING=1
+;  -D MESH_DEBUG=1
+build_src_filter = ${Xiao_S3_WIO.build_src_filter}
+  +<helpers/ui/SSD1306Display.cpp>
+  +<helpers/esp32/*.cpp>
+  +<../examples/companion_radio>
+lib_deps =
+  ${Xiao_S3_WIO.lib_deps}
+  densaugeo/base64 @ ~1.4.0
+  adafruit/Adafruit SSD1306 @ ^2.5.13

+ 3 - 1
variants/xiao_s3_wio/target.cpp

@@ -22,7 +22,9 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
 bool radio_init() {
   fallback_clock.begin();
   rtc_clock.begin(Wire);
-  
+  pinMode(21, INPUT);
+  pinMode(48, OUTPUT);
+
 #ifdef SX126X_DIO3_TCXO_VOLTAGE
   float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
 #else