diff --git a/Boards.h b/Boards.h
index 1345081..49791d9 100644
--- a/Boards.h
+++ b/Boards.h
@@ -104,6 +104,11 @@
#define MODEL_C6 0xC6 // Heltec Mesh Node T114, 470-510 MHz
#define MODEL_C7 0xC7 // Heltec Mesh Node T114, 863-928 MHz
+ #define PRODUCT_VME213 0xC9 // Heltec VisionMaster E213
+ #define BOARD_VME213 0x40
+ #define MODEL_CD 0xCD // VisionMaster E213, 470-510 MHz
+ #define MODEL_CE 0xCE // VisionMaster E213, 863-928 MHz
+
#define PRODUCT_TECHO 0x15 // LilyGO T-Echo devices
#define BOARD_TECHO 0x44
#define MODEL_16 0x16 // T-Echo 433 MHz
@@ -432,6 +437,64 @@
const int pin_miso = 11;
const int pin_sclk = 9;
+ #elif BOARD_MODEL == BOARD_VME213
+ // Heltec VisionMaster E213 with ESP32-S3R8 + SX1262 + 2.13" E-Ink
+ #define IS_ESP32S3 true
+ #define HAS_DISPLAY true // 2.13" E-Ink LCMEN2R13EFC1, 250x122 pixels
+ #define HAS_EINK true // Enable E-Ink specific display code
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_PMU false
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+
+ const int pin_btn_usr1 = 0;
+
+ // No physical LEDs on VME213, display only
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 35;
+ const int pin_led_tx = 35;
+ #else
+ const int pin_led_rx = -1;
+ const int pin_led_tx = -1;
+ #endif
+
+ // SX1262 LoRa radio configuration
+ #define MODEM SX1262
+ #define HAS_TCXO true
+ const int pin_tcxo_enable = -1;
+ #define HAS_BUSY true
+ #define DIO2_AS_RF_SWITCH true
+
+ // SX1262 pins (from Heltec board-config.h)
+ const int pin_cs = 8;
+ const int pin_busy = 13;
+ const int pin_dio = 14;
+ const int pin_reset = 12;
+ const int pin_mosi = 10;
+ const int pin_miso = 11;
+ const int pin_sclk = 9;
+
+ // E-Ink display pins (HT_ICMEN2R13EFC1 controller)
+ const int pin_disp_reset = 3;
+ const int pin_disp_dc = 2;
+ const int pin_disp_cs = 5;
+ const int pin_disp_busy = 1;
+ const int pin_disp_sck = 4;
+ const int pin_disp_mosi = 6;
+ const int pin_disp_miso = -1;
+
+ // Vext power control (HIGH=ON, LOW=OFF)
+ #define Vext GPIO_NUM_18
+
+ // I2C for sensors (QuickLink)
+ #define I2C_SDA 39
+ #define I2C_SCL 38
+
#elif BOARD_MODEL == BOARD_RNODE_NG_20
#define HAS_DISPLAY true
#define HAS_BLUETOOTH true
diff --git a/Display.h b/Display.h
index 83dd394..bea9000 100644
--- a/Display.h
+++ b/Display.h
@@ -84,6 +84,14 @@
#define DISP_W 128
#define DISP_H 64
#define DISP_ADDR -1
+#elif BOARD_MODEL == BOARD_VME213
+ #include "src/LCMEN2R13EFC1.h"
+ #include "src/einkDetect_VME213.h"
+ // VME213 E-Ink display: 250x122 pixels
+ #define DISP_W 250
+ #define DISP_H 122
+ #define DISP_ADDR -1
+ #define VME213_REFRESH_RATIO 10 // 10 FAST : 1 FULL refresh
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
#define DISP_RST -1
#define DISP_ADDR 0x3C
@@ -121,6 +129,18 @@
uint32_t last_epd_refresh = 0;
uint32_t last_epd_full_refresh = 0;
#define REFRESH_PERIOD 300000
+#elif BOARD_MODEL == BOARD_VME213
+ LCMEN2R13EFC1 vme213_display;
+ EInkChipType vme213_chip_type = EINK_LCMEN213EFC1;
+ uint8_t vme213_displayBuffer[4000]; // 250x122 pixels = 4000 bytes
+ uint32_t last_epd_refresh = 0;
+ uint32_t last_epd_full_refresh = 0;
+ uint8_t vme213_fast_refresh_count = 0;
+ #define REFRESH_PERIOD 300000
+ // Compatibility macro
+ #define display vme213_display
+ #define SSD1306_BLACK 0
+ #define SSD1306_WHITE 1
#else
Adafruit_SSD1306 display(DISP_W, DISP_H, &Wire, DISP_RST);
#endif
@@ -189,6 +209,19 @@ void update_area_positions() {
p_as_x = 64;
p_as_y = 0;
}
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213: 250x122 landscape - split into two 125x122 areas
+ if (disp_mode == DISP_MODE_LANDSCAPE) {
+ p_ad_x = 0;
+ p_ad_y = 0;
+ p_as_x = 125;
+ p_as_y = 0;
+ } else {
+ p_ad_x = 0;
+ p_ad_y = 0;
+ p_as_x = 0;
+ p_as_y = 61;
+ }
#else
if (disp_mode == DISP_MODE_PORTRAIT) {
p_ad_x = 0 * DISPLAY_SCALE;
@@ -247,6 +280,58 @@ uint8_t display_contrast = 0x00;
}
#endif
+#if BOARD_MODEL == BOARD_VME213
+// VME213 E-Ink display rendering helpers
+void vme213_drawPixel(uint16_t x, uint16_t y, uint8_t color) {
+ if (x >= DISP_W || y >= DISP_H) return;
+ uint16_t byteIndex = y * ((DISP_W + 7) / 8) + (x / 8);
+ uint8_t bitMask = 0x80 >> (x % 8);
+
+ if (color == SSD1306_WHITE) {
+ vme213_displayBuffer[byteIndex] |= bitMask; // White pixel
+ } else {
+ vme213_displayBuffer[byteIndex] &= ~bitMask; // Black pixel
+ }
+}
+
+void vme213_fillScreen(uint8_t color) {
+ memset(vme213_displayBuffer, (color == SSD1306_WHITE) ? 0xFF : 0x00, sizeof(vme213_displayBuffer));
+}
+
+void vme213_drawText(uint16_t x, uint16_t y, const char* text, uint8_t size) {
+ // Simple text rendering using 5x7 font
+ // This is a placeholder - full implementation would use Adafruit_GFX font rendering
+ // For now, just mark text position
+ for (int i = 0; i < 10; i++) {
+ vme213_drawPixel(x + i, y, SSD1306_BLACK);
+ }
+}
+
+void vme213_renderStatus() {
+ vme213_fillScreen(SSD1306_WHITE);
+
+ // Header area (top 20 pixels)
+ for (int y = 0; y < 20; y++) {
+ for (int x = 0; x < DISP_W; x++) {
+ vme213_drawPixel(x, y, (y % 2 == 0) ? SSD1306_BLACK : SSD1306_WHITE);
+ }
+ }
+
+ // Status text placeholder
+ vme213_drawText(10, 30, "RNode", 2);
+
+ // Signal indicator (simple bars)
+ if (radio_online) {
+ for (int i = 0; i < 5; i++) {
+ int barHeight = 10 + i * 5;
+ for (int y = 0; y < barHeight; y++) {
+ vme213_drawPixel(10 + i * 8, 100 - y, SSD1306_BLACK);
+ }
+ }
+ }
+}
+#endif
+
bool display_init() {
#if HAS_DISPLAY
#if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0
@@ -299,6 +384,27 @@ bool display_init() {
pinMode(pin_backlight, OUTPUT);
analogWrite(pin_backlight, 0);
#endif
+ #elif BOARD_MODEL == BOARD_VME213
+ // Enable Vext power (GPIO 18) for peripherals
+ pinMode(Vext, OUTPUT);
+ digitalWrite(Vext, HIGH);
+ delay(100);
+
+ // Detect E-Ink chip type
+ vme213_chip_type = detectEInkChip(pin_disp_reset, pin_disp_busy);
+
+ // Initialize SPI for E-Ink display
+ SPI.begin(pin_disp_sck, pin_disp_miso, pin_disp_mosi, pin_disp_cs);
+
+ // Initialize E-Ink driver
+ vme213_display.begin(&SPI, pin_disp_dc, pin_disp_cs, pin_disp_busy, pin_disp_reset);
+
+ // Clear buffer
+ memset(vme213_displayBuffer, 0xFF, sizeof(vme213_displayBuffer));
+
+ // Perform initial FULL refresh to clear display
+ vme213_display.update(vme213_displayBuffer, LCMEN2R13EFC1::UPDATE_FULL);
+ vme213_fast_refresh_count = 0;
#elif BOARD_MODEL == BOARD_TBEAM_S_V1
Wire.begin(SDA_OLED, SCL_OLED);
#elif BOARD_MODEL == BOARD_XIAO_S3
@@ -348,6 +454,9 @@ bool display_init() {
#if BOARD_MODEL == BOARD_TECHO
// Don't check if display is actually connected
if(false) {
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213 E-Ink display initialization already done, skip connection check
+ if(false) {
#elif BOARD_MODEL == BOARD_TDECK
display.init(240, 320);
display.setSPISpeed(80e6);
@@ -414,6 +523,10 @@ bool display_init() {
#elif BOARD_MODEL == BOARD_TECHO
disp_mode = DISP_MODE_PORTRAIT;
display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213 E-Ink: 250x122 landscape by default
+ disp_mode = DISP_MODE_LANDSCAPE;
+ // No setRotation for VME213 (driver handles orientation)
#else
disp_mode = DISP_MODE_PORTRAIT;
display.setRotation(3);
@@ -429,7 +542,7 @@ bool display_init() {
stat_area.cp437(true);
disp_area.cp437(true);
- #if BOARD_MODEL != BOARD_HELTEC_T114
+ #if BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_VME213
display.cp437(true);
#endif
@@ -990,6 +1103,12 @@ void update_display(bool blank = false) {
epd_blank();
epd_blanked = true;
}
+ #elif BOARD_MODEL == BOARD_VME213
+ if (!epd_blanked) {
+ vme213_fillScreen(SSD1306_WHITE);
+ vme213_display.update(vme213_displayBuffer, LCMEN2R13EFC1::UPDATE_FULL);
+ epd_blanked = true;
+ }
#endif
#if BOARD_MODEL == BOARD_HELTEC_T114
@@ -1015,6 +1134,8 @@ void update_display(bool blank = false) {
#if BOARD_MODEL == BOARD_HELTEC_T114
display.clear();
+ #elif BOARD_MODEL == BOARD_VME213
+ // E-Ink buffer will be redrawn completely
#elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO
display.clearDisplay();
#endif
@@ -1027,6 +1148,9 @@ void update_display(bool blank = false) {
#if BOARD_MODEL == BOARD_TECHO
display.setFullWindow();
display.fillScreen(SSD1306_WHITE);
+ #elif BOARD_MODEL == BOARD_VME213
+ // Render RNode UI to E-Ink buffer
+ vme213_renderStatus();
#endif
update_stat_area();
@@ -1040,6 +1164,24 @@ void update_display(bool blank = false) {
last_epd_refresh = millis();
epd_blanked = false;
}
+ #elif BOARD_MODEL == BOARD_VME213
+ if (current-last_epd_refresh >= epd_update_interval) {
+ // Decide refresh type: 10 FAST : 1 FULL
+ LCMEN2R13EFC1::UpdateType refreshType;
+ if (vme213_fast_refresh_count >= VME213_REFRESH_RATIO || current-last_epd_full_refresh >= REFRESH_PERIOD) {
+ refreshType = LCMEN2R13EFC1::UPDATE_FULL;
+ vme213_fast_refresh_count = 0;
+ last_epd_full_refresh = millis();
+ } else {
+ refreshType = LCMEN2R13EFC1::UPDATE_FAST;
+ vme213_fast_refresh_count++;
+ }
+
+ // Perform E-Ink update
+ vme213_display.update(vme213_displayBuffer, refreshType);
+ last_epd_refresh = millis();
+ epd_blanked = false;
+ }
#elif BOARD_MODEL != BOARD_TDECK
display.display();
#endif
diff --git a/Makefile b/Makefile
index 758f664..47ab65f 100644
--- a/Makefile
+++ b/Makefile
@@ -128,6 +128,9 @@ firmware-heltec32_v3:
firmware-heltec32_v4:
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=0x3F\""
+firmware-vme213:
+ 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=0x40\""
+
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\""
@@ -400,6 +403,15 @@ release-heltec32_v4: check_bt_buffers
zip --junk-paths ./Release/rnode_firmware_heltec32v4pa.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_heltec32v4pa.boot_app0 build/rnode_firmware_heltec32v4pa.bin build/rnode_firmware_heltec32v4pa.bootloader build/rnode_firmware_heltec32v4pa.partitions
rm -r build
+release-vme213:
+ arduino-cli compile --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=0x40\""
+ cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_vme213.boot_app0
+ cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin build/rnode_firmware_vme213.bin
+ cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_vme213.bootloader
+ cp build/esp32.esp32.esp32s3/RNode_Firmware.ino.partitions.bin build/rnode_firmware_vme213.partitions
+ zip --junk-paths ./Release/rnode_firmware_vme213.zip ./Release/esptool/esptool.py ./Release/console_image.bin build/rnode_firmware_vme213.boot_app0 build/rnode_firmware_vme213.bin build/rnode_firmware_vme213.bootloader build/rnode_firmware_vme213.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\""
cp ~/.arduino15/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)/tools/partitions/boot_app0.bin build/rnode_firmware_heltec32v2.boot_app0
diff --git a/RNode_Firmware/BLESerial.cpp b/RNode_Firmware/BLESerial.cpp
new file mode 100644
index 0000000..2957755
--- /dev/null
+++ b/RNode_Firmware/BLESerial.cpp
@@ -0,0 +1,171 @@
+// 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 .
+
+#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
\ No newline at end of file
diff --git a/RNode_Firmware/BLESerial.h b/RNode_Firmware/BLESerial.h
new file mode 100644
index 0000000..f845b56
--- /dev/null
+++ b/RNode_Firmware/BLESerial.h
@@ -0,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 .
+
+#include "Boards.h"
+
+#if PLATFORM != PLATFORM_NRF52
+#if HAS_BLE
+
+#include
+
+#include
+#include
+#include
+#include
+
+template
+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_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
\ No newline at end of file
diff --git a/RNode_Firmware/Bluetooth.h b/RNode_Firmware/Bluetooth.h
new file mode 100644
index 0000000..616c4b6
--- /dev/null
+++ b/RNode_Firmware/Bluetooth.h
@@ -0,0 +1,615 @@
+// 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 .
+
+#if MCU_VARIANT == MCU_ESP32
+
+#elif MCU_VARIANT == MCU_NRF52
+#endif
+
+#if MCU_VARIANT == MCU_ESP32
+ #if HAS_BLUETOOTH == true
+ #include "BluetoothSerial.h"
+ #include "esp_bt_main.h"
+ #include "esp_bt_device.h"
+ BluetoothSerial SerialBT;
+ #elif HAS_BLE == true
+ #include "esp_bt_main.h"
+ #include "esp_bt_device.h"
+ #include "BLESerial.h"
+ BLESerial SerialBT;
+ #endif
+
+#elif MCU_VARIANT == MCU_NRF52
+ #include
+ #include
+ #define BLE_RX_BUF 6144
+ BLEUart SerialBT(BLE_RX_BUF);
+ BLEDis bledis;
+ BLEBas blebas;
+ bool SerialBT_init = false;
+#endif
+
+#define BT_PAIRING_TIMEOUT 35000
+#define BLE_FLUSH_TIMEOUT 20
+uint32_t bt_pairing_started = 0;
+
+#define BT_DEV_ADDR_LEN 6
+#define BT_DEV_HASH_LEN 16
+uint8_t dev_bt_mac[BT_DEV_ADDR_LEN];
+char bt_da[BT_DEV_ADDR_LEN];
+char bt_dh[BT_DEV_HASH_LEN];
+char bt_devname[11];
+
+#if MCU_VARIANT == MCU_ESP32
+ #if HAS_BLUETOOTH == true
+
+ void bt_confirm_pairing(uint32_t numVal) {
+ bt_ssp_pin = numVal;
+ kiss_indicate_btpin();
+ if (bt_allow_pairing) {
+ SerialBT.confirmReply(true);
+ } else {
+ SerialBT.confirmReply(false);
+ }
+ }
+
+ void bt_stop() {
+ display_unblank();
+ if (bt_state != BT_STATE_OFF) {
+ SerialBT.end();
+ bt_allow_pairing = false;
+ bt_state = BT_STATE_OFF;
+ }
+ }
+
+ void bt_start() {
+ display_unblank();
+ if (bt_state == BT_STATE_OFF) {
+ SerialBT.begin(bt_devname);
+ bt_state = BT_STATE_ON;
+ }
+ }
+
+ void bt_enable_pairing() {
+ display_unblank();
+ if (bt_state == BT_STATE_OFF) bt_start();
+ bt_allow_pairing = true;
+ bt_pairing_started = millis();
+ bt_state = BT_STATE_PAIRING;
+ }
+
+ void bt_disable_pairing() {
+ display_unblank();
+ bt_allow_pairing = false;
+ bt_ssp_pin = 0;
+ bt_state = BT_STATE_ON;
+ }
+
+ void bt_pairing_complete(boolean success) {
+ display_unblank();
+ if (success) {
+ bt_disable_pairing();
+ } else {
+ bt_ssp_pin = 0;
+ }
+ }
+
+ void bt_connection_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
+ display_unblank();
+ if(event == ESP_SPP_SRV_OPEN_EVT) {
+ bt_state = BT_STATE_CONNECTED;
+ cable_state = CABLE_STATE_DISCONNECTED;
+ }
+
+ if(event == ESP_SPP_CLOSE_EVT ){
+ bt_state = BT_STATE_ON;
+ }
+ }
+
+ bool bt_setup_hw() {
+ if (!bt_ready) {
+ if (EEPROM.read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) {
+ bt_enabled = true;
+ } else {
+ bt_enabled = false;
+ }
+ if (btStart()) {
+ if (esp_bluedroid_init() == ESP_OK) {
+ if (esp_bluedroid_enable() == ESP_OK) {
+ const uint8_t* bda_ptr = esp_bt_dev_get_address();
+ char *data = (char*)malloc(BT_DEV_ADDR_LEN+1);
+ for (int i = 0; i < BT_DEV_ADDR_LEN; i++) {
+ data[i] = bda_ptr[i];
+ }
+ data[BT_DEV_ADDR_LEN] = EEPROM.read(eeprom_addr(ADDR_SIGNATURE));
+ unsigned char *hash = MD5::make_hash(data, BT_DEV_ADDR_LEN);
+ memcpy(bt_dh, hash, BT_DEV_HASH_LEN);
+ sprintf(bt_devname, "RNode %02X%02X", bt_dh[14], bt_dh[15]);
+ free(data);
+
+ SerialBT.enableSSP();
+ SerialBT.onConfirmRequest(bt_confirm_pairing);
+ SerialBT.onAuthComplete(bt_pairing_complete);
+ SerialBT.register_callback(bt_connection_callback);
+
+ bt_ready = true;
+ return true;
+
+ } else { return false; }
+ } else { return false; }
+ } else { return false; }
+ } else { return false; }
+ }
+
+ bool bt_init() {
+ bt_state = BT_STATE_OFF;
+ if (bt_setup_hw()) {
+ if (bt_enabled && !console_active) bt_start();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void update_bt() {
+ if (bt_allow_pairing && millis()-bt_pairing_started >= BT_PAIRING_TIMEOUT) {
+ bt_disable_pairing();
+ }
+ }
+
+ #elif HAS_BLE == true
+ bool bt_setup_hw(); void bt_security_setup();
+ BLESecurity *ble_security = new BLESecurity();
+ bool ble_authenticated = false;
+ uint32_t pairing_pin = 0;
+
+ void bt_flush() { if (bt_state == BT_STATE_CONNECTED) { SerialBT.flush(); } }
+
+ void bt_start() {
+ // Serial.println("BT start");
+ display_unblank();
+ if (bt_state == BT_STATE_OFF) {
+ bt_state = BT_STATE_ON;
+ SerialBT.begin(bt_devname);
+ SerialBT.setTimeout(10);
+ }
+ }
+
+ void bt_stop() {
+ // Serial.println("BT stop");
+ display_unblank();
+ if (bt_state != BT_STATE_OFF) {
+ bt_allow_pairing = false;
+ bt_state = BT_STATE_OFF;
+ SerialBT.end();
+ }
+ }
+
+ bool bt_init() {
+ // Serial.println("BT init");
+ bt_state = BT_STATE_OFF;
+ if (bt_setup_hw()) {
+ if (bt_enabled && !console_active) bt_start();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void bt_debond_all() {
+ // Serial.println("Debonding all");
+ int dev_num = esp_ble_get_bond_device_num();
+ esp_ble_bond_dev_t *dev_list = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * dev_num);
+ esp_ble_get_bond_device_list(&dev_num, dev_list);
+ for (int i = 0; i < dev_num; i++) { esp_ble_remove_bond_device(dev_list[i].bd_addr); }
+ free(dev_list);
+ }
+
+ void bt_enable_pairing() {
+ // Serial.println("BT enable pairing");
+ display_unblank();
+ if (bt_state == BT_STATE_OFF) bt_start();
+
+ bt_security_setup();
+
+ bt_allow_pairing = true;
+ bt_pairing_started = millis();
+ bt_state = BT_STATE_PAIRING;
+ bt_ssp_pin = pairing_pin;
+ }
+
+ void bt_disable_pairing() {
+ // Serial.println("BT disable pairing");
+ display_unblank();
+ bt_allow_pairing = false;
+ bt_ssp_pin = 0;
+ bt_state = BT_STATE_ON;
+ }
+
+ void bt_passkey_notify_callback(uint32_t passkey) {
+ // Serial.printf("Got passkey notification: %d\n", passkey);
+ if (bt_allow_pairing) {
+ bt_ssp_pin = passkey;
+ bt_pairing_started = millis();
+ kiss_indicate_btpin();
+ } else {
+ // Serial.println("Pairing not allowed, re-init");
+ SerialBT.disconnect();
+ }
+ }
+
+ bool bt_confirm_pin_callback(uint32_t pin) {
+ // Serial.printf("Confirm PIN callback: %d\n", pin);
+ return true;
+ }
+
+ void bt_update_passkey() {
+ // Serial.println("Updating passkey");
+ pairing_pin = random(899999)+100000;
+ bt_ssp_pin = pairing_pin;
+ }
+
+ uint32_t bt_passkey_callback() {
+ // Serial.println("API passkey request");
+ if (pairing_pin == 0) { bt_update_passkey(); }
+ return pairing_pin;
+ }
+
+ bool bt_client_authenticated() {
+ return ble_authenticated;
+ }
+
+ bool bt_security_request_callback() {
+ if (bt_allow_pairing) {
+ // Serial.println("Accepting security request");
+ return true;
+ } else {
+ // Serial.println("Rejecting security request");
+ return false;
+ }
+ }
+
+ void bt_authentication_complete_callback(esp_ble_auth_cmpl_t auth_result) {
+ if (auth_result.success == true) {
+ // Serial.println("Authentication success");
+ ble_authenticated = true;
+ if (bt_state == BT_STATE_PAIRING) {
+ // Serial.println("Pairing complete, disconnecting");
+ delay(2000); SerialBT.disconnect();
+ } else { bt_state = BT_STATE_CONNECTED; }
+ } else {
+ // Serial.println("Authentication fail");
+ ble_authenticated = false;
+ bt_state = BT_STATE_ON;
+ bt_update_passkey();
+ bt_security_setup();
+ }
+ bt_allow_pairing = false;
+ bt_ssp_pin = 0;
+ }
+
+ void bt_connect_callback(BLEServer *server) {
+ uint16_t conn_id = server->getConnId();
+ // Serial.printf("Connected: %d\n", conn_id);
+ display_unblank();
+ ble_authenticated = false;
+ if (bt_state != BT_STATE_PAIRING) { bt_state = BT_STATE_CONNECTED; }
+ cable_state = CABLE_STATE_DISCONNECTED;
+ }
+
+ void bt_disconnect_callback(BLEServer *server) {
+ uint16_t conn_id = server->getConnId();
+ // Serial.printf("Disconnected: %d\n", conn_id);
+ display_unblank();
+ ble_authenticated = false;
+ bt_state = BT_STATE_ON;
+ }
+
+ bool bt_setup_hw() {
+ // Serial.println("BT setup hw");
+ if (!bt_ready) {
+ if (EEPROM.read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) {
+ bt_enabled = true;
+ } else {
+ bt_enabled = false;
+ }
+ if (btStart()) {
+ if (esp_bluedroid_init() == ESP_OK) {
+ if (esp_bluedroid_enable() == ESP_OK) {
+ const uint8_t* bda_ptr = esp_bt_dev_get_address();
+ char *data = (char*)malloc(BT_DEV_ADDR_LEN+1);
+ for (int i = 0; i < BT_DEV_ADDR_LEN; i++) {
+ data[i] = bda_ptr[i];
+ }
+ data[BT_DEV_ADDR_LEN] = EEPROM.read(eeprom_addr(ADDR_SIGNATURE));
+ unsigned char *hash = MD5::make_hash(data, BT_DEV_ADDR_LEN);
+ memcpy(bt_dh, hash, BT_DEV_HASH_LEN);
+ sprintf(bt_devname, "RNode %02X%02X", bt_dh[14], bt_dh[15]);
+ free(data);
+
+ bt_security_setup();
+
+ bt_ready = true;
+ return true;
+
+ } else { return false; }
+ } else { return false; }
+ } else { return false; }
+ } else { return false; }
+ }
+
+ void bt_security_setup() {
+ // Serial.println("Executing BT security setup");
+ if (pairing_pin == 0) { bt_update_passkey(); }
+ uint32_t passkey = pairing_pin;
+ // Serial.printf("Passkey is %d\n", passkey);
+
+ uint8_t key_size = 16;
+ uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
+ uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
+
+ esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND;
+ uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_ENABLE;
+ uint8_t oob_support = ESP_BLE_OOB_DISABLE;
+
+ esp_ble_io_cap_t iocap = ESP_IO_CAP_OUT;
+
+ esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_ONLY_ACCEPT_SPECIFIED_SEC_AUTH, &auth_option, sizeof(uint8_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_OOB_SUPPORT, &oob_support, sizeof(uint8_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t));
+ esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t));
+ }
+
+ void update_bt() {
+ if (bt_allow_pairing && millis()-bt_pairing_started >= BT_PAIRING_TIMEOUT) {
+ bt_disable_pairing();
+ }
+ if (bt_state == BT_STATE_CONNECTED && millis()-SerialBT.lastFlushTime >= BLE_FLUSH_TIMEOUT) {
+ if (SerialBT.transmitBufferLength > 0) {
+ bt_flush();
+ }
+ }
+ }
+ #endif
+
+#elif MCU_VARIANT == MCU_NRF52
+ uint32_t pairing_pin = 0;
+
+ uint8_t eeprom_read(uint32_t mapped_addr);
+
+ void bt_stop() {
+ // Serial.println("BT Stop");
+ if (bt_state != BT_STATE_OFF) {
+ bt_allow_pairing = false;
+ bt_state = BT_STATE_OFF;
+ }
+ }
+
+ 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_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) {
+ 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
+ // bypassing pin entry through pairing using the "just works" mode.
+ // Therefore, we must check the security level of the connection after
+ // pairing to ensure "just works" has not been used. If it has, we need
+ // to disconnect, unpair and delete any bonding information immediately.
+ // Settings on the SerialBT service should prevent unauthorised access to
+ // the serial port anyway, but this is still wise to do regardless.
+ //
+ // Note: It may be nice to have this done in the BLESecurity class in the
+ // future, but as it stands right now I'd have to fork the BSP to do
+ // that, which I don't fancy doing. Impact on security is likely minimal.
+ // Requires investigation.
+
+ if (security.sm == 1 && security.lv >= 3) {
+ // Serial.println("Auth level success");
+ bt_state = BT_STATE_CONNECTED;
+ cable_state = CABLE_STATE_DISCONNECTED;
+ connection->disconnect();
+ bt_disable_pairing();
+ } else {
+ // Serial.println("Auth level failure, debonding");
+ if (connection->bonded()) { connection->removeBondKey(); }
+ connection->disconnect();
+ bt_disable_pairing();
+ }
+ } else {
+ // Serial.println("Bonding failure");
+ connection->disconnect();
+ bt_disable_pairing();
+ }
+ }
+
+ bool bt_passkey_callback(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) {
+ // Serial.println("Passkey callback");
+ if (bt_allow_pairing) {
+ return true;
+ }
+ return false;
+ }
+
+ void bt_connect_callback(uint16_t conn_handle) {
+ // Serial.println("Connect callback");
+ bt_state = BT_STATE_CONNECTED;
+ cable_state = CABLE_STATE_DISCONNECTED;
+
+ BLEConnection* conn = Bluefruit.Connection(conn_handle);
+ conn->requestPHY(BLE_GAP_PHY_2MBPS);
+ conn->requestMtuExchange(512+3);
+ conn->requestDataLengthUpdate();
+ }
+
+ void bt_disconnect_callback(uint16_t conn_handle, uint8_t reason) {
+ // Serial.println("Disconnect callback");
+ if (reason != BLE_GAP_SEC_STATUS_SUCCESS) {
+ bt_state = BT_STATE_ON;
+ }
+ }
+
+ void bt_update_passkey() {
+ // Serial.println("Update passkey");
+ pairing_pin = random(899999)+100000;
+ bt_ssp_pin = pairing_pin;
+ }
+
+ uint32_t bt_get_passkey() {
+ // Serial.println("API passkey request");
+ if (pairing_pin == 0) { bt_update_passkey(); }
+ return pairing_pin;
+ }
+
+ bool bt_setup_hw() {
+ // Serial.println("Setup HW");
+ if (!bt_ready) {
+ #if HAS_EEPROM
+ if (EEPROM.read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) {
+ #else
+ if (eeprom_read(eeprom_addr(ADDR_CONF_BT)) == BT_ENABLE_BYTE) {
+ #endif
+ bt_enabled = true;
+ } else {
+ bt_enabled = false;
+ }
+ Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
+ Bluefruit.autoConnLed(false);
+ if (Bluefruit.begin()) {
+ uint32_t pin = bt_get_passkey();
+ char pin_char[6];
+ sprintf(pin_char,"%lu", pin);
+
+ Bluefruit.setTxPower(8); // Check bluefruit.h for supported values
+ Bluefruit.Security.setIOCaps(true, false, false); // display, yes; yes / no, no; keyboard, no
+ // This device is indeed capable of yes / no through the pairing mode
+ // being set, but I have chosen to set it thus to force the input of the
+ // pin on the device initiating the pairing.
+
+ Bluefruit.Security.setMITM(true);
+ Bluefruit.Security.setPairPasskeyCallback(bt_passkey_callback);
+ Bluefruit.Security.setSecuredCallback(bt_connect_callback);
+ Bluefruit.Security.setPIN(pin_char);
+ Bluefruit.Periph.setDisconnectCallback(bt_disconnect_callback);
+ Bluefruit.Security.setPairCompleteCallback(bt_pairing_complete);
+ Bluefruit.Periph.setConnInterval(6, 12); // 7.5 - 15 ms
+
+ const ble_gap_addr_t gap_addr = Bluefruit.getAddr();
+ char *data = (char*)malloc(BT_DEV_ADDR_LEN+1);
+ for (int i = 0; i < BT_DEV_ADDR_LEN; i++) {
+ data[i] = gap_addr.addr[i];
+ }
+ #if HAS_EEPROM
+ data[BT_DEV_ADDR_LEN] = EEPROM.read(eeprom_addr(ADDR_SIGNATURE));
+ #else
+ data[BT_DEV_ADDR_LEN] = eeprom_read(eeprom_addr(ADDR_SIGNATURE));
+ #endif
+ unsigned char *hash = MD5::make_hash(data, BT_DEV_ADDR_LEN);
+ memcpy(bt_dh, hash, BT_DEV_HASH_LEN);
+ sprintf(bt_devname, "RNode %02X%02X", bt_dh[14], bt_dh[15]);
+ free(data);
+
+ bt_ready = true;
+ return true;
+
+ } else { return false; }
+ } else { return false; }
+ }
+
+ void bt_start() {
+ // Serial.println("BT Start");
+ if (bt_state == BT_STATE_OFF) {
+ Bluefruit.setName(bt_devname);
+ bledis.setManufacturer(BLE_MANUFACTURER);
+ bledis.setModel(BLE_MODEL);
+ // start device information service
+ bledis.begin();
+ blebas.begin();
+
+ // Guard to ensure SerialBT service is not duplicated through BT being power cycled
+ if (!SerialBT_init) {
+ SerialBT.bufferTXD(true); // enable buffering
+
+ SerialBT.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); // enable encryption for BLE serial
+ SerialBT.begin();
+ SerialBT_init = true;
+ }
+
+ Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
+ Bluefruit.Advertising.addTxPower();
+
+ // Include bleuart 128-bit uuid
+ Bluefruit.Advertising.addService(SerialBT);
+
+ // There is no room for Name in Advertising packet
+ // Use Scan response for Name
+ Bluefruit.ScanResponse.addName();
+
+ Bluefruit.Advertising.start(0);
+
+ bt_state = BT_STATE_ON;
+ }
+ }
+
+ bool bt_init() {
+ // Serial.println("BT init");
+ bt_state = BT_STATE_OFF;
+ if (bt_setup_hw()) {
+ if (bt_enabled && !console_active) bt_start();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void bt_enable_pairing() {
+ // Serial.println("BT enable pairing");
+ if (bt_state == BT_STATE_OFF) bt_start();
+
+ uint32_t pin = bt_get_passkey();
+ char pin_char[6];
+ sprintf(pin_char,"%lu", pin);
+ Bluefruit.Security.setPIN(pin_char);
+
+ bt_allow_pairing = true;
+ bt_pairing_started = millis();
+ bt_state = BT_STATE_PAIRING;
+ kiss_indicate_btpin();
+ }
+
+ void bt_debond_all() { }
+
+ void update_bt() {
+ if (bt_allow_pairing && millis()-bt_pairing_started >= BT_PAIRING_TIMEOUT) {
+ bt_disable_pairing();
+ }
+ }
+#endif
diff --git a/RNode_Firmware/Boards.h b/RNode_Firmware/Boards.h
new file mode 100644
index 0000000..49791d9
--- /dev/null
+++ b/RNode_Firmware/Boards.h
@@ -0,0 +1,983 @@
+// 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 .
+
+#include "Modem.h"
+
+#ifndef BOARDS_H
+ #define BOARDS_H
+
+ #define PLATFORM_AVR 0x90
+ #define PLATFORM_ESP32 0x80
+ #define PLATFORM_NRF52 0x70
+
+ #define MCU_1284P 0x91
+ #define MCU_2560 0x92
+ #define MCU_ESP32 0x81
+ #define MCU_NRF52 0x71
+
+ // Products, boards and models ////
+ #define PRODUCT_RNODE 0x03 // RNode devices
+ #define BOARD_RNODE 0x31 // Original v1.0 RNode
+ #define MODEL_A4 0xA4 // RNode v1.0, 433 MHz
+ #define MODEL_A9 0xA9 // RNode v1.0, 868 MHz
+
+ #define BOARD_RNODE_NG_20 0x40 // RNode hardware revision v2.0
+ #define MODEL_A3 0xA3 // RNode v2.0, 433 MHz
+ #define MODEL_A8 0xA8 // RNode v2.0, 868 MHz
+
+ #define BOARD_RNODE_NG_21 0x41 // RNode hardware revision v2.1
+ #define MODEL_A2 0xA2 // RNode v2.1, 433 MHz
+ #define MODEL_A7 0xA7 // RNode v2.1, 868 MHz
+
+ #define BOARD_T3S3 0x42 // T3S3 devices
+ #define MODEL_A1 0xA1 // T3S3, 433 MHz with SX1268
+ #define MODEL_A5 0xA5 // T3S3, 433 MHz with SX1278
+ #define MODEL_A6 0xA6 // T3S3, 868 MHz with SX1262
+ #define MODEL_AA 0xAA // T3S3, 868 MHz with SX1276
+ #define MODEL_AC 0xAC // T3S3, 2.4 GHz with SX1280 and PA
+
+ #define PRODUCT_TBEAM 0xE0 // T-Beam devices
+ #define BOARD_TBEAM 0x33
+ #define MODEL_E4 0xE4 // T-Beam SX1278, 433 Mhz
+ #define MODEL_E9 0xE9 // T-Beam SX1276, 868 Mhz
+ #define MODEL_E3 0xE3 // T-Beam SX1268, 433 Mhz
+ #define MODEL_E8 0xE8 // T-Beam SX1262, 868 Mhz
+
+ #define PRODUCT_TDECK_V1 0xD0
+ #define BOARD_TDECK 0x3B
+ #define MODEL_D4 0xD4 // LilyGO T-Deck, 433 MHz
+ #define MODEL_D9 0xD9 // LilyGO T-Deck, 868 MHz
+
+ #define PRODUCT_TBEAM_S_V1 0xEA
+ #define BOARD_TBEAM_S_V1 0x3D
+ #define MODEL_DB 0xDB // LilyGO T-Beam Supreme, 433 MHz
+ #define MODEL_DC 0xDC // LilyGO T-Beam Supreme, 868 MHz
+
+ #define PRODUCT_XIAO_S3 0xEB
+ #define BOARD_XIAO_S3 0x3E
+ #define MODEL_DE 0xDE // Xiao ESP32S3 with Wio-SX1262 module, 433 MHz
+ #define MODEL_DD 0xDD // Xiao ESP32S3 with Wio-SX1262 module, 868 MHz
+
+ #define PRODUCT_T32_10 0xB2
+ #define BOARD_LORA32_V1_0 0x39
+ #define MODEL_BA 0xBA // LilyGO T3 v1.0, 433 MHz
+ #define MODEL_BB 0xBB // LilyGO T3 v1.0, 868 MHz
+
+ #define PRODUCT_T32_20 0xB0
+ #define BOARD_LORA32_V2_0 0x36
+ #define MODEL_B3 0xB3 // LilyGO T3 v2.0, 433 MHz
+ #define MODEL_B8 0xB8 // LilyGO T3 v2.0, 868 MHz
+
+ #define PRODUCT_T32_21 0xB1
+ #define BOARD_LORA32_V2_1 0x37
+ #define MODEL_B4 0xB4 // LilyGO T3 v2.1, 433 MHz
+ #define MODEL_B9 0xB9 // LilyGO T3 v2.1, 868 MHz
+
+ #define PRODUCT_H32_V2 0xC0 // Board code 0x38
+ #define BOARD_HELTEC32_V2 0x38
+ #define MODEL_C4 0xC4 // Heltec Lora32 v2, 433 MHz
+ #define MODEL_C9 0xC9 // Heltec Lora32 v2, 868 MHz
+
+ #define PRODUCT_H32_V3 0xC1
+ #define BOARD_HELTEC32_V3 0x3A
+ #define MODEL_C5 0xC5 // Heltec Lora32 v3, 433 MHz
+ #define MODEL_CA 0xCA // Heltec Lora32 v3, 868 MHz
+
+ #define PRODUCT_H32_V4 0xC3
+ #define BOARD_HELTEC32_V4 0x3F
+ #define MODEL_C8 0xC8 // Heltec Lora32 v3, 850-950 MHz, 28dBm
+
+ #define PRODUCT_HELTEC_T114 0xC2 // Heltec Mesh Node T114
+ #define BOARD_HELTEC_T114 0x3C
+ #define MODEL_C6 0xC6 // Heltec Mesh Node T114, 470-510 MHz
+ #define MODEL_C7 0xC7 // Heltec Mesh Node T114, 863-928 MHz
+
+ #define PRODUCT_VME213 0xC9 // Heltec VisionMaster E213
+ #define BOARD_VME213 0x40
+ #define MODEL_CD 0xCD // VisionMaster E213, 470-510 MHz
+ #define MODEL_CE 0xCE // VisionMaster E213, 863-928 MHz
+
+ #define PRODUCT_TECHO 0x15 // LilyGO T-Echo devices
+ #define BOARD_TECHO 0x44
+ #define MODEL_16 0x16 // T-Echo 433 MHz
+ #define MODEL_17 0x17 // T-Echo 868/915 MHz
+
+ #define PRODUCT_RAK4631 0x10
+ #define BOARD_RAK4631 0x51
+ #define MODEL_11 0x11 // RAK4631, 433 Mhz
+ #define MODEL_12 0x12 // RAK4631, 868 Mhz
+
+ #define PRODUCT_HMBRW 0xF0
+ #define BOARD_HMBRW 0x32
+ #define BOARD_HUZZAH32 0x34
+ #define BOARD_GENERIC_ESP32 0x35
+ #define BOARD_GENERIC_NRF52 0x50
+ #define MODEL_FE 0xFE // Homebrew board, max 17dBm output power
+ #define MODEL_FF 0xFF // Homebrew board, max 14dBm output power
+
+ #if defined(__AVR_ATmega1284P__)
+ #define PLATFORM PLATFORM_AVR
+ #define MCU_VARIANT MCU_1284P
+ #elif defined(__AVR_ATmega2560__)
+ #define PLATFORM PLATFORM_AVR
+ #define MCU_VARIANT MCU_2560
+ #elif defined(ESP32)
+ #define PLATFORM PLATFORM_ESP32
+ #define MCU_VARIANT MCU_ESP32
+ #elif defined(NRF52840_XXAA)
+ #include
+ #define PLATFORM PLATFORM_NRF52
+ #define MCU_VARIANT MCU_NRF52
+ #else
+ #error "The firmware cannot be compiled for the selected MCU variant"
+ #endif
+
+ #ifndef MODEM
+ #if BOARD_MODEL == BOARD_RAK4631
+ #define MODEM SX1262
+ #elif BOARD_MODEL == BOARD_GENERIC_NRF52
+ #define MODEM SX1262
+ #else
+ #define MODEM SX1276
+ #endif
+ #endif
+
+ #define HAS_DISPLAY false
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE false
+ #define HAS_TCXO false
+ #define HAS_PMU false
+ #define HAS_NP false
+ #define HAS_EEPROM false
+ #define HAS_INPUT false
+ #define HAS_SLEEP false
+ #define HAS_LORA_PA false
+ #define HAS_LORA_LNA false
+ #define PIN_DISP_SLEEP -1
+ #define VALIDATE_FIRMWARE true
+
+ #if defined(ENABLE_TCXO)
+ #define HAS_TCXO true
+ #endif
+
+ #if MCU_VARIANT == MCU_1284P
+ const int pin_cs = 4;
+ const int pin_reset = 3;
+ const int pin_dio = 2;
+ const int pin_led_rx = 12;
+ const int pin_led_tx = 13;
+
+ #define BOARD_MODEL BOARD_RNODE
+ #define HAS_EEPROM true
+ #define CONFIG_UART_BUFFER_SIZE 6144
+ #define CONFIG_QUEUE_SIZE 6144
+ #define CONFIG_QUEUE_MAX_LENGTH 200
+ #define EEPROM_SIZE 4096
+ #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
+
+ #elif MCU_VARIANT == MCU_2560
+ const int pin_cs = 5;
+ const int pin_reset = 4;
+ const int pin_dio = 2;
+ const int pin_led_rx = 12;
+ const int pin_led_tx = 13;
+
+ #define BOARD_MODEL BOARD_HMBRW
+ #define HAS_EEPROM true
+ #define CONFIG_UART_BUFFER_SIZE 768
+ #define CONFIG_QUEUE_SIZE 5120
+ #define CONFIG_QUEUE_MAX_LENGTH 24
+ #define EEPROM_SIZE 4096
+ #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
+
+ #elif MCU_VARIANT == MCU_ESP32
+
+ // Board models for ESP32 based builds are
+ // defined by the build target in the makefile.
+ // If you are not using make to compile this
+ // firmware, you can manually define model here.
+ //
+ // #define BOARD_MODEL BOARD_GENERIC_ESP32
+ #define CONFIG_UART_BUFFER_SIZE 6144
+ #define CONFIG_QUEUE_SIZE 6144
+ #define CONFIG_QUEUE_MAX_LENGTH 200
+
+ #define EEPROM_SIZE 1024
+ #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
+
+ #define GPS_BAUD_RATE 9600
+ #define PIN_GPS_TX 12
+ #define PIN_GPS_RX 34
+
+ #if BOARD_MODEL == BOARD_GENERIC_ESP32
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ const int pin_cs = 4;
+ const int pin_reset = 33;
+ const int pin_dio = 39;
+ const int pin_led_rx = 14;
+ const int pin_led_tx = 32;
+
+ #elif BOARD_MODEL == BOARD_TBEAM
+ #define HAS_DISPLAY true
+ #define HAS_PMU true
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_SD false
+ #define HAS_EEPROM true
+ #define I2C_SDA 21
+ #define I2C_SCL 22
+ #define PMU_IRQ 35
+
+ #define HAS_INPUT true
+ const int pin_btn_usr1 = 38;
+
+ const int pin_cs = 18;
+ const int pin_reset = 23;
+ const int pin_led_rx = 2;
+ const int pin_led_tx = 4;
+
+ #if MODEM == SX1262
+ #define HAS_TCXO true
+ #define HAS_BUSY true
+ #define DIO2_AS_RF_SWITCH true
+ #define OCP_TUNED 0x18
+ const int pin_busy = 32;
+ const int pin_dio = 33;
+ const int pin_tcxo_enable = -1;
+ #else
+ const int pin_dio = 26;
+ #endif
+
+ #elif BOARD_MODEL == BOARD_HUZZAH32
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ const int pin_cs = 4;
+ const int pin_reset = 33;
+ const int pin_dio = 39;
+ const int pin_led_rx = 14;
+ const int pin_led_tx = 32;
+
+ #elif BOARD_MODEL == BOARD_LORA32_V1_0
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ const int pin_cs = 18;
+ const int pin_reset = 14;
+ const int pin_dio = 26;
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 25;
+ const int pin_led_tx = 2;
+ #else
+ const int pin_led_rx = 2;
+ const int pin_led_tx = 2;
+ #endif
+
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ const int pin_cs = 18;
+ const int pin_reset = 12;
+ const int pin_dio = 26;
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 2;
+ const int pin_led_tx = 0;
+ #else
+ const int pin_led_rx = 22;
+ const int pin_led_tx = 22;
+ #endif
+
+ #elif BOARD_MODEL == BOARD_LORA32_V2_1
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH true
+ #define HAS_PMU true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ const int pin_cs = 18;
+ const int pin_reset = 23;
+ const int pin_dio = 26;
+ #if HAS_TCXO == true
+ const int pin_tcxo_enable = 33;
+ #endif
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 15;
+ const int pin_led_tx = 4;
+ #else
+ const int pin_led_rx = 25;
+ const int pin_led_tx = 25;
+ #endif
+
+ #elif BOARD_MODEL == BOARD_HELTEC32_V2
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+
+ const int pin_btn_usr1 = 0;
+
+ const int pin_cs = 18;
+ const int pin_reset = 14;
+ const int pin_dio = 26;
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 36;
+ const int pin_led_tx = 37;
+ #else
+ const int pin_led_rx = 25;
+ const int pin_led_tx = 25;
+ #endif
+
+ #elif BOARD_MODEL == BOARD_HELTEC32_V3
+ #define IS_ESP32S3 true
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_PMU true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+ #define OCP_TUNED 0x18
+
+ const int pin_btn_usr1 = 0;
+
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 13;
+ const int pin_led_tx = 14;
+ #else
+ const int pin_led_rx = 35;
+ const int pin_led_tx = 35;
+ #endif
+
+ #define MODEM SX1262
+ #define HAS_TCXO true
+ const int pin_tcxo_enable = -1;
+ #define HAS_BUSY true
+ #define DIO2_AS_RF_SWITCH true
+
+ // Following pins are for the SX1262
+ const int pin_cs = 8;
+ const int pin_busy = 13;
+ const int pin_dio = 14;
+ const int pin_reset = 12;
+ const int pin_mosi = 10;
+ const int pin_miso = 11;
+ const int pin_sclk = 9;
+
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ #define IS_ESP32S3 true
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_PMU true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define HAS_LORA_PA true
+ #define HAS_LORA_LNA true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+ #define OCP_TUNED 0x18
+ #define Vext GPIO_NUM_36
+
+ const int pin_btn_usr1 = 0;
+
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 13;
+ const int pin_led_tx = 14;
+ #else
+ const int pin_led_rx = 35;
+ const int pin_led_tx = 35;
+ #endif
+
+ #define MODEM SX1262
+ #define HAS_TCXO true
+ const int pin_tcxo_enable = -1;
+ #define HAS_BUSY true
+ #define DIO2_AS_RF_SWITCH true
+
+ #define LORA_LNA_GAIN 17
+ #define LORA_PA_GC1109 true
+ #define LORA_PA_PWR_EN 7
+ #define LORA_PA_CSD 2
+ #define LORA_PA_CPS 46
+
+ #define PA_MAX_OUTPUT 28
+ #define PA_GAIN_POINTS 22
+ #define PA_GAIN_VALUES 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
+
+ const int pin_cs = 8;
+ const int pin_busy = 13;
+ const int pin_dio = 14;
+ const int pin_reset = 12;
+ const int pin_mosi = 10;
+ const int pin_miso = 11;
+ const int pin_sclk = 9;
+
+ #elif BOARD_MODEL == BOARD_VME213
+ // Heltec VisionMaster E213 with ESP32-S3R8 + SX1262 + 2.13" E-Ink
+ #define IS_ESP32S3 true
+ #define HAS_DISPLAY true // 2.13" E-Ink LCMEN2R13EFC1, 250x122 pixels
+ #define HAS_EINK true // Enable E-Ink specific display code
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_PMU false
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+
+ const int pin_btn_usr1 = 0;
+
+ // No physical LEDs on VME213, display only
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 35;
+ const int pin_led_tx = 35;
+ #else
+ const int pin_led_rx = -1;
+ const int pin_led_tx = -1;
+ #endif
+
+ // SX1262 LoRa radio configuration
+ #define MODEM SX1262
+ #define HAS_TCXO true
+ const int pin_tcxo_enable = -1;
+ #define HAS_BUSY true
+ #define DIO2_AS_RF_SWITCH true
+
+ // SX1262 pins (from Heltec board-config.h)
+ const int pin_cs = 8;
+ const int pin_busy = 13;
+ const int pin_dio = 14;
+ const int pin_reset = 12;
+ const int pin_mosi = 10;
+ const int pin_miso = 11;
+ const int pin_sclk = 9;
+
+ // E-Ink display pins (HT_ICMEN2R13EFC1 controller)
+ const int pin_disp_reset = 3;
+ const int pin_disp_dc = 2;
+ const int pin_disp_cs = 5;
+ const int pin_disp_busy = 1;
+ const int pin_disp_sck = 4;
+ const int pin_disp_mosi = 6;
+ const int pin_disp_miso = -1;
+
+ // Vext power control (HIGH=ON, LOW=OFF)
+ #define Vext GPIO_NUM_18
+
+ // I2C for sensors (QuickLink)
+ #define I2C_SDA 39
+ #define I2C_SCL 38
+
+ #elif BOARD_MODEL == BOARD_RNODE_NG_20
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH true
+ #define HAS_NP true
+ #define HAS_CONSOLE true
+ #define HAS_EEPROM true
+ const int pin_cs = 18;
+ const int pin_reset = 12;
+ const int pin_dio = 26;
+ const int pin_np = 4;
+ #if HAS_NP == false
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 2;
+ const int pin_led_tx = 0;
+ #else
+ const int pin_led_rx = 22;
+ const int pin_led_tx = 22;
+ #endif
+ #endif
+
+ #elif BOARD_MODEL == BOARD_RNODE_NG_21
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH true
+ #define HAS_CONSOLE true
+ #define HAS_PMU true
+ #define HAS_NP true
+ #define HAS_SD false
+ #define HAS_EEPROM true
+ const int pin_cs = 18;
+ const int pin_reset = 23;
+ const int pin_dio = 26;
+ const int pin_np = 12;
+ const int pin_dac = 25;
+ const int pin_adc = 34;
+ const int SD_MISO = 2;
+ const int SD_MOSI = 15;
+ const int SD_CLK = 14;
+ const int SD_CS = 13;
+ #if HAS_NP == false
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 12;
+ const int pin_led_tx = 4;
+ #else
+ const int pin_led_rx = 25;
+ const int pin_led_tx = 25;
+ #endif
+ #endif
+
+ #elif BOARD_MODEL == BOARD_T3S3
+ #define IS_ESP32S3 true
+ #define HAS_DISPLAY true
+ #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 HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+ const int pin_btn_usr1 = 0;
+
+ const int pin_cs = 7;
+ const int pin_reset = 8;
+ const int pin_sclk = 5;
+ const int pin_mosi = 6;
+ const int pin_miso = 3;
+
+ #if MODEM == SX1262
+ #define DIO2_AS_RF_SWITCH true
+ #define HAS_BUSY true
+ #define HAS_TCXO true
+ const int pin_busy = 34;
+ const int pin_dio = 33;
+ const int pin_tcxo_enable = -1;
+ #elif MODEM == SX1280
+ #define CONFIG_QUEUE_SIZE 6144
+ #define DIO2_AS_RF_SWITCH false
+ #define HAS_BUSY true
+ #define HAS_TCXO true
+ #define HAS_PA true
+ const int pa_max_input = 3;
+
+ #define HAS_RF_SWITCH_RX_TX true
+ const int pin_rxen = 21;
+ const int pin_txen = 10;
+
+ const int pin_busy = 36;
+ const int pin_dio = 9;
+ const int pin_tcxo_enable = -1;
+ #else
+ const int pin_dio = 9;
+ #endif
+
+ const int pin_np = 38;
+ const int pin_dac = 25;
+ const int pin_adc = 1;
+
+ const int SD_MISO = 2;
+ const int SD_MOSI = 11;
+ const int SD_CLK = 14;
+ const int SD_CS = 13;
+
+ #if HAS_NP == false
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 37;
+ const int pin_led_tx = 37;
+ #else
+ const int pin_led_rx = 37;
+ const int pin_led_tx = 37;
+ #endif
+ #endif
+
+ #elif BOARD_MODEL == BOARD_TDECK
+ #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 false
+ #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 HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_0
+ #define WAKEUP_LEVEL 0
+
+ const int pin_poweron = 10;
+ const int pin_btn_usr1 = 0;
+
+ const int pin_cs = 9;
+ const int pin_reset = 17;
+ const int pin_sclk = 40;
+ const int pin_mosi = 41;
+ const int pin_miso = 38;
+ const int pin_tcxo_enable = -1;
+ const int pin_dio = 45;
+ const int pin_busy = 13;
+
+ const int SD_MISO = 38;
+ const int SD_MOSI = 41;
+ const int SD_CLK = 40;
+ const int SD_CS = 39;
+
+ const int DISPLAY_DC = 11;
+ const int DISPLAY_CS = 12;
+ const int DISPLAY_MISO = 38;
+ const int DISPLAY_MOSI = 41;
+ const int DISPLAY_CLK = 40;
+ const int DISPLAY_BL_PIN = 42;
+
+ #if HAS_NP == false
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 43;
+ const int pin_led_tx = 43;
+ #else
+ const int pin_led_rx = 43;
+ const int pin_led_tx = 43;
+ #endif
+ #endif
+
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ #define IS_ESP32S3 true
+ #define MODEM SX1262
+ #define DIO2_AS_RF_SWITCH true
+ #define HAS_BUSY true
+ #define HAS_TCXO true
+ #define OCP_TUNED 0x18
+
+ #define HAS_DISPLAY true
+ #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 HAS_INPUT true
+ #define HAS_SLEEP false
+
+ #define PMU_IRQ 40
+ #define I2C_SCL 41
+ #define I2C_SDA 42
+
+ const int pin_btn_usr1 = 0;
+
+ const int pin_cs = 10;
+ const int pin_reset = 5;
+ const int pin_sclk = 12;
+ const int pin_mosi = 11;
+ const int pin_miso = 13;
+ const int pin_tcxo_enable = -1;
+ const int pin_dio = 1;
+ const int pin_busy = 4;
+
+ 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;
+
+ #if HAS_NP == false
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 43;
+ const int pin_led_tx = 43;
+ #else
+ const int pin_led_rx = 43;
+ const int pin_led_tx = 43;
+ #endif
+ #endif
+
+ #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_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_NP false
+ #define HAS_SD false
+ #define HAS_EEPROM true
+
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define PIN_WAKEUP GPIO_NUM_21
+ #define WAKEUP_LEVEL 0
+
+ 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;
+
+ #if HAS_NP == false
+ #if defined(EXTERNAL_LEDS)
+ const int pin_led_rx = 48;
+ const int pin_led_tx = 48;
+ #else
+ const int pin_led_rx = 48;
+ const int pin_led_tx = 48;
+ #endif
+ #endif
+
+ #else
+ #error An unsupported ESP32 board was selected. Cannot compile RNode firmware.
+ #endif
+
+ #elif MCU_VARIANT == MCU_NRF52
+ #if BOARD_MODEL == BOARD_RAK4631
+ #define HAS_EEPROM false
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_CONSOLE false
+ #define HAS_PMU false
+ #define HAS_NP false
+ #define HAS_SD false
+ #define HAS_TCXO true
+ #define HAS_RF_SWITCH_RX_TX true
+ #define HAS_BUSY true
+ #define HAS_INPUT true
+ #define DIO2_AS_RF_SWITCH true
+ #define CONFIG_UART_BUFFER_SIZE 6144
+ #define CONFIG_QUEUE_SIZE 6144
+ #define CONFIG_QUEUE_MAX_LENGTH 200
+ #define EEPROM_SIZE 296
+ #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
+ #define BLE_MANUFACTURER "RAK Wireless"
+ #define BLE_MODEL "RAK4640"
+
+ const int pin_btn_usr1 = 9;
+
+ // Following pins are for the sx1262
+ const int pin_rxen = 37;
+ const int pin_txen = -1;
+ const int pin_reset = 38;
+ const int pin_cs = 42;
+ const int pin_sclk = 43;
+ const int pin_mosi = 44;
+ const int pin_miso = 45;
+ const int pin_busy = 46;
+ const int pin_dio = 47;
+ const int pin_led_rx = LED_BLUE;
+ const int pin_led_tx = LED_GREEN;
+ const int pin_tcxo_enable = -1;
+
+ #elif BOARD_MODEL == BOARD_TECHO
+ #define _PINNUM(port, pin) ((port) * 32 + (pin))
+ #define MODEM SX1262
+ #define HAS_EEPROM false
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_CONSOLE false
+ #define HAS_PMU true
+ #define HAS_NP false
+ #define HAS_SD false
+ #define HAS_TCXO true
+ #define HAS_BUSY true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define BLE_MANUFACTURER "LilyGO"
+ #define BLE_MODEL "T-Echo"
+
+ #define HAS_INPUT true
+ #define EEPROM_SIZE 296
+ #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
+
+ #define CONFIG_UART_BUFFER_SIZE 32768
+ #define CONFIG_QUEUE_SIZE 6144
+ #define CONFIG_QUEUE_MAX_LENGTH 200
+
+ #define HAS_DISPLAY true
+ #define HAS_BACKLIGHT true
+ #define DISPLAY_SCALE 1
+
+ #define LED_ON LOW
+ #define LED_OFF HIGH
+ #define PIN_LED_GREEN _PINNUM(1, 1)
+ #define PIN_LED_RED _PINNUM(1, 3)
+ #define PIN_LED_BLUE _PINNUM(0, 14)
+ #define PIN_VEXT_EN _PINNUM(0, 12)
+
+ const int pin_disp_cs = 30;
+ const int pin_disp_dc = 28;
+ const int pin_disp_reset = 2;
+ const int pin_disp_busy = 3;
+ const int pin_disp_en = -1;
+ const int pin_disp_sck = 31;
+ const int pin_disp_mosi = 29;
+ const int pin_disp_miso = -1;
+ const int pin_backlight = 43;
+
+ const int pin_btn_usr1 = _PINNUM(1, 10);
+ const int pin_btn_touch = _PINNUM(0, 11);
+
+ const int pin_reset = 25;
+ const int pin_cs = 24;
+ const int pin_sclk = 19;
+ const int pin_mosi = 22;
+ const int pin_miso = 23;
+ const int pin_busy = 17;
+ const int pin_dio = 20;
+ const int pin_tcxo_enable = 21;
+ const int pin_led_rx = PIN_LED_BLUE;
+ const int pin_led_tx = PIN_LED_RED;
+
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ #define MODEM SX1262
+ #define HAS_EEPROM false
+ #define HAS_DISPLAY true
+ #define HAS_BLUETOOTH false
+ #define HAS_BLE true
+ #define HAS_CONSOLE false
+ #define HAS_PMU true
+ #define HAS_NP true
+ #define HAS_SD false
+ #define HAS_TCXO true
+ #define HAS_BUSY true
+ #define HAS_INPUT true
+ #define HAS_SLEEP true
+ #define DIO2_AS_RF_SWITCH true
+ #define CONFIG_UART_BUFFER_SIZE 6144
+ #define CONFIG_QUEUE_SIZE 6144
+ #define CONFIG_QUEUE_MAX_LENGTH 200
+ #define EEPROM_SIZE 296
+ #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED
+ #define BLE_MANUFACTURER "Heltec"
+ #define BLE_MODEL "T114"
+
+ #define PIN_T114_ADC_EN 6
+ #define PIN_VEXT_EN 21
+
+ // LED
+ #define LED_T114_GREEN 3
+ #define PIN_T114_LED 14
+ #define NP_M 1
+ const int pin_np = PIN_T114_LED;
+
+ // SPI
+ #define PIN_T114_MOSI 22
+ #define PIN_T114_MISO 23
+ #define PIN_T114_SCK 19
+ #define PIN_T114_SS 24
+
+ // SX1262
+ #define PIN_T114_RST 25
+ #define PIN_T114_DIO1 20
+ #define PIN_T114_BUSY 17
+
+ // TFT
+ #define DISPLAY_SCALE 2
+ #define PIN_T114_TFT_MOSI 9
+ #define PIN_T114_TFT_MISO 11 // not connected
+ #define PIN_T114_TFT_SCK 8
+ #define PIN_T114_TFT_SS 11
+ #define PIN_T114_TFT_DC 12
+ #define PIN_T114_TFT_RST 2
+ #define PIN_T114_TFT_EN 3
+ #define PIN_T114_TFT_BLGT 15
+
+ // pins for buttons on Heltec T114
+ const int pin_btn_usr1 = 42;
+
+ // pins for sx1262 on Heltec T114
+ const int pin_reset = PIN_T114_RST;
+ const int pin_cs = PIN_T114_SS;
+ const int pin_sclk = PIN_T114_SCK;
+ const int pin_mosi = PIN_T114_MOSI;
+ const int pin_miso = PIN_T114_MISO;
+ const int pin_busy = PIN_T114_BUSY;
+ const int pin_dio = PIN_T114_DIO1;
+ const int pin_led_rx = 35;
+ const int pin_led_tx = 35;
+ const int pin_tcxo_enable = -1;
+
+ // pins for ST7789 display on Heltec T114
+ const int DISPLAY_DC = PIN_T114_TFT_DC;
+ const int DISPLAY_CS = PIN_T114_TFT_SS;
+ const int DISPLAY_MISO = PIN_T114_TFT_MISO;
+ const int DISPLAY_MOSI = PIN_T114_TFT_MOSI;
+ const int DISPLAY_CLK = PIN_T114_TFT_SCK;
+ const int DISPLAY_BL_PIN = PIN_T114_TFT_BLGT;
+ const int DISPLAY_RST = PIN_T114_TFT_RST;
+
+ #else
+ #error An unsupported nRF board was selected. Cannot compile RNode firmware.
+ #endif
+
+ #endif
+
+ #ifndef DISPLAY_SCALE
+ #define DISPLAY_SCALE 1
+ #endif
+
+ #ifndef HAS_RF_SWITCH_RX_TX
+ const int pin_rxen = -1;
+ const int pin_txen = -1;
+ #endif
+
+ #ifndef HAS_BUSY
+ const int pin_busy = -1;
+ #endif
+
+ #ifndef LED_ON
+ #define LED_ON HIGH
+ #endif
+
+ #ifndef LED_OFF
+ #define LED_OFF LOW
+ #endif
+
+ #ifndef DIO2_AS_RF_SWITCH
+ #define DIO2_AS_RF_SWITCH false
+ #endif
+
+ // Default OCP value if not specified
+ // in board configuration
+ #ifndef OCP_TUNED
+ #define OCP_TUNED 0x18
+ #endif
+
+ #ifndef NP_M
+ #define NP_M 0.15
+ #endif
+
+#endif
diff --git a/RNode_Firmware/Config.h b/RNode_Firmware/Config.h
new file mode 100644
index 0000000..ca102d0
--- /dev/null
+++ b/RNode_Firmware/Config.h
@@ -0,0 +1,224 @@
+// 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 .
+
+#include "ROM.h"
+#include "Boards.h"
+
+#ifndef CONFIG_H
+ #define CONFIG_H
+
+ #define MAJ_VERS 0x01
+ #define MIN_VERS 0x54
+
+ #define MODE_HOST 0x11
+ #define MODE_TNC 0x12
+
+ #define CABLE_STATE_DISCONNECTED 0x00
+ #define CABLE_STATE_CONNECTED 0x01
+ uint8_t cable_state = CABLE_STATE_DISCONNECTED;
+
+ #define BT_STATE_NA 0xff
+ #define BT_STATE_OFF 0x00
+ #define BT_STATE_ON 0x01
+ #define BT_STATE_PAIRING 0x02
+ #define BT_STATE_CONNECTED 0x03
+ uint8_t bt_state = BT_STATE_NA;
+ uint32_t bt_ssp_pin = 0;
+ bool bt_ready = false;
+ bool bt_enabled = false;
+ bool bt_allow_pairing = false;
+
+ #define M_FRQ_S 27388122
+ #define M_FRQ_R 27388061
+ bool console_active = false;
+ bool modem_installed = false;
+
+ #define MTU 508
+ #define SINGLE_MTU 255
+ #define HEADER_L 1
+ #define MIN_L 1
+ #define CMD_L 64
+
+ bool mw_radio_online = false;
+
+ #define eeprom_addr(a) (a+EEPROM_OFFSET)
+
+ #if (MODEM == SX1262 || MODEM == SX1280) && defined(NRF52840_XXAA)
+ SPIClass spiModem(NRF_SPIM2, pin_miso, pin_sclk, pin_mosi);
+ #endif
+
+ // MCU independent configuration parameters
+ const long serial_baudrate = 115200;
+
+ // SX1276 RSSI offset to get dBm value from
+ // packet RSSI register
+ const int rssi_offset = 157;
+
+ // Default LoRa settings
+ #define PHY_HEADER_LORA_SYMBOLS 20
+ #define PHY_CRC_LORA_BITS 16
+ #define LORA_PREAMBLE_SYMBOLS_MIN 18
+ #define LORA_PREAMBLE_TARGET_MS 24
+ #define LORA_PREAMBLE_FAST_DELTA 18
+ #define LORA_FAST_THRESHOLD_BPS 30E3
+ #define LORA_LIMIT_THRESHOLD_BPS 60E3
+ long lora_preamble_symbols = LORA_PREAMBLE_SYMBOLS_MIN;
+ long lora_preamble_time_ms = 0;
+ long lora_header_time_ms = 0;
+ float lora_symbol_time_ms = 0.0;
+ float lora_symbol_rate = 0.0;
+ float lora_us_per_byte = 0.0;
+ bool lora_low_datarate = false;
+ bool lora_limit_rate = false;
+
+ // CSMA Parameters
+ #define CSMA_SIFS_MS 0
+ #define CSMA_POST_TX_YIELD_SLOTS 3
+ #define CSMA_SLOT_MAX_MS 100
+ #define CSMA_SLOT_MIN_MS 24
+ #define CSMA_SLOT_MIN_FAST_DELTA 18
+ #define CSMA_SLOT_SYMBOLS 12
+ #define CSMA_CW_BANDS 4
+ #define CSMA_CW_MIN 0
+ #define CSMA_CW_PER_BAND_WINDOWS 15
+ #define CSMA_BAND_1_MAX_AIRTIME 7
+ #define CSMA_BAND_N_MIN_AIRTIME 85
+ #define CSMA_INFR_THRESHOLD_DB 12
+ bool interference_detected = false;
+ bool avoid_interference = true;
+ int csma_slot_ms = CSMA_SLOT_MIN_MS;
+ unsigned long difs_ms = CSMA_SIFS_MS + 2*csma_slot_ms;
+ unsigned long difs_wait_start = -1;
+ unsigned long cw_wait_start = -1;
+ unsigned long cw_wait_target = -1;
+ unsigned long cw_wait_passed = 0;
+ int csma_cw = -1;
+ uint8_t cw_band = 1;
+ uint8_t cw_min = 0;
+ uint8_t cw_max = CSMA_CW_PER_BAND_WINDOWS;
+
+ // LoRa settings
+ int lora_sf = 0;
+ int lora_cr = 5;
+ int lora_txp = 0xFF;
+ uint32_t lora_bw = 0;
+ uint32_t lora_freq = 0;
+ uint32_t lora_bitrate = 0;
+
+ // Operational variables
+ bool radio_locked = true;
+ bool radio_online = false;
+ bool community_fw = true;
+ bool hw_ready = false;
+ bool radio_error = false;
+ bool disp_ready = false;
+ bool pmu_ready = false;
+ bool promisc = false;
+ bool implicit = false;
+ bool memory_low = false;
+ uint8_t implicit_l = 0;
+
+ uint8_t op_mode = MODE_HOST;
+ uint8_t model = 0x00;
+ uint8_t hwrev = 0x00;
+
+ #define NOISE_FLOOR_SAMPLES 64
+ int noise_floor = -292;
+ int current_rssi = -292;
+ int last_rssi = -292;
+ uint8_t last_rssi_raw = 0x00;
+ uint8_t last_snr_raw = 0x80;
+ uint8_t seq = 0xFF;
+ uint16_t read_len = 0;
+
+ // Incoming packet buffer
+ uint8_t pbuf[MTU];
+
+ // KISS command buffer
+ uint8_t cmdbuf[CMD_L];
+
+ // LoRa transmit buffer
+ uint8_t tbuf[MTU];
+
+ uint32_t stat_rx = 0;
+ uint32_t stat_tx = 0;
+
+ #define STATUS_INTERVAL_MS 3
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #define DCD_SAMPLES 2500
+ #define UTIL_UPDATE_INTERVAL_MS 1000
+ #define UTIL_UPDATE_INTERVAL (UTIL_UPDATE_INTERVAL_MS/STATUS_INTERVAL_MS)
+ #define AIRTIME_LONGTERM 3600
+ #define AIRTIME_LONGTERM_MS (AIRTIME_LONGTERM*1000)
+ #define AIRTIME_BINLEN_MS (STATUS_INTERVAL_MS*DCD_SAMPLES)
+ #define AIRTIME_BINS ((AIRTIME_LONGTERM*1000)/AIRTIME_BINLEN_MS)
+ bool util_samples[DCD_SAMPLES];
+ uint16_t airtime_bins[AIRTIME_BINS];
+ float longterm_bins[AIRTIME_BINS];
+ int dcd_sample = 0;
+ float local_channel_util = 0.0;
+ float total_channel_util = 0.0;
+ float longterm_channel_util = 0.0;
+ float airtime = 0.0;
+ float longterm_airtime = 0.0;
+ #define current_airtime_bin(void) (millis()%AIRTIME_LONGTERM_MS)/AIRTIME_BINLEN_MS
+ #endif
+ float st_airtime_limit = 0.0;
+ float lt_airtime_limit = 0.0;
+ bool airtime_lock = false;
+
+ bool stat_signal_detected = false;
+ bool stat_signal_synced = false;
+ bool stat_rx_ongoing = false;
+ bool dcd = false;
+ bool dcd_led = false;
+ bool dcd_waiting = false;
+ long dcd_wait_until = 0;
+ uint16_t dcd_count = 0;
+ uint16_t dcd_threshold = 2;
+
+ uint32_t status_interval_ms = STATUS_INTERVAL_MS;
+ uint32_t last_status_update = 0;
+ uint32_t last_dcd = 0;
+
+ // Power management
+ #define BATTERY_STATE_UNKNOWN 0x00
+ #define BATTERY_STATE_DISCHARGING 0x01
+ #define BATTERY_STATE_CHARGING 0x02
+ #define BATTERY_STATE_CHARGED 0x03
+ bool battery_installed = false;
+ bool battery_indeterminate = false;
+ bool external_power = false;
+ bool battery_ready = false;
+ float battery_voltage = 0.0;
+ float battery_percent = 0.0;
+ uint8_t battery_state = 0x00;
+ uint8_t display_intensity = 0xFF;
+ uint8_t display_addr = 0xFF;
+ volatile bool display_updating = false;
+ bool display_blanking_enabled = false;
+ bool display_diagnostics = true;
+ bool device_init_done = false;
+ bool eeprom_ok = false;
+ bool firmware_update_mode = false;
+ bool serial_in_frame = false;
+
+ // Boot flags
+ #define START_FROM_BOOTLOADER 0x01
+ #define START_FROM_POWERON 0x02
+ #define START_FROM_BROWNOUT 0x03
+ #define START_FROM_JTAG 0x04
+
+#endif
diff --git a/RNode_Firmware/Console.h b/RNode_Firmware/Console.h
new file mode 100644
index 0000000..c59d348
--- /dev/null
+++ b/RNode_Firmware/Console.h
@@ -0,0 +1,203 @@
+// 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 .
+
+#include
+#include
+#include
+#include
+
+#include "SD.h"
+#include "SPI.h"
+
+#if HAS_SD
+ SPIClass *spi = NULL;
+#endif
+
+
+#if CONFIG_IDF_TARGET_ESP32
+#include "esp32/rom/rtc.h"
+#elif CONFIG_IDF_TARGET_ESP32S2
+#include "esp32s2/rom/rtc.h"
+#elif CONFIG_IDF_TARGET_ESP32C3
+#include "esp32c3/rom/rtc.h"
+#elif CONFIG_IDF_TARGET_ESP32S3
+#include "esp32s3/rom/rtc.h"
+#else
+#error Target CONFIG_IDF_TARGET is not supported
+#endif
+
+WebServer server(80);
+
+void console_dbg(String msg) {
+ Serial.print("[Webserver] ");
+ Serial.println(msg);
+}
+
+bool exists(String path){
+ bool yes = false;
+ File file = SPIFFS.open(path, "r");
+ if(!file.isDirectory()){
+ yes = true;
+ }
+ file.close();
+ return yes;
+}
+
+String console_get_content_type(String filename) {
+ if (server.hasArg("download")) {
+ return "application/octet-stream";
+ } else if (filename.endsWith(".htm")) {
+ return "text/html";
+ } else if (filename.endsWith(".html")) {
+ return "text/html";
+ } else if (filename.endsWith(".css")) {
+ return "text/css";
+ } else if (filename.endsWith(".js")) {
+ return "application/javascript";
+ } else if (filename.endsWith(".png")) {
+ return "image/png";
+ } else if (filename.endsWith(".gif")) {
+ return "image/gif";
+ } else if (filename.endsWith(".jpg")) {
+ return "image/jpeg";
+ } else if (filename.endsWith(".ico")) {
+ return "image/x-icon";
+ } else if (filename.endsWith(".xml")) {
+ return "text/xml";
+ } else if (filename.endsWith(".pdf")) {
+ return "application/x-pdf";
+ } else if (filename.endsWith(".zip")) {
+ return "application/x-zip";
+ } else if (filename.endsWith(".gz")) {
+ return "application/x-gzip";
+ } else if (filename.endsWith(".whl")) {
+ return "application/octet-stream";
+ }
+ return "text/plain";
+}
+
+bool console_serve_file(String path) {
+ console_dbg("Request for: "+path);
+ if (path.endsWith("/")) {
+ path += "index.html";
+ }
+
+ if (path == "/r/manual/index.html") {
+ path = "/m.html";
+ }
+ if (path == "/r/manual/Reticulum Manual.pdf") {
+ path = "/h.html";
+ }
+
+
+ String content_type = console_get_content_type(path);
+ String pathWithGz = path + ".gz";
+ if (exists(pathWithGz) || exists(path)) {
+ if (exists(pathWithGz)) {
+ path += ".gz";
+ }
+
+ File file = SPIFFS.open(path, "r");
+ console_dbg("Serving file to client");
+ server.streamFile(file, content_type);
+ file.close();
+
+ console_dbg("File serving done\n");
+ return true;
+ } else {
+ int spos = pathWithGz.lastIndexOf('/');
+ if (spos > 0) {
+ String remap_path = "/d";
+ remap_path.concat(pathWithGz.substring(spos));
+ Serial.println(remap_path);
+
+ if (exists(remap_path)) {
+ File file = SPIFFS.open(remap_path, "r");
+ console_dbg("Serving remapped file to client");
+ server.streamFile(file, content_type);
+ console_dbg("Closing file");
+ file.close();
+
+ console_dbg("File serving done\n");
+ return true;
+ }
+ }
+ }
+
+ console_dbg("Error: Could not open file for serving\n");
+ return false;
+}
+
+void console_register_pages() {
+ server.onNotFound([]() {
+ if (!console_serve_file(server.uri())) {
+ server.send(404, "text/plain", "Not Found");
+ }
+ });
+}
+
+void console_start() {
+ Serial.println("");
+ console_dbg("Starting Access Point...");
+ WiFi.softAP(bt_devname);
+ delay(150);
+ IPAddress ip(10, 0, 0, 1);
+ IPAddress nm(255, 255, 255, 0);
+ WiFi.softAPConfig(ip, ip, nm);
+
+ if(!SPIFFS.begin(true)){
+ console_dbg("Error: Could not mount SPIFFS");
+ return;
+ } else {
+ console_dbg("SPIFFS Ready");
+ }
+
+ #if HAS_SD
+ spi = new SPIClass(HSPI);
+ spi->begin(SD_CLK, SD_MISO, SD_MOSI, SD_CS);
+ if(!SD.begin(SD_CS, *spi)){
+ console_dbg("No SD card inserted");
+ } else {
+ uint8_t cardType = SD.cardType();
+ if(cardType == CARD_NONE){
+ console_dbg("No SD card type");
+ } else {
+ console_dbg("SD Card Type: ");
+ if(cardType == CARD_MMC){
+ console_dbg("MMC");
+ } else if(cardType == CARD_SD){
+ console_dbg("SDSC");
+ } else if(cardType == CARD_SDHC){
+ console_dbg("SDHC");
+ } else {
+ console_dbg("UNKNOWN");
+ }
+ uint64_t cardSize = SD.cardSize() / (1024 * 1024);
+ Serial.printf("SD Card Size: %lluMB\n", cardSize);
+ }
+ }
+ #endif
+
+ console_register_pages();
+ server.begin();
+ led_indicate_console();
+}
+
+void console_loop(){
+ server.handleClient();
+ // Internally, this yields the thread and allows
+ // other tasks to run.
+ delay(2);
+}
\ No newline at end of file
diff --git a/RNode_Firmware/Device.h b/RNode_Firmware/Device.h
new file mode 100644
index 0000000..e2a1d06
--- /dev/null
+++ b/RNode_Firmware/Device.h
@@ -0,0 +1,267 @@
+// 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 .
+
+#include
+
+#if MCU_VARIANT == MCU_ESP32
+#include "mbedtls/md.h"
+#include "esp_ota_ops.h"
+#include "esp_flash_partitions.h"
+#include "esp_partition.h"
+
+#elif MCU_VARIANT == MCU_NRF52
+#include "Adafruit_nRFCrypto.h"
+
+// size of chunk to retrieve from flash sector
+#define CHUNK_SIZE 128
+
+#define END_SECTION_SIZE 256
+
+#if defined(NRF52840_XXAA)
+// https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather/hathach-memory-map
+// each section follows along from one another, in this order
+// this is always at the start of the memory map
+#define APPLICATION_START 0x26000
+
+#define USER_DATA_START 0xED000
+
+#define IMG_SIZE_START 0xFF008
+#endif
+
+#endif
+
+// Forward declaration from Utilities.h
+void eeprom_update(int mapped_addr, uint8_t byte);
+uint8_t eeprom_read(uint32_t addr);
+void hard_reset(void);
+
+#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ void eeprom_flush();
+#endif
+
+const uint8_t dev_keys [] PROGMEM = {
+ 0x0f, 0x15, 0x86, 0x74, 0xa0, 0x7d, 0xf2, 0xde, 0x32, 0x11, 0x29, 0xc1, 0x0d, 0xda, 0xcc, 0xc3,
+ 0xe1, 0x9b, 0xac, 0xf2, 0x27, 0x06, 0xee, 0x89, 0x1f, 0x7a, 0xfc, 0xc3, 0x6a, 0xf5, 0x38, 0x08
+};
+
+#define DEV_SIG_LEN 64
+uint8_t dev_sig[DEV_SIG_LEN];
+
+#define DEV_KEY_LEN 32
+uint8_t dev_k_prv[DEV_KEY_LEN];
+uint8_t dev_k_pub[DEV_KEY_LEN];
+
+#define DEV_HASH_LEN 32
+uint8_t dev_hash[DEV_HASH_LEN];
+uint8_t dev_partition_table_hash[DEV_HASH_LEN];
+uint8_t dev_bootloader_hash[DEV_HASH_LEN];
+uint8_t dev_firmware_hash[DEV_HASH_LEN];
+uint8_t dev_firmware_hash_target[DEV_HASH_LEN];
+
+#define EEPROM_SIG_LEN 128
+uint8_t dev_eeprom_signature[EEPROM_SIG_LEN];
+
+bool dev_signature_validated = false;
+bool fw_signature_validated = true;
+
+#define DEV_SIG_OFFSET EEPROM_SIZE-EEPROM_RESERVED-DEV_SIG_LEN
+#define dev_sig_addr(a) (a+DEV_SIG_OFFSET)
+
+#define DEV_FWHASH_OFFSET EEPROM_SIZE-EEPROM_RESERVED-DEV_SIG_LEN-DEV_HASH_LEN
+#define dev_fwhash_addr(a) (a+DEV_FWHASH_OFFSET)
+
+bool device_signatures_ok() {
+ return dev_signature_validated && fw_signature_validated;
+}
+
+void device_validate_signature() {
+ int n_keys = sizeof(dev_keys)/DEV_KEY_LEN;
+ bool valid_signature_found = false;
+ for (int i = 0; i < n_keys; i++) {
+ memcpy(dev_k_pub, dev_keys+DEV_KEY_LEN*i, DEV_KEY_LEN);
+ if (Ed25519::verify(dev_sig, dev_k_pub, dev_hash, DEV_HASH_LEN)) {
+ valid_signature_found = true;
+ }
+ }
+
+ if (valid_signature_found) {
+ dev_signature_validated = true;
+ } else {
+ dev_signature_validated = false;
+ }
+}
+
+void device_save_signature() {
+ device_validate_signature();
+ if (dev_signature_validated) {
+ for (uint8_t i = 0; i < DEV_SIG_LEN; i++) {
+ eeprom_update(dev_sig_addr(i), dev_sig[i]);
+ }
+ }
+}
+
+void device_load_signature() {
+ for (uint8_t i = 0; i < DEV_SIG_LEN; i++) {
+ #if HAS_EEPROM
+ dev_sig[i] = EEPROM.read(dev_sig_addr(i));
+ #elif MCU_VARIANT == MCU_NRF52
+ dev_sig[i] = eeprom_read(dev_sig_addr(i));
+ #endif
+ }
+}
+
+void device_load_firmware_hash() {
+ for (uint8_t i = 0; i < DEV_HASH_LEN; i++) {
+ #if HAS_EEPROM
+ dev_firmware_hash_target[i] = EEPROM.read(dev_fwhash_addr(i));
+ #elif MCU_VARIANT == MCU_NRF52
+ dev_firmware_hash_target[i] = eeprom_read(dev_fwhash_addr(i));
+ #endif
+ }
+}
+
+void device_save_firmware_hash() {
+ for (uint8_t i = 0; i < DEV_HASH_LEN; i++) {
+ eeprom_update(dev_fwhash_addr(i), dev_firmware_hash_target[i]);
+ }
+ #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ eeprom_flush();
+ #endif
+ if (!fw_signature_validated) hard_reset();
+}
+
+#if MCU_VARIANT == MCU_NRF52
+uint32_t retrieve_application_size() {
+ uint8_t bytes[4];
+ memcpy(bytes, (const void*)IMG_SIZE_START, 4);
+ uint32_t fw_len = bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24;
+ return fw_len;
+}
+
+void calculate_region_hash(unsigned long long start, unsigned long long end, uint8_t* return_hash) {
+ // this function calculates the hash digest of a region of memory,
+ // currently it is only designed to work for the application region
+ uint8_t chunk[CHUNK_SIZE] = {0};
+
+ // to store potential last chunk of program
+ uint8_t chunk_next[CHUNK_SIZE] = {0};
+ nRFCrypto_Hash hash;
+
+ hash.begin(CRYS_HASH_SHA256_mode);
+
+ uint8_t size;
+
+ while (start < end ) {
+ const void* src = (const void*)start;
+ if (start + CHUNK_SIZE >= end) {
+ size = end - start;
+ }
+ else {
+ size = CHUNK_SIZE;
+ }
+
+ memcpy(chunk, src, CHUNK_SIZE);
+
+ hash.update(chunk, size);
+
+ start += CHUNK_SIZE;
+ }
+ hash.end(return_hash);
+}
+#endif
+
+void device_validate_partitions() {
+ device_load_firmware_hash();
+ #if MCU_VARIANT == MCU_ESP32
+ esp_partition_t partition;
+ partition.address = ESP_PARTITION_TABLE_OFFSET;
+ partition.size = ESP_PARTITION_TABLE_MAX_LEN;
+ partition.type = ESP_PARTITION_TYPE_DATA;
+ esp_partition_get_sha256(&partition, dev_partition_table_hash);
+ partition.address = ESP_BOOTLOADER_OFFSET;
+ partition.size = ESP_PARTITION_TABLE_OFFSET;
+ partition.type = ESP_PARTITION_TYPE_APP;
+ esp_partition_get_sha256(&partition, dev_bootloader_hash);
+ esp_partition_get_sha256(esp_ota_get_running_partition(), dev_firmware_hash);
+ #elif MCU_VARIANT == MCU_NRF52
+ // todo, add bootloader, partition table, or softdevice?
+ calculate_region_hash(APPLICATION_START, APPLICATION_START+retrieve_application_size(), dev_firmware_hash);
+ #endif
+ #if VALIDATE_FIRMWARE
+ for (uint8_t i = 0; i < DEV_HASH_LEN; i++) {
+ if (dev_firmware_hash_target[i] != dev_firmware_hash[i]) {
+ fw_signature_validated = false;
+ break;
+ }
+ }
+ #endif
+}
+
+bool device_firmware_ok() {
+ return fw_signature_validated;
+}
+
+#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+bool device_init() {
+ if (bt_ready) {
+ #if MCU_VARIANT == MCU_ESP32
+ for (uint8_t i=0; i.
+
+#include "Graphics.h"
+#include
+
+#if BOARD_MODEL != BOARD_TECHO
+ #if BOARD_MODEL == BOARD_TDECK
+ #include
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ #include "ST7789.h"
+ #define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ #include
+ #else
+ #include
+ #include
+ #endif
+
+#else
+ void (*display_callback)();
+ void display_add_callback(void (*callback)()) { display_callback = callback; }
+ void busyCallback(const void* p) { display_callback(); }
+ #define SSD1306_BLACK GxEPD_BLACK
+ #define SSD1306_WHITE GxEPD_WHITE
+ #include
+ #include
+#endif
+
+#include "Fonts/Org_01.h"
+#define DISP_W 128
+#define DISP_H 64
+
+#if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0
+ #define DISP_RST -1
+ #define DISP_ADDR 0x3C
+#elif BOARD_MODEL == BOARD_TBEAM
+ #define DISP_RST 13
+ #define DISP_ADDR 0x3C
+ #define DISP_CUSTOM_ADDR true
+#elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_LORA32_V1_0
+ #define DISP_RST 16
+ #define DISP_ADDR 0x3C
+ #define SCL_OLED 15
+ #define SDA_OLED 4
+#elif BOARD_MODEL == BOARD_HELTEC32_V3
+ #define DISP_RST 21
+ #define DISP_ADDR 0x3C
+ #define SCL_OLED 18
+ #define SDA_OLED 17
+#elif BOARD_MODEL == BOARD_HELTEC32_V4
+ #define DISP_RST 21
+ #define DISP_ADDR 0x3C
+ #define SCL_OLED 18
+ #define SDA_OLED 17
+#elif BOARD_MODEL == BOARD_RAK4631
+ // RAK1921/SSD1306
+ #define DISP_RST -1
+ #define DISP_ADDR 0x3C
+ #define SCL_OLED 14
+ #define SDA_OLED 13
+#elif BOARD_MODEL == BOARD_RNODE_NG_21
+ #define DISP_RST -1
+ #define DISP_ADDR 0x3C
+#elif BOARD_MODEL == BOARD_T3S3
+ #define DISP_RST 21
+ #define DISP_ADDR 0x3C
+ #define SCL_OLED 17
+ #define SDA_OLED 18
+#elif BOARD_MODEL == BOARD_TECHO
+ SPIClass displaySPI = SPIClass(NRF_SPIM0, pin_disp_miso, pin_disp_sck, pin_disp_mosi);
+ #define DISP_W 128
+ #define DISP_H 64
+ #define DISP_ADDR -1
+#elif BOARD_MODEL == BOARD_VME213
+ #include "src/LCMEN2R13EFC1.h"
+ #include "src/einkDetect_VME213.h"
+ // VME213 E-Ink display: 250x122 pixels
+ #define DISP_W 250
+ #define DISP_H 122
+ #define DISP_ADDR -1
+ #define VME213_REFRESH_RATIO 10 // 10 FAST : 1 FULL refresh
+#elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ #define DISP_RST -1
+ #define DISP_ADDR 0x3C
+ #define SCL_OLED 18
+ #define SDA_OLED 17
+ #define DISP_CUSTOM_ADDR false
+#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
+ #define DISP_CUSTOM_ADDR true
+#endif
+
+#define SMALL_FONT &Org_01
+
+#if BOARD_MODEL == BOARD_TDECK
+ Adafruit_ST7789 display = Adafruit_ST7789(DISPLAY_CS, DISPLAY_DC, -1);
+ #define SSD1306_WHITE ST77XX_WHITE
+ #define SSD1306_BLACK ST77XX_BLACK
+#elif BOARD_MODEL == BOARD_HELTEC_T114
+ ST7789Spi display(&SPI1, DISPLAY_RST, DISPLAY_DC, DISPLAY_CS);
+ #define SSD1306_WHITE ST77XX_WHITE
+ #define SSD1306_BLACK ST77XX_BLACK
+#elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);
+ #define SSD1306_WHITE SH110X_WHITE
+ #define SSD1306_BLACK SH110X_BLACK
+#elif BOARD_MODEL == BOARD_TECHO
+ GxEPD2_BW display(GxEPD2_154_D67(pin_disp_cs, pin_disp_dc, pin_disp_reset, pin_disp_busy));
+ uint32_t last_epd_refresh = 0;
+ uint32_t last_epd_full_refresh = 0;
+ #define REFRESH_PERIOD 300000
+#elif BOARD_MODEL == BOARD_VME213
+ LCMEN2R13EFC1 vme213_display;
+ EInkChipType vme213_chip_type = EINK_LCMEN213EFC1;
+ uint8_t vme213_displayBuffer[4000]; // 250x122 pixels = 4000 bytes
+ uint32_t last_epd_refresh = 0;
+ uint32_t last_epd_full_refresh = 0;
+ uint8_t vme213_fast_refresh_count = 0;
+ #define REFRESH_PERIOD 300000
+ // Compatibility macro
+ #define display vme213_display
+ #define SSD1306_BLACK 0
+ #define SSD1306_WHITE 1
+#else
+ Adafruit_SSD1306 display(DISP_W, DISP_H, &Wire, DISP_RST);
+#endif
+
+float disp_target_fps = 7;
+float epd_update_fps = 0.5;
+
+#define DISP_MODE_UNKNOWN 0x00
+#define DISP_MODE_LANDSCAPE 0x01
+#define DISP_MODE_PORTRAIT 0x02
+#define DISP_PIN_SIZE 6
+#define DISPLAY_BLANKING_TIMEOUT 15*1000
+uint8_t disp_mode = DISP_MODE_UNKNOWN;
+uint8_t disp_ext_fb = false;
+unsigned char fb[512];
+uint32_t last_disp_update = 0;
+uint32_t last_unblank_event = 0;
+uint32_t display_blanking_timeout = DISPLAY_BLANKING_TIMEOUT;
+uint8_t display_unblank_intensity = display_intensity;
+bool display_blanked = false;
+bool display_tx = false;
+bool recondition_display = false;
+int disp_update_interval = 1000/disp_target_fps;
+int epd_update_interval = 1000/disp_target_fps;
+uint32_t last_page_flip = 0;
+int page_interval = 4000;
+bool device_signatures_ok();
+bool device_firmware_ok();
+
+#define WATERFALL_SIZE 46
+int waterfall[WATERFALL_SIZE];
+int waterfall_head = 0;
+
+int p_ad_x = 0;
+int p_ad_y = 0;
+int p_as_x = 0;
+int p_as_y = 0;
+
+GFXcanvas1 stat_area(64, 64);
+GFXcanvas1 disp_area(64, 64);
+
+void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour);
+
+void update_area_positions() {
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ if (disp_mode == DISP_MODE_PORTRAIT) {
+ p_ad_x = 16;
+ p_ad_y = 64;
+ p_as_x = 16;
+ p_as_y = p_ad_y+126;
+ } else if (disp_mode == DISP_MODE_LANDSCAPE) {
+ p_ad_x = 0;
+ p_ad_y = 96;
+ p_as_x = 126;
+ p_as_y = p_ad_y;
+ }
+ #elif BOARD_MODEL == BOARD_TECHO
+ if (disp_mode == DISP_MODE_PORTRAIT) {
+ p_ad_x = 61;
+ p_ad_y = 36;
+ p_as_x = 64;
+ p_as_y = 64+36;
+ } else if (disp_mode == DISP_MODE_LANDSCAPE) {
+ p_ad_x = 0;
+ p_ad_y = 0;
+ p_as_x = 64;
+ p_as_y = 0;
+ }
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213: 250x122 landscape - split into two 125x122 areas
+ if (disp_mode == DISP_MODE_LANDSCAPE) {
+ p_ad_x = 0;
+ p_ad_y = 0;
+ p_as_x = 125;
+ p_as_y = 0;
+ } else {
+ p_ad_x = 0;
+ p_ad_y = 0;
+ p_as_x = 0;
+ p_as_y = 61;
+ }
+ #else
+ if (disp_mode == DISP_MODE_PORTRAIT) {
+ p_ad_x = 0 * DISPLAY_SCALE;
+ p_ad_y = 0 * DISPLAY_SCALE;
+ p_as_x = 0 * DISPLAY_SCALE;
+ p_as_y = 64 * DISPLAY_SCALE;
+ } else if (disp_mode == DISP_MODE_LANDSCAPE) {
+ p_ad_x = 0 * DISPLAY_SCALE;
+ p_ad_y = 0 * DISPLAY_SCALE;
+ p_as_x = 64 * DISPLAY_SCALE;
+ p_as_y = 0 * DISPLAY_SCALE;
+ }
+ #endif
+}
+
+uint8_t display_contrast = 0x00;
+#if BOARD_MODEL == BOARD_TBEAM_S_V1
+ void set_contrast(Adafruit_SH1106G *display, uint8_t value) {
+ }
+#elif BOARD_MODEL == BOARD_HELTEC_T114
+ void set_contrast(ST7789Spi *display, uint8_t value) { }
+#elif BOARD_MODEL == BOARD_TECHO
+ void set_contrast(void *display, uint8_t value) {
+ if (value == 0) { analogWrite(pin_backlight, 0); }
+ else { analogWrite(pin_backlight, value); }
+ }
+#elif BOARD_MODEL == BOARD_TDECK
+ void set_contrast(Adafruit_ST7789 *display, uint8_t value) {
+ static uint8_t level = 0;
+ static uint8_t steps = 16;
+ if (value > 15) value = 15;
+ if (value == 0) {
+ digitalWrite(DISPLAY_BL_PIN, 0);
+ delay(3);
+ level = 0;
+ return;
+ }
+ if (level == 0) {
+ digitalWrite(DISPLAY_BL_PIN, 1);
+ level = steps;
+ delayMicroseconds(30);
+ }
+ int from = steps - level;
+ int to = steps - value;
+ int num = (steps + to - from) % steps;
+ for (int i = 0; i < num; i++) {
+ digitalWrite(DISPLAY_BL_PIN, 0);
+ digitalWrite(DISPLAY_BL_PIN, 1);
+ }
+ level = value;
+ }
+#else
+ void set_contrast(Adafruit_SSD1306 *display, uint8_t contrast) {
+ display->ssd1306_command(SSD1306_SETCONTRAST);
+ display->ssd1306_command(contrast);
+ }
+#endif
+
+#if BOARD_MODEL == BOARD_VME213
+// VME213 E-Ink display rendering helpers
+void vme213_drawPixel(uint16_t x, uint16_t y, uint8_t color) {
+ if (x >= DISP_W || y >= DISP_H) return;
+ uint16_t byteIndex = y * ((DISP_W + 7) / 8) + (x / 8);
+ uint8_t bitMask = 0x80 >> (x % 8);
+
+ if (color == SSD1306_WHITE) {
+ vme213_displayBuffer[byteIndex] |= bitMask; // White pixel
+ } else {
+ vme213_displayBuffer[byteIndex] &= ~bitMask; // Black pixel
+ }
+}
+
+void vme213_fillScreen(uint8_t color) {
+ memset(vme213_displayBuffer, (color == SSD1306_WHITE) ? 0xFF : 0x00, sizeof(vme213_displayBuffer));
+}
+
+void vme213_drawText(uint16_t x, uint16_t y, const char* text, uint8_t size) {
+ // Simple text rendering using 5x7 font
+ // This is a placeholder - full implementation would use Adafruit_GFX font rendering
+ // For now, just mark text position
+ for (int i = 0; i < 10; i++) {
+ vme213_drawPixel(x + i, y, SSD1306_BLACK);
+ }
+}
+
+void vme213_renderStatus() {
+ vme213_fillScreen(SSD1306_WHITE);
+
+ // Header area (top 20 pixels)
+ for (int y = 0; y < 20; y++) {
+ for (int x = 0; x < DISP_W; x++) {
+ vme213_drawPixel(x, y, (y % 2 == 0) ? SSD1306_BLACK : SSD1306_WHITE);
+ }
+ }
+
+ // Status text placeholder
+ vme213_drawText(10, 30, "RNode", 2);
+
+ // Signal indicator (simple bars)
+ if (radio_online) {
+ for (int i = 0; i < 5; i++) {
+ int barHeight = 10 + i * 5;
+ for (int y = 0; y < barHeight; y++) {
+ vme213_drawPixel(10 + i * 8, 100 - y, SSD1306_BLACK);
+ }
+ }
+ }
+}
+#endif
+
+bool display_init() {
+ #if HAS_DISPLAY
+ #if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0
+ int pin_display_en = 16;
+ digitalWrite(pin_display_en, LOW);
+ delay(50);
+ digitalWrite(pin_display_en, HIGH);
+ #elif BOARD_MODEL == BOARD_T3S3
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #elif BOARD_MODEL == BOARD_HELTEC32_V2
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #elif BOARD_MODEL == BOARD_HELTEC32_V3
+ // enable vext / pin 36
+ pinMode(Vext, OUTPUT);
+ digitalWrite(Vext, LOW);
+ delay(50);
+ int pin_display_en = 21;
+ pinMode(pin_display_en, OUTPUT);
+ digitalWrite(pin_display_en, LOW);
+ delay(50);
+ digitalWrite(pin_display_en, HIGH);
+ delay(50);
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ // enable vext / pin 36
+ pinMode(Vext, OUTPUT);
+ digitalWrite(Vext, LOW);
+ delay(50);
+ int pin_display_en = 21;
+ pinMode(pin_display_en, OUTPUT);
+ digitalWrite(pin_display_en, LOW);
+ delay(50);
+ digitalWrite(pin_display_en, HIGH);
+ delay(50);
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #elif BOARD_MODEL == BOARD_LORA32_V1_0
+ int pin_display_en = 16;
+ digitalWrite(pin_display_en, LOW);
+ delay(50);
+ digitalWrite(pin_display_en, HIGH);
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ pinMode(PIN_T114_TFT_EN, OUTPUT);
+ digitalWrite(PIN_T114_TFT_EN, LOW);
+ #elif BOARD_MODEL == BOARD_TECHO
+ display.init(0, true, 10, false, displaySPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
+ display.setPartialWindow(0, 0, DISP_W, DISP_H);
+ display.epd2.setBusyCallback(busyCallback);
+ #if HAS_BACKLIGHT
+ pinMode(pin_backlight, OUTPUT);
+ analogWrite(pin_backlight, 0);
+ #endif
+ #elif BOARD_MODEL == BOARD_VME213
+ // Enable Vext power (GPIO 18) for peripherals
+ pinMode(Vext, OUTPUT);
+ digitalWrite(Vext, HIGH);
+ delay(100);
+
+ // Detect E-Ink chip type
+ vme213_chip_type = detectEInkChip(pin_disp_reset, pin_disp_busy);
+
+ // Initialize SPI for E-Ink display
+ SPI.begin(pin_disp_sck, pin_disp_miso, pin_disp_mosi, pin_disp_cs);
+
+ // Initialize E-Ink driver
+ vme213_display.begin(&SPI, pin_disp_dc, pin_disp_cs, pin_disp_busy, pin_disp_reset);
+
+ // Clear buffer
+ memset(vme213_displayBuffer, 0xFF, sizeof(vme213_displayBuffer));
+
+ // Perform initial FULL refresh to clear display
+ vme213_display.update(vme213_displayBuffer, LCMEN2R13EFC1::UPDATE_FULL);
+ vme213_fast_refresh_count = 0;
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #elif BOARD_MODEL == BOARD_XIAO_S3
+ Wire.begin(SDA_OLED, SCL_OLED);
+ #endif
+
+ #if HAS_EEPROM
+ uint8_t display_rotation = EEPROM.read(eeprom_addr(ADDR_CONF_DROT));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t display_rotation = eeprom_read(eeprom_addr(ADDR_CONF_DROT));
+ #endif
+ if (display_rotation < 0 or display_rotation > 3) display_rotation = 0xFF;
+
+ #if DISP_CUSTOM_ADDR == true
+ #if HAS_EEPROM
+ uint8_t display_address = EEPROM.read(eeprom_addr(ADDR_CONF_DADR));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t display_address = eeprom_read(eeprom_addr(ADDR_CONF_DADR));
+ #endif
+ if (display_address == 0xFF) display_address = DISP_ADDR;
+ #else
+ uint8_t display_address = DISP_ADDR;
+ #endif
+
+ #if HAS_EEPROM
+ if (EEPROM.read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) {
+ uint8_t db_timeout = EEPROM.read(eeprom_addr(ADDR_CONF_DBLK));
+ if (db_timeout == 0x00) {
+ display_blanking_enabled = false;
+ } else {
+ display_blanking_enabled = true;
+ display_blanking_timeout = db_timeout*1000;
+ }
+ }
+ #elif MCU_VARIANT == MCU_NRF52
+ if (eeprom_read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) {
+ uint8_t db_timeout = eeprom_read(eeprom_addr(ADDR_CONF_DBLK));
+ if (db_timeout == 0x00) {
+ display_blanking_enabled = false;
+ } else {
+ display_blanking_enabled = true;
+ display_blanking_timeout = db_timeout*1000;
+ }
+ }
+ #endif
+
+ #if BOARD_MODEL == BOARD_TECHO
+ // Don't check if display is actually connected
+ if(false) {
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213 E-Ink display initialization already done, skip connection check
+ if(false) {
+ #elif BOARD_MODEL == BOARD_TDECK
+ display.init(240, 320);
+ display.setSPISpeed(80e6);
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ display.init();
+ // set white as default pixel colour for Heltec T114
+ display.setRGB(COLOR565(0xFF, 0xFF, 0xFF));
+ if (false) {
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ if (!display.begin(display_address, true)) {
+ #else
+ if (!display.begin(SSD1306_SWITCHCAPVCC, display_address)) {
+ #endif
+ return false;
+ } else {
+ set_contrast(&display, display_contrast);
+ if (display_rotation != 0xFF) {
+ if (display_rotation == 0 || display_rotation == 2) {
+ disp_mode = DISP_MODE_LANDSCAPE;
+ } else {
+ disp_mode = DISP_MODE_PORTRAIT;
+ }
+ display.setRotation(display_rotation);
+ } else {
+ #if BOARD_MODEL == BOARD_RNODE_NG_20
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_RNODE_NG_21
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_LORA32_V1_0
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_LORA32_V2_1
+ disp_mode = DISP_MODE_LANDSCAPE;
+ display.setRotation(0);
+ #elif BOARD_MODEL == BOARD_TBEAM
+ disp_mode = DISP_MODE_LANDSCAPE;
+ display.setRotation(0);
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(1);
+ #elif BOARD_MODEL == BOARD_HELTEC32_V2
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(1);
+ #elif BOARD_MODEL == BOARD_HELTEC32_V3
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(1);
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(1);
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(1);
+ #elif BOARD_MODEL == BOARD_RAK4631
+ disp_mode = DISP_MODE_LANDSCAPE;
+ display.setRotation(0);
+ #elif BOARD_MODEL == BOARD_TDECK
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_TECHO
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213 E-Ink: 250x122 landscape by default
+ disp_mode = DISP_MODE_LANDSCAPE;
+ // No setRotation for VME213 (driver handles orientation)
+ #else
+ disp_mode = DISP_MODE_PORTRAIT;
+ display.setRotation(3);
+ #endif
+ }
+
+ update_area_positions();
+
+ for (int i = 0; i < WATERFALL_SIZE; i++) { waterfall[i] = 0; }
+
+ last_page_flip = millis();
+
+ stat_area.cp437(true);
+ disp_area.cp437(true);
+
+ #if BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_VME213
+ display.cp437(true);
+ #endif
+
+ #if HAS_EEPROM
+ display_intensity = EEPROM.read(eeprom_addr(ADDR_CONF_DINT));
+ #elif MCU_VARIANT == MCU_NRF52
+ display_intensity = eeprom_read(eeprom_addr(ADDR_CONF_DINT));
+ #endif
+ display_unblank_intensity = display_intensity;
+
+ #if BOARD_MODEL == BOARD_TECHO
+ #if HAS_BACKLIGHT
+ if (display_intensity == 0) { analogWrite(pin_backlight, 0); }
+ else { analogWrite(pin_backlight, display_intensity); }
+ #endif
+ #endif
+
+ #if BOARD_MODEL == BOARD_TDECK
+ display.fillScreen(SSD1306_BLACK);
+ #endif
+
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ // Enable backlight led (display is always black without this)
+ fillRect(p_ad_x, p_ad_y, 128, 128, SSD1306_BLACK);
+ fillRect(p_as_x, p_as_y, 128, 128, SSD1306_BLACK);
+ pinMode(PIN_T114_TFT_BLGT, OUTPUT);
+ digitalWrite(PIN_T114_TFT_BLGT, LOW);
+ #endif
+
+ return true;
+ }
+ #else
+ return false;
+ #endif
+}
+
+// Draws a line on the screen
+void drawLine(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour) {
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ if(colour == SSD1306_WHITE){
+ display.setColor(WHITE);
+ } else if(colour == SSD1306_BLACK) {
+ display.setColor(BLACK);
+ }
+ display.drawLine(x, y, width, height);
+ #else
+ display.drawLine(x, y, width, height, colour);
+ #endif
+}
+
+// Draws a filled rectangle on the screen
+void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour) {
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ if(colour == SSD1306_WHITE){
+ display.setColor(WHITE);
+ } else if(colour == SSD1306_BLACK) {
+ display.setColor(BLACK);
+ }
+ display.fillRect(x, y, width, height);
+ #else
+ display.fillRect(x, y, width, height, colour);
+ #endif
+}
+
+// Draws a bitmap to the display and auto scales it based on the boards configured DISPLAY_SCALE
+void drawBitmap(int16_t startX, int16_t startY, const uint8_t* bitmap, int16_t bitmapWidth, int16_t bitmapHeight, uint16_t foregroundColour, uint16_t backgroundColour) {
+ #if DISPLAY_SCALE == 1
+ display.drawBitmap(startX, startY, bitmap, bitmapWidth, bitmapHeight, foregroundColour, backgroundColour);
+ #else
+ for(int16_t row = 0; row < bitmapHeight; row++){
+ for(int16_t col = 0; col < bitmapWidth; col++){
+
+ // determine index and bitmask
+ int16_t index = row * ((bitmapWidth + 7) / 8) + (col / 8);
+ uint8_t bitmask = 1 << (7 - (col % 8));
+
+ // check if the current pixel is set in the bitmap
+ if(bitmap[index] & bitmask){
+ // draw a scaled rectangle for the foreground pixel
+ fillRect(startX + col * DISPLAY_SCALE, startY + row * DISPLAY_SCALE, DISPLAY_SCALE, DISPLAY_SCALE, foregroundColour);
+ } else {
+ // draw a scaled rectangle for the background pixel
+ fillRect(startX + col * DISPLAY_SCALE, startY + row * DISPLAY_SCALE, DISPLAY_SCALE, DISPLAY_SCALE, backgroundColour);
+ }
+
+ }
+ }
+ #endif
+}
+
+void draw_cable_icon(int px, int py) {
+ if (cable_state == CABLE_STATE_DISCONNECTED) {
+ stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else if (cable_state == CABLE_STATE_CONNECTED) {
+ stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ }
+}
+
+void draw_bt_icon(int px, int py) {
+ if (bt_state == BT_STATE_OFF) {
+ stat_area.drawBitmap(px, py, bm_bt+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else if (bt_state == BT_STATE_ON) {
+ stat_area.drawBitmap(px, py, bm_bt+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else if (bt_state == BT_STATE_PAIRING) {
+ stat_area.drawBitmap(px, py, bm_bt+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else if (bt_state == BT_STATE_CONNECTED) {
+ stat_area.drawBitmap(px, py, bm_bt+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ stat_area.drawBitmap(px, py, bm_bt+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ }
+}
+
+void draw_lora_icon(int px, int py) {
+ if (radio_online) {
+ stat_area.drawBitmap(px, py, bm_rf+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ stat_area.drawBitmap(px, py, bm_rf+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ }
+}
+
+void draw_mw_icon(int px, int py) {
+ if (mw_radio_online) {
+ stat_area.drawBitmap(px, py, bm_rf+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ stat_area.drawBitmap(px, py, bm_rf+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
+ }
+}
+
+uint8_t charge_tick = 0;
+void draw_battery_bars(int px, int py) {
+ if (pmu_ready) {
+ if (battery_ready) {
+ if (battery_installed) {
+ float battery_value = battery_percent;
+
+ // Disable charging state display for now, since
+ // boards without dedicated PMU are completely
+ // unreliable for determining actual charging state.
+ bool disable_charge_status = false;
+ if (battery_indeterminate && battery_state == BATTERY_STATE_CHARGING) {
+ disable_charge_status = true;
+ }
+
+ if (battery_state == BATTERY_STATE_CHARGING && !disable_charge_status) {
+ float battery_prog = battery_percent;
+ if (battery_prog > 85) { battery_prog = 84; }
+ if (charge_tick < battery_prog ) { charge_tick = battery_prog; }
+ battery_value = charge_tick;
+ charge_tick += 3;
+ if (charge_tick > 100) charge_tick = 0;
+ }
+
+ if (battery_indeterminate && battery_state == BATTERY_STATE_CHARGING && !disable_charge_status) {
+ stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
+ stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ if (battery_state == BATTERY_STATE_CHARGED) {
+ stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
+ stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ // stat_area.fillRect(px, py, 14, 3, SSD1306_BLACK);
+ stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
+ stat_area.drawRect(px-2, py-2, 17, 7, SSD1306_WHITE);
+ stat_area.drawLine(px+15, py, px+15, py+3, SSD1306_WHITE);
+ if (battery_value > 7) stat_area.drawLine(px, py, px, py+2, SSD1306_WHITE);
+ if (battery_value > 20) stat_area.drawLine(px+1*2, py, px+1*2, py+2, SSD1306_WHITE);
+ if (battery_value > 33) stat_area.drawLine(px+2*2, py, px+2*2, py+2, SSD1306_WHITE);
+ if (battery_value > 46) stat_area.drawLine(px+3*2, py, px+3*2, py+2, SSD1306_WHITE);
+ if (battery_value > 59) stat_area.drawLine(px+4*2, py, px+4*2, py+2, SSD1306_WHITE);
+ if (battery_value > 72) stat_area.drawLine(px+5*2, py, px+5*2, py+2, SSD1306_WHITE);
+ if (battery_value > 85) stat_area.drawLine(px+6*2, py, px+6*2, py+2, SSD1306_WHITE);
+ }
+ }
+ } else {
+ stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
+ stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ }
+ } else {
+ stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
+ stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
+ }
+}
+
+#define Q_SNR_STEP 2.0
+#define Q_SNR_MIN_BASE -9.0
+#define Q_SNR_MAX 6.0
+void draw_quality_bars(int px, int py) {
+ stat_area.fillRect(px, py, 13, 7, SSD1306_BLACK);
+ if (radio_online) {
+ signed char t_snr = (signed int)last_snr_raw;
+ int snr_int = (int)t_snr;
+ float snr_min = Q_SNR_MIN_BASE-(int)lora_sf*Q_SNR_STEP;
+ float snr_span = (Q_SNR_MAX-snr_min);
+ float snr = ((int)snr_int) * 0.25;
+ float quality = ((snr-snr_min)/(snr_span))*100;
+ if (quality > 100.0) quality = 100.0;
+ 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);
+ }
+}
+
+#if MODEM == SX1280
+ #define S_RSSI_MIN -105.0
+ #define S_RSSI_MAX -65.0
+#else
+ #define S_RSSI_MIN -135.0
+ #define S_RSSI_MAX -75.0
+#endif
+#define S_RSSI_SPAN (S_RSSI_MAX-S_RSSI_MIN)
+void draw_signal_bars(int px, int py) {
+ stat_area.fillRect(px, py, 13, 7, SSD1306_BLACK);
+
+ if (radio_online) {
+ int rssi_val = last_rssi;
+ if (rssi_val < S_RSSI_MIN) rssi_val = S_RSSI_MIN;
+ if (rssi_val > S_RSSI_MAX) rssi_val = S_RSSI_MAX;
+ int signal = ((rssi_val - S_RSSI_MIN)*(1.0/S_RSSI_SPAN))*100.0;
+
+ if (signal > 100.0) signal = 100.0;
+ if (signal < 0.0) signal = 0.0;
+
+ // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality);
+ if (signal > 85) stat_area.drawLine(px+0*2, py+7, px+0*2, py+0, SSD1306_WHITE);
+ if (signal > 72) stat_area.drawLine(px+1*2, py+7, px+1*2, py+1, SSD1306_WHITE);
+ if (signal > 59) stat_area.drawLine(px+2*2, py+7, px+2*2, py+2, SSD1306_WHITE);
+ if (signal > 46) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, SSD1306_WHITE);
+ if (signal > 33) stat_area.drawLine(px+4*2, py+7, px+4*2, py+4, SSD1306_WHITE);
+ if (signal > 20) stat_area.drawLine(px+5*2, py+7, px+5*2, py+5, SSD1306_WHITE);
+ if (signal > 7) stat_area.drawLine(px+6*2, py+7, px+6*2, py+6, SSD1306_WHITE);
+ }
+}
+
+#if MODEM == SX1280
+ #define WF_TX_SIZE 5
+#else
+ #define WF_TX_SIZE 5
+#endif
+#define WF_RSSI_MAX -60
+#define WF_RSSI_MIN -135
+#define WF_RSSI_SPAN (WF_RSSI_MAX-WF_RSSI_MIN)
+#define WF_PIXEL_WIDTH 10
+void draw_waterfall(int px, int py) {
+ int rssi_val = current_rssi;
+ if (rssi_val < WF_RSSI_MIN) rssi_val = WF_RSSI_MIN;
+ if (rssi_val > WF_RSSI_MAX) rssi_val = WF_RSSI_MAX;
+ int rssi_normalised = ((rssi_val - WF_RSSI_MIN)*(1.0/WF_RSSI_SPAN))*WF_PIXEL_WIDTH;
+ if (display_tx) {
+ for (uint8_t i = 0; i < WF_TX_SIZE; i++) {
+ waterfall[waterfall_head++] = -1;
+ if (waterfall_head >= WATERFALL_SIZE) waterfall_head = 0;
+ }
+ display_tx = false;
+ } else {
+ waterfall[waterfall_head++] = rssi_normalised;
+ if (waterfall_head >= WATERFALL_SIZE) waterfall_head = 0;
+ }
+
+ stat_area.fillRect(px,py,WF_PIXEL_WIDTH, WATERFALL_SIZE, SSD1306_BLACK);
+ for (int i = 0; i < WATERFALL_SIZE; i++){
+ int wi = (waterfall_head+i)%WATERFALL_SIZE;
+ int ws = waterfall[wi];
+ if (ws > 0) {
+ stat_area.drawLine(px, py+i, px+ws-1, py+i, SSD1306_WHITE);
+ } else if (ws == -1) {
+ uint8_t o = i%2;
+ for (uint8_t ti = 0; ti < WF_PIXEL_WIDTH/2; ti++) {
+ stat_area.drawPixel(px+ti*2+o, py+i, SSD1306_WHITE);
+ }
+ }
+ }
+}
+
+bool stat_area_intialised = false;
+void draw_stat_area() {
+ if (device_init_done) {
+ if (!stat_area_intialised) {
+ stat_area.drawBitmap(0, 0, bm_frame, 64, 64, SSD1306_WHITE, SSD1306_BLACK);
+ stat_area_intialised = true;
+ }
+
+ draw_cable_icon(3, 8);
+ draw_bt_icon(3, 30);
+ draw_lora_icon(45, 8);
+ draw_mw_icon(45, 30);
+ draw_battery_bars(4, 58);
+ draw_quality_bars(28, 56);
+ draw_signal_bars(44, 56);
+ if (radio_online) {
+ draw_waterfall(27, 4);
+ }
+ }
+}
+
+void update_stat_area() {
+ if (eeprom_ok && !firmware_update_mode && !console_active) {
+
+ draw_stat_area();
+ if (disp_mode == DISP_MODE_PORTRAIT) {
+ drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ } else if (disp_mode == DISP_MODE_LANDSCAPE) {
+ drawBitmap(p_as_x+2, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ if (device_init_done && !disp_ext_fb) drawLine(p_as_x, 0, p_as_x, 64, SSD1306_WHITE);
+ }
+
+ } else {
+ if (firmware_update_mode) {
+ drawBitmap(p_as_x, p_as_y, bm_updating, stat_area.width(), stat_area.height(), SSD1306_BLACK, SSD1306_WHITE);
+ } else if (console_active && device_init_done) {
+ drawBitmap(p_as_x, p_as_y, bm_console, stat_area.width(), stat_area.height(), SSD1306_BLACK, SSD1306_WHITE);
+ if (disp_mode == DISP_MODE_LANDSCAPE) {
+ drawLine(p_as_x, 0, p_as_x, 64, SSD1306_WHITE);
+ }
+ }
+ }
+}
+
+#define START_PAGE 0
+const uint8_t pages = 3;
+uint8_t disp_page = START_PAGE;
+extern char bt_devname[11];
+extern char bt_dh[16];
+void draw_disp_area() {
+ if (!device_init_done || firmware_update_mode) {
+ uint8_t p_by = 37;
+ if (disp_mode == DISP_MODE_LANDSCAPE || firmware_update_mode) {
+ p_by = 18;
+ disp_area.fillRect(0, 0, disp_area.width(), disp_area.height(), SSD1306_BLACK);
+ }
+ if (!device_init_done) disp_area.drawBitmap(0, p_by, bm_boot, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ if (firmware_update_mode) disp_area.drawBitmap(0, p_by, bm_fw_update, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ if (!disp_ext_fb or bt_ssp_pin != 0) {
+ if (radio_online && display_diagnostics) {
+ disp_area.fillRect(0,8,disp_area.width(),37, SSD1306_BLACK); disp_area.fillRect(0,37,disp_area.width(),27, SSD1306_WHITE);
+ disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1);
+
+ disp_area.setCursor(2, 13);
+ disp_area.print("On");
+ disp_area.setCursor(14, 13);
+ disp_area.print("@");
+ disp_area.setCursor(21, 13);
+ disp_area.printf("%.1fKbps", (float)lora_bitrate/1000.0);
+
+ //disp_area.setCursor(31, 23-1);
+ disp_area.setCursor(2, 23-1);
+ disp_area.print("Airtime:");
+
+ disp_area.setCursor(11, 33-1);
+ if (total_channel_util < 0.099) {
+ //disp_area.printf("%.1f%%", total_channel_util*100.0);
+ disp_area.printf("%.1f%%", airtime*100.0);
+ } else {
+ //disp_area.printf("%.0f%%", total_channel_util*100.0);
+ disp_area.printf("%.0f%%", airtime*100.0);
+ }
+ disp_area.drawBitmap(2, 26-1, bm_hg_low, 5, 9, SSD1306_WHITE, SSD1306_BLACK);
+
+ disp_area.setCursor(32+11, 33-1);
+ if (longterm_channel_util < 0.099) {
+ //disp_area.printf("%.1f%%", longterm_channel_util*100.0);
+ disp_area.printf("%.1f%%", longterm_airtime*100.0);
+ } else {
+ //disp_area.printf("%.0f%%", longterm_channel_util*100.0);
+ disp_area.printf("%.0f%%", longterm_airtime*100.0);
+ }
+ disp_area.drawBitmap(32+2, 26-1, bm_hg_high, 5, 9, SSD1306_WHITE, SSD1306_BLACK);
+
+
+ disp_area.setTextColor(SSD1306_BLACK);
+ disp_area.setCursor(2, 46);
+ disp_area.print("Channel");
+ disp_area.setCursor(38, 46);
+ disp_area.print("Load:");
+
+ disp_area.setCursor(11, 57);
+ if (total_channel_util < 0.099) {
+ //disp_area.printf("%.1f%%", airtime*100.0);
+ disp_area.printf("%.1f%%", total_channel_util*100.0);
+ } else {
+ //disp_area.printf("%.0f%%", airtime*100.0);
+ disp_area.printf("%.0f%%", total_channel_util*100.0);
+ }
+ disp_area.drawBitmap(2, 50, bm_hg_low, 5, 9, SSD1306_BLACK, SSD1306_WHITE);
+
+ disp_area.setCursor(32+11, 57);
+ if (longterm_channel_util < 0.099) {
+ //disp_area.printf("%.1f%%", longterm_airtime*100.0);
+ disp_area.printf("%.1f%%", longterm_channel_util*100.0);
+ } else {
+ //disp_area.printf("%.0f%%", longterm_airtime*100.0);
+ disp_area.printf("%.0f%%", longterm_channel_util*100.0);
+ }
+ disp_area.drawBitmap(32+2, 50, bm_hg_high, 5, 9, SSD1306_BLACK, SSD1306_WHITE);
+
+ } else {
+ if (device_signatures_ok()) { disp_area.drawBitmap(0, 0, bm_def_lc, disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); }
+ else { disp_area.drawBitmap(0, 0, bm_def, disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); }
+
+ disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(2);
+ disp_area.fillRect(0, 20, disp_area.width(), 17, SSD1306_BLACK); uint8_t ofsc = 0;
+ if ((bt_dh[14] & 0b00001111) == 0x01) { ofsc += 8; }
+ if ((bt_dh[14] >> 4) == 0x01) { ofsc += 8; }
+ if ((bt_dh[15] & 0b00001111) == 0x01) { ofsc += 8; }
+ if ((bt_dh[15] >> 4) == 0x01) { ofsc += 8; }
+ disp_area.setCursor(17+ofsc, 32); disp_area.printf("%02X%02X", bt_dh[14], bt_dh[15]);
+ }
+
+ if (!hw_ready || radio_error || !device_firmware_ok()) {
+ if (!device_firmware_ok()) {
+ disp_area.drawBitmap(0, 37, bm_fw_corrupt, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ if (!modem_installed) {
+ disp_area.drawBitmap(0, 37, bm_no_radio, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ disp_area.drawBitmap(0, 37, bm_conf_missing, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ }
+ } else if (bt_state == BT_STATE_PAIRING and bt_ssp_pin != 0) {
+ char *pin_str = (char*)malloc(DISP_PIN_SIZE+1);
+ sprintf(pin_str, "%06d", bt_ssp_pin);
+
+ disp_area.drawBitmap(0, 37, bm_pairing, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ for (int i = 0; i < DISP_PIN_SIZE; i++) {
+ uint8_t numeric = pin_str[i]-48;
+ uint8_t offset = numeric*5;
+ disp_area.drawBitmap(7+9*i, 37+16, bm_n_uh+offset, 8, 5, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ free(pin_str);
+ } else {
+ if (millis()-last_page_flip >= page_interval) {
+ disp_page = (++disp_page%pages);
+ last_page_flip = millis();
+ if (not community_fw and disp_page == 0) disp_page = 1;
+ }
+
+ if (radio_online) {
+ if (!display_diagnostics) {
+ disp_area.drawBitmap(0, 37, bm_online, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ } else {
+ if (disp_page == 0) {
+ if (true || device_signatures_ok()) {
+ disp_area.drawBitmap(0, 37, bm_checks, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ disp_area.drawBitmap(0, 37, bm_nfr, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ } else if (disp_page == 1) {
+ if (!console_active) {
+ disp_area.drawBitmap(0, 37, bm_hwok, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ } else {
+ disp_area.drawBitmap(0, 37, bm_console_active, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ } else if (disp_page == 2) {
+ disp_area.drawBitmap(0, 37, bm_version, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
+ char *v_str = (char*)malloc(3+1);
+ sprintf(v_str, "%01d%02d", MAJ_VERS, MIN_VERS);
+ for (int i = 0; i < 3; i++) {
+ uint8_t numeric = v_str[i]-48; uint8_t bm_offset = numeric*5;
+ uint8_t dxp = 20;
+ if (i == 1) dxp += 9*1+4;
+ if (i == 2) dxp += 9*2+4;
+ disp_area.drawBitmap(dxp, 37+16, bm_n_uh+bm_offset, 8, 5, SSD1306_WHITE, SSD1306_BLACK);
+ }
+ free(v_str);
+ disp_area.drawLine(27, 37+19, 28, 37+19, SSD1306_BLACK);
+ disp_area.drawLine(27, 37+20, 28, 37+20, SSD1306_BLACK);
+ }
+ }
+ }
+ } else {
+ disp_area.drawBitmap(0, 0, fb, disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ }
+ }
+}
+
+void update_disp_area() {
+ draw_disp_area();
+
+ drawBitmap(p_ad_x, p_ad_y, disp_area.getBuffer(), disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ if (disp_mode == DISP_MODE_LANDSCAPE) {
+ if (device_init_done && !firmware_update_mode && !disp_ext_fb) {
+ drawLine(0, 0, 0, 63, SSD1306_WHITE);
+ }
+ }
+}
+
+void display_recondition() {
+ #if PLATFORM == PLATFORM_ESP32
+ for (uint8_t iy = 0; iy < disp_area.height(); iy++) {
+ unsigned char rand_seg [] = {random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF)};
+ stat_area.drawBitmap(0, iy, rand_seg, 64, 1, SSD1306_WHITE, SSD1306_BLACK);
+ disp_area.drawBitmap(0, iy, rand_seg, 64, 1, SSD1306_WHITE, SSD1306_BLACK);
+ }
+
+ drawBitmap(p_ad_x, p_ad_y, disp_area.getBuffer(), disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ if (disp_mode == DISP_MODE_PORTRAIT) {
+ drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ } else if (disp_mode == DISP_MODE_LANDSCAPE) {
+ drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
+ }
+ #endif
+}
+
+bool epd_blanked = false;
+#if BOARD_MODEL == BOARD_TECHO
+ void epd_blank(bool full_update = true) {
+ display.setFullWindow();
+ display.fillScreen(SSD1306_WHITE);
+ display.display(full_update);
+ }
+
+ void epd_black(bool full_update = true) {
+ display.setFullWindow();
+ display.fillScreen(SSD1306_BLACK);
+ display.display(full_update);
+ }
+#endif
+
+void update_display(bool blank = false) {
+ display_updating = true;
+ if (blank == true) {
+ last_disp_update = millis()-disp_update_interval-1;
+ } else {
+ if (display_blanking_enabled && millis()-last_unblank_event >= display_blanking_timeout) {
+ blank = true;
+ display_blanked = true;
+ if (display_intensity != 0) {
+ display_unblank_intensity = display_intensity;
+ }
+ display_intensity = 0;
+ } else {
+ display_blanked = false;
+ if (display_unblank_intensity != 0x00) {
+ display_intensity = display_unblank_intensity;
+ display_unblank_intensity = 0x00;
+ }
+ }
+ }
+
+ if (blank) {
+ if (millis()-last_disp_update >= disp_update_interval) {
+ if (display_contrast != display_intensity) {
+ display_contrast = display_intensity;
+ set_contrast(&display, display_contrast);
+ }
+
+ #if BOARD_MODEL == BOARD_TECHO
+ if (!epd_blanked) {
+ epd_blank();
+ epd_blanked = true;
+ }
+ #elif BOARD_MODEL == BOARD_VME213
+ if (!epd_blanked) {
+ vme213_fillScreen(SSD1306_WHITE);
+ vme213_display.update(vme213_displayBuffer, LCMEN2R13EFC1::UPDATE_FULL);
+ epd_blanked = true;
+ }
+ #endif
+
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ display.clear();
+ display.display();
+ #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO
+ display.clearDisplay();
+ display.display();
+ #else
+ // TODO: Clear screen
+ #endif
+
+ last_disp_update = millis();
+ }
+
+ } else {
+ if (millis()-last_disp_update >= disp_update_interval) {
+ uint32_t current = millis();
+ if (display_contrast != display_intensity) {
+ display_contrast = display_intensity;
+ set_contrast(&display, display_contrast);
+ }
+
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ display.clear();
+ #elif BOARD_MODEL == BOARD_VME213
+ // E-Ink buffer will be redrawn completely
+ #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO
+ display.clearDisplay();
+ #endif
+
+ if (recondition_display) {
+ disp_target_fps = 30;
+ disp_update_interval = 1000/disp_target_fps;
+ display_recondition();
+ } else {
+ #if BOARD_MODEL == BOARD_TECHO
+ display.setFullWindow();
+ display.fillScreen(SSD1306_WHITE);
+ #elif BOARD_MODEL == BOARD_VME213
+ // Render RNode UI to E-Ink buffer
+ vme213_renderStatus();
+ #endif
+
+ update_stat_area();
+ update_disp_area();
+ }
+
+ #if BOARD_MODEL == BOARD_TECHO
+ if (current-last_epd_refresh >= epd_update_interval) {
+ if (current-last_epd_full_refresh >= REFRESH_PERIOD) { display.display(false); last_epd_full_refresh = millis(); }
+ else { display.display(true); }
+ last_epd_refresh = millis();
+ epd_blanked = false;
+ }
+ #elif BOARD_MODEL == BOARD_VME213
+ if (current-last_epd_refresh >= epd_update_interval) {
+ // Decide refresh type: 10 FAST : 1 FULL
+ LCMEN2R13EFC1::UpdateType refreshType;
+ if (vme213_fast_refresh_count >= VME213_REFRESH_RATIO || current-last_epd_full_refresh >= REFRESH_PERIOD) {
+ refreshType = LCMEN2R13EFC1::UPDATE_FULL;
+ vme213_fast_refresh_count = 0;
+ last_epd_full_refresh = millis();
+ } else {
+ refreshType = LCMEN2R13EFC1::UPDATE_FAST;
+ vme213_fast_refresh_count++;
+ }
+
+ // Perform E-Ink update
+ vme213_display.update(vme213_displayBuffer, refreshType);
+ last_epd_refresh = millis();
+ epd_blanked = false;
+ }
+ #elif BOARD_MODEL != BOARD_TDECK
+ display.display();
+ #endif
+
+ last_disp_update = millis();
+ }
+ }
+ display_updating = false;
+}
+
+void display_unblank() {
+ last_unblank_event = millis();
+}
+
+void ext_fb_enable() {
+ disp_ext_fb = true;
+}
+
+void ext_fb_disable() {
+ disp_ext_fb = false;
+}
diff --git a/RNode_Firmware/Fonts/Org_01.h b/RNode_Firmware/Fonts/Org_01.h
new file mode 100644
index 0000000..9b80258
--- /dev/null
+++ b/RNode_Firmware/Fonts/Org_01.h
@@ -0,0 +1,131 @@
+#pragma once
+#include
+
+// Org_v01 by Orgdot (www.orgdot.com/aliasfonts). A tiny,
+// stylized font with all characters within a 6 pixel height.
+
+const uint8_t Org_01Bitmaps[] PROGMEM = {
+ 0xE8, 0xA0, 0x57, 0xD5, 0xF5, 0x00, 0xFD, 0x3E, 0x5F, 0x80, 0x88, 0x88,
+ 0x88, 0x80, 0xF4, 0xBF, 0x2E, 0x80, 0x80, 0x6A, 0x40, 0x95, 0x80, 0xAA,
+ 0x80, 0x5D, 0x00, 0xC0, 0xF0, 0x80, 0x08, 0x88, 0x88, 0x00, 0xFC, 0x63,
+ 0x1F, 0x80, 0xF8, 0xF8, 0x7F, 0x0F, 0x80, 0xF8, 0x7E, 0x1F, 0x80, 0x8C,
+ 0x7E, 0x10, 0x80, 0xFC, 0x3E, 0x1F, 0x80, 0xFC, 0x3F, 0x1F, 0x80, 0xF8,
+ 0x42, 0x10, 0x80, 0xFC, 0x7F, 0x1F, 0x80, 0xFC, 0x7E, 0x1F, 0x80, 0x90,
+ 0xB0, 0x2A, 0x22, 0xF0, 0xF0, 0x88, 0xA8, 0xF8, 0x4E, 0x02, 0x00, 0xFD,
+ 0x6F, 0x0F, 0x80, 0xFC, 0x7F, 0x18, 0x80, 0xF4, 0x7D, 0x1F, 0x00, 0xFC,
+ 0x21, 0x0F, 0x80, 0xF4, 0x63, 0x1F, 0x00, 0xFC, 0x3F, 0x0F, 0x80, 0xFC,
+ 0x3F, 0x08, 0x00, 0xFC, 0x2F, 0x1F, 0x80, 0x8C, 0x7F, 0x18, 0x80, 0xF9,
+ 0x08, 0x4F, 0x80, 0x78, 0x85, 0x2F, 0x80, 0x8D, 0xB1, 0x68, 0x80, 0x84,
+ 0x21, 0x0F, 0x80, 0xFD, 0x6B, 0x5A, 0x80, 0xFC, 0x63, 0x18, 0x80, 0xFC,
+ 0x63, 0x1F, 0x80, 0xFC, 0x7F, 0x08, 0x00, 0xFC, 0x63, 0x3F, 0x80, 0xFC,
+ 0x7F, 0x29, 0x00, 0xFC, 0x3E, 0x1F, 0x80, 0xF9, 0x08, 0x42, 0x00, 0x8C,
+ 0x63, 0x1F, 0x80, 0x8C, 0x62, 0xA2, 0x00, 0xAD, 0x6B, 0x5F, 0x80, 0x8A,
+ 0x88, 0xA8, 0x80, 0x8C, 0x54, 0x42, 0x00, 0xF8, 0x7F, 0x0F, 0x80, 0xEA,
+ 0xC0, 0x82, 0x08, 0x20, 0x80, 0xD5, 0xC0, 0x54, 0xF8, 0x80, 0xF1, 0xFF,
+ 0x8F, 0x99, 0xF0, 0xF8, 0x8F, 0x1F, 0x99, 0xF0, 0xFF, 0x8F, 0x6B, 0xA4,
+ 0xF9, 0x9F, 0x10, 0x8F, 0x99, 0x90, 0xF0, 0x55, 0xC0, 0x8A, 0xF9, 0x90,
+ 0xF8, 0xFD, 0x63, 0x10, 0xF9, 0x99, 0xF9, 0x9F, 0xF9, 0x9F, 0x80, 0xF9,
+ 0x9F, 0x20, 0xF8, 0x88, 0x47, 0x1F, 0x27, 0xC8, 0x42, 0x00, 0x99, 0x9F,
+ 0x99, 0x97, 0x8C, 0x6B, 0xF0, 0x96, 0x69, 0x99, 0x9F, 0x10, 0x2E, 0x8F,
+ 0x2B, 0x22, 0xF8, 0x89, 0xA8, 0x0F, 0xE0};
+
+const GFXglyph Org_01Glyphs[] PROGMEM = {{0, 0, 0, 6, 0, 1}, // 0x20 ' '
+ {0, 1, 5, 2, 0, -4}, // 0x21 '!'
+ {1, 3, 1, 4, 0, -4}, // 0x22 '"'
+ {2, 5, 5, 6, 0, -4}, // 0x23 '#'
+ {6, 5, 5, 6, 0, -4}, // 0x24 '$'
+ {10, 5, 5, 6, 0, -4}, // 0x25 '%'
+ {14, 5, 5, 6, 0, -4}, // 0x26 '&'
+ {18, 1, 1, 2, 0, -4}, // 0x27 '''
+ {19, 2, 5, 3, 0, -4}, // 0x28 '('
+ {21, 2, 5, 3, 0, -4}, // 0x29 ')'
+ {23, 3, 3, 4, 0, -3}, // 0x2A '*'
+ {25, 3, 3, 4, 0, -3}, // 0x2B '+'
+ {27, 1, 2, 2, 0, 0}, // 0x2C ','
+ {28, 4, 1, 5, 0, -2}, // 0x2D '-'
+ {29, 1, 1, 2, 0, 0}, // 0x2E '.'
+ {30, 5, 5, 6, 0, -4}, // 0x2F '/'
+ {34, 5, 5, 6, 0, -4}, // 0x30 '0'
+ {38, 1, 5, 2, 0, -4}, // 0x31 '1'
+ {39, 5, 5, 6, 0, -4}, // 0x32 '2'
+ {43, 5, 5, 6, 0, -4}, // 0x33 '3'
+ {47, 5, 5, 6, 0, -4}, // 0x34 '4'
+ {51, 5, 5, 6, 0, -4}, // 0x35 '5'
+ {55, 5, 5, 6, 0, -4}, // 0x36 '6'
+ {59, 5, 5, 6, 0, -4}, // 0x37 '7'
+ {63, 5, 5, 6, 0, -4}, // 0x38 '8'
+ {67, 5, 5, 6, 0, -4}, // 0x39 '9'
+ {71, 1, 4, 2, 0, -3}, // 0x3A ':'
+ {72, 1, 4, 2, 0, -3}, // 0x3B ';'
+ {73, 3, 5, 4, 0, -4}, // 0x3C '<'
+ {75, 4, 3, 5, 0, -3}, // 0x3D '='
+ {77, 3, 5, 4, 0, -4}, // 0x3E '>'
+ {79, 5, 5, 6, 0, -4}, // 0x3F '?'
+ {83, 5, 5, 6, 0, -4}, // 0x40 '@'
+ {87, 5, 5, 6, 0, -4}, // 0x41 'A'
+ {91, 5, 5, 6, 0, -4}, // 0x42 'B'
+ {95, 5, 5, 6, 0, -4}, // 0x43 'C'
+ {99, 5, 5, 6, 0, -4}, // 0x44 'D'
+ {103, 5, 5, 6, 0, -4}, // 0x45 'E'
+ {107, 5, 5, 6, 0, -4}, // 0x46 'F'
+ {111, 5, 5, 6, 0, -4}, // 0x47 'G'
+ {115, 5, 5, 6, 0, -4}, // 0x48 'H'
+ {119, 5, 5, 6, 0, -4}, // 0x49 'I'
+ {123, 5, 5, 6, 0, -4}, // 0x4A 'J'
+ {127, 5, 5, 6, 0, -4}, // 0x4B 'K'
+ {131, 5, 5, 6, 0, -4}, // 0x4C 'L'
+ {135, 5, 5, 6, 0, -4}, // 0x4D 'M'
+ {139, 5, 5, 6, 0, -4}, // 0x4E 'N'
+ {143, 5, 5, 6, 0, -4}, // 0x4F 'O'
+ {147, 5, 5, 6, 0, -4}, // 0x50 'P'
+ {151, 5, 5, 6, 0, -4}, // 0x51 'Q'
+ {155, 5, 5, 6, 0, -4}, // 0x52 'R'
+ {159, 5, 5, 6, 0, -4}, // 0x53 'S'
+ {163, 5, 5, 6, 0, -4}, // 0x54 'T'
+ {167, 5, 5, 6, 0, -4}, // 0x55 'U'
+ {171, 5, 5, 6, 0, -4}, // 0x56 'V'
+ {175, 5, 5, 6, 0, -4}, // 0x57 'W'
+ {179, 5, 5, 6, 0, -4}, // 0x58 'X'
+ {183, 5, 5, 6, 0, -4}, // 0x59 'Y'
+ {187, 5, 5, 6, 0, -4}, // 0x5A 'Z'
+ {191, 2, 5, 3, 0, -4}, // 0x5B '['
+ {193, 5, 5, 6, 0, -4}, // 0x5C '\'
+ {197, 2, 5, 3, 0, -4}, // 0x5D ']'
+ {199, 3, 2, 4, 0, -4}, // 0x5E '^'
+ {200, 5, 1, 6, 0, 1}, // 0x5F '_'
+ {201, 1, 1, 2, 0, -4}, // 0x60 '`'
+ {202, 4, 4, 5, 0, -3}, // 0x61 'a'
+ {204, 4, 5, 5, 0, -4}, // 0x62 'b'
+ {207, 4, 4, 5, 0, -3}, // 0x63 'c'
+ {209, 4, 5, 5, 0, -4}, // 0x64 'd'
+ {212, 4, 4, 5, 0, -3}, // 0x65 'e'
+ {214, 3, 5, 4, 0, -4}, // 0x66 'f'
+ {216, 4, 5, 5, 0, -3}, // 0x67 'g'
+ {219, 4, 5, 5, 0, -4}, // 0x68 'h'
+ {222, 1, 4, 2, 0, -3}, // 0x69 'i'
+ {223, 2, 5, 3, 0, -3}, // 0x6A 'j'
+ {225, 4, 5, 5, 0, -4}, // 0x6B 'k'
+ {228, 1, 5, 2, 0, -4}, // 0x6C 'l'
+ {229, 5, 4, 6, 0, -3}, // 0x6D 'm'
+ {232, 4, 4, 5, 0, -3}, // 0x6E 'n'
+ {234, 4, 4, 5, 0, -3}, // 0x6F 'o'
+ {236, 4, 5, 5, 0, -3}, // 0x70 'p'
+ {239, 4, 5, 5, 0, -3}, // 0x71 'q'
+ {242, 4, 4, 5, 0, -3}, // 0x72 'r'
+ {244, 4, 4, 5, 0, -3}, // 0x73 's'
+ {246, 5, 5, 6, 0, -4}, // 0x74 't'
+ {250, 4, 4, 5, 0, -3}, // 0x75 'u'
+ {252, 4, 4, 5, 0, -3}, // 0x76 'v'
+ {254, 5, 4, 6, 0, -3}, // 0x77 'w'
+ {257, 4, 4, 5, 0, -3}, // 0x78 'x'
+ {259, 4, 5, 5, 0, -3}, // 0x79 'y'
+ {262, 4, 4, 5, 0, -3}, // 0x7A 'z'
+ {264, 3, 5, 4, 0, -4}, // 0x7B '{'
+ {266, 1, 5, 2, 0, -4}, // 0x7C '|'
+ {267, 3, 5, 4, 0, -4}, // 0x7D '}'
+ {269, 5, 3, 6, 0, -3}}; // 0x7E '~'
+
+const GFXfont Org_01 PROGMEM = {(uint8_t *)Org_01Bitmaps,
+ (GFXglyph *)Org_01Glyphs, 0x20, 0x7E, 7};
+
+// Approx. 943 bytes
diff --git a/RNode_Firmware/Fonts/PicoPixel.h b/RNode_Firmware/Fonts/PicoPixel.h
new file mode 100644
index 0000000..01c79d5
--- /dev/null
+++ b/RNode_Firmware/Fonts/PicoPixel.h
@@ -0,0 +1,123 @@
+#pragma once
+#include
+
+// Picopixel by Sebastian Weber. A tiny font
+// with all characters within a 6 pixel height.
+
+const uint8_t PicopixelBitmaps[] PROGMEM = {
+ 0xE8, 0xB4, 0x57, 0xD5, 0xF5, 0x00, 0x4E, 0x3E, 0x80, 0xA5, 0x4A, 0x4A,
+ 0x5A, 0x50, 0xC0, 0x6A, 0x40, 0x95, 0x80, 0xAA, 0x80, 0x5D, 0x00, 0x60,
+ 0xE0, 0x80, 0x25, 0x48, 0x56, 0xD4, 0x75, 0x40, 0xC5, 0x4E, 0xC5, 0x1C,
+ 0x97, 0x92, 0xF3, 0x1C, 0x53, 0x54, 0xE5, 0x48, 0x55, 0x54, 0x55, 0x94,
+ 0xA0, 0x46, 0x64, 0xE3, 0x80, 0x98, 0xC5, 0x04, 0x56, 0xC6, 0x57, 0xDA,
+ 0xD7, 0x5C, 0x72, 0x46, 0xD6, 0xDC, 0xF3, 0xCE, 0xF3, 0x48, 0x72, 0xD4,
+ 0xB7, 0xDA, 0xF8, 0x24, 0xD4, 0xBB, 0x5A, 0x92, 0x4E, 0x8E, 0xEB, 0x58,
+ 0x80, 0x9D, 0xB9, 0x90, 0x56, 0xD4, 0xD7, 0x48, 0x56, 0xD4, 0x40, 0xD7,
+ 0x5A, 0x71, 0x1C, 0xE9, 0x24, 0xB6, 0xD4, 0xB6, 0xA4, 0x8C, 0x6B, 0x55,
+ 0x00, 0xB5, 0x5A, 0xB5, 0x24, 0xE5, 0x4E, 0xEA, 0xC0, 0x91, 0x12, 0xD5,
+ 0xC0, 0x54, 0xF0, 0x90, 0xC7, 0xF0, 0x93, 0x5E, 0x71, 0x80, 0x25, 0xDE,
+ 0x5E, 0x30, 0x6E, 0x80, 0x77, 0x9C, 0x93, 0x5A, 0xB8, 0x45, 0x60, 0x92,
+ 0xEA, 0xAA, 0x40, 0xD5, 0x6A, 0xD6, 0x80, 0x55, 0x00, 0xD7, 0x40, 0x75,
+ 0x90, 0xE8, 0x71, 0xE0, 0xBA, 0x40, 0xB5, 0x80, 0xB5, 0x00, 0x8D, 0x54,
+ 0xAA, 0x80, 0xAC, 0xE0, 0xE5, 0x70, 0x6A, 0x26, 0xFC, 0xC8, 0xAC, 0x5A};
+
+const GFXglyph PicopixelGlyphs[] PROGMEM = {{0, 0, 0, 2, 0, 1}, // 0x20 ' '
+ {0, 1, 5, 2, 0, -4}, // 0x21 '!'
+ {1, 3, 2, 4, 0, -4}, // 0x22 '"'
+ {2, 5, 5, 6, 0, -4}, // 0x23 '#'
+ {6, 3, 6, 4, 0, -4}, // 0x24 '$'
+ {9, 3, 5, 4, 0, -4}, // 0x25 '%'
+ {11, 4, 5, 5, 0, -4}, // 0x26 '&'
+ {14, 1, 2, 2, 0, -4}, // 0x27 '''
+ {15, 2, 5, 3, 0, -4}, // 0x28 '('
+ {17, 2, 5, 3, 0, -4}, // 0x29 ')'
+ {19, 3, 3, 4, 0, -3}, // 0x2A '*'
+ {21, 3, 3, 4, 0, -3}, // 0x2B '+'
+ {23, 2, 2, 3, 0, 0}, // 0x2C ','
+ {24, 3, 1, 4, 0, -2}, // 0x2D '-'
+ {25, 1, 1, 2, 0, 0}, // 0x2E '.'
+ {26, 3, 5, 4, 0, -4}, // 0x2F '/'
+ {28, 3, 5, 4, 0, -4}, // 0x30 '0'
+ {30, 2, 5, 3, 0, -4}, // 0x31 '1'
+ {32, 3, 5, 4, 0, -4}, // 0x32 '2'
+ {34, 3, 5, 4, 0, -4}, // 0x33 '3'
+ {36, 3, 5, 4, 0, -4}, // 0x34 '4'
+ {38, 3, 5, 4, 0, -4}, // 0x35 '5'
+ {40, 3, 5, 4, 0, -4}, // 0x36 '6'
+ {42, 3, 5, 4, 0, -4}, // 0x37 '7'
+ {44, 3, 5, 4, 0, -4}, // 0x38 '8'
+ {46, 3, 5, 4, 0, -4}, // 0x39 '9'
+ {48, 1, 3, 2, 0, -3}, // 0x3A ':'
+ {49, 2, 4, 3, 0, -3}, // 0x3B ';'
+ {50, 2, 3, 3, 0, -3}, // 0x3C '<'
+ {51, 3, 3, 4, 0, -3}, // 0x3D '='
+ {53, 2, 3, 3, 0, -3}, // 0x3E '>'
+ {54, 3, 5, 4, 0, -4}, // 0x3F '?'
+ {56, 3, 5, 4, 0, -4}, // 0x40 '@'
+ {58, 3, 5, 4, 0, -4}, // 0x41 'A'
+ {60, 3, 5, 4, 0, -4}, // 0x42 'B'
+ {62, 3, 5, 4, 0, -4}, // 0x43 'C'
+ {64, 3, 5, 4, 0, -4}, // 0x44 'D'
+ {66, 3, 5, 4, 0, -4}, // 0x45 'E'
+ {68, 3, 5, 4, 0, -4}, // 0x46 'F'
+ {70, 3, 5, 4, 0, -4}, // 0x47 'G'
+ {72, 3, 5, 4, 0, -4}, // 0x48 'H'
+ {74, 1, 5, 2, 0, -4}, // 0x49 'I'
+ {75, 3, 5, 4, 0, -4}, // 0x4A 'J'
+ {77, 3, 5, 4, 0, -4}, // 0x4B 'K'
+ {79, 3, 5, 4, 0, -4}, // 0x4C 'L'
+ {81, 5, 5, 6, 0, -4}, // 0x4D 'M'
+ {85, 4, 5, 5, 0, -4}, // 0x4E 'N'
+ {88, 3, 5, 4, 0, -4}, // 0x4F 'O'
+ {90, 3, 5, 4, 0, -4}, // 0x50 'P'
+ {92, 3, 6, 4, 0, -4}, // 0x51 'Q'
+ {95, 3, 5, 4, 0, -4}, // 0x52 'R'
+ {97, 3, 5, 4, 0, -4}, // 0x53 'S'
+ {99, 3, 5, 4, 0, -4}, // 0x54 'T'
+ {101, 3, 5, 4, 0, -4}, // 0x55 'U'
+ {103, 3, 5, 4, 0, -4}, // 0x56 'V'
+ {105, 5, 5, 6, 0, -4}, // 0x57 'W'
+ {109, 3, 5, 4, 0, -4}, // 0x58 'X'
+ {111, 3, 5, 4, 0, -4}, // 0x59 'Y'
+ {113, 3, 5, 4, 0, -4}, // 0x5A 'Z'
+ {115, 2, 5, 3, 0, -4}, // 0x5B '['
+ {117, 3, 5, 4, 0, -4}, // 0x5C '\'
+ {119, 2, 5, 3, 0, -4}, // 0x5D ']'
+ {121, 3, 2, 4, 0, -4}, // 0x5E '^'
+ {122, 4, 1, 4, 0, 1}, // 0x5F '_'
+ {123, 2, 2, 3, 0, -4}, // 0x60 '`'
+ {124, 3, 4, 4, 0, -3}, // 0x61 'a'
+ {126, 3, 5, 4, 0, -4}, // 0x62 'b'
+ {128, 3, 3, 4, 0, -2}, // 0x63 'c'
+ {130, 3, 5, 4, 0, -4}, // 0x64 'd'
+ {132, 3, 4, 4, 0, -3}, // 0x65 'e'
+ {134, 2, 5, 3, 0, -4}, // 0x66 'f'
+ {136, 3, 5, 4, 0, -3}, // 0x67 'g'
+ {138, 3, 5, 4, 0, -4}, // 0x68 'h'
+ {140, 1, 5, 2, 0, -4}, // 0x69 'i'
+ {141, 2, 6, 3, 0, -4}, // 0x6A 'j'
+ {143, 3, 5, 4, 0, -4}, // 0x6B 'k'
+ {145, 2, 5, 3, 0, -4}, // 0x6C 'l'
+ {147, 5, 3, 6, 0, -2}, // 0x6D 'm'
+ {149, 3, 3, 4, 0, -2}, // 0x6E 'n'
+ {151, 3, 3, 4, 0, -2}, // 0x6F 'o'
+ {153, 3, 4, 4, 0, -2}, // 0x70 'p'
+ {155, 3, 4, 4, 0, -2}, // 0x71 'q'
+ {157, 2, 3, 3, 0, -2}, // 0x72 'r'
+ {158, 3, 4, 4, 0, -3}, // 0x73 's'
+ {160, 2, 5, 3, 0, -4}, // 0x74 't'
+ {162, 3, 3, 4, 0, -2}, // 0x75 'u'
+ {164, 3, 3, 4, 0, -2}, // 0x76 'v'
+ {166, 5, 3, 6, 0, -2}, // 0x77 'w'
+ {168, 3, 3, 4, 0, -2}, // 0x78 'x'
+ {170, 3, 4, 4, 0, -2}, // 0x79 'y'
+ {172, 3, 4, 4, 0, -3}, // 0x7A 'z'
+ {174, 3, 5, 4, 0, -4}, // 0x7B '{'
+ {176, 1, 6, 2, 0, -4}, // 0x7C '|'
+ {177, 3, 5, 4, 0, -4}, // 0x7D '}'
+ {179, 4, 2, 5, 0, -3}}; // 0x7E '~'
+
+const GFXfont Picopixel PROGMEM = {(uint8_t *)PicopixelBitmaps,
+ (GFXglyph *)PicopixelGlyphs, 0x20, 0x7E, 7};
+
+// Approx. 852 bytes
\ No newline at end of file
diff --git a/RNode_Firmware/Framing.h b/RNode_Firmware/Framing.h
new file mode 100644
index 0000000..94f4e37
--- /dev/null
+++ b/RNode_Firmware/Framing.h
@@ -0,0 +1,117 @@
+// 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 .
+
+#ifndef FRAMING_H
+ #define FRAMING_H
+
+ #define FEND 0xC0
+ #define FESC 0xDB
+ #define TFEND 0xDC
+ #define TFESC 0xDD
+
+ #define CMD_UNKNOWN 0xFE
+ #define CMD_DATA 0x00
+ #define CMD_FREQUENCY 0x01
+ #define CMD_BANDWIDTH 0x02
+ #define CMD_TXPOWER 0x03
+ #define CMD_SF 0x04
+ #define CMD_CR 0x05
+ #define CMD_RADIO_STATE 0x06
+ #define CMD_RADIO_LOCK 0x07
+ #define CMD_DETECT 0x08
+ #define CMD_IMPLICIT 0x09
+ #define CMD_LEAVE 0x0A
+ #define CMD_ST_ALOCK 0x0B
+ #define CMD_LT_ALOCK 0x0C
+ #define CMD_PROMISC 0x0E
+ #define CMD_READY 0x0F
+
+ #define CMD_STAT_RX 0x21
+ #define CMD_STAT_TX 0x22
+ #define CMD_STAT_RSSI 0x23
+ #define CMD_STAT_SNR 0x24
+ #define CMD_STAT_CHTM 0x25
+ #define CMD_STAT_PHYPRM 0x26
+ #define CMD_STAT_BAT 0x27
+ #define CMD_STAT_CSMA 0x28
+ #define CMD_STAT_TEMP 0x29
+ #define CMD_BLINK 0x30
+ #define CMD_RANDOM 0x40
+
+ #define CMD_FB_EXT 0x41
+ #define CMD_FB_READ 0x42
+ #define CMD_FB_WRITE 0x43
+ #define CMD_FB_READL 0x44
+ #define CMD_DISP_READ 0x66
+ #define CMD_DISP_INT 0x45
+ #define CMD_DISP_ADDR 0x63
+ #define CMD_DISP_BLNK 0x64
+ #define CMD_DISP_ROT 0x67
+ #define CMD_DISP_RCND 0x68
+ #define CMD_NP_INT 0x65
+ #define CMD_BT_CTRL 0x46
+ #define CMD_BT_UNPAIR 0x70
+ #define CMD_BT_PIN 0x62
+ #define CMD_DIS_IA 0x69
+
+ #define CMD_BOARD 0x47
+ #define CMD_PLATFORM 0x48
+ #define CMD_MCU 0x49
+ #define CMD_FW_VERSION 0x50
+ #define CMD_ROM_READ 0x51
+ #define CMD_ROM_WRITE 0x52
+ #define CMD_CONF_SAVE 0x53
+ #define CMD_CONF_DELETE 0x54
+ #define CMD_DEV_HASH 0x56
+ #define CMD_DEV_SIG 0x57
+ #define CMD_FW_HASH 0x58
+ #define CMD_HASHES 0x60
+ #define CMD_FW_UPD 0x61
+ #define CMD_UNLOCK_ROM 0x59
+ #define ROM_UNLOCK_BYTE 0xF8
+ #define CMD_RESET 0x55
+ #define CMD_RESET_BYTE 0xF8
+
+ #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
+
+ #define RADIO_STATE_OFF 0x00
+ #define RADIO_STATE_ON 0x01
+
+ #define NIBBLE_SEQ 0xF0
+ #define NIBBLE_FLAGS 0x0F
+ #define FLAG_SPLIT 0x01
+ #define SEQ_UNSET 0xFF
+
+ #define CMD_ERROR 0x90
+ #define ERROR_INITRADIO 0x01
+ #define ERROR_TXFAILED 0x02
+ #define ERROR_EEPROM_LOCKED 0x03
+ #define ERROR_QUEUE_FULL 0x04
+ #define ERROR_MEMORY_LOW 0x05
+ #define ERROR_MODEM_TIMEOUT 0x06
+
+ // Serial framing variables
+ size_t frame_len;
+ bool IN_FRAME = false;
+ bool ESCAPE = false;
+ uint8_t command = CMD_UNKNOWN;
+
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/Graphics.h b/RNode_Firmware/Graphics.h
new file mode 100644
index 0000000..3da21f1
--- /dev/null
+++ b/RNode_Firmware/Graphics.h
@@ -0,0 +1,434 @@
+// 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 .
+
+const unsigned char bm_cable [] PROGMEM = {
+ 0x00, 0x00, 0x00, 0x1c, 0x00, 0x38, 0x07, 0xfc, 0x08, 0x38, 0x10, 0x1c, 0x10, 0x00, 0x08, 0x00,
+ 0x07, 0xc0, 0x00, 0x20, 0x00, 0x10, 0x00, 0x10, 0x00, 0x20, 0x07, 0xc0, 0x08, 0x00, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x80, 0x04, 0x43, 0x08, 0x46,
+ 0xf1, 0x8f, 0x02, 0x16, 0x02, 0x23, 0x01, 0x20, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+const unsigned char bm_rf [] PROGMEM = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0xc4,
+ 0x4a, 0xaa, 0x4a, 0xce, 0x6e, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x04, 0x20, 0x01, 0x80, 0x00, 0x00, 0x4e, 0xc4,
+ 0x4a, 0xaa, 0x4a, 0xce, 0x6e, 0xaa, 0x00, 0x00, 0x01, 0x80, 0x04, 0x20, 0x03, 0xc0, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x4e,
+ 0x31, 0x48, 0x61, 0xca, 0x74, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x07, 0xe0, 0x08, 0x10, 0x13, 0xc8, 0x04, 0x20, 0x01, 0x80, 0x00, 0x00, 0x71, 0x4e,
+ 0x31, 0x48, 0x61, 0xca, 0x74, 0x4e, 0x00, 0x00, 0x01, 0x80, 0x04, 0x20, 0x03, 0xc0, 0x00, 0x00
+};
+
+const unsigned char bm_bt [] PROGMEM = {
+ 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x11, 0x40, 0x00, 0x00, 0x05, 0x10, 0x00, 0x00, 0x01, 0x40,
+ 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x05, 0x10, 0x00, 0x00, 0x11, 0x40, 0x00, 0x00, 0x01, 0x00,
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x40, 0x09, 0x20, 0x05, 0x10, 0x03, 0x20, 0x01, 0x40,
+ 0x01, 0x80, 0x01, 0x40, 0x03, 0x20, 0x05, 0x10, 0x09, 0x20, 0x01, 0x40, 0x01, 0x80, 0x01, 0x00,
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x40, 0x09, 0x20, 0x05, 0x10, 0x03, 0x20, 0x01, 0x40,
+ 0x29, 0x94, 0x01, 0x40, 0x03, 0x20, 0x05, 0x10, 0x09, 0x20, 0x01, 0x40, 0x01, 0x80, 0x01, 0x00,
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x40, 0x09, 0x20, 0x05, 0x10, 0x03, 0x20, 0x11, 0x48,
+ 0x29, 0x94, 0x11, 0x48, 0x03, 0x20, 0x05, 0x10, 0x09, 0x20, 0x01, 0x40, 0x01, 0x80, 0x01, 0x00
+};
+
+const unsigned char bm_boot [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xfc, 0x38, 0x66, 0x67, 0x1c, 0x3f, 0xff, 0xff, 0xfc, 0x99, 0xe6, 0x66, 0x4c, 0xff, 0xff,
+ 0xff, 0xfc, 0x98, 0x70, 0xe6, 0x7c, 0x3f, 0xff, 0xff, 0xfc, 0x99, 0xf0, 0xe6, 0x4c, 0xff, 0xff,
+ 0xff, 0xfc, 0x38, 0x79, 0xe7, 0x1c, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x0c, 0x38, 0xe1, 0xc3, 0x33, 0x38, 0x7f, 0xfe, 0x7e, 0x72, 0x64, 0xe7, 0x31, 0x33, 0xff,
+ 0xff, 0x1e, 0x70, 0x61, 0xe7, 0x30, 0x32, 0x7f, 0xff, 0xce, 0x72, 0x61, 0xe7, 0x32, 0x32, 0x7f,
+ 0xfe, 0x1e, 0x72, 0x64, 0xe7, 0x33, 0x38, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_fw_update [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfc, 0x98, 0x70, 0xf1, 0xc3, 0x33, 0x38, 0x7f, 0xfc, 0x99, 0x32, 0x64, 0xe7, 0x31, 0x33, 0xff,
+ 0xfc, 0x98, 0x72, 0x60, 0xe7, 0x30, 0x32, 0x7f, 0xfc, 0x99, 0xf2, 0x64, 0xe7, 0x32, 0x32, 0x7f,
+ 0xfe, 0x39, 0xf0, 0xe4, 0xe7, 0x33, 0x38, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf8, 0x66, 0x1c, 0xe6, 0x73, 0x8e, 0x1c, 0x3f, 0xf9, 0xe6, 0x4c, 0x46, 0x53, 0x26, 0x4c, 0xff,
+ 0xf8, 0x66, 0x1c, 0x06, 0x53, 0x06, 0x1c, 0x3f, 0xf9, 0xe6, 0x1c, 0xa6, 0x03, 0x26, 0x1c, 0xff,
+ 0xf9, 0xe6, 0x4c, 0xe7, 0x27, 0x26, 0x4c, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_console_active [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0x8e, 0x67, 0x0e, 0x39, 0xe1, 0xff,
+ 0xff, 0x93, 0x26, 0x26, 0x7c, 0x99, 0xe7, 0xff, 0xff, 0x9f, 0x26, 0x07, 0x1c, 0x99, 0xe1, 0xff,
+ 0xff, 0x93, 0x26, 0x47, 0xcc, 0x99, 0xe7, 0xff, 0xff, 0xc7, 0x8e, 0x66, 0x1e, 0x38, 0x61, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3c, 0x70, 0xcc, 0xcc, 0x3f, 0xff,
+ 0xff, 0xfc, 0x99, 0x39, 0xcc, 0xcc, 0xff, 0xff, 0xff, 0xfc, 0x19, 0xf9, 0xce, 0x1c, 0x3f, 0xff,
+ 0xff, 0xfc, 0x99, 0x39, 0xce, 0x1c, 0xff, 0xff, 0xff, 0xfc, 0x9c, 0x79, 0xcf, 0x3c, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_updating [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0x71, 0x7f, 0xff,
+ 0xff, 0xff, 0x7f, 0xf5, 0xff, 0x75, 0x7f, 0xff, 0xff, 0xff, 0x7f, 0xf1, 0xff, 0x71, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x17, 0xd4, 0x7f, 0x44, 0x7f, 0xff,
+ 0xff, 0xff, 0x57, 0xd5, 0x7f, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x17, 0xd4, 0x7f, 0x44, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x47, 0x44, 0x71, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0x57, 0x55, 0x75, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x47, 0x44, 0x71, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x44, 0x51, 0x51, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x44, 0x51, 0x51, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x54, 0x45, 0x44, 0x7f, 0xff,
+ 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x14, 0x54, 0x45, 0x44, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff,
+ 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xff, 0xff,
+ 0xff, 0xff, 0x60, 0x00, 0x00, 0x03, 0x7f, 0xff, 0xff, 0xff, 0x30, 0x00, 0x00, 0x07, 0x7f, 0xff,
+ 0xff, 0xff, 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x5c, 0x00, 0x00, 0x1c, 0x7f, 0xff,
+ 0xff, 0xff, 0x56, 0x00, 0x00, 0x35, 0x7f, 0xff, 0xff, 0xff, 0x57, 0x00, 0x00, 0x74, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x44, 0xc0, 0x01, 0xd1, 0x7f, 0xff,
+ 0xff, 0xff, 0x55, 0x60, 0x03, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x44, 0x70, 0x07, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x14, 0x5c, 0x1d, 0x44, 0x7f, 0xff,
+ 0xff, 0xff, 0x55, 0x56, 0x35, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x14, 0x57, 0xe5, 0x44, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0x55, 0x55, 0x55, 0x55, 0x7f, 0xff, 0xff, 0xff, 0x45, 0x44, 0x51, 0x51, 0x7f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff,
+ 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff,
+ 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_version [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0x99, 0x86, 0x1e, 0x19, 0xc7, 0x33, 0xff, 0xff, 0x99, 0x9e, 0x4c, 0xf9, 0x93, 0x13, 0xff,
+ 0xff, 0xc3, 0x86, 0x1e, 0x39, 0x93, 0x03, 0xff, 0xff, 0xc3, 0x9e, 0x1f, 0x99, 0x93, 0x23, 0xff,
+ 0xff, 0xe7, 0x86, 0x4c, 0x39, 0xc7, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_fw_corrupt [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0x30, 0xe7, 0x33, 0x9c, 0x70, 0xe1, 0xff,
+ 0xcf, 0x32, 0x62, 0x32, 0x99, 0x32, 0x67, 0xff, 0xc3, 0x30, 0xe0, 0x32, 0x98, 0x30, 0xe1, 0xff,
+ 0xcf, 0x30, 0xe5, 0x30, 0x19, 0x30, 0xe7, 0xff, 0xcf, 0x32, 0x67, 0x39, 0x39, 0x32, 0x61, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xc7, 0x0e, 0x1c, 0x98, 0x70, 0xfc, 0xff,
+ 0xc9, 0x93, 0x26, 0x4c, 0x99, 0x39, 0xfb, 0x7f, 0xcf, 0x93, 0x0e, 0x1c, 0x98, 0x79, 0xfb, 0x7f,
+ 0xc9, 0x93, 0x0e, 0x1c, 0x99, 0xf9, 0xf7, 0xbf, 0xe3, 0xc7, 0x26, 0x4e, 0x39, 0xf9, 0xf4, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbc, 0xf7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7c, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xfb,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+static unsigned char bm_def[] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb4, 0x61, 0x10, 0x8c, 0x23, 0xc4, 0x3f, 0xff,
+ 0xb5, 0xa7, 0xb7, 0xb5, 0xed, 0xed, 0xbf, 0xff, 0xb5, 0xb9, 0xb4, 0xb4, 0x6d, 0xed, 0xbf, 0xff,
+ 0x85, 0xa1, 0x10, 0xb4, 0x21, 0x44, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe7, 0x1c, 0xfe, 0x7f, 0x8f, 0xf0, 0x00,
+ 0x1f, 0xf7, 0x9d, 0xff, 0x7f, 0x9f, 0xf0, 0x00, 0x1c, 0x77, 0xfd, 0xc7, 0x73, 0xdc, 0x00, 0x00,
+ 0x1f, 0xe7, 0xfd, 0xc7, 0x71, 0xdf, 0x00, 0x00, 0x1f, 0xe7, 0x7d, 0xc7, 0x71, 0xdf, 0x00, 0x00,
+ 0x1c, 0x77, 0x3d, 0xc7, 0x73, 0xdc, 0x00, 0x00, 0x1c, 0x77, 0x1d, 0xff, 0x7f, 0x9f, 0xf0, 0x00,
+ 0x1c, 0x77, 0x1c, 0xfe, 0x7f, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x54,
+ 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x62, 0x24, 0x49, 0x22, 0x4e, 0x44,
+ 0x00, 0x24, 0x93, 0x66, 0xc9, 0x32, 0x44, 0x28, 0x00, 0x20, 0x92, 0xa5, 0x49, 0x2a, 0x44, 0x10,
+ 0x00, 0x24, 0x92, 0x24, 0x49, 0x26, 0x44, 0x10, 0x00, 0x18, 0x62, 0x24, 0x46, 0x22, 0x44, 0x10,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x1c, 0x9c, 0x44, 0x88, 0xc7, 0x1c, 0x00, 0x00, 0x10, 0x92, 0x6c, 0xa9, 0x24, 0x90,
+ 0x00, 0x00, 0x1c, 0x9c, 0x54, 0xa9, 0xe7, 0x1c, 0x00, 0x00, 0x10, 0x94, 0x44, 0xa9, 0x25, 0x10,
+ 0x00, 0x00, 0x10, 0x92, 0x44, 0x51, 0x24, 0x9c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+const unsigned char bm_def_lc [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb4, 0x61, 0x10, 0x8c, 0x23, 0xc4, 0x3f, 0xff,
+ 0xb5, 0xa7, 0xb7, 0xb5, 0xed, 0xed, 0xbf, 0xff, 0xb5, 0xb9, 0xb4, 0xb4, 0x6d, 0xed, 0xbf, 0xff,
+ 0x85, 0xa1, 0x10, 0xb4, 0x21, 0x44, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe7, 0x1c, 0xfe, 0x7f, 0x8f, 0xf0, 0x00,
+ 0x1f, 0xf7, 0x9d, 0xff, 0x7f, 0x9f, 0xf0, 0x00, 0x1c, 0x77, 0xfd, 0xc7, 0x73, 0xdc, 0x00, 0x00,
+ 0x1f, 0xe7, 0xfd, 0xc7, 0x71, 0xdf, 0x00, 0x00, 0x1f, 0xe7, 0x7d, 0xc7, 0x71, 0xdf, 0x00, 0x00,
+ 0x1c, 0x77, 0x3d, 0xc7, 0x73, 0xdc, 0x00, 0x00, 0x1c, 0x77, 0x1d, 0xff, 0x7f, 0x9f, 0xf0, 0x00,
+ 0x1c, 0x77, 0x1c, 0xfe, 0x7f, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x54,
+ 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x8e, 0x39, 0x10, 0x61, 0x88, 0x91, 0x1c,
+ 0x02, 0x49, 0x21, 0x90, 0x92, 0x4d, 0x9b, 0x20, 0x02, 0x4e, 0x39, 0x50, 0x82, 0x4a, 0x95, 0x18,
+ 0x02, 0x48, 0x21, 0x30, 0x92, 0x48, 0x91, 0x04, 0x01, 0x88, 0x39, 0x10, 0x61, 0x88, 0x91, 0x38,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0xc8, 0x8e, 0x73, 0x91, 0x1c, 0x00, 0x00, 0x02, 0x05, 0x10, 0x22, 0x1b, 0x20,
+ 0x00, 0x00, 0x01, 0x82, 0x0c, 0x23, 0x95, 0x18, 0x00, 0x00, 0x00, 0x42, 0x02, 0x22, 0x11, 0x04,
+ 0x00, 0x00, 0x03, 0x82, 0x1c, 0x23, 0x91, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+const unsigned char bm_frame [] PROGMEM = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x40, 0x02, 0x0f, 0xff, 0xfc,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x1e, 0x40, 0x02, 0x78, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x3f, 0xff, 0xf2, 0x40, 0x02, 0x4f, 0xff, 0xfc,
+ 0x00, 0x00, 0x02, 0x40, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x03, 0xc0, 0x00, 0x00,
+ 0x00, 0x00, 0x02, 0x40, 0x02, 0x40, 0x00, 0x00, 0x3f, 0xff, 0xf2, 0x40, 0x02, 0x4f, 0xff, 0xfc,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04, 0x20, 0x00, 0x12, 0x40, 0x02, 0x48, 0x00, 0x04,
+ 0x20, 0x00, 0x1e, 0x40, 0x02, 0x78, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04,
+ 0x20, 0x00, 0x10, 0x40, 0x02, 0x08, 0x00, 0x04, 0x3f, 0xff, 0xf0, 0x40, 0x02, 0x0f, 0xff, 0xfc,
+ 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x1c,
+ 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x18,
+ 0x00, 0x00, 0x01, 0x40, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x38,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xaa, 0x8a, 0xaa, 0x80
+};
+
+const unsigned char bm_console [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff,
+ 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x1f, 0xcf, 0xff, 0xff, 0xf3, 0xf8, 0xff,
+ 0xfe, 0x3f, 0x9f, 0xff, 0xff, 0xf9, 0xfc, 0x7f, 0xfc, 0x7f, 0x99, 0xe6, 0x61, 0x99, 0xfe, 0x3f,
+ 0xf8, 0xe7, 0x99, 0x26, 0x67, 0x99, 0xe7, 0x1f, 0xf9, 0xc7, 0x99, 0x26, 0x61, 0x99, 0xe3, 0x9f,
+ 0xf1, 0x8f, 0x98, 0x06, 0x67, 0x99, 0xf1, 0x8f, 0xf3, 0x9f, 0x9c, 0xce, 0x67, 0x99, 0xf9, 0xcf,
+ 0xf3, 0x99, 0x9f, 0xff, 0xff, 0xf9, 0x99, 0xcf, 0xf3, 0x99, 0x9f, 0xff, 0xff, 0xf9, 0x99, 0xcf,
+ 0xf3, 0x9f, 0x9f, 0xe3, 0x83, 0xf9, 0xf9, 0xcf, 0xf1, 0x8f, 0x9f, 0xc9, 0x93, 0xf9, 0xf1, 0x8f,
+ 0xf9, 0xc7, 0x9f, 0xc1, 0x83, 0xf9, 0xe3, 0x9f, 0xf8, 0xe7, 0x9f, 0xc9, 0x9f, 0xf9, 0xe7, 0x1f,
+ 0xfc, 0x7f, 0x9f, 0xc9, 0x9f, 0xf9, 0xfe, 0x3f, 0xfe, 0x3f, 0x9f, 0xff, 0xff, 0xf9, 0xfc, 0x7f,
+ 0xff, 0x1f, 0xcf, 0xff, 0xff, 0xf3, 0xf8, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x07, 0xff, 0xff,
+ 0xff, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff,
+ 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfd, 0xff, 0x7f, 0xdf, 0xf7, 0xfd, 0xff, 0x7f,
+ 0xfd, 0xff, 0x7f, 0xdf, 0xf7, 0xfd, 0xff, 0x7f, 0xfd, 0xff, 0x7f, 0xdf, 0xf7, 0xfd, 0xff, 0x7f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x78, 0x1e, 0x07, 0x81, 0xe0, 0x78, 0x1f,
+ 0xef, 0xbb, 0xee, 0xfb, 0xbe, 0xef, 0xbb, 0xef, 0xe8, 0xda, 0xb6, 0x9d, 0xb3, 0x6d, 0xda, 0x37,
+ 0xef, 0xda, 0xf6, 0xb5, 0xad, 0x6c, 0xdb, 0xf7, 0xe8, 0x5a, 0x36, 0x95, 0xad, 0x6c, 0xda, 0x97,
+ 0xef, 0xdb, 0xf6, 0x85, 0xb3, 0x6c, 0xda, 0x97, 0xea, 0x5a, 0x36, 0xb5, 0xb3, 0x6c, 0xdb, 0xf7,
+ 0xef, 0xda, 0xf6, 0xa5, 0xad, 0x6f, 0xda, 0x57, 0xe8, 0x5a, 0xb6, 0x85, 0xad, 0x6c, 0xda, 0x57,
+ 0xef, 0xdb, 0xf6, 0xfd, 0xbf, 0x6f, 0xdb, 0xf7, 0xe0, 0x18, 0x06, 0x01, 0x80, 0x60, 0x18, 0x07,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x42, 0x7c, 0x60, 0xf0, 0x78, 0x3c, 0x7f, 0xfe, 0x4a, 0x7c, 0x64, 0xf2, 0x79, 0x3c, 0x7f,
+ 0xfe, 0x43, 0xfe, 0x64, 0xf2, 0x79, 0x3e, 0x7f, 0xfe, 0x4e, 0x7e, 0x64, 0x92, 0x49, 0x26, 0x7f,
+ 0xfe, 0x4e, 0x7e, 0x60, 0x90, 0x48, 0x26, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+
+const unsigned char bm_checks [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x87, 0x0c, 0x99, 0xc7, 0x0f,
+ 0xe6, 0x00, 0x7f, 0x93, 0x3c, 0x99, 0x93, 0x3f, 0xe6, 0x00, 0x7f, 0x93, 0x0e, 0x39, 0x9f, 0x0f,
+ 0xff, 0xff, 0xff, 0x93, 0x3e, 0x39, 0x93, 0x3f, 0xff, 0xff, 0xff, 0x87, 0x0f, 0x79, 0xc7, 0x0f,
+ 0xe6, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe6, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0x30, 0xe3, 0x93, 0x87,
+ 0xe6, 0x00, 0x7c, 0x99, 0x33, 0xc9, 0x87, 0x3f, 0xe6, 0x00, 0x7c, 0xf8, 0x30, 0xcf, 0x8f, 0x8f,
+ 0xff, 0xff, 0xfc, 0x99, 0x33, 0xc9, 0x87, 0xe7, 0xff, 0xff, 0xfe, 0x39, 0x30, 0xe3, 0x93, 0x0f,
+ 0xe6, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe6, 0x00, 0x6f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x9c, 0x3c, 0x78, 0x70, 0xc3, 0x0f,
+ 0xe6, 0x0d, 0x3c, 0x99, 0x33, 0xe7, 0xcf, 0x27, 0xe6, 0x04, 0x7c, 0x38, 0x38, 0xf1, 0xc3, 0x27,
+ 0xff, 0xfe, 0xfc, 0xf9, 0x3e, 0x7c, 0xcf, 0x27, 0xff, 0xff, 0xfc, 0xf9, 0x30, 0xe1, 0xc3, 0x0f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_hwfail [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe4, 0xe3, 0x87, 0x0e, 0x73, 0x8e, 0x1c, 0x3f,
+ 0xe4, 0xc9, 0x93, 0x26, 0x53, 0x26, 0x4c, 0xff, 0xe0, 0xc1, 0x87, 0x26, 0x53, 0x06, 0x1c, 0x3f,
+ 0xe4, 0xc9, 0x87, 0x26, 0x03, 0x26, 0x1c, 0xff, 0xe4, 0xc9, 0x93, 0x0f, 0x27, 0x26, 0x4c, 0x3f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xe1, 0xc7, 0x33, 0xc9, 0x87, 0x0f, 0xf9, 0xff, 0xe7, 0x93, 0x33, 0xc9, 0x93, 0x3f, 0xf6, 0xff,
+ 0xe1, 0x83, 0x33, 0xc9, 0x87, 0x0f, 0xf6, 0xff, 0xe7, 0x93, 0x33, 0xc9, 0x87, 0x3f, 0xef, 0x7f,
+ 0xe7, 0x93, 0x30, 0xe3, 0x93, 0x0f, 0xe9, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd9, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd9, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x79, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0xf7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_conf_missing [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0x33, 0x87, 0x0c, 0xcc, 0xe1, 0xff, 0xff,
+ 0xe2, 0x33, 0x3e, 0x7c, 0xc4, 0xcf, 0xff, 0xff, 0xe0, 0x33, 0x8f, 0x1c, 0xc0, 0xc9, 0xff, 0xff,
+ 0xe5, 0x33, 0xe7, 0xcc, 0xc8, 0xc9, 0xff, 0xff, 0xe7, 0x33, 0x0e, 0x1c, 0xcc, 0xe3, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xf1, 0xe3, 0x99, 0x86, 0x70, 0xff, 0xf6, 0xff,
+ 0xe4, 0xc9, 0x89, 0x9e, 0x67, 0xff, 0xf6, 0xff, 0xe7, 0xc9, 0x81, 0x86, 0x64, 0xff, 0xef, 0x7f,
+ 0xe4, 0xc9, 0x91, 0x9e, 0x64, 0xff, 0xe9, 0x7f, 0xf1, 0xe3, 0x99, 0x9e, 0x71, 0xff, 0xd9, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xd9, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb9, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x79, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xf9, 0xf7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_no_radio [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xc7, 0x0e, 0x71, 0xfc, 0xce, 0x38, 0x7f,
+ 0xc9, 0x93, 0x26, 0x64, 0xfc, 0x4c, 0x9c, 0xff, 0xc3, 0x83, 0x26, 0x64, 0xfc, 0x0c, 0x9c, 0xff,
+ 0xc3, 0x93, 0x26, 0x64, 0xfc, 0x8c, 0x9c, 0xff, 0xc9, 0x93, 0x0e, 0x71, 0xfc, 0xce, 0x3c, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0x8e, 0x4c, 0xcc, 0x3f, 0xff, 0xfc, 0xff,
+ 0xcf, 0x26, 0x4c, 0x4c, 0x9f, 0xff, 0xfb, 0x7f, 0xc3, 0x26, 0x4c, 0x0c, 0x9f, 0xff, 0xfb, 0x7f,
+ 0xcf, 0x26, 0x4c, 0x8c, 0x9f, 0xff, 0xf7, 0xbf, 0xcf, 0x8f, 0x1c, 0xcc, 0x3f, 0xff, 0xf4, 0xbf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xec, 0xdf,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xdc, 0xef,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbc, 0xf7,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7c, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xfb,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_hwok [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xf2, 0x71, 0xc3, 0x87, 0x39, 0xc7, 0x0e, 0x1f, 0xf2, 0x64, 0xc9, 0x93, 0x29, 0x93, 0x26, 0x7f,
+ 0xf0, 0x60, 0xc3, 0x93, 0x29, 0x83, 0x0e, 0x1f, 0xf2, 0x64, 0xc3, 0x93, 0x01, 0x93, 0x0e, 0x7f,
+ 0xf2, 0x64, 0xc9, 0x87, 0x93, 0x93, 0x26, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x33, 0x30, 0xff, 0x8e, 0x4f, 0xff,
+ 0xff, 0xf3, 0x13, 0x39, 0xff, 0x26, 0x1f, 0xff, 0xff, 0xf3, 0x03, 0x39, 0xff, 0x26, 0x3f, 0xff,
+ 0xff, 0xf3, 0x23, 0x39, 0xff, 0x26, 0x1f, 0xff, 0xff, 0xf3, 0x33, 0x39, 0xff, 0x8e, 0x4f, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xc3, 0x0e, 0x67, 0xf8, 0x70, 0xe3, 0x87, 0x33, 0xe7, 0x27, 0x0f, 0xf9, 0x33, 0xc9, 0x93, 0x87,
+ 0xe7, 0x0f, 0x9f, 0xf8, 0x70, 0xc1, 0x93, 0xcf, 0xe7, 0x0f, 0x0f, 0xf8, 0x73, 0xc9, 0x93, 0xcf,
+ 0xe7, 0x26, 0x67, 0xf9, 0x30, 0xc9, 0x87, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_nfr [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xfc, 0x38, 0x66, 0x67, 0x1c, 0x3f, 0xff, 0xff, 0xfc, 0x99, 0xe6, 0x66, 0x4c, 0xff,
+ 0xff, 0x9f, 0xfc, 0x98, 0x70, 0xe6, 0x7c, 0x3f, 0xff, 0x6f, 0xfc, 0x99, 0xf0, 0xe6, 0x4c, 0xff,
+ 0xff, 0x6f, 0xfc, 0x38, 0x79, 0xe7, 0x1c, 0x3f, 0xfe, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xfe, 0x97, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x9b, 0xe6, 0x71, 0xc3, 0xe1, 0xc7, 0x0f,
+ 0xfd, 0x9b, 0xe2, 0x64, 0xe7, 0xe7, 0x93, 0x27, 0xfb, 0x9d, 0xe0, 0x64, 0xe7, 0xe1, 0x93, 0x0f,
+ 0xfb, 0x9d, 0xe4, 0x64, 0xe7, 0xe7, 0x93, 0x0f, 0xf7, 0xfe, 0xe6, 0x71, 0xe7, 0xe7, 0xc7, 0x27,
+ 0xf7, 0x9e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0x9f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xef, 0xff, 0x7f, 0xf8, 0x71, 0xcf, 0x0f, 0xff, 0xf0, 0x00, 0xff, 0xf3, 0xe4, 0xcf, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xf8, 0xe0, 0xcf, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x64, 0xcf, 0x3f, 0xff,
+ 0xff, 0xff, 0xff, 0xf0, 0xe4, 0xc3, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_online [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc2, 0x1c, 0x66, 0x61, 0x8c, 0x24, 0x90, 0x87,
+ 0xe6, 0x49, 0x22, 0x4f, 0x24, 0xe4, 0x93, 0x93, 0xe6, 0x18, 0x20, 0x63, 0x3c, 0x26, 0x30, 0x87,
+ 0xe6, 0x19, 0x24, 0x79, 0x24, 0xe6, 0x33, 0x87, 0xe6, 0x49, 0x26, 0x43, 0x8c, 0x27, 0x70, 0x93,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe6, 0x73, 0xe7, 0x33, 0x87, 0xff,
+ 0xff, 0xe4, 0xe2, 0x73, 0xe7, 0x13, 0x9f, 0xff, 0xff, 0xe4, 0xe0, 0x73, 0xe7, 0x03, 0x87, 0xff,
+ 0xff, 0xe4, 0xe4, 0x73, 0xe7, 0x23, 0x9f, 0xff, 0xff, 0xf1, 0xe6, 0x70, 0xe7, 0x33, 0x87, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_pairing [] PROGMEM = {
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xf0, 0xe3, 0x98, 0x73, 0x33, 0x87, 0xff, 0xff, 0xf2, 0xc9, 0x99, 0x33, 0x13, 0x3f, 0xff,
+ 0xff, 0xf0, 0xc1, 0x98, 0x73, 0x03, 0x27, 0xff, 0xff, 0xf3, 0xc9, 0x98, 0x73, 0x23, 0x27, 0xff,
+ 0xff, 0xf3, 0xc9, 0x99, 0x33, 0x33, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+const unsigned char bm_n_uh [] PROGMEM = {
+ 0x07, 0x27, 0x27, 0x27, 0x07, 0x8f, 0x8f, 0xcf, 0xcf, 0xcf, 0x07, 0xe7, 0x07, 0x3f, 0x07, 0x07,
+ 0xe7, 0xc7, 0xe7, 0x07, 0x27, 0x27, 0x07, 0xe7, 0xe7, 0x07, 0x3f, 0x07, 0xe7, 0x07, 0x07, 0x3f,
+ 0x07, 0x27, 0x07, 0x07, 0xc7, 0xcf, 0x9f, 0x1f, 0x07, 0x27, 0x07, 0x27, 0x07, 0x07, 0x27, 0x07,
+ 0xe7, 0xe7
+};
+
+const unsigned char bm_plug [] PROGMEM = {
+ 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x7f, 0x80, 0x55, 0xfc, 0x00, 0xaa, 0xfc, 0x00, 0x00,
+ 0x7f, 0x80, 0x00, 0x1c, 0x00
+};
+
+const unsigned char bm_hg_low [] PROGMEM = {
+ 0xf8, 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0xf8, 0xf8
+};
+
+const unsigned char bm_hg_high [] PROGMEM = {
+ 0xf8, 0x88, 0xf8, 0x70, 0x20, 0x70, 0xf8, 0xf8, 0xf8
+};
\ No newline at end of file
diff --git a/RNode_Firmware/Input.h b/RNode_Firmware/Input.h
new file mode 100644
index 0000000..996a09c
--- /dev/null
+++ b/RNode_Firmware/Input.h
@@ -0,0 +1,95 @@
+// 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 .
+
+#ifndef INPUT_H
+ #define INPUT_H
+
+ #define PIN_BUTTON pin_btn_usr1
+
+ #define PRESSED LOW
+ #define RELEASED HIGH
+
+ #define EVENT_ALL 0x00
+ #define EVENT_CLICKS 0x01
+ #define EVENT_BUTTON_DOWN 0x11
+ #define EVENT_BUTTON_UP 0x12
+ #define EVENT_BUTTON_CLICK 0x13
+ #define EVENT_BUTTON_DOUBLE_CLICK 0x14
+ #define EVENT_BUTTON_TRIPLE_CLICK 0x15
+
+ int button_events = EVENT_CLICKS;
+ int button_state = RELEASED;
+ int debounce_state = button_state;
+ unsigned long button_debounce_last = 0;
+ unsigned long button_debounce_delay = 25;
+ unsigned long button_down_last = 0;
+ unsigned long button_up_last = 0;
+
+ // Forward declaration
+ void button_event(uint8_t event, unsigned long duration);
+
+ void input_init() {
+ pinMode(PIN_BUTTON, INPUT_PULLUP);
+ }
+
+ void input_get_all_events() {
+ button_events = EVENT_ALL;
+ }
+
+ void input_get_click_events() {
+ button_events = EVENT_CLICKS;
+ }
+
+ void input_read() {
+ int button_reading = digitalRead(PIN_BUTTON);
+ if (button_reading != debounce_state) {
+ button_debounce_last = millis();
+ debounce_state = button_reading;
+ }
+
+ if ((millis() - button_debounce_last) > button_debounce_delay) {
+ if (button_reading != button_state) {
+ // State changed
+ int previous_state = button_state;
+ button_state = button_reading;
+
+ if (button_events == EVENT_ALL) {
+ if (button_state == PRESSED) {
+ button_event(EVENT_BUTTON_DOWN, 0);
+ } else if (button_state == RELEASED) {
+ button_event(EVENT_BUTTON_UP, 0);
+ }
+ } else if (button_events == EVENT_CLICKS) {
+ if (previous_state == PRESSED && button_state == RELEASED) {
+ button_up_last = millis();
+ button_event(EVENT_BUTTON_CLICK, button_up_last-button_down_last);
+ } else if (previous_state == RELEASED && button_state == PRESSED) {
+ button_down_last = millis();
+ }
+ }
+ }
+ }
+
+ }
+
+ bool button_pressed() {
+ if (button_state == PRESSED) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/MD5.cpp b/RNode_Firmware/MD5.cpp
new file mode 100644
index 0000000..56daa77
--- /dev/null
+++ b/RNode_Firmware/MD5.cpp
@@ -0,0 +1,301 @@
+#include "MD5.h"
+
+MD5::MD5()
+{
+ //nothing
+ return;
+}
+
+char* MD5::make_digest(const unsigned char *digest, int len) /* {{{ */
+{
+ char * md5str = (char*) malloc(sizeof(char)*(len*2+1));
+ static const char hexits[17] = "0123456789abcdef";
+ int i;
+
+ for (i = 0; i < len; i++) {
+ md5str[i * 2] = hexits[digest[i] >> 4];
+ md5str[(i * 2) + 1] = hexits[digest[i] & 0x0F];
+ }
+ md5str[len * 2] = '\0';
+ return md5str;
+}
+
+/*
+ * The basic MD5 functions.
+ *
+ * E and G are optimized compared to their RFC 1321 definitions for
+ * architectures that lack an AND-NOT instruction, just like in Colin Plumb's
+ * implementation.
+ * E() has been used instead of F() because F() is already defined in the Arduino core
+ */
+#define E(x, y, z) ((z) ^ ((x) & ((y) ^ (z))))
+#define G(x, y, z) ((y) ^ ((z) & ((x) ^ (y))))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | ~(z)))
+
+/*
+ * The MD5 transformation for all four rounds.
+ */
+#define STEP(f, a, b, c, d, x, t, s) \
+ (a) += f((b), (c), (d)) + (x) + (t); \
+ (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); \
+ (a) += (b);
+
+/*
+ * SET reads 4 input bytes in little-endian byte order and stores them
+ * in a properly aligned word in host byte order.
+ *
+ * The check for little-endian architectures that tolerate unaligned
+ * memory accesses is just an optimization. Nothing will break if it
+ * doesn't work.
+ */
+#if defined(__i386__) || defined(__x86_64__) || defined(__vax__)
+# define SET(n) \
+ (*(MD5_u32plus *)&ptr[(n) * 4])
+# define GET(n) \
+ SET(n)
+#else
+# define SET(n) \
+ (ctx->block[(n)] = \
+ (MD5_u32plus)ptr[(n) * 4] | \
+ ((MD5_u32plus)ptr[(n) * 4 + 1] << 8) | \
+ ((MD5_u32plus)ptr[(n) * 4 + 2] << 16) | \
+ ((MD5_u32plus)ptr[(n) * 4 + 3] << 24))
+# define GET(n) \
+ (ctx->block[(n)])
+#endif
+
+/*
+ * This processes one or more 64-byte data blocks, but does NOT update
+ * the bit counters. There are no alignment requirements.
+ */
+const void *MD5::body(void *ctxBuf, const void *data, size_t size)
+{
+ MD5_CTX *ctx = (MD5_CTX*)ctxBuf;
+ const unsigned char *ptr;
+ MD5_u32plus a, b, c, d;
+ MD5_u32plus saved_a, saved_b, saved_c, saved_d;
+
+ ptr = (unsigned char*)data;
+
+ a = ctx->a;
+ b = ctx->b;
+ c = ctx->c;
+ d = ctx->d;
+
+ do {
+ saved_a = a;
+ saved_b = b;
+ saved_c = c;
+ saved_d = d;
+
+/* Round 1
+ * E() has been used instead of F() because F() is already defined in the Arduino core
+ */
+ STEP(E, a, b, c, d, SET(0), 0xd76aa478, 7)
+ STEP(E, d, a, b, c, SET(1), 0xe8c7b756, 12)
+ STEP(E, c, d, a, b, SET(2), 0x242070db, 17)
+ STEP(E, b, c, d, a, SET(3), 0xc1bdceee, 22)
+ STEP(E, a, b, c, d, SET(4), 0xf57c0faf, 7)
+ STEP(E, d, a, b, c, SET(5), 0x4787c62a, 12)
+ STEP(E, c, d, a, b, SET(6), 0xa8304613, 17)
+ STEP(E, b, c, d, a, SET(7), 0xfd469501, 22)
+ STEP(E, a, b, c, d, SET(8), 0x698098d8, 7)
+ STEP(E, d, a, b, c, SET(9), 0x8b44f7af, 12)
+ STEP(E, c, d, a, b, SET(10), 0xffff5bb1, 17)
+ STEP(E, b, c, d, a, SET(11), 0x895cd7be, 22)
+ STEP(E, a, b, c, d, SET(12), 0x6b901122, 7)
+ STEP(E, d, a, b, c, SET(13), 0xfd987193, 12)
+ STEP(E, c, d, a, b, SET(14), 0xa679438e, 17)
+ STEP(E, b, c, d, a, SET(15), 0x49b40821, 22)
+
+/* Round 2 */
+ STEP(G, a, b, c, d, GET(1), 0xf61e2562, 5)
+ STEP(G, d, a, b, c, GET(6), 0xc040b340, 9)
+ STEP(G, c, d, a, b, GET(11), 0x265e5a51, 14)
+ STEP(G, b, c, d, a, GET(0), 0xe9b6c7aa, 20)
+ STEP(G, a, b, c, d, GET(5), 0xd62f105d, 5)
+ STEP(G, d, a, b, c, GET(10), 0x02441453, 9)
+ STEP(G, c, d, a, b, GET(15), 0xd8a1e681, 14)
+ STEP(G, b, c, d, a, GET(4), 0xe7d3fbc8, 20)
+ STEP(G, a, b, c, d, GET(9), 0x21e1cde6, 5)
+ STEP(G, d, a, b, c, GET(14), 0xc33707d6, 9)
+ STEP(G, c, d, a, b, GET(3), 0xf4d50d87, 14)
+ STEP(G, b, c, d, a, GET(8), 0x455a14ed, 20)
+ STEP(G, a, b, c, d, GET(13), 0xa9e3e905, 5)
+ STEP(G, d, a, b, c, GET(2), 0xfcefa3f8, 9)
+ STEP(G, c, d, a, b, GET(7), 0x676f02d9, 14)
+ STEP(G, b, c, d, a, GET(12), 0x8d2a4c8a, 20)
+
+/* Round 3 */
+ STEP(H, a, b, c, d, GET(5), 0xfffa3942, 4)
+ STEP(H, d, a, b, c, GET(8), 0x8771f681, 11)
+ STEP(H, c, d, a, b, GET(11), 0x6d9d6122, 16)
+ STEP(H, b, c, d, a, GET(14), 0xfde5380c, 23)
+ STEP(H, a, b, c, d, GET(1), 0xa4beea44, 4)
+ STEP(H, d, a, b, c, GET(4), 0x4bdecfa9, 11)
+ STEP(H, c, d, a, b, GET(7), 0xf6bb4b60, 16)
+ STEP(H, b, c, d, a, GET(10), 0xbebfbc70, 23)
+ STEP(H, a, b, c, d, GET(13), 0x289b7ec6, 4)
+ STEP(H, d, a, b, c, GET(0), 0xeaa127fa, 11)
+ STEP(H, c, d, a, b, GET(3), 0xd4ef3085, 16)
+ STEP(H, b, c, d, a, GET(6), 0x04881d05, 23)
+ STEP(H, a, b, c, d, GET(9), 0xd9d4d039, 4)
+ STEP(H, d, a, b, c, GET(12), 0xe6db99e5, 11)
+ STEP(H, c, d, a, b, GET(15), 0x1fa27cf8, 16)
+ STEP(H, b, c, d, a, GET(2), 0xc4ac5665, 23)
+
+/* Round 4 */
+ STEP(I, a, b, c, d, GET(0), 0xf4292244, 6)
+ STEP(I, d, a, b, c, GET(7), 0x432aff97, 10)
+ STEP(I, c, d, a, b, GET(14), 0xab9423a7, 15)
+ STEP(I, b, c, d, a, GET(5), 0xfc93a039, 21)
+ STEP(I, a, b, c, d, GET(12), 0x655b59c3, 6)
+ STEP(I, d, a, b, c, GET(3), 0x8f0ccc92, 10)
+ STEP(I, c, d, a, b, GET(10), 0xffeff47d, 15)
+ STEP(I, b, c, d, a, GET(1), 0x85845dd1, 21)
+ STEP(I, a, b, c, d, GET(8), 0x6fa87e4f, 6)
+ STEP(I, d, a, b, c, GET(15), 0xfe2ce6e0, 10)
+ STEP(I, c, d, a, b, GET(6), 0xa3014314, 15)
+ STEP(I, b, c, d, a, GET(13), 0x4e0811a1, 21)
+ STEP(I, a, b, c, d, GET(4), 0xf7537e82, 6)
+ STEP(I, d, a, b, c, GET(11), 0xbd3af235, 10)
+ STEP(I, c, d, a, b, GET(2), 0x2ad7d2bb, 15)
+ STEP(I, b, c, d, a, GET(9), 0xeb86d391, 21)
+
+ a += saved_a;
+ b += saved_b;
+ c += saved_c;
+ d += saved_d;
+
+ ptr += 64;
+ } while (size -= 64);
+
+ ctx->a = a;
+ ctx->b = b;
+ ctx->c = c;
+ ctx->d = d;
+
+ return ptr;
+}
+
+void MD5::MD5Init(void *ctxBuf)
+{
+ MD5_CTX *ctx = (MD5_CTX*)ctxBuf;
+ ctx->a = 0x67452301;
+ ctx->b = 0xefcdab89;
+ ctx->c = 0x98badcfe;
+ ctx->d = 0x10325476;
+
+ ctx->lo = 0;
+ ctx->hi = 0;
+
+ memset(ctx->block, 0, sizeof(ctx->block));
+ memset(ctx->buffer, 0, sizeof(ctx->buffer));
+}
+
+void MD5::MD5Update(void *ctxBuf, const void *data, size_t size)
+{
+ MD5_CTX *ctx = (MD5_CTX*)ctxBuf;
+ MD5_u32plus saved_lo;
+ MD5_u32plus used, free;
+
+ saved_lo = ctx->lo;
+ if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo) {
+ ctx->hi++;
+ }
+ ctx->hi += size >> 29;
+
+ used = saved_lo & 0x3f;
+
+ if (used) {
+ free = 64 - used;
+
+ if (size < free) {
+ memcpy(&ctx->buffer[used], data, size);
+ return;
+ }
+
+ memcpy(&ctx->buffer[used], data, free);
+ data = (unsigned char *)data + free;
+ size -= free;
+ body(ctx, ctx->buffer, 64);
+ }
+
+ if (size >= 64) {
+ data = body(ctx, data, size & ~(size_t)0x3f);
+ size &= 0x3f;
+ }
+
+ memcpy(ctx->buffer, data, size);
+}
+
+void MD5::MD5Final(unsigned char *result, void *ctxBuf)
+{
+ MD5_CTX *ctx = (MD5_CTX*)ctxBuf;
+ MD5_u32plus used, free;
+
+ used = ctx->lo & 0x3f;
+
+ ctx->buffer[used++] = 0x80;
+
+ free = 64 - used;
+
+ if (free < 8) {
+ memset(&ctx->buffer[used], 0, free);
+ body(ctx, ctx->buffer, 64);
+ used = 0;
+ free = 64;
+ }
+
+ memset(&ctx->buffer[used], 0, free - 8);
+
+ ctx->lo <<= 3;
+ ctx->buffer[56] = ctx->lo;
+ ctx->buffer[57] = ctx->lo >> 8;
+ ctx->buffer[58] = ctx->lo >> 16;
+ ctx->buffer[59] = ctx->lo >> 24;
+ ctx->buffer[60] = ctx->hi;
+ ctx->buffer[61] = ctx->hi >> 8;
+ ctx->buffer[62] = ctx->hi >> 16;
+ ctx->buffer[63] = ctx->hi >> 24;
+
+ body(ctx, ctx->buffer, 64);
+
+ result[0] = ctx->a;
+ result[1] = ctx->a >> 8;
+ result[2] = ctx->a >> 16;
+ result[3] = ctx->a >> 24;
+ result[4] = ctx->b;
+ result[5] = ctx->b >> 8;
+ result[6] = ctx->b >> 16;
+ result[7] = ctx->b >> 24;
+ result[8] = ctx->c;
+ result[9] = ctx->c >> 8;
+ result[10] = ctx->c >> 16;
+ result[11] = ctx->c >> 24;
+ result[12] = ctx->d;
+ result[13] = ctx->d >> 8;
+ result[14] = ctx->d >> 16;
+ result[15] = ctx->d >> 24;
+
+ memset(ctx, 0, sizeof(*ctx));
+}
+unsigned char* MD5::make_hash(char *arg)
+{
+ MD5_CTX context;
+ unsigned char * hash = (unsigned char *) malloc(16);
+ MD5Init(&context);
+ MD5Update(&context, arg, strlen(arg));
+ MD5Final(hash, &context);
+ return hash;
+}
+unsigned char* MD5::make_hash(char *arg,size_t size)
+{
+ MD5_CTX context;
+ unsigned char * hash = (unsigned char *) malloc(16);
+ MD5Init(&context);
+ MD5Update(&context, arg, size);
+ MD5Final(hash, &context);
+ return hash;
+}
\ No newline at end of file
diff --git a/RNode_Firmware/MD5.h b/RNode_Firmware/MD5.h
new file mode 100644
index 0000000..3ec8d81
--- /dev/null
+++ b/RNode_Firmware/MD5.h
@@ -0,0 +1,52 @@
+#ifndef MD5_h
+#define MD5_h
+
+#include "Arduino.h"
+
+/*
+ * This is an OpenSSL-compatible implementation of the RSA Data Security,
+ * Inc. MD5 Message-Digest Algorithm (RFC 1321).
+ *
+ * Written by Solar Designer in 2001, and placed
+ * in the public domain. There's absolutely no warranty.
+ *
+ * This differs from Colin Plumb's older public domain implementation in
+ * that no 32-bit integer data type is required, there's no compile-time
+ * endianness configuration, and the function prototypes match OpenSSL's.
+ * The primary goals are portability and ease of use.
+ *
+ * This implementation is meant to be fast, but not as fast as possible.
+ * Some known optimizations are not included to reduce source code size
+ * and avoid compile-time configuration.
+ */
+
+/*
+ * Updated by Scott MacVicar for arduino
+ *
+ */
+
+#include
+
+typedef unsigned long MD5_u32plus;
+
+typedef struct {
+ MD5_u32plus lo, hi;
+ MD5_u32plus a, b, c, d;
+ unsigned char buffer[64];
+ MD5_u32plus block[16];
+} MD5_CTX;
+
+class MD5
+{
+public:
+ MD5();
+ static unsigned char* make_hash(char *arg);
+ static unsigned char* make_hash(char *arg,size_t size);
+ static char* make_digest(const unsigned char *digest, int len);
+ static const void *body(void *ctxBuf, const void *data, size_t size);
+ static void MD5Init(void *ctxBuf);
+ static void MD5Final(unsigned char *result, void *ctxBuf);
+ static void MD5Update(void *ctxBuf, const void *data, size_t size);
+};
+
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/Modem.h b/RNode_Firmware/Modem.h
new file mode 100644
index 0000000..027e314
--- /dev/null
+++ b/RNode_Firmware/Modem.h
@@ -0,0 +1,4 @@
+#define SX1276 0x01
+#define SX1278 0x02
+#define SX1262 0x03
+#define SX1280 0x04
diff --git a/RNode_Firmware/Power.h b/RNode_Firmware/Power.h
new file mode 100644
index 0000000..7ce6d8a
--- /dev/null
+++ b/RNode_Firmware/Power.h
@@ -0,0 +1,650 @@
+// 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 .
+
+#define PMU_TEMP_MIN -30
+#define PMU_TEMP_MAX 90
+#define PMU_TEMP_OFFSET 120
+bool pmu_temp_sensor_ready = false;
+float pmu_temperature = PMU_TEMP_MIN-1;
+
+#if BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1
+ #include
+ XPowersLibInterface* PMU = NULL;
+
+ #ifndef PMU_WIRE_PORT
+ #if BOARD_MODEL == BOARD_TBEAM_S_V1
+ #define PMU_WIRE_PORT Wire1
+ #else
+ #define PMU_WIRE_PORT Wire
+ #endif
+ #endif
+
+ #define BAT_V_MIN 3.15
+ #define BAT_V_MAX 4.14
+
+ void disablePeripherals() {
+ if (PMU) {
+ // GNSS RTC PowerVDD
+ PMU->enablePowerOutput(XPOWERS_VBACKUP);
+
+ // LoRa VDD
+ PMU->disablePowerOutput(XPOWERS_ALDO2);
+
+ // GNSS VDD
+ PMU->disablePowerOutput(XPOWERS_ALDO3);
+ }
+ }
+
+ bool pmuInterrupt;
+ void setPmuFlag()
+ {
+ pmuInterrupt = true;
+ }
+#elif BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1
+ #define BAT_V_MIN 3.15
+ #define BAT_V_MAX 4.3
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 5
+ const uint8_t pin_vbat = 35;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#elif BOARD_MODEL == BOARD_T3S3
+ #define BAT_V_MIN 3.15
+ #define BAT_V_MAX 4.217
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 5
+ const uint8_t pin_vbat = 1;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#elif BOARD_MODEL == BOARD_TDECK
+ #define BAT_V_MIN 3.15
+ #define BAT_V_MAX 4.3
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 5
+ const uint8_t pin_vbat = 4;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#elif BOARD_MODEL == BOARD_HELTEC32_V3
+ // Unless we implement some real voodoo
+ // on these boards, we can't say with
+ // any certainty whether we are actually
+ // charging and have reached a charge
+ // complete state. The *only* data point
+ // we have to go from is the bus voltage.
+ // The BAT_V_CHG and BAT_V_FLOAT values
+ // are set high here to avoid the display
+ // indication confusingly flapping
+ // between charge completed, charging and
+ // discharging states.
+ // Update: Vodoo implemented. Hopefully
+ // it will work accross different boards.
+ #define BAT_V_MIN 3.05
+ #define BAT_V_MAX 4.0
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 7
+ const uint8_t pin_vbat = 1;
+ const uint8_t pin_ctrl = 37;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#elif BOARD_MODEL == BOARD_HELTEC32_V4
+ #define BAT_V_MIN 3.05
+ #define BAT_V_MAX 4.0
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 7
+ const uint8_t pin_vbat = 1;
+ const uint8_t pin_ctrl = 37;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#elif BOARD_MODEL == BOARD_HELTEC_T114
+ #define BAT_V_MIN 3.15
+ #define BAT_V_MAX 4.165
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 7
+ const uint8_t pin_vbat = 4;
+ const uint8_t pin_ctrl = 6;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#elif BOARD_MODEL == BOARD_TECHO
+ #define BAT_V_MIN 3.15
+ #define BAT_V_MAX 4.16
+ #define BAT_V_CHG 4.48
+ #define BAT_V_FLOAT 4.33
+ #define BAT_SAMPLES 7
+ const uint8_t pin_vbat = 4;
+ float bat_p_samples[BAT_SAMPLES];
+ float bat_v_samples[BAT_SAMPLES];
+ uint8_t bat_samples_count = 0;
+ int bat_discharging_samples = 0;
+ int bat_charging_samples = 0;
+ int bat_charged_samples = 0;
+ bool bat_voltage_dropping = false;
+ float bat_delay_v = 0;
+ float bat_state_change_v = 0;
+#endif
+
+uint32_t last_pmu_update = 0;
+uint8_t pmu_target_pps = 1;
+int pmu_update_interval = 1000/pmu_target_pps;
+uint8_t pmu_charged_ascertain = 0;
+uint8_t pmu_rc = 0;
+uint8_t pmu_sc = 0;
+float bat_delay_diff = 0;
+bool bat_diff_positive = false;
+#define PMU_R_INTERVAL 5
+#define PMU_SCV_RESET_INTERVAL 3
+void kiss_indicate_battery();
+void kiss_indicate_temperature();
+
+void measure_temperature() {
+ #if PLATFORM == PLATFORM_ESP32
+ if (pmu_temp_sensor_ready) { pmu_temperature = temperatureRead(); } else { pmu_temperature = PMU_TEMP_MIN-1; }
+ #endif
+}
+
+void measure_battery() {
+ #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TECHO
+ battery_installed = true;
+ #if BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4
+ battery_indeterminate = false;
+ #else
+ battery_indeterminate = true;
+ #endif
+
+ #if BOARD_MODEL == BOARD_HELTEC32_V3
+ float battery_measurement = (float)(analogRead(pin_vbat)) * 0.0041;
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ float battery_measurement = (float)(analogRead(pin_vbat)) * 0.00418;
+ #elif BOARD_MODEL == BOARD_T3S3
+ float battery_measurement = (float)(analogRead(pin_vbat)) / 4095.0*6.7828;
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ float battery_measurement = (float)(analogRead(pin_vbat)) * 0.017165;
+ #elif BOARD_MODEL == BOARD_TECHO
+ float battery_measurement = (float)(analogRead(pin_vbat)) * 0.007067;
+ #else
+ float battery_measurement = (float)(analogRead(pin_vbat)) / 4095.0*7.26;
+ #endif
+
+ bat_v_samples[bat_samples_count%BAT_SAMPLES] = battery_measurement;
+ bat_p_samples[bat_samples_count%BAT_SAMPLES] = ((battery_voltage-BAT_V_MIN) / (BAT_V_MAX-BAT_V_MIN))*100.0;
+
+ bat_samples_count++;
+ if (!battery_ready && bat_samples_count >= BAT_SAMPLES) {
+ battery_ready = true;
+ }
+
+ if (battery_ready) {
+
+ battery_percent = 0;
+ for (uint8_t bi = 0; bi < BAT_SAMPLES; bi++) {
+ battery_percent += bat_p_samples[bi];
+ }
+ battery_percent = battery_percent/BAT_SAMPLES;
+
+ battery_voltage = 0;
+ for (uint8_t bi = 0; bi < BAT_SAMPLES; bi++) {
+ battery_voltage += bat_v_samples[bi];
+ }
+ battery_voltage = battery_voltage/BAT_SAMPLES;
+
+ if (bat_delay_v == 0) bat_delay_v = battery_voltage;
+ if (bat_state_change_v == 0) bat_state_change_v = battery_voltage;
+ if (battery_percent > 100.0) battery_percent = 100.0;
+ if (battery_percent < 0.0) battery_percent = 0.0;
+
+ if (bat_samples_count%BAT_SAMPLES == 0) {
+ pmu_sc++;
+ bat_delay_diff = battery_voltage-bat_state_change_v;
+
+ if (battery_voltage < bat_delay_v && battery_voltage < BAT_V_FLOAT) {
+ if (bat_voltage_dropping == false) {
+ if (bat_delay_diff < -0.008) {
+ bat_voltage_dropping = true;
+ bat_state_change_v = battery_voltage;
+ }
+ } else {
+ if (pmu_sc%PMU_SCV_RESET_INTERVAL == 0) { bat_state_change_v = battery_voltage; }
+ }
+ } else {
+ if (bat_voltage_dropping == true) {
+ if (bat_delay_diff > 0.01) {
+ bat_voltage_dropping = false;
+ bat_state_change_v = battery_voltage;
+ }
+ }
+ }
+ bat_samples_count = 0;
+ bat_delay_v = battery_voltage;
+ }
+
+ if (bat_voltage_dropping && battery_voltage < BAT_V_FLOAT) {
+ // if (battery_state != BATTERY_STATE_DISCHARGING) { SerialBT.printf("STATE CHANGE to DISCHARGING at delta=%.3fv. State change v is now %.3fv.\n", bat_delay_diff, bat_state_change_v); }
+ battery_state = BATTERY_STATE_DISCHARGING;
+ pmu_charged_ascertain = 0;
+ } else {
+ if (pmu_charged_ascertain < 8) { pmu_charged_ascertain++; }
+ else {
+ if (battery_percent < 100.0) {
+ // if (battery_state != BATTERY_STATE_CHARGING) { SerialBT.printf("STATE CHANGE to CHARGING at delta=%.3fv. State change v is now %.3fv.\n", bat_delay_diff, bat_state_change_v); }
+ battery_state = BATTERY_STATE_CHARGING;
+ } else {
+ // if (battery_state != BATTERY_STATE_CHARGED) { SerialBT.printf("STATE CHANGE to CHARGED at delta=%.3fv. State change v is now %.3fv.\n", bat_delay_diff, bat_state_change_v); }
+ battery_state = BATTERY_STATE_CHARGED;
+ }
+ }
+ }
+
+ #if MCU_VARIANT == MCU_NRF52
+ if (bt_state != BT_STATE_OFF) { blebas.write(battery_percent); }
+ #endif
+
+ // if (bt_state == BT_STATE_CONNECTED) {
+ // SerialBT.printf("\nBus voltage %.3fv. Unfiltered %.3fv. Diff %.3f", battery_voltage, bat_v_samples[BAT_SAMPLES-1], bat_delay_diff);
+ // if (bat_voltage_dropping) { SerialBT.printf("\n Voltage is dropping. Percentage %.1f%%.", battery_percent); }
+ // else { SerialBT.printf("\n Voltage is not dropping. Percentage %.1f%%.", battery_percent); }
+ // if (battery_state == BATTERY_STATE_DISCHARGING) { SerialBT.printf("\n Battery discharging. delay_v %.3fv\nState change at %.3fv", bat_delay_v, bat_state_change_v); }
+ // if (battery_state == BATTERY_STATE_CHARGING) { SerialBT.printf("\n Battery charging. delay_v %.3fv\nState change at %.3fv", bat_delay_v, bat_state_change_v); }
+ // if (battery_state == BATTERY_STATE_CHARGED) { SerialBT.print("\n Battery is charged."); }
+ // SerialBT.print("\n");
+ // }
+ }
+
+ #elif BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_TBEAM_S_V1
+ if (PMU) {
+ float discharge_current = 0;
+ float charge_current = 0;
+ float ext_voltage = 0;
+ float ext_current = 0;
+ if (PMU->getChipModel() == XPOWERS_AXP192) {
+ discharge_current = ((XPowersAXP192*)PMU)->getBattDischargeCurrent();
+ charge_current = ((XPowersAXP192*)PMU)->getBatteryChargeCurrent();
+ battery_voltage = PMU->getBattVoltage()/1000.0;
+ // battery_percent = PMU->getBattPercentage()*1.0;
+ battery_installed = PMU->isBatteryConnect();
+ external_power = PMU->isVbusIn();
+ ext_voltage = PMU->getVbusVoltage()/1000.0;
+ ext_current = ((XPowersAXP192*)PMU)->getVbusCurrent();
+ }
+ else if (PMU->getChipModel() == XPOWERS_AXP2101) {
+ battery_voltage = PMU->getBattVoltage()/1000.0;
+ // battery_percent = PMU->getBattPercentage()*1.0;
+ battery_installed = PMU->isBatteryConnect();
+ external_power = PMU->isVbusIn();
+ ext_voltage = PMU->getVbusVoltage()/1000.0;
+ }
+
+ if (battery_installed) {
+ if (PMU->isCharging()) {
+ battery_state = BATTERY_STATE_CHARGING;
+ battery_percent = ((battery_voltage-BAT_V_MIN) / (BAT_V_MAX-BAT_V_MIN))*100.0;
+ } else {
+ if (PMU->isDischarge()) {
+ battery_state = BATTERY_STATE_DISCHARGING;
+ battery_percent = ((battery_voltage-BAT_V_MIN) / (BAT_V_MAX-BAT_V_MIN))*100.0;
+ } else {
+ battery_state = BATTERY_STATE_CHARGED;
+ battery_percent = 100.0;
+ }
+ }
+ } else {
+ battery_state = BATTERY_STATE_UNKNOWN;
+ battery_percent = 0.0;
+ battery_voltage = 0.0;
+ }
+
+ if (battery_percent > 100.0) battery_percent = 100.0;
+ if (battery_percent < 0.0) battery_percent = 0.0;
+
+ float charge_watts = battery_voltage*(charge_current/1000.0);
+ float discharge_watts = battery_voltage*(discharge_current/1000.0);
+ float ext_watts = ext_voltage*(ext_current/1000.0);
+
+ battery_ready = true;
+
+ // if (bt_state == BT_STATE_CONNECTED) {
+ // if (battery_installed) {
+ // if (external_power) {
+ // SerialBT.printf("External power connected, drawing %.2fw, %.1fmA at %.1fV\n", ext_watts, ext_current, ext_voltage);
+ // } else {
+ // SerialBT.println("Running on battery");
+ // }
+ // SerialBT.printf("Battery percentage %.1f%%\n", battery_percent);
+ // SerialBT.printf("Battery voltage %.2fv\n", battery_voltage);
+ // // SerialBT.printf("Temperature %.1f%\n", auxillary_temperature);
+
+ // if (battery_state == BATTERY_STATE_CHARGING) {
+ // SerialBT.printf("Charging with %.2fw, %.1fmA at %.1fV\n", charge_watts, charge_current, battery_voltage);
+ // } else if (battery_state == BATTERY_STATE_DISCHARGING) {
+ // SerialBT.printf("Discharging at %.2fw, %.1fmA at %.1fV\n", discharge_watts, discharge_current, battery_voltage);
+ // } else if (battery_state == BATTERY_STATE_CHARGED) {
+ // SerialBT.printf("Battery charged\n");
+ // }
+ // } else {
+ // SerialBT.println("No battery installed");
+ // }
+ // SerialBT.println("");
+ // }
+ }
+ else {
+ battery_ready = false;
+ }
+ #endif
+
+ if (battery_ready) {
+ pmu_rc++;
+ if (pmu_rc%PMU_R_INTERVAL == 0) {
+ kiss_indicate_battery();
+ if (pmu_temp_sensor_ready) { kiss_indicate_temperature(); }
+ }
+ }
+}
+
+void update_pmu() {
+ if (millis()-last_pmu_update >= pmu_update_interval) {
+ measure_battery();
+ measure_temperature();
+ last_pmu_update = millis();
+ }
+}
+
+bool init_pmu() {
+ #if IS_ESP32S3
+ pmu_temp_sensor_ready = true;
+ #endif
+
+ #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_TECHO
+ pinMode(pin_vbat, INPUT);
+ return true;
+ #elif BOARD_MODEL == BOARD_HELTEC32_V3
+ // there are three version of V3: V3, V3.1, and V3.2
+ // V3 and V3.1 have a pull up on pin_ctrl and are active low
+ // V3.2 has a transistor and active high
+ // put the pin input mode and read it. if it's high, we have V3 or V3.1
+ // other wise, it's a V3.2
+ uint16_t pin_ctrl_value;
+ uint8_t pin_ctrl_active = LOW;
+ pinMode(pin_ctrl, INPUT);
+ pin_ctrl_value = digitalRead(pin_ctrl);
+ if(pin_ctrl_value == HIGH) {
+ // We have either a V3 or V3.1
+ pin_ctrl_active = LOW;
+ }
+ else {
+ // We have a V3.2
+ pin_ctrl_active = HIGH;
+ }
+ pinMode(pin_ctrl,OUTPUT);
+ digitalWrite(pin_ctrl, pin_ctrl_active);
+ return true;
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ pinMode(pin_ctrl,OUTPUT);
+ digitalWrite(pin_ctrl, HIGH);
+ return true;
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ pinMode(pin_ctrl,OUTPUT);
+ digitalWrite(pin_ctrl, HIGH);
+ return true;
+ #elif BOARD_MODEL == BOARD_TBEAM
+ Wire.begin(I2C_SDA, I2C_SCL);
+
+ if (!PMU) {
+ PMU = new XPowersAXP2101(PMU_WIRE_PORT);
+ if (!PMU->init()) {
+ delete PMU;
+ PMU = NULL;
+ }
+ }
+
+ if (!PMU) {
+ PMU = new XPowersAXP192(PMU_WIRE_PORT);
+ if (!PMU->init()) {
+ delete PMU;
+ PMU = NULL;
+ }
+ }
+
+ if (!PMU) {
+ return false;
+ }
+
+ // Configure charging indicator
+ PMU->setChargingLedMode(XPOWERS_CHG_LED_OFF);
+
+ pinMode(PMU_IRQ, INPUT_PULLUP);
+ attachInterrupt(PMU_IRQ, setPmuFlag, FALLING);
+
+ if (PMU->getChipModel() == XPOWERS_AXP192) {
+
+ // Turn off unused power sources to save power
+ PMU->disablePowerOutput(XPOWERS_DCDC1);
+ PMU->disablePowerOutput(XPOWERS_DCDC2);
+ PMU->disablePowerOutput(XPOWERS_LDO2);
+ PMU->disablePowerOutput(XPOWERS_LDO3);
+
+ // Set the power of LoRa and GPS module to 3.3V
+ // LoRa
+ PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300);
+ // GPS
+ PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300);
+ // OLED
+ PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
+
+ // Turn on LoRa
+ PMU->enablePowerOutput(XPOWERS_LDO2);
+
+ // Turn on GPS
+ //PMU->enablePowerOutput(XPOWERS_LDO3);
+
+ // protected oled power source
+ PMU->setProtectedChannel(XPOWERS_DCDC1);
+ // protected esp32 power source
+ PMU->setProtectedChannel(XPOWERS_DCDC3);
+ // enable oled power
+ PMU->enablePowerOutput(XPOWERS_DCDC1);
+
+ PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ);
+
+ PMU->enableIRQ(XPOWERS_AXP192_VBUS_REMOVE_IRQ |
+ XPOWERS_AXP192_VBUS_INSERT_IRQ |
+ XPOWERS_AXP192_BAT_CHG_DONE_IRQ |
+ XPOWERS_AXP192_BAT_CHG_START_IRQ |
+ XPOWERS_AXP192_BAT_REMOVE_IRQ |
+ XPOWERS_AXP192_BAT_INSERT_IRQ |
+ XPOWERS_AXP192_PKEY_SHORT_IRQ
+ );
+
+ }
+ else if (PMU->getChipModel() == XPOWERS_AXP2101) {
+
+ // Turn off unused power sources to save power
+ PMU->disablePowerOutput(XPOWERS_DCDC2);
+ PMU->disablePowerOutput(XPOWERS_DCDC3);
+ PMU->disablePowerOutput(XPOWERS_DCDC4);
+ PMU->disablePowerOutput(XPOWERS_DCDC5);
+ PMU->disablePowerOutput(XPOWERS_ALDO1);
+ PMU->disablePowerOutput(XPOWERS_ALDO2);
+ PMU->disablePowerOutput(XPOWERS_ALDO3);
+ PMU->disablePowerOutput(XPOWERS_ALDO4);
+ PMU->disablePowerOutput(XPOWERS_BLDO1);
+ PMU->disablePowerOutput(XPOWERS_BLDO2);
+ PMU->disablePowerOutput(XPOWERS_DLDO1);
+ PMU->disablePowerOutput(XPOWERS_DLDO2);
+ PMU->disablePowerOutput(XPOWERS_VBACKUP);
+
+ // Set the power of LoRa and GPS module to 3.3V
+ // LoRa
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
+ // GPS
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
+ PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300);
+
+ // ESP32 VDD
+ // ! No need to set, automatically open , Don't close it
+ // PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
+ // PMU->setProtectedChannel(XPOWERS_DCDC1);
+ PMU->setProtectedChannel(XPOWERS_DCDC1);
+
+ // LoRa VDD
+ PMU->enablePowerOutput(XPOWERS_ALDO2);
+
+ // GNSS VDD
+ //PMU->enablePowerOutput(XPOWERS_ALDO3);
+
+ // GNSS RTC PowerVDD
+ //PMU->enablePowerOutput(XPOWERS_VBACKUP);
+ }
+
+ PMU->enableSystemVoltageMeasure();
+ PMU->enableVbusVoltageMeasure();
+ PMU->enableBattVoltageMeasure();
+ // It is necessary to disable the detection function of the TS pin on the board
+ // without the battery temperature detection function, otherwise it will cause abnormal charging
+ PMU->disableTSPinMeasure();
+
+ // Set the time of pressing the button to turn off
+ PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
+
+ return true;
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ Wire1.begin(I2C_SDA, I2C_SCL);
+
+ if (!PMU) {
+ PMU = new XPowersAXP2101(PMU_WIRE_PORT);
+ if (!PMU->init()) {
+ delete PMU;
+ PMU = NULL;
+ }
+ }
+
+ if (!PMU) {
+ return false;
+ }
+
+ /**
+ * gnss module power channel
+ * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during
+ * initialization
+ */
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
+ PMU->enablePowerOutput(XPOWERS_ALDO4);
+
+ // lora radio power channel
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
+ PMU->enablePowerOutput(XPOWERS_ALDO3);
+
+ // m.2 interface
+ PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300);
+ PMU->enablePowerOutput(XPOWERS_DCDC3);
+
+ /**
+ * ALDO2 cannot be turned off.
+ * It is a necessary condition for sensor communication.
+ * It must be turned on to properly access the sensor and screen
+ * It is also responsible for the power supply of PCF8563
+ */
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
+ PMU->enablePowerOutput(XPOWERS_ALDO2);
+
+ // 6-axis , magnetometer ,bme280 , oled screen power channel
+ PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
+ PMU->enablePowerOutput(XPOWERS_ALDO1);
+
+ // sdcard power channle
+ PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
+ PMU->enablePowerOutput(XPOWERS_BLDO1);
+
+ // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300);
+ // PMU->enablePowerOutput(XPOWERS_DCDC4);
+
+ // not use channel
+ PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited
+ PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited
+ PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
+ PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
+ PMU->disablePowerOutput(XPOWERS_VBACKUP);
+
+ // Configure charging
+ PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
+ PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
+ // TODO: Reset
+ PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG);
+
+ // Set the time of pressing the button to turn off
+ PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
+ PMU->setPowerKeyPressOnTime(XPOWERS_POWERON_128MS);
+
+ // disable all axp chip interrupt
+ PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
+ PMU->clearIrqStatus();
+
+ // It is necessary to disable the detection function of the TS pin on the board
+ // without the battery temperature detection function, otherwise it will cause abnormal charging
+ PMU->disableTSPinMeasure();
+ PMU->enableVbusVoltageMeasure();
+ PMU->enableBattVoltageMeasure();
+
+
+ return true;
+ #else
+ return false;
+ #endif
+}
diff --git a/RNode_Firmware/RNode_Firmware.ino b/RNode_Firmware/RNode_Firmware.ino
new file mode 100644
index 0000000..42177ba
--- /dev/null
+++ b/RNode_Firmware/RNode_Firmware.ino
@@ -0,0 +1,1822 @@
+// 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 .
+
+#include
+#include
+#include "Utilities.h"
+
+FIFOBuffer serialFIFO;
+uint8_t serialBuffer[CONFIG_UART_BUFFER_SIZE+1];
+
+FIFOBuffer16 packet_starts;
+uint16_t packet_starts_buf[CONFIG_QUEUE_MAX_LENGTH+1];
+
+FIFOBuffer16 packet_lengths;
+uint16_t packet_lengths_buf[CONFIG_QUEUE_MAX_LENGTH+1];
+
+uint8_t packet_queue[CONFIG_QUEUE_SIZE];
+
+volatile uint8_t queue_height = 0;
+volatile uint16_t queued_bytes = 0;
+volatile uint16_t queue_cursor = 0;
+volatile uint16_t current_packet_start = 0;
+volatile bool serial_buffering = false;
+#if HAS_BLUETOOTH || HAS_BLE == true
+ bool bt_init_ran = false;
+#endif
+
+#if HAS_CONSOLE
+ #include "Console.h"
+#endif
+
+#if PLATFORM == PLATFORM_ESP32 || PLATFORM == PLATFORM_NRF52
+ #define MODEM_QUEUE_SIZE 4
+ typedef struct {
+ size_t len;
+ int rssi;
+ int snr_raw;
+ uint8_t data[];
+ } modem_packet_t;
+ static xQueueHandle modem_packet_queue = NULL;
+#endif
+
+char sbuf[128];
+
+#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ bool packet_ready = false;
+#endif
+
+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
+ #endif
+
+ #if 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);
+ #endif
+
+ if (!eeprom_begin()) { Serial.write("EEPROM initialisation failed.\r\n"); }
+ #endif
+
+ // Seed the PRNG for CSMA R-value selection
+ #if MCU_VARIANT == MCU_ESP32
+ // On ESP32, get the seed value from the
+ // hardware RNG
+ 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
+ //
+ // 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_HELTEC32_V4
+ // Some boards need to wait until the hardware UART is set up before booting
+ // the full firmware. In the case of the RAK4631 and Heltec T114, 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
+
+ serial_interrupt_init();
+
+ // 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
+
+ #if HAS_TCXO == true
+ if (pin_tcxo_enable != -1) {
+ pinMode(pin_tcxo_enable, OUTPUT);
+ digitalWrite(pin_tcxo_enable, HIGH);
+ }
+ #endif
+
+ // Initialise buffers
+ memset(pbuf, 0, sizeof(pbuf));
+ memset(cmdbuf, 0, sizeof(cmdbuf));
+
+ memset(packet_queue, 0, sizeof(packet_queue));
+
+ memset(packet_starts_buf, 0, sizeof(packet_starts_buf));
+ fifo16_init(&packet_starts, packet_starts_buf, CONFIG_QUEUE_MAX_LENGTH);
+
+ memset(packet_lengths_buf, 0, sizeof(packet_starts_buf));
+ fifo16_init(&packet_lengths, packet_lengths_buf, CONFIG_QUEUE_MAX_LENGTH);
+
+ #if PLATFORM == PLATFORM_ESP32 || PLATFORM == PLATFORM_NRF52
+ modem_packet_queue = xQueueCreate(MODEM_QUEUE_SIZE, sizeof(modem_packet_t*));
+ #endif
+
+ // Set chip select, reset and interrupt
+ // pins for the LoRa module
+ #if MODEM == SX1276 || MODEM == SX1278
+ LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy);
+ #elif MODEM == SX1262
+ LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen);
+ #elif MODEM == SX1280
+ LoRa->setPins(pin_cs, pin_reset, pin_dio, pin_busy, pin_rxen, pin_txen);
+ #endif
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ init_channel_stats();
+
+ #if BOARD_MODEL == BOARD_T3S3
+ #if MODEM == SX1280
+ delay(300);
+ LoRa->reset();
+ delay(100);
+ #endif
+ #endif
+
+ #if BOARD_MODEL == BOARD_XIAO_S3
+ // Improve wakeup from sleep
+ delay(300);
+ LoRa->reset();
+ delay(100);
+ #endif
+
+ // Check installed transceiver chip and
+ // probe boot parameters.
+ if (LoRa->preInit()) {
+ modem_installed = true;
+
+ #if HAS_INPUT
+ // Skip quick-reset console activation
+ #else
+ uint32_t lfr = LoRa->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
+ }
+ LoRa->setFrequency(M_FRQ_S);
+ #endif
+
+ } else {
+ modem_installed = false;
+ }
+ #else
+ // Older variants only came with SX1276/78 chips,
+ // so assume that to be the case for now.
+ modem_installed = true;
+ #endif
+
+ #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_TECHO
+ display_add_callback(work_while_waiting);
+ #endif
+
+ display_unblank();
+ disp_ready = display_init();
+ update_display();
+ #endif
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #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();
+ }
+ #endif
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #if MODEM == SX1280
+ avoid_interference = false;
+ #else
+ #if HAS_EEPROM
+ uint8_t ia_conf = EEPROM.read(eeprom_addr(ADDR_CONF_DIA));
+ if (ia_conf == 0x00) { avoid_interference = true; }
+ else { avoid_interference = false; }
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t ia_conf = eeprom_read(eeprom_addr(ADDR_CONF_DIA));
+ if (ia_conf == 0x00) { avoid_interference = true; }
+ else { avoid_interference = false; }
+ #endif
+ #endif
+ #endif
+
+ // Validate board health, EEPROM and config
+ validate_status();
+
+ if (op_mode != MODE_TNC) LoRa->setFrequency(0);
+}
+
+void lora_receive() {
+ if (!implicit) {
+ LoRa->receive();
+ } else {
+ LoRa->receive(implicit_l);
+ }
+}
+
+inline void kiss_write_packet() {
+ serial_write(FEND);
+ serial_write(CMD_DATA);
+
+ for (uint16_t i = 0; i < read_len; 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 = 0;
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ packet_ready = false;
+ #endif
+
+ #if MCU_VARIANT == MCU_ESP32
+ #if HAS_BLE
+ bt_flush();
+ #endif
+ #endif
+}
+
+inline void getPacketData(uint16_t len) {
+ #if MCU_VARIANT != MCU_NRF52
+ while (len-- && read_len < MTU) {
+ pbuf[read_len++] = LoRa->read();
+ }
+ #else
+ BaseType_t int_mask = taskENTER_CRITICAL_FROM_ISR();
+ while (len-- && read_len < MTU) {
+ pbuf[read_len++] = LoRa->read();
+ }
+ taskEXIT_CRITICAL_FROM_ISR(int_mask);
+ #endif
+}
+
+void ISR_VECT receive_callback(int packet_size) {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ BaseType_t int_mask;
+ #endif
+
+ 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 = LoRa->read(); packet_size--;
+ uint8_t sequence = packetSequence(header);
+ bool ready = false;
+
+ if (isSplitPacket(header) && seq == 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 = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask);
+ #else
+ read_len = 0;
+ #endif
+
+ seq = sequence;
+
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ last_rssi = LoRa->packetRssi();
+ last_snr_raw = LoRa->packetSnrRaw();
+ #endif
+
+ getPacketData(packet_size);
+
+ } else if (isSplitPacket(header) && seq == sequence) {
+ // This is the second part of a split
+ // packet, so we add it to the buffer
+ // and set the ready flag.
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ last_rssi = (last_rssi+LoRa->packetRssi())/2;
+ last_snr_raw = (last_snr_raw+LoRa->packetSnrRaw())/2;
+ #endif
+
+ getPacketData(packet_size);
+ seq = SEQ_UNSET;
+ ready = true;
+
+ } else if (isSplitPacket(header) && seq != 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 = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask);
+ #else
+ read_len = 0;
+ #endif
+ seq = sequence;
+
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ last_rssi = LoRa->packetRssi();
+ last_snr_raw = LoRa->packetSnrRaw();
+ #endif
+
+ getPacketData(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 != 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 = 0; taskEXIT_CRITICAL_FROM_ISR(int_mask);
+ #else
+ read_len = 0;
+ #endif
+ seq = SEQ_UNSET;
+ }
+
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ last_rssi = LoRa->packetRssi();
+ last_snr_raw = LoRa->packetSnrRaw();
+ #endif
+
+ getPacketData(packet_size);
+ ready = true;
+ }
+
+ if (ready) {
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ // We first signal the RSSI of the
+ // recieved packet to the host.
+ kiss_indicate_stat_rssi();
+ kiss_indicate_stat_snr();
+
+ // And then write the entire packet
+ kiss_write_packet();
+
+ #else
+ // 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);
+ if(!modem_packet) { memory_low = true; return; }
+
+ // Get packet RSSI and SNR
+ #if MCU_VARIANT == MCU_ESP32
+ modem_packet->snr_raw = LoRa->packetSnrRaw();
+ modem_packet->rssi = LoRa->packetRssi(modem_packet->snr_raw);
+ #endif
+
+ // 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;
+ memcpy(modem_packet->data, pbuf, read_len);
+ if (!modem_packet_queue || xQueueSendFromISR(modem_packet_queue, &modem_packet, NULL) != pdPASS) {
+ free(modem_packet);
+ }
+ #endif
+ }
+ } else {
+ // In promiscuous mode, raw packets are
+ // output directly to the host
+ read_len = 0;
+
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ last_rssi = LoRa->packetRssi();
+ last_snr_raw = LoRa->packetSnrRaw();
+ getPacketData(packet_size);
+
+ // We first signal the RSSI of the
+ // recieved packet to the host.
+ kiss_indicate_stat_rssi();
+ kiss_indicate_stat_snr();
+
+ // And then write the entire packet
+ kiss_write_packet();
+
+ #else
+ getPacketData(packet_size);
+ packet_ready = true;
+ #endif
+ }
+}
+
+bool startRadio() {
+ update_radio_lock();
+ if (!radio_online && !console_active) {
+ if (!radio_locked && hw_ready) {
+ if (!LoRa->begin(lora_freq)) {
+ // The radio could not be started.
+ // Indicate this failure over both the
+ // serial port and with the onboard LEDs
+ radio_error = true;
+ kiss_indicate_error(ERROR_INITRADIO);
+ led_indicate_error(0);
+ return false;
+ } else {
+ radio_online = true;
+
+ init_channel_stats();
+
+ setTXPower();
+ setBandwidth();
+ setSpreadingFactor();
+ setCodingRate();
+ getFrequency();
+
+ LoRa->enableCrc();
+ LoRa->onReceive(receive_callback);
+ lora_receive();
+
+ // Flash an info pattern to indicate
+ // that the radio is now on
+ kiss_indicate_radiostate();
+ led_indicate_info(3);
+ return true;
+ }
+
+ } else {
+ // Flash a warning pattern to indicate
+ // that the radio was locked, and thus
+ // not started
+ radio_online = false;
+ kiss_indicate_radiostate();
+ led_indicate_warning(3);
+ return false;
+ }
+ } else {
+ // If radio is already on, we silently
+ // ignore the request.
+ kiss_indicate_radiostate();
+ return true;
+ }
+}
+
+void stopRadio() {
+ LoRa->end();
+ radio_online = false;
+}
+
+void update_radio_lock() {
+ if (lora_freq != 0 && lora_bw != 0 && lora_txp != 0xFF && lora_sf != 0) {
+ radio_locked = false;
+ } else {
+ radio_locked = true;
+ }
+}
+
+bool queue_full() { return (queue_height >= CONFIG_QUEUE_MAX_LENGTH || queued_bytes >= CONFIG_QUEUE_SIZE); }
+
+volatile bool queue_flushing = false;
+void flush_queue(void) {
+ if (!queue_flushing) {
+ queue_flushing = true;
+ led_tx_on(); uint16_t processed = 0;
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ while (!fifo16_isempty(&packet_starts)) {
+ #else
+ while (!fifo16_isempty_locked(&packet_starts)) {
+ #endif
+
+ uint16_t start = fifo16_pop(&packet_starts);
+ uint16_t length = fifo16_pop(&packet_lengths);
+
+ if (length >= MIN_L && length <= MTU) {
+ for (uint16_t i = 0; i < length; i++) {
+ uint16_t pos = (start+i)%CONFIG_QUEUE_SIZE;
+ tbuf[i] = packet_queue[pos];
+ }
+
+ transmit(length); processed++;
+ }
+ }
+
+ lora_receive(); led_tx_off();
+ }
+
+ queue_height = 0;
+ queued_bytes = 0;
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ update_airtime();
+ #endif
+
+ queue_flushing = false;
+
+ #if HAS_DISPLAY
+ display_tx = true;
+ #endif
+}
+
+void pop_queue() {
+ if (!queue_flushing) {
+ queue_flushing = true;
+ led_tx_on(); uint16_t processed = 0;
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ if (!fifo16_isempty(&packet_starts)) {
+ #else
+ if (!fifo16_isempty_locked(&packet_starts)) {
+ #endif
+
+ uint16_t start = fifo16_pop(&packet_starts);
+ uint16_t length = fifo16_pop(&packet_lengths);
+ if (length >= MIN_L && length <= MTU) {
+ for (uint16_t i = 0; i < length; i++) {
+ uint16_t pos = (start+i)%CONFIG_QUEUE_SIZE;
+ tbuf[i] = packet_queue[pos];
+ }
+
+ transmit(length); processed++;
+ }
+ queue_height -= processed;
+ queued_bytes -= length;
+ }
+
+ lora_receive(); led_tx_off();
+ }
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ update_airtime();
+ #endif
+
+ queue_flushing = false;
+
+ #if HAS_DISPLAY
+ display_tx = true;
+ #endif
+}
+
+void add_airtime(uint16_t written) {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ float lora_symbols = 0;
+ float packet_cost_ms = 0.0;
+ int ldr_opt = 0; if (lora_low_datarate) ldr_opt = 1;
+
+ #if MODEM == SX1276 || MODEM == SX1278
+ lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + 8 + PHY_HEADER_LORA_SYMBOLS);
+ lora_symbols /= 4*(lora_sf-2*ldr_opt);
+ lora_symbols *= lora_cr;
+ lora_symbols += lora_preamble_symbols + 0.25 + 8;
+ packet_cost_ms += lora_symbols * lora_symbol_time_ms;
+
+ #elif MODEM == SX1262 || MODEM == SX1280
+ if (lora_sf < 7) {
+ lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + PHY_HEADER_LORA_SYMBOLS);
+ lora_symbols /= 4*lora_sf;
+ lora_symbols *= lora_cr;
+ lora_symbols += lora_preamble_symbols + 2.25 + 8;
+ packet_cost_ms += lora_symbols * lora_symbol_time_ms;
+
+ } else {
+ lora_symbols += (8*written + PHY_CRC_LORA_BITS - 4*lora_sf + 8 + PHY_HEADER_LORA_SYMBOLS);
+ lora_symbols /= 4*(lora_sf-2*ldr_opt);
+ lora_symbols *= lora_cr;
+ lora_symbols += lora_preamble_symbols + 0.25 + 8;
+ packet_cost_ms += lora_symbols * lora_symbol_time_ms;
+ }
+
+ #endif
+
+ uint16_t cb = current_airtime_bin();
+ uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; }
+ airtime_bins[cb] += packet_cost_ms;
+ airtime_bins[nb] = 0;
+
+ #endif
+}
+
+void update_airtime() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ uint16_t cb = current_airtime_bin();
+ uint16_t pb = cb-1; if (cb-1 < 0) { pb = AIRTIME_BINS-1; }
+ uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; }
+ airtime_bins[nb] = 0; airtime = (float)(airtime_bins[cb]+airtime_bins[pb])/(2.0*AIRTIME_BINLEN_MS);
+
+ uint32_t longterm_airtime_sum = 0;
+ for (uint16_t bin = 0; bin < AIRTIME_BINS; bin++) { longterm_airtime_sum += airtime_bins[bin]; }
+ longterm_airtime = (float)longterm_airtime_sum/(float)AIRTIME_LONGTERM_MS;
+
+ float longterm_channel_util_sum = 0.0;
+ for (uint16_t bin = 0; bin < AIRTIME_BINS; bin++) { longterm_channel_util_sum += longterm_bins[bin]; }
+ longterm_channel_util = (float)longterm_channel_util_sum/(float)AIRTIME_BINS;
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ update_csma_parameters();
+ #endif
+
+ kiss_indicate_channel_stats();
+ #endif
+}
+
+void transmit(uint16_t size) {
+ if (radio_online) {
+ if (!promisc) {
+ uint16_t written = 0;
+ uint8_t header = random(256) & 0xF0;
+ if (size > SINGLE_MTU - HEADER_L) { header = header | FLAG_SPLIT; }
+
+ LoRa->beginPacket();
+ LoRa->write(header); written++;
+
+ for (uint16_t i=0; i < size; i++) {
+ LoRa->write(tbuf[i]); written++;
+
+ if (written == 255 && isSplitPacket(header)) {
+ if (!LoRa->endPacket()) {
+ kiss_indicate_error(ERROR_MODEM_TIMEOUT);
+ kiss_indicate_error(ERROR_TXFAILED);
+ led_indicate_error(5);
+ hard_reset();
+ }
+
+ add_airtime(written);
+ LoRa->beginPacket();
+ LoRa->write(header);
+ written = 1;
+ }
+ }
+
+ if (!LoRa->endPacket()) {
+ kiss_indicate_error(ERROR_MODEM_TIMEOUT);
+ kiss_indicate_error(ERROR_TXFAILED);
+ led_indicate_error(5);
+ hard_reset();
+ }
+
+ add_airtime(written);
+
+ } else {
+ led_tx_on(); uint16_t written = 0;
+ if (size > SINGLE_MTU) { size = SINGLE_MTU; }
+ if (!implicit) { LoRa->beginPacket(); }
+ else { LoRa->beginPacket(size); }
+ for (uint16_t i=0; i < size; i++) { LoRa->write(tbuf[i]); written++; }
+ LoRa->endPacket(); add_airtime(written);
+ }
+
+ } 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 (!fifo16_isfull(&packet_starts) && queued_bytes < CONFIG_QUEUE_SIZE) {
+ uint16_t s = current_packet_start;
+ int16_t e = queue_cursor-1; if (e == -1) e = CONFIG_QUEUE_SIZE-1;
+ uint16_t l;
+
+ if (s != e) { l = (s < e) ? e - s + 1 : CONFIG_QUEUE_SIZE - s + e + 1; }
+ else { l = 1; }
+
+ if (l >= MIN_L) {
+ queue_height++;
+ fifo16_push(&packet_starts, s);
+ fifo16_push(&packet_lengths, l);
+ current_packet_start = queue_cursor;
+ }
+ }
+
+ } 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_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 (queue_height < CONFIG_QUEUE_MAX_LENGTH && queued_bytes < CONFIG_QUEUE_SIZE) {
+ queued_bytes++;
+ packet_queue[queue_cursor++] = sbyte;
+ if (queue_cursor == CONFIG_QUEUE_SIZE) queue_cursor = 0;
+ }
+ }
+ } 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) {
+ 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();
+ } else {
+ lora_freq = freq;
+ if (op_mode == MODE_HOST) setFrequency();
+ kiss_indicate_frequency();
+ }
+ }
+ } 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) {
+ 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();
+ } else {
+ lora_bw = bw;
+ if (op_mode == MODE_HOST) setBandwidth();
+ kiss_indicate_bandwidth();
+ }
+ }
+ } else if (command == CMD_TXPOWER) {
+ if (sbyte == 0xFF) {
+ kiss_indicate_txpower();
+ } else {
+ int txp = sbyte;
+ #if MODEM == SX1262
+ #if HAS_LORA_PA
+ if (txp > PA_MAX_OUTPUT) txp = PA_MAX_OUTPUT;
+ #else
+ if (txp > 22) txp = 22;
+ #endif
+ #elif MODEM == SX1280
+ #if HAS_PA
+ if (txp > 20) txp = 20;
+ #else
+ if (txp > 13) txp = 13;
+ #endif
+ #else
+ if (txp > 17) txp = 17;
+ #endif
+
+ lora_txp = txp;
+ if (op_mode == MODE_HOST) setTXPower();
+ kiss_indicate_txpower();
+ }
+ } else if (command == CMD_SF) {
+ if (sbyte == 0xFF) {
+ kiss_indicate_spreadingfactor();
+ } else {
+ int sf = sbyte;
+ if (sf < 5) sf = 5;
+ if (sf > 12) sf = 12;
+
+ lora_sf = sf;
+ if (op_mode == MODE_HOST) setSpreadingFactor();
+ kiss_indicate_spreadingfactor();
+ }
+ } else if (command == CMD_CR) {
+ if (sbyte == 0xFF) {
+ kiss_indicate_codingrate();
+ } else {
+ int cr = sbyte;
+ if (cr < 5) cr = 5;
+ if (cr > 8) cr = 8;
+
+ lora_cr = cr;
+ if (op_mode == MODE_HOST) setCodingRate();
+ kiss_indicate_codingrate();
+ }
+ } 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) {
+ if (bt_state != BT_STATE_CONNECTED) {
+ cable_state = CABLE_STATE_CONNECTED;
+ display_unblank();
+ }
+ if (sbyte == 0xFF) {
+ kiss_indicate_radiostate();
+ } else if (sbyte == 0x00) {
+ stopRadio();
+ kiss_indicate_radiostate();
+ } else if (sbyte == 0x01) {
+ startRadio();
+ kiss_indicate_radiostate();
+ }
+ } 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) {
+ uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1];
+
+ if (at == 0) {
+ st_airtime_limit = 0.0;
+ } else {
+ st_airtime_limit = (float)at/(100.0*100.0);
+ if (st_airtime_limit >= 1.0) { st_airtime_limit = 0.0; }
+ }
+ kiss_indicate_st_alock();
+ }
+ } 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) {
+ uint16_t at = (uint16_t)cmdbuf[0] << 8 | (uint16_t)cmdbuf[1];
+
+ if (at == 0) {
+ lt_airtime_limit = 0.0;
+ } else {
+ lt_airtime_limit = (float)at/(100.0*100.0);
+ if (lt_airtime_limit >= 1.0) { lt_airtime_limit = 0.0; }
+ }
+ kiss_indicate_lt_alock();
+ }
+ } 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();
+ } else if (command == CMD_RADIO_LOCK) {
+ update_radio_lock();
+ kiss_indicate_radio_lock();
+ } else if (command == CMD_BLINK) {
+ led_indicate_info(sbyte);
+ } else if (command == CMD_RANDOM) {
+ kiss_indicate_random(getRandom());
+ } 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) {
+ if (!queue_full()) {
+ 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) {
+ eeprom_conf_save();
+ } 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 MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ if (sbyte != 0x00) {
+ kiss_indicate_device_hash();
+ }
+ #endif
+ } else if (command == CMD_DEV_SIG) {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ 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();
+ }
+ #endif
+ } else if (command == CMD_FW_UPD) {
+ if (sbyte == 0x01) {
+ firmware_update_mode = true;
+ } else {
+ firmware_update_mode = false;
+ }
+ } else if (command == CMD_HASHES) {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ 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();
+ }
+ #endif
+ } else if (command == CMD_FW_HASH) {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ 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();
+ }
+ #endif
+ } 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() {
+ update_modem_status();
+ if (avoid_interference && interference_detected) { return false; }
+ return !dcd;
+}
+
+bool noise_floor_sampled = false;
+int noise_floor_sample = 0;
+int noise_floor_buffer[NOISE_FLOOR_SAMPLES] = {0};
+void update_noise_floor() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ if (!dcd) {
+ if (!noise_floor_sampled || current_rssi < noise_floor + CSMA_INFR_THRESHOLD_DB) {
+ noise_floor_buffer[noise_floor_sample] = current_rssi;
+ noise_floor_sample = noise_floor_sample+1;
+ if (noise_floor_sample >= NOISE_FLOOR_SAMPLES) {
+ noise_floor_sample %= NOISE_FLOOR_SAMPLES;
+ noise_floor_sampled = true;
+ }
+
+ if (noise_floor_sampled) {
+ noise_floor = 0;
+ for (int ni = 0; ni < NOISE_FLOOR_SAMPLES; ni++) { noise_floor += noise_floor_buffer[ni]; }
+ noise_floor /= NOISE_FLOOR_SAMPLES;
+ }
+ }
+ }
+ #endif
+}
+
+#define LED_ID_TRIG 16
+uint8_t led_id_filter = 0;
+void update_modem_status() {
+ #if MCU_VARIANT == MCU_ESP32
+ portENTER_CRITICAL(&update_lock);
+ #elif MCU_VARIANT == MCU_NRF52
+ portENTER_CRITICAL();
+ #endif
+
+ bool carrier_detected = LoRa->dcd();
+ current_rssi = LoRa->currentRssi();
+ last_status_update = millis();
+
+ #if MCU_VARIANT == MCU_ESP32
+ portEXIT_CRITICAL(&update_lock);
+ #elif MCU_VARIANT == MCU_NRF52
+ portEXIT_CRITICAL();
+ #endif
+
+ interference_detected = !carrier_detected && (current_rssi > (noise_floor+CSMA_INFR_THRESHOLD_DB));
+ if (interference_detected) { if (led_id_filter < LED_ID_TRIG) { led_id_filter += 1; } }
+ else { if (led_id_filter > 0) {led_id_filter -= 1; } }
+
+ if (carrier_detected) { dcd = true; } else { dcd = false; }
+
+ dcd_led = dcd;
+ if (dcd_led) { led_rx_on(); }
+ else {
+ if (interference_detected) {
+ if (led_id_filter >= LED_ID_TRIG && noise_floor_sampled) { led_id_on(); }
+ } else {
+ if (airtime_lock) { led_indicate_airtime_lock(); }
+ else { led_rx_off(); led_id_off(); }
+ }
+ }
+}
+
+void check_modem_status() {
+ if (millis()-last_status_update >= status_interval_ms) {
+ update_modem_status();
+ update_noise_floor();
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ util_samples[dcd_sample] = dcd;
+ dcd_sample = (dcd_sample+1)%DCD_SAMPLES;
+ if (dcd_sample % UTIL_UPDATE_INTERVAL == 0) {
+ int util_count = 0;
+ for (int ui = 0; ui < DCD_SAMPLES; ui++) {
+ if (util_samples[ui]) util_count++;
+ }
+ local_channel_util = (float)util_count / (float)DCD_SAMPLES;
+ total_channel_util = local_channel_util + airtime;
+ if (total_channel_util > 1.0) total_channel_util = 1.0;
+
+ int16_t cb = current_airtime_bin();
+ uint16_t nb = cb+1; if (nb == AIRTIME_BINS) { nb = 0; }
+ if (total_channel_util > longterm_bins[cb]) longterm_bins[cb] = total_channel_util;
+ longterm_bins[nb] = 0.0;
+
+ update_airtime();
+ }
+ #endif
+ }
+}
+
+void validate_status() {
+ #if MCU_VARIANT == MCU_1284P
+ uint8_t boot_flags = OPTIBOOT_MCUSR;
+ uint8_t F_POR = PORF;
+ uint8_t F_BOR = BORF;
+ uint8_t F_WDR = WDRF;
+ #elif MCU_VARIANT == MCU_2560
+ uint8_t boot_flags = OPTIBOOT_MCUSR;
+ if (boot_flags == 0x00) boot_flags = 0x03;
+ uint8_t F_POR = PORF;
+ uint8_t F_BOR = BORF;
+ uint8_t F_WDR = WDRF;
+ #elif 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< CSMA_CW_BANDS) { new_cw_band = CSMA_CW_BANDS; }
+ if (new_cw_band != cw_band) {
+ cw_band = (uint8_t)(new_cw_band);
+ cw_min = (cw_band-1) * CSMA_CW_PER_BAND_WINDOWS;
+ cw_max = (cw_band) * CSMA_CW_PER_BAND_WINDOWS - 1;
+ kiss_indicate_csma_stats();
+ }
+ }
+#endif
+
+void tx_queue_handler() {
+ if (!airtime_lock && queue_height > 0) {
+ if (csma_cw == -1) {
+ csma_cw = random(cw_min, cw_max);
+ cw_wait_target = csma_cw * csma_slot_ms;
+ }
+
+ if (difs_wait_start == -1) { // DIFS wait not yet started
+ if (medium_free()) { difs_wait_start = 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()) { difs_wait_start = -1; cw_wait_start = -1; return; } // Medium became occupied while in DIFS wait, restart waiting when free again
+ else { // Medium is free, so continue waiting
+ if (millis() < difs_wait_start+difs_ms) { return; } // DIFS has not yet passed, continue waiting
+ else { // DIFS has passed, and we are now in CW wait
+ if (cw_wait_start == -1) { cw_wait_start = 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
+ cw_wait_passed += millis()-cw_wait_start; cw_wait_start = millis();
+ if (cw_wait_passed < cw_wait_target) { return; } // Contention window wait time has not yet passed, continue waiting
+ else { // Wait time has passed, flush the queue
+ if (!lora_limit_rate) { flush_queue(); } else { pop_queue(); }
+ cw_wait_passed = 0; csma_cw = -1; difs_wait_start = -1; }
+ }
+ }
+ }
+ }
+ }
+}
+
+void work_while_waiting() { loop(); }
+
+void loop() {
+ if (radio_online) {
+ #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) {
+ read_len = 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();
+ kiss_indicate_stat_snr();
+ kiss_write_packet();
+ }
+
+ airtime_lock = false;
+ if (st_airtime_limit != 0.0 && airtime >= st_airtime_limit) airtime_lock = true;
+ if (lt_airtime_limit != 0.0 && longterm_airtime >= lt_airtime_limit) airtime_lock = true;
+
+ #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) {
+ memcpy(&pbuf, modem_packet->data, modem_packet->len);
+ read_len = modem_packet->len;
+ free(modem_packet);
+ modem_packet = NULL;
+
+ portENTER_CRITICAL();
+ last_rssi = LoRa->packetRssi();
+ last_snr_raw = LoRa->packetSnrRaw();
+ portEXIT_CRITICAL();
+ kiss_indicate_stat_rssi();
+ kiss_indicate_stat_snr();
+ kiss_write_packet();
+ }
+
+ airtime_lock = false;
+ if (st_airtime_limit != 0.0 && airtime >= st_airtime_limit) airtime_lock = true;
+ if (lt_airtime_limit != 0.0 && longterm_airtime >= lt_airtime_limit) airtime_lock = true;
+
+ #endif
+
+ tx_queue_handler();
+ check_modem_status();
+
+ } else {
+ if (hw_ready) {
+ if (console_active) {
+ #if HAS_CONSOLE
+ console_loop();
+ #endif
+ } else {
+ led_indicate_standby();
+ }
+ } else {
+
+ led_indicate_not_ready();
+ stopRadio();
+ }
+ }
+
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ buffer_serial();
+ if (!fifo_isempty(&serialFIFO)) serial_poll();
+ #else
+ if (!fifo_isempty_locked(&serialFIFO)) serial_poll();
+ #endif
+
+ #if HAS_DISPLAY
+ if (disp_ready && !display_updating) update_display();
+ #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 (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 sleep_now() {
+ #if HAS_SLEEP == true
+ stopRadio(); // TODO: Check this on all platforms
+ #if PLATFORM == PLATFORM_ESP32
+ #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_XIAO_S3
+ #if HAS_DISPLAY
+ display_intensity = 0;
+ update_display(true);
+ #endif
+ #endif
+ #if BOARD_MODEL == BOARD_HELTEC32_V4
+ digitalWrite(LORA_PA_CPS, LOW);
+ digitalWrite(LORA_PA_CSD, LOW);
+ digitalWrite(LORA_PA_PWR_EN, LOW);
+ digitalWrite(Vext, HIGH);
+ #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 MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ 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
+ }
+ }
+ #endif
+}
+
+volatile bool serial_polling = false;
+void serial_poll() {
+ serial_polling = true;
+
+ #if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ while (!fifo_isempty_locked(&serialFIFO)) {
+ #else
+ while (!fifo_isempty(&serialFIFO)) {
+ #endif
+ char sbyte = fifo_pop(&serialFIFO);
+ serial_callback(sbyte);
+ }
+
+ serial_polling = false;
+}
+
+#if MCU_VARIANT != MCU_ESP32
+ #define MAX_CYCLES 20
+#else
+ #define MAX_CYCLES 10
+#endif
+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 MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ if (!fifo_isfull_locked(&serialFIFO)) {
+ fifo_push_locked(&serialFIFO, Serial.read());
+ }
+ #elif 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;
+ }
+}
+
+void serial_interrupt_init() {
+ #if MCU_VARIANT == MCU_1284P
+ TCCR3A = 0;
+ TCCR3B = _BV(CS10) |
+ _BV(WGM33)|
+ _BV(WGM32);
+
+ // Buffer incoming frames every 1ms
+ ICR3 = 16000;
+ TIMSK3 = _BV(ICIE3);
+
+ #elif MCU_VARIANT == MCU_2560
+ // TODO: This should probably be updated for
+ // atmega2560 support. Might be source of
+ // reported issues from snh.
+ TCCR3A = 0;
+ TCCR3B = _BV(CS10) |
+ _BV(WGM33)|
+ _BV(WGM32);
+
+ // Buffer incoming frames every 1ms
+ ICR3 = 16000;
+ TIMSK3 = _BV(ICIE3);
+
+ #elif MCU_VARIANT == MCU_ESP32
+ // No interrupt-based polling on ESP32
+ #endif
+
+}
+
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ ISR(TIMER3_CAPT_vect) { buffer_serial(); }
+#endif
diff --git a/RNode_Firmware/ROM.h b/RNode_Firmware/ROM.h
new file mode 100644
index 0000000..27c38bb
--- /dev/null
+++ b/RNode_Firmware/ROM.h
@@ -0,0 +1,55 @@
+// 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 .
+
+#ifndef ROM_H
+ #define ROM_H
+ #define CHECKSUMMED_SIZE 0x0B
+
+ // ROM address map ///////////////
+ #define ADDR_PRODUCT 0x00
+ #define ADDR_MODEL 0x01
+ #define ADDR_HW_REV 0x02
+ #define ADDR_SERIAL 0x03
+ #define ADDR_MADE 0x07
+ #define ADDR_CHKSUM 0x0B
+ #define ADDR_SIGNATURE 0x1B
+ #define ADDR_INFO_LOCK 0x9B
+
+ #define ADDR_CONF_SF 0x9C
+ #define ADDR_CONF_CR 0x9D
+ #define ADDR_CONF_TXP 0x9E
+ #define ADDR_CONF_BW 0x9F
+ #define ADDR_CONF_FREQ 0xA3
+ #define ADDR_CONF_OK 0xA7
+
+ #define ADDR_CONF_BT 0xB0
+ #define ADDR_CONF_DSET 0xB1
+ #define ADDR_CONF_DINT 0xB2
+ #define ADDR_CONF_DADR 0xB3
+ #define ADDR_CONF_DBLK 0xB4
+ #define ADDR_CONF_DROT 0xB8
+ #define ADDR_CONF_PSET 0xB5
+ #define ADDR_CONF_PINT 0xB6
+ #define ADDR_CONF_BSET 0xB7
+ #define ADDR_CONF_DIA 0xB9
+
+ #define INFO_LOCK_BYTE 0x73
+ #define CONF_OK_BYTE 0x73
+ #define BT_ENABLE_BYTE 0x73
+
+ #define EEPROM_RESERVED 200
+ //////////////////////////////////
+
+#endif
diff --git a/RNode_Firmware/ST7789.h b/RNode_Firmware/ST7789.h
new file mode 100644
index 0000000..85012e8
--- /dev/null
+++ b/RNode_Firmware/ST7789.h
@@ -0,0 +1,440 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn
+ * Copyright (c) 2018 by Fabrice Weinberg
+ * Copyright (c) 2024 by Heltec AutoMation
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * ThingPulse invests considerable time and money to develop these open source libraries.
+ * Please support us by buying our products (and not the clones) from
+ * https://thingpulse.com
+ *
+ */
+
+#ifndef ST7789Spi_h
+#define ST7789Spi_h
+
+#include "OLEDDisplay.h"
+#include
+
+
+#define ST_CMD_DELAY 0x80 // special signifier for command lists
+
+#define ST77XX_NOP 0x00
+#define ST77XX_SWRESET 0x01
+#define ST77XX_RDDID 0x04
+#define ST77XX_RDDST 0x09
+
+#define ST77XX_SLPIN 0x10
+#define ST77XX_SLPOUT 0x11
+#define ST77XX_PTLON 0x12
+#define ST77XX_NORON 0x13
+
+#define ST77XX_INVOFF 0x20
+#define ST77XX_INVON 0x21
+#define ST77XX_DISPOFF 0x28
+#define ST77XX_DISPON 0x29
+#define ST77XX_CASET 0x2A
+#define ST77XX_RASET 0x2B
+#define ST77XX_RAMWR 0x2C
+#define ST77XX_RAMRD 0x2E
+
+#define ST77XX_PTLAR 0x30
+#define ST77XX_TEOFF 0x34
+#define ST77XX_TEON 0x35
+#define ST77XX_MADCTL 0x36
+#define ST77XX_COLMOD 0x3A
+
+#define ST77XX_MADCTL_MY 0x80
+#define ST77XX_MADCTL_MX 0x40
+#define ST77XX_MADCTL_MV 0x20
+#define ST77XX_MADCTL_ML 0x10
+#define ST77XX_MADCTL_RGB 0x00
+
+#define ST77XX_RDID1 0xDA
+#define ST77XX_RDID2 0xDB
+#define ST77XX_RDID3 0xDC
+#define ST77XX_RDID4 0xDD
+
+// Some ready-made 16-bit ('565') color settings:
+#define ST77XX_BLACK 0x0000
+#define ST77XX_WHITE 0xFFFF
+#define ST77XX_RED 0xF800
+#define ST77XX_GREEN 0x07E0
+#define ST77XX_BLUE 0x001F
+#define ST77XX_CYAN 0x07FF
+#define ST77XX_MAGENTA 0xF81F
+#define ST77XX_YELLOW 0xFFE0
+#define ST77XX_ORANGE 0xFC00
+
+#define LED_A_ON LOW
+
+#ifdef ESP_PLATFORM
+#undef LED_A_ON
+#define LED_A_ON HIGH
+#define rtos_free free
+#define rtos_malloc malloc
+//SPIClass SPI1(HSPI);
+#endif
+class ST7789Spi : public OLEDDisplay {
+ private:
+ uint8_t _rst;
+ uint8_t _dc;
+ uint8_t _cs;
+ uint8_t _ledA;
+ int _miso;
+ int _mosi;
+ int _clk;
+ SPIClass * _spi;
+ SPISettings _spiSettings;
+ uint16_t _RGB=0xFFFF;
+ uint8_t _buffheight;
+ public:
+ /* pass _cs as -1 to indicate "do not use CS pin", for cases where it is hard wired low */
+ ST7789Spi(SPIClass *spiClass,uint8_t _rst, uint8_t _dc, uint8_t _cs, OLEDDISPLAY_GEOMETRY g = GEOMETRY_RAWMODE,uint16_t width=240,uint16_t height=320,int mosi=-1,int miso=-1,int clk=-1) {
+ this->_spi = spiClass;
+ this->_rst = _rst;
+ this->_dc = _dc;
+ this->_cs = _cs;
+ this->_mosi=mosi;
+ this->_miso=miso;
+ this->_clk=clk;
+ //this->_ledA = _ledA;
+ _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0);
+ setGeometry(g,width,height);
+ }
+
+ bool connect(){
+ this->_buffheight=displayHeight / 8;
+ this->_buffheight+=displayHeight % 8 ? 1:0;
+ pinMode(_cs, OUTPUT);
+ pinMode(_dc, OUTPUT);
+ //pinMode(_ledA, OUTPUT);
+ if (_cs != (uint8_t) -1) {
+ pinMode(_cs, OUTPUT);
+ }
+ pinMode(_rst, OUTPUT);
+
+#ifdef ESP_PLATFORM
+ _spi->begin(_clk,_miso,_mosi,-1);
+#else
+ _spi->begin();
+#endif
+ _spi->setClockDivider (SPI_CLOCK_DIV2);
+
+ // Pulse Reset low for 10ms
+ digitalWrite(_rst, HIGH);
+ delay(1);
+ digitalWrite(_rst, LOW);
+ delay(10);
+ digitalWrite(_rst, HIGH);
+ _spi->begin ();
+ //digitalWrite(_ledA, LED_A_ON);
+ return true;
+ }
+
+ void display(void) {
+ #ifdef OLEDDISPLAY_DOUBLE_BUFFER
+
+ uint16_t minBoundY = UINT16_MAX;
+ uint16_t maxBoundY = 0;
+
+ uint16_t minBoundX = UINT16_MAX;
+ uint16_t maxBoundX = 0;
+
+ uint16_t x, y;
+
+ // Calculate the Y bounding box of changes
+ // and copy buffer[pos] to buffer_back[pos];
+ for (y = 0; y < _buffheight; y++) {
+ for (x = 0; x < displayWidth; x++) {
+ //Serial.printf("x %d y %d\r\n",x,y);
+ uint16_t pos = x + y * displayWidth;
+ if (buffer[pos] != buffer_back[pos]) {
+ minBoundY = min(minBoundY, y);
+ maxBoundY = max(maxBoundY, y);
+ minBoundX = min(minBoundX, x);
+ maxBoundX = max(maxBoundX, x);
+ }
+ buffer_back[pos] = buffer[pos];
+ }
+ yield();
+ }
+
+ // If the minBoundY wasn't updated
+ // we can savely assume that buffer_back[pos] == buffer[pos]
+ // holdes true for all values of pos
+ if (minBoundY == UINT16_MAX) return;
+
+ set_CS(LOW);
+ _spi->beginTransaction(_spiSettings);
+
+ for (y = minBoundY; y <= maxBoundY; y++)
+ {
+ for(int temp = 0; temp<8;temp++)
+ {
+ //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
+ setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
+ //setAddrWindow(y*8+temp,minBoundX,1,maxBoundX-minBoundX+1);
+ uint32_t const pixbufcount = maxBoundX-minBoundX+1;
+ uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount);
+ for (x = minBoundX; x <= maxBoundX; x++)
+ {
+ pixbuf[x-minBoundX] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0;
+ }
+#ifdef ESP_PLATFORM
+ _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount);
+#else
+ _spi->transfer(pixbuf, NULL, 2 * pixbufcount);
+#endif
+ rtos_free(pixbuf);
+ }
+ }
+ _spi->endTransaction();
+ set_CS(HIGH);
+
+ #else
+ set_CS(LOW);
+ _spi->beginTransaction(_spiSettings);
+ uint8_t x, y;
+ for (y = 0; y < _buffheight; y++)
+ {
+ for(int temp = 0; temp<8;temp++)
+ {
+ //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
+ //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
+ setAddrWindow(y*8+temp,0,1,displayWidth);
+ uint32_t const pixbufcount = displayWidth;
+ uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount);
+ for (x = 0; x < displayWidth; x++)
+ {
+ pixbuf[x] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0;
+ }
+#ifdef ESP_PLATFORM
+ _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount);
+#else
+ _spi->transfer(pixbuf, NULL, 2 * pixbufcount);
+#endif
+ rtos_free(pixbuf);
+ }
+ }
+ _spi->endTransaction();
+ set_CS(HIGH);
+
+ #endif
+ }
+
+ virtual void resetOrientation() {
+ uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX;
+ sendCommand(ST77XX_MADCTL);
+ WriteData(madctl);
+ delay(10);
+ }
+
+ virtual void flipScreenVertically() {
+ uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MY;
+ sendCommand(ST77XX_MADCTL);
+ WriteData(madctl);
+ delay(10);
+ }
+
+ virtual void mirrorScreen() {
+ uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX|ST77XX_MADCTL_MY;
+ sendCommand(ST77XX_MADCTL);
+ WriteData(madctl);
+ delay(10);
+ }
+
+ virtual void setRotation(uint8_t r) {
+ uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX;
+ if (r == 1) { madctl = 0xC0; }
+ if (r == 2) { madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MY; }
+ if (r == 3) { madctl = 0x00; }
+ sendCommand(ST77XX_MADCTL);
+ WriteData(madctl);
+ delay(10);
+ }
+
+ void setRGB(uint16_t c)
+ {
+
+ this->_RGB=0x00|c>>8|c<<8&0xFF00;
+ }
+
+ void displayOn(void) {
+ //sendCommand(DISPLAYON);
+ }
+
+ void displayOff(void) {
+ //sendCommand(DISPLAYOFF);
+ }
+
+//#define ST77XX_MADCTL_MY 0x80
+//#define ST77XX_MADCTL_MX 0x40
+//#define ST77XX_MADCTL_MV 0x20
+//#define ST77XX_MADCTL_ML 0x10
+ protected:
+ // Send all the init commands
+ virtual void sendInitCommands()
+ {
+ sendCommand(ST77XX_SWRESET); // 1: Software reset, no args, w/delay
+ delay(150);
+
+ sendCommand(ST77XX_SLPOUT); // 2: Out of sleep mode, no args, w/delay
+ delay(10);
+
+ sendCommand(ST77XX_COLMOD); // 3: Set color mode, 16-bit color
+ WriteData(0x55);
+ delay(10);
+
+ sendCommand(ST77XX_MADCTL); // 4: Mem access ctrl (directions), Row/col addr, bottom-top refresh
+ WriteData(0x08);
+
+ sendCommand(ST77XX_CASET); // 5: Column addr set,
+ WriteData(0x00);
+ WriteData(0x00); // XSTART = 0
+ WriteData(0x00);
+ WriteData(240); // XEND = 240
+
+ sendCommand(ST77XX_RASET); // 6: Row addr set,
+ WriteData(0x00);
+ WriteData(0x00); // YSTART = 0
+ WriteData(320>>8);
+ WriteData(320&0xFF); // YSTART = 320
+
+ sendCommand(ST77XX_SLPOUT); // 7: hack
+ delay(10);
+
+ sendCommand(ST77XX_NORON); // 8: Normal display on, no args, w/delay
+ delay(10);
+
+ sendCommand(ST77XX_DISPON); // 9: Main screen turn on, no args, delay
+ delay(10);
+
+ sendCommand(ST77XX_INVON); // 10: invert
+ delay(10);
+
+ //uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MX;
+ uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX;
+ sendCommand(ST77XX_MADCTL);
+ WriteData(madctl);
+ delay(10);
+ setRGB(ST77XX_GREEN);
+ }
+
+
+ private:
+
+ void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
+ x += (320-displayWidth)/2;
+ y += (240-displayHeight)/2;
+ uint32_t xa = ((uint32_t)x << 16) | (x + w - 1);
+ uint32_t ya = ((uint32_t)y << 16) | (y + h - 1);
+
+ writeCommand(ST77XX_CASET); // Column addr set
+ SPI_WRITE32(xa);
+
+ writeCommand(ST77XX_RASET); // Row addr set
+ SPI_WRITE32(ya);
+
+ writeCommand(ST77XX_RAMWR); // write to RAM
+ }
+ int getBufferOffset(void) {
+ return 0;
+ }
+ inline void set_CS(bool level) {
+ if (_cs != (uint8_t) -1) {
+ digitalWrite(_cs, level);
+ }
+ };
+ inline void sendCommand(uint8_t com) __attribute__((always_inline)){
+ set_CS(HIGH);
+ digitalWrite(_dc, LOW);
+ set_CS(LOW);
+ _spi->beginTransaction(_spiSettings);
+ _spi->transfer(com);
+ _spi->endTransaction();
+ set_CS(HIGH);
+ digitalWrite(_dc, HIGH);
+ }
+
+ inline void WriteData(uint8_t data) __attribute__((always_inline)){
+ digitalWrite(_cs, LOW);
+ _spi->beginTransaction(_spiSettings);
+ _spi->transfer(data);
+ _spi->endTransaction();
+ digitalWrite(_cs, HIGH);
+ }
+ void SPI_WRITE32(uint32_t l)
+ {
+ _spi->transfer(l >> 24);
+ _spi->transfer(l >> 16);
+ _spi->transfer(l >> 8);
+ _spi->transfer(l);
+ }
+ void writeCommand(uint8_t cmd) {
+ digitalWrite(_dc, LOW);
+ _spi->transfer(cmd);
+ digitalWrite(_dc, HIGH);
+ }
+
+// Private functions
+ void setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width, uint16_t height) {
+ this->geometry = g;
+
+ switch (g) {
+ case GEOMETRY_128_128:
+ this->displayWidth = 128;
+ this->displayHeight = 128;
+ break;
+ case GEOMETRY_128_64:
+ this->displayWidth = 128;
+ this->displayHeight = 64;
+ break;
+ case GEOMETRY_128_32:
+ this->displayWidth = 128;
+ this->displayHeight = 32;
+ break;
+ case GEOMETRY_64_48:
+ this->displayWidth = 64;
+ this->displayHeight = 48;
+ break;
+ case GEOMETRY_64_32:
+ this->displayWidth = 64;
+ this->displayHeight = 32;
+ break;
+ case GEOMETRY_RAWMODE:
+ this->displayWidth = width > 0 ? width : 128;
+ this->displayHeight = height > 0 ? height : 64;
+ break;
+ }
+ uint8_t tmp=displayHeight % 8;
+ uint8_t _buffheight=displayHeight / 8;
+
+ if(tmp!=0)
+ _buffheight++;
+ this->displayBufferSize = displayWidth * _buffheight ;
+ }
+
+
+
+};
+
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/Utilities.h b/RNode_Firmware/Utilities.h
new file mode 100644
index 0000000..0f85a80
--- /dev/null
+++ b/RNode_Firmware/Utilities.h
@@ -0,0 +1,1987 @@
+// 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 .
+
+#include "Config.h"
+
+#if HAS_EEPROM
+ #include
+#elif PLATFORM == PLATFORM_NRF52
+ #include
+ #include
+ #include
+ using namespace Adafruit_LittleFS_Namespace;
+ #define EEPROM_FILE "eeprom"
+ bool file_exists = false;
+ int written_bytes = 4;
+ File file(InternalFS);
+#endif
+#include
+
+#if MODEM == SX1262
+#include "sx126x.h"
+sx126x *LoRa = &sx126x_modem;
+#elif MODEM == SX1276 || MODEM == SX1278
+#include "sx127x.h"
+sx127x *LoRa = &sx127x_modem;
+#elif MODEM == SX1280
+#include "sx128x.h"
+sx128x *LoRa = &sx128x_modem;
+#endif
+
+#include "ROM.h"
+#include "Framing.h"
+#include "MD5.h"
+
+#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+uint8_t eeprom_read(uint32_t mapped_addr);
+#endif
+
+#if HAS_DISPLAY == true
+ #include "Display.h"
+#else
+ void display_unblank() {}
+ bool display_blanked = false;
+#endif
+
+#if HAS_BLUETOOTH == true || HAS_BLE == true
+ void kiss_indicate_btpin();
+ #include "Bluetooth.h"
+#endif
+
+#if HAS_PMU == true
+ #include "Power.h"
+#endif
+
+#if HAS_INPUT == true
+ #include "Input.h"
+#endif
+
+#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #include "Device.h"
+#endif
+#if MCU_VARIANT == MCU_ESP32
+ //https://github.com/espressif/esp-idf/issues/8855
+ #if BOARD_MODEL == BOARD_HELTEC32_V3
+ #include "hal/wdt_hal.h"
+ #elif BOARD_MODEL == BOARD_T3S3
+ #include "hal/wdt_hal.h"
+ #else
+ #include "hal/wdt_hal.h"
+ #endif
+ #define ISR_VECT IRAM_ATTR
+#else
+ #define ISR_VECT
+#endif
+
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ #include
+ #include
+#endif
+
+uint8_t boot_vector = 0x00;
+
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ uint8_t OPTIBOOT_MCUSR __attribute__ ((section(".noinit")));
+ void resetFlagsInit(void) __attribute__ ((naked)) __attribute__ ((used)) __attribute__ ((section (".init0")));
+ void resetFlagsInit(void) {
+ __asm__ __volatile__ ("sts %0, r2\n" : "=m" (OPTIBOOT_MCUSR) :);
+ }
+#elif MCU_VARIANT == MCU_ESP32
+ // TODO: Get ESP32 boot flags
+#elif MCU_VARIANT == MCU_NRF52
+ // TODO: Get NRF52 boot flags
+#endif
+
+#if MCU_VARIANT == MCU_NRF52
+ unsigned long get_rng_seed() {
+ nrf_rng_error_correction_enable(NRF_RNG);
+ nrf_rng_shorts_disable(NRF_RNG, NRF_RNG_SHORT_VALRDY_STOP_MASK);
+ nrf_rng_task_trigger(NRF_RNG, NRF_RNG_TASK_START);
+ while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY));
+ uint8_t rb_a = nrf_rng_random_value_get(NRF_RNG);
+ nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY);
+ while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY));
+ uint8_t rb_b = nrf_rng_random_value_get(NRF_RNG);
+ nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY);
+ while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY));
+ uint8_t rb_c = nrf_rng_random_value_get(NRF_RNG);
+ nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY);
+ while (!nrf_rng_event_check(NRF_RNG, NRF_RNG_EVENT_VALRDY));
+ uint8_t rb_d = nrf_rng_random_value_get(NRF_RNG);
+ nrf_rng_event_clear(NRF_RNG, NRF_RNG_EVENT_VALRDY);
+ nrf_rng_task_trigger(NRF_RNG, NRF_RNG_TASK_STOP);
+ return rb_a << 24 | rb_b << 16 | rb_c << 8 | rb_d;
+ }
+#endif
+
+#if HAS_NP == true
+ #include
+ #define NUMPIXELS 1
+ Adafruit_NeoPixel pixels(NUMPIXELS, pin_np, NEO_GRB + NEO_KHZ800);
+
+ uint8_t npr = 0;
+ uint8_t npg = 0;
+ uint8_t npb = 0;
+ float npi = NP_M;
+ bool pixels_started = false;
+
+ void led_set_intensity(uint8_t intensity) {
+ npi = (float)intensity/255.0;
+ }
+
+ void led_init() {
+ #if BOARD_MODEL == BOARD_HELTEC_T114
+ // Enable vext power supply to neopixel
+ pinMode(PIN_VEXT_EN, OUTPUT);
+ digitalWrite(PIN_VEXT_EN, HIGH);
+ #endif
+
+ #if MCU_VARIANT == MCU_NRF52
+ if (eeprom_read(eeprom_addr(ADDR_CONF_PSET)) == CONF_OK_BYTE) {
+ uint8_t int_val = eeprom_read(eeprom_addr(ADDR_CONF_PINT));
+ led_set_intensity(int_val);
+ }
+ #else
+ if (EEPROM.read(eeprom_addr(ADDR_CONF_PSET)) == CONF_OK_BYTE) {
+ uint8_t int_val = EEPROM.read(eeprom_addr(ADDR_CONF_PINT));
+ led_set_intensity(int_val);
+ }
+ #endif
+ }
+
+ void npset(uint8_t r, uint8_t g, uint8_t b) {
+ if (pixels_started != true) {
+ pixels.begin();
+ pixels_started = true;
+ }
+
+ if (r != npr || g != npg || b != npb) {
+ npr = r; npg = g; npb = b;
+ pixels.setPixelColor(0, pixels.Color(npr*npi, npg*npi, npb*npi));
+ pixels.show();
+ }
+ }
+
+ void boot_seq() {
+ uint8_t rs[] = { 0x00, 0x00, 0x00 };
+ uint8_t gs[] = { 0x10, 0x08, 0x00 };
+ uint8_t bs[] = { 0x00, 0x08, 0x10 };
+ for (int i = 0; i < 1*sizeof(rs); i++) {
+ npset(rs[i%sizeof(rs)], gs[i%sizeof(gs)], bs[i%sizeof(bs)]);
+ delay(33);
+ npset(0x00, 0x00, 0x00);
+ delay(66);
+ }
+ }
+#else
+ void boot_seq() { }
+#endif
+
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+#elif MCU_VARIANT == MCU_ESP32
+ #if HAS_NP == true
+ void led_rx_on() { npset(0, 0, 0xFF); }
+ void led_rx_off() { npset(0, 0, 0); }
+ void led_tx_on() { npset(0xFF, 0x50, 0x00); }
+ void led_tx_off() { npset(0, 0, 0); }
+ void led_id_on() { npset(0x90, 0, 0x70); }
+ void led_id_off() { npset(0, 0, 0); }
+ #elif BOARD_MODEL == BOARD_RNODE_NG_20
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_RNODE_NG_21
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_T3S3
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_TBEAM
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, LOW); }
+ void led_tx_off() { digitalWrite(pin_led_tx, HIGH); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_TDECK
+ void led_rx_on() { }
+ void led_rx_off() { }
+ void led_tx_on() { }
+ void led_tx_off() { }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ void led_rx_on() { }
+ void led_rx_off() { }
+ void led_tx_on() { }
+ void led_tx_off() { }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_LORA32_V1_0
+ #if defined(EXTERNAL_LEDS)
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #else
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #endif
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ #if defined(EXTERNAL_LEDS)
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #else
+ void led_rx_on() { digitalWrite(pin_led_rx, LOW); }
+ void led_rx_off() { digitalWrite(pin_led_rx, HIGH); }
+ void led_tx_on() { digitalWrite(pin_led_tx, LOW); }
+ void led_tx_off() { digitalWrite(pin_led_tx, HIGH); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #endif
+ #elif BOARD_MODEL == BOARD_HELTEC32_V2
+ #if defined(EXTERNAL_LEDS)
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #else
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #endif
+ #elif BOARD_MODEL == BOARD_HELTEC32_V3
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_LORA32_V2_1
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_XIAO_S3
+ void led_rx_on() { digitalWrite(pin_led_rx, LED_ON); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LED_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_VME213
+ // VME213 has no physical LEDs, only E-Ink display
+ void led_rx_on() { }
+ void led_rx_off() { }
+ void led_tx_on() { }
+ void led_tx_off() { }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_HUZZAH32
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ 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); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #endif
+#elif MCU_VARIANT == MCU_NRF52
+ #if HAS_NP == true
+ void led_rx_on() { npset(0, 0, 0xFF); }
+ void led_rx_off() { npset(0, 0, 0); }
+ void led_tx_on() { npset(0xFF, 0x50, 0x00); }
+ void led_tx_off() { npset(0, 0, 0); }
+ void led_id_on() { npset(0x90, 0, 0x70); }
+ void led_id_off() { npset(0, 0, 0); }
+ #elif BOARD_MODEL == BOARD_RAK4631
+ void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
+ void led_tx_on() { digitalWrite(pin_led_tx, HIGH); }
+ void led_tx_off() { digitalWrite(pin_led_tx, LOW); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ // Heltec T114 pulls pins LOW to turn on
+ void led_rx_on() { digitalWrite(pin_led_rx, LOW); }
+ void led_rx_off() { digitalWrite(pin_led_rx, HIGH); }
+ void led_tx_on() { digitalWrite(pin_led_tx, LOW); }
+ void led_tx_off() { digitalWrite(pin_led_tx, HIGH); }
+ void led_id_on() { }
+ void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_TECHO
+ void led_rx_on() { digitalWrite(pin_led_rx, LED_ON); }
+ void led_rx_off() { digitalWrite(pin_led_rx, LED_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() { }
+ #endif
+#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
+ ESP.restart();
+ #elif MCU_VARIANT == MCU_NRF52
+ NVIC_SystemReset();
+ #endif
+}
+
+// LED Indication: Error
+void led_indicate_error(int cycles) {
+ #if HAS_NP == true
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ npset(0xFF, 0x00, 0x00);
+ delay(100);
+ npset(0xFF, 0x50, 0x00);
+ delay(100);
+ if (!forever) cycles--;
+ }
+ npset(0,0,0);
+ #else
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ digitalWrite(pin_led_rx, HIGH);
+ digitalWrite(pin_led_tx, LOW);
+ delay(100);
+ digitalWrite(pin_led_rx, LOW);
+ digitalWrite(pin_led_tx, HIGH);
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_rx_off();
+ led_tx_off();
+ #endif
+}
+
+// LED Indication: Airtime Lock
+void led_indicate_airtime_lock() {
+ #if HAS_NP == true
+ npset(32,0,2);
+ #endif
+}
+
+// LED Indication: Boot Error
+void led_indicate_boot_error() {
+ #if HAS_NP == true
+ while(true) {
+ npset(0xFF, 0xFF, 0xFF);
+ }
+ #else
+ while (true) {
+ led_tx_on();
+ led_rx_off();
+ delay(10);
+ led_rx_on();
+ led_tx_off();
+ delay(5);
+ }
+ #endif
+}
+
+// LED Indication: Warning
+void led_indicate_warning(int cycles) {
+ #if HAS_NP == true
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ npset(0xFF, 0x50, 0x00);
+ delay(100);
+ npset(0x00, 0x00, 0x00);
+ delay(100);
+ if (!forever) cycles--;
+ }
+ npset(0,0,0);
+ #else
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ digitalWrite(pin_led_tx, HIGH);
+ while(cycles > 0) {
+ led_tx_off();
+ delay(100);
+ led_tx_on();
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_tx_off();
+ #endif
+}
+
+// LED Indication: Info
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ void led_indicate_info(int cycles) {
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ led_rx_off();
+ delay(100);
+ led_rx_on();
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_rx_off();
+ }
+#elif MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #if HAS_NP == true
+ void led_indicate_info(int cycles) {
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ npset(0x00, 0x00, 0xFF);
+ delay(100);
+ npset(0x00, 0x00, 0x00);
+ delay(100);
+ if (!forever) cycles--;
+ }
+ npset(0,0,0);
+ }
+ #elif BOARD_MODEL == BOARD_LORA32_V2_1
+ void led_indicate_info(int cycles) {
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ led_rx_off();
+ delay(100);
+ led_rx_on();
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_rx_off();
+ }
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ void led_indicate_info(int cycles) {
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ led_rx_off();
+ delay(100);
+ led_rx_on();
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_rx_off();
+ }
+ #elif BOARD_MODEL == BOARD_TECHO
+ void led_indicate_info(int cycles) {
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ led_rx_off();
+ delay(100);
+ led_rx_on();
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_rx_off();
+ }
+ #else
+ void led_indicate_info(int cycles) {
+ bool forever = (cycles == 0) ? true : false;
+ cycles = forever ? 1 : cycles;
+ while(cycles > 0) {
+ led_tx_off();
+ delay(100);
+ led_tx_on();
+ delay(100);
+ if (!forever) cycles--;
+ }
+ led_tx_off();
+ }
+ #endif
+#endif
+
+
+unsigned long led_standby_ticks = 0;
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ uint8_t led_standby_min = 1;
+ uint8_t led_standby_max = 40;
+ unsigned long led_standby_wait = 11000;
+
+#elif MCU_VARIANT == MCU_ESP32
+
+ #if HAS_NP == true
+ int led_standby_lng = 200;
+ int led_standby_cut = 100;
+ int led_standby_min = 0;
+ int led_standby_max = 375+led_standby_lng;
+ int led_notready_min = 0;
+ int led_notready_max = led_standby_max;
+ int led_notready_value = led_notready_min;
+ int8_t led_notready_direction = 0;
+ unsigned long led_notready_ticks = 0;
+ unsigned long led_standby_wait = 350;
+ unsigned long led_console_wait = 1;
+ unsigned long led_notready_wait = 200;
+
+ #else
+ uint8_t led_standby_min = 200;
+ uint8_t led_standby_max = 255;
+ uint8_t led_notready_min = 0;
+ uint8_t led_notready_max = 255;
+ uint8_t led_notready_value = led_notready_min;
+ int8_t led_notready_direction = 0;
+ unsigned long led_notready_ticks = 0;
+ unsigned long led_standby_wait = 1768;
+ unsigned long led_notready_wait = 150;
+ #endif
+
+#elif MCU_VARIANT == MCU_NRF52
+ int led_standby_lng = 200;
+ int led_standby_cut = 100;
+ uint8_t led_standby_min = 200;
+ uint8_t led_standby_max = 255;
+ uint8_t led_notready_min = 0;
+ uint8_t led_notready_max = 255;
+ uint8_t led_notready_value = led_notready_min;
+ int8_t led_notready_direction = 0;
+ unsigned long led_notready_ticks = 0;
+ unsigned long led_standby_wait = 1768;
+ unsigned long led_notready_wait = 150;
+#endif
+
+unsigned long led_standby_value = led_standby_min;
+int8_t led_standby_direction = 0;
+
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ void led_indicate_standby() {
+ led_standby_ticks++;
+ if (led_standby_ticks > led_standby_wait) {
+ led_standby_ticks = 0;
+ if (led_standby_value <= led_standby_min) {
+ led_standby_direction = 1;
+ } else if (led_standby_value >= led_standby_max) {
+ led_standby_direction = -1;
+ }
+ led_standby_value += led_standby_direction;
+ analogWrite(pin_led_rx, led_standby_value);
+ led_tx_off();
+ }
+ }
+
+#elif MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #if HAS_NP == true
+ void led_indicate_standby() {
+ led_standby_ticks++;
+
+ if (led_standby_ticks > led_standby_wait) {
+ led_standby_ticks = 0;
+
+ if (led_standby_value <= led_standby_min) {
+ led_standby_direction = 1;
+ } else if (led_standby_value >= led_standby_max) {
+ led_standby_direction = -1;
+ }
+
+ uint8_t led_standby_intensity;
+ led_standby_value += led_standby_direction;
+ int led_standby_ti = led_standby_value - led_standby_lng;
+
+ if (led_standby_ti < 0) {
+ led_standby_intensity = 0;
+ } else if (led_standby_ti > led_standby_cut) {
+ led_standby_intensity = led_standby_cut;
+ } else {
+ led_standby_intensity = led_standby_ti;
+ }
+ npset(led_standby_intensity/3, led_standby_intensity/3, led_standby_intensity/3);
+ }
+ }
+
+ void led_indicate_console() {
+ npset(0x60, 0x00, 0x60);
+ // led_standby_ticks++;
+
+ // if (led_standby_ticks > led_console_wait) {
+ // led_standby_ticks = 0;
+
+ // if (led_standby_value <= led_standby_min) {
+ // led_standby_direction = 1;
+ // } else if (led_standby_value >= led_standby_max) {
+ // led_standby_direction = -1;
+ // }
+
+ // uint8_t led_standby_intensity;
+ // led_standby_value += led_standby_direction;
+ // int led_standby_ti = led_standby_value - led_standby_lng;
+
+ // if (led_standby_ti < 0) {
+ // led_standby_intensity = 0;
+ // } else if (led_standby_ti > led_standby_cut) {
+ // led_standby_intensity = led_standby_cut;
+ // } else {
+ // led_standby_intensity = led_standby_ti;
+ // }
+ // npset(led_standby_intensity, 0x00, led_standby_intensity);
+ // }
+ }
+
+ #else
+ void led_indicate_standby() {
+ led_standby_ticks++;
+ if (led_standby_ticks > led_standby_wait) {
+ led_standby_ticks = 0;
+ if (led_standby_value <= led_standby_min) {
+ led_standby_direction = 1;
+ } else if (led_standby_value >= led_standby_max) {
+ led_standby_direction = -1;
+ }
+ led_standby_value += led_standby_direction;
+ if (led_standby_value > 253) {
+ #if BOARD_MODEL == BOARD_TECHO
+ led_rx_on();
+ #else
+ led_tx_on();
+ #endif
+ } else {
+ #if BOARD_MODEL == BOARD_TECHO
+ led_rx_off();
+ #else
+ led_tx_off();
+ #endif
+ }
+ #if BOARD_MODEL == BOARD_LORA32_V2_1
+ #if defined(EXTERNAL_LEDS)
+ led_rx_off();
+ #endif
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ #if defined(EXTERNAL_LEDS)
+ led_rx_off();
+ #endif
+ #else
+ led_rx_off();
+ #endif
+ }
+ }
+
+ void led_indicate_console() {
+ led_indicate_standby();
+ }
+ #endif
+#endif
+
+#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ void led_indicate_not_ready() {
+ led_standby_ticks++;
+ if (led_standby_ticks > led_standby_wait) {
+ led_standby_ticks = 0;
+ if (led_standby_value <= led_standby_min) {
+ led_standby_direction = 1;
+ } else if (led_standby_value >= led_standby_max) {
+ led_standby_direction = -1;
+ }
+ led_standby_value += led_standby_direction;
+ analogWrite(pin_led_tx, led_standby_value);
+ led_rx_off();
+ }
+ }
+#elif MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ #if HAS_NP == true
+ void led_indicate_not_ready() {
+ led_standby_ticks++;
+
+ if (led_standby_ticks > led_notready_wait) {
+ led_standby_ticks = 0;
+
+ if (led_standby_value <= led_standby_min) {
+ led_standby_direction = 1;
+ } else if (led_standby_value >= led_standby_max) {
+ led_standby_direction = -1;
+ }
+
+ uint8_t led_standby_intensity;
+ led_standby_value += led_standby_direction;
+ int led_standby_ti = led_standby_value - led_standby_lng;
+
+ if (led_standby_ti < 0) {
+ led_standby_intensity = 0;
+ } else if (led_standby_ti > led_standby_cut) {
+ led_standby_intensity = led_standby_cut;
+ } else {
+ led_standby_intensity = led_standby_ti;
+ }
+
+ npset(led_standby_intensity, 0x00, 0x00);
+ }
+ }
+ #else
+ void led_indicate_not_ready() {
+ led_notready_ticks++;
+ if (led_notready_ticks > led_notready_wait) {
+ led_notready_ticks = 0;
+ if (led_notready_value <= led_notready_min) {
+ led_notready_direction = 1;
+ } else if (led_notready_value >= led_notready_max) {
+ led_notready_direction = -1;
+ }
+ led_notready_value += led_notready_direction;
+ if (led_notready_value > 128) {
+ led_tx_on();
+ } else {
+ led_tx_off();
+ }
+ #if BOARD_MODEL == BOARD_LORA32_V2_1
+ #if defined(EXTERNAL_LEDS)
+ led_rx_off();
+ #endif
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ #if defined(EXTERNAL_LEDS)
+ led_rx_off();
+ #endif
+ #else
+ led_rx_off();
+ #endif
+ }
+ }
+ #endif
+#endif
+
+void serial_write(uint8_t byte) {
+ #if HAS_BLUETOOTH || HAS_BLE == true
+ if (bt_state != BT_STATE_CONNECTED) {
+ Serial.write(byte);
+ } else {
+ SerialBT.write(byte);
+ #if MCU_VARIANT == MCU_NRF52 && HAS_BLE
+ // This ensures that the TX buffer is flushed after a frame is queued in serial.
+ // serial_in_frame is used to ensure that the flush only happens at the end of the frame
+ if (serial_in_frame && byte == FEND) { SerialBT.flushTXD(); serial_in_frame = false; }
+ else if (!serial_in_frame && byte == FEND) { serial_in_frame = true; }
+ #endif
+ }
+ #else
+ Serial.write(byte);
+ #endif
+}
+
+void escaped_serial_write(uint8_t byte) {
+ if (byte == FEND) { serial_write(FESC); byte = TFEND; }
+ if (byte == FESC) { serial_write(FESC); byte = TFESC; }
+ serial_write(byte);
+}
+
+void kiss_indicate_reset() {
+ serial_write(FEND);
+ serial_write(CMD_RESET);
+ serial_write(CMD_RESET_BYTE);
+ serial_write(FEND);
+}
+
+void kiss_indicate_error(uint8_t error_code) {
+ serial_write(FEND);
+ serial_write(CMD_ERROR);
+ serial_write(error_code);
+ serial_write(FEND);
+}
+
+void kiss_indicate_radiostate() {
+ serial_write(FEND);
+ serial_write(CMD_RADIO_STATE);
+ serial_write(radio_online);
+ serial_write(FEND);
+}
+
+void kiss_indicate_stat_rx() {
+ serial_write(FEND);
+ serial_write(CMD_STAT_RX);
+ escaped_serial_write(stat_rx>>24);
+ escaped_serial_write(stat_rx>>16);
+ escaped_serial_write(stat_rx>>8);
+ escaped_serial_write(stat_rx);
+ serial_write(FEND);
+}
+
+void kiss_indicate_stat_tx() {
+ serial_write(FEND);
+ serial_write(CMD_STAT_TX);
+ escaped_serial_write(stat_tx>>24);
+ escaped_serial_write(stat_tx>>16);
+ escaped_serial_write(stat_tx>>8);
+ escaped_serial_write(stat_tx);
+ serial_write(FEND);
+}
+
+void kiss_indicate_stat_rssi() {
+ uint8_t packet_rssi_val = (uint8_t)(last_rssi+rssi_offset);
+ serial_write(FEND);
+ serial_write(CMD_STAT_RSSI);
+ escaped_serial_write(packet_rssi_val);
+ serial_write(FEND);
+}
+
+void kiss_indicate_stat_snr() {
+ serial_write(FEND);
+ serial_write(CMD_STAT_SNR);
+ escaped_serial_write(last_snr_raw);
+ serial_write(FEND);
+}
+
+void kiss_indicate_radio_lock() {
+ serial_write(FEND);
+ serial_write(CMD_RADIO_LOCK);
+ serial_write(radio_locked);
+ serial_write(FEND);
+}
+
+void kiss_indicate_spreadingfactor() {
+ serial_write(FEND);
+ serial_write(CMD_SF);
+ serial_write((uint8_t)lora_sf);
+ serial_write(FEND);
+}
+
+void kiss_indicate_codingrate() {
+ serial_write(FEND);
+ serial_write(CMD_CR);
+ serial_write((uint8_t)lora_cr);
+ serial_write(FEND);
+}
+
+void kiss_indicate_implicit_length() {
+ serial_write(FEND);
+ serial_write(CMD_IMPLICIT);
+ serial_write(implicit_l);
+ serial_write(FEND);
+}
+
+void kiss_indicate_txpower() {
+ serial_write(FEND);
+ serial_write(CMD_TXPOWER);
+ serial_write((uint8_t)lora_txp);
+ serial_write(FEND);
+}
+
+void kiss_indicate_bandwidth() {
+ serial_write(FEND);
+ serial_write(CMD_BANDWIDTH);
+ escaped_serial_write(lora_bw>>24);
+ escaped_serial_write(lora_bw>>16);
+ escaped_serial_write(lora_bw>>8);
+ escaped_serial_write(lora_bw);
+ serial_write(FEND);
+}
+
+void kiss_indicate_frequency() {
+ serial_write(FEND);
+ serial_write(CMD_FREQUENCY);
+ escaped_serial_write(lora_freq>>24);
+ escaped_serial_write(lora_freq>>16);
+ escaped_serial_write(lora_freq>>8);
+ escaped_serial_write(lora_freq);
+ serial_write(FEND);
+}
+
+void kiss_indicate_st_alock() {
+ uint16_t at = (uint16_t)(st_airtime_limit*100*100);
+ serial_write(FEND);
+ serial_write(CMD_ST_ALOCK);
+ escaped_serial_write(at>>8);
+ escaped_serial_write(at);
+ serial_write(FEND);
+}
+
+void kiss_indicate_lt_alock() {
+ uint16_t at = (uint16_t)(lt_airtime_limit*100*100);
+ serial_write(FEND);
+ serial_write(CMD_LT_ALOCK);
+ escaped_serial_write(at>>8);
+ escaped_serial_write(at);
+ serial_write(FEND);
+}
+
+void kiss_indicate_channel_stats() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ uint16_t ats = (uint16_t)(airtime*100*100);
+ uint16_t atl = (uint16_t)(longterm_airtime*100*100);
+ uint16_t cls = (uint16_t)(total_channel_util*100*100);
+ uint16_t cll = (uint16_t)(longterm_channel_util*100*100);
+ uint8_t crs = (uint8_t)(current_rssi+rssi_offset);
+ uint8_t nfl = (uint8_t)(noise_floor+rssi_offset);
+ uint8_t ntf = 0xFF; if (interference_detected) { ntf = (uint8_t)(current_rssi+rssi_offset); }
+ serial_write(FEND);
+ serial_write(CMD_STAT_CHTM);
+ escaped_serial_write(ats>>8);
+ escaped_serial_write(ats);
+ escaped_serial_write(atl>>8);
+ escaped_serial_write(atl);
+ escaped_serial_write(cls>>8);
+ escaped_serial_write(cls);
+ escaped_serial_write(cll>>8);
+ escaped_serial_write(cll);
+ escaped_serial_write(crs);
+ escaped_serial_write(nfl);
+ escaped_serial_write(ntf);
+ serial_write(FEND);
+ #endif
+}
+
+void kiss_indicate_csma_stats() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ serial_write(FEND);
+ serial_write(CMD_STAT_CSMA);
+ escaped_serial_write(cw_band);
+ escaped_serial_write(cw_min);
+ escaped_serial_write(cw_max);
+ serial_write(FEND);
+ #endif
+}
+
+void kiss_indicate_phy_stats() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ uint16_t lst = (uint16_t)(lora_symbol_time_ms*1000);
+ uint16_t lsr = (uint16_t)(lora_symbol_rate);
+ uint16_t prs = (uint16_t)(lora_preamble_symbols);
+ uint16_t prt = (uint16_t)(lora_preamble_time_ms);
+ uint16_t cst = (uint16_t)(csma_slot_ms);
+ uint16_t dft = (uint16_t)(difs_ms);
+ serial_write(FEND);
+ serial_write(CMD_STAT_PHYPRM);
+ escaped_serial_write(lst>>8); escaped_serial_write(lst);
+ escaped_serial_write(lsr>>8); escaped_serial_write(lsr);
+ escaped_serial_write(prs>>8); escaped_serial_write(prs);
+ escaped_serial_write(prt>>8); escaped_serial_write(prt);
+ escaped_serial_write(cst>>8); escaped_serial_write(cst);
+ escaped_serial_write(dft>>8); escaped_serial_write(dft);
+ serial_write(FEND);
+ #endif
+}
+
+void kiss_indicate_battery() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ serial_write(FEND);
+ serial_write(CMD_STAT_BAT);
+ escaped_serial_write(battery_state);
+ escaped_serial_write((uint8_t)int(battery_percent));
+ serial_write(FEND);
+ #endif
+}
+
+void kiss_indicate_temperature() {
+ #if HAS_PMU
+ #if MCU_VARIANT == MCU_ESP32
+ float pmu_temp = pmu_temperature+PMU_TEMP_OFFSET;
+ uint8_t temp = (uint8_t)pmu_temp;
+ serial_write(FEND);
+ serial_write(CMD_STAT_TEMP);
+ escaped_serial_write(pmu_temp);
+ serial_write(FEND);
+ #endif
+ #endif
+}
+
+void kiss_indicate_btpin() {
+ #if HAS_BLUETOOTH || HAS_BLE == true
+ serial_write(FEND);
+ serial_write(CMD_BT_PIN);
+ escaped_serial_write(bt_ssp_pin>>24);
+ escaped_serial_write(bt_ssp_pin>>16);
+ escaped_serial_write(bt_ssp_pin>>8);
+ escaped_serial_write(bt_ssp_pin);
+ serial_write(FEND);
+ #endif
+}
+
+void kiss_indicate_random(uint8_t byte) {
+ serial_write(FEND);
+ serial_write(CMD_RANDOM);
+ serial_write(byte);
+ serial_write(FEND);
+}
+
+void kiss_indicate_fbstate() {
+ serial_write(FEND);
+ serial_write(CMD_FB_EXT);
+ #if HAS_DISPLAY
+ if (disp_ext_fb) {
+ serial_write(0x01);
+ } else {
+ serial_write(0x00);
+ }
+ #else
+ serial_write(0xFF);
+ #endif
+ serial_write(FEND);
+}
+
+#if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ void kiss_indicate_device_hash() {
+ serial_write(FEND);
+ serial_write(CMD_DEV_HASH);
+ for (int i = 0; i < DEV_HASH_LEN; i++) {
+ uint8_t byte = dev_hash[i];
+ escaped_serial_write(byte);
+ }
+ serial_write(FEND);
+ }
+
+ void kiss_indicate_target_fw_hash() {
+ serial_write(FEND);
+ serial_write(CMD_HASHES);
+ serial_write(0x01);
+ for (int i = 0; i < DEV_HASH_LEN; i++) {
+ uint8_t byte = dev_firmware_hash_target[i];
+ escaped_serial_write(byte);
+ }
+ serial_write(FEND);
+ }
+
+ void kiss_indicate_fw_hash() {
+ serial_write(FEND);
+ serial_write(CMD_HASHES);
+ serial_write(0x02);
+ for (int i = 0; i < DEV_HASH_LEN; i++) {
+ uint8_t byte = dev_firmware_hash[i];
+ escaped_serial_write(byte);
+ }
+ serial_write(FEND);
+ }
+
+ void kiss_indicate_bootloader_hash() {
+ serial_write(FEND);
+ serial_write(CMD_HASHES);
+ serial_write(0x03);
+ for (int i = 0; i < DEV_HASH_LEN; i++) {
+ uint8_t byte = dev_bootloader_hash[i];
+ escaped_serial_write(byte);
+ }
+ serial_write(FEND);
+ }
+
+ void kiss_indicate_partition_table_hash() {
+ serial_write(FEND);
+ serial_write(CMD_HASHES);
+ serial_write(0x04);
+ for (int i = 0; i < DEV_HASH_LEN; i++) {
+ uint8_t byte = dev_partition_table_hash[i];
+ escaped_serial_write(byte);
+ }
+ serial_write(FEND);
+ }
+#endif
+
+void kiss_indicate_fb() {
+ serial_write(FEND);
+ serial_write(CMD_FB_READ);
+ #if HAS_DISPLAY
+ for (int i = 0; i < 512; i++) {
+ uint8_t byte = fb[i];
+ escaped_serial_write(byte);
+ }
+ #else
+ serial_write(0xFF);
+ #endif
+ serial_write(FEND);
+}
+
+void kiss_indicate_disp() {
+ serial_write(FEND);
+ serial_write(CMD_DISP_READ);
+ #if HAS_DISPLAY
+ uint8_t *da = disp_area.getBuffer();
+ uint8_t *sa = stat_area.getBuffer();
+ for (int i = 0; i < 512; i++) { escaped_serial_write(da[i]); }
+ for (int i = 0; i < 512; i++) { escaped_serial_write(sa[i]); }
+ #else
+ serial_write(0xFF);
+ #endif
+ serial_write(FEND);
+}
+
+void kiss_indicate_ready() {
+ serial_write(FEND);
+ serial_write(CMD_READY);
+ serial_write(0x01);
+ serial_write(FEND);
+}
+
+void kiss_indicate_not_ready() {
+ serial_write(FEND);
+ serial_write(CMD_READY);
+ serial_write(0x00);
+ serial_write(FEND);
+}
+
+void kiss_indicate_promisc() {
+ serial_write(FEND);
+ serial_write(CMD_PROMISC);
+ if (promisc) {
+ serial_write(0x01);
+ } else {
+ serial_write(0x00);
+ }
+ serial_write(FEND);
+}
+
+void kiss_indicate_detect() {
+ serial_write(FEND);
+ serial_write(CMD_DETECT);
+ serial_write(DETECT_RESP);
+ serial_write(FEND);
+}
+
+void kiss_indicate_version() {
+ serial_write(FEND);
+ serial_write(CMD_FW_VERSION);
+ serial_write(MAJ_VERS);
+ serial_write(MIN_VERS);
+ serial_write(FEND);
+}
+
+void kiss_indicate_platform() {
+ serial_write(FEND);
+ serial_write(CMD_PLATFORM);
+ serial_write(PLATFORM);
+ serial_write(FEND);
+}
+
+void kiss_indicate_board() {
+ serial_write(FEND);
+ serial_write(CMD_BOARD);
+ serial_write(BOARD_MODEL);
+ serial_write(FEND);
+}
+
+void kiss_indicate_mcu() {
+ serial_write(FEND);
+ serial_write(CMD_MCU);
+ serial_write(MCU_VARIANT);
+ serial_write(FEND);
+}
+
+inline bool isSplitPacket(uint8_t header) {
+ return (header & FLAG_SPLIT);
+}
+
+inline uint8_t packetSequence(uint8_t header) {
+ return header >> 4;
+}
+
+void setPreamble() {
+ if (radio_online) LoRa->setPreambleLength(lora_preamble_symbols);
+ kiss_indicate_phy_stats();
+}
+
+void updateBitrate() {
+ #if MCU_VARIANT == MCU_ESP32 || MCU_VARIANT == MCU_NRF52
+ if (!radio_online) { lora_bitrate = 0; }
+ else {
+ lora_symbol_rate = (float)lora_bw/(float)(pow(2, lora_sf));
+ lora_symbol_time_ms = (1.0/lora_symbol_rate)*1000.0;
+ lora_bitrate = (uint32_t)(lora_sf * ( (4.0/(float)lora_cr) / ((float)(pow(2, lora_sf))/((float)lora_bw/1000.0)) ) * 1000.0);
+ lora_us_per_byte = 1000000.0/((float)lora_bitrate/8.0);
+
+ bool fast_rate = lora_bitrate > LORA_FAST_THRESHOLD_BPS;
+ lora_limit_rate = lora_bitrate > LORA_LIMIT_THRESHOLD_BPS;
+
+ int csma_slot_min_ms = CSMA_SLOT_MIN_MS;
+ float lora_preamble_target_ms = LORA_PREAMBLE_TARGET_MS;
+ if (fast_rate) { csma_slot_min_ms -= CSMA_SLOT_MIN_FAST_DELTA;
+ lora_preamble_target_ms -= LORA_PREAMBLE_FAST_DELTA; }
+
+ csma_slot_ms = lora_symbol_time_ms*CSMA_SLOT_SYMBOLS;
+ if (csma_slot_ms > CSMA_SLOT_MAX_MS) { csma_slot_ms = CSMA_SLOT_MAX_MS; }
+ if (csma_slot_ms < CSMA_SLOT_MIN_MS) { csma_slot_ms = csma_slot_min_ms; }
+ difs_ms = CSMA_SIFS_MS + 2*csma_slot_ms;
+
+ float target_preamble_symbols = lora_preamble_target_ms/lora_symbol_time_ms;
+ if (target_preamble_symbols < LORA_PREAMBLE_SYMBOLS_MIN) { target_preamble_symbols = LORA_PREAMBLE_SYMBOLS_MIN; }
+ else { target_preamble_symbols = (ceil)(target_preamble_symbols); }
+
+ lora_preamble_symbols = (long)target_preamble_symbols; setPreamble();
+ lora_preamble_time_ms = (ceil)(lora_preamble_symbols * lora_symbol_time_ms);
+ lora_header_time_ms = (ceil)(PHY_HEADER_LORA_SYMBOLS * lora_symbol_time_ms);
+ }
+ #endif
+}
+
+void setSpreadingFactor() {
+ if (radio_online) LoRa->setSpreadingFactor(lora_sf);
+ updateBitrate();
+}
+
+void setCodingRate() {
+ if (radio_online) LoRa->setCodingRate4(lora_cr);
+ updateBitrate();
+}
+
+void set_implicit_length(uint8_t len) {
+ implicit_l = len;
+ if (implicit_l != 0) {
+ implicit = true;
+ } else {
+ implicit = false;
+ }
+}
+
+int getTxPower() {
+ uint8_t txp = LoRa->getTxPower();
+ return (int)txp;
+}
+
+#if HAS_LORA_PA
+ const int tx_gain[PA_GAIN_POINTS] = {PA_GAIN_VALUES};
+#endif
+
+int map_target_power_to_modem_output(int target_tx_power) {
+ #if HAS_LORA_PA
+ int modem_output_dbm = -9;
+ for (int i = 0; i < PA_GAIN_POINTS; i++) {
+ int gain = tx_gain[i];
+ int effective_output_dbm = i + gain;
+ printf("At %d dBm modem output, gain is %d dBm, effective output is %d\n", i, gain, effective_output_dbm);
+ if (effective_output_dbm > target_tx_power) {
+ int diff = effective_output_dbm - target_tx_power;
+ modem_output_dbm = -1*diff;
+ break;
+ } else if (effective_output_dbm == target_tx_power) {
+ modem_output_dbm = i; break;
+ } else if (i == PA_GAIN_POINTS-1) {
+ int diff = target_tx_power - effective_output_dbm;
+ modem_output_dbm = i+diff; break;
+ }
+ }
+ #else
+ int modem_output_dbm = target_tx_power;
+ #endif
+
+ return modem_output_dbm;
+}
+
+int map_modem_output_to_target_power(int modem_output_dbm) {
+ #if HAS_LORA_PA
+ if (modem_output_dbm < 0) { modem_output_dbm = 0; }
+ if (modem_output_dbm >= PA_GAIN_POINTS) { modem_output_dbm = PA_GAIN_POINTS-1; }
+ int gain = tx_gain[modem_output_dbm];
+ int target_tx_power = modem_output_dbm+gain;
+ #else
+ int target_tx_power = modem_output_dbm;
+ #endif
+
+ return target_tx_power;
+}
+
+void setTXPower() {
+ if (radio_online) {
+ int mapped_lora_txp = map_target_power_to_modem_output(lora_txp);
+
+ #if HAS_LORA_PA
+ int real_lora_txp = map_modem_output_to_target_power(mapped_lora_txp);
+ lora_txp = real_lora_txp;
+ #endif
+
+ if (model == MODEL_11) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+ if (model == MODEL_12) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+
+ if (model == MODEL_C6) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+ if (model == MODEL_C7) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+
+ if (model == MODEL_A1) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_A2) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_A3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+ if (model == MODEL_A4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+ if (model == MODEL_A5) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_A6) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_A7) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_A8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_A9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_AA) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_AC) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_BA) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_BB) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_B3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_B4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_B8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_B9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_C4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_C9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_C5) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_CA) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_C8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_D4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_D9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_DB) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_DC) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_DD) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_DE) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_E4) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_E9) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_E3) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_E8) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+
+ if (model == MODEL_FE) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_PA_BOOST_PIN);
+ if (model == MODEL_FF) LoRa->setTxPower(mapped_lora_txp, PA_OUTPUT_RFO_PIN);
+ }
+}
+
+
+void getBandwidth() {
+ if (radio_online) {
+ lora_bw = LoRa->getSignalBandwidth();
+ }
+ updateBitrate();
+}
+
+void setBandwidth() {
+ if (radio_online) {
+ LoRa->setSignalBandwidth(lora_bw);
+ getBandwidth();
+ }
+}
+
+void getFrequency() {
+ if (radio_online) {
+ lora_freq = LoRa->getFrequency();
+ }
+}
+
+void setFrequency() {
+ if (radio_online) {
+ LoRa->setFrequency(lora_freq);
+ getFrequency();
+ }
+}
+
+uint8_t getRandom() { return random(0xFF); }
+
+void promisc_enable() {
+ promisc = true;
+}
+
+void promisc_disable() {
+ promisc = false;
+}
+
+#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ bool eeprom_begin() {
+ InternalFS.begin();
+
+ file.open(EEPROM_FILE, FILE_O_READ);
+ if (!file) {
+ if (file.open(EEPROM_FILE, FILE_O_WRITE)) {
+ for (uint32_t mapped_addr = 0; mapped_addr < EEPROM_SIZE; mapped_addr++) { file.seek(mapped_addr); file.write(0xFF); }
+ eeprom_flush();
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ file.close();
+ file.open(EEPROM_FILE, FILE_O_WRITE);
+ return true;
+ }
+ }
+
+ uint8_t eeprom_read(uint32_t mapped_addr) {
+ uint8_t byte;
+ void* byte_ptr = &byte;
+ file.seek(mapped_addr);
+ file.read(byte_ptr, 1);
+ return byte;
+ }
+#endif
+
+bool eeprom_info_locked() {
+ #if HAS_EEPROM
+ uint8_t lock_byte = EEPROM.read(eeprom_addr(ADDR_INFO_LOCK));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t lock_byte = eeprom_read(eeprom_addr(ADDR_INFO_LOCK));
+ #endif
+ if (lock_byte == INFO_LOCK_BYTE) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void eeprom_dump_info() {
+ for (int addr = ADDR_PRODUCT; addr <= ADDR_INFO_LOCK; addr++) {
+ #if HAS_EEPROM
+ uint8_t byte = EEPROM.read(eeprom_addr(addr));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t byte = eeprom_read(eeprom_addr(addr));
+ #endif
+ escaped_serial_write(byte);
+ }
+}
+
+void eeprom_dump_config() {
+ for (int addr = ADDR_CONF_SF; addr <= ADDR_CONF_OK; addr++) {
+ #if HAS_EEPROM
+ uint8_t byte = EEPROM.read(eeprom_addr(addr));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t byte = eeprom_read(eeprom_addr(addr));
+ #endif
+ escaped_serial_write(byte);
+ }
+}
+
+void eeprom_dump_all() {
+ for (int addr = 0; addr < EEPROM_RESERVED; addr++) {
+ #if HAS_EEPROM
+ uint8_t byte = EEPROM.read(eeprom_addr(addr));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t byte = eeprom_read(eeprom_addr(addr));
+ #endif
+ escaped_serial_write(byte);
+ }
+}
+
+void kiss_dump_eeprom() {
+ serial_write(FEND);
+ serial_write(CMD_ROM_READ);
+ eeprom_dump_all();
+ serial_write(FEND);
+}
+
+#if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+void eeprom_flush() {
+ file.close();
+ file.open(EEPROM_FILE, FILE_O_WRITE);
+ written_bytes = 0;
+}
+#endif
+
+void eeprom_update(int mapped_addr, uint8_t byte) {
+ #if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
+ EEPROM.update(mapped_addr, byte);
+ #elif MCU_VARIANT == MCU_ESP32
+ if (EEPROM.read(mapped_addr) != byte) {
+ EEPROM.write(mapped_addr, byte);
+ EEPROM.commit();
+ }
+ #elif !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ // todo: clean up this implementation, writing one byte and syncing
+ // each time is really slow, but this is also suboptimal
+ uint8_t read_byte;
+ void* read_byte_ptr = &read_byte;
+ file.seek(mapped_addr);
+ file.read(read_byte_ptr, 1);
+ file.seek(mapped_addr);
+ if (read_byte != byte) {
+ file.write(byte);
+ }
+ written_bytes++;
+ eeprom_flush();
+ #endif
+}
+
+void eeprom_write(uint8_t addr, uint8_t byte) {
+ if (!eeprom_info_locked() && addr >= 0 && addr < EEPROM_RESERVED) {
+ eeprom_update(eeprom_addr(addr), byte);
+ } else {
+ kiss_indicate_error(ERROR_EEPROM_LOCKED);
+ }
+}
+
+void eeprom_erase() {
+ #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ InternalFS.format();
+ #else
+ for (int addr = 0; addr < EEPROM_RESERVED; addr++) {
+ eeprom_update(eeprom_addr(addr), 0xFF);
+ }
+ #endif
+ hard_reset();
+}
+
+bool eeprom_lock_set() {
+ #if HAS_EEPROM
+ if (EEPROM.read(eeprom_addr(ADDR_INFO_LOCK)) == INFO_LOCK_BYTE) {
+ #elif MCU_VARIANT == MCU_NRF52
+ if (eeprom_read(eeprom_addr(ADDR_INFO_LOCK)) == INFO_LOCK_BYTE) {
+ #endif
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool eeprom_product_valid() {
+ #if HAS_EEPROM
+ uint8_t rval = EEPROM.read(eeprom_addr(ADDR_PRODUCT));
+ #elif MCU_VARIANT == MCU_NRF52
+ 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_H32_V4 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3) {
+ #elif PLATFORM == PLATFORM_NRF52
+ if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) {
+ #else
+ if (false) {
+ #endif
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool eeprom_model_valid() {
+ #if HAS_EEPROM
+ model = EEPROM.read(eeprom_addr(ADDR_MODEL));
+ #elif MCU_VARIANT == MCU_NRF52
+ model = eeprom_read(eeprom_addr(ADDR_MODEL));
+ #endif
+ #if BOARD_MODEL == BOARD_RNODE
+ if (model == MODEL_A4 || model == MODEL_A9 || model == MODEL_FF || model == MODEL_FE) {
+ #elif BOARD_MODEL == BOARD_RNODE_NG_20
+ if (model == MODEL_A3 || model == MODEL_A8) {
+ #elif BOARD_MODEL == BOARD_RNODE_NG_21
+ if (model == MODEL_A2 || model == MODEL_A7) {
+ #elif BOARD_MODEL == BOARD_T3S3
+ if (model == MODEL_A1 || model == MODEL_A6 || model == MODEL_A5 || model == MODEL_AA || model == MODEL_AC) {
+ #elif BOARD_MODEL == BOARD_HMBRW
+ if (model == MODEL_FF || model == MODEL_FE) {
+ #elif BOARD_MODEL == BOARD_TBEAM
+ if (model == MODEL_E4 || model == MODEL_E9 || model == MODEL_E3 || model == MODEL_E8) {
+ #elif BOARD_MODEL == BOARD_TDECK
+ if (model == MODEL_D4 || model == MODEL_D9) {
+ #elif BOARD_MODEL == BOARD_TECHO
+ if (model == MODEL_16 || model == MODEL_17) {
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ if (model == MODEL_DB || model == MODEL_DC) {
+ #elif BOARD_MODEL == BOARD_XIAO_S3
+ if (model == MODEL_DD || model == MODEL_DE) {
+ #elif BOARD_MODEL == BOARD_LORA32_V1_0
+ if (model == MODEL_BA || model == MODEL_BB) {
+ #elif BOARD_MODEL == BOARD_LORA32_V2_0
+ if (model == MODEL_B3 || model == MODEL_B8) {
+ #elif BOARD_MODEL == BOARD_LORA32_V2_1
+ if (model == MODEL_B4 || model == MODEL_B9) {
+ #elif BOARD_MODEL == BOARD_HELTEC32_V2
+ if (model == MODEL_C4 || model == MODEL_C9) {
+ #elif BOARD_MODEL == BOARD_HELTEC32_V3
+ if (model == MODEL_C5 || model == MODEL_CA) {
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ if (model == MODEL_C8) {
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ if (model == MODEL_C6 || model == MODEL_C7) {
+ #elif BOARD_MODEL == BOARD_RAK4631
+ if (model == MODEL_11 || model == MODEL_12) {
+ #elif BOARD_MODEL == BOARD_HUZZAH32
+ if (model == MODEL_FF) {
+ #elif BOARD_MODEL == BOARD_GENERIC_ESP32
+ if (model == MODEL_FF || model == MODEL_FE) {
+ #else
+ if (false) {
+ #endif
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool eeprom_hwrev_valid() {
+ #if HAS_EEPROM
+ hwrev = EEPROM.read(eeprom_addr(ADDR_HW_REV));
+ #elif MCU_VARIANT == MCU_NRF52
+ hwrev = eeprom_read(eeprom_addr(ADDR_HW_REV));
+ #endif
+ if (hwrev != 0x00 && hwrev != 0xFF) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool eeprom_checksum_valid() {
+ char *data = (char*)malloc(CHECKSUMMED_SIZE);
+ for (uint8_t i = 0; i < CHECKSUMMED_SIZE; i++) {
+ #if HAS_EEPROM
+ char byte = EEPROM.read(eeprom_addr(i));
+ #elif MCU_VARIANT == MCU_NRF52
+ char byte = eeprom_read(eeprom_addr(i));
+ #endif
+ data[i] = byte;
+ }
+
+ unsigned char *hash = MD5::make_hash(data, CHECKSUMMED_SIZE);
+ bool checksum_valid = true;
+ for (uint8_t i = 0; i < 16; i++) {
+ #if HAS_EEPROM
+ uint8_t stored_chk_byte = EEPROM.read(eeprom_addr(ADDR_CHKSUM+i));
+ #elif MCU_VARIANT == MCU_NRF52
+ uint8_t stored_chk_byte = eeprom_read(eeprom_addr(ADDR_CHKSUM+i));
+ #endif
+ uint8_t calced_chk_byte = (uint8_t)hash[i];
+ if (stored_chk_byte != calced_chk_byte) {
+ checksum_valid = false;
+ }
+ }
+
+ free(hash);
+ free(data);
+ return checksum_valid;
+}
+
+void bt_conf_save(bool is_enabled) {
+ if (is_enabled) {
+ eeprom_update(eeprom_addr(ADDR_CONF_BT), BT_ENABLE_BYTE);
+ #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ // have to do a flush because we're only writing 1 byte and it syncs after 8
+ eeprom_flush();
+ #endif
+ } else {
+ eeprom_update(eeprom_addr(ADDR_CONF_BT), 0x00);
+ #if !HAS_EEPROM && MCU_VARIANT == MCU_NRF52
+ // have to do a flush because we're only writing 1 byte and it syncs after 8
+ eeprom_flush();
+ #endif
+ }
+}
+
+void di_conf_save(uint8_t dint) {
+ eeprom_update(eeprom_addr(ADDR_CONF_DINT), dint);
+}
+
+void da_conf_save(uint8_t dadr) {
+ eeprom_update(eeprom_addr(ADDR_CONF_DADR), dadr);
+}
+
+void db_conf_save(uint8_t val) {
+ #if HAS_DISPLAY
+ if (val == 0x00) {
+ display_blanking_enabled = false;
+ } else {
+ display_blanking_enabled = true;
+ display_blanking_timeout = val*1000;
+ }
+ eeprom_update(eeprom_addr(ADDR_CONF_BSET), CONF_OK_BYTE);
+ eeprom_update(eeprom_addr(ADDR_CONF_DBLK), val);
+ #endif
+}
+
+void drot_conf_save(uint8_t val) {
+ #if HAS_DISPLAY
+ if (val >= 0x00 and val <= 0x03) {
+ eeprom_update(eeprom_addr(ADDR_CONF_DROT), val);
+ hard_reset();
+ }
+ #endif
+}
+
+void dia_conf_save(uint8_t val) {
+ if (val > 0x00) { eeprom_update(eeprom_addr(ADDR_CONF_DIA), 0x01); }
+ else { eeprom_update(eeprom_addr(ADDR_CONF_DIA), 0x00); }
+ hard_reset();
+}
+
+void np_int_conf_save(uint8_t p_int) {
+ eeprom_update(eeprom_addr(ADDR_CONF_PSET), CONF_OK_BYTE);
+ eeprom_update(eeprom_addr(ADDR_CONF_PINT), p_int);
+}
+
+
+bool eeprom_have_conf() {
+ #if HAS_EEPROM
+ if (EEPROM.read(eeprom_addr(ADDR_CONF_OK)) == CONF_OK_BYTE) {
+ #elif MCU_VARIANT == MCU_NRF52
+ if (eeprom_read(eeprom_addr(ADDR_CONF_OK)) == CONF_OK_BYTE) {
+ #endif
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void eeprom_conf_load() {
+ if (eeprom_have_conf()) {
+ #if HAS_EEPROM
+ lora_sf = EEPROM.read(eeprom_addr(ADDR_CONF_SF));
+ lora_cr = EEPROM.read(eeprom_addr(ADDR_CONF_CR));
+ lora_txp = EEPROM.read(eeprom_addr(ADDR_CONF_TXP));
+ lora_freq = (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x00) << 24 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x01) << 16 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x02) << 8 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_FREQ)+0x03);
+ lora_bw = (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x00) << 24 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x01) << 16 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x02) << 8 | (uint32_t)EEPROM.read(eeprom_addr(ADDR_CONF_BW)+0x03);
+ #elif MCU_VARIANT == MCU_NRF52
+ lora_sf = eeprom_read(eeprom_addr(ADDR_CONF_SF));
+ lora_cr = eeprom_read(eeprom_addr(ADDR_CONF_CR));
+ lora_txp = eeprom_read(eeprom_addr(ADDR_CONF_TXP));
+ lora_freq = (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x00) << 24 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x01) << 16 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x02) << 8 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_FREQ)+0x03);
+ lora_bw = (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x00) << 24 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x01) << 16 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x02) << 8 | (uint32_t)eeprom_read(eeprom_addr(ADDR_CONF_BW)+0x03);
+ #endif
+ }
+}
+
+void eeprom_conf_save() {
+ if (hw_ready && radio_online) {
+ eeprom_update(eeprom_addr(ADDR_CONF_SF), lora_sf);
+ eeprom_update(eeprom_addr(ADDR_CONF_CR), lora_cr);
+ eeprom_update(eeprom_addr(ADDR_CONF_TXP), lora_txp);
+
+ eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x00, lora_bw>>24);
+ eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x01, lora_bw>>16);
+ eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x02, lora_bw>>8);
+ eeprom_update(eeprom_addr(ADDR_CONF_BW)+0x03, lora_bw);
+
+ eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x00, lora_freq>>24);
+ eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x01, lora_freq>>16);
+ eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x02, lora_freq>>8);
+ eeprom_update(eeprom_addr(ADDR_CONF_FREQ)+0x03, lora_freq);
+
+ eeprom_update(eeprom_addr(ADDR_CONF_OK), CONF_OK_BYTE);
+ led_indicate_info(10);
+ } else {
+ led_indicate_warning(10);
+ }
+}
+
+void eeprom_conf_delete() {
+ eeprom_update(eeprom_addr(ADDR_CONF_OK), 0x00);
+}
+
+void unlock_rom() {
+ led_indicate_error(50);
+ eeprom_erase();
+}
+
+void init_channel_stats() {
+ #if MCU_VARIANT == MCU_ESP32
+ for (uint16_t ai = 0; ai < DCD_SAMPLES; ai++) { util_samples[ai] = false; }
+ for (uint16_t ai = 0; ai < AIRTIME_BINS; ai++) { airtime_bins[ai] = 0; }
+ for (uint16_t ai = 0; ai < AIRTIME_BINS; ai++) { longterm_bins[ai] = 0.0; }
+ local_channel_util = 0.0;
+ total_channel_util = 0.0;
+ airtime = 0.0;
+ longterm_airtime = 0.0;
+ #endif
+}
+
+typedef struct FIFOBuffer
+{
+ unsigned char *begin;
+ unsigned char *end;
+ unsigned char * volatile head;
+ unsigned char * volatile tail;
+} FIFOBuffer;
+
+inline bool fifo_isempty(const FIFOBuffer *f) {
+ return f->head == f->tail;
+}
+
+inline bool fifo_isfull(const FIFOBuffer *f) {
+ return ((f->head == f->begin) && (f->tail == f->end)) || (f->tail == f->head - 1);
+}
+
+inline void fifo_push(FIFOBuffer *f, unsigned char c) {
+ *(f->tail) = c;
+
+ if (f->tail == f->end) {
+ f->tail = f->begin;
+ } else {
+ f->tail++;
+ }
+}
+
+inline unsigned char fifo_pop(FIFOBuffer *f) {
+ if(f->head == f->end) {
+ f->head = f->begin;
+ return *(f->end);
+ } else {
+ return *(f->head++);
+ }
+}
+
+inline void fifo_flush(FIFOBuffer *f) {
+ f->head = f->tail;
+}
+
+#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ static inline bool fifo_isempty_locked(const FIFOBuffer *f) {
+ bool result;
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ result = fifo_isempty(f);
+ }
+ return result;
+ }
+
+ static inline bool fifo_isfull_locked(const FIFOBuffer *f) {
+ bool result;
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ result = fifo_isfull(f);
+ }
+ return result;
+ }
+
+ static inline void fifo_push_locked(FIFOBuffer *f, unsigned char c) {
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ fifo_push(f, c);
+ }
+ }
+#endif
+
+/*
+static inline unsigned char fifo_pop_locked(FIFOBuffer *f) {
+ unsigned char c;
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ c = fifo_pop(f);
+ }
+ return c;
+}
+*/
+
+inline void fifo_init(FIFOBuffer *f, unsigned char *buffer, size_t size) {
+ f->head = f->tail = f->begin = buffer;
+ f->end = buffer + size;
+}
+
+inline size_t fifo_len(FIFOBuffer *f) {
+ return f->end - f->begin;
+}
+
+typedef struct FIFOBuffer16
+{
+ uint16_t *begin;
+ uint16_t *end;
+ uint16_t * volatile head;
+ uint16_t * volatile tail;
+} FIFOBuffer16;
+
+inline bool fifo16_isempty(const FIFOBuffer16 *f) {
+ return f->head == f->tail;
+}
+
+inline bool fifo16_isfull(const FIFOBuffer16 *f) {
+ return ((f->head == f->begin) && (f->tail == f->end)) || (f->tail == f->head - 1);
+}
+
+inline void fifo16_push(FIFOBuffer16 *f, uint16_t c) {
+ *(f->tail) = c;
+
+ if (f->tail == f->end) {
+ f->tail = f->begin;
+ } else {
+ f->tail++;
+ }
+}
+
+inline uint16_t fifo16_pop(FIFOBuffer16 *f) {
+ if(f->head == f->end) {
+ f->head = f->begin;
+ return *(f->end);
+ } else {
+ return *(f->head++);
+ }
+}
+
+inline void fifo16_flush(FIFOBuffer16 *f) {
+ f->head = f->tail;
+}
+
+#if MCU_VARIANT != MCU_ESP32 && MCU_VARIANT != MCU_NRF52
+ static inline bool fifo16_isempty_locked(const FIFOBuffer16 *f) {
+ bool result;
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ result = fifo16_isempty(f);
+ }
+
+ return result;
+ }
+#endif
+
+/*
+static inline bool fifo16_isfull_locked(const FIFOBuffer16 *f) {
+ bool result;
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ result = fifo16_isfull(f);
+ }
+ return result;
+}
+
+
+static inline void fifo16_push_locked(FIFOBuffer16 *f, uint16_t c) {
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ fifo16_push(f, c);
+ }
+}
+
+static inline size_t fifo16_pop_locked(FIFOBuffer16 *f) {
+ size_t c;
+ ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
+ c = fifo16_pop(f);
+ }
+ return c;
+}
+*/
+
+inline void fifo16_init(FIFOBuffer16 *f, uint16_t *buffer, uint16_t size) {
+ f->head = f->tail = f->begin = buffer;
+ f->end = buffer + size;
+}
+
+inline uint16_t fifo16_len(FIFOBuffer16 *f) {
+ return (f->end - f->begin);
+}
diff --git a/RNode_Firmware/src/LCMEN2R13EFC1.cpp b/RNode_Firmware/src/LCMEN2R13EFC1.cpp
new file mode 100644
index 0000000..4b0b1c5
--- /dev/null
+++ b/RNode_Firmware/src/LCMEN2R13EFC1.cpp
@@ -0,0 +1,242 @@
+// E-Ink display driver for LCMEN2R13EFC1
+// Adapted from Meshtastic NicheGraphics driver
+
+#include "LCMEN2R13EFC1.h"
+
+// Look-up tables for FAST refresh
+static const uint8_t LUT_FAST_VCOMDC[] = {
+ 0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01,
+ 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_WW[] = {
+ 0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01,
+ 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_BW[] = {
+ 0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01,
+ 0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_WB[] = {
+ 0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01,
+ 0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_BB[] = {
+ 0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01,
+ 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+LCMEN2R13EFC1::LCMEN2R13EFC1()
+ : _spiSettings(6000000, MSBFIRST, SPI_MODE0)
+{
+ // Calculate buffer size (8 pixels per byte)
+ _bufferRowSize = ((DISPLAY_WIDTH - 1) / 8) + 1; // 122px -> 16 bytes
+ _bufferSize = _bufferRowSize * DISPLAY_HEIGHT; // 16 * 250 = 4000 bytes
+}
+
+void LCMEN2R13EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) {
+ _spi = spi;
+ _pin_dc = pin_dc;
+ _pin_cs = pin_cs;
+ _pin_busy = pin_busy;
+ _pin_rst = pin_rst;
+
+ pinMode(_pin_dc, OUTPUT);
+ pinMode(_pin_cs, OUTPUT);
+ pinMode(_pin_busy, INPUT);
+ pinMode(_pin_rst, INPUT_PULLUP); // Active low, hold high
+
+ reset();
+}
+
+void LCMEN2R13EFC1::reset() {
+ pinMode(_pin_rst, OUTPUT);
+ digitalWrite(_pin_rst, LOW);
+ delay(10);
+ pinMode(_pin_rst, INPUT_PULLUP);
+ wait();
+
+ sendCommand(0x12); // Software reset
+ wait();
+}
+
+void LCMEN2R13EFC1::wait() {
+ // Busy when LOW
+ while (digitalRead(_pin_busy) == LOW) {
+ yield();
+ }
+}
+
+bool LCMEN2R13EFC1::isBusy() {
+ return (digitalRead(_pin_busy) == LOW);
+}
+
+void LCMEN2R13EFC1::sendCommand(const uint8_t command) {
+ _spi->beginTransaction(_spiSettings);
+ digitalWrite(_pin_dc, LOW); // DC low = command
+ digitalWrite(_pin_cs, LOW);
+ _spi->transfer(command);
+ digitalWrite(_pin_cs, HIGH);
+ digitalWrite(_pin_dc, HIGH);
+ _spi->endTransaction();
+}
+
+void LCMEN2R13EFC1::sendData(uint8_t data) {
+ sendData(&data, 1);
+}
+
+void LCMEN2R13EFC1::sendData(const uint8_t *data, uint32_t size) {
+ _spi->beginTransaction(_spiSettings);
+ digitalWrite(_pin_dc, HIGH); // DC high = data
+ digitalWrite(_pin_cs, LOW);
+
+ #if defined(ARCH_ESP32) || defined(ESP32)
+ _spi->transferBytes(data, NULL, size);
+ #else
+ for (uint32_t i = 0; i < size; i++) {
+ _spi->transfer(data[i]);
+ }
+ #endif
+
+ digitalWrite(_pin_cs, HIGH);
+ digitalWrite(_pin_dc, HIGH);
+ _spi->endTransaction();
+}
+
+void LCMEN2R13EFC1::configFull() {
+ sendCommand(0x00); // Panel setting register
+ sendData(0b11 << 6 // Display resolution
+ | 1 << 4 // B&W only
+ | 1 << 3 // Vertical scan direction
+ | 1 << 2 // Horizontal scan direction
+ | 1 << 1 // Shutdown: no
+ | 1 << 0 // Reset: no
+ );
+
+ sendCommand(0x50); // VCOM and data interval setting
+ sendData(0b10 << 6 // Border driven white
+ | 0b11 << 4 // Invert image colors: no
+ | 0b0111 // Interval between VCOM on and image data
+ );
+}
+
+void LCMEN2R13EFC1::configFast() {
+ sendCommand(0x00); // Panel setting register
+ sendData(0b11 << 6 // Display resolution
+ | 1 << 5 // LUT from registers (set below)
+ | 1 << 4 // B&W only
+ | 1 << 3 // Vertical scan direction
+ | 1 << 2 // Horizontal scan direction
+ | 1 << 1 // Shutdown: no
+ | 1 << 0 // Reset: no
+ );
+
+ sendCommand(0x50); // VCOM and data interval setting
+ sendData(0b11 << 6 // Border floating
+ | 0b01 << 4 // Invert image colors: no
+ | 0b0111 // Interval between VCOM on and image data
+ );
+
+ // Load LUT tables
+ sendCommand(0x20); // VCOM
+ sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
+
+ sendCommand(0x21); // White -> White
+ sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
+
+ sendCommand(0x22); // Black -> White
+ sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
+
+ sendCommand(0x23); // White -> Black
+ sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
+
+ sendCommand(0x24); // Black -> Black
+ sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
+}
+
+void LCMEN2R13EFC1::writeNewImage() {
+ sendCommand(0x13);
+ sendData(_buffer, _bufferSize);
+}
+
+void LCMEN2R13EFC1::writeOldImage() {
+ sendCommand(0x10);
+ sendData(_buffer, _bufferSize);
+}
+
+void LCMEN2R13EFC1::update(uint8_t *imageData, UpdateType type) {
+ _updateType = type;
+ _buffer = imageData;
+
+ reset();
+
+ // Configure display
+ if (_updateType == UPDATE_FULL) {
+ configFull();
+ } else {
+ configFast();
+ }
+
+ // Transfer image data
+ if (_updateType == UPDATE_FULL) {
+ writeNewImage();
+ writeOldImage();
+ } else {
+ writeNewImage();
+ }
+
+ // Power on and start refresh
+ sendCommand(0x04); // Power on panel voltage
+ wait();
+
+ sendCommand(0x12); // Begin executing update
+ wait();
+
+ // Power off
+ sendCommand(0x02);
+ wait();
+
+ // Update "old memory" for next differential refresh
+ if (_updateType != UPDATE_FULL) {
+ writeOldImage();
+ wait();
+ }
+}
+
+void LCMEN2R13EFC1::powerOn() {
+ sendCommand(0x04);
+ wait();
+}
+
+void LCMEN2R13EFC1::powerOff() {
+ sendCommand(0x02);
+ wait();
+}
diff --git a/RNode_Firmware/src/LCMEN2R13EFC1.h b/RNode_Firmware/src/LCMEN2R13EFC1.h
new file mode 100644
index 0000000..8bae3c9
--- /dev/null
+++ b/RNode_Firmware/src/LCMEN2R13EFC1.h
@@ -0,0 +1,59 @@
+// E-Ink display driver for LCMEN2R13EFC1 (Heltec VisionMaster E213)
+// Adapted from Meshtastic firmware for RNode use
+// Controller IC: Fitipower JD79656
+// Resolution: 122x250 pixels (width x height)
+// Supports FAST (partial) and FULL refresh
+
+#ifndef LCMEN2R13EFC1_H
+#define LCMEN2R13EFC1_H
+
+#include
+#include
+
+class LCMEN2R13EFC1 {
+public:
+ // Display properties
+ static constexpr uint16_t DISPLAY_WIDTH = 122;
+ static constexpr uint16_t DISPLAY_HEIGHT = 250;
+
+ // Update types
+ enum UpdateType {
+ UPDATE_FULL = 0, // Full refresh (slower, ~3.6s)
+ UPDATE_FAST = 1 // Partial refresh (faster, ~720ms)
+ };
+
+ LCMEN2R13EFC1();
+
+ void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
+ void update(uint8_t *imageData, UpdateType type);
+ bool isBusy();
+ void powerOn();
+ void powerOff();
+
+private:
+ SPIClass *_spi;
+ uint8_t _pin_dc;
+ uint8_t _pin_cs;
+ uint8_t _pin_busy;
+ uint8_t _pin_rst;
+
+ SPISettings _spiSettings;
+
+ uint8_t *_buffer;
+ uint16_t _bufferRowSize;
+ uint32_t _bufferSize;
+ UpdateType _updateType;
+
+ void reset();
+ void wait();
+ void sendCommand(const uint8_t command);
+ void sendData(uint8_t data);
+ void sendData(const uint8_t *data, uint32_t size);
+
+ void configFull();
+ void configFast();
+ void writeNewImage();
+ void writeOldImage();
+};
+
+#endif // LCMEN2R13EFC1_H
diff --git a/RNode_Firmware/src/einkDetect_VME213.h b/RNode_Firmware/src/einkDetect_VME213.h
new file mode 100644
index 0000000..cdd5af4
--- /dev/null
+++ b/RNode_Firmware/src/einkDetect_VME213.h
@@ -0,0 +1,41 @@
+// E-Ink display chip detection for VME213
+// Detects between LCMEN213EFC1 (V1) and E0213A367 (V1.1)
+// Based on Meshtastic einkDetect.h
+
+#ifndef EINK_DETECT_VME213_H
+#define EINK_DETECT_VME213_H
+
+#include
+
+enum EInkChipType {
+ EINK_LCMEN213EFC1 = 0, // Initial version (Fitipower JD79656)
+ EINK_E0213A367 = 1 // V1.1+ (Solomon Systech SSD1682)
+};
+
+// Detect E-Ink controller IC type by BUSY pin logic
+// Fitipower: BUSY=LOW when busy
+// Solomon Systech: BUSY=HIGH when busy
+inline EInkChipType detectEInkChip(uint8_t pin_reset, uint8_t pin_busy) {
+ // Force display BUSY by holding reset pin active
+ pinMode(pin_reset, OUTPUT);
+ digitalWrite(pin_reset, LOW);
+
+ delay(10);
+
+ // Read BUSY pin logic while display is busy
+ pinMode(pin_busy, INPUT);
+ bool busyLogic = digitalRead(pin_busy);
+
+ // Release reset pin
+ pinMode(pin_reset, INPUT);
+
+ // Fitipower = LOW when busy
+ // Solomon = HIGH when busy
+ if (busyLogic == LOW) {
+ return EINK_LCMEN213EFC1;
+ } else {
+ return EINK_E0213A367;
+ }
+}
+
+#endif // EINK_DETECT_VME213_H
diff --git a/RNode_Firmware/sx126x.cpp b/RNode_Firmware/sx126x.cpp
new file mode 100644
index 0000000..36879be
--- /dev/null
+++ b/RNode_Firmware/sx126x.cpp
@@ -0,0 +1,803 @@
+// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva.
+// Licensed under the MIT license.
+
+#include "Boards.h"
+
+#if MODEM == SX1262
+#include "sx126x.h"
+
+#if MCU_VARIANT == MCU_ESP32
+ #if MCU_VARIANT == MCU_ESP32 and !defined(CONFIG_IDF_TARGET_ESP32S3)
+ #include "soc/rtc_wdt.h"
+ #endif
+ #define ISR_VECT IRAM_ATTR
+#else
+ #define ISR_VECT
+#endif
+
+#define OP_RF_FREQ_6X 0x86
+#define OP_SLEEP_6X 0x84
+#define OP_STANDBY_6X 0x80
+#define OP_TX_6X 0x83
+#define OP_RX_6X 0x82
+#define OP_PA_CONFIG_6X 0x95
+#define OP_SET_IRQ_FLAGS_6X 0x08 // Also provides info such as
+ // preamble detection, etc for
+ // knowing when it's safe to switch
+ // antenna modes
+#define OP_CLEAR_IRQ_STATUS_6X 0x02
+#define OP_GET_IRQ_STATUS_6X 0x12
+#define OP_RX_BUFFER_STATUS_6X 0x13
+#define OP_PACKET_STATUS_6X 0x14 // Get snr & rssi of last packet
+#define OP_CURRENT_RSSI_6X 0x15
+#define OP_MODULATION_PARAMS_6X 0x8B // BW, SF, CR, etc.
+#define OP_PACKET_PARAMS_6X 0x8C // CRC, preamble, payload length, etc.
+#define OP_STATUS_6X 0xC0
+#define OP_TX_PARAMS_6X 0x8E // Set dbm, etc
+#define OP_PACKET_TYPE_6X 0x8A
+#define OP_BUFFER_BASE_ADDR_6X 0x8F
+#define OP_READ_REGISTER_6X 0x1D
+#define OP_WRITE_REGISTER_6X 0x0D
+#define OP_DIO3_TCXO_CTRL_6X 0x97
+#define OP_DIO2_RF_CTRL_6X 0x9D
+#define OP_CAD_PARAMS 0x88
+#define OP_CALIBRATE_6X 0x89
+#define OP_RX_TX_FALLBACK_MODE_6X 0x93
+#define OP_REGULATOR_MODE_6X 0x96
+#define OP_CALIBRATE_IMAGE_6X 0x98
+
+#define MASK_CALIBRATE_ALL 0x7f
+
+#define IRQ_TX_DONE_MASK_6X 0x01
+#define IRQ_RX_DONE_MASK_6X 0x02
+#define IRQ_HEADER_DET_MASK_6X 0x10
+#define IRQ_PREAMBLE_DET_MASK_6X 0x04
+#define IRQ_PAYLOAD_CRC_ERROR_MASK_6X 0x40
+#define IRQ_ALL_MASK_6X 0b0100001111111111
+
+#define MODE_LONG_RANGE_MODE_6X 0x01
+
+#define OP_FIFO_WRITE_6X 0x0E
+#define OP_FIFO_READ_6X 0x1E
+#define REG_OCP_6X 0x08E7
+#define REG_LNA_6X 0x08AC // No agc in sx1262
+#define REG_SYNC_WORD_MSB_6X 0x0740
+#define REG_SYNC_WORD_LSB_6X 0x0741
+#define REG_PAYLOAD_LENGTH_6X 0x0702 // https://github.com/beegee-tokyo/SX126x-Arduino/blob/master/src/radio/sx126x/sx126x.h#L98
+#define REG_RANDOM_GEN_6X 0x0819
+
+#define MODE_TCXO_3_3V_6X 0x07
+#define MODE_TCXO_3_0V_6X 0x06
+#define MODE_TCXO_2_7V_6X 0x06
+#define MODE_TCXO_2_4V_6X 0x06
+#define MODE_TCXO_2_2V_6X 0x03
+#define MODE_TCXO_1_8V_6X 0x02
+#define MODE_TCXO_1_7V_6X 0x01
+#define MODE_TCXO_1_6V_6X 0x00
+
+#define MODE_STDBY_RC_6X 0x00
+#define MODE_STDBY_XOSC_6X 0x01
+#define MODE_FALLBACK_STDBY_RC_6X 0x20
+#define MODE_IMPLICIT_HEADER 0x01
+#define MODE_EXPLICIT_HEADER 0x00
+
+#define SYNC_WORD_6X 0x1424
+
+#define XTAL_FREQ_6X (double)32000000
+#define FREQ_DIV_6X (double)pow(2.0, 25.0)
+#define FREQ_STEP_6X (double)(XTAL_FREQ_6X / FREQ_DIV_6X)
+
+#if BOARD_MODEL == BOARD_TECHO
+ SPIClass spim3 = SPIClass(NRF_SPIM3, pin_miso, pin_sclk, pin_mosi) ;
+ #define SPI spim3
+
+#elif defined(NRF52840_XXAA)
+ extern SPIClass spiModem;
+ #define SPI spiModem
+#endif
+
+extern SPIClass SPI;
+
+#define MAX_PKT_LENGTH 255
+
+sx126x::sx126x() :
+ _spiSettings(16E6, MSBFIRST, SPI_MODE0),
+ _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), _busy(LORA_DEFAULT_BUSY_PIN), _rxen(LORA_DEFAULT_RXEN_PIN),
+ _frequency(0),
+ _txp(0),
+ _sf(0x07),
+ _bw(0x04),
+ _cr(0x01),
+ _ldro(0x00),
+ _packetIndex(0),
+ _preambleLength(18),
+ _implicitHeaderMode(0),
+ _payloadLength(255),
+ _crcMode(1),
+ _fifo_tx_addr_ptr(0),
+ _fifo_rx_addr_ptr(0),
+ _packet({0}),
+ _preinit_done(false),
+ _onReceive(NULL)
+{ setTimeout(0); }
+
+bool sx126x::preInit() {
+ pinMode(_ss, OUTPUT);
+ digitalWrite(_ss, HIGH);
+
+ #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 || BOARD_MODEL == BOARD_VME213
+ SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
+ #elif BOARD_MODEL == BOARD_TECHO
+ SPI.setPins(pin_miso, pin_sclk, pin_mosi);
+ SPI.begin();
+ #else
+ SPI.begin();
+ #endif
+
+ // Check version (retry for up to 2 seconds)
+ // TODO: Actually read version registers, not syncwords
+ long start = millis();
+ uint8_t syncmsb;
+ uint8_t synclsb;
+ while (((millis() - start) < 2000) && (millis() >= start)) {
+ syncmsb = readRegister(REG_SYNC_WORD_MSB_6X);
+ synclsb = readRegister(REG_SYNC_WORD_LSB_6X);
+ if ( uint16_t(syncmsb << 8 | synclsb) == 0x1424 || uint16_t(syncmsb << 8 | synclsb) == 0x4434) {
+ break;
+ }
+ delay(100);
+ }
+ if ( uint16_t(syncmsb << 8 | synclsb) != 0x1424 && uint16_t(syncmsb << 8 | synclsb) != 0x4434) {
+ return false;
+ }
+
+ _preinit_done = true;
+ return true;
+}
+
+uint8_t ISR_VECT sx126x::readRegister(uint16_t address) {
+ return singleTransfer(OP_READ_REGISTER_6X, address, 0x00);
+}
+
+void sx126x::writeRegister(uint16_t address, uint8_t value) {
+ singleTransfer(OP_WRITE_REGISTER_6X, address, value);
+}
+
+uint8_t ISR_VECT sx126x::singleTransfer(uint8_t opcode, uint16_t address, uint8_t value) {
+ waitOnBusy();
+
+ uint8_t response;
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(opcode);
+ SPI.transfer((address & 0xFF00) >> 8);
+ SPI.transfer(address & 0x00FF);
+ if (opcode == OP_READ_REGISTER_6X) { SPI.transfer(0x00); }
+ response = SPI.transfer(value);
+ SPI.endTransaction();
+
+ digitalWrite(_ss, HIGH);
+
+ return response;
+}
+
+void sx126x::rxAntEnable() {
+ if (_rxen != -1) { digitalWrite(_rxen, HIGH); }
+}
+
+void sx126x::loraMode() {
+ // Enable lora mode on the SX1262 chip
+ uint8_t mode = MODE_LONG_RANGE_MODE_6X;
+ executeOpcode(OP_PACKET_TYPE_6X, &mode, 1);
+}
+
+void sx126x::waitOnBusy() {
+ unsigned long time = millis();
+ if (_busy != -1) {
+ while (digitalRead(_busy) == HIGH) {
+ if (millis() >= (time + 100)) { break; }
+ }
+ }
+}
+
+void sx126x::executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(opcode);
+ for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx126x::executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(opcode);
+ SPI.transfer(0x00);
+ for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx126x::writeBuffer(const uint8_t* buffer, size_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(OP_FIFO_WRITE_6X);
+ SPI.transfer(_fifo_tx_addr_ptr);
+ for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); _fifo_tx_addr_ptr++; }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx126x::readBuffer(uint8_t* buffer, size_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(OP_FIFO_READ_6X);
+ SPI.transfer(_fifo_rx_addr_ptr);
+ SPI.transfer(0x00);
+ for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx126x::setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro) {
+ // Because there is no access to these registers on the sx1262, we have
+ // to set all these parameters at once or not at all.
+ uint8_t buf[8];
+ buf[0] = sf;
+ buf[1] = bw;
+ buf[2] = cr;
+ buf[3] = ldro; // Low data rate toggle
+ buf[4] = 0x00; // Unused params in LoRa mode
+ buf[5] = 0x00;
+ buf[6] = 0x00;
+ buf[7] = 0x00;
+ executeOpcode(OP_MODULATION_PARAMS_6X, buf, 8);
+}
+
+void sx126x::setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc) {
+ // Because there is no access to these registers on the sx1262, we have
+ // to set all these parameters at once or not at all.
+ uint8_t buf[9];
+ buf[0] = uint8_t((preamble_symbols & 0xFF00) >> 8);
+ buf[1] = uint8_t((preamble_symbols & 0x00FF));
+ buf[2] = headermode;
+ buf[3] = payload_length;
+ buf[4] = crc;
+ buf[5] = 0x00; // standard IQ setting (no inversion)
+ buf[6] = 0x00; // unused params
+ buf[7] = 0x00;
+ buf[8] = 0x00;
+ executeOpcode(OP_PACKET_PARAMS_6X, buf, 9);
+}
+
+void sx126x::reset(void) {
+ if (_reset != -1) {
+ pinMode(_reset, OUTPUT);
+ digitalWrite(_reset, LOW);
+ delay(10);
+ digitalWrite(_reset, HIGH);
+ delay(10);
+ }
+}
+
+void sx126x::calibrate(void) {
+ // Put in STDBY_RC mode before calibration
+ uint8_t mode_byte = MODE_STDBY_RC_6X;
+ executeOpcode(OP_STANDBY_6X, &mode_byte, 1);
+
+ // Calibrate RC64k, RC13M, PLL, ADC and image
+ uint8_t calibrate = MASK_CALIBRATE_ALL;
+ executeOpcode(OP_CALIBRATE_6X, &calibrate, 1);
+
+ delay(5);
+ waitOnBusy();
+}
+
+void sx126x::calibrate_image(long frequency) {
+ uint8_t image_freq[2] = {0};
+ if (frequency >= 430E6 && frequency <= 440E6) { image_freq[0] = 0x6B; image_freq[1] = 0x6F; }
+ else if (frequency >= 470E6 && frequency <= 510E6) { image_freq[0] = 0x75; image_freq[1] = 0x81; }
+ else if (frequency >= 779E6 && frequency <= 787E6) { image_freq[0] = 0xC1; image_freq[1] = 0xC5; }
+ else if (frequency >= 863E6 && frequency <= 870E6) { image_freq[0] = 0xD7; image_freq[1] = 0xDB; }
+ else if (frequency >= 902E6 && frequency <= 928E6) { image_freq[0] = 0xE1; image_freq[1] = 0xE9; } // TODO: Allow higher freq calibration
+ executeOpcode(OP_CALIBRATE_IMAGE_6X, image_freq, 2);
+ waitOnBusy();
+}
+
+int sx126x::begin(long frequency) {
+ reset();
+
+ if (_busy != -1) { pinMode(_busy, INPUT); }
+ if (!_preinit_done) { if (!preInit()) { return false; } }
+ if (_rxen != -1) { pinMode(_rxen, OUTPUT); }
+
+ calibrate();
+ calibrate_image(frequency);
+ enableTCXO();
+ loraMode();
+ standby();
+
+ // Set sync word
+ setSyncWord(SYNC_WORD_6X);
+
+ #if DIO2_AS_RF_SWITCH
+ // enable dio2 rf switch
+ uint8_t byte = 0x01;
+ executeOpcode(OP_DIO2_RF_CTRL_6X, &byte, 1);
+ #endif
+
+ rxAntEnable();
+ setFrequency(frequency);
+ setTxPower(2);
+ enableCrc();
+ writeRegister(REG_LNA_6X, 0x96); // Set LNA boost
+ uint8_t basebuf[2] = {0}; // Set base addresses
+ executeOpcode(OP_BUFFER_BASE_ADDR_6X, basebuf, 2);
+
+ setModulationParams(_sf, _bw, _cr, _ldro);
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+
+ #if HAS_LORA_PA
+ #if LORA_PA_GC1109
+ // Enable Vfem_ctl for supply to
+ // PA power net.
+ pinMode(LORA_PA_PWR_EN, OUTPUT);
+ digitalWrite(LORA_PA_PWR_EN, HIGH);
+
+ // Enable PA LNA and TX standby
+ pinMode(LORA_PA_CSD, OUTPUT);
+ digitalWrite(LORA_PA_CSD, HIGH);
+
+ // Keep PA CPS low until actual
+ // transmit. Does it save power?
+ // Who knows? Will have to measure.
+ pinMode(LORA_PA_CPS, OUTPUT);
+ digitalWrite(LORA_PA_CPS, LOW);
+
+ // On Heltec V4, the PA CTX pin
+ // is driven by the SX1262 DIO2
+ // pin directly, so we do not
+ // need to manually raise this.
+ #endif
+ #endif
+
+ return 1;
+}
+
+void sx126x::end() { sleep(); SPI.end(); _preinit_done = false; }
+
+int sx126x::beginPacket(int implicitHeader) {
+ #if HAS_LORA_PA
+ #if LORA_PA_GC1109
+ // Enable PA CPS for transmit
+ digitalWrite(LORA_PA_CPS, HIGH);
+ #endif
+ #endif
+
+ standby();
+ if (implicitHeader) { implicitHeaderMode(); }
+ else { explicitHeaderMode(); }
+
+ _payloadLength = 0;
+ _fifo_tx_addr_ptr = 0;
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+
+ return 1;
+}
+
+int sx126x::endPacket() {
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+ uint8_t timeout[3] = {0}; // Put in single TX mode
+ executeOpcode(OP_TX_6X, timeout, 3);
+
+ uint8_t buf[2];
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2);
+
+ // Wait for TX done
+ bool timed_out = false;
+ uint32_t w_timeout = millis()+LORA_MODEM_TIMEOUT_MS;
+ while ((millis() < w_timeout) && ((buf[1] & IRQ_TX_DONE_MASK_6X) == 0)) {
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2);
+ yield();
+ }
+
+ if (!(millis() < w_timeout)) { timed_out = true; }
+
+ // Clear IRQs
+ uint8_t mask[2];
+ mask[0] = 0x00;
+ mask[1] = IRQ_TX_DONE_MASK_6X;
+ executeOpcode(OP_CLEAR_IRQ_STATUS_6X, mask, 2);
+ if (timed_out) { return 0; } else { return 1; }
+}
+
+unsigned long preamble_detected_at = 0;
+extern long lora_preamble_time_ms;
+extern long lora_header_time_ms;
+bool false_preamble_detected = false;
+
+bool sx126x::dcd() {
+ uint8_t buf[2] = {0}; executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2);
+ uint32_t now = millis();
+
+ bool header_detected = false;
+ bool carrier_detected = false;
+
+ if ((buf[1] & IRQ_HEADER_DET_MASK_6X) != 0) { header_detected = true; carrier_detected = true; }
+ else { header_detected = false; }
+
+ if ((buf[1] & IRQ_PREAMBLE_DET_MASK_6X) != 0) {
+ carrier_detected = true;
+ if (preamble_detected_at == 0) { preamble_detected_at = now; }
+ if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) {
+ preamble_detected_at = 0;
+ if (!header_detected) { false_preamble_detected = true; }
+ uint8_t clearbuf[2] = {0};
+ clearbuf[1] = IRQ_PREAMBLE_DET_MASK_6X;
+ executeOpcode(OP_CLEAR_IRQ_STATUS_6X, clearbuf, 2);
+ }
+ }
+
+ // TODO: Maybe there's a way of unlatching the RSSI
+ // status without re-activating receive mode?
+ if (false_preamble_detected) { sx126x_modem.receive(); false_preamble_detected = false; }
+ return carrier_detected;
+}
+
+uint8_t sx126x::currentRssiRaw() {
+ uint8_t byte = 0;
+ executeOpcodeRead(OP_CURRENT_RSSI_6X, &byte, 1);
+ return byte;
+}
+
+int ISR_VECT sx126x::currentRssi() {
+ uint8_t byte = 0;
+ executeOpcodeRead(OP_CURRENT_RSSI_6X, &byte, 1);
+ int rssi = -(int(byte)) / 2;
+ #if HAS_LORA_LNA
+ rssi -= LORA_LNA_GAIN;
+ #endif
+ return rssi;
+}
+
+uint8_t sx126x::packetRssiRaw() {
+ uint8_t buf[3] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3);
+ return buf[2];
+}
+
+int ISR_VECT sx126x::packetRssi() {
+ // TODO: May need more calculations here
+ uint8_t buf[3] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3);
+ int pkt_rssi = -buf[0] / 2;
+ #if HAS_LORA_LNA
+ pkt_rssi -= LORA_LNA_GAIN;
+ #endif
+ return pkt_rssi;
+}
+
+int ISR_VECT sx126x::packetRssi(uint8_t pkt_snr_raw) {
+ // TODO: May need more calculations here
+ uint8_t buf[3] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3);
+ int pkt_rssi = -buf[0] / 2;
+ return pkt_rssi;
+}
+
+uint8_t ISR_VECT sx126x::packetSnrRaw() {
+ uint8_t buf[3] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3);
+ return buf[1];
+}
+
+float ISR_VECT sx126x::packetSnr() {
+ uint8_t buf[3] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_6X, buf, 3);
+ return float(buf[1]) * 0.25;
+}
+
+long sx126x::packetFrequencyError() {
+ // TODO: Implement this, no idea how to check it on the sx1262
+ const float fError = 0.0;
+ return static_cast(fError);
+}
+
+size_t sx126x::write(uint8_t byte) { return write(&byte, sizeof(byte)); }
+size_t sx126x::write(const uint8_t *buffer, size_t size) {
+ if ((_payloadLength + size) > MAX_PKT_LENGTH) { size = MAX_PKT_LENGTH - _payloadLength; }
+ writeBuffer(buffer, size);
+ _payloadLength = _payloadLength + size;
+ return size;
+}
+
+int ISR_VECT sx126x::available() {
+ uint8_t buf[2] = {0};
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, buf, 2);
+ return buf[0] - _packetIndex;
+}
+
+int ISR_VECT sx126x::read(){
+ if (!available()) { return -1; }
+ if (_packetIndex == 0) {
+ uint8_t rxbuf[2] = {0};
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2);
+ int size = rxbuf[0];
+ _fifo_rx_addr_ptr = rxbuf[1];
+ readBuffer(_packet, size);
+ }
+
+ uint8_t byte = _packet[_packetIndex];
+ _packetIndex++;
+ return byte;
+}
+
+int sx126x::peek() {
+ if (!available()) { return -1; }
+ if (_packetIndex == 0) {
+ uint8_t rxbuf[2] = {0};
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2);
+ int size = rxbuf[0];
+ _fifo_rx_addr_ptr = rxbuf[1];
+ readBuffer(_packet, size);
+ }
+
+ uint8_t b = _packet[_packetIndex];
+ return b;
+}
+
+void sx126x::flush() { }
+
+void sx126x::onReceive(void(*callback)(int)){
+ _onReceive = callback;
+
+ if (callback) {
+ pinMode(_dio0, INPUT);
+ uint8_t buf[8]; // Set preamble and header detection irqs, plus dio0 mask
+ buf[0] = 0xFF; // Set irq masks, enable all
+ buf[1] = 0xFF;
+ buf[2] = 0x00; // Set dio0 masks
+ buf[3] = IRQ_RX_DONE_MASK_6X;
+ buf[4] = 0x00; // Set dio1 masks
+ buf[5] = 0x00;
+ buf[6] = 0x00; // Set dio2 masks
+ buf[7] = 0x00;
+ executeOpcode(OP_SET_IRQ_FLAGS_6X, buf, 8);
+
+ #ifdef SPI_HAS_NOTUSINGINTERRUPT
+ SPI.usingInterrupt(digitalPinToInterrupt(_dio0));
+ #endif
+ attachInterrupt(digitalPinToInterrupt(_dio0), sx126x::onDio0Rise, RISING);
+
+ } else {
+ detachInterrupt(digitalPinToInterrupt(_dio0));
+ #ifdef SPI_HAS_NOTUSINGINTERRUPT
+ SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0));
+ #endif
+ }
+}
+
+void sx126x::receive(int size) {
+ #if HAS_LORA_PA
+ #if LORA_PA_GC1109
+ // Disable PA CPS for receive
+ digitalWrite(LORA_PA_CPS, LOW);
+ #endif
+ #endif
+
+ if (size > 0) {
+ implicitHeaderMode();
+ _payloadLength = size;
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+ } else { explicitHeaderMode(); }
+
+ if (_rxen != -1) { rxAntEnable(); }
+ uint8_t mode[3] = {0xFF, 0xFF, 0xFF}; // Continuous mode
+ executeOpcode(OP_RX_6X, mode, 3);
+}
+
+void sx126x::standby() {
+ uint8_t byte = MODE_STDBY_XOSC_6X; // STDBY_XOSC
+ executeOpcode(OP_STANDBY_6X, &byte, 1);
+}
+
+void sx126x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_6X, &byte, 1); }
+
+void sx126x::enableTCXO() {
+ #if HAS_TCXO
+ #if BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_XIAO_S3 || BOARD_MODEL == BOARD_VME213
+ uint8_t buf[4] = {MODE_TCXO_3_3V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_TBEAM
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_TDECK
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_TBEAM_S_V1
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_T3S3
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_HELTEC_T114
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_TECHO
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #elif BOARD_MODEL == BOARD_HELTEC32_V4
+ uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
+ #endif
+ executeOpcode(OP_DIO3_TCXO_CTRL_6X, buf, 4);
+ #endif
+}
+
+// TODO: Once enabled, SX1262 needs a complete reset to disable TCXO
+void sx126x::disableTCXO() { }
+
+void sx126x::setTxPower(int level, int outputPin) {
+ // Currently no low power mode for SX1262 implemented, assuming PA boost
+
+ // WORKAROUND - Better Resistance of the SX1262 Tx to Antenna Mismatch, see DS_SX1261-2_V1.2 datasheet chapter 15.2
+ // RegTxClampConfig = @address 0x08D8
+ writeRegister(0x08D8, readRegister(0x08D8) | (0x0F << 1));
+
+ uint8_t pa_buf[4];
+ pa_buf[0] = 0x04; // PADutyCycle needs to be 0x04 to achieve 22dBm output, but can be lowered for better efficiency at lower outputs
+ pa_buf[1] = 0x07; // HPMax at 0x07 is maximum supported for SX1262
+ pa_buf[2] = 0x00; // DeviceSel 0x00 for SX1262 (0x01 for SX1261)
+ pa_buf[3] = 0x01; // PALut always 0x01 (reserved according to datasheet)
+ executeOpcode(OP_PA_CONFIG_6X, pa_buf, 4); // set pa_config for high power
+
+ if (level > 22) { level = 22; }
+ else if (level < -9) { level = -9; }
+ writeRegister(REG_OCP_6X, OCP_TUNED); // Use board-specific tuned OCP
+
+ uint8_t tx_buf[2];
+ tx_buf[0] = level;
+ tx_buf[1] = 0x02; // PA ramping time - 40 microseconds
+ executeOpcode(OP_TX_PARAMS_6X, tx_buf, 2);
+
+ _txp = level;
+}
+
+uint8_t sx126x::getTxPower() { return _txp; }
+
+void sx126x::setFrequency(long frequency) {
+ _frequency = frequency;
+ uint8_t buf[4];
+ uint32_t freq = (uint32_t)((double)frequency / (double)FREQ_STEP_6X);
+ buf[0] = ((freq >> 24) & 0xFF);
+ buf[1] = ((freq >> 16) & 0xFF);
+ buf[2] = ((freq >> 8) & 0xFF);
+ buf[3] = (freq & 0xFF);
+ executeOpcode(OP_RF_FREQ_6X, buf, 4);
+}
+
+uint32_t sx126x::getFrequency() {
+ // We can't read the frequency on the sx1262 / 80
+ uint32_t frequency = _frequency;
+ return frequency;
+}
+
+void sx126x::setSpreadingFactor(int sf) {
+ if (sf < 5) { sf = 5; }
+ else if (sf > 12) { sf = 12; }
+ _sf = sf;
+
+ handleLowDataRate();
+ setModulationParams(sf, _bw, _cr, _ldro);
+}
+
+long sx126x::getSignalBandwidth() {
+ int bw = _bw;
+ switch (bw) {
+ case 0x00: return 7.8E3;
+ case 0x01: return 15.6E3;
+ case 0x02: return 31.25E3;
+ case 0x03: return 62.5E3;
+ case 0x04: return 125E3;
+ case 0x05: return 250E3;
+ case 0x06: return 500E3;
+ case 0x08: return 10.4E3;
+ case 0x09: return 20.8E3;
+ case 0x0A: return 41.7E3;
+ }
+ return 0;
+}
+
+extern bool lora_low_datarate;
+void sx126x::handleLowDataRate() {
+ if ( long( (1<<_sf) / (getSignalBandwidth()/1000)) > 16)
+ { _ldro = 0x01; lora_low_datarate = true; }
+ else { _ldro = 0x00; lora_low_datarate = false; }
+}
+
+// TODO: Check if there's anything the sx1262 can do here
+void sx126x::optimizeModemSensitivity(){ }
+
+void sx126x::setSignalBandwidth(long sbw) {
+ if (sbw <= 7.8E3) { _bw = 0x00; }
+ else if (sbw <= 10.4E3) { _bw = 0x08; }
+ else if (sbw <= 15.6E3) { _bw = 0x01; }
+ else if (sbw <= 20.8E3) { _bw = 0x09; }
+ else if (sbw <= 31.25E3) { _bw = 0x02; }
+ else if (sbw <= 41.7E3) { _bw = 0x0A; }
+ else if (sbw <= 62.5E3) { _bw = 0x03; }
+ else if (sbw <= 125E3) { _bw = 0x04; }
+ else if (sbw <= 250E3) { _bw = 0x05; }
+ else { _bw = 0x06; }
+
+ handleLowDataRate();
+ setModulationParams(_sf, _bw, _cr, _ldro);
+ optimizeModemSensitivity();
+}
+
+void sx126x::setCodingRate4(int denominator) {
+ if (denominator < 5) { denominator = 5; }
+ else if (denominator > 8) { denominator = 8; }
+ int cr = denominator - 4;
+ _cr = cr;
+ setModulationParams(_sf, _bw, cr, _ldro);
+}
+
+void sx126x::setPreambleLength(long preamble_symbols) {
+ _preambleLength = preamble_symbols;
+ setPacketParams(preamble_symbols, _implicitHeaderMode, _payloadLength, _crcMode);
+}
+
+void sx126x::setSyncWord(uint16_t sw) {
+ // TODO: Why was this hardcoded instead of using the config value?
+ // writeRegister(REG_SYNC_WORD_MSB_6X, (sw & 0xFF00) >> 8);
+ // writeRegister(REG_SYNC_WORD_LSB_6X, sw & 0x00FF);
+ writeRegister(REG_SYNC_WORD_MSB_6X, 0x14);
+ writeRegister(REG_SYNC_WORD_LSB_6X, 0x24);
+}
+
+void sx126x::setPins(int ss, int reset, int dio0, int busy, int rxen) {
+ _ss = ss;
+ _reset = reset;
+ _dio0 = dio0;
+ _busy = busy;
+ _rxen = rxen;
+}
+
+void sx126x::dumpRegisters(Stream& out) {
+ for (int i = 0; i < 128; i++) {
+ out.print("0x");
+ out.print(i, HEX);
+ out.print(": 0x");
+ out.println(readRegister(i), HEX);
+ }
+}
+
+void ISR_VECT sx126x::handleDio0Rise() {
+ uint8_t buf[2];
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_6X, buf, 2);
+ executeOpcode(OP_CLEAR_IRQ_STATUS_6X, buf, 2);
+
+ if ((buf[1] & IRQ_PAYLOAD_CRC_ERROR_MASK_6X) == 0) {
+ _packetIndex = 0;
+ uint8_t rxbuf[2] = {0}; // Read packet length
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_6X, rxbuf, 2);
+ int packetLength = rxbuf[0];
+ if (_onReceive) { _onReceive(packetLength); }
+ }
+}
+
+void ISR_VECT sx126x::onDio0Rise() { sx126x_modem.handleDio0Rise(); }
+void sx126x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); }
+void sx126x::enableCrc() { _crcMode = 1; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx126x::disableCrc() { _crcMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx126x::explicitHeaderMode() { _implicitHeaderMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx126x::implicitHeaderMode() { _implicitHeaderMode = 1; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+byte sx126x::random() { return readRegister(REG_RANDOM_GEN_6X); }
+
+sx126x sx126x_modem;
+
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/sx126x.h b/RNode_Firmware/sx126x.h
new file mode 100644
index 0000000..068a1bb
--- /dev/null
+++ b/RNode_Firmware/sx126x.h
@@ -0,0 +1,145 @@
+// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva.
+// Licensed under the MIT license.
+
+#ifndef SX126X_H
+#define SX126X_H
+
+#include
+#include
+#include "Modem.h"
+
+#define LORA_DEFAULT_SS_PIN 10
+#define LORA_DEFAULT_RESET_PIN 9
+#define LORA_DEFAULT_DIO0_PIN 2
+#define LORA_DEFAULT_RXEN_PIN -1
+#define LORA_DEFAULT_TXEN_PIN -1
+#define LORA_DEFAULT_BUSY_PIN -1
+#define LORA_MODEM_TIMEOUT_MS 20E3
+
+#define PA_OUTPUT_RFO_PIN 0
+#define PA_OUTPUT_PA_BOOST_PIN 1
+
+#define RSSI_OFFSET 157
+
+class sx126x : public Stream {
+public:
+ sx126x();
+
+ int begin(long frequency);
+ void end();
+
+ int beginPacket(int implicitHeader = false);
+ int endPacket();
+
+ int parsePacket(int size = 0);
+ int packetRssi();
+ int packetRssi(uint8_t pkt_snr_raw);
+ int currentRssi();
+ uint8_t packetRssiRaw();
+ uint8_t currentRssiRaw();
+ uint8_t packetSnrRaw();
+ float packetSnr();
+ long packetFrequencyError();
+
+ // from Print
+ virtual size_t write(uint8_t byte);
+ virtual size_t write(const uint8_t *buffer, size_t size);
+
+ // from Stream
+ virtual int available();
+ virtual int read();
+ virtual int peek();
+ virtual void flush();
+
+ void onReceive(void(*callback)(int));
+
+ void receive(int size = 0);
+ void standby();
+ void sleep();
+ void reset(void);
+
+ bool preInit();
+ uint8_t getTxPower();
+ void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN);
+ uint32_t getFrequency();
+ void setFrequency(long frequency);
+ void setSpreadingFactor(int sf);
+ long getSignalBandwidth();
+ void setSignalBandwidth(long sbw);
+ void setCodingRate4(int denominator);
+ void setPreambleLength(long preamble_symbols);
+ void setSyncWord(uint16_t sw);
+ bool dcd();
+ void enableCrc();
+ void disableCrc();
+ void enableTCXO();
+ void disableTCXO();
+
+ void rxAntEnable();
+ void loraMode();
+ void waitOnBusy();
+ void executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size);
+ void executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size);
+ void writeBuffer(const uint8_t* buffer, size_t size);
+ void readBuffer(uint8_t* buffer, size_t size);
+ void setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc);
+
+ void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr, int ldro);
+
+ // deprecated
+ void crc() { enableCrc(); }
+ void noCrc() { disableCrc(); }
+
+ byte random();
+
+ void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN);
+ void setSPIFrequency(uint32_t frequency);
+
+ void dumpRegisters(Stream& out);
+
+private:
+ void explicitHeaderMode();
+ void implicitHeaderMode();
+
+ void handleDio0Rise();
+
+ uint8_t readRegister(uint16_t address);
+ void writeRegister(uint16_t address, uint8_t value);
+ uint8_t singleTransfer(uint8_t opcode, uint16_t address, uint8_t value);
+
+ static void onDio0Rise();
+
+ void handleLowDataRate();
+ void optimizeModemSensitivity();
+
+ void calibrate(void);
+ void calibrate_image(long frequency);
+
+private:
+ SPISettings _spiSettings;
+ int _ss;
+ int _reset;
+ int _dio0;
+ int _rxen;
+ int _busy;
+ long _frequency;
+ int _txp;
+ uint8_t _sf;
+ uint8_t _bw;
+ uint8_t _cr;
+ uint8_t _ldro;
+ int _packetIndex;
+ int _preambleLength;
+ int _implicitHeaderMode;
+ int _payloadLength;
+ int _crcMode;
+ int _fifo_tx_addr_ptr;
+ int _fifo_rx_addr_ptr;
+ uint8_t _packet[255];
+ bool _preinit_done;
+ void (*_onReceive)(int);
+};
+
+extern sx126x sx126x_modem;
+
+#endif
diff --git a/RNode_Firmware/sx127x.cpp b/RNode_Firmware/sx127x.cpp
new file mode 100644
index 0000000..ef9038d
--- /dev/null
+++ b/RNode_Firmware/sx127x.cpp
@@ -0,0 +1,503 @@
+// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva.
+// Licensed under the MIT license.
+
+#include "Boards.h"
+
+#if MODEM == SX1276
+#include "sx127x.h"
+
+#if MCU_VARIANT == MCU_ESP32
+ #if MCU_VARIANT == MCU_ESP32 and !defined(CONFIG_IDF_TARGET_ESP32S3)
+ #include "hal/wdt_hal.h"
+ #endif
+ #define ISR_VECT IRAM_ATTR
+#else
+ #define ISR_VECT
+#endif
+
+// Registers
+#define REG_FIFO_7X 0x00
+#define REG_OP_MODE_7X 0x01
+#define REG_FRF_MSB_7X 0x06
+#define REG_FRF_MID_7X 0x07
+#define REG_FRF_LSB_7X 0x08
+#define REG_PA_CONFIG_7X 0x09
+#define REG_OCP_7X 0x0b
+#define REG_LNA_7X 0x0c
+#define REG_FIFO_ADDR_PTR_7X 0x0d
+#define REG_FIFO_TX_BASE_ADDR_7X 0x0e
+#define REG_FIFO_RX_BASE_ADDR_7X 0x0f
+#define REG_FIFO_RX_CURRENT_ADDR_7X 0x10
+#define REG_IRQ_FLAGS_7X 0x12
+#define REG_RX_NB_BYTES_7X 0x13
+#define REG_MODEM_STAT_7X 0x18
+#define REG_PKT_SNR_VALUE_7X 0x19
+#define REG_PKT_RSSI_VALUE_7X 0x1a
+#define REG_RSSI_VALUE_7X 0x1b
+#define REG_MODEM_CONFIG_1_7X 0x1d
+#define REG_MODEM_CONFIG_2_7X 0x1e
+#define REG_PREAMBLE_MSB_7X 0x20
+#define REG_PREAMBLE_LSB_7X 0x21
+#define REG_PAYLOAD_LENGTH_7X 0x22
+#define REG_MODEM_CONFIG_3_7X 0x26
+#define REG_FREQ_ERROR_MSB_7X 0x28
+#define REG_FREQ_ERROR_MID_7X 0x29
+#define REG_FREQ_ERROR_LSB_7X 0x2a
+#define REG_RSSI_WIDEBAND_7X 0x2c
+#define REG_DETECTION_OPTIMIZE_7X 0x31
+#define REG_HIGH_BW_OPTIMIZE_1_7X 0x36
+#define REG_DETECTION_THRESHOLD_7X 0x37
+#define REG_SYNC_WORD_7X 0x39
+#define REG_HIGH_BW_OPTIMIZE_2_7X 0x3a
+#define REG_DIO_MAPPING_1_7X 0x40
+#define REG_VERSION_7X 0x42
+#define REG_TCXO_7X 0x4b
+#define REG_PA_DAC_7X 0x4d
+
+// Modes
+#define MODE_LONG_RANGE_MODE_7X 0x80
+#define MODE_SLEEP_7X 0x00
+#define MODE_STDBY_7X 0x01
+#define MODE_TX_7X 0x03
+#define MODE_RX_CONTINUOUS_7X 0x05
+#define MODE_RX_SINGLE_7X 0x06
+
+// PA config
+#define PA_BOOST_7X 0x80
+
+// IRQ masks
+#define IRQ_TX_DONE_MASK_7X 0x08
+#define IRQ_RX_DONE_MASK_7X 0x40
+#define IRQ_PAYLOAD_CRC_ERROR_MASK_7X 0x20
+
+#define SYNC_WORD_7X 0x12
+#define MAX_PKT_LENGTH 255
+
+extern SPIClass SPI;
+
+sx127x::sx127x() :
+ _spiSettings(8E6, MSBFIRST, SPI_MODE0),
+ _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN),
+ _frequency(0), _packetIndex(0), _preinit_done(false), _onReceive(NULL) { setTimeout(0); }
+
+void sx127x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); }
+void sx127x::setPins(int ss, int reset, int dio0, int busy) { _ss = ss; _reset = reset; _dio0 = dio0; _busy = busy; }
+uint8_t ISR_VECT sx127x::readRegister(uint8_t address) { return singleTransfer(address & 0x7f, 0x00); }
+void sx127x::writeRegister(uint8_t address, uint8_t value) { singleTransfer(address | 0x80, value); }
+void sx127x::standby() { writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_STDBY_7X); }
+void sx127x::sleep() { writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_SLEEP_7X); }
+void sx127x::setSyncWord(uint8_t sw) { writeRegister(REG_SYNC_WORD_7X, sw); }
+void sx127x::enableCrc() { writeRegister(REG_MODEM_CONFIG_2_7X, readRegister(REG_MODEM_CONFIG_2_7X) | 0x04); }
+void sx127x::disableCrc() { writeRegister(REG_MODEM_CONFIG_2_7X, readRegister(REG_MODEM_CONFIG_2_7X) & 0xfb); }
+void sx127x::enableTCXO() { uint8_t tcxo_reg = readRegister(REG_TCXO_7X); writeRegister(REG_TCXO_7X, tcxo_reg | 0x10); }
+void sx127x::disableTCXO() { uint8_t tcxo_reg = readRegister(REG_TCXO_7X); writeRegister(REG_TCXO_7X, tcxo_reg & 0xEF); }
+void sx127x::explicitHeaderMode() { _implicitHeaderMode = 0; writeRegister(REG_MODEM_CONFIG_1_7X, readRegister(REG_MODEM_CONFIG_1_7X) & 0xfe); }
+void sx127x::implicitHeaderMode() { _implicitHeaderMode = 1; writeRegister(REG_MODEM_CONFIG_1_7X, readRegister(REG_MODEM_CONFIG_1_7X) | 0x01); }
+byte sx127x::random() { return readRegister(REG_RSSI_WIDEBAND_7X); }
+void sx127x::flush() { }
+
+bool sx127x::preInit() {
+ pinMode(_ss, OUTPUT);
+ digitalWrite(_ss, HIGH);
+
+ #if BOARD_MODEL == BOARD_T3S3
+ SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
+ #else
+ SPI.begin();
+ #endif
+
+ // Check modem version
+ uint8_t version;
+ long start = millis();
+ while (((millis() - start) < 500) && (millis() >= start)) {
+ version = readRegister(REG_VERSION_7X);
+ if (version == 0x12) { break; }
+ delay(100);
+ }
+
+ if (version != 0x12) { return false; }
+ _preinit_done = true;
+ return true;
+}
+
+uint8_t ISR_VECT sx127x::singleTransfer(uint8_t address, uint8_t value) {
+ uint8_t response;
+
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(address);
+ response = SPI.transfer(value);
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+
+ return response;
+}
+
+int sx127x::begin(long frequency) {
+ if (_reset != -1) {
+ pinMode(_reset, OUTPUT);
+ digitalWrite(_reset, LOW);
+ delay(10);
+ digitalWrite(_reset, HIGH);
+ delay(10);
+ }
+
+ if (_busy != -1) { pinMode(_busy, INPUT); }
+ if (!_preinit_done) { if (!preInit()) { return false; } }
+
+ sleep();
+ setFrequency(frequency);
+
+ // Set base addresses
+ writeRegister(REG_FIFO_TX_BASE_ADDR_7X, 0);
+ writeRegister(REG_FIFO_RX_BASE_ADDR_7X, 0);
+
+ // Set LNA boost and auto AGC
+ writeRegister(REG_LNA_7X, readRegister(REG_LNA_7X) | 0x03);
+ writeRegister(REG_MODEM_CONFIG_3_7X, 0x04);
+
+ setSyncWord(SYNC_WORD_7X);
+ enableCrc();
+ setTxPower(2);
+
+ standby();
+
+ return 1;
+}
+
+void sx127x::end() { sleep(); SPI.end(); _preinit_done = false; }
+
+int sx127x::beginPacket(int implicitHeader) {
+ standby();
+
+ if (implicitHeader) { implicitHeaderMode(); }
+ else { explicitHeaderMode(); }
+
+ // Reset FIFO address and payload length
+ writeRegister(REG_FIFO_ADDR_PTR_7X, 0);
+ writeRegister(REG_PAYLOAD_LENGTH_7X, 0);
+
+ return 1;
+}
+
+int sx127x::endPacket() {
+ // Enter TX mode
+ writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_TX_7X);
+
+ // Wait for TX completion
+ while ((readRegister(REG_IRQ_FLAGS_7X) & IRQ_TX_DONE_MASK_7X) == 0) {
+ yield();
+ }
+
+ // Clear TX complete IRQ
+ writeRegister(REG_IRQ_FLAGS_7X, IRQ_TX_DONE_MASK_7X);
+ return 1;
+}
+
+bool sx127x::dcd() {
+ bool carrier_detected = false;
+ uint8_t status = readRegister(REG_MODEM_STAT_7X);
+ if ((status & SIG_DETECT) == SIG_DETECT) { carrier_detected = true; }
+ if ((status & SIG_SYNCED) == SIG_SYNCED) { carrier_detected = true; }
+ return carrier_detected;
+}
+
+uint8_t sx127x::currentRssiRaw() {
+ uint8_t rssi = readRegister(REG_RSSI_VALUE_7X);
+ return rssi;
+}
+
+int ISR_VECT sx127x::currentRssi() {
+ int rssi = (int)readRegister(REG_RSSI_VALUE_7X) - RSSI_OFFSET;
+ if (_frequency < 820E6) rssi -= 7;
+ return rssi;
+}
+
+uint8_t sx127x::packetRssiRaw() {
+ uint8_t pkt_rssi_value = readRegister(REG_PKT_RSSI_VALUE_7X);
+ return pkt_rssi_value;
+}
+
+int ISR_VECT sx127x::packetRssi(uint8_t pkt_snr_raw) {
+ int pkt_rssi = (int)readRegister(REG_PKT_RSSI_VALUE_7X) - RSSI_OFFSET;
+ int pkt_snr = ((int8_t)pkt_snr_raw)*0.25;
+
+ if (_frequency < 820E6) pkt_rssi -= 7;
+
+ if (pkt_snr < 0) {
+ pkt_rssi += pkt_snr;
+ } else {
+ // Slope correction is (16/15)*pkt_rssi,
+ // this estimation looses one floating point
+ // operation, and should be precise enough.
+ pkt_rssi = (int)(1.066 * pkt_rssi);
+ }
+ return pkt_rssi;
+}
+
+int ISR_VECT sx127x::packetRssi() {
+ int pkt_rssi = (int)readRegister(REG_PKT_RSSI_VALUE_7X) - RSSI_OFFSET;
+ int pkt_snr = packetSnr();
+
+ if (_frequency < 820E6) pkt_rssi -= 7;
+
+ if (pkt_snr < 0) { pkt_rssi += pkt_snr; }
+ else {
+ // Slope correction is (16/15)*pkt_rssi,
+ // this estimation looses one floating point
+ // operation, and should be precise enough.
+ pkt_rssi = (int)(1.066 * pkt_rssi);
+ }
+ return pkt_rssi;
+}
+
+uint8_t ISR_VECT sx127x::packetSnrRaw() { return readRegister(REG_PKT_SNR_VALUE_7X); }
+
+float ISR_VECT sx127x::packetSnr() { return ((int8_t)readRegister(REG_PKT_SNR_VALUE_7X)) * 0.25; }
+
+long sx127x::packetFrequencyError() {
+ int32_t freqError = 0;
+ freqError = static_cast(readRegister(REG_FREQ_ERROR_MSB_7X) & B111);
+ freqError <<= 8L;
+ freqError += static_cast(readRegister(REG_FREQ_ERROR_MID_7X));
+ freqError <<= 8L;
+ freqError += static_cast(readRegister(REG_FREQ_ERROR_LSB_7X));
+
+ if (readRegister(REG_FREQ_ERROR_MSB_7X) & B1000) { // Sign bit is on
+ freqError -= 524288; // B1000'0000'0000'0000'0000
+ }
+
+ const float fXtal = 32E6; // FXOSC: crystal oscillator (XTAL) frequency (2.5. Chip Specification, p. 14)
+ const float fError = ((static_cast(freqError) * (1L << 24)) / fXtal) * (getSignalBandwidth() / 500000.0f);
+
+ return static_cast(fError);
+}
+
+size_t sx127x::write(uint8_t byte) { return write(&byte, sizeof(byte)); }
+
+size_t sx127x::write(const uint8_t *buffer, size_t size) {
+ int currentLength = readRegister(REG_PAYLOAD_LENGTH_7X);
+ if ((currentLength + size) > MAX_PKT_LENGTH) { size = MAX_PKT_LENGTH - currentLength; }
+
+ for (size_t i = 0; i < size; i++) { writeRegister(REG_FIFO_7X, buffer[i]); }
+ writeRegister(REG_PAYLOAD_LENGTH_7X, currentLength + size);
+
+ return size;
+}
+
+int ISR_VECT sx127x::available() { return (readRegister(REG_RX_NB_BYTES_7X) - _packetIndex); }
+
+int ISR_VECT sx127x::read() {
+ if (!available()) { return -1; }
+ _packetIndex++;
+ return readRegister(REG_FIFO_7X);
+}
+
+int sx127x::peek() {
+ if (!available()) { return -1; }
+
+ // Remember current FIFO address, read, and then reset address
+ int currentAddress = readRegister(REG_FIFO_ADDR_PTR_7X);
+ uint8_t b = readRegister(REG_FIFO_7X);
+ writeRegister(REG_FIFO_ADDR_PTR_7X, currentAddress);
+
+ return b;
+}
+
+void sx127x::onReceive(void(*callback)(int)) {
+ _onReceive = callback;
+
+ if (callback) {
+ pinMode(_dio0, INPUT);
+ writeRegister(REG_DIO_MAPPING_1_7X, 0x00);
+
+ #ifdef SPI_HAS_NOTUSINGINTERRUPT
+ SPI.usingInterrupt(digitalPinToInterrupt(_dio0));
+ #endif
+
+ attachInterrupt(digitalPinToInterrupt(_dio0), sx127x::onDio0Rise, RISING);
+
+ } else {
+ detachInterrupt(digitalPinToInterrupt(_dio0));
+
+ #ifdef SPI_HAS_NOTUSINGINTERRUPT
+ SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0));
+ #endif
+ }
+}
+
+void sx127x::receive(int size) {
+ if (size > 0) {
+ implicitHeaderMode();
+ writeRegister(REG_PAYLOAD_LENGTH_7X, size & 0xff);
+ } else { explicitHeaderMode(); }
+
+ writeRegister(REG_OP_MODE_7X, MODE_LONG_RANGE_MODE_7X | MODE_RX_CONTINUOUS_7X);
+}
+
+void sx127x::setTxPower(int level, int outputPin) {
+ // Setup according to RFO or PA_BOOST output pin
+ if (PA_OUTPUT_RFO_PIN == outputPin) {
+ if (level < 0) { level = 0; }
+ else if (level > 14) { level = 14; }
+
+ writeRegister(REG_PA_DAC_7X, 0x84);
+ writeRegister(REG_PA_CONFIG_7X, 0x70 | level);
+
+ } else {
+ if (level < 2) { level = 2; }
+ else if (level > 17) { level = 17; }
+
+ writeRegister(REG_PA_DAC_7X, 0x84);
+ writeRegister(REG_PA_CONFIG_7X, PA_BOOST_7X | (level - 2));
+ }
+}
+
+uint8_t sx127x::getTxPower() { byte txp = readRegister(REG_PA_CONFIG_7X); return txp; }
+
+void sx127x::setFrequency(unsigned long frequency) {
+ _frequency = frequency;
+ uint32_t frf = ((uint64_t)frequency << 19) / 32000000;
+
+ writeRegister(REG_FRF_MSB_7X, (uint8_t)(frf >> 16));
+ writeRegister(REG_FRF_MID_7X, (uint8_t)(frf >> 8));
+ writeRegister(REG_FRF_LSB_7X, (uint8_t)(frf >> 0));
+
+ optimizeModemSensitivity();
+}
+
+uint32_t sx127x::getFrequency() {
+ uint8_t msb = readRegister(REG_FRF_MSB_7X);
+ uint8_t mid = readRegister(REG_FRF_MID_7X);
+ uint8_t lsb = readRegister(REG_FRF_LSB_7X);
+
+ uint32_t frf = ((uint32_t)msb << 16) | ((uint32_t)mid << 8) | (uint32_t)lsb;
+ uint64_t frm = (uint64_t)frf*32000000;
+ uint32_t frequency = (frm >> 19);
+
+ return frequency;
+}
+
+void sx127x::setSpreadingFactor(int sf) {
+ if (sf < 6) { sf = 6; }
+ else if (sf > 12) { sf = 12; }
+
+ if (sf == 6) {
+ writeRegister(REG_DETECTION_OPTIMIZE_7X, 0xc5);
+ writeRegister(REG_DETECTION_THRESHOLD_7X, 0x0c);
+ } else {
+ writeRegister(REG_DETECTION_OPTIMIZE_7X, 0xc3);
+ writeRegister(REG_DETECTION_THRESHOLD_7X, 0x0a);
+ }
+
+ writeRegister(REG_MODEM_CONFIG_2_7X, (readRegister(REG_MODEM_CONFIG_2_7X) & 0x0f) | ((sf << 4) & 0xf0));
+ handleLowDataRate();
+}
+
+long sx127x::getSignalBandwidth() {
+ byte bw = (readRegister(REG_MODEM_CONFIG_1_7X) >> 4);
+ switch (bw) {
+ case 0: return 7.8E3;
+ case 1: return 10.4E3;
+ case 2: return 15.6E3;
+ case 3: return 20.8E3;
+ case 4: return 31.25E3;
+ case 5: return 41.7E3;
+ case 6: return 62.5E3;
+ case 7: return 125E3;
+ case 8: return 250E3;
+ case 9: return 500E3; }
+
+ return 0;
+}
+
+void sx127x::setSignalBandwidth(long sbw) {
+ int bw;
+ if (sbw <= 7.8E3) {
+ bw = 0;
+ } else if (sbw <= 10.4E3) {
+ bw = 1;
+ } else if (sbw <= 15.6E3) {
+ bw = 2;
+ } else if (sbw <= 20.8E3) {
+ bw = 3;
+ } else if (sbw <= 31.25E3) {
+ bw = 4;
+ } else if (sbw <= 41.7E3) {
+ bw = 5;
+ } else if (sbw <= 62.5E3) {
+ bw = 6;
+ } else if (sbw <= 125E3) {
+ bw = 7;
+ } else if (sbw <= 250E3) {
+ bw = 8;
+ } else /*if (sbw <= 250E3)*/ {
+ bw = 9;
+ }
+
+ writeRegister(REG_MODEM_CONFIG_1_7X, (readRegister(REG_MODEM_CONFIG_1_7X) & 0x0f) | (bw << 4));
+ handleLowDataRate();
+ optimizeModemSensitivity();
+}
+
+void sx127x::setCodingRate4(int denominator) {
+ if (denominator < 5) { denominator = 5; }
+ else if (denominator > 8) { denominator = 8; }
+ int cr = denominator - 4;
+ writeRegister(REG_MODEM_CONFIG_1_7X, (readRegister(REG_MODEM_CONFIG_1_7X) & 0xf1) | (cr << 1));
+}
+
+void sx127x::setPreambleLength(long preamble_symbols) {
+ long length = preamble_symbols - 4;
+ writeRegister(REG_PREAMBLE_MSB_7X, (uint8_t)(length >> 8));
+ writeRegister(REG_PREAMBLE_LSB_7X, (uint8_t)(length >> 0));
+}
+
+extern bool lora_low_datarate;
+void sx127x::handleLowDataRate() {
+ int sf = (readRegister(REG_MODEM_CONFIG_2_7X) >> 4);
+ if ( long( (1< 16) {
+ // Set auto AGC and LowDataRateOptimize
+ writeRegister(REG_MODEM_CONFIG_3_7X, (1<<3)|(1<<2));
+ lora_low_datarate = true;
+ } else {
+ // Only set auto AGC
+ writeRegister(REG_MODEM_CONFIG_3_7X, (1<<2));
+ lora_low_datarate = false;
+ }
+}
+
+void sx127x::optimizeModemSensitivity() {
+ byte bw = (readRegister(REG_MODEM_CONFIG_1_7X) >> 4);
+ uint32_t freq = getFrequency();
+
+ if (bw == 9 && (410E6 <= freq) && (freq <= 525E6)) {
+ writeRegister(REG_HIGH_BW_OPTIMIZE_1_7X, 0x02);
+ writeRegister(REG_HIGH_BW_OPTIMIZE_2_7X, 0x7f);
+ } else if (bw == 9 && (820E6 <= freq) && (freq <= 1020E6)) {
+ writeRegister(REG_HIGH_BW_OPTIMIZE_1_7X, 0x02);
+ writeRegister(REG_HIGH_BW_OPTIMIZE_2_7X, 0x64);
+ } else {
+ writeRegister(REG_HIGH_BW_OPTIMIZE_1_7X, 0x03);
+ }
+}
+
+void ISR_VECT sx127x::handleDio0Rise() {
+ int irqFlags = readRegister(REG_IRQ_FLAGS_7X);
+
+ // Clear IRQs
+ writeRegister(REG_IRQ_FLAGS_7X, irqFlags);
+ if ((irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK_7X) == 0) {
+ _packetIndex = 0;
+ int packetLength = _implicitHeaderMode ? readRegister(REG_PAYLOAD_LENGTH_7X) : readRegister(REG_RX_NB_BYTES_7X);
+ writeRegister(REG_FIFO_ADDR_PTR_7X, readRegister(REG_FIFO_RX_CURRENT_ADDR_7X));
+ if (_onReceive) { _onReceive(packetLength); }
+ writeRegister(REG_FIFO_ADDR_PTR_7X, 0);
+ }
+}
+
+void ISR_VECT sx127x::onDio0Rise() { sx127x_modem.handleDio0Rise(); }
+
+sx127x sx127x_modem;
+
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/sx127x.h b/RNode_Firmware/sx127x.h
new file mode 100644
index 0000000..7639857
--- /dev/null
+++ b/RNode_Firmware/sx127x.h
@@ -0,0 +1,114 @@
+// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva.
+// Licensed under the MIT license.
+
+#ifndef SX1276_H
+#define SX1276_H
+
+#include
+#include
+#include "Modem.h"
+
+#define LORA_DEFAULT_SS_PIN 10
+#define LORA_DEFAULT_RESET_PIN 9
+#define LORA_DEFAULT_DIO0_PIN 2
+#define LORA_DEFAULT_BUSY_PIN -1
+
+#define PA_OUTPUT_RFO_PIN 0
+#define PA_OUTPUT_PA_BOOST_PIN 1
+
+#define RSSI_OFFSET 157
+
+// Modem status flags
+#define SIG_DETECT 0x01
+#define SIG_SYNCED 0x02
+#define RX_ONGOING 0x04
+
+class sx127x : public Stream {
+public:
+ sx127x();
+
+ int begin(long frequency);
+ void end();
+
+ int beginPacket(int implicitHeader = false);
+ int endPacket();
+
+ int parsePacket(int size = 0);
+ int packetRssi();
+ int packetRssi(uint8_t pkt_snr_raw);
+ int currentRssi();
+ uint8_t packetRssiRaw();
+ uint8_t currentRssiRaw();
+ uint8_t packetSnrRaw();
+ float packetSnr();
+ long packetFrequencyError();
+
+ // from Print
+ virtual size_t write(uint8_t byte);
+ virtual size_t write(const uint8_t *buffer, size_t size);
+
+ // from Stream
+ virtual int available();
+ virtual int read();
+ virtual int peek();
+ virtual void flush();
+
+ void onReceive(void(*callback)(int));
+
+ void receive(int size = 0);
+ void standby();
+ void sleep();
+
+ bool preInit();
+ uint8_t getTxPower();
+ void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN);
+ uint32_t getFrequency();
+ void setFrequency(unsigned long frequency);
+ void setSpreadingFactor(int sf);
+ long getSignalBandwidth();
+ void setSignalBandwidth(long sbw);
+ void setCodingRate4(int denominator);
+ void setPreambleLength(long preamble_symbols);
+ void setSyncWord(uint8_t sw);
+ bool dcd();
+ void enableCrc();
+ void disableCrc();
+ void enableTCXO();
+ void disableTCXO();
+
+ byte random();
+
+ void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN);
+ void setSPIFrequency(uint32_t frequency);
+
+private:
+ void explicitHeaderMode();
+ void implicitHeaderMode();
+
+ void handleDio0Rise();
+
+ uint8_t readRegister(uint8_t address);
+ void writeRegister(uint8_t address, uint8_t value);
+ uint8_t singleTransfer(uint8_t address, uint8_t value);
+
+ static void onDio0Rise();
+
+ void handleLowDataRate();
+ void optimizeModemSensitivity();
+
+private:
+ SPISettings _spiSettings;
+ int _ss;
+ int _reset;
+ int _dio0;
+ int _busy;
+ long _frequency;
+ int _packetIndex;
+ int _implicitHeaderMode;
+ bool _preinit_done;
+ void (*_onReceive)(int);
+};
+
+extern sx127x sx127x_modem;
+
+#endif
diff --git a/RNode_Firmware/sx128x.cpp b/RNode_Firmware/sx128x.cpp
new file mode 100644
index 0000000..59c464e
--- /dev/null
+++ b/RNode_Firmware/sx128x.cpp
@@ -0,0 +1,887 @@
+// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva.
+// Licensed under the MIT license.
+
+#include "Boards.h"
+
+#if MODEM == SX1280
+#include "sx128x.h"
+
+#define MCU_1284P 0x91
+#define MCU_2560 0x92
+#define MCU_ESP32 0x81
+#define MCU_NRF52 0x71
+#if defined(__AVR_ATmega1284P__)
+ #define PLATFORM PLATFORM_AVR
+ #define MCU_VARIANT MCU_1284P
+#elif defined(__AVR_ATmega2560__)
+ #define PLATFORM PLATFORM_AVR
+ #define MCU_VARIANT MCU_2560
+#elif defined(ESP32)
+ #define PLATFORM PLATFORM_ESP32
+ #define MCU_VARIANT MCU_ESP32
+#elif defined(NRF52840_XXAA)
+ #define PLATFORM PLATFORM_NRF52
+ #define MCU_VARIANT MCU_NRF52
+#endif
+
+#ifndef MCU_VARIANT
+ #error No MCU variant defined, cannot compile
+#endif
+
+#if MCU_VARIANT == MCU_ESP32
+ #if MCU_VARIANT == MCU_ESP32 and !defined(CONFIG_IDF_TARGET_ESP32S3)
+ #include "hal/wdt_hal.h"
+ #endif
+ #define ISR_VECT IRAM_ATTR
+#else
+ #define ISR_VECT
+#endif
+
+// SX128x registers
+#define OP_RF_FREQ_8X 0x86
+#define OP_SLEEP_8X 0x84
+#define OP_STANDBY_8X 0x80
+#define OP_TX_8X 0x83
+#define OP_RX_8X 0x82
+#define OP_SET_IRQ_FLAGS_8X 0x8D
+#define OP_CLEAR_IRQ_STATUS_8X 0x97
+#define OP_GET_IRQ_STATUS_8X 0x15
+#define OP_RX_BUFFER_STATUS_8X 0x17
+#define OP_PACKET_STATUS_8X 0x1D
+#define OP_CURRENT_RSSI_8X 0x1F
+#define OP_MODULATION_PARAMS_8X 0x8B
+#define OP_PACKET_PARAMS_8X 0x8C
+#define OP_STATUS_8X 0xC0
+#define OP_TX_PARAMS_8X 0x8E
+#define OP_PACKET_TYPE_8X 0x8A
+#define OP_BUFFER_BASE_ADDR_8X 0x8F
+#define OP_READ_REGISTER_8X 0x19
+#define OP_WRITE_REGISTER_8X 0x18
+#define IRQ_TX_DONE_MASK_8X 0x01
+#define IRQ_RX_DONE_MASK_8X 0x02
+#define IRQ_HEADER_DET_MASK_8X 0x10
+#define IRQ_HEADER_ERROR_MASK_8X 0x20
+#define IRQ_PAYLOAD_CRC_ERROR_MASK_8X 0x40
+
+#define MODE_LONG_RANGE_MODE_8X 0x01
+
+#define OP_FIFO_WRITE_8X 0x1A
+#define OP_FIFO_READ_8X 0x1B
+#define IRQ_PREAMBLE_DET_MASK_8X 0x80
+
+#define REG_PACKET_SIZE 0x901
+#define REG_FIRM_VER_MSB 0x154
+#define REG_FIRM_VER_LSB 0x153
+
+#define XTAL_FREQ_8X (double)52000000
+#define FREQ_DIV_8X (double)pow(2.0, 18.0)
+#define FREQ_STEP_8X (double)(XTAL_FREQ_8X / FREQ_DIV_8X)
+
+#if defined(NRF52840_XXAA)
+ extern SPIClass spiModem;
+ #define SPI spiModem
+#endif
+
+extern SPIClass SPI;
+
+#define MAX_PKT_LENGTH 255
+
+sx128x::sx128x() :
+ _spiSettings(8E6, MSBFIRST, SPI_MODE0),
+ _ss(LORA_DEFAULT_SS_PIN), _reset(LORA_DEFAULT_RESET_PIN), _dio0(LORA_DEFAULT_DIO0_PIN), _rxen(pin_rxen), _busy(LORA_DEFAULT_BUSY_PIN), _txen(pin_txen),
+ _frequency(0), _txp(0), _sf(0x05), _bw(0x34), _cr(0x01), _packetIndex(0), _implicitHeaderMode(0), _payloadLength(255), _crcMode(0), _fifo_tx_addr_ptr(0),
+ _fifo_rx_addr_ptr(0), _rxPacketLength(0), _preinit_done(false), _tcxo(false) { setTimeout(0); }
+
+bool ISR_VECT sx128x::getPacketValidity() {
+ uint8_t buf[2];
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2);
+ executeOpcode(OP_CLEAR_IRQ_STATUS_8X, buf, 2);
+ if ((buf[1] & IRQ_PAYLOAD_CRC_ERROR_MASK_8X) == 0) { return true; }
+ else { return false; }
+}
+
+void ISR_VECT sx128x::onDio0Rise() {
+ BaseType_t int_status = taskENTER_CRITICAL_FROM_ISR();
+ // On the SX1280, there is a bug which can cause the busy line
+ // to remain high if a high amount of packets are received when
+ // in continuous RX mode. This is documented as Errata 16.1 in
+ // the SX1280 datasheet v3.2 (page 149)
+ // Therefore, the modem is set into receive mode each time a packet is received.
+ if (sx128x_modem.getPacketValidity()) { sx128x_modem.receive(); sx128x_modem.handleDio0Rise(); }
+ else { sx128x_modem.receive(); }
+
+ taskEXIT_CRITICAL_FROM_ISR(int_status);
+}
+
+void sx128x::handleDio0Rise() {
+ _packetIndex = 0;
+ uint8_t rxbuf[2] = {0};
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_8X, rxbuf, 2);
+
+ // If implicit header mode is enabled, use pre-set packet length as payload length instead.
+ // See SX1280 datasheet v3.2, page 92
+ if (_implicitHeaderMode == 0x80) { _rxPacketLength = _payloadLength; }
+ else { _rxPacketLength = rxbuf[0]; }
+
+ if (_receive_callback) { _receive_callback(_rxPacketLength); }
+}
+
+bool sx128x::preInit() {
+ pinMode(_ss, OUTPUT);
+ digitalWrite(_ss, HIGH);
+
+ // TODO: Check if this change causes issues on any platforms
+ #if MCU_VARIANT == MCU_ESP32
+ #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK
+ SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
+ #else
+ SPI.begin();
+ #endif
+ #else
+ SPI.begin();
+ #endif
+
+ // Detect modem (retry for up to 500ms)
+ long start = millis();
+ uint8_t version_msb;
+ uint8_t version_lsb;
+ while (((millis() - start) < 500) && (millis() >= start)) {
+ version_msb = readRegister(REG_FIRM_VER_MSB);
+ version_lsb = readRegister(REG_FIRM_VER_LSB);
+ if ((version_msb == 0xB7 && version_lsb == 0xA9) || (version_msb == 0xB5 && version_lsb == 0xA9)) { break; }
+ delay(100);
+ }
+
+ if ((version_msb != 0xB7 || version_lsb != 0xA9) && (version_msb != 0xB5 || version_lsb != 0xA9)) { return false; }
+ _preinit_done = true;
+ return true;
+}
+
+uint8_t ISR_VECT sx128x::readRegister(uint16_t address) { return singleTransfer(OP_READ_REGISTER_8X, address, 0x00); }
+void sx128x::writeRegister(uint16_t address, uint8_t value) { singleTransfer(OP_WRITE_REGISTER_8X, address, value); }
+
+uint8_t ISR_VECT sx128x::singleTransfer(uint8_t opcode, uint16_t address, uint8_t value) {
+ waitOnBusy();
+ uint8_t response;
+ digitalWrite(_ss, LOW);
+
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(opcode);
+ SPI.transfer((address & 0xFF00) >> 8);
+ SPI.transfer(address & 0x00FF);
+ if (opcode == OP_READ_REGISTER_8X) { SPI.transfer(0x00); }
+ response = SPI.transfer(value);
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+
+ return response;
+}
+
+void sx128x::rxAntEnable() {
+ if (_txen != -1) { digitalWrite(_txen, LOW); }
+ if (_rxen != -1) { digitalWrite(_rxen, HIGH); }
+}
+
+void sx128x::txAntEnable() {
+ if (_txen != -1) { digitalWrite(_txen, HIGH); }
+ if (_rxen != -1) { digitalWrite(_rxen, LOW); }
+}
+
+void sx128x::loraMode() {
+ uint8_t mode = MODE_LONG_RANGE_MODE_8X;
+ executeOpcode(OP_PACKET_TYPE_8X, &mode, 1);
+}
+
+void sx128x::waitOnBusy() {
+ unsigned long time = millis();
+ while (digitalRead(_busy) == HIGH) {
+ if (millis() >= (time + 100)) { break; }
+ }
+}
+
+void sx128x::executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(opcode);
+ for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx128x::executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(opcode);
+ SPI.transfer(0x00);
+ for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx128x::writeBuffer(const uint8_t* buffer, size_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(OP_FIFO_WRITE_8X);
+ SPI.transfer(_fifo_tx_addr_ptr);
+ for (int i = 0; i < size; i++) { SPI.transfer(buffer[i]); _fifo_tx_addr_ptr++; }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx128x::readBuffer(uint8_t* buffer, size_t size) {
+ waitOnBusy();
+ digitalWrite(_ss, LOW);
+ SPI.beginTransaction(_spiSettings);
+ SPI.transfer(OP_FIFO_READ_8X);
+ SPI.transfer(_fifo_rx_addr_ptr);
+ SPI.transfer(0x00);
+ for (int i = 0; i < size; i++) { buffer[i] = SPI.transfer(0x00); }
+ SPI.endTransaction();
+ digitalWrite(_ss, HIGH);
+}
+
+void sx128x::setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr) {
+ // because there is no access to these registers on the sx1280, we have
+ // to set all these parameters at once or not at all.
+ uint8_t buf[3];
+ buf[0] = sf << 4;
+ buf[1] = bw;
+ buf[2] = cr;
+ executeOpcode(OP_MODULATION_PARAMS_8X, buf, 3);
+
+ if (sf <= 6) { writeRegister(0x925, 0x1E); }
+ else if (sf <= 8) { writeRegister(0x925, 0x37); }
+ else if (sf >= 9) { writeRegister(0x925, 0x32); }
+ writeRegister(0x093C, 0x1);
+}
+
+uint8_t preamble_e = 0;
+uint8_t preamble_m = 0;
+uint32_t last_me_result_target = 0;
+extern long lora_preamble_symbols;
+void sx128x::setPacketParams(uint32_t target_preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc) {
+ if (last_me_result_target != target_preamble_symbols) {
+ // Calculate exponent and mantissa values for modem
+ if (target_preamble_symbols >= 0xF000) target_preamble_symbols = 0xF000;
+ uint32_t calculated_preamble_symbols;
+ uint8_t e = 1;
+ uint8_t m = 1;
+ while (e <= 15) {
+ while (m <= 15) {
+ calculated_preamble_symbols = m * (pow(2,e));
+ if (calculated_preamble_symbols >= target_preamble_symbols-4) break;
+ m++;
+ }
+
+ if (calculated_preamble_symbols >= target_preamble_symbols-4) break;
+ m = 1; e++;
+ }
+
+ last_me_result_target = target_preamble_symbols;
+ lora_preamble_symbols = calculated_preamble_symbols+4;
+ _preambleLength = lora_preamble_symbols;
+
+ preamble_e = e;
+ preamble_m = m;
+ }
+
+ uint8_t buf[7];
+ buf[0] = (preamble_e << 4) | preamble_m;
+ buf[1] = headermode;
+ buf[2] = payload_length;
+ buf[3] = crc;
+ buf[4] = 0x40; // Standard IQ setting (no inversion)
+ buf[5] = 0x00; // Unused params
+ buf[6] = 0x00;
+
+ executeOpcode(OP_PACKET_PARAMS_8X, buf, 7);
+}
+
+void sx128x::reset() {
+ if (_reset != -1) {
+ pinMode(_reset, OUTPUT);
+ digitalWrite(_reset, LOW);
+ delay(10);
+ digitalWrite(_reset, HIGH);
+ delay(10);
+ }
+}
+
+int sx128x::begin(unsigned long frequency) {
+ reset();
+
+ if (_rxen != -1) { pinMode(_rxen, OUTPUT); }
+ if (_txen != -1) { pinMode(_txen, OUTPUT); }
+ if (_busy != -1) { pinMode(_busy, INPUT); }
+
+ if (!_preinit_done) {
+ if (!preInit()) {
+ return false;
+ }
+ }
+
+ standby();
+ loraMode();
+ rxAntEnable();
+ setFrequency(frequency);
+
+ // TODO: Implement LNA boost
+ //writeRegister(REG_LNA, 0x96);
+
+ setModulationParams(_sf, _bw, _cr);
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+ setTxPower(_txp);
+
+ // Set base addresses
+ uint8_t basebuf[2] = {0};
+ executeOpcode(OP_BUFFER_BASE_ADDR_8X, basebuf, 2);
+
+ _radio_online = true;
+ return 1;
+}
+
+void sx128x::end() {
+ sleep();
+ SPI.end();
+ _bitrate = 0;
+ _radio_online = false;
+ _preinit_done = false;
+}
+
+int sx128x::beginPacket(int implicitHeader) {
+ standby();
+
+ if (implicitHeader) { implicitHeaderMode(); }
+ else { explicitHeaderMode(); }
+
+ _payloadLength = 0;
+ _fifo_tx_addr_ptr = 0;
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+
+ return 1;
+}
+
+int sx128x::endPacket() {
+ setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+ txAntEnable();
+
+ // Put in single TX mode
+ uint8_t timeout[3] = {0};
+ executeOpcode(OP_TX_8X, timeout, 3);
+
+ uint8_t buf[2];
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2);
+
+ // Wait for TX done
+ bool timed_out = false;
+ uint32_t w_timeout = millis()+LORA_MODEM_TIMEOUT_MS;
+ while ((millis() < w_timeout) && ((buf[1] & IRQ_TX_DONE_MASK_8X) == 0)) {
+ buf[0] = 0x00;
+ buf[1] = 0x00;
+ executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2);
+ yield();
+ }
+
+ if (!(millis() < w_timeout)) { timed_out = true; }
+
+ // clear IRQ's
+ uint8_t mask[2];
+ mask[0] = 0x00;
+ mask[1] = IRQ_TX_DONE_MASK_8X;
+ executeOpcode(OP_CLEAR_IRQ_STATUS_8X, mask, 2);
+
+ if (timed_out) { return 0; }
+ else { return 1; }
+}
+
+unsigned long preamble_detected_at = 0;
+extern long lora_preamble_time_ms;
+extern long lora_header_time_ms;
+bool false_preamble_detected = false;
+bool sx128x::dcd() {
+ uint8_t buf[2] = {0}; executeOpcodeRead(OP_GET_IRQ_STATUS_8X, buf, 2);
+ uint32_t now = millis();
+
+ bool header_detected = false;
+ bool carrier_detected = false;
+
+ if ((buf[1] & IRQ_HEADER_DET_MASK_8X) != 0) { header_detected = true; carrier_detected = true; }
+ else { header_detected = false; }
+
+ if ((buf[0] & IRQ_PREAMBLE_DET_MASK_8X) != 0) {
+ carrier_detected = true;
+ if (preamble_detected_at == 0) { preamble_detected_at = now; }
+ if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) {
+ preamble_detected_at = 0;
+ if (!header_detected) { false_preamble_detected = true; }
+ uint8_t clearbuf[2] = {0}; clearbuf[0] = IRQ_PREAMBLE_DET_MASK_8X;
+ executeOpcode(OP_CLEAR_IRQ_STATUS_8X, clearbuf, 2);
+ }
+ }
+
+ // TODO: Maybe there's a way of unlatching the RSSI
+ // status without re-activating receive mode?
+ if (false_preamble_detected) { sx128x_modem.receive(); false_preamble_detected = false; }
+ return carrier_detected;
+}
+
+
+uint8_t sx128x::currentRssiRaw() {
+ uint8_t byte = 0;
+ executeOpcodeRead(OP_CURRENT_RSSI_8X, &byte, 1);
+ return byte;
+}
+
+int ISR_VECT sx128x::currentRssi() {
+ uint8_t byte = 0;
+ executeOpcodeRead(OP_CURRENT_RSSI_8X, &byte, 1);
+ int rssi = -byte / 2;
+ return rssi;
+}
+
+uint8_t sx128x::packetRssiRaw() {
+ uint8_t buf[5] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5);
+ return buf[0];
+}
+
+int ISR_VECT sx128x::packetRssi(uint8_t pkt_snr_raw) {
+ // TODO: May need more calculations here
+ uint8_t buf[5] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5);
+ int pkt_rssi = -buf[0] / 2;
+ return pkt_rssi;
+}
+
+uint8_t ISR_VECT sx128x::packetSnrRaw() {
+ uint8_t buf[5] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5);
+ return buf[1];
+}
+
+float ISR_VECT sx128x::packetSnr() {
+ uint8_t buf[5] = {0};
+ executeOpcodeRead(OP_PACKET_STATUS_8X, buf, 5);
+ return float(buf[1]) * 0.25;
+}
+
+long sx128x::packetFrequencyError() {
+ // TODO: Implement this, page 120 of sx1280 datasheet
+ int32_t freqError = 0;
+ const float fError = 0.0;
+ return static_cast(fError);
+}
+
+void sx128x::flush() { }
+int ISR_VECT sx128x::available() { return _rxPacketLength - _packetIndex; }
+size_t sx128x::write(uint8_t byte) { return write(&byte, sizeof(byte)); }
+size_t sx128x::write(const uint8_t *buffer, size_t size) {
+ if ((_payloadLength + size) > MAX_PKT_LENGTH) { size = MAX_PKT_LENGTH - _payloadLength; }
+ writeBuffer(buffer, size);
+ _payloadLength = _payloadLength + size;
+ return size;
+}
+
+int ISR_VECT sx128x::read() {
+ if (!available()) { return -1; }
+
+ // If received new packet
+ if (_packetIndex == 0) {
+ uint8_t rxbuf[2] = {0};
+ executeOpcodeRead(OP_RX_BUFFER_STATUS_8X, rxbuf, 2);
+ int size;
+
+ // If implicit header mode is enabled, read packet length as payload length instead.
+ // See SX1280 datasheet v3.2, page 92
+ if (_implicitHeaderMode == 0x80) {
+ size = _payloadLength;
+ } else {
+ size = rxbuf[0];
+ }
+
+ _fifo_rx_addr_ptr = rxbuf[1];
+ if (size > 255) { size = 255; }
+
+ readBuffer(_packet, size);
+ }
+
+ uint8_t byte = _packet[_packetIndex];
+ _packetIndex++;
+ return byte;
+}
+
+int sx128x::peek() {
+ if (!available()) { return -1; }
+ uint8_t b = _packet[_packetIndex];
+ return b;
+}
+
+
+void sx128x::onReceive(void(*callback)(int)) {
+ _receive_callback = callback;
+
+ if (callback) {
+ pinMode(_dio0, INPUT);
+
+ // Set preamble and header detection irqs, plus dio0 mask
+ uint8_t buf[8];
+
+ // Set irq masks, enable all
+ buf[0] = 0xFF;
+ buf[1] = 0xFF;
+
+ // On the SX1280, no RxDone IRQ is generated if a packet is received with
+ // an invalid header, but the modem will be taken out of single RX mode.
+ // This can cause the modem to not receive packets until it is reset
+ // again. This is documented as Errata 16.2 in the SX1280 datasheet v3.2
+ // (page 150) Below, the header error IRQ is mapped to dio0 so that the
+ // modem can be set into RX mode again on reception of a corrupted
+ // header.
+ // set dio0 masks
+ buf[2] = 0x00;
+ buf[3] = IRQ_RX_DONE_MASK_8X | IRQ_HEADER_ERROR_MASK_8X;
+
+ // Set dio1 masks
+ buf[4] = 0x00;
+ buf[5] = 0x00;
+
+ // Set dio2 masks
+ buf[6] = 0x00;
+ buf[7] = 0x00;
+
+ executeOpcode(OP_SET_IRQ_FLAGS_8X, buf, 8);
+
+ #ifdef SPI_HAS_NOTUSINGINTERRUPT
+ SPI.usingInterrupt(digitalPinToInterrupt(_dio0));
+ #endif
+
+ attachInterrupt(digitalPinToInterrupt(_dio0), onDio0Rise, RISING);
+
+ } else {
+ detachInterrupt(digitalPinToInterrupt(_dio0));
+ #ifdef SPI_HAS_NOTUSINGINTERRUPT
+ _spiModem->notUsingInterrupt(digitalPinToInterrupt(_dio0));
+ #endif
+ }
+}
+
+void sx128x::receive(int size) {
+ if (size > 0) {
+ implicitHeaderMode();
+ // Tell radio payload length
+ //_rxPacketLength = size;
+ //_payloadLength = size;
+ //setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode);
+ } else {
+ explicitHeaderMode();
+ }
+
+ rxAntEnable();
+
+ // On the SX1280, there is a bug which can cause the busy line
+ // to remain high if a high amount of packets are received when
+ // in continuous RX mode. This is documented as Errata 16.1 in
+ // the SX1280 datasheet v3.2 (page 149)
+ // Therefore, the modem is set to single RX mode below instead.
+
+ // uint8_t mode[3] = {0x03, 0xFF, 0xFF}; // Countinuous RX mode
+ uint8_t mode[3] = {0}; // single RX mode
+ executeOpcode(OP_RX_8X, mode, 3);
+}
+
+void sx128x::standby() {
+ uint8_t byte = 0x01; // Always use STDBY_XOSC
+ executeOpcode(OP_STANDBY_8X, &byte, 1);
+}
+
+void sx128x::setPins(int ss, int reset, int dio0, int busy, int rxen, int txen) {
+ _ss = ss;
+ _reset = reset;
+ _dio0 = dio0;
+ _busy = busy;
+ _rxen = rxen;
+ _txen = txen;
+}
+
+void sx128x::setTxPower(int level, int outputPin) {
+ uint8_t tx_buf[2];
+
+ // RAK4631 with WisBlock SX1280 module (LIBSYS002)
+ #if BOARD_VARIANT == MODEL_13 || BOARD_VARIANT == MODEL_21
+ if (level > 27) { level = 27; }
+ else if (level < 0) { level = 0; }
+
+ _txp = level;
+ int reg_value;
+ switch (level) {
+ case 0:
+ reg_value = -18;
+ break;
+ case 1:
+ reg_value = -16;
+ break;
+ case 2:
+ reg_value = -15;
+ break;
+ case 3:
+ reg_value = -14;
+ break;
+ case 4:
+ reg_value = -13;
+ break;
+ case 5:
+ reg_value = -12;
+ break;
+ case 6:
+ reg_value = -11;
+ break;
+ case 7:
+ reg_value = -9;
+ break;
+ case 8:
+ reg_value = -8;
+ break;
+ case 9:
+ reg_value = -7;
+ break;
+ case 10:
+ reg_value = -6;
+ break;
+ case 11:
+ reg_value = -5;
+ break;
+ case 12:
+ reg_value = -4;
+ break;
+ case 13:
+ reg_value = -3;
+ break;
+ case 14:
+ reg_value = -2;
+ break;
+ case 15:
+ reg_value = -1;
+ break;
+ case 16:
+ reg_value = 0;
+ break;
+ case 17:
+ reg_value = 1;
+ break;
+ case 18:
+ reg_value = 2;
+ break;
+ case 19:
+ reg_value = 3;
+ break;
+ case 20:
+ reg_value = 4;
+ break;
+ case 21:
+ reg_value = 5;
+ break;
+ case 22:
+ reg_value = 6;
+ break;
+ case 23:
+ reg_value = 7;
+ break;
+ case 24:
+ reg_value = 8;
+ break;
+ case 25:
+ reg_value = 9;
+ break;
+ case 26:
+ reg_value = 12;
+ break;
+ case 27:
+ reg_value = 13;
+ break;
+ default:
+ reg_value = 0;
+ break;
+ }
+
+ tx_buf[0] = reg_value + 18;
+ tx_buf[1] = 0xE0; // Ramping time, 20 microseconds
+ executeOpcode(OP_TX_PARAMS_8X, tx_buf, 2);
+
+ // T3S3 SX1280 PA
+ #elif BOARD_VARIANT == MODEL_AC
+ if (level > 20) { level = 20; }
+ else if (level < 0) { level = 0; }
+
+ _txp = level;
+ int reg_value;
+ switch (level) {
+ case 0:
+ reg_value = -18;
+ break;
+ case 1:
+ reg_value = -17;
+ break;
+ case 2:
+ reg_value = -16;
+ break;
+ case 3:
+ reg_value = -15;
+ break;
+ case 4:
+ reg_value = -14;
+ break;
+ case 5:
+ reg_value = -13;
+ break;
+ case 6:
+ reg_value = -12;
+ break;
+ case 7:
+ reg_value = -10;
+ break;
+ case 8:
+ reg_value = -9;
+ break;
+ case 9:
+ reg_value = -8;
+ break;
+ case 10:
+ reg_value = -7;
+ break;
+ case 11:
+ reg_value = -6;
+ break;
+ case 12:
+ reg_value = -5;
+ break;
+ case 13:
+ reg_value = -4;
+ break;
+ case 14:
+ reg_value = -3;
+ break;
+ case 15:
+ reg_value = -2;
+ break;
+ case 16:
+ reg_value = -1;
+ break;
+ case 17:
+ reg_value = 0;
+ break;
+ case 18:
+ reg_value = 1;
+ break;
+ case 19:
+ reg_value = 2;
+ break;
+ case 20:
+ reg_value = 3;
+ break;
+ default:
+ reg_value = 0;
+ break;
+ }
+ tx_buf[0] = reg_value;
+ tx_buf[1] = 0xE0; // Ramping time, 20 microseconds
+
+ // For SX1280 boards with no specific PA requirements
+ #else
+ if (level > 13) { level = 13; }
+ else if (level < -18) { level = -18; }
+ _txp = level;
+ tx_buf[0] = level + 18;
+ tx_buf[1] = 0xE0; // Ramping time, 20 microseconds
+ #endif
+
+ executeOpcode(OP_TX_PARAMS_8X, tx_buf, 2);
+}
+
+void sx128x::setFrequency(uint32_t frequency) {
+ _frequency = frequency;
+ uint8_t buf[3];
+ uint32_t freq = (uint32_t)((double)frequency / (double)FREQ_STEP_8X);
+ buf[0] = ((freq >> 16) & 0xFF);
+ buf[1] = ((freq >> 8) & 0xFF);
+ buf[2] = (freq & 0xFF);
+
+ executeOpcode(OP_RF_FREQ_8X, buf, 3);
+}
+
+uint32_t sx128x::getFrequency() {
+ // We can't read the frequency on the sx1280
+ uint32_t frequency = _frequency;
+ return frequency;
+}
+
+void sx128x::setSpreadingFactor(int sf) {
+ if (sf < 5) { sf = 5; }
+ else if (sf > 12) { sf = 12; }
+ _sf = sf;
+
+ setModulationParams(sf, _bw, _cr);
+ handleLowDataRate();
+}
+
+uint32_t sx128x::getSignalBandwidth() {
+ int bw = _bw;
+ switch (bw) {
+ case 0x34: return 203.125E3;
+ case 0x26: return 406.25E3;
+ case 0x18: return 812.5E3;
+ case 0x0A: return 1625E3;
+ }
+
+ return 0;
+}
+
+void sx128x::setSignalBandwidth(uint32_t sbw) {
+ if (sbw <= 203.125E3) { _bw = 0x34; }
+ else if (sbw <= 406.25E3) { _bw = 0x26; }
+ else if (sbw <= 812.5E3) { _bw = 0x18; }
+ else { _bw = 0x0A; }
+
+ setModulationParams(_sf, _bw, _cr);
+ handleLowDataRate();
+ optimizeModemSensitivity();
+}
+
+// TODO: add support for new interleaving scheme, see page 117 of sx1280 datasheet
+void sx128x::setCodingRate4(int denominator) {
+ if (denominator < 5) { denominator = 5; }
+ else if (denominator > 8) { denominator = 8; }
+ _cr = denominator - 4;
+ setModulationParams(_sf, _bw, _cr);
+}
+
+extern bool lora_low_datarate;
+void sx128x::handleLowDataRate() {
+ if (_sf > 10) { lora_low_datarate = true; }
+ else { lora_low_datarate = false; }
+}
+
+void sx128x::optimizeModemSensitivity() { } // TODO: Check if there's anything the sx1280 can do here
+uint8_t sx128x::getCodingRate4() { return _cr + 4; }
+void sx128x::setPreambleLength(long preamble_symbols) { setPacketParams(preamble_symbols, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx128x::setSyncWord(int sw) { } // TODO: Implement
+void sx128x::enableTCXO() { } // TODO: Need to check how to implement on sx1280
+void sx128x::disableTCXO() { } // TODO: Need to check how to implement on sx1280
+void sx128x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_8X, &byte, 1); }
+uint8_t sx128x::getTxPower() { return _txp; }
+uint8_t sx128x::getSpreadingFactor() { return _sf; }
+void sx128x::enableCrc() { _crcMode = 0x20; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx128x::disableCrc() { _crcMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx128x::setSPIFrequency(uint32_t frequency) { _spiSettings = SPISettings(frequency, MSBFIRST, SPI_MODE0); }
+void sx128x::explicitHeaderMode() { _implicitHeaderMode = 0; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx128x::implicitHeaderMode() { _implicitHeaderMode = 0x80; setPacketParams(_preambleLength, _implicitHeaderMode, _payloadLength, _crcMode); }
+void sx128x::dumpRegisters(Stream& out) { for (int i = 0; i < 128; i++) { out.print("0x"); out.print(i, HEX); out.print(": 0x"); out.println(readRegister(i), HEX); } }
+
+sx128x sx128x_modem;
+#endif
\ No newline at end of file
diff --git a/RNode_Firmware/sx128x.h b/RNode_Firmware/sx128x.h
new file mode 100644
index 0000000..ccaeb04
--- /dev/null
+++ b/RNode_Firmware/sx128x.h
@@ -0,0 +1,146 @@
+// Copyright Sandeep Mistry, Mark Qvist and Jacob Eva.
+// Licensed under the MIT license.
+
+#ifndef SX128X_H
+#define SX128X_H
+
+#include
+#include
+#include "Modem.h"
+
+#define LORA_DEFAULT_SS_PIN 10
+#define LORA_DEFAULT_RESET_PIN 9
+#define LORA_DEFAULT_DIO0_PIN 2
+#define LORA_DEFAULT_RXEN_PIN -1
+#define LORA_DEFAULT_TXEN_PIN -1
+#define LORA_DEFAULT_BUSY_PIN -1
+#define LORA_MODEM_TIMEOUT_MS 15E3
+#define PA_OUTPUT_RFO_PIN 0
+#define PA_OUTPUT_PA_BOOST_PIN 1
+#define RSSI_OFFSET 157
+
+class sx128x : public Stream {
+public:
+ sx128x();
+
+ int begin(unsigned long frequency);
+ void end();
+ void reset();
+
+ int beginPacket(int implicitHeader = false);
+ int endPacket();
+
+ int parsePacket(int size = 0);
+ int packetRssi();
+ int packetRssi(uint8_t pkt_snr_raw);
+ int currentRssi();
+ uint8_t packetRssiRaw();
+ uint8_t currentRssiRaw();
+ uint8_t packetSnrRaw();
+ float packetSnr();
+ long packetFrequencyError();
+
+ // from Print
+ virtual size_t write(uint8_t byte);
+ virtual size_t write(const uint8_t *buffer, size_t size);
+
+ // from Stream
+ virtual int available();
+ virtual int read();
+ virtual int peek();
+ virtual void flush();
+
+ void onReceive(void(*callback)(int));
+
+ void receive(int size = 0);
+ void standby();
+ void sleep();
+
+ bool preInit();
+ uint8_t getTxPower();
+ void setTxPower(int level, int outputPin = PA_OUTPUT_PA_BOOST_PIN);
+ uint32_t getFrequency();
+ void setFrequency(uint32_t frequency);
+ void setSpreadingFactor(int sf);
+ uint8_t getSpreadingFactor();
+ uint32_t getSignalBandwidth();
+ void setSignalBandwidth(uint32_t sbw);
+ void setCodingRate4(int denominator);
+ uint8_t getCodingRate4();
+ void setPreambleLength(long preamble_symbols);
+ void setSyncWord(int sw);
+ bool dcd();
+ void clearIRQStatus();
+ void enableCrc();
+ void disableCrc();
+ void enableTCXO();
+ void disableTCXO();
+
+ void txAntEnable();
+ void rxAntEnable();
+ void loraMode();
+ void waitOnBusy();
+ void executeOpcode(uint8_t opcode, uint8_t *buffer, uint8_t size);
+ void executeOpcodeRead(uint8_t opcode, uint8_t *buffer, uint8_t size);
+ void writeBuffer(const uint8_t* buffer, size_t size);
+ void readBuffer(uint8_t* buffer, size_t size);
+ void setPacketParams(uint32_t target_preamble_symbols, uint8_t headermode, uint8_t payload_length, uint8_t crc);
+ void setModulationParams(uint8_t sf, uint8_t bw, uint8_t cr);
+
+ void crc() { enableCrc(); }
+ void noCrc() { disableCrc(); }
+
+ void setPins(int ss = LORA_DEFAULT_SS_PIN, int reset = LORA_DEFAULT_RESET_PIN, int dio0 = LORA_DEFAULT_DIO0_PIN, int busy = LORA_DEFAULT_BUSY_PIN, int rxen = LORA_DEFAULT_RXEN_PIN, int txen = LORA_DEFAULT_TXEN_PIN);
+ void setSPIFrequency(uint32_t frequency);
+
+ void dumpRegisters(Stream& out);
+
+private:
+ void explicitHeaderMode();
+ void implicitHeaderMode();
+
+ bool getPacketValidity();
+ void handleDio0Rise();
+
+ uint8_t readRegister(uint16_t address);
+ void writeRegister(uint16_t address, uint8_t value);
+ uint8_t singleTransfer(uint8_t opcode, uint16_t address, uint8_t value);
+
+ static void onDio0Rise();
+
+ void handleLowDataRate();
+ void optimizeModemSensitivity();
+
+private:
+ SPISettings _spiSettings;
+ int _ss;
+ int _reset;
+ int _dio0;
+ int _rxen;
+ int _txen;
+ int _busy;
+ int _modem;
+ unsigned long _frequency;
+ int _txp;
+ uint8_t _sf;
+ uint8_t _bw;
+ uint8_t _cr;
+ int _packetIndex;
+ uint32_t _preambleLength;
+ int _implicitHeaderMode;
+ int _payloadLength;
+ int _crcMode;
+ int _fifo_tx_addr_ptr;
+ int _fifo_rx_addr_ptr;
+ uint8_t _packet[256];
+ bool _preinit_done;
+ bool _tcxo;
+ bool _radio_online;
+ int _rxPacketLength;
+ uint32_t _bitrate;
+ void (*_receive_callback)(int);
+};
+
+extern sx128x sx128x_modem;
+
+#endif
diff --git a/Utilities.h b/Utilities.h
index b31ff90..0f85a80 100644
--- a/Utilities.h
+++ b/Utilities.h
@@ -322,6 +322,14 @@ uint8_t boot_vector = 0x00;
void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); }
void led_id_on() { }
void led_id_off() { }
+ #elif BOARD_MODEL == BOARD_VME213
+ // VME213 has no physical LEDs, only E-Ink display
+ void led_rx_on() { }
+ void led_rx_off() { }
+ void led_tx_on() { }
+ void led_tx_off() { }
+ void led_id_on() { }
+ void led_id_off() { }
#elif BOARD_MODEL == BOARD_HUZZAH32
void led_rx_on() { digitalWrite(pin_led_rx, HIGH); }
void led_rx_off() { digitalWrite(pin_led_rx, LOW); }
diff --git a/build-vme213.ps1 b/build-vme213.ps1
new file mode 100644
index 0000000..575a45a
--- /dev/null
+++ b/build-vme213.ps1
@@ -0,0 +1,74 @@
+# Build script for Heltec VisionMaster E213
+$ErrorActionPreference = "Stop"
+
+$ARDUINO_CLI = ".\tools\arduino-cli.exe"
+$BOARD_MODEL = "0x40"
+$ARDUINO_ESP_CORE_VER = "2.0.17"
+
+Write-Host "=== RNode Firmware Build for VME213 ===" -ForegroundColor Cyan
+
+# Check if arduino-cli exists
+if (-not (Test-Path $ARDUINO_CLI)) {
+ Write-Host "ERROR: arduino-cli not found at $ARDUINO_CLI" -ForegroundColor Red
+ exit 1
+}
+
+# Update core index
+Write-Host "`nUpdating Arduino core index..." -ForegroundColor Yellow
+& $ARDUINO_CLI core update-index --config-file arduino-cli.yaml
+
+# Install ESP32 core if not installed
+Write-Host "`nChecking ESP32 core..." -ForegroundColor Yellow
+$coreInstalled = & $ARDUINO_CLI core list | Select-String "esp32:esp32"
+if (-not $coreInstalled) {
+ Write-Host "Installing ESP32 core $ARDUINO_ESP_CORE_VER..." -ForegroundColor Yellow
+ & $ARDUINO_CLI core install "esp32:esp32@$ARDUINO_ESP_CORE_VER" --config-file arduino-cli.yaml
+}
+
+# Install required libraries
+Write-Host "`nChecking required libraries..." -ForegroundColor Yellow
+$requiredLibs = @(
+ "Adafruit SSD1306",
+ "Adafruit SH110X",
+ "Adafruit GFX Library",
+ "Adafruit BusIO",
+ "Crypto"
+)
+
+foreach ($lib in $requiredLibs) {
+ $libInstalled = & $ARDUINO_CLI lib list | Select-String $lib
+ if (-not $libInstalled) {
+ Write-Host "Installing $lib..." -ForegroundColor Yellow
+ & $ARDUINO_CLI lib install $lib
+ }
+}
+
+# Create build directory
+if (-not (Test-Path "build")) {
+ New-Item -ItemType Directory -Path "build" | Out-Null
+}
+
+# Compile firmware
+Write-Host "`nCompiling firmware for VME213..." -ForegroundColor Green
+& $ARDUINO_CLI compile `
+ --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc,PartitionScheme=no_ota `
+ --build-property "build.extra_flags=-DBOARD_MODEL=$BOARD_MODEL" `
+ --output-dir build `
+ --verbose `
+ RNode_Firmware.ino
+
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "`n=== Build SUCCESS ===" -ForegroundColor Green
+ Write-Host "Firmware binary: build\RNode_Firmware.ino.bin" -ForegroundColor Cyan
+
+ # Show file size
+ $binFile = "build\RNode_Firmware.ino.bin"
+ if (Test-Path $binFile) {
+ $size = (Get-Item $binFile).Length
+ $sizeKB = [math]::Round($size / 1KB, 2)
+ Write-Host "Binary size: $sizeKB KB" -ForegroundColor Cyan
+ }
+} else {
+ Write-Host "`n=== Build FAILED ===" -ForegroundColor Red
+ exit 1
+}
diff --git a/src/LCMEN2R13EFC1.cpp b/src/LCMEN2R13EFC1.cpp
new file mode 100644
index 0000000..4b0b1c5
--- /dev/null
+++ b/src/LCMEN2R13EFC1.cpp
@@ -0,0 +1,242 @@
+// E-Ink display driver for LCMEN2R13EFC1
+// Adapted from Meshtastic NicheGraphics driver
+
+#include "LCMEN2R13EFC1.h"
+
+// Look-up tables for FAST refresh
+static const uint8_t LUT_FAST_VCOMDC[] = {
+ 0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01,
+ 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_WW[] = {
+ 0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01,
+ 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_BW[] = {
+ 0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01,
+ 0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_WB[] = {
+ 0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01,
+ 0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+static const uint8_t LUT_FAST_BB[] = {
+ 0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01,
+ 0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+LCMEN2R13EFC1::LCMEN2R13EFC1()
+ : _spiSettings(6000000, MSBFIRST, SPI_MODE0)
+{
+ // Calculate buffer size (8 pixels per byte)
+ _bufferRowSize = ((DISPLAY_WIDTH - 1) / 8) + 1; // 122px -> 16 bytes
+ _bufferSize = _bufferRowSize * DISPLAY_HEIGHT; // 16 * 250 = 4000 bytes
+}
+
+void LCMEN2R13EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) {
+ _spi = spi;
+ _pin_dc = pin_dc;
+ _pin_cs = pin_cs;
+ _pin_busy = pin_busy;
+ _pin_rst = pin_rst;
+
+ pinMode(_pin_dc, OUTPUT);
+ pinMode(_pin_cs, OUTPUT);
+ pinMode(_pin_busy, INPUT);
+ pinMode(_pin_rst, INPUT_PULLUP); // Active low, hold high
+
+ reset();
+}
+
+void LCMEN2R13EFC1::reset() {
+ pinMode(_pin_rst, OUTPUT);
+ digitalWrite(_pin_rst, LOW);
+ delay(10);
+ pinMode(_pin_rst, INPUT_PULLUP);
+ wait();
+
+ sendCommand(0x12); // Software reset
+ wait();
+}
+
+void LCMEN2R13EFC1::wait() {
+ // Busy when LOW
+ while (digitalRead(_pin_busy) == LOW) {
+ yield();
+ }
+}
+
+bool LCMEN2R13EFC1::isBusy() {
+ return (digitalRead(_pin_busy) == LOW);
+}
+
+void LCMEN2R13EFC1::sendCommand(const uint8_t command) {
+ _spi->beginTransaction(_spiSettings);
+ digitalWrite(_pin_dc, LOW); // DC low = command
+ digitalWrite(_pin_cs, LOW);
+ _spi->transfer(command);
+ digitalWrite(_pin_cs, HIGH);
+ digitalWrite(_pin_dc, HIGH);
+ _spi->endTransaction();
+}
+
+void LCMEN2R13EFC1::sendData(uint8_t data) {
+ sendData(&data, 1);
+}
+
+void LCMEN2R13EFC1::sendData(const uint8_t *data, uint32_t size) {
+ _spi->beginTransaction(_spiSettings);
+ digitalWrite(_pin_dc, HIGH); // DC high = data
+ digitalWrite(_pin_cs, LOW);
+
+ #if defined(ARCH_ESP32) || defined(ESP32)
+ _spi->transferBytes(data, NULL, size);
+ #else
+ for (uint32_t i = 0; i < size; i++) {
+ _spi->transfer(data[i]);
+ }
+ #endif
+
+ digitalWrite(_pin_cs, HIGH);
+ digitalWrite(_pin_dc, HIGH);
+ _spi->endTransaction();
+}
+
+void LCMEN2R13EFC1::configFull() {
+ sendCommand(0x00); // Panel setting register
+ sendData(0b11 << 6 // Display resolution
+ | 1 << 4 // B&W only
+ | 1 << 3 // Vertical scan direction
+ | 1 << 2 // Horizontal scan direction
+ | 1 << 1 // Shutdown: no
+ | 1 << 0 // Reset: no
+ );
+
+ sendCommand(0x50); // VCOM and data interval setting
+ sendData(0b10 << 6 // Border driven white
+ | 0b11 << 4 // Invert image colors: no
+ | 0b0111 // Interval between VCOM on and image data
+ );
+}
+
+void LCMEN2R13EFC1::configFast() {
+ sendCommand(0x00); // Panel setting register
+ sendData(0b11 << 6 // Display resolution
+ | 1 << 5 // LUT from registers (set below)
+ | 1 << 4 // B&W only
+ | 1 << 3 // Vertical scan direction
+ | 1 << 2 // Horizontal scan direction
+ | 1 << 1 // Shutdown: no
+ | 1 << 0 // Reset: no
+ );
+
+ sendCommand(0x50); // VCOM and data interval setting
+ sendData(0b11 << 6 // Border floating
+ | 0b01 << 4 // Invert image colors: no
+ | 0b0111 // Interval between VCOM on and image data
+ );
+
+ // Load LUT tables
+ sendCommand(0x20); // VCOM
+ sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
+
+ sendCommand(0x21); // White -> White
+ sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
+
+ sendCommand(0x22); // Black -> White
+ sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
+
+ sendCommand(0x23); // White -> Black
+ sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
+
+ sendCommand(0x24); // Black -> Black
+ sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
+}
+
+void LCMEN2R13EFC1::writeNewImage() {
+ sendCommand(0x13);
+ sendData(_buffer, _bufferSize);
+}
+
+void LCMEN2R13EFC1::writeOldImage() {
+ sendCommand(0x10);
+ sendData(_buffer, _bufferSize);
+}
+
+void LCMEN2R13EFC1::update(uint8_t *imageData, UpdateType type) {
+ _updateType = type;
+ _buffer = imageData;
+
+ reset();
+
+ // Configure display
+ if (_updateType == UPDATE_FULL) {
+ configFull();
+ } else {
+ configFast();
+ }
+
+ // Transfer image data
+ if (_updateType == UPDATE_FULL) {
+ writeNewImage();
+ writeOldImage();
+ } else {
+ writeNewImage();
+ }
+
+ // Power on and start refresh
+ sendCommand(0x04); // Power on panel voltage
+ wait();
+
+ sendCommand(0x12); // Begin executing update
+ wait();
+
+ // Power off
+ sendCommand(0x02);
+ wait();
+
+ // Update "old memory" for next differential refresh
+ if (_updateType != UPDATE_FULL) {
+ writeOldImage();
+ wait();
+ }
+}
+
+void LCMEN2R13EFC1::powerOn() {
+ sendCommand(0x04);
+ wait();
+}
+
+void LCMEN2R13EFC1::powerOff() {
+ sendCommand(0x02);
+ wait();
+}
diff --git a/src/LCMEN2R13EFC1.h b/src/LCMEN2R13EFC1.h
new file mode 100644
index 0000000..8bae3c9
--- /dev/null
+++ b/src/LCMEN2R13EFC1.h
@@ -0,0 +1,59 @@
+// E-Ink display driver for LCMEN2R13EFC1 (Heltec VisionMaster E213)
+// Adapted from Meshtastic firmware for RNode use
+// Controller IC: Fitipower JD79656
+// Resolution: 122x250 pixels (width x height)
+// Supports FAST (partial) and FULL refresh
+
+#ifndef LCMEN2R13EFC1_H
+#define LCMEN2R13EFC1_H
+
+#include
+#include
+
+class LCMEN2R13EFC1 {
+public:
+ // Display properties
+ static constexpr uint16_t DISPLAY_WIDTH = 122;
+ static constexpr uint16_t DISPLAY_HEIGHT = 250;
+
+ // Update types
+ enum UpdateType {
+ UPDATE_FULL = 0, // Full refresh (slower, ~3.6s)
+ UPDATE_FAST = 1 // Partial refresh (faster, ~720ms)
+ };
+
+ LCMEN2R13EFC1();
+
+ void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
+ void update(uint8_t *imageData, UpdateType type);
+ bool isBusy();
+ void powerOn();
+ void powerOff();
+
+private:
+ SPIClass *_spi;
+ uint8_t _pin_dc;
+ uint8_t _pin_cs;
+ uint8_t _pin_busy;
+ uint8_t _pin_rst;
+
+ SPISettings _spiSettings;
+
+ uint8_t *_buffer;
+ uint16_t _bufferRowSize;
+ uint32_t _bufferSize;
+ UpdateType _updateType;
+
+ void reset();
+ void wait();
+ void sendCommand(const uint8_t command);
+ void sendData(uint8_t data);
+ void sendData(const uint8_t *data, uint32_t size);
+
+ void configFull();
+ void configFast();
+ void writeNewImage();
+ void writeOldImage();
+};
+
+#endif // LCMEN2R13EFC1_H
diff --git a/src/einkDetect_VME213.h b/src/einkDetect_VME213.h
new file mode 100644
index 0000000..cdd5af4
--- /dev/null
+++ b/src/einkDetect_VME213.h
@@ -0,0 +1,41 @@
+// E-Ink display chip detection for VME213
+// Detects between LCMEN213EFC1 (V1) and E0213A367 (V1.1)
+// Based on Meshtastic einkDetect.h
+
+#ifndef EINK_DETECT_VME213_H
+#define EINK_DETECT_VME213_H
+
+#include
+
+enum EInkChipType {
+ EINK_LCMEN213EFC1 = 0, // Initial version (Fitipower JD79656)
+ EINK_E0213A367 = 1 // V1.1+ (Solomon Systech SSD1682)
+};
+
+// Detect E-Ink controller IC type by BUSY pin logic
+// Fitipower: BUSY=LOW when busy
+// Solomon Systech: BUSY=HIGH when busy
+inline EInkChipType detectEInkChip(uint8_t pin_reset, uint8_t pin_busy) {
+ // Force display BUSY by holding reset pin active
+ pinMode(pin_reset, OUTPUT);
+ digitalWrite(pin_reset, LOW);
+
+ delay(10);
+
+ // Read BUSY pin logic while display is busy
+ pinMode(pin_busy, INPUT);
+ bool busyLogic = digitalRead(pin_busy);
+
+ // Release reset pin
+ pinMode(pin_reset, INPUT);
+
+ // Fitipower = LOW when busy
+ // Solomon = HIGH when busy
+ if (busyLogic == LOW) {
+ return EINK_LCMEN213EFC1;
+ } else {
+ return EINK_E0213A367;
+ }
+}
+
+#endif // EINK_DETECT_VME213_H
diff --git a/sx126x.cpp b/sx126x.cpp
index c1e7c20..36879be 100644
--- a/sx126x.cpp
+++ b/sx126x.cpp
@@ -125,7 +125,7 @@ bool sx126x::preInit() {
pinMode(_ss, OUTPUT);
digitalWrite(_ss, HIGH);
- #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3
+ #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 || BOARD_MODEL == BOARD_VME213
SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs);
#elif BOARD_MODEL == BOARD_TECHO
SPI.setPins(pin_miso, pin_sclk, pin_mosi);
@@ -614,7 +614,7 @@ void sx126x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_6X, &byte, 1)
void sx126x::enableTCXO() {
#if HAS_TCXO
- #if BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_XIAO_S3
+ #if BOARD_MODEL == BOARD_RAK4631 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_XIAO_S3 || BOARD_MODEL == BOARD_VME213
uint8_t buf[4] = {MODE_TCXO_3_3V_6X, 0x00, 0x00, 0xFF};
#elif BOARD_MODEL == BOARD_TBEAM
uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF};
diff --git a/tools/LICENSE.txt b/tools/LICENSE.txt
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/tools/LICENSE.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ 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 .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/tools/arduino-cli.exe b/tools/arduino-cli.exe
new file mode 100644
index 0000000..5061829
Binary files /dev/null and b/tools/arduino-cli.exe differ