]> git.tnoah.ca Git - lora-radio.git/commitdiff
Incorporate changes from upstream 1.82
authorjacob.eva <x00010@disroot.org>
Sat, 31 May 2025 14:28:56 +0000 (15:28 +0100)
committerjacob.eva <x00010@disroot.org>
Sat, 31 May 2025 14:28:56 +0000 (15:28 +0100)
1  2 
Bluetooth.h
Boards.h
Display.h
Framing.h
Makefile
README.md
RNode_Firmware_CE.ino
Release/console_image.bin
Utilities.h
src/ble/BLESerial.cpp
src/ble/BLESerial.h

diff --cc Bluetooth.h
index ebbeb758431207f9b0bbc6f874ef87ebb9918aa3,616c4b6347419dd479f9de9074381b6ea9ff8176..e596ae9e02c8fa5f06e673f79750626cf452ab5b
@@@ -381,19 -401,22 +396,22 @@@ char bt_devname[11]
      }
    }
  
 -  void bt_flush() { if (bt_state == BT_STATE_CONNECTED) { SerialBT.flushTXD(); } }
 -
    void bt_disable_pairing() {
+     // Serial.println("BT Disable pairing");
      bt_allow_pairing = false;
+     pairing_pin = 0;
      bt_ssp_pin = 0;
      bt_state = BT_STATE_ON;
    }
  
 +  void bt_flush() { if (bt_state == BT_STATE_CONNECTED) { SerialBT.flushTXD(); } }
 +
    void bt_pairing_complete(uint16_t conn_handle, uint8_t auth_status) {
+     // Serial.println("BT pairing complete");
+     BLEConnection* connection = Bluefruit.Connection(conn_handle);
      if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
-       BLEConnection* connection = Bluefruit.Connection(conn_handle);
        ble_gap_conn_sec_mode_t security = connection->getSecureMode();
+       // Serial.println("Bonding success");
  
        // On the NRF52 it is not possible with the Arduino library to reject
        // requests from devices with no IO capabilities, which would allow
diff --cc Boards.h
index 649495e91e4612c67e6f81ec60301f5d90f4fccd,410afd37479d61e421a3b34351501bd6919f97eb..3a41183d267266e72e6eead11ce152ed690e5410
+++ b/Boards.h
          #endif
        #endif
  
 -    #elif BOARD_MODEL == BOARD_XIAO_S3
 +    #elif BOARD_MODEL == BOARD_E22_ESP32
 +      #define HAS_DISPLAY true
 +      #define DISPLAY OLED
 +      // currently there is only support for using one Bluetooth type,
 +      // Bluetooth has been chosen over BLE as it is less experimental
 +      #define HAS_BLUETOOTH true
 +      //#define HAS_BLE true 
 +      #define HAS_CONSOLE true
 +      #define HAS_SD false
 +      #define HAS_EEPROM true
 +      #define I2C_SDA 21
 +      #define I2C_SCL 22
 +      #define INTERFACE_COUNT 1
 +      const int pin_led_rx = 2;
 +      const int pin_led_tx = 4;
 +
 +      const uint8_t interfaces[INTERFACE_COUNT] = {SX1262};
 +      const bool interface_cfg[INTERFACE_COUNT][3] = { 
 +                    // SX1262
 +          {
 +              false, // DEFAULT_SPI
 +              true, // HAS_TCXO
 +              true  // DIO2_AS_RF_SWITCH
 +          }, 
 +      };
 +      const int8_t interface_pins[INTERFACE_COUNT][10] = { 
 +                  // SX1262
 +          {
 +              18, // pin_ss
 +              5, // pin_sclk
 +              27, // pin_mosi
 +              19, // pin_miso
 +              32, // pin_busy
 +              33, // pin_dio
 +              23, // pin_reset
 +              -1, // pin_txen
 +              14, // pin_rxen
 +              -1  // pin_tcxo_enable
 +          }
 +      };
-  #elif BOARD_MODEL == BOARD_XIAO_ESP32S3
++ #elif BOARD_MODEL == BOARD_XIAO_S3
        #define IS_ESP32S3 true
 -      #define MODEM SX1262
 -      #define DIO2_AS_RF_SWITCH true
 -      #define HAS_BUSY true
 -      #define HAS_TCXO true
  
 -      #define HAS_DISPLAY false
 -      #define HAS_CONSOLE true
 +      #define HAS_DISPLAY true
 +      #define DISPLAY OLED
 +      //#define HAS_CONSOLE true
        #define HAS_BLUETOOTH false
        #define HAS_BLE true
 +      //#define HAS_PMU true
        #define HAS_NP false
        #define HAS_SD false
        #define HAS_EEPROM true
        #define PIN_WAKEUP GPIO_NUM_21
        #define WAKEUP_LEVEL 0
  
 +      #define INTERFACE_COUNT 1
 +
 +//      #define PMU_IRQ 40
 +//      #define I2C_SCL 41
 +//      #define I2C_SDA 42
 +
 +      // Wio-SX1262 button pulls down GPIO21
 +      // THis is shared with the Yellow LED
 +      // on the ESP32S3 (also active Low)
        const int pin_btn_usr1 = 21;
 -      const int pin_cs = 41;
 -      const int pin_reset = 42;
 -      const int pin_sclk = 7;
 -      const int pin_mosi = 9;
 -      const int pin_miso = 8;
 -      const int pin_tcxo_enable = -1;
 -      const int pin_dio = 39;
 -      const int pin_busy = 40;
 -      
 +
 +      const uint8_t interfaces[INTERFACE_COUNT] = {SX1262};
 +      const bool interface_cfg[INTERFACE_COUNT][3] = { 
 +                    // SX1262
 +          {
 +              true, // DEFAULT_SPI
 +              true, // HAS_TCXO
 +              true  // DIO2_AS_RF_SWITCH
 +          }, 
 +      };
 +      const int8_t interface_pins[INTERFACE_COUNT][10] = { 
 +                  // SX1262
 +          {
 +              41, // pin_ss
 +              7, // pin_sclk
 +              9, // pin_mosi
 +              8, // pin_miso
 +              40, // pin_busy
 +              39, // pin_dio
 +              42, // pin_reset
 +              -1, // pin_txen
 +              -1, // pin_rxen
 +              -1  // pin_tcxo_enable
 +          }
 +      };
 +
 +//      const int SD_MISO = 37;
 +//      const int SD_MOSI = 35;
 +//      const int SD_CLK = 36;
 +//      const int SD_CS = 47;
 +
 +//      const int IMU_CS = 34;
 +
 +      // HAS LED/tx on Wio board - 48 Hi/ON
 +      // LED 21 on ESP board - Lo/ON/Yellow
 +      // shared with button input.
        #if HAS_NP == false
          #if defined(EXTERNAL_LEDS)
-           const int pin_led_rx = -1;
+           const int pin_led_rx = 48;
 -          const int pin_led_tx = 48;
 +          const int pin_led_tx = 48; //47;
          #else
-           const int pin_led_rx = -1;
+           const int pin_led_rx = 48;
 -          const int pin_led_tx = 48;
 +          const int pin_led_tx = 48; //47;
          #endif
        #endif
 -
      #else
        #error An unsupported ESP32 board was selected. Cannot compile RNode firmware.
      #endif
diff --cc Display.h
index 3d6aecf095a0d22dc822ef5ce6db6e8af9e200ba,c42283ca360f558ad0e1825502f9bce46b4eb48b..4b60819654a2150ac50ac2678077b5df61b8284a
+++ b/Display.h
@@@ -125,10 -85,12 +125,16 @@@ void busyCallback(const void* p) { disp
    #define SCL_OLED 18
    #define SDA_OLED 17
    #define DISP_CUSTOM_ADDR false
 +#elif BOARD_MODEL == BOARD_H_W_PAPER
 +  #define DISP_W 250
 +  #define DISP_H 122
 +  #define DISP_ADDR -1
+ #elif BOARD_MODEL == BOARD_XIAO_S3
+   #define DISP_RST -1
+   #define DISP_ADDR 0x3C
+   #define SCL_OLED 6
+   #define SDA_OLED 5
+   #define DISP_CUSTOM_ADDR true
  #else
    #define DISP_RST -1
    #define DISP_ADDR 0x3C
@@@ -710,18 -607,17 +718,18 @@@ void draw_quality_bars(int px, int py) 
      if (quality < 0.0) quality = 0.0;
  
      // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality);
 -    if (quality > 0)  stat_area.drawLine(px+0*2, py+7, px+0*2, py+6, SSD1306_WHITE);
 -    if (quality > 15) stat_area.drawLine(px+1*2, py+7, px+1*2, py+5, SSD1306_WHITE);
 -    if (quality > 30) stat_area.drawLine(px+2*2, py+7, px+2*2, py+4, SSD1306_WHITE);
 -    if (quality > 45) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, SSD1306_WHITE);
 -    if (quality > 60) stat_area.drawLine(px+4*2, py+7, px+4*2, py+2, SSD1306_WHITE);
 -    if (quality > 75) stat_area.drawLine(px+5*2, py+7, px+5*2, py+1, SSD1306_WHITE);
 -    if (quality > 90) stat_area.drawLine(px+6*2, py+7, px+6*2, py+0, SSD1306_WHITE);
 -  }
 +    // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality);
 +    if (quality > 0)  stat_area.drawLine(px+0*2, py+7, px+0*2, py+6, DISPLAY_WHITE);
 +    if (quality > 15) stat_area.drawLine(px+1*2, py+7, px+1*2, py+5, DISPLAY_WHITE);
 +    if (quality > 30) stat_area.drawLine(px+2*2, py+7, px+2*2, py+4, DISPLAY_WHITE);
 +    if (quality > 45) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, DISPLAY_WHITE);
 +    if (quality > 60) stat_area.drawLine(px+4*2, py+7, px+4*2, py+2, DISPLAY_WHITE);
 +    if (quality > 75) stat_area.drawLine(px+5*2, py+7, px+5*2, py+1, DISPLAY_WHITE);
 +    if (quality > 90) stat_area.drawLine(px+6*2, py+7, px+6*2, py+0, DISPLAY_WHITE);
 +  //}
  }
  
- #if MODE == SX1280
+ #if MODEM == SX1280
    #define S_RSSI_MIN -105.0
    #define S_RSSI_MAX -65.0
  #else
diff --cc Framing.h
index 74b81d967e698ada42da6676787aa91291de23d6,9346f70b7106f53547ba23719c1ffbaa13c30ffc..47a8eebed924196d60f8002139a72b79faa4286a
+++ b/Framing.h
    #define CMD_RESET       0x55
    #define CMD_RESET_BYTE  0xF8
  
 +  #define CMD_INTERFACES  0x64
 +    
 +  #define CMD_DATA        0x00
 +
 +  #define CMD_SEL_INT     0x1F
+   #define CMD_LOG         0x80
+   #define CMD_TIME        0x81
+   #define CMD_MUX_CHAIN   0x82
+   #define CMD_MUX_DSCVR   0x83
  
    #define DETECT_REQ      0x73
    #define DETECT_RESP     0x46
diff --cc Makefile
index f529c1d4b115439640a0aee5024f2c896f91ec61,b6f5a7b28dc25515cda2b3ebfbf5146bfef3cfd7..3a8644192b9217fb18958e7e284ab6567bbbfff6
+++ b/Makefile
@@@ -77,93 -71,96 +77,93 @@@ firmware: $(shell grep ^firmware- Makef
  check_bt_buffers:
        @./esp32_btbufs.py ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/libraries/BluetoothSerial/src/BluetoothSerial.cpp
  
 -firmware:
 -      arduino-cli compile --log --fqbn unsignedio:avr:rnode
 -
 -firmware-mega2560:
 -      arduino-cli compile --log --fqbn arduino:avr:mega
 -
  firmware-tbeam: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:t-beam -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\""
 +      arduino-cli compile --fqbn esp32:esp32:t-beam $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\" \"-DBOARD_VARIANT=0xE4\""
  
 -firmware-tbeam_sx126x: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:t-beam -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\" \"-DMODEM=0x03\""
 +firmware-tbeam_sx1262: check_bt_buffers
 +      arduino-cli compile --fqbn esp32:esp32:t-beam $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x33\" \"-DBOARD_VARIANT=0xE8\""
 +
 +firmware-techo:
 +      arduino-cli compile --fqbn adafruit:nrf52:pca10056 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x44\""
  
  firmware-t3s3:
 -      arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x03\""
 +      arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DBOARD_VARIANT=0xAB\""
  
 -firmware-t3s3_sx127x:
 -      arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x01\""
 +firmware-t3s3_sx126x:
 +      arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DBOARD_VARIANT=0xA1\""
  
  firmware-t3s3_sx1280_pa:
 -      arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DMODEM=0x04\""
 -
 -firmware-tdeck:
 -      arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3B\""
 +      arduino-cli compile --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x42\" \"-DBOARD_VARIANT=0xAC\""
  
 -firmware-tbeam_supreme:
 -      arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x3D"
 +firmware-e22_esp32:
 +      arduino-cli compile --fqbn esp32:esp32:esp32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x45\" \"-DEXTERNAL_LEDS=true\""
  
  firmware-lora32_v10: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\""
  
  firmware-lora32_v10_extled: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\" \"-DEXTERNAL_LEDS=true\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\" \"-DEXTERNAL_LEDS=true\""
  
  firmware-lora32_v20: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x36\" \"-DEXTERNAL_LEDS=true\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x36\" \"-DEXTERNAL_LEDS=true\""
  
  firmware-lora32_v21: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\""
  
  firmware-lora32_v21_extled: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DEXTERNAL_LEDS=true\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DEXTERNAL_LEDS=true\""
  
  firmware-lora32_v21_tcxo: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DENABLE_TCXO=true\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x37\" \"-DENABLE_TCXO=true\""
  
  firmware-heltec32_v2: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\""
 +      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V2 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\""
  
  firmware-heltec32_v2_extled: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\" \"-DEXTERNAL_LEDS=true\""
 +      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V2 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\" \"-DEXTERNAL_LEDS=true\""
  
  firmware-heltec32_v3:
 -      arduino-cli compile --log --fqbn esp32:esp32:heltec_wifi_lora_32_V3 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3A\""
 +      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V3 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3A\""
 +
 +firmware-heltec_w_paper:
 +      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V3 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\""
 +
- firmware-xiao_esp32s3:
++firmware-xiao_s3:
 +      arduino-cli compile --fqbn esp32:esp32:XIAO_ESP32S3 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\""
  
  firmware-rnode_ng_20: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x40\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x40\""
  
  firmware-rnode_ng_21: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x41\""
 +      arduino-cli compile --fqbn esp32:esp32:ttgo-lora32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x41\""
  
  firmware-featheresp32: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:featheresp32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x34\""
 +      arduino-cli compile --fqbn esp32:esp32:featheresp32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x34\""
  
  firmware-genericesp32: check_bt_buffers
 -      arduino-cli compile --log --fqbn esp32:esp32:esp32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x35\""
 +      arduino-cli compile --fqbn esp32:esp32:esp32 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x35\""
  
  firmware-rak4631:
 -      arduino-cli compile --log --fqbn rakwireless:nrf52:WisCoreRAK4631Board -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\""
 +      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\" \"-DBOARD_VARIANT=0x12\""
  
 -firmware-heltec_t114:
 -      arduino-cli compile --log --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\""
 +firmware-rak4631_sx1280:
 +      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\" \"-DBOARD_VARIANT=0x14\""
  
 -firmware-techo:
 -      arduino-cli compile --log --fqbn adafruit:nrf52:pca10056 -e --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x44\""
 -
 -firmware-xiao_s3:
 -      arduino-cli compile --log --fqbn "esp32:esp32:XIAO_ESP32S3" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\""
 +firmware-opencom-xl:
 +      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x52\" \"-DBOARD_VARIANT=0x21\""
  
 -upload:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn unsignedio:avr:rnode
 +firmware-heltec_t114:
 +      arduino-cli compile --log --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\""
  
 -upload-mega2560:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega
 +firmware-heltec_t114_gps:
 +      arduino-cli compile --log --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\" \"-DBOARD_VARIANT=0xCB\""
  
  upload-tbeam:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:t-beam
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:t-beam
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.t-beam/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.t-beam/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
  upload-tbeam_sx1262:
        arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:t-beam
        #python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
  
  upload-lora32_v10:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:ttgo-lora32
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
  upload-lora32_v20:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:ttgo-lora32
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
  upload-lora32_v21:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:ttgo-lora32
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:ttgo-lora32
        @sleep 1
 -      rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
  upload-heltec32_v2:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:heltec_wifi_lora_32_V2
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:heltec_wifi_lora_32_V2
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB1 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
 -upload-heltec32_v3:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:heltec_wifi_lora_32_V3
 +upload-heltec_w_paper upload-heltec32_v3:
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:heltec_wifi_lora_32_V3
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +
- upload-xiao_esp32s3:
++upload-xiao_s3:
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:XIAO_ESP32S3
 +      @sleep 1
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware_CE.ino.bin)
 +      @sleep 3
 +      python ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 8MB 0x210000 ./Release/console_image.bin
  
  upload-tdeck:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:esp32s3
        @sleep 1
 -      rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
  
  upload-tbeam_supreme:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:esp32s3
        @sleep 1
 -      rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32-s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
  
  upload-rnode_ng_20:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:ttgo-lora32
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:ttgo-lora32
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
  upload-rnode_ng_21:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:ttgo-lora32
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:ttgo-lora32
        @sleep 1
 -      rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.ttgo-lora32/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) $(COMMON_ESP_UPLOAD_FLAGS) ./Release/console_image.bin
  
  upload-t3s3:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn esp32:esp32:esp32s3
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn esp32:esp32:esp32s3
        @sleep 1
 -      rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
 +      python ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyACM0) --chip esp32-s3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32s3 --port /dev/ttyACM0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware_CE.ino.bin)
  
  upload-featheresp32:
 -      arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:featheresp32
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:featheresp32
        @sleep 1
 -      rnodeconf /dev/ttyUSB0 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.featheresp32/RNode_Firmware.ino.bin)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.featheresp32/RNode_Firmware_CE.ino.bin)
        @sleep 3
 -      python ./Release/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size 4MB 0x210000 ./Release/console_image.bin
 +      python ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) $(COMMON_UPLOAD_FLAGS) ./Release/console_image.bin
 +
 +upload-opencom-xl upload-rak4631:
 +      arduino-cli upload -p $(or $(port), /dev/ttyACM0) --fqbn rakwireless:nrf52:WisCoreRAK4631Board
 +      unzip -o build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware_CE.ino.zip -d build/rakwireless.nrf52.WisCoreRAK4631Board
 +      rnodeconf $(or $(port), /dev/ttyACM0) --firmware-hash $$(sha256sum ./build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware_CE.ino.bin | grep -o '^\S*')
  
 -upload-rak4631:
 -      arduino-cli upload -p /dev/ttyACM0 --fqbn rakwireless:nrf52:WisCoreRAK4631Board
 +upload-e22_esp32:
 +      arduino-cli upload -p $(or $(port), /dev/ttyUSB0) --fqbn esp32:esp32:esp32
        @sleep 1
 -      rnodeconf /dev/ttyACM0 --firmware-hash $$(./partition_hashes from_device /dev/ttyACM0)
 +      rnodeconf $(or $(port), /dev/ttyUSB0) --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32/RNode_Firmware_CE.ino.bin)
 +      @sleep 3
 +      python3 ./Release/esptool/esptool.py --port $(or $(port), /dev/ttyUSB0) $(COMMON_ESP_UPLOAD_FLAGS)  ./Release/console_image.bin
  
  upload-heltec_t114:
        arduino-cli upload -p /dev/ttyACM0 --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262
@@@ -381,30 -379,12 +380,30 @@@ release-heltec32_v3
        zip --junk-paths ./Release/rnode_firmware_heltec32v3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v3.boot_app0 build/rnode_firmware_heltec32v3.bin build/rnode_firmware_heltec32v3.bootloader build/rnode_firmware_heltec32v3.partitions
        rm -r build
  
- release-xiao_esp32s3:
 +release-heltec_w_paper:
 +      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V3 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\""
 +      cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltecwpaper.boot_app0
 +      cp build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware_CE.ino.bin build/rnode_firmware_heltecwpaper.bin
 +      cp build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware_CE.ino.bootloader.bin build/rnode_firmware_heltecwpaper.bootloader
 +      cp build/esp32.esp32.heltec_wifi_lora_32_V3/RNode_Firmware_CE.ino.partitions.bin build/rnode_firmware_heltecwpaper.partitions
 +      zip --junk-paths ./Release/rnode_firmware_heltecwpaper.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltecwpaper.boot_app0 build/rnode_firmware_heltecwpaper.bin build/rnode_firmware_heltecwpaper.bootloader build/rnode_firmware_heltecwpaper.partitions
 +      rm -r build
 +
++release-xiao_s3:
 +      arduino-cli compile --fqbn esp32:esp32:XIAO_ESP32S3 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\""
 +      cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_xiao_esp32s3.boot_app0
 +      cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware_CE.ino.bin build/rnode_firmware_xiao_esp32s3.bin
 +      cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware_CE.ino.bootloader.bin build/rnode_firmware_xiao_esp32s3.bootloader
 +      cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware_CE.ino.partitions.bin build/rnode_firmware_xiao_esp32s3.partitions
 +      zip --junk-paths ./Release/rnode_firmware_xiao_esp32s3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_xiao_esp32s3.boot_app0 build/rnode_firmware_xiao_esp32s3.bin build/rnode_firmware_xiao_esp32s3.bootloader build/rnode_firmware_xiao_esp32s3.partitions
 +      rm -r build
 +
  release-heltec32_v2_extled: check_bt_buffers
 -      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V2 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\" \"-DEXTERNAL_LEDS=true\""
 +      arduino-cli compile --fqbn esp32:esp32:heltec_wifi_lora_32_V2 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x38\" \"-DEXTERNAL_LEDS=true\""
        cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltec32v2.boot_app0
 -      cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bin build/rnode_firmware_heltec32v2.bin
 -      cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_heltec32v2.bootloader
 -      cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware.ino.partitions.bin build/rnode_firmware_heltec32v2.partitions
 +      cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware_CE.ino.bin build/rnode_firmware_heltec32v2.bin
 +      cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware_CE.ino.bootloader.bin build/rnode_firmware_heltec32v2.bootloader
 +      cp build/esp32.esp32.heltec_wifi_lora_32_V2/RNode_Firmware_CE.ino.partitions.bin build/rnode_firmware_heltec32v2.partitions
        zip --junk-paths ./Release/rnode_firmware_heltec32v2.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v2.boot_app0 build/rnode_firmware_heltec32v2.bin build/rnode_firmware_heltec32v2.bootloader build/rnode_firmware_heltec32v2.partitions
        rm -r build
  
@@@ -506,27 -469,31 +505,26 @@@ release-genericesp32: check_bt_buffer
        zip --junk-paths ./Release/rnode_firmware_esp32_generic.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_esp32_generic.boot_app0 build/rnode_firmware_esp32_generic.bin build/rnode_firmware_esp32_generic.bootloader build/rnode_firmware_esp32_generic.partitions
        rm -r build
  
 -release-mega2560:
 -      arduino-cli compile --fqbn arduino:avr:mega -e --build-property "compiler.cpp.extra_flags=\"-DMODEM=0x01\""
 -      cp build/arduino.avr.mega/RNode_Firmware.ino.hex Release/rnode_firmware_m2560.hex
 -      rm -r build
 -
  release-rak4631:
 -      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\""
 -      cp build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware.ino.hex build/rnode_firmware_rak4631.hex
 +      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\" \"-DBOARD_VARIANT=0x12\""
 +      cp build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware_CE.ino.hex build/rnode_firmware_rak4631.hex
        adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_rak4631.hex Release/rnode_firmware_rak4631.zip
 +      rm -r build
  
 -release-heltec_t114:
 -      arduino-cli compile --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\""
 -      cp build/Heltec_nRF52.Heltec_nRF52.HT-n5262/RNode_Firmware.ino.hex build/rnode_firmware_heltec_t114.hex
 -      adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_heltec_t114.hex Release/rnode_firmware_heltec_t114.zip
 +release-rak4631_sx1280:
 +      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x51\" \"-DBOARD_VARIANT=0x14\""
 +      cp build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware_CE.ino.hex build/rnode_firmware_rak4631_sx1280.hex
 +      adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_rak4631_sx1280.hex Release/rnode_firmware_rak4631_sx1280.zip
 +      rm -r build
  
 -release-techo:
 -      arduino-cli compile --log --fqbn adafruit:nrf52:pca10056 -e --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x44\""
 -      cp build/adafruit.nrf52.pca10056/RNode_Firmware.ino.hex build/rnode_firmware_techo.hex
 -      adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_techo.hex Release/rnode_firmware_techo.zip
 +release-opencom-xl:
 +      arduino-cli compile --fqbn rakwireless:nrf52:WisCoreRAK4631Board $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x52\" \"-DBOARD_VARIANT=0x21\""
 +      cp build/rakwireless.nrf52.WisCoreRAK4631Board/RNode_Firmware_CE.ino.hex build/rnode_firmware_opencom_xl.hex
 +      adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_opencom_xl.hex Release/rnode_firmware_opencom_xl.zip
 +      rm -r build
  
 -release-xiao_s3:
 -      arduino-cli compile --fqbn "esp32:esp32:XIAO_ESP32S3" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3E\""
 -      cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_xiao_esp32s3.boot_app0
 -      cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.bin build/rnode_firmware_xiao_esp32s3.bin
 -      cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_xiao_esp32s3.bootloader
 -      cp build/esp32.esp32.XIAO_ESP32S3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_xiao_esp32s3.partitions
 -      zip --junk-paths ./Release/rnode_firmware_xiao_esp32s3.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_xiao_esp32s3.boot_app0 build/rnode_firmware_xiao_esp32s3.bin build/rnode_firmware_xiao_esp32s3.bootloader build/rnode_firmware_xiao_esp32s3.partitions
 -      rm -r build
 +release-heltec_t114:
 +      arduino-cli compile --fqbn Heltec_nRF52:Heltec_nRF52:HT-n5262 $(COMMON_BUILD_FLAGS) --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x3C\""
 +      cp build/Heltec_nRF52.Heltec_nRF52.HT-n5262/RNode_Firmware_CE.ino.hex build/rnode_firmware_heltec_t114.hex
 +      adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/rnode_firmware_heltec_t114.hex Release/rnode_firmware_heltec_t114.zip
 +      rm -r build
diff --cc README.md
index 71859605e9996cce1ddc11711c324590e2623d53,d6cf721ca2251ff87e7f09298bdd5497cdbb602e..448563ece42dbcd0b31e527dca13853b74c90e5c
+++ b/README.md
@@@ -88,23 -55,39 +88,22 @@@ The RNode system has been designed to a
  
  While **speeds are lower** than WiFi, typical communication **ranges are many times higher**. Several kilometers can be acheived with usable bitrates, even in urban areas, and over **100 kilometers** can be achieved in line-of-sight conditions.
  
 -## Supported Boards & Devices
 -It's easy to create your own RNodes from one of the supported development boards and devices. If a device or board you want to use is not yet supported, you are welcome to join the effort and help creating a board definition and pin mapping for it!
 -
 -<img src="Documentation/images/devboards_1.webp" width="100%"/>
 -
 -The RNode Firmware supports the following boards:
 -
 -- LilyGO T-Beam v1.1 devices with SX1276/8 LoRa chips
 -- LilyGO T-Beam v1.1 devices with SX1262/8 LoRa chips
 -- LilyGO T-Beam Supreme devices
 -- LilyGO T-Deck devices (currently display is disabled)
 -- LilyGO LoRa32 v1.0 devices
 -- LilyGO LoRa32 v2.0 devices
 -- LilyGO LoRa32 v2.1 devices (with and without TCXO)
 -- LilyGO T3S3 devices with SX1276/8 LoRa chips
 -- LilyGO T3S3 devices with SX1262/8 LoRa chips
 -- LilyGO T3S3 devices with SX1280 LoRa chips
 -- LilyGO T-Echo devices
 -- Heltec LoRa32 v2 devices
 -- Heltec LoRa32 v3 devices
 -- Heltec T114 devices
 -- RAK4631 devices
 -- SeeedStudio XIAO ESP32S3 devices (with Wio-SX1262)
 -- Homebrew RNodes based on ATmega1284p boards
 -- Homebrew RNodes based on ATmega2560 boards
 -- Homebrew RNodes based on Adafruit Feather ESP32 boards
 -- Homebrew RNodes based on generic ESP32 boards
 +## A Self-Replicating System
  
 -## Supported Transceiver Modules
 -The RNode Firmware supports all transceiver modules based on Semtech **SX1276**, **SX1278**, **SX1262**, **SX1268** and **SX1280** chips, that have an **SPI interface** and expose the relevant **DIO** interrupt pins from the chip.
 +If you notice the presence of a circularity in the naming of the system as a whole, and the physical devices, it is no coincidence. Every RNode contains the seeds necessary to reproduce the system, the [RNode Bootstrap Console](https://unsigned.io/rnode_bootstrap_console), which is hosted locally on every RNode, and can be activated and accesses at any time - no Internet required.
 +
 +The designs, guides and software stored within allows users to create more RNodes, and even to bootstrap entire communications networks, completely independently of existing infrastructure, or in situations where infrastructure has become unreliable or is broken.
 +
 +<img src="Documentation/images/126dcfe92fb7.webp" width="100%"/>
 +
 +<center><i>Where there is no Internet, RNodes will still communicate</i></center><br/><br/>
 +
 +The production of one particular RNode device is not an end, but the potential starting point of a new branch of devices on the tree of the RNode system as a whole.
 +
 +This tree fits into the larger biome of Free & Open Communications Systems, which I hope that you - by using communications tools like RNode - will help grow and prosper.
  
  ## Getting Started Fast
 -You can download and flash the firmware to all the supported boards using the [RNode Config Utility](https://github.com/markqvist/rnodeconfigutil). All firmware releases are now handled and installed directly through the `rnodeconf` utility, which is included in the `rns` package. It can be installed via `pip`:
 +You can download and flash the firmware to all the supported boards using [rnodeconf](https://github.com/markqvist/Reticulum). All firmware releases are handled and installed directly through the `rnodeconf` utility, which is included in the `rns` package. It can be installed via `pip`:
  
  ```
  # Install rnodeconf via rns package
index 7db1c8fd1084fa340db2f2b9c818204eb6963448,0000000000000000000000000000000000000000..b45d0391617a226d2d093381de2e8917ccf3dd4a
mode 100644,000000..100644
--- /dev/null
@@@ -1,1752 -1,0 +1,1773 @@@
-   # if MCU_VARIANT == MCU_ESP32
 +// Copyright (C) 2024, Mark Qvist
 +
 +// This program is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +
 +// This program is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 +// GNU General Public License for more details.
 +
 +// You should have received a copy of the GNU General Public License
 +// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 +
 +#include <Arduino.h>
 +#include <SPI.h>
 +#include "Utilities.h"
 +
 +#if MCU_VARIANT == MCU_NRF52
 +  #if BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_OPENCOM_XL
 +      #define INTERFACE_SPI
 +        // Required because on RAK4631, non-default SPI pins must be initialised when class is declared.
 +      SPIClass interface_spi[1] = {
 +            // SX1262
 +            SPIClass(
 +                NRF_SPIM2, 
 +                interface_pins[0][3], 
 +                interface_pins[0][1], 
 +                interface_pins[0][2]
 +               )
 +      };
 +  #elif BOARD_MODEL == BOARD_TECHO
 +    #define INTERFACE_SPI
 +    SPIClass interface_spi[1] = {
 +            // SX1262
 +            SPIClass(
 +                NRF_SPIM3, 
 +                interface_pins[0][3], 
 +                interface_pins[0][1], 
 +                interface_pins[0][2]
 +               )
 +      };
 +  #elif BOARD_MODEL == BOARD_HELTEC_T114
 +    #define INTERFACE_SPI
 +    SPIClass interface_spi[1] = {
 +            // SX1262
 +            SPIClass(
 +                NRF_SPIM1, 
 +                interface_pins[0][3], 
 +                interface_pins[0][1], 
 +                interface_pins[0][2]
 +               )
 +      };
 +  #endif
 +#endif
 +
 +#ifndef INTERFACE_SPI
 +// INTERFACE_SPI is only required on NRF52 platforms, as the SPI pins are set in the class constructor and not by a setter method.
 +// Even if custom SPI interfaces are not needed, the array must exist to prevent compilation errors.
 +#define INTERFACE_SPI
 +SPIClass interface_spi[1];
 +#endif
 +
 +FIFOBuffer serialFIFO;
 +uint8_t serialBuffer[CONFIG_UART_BUFFER_SIZE+1];
 +
 +uint16_t packet_starts_buf[(CONFIG_QUEUE_MAX_LENGTH)+1];
 +
 +uint16_t packet_lengths_buf[(CONFIG_QUEUE_MAX_LENGTH)+1];
 +
 +FIFOBuffer16 packet_starts[INTERFACE_COUNT];
 +FIFOBuffer16 packet_lengths[INTERFACE_COUNT];
 +
 +volatile uint8_t queue_height[INTERFACE_COUNT] = {0};
 +volatile uint16_t queued_bytes[INTERFACE_COUNT] = {0};
 +
 +volatile uint16_t queue_cursor[INTERFACE_COUNT] = {0};
 +volatile uint16_t current_packet_start[INTERFACE_COUNT] = {0};
 +volatile bool serial_buffering = false;
 +
 +extern void setup_interfaces(); // from /src/misc/ModemISR.h
 +
 +#if HAS_BLUETOOTH || HAS_BLE == true
 +  bool bt_init_ran = false;
 +#endif
 +
 +#if HAS_CONSOLE
 +  #include "Console.h"
 +#endif
 +
 +#define MODEM_QUEUE_SIZE 4*INTERFACE_COUNT
 +typedef struct {
 +      size_t len;
 +      int rssi;
 +      int snr_raw;
 +      uint8_t interface;
 +      uint8_t data[];
 +} modem_packet_t;
 +static xQueueHandle modem_packet_queue = NULL;
 +
 +char sbuf[128];
 +
 +uint8_t *packet_queue[INTERFACE_COUNT];
 +
 +void setup() {
 +  #if MCU_VARIANT == MCU_ESP32
 +    boot_seq();
 +    EEPROM.begin(EEPROM_SIZE);
 +    Serial.setRxBufferSize(CONFIG_UART_BUFFER_SIZE);
 +
 +    #if BOARD_MODEL == BOARD_TDECK
 +      pinMode(pin_poweron, OUTPUT);
 +      digitalWrite(pin_poweron, HIGH);
 +
 +      pinMode(SD_CS, OUTPUT);
 +      pinMode(DISPLAY_CS, OUTPUT);
 +      digitalWrite(SD_CS, HIGH);
 +      digitalWrite(DISPLAY_CS, HIGH);
 +
 +      pinMode(DISPLAY_BL_PIN, OUTPUT);
 +    #endif
 +
 +  #elif MCU_VARIANT == MCU_NRF52
 +    #if BOARD_MODEL == BOARD_TECHO
 +      delay(200);
 +      pinMode(PIN_VEXT_EN, OUTPUT);
 +      digitalWrite(PIN_VEXT_EN, HIGH);
 +      pinMode(pin_btn_usr1, INPUT_PULLUP);
 +      pinMode(pin_btn_touch, INPUT_PULLUP);
 +      pinMode(PIN_LED_RED, OUTPUT);
 +      pinMode(PIN_LED_GREEN, OUTPUT);
 +      pinMode(PIN_LED_BLUE, OUTPUT);
 +      delay(200);
 +    #elif BOARD_MODEL == BOARD_HELTEC_T114
 +      delay(200);
 +      pinMode(PIN_VEXT_EN, OUTPUT);
 +      digitalWrite(PIN_VEXT_EN, HIGH);
 +      delay(100);
 +    #endif
 +
 +
 +    if (!eeprom_begin()) { Serial.write("EEPROM initialisation failed.\r\n"); }
 +  #endif
 +
 +  // Seed the PRNG for CSMA R-value selection
-     int seed_val = (int)esp_random();
++  #if MCU_VARIANT == MCU_ESP32
 +    // On ESP32, get the seed value from the
 +    // hardware RNG
-     int seed_val = analogRead(0);
++    unsigned long seed_val = (unsigned long)esp_random();
++  #elif MCU_VARIANT == MCU_NRF52
++    // On nRF, get the seed value from the
++    // hardware RNG
++    unsigned long seed_val = get_rng_seed();
 +  #else
 +    // Otherwise, get a pseudo-random seed
 +    // value from an unconnected analog pin
-               if (interface_cfg[i][0]) {
-             obj = new sx128x(i, &SPI, interface_cfg[i][1],
-             interface_pins[i][0], interface_pins[i][1], interface_pins[i][2],
-             interface_pins[i][3], interface_pins[i][6], interface_pins[i][5],
-             interface_pins[i][4], interface_pins[i][8], interface_pins[i][7]);
++    //
++    // CAUTION! If you are implementing the
++    // firmware on a platform that does not
++    // have a hardware RNG, you MUST take
++    // care to get a seed value with enough
++    // entropy at each device reset!
++    unsigned long seed_val = analogRead(0);
 +  #endif
 +  randomSeed(seed_val);
 +
 +  // Initialise serial communication
 +  memset(serialBuffer, 0, sizeof(serialBuffer));
 +  fifo_init(&serialFIFO, serialBuffer, CONFIG_UART_BUFFER_SIZE);
 +
 +  Serial.begin(serial_baudrate);
 +
 +  #if HAS_NP
 +    led_init();
 +  #endif
 +
++  #if MCU_VARIANT == MCU_NRF52 && HAS_NP == true
++    boot_seq();
++  #endif
++
 +  #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_OPENCOM_XL
 +  // Some boards need to wait until the hardware UART is set up before booting
 +  // the full firmware. In the case of the RAK4631/TECHO, the line below will wait
 +  // until a serial connection is actually established with a master. Thus, it
 +  // is disabled on this platform.
 +    while (!Serial);
 +  #endif
 +
 +  // Configure input and output pins
 +  #if HAS_INPUT
 +    input_init();
 +  #endif
 +
 +  #if HAS_NP == false
 +    pinMode(pin_led_rx, OUTPUT);
 +    pinMode(pin_led_tx, OUTPUT);
 +  #endif
 +
 +  for (int i = 0; i < INTERFACE_COUNT; i++) {
 +    if (interface_pins[i][9] != -1) {
 +        pinMode(interface_pins[i][9], OUTPUT);
 +        digitalWrite(interface_pins[i][9], HIGH);
 +    }
 +  }
 +
 +  // Initialise buffers
 +  memset(pbuf, 0, sizeof(pbuf));
 +  memset(cmdbuf, 0, sizeof(cmdbuf));
 +  
 +  memset(packet_starts_buf, 0, sizeof(packet_starts_buf));
 +  memset(packet_lengths_buf, 0, sizeof(packet_starts_buf));
 +
 +  memset(seq, 0xFF, sizeof(seq));
 +  memset(read_len, 0, sizeof(read_len));
 +
 +  setup_interfaces();
 +
 +  modem_packet_queue = xQueueCreate(MODEM_QUEUE_SIZE, sizeof(modem_packet_t*));
 +
 +  for (int i = 0; i < INTERFACE_COUNT; i++) {
 +      fifo16_init(&packet_starts[i], packet_starts_buf, CONFIG_QUEUE_MAX_LENGTH);
 +      fifo16_init(&packet_lengths[i], packet_lengths_buf, CONFIG_QUEUE_MAX_LENGTH);
 +      packet_queue[i] = (uint8_t*)malloc(getQueueSize(i)+1);
 +  }
 +
 +  memset(packet_rdy_interfaces_buf, 0, sizeof(packet_rdy_interfaces_buf));
 +
 +  fifo_init(&packet_rdy_interfaces, packet_rdy_interfaces_buf, MAX_INTERFACES);
 +
 +  #if HAS_GPS
 +  // init GPS
 +  gps_s.begin(GPS_BAUD_RATE);
 +  #endif
 +
 +  // add call to init_channel_stats here? \todo
 +
 +  // Create and configure interface objects
 +  for (uint8_t i = 0; i < INTERFACE_COUNT; i++) {
 +      switch (interfaces[i]) {
 +          case SX1262:
 +          {
 +              sx126x* obj;
 +              // if default spi enabled
 +              if (interface_cfg[i][0]) {
 +                obj = new sx126x(i, &SPI, interface_cfg[i][1],
 +                interface_cfg[i][2], interface_pins[i][0], interface_pins[i][1],
 +                interface_pins[i][2], interface_pins[i][3], interface_pins[i][6],
 +                interface_pins[i][5], interface_pins[i][4], interface_pins[i][8]);
 +              }
 +              else {
 +            obj = new sx126x(i, &interface_spi[i], interface_cfg[i][1],
 +            interface_cfg[i][2], interface_pins[i][0], interface_pins[i][1],
 +            interface_pins[i][2], interface_pins[i][3], interface_pins[i][6],
 +            interface_pins[i][5], interface_pins[i][4], interface_pins[i][8]);
 +              }
 +            interface_obj[i] = obj;
 +            interface_obj_sorted[i] = obj;
 +            break;
 +          }
 +
 +          case SX1276:
 +          case SX1278:
 +          {
 +              sx127x* obj;
 +              // if default spi enabled
 +              if (interface_cfg[i][0]) {
 +            obj = new sx127x(i, &SPI, interface_pins[i][0],
 +            interface_pins[i][1], interface_pins[i][2], interface_pins[i][3],
 +            interface_pins[i][6], interface_pins[i][5], interface_pins[i][4]);
 +              }
 +              else {
 +            obj = new sx127x(i, &interface_spi[i], interface_pins[i][0],
 +            interface_pins[i][1], interface_pins[i][2], interface_pins[i][3],
 +            interface_pins[i][6], interface_pins[i][5], interface_pins[i][4]);
 +              }
 +            interface_obj[i] = obj;
 +            interface_obj_sorted[i] = obj;
 +            break;
 +          }
 +
 +          case SX1280:
 +          {
 +              sx128x* obj;
 +              // if default spi enabled
-             obj = new sx128x(i, &interface_spi[i], interface_cfg[i][1],
-             interface_pins[i][0], interface_pins[i][1], interface_pins[i][2],
-             interface_pins[i][3], interface_pins[i][6], interface_pins[i][5],
-             interface_pins[i][4], interface_pins[i][8], interface_pins[i][7]);
++            if (interface_cfg[i][0]) {
++                obj = new sx128x(i, &SPI, interface_cfg[i][1],
++                interface_pins[i][0], interface_pins[i][1], interface_pins[i][2],
++                interface_pins[i][3], interface_pins[i][6], interface_pins[i][5],
++                interface_pins[i][4], interface_pins[i][8], interface_pins[i][7]);
 +            }
 +            else {
-       #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_XIAO_ESP32C3
++                obj = new sx128x(i, &interface_spi[i], interface_cfg[i][1],
++                interface_pins[i][0], interface_pins[i][1], interface_pins[i][2],
++                interface_pins[i][3], interface_pins[i][6], interface_pins[i][5],
++                interface_pins[i][4], interface_pins[i][8], interface_pins[i][7]);
 +            }
 +            interface_obj[i] = obj;
 +            interface_obj_sorted[i] = obj;
 +            break;
 +          }
 +          
 +          default:
 +            break;
 +      }
 +  }
 +
 +  #if BOARD_MODEL == BOARD_T3S3 && BOARD_VARIANT == MODEL_AC
 +    // Fix weird radio not found bug on T3S3 SX1280
 +    delay(300);
 +    interface_obj[0]->reset();
 +    delay(100);
 +  #endif
 +
 +    // Check installed transceiver chip(s) and probe boot parameters. If any of
 +    // the configured modems cannot be initialised, do not boot
 +    for (int i = 0; i < INTERFACE_COUNT; i++) {
 +        switch (interfaces[i]) {
 +            case SX1262:
 +            case SX1276:
 +            case SX1278:
 +            case SX1280:
 +                selected_radio = interface_obj[i];
 +                break;
 +
 +            default:
 +                modems_installed = false;
 +                break;
 +        }
 +        if (selected_radio->preInit()) {
 +          modems_installed = true;
 +          #if HAS_INPUT
 +            // Skip quick-reset console activation
 +          #else
 +              uint32_t lfr = selected_radio->getFrequency();
 +              if (lfr == 0) {
 +                // Normal boot
 +              } else if (lfr == M_FRQ_R) {
 +                // Quick reboot
 +                #if HAS_CONSOLE
 +                  if (rtc_get_reset_reason(0) == POWERON_RESET) {
 +                    console_active = true;
 +                  }
 +                #endif
 +              } else {
 +                // Unknown boot
 +              }
 +              selected_radio->setFrequency(M_FRQ_S);
 +          #endif
 +        } else {
 +          modems_installed = false;
 +        }
 +        if (!modems_installed) {
 +            break;
 +        }
 +    }
 +
 +  #if HAS_DISPLAY
 +    #if HAS_EEPROM
 +    if (EEPROM.read(eeprom_addr(ADDR_CONF_DSET)) != CONF_OK_BYTE) {
 +    #elif MCU_VARIANT == MCU_NRF52
 +    if (eeprom_read(eeprom_addr(ADDR_CONF_DSET)) != CONF_OK_BYTE) {
 +    #endif
 +      eeprom_update(eeprom_addr(ADDR_CONF_DSET), CONF_OK_BYTE);
 +      #if BOARD_MODEL == BOARD_TECHO
 +        eeprom_update(eeprom_addr(ADDR_CONF_DINT), 0x03);
 +      #else
 +        eeprom_update(eeprom_addr(ADDR_CONF_DINT), 0xFF);
 +      #endif
 +    }
 +    #if BOARD_MODEL == BOARD_OPENCOM_XL && (DISPLAY == EINK_BW || DISPLAY == EINK_3C)
 +      // On this board it isn't possible to run the main loop whilst the
 +      // display is updating as the SPI pins are shared between the display and
 +      // secondary modem. Because running the main loop causes a lockup, we
 +      // just run the serial poll loop instead.
 +      display_add_callback(process_serial);
 +    #elif DISPLAY == EINK_BW || DISPLAY == EINK_3C
 +      display_add_callback(work_while_waiting);
 +    #endif
 +
 +    display_unblank();
 +    disp_ready = display_init();
 +    if (disp_ready) update_display();
 +  #endif
 +
 +    #if HAS_PMU == true
 +      pmu_ready = init_pmu();
 +    #endif
 +
 +    #if HAS_BLUETOOTH || HAS_BLE == true
 +      bt_init();
 +      bt_init_ran = true;
 +    #endif
 +
 +    if (console_active) {
 +      #if HAS_CONSOLE
 +        console_start();
 +      #else
 +        kiss_indicate_reset();
 +      #endif
 +    } else {
 +      kiss_indicate_reset();
 +    }
 +
 +    for (int i = 0; i < INTERFACE_COUNT; i++) {
 +        selected_radio = interface_obj[i];
 +        if (interfaces[i] == SX1280) {
 +            selected_radio->setAvdInterference(false);
 +        }
 +        if (selected_radio->getAvdInterference()) {
 +          #if HAS_EEPROM
 +            uint8_t ia_conf = EEPROM.read(eeprom_addr(ADDR_CONF_DIA));
 +            if (ia_conf == 0x00) { selected_radio->setAvdInterference(true); }
 +            else                 { selected_radio->setAvdInterference(false); }
 +          #elif MCU_VARIANT == MCU_NRF52
 +            uint8_t ia_conf = eeprom_read(eeprom_addr(ADDR_CONF_DIA));
 +            if (ia_conf == 0x00) { selected_radio->setAvdInterference(true); }
 +            else                 { selected_radio->setAvdInterference(false); }
 +          #endif
 +        }
 +    }
 +
 +
 +  // Validate board health, EEPROM and config
 +  validate_status();
 +}
 +
 +void lora_receive(RadioInterface* radio) {
 +  if (!implicit) {
 +    radio->receive();
 +  } else {
 +    radio->receive(implicit_l);
 +  }
 +}
 +
 +inline void kiss_write_packet(int index) {
 +  // Print index of interface the packet came from
 +  serial_write(FEND);
 +  serial_write(CMD_SEL_INT);
 +  serial_write(index);
 +  serial_write(FEND);
 +
 +  serial_write(FEND);
 +  serial_write(CMD_DATA);
 +
 +  for (uint16_t i = 0; i < read_len[index]; i++) {
 +    #if MCU_VARIANT == MCU_NRF52
 +      portENTER_CRITICAL();
 +      uint8_t byte = pbuf[i];
 +      portEXIT_CRITICAL();
 +    #else
 +      uint8_t byte = pbuf[i];
 +    #endif
 +
 +    if (byte == FEND) { serial_write(FESC); byte = TFEND; }
 +    if (byte == FESC) { serial_write(FESC); byte = TFESC; }
 +    serial_write(byte);
 +  }
 +
 +  serial_write(FEND);
 +  read_len[index] = 0;
 +
 +  #if MCU_VARIANT == MCU_ESP32 && HAS_BLE
 +      bt_flush();
 +  #endif
 +}
 +
 +inline void getPacketData(RadioInterface* radio, uint16_t len) {
 +    uint8_t index = radio->getIndex();
 +  #if MCU_VARIANT != MCU_NRF52
 +    while (len-- && read_len[index] < MTU) {
 +      pbuf[read_len[index]++] = radio->read();
 +    }  
 +  #else
 +    BaseType_t int_mask = taskENTER_CRITICAL_FROM_ISR();
 +    while (len-- && read_len[index] < MTU) {
 +      pbuf[read_len[index]++] = radio->read();
 +    }
 +    taskEXIT_CRITICAL_FROM_ISR(int_mask);
 +  #endif
 +}
 +
 +inline bool queue_packet(RadioInterface* radio, uint8_t index) {
 +    // Allocate packet struct, but abort if there
 +    // is not enough memory available.
 +    modem_packet_t *modem_packet = (modem_packet_t*)malloc(sizeof(modem_packet_t) + read_len[index]);
 +    if(!modem_packet) { memory_low = true; return false; }
 +
 +    // Get packet RSSI and SNR
 +    modem_packet->snr_raw = radio->packetSnrRaw();
 +
 +    // Pass raw SNR to get RSSI as SX127X driver requires it for calculations
 +    modem_packet->rssi = radio->packetRssi(modem_packet->snr_raw);
 +
 +    modem_packet->interface = index;
 +
 +    // Send packet to event queue, but free the
 +    // allocated memory again if the queue is
 +    // unable to receive the packet.
 +    modem_packet->len = read_len[index];
 +    memcpy(modem_packet->data, pbuf, read_len[index]);
 +    if (!modem_packet_queue || xQueueSendFromISR(modem_packet_queue, &modem_packet, NULL) != pdPASS) {
 +        free(modem_packet);
 +        return false;
 +    }
 +    return true;
 +}
 +
 +void ISR_VECT receive_callback(uint8_t index, int packet_size) {
 +    selected_radio = interface_obj[index];
 +    bool    ready    = false;
 +
 +  BaseType_t int_mask;
 +  if (!promisc) {
 +    // The standard operating mode allows large
 +    // packets with a payload up to 500 bytes,
 +    // by combining two raw LoRa packets.
 +    // We read the 1-byte header and extract
 +    // packet sequence number and split flags
 +    
 +    uint8_t header   = selected_radio->read(); packet_size--;
 +    uint8_t sequence = packetSequence(header);
 +
 +    if (isSplitPacket(header) && seq[index] == SEQ_UNSET) {
 +      // This is the first part of a split
 +      // packet, so we set the seq variable
 +      // and add the data to the buffer
 +      #if MCU_VARIANT == MCU_NRF52
 +        int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len[index] = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask);
 +      #else
 +        read_len[index] = 0;
 +      #endif
 +      
 +      seq[index] = sequence;
 +
 +      getPacketData(selected_radio, packet_size);
 +
 +    } else if (isSplitPacket(header) && seq[index] == sequence) {
 +      // This is the second part of a split
 +      // packet, so we add it to the buffer
 +      // and set the ready flag.
 +      
 +      getPacketData(selected_radio, packet_size);
 +
 +      seq[index] = SEQ_UNSET;
 +      ready = true;
 +
 +    } else if (isSplitPacket(header) && seq[index] != sequence) {
 +      // This split packet does not carry the
 +      // same sequence id, so we must assume
 +      // that we are seeing the first part of
 +      // a new split packet.
 +      #if MCU_VARIANT == MCU_NRF52
 +        int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len[index] = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask);
 +      #else
 +        read_len[index] = 0;
 +      #endif
 +      seq[index] = sequence;
 +
 +      getPacketData(selected_radio, packet_size);
 +
 +    } else if (!isSplitPacket(header)) {
 +      // This is not a split packet, so we
 +      // just read it and set the ready
 +      // flag to true.
 +
 +      if (seq[index] != SEQ_UNSET) {
 +        // If we already had part of a split
 +        // packet in the buffer, we clear it.
 +        #if MCU_VARIANT == MCU_NRF52
 +          int_mask = taskENTER_CRITICAL_FROM_ISR(); read_len[index] = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask);
 +        #else
 +          read_len[index] = 0;
 +        #endif
 +        seq[index] = SEQ_UNSET;
 +      }
 +
 +      getPacketData(selected_radio, packet_size);
 +
 +      ready = true;
 +    }
 +  } else {
 +    // In promiscuous mode, raw packets are
 +    // output directly to the host
 +    read_len[index] = 0;
 +
 +    getPacketData(selected_radio, packet_size);
 +
 +    ready = true;
 +  }
 +
 +  if (ready) {
 +      queue_packet(selected_radio, index);
 +  }
 +
 +  last_rx = millis();
 +}
 +
 +bool startRadio(RadioInterface* radio) {
 +  update_radio_lock(radio);
 +  
 +  if (modems_installed && !console_active) {
 +    if (!radio->getRadioOnline()) {
 +        if (!radio->getRadioLock() && hw_ready) {
 +          if (!radio->begin()) {
 +            // The radio could not be started.
 +            // Indicate this failure over both the
 +            // serial port and with the onboard LEDs
 +            kiss_indicate_error(ERROR_INITRADIO);
 +            led_indicate_error(0);
 +            return false;
 +          } else {
 +            radio->enableCrc();
 +
 +            radio->onReceive(receive_callback);
 +
 +            radio->updateBitrate();
 +            sort_interfaces();
 +            kiss_indicate_phy_stats(radio);
 +
 +            lora_receive(radio);
 +
 +            // Flash an info pattern to indicate
 +            // that the radio is now on
 +            kiss_indicate_radiostate(radio);
 +            led_indicate_info(3);
 +            return true;
 +          }
 +
 +        } else {
 +          // Flash a warning pattern to indicate
 +          // that the radio was locked, and thus
 +          // not started
 +          kiss_indicate_radiostate(radio);
 +          led_indicate_warning(3);
 +          return false;
 +        }
 +      } else {
 +        // If radio is already on, we silently
 +        // ignore the request.
 +        kiss_indicate_radiostate(radio);
 +        return true;
 +      }
 +  }
 +  return false;
 +}
 +
 +void stopRadio(RadioInterface* radio) {
 +  if (radio->getRadioOnline()) {
 +      radio->end();
 +      sort_interfaces();
 +      kiss_indicate_radiostate(radio);
 +  }
 +}
 +
 +void update_radio_lock(RadioInterface* radio) {
 +  if (radio->getFrequency() != 0 && radio->getSignalBandwidth() != 0 && radio->getTxPower() != 0xFF && radio->getSpreadingFactor() != 0) {
 +    radio->setRadioLock(false);
 +  } else {
 +    radio->setRadioLock(true);
 +  }
 +}
 +
 +// Check if the queue is full for the selected radio.
 +// Returns true if full, false if not
 +bool queue_full(RadioInterface* radio) {
 +  return (queue_height[radio->getIndex()] >= (CONFIG_QUEUE_MAX_LENGTH) || queued_bytes[radio->getIndex()] >= (getQueueSize(radio->getIndex())));
 +}
 +
 +volatile bool queue_flushing = false;
 +
 +// Flushes all packets for the interface
 +void flush_queue(RadioInterface* radio) {
 +    uint8_t index = radio->getIndex();
 +  if (!queue_flushing) {
 +    queue_flushing = true;
 +
 +    led_tx_on();
 +    uint16_t processed = 0;
 +    uint8_t data_byte;
 +
 +    while (!fifo16_isempty(&packet_starts[index])) {
 +      uint16_t start = fifo16_pop(&packet_starts[index]);
 +      uint16_t length = fifo16_pop(&packet_lengths[index]);
 +
 +      if (length >= MIN_L && length <= MTU) {
 +        for (uint16_t i = 0; i < length; i++) {
 +          uint16_t pos = (start+i)%(getQueueSize(index));
 +          tbuf[i] = packet_queue[index][pos];
 +        }
 +        transmit(radio, length);
 +        processed++;
 +      }
 +    }
 +
 +    lora_receive(radio);
 +    led_tx_off();
 +
 +  }
 +
 +  queue_flushing = false;
 +  queue_height[index] = 0;
 +  queued_bytes[index] = 0;
 +  radio->updateAirtime();
 +
 +  #if HAS_DISPLAY
 +    display_tx[radio->getIndex()] = true;
 +  #endif
 +}
 +
 +void pop_queue(RadioInterface* radio) {
 +  uint8_t index = radio->getIndex();
 +  if (!queue_flushing) {
 +    queue_flushing = true;
 +    led_tx_on(); uint16_t processed = 0;
 +
 +    if (!fifo16_isempty(&packet_starts[index])) {
 +
 +      uint16_t start = fifo16_pop(&packet_starts[index]);
 +      uint16_t length = fifo16_pop(&packet_lengths[index]);
 +      if (length >= MIN_L && length <= MTU) {
 +        for (uint16_t i = 0; i < length; i++) {
 +          uint16_t pos = (start+i)%getQueueSize(index);
 +          tbuf[i] = packet_queue[index][pos];
 +        }
 +
 +        transmit(radio, length); processed++;
 +      }
 +
 +      queue_height[index] -= processed;
 +      queued_bytes[index] -= length;
 +    }
 +
 +    lora_receive(radio); led_tx_off();
 +  }
 +
 +  radio->updateAirtime();
 +
 +  queue_flushing = false;
 +  
 +  #if HAS_DISPLAY
 +    display_tx[radio->getIndex()] = true;
 +  #endif
 +
 +}
 +
 +void transmit(RadioInterface* radio, uint16_t size) {
 +  if (radio->getRadioOnline()) { 
 +    if (!promisc) {
 +      uint16_t  written = 0;
 +      uint8_t header  = random(256) & 0xF0;
 +
 +      if (size > SINGLE_MTU - HEADER_L) {
 +        header = header | FLAG_SPLIT;
 +      }
 +
 +
 +      radio->beginPacket();
 +      radio->write(header); written++;
 +
 +      for (uint16_t i=0; i < size; i++) {
 +        radio->write(tbuf[i]);
 +
 +        written++;
 +
 +        // Only start a new packet if this is a split packet and it has
 +        // exceeded the length of a single packet
 +        if (written == 255 && isSplitPacket(header)) {
 +          if (!radio->endPacket()) {
 +              kiss_indicate_error(ERROR_MODEM_TIMEOUT);
 +              kiss_indicate_error(ERROR_TXFAILED);
 +              led_indicate_error(5);
 +              hard_reset();
 +          }
 +
 +          radio->addAirtime();
 +          radio->beginPacket();
 +          radio->write(header);
 +          written = 1;
 +        }
 +      }
 +
 +      if (!radio->endPacket()) {
 +        kiss_indicate_error(ERROR_MODEM_TIMEOUT);
 +        kiss_indicate_error(ERROR_TXFAILED);
 +        led_indicate_error(5);
 +        hard_reset();
 +      }
 +      radio->addAirtime();
 +
 +    } else {
 +      // In promiscuous mode, we only send out
 +      // plain raw LoRa packets with a maximum
 +      // payload of 255 bytes
 +      led_tx_on();
 +      uint16_t  written = 0;
 +      
 +      // Cap packets at 255 bytes
 +      if (size > SINGLE_MTU) {
 +        size = SINGLE_MTU;
 +      }
 +
 +      // If implicit header mode has been set,
 +      // set packet length to payload data length
 +      if (!implicit) {
 +        radio->beginPacket();
 +      } else {
 +        radio->beginPacket(size);
 +      }
 +
 +      for (uint16_t i=0; i < size; i++) {
 +        radio->write(tbuf[i]);
 +
 +        written++;
 +      }
 +      radio->endPacket(); radio->addAirtime();
 +    }
 +    last_tx = millis();
 +  } else {
 +    kiss_indicate_error(ERROR_TXFAILED);
 +    led_indicate_error(5);
 +  }
 +}
 +
 +void serial_callback(uint8_t sbyte) {
 +  if (IN_FRAME && sbyte == FEND && 
 +            command == CMD_DATA) {
 +    IN_FRAME = false;
 +
 +    if (interface < INTERFACE_COUNT) {
 +        if (!fifo16_isfull(&packet_starts[interface]) && (queued_bytes[interface] < (getQueueSize(interface)))) {
 +            uint16_t s = current_packet_start[interface];
 +            int32_t e = queue_cursor[interface]-1; if (e == -1) e = (getQueueSize(interface))-1;
 +            uint16_t l;
 +
 +            if (s != e) {
 +                l = (s < e) ? e - s + 1: (getQueueSize(interface)) - s + e + 1;
 +            } else {
 +                l = 1;
 +            }
 +
 +            if (l >= MIN_L) {
 +                queue_height[interface]++;
 +
 +                fifo16_push(&packet_starts[interface], s);
 +                fifo16_push(&packet_lengths[interface], l);
 +                current_packet_start[interface] = queue_cursor[interface];
 +            }
 +
 +        }
 +    }
 +
 +  } else if (sbyte == FEND) {
 +    IN_FRAME = true;
 +    command = CMD_UNKNOWN;
 +    frame_len = 0;
 +  } else if (IN_FRAME && frame_len < MTU) {
 +    // Have a look at the command byte first
 +    if (frame_len == 0 && command == CMD_UNKNOWN) {
 +        command = sbyte;
 +
 +    } else if  (command == CMD_SEL_INT) {
 +            interface = sbyte;
 +    } else if  (command == CMD_DATA) {
 +        if (bt_state != BT_STATE_CONNECTED) cable_state = CABLE_STATE_CONNECTED;
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +
 +            if (interface < INTERFACE_COUNT) {
 +                if (queue_height[interface] < CONFIG_QUEUE_MAX_LENGTH && queued_bytes[interface] < (getQueueSize(interface))) {
 +                  queued_bytes[interface]++;
 +                  packet_queue[interface][queue_cursor[interface]++] = sbyte;
 +                  if (queue_cursor[interface] == (getQueueSize(interface))) queue_cursor[interface] = 0;
 +                }
 +            }
 +        }
 +    } else if (command == CMD_INTERFACES) {
 +        for (int i = 0; i < INTERFACE_COUNT; i++) {
 +            kiss_indicate_interface(i);
 +        }
 +    } else if (command == CMD_FREQUENCY) {
 +      if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +        }
 +
 +        if (frame_len == 4) {
 +          selected_radio = interface_obj[interface];
 +          uint32_t freq = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3];
 +
 +          if (freq == 0) {
 +            kiss_indicate_frequency(selected_radio);
 +          } else {
 +            if (op_mode == MODE_HOST) selected_radio->setFrequency(freq);
 +            kiss_indicate_frequency(selected_radio);
 +          }
 +          interface = 0;
 +        }
 +    } else if (command == CMD_BANDWIDTH) {
 +      if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +        }
 +        
 +
 +        if (frame_len == 4) {
 +          selected_radio = interface_obj[interface];
 +          uint32_t bw = (uint32_t)cmdbuf[0] << 24 | (uint32_t)cmdbuf[1] << 16 | (uint32_t)cmdbuf[2] << 8 | (uint32_t)cmdbuf[3];
 +
 +          if (bw == 0) {
 +            kiss_indicate_bandwidth(selected_radio);
 +          } else {
 +            if (op_mode == MODE_HOST) selected_radio->setSignalBandwidth(bw);
 +            selected_radio->updateBitrate();
 +            sort_interfaces();
 +            kiss_indicate_bandwidth(selected_radio);
 +            kiss_indicate_phy_stats(selected_radio);
 +          }
 +          interface = 0;
 +        }
 +    } else if (command == CMD_TXPOWER) {
 +      selected_radio = interface_obj[interface];
 +
 +      if (sbyte == 0xFF) {
 +        kiss_indicate_txpower(selected_radio);
 +      } else {
 +        int8_t txp = (int8_t)sbyte;
 +
 +        if (op_mode == MODE_HOST) setTXPower(selected_radio, txp);
 +        kiss_indicate_txpower(selected_radio);
 +      }
 +      interface = 0;
 +    } else if (command == CMD_SF) {
 +      selected_radio = interface_obj[interface];
 +
 +      if (sbyte == 0xFF) {
 +        kiss_indicate_spreadingfactor(selected_radio);
 +      } else {
 +        int sf = sbyte;
 +        if (sf < 5) sf = 5;
 +        if (sf > 12) sf = 12;
 +
 +        if (op_mode == MODE_HOST) selected_radio->setSpreadingFactor(sf);
 +        selected_radio->updateBitrate();
 +        sort_interfaces();
 +        kiss_indicate_spreadingfactor(selected_radio);
 +        kiss_indicate_phy_stats(selected_radio);
 +      }
 +      interface = 0;
 +    } else if (command == CMD_CR) {
 +      selected_radio = interface_obj[interface];
 +      if (sbyte == 0xFF) {
 +        kiss_indicate_codingrate(selected_radio);
 +      } else {
 +        int cr = sbyte;
 +        if (cr < 5) cr = 5;
 +        if (cr > 8) cr = 8;
 +
 +        if (op_mode == MODE_HOST) selected_radio->setCodingRate4(cr);
 +        selected_radio->updateBitrate();
 +        sort_interfaces();
 +        kiss_indicate_codingrate(selected_radio);
 +        kiss_indicate_phy_stats(selected_radio);
 +      }
 +      interface = 0;
 +    } else if (command == CMD_IMPLICIT) {
 +      set_implicit_length(sbyte);
 +      kiss_indicate_implicit_length();
 +    } else if (command == CMD_LEAVE) {
 +      if (sbyte == 0xFF) {
 +        display_unblank();
 +        cable_state   = CABLE_STATE_DISCONNECTED;
 +        //current_rssi  = -292;
 +        last_rssi     = -292;
 +        last_rssi_raw = 0x00;
 +        last_snr_raw  = 0x80;
 +      }
 +    } else if (command == CMD_RADIO_STATE) {
 +      selected_radio = interface_obj[interface];
 +      if (bt_state != BT_STATE_CONNECTED) cable_state = CABLE_STATE_CONNECTED;
 +      if (sbyte == 0xFF) {
 +        kiss_indicate_radiostate(selected_radio);
 +      } else if (sbyte == 0x00) {
 +        stopRadio(selected_radio);
 +      } else if (sbyte == 0x01) {
 +        startRadio(selected_radio);
 +      }
 +      interface = 0;
 +    } else if (command == CMD_ST_ALOCK) {
 +      if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +        }
 +
 +        if (frame_len == 2) {
 +          selected_radio = interface_obj[interface];
 +          uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1];
 +
 +          if (at == 0) {
 +            selected_radio->setSTALock(0.0);
 +          } else {
 +            int st_airtime_limit = (float)at/(100.0*100.0);
 +            if (st_airtime_limit >= 1.0) { st_airtime_limit = 0.0; }
 +            selected_radio->setSTALock(st_airtime_limit);
 +          }
 +          kiss_indicate_st_alock(selected_radio);
 +          interface = 0;
 +        }
 +    } else if (command == CMD_LT_ALOCK) {
 +      if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +        }
 +
 +        if (frame_len == 2) {
 +          selected_radio = interface_obj[interface];
 +          uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1];
 +
 +          if (at == 0) {
 +            selected_radio->setLTALock(0.0);
 +          } else {
 +            int lt_airtime_limit = (float)at/(100.0*100.0);
 +            if (lt_airtime_limit >= 1.0) { lt_airtime_limit = 0.0; }
 +            selected_radio->setLTALock(lt_airtime_limit);
 +          }
 +          kiss_indicate_lt_alock(selected_radio);
 +          interface = 0;
 +        }
 +    } else if (command == CMD_STAT_RX) {
 +      kiss_indicate_stat_rx();
 +    } else if (command == CMD_STAT_TX) {
 +      kiss_indicate_stat_tx();
 +    } else if (command == CMD_STAT_RSSI) {
 +      kiss_indicate_stat_rssi(interface_obj[interface]);
 +    } else if (command == CMD_RADIO_LOCK) {
 +      selected_radio = interface_obj[interface];
 +      update_radio_lock(selected_radio);
 +      kiss_indicate_radio_lock(selected_radio);
 +      interface = 0;
 +    } else if (command == CMD_BLINK) {
 +      led_indicate_info(sbyte);
 +    } else if (command == CMD_RANDOM) {
 +      // pick an interface at random to get data from
 +      int int_index = random(INTERFACE_COUNT);
 +      selected_radio = interface_obj[int_index];
 +      kiss_indicate_random(getRandom(selected_radio));
 +      interface = 0;
 +    } else if (command == CMD_DETECT) {
 +      if (sbyte == DETECT_REQ) {
 +        if (bt_state != BT_STATE_CONNECTED) cable_state = CABLE_STATE_CONNECTED;
 +        kiss_indicate_detect();
 +      }
 +    } else if (command == CMD_PROMISC) {
 +      if (sbyte == 0x01) {
 +        promisc_enable();
 +      } else if (sbyte == 0x00) {
 +        promisc_disable();
 +      }
 +      kiss_indicate_promisc();
 +    } else if (command == CMD_READY) {
 +      selected_radio = interface_obj[interface];
 +      if (!queue_full(selected_radio)) {
 +        kiss_indicate_ready();
 +      } else {
 +        kiss_indicate_not_ready();
 +      }
 +    } else if (command == CMD_UNLOCK_ROM) {
 +      if (sbyte == ROM_UNLOCK_BYTE) {
 +        unlock_rom();
 +      }
 +    } else if (command == CMD_RESET) {
 +      if (sbyte == CMD_RESET_BYTE) {
 +        hard_reset();
 +      }
 +    } else if (command == CMD_ROM_READ) {
 +      kiss_dump_eeprom();
 +    } else if (command == CMD_ROM_WRITE) {
 +      if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +        }
 +
 +        if (frame_len == 2) {
 +          eeprom_write(cmdbuf[0], cmdbuf[1]);
 +        }
 +    } else if (command == CMD_FW_VERSION) {
 +      kiss_indicate_version();
 +    } else if (command == CMD_PLATFORM) {
 +      kiss_indicate_platform();
 +    } else if (command == CMD_MCU) {
 +      kiss_indicate_mcu();
 +    } else if (command == CMD_BOARD) {
 +      kiss_indicate_board();
 +    } else if (command == CMD_CONF_SAVE) {
 +        // todo: add extra space in EEPROM so this isn't hardcoded
 +      eeprom_conf_save(interface_obj[0]);
 +    } else if (command == CMD_CONF_DELETE) {
 +      eeprom_conf_delete();
 +    } else if (command == CMD_FB_EXT) {
 +      #if HAS_DISPLAY == true
 +        if (sbyte == 0xFF) {
 +          kiss_indicate_fbstate();
 +        } else if (sbyte == 0x00) {
 +          ext_fb_disable();
 +          kiss_indicate_fbstate();
 +        } else if (sbyte == 0x01) {
 +          ext_fb_enable();
 +          kiss_indicate_fbstate();
 +        }
 +      #endif
 +    } else if (command == CMD_FB_WRITE) {
 +      if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +        }
 +        #if HAS_DISPLAY
 +          if (frame_len == 9) {
 +            uint8_t line = cmdbuf[0];
 +            if (line > 63) line = 63;
 +            int fb_o = line*8; 
 +            memcpy(fb+fb_o, cmdbuf+1, 8);
 +          }
 +        #endif
 +    } else if (command == CMD_FB_READ) {
 +      if (sbyte != 0x00) {
 +        kiss_indicate_fb();
 +      }
 +    } else if (command == CMD_DISP_READ) {
 +      if (sbyte != 0x00) { kiss_indicate_disp(); }
 +    } else if (command == CMD_DEV_HASH) {
 +        if (sbyte != 0x00) {
 +          kiss_indicate_device_hash();
 +        }
 +    } else if (command == CMD_DEV_SIG) {
 +        if (sbyte == FESC) {
 +              ESCAPE = true;
 +          } else {
 +              if (ESCAPE) {
 +                  if (sbyte == TFEND) sbyte = FEND;
 +                  if (sbyte == TFESC) sbyte = FESC;
 +                  ESCAPE = false;
 +              }
 +              if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +          }
 +
 +          if (frame_len == DEV_SIG_LEN) {
 +            memcpy(dev_sig, cmdbuf, DEV_SIG_LEN);
 +            device_save_signature();
 +          }
 +    } else if (command == CMD_FW_UPD) {
 +      if (sbyte == 0x01) {
 +        firmware_update_mode = true;
 +      } else {
 +        firmware_update_mode = false;
 +      }
 +    } else if (command == CMD_HASHES) {
 +        if (sbyte == 0x01) {
 +          kiss_indicate_target_fw_hash();
 +        } else if (sbyte == 0x02) {
 +          kiss_indicate_fw_hash();
 +        } else if (sbyte == 0x03) {
 +          kiss_indicate_bootloader_hash();
 +        } else if (sbyte == 0x04) {
 +          kiss_indicate_partition_table_hash();
 +        }
 +    } else if (command == CMD_FW_HASH) {
 +        if (sbyte == FESC) {
 +              ESCAPE = true;
 +          } else {
 +              if (ESCAPE) {
 +                  if (sbyte == TFEND) sbyte = FEND;
 +                  if (sbyte == TFESC) sbyte = FESC;
 +                  ESCAPE = false;
 +              }
 +              if (frame_len < CMD_L) cmdbuf[frame_len++] = sbyte;
 +          }
 +
 +          if (frame_len == DEV_HASH_LEN) {
 +            memcpy(dev_firmware_hash_target, cmdbuf, DEV_HASH_LEN);
 +            device_save_firmware_hash();
 +          }
 +    } else if (command == CMD_BT_CTRL) {
 +      #if HAS_BLUETOOTH || HAS_BLE
 +        if (sbyte == 0x00) {
 +          bt_stop();
 +          bt_conf_save(false);
 +        } else if (sbyte == 0x01) {
 +          bt_start();
 +          bt_conf_save(true);
 +        } else if (sbyte == 0x02) {
 +          if (bt_state == BT_STATE_OFF) {
 +            bt_start();
 +            bt_conf_save(true);
 +          }
 +          if (bt_state != BT_STATE_CONNECTED) {
 +            bt_enable_pairing();
 +          }
 +        }
 +      #endif
++    } else if (command == CMD_BT_UNPAIR) {
++      #if HAS_BLE
++        if (sbyte == 0x01) { bt_debond_all(); }
++      #endif
 +    } else if (command == CMD_DISP_INT) {
 +      #if HAS_DISPLAY
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            display_intensity = sbyte;
 +            di_conf_save(display_intensity);
 +            display_unblank();
 +        }
 +
 +      #endif
 +    } else if (command == CMD_DISP_ADDR) {
 +      #if HAS_DISPLAY
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            display_addr = sbyte;
 +            da_conf_save(display_addr);
 +        }
 +
 +      #endif
 +    } else if (command == CMD_DISP_BLNK) {
 +      #if HAS_DISPLAY
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            db_conf_save(sbyte);
 +            display_unblank();
 +        }
 +
 +      #endif
 +    } else if (command == CMD_DISP_ROT) {
 +      #if HAS_DISPLAY
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            drot_conf_save(sbyte);
 +            display_unblank();
 +        }
 +      #endif
 +    } else if (command == CMD_DIS_IA) {
 +      if (sbyte == FESC) {
 +          ESCAPE = true;
 +      } else {
 +          if (ESCAPE) {
 +              if (sbyte == TFEND) sbyte = FEND;
 +              if (sbyte == TFESC) sbyte = FESC;
 +              ESCAPE = false;
 +          }
 +          dia_conf_save(sbyte);
 +      }
 +    } else if (command == CMD_DISP_RCND) {
 +      #if HAS_DISPLAY
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            if (sbyte > 0x00) recondition_display = true;
 +        }
 +      #endif
 +    } else if (command == CMD_NP_INT) {
 +      #if HAS_NP
 +        if (sbyte == FESC) {
 +            ESCAPE = true;
 +        } else {
 +            if (ESCAPE) {
 +                if (sbyte == TFEND) sbyte = FEND;
 +                if (sbyte == TFESC) sbyte = FESC;
 +                ESCAPE = false;
 +            }
 +            sbyte;
 +            led_set_intensity(sbyte);
 +            np_int_conf_save(sbyte);
 +        }
 +
 +      #endif
 +    }
 +  }
 +}
 +
 +#if MCU_VARIANT == MCU_ESP32
 +  portMUX_TYPE update_lock = portMUX_INITIALIZER_UNLOCKED;
 +#endif
 +
 +bool medium_free(RadioInterface* radio) {
 +  radio->updateModemStatus();
 +  if (radio->getAvdInterference() && radio->getInterference()) { return false; }
 +  return !radio->getDCD();
 +}
 +
 +void validate_status() {
 +  #if MCU_VARIANT == MCU_ESP32
 +      // TODO: Get ESP32 boot flags
 +      uint8_t boot_flags = 0x02;
 +      uint8_t F_POR = 0x00;
 +      uint8_t F_BOR = 0x00;
 +      uint8_t F_WDR = 0x01;
 +  #elif MCU_VARIANT == MCU_NRF52
 +      // TODO: Get NRF52 boot flags
 +      uint8_t boot_flags = 0x02;
 +      uint8_t F_POR = 0x00;
 +      uint8_t F_BOR = 0x00;
 +      uint8_t F_WDR = 0x01;
 +  #endif
 +
 +  if (hw_ready || device_init_done) {
 +    hw_ready = false;
 +    Serial.write("Error, invalid hardware check state\r\n");
 +    #if HAS_DISPLAY
 +      if (disp_ready) {
 +        device_init_done = true;
 +        update_display();
 +      }
 +    #endif
 +    led_indicate_boot_error();
 +  }
 +
 +  if (boot_flags & (1<<F_POR)) {
 +    boot_vector = START_FROM_POWERON;
 +  } else if (boot_flags & (1<<F_BOR)) {
 +    boot_vector = START_FROM_BROWNOUT;
 +  } else if (boot_flags & (1<<F_WDR)) {
 +    boot_vector = START_FROM_BOOTLOADER;
 +  } else {
 +      Serial.write("Error, indeterminate boot vector\r\n");
 +      #if HAS_DISPLAY
 +        if (disp_ready) {
 +          device_init_done = true;
 +          update_display();
 +        }
 +      #endif
 +      led_indicate_boot_error();
 +  }
 +
 +  if (boot_vector == START_FROM_BOOTLOADER || boot_vector == START_FROM_POWERON) {
 +    if (eeprom_lock_set()) {
 +      if (eeprom_product_valid() && eeprom_model_valid() && eeprom_hwrev_valid()) {
 +        if (eeprom_checksum_valid()) {
 +          eeprom_ok = true;
 +          if (modems_installed) {
 +            if (device_init()) {
 +              hw_ready = true;
 +            } else {
 +              hw_ready = false;
 +            }
 +          } else {
 +            hw_ready = false;
 +            Serial.write("No radio module found\r\n");
 +            #if HAS_DISPLAY
 +              if (disp_ready) {
 +                device_init_done = true;
 +                update_display();
 +              }
 +            #endif
 +          }
 +        } else {
 +          hw_ready = false;
 +          Serial.write("Invalid EEPROM checksum\r\n");
 +          #if HAS_DISPLAY
 +            if (disp_ready) {
 +              device_init_done = true;
 +              update_display();
 +            }
 +          #endif
 +        }
 +      } else {
 +        hw_ready = false;
 +        Serial.write("Invalid EEPROM configuration\r\n");
 +        #if HAS_DISPLAY
 +          if (disp_ready) {
 +            device_init_done = true;
 +            update_display();
 +          }
 +        #endif
 +      }
 +    } else {
 +      hw_ready = false;
 +      Serial.write("Device unprovisioned, no device configuration found in EEPROM\r\n");
 +      #if HAS_DISPLAY
 +        if (disp_ready) {
 +          device_init_done = true;
 +          update_display();
 +        }
 +      #endif
 +    }
 +  } else {
 +    hw_ready = false;
 +    Serial.write("Error, incorrect boot vector\r\n");
 +    #if HAS_DISPLAY
 +      if (disp_ready) {
 +        device_init_done = true;
 +        update_display();
 +      }
 +    #endif
 +    led_indicate_boot_error();
 +  }
 +}
 +
 +void tx_queue_handler(RadioInterface* radio) {
 +  if (queue_height[radio->getIndex()] > 0) {
 +    if (radio->getCW() == -1) {
 +      radio->setCW(random(radio->getCWMin(), radio->getCWMax()));
 +      radio->setCWWaitTarget(radio->getCW() * radio->getCSMASlotMS());
 +    }
 +
 +    if (radio->getDifsWaitStart() == 0) {                                                  // DIFS wait not yet started
 +      if (medium_free(radio)) { radio->setDifsWaitStart(millis()); return; }                  // Set DIFS wait start time
 +      else               { return; } }                                            // Medium not yet free, continue waiting
 +    
 +    else {                                                                        // We are waiting for DIFS or CW to pass
 +      if (!medium_free(radio)) { radio->setDifsWaitStart(0); radio->setCWWaitStart(0); return; }   // Medium became occupied while in DIFS wait, restart waiting when free again
 +      else {                                                                      // Medium is free, so continue waiting
 +        if (millis() < radio->getDifsWaitStart()+radio->getDifsMS()) { return; }                       // DIFS has not yet passed, continue waiting
 +        else {                                                                    // DIFS has passed, and we are now in CW wait
 +          if (radio->getCWWaitStart() == 0) { radio->setCWWaitStart(millis()); return; }          // If we haven't started counting CW wait time, do it from now
 +          else {                                                                  // If we are already counting CW wait time, add it to the counter
 +            radio->addCWWaitPassed(millis()-radio->getCWWaitStart()); radio->setCWWaitStart(millis());
 +            if (radio->getCWWaitStatus()) { return; }                      // Contention window wait time has not yet passed, continue waiting
 +            else {                                                                // Wait time has passed, flush the queue
 +              if (!radio->getLimitRate()) { flush_queue(radio); } else { pop_queue(radio); }
 +              radio->resetCWWaitPassed(); radio->setCW(-1); radio->setDifsWaitStart(0); }
 +          }
 +        }
 +      }
 +    }
 +  }
 +}
 +
 +void work_while_waiting() { loop(); }
 +
 +void loop() {
 +    #if MCU_VARIANT == MCU_ESP32
 +      modem_packet_t *modem_packet = NULL;
 +      if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) {
 +        uint8_t packet_interface = modem_packet->interface;
 +        read_len[packet_interface] = modem_packet->len;
 +        last_rssi = modem_packet->rssi;
 +        last_snr_raw = modem_packet->snr_raw;
 +        memcpy(&pbuf, modem_packet->data, modem_packet->len);
 +        free(modem_packet);
 +        modem_packet = NULL;
 +
 +        kiss_indicate_stat_rssi(interface_obj[packet_interface]);
 +        kiss_indicate_stat_snr(interface_obj[packet_interface]);
 +        kiss_write_packet(packet_interface);
 +      }
 +
 +    #elif MCU_VARIANT == MCU_NRF52
 +      modem_packet_t *modem_packet = NULL;
 +      if(modem_packet_queue && xQueueReceive(modem_packet_queue, &modem_packet, 0) == pdTRUE && modem_packet) {
 +        uint8_t packet_interface = modem_packet->interface;
 +        read_len[packet_interface] = modem_packet->len;
 +        last_rssi = modem_packet->rssi;
 +        last_snr_raw = modem_packet->snr_raw;
 +        memcpy(&pbuf, modem_packet->data, modem_packet->len);
 +        free(modem_packet);
 +        modem_packet = NULL;
 +
 +        kiss_indicate_stat_rssi(interface_obj[packet_interface]);
 +        kiss_indicate_stat_snr(interface_obj[packet_interface]);
 +        kiss_write_packet(packet_interface);
 +      }
 +    #endif
 +
 +    bool ready = false;
 +    for (int i = 0; i < INTERFACE_COUNT; i++) {
 +        selected_radio = interface_obj[i];
 +        if (selected_radio->getRadioOnline()) {
 +            ready = true;
 +        }
 +    }
 +
 +
 +  // If at least one radio is online then we can continue
 +  if (ready) {
 +    for (int i = 0; i < INTERFACE_COUNT; i++) {
 +        selected_radio = interface_obj_sorted[i];
 +
 +        if (selected_radio->calculateALock() || !selected_radio->getRadioOnline()) {
 +            // skip this interface
 +            continue;
 +        }
 +        tx_queue_handler(selected_radio);
 +        selected_radio->checkModemStatus();
 +    }
 +  
 +  } else {
 +    if (hw_ready) {
 +      if (console_active) {
 +        #if HAS_CONSOLE
 +          console_loop();
 +        #endif
 +      } else {
 +        led_indicate_standby();
 +      }
 +    } else {
 +      led_indicate_not_ready();
 +      // shut down all radio interfaces
 +      for (int i = 0; i < INTERFACE_COUNT; i++) {
 +          stopRadio(interface_obj[i]);
 +      }
 +    }
 +  }
 +
 +  process_serial();
 +
 +  #if HAS_DISPLAY
 +    #if DISPLAY == OLED || DISPLAY == TFT || DISPLAY == ADAFRUIT_TFT
 +    if (disp_ready) update_display();
 +    #elif DISPLAY == EINK_BW || DISPLAY == EINK_3C
 +    // Display refreshes take so long on e-paper displays that they can disrupt
 +    // the regular operation of the device. To combat this the time it is
 +    // chosen to do so must be strategically chosen. Particularly on the
 +    // RAK4631, the display and the potentially installed SX1280 modem share
 +    // the same SPI bus. Thus it is not possible to solve this by utilising the
 +    // callback functionality to poll the modem in this case. todo, this may be
 +    // able to be improved in the future.
 +    if (disp_ready) {
 +        if (millis() - last_tx >= 4000) {
 +            if (millis() - last_rx >= 1000) {
 +                update_display();
 +            }
 +        }
 +    }
 +    #endif
 +  #endif
 +
 +  #if HAS_PMU
 +    if (pmu_ready) update_pmu();
 +  #endif
 +
 +  #if HAS_BLUETOOTH || HAS_BLE == true
 +    if (!console_active && bt_ready) update_bt();
 +  #endif
 +
 +  #if HAS_INPUT
 +    input_read();
 +  #endif
 +
 +  #if HAS_GPS
 +    while (gps_s.available() > 0) {
 +      if (gps.encode(gps_s.read()) && millis() - last_gps >= GPS_INTERVAL) {
 +          kiss_indicate_location();
 +          last_gps = millis();
 +      }
 +    }
 +    if (millis() > 5000 && gps.charsProcessed() < 10) {
 +        while (true) {
 +            Serial.println(F("No GPS detected: check wiring."));
 +        }
 +    }
 +  #endif
 +
 +  if (memory_low) {
 +    #if PLATFORM == PLATFORM_ESP32
 +      if (esp_get_free_heap_size() < 8192) {
 +        kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false;
 +      } else {
 +        memory_low = false;
 +      }
 +    #else
 +      kiss_indicate_error(ERROR_MEMORY_LOW); memory_low = false;
 +    #endif
 +  }
 +}
 +
 +void process_serial() {
 +      buffer_serial();
 +      if (!fifo_isempty(&serialFIFO)) serial_poll();
 +}
 +
 +void sleep_now() {
 +  #if HAS_SLEEP == true
++    for (int i = 0; i < INTERFACE_COUNT; i++) {
++        stopRadio(interface_obj[i]); // TODO: Check this on all platforms
++    }
 +    #if PLATFORM == PLATFORM_ESP32
++      #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_XIAO_S3
 +        display_intensity = 0;
 +        update_display(true);
 +      #endif
 +      #if PIN_DISP_SLEEP >= 0
 +        pinMode(PIN_DISP_SLEEP, OUTPUT);
 +        digitalWrite(PIN_DISP_SLEEP, DISP_SLEEP_LEVEL);
 +      #endif
 +      #if HAS_BLUETOOTH
 +        if (bt_state == BT_STATE_CONNECTED) {
 +          bt_stop();
 +          delay(100);
 +        }
 +      #endif
 +      esp_sleep_enable_ext0_wakeup(PIN_WAKEUP, WAKEUP_LEVEL);
 +      esp_deep_sleep_start();
 +    #elif PLATFORM == PLATFORM_NRF52
 +      #if BOARD_MODEL == BOARD_HELTEC_T114
 +        npset(0,0,0);
 +        digitalWrite(PIN_VEXT_EN, LOW);
 +        digitalWrite(PIN_T114_TFT_BLGT, HIGH);
 +        digitalWrite(PIN_T114_TFT_EN, HIGH);
 +      #elif BOARD_MODEL == BOARD_TECHO
 +        for (uint8_t i = display_intensity; i > 0; i--) { analogWrite(pin_backlight, i-1); delay(1); }
 +        epd_black(true); delay(300); epd_black(true); delay(300); epd_black(false);
 +        delay(2000);
 +        analogWrite(PIN_VEXT_EN, 0);
 +        delay(100);
 +      #endif
 +      sd_power_gpregret_set(0, 0x6d);
 +      nrf_gpio_cfg_sense_input(pin_btn_usr1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW);
 +      NRF_POWER->SYSTEMOFF = 1;
 +    #endif
 +  #endif
 +}
 +
 +void button_event(uint8_t event, unsigned long duration) {
 +    if (display_blanked) {
 +      display_unblank();
 +    } else {
 +      if (duration > 10000) {
 +        #if HAS_CONSOLE
 +          #if HAS_BLUETOOTH || HAS_BLE
 +            bt_stop();
 +          #endif
 +          console_active = true;
 +          console_start();
 +        #endif
 +      } else if (duration > 5000) {
 +        #if HAS_BLUETOOTH || HAS_BLE
 +          if (bt_state != BT_STATE_CONNECTED) { bt_enable_pairing(); }
 +        #endif
 +      } else if (duration > 700) {
 +        #if HAS_SLEEP
 +          sleep_now();
 +        #endif
 +      } else {
 +        #if HAS_BLUETOOTH || HAS_BLE
 +        if (bt_state != BT_STATE_CONNECTED) {
 +          if (bt_state == BT_STATE_OFF) {
 +            bt_start();
 +            bt_conf_save(true);
 +          } else {
 +            bt_stop();
 +            bt_conf_save(false);
 +          }
 +        }
 +        #endif
 +      }
 +    }
 +}
 +
 +void poll_buffers() {
 +    process_serial();
 +}
 +
 +volatile bool serial_polling = false;
 +void serial_poll() {
 +  serial_polling = true;
 +
 +  while (!fifo_isempty(&serialFIFO)) {
 +    char sbyte = fifo_pop(&serialFIFO);
 +    serial_callback(sbyte);
 +  }
 +
 +  serial_polling = false;
 +}
 +
 +#define MAX_CYCLES 20
 +
 +void buffer_serial() {
 +  if (!serial_buffering) {
 +    serial_buffering = true;
 +
 +    uint8_t c = 0;
 +
 +    #if HAS_BLUETOOTH || HAS_BLE == true
 +    while (
 +      c < MAX_CYCLES &&
 +      ( (bt_state != BT_STATE_CONNECTED && Serial.available()) || (bt_state == BT_STATE_CONNECTED && SerialBT.available()) )
 +      )
 +    #else
 +    while (c < MAX_CYCLES && Serial.available())
 +    #endif
 +    {
 +      c++;
 +
 +      #if HAS_BLUETOOTH || HAS_BLE == true
 +        if (bt_state == BT_STATE_CONNECTED) {
 +          if (!fifo_isfull(&serialFIFO)) {
 +            fifo_push(&serialFIFO, SerialBT.read());
 +          }
 +        } else {
 +          if (!fifo_isfull(&serialFIFO)) {
 +            fifo_push(&serialFIFO, Serial.read());
 +          }
 +        }
 +      #else
 +        if (!fifo_isfull(&serialFIFO)) {
 +          fifo_push(&serialFIFO, Serial.read());
 +        }
 +      #endif
 +    }
 +
 +    serial_buffering = false;
 +  }
 +}
index 09d355d1f02d0b862adb0d1b5ff64d583008f636,9f39167155e13489c9c7e1350a6a6c896507196d..e7509b4e81c8359138576766d378fd36b1b806ec
Binary files differ
diff --cc Utilities.h
index 01b82dfd0bc8c91ecef131e2be6bd358a4814cd3,cfd523c77bc03d1600430783088547f8f0ffcba8..8e0f81dd3d609d8b8907e90cffd7a1a610612be6
  // You should have received a copy of the GNU General Public License
  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
  
 +#include "Radio.hpp"
  #include "Config.h"
  
 -#if HAS_EEPROM
 +// Included for sorting
 +#include <algorithm>
 +#include <iterator>
 +
 +#if HAS_EEPROM 
      #include <EEPROM.h>
  #elif PLATFORM == PLATFORM_NRF52
+               #include <hal/nrf_rng.h>
      #include <Adafruit_LittleFS.h>
      #include <InternalFileSystem.h>
      using namespace Adafruit_LittleFS_Namespace;
@@@ -286,13 -322,6 +316,13 @@@ uint8_t boot_vector = 0x00
                void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
                void led_id_on()  { }
                void led_id_off() { }
-     #elif BOARD_MODEL == BOARD_XIAO_ESP32S3
++    #elif BOARD_MODEL == BOARD_XIAO_S3
 +        void led_rx_on() {}
 +        void led_rx_off() {}
 +        void led_tx_on() { digitalWrite(pin_led_tx, LED_ON); }
 +        void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); }
 +        void led_id_on() {}
 +        void led_id_off() {}
        #elif BOARD_MODEL == BOARD_GENERIC_ESP32
                void led_rx_on()  { digitalWrite(pin_led_rx, HIGH); }
                void led_rx_off() {     digitalWrite(pin_led_rx, LOW); }
  #endif
  
  void hard_reset(void) {
 -      #if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
 -              wdt_enable(WDTO_15MS);
 -              while(true) {
 -                      led_tx_on(); led_rx_off();
 -              }
 -      #elif MCU_VARIANT == MCU_ESP32
 +      #if MCU_VARIANT == MCU_ESP32
                ESP.restart();
        #elif MCU_VARIANT == MCU_NRF52
-         NVIC_SystemReset();
+     NVIC_SystemReset();
        #endif
  }
  
@@@ -1191,81 -1251,80 +1221,82 @@@ void set_implicit_length(uint8_t len) 
        }
  }
  
 -int getTxPower() {
 -      uint8_t txp = LoRa->getTxPower();
 -      return (int)txp;
 -}
 -
 -void setTXPower() {
 -      if (radio_online) {
 -              if (model == MODEL_11) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -              if (model == MODEL_12) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -
 -              if (model == MODEL_C6) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -        if (model == MODEL_C7) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -
 -              if (model == MODEL_A1) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_A2) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_A3) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -              if (model == MODEL_A4) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -              if (model == MODEL_A5) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_A6) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_A7) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_A8) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_A9) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_AA) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_AC) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_BA) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_BB) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_B3) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_B4) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_B8) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_B9) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_C4) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_C9) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_C5) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_CA) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_D4) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_D9) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_DB) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_DC) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_DD) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_DE) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_E4) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_E9) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_E3) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_E8) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -
 -              if (model == MODEL_FE) LoRa->setTxPower(lora_txp, PA_OUTPUT_PA_BOOST_PIN);
 -              if (model == MODEL_FF) LoRa->setTxPower(lora_txp, PA_OUTPUT_RFO_PIN);
 -      }
 -}
 -
 -
 -void getBandwidth() {
 -      if (radio_online) {
 -                      lora_bw = LoRa->getSignalBandwidth();
 -      }
 -      updateBitrate();
 -}
 +void setTXPower(RadioInterface* radio, int txp) {
 +    // Todo, revamp this function. The current parameters for setTxPower are
 +    // suboptimal, as some chips have power amplifiers which means that the max
 +    // dBm is not always the same.
 +    if (model == MODEL_12) {
 +        if (interfaces[radio->getIndex()] == SX1280) {
 +            radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +        } else {
 +            radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +        }
 +    }
 +    if (model == MODEL_13) {
 +        if (interfaces[radio->getIndex()] == SX1280) {
 +            radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +        } else {
 +            radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +        }
 +    }
  
 -void setBandwidth() {
 -      if (radio_online) {
 -              LoRa->setSignalBandwidth(lora_bw);
 -              getBandwidth();
 -      }
 -}
 +    if (model == MODEL_21) {
 +        if (interfaces[radio->getIndex()] == SX1280) {
 +            radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +        } else {
 +            radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +        }
 +    }
  
 -void getFrequency() {
 -      if (radio_online) {
 -              lora_freq = LoRa->getFrequency();
 +    if (model == MODEL_A1) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_A2) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_A3) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +    if (model == MODEL_A4) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +    if (model == MODEL_A5) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +    if (model == MODEL_A6) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_A7) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_A8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_A9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_AA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_AC) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_B3) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_B4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_B8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_B9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_BA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_BB) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_C4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_C9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_C5) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_C6) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +    if (model == MODEL_C7) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +    if (model == MODEL_CA) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_D4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_D9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_DB) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_DC) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_DD) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
++    if (model == MODEL_DE) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_E4) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_E9) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_E3) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_E8) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +
 +    if (model == MODEL_FE) radio->setTxPower(txp, PA_OUTPUT_PA_BOOST_PIN);
 +    if (model == MODEL_FF) radio->setTxPower(txp, PA_OUTPUT_RFO_PIN);
 +}
 +
 +uint8_t getRandom(RadioInterface* radio) {
 +      if (radio->getRadioOnline()) {
 +              return radio->random();
 +      } else {
 +              return 0x00;
        }
  }
  
@@@ -1480,10 -1497,12 +1511,10 @@@ bool eeprom_product_valid() 
      uint8_t rval = eeprom_read(eeprom_addr(ADDR_PRODUCT));
    #endif
  
 -      #if PLATFORM == PLATFORM_AVR
 -      if (rval == PRODUCT_RNODE || rval == PRODUCT_HMBRW) {
 -      #elif PLATFORM == PLATFORM_ESP32
 -      if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1  || rval == PRODUCT_XIAO_S3) {
 +      #if PLATFORM == PLATFORM_ESP32
-       if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_H_W_PAPER || rval == PRODUCT_XIAO_ESP32S3) {
++      if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_H_W_PAPER || rval == PRODUCT_XIAO_S3) {
        #elif PLATFORM == PLATFORM_NRF52
 -      if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) {
 +      if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_OPENCOM_XL || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) {
        #else
        if (false) {
        #endif
index 43c85ae83d7e09bb663d4b7a871dbc2cd2bd5282,0000000000000000000000000000000000000000..555144b08b5f338d3822d4114bef8c1d22426fd9
mode 100644,000000..100644
--- /dev/null
@@@ -1,153 -1,0 +1,172 @@@
 +// Copyright (C) 2024, Mark Qvist
 +
 +// This program is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +
 +// This program is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 +// GNU General Public License for more details.
 +
 +// You should have received a copy of the GNU General Public License
 +// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 +
 +#include <Arduino.h>
 +#include "../../Boards.h"
 +
 +#if PLATFORM != PLATFORM_NRF52
 +#if HAS_BLE
 +
 +#include "BLESerial.h"
 +
 +uint32_t bt_passkey_callback();
 +void bt_passkey_notify_callback(uint32_t passkey);
 +bool bt_security_request_callback();
 +void bt_authentication_complete_callback(esp_ble_auth_cmpl_t auth_result);
 +bool bt_confirm_pin_callback(uint32_t pin);
 +void bt_connect_callback(BLEServer *server);
 +void bt_disconnect_callback(BLEServer *server);
 +bool bt_client_authenticated();
 +
 +uint32_t BLESerial::onPassKeyRequest() { return bt_passkey_callback(); }
 +void BLESerial::onPassKeyNotify(uint32_t passkey) { bt_passkey_notify_callback(passkey); }
 +bool BLESerial::onSecurityRequest() { return bt_security_request_callback(); }
 +void BLESerial::onAuthenticationComplete(esp_ble_auth_cmpl_t auth_result) { bt_authentication_complete_callback(auth_result); }
 +void BLESerial::onConnect(BLEServer *server) { bt_connect_callback(server); }
 +void BLESerial::onDisconnect(BLEServer *server) { bt_disconnect_callback(server); ble_server->startAdvertising(); }
 +bool BLESerial::onConfirmPIN(uint32_t pin) { return bt_confirm_pin_callback(pin); };
 +bool BLESerial::connected() { return ble_server->getConnectedCount() > 0; }
 +
 +int BLESerial::read() {
 +  int result = this->rx_buffer.pop();
 +  if (result == '\n') { this->numAvailableLines--; }
 +  return result;
 +}
 +
 +size_t BLESerial::readBytes(uint8_t *buffer, size_t bufferSize) {
 +  int i = 0;
 +  while (i < bufferSize && available()) { buffer[i] = (uint8_t)this->rx_buffer.pop(); i++; }
 +  return i;
 +}
 +
 +int BLESerial::peek() {
 +  if (this->rx_buffer.getLength() == 0) return -1;
 +  return this->rx_buffer.get(0);
 +}
 +
 +int BLESerial::available() { return this->rx_buffer.getLength(); }
 +
 +size_t BLESerial::print(const char *str) {
 +  if (ble_server->getConnectedCount() <= 0) return 0;
 +  size_t written = 0; for (size_t i = 0; str[i] != '\0'; i++)  { written += this->write(str[i]); }
 +  flush();
 +
 +  return written;
 +}
 +
 +size_t BLESerial::write(const uint8_t *buffer, size_t bufferSize) {
 +  if (ble_server->getConnectedCount() <= 0) { return 0; } else {
 +    size_t written = 0; for (int i = 0; i < bufferSize; i++) { written += this->write(buffer[i]); }
 +    flush();
 +
 +    return written;
 +  }
 +}
 +
 +size_t BLESerial::write(uint8_t byte) {
 +  if (bt_client_authenticated()) {
 +    if (ble_server->getConnectedCount() <= 0) { return 0; } else {
 +      this->transmitBuffer[this->transmitBufferLength] = byte;
 +      this->transmitBufferLength++;
 +      if (this->transmitBufferLength == maxTransferSize) { flush(); }
 +      return 1;
 +    }
 +  } else {
 +    return 0;
 +  }
 +}
 +
 +void BLESerial::flush() {
 +  if (this->transmitBufferLength > 0) {
 +    TxCharacteristic->setValue(this->transmitBuffer, this->transmitBufferLength);
 +    this->transmitBufferLength = 0;
 +    this->lastFlushTime = millis();
 +    TxCharacteristic->notify(true);
 +  }
 +}
 +
++void BLESerial::disconnect() {
++  if (ble_server->getConnectedCount() > 0) {
++    uint16_t conn_id = ble_server->getConnId();
++    // Serial.printf("Have connected: %d\n", conn_id);
++    ble_server->disconnect(conn_id);
++    // Serial.println("Disconnected");
++  } else {
++    // Serial.println("No connected");
++  }
++}
++
 +void BLESerial::begin(const char *name) {
 +  ConnectedDeviceCount = 0;
 +  BLEDevice::init(name);
 +
 +  esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); 
 +  esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9);
 +  esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN ,ESP_PWR_LVL_P9);
 +
 +  ble_server = BLEDevice::createServer();
 +  ble_server->setCallbacks(this);
 +  BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM);
 +  BLEDevice::setSecurityCallbacks(this);
 +
 +  SetupSerialService();
++  this->startAdvertising();
++}
 +
++void BLESerial::startAdvertising() {
 +  ble_adv = BLEDevice::getAdvertising();
 +  ble_adv->addServiceUUID(BLE_SERIAL_SERVICE_UUID);
 +  ble_adv->setMinPreferred(0x20);
 +  ble_adv->setMaxPreferred(0x40);
 +  ble_adv->setScanResponse(true);
 +  ble_adv->start();
 +}
 +
++void BLESerial::stopAdvertising() {
++  ble_adv = BLEDevice::getAdvertising();
++  ble_adv->stop();
++}
++
 +void BLESerial::end() { BLEDevice::deinit(); }
 +
 +void BLESerial::onWrite(BLECharacteristic *characteristic) {
 +  if (characteristic->getUUID().toString() == BLE_RX_UUID) {
 +    auto value = characteristic->getValue();
 +    for (int i = 0; i < value.length(); i++) { rx_buffer.push(value[i]); }
 +  }
 +}
 +
 +void BLESerial::SetupSerialService() {
 +  SerialService = ble_server->createService(BLE_SERIAL_SERVICE_UUID);
 +
 +  RxCharacteristic = SerialService->createCharacteristic(BLE_RX_UUID, BLECharacteristic::PROPERTY_WRITE);
 +  RxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM);
 +  RxCharacteristic->addDescriptor(new BLE2902());
 +  RxCharacteristic->setWriteProperty(true);
 +  RxCharacteristic->setCallbacks(this);
 +
 +  TxCharacteristic = SerialService->createCharacteristic(BLE_TX_UUID, BLECharacteristic::PROPERTY_NOTIFY);
 +  TxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM);
 +  TxCharacteristic->addDescriptor(new BLE2902());
 +  TxCharacteristic->setNotifyProperty(true);
 +  TxCharacteristic->setReadProperty(true);
 +
 +  SerialService->start();
 +}
 +
 +BLESerial::BLESerial() { }
 +
 +#endif
 +#endif
index e3b25d45a7d0ca25d48b6a614c29f073ba76521d,0000000000000000000000000000000000000000..e7e0d0f754ba5f04f9fd12e8326423ec4ff66025
mode 100644,000000..100644
--- /dev/null
@@@ -1,133 -1,0 +1,136 @@@
 +// Copyright (C) 2024, Mark Qvist
 +
 +// This program is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +
 +// This program is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 +// GNU General Public License for more details.
 +
 +// You should have received a copy of the GNU General Public License
 +// along with this program.  If not, see <https://www.gnu.org/licenses/>.
 +
 +#include "../../Boards.h"
 +
 +#if PLATFORM != PLATFORM_NRF52
 +#if HAS_BLE
 +
 +#include <Arduino.h>
 +
 +#include <BLEDevice.h>
 +#include <BLEUtils.h>
 +#include <BLEServer.h>
 +#include <BLE2902.h>
 +
 +template <size_t n>
 +class BLEFIFO {
 +private:
 +      uint8_t buffer[n];
 +      int head = 0;
 +      int tail = 0;
 +
 +public:
 +      void push(uint8_t value) {
 +              buffer[head] = value;
 +              head = (head + 1) % n;
 +              if (head == tail) { tail = (tail + 1) % n; }
 +      }
 +
 +      int pop() {
 +              if (head == tail) {
 +                      return -1;
 +              } else {
 +                      uint8_t value = buffer[tail];
 +                      tail = (tail + 1) % n;
 +                      return value;
 +              }
 +      }
 +
 +      void clear() { head = 0; tail = 0; }
 +
 +      int get(size_t index) {
 +              if (index >= this->getLength()) {
 +                      return -1;
 +              } else {
 +                      return buffer[(tail + index) % n];
 +              }
 +      }
 +
 +      size_t getLength() {
 +              if (head >= tail) {
 +                      return head - tail;
 +              } else {
 +                      return n - tail + head;
 +              }
 +      }
 +};
 +
 +#define RX_BUFFER_SIZE 6144
 +#define BLE_BUFFER_SIZE 512 // Must fit in max GATT attribute length
 +#define MIN_MTU 50
 +
 +class BLESerial : public BLECharacteristicCallbacks, public BLEServerCallbacks, public BLESecurityCallbacks, public Stream {
 +public:
 +  BLESerial();
 +
 +  void begin(const char *name);
 +  void end();
++  void disconnect();
++  void startAdvertising();
++  void stopAdvertising();
 +  void onWrite(BLECharacteristic *characteristic);
 +  int available();
 +  int peek();
 +  int read();
 +  size_t readBytes(uint8_t *buffer, size_t bufferSize);
 +  size_t write(uint8_t byte);
 +  size_t write(const uint8_t *buffer, size_t bufferSize);
 +  size_t print(const char *value);
 +  void flush();
 +  void onConnect(BLEServer *server);
 +  void onDisconnect(BLEServer *server);
 +
 +  uint32_t onPassKeyRequest();
 +  void onPassKeyNotify(uint32_t passkey);
 +  bool onSecurityRequest();
 +  void onAuthenticationComplete(esp_ble_auth_cmpl_t);
 +  bool onConfirmPIN(uint32_t pin);
 +
 +  bool connected();
 +
 +  BLEServer *ble_server;
 +  BLEAdvertising *ble_adv;
 +  BLEService *SerialService;
 +  BLECharacteristic *TxCharacteristic;
 +  BLECharacteristic *RxCharacteristic;
 +  size_t transmitBufferLength;
 +  unsigned long long lastFlushTime;
 +
 +private:
 +  BLESerial(BLESerial const &other) = delete;
 +  void operator=(BLESerial const &other) = delete;
 +
 +  BLEFIFO<RX_BUFFER_SIZE> rx_buffer;
 +  size_t numAvailableLines;
 +  uint8_t transmitBuffer[BLE_BUFFER_SIZE];
 +
 +  int ConnectedDeviceCount;
 +  void SetupSerialService();
 +
 +  uint16_t peerMTU;
 +  uint16_t maxTransferSize = BLE_BUFFER_SIZE;
 +
 +  bool checkMTU();
 +
 +  const char *BLE_SERIAL_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
 +  const char *BLE_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
 +  const char *BLE_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
 +
 +  bool started = false;
 +};
 +
 +#endif
 +#endif