From 9844b541b6e37b9fbcbe789f28590fede7c72515 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 26 Feb 2026 20:37:27 -0800 Subject: [PATCH 01/48] fix: JSON parsing regression in build --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 313c4c47..23e31ab2 100755 --- a/build.sh +++ b/build.sh @@ -93,7 +93,7 @@ get_pio_envs_ending_with_string() { # $1 should be the environment name get_platform_for_env() { local env_name=$1 - echo "$PIO_CONFIG_JSON" | python3 -c " + printf '%s' "$PIO_CONFIG_JSON" | python3 -c " import sys, json, re data = json.load(sys.stdin) for section, options in data: From c183050935f3ca5e589c7759e14262be9cf24f25 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 15 May 2026 16:53:18 +0800 Subject: [PATCH 02/48] Add heltec -v4-r8 board --- boards/heltec_v4_r8.json | 43 +++ src/helpers/ui/ST7789LCDDisplay.cpp | 8 +- src/helpers/ui/ST7789LCDDisplay.h | 4 +- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 82 ++++++ variants/heltec_v4_r8/HeltecV4R8Board.h | 39 +++ variants/heltec_v4_r8/LoRaFEMControl.cpp | 52 ++++ variants/heltec_v4_r8/LoRaFEMControl.h | 24 ++ variants/heltec_v4_r8/pins_arduino.h | 56 ++++ variants/heltec_v4_r8/platformio.ini | 342 ++++++++++++++++++++++ variants/heltec_v4_r8/target.cpp | 45 +++ variants/heltec_v4_r8/target.h | 31 ++ 11 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 boards/heltec_v4_r8.json create mode 100644 variants/heltec_v4_r8/HeltecV4R8Board.cpp create mode 100644 variants/heltec_v4_r8/HeltecV4R8Board.h create mode 100644 variants/heltec_v4_r8/LoRaFEMControl.cpp create mode 100644 variants/heltec_v4_r8/LoRaFEMControl.h create mode 100644 variants/heltec_v4_r8/pins_arduino.h create mode 100644 variants/heltec_v4_r8/platformio.ini create mode 100644 variants/heltec_v4_r8/target.cpp create mode 100644 variants/heltec_v4_r8/target.h diff --git a/boards/heltec_v4_r8.json b/boards/heltec_v4_r8.json new file mode 100644 index 00000000..6dd97c84 --- /dev/null +++ b/boards/heltec_v4_r8.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_v4_r8" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index dc75e963..7a02668b 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -1,5 +1,9 @@ #include "ST7789LCDDisplay.h" +#ifndef PIN_TFT_MISO + #define PIN_TFT_MISO -1 +#endif + #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 3 #endif @@ -29,8 +33,8 @@ bool ST7789LCDDisplay::begin() { } // Im not sure if this is just a t-deck problem or not, if your display is slow try this. - #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) - displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS); + #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) + displaySPI.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS); #endif display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT); diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index 5b960ca1..03a6d3f1 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -8,7 +8,7 @@ #include class ST7789LCDDisplay : public DisplayDriver { - #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) + #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) SPIClass displaySPI; #endif Adafruit_ST7789 display; @@ -25,7 +25,7 @@ public: { _isOn = false; } -#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) +#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), displaySPI(HSPI), display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp new file mode 100644 index 00000000..1d481fcc --- /dev/null +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -0,0 +1,82 @@ +#include "HeltecV4R8Board.h" + +void HeltecV4R8Board::begin() { + ESP32Board::begin(); + + periph_power.begin(); + periph_power.claim(); // R8 VEXT also feeds the LoRa antenna boost rail. + + loRaFEMControl.init(); + +#ifdef PIN_TOUCH_RST + pinMode(PIN_TOUCH_RST, OUTPUT); + digitalWrite(PIN_TOUCH_RST, HIGH); +#endif + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } +} + +void HeltecV4R8Board::onBeforeTransmit(void) { + digitalWrite(P_LORA_TX_LED, HIGH); + loRaFEMControl.setTxModeEnable(); +} + +void HeltecV4R8Board::onAfterTransmit(void) { + digitalWrite(P_LORA_TX_LED, LOW); + loRaFEMControl.setRxModeEnable(); +} + +void HeltecV4R8Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + loRaFEMControl.setRxModeEnableWhenMCUSleep(); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + esp_deep_sleep_start(); +} + +void HeltecV4R8Board::powerOff() { + enterDeepSleep(0); +} + +uint16_t HeltecV4R8Board::getBattMilliVolts() { + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / 8; + + return (adc_mult * (3.3f / 4096.0f) * raw) * 1000; +} + +const char* HeltecV4R8Board::getManufacturerName() const { +#ifdef HELTEC_V4_R8_TFT + return "Heltec V4 R8 TFT"; +#else + return "Heltec V4 R8 OLED"; +#endif +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h new file mode 100644 index 00000000..20811abb --- /dev/null +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include "LoRaFEMControl.h" + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER (4.9f * 1.035f) +#endif + +class HeltecV4R8Board : public ESP32Board { +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + LoRaFEMControl loRaFEMControl; + + HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + void begin(); + void onBeforeTransmit(void) override; + void onAfterTransmit(void) override; + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; + uint16_t getBattMilliVolts() override; + bool setAdcMultiplier(float multiplier) override { + if (multiplier == 0.0f) { + adc_mult = ADC_MULTIPLIER; + } else { + adc_mult = multiplier; + } + return true; + } + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/heltec_v4_r8/LoRaFEMControl.cpp b/variants/heltec_v4_r8/LoRaFEMControl.cpp new file mode 100644 index 00000000..bb530de3 --- /dev/null +++ b/variants/heltec_v4_r8/LoRaFEMControl.cpp @@ -0,0 +1,52 @@ +#include "LoRaFEMControl.h" + +#include +#include +#include + +void LoRaFEMControl::init(void) { + pinMode(P_LORA_PA_POWER, OUTPUT); + digitalWrite(P_LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason != ESP_RST_DEEPSLEEP) { + delay(1); + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CTX); + + pinMode(P_LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); +} + +void LoRaFEMControl::setSleepModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, LOW); +} + +void LoRaFEMControl::setTxModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH); +} + +void LoRaFEMControl::setRxModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); +} + +void LoRaFEMControl::setRxModeEnableWhenMCUSleep(void) { + digitalWrite(P_LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CSD); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CTX); +} + +void LoRaFEMControl::setLNAEnable(bool enabled) { + lna_enabled = enabled; +} diff --git a/variants/heltec_v4_r8/LoRaFEMControl.h b/variants/heltec_v4_r8/LoRaFEMControl.h new file mode 100644 index 00000000..961cfd07 --- /dev/null +++ b/variants/heltec_v4_r8/LoRaFEMControl.h @@ -0,0 +1,24 @@ +#pragma once + +typedef enum { + KCT8103L_PA, + OTHER_FEM_TYPES +} LoRaFEMType; + +class LoRaFEMControl { +public: + LoRaFEMControl() { } + virtual ~LoRaFEMControl() { } + void init(void); + void setSleepModeEnable(void); + void setTxModeEnable(void); + void setRxModeEnable(void); + void setRxModeEnableWhenMCUSleep(void); + void setLNAEnable(bool enabled); + bool isLnaCanControl(void) { return true; } + void setLnaCanControl(bool can_control) { } + LoRaFEMType getFEMType(void) const { return KCT8103L_PA; } + +private: + bool lna_enabled = false; +}; diff --git a/variants/heltec_v4_r8/pins_arduino.h b/variants/heltec_v4_r8/pins_arduino.h new file mode 100644 index 00000000..9e412aac --- /dev/null +++ b/variants/heltec_v4_r8/pins_arduino.h @@ -0,0 +1,56 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 17; +static const uint8_t SCL = 18; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini new file mode 100644 index 00000000..4057d6f1 --- /dev/null +++ b/variants/heltec_v4_r8/platformio.ini @@ -0,0 +1,342 @@ +[Heltec_v4_r8] +extends = esp32_base +board = heltec_v4_r8 +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/heltec_v4_r8 + -D HELTEC_V4_R8 + -D USE_SX1262 + -D ESP32_CPU_FREQ=80 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=8 + -D P_LORA_RESET=12 + -D P_LORA_BUSY=13 + -D P_LORA_SCLK=9 + -D P_LORA_MISO=11 + -D P_LORA_MOSI=10 + -D P_LORA_PA_POWER=7 + -D P_LORA_KCT8103L_PA_CSD=2 + -D P_LORA_KCT8103L_PA_CTX=5 + -D P_LORA_TX_LED=46 + -D PIN_USER_BTN=0 + -D PIN_VEXT_EN=40 + -D PIN_VEXT_EN_ACTIVE=LOW + -D ADC_MULTIPLIER=5.0715f + -D PIN_VBAT_READ=1 + -D LORA_TX_POWER=10 + -D MAX_LORA_TX_POWER=22 + -D SX126X_REGISTER_PATCH=1 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_GPS_RX=38 + -D PIN_GPS_TX=39 + -D PIN_GPS_EN=42 + -D PIN_GPS_EN_ACTIVE=LOW + -D ENV_INCLUDE_GPS=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_v4_r8> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[heltec_v4_r8_oled] +extends = Heltec_v4_r8 +build_flags = + ${Heltec_v4_r8.build_flags} + -D HELTEC_V4_R8_OLED + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D PIN_OLED_RESET=21 +build_src_filter = ${Heltec_v4_r8.build_src_filter} +lib_deps = ${Heltec_v4_r8.lib_deps} + +[heltec_v4_r8_tft] +extends = Heltec_v4_r8 +build_flags = + ${Heltec_v4_r8.build_flags} + -D HELTEC_V4_R8_TFT + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D DISPLAY_SCALE_X=2.5 + -D DISPLAY_SCALE_Y=3.75 + -D PIN_TFT_RST=-1 + -D PIN_TFT_VDD_CTL=-1 + -D PIN_TFT_LEDA_CTL=44 + -D PIN_TFT_LEDA_CTL_ACTIVE=HIGH + -D PIN_TFT_CS=47 + -D PIN_TFT_DC=48 + -D PIN_TFT_SCL=16 + -D PIN_TFT_SDA=15 + -D PIN_TFT_MISO=45 + -D PIN_BUZZER=4 + -D PIN_TOUCH_RST=21 +build_src_filter = ${Heltec_v4_r8.build_src_filter} + + +lib_deps = + ${Heltec_v4_r8.lib_deps} + adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_v4_r8_repeater] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec R8 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_v4_r8_room_server] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec R8 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_terminal_chat] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_usb] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_ble] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_wifi] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SSD1306Display + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_sensor] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D ADVERT_NAME='"Heltec R8 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D DISPLAY_CLASS=SSD1306Display +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_tft_repeater] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADVERT_NAME='"Heltec R8 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_v4_r8_tft_room_server] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADVERT_NAME='"Heltec R8 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_tft_terminal_chat] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_usb] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=ST7789LCDDisplay +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_ble] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=ST7789LCDDisplay + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_wifi] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=ST7789LCDDisplay + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_sensor] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D ADVERT_NAME='"Heltec R8 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D DISPLAY_CLASS=ST7789LCDDisplay +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_kiss_modem] +extends = Heltec_v4_r8 +build_src_filter = ${Heltec_v4_r8.build_src_filter} + +<../examples/kiss_modem/> + +[env:heltec_v4_r8_tft_kiss_modem] +extends = heltec_v4_r8_tft +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_v4_r8/target.cpp b/variants/heltec_v4_r8/target.cpp new file mode 100644 index 00000000..0b38531e --- /dev/null +++ b/variants/heltec_v4_r8/target.cpp @@ -0,0 +1,45 @@ +#include +#include "target.h" + +HeltecV4R8Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display(&board.periph_power); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/heltec_v4_r8/target.h b/variants/heltec_v4_r8/target.h new file mode 100644 index 00000000..2d1d7a5b --- /dev/null +++ b/variants/heltec_v4_r8/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #ifdef HELTEC_V4_R8_OLED + #include + #elif defined(HELTEC_V4_R8_TFT) + #include + #endif + #include +#endif + +extern HeltecV4R8Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); From 1ac5f359ca572141ca0fa297a9540656a959d842 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 16 May 2026 10:59:02 +0800 Subject: [PATCH 03/48] Added touch reset --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1d481fcc..f9a771df 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -11,6 +11,10 @@ void HeltecV4R8Board::begin() { #ifdef PIN_TOUCH_RST pinMode(PIN_TOUCH_RST, OUTPUT); digitalWrite(PIN_TOUCH_RST, HIGH); + delay(10); + digitalWrite(PIN_TOUCH_RST, LOW); + delay(100); + digitalWrite(PIN_TOUCH_RST, HIGH); #endif esp_reset_reason_t reason = esp_reset_reason(); From e5a3839d64192c984e2fa5c76450c77bd06dc36e Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 27 May 2026 15:12:59 +0800 Subject: [PATCH 04/48] Optimize ADC readout --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index f9a771df..1fb123b2 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -70,11 +70,11 @@ uint16_t HeltecV4R8Board::getBattMilliVolts() { uint32_t raw = 0; for (int i = 0; i < 8; i++) { - raw += analogRead(PIN_VBAT_READ); + raw += analogReadMilliVolts(PIN_VBAT_READ); } raw = raw / 8; - return (adc_mult * (3.3f / 4096.0f) * raw) * 1000; + return (adc_mult * raw); } const char* HeltecV4R8Board::getManufacturerName() const { From dea5ed790fe2166f7248da97270f186c1f05e752 Mon Sep 17 00:00:00 2001 From: Nick Le Mouton Date: Fri, 5 Jun 2026 21:25:25 +1200 Subject: [PATCH 05/48] Add SECURITY.md --- SECURITY.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a4b2207d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the latest release only. We do not backport +fixes to older versions. + +| Version | Supported | +|---------|-----------| +| 1.15+ | ✅ | +| <1.15 | ❌ | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Use GitHub's private vulnerability reporting instead: +1. Go to the **Security** tab of this repository +2. Click **Report a vulnerability** +3. Fill in the details and submit + +### What to include + +A useful report tells us: +- Which component or file is affected +- What an attacker can do (impact) and under what conditions +- A minimal reproduction case or proof-of-concept if you have one +- Whether you believe it is remotely exploitable + +You do not need a working exploit to report. An incomplete report is better +than no report. + +## What to expect + +This is a volunteer-maintained open-source project. We will do our best to +respond in a reasonable timeframe, but cannot commit to specific deadlines. + +We ask that you give us a fair opportunity to investigate and address the +issue before any public disclosure. If you have not heard back after +**90 days**, feel free to follow up or proceed with disclosure at your +discretion. + +## Scope + +In scope: +- Remote code execution, memory corruption, or denial-of-service via crafted + radio packets +- Authentication or encryption bypasses +- Vulnerabilities in the packet routing or path handling logic + +Out of scope: +- Physical access attacks (e.g., JTAG, UART extraction of keys) +- Regulatory compliance (duty cycle, frequency restrictions) +- Jamming or other physical-layer radio interference +- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those + upstream +- "Best practice" suggestions without a demonstrated attack path From c94ed29ca36e73d9026319af18b8977648ec8809 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 12 Jun 2026 03:18:10 +1200 Subject: [PATCH 06/48] add github workflow to close stale issues --- .github/workflows/stale-bot.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/stale-bot.yml diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 00000000..afe874f8 --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,32 @@ +name: 'Run Stale Bot' +on: + schedule: + - cron: '30 1 * * *' # daily at 1:30am + workflow_dispatch: {} + +permissions: + actions: write + issues: write + pull-requests: write + +jobs: + close-issues: + # only run on main repo, not forks + if: github.repository == "meshcore-dev/MeshCore" + runs-on: ubuntu-latest + steps: + - name: Close Stale Issues + uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # auto close issues + days-before-issue-stale: 60 + days-before-issue-close: 7 + exempt-issue-labels: "keep-open" + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days." + close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." + # don't auto close prs + days-before-pr-stale: -1 + days-before-pr-close: -1 + \ No newline at end of file From 3b3992539945b861801ab709b2361cb03edee657 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 12 Jun 2026 03:20:44 +1200 Subject: [PATCH 07/48] use single quotes for repo name --- .github/workflows/stale-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index afe874f8..ec166587 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -12,7 +12,7 @@ permissions: jobs: close-issues: # only run on main repo, not forks - if: github.repository == "meshcore-dev/MeshCore" + if: github.repository == 'meshcore-dev/MeshCore' runs-on: ubuntu-latest steps: - name: Close Stale Issues From 06130dce29c907ebc014dc52156fbb1e52210903 Mon Sep 17 00:00:00 2001 From: formtapez Date: Fri, 12 Jun 2026 12:11:12 +0200 Subject: [PATCH 08/48] added some missing CLI commands --- docs/cli_commands.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9accb299..f482cfcb 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -28,12 +28,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `reboot` +**Note:** No reply is sent. + +--- + +### Power-off the node +**Usage:** +- `poweroff`, or +- `shutdown +` +**Note:** No reply is sent. + --- ### Reset the clock and reboot **Usage:** - `clkreboot` +**Note:** No reply is sent. + --- ### Sync the clock with the remote device @@ -632,10 +645,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - indicates it hasn't been set, will track flood.max until it is. **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. +--- + +#### Limit the number of hops for an advert flood message +**Usage:** +- `get flood.max.advert` +- `set flood.max.advert ` + +**Parameters:** +- `value`: Maximum flood hop count (0-64) for an advert packet + +**Default:** `8` --- From d3444e6b0be513f982f3bcb7d2f05a091f16a2fd Mon Sep 17 00:00:00 2001 From: formtapez Date: Fri, 12 Jun 2026 12:14:46 +0200 Subject: [PATCH 09/48] fix formatting --- docs/cli_commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f482cfcb..66a9b77a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -35,8 +35,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Power-off the node **Usage:** - `poweroff`, or -- `shutdown -` +- `shutdown` + **Note:** No reply is sent. --- @@ -645,7 +645,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `64` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - (`0xFF` indicates it hasn't been set, will track flood.max until it is.) **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. From b8b6598dd3ab47489767167aa85ce727c5823054 Mon Sep 17 00:00:00 2001 From: UltimateCodeWarrior Date: Mon, 22 Jun 2026 23:30:35 -0700 Subject: [PATCH 10/48] Fix blank OLED on T-Beam Supreme V3 (I2C bus + 0x3D address) The SH1106 OLED on the LilyGo T-Beam Supreme V3 never lights up because: 1. The primary I2C bus (Wire) that the OLED, BME280 and magnetometer share is never initialized. The PMU/RTC use Wire1 (via XPowersLib); nothing brings up Wire on PIN_BOARD_SDA/PIN_BOARD_SCL, so the display driver talks on the wrong default pins. 2. The OLED is at I2C address 0x3D on this board (0x3C is taken by the QMC6310 magnetometer), but SH1106Display::begin() only tried 0x3C. Initialize Wire for TBEAM_SUPREME_SX1262, and have SH1106Display::begin() prefer 0x3D (only ever used by the OLED) before falling back to 0x3C. Backwards-compatible for boards with the OLED at 0x3C. Closes #2609 --- src/helpers/esp32/TBeamBoard.cpp | 10 ++++++++++ src/helpers/ui/SH1106Display.cpp | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/helpers/esp32/TBeamBoard.cpp b/src/helpers/esp32/TBeamBoard.cpp index 5f708d71..d9f6e022 100644 --- a/src/helpers/esp32/TBeamBoard.cpp +++ b/src/helpers/esp32/TBeamBoard.cpp @@ -16,6 +16,16 @@ void TBeamBoard::begin() { ESP32Board::begin(); +#ifdef TBEAM_SUPREME_SX1262 + // On the T-Beam S3 Supreme the PMU + RTC sit on Wire1 (GPIO 42/41, brought + // up by XPowersLib), while the SH1106 OLED and the BME280/QMC6310 sensors + // sit on the primary bus, Wire (GPIO PIN_BOARD_SDA/PIN_BOARD_SCL). Nothing + // else initialises Wire on this board, so the display driver would + // otherwise talk on the wrong (default) pins and display.begin() fails, + // leaving the screen blank. Bring the OLED bus up here. + Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + power_init(); //Configure user button diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index f383bb00..86c6089b 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,7 +11,17 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + // Address selection: on some board revisions (notably the LilyGo T-Beam + // Supreme V3) the OLED lives at 0x3D because 0x3C is occupied by a + // magnetometer (QMC6310N). 0x3D is only ever used by the OLED, so prefer it + // when present, otherwise fall back to the standard 0x3C (DISPLAY_ADDRESS). + uint8_t addr = 0; + if (i2c_probe(Wire, 0x3D)) { + addr = 0x3D; + } else if (i2c_probe(Wire, DISPLAY_ADDRESS)) { + addr = DISPLAY_ADDRESS; + } + return addr && display.begin(addr, true); } void SH1106Display::turnOn() From 3af5ffe11923eff8da35a14baf505f207fc33d2d Mon Sep 17 00:00:00 2001 From: Naomi Smith Date: Fri, 3 Jul 2026 11:55:06 -0700 Subject: [PATCH 11/48] Fix GPS and LEDs on ThinkNode M3 GPS support was implemented incorrectly for the ThinkNode M3. The reset pin was being driven when it should be left floating for this unit. The enable pin also wasn't being picked up by `MicroNMEALocationProvider` because of a mismatch in constant naming conventions. I did a general cleanup of the GPS and ThinkNode M3 bring-up code so that constant names line up and "*_ACTIVE" constants are used consistently vs hardcoding `HIGH`/`LOW`. After making these changes, serial data immediately starts streaming in from the NMEA on boot and GPS detection just works. LED handling was also not quite right for the ThinkNode M3. The LoRa TX LED was being driven high to turn it on when it should actually be driven low. I changed the code to use the `LED_STATE_ON` constant and also added a little code to the shutdown path to properly make sure that all LEDs are turned off. Tested and confirmed working on real hardware. Resolves both #1864 and #2879. --- .../sensors/MicroNMEALocationProvider.h | 32 +++++++++++-------- variants/thinknode_m3/ThinkNodeM3Board.h | 12 +++---- variants/thinknode_m3/platformio.ini | 2 -- variants/thinknode_m3/variant.cpp | 8 +++-- variants/thinknode_m3/variant.h | 9 +++--- 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3..9d697fed 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -13,8 +13,12 @@ #endif #endif -#ifndef PIN_GPS_EN_ACTIVE - #define PIN_GPS_EN_ACTIVE HIGH +#ifndef GPS_EN_ACTIVE + #ifdef PIN_GPS_EN_ACTIVE + #define GPS_EN_ACTIVE PIN_GPS_EN_ACTIVE + #else + #define GPS_EN_ACTIVE HIGH + #endif #endif #ifndef GPS_RESET @@ -25,11 +29,11 @@ #endif #endif -#ifndef GPS_RESET_FORCE +#ifndef GPS_RESET_ACTIVE #ifdef PIN_GPS_RESET_ACTIVE - #define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE + #define GPS_RESET_ACTIVE PIN_GPS_RESET_ACTIVE #else - #define GPS_RESET_FORCE LOW + #define GPS_RESET_ACTIVE LOW #endif #endif @@ -52,11 +56,11 @@ public : _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) { if (_pin_reset != -1) { pinMode(_pin_reset, OUTPUT); - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); } if (_pin_en != -1) { pinMode(_pin_en, OUTPUT); - digitalWrite(_pin_en, LOW); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); } } @@ -76,27 +80,27 @@ public : void begin() override { claim(); if (_pin_en != -1) { - digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE); + digitalWrite(_pin_en, GPS_EN_ACTIVE); } if (_pin_reset != -1) { - digitalWrite(_pin_reset, !GPS_RESET_FORCE); + digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } } void reset() override { if (_pin_reset != -1) { - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); delay(10); - digitalWrite(_pin_reset, !GPS_RESET_FORCE); + digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } } void stop() override { if (_pin_en != -1) { - digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); } if (_pin_reset != -1) { - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); } release(); } @@ -105,7 +109,7 @@ public : // directly read the enable pin if present as gps can be // activated/deactivated outside of here ... if (_pin_en != -1) { - return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE; + return digitalRead(_pin_en) == GPS_EN_ACTIVE; } else { return true; // no enable so must be active } diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 1435d31d..63b14116 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -18,12 +18,12 @@ public: void begin(); uint16_t getBattMilliVolts() override; -#if defined(P_LORA_TX_LED) +#ifdef P_LORA_TX_LED void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on } void onAfterTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off } #endif @@ -44,9 +44,9 @@ public: void powerOff() override { // turn off all leds, sd_power_system_off will not do this for us - #ifdef P_LORA_TX_LED - digitalWrite(P_LORA_TX_LED, LOW); - #endif + digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); + digitalWrite(PIN_LED_GREEN, !LED_STATE_ON); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); // power off board sd_power_system_off(); diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0a3d4eda..0004f4c7 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -24,7 +24,6 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_MOSI=46 -D P_LORA_RESET=42 -D P_LORA_TX_LED=PIN_LED_BLUE - -D P_LORA_TX_LED_ON=LOW -D LR11X0_DIO_AS_RF_SWITCH=true -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 -D MESH_DEBUG=1 @@ -105,7 +104,6 @@ build_flags = ${ThinkNode_M3.build_flags} -D BLE_TX_POWER=0 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 - -D GPS_NMEA_DEBUG -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=23 diff --git a/variants/thinknode_m3/variant.cpp b/variants/thinknode_m3/variant.cpp index dad0f3f5..b47b8354 100644 --- a/variants/thinknode_m3/variant.cpp +++ b/variants/thinknode_m3/variant.cpp @@ -80,16 +80,18 @@ void initVariant() digitalWrite(LED_POWER, HIGH); pinMode(PIN_LED_BLUE, OUTPUT); + digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); pinMode(PIN_LED_GREEN, OUTPUT); + digitalWrite(PIN_LED_GREEN, !LED_STATE_ON); pinMode(PIN_LED_RED, OUTPUT); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(PIN_GPS_POWER, OUTPUT); pinMode(PIN_GPS_EN, OUTPUT); - pinMode(PIN_GPS_RESET, OUTPUT); // Power on gps but in standby - digitalWrite(PIN_GPS_EN, LOW); - digitalWrite(PIN_GPS_POWER, HIGH); + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); } diff --git a/variants/thinknode_m3/variant.h b/variants/thinknode_m3/variant.h index 02ed78a8..78dfab85 100644 --- a/variants/thinknode_m3/variant.h +++ b/variants/thinknode_m3/variant.h @@ -32,7 +32,7 @@ #define EXT_CHRG_DETECT (32) // P1.3 #define EXT_PWR_DETECT (31) // P0.5 -#define PIN_VBAT_READ (5) +#define PIN_VBAT_READ (5) #define AREF_VOLTAGE (2.4f) #define ADC_MULTIPLIER (2.0) //(1.75f) // 2.0 gives more coherent value, 4.2V when charged, needs tweaking @@ -92,18 +92,19 @@ // GPS #define HAS_GPS 1 -#define PIN_GPS_RX (22) +#define PIN_GPS_RX (22) #define PIN_GPS_TX (20) #define PIN_GPS_POWER (14) #define PIN_GPS_EN (21) // STANDBY #define PIN_GPS_RESET (25) // REINIT -#define GPS_RESET_ACTIVE LOW +#define GPS_POWER_ACTIVE HIGH #define GPS_EN_ACTIVE HIGH +#define GPS_RESET (-1) #define GPS_BAUDRATE 9600 //////////////////////////////////////////////////////////////////////////////// // Buzzer #define BUZZER_EN (37) // P1.5 -#define BUZZER_PIN (25) // P0.25 \ No newline at end of file +#define BUZZER_PIN (25) // P0.25 From 57563ab8f92be3a6396b3eead4898b36f186a44b Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 6 Jul 2026 02:04:56 +1200 Subject: [PATCH 12/48] update qr code docs --- docs/qr_codes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/qr_codes.md b/docs/qr_codes.md index 364efa8a..3516a76a 100644 --- a/docs/qr_codes.md +++ b/docs/qr_codes.md @@ -12,8 +12,10 @@ meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72 **Parameters**: -- `name`: Channel name (URL-encoded if needed) +- `name`: Channel name (URL-encoded) - `secret`: 16-byte secret represented as 32 hex characters +- `region_scope`: Region Scope (optional, URL-encoded if provided) + - Supported by MeshCore App v1.47.0+ ## Add Contact From 2efb3af69b9207b23d57b630bd0b1380c4f4405c Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 2 Jul 2026 21:55:19 -0400 Subject: [PATCH 13/48] repeater: poweroff on long press --- .vscode/extensions.json | 9 ---- examples/simple_repeater/UITask.cpp | 79 +++++++++++++++++++---------- examples/simple_repeater/UITask.h | 5 +- examples/simple_repeater/main.cpp | 5 +- 4 files changed, 60 insertions(+), 38 deletions(-) delete mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 8057bc70..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - "pioarduino.pioarduino-ide", - "platformio.platformio-ide" - ], - "unwantedRecommendations": [ - "ms-vscode.cpptools-extension-pack" - ] -} diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 05d863fc..17c708e3 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -1,4 +1,5 @@ #include "UITask.h" +#include "target.h" #include #include @@ -9,6 +10,8 @@ #define AUTO_OFF_MILLIS 20000 // 20 seconds #define BOOT_SCREEN_MILLIS 4000 // 4 seconds +#define POWEROFF_DELAY 3000 + // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, @@ -29,9 +32,14 @@ static const uint8_t meshcore_logo [] PROGMEM = { void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; + _started_at = millis(); _node_prefs = node_prefs; _display->turnOn(); +#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) + user_btn.begin(); +#endif + // strip off dash and commit hash by changing dash to null terminator // e.g: v1.2.3-abcdef -> v1.2.3 char *version = strdup(firmware_version); @@ -47,7 +55,7 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi void UITask::renderCurrScreen() { char tmp[80]; - if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo _display->setColor(DisplayDriver::BLUE); int logoWidth = 128; @@ -57,24 +65,34 @@ void UITask::renderCurrScreen() { const char* website = "https://meshcore.io"; _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); - uint16_t websiteWidth = _display->getTextWidth(website); - _display->setCursor((_display->width() - websiteWidth) / 2, 22); - _display->print(website); + _display->drawTextCentered(_display->width() / 2, 22, website); // version info _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); - uint16_t versionWidth = _display->getTextWidth(_version_info); - _display->setCursor((_display->width() - versionWidth) / 2, 35); - _display->print(_version_info); + _display->drawTextCentered(_display->width() / 2, 35, _version_info); // node type const char* node_type = "< Repeater >"; - uint16_t typeWidth = _display->getTextWidth(node_type); - _display->setCursor((_display->width() - typeWidth) / 2, 48); - _display->print(node_type); - } else { // home screen - // node name + _display->drawTextCentered(_display->width() / 2, 48, node_type); + } else if (_powering_off_at > 0) { + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + + // meshcore website + const char* website = "https://meshcore.io"; + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + _display->drawTextCentered(_display->width()/ 2, 22, website); + + // Powering off + const char* poweroff_string = "Turning OFF"; + uint16_t poffWidth = _display->getTextWidth(poweroff_string); + _display->setCursor((_display->width() - poffWidth) / 2, 48); + _display->drawTextCentered(_display->width()/2, 48, poweroff_string); + } else { _display->setCursor(0, 0); _display->setTextSize(1); _display->setColor(DisplayDriver::GREEN); @@ -94,21 +112,19 @@ void UITask::renderCurrScreen() { } void UITask::loop() { -#ifdef PIN_USER_BTN - if (millis() >= _next_read) { - int btnState = digitalRead(PIN_USER_BTN); - if (btnState != _prevBtnState) { - if (btnState == USER_BTN_PRESSED) { // pressed? - if (_display->isOn()) { - // TODO: any action ? - } else { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer - } - _prevBtnState = btnState; +#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) + int ev = user_btn.check(); + if (ev == BUTTON_EVENT_CLICK) { + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); } - _next_read = millis() + 200; // 5 reads per second + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _display->turnOn(); + Serial.println("Powering Off"); + _powering_off_at = millis() + POWEROFF_DELAY; } #endif @@ -124,4 +140,15 @@ void UITask::loop() { _display->turnOff(); } } + + if (_powering_off_at > 0) { // power off timer armed +#ifdef LED_PIN + digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff +#endif + if (millis() > _powering_off_at) { + _display->turnOff(); + radio_driver.powerOff(); + _board->powerOff(); // should not return + } + } } diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index a27259f1..d8e3ce1d 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -4,15 +4,18 @@ #include class UITask { + mesh::MainBoard* _board; DisplayDriver* _display; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; NodePrefs* _node_prefs; char _version_info[32]; + unsigned long _powering_off_at = 0; + unsigned long _started_at = 0; void renderCurrScreen(); public: - UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; } void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 2ce056f5..c13cd99d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -5,7 +5,7 @@ #ifdef DISPLAY_CLASS #include "UITask.h" - static UITask ui_task(display); + static UITask ui_task(board, display); #endif StdRNG fast_rng; @@ -130,7 +130,8 @@ void loop() { command[0] = 0; // reset command buffer } -#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) +#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) && !defined(DISPLAY_CLASS) + // // Hold the user button to power off the SenseCAP Solar repeater. int btnState = digitalRead(PIN_USER_BTN); if (btnState == LOW) { From 54234b58371d2ecbf3031d807156277e1afff3a1 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 6 Jul 2026 22:18:49 +1200 Subject: [PATCH 14/48] implement matrix builds for faster firmware releases --- .../setup-build-environment/action.yml | 13 ++- .../workflows/build-companion-firmwares.yml | 35 ++------ .../workflows/build-repeater-firmwares.yml | 35 ++------ .../workflows/build-room-server-firmwares.yml | 35 ++------ .github/workflows/firmware-builder.yml | 90 +++++++++++++++++++ build.sh | 10 +++ 6 files changed, 126 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/firmware-builder.yml diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index 02aaf424..cba2d2e5 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -25,5 +25,14 @@ runs: - name: Extract Version from Git Tag shell: bash run: | - GIT_TAG_NAME="${GITHUB_REF#refs/tags/}" - echo "GIT_TAG_VERSION=${GIT_TAG_NAME##*-}" >> $GITHUB_ENV + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + # triggered by a tag push (e.g: refs/tags/companion-v1.2.3) + GIT_TAG_NAME="${GITHUB_REF#refs/tags/}" + VERSION_STRING="${GIT_TAG_NAME##*-}" + else + # triggered by a workflow dispatch (e.g: refs/heads/main) + # strip "refs/heads/" prefix and replace any remaining "/" with "-" to protect file paths + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + VERSION_STRING=$(echo "$BRANCH_NAME" | tr '/' '-') + fi + echo "GIT_TAG_VERSION=${VERSION_STRING}" >> $GITHUB_ENV diff --git a/.github/workflows/build-companion-firmwares.yml b/.github/workflows/build-companion-firmwares.yml index 771fa6d5..8fd796f7 100644 --- a/.github/workflows/build-companion-firmwares.yml +++ b/.github/workflows/build-companion-firmwares.yml @@ -10,33 +10,8 @@ on: - 'companion-*' jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - env: - FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} - run: /usr/bin/env bash build.sh build-companion-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: companion-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Companion Firmware ${{ env.GIT_TAG_VERSION }} - body: "" - draft: true - files: out/* \ No newline at end of file + build-companion-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'companion' + release_title_prefix: 'Companion Firmware' diff --git a/.github/workflows/build-repeater-firmwares.yml b/.github/workflows/build-repeater-firmwares.yml index 3185d4b2..46b9076c 100644 --- a/.github/workflows/build-repeater-firmwares.yml +++ b/.github/workflows/build-repeater-firmwares.yml @@ -10,33 +10,8 @@ on: - 'repeater-*' jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - env: - FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} - run: /usr/bin/env bash build.sh build-repeater-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: repeater-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Repeater Firmware ${{ env.GIT_TAG_VERSION }} - body: "" - draft: true - files: out/* \ No newline at end of file + build-repeater-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'repeater' + release_title_prefix: 'Repeater Firmware' diff --git a/.github/workflows/build-room-server-firmwares.yml b/.github/workflows/build-room-server-firmwares.yml index 127095a8..c8bd19f7 100644 --- a/.github/workflows/build-room-server-firmwares.yml +++ b/.github/workflows/build-room-server-firmwares.yml @@ -10,33 +10,8 @@ on: - 'room-server-*' jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - env: - FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} - run: /usr/bin/env bash build.sh build-room-server-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: room-server-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Room Server Firmware ${{ env.GIT_TAG_VERSION }} - body: "" - draft: true - files: out/* \ No newline at end of file + build-room-server-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'room-server' + release_title_prefix: 'Room Server Firmware' diff --git a/.github/workflows/firmware-builder.yml b/.github/workflows/firmware-builder.yml new file mode 100644 index 00000000..adc9e424 --- /dev/null +++ b/.github/workflows/firmware-builder.yml @@ -0,0 +1,90 @@ +name: Firmware Builder + +on: + workflow_call: + inputs: + firmware_type: + required: true + type: string + release_title_prefix: + required: true + type: string + +jobs: + + generate-build-matrix: + runs-on: ubuntu-latest + outputs: + targets: ${{ steps.get-build-targets.outputs.targets }} + steps: + + - name: Clone Repo + uses: actions/checkout@v6 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Get Build Targets + id: get-build-targets + run: | + # get list of firmwares to build + TARGET_LIST=$(/usr/bin/env bash build.sh get-${{ inputs.firmware_type }}-firmwares-to-build) + + # convert targets separated by new line into a json array string + JSON_ARRAY=$(echo "$TARGET_LIST" | jq -R -s -c 'split("\n") | map(select(length > 0))') + + # use json array as targets result + echo "targets=$JSON_ARRAY" >> $GITHUB_OUTPUT + + build: + needs: generate-build-matrix + runs-on: ubuntu-latest + continue-on-error: true # don't fail entire build if one board fails to build + strategy: + matrix: + target: ${{ fromJson(needs.generate-build-matrix.outputs.targets) }} + fail-fast: false # don't cancel other builds if one board fails to build + steps: + + - name: Clone Repo + uses: actions/checkout@v6 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build Firmware + env: + FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} + run: /usr/bin/env bash build.sh build-firmware ${{ matrix.target }} + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v7 + with: + name: "${{ matrix.target }}" + path: out + + create-release: + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') # only create release for tagged builds + steps: + + - name: Clone Repo + uses: actions/checkout@v6 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Download All Artifacts + uses: actions/download-artifact@v8 + with: + merge-multiple: true + path: out + + - name: Create Release + uses: softprops/action-gh-release@v3 + with: + name: "${{ inputs.release_title_prefix }} ${{ env.GIT_TAG_VERSION }}" + body: "" + draft: true + files: out/* diff --git a/build.sh b/build.sh index 313c4c47..41719a44 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# exit when any command fails +set -e + global_usage() { cat - < Date: Tue, 7 Jul 2026 10:20:03 -0400 Subject: [PATCH 15/48] introduce shutdownPeripherals in NRF52Board to prepare for shutdown --- src/helpers/NRF52Board.cpp | 6 +++++- src/helpers/NRF52Board.h | 1 + variants/lilygo_techo/TechoBoard.h | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index beee3212..088ddc23 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -298,7 +298,7 @@ float NRF52Board::getMCUTemperature() { return temp * 0.25f; // Convert to *C } -void NRF52Board::powerOff() { +void NRF52Board::shutdownPeripherals() { // Power off the display if any #ifdef DISPLAY_CLASS display.turnOff(); @@ -318,6 +318,10 @@ void NRF52Board::powerOff() { // Flush serial buffers Serial.flush(); delay(100); +} + +void NRF52Board::powerOff() { + shutdownPeripherals(); // Enter SYSTEMOFF uint8_t sd_enabled = 0; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index cbf4cd49..dba15f97 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,6 +50,7 @@ public: virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } + virtual void shutdownPeripherals(); virtual void powerOff() override; virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; diff --git a/variants/lilygo_techo/TechoBoard.h b/variants/lilygo_techo/TechoBoard.h index e957d2e5..867fc24c 100644 --- a/variants/lilygo_techo/TechoBoard.h +++ b/variants/lilygo_techo/TechoBoard.h @@ -23,8 +23,8 @@ public: return "LilyGo T-Echo"; } - void powerOff() override { - NRF52Board::powerOff(); + void shutdownPeripherals() override { + NRF52Board::shutdownPeripherals(); #ifdef LED_RED digitalWrite(LED_RED, HIGH); #endif From 17d68e328f0eb5d016fe3c6cf4ada6ba64f7aee2 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 8 Jul 2026 16:20:12 +0800 Subject: [PATCH 16/48] Add heltec rc32 board --- boards/heltec-rc32.json | 43 ++ src/helpers/ui/NV3001BDisplay.cpp | 553 +++++++++++++++++++++++ src/helpers/ui/NV3001BDisplay.h | 68 +++ variants/heltec_rc32/HeltecRC32Board.cpp | 66 +++ variants/heltec_rc32/HeltecRC32Board.h | 32 ++ variants/heltec_rc32/pins_arduino.h | 60 +++ variants/heltec_rc32/platformio.ini | 346 ++++++++++++++ variants/heltec_rc32/target.cpp | 45 ++ variants/heltec_rc32/target.h | 31 ++ variants/heltec_rc32/variant.h | 51 +++ 10 files changed, 1295 insertions(+) create mode 100644 boards/heltec-rc32.json create mode 100644 src/helpers/ui/NV3001BDisplay.cpp create mode 100644 src/helpers/ui/NV3001BDisplay.h create mode 100644 variants/heltec_rc32/HeltecRC32Board.cpp create mode 100644 variants/heltec_rc32/HeltecRC32Board.h create mode 100644 variants/heltec_rc32/pins_arduino.h create mode 100644 variants/heltec_rc32/platformio.ini create mode 100644 variants/heltec_rc32/target.cpp create mode 100644 variants/heltec_rc32/target.h create mode 100644 variants/heltec_rc32/variant.h diff --git a/boards/heltec-rc32.json b/boards/heltec-rc32.json new file mode 100644 index 00000000..b9bafa26 --- /dev/null +++ b/boards/heltec-rc32.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_rc32" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec RC32 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/src/helpers/ui/NV3001BDisplay.cpp b/src/helpers/ui/NV3001BDisplay.cpp new file mode 100644 index 00000000..03825cc0 --- /dev/null +++ b/src/helpers/ui/NV3001BDisplay.cpp @@ -0,0 +1,553 @@ +#include "NV3001BDisplay.h" +#include +#include + +#ifndef SPI_FREQUENCY + #define SPI_FREQUENCY 8000000 +#endif + +#ifndef PIN_TFT_SCL + #error "PIN_TFT_SCL must be defined" +#endif + +#ifndef PIN_TFT_SDA + #error "PIN_TFT_SDA must be defined" +#endif + +#ifndef PIN_TFT_CS + #error "PIN_TFT_CS must be defined" +#endif + +#ifndef PIN_TFT_DC + #error "PIN_TFT_DC must be defined" +#endif + +#ifndef PIN_TFT_MISO + #define PIN_TFT_MISO -1 +#endif + +#ifndef PIN_TFT_RST + #define PIN_TFT_RST -1 +#endif + +#ifndef PIN_TFT_EN + #define PIN_TFT_EN -1 +#endif + +#ifndef PIN_TFT_BL + #define PIN_TFT_BL -1 +#endif + +#ifndef PIN_TFT_EN_ACTIVE + #define PIN_TFT_EN_ACTIVE LOW +#endif + +#ifndef PIN_TFT_BL_ACTIVE + #define PIN_TFT_BL_ACTIVE HIGH +#endif + +#ifndef DISPLAY_ROTATION + #define DISPLAY_ROTATION 0 +#endif + +#ifndef NV3001B_SCREEN_WIDTH + #define NV3001B_SCREEN_WIDTH 220 +#endif + +#ifndef NV3001B_SCREEN_HEIGHT + #define NV3001B_SCREEN_HEIGHT 128 +#endif + +#ifndef DISPLAY_SCALE_X + #define DISPLAY_SCALE_X ((float)NV3001B_SCREEN_WIDTH / NV3001B_LOGICAL_WIDTH) +#endif + +#ifndef DISPLAY_SCALE_Y + #define DISPLAY_SCALE_Y ((float)NV3001B_SCREEN_HEIGHT / NV3001B_LOGICAL_HEIGHT) +#endif + +#define NV3001B_SWRESET 0x01 +#define NV3001B_SLPOUT 0x11 +#define NV3001B_DISPON 0x29 +#define NV3001B_CASET 0x2A +#define NV3001B_RASET 0x2B +#define NV3001B_RAMWR 0x2C +#define NV3001B_MADCTL 0x36 +#define NV3001B_COLMOD 0x3A + +#define NV3001B_MADCTL_MY 0x80 +#define NV3001B_MADCTL_MX 0x40 +#define NV3001B_MADCTL_MV 0x20 +#define NV3001B_MADCTL_RGB 0x00 + +#ifndef NV3001B_TEXT_SIZE1_SCALE_X + #define NV3001B_TEXT_SIZE1_SCALE_X 1 +#endif + +#ifndef NV3001B_TEXT_SIZE1_SCALE_Y + #define NV3001B_TEXT_SIZE1_SCALE_Y 2 +#endif + +#ifndef NV3001B_TEXT_SIZE2_SCALE_X + #define NV3001B_TEXT_SIZE2_SCALE_X 2 +#endif + +#ifndef NV3001B_TEXT_SIZE2_SCALE_Y + #define NV3001B_TEXT_SIZE2_SCALE_Y 3 +#endif + +static uint16_t mapColor(DisplayDriver::Color c) { + switch (c) { + case DisplayDriver::DARK: return 0x0000; + case DisplayDriver::LIGHT: return 0xffff; + case DisplayDriver::RED: return 0xf800; + case DisplayDriver::GREEN: return 0x07e0; + case DisplayDriver::BLUE: return 0x001f; + case DisplayDriver::YELLOW: return 0xffe0; + case DisplayDriver::ORANGE: return 0xfd20; + default: return 0xffff; + } +} + +static int scaleX(int x) { + return (int)(x * DISPLAY_SCALE_X); +} + +static int scaleY(int y) { + return (int)(y * DISPLAY_SCALE_Y); +} + +static int scaleWidth(int x, int w) { + if (w <= 0) return 0; + int scaled = scaleX(x + w) - scaleX(x); + return scaled > 0 ? scaled : 1; +} + +static int scaleHeight(int y, int h) { + if (h <= 0) return 0; + int scaled = scaleY(y + h) - scaleY(y); + return scaled > 0 ? scaled : 1; +} + +static uint8_t nv3001bMADCTL(uint8_t rotation) { + uint8_t madctl; + switch (rotation & 3) { + case 0: + madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB; + break; + case 1: + madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MX | NV3001B_MADCTL_RGB; + break; + case 2: + madctl = NV3001B_MADCTL_RGB; + break; + default: + madctl = NV3001B_MADCTL_MX | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB; + break; + } + return madctl; +} + +static const uint8_t font5x7[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x14, + 0x7f, 0x14, 0x7f, 0x14, 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62, 0x36, 0x49, + 0x55, 0x22, 0x50, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00, 0x00, 0x41, 0x22, + 0x1c, 0x00, 0x14, 0x08, 0x3e, 0x08, 0x14, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x50, 0x30, 0x00, + 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x60, 0x60, 0x00, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02, + 0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00, 0x42, 0x7f, 0x40, 0x00, 0x42, 0x61, 0x51, 0x49, 0x46, 0x21, + 0x41, 0x45, 0x4b, 0x31, 0x18, 0x14, 0x12, 0x7f, 0x10, 0x27, 0x45, 0x45, 0x45, 0x39, 0x3c, 0x4a, + 0x49, 0x49, 0x30, 0x01, 0x71, 0x09, 0x05, 0x03, 0x36, 0x49, 0x49, 0x49, 0x36, 0x06, 0x49, 0x49, + 0x29, 0x1e, 0x00, 0x36, 0x36, 0x00, 0x00, 0x00, 0x56, 0x36, 0x00, 0x00, 0x08, 0x14, 0x22, 0x41, + 0x00, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x41, 0x22, 0x14, 0x08, 0x02, 0x01, 0x51, 0x09, 0x06, + 0x32, 0x49, 0x79, 0x41, 0x3e, 0x7e, 0x11, 0x11, 0x11, 0x7e, 0x7f, 0x49, 0x49, 0x49, 0x36, 0x3e, + 0x41, 0x41, 0x41, 0x22, 0x7f, 0x41, 0x41, 0x22, 0x1c, 0x7f, 0x49, 0x49, 0x49, 0x41, 0x7f, 0x09, + 0x09, 0x09, 0x01, 0x3e, 0x41, 0x49, 0x49, 0x7a, 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, 0x41, 0x7f, + 0x41, 0x00, 0x20, 0x40, 0x41, 0x3f, 0x01, 0x7f, 0x08, 0x14, 0x22, 0x41, 0x7f, 0x40, 0x40, 0x40, + 0x40, 0x7f, 0x02, 0x0c, 0x02, 0x7f, 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x3e, 0x41, 0x41, 0x41, 0x3e, + 0x7f, 0x09, 0x09, 0x09, 0x06, 0x3e, 0x41, 0x51, 0x21, 0x5e, 0x7f, 0x09, 0x19, 0x29, 0x46, 0x46, + 0x49, 0x49, 0x49, 0x31, 0x01, 0x01, 0x7f, 0x01, 0x01, 0x3f, 0x40, 0x40, 0x40, 0x3f, 0x1f, 0x20, + 0x40, 0x20, 0x1f, 0x3f, 0x40, 0x38, 0x40, 0x3f, 0x63, 0x14, 0x08, 0x14, 0x63, 0x07, 0x08, 0x70, + 0x08, 0x07, 0x61, 0x51, 0x49, 0x45, 0x43, 0x00, 0x7f, 0x41, 0x41, 0x00, 0x02, 0x04, 0x08, 0x10, + 0x20, 0x00, 0x41, 0x41, 0x7f, 0x00, 0x04, 0x02, 0x01, 0x02, 0x04, 0x40, 0x40, 0x40, 0x40, 0x40, + 0x00, 0x01, 0x02, 0x04, 0x00, 0x20, 0x54, 0x54, 0x54, 0x78, 0x7f, 0x48, 0x44, 0x44, 0x38, 0x38, + 0x44, 0x44, 0x44, 0x20, 0x38, 0x44, 0x44, 0x48, 0x7f, 0x38, 0x54, 0x54, 0x54, 0x18, 0x08, 0x7e, + 0x09, 0x01, 0x02, 0x0c, 0x52, 0x52, 0x52, 0x3e, 0x7f, 0x08, 0x04, 0x04, 0x78, 0x00, 0x44, 0x7d, + 0x40, 0x00, 0x20, 0x40, 0x44, 0x3d, 0x00, 0x7f, 0x10, 0x28, 0x44, 0x00, 0x00, 0x41, 0x7f, 0x40, + 0x00, 0x7c, 0x04, 0x18, 0x04, 0x78, 0x7c, 0x08, 0x04, 0x04, 0x78, 0x38, 0x44, 0x44, 0x44, 0x38, + 0x7c, 0x14, 0x14, 0x14, 0x08, 0x08, 0x14, 0x14, 0x18, 0x7c, 0x7c, 0x08, 0x04, 0x04, 0x08, 0x48, + 0x54, 0x54, 0x54, 0x20, 0x04, 0x3f, 0x44, 0x40, 0x20, 0x3c, 0x40, 0x40, 0x20, 0x7c, 0x1c, 0x20, + 0x40, 0x20, 0x1c, 0x3c, 0x40, 0x30, 0x40, 0x3c, 0x44, 0x28, 0x10, 0x28, 0x44, 0x0c, 0x50, 0x50, + 0x50, 0x3c, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, 0x00, 0x7f, 0x00, + 0x00, 0x00, 0x41, 0x36, 0x08, 0x00, 0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00, 0x06, 0x09, 0x09, 0x06 +}; + +static int textPixelScaleX(uint8_t size) { + return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_X : NV3001B_TEXT_SIZE2_SCALE_X; +} + +static int textPixelScaleY(uint8_t size) { + return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_Y : NV3001B_TEXT_SIZE2_SCALE_Y; +} + +static void setupOptionalOutput(int pin, int level) { + if (pin < 0) return; + + pinMode(pin, OUTPUT); + digitalWrite(pin, level); +} + +static void writeOptionalPin(int pin, int level) { + if (pin < 0) return; + + digitalWrite(pin, level); +} + +void NV3001BDisplay::writeCommand(uint8_t cmd) { + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, LOW); + digitalWrite(PIN_TFT_CS, LOW); + spi.transfer(cmd); + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::writeBytes(const uint8_t* data, size_t len) { + if (!data || len == 0) return; + + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, HIGH); + digitalWrite(PIN_TFT_CS, LOW); + for (size_t i = 0; i < len; i++) { + spi.transfer(data[i]); + } + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::writeCommandData(uint8_t cmd, const uint8_t* data, size_t len) { + writeCommand(cmd); + writeBytes(data, len); +} + +void NV3001BDisplay::setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + uint16_t x2 = x + w - 1; + uint16_t y2 = y + h - 1; + uint8_t data[4]; + + data[0] = x >> 8; + data[1] = x & 0xff; + data[2] = x2 >> 8; + data[3] = x2 & 0xff; + writeCommandData(NV3001B_CASET, data, sizeof(data)); + + data[0] = y >> 8; + data[1] = y & 0xff; + data[2] = y2 >> 8; + data[3] = y2 & 0xff; + writeCommandData(NV3001B_RASET, data, sizeof(data)); + + writeCommand(NV3001B_RAMWR); +} + +void NV3001BDisplay::writeColor(uint16_t rgb, uint32_t count) { + uint8_t hi = rgb >> 8; + uint8_t lo = rgb & 0xff; + + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, HIGH); + digitalWrite(PIN_TFT_CS, LOW); + while (count--) { + spi.transfer(hi); + spi.transfer(lo); + } + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::initPanel() { +#define CMD0(C) do { writeCommand(C); } while (0) +#define CMD1(C, A) do { const uint8_t d[] = { A }; writeCommandData(C, d, sizeof(d)); } while (0) +#define CMD2(C, A, B) do { const uint8_t d[] = { A, B }; writeCommandData(C, d, sizeof(d)); } while (0) + + CMD0(NV3001B_SWRESET); + delay(120); + CMD1(0xFF, 0xA5); + CMD1(0x41, 0x00); + CMD1(0x50, 0x02); + CMD1(0x52, 0x6E); + CMD1(0x57, 0x02); + CMD1(0x46, 0x11); + CMD2(0x47, 0x00, 0x01); + CMD2(0x8F, 0x22, 0x03); + CMD1(0x9A, 0x78); + CMD1(0x9B, 0x78); + CMD1(0x9C, 0xA0); + CMD1(0x9D, 0x17); + CMD1(0x9E, 0xC1); + CMD1(0x83, 0x5A); + CMD1(0x84, 0xB6); + CMD1(0xFF, 0xA5); + CMD1(0x85, 0x5F); + CMD1(0x6E, 0x0F); + CMD1(0x7E, 0x0F); + CMD1(0x60, 0x00); + CMD1(0x70, 0x00); + CMD1(0x6D, 0x33); + CMD1(0x7D, 0x37); + CMD1(0x61, 0x09); + CMD1(0x71, 0x0A); + CMD1(0x6C, 0x2A); + CMD1(0x7C, 0x36); + CMD1(0x62, 0x11); + CMD1(0x72, 0x10); + CMD1(0x68, 0x4E); + CMD1(0x78, 0x4E); + CMD1(0x66, 0x36); + CMD1(0x76, 0x3C); + CMD1(0x1A, 0x1C); + CMD1(0x7B, 0x14); + CMD1(0x63, 0x0D); + CMD1(0x73, 0x0A); + CMD1(0x6A, 0x16); + CMD1(0x7A, 0x12); + CMD1(0x64, 0x0B); + CMD1(0x74, 0x0A); + CMD1(0x69, 0x08); + CMD1(0x79, 0x0A); + CMD1(0x65, 0x06); + CMD1(0x75, 0x07); + CMD1(0x67, 0x23); + CMD1(0x77, 0x44); + CMD1(0xE0, 0x00); + CMD1(0xE9, 0x30); + CMD1(0xEB, 0xB7); + CMD1(0xEC, 0x00); + CMD1(0xED, 0x11); + CMD1(0xF0, 0xB7); + CMD1(0x53, 0x04); + CMD1(0x54, 0x04); + CMD1(0x55, 0x40); + CMD1(0x56, 0x40); + CMD2(0xA0, 0x60, 0x01); + CMD1(0xA1, 0x84); + CMD1(0xA2, 0x85); + CMD2(0xAB, 0x00, 0x02); + CMD2(0xAC, 0x00, 0x06); + CMD2(0xAD, 0x00, 0x03); + CMD2(0xAE, 0x00, 0x07); + CMD1(0xC7, 0x01); + CMD1(0xB9, 0x82); + CMD1(0xBA, 0x83); + CMD1(0xBB, 0x00); + CMD1(0xBC, 0x81); + CMD1(0xBD, 0x02); + CMD1(0xBE, 0x01); + CMD1(0xBF, 0x04); + CMD1(0xC0, 0x03); + CMD1(0xC8, 0x55); + CMD1(0xC9, 0xC9); + CMD1(0xCA, 0xC8); + CMD1(0xCB, 0xCB); + CMD1(0xCC, 0xCA); + CMD1(0xCD, 0x55); + CMD1(0xCE, 0xCE); + CMD1(0xCF, 0xCD); + CMD1(0xD0, 0xD0); + CMD1(0xD1, 0xCF); + CMD1(0xF2, 0x46); + CMD1(0xA8, 0x04); + CMD1(0xA9, 0xB0); + CMD1(0xAA, 0xA3); + CMD1(0xB6, 0x00); + CMD1(0xB7, 0xB0); + CMD1(0xB8, 0xA3); + CMD1(0xC4, 0x03); + CMD1(0xC5, 0xB0); + CMD1(0xC6, 0xA3); + CMD1(0x80, 0x10); + CMD1(0xFF, 0x00); + CMD1(0x35, 0x00); + CMD0(NV3001B_SLPOUT); + delay(120); + CMD1(NV3001B_COLMOD, 0x05); + CMD1(NV3001B_MADCTL, nv3001bMADCTL(DISPLAY_ROTATION)); + CMD0(NV3001B_DISPON); + delay(10); + +#undef CMD0 +#undef CMD1 +#undef CMD2 +} + +void NV3001BDisplay::fillPhysicalRect(int x, int y, int w, int h) { + if (!is_on || w <= 0 || h <= 0) return; + + if (x < 0) { + w += x; + x = 0; + } + if (y < 0) { + h += y; + y = 0; + } + if (x + w > NV3001B_SCREEN_WIDTH) w = NV3001B_SCREEN_WIDTH - x; + if (y + h > NV3001B_SCREEN_HEIGHT) h = NV3001B_SCREEN_HEIGHT - y; + if (w <= 0 || h <= 0) return; + + setAddrWindow(x, y, w, h); + writeColor(color, (uint32_t)w * h); +} + +void NV3001BDisplay::drawChar(int x, int y, char ch) { + if (ch < 32 || ch > 127) ch = '?'; + + uint16_t index = (uint16_t)(ch - 32) * 5; + int scale_x = textPixelScaleX(text_size); + int scale_y = textPixelScaleY(text_size); + for (int col = 0; col < 5; col++) { + uint8_t line = pgm_read_byte(font5x7 + index + col); + for (int row = 0; row < 7; row++) { + if (line & (1 << row)) { + fillPhysicalRect(x + col * scale_x, y + row * scale_y, scale_x, scale_y); + } + } + } +} + +bool NV3001BDisplay::begin() { + if (is_on) return true; + + if (periph_power) periph_power->claim(); + + setupOptionalOutput(PIN_TFT_EN, PIN_TFT_EN_ACTIVE); + setupOptionalOutput(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE); + pinMode(PIN_TFT_CS, OUTPUT); + pinMode(PIN_TFT_DC, OUTPUT); + digitalWrite(PIN_TFT_CS, HIGH); + digitalWrite(PIN_TFT_DC, HIGH); + delay(20); + + spi.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS); + if (PIN_TFT_RST >= 0) { + pinMode(PIN_TFT_RST, OUTPUT); + digitalWrite(PIN_TFT_RST, HIGH); + delay(10); + digitalWrite(PIN_TFT_RST, LOW); + delay(20); + digitalWrite(PIN_TFT_RST, HIGH); + delay(120); + } + + initPanel(); + is_on = true; + color = 0x0000; + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = 0xffff; + text_size = 1; + cursor_x = 0; + cursor_y = 0; + writeOptionalPin(PIN_TFT_BL, PIN_TFT_BL_ACTIVE); + return true; +} + +void NV3001BDisplay::turnOn() { + begin(); +} + +void NV3001BDisplay::turnOff() { + if (!is_on) return; + + writeOptionalPin(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE); + writeOptionalPin(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE); + is_on = false; + if (periph_power) periph_power->release(); +} + +void NV3001BDisplay::clear() { + uint16_t saved = color; + color = 0x0000; + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = saved; +} + +void NV3001BDisplay::startFrame(Color bkg) { + color = mapColor(bkg); + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = 0xffff; + text_size = 1; + cursor_x = 0; + cursor_y = 0; +} + +void NV3001BDisplay::setTextSize(int sz) { + text_size = sz < 1 ? 1 : sz; +} + +void NV3001BDisplay::setColor(Color c) { + color = mapColor(c); +} + +void NV3001BDisplay::setCursor(int x, int y) { + cursor_x = scaleX(x); + cursor_y = scaleY(y); +} + +void NV3001BDisplay::print(const char* str) { + if (!str || !is_on) return; + + int scale_x = textPixelScaleX(text_size); + int scale_y = textPixelScaleY(text_size); + while (*str) { + if (*str == '\n') { + cursor_x = 0; + cursor_y += 8 * scale_y; + } else if (*str == '\r') { + cursor_x = 0; + } else { + drawChar(cursor_x, cursor_y, *str); + cursor_x += 6 * scale_x; + } + str++; + } +} + +void NV3001BDisplay::fillRect(int x, int y, int w, int h) { + fillPhysicalRect(scaleX(x), scaleY(y), scaleWidth(x, w), scaleHeight(y, h)); +} + +void NV3001BDisplay::drawRect(int x, int y, int w, int h) { + int x1 = scaleX(x); + int y1 = scaleY(y); + int sw = scaleWidth(x, w); + int sh = scaleHeight(y, h); + + fillPhysicalRect(x1, y1, sw, 1); + fillPhysicalRect(x1, y1 + sh - 1, sw, 1); + fillPhysicalRect(x1, y1, 1, sh); + fillPhysicalRect(x1 + sw - 1, y1, 1, sh); +} + +void NV3001BDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + if (!bits || !is_on) return; + + int byte_width = (w + 7) / 8; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + uint8_t byte = pgm_read_byte(bits + j * byte_width + i / 8); + if (byte & (0x80 >> (i & 7))) { + fillPhysicalRect(scaleX(x + i), scaleY(y + j), scaleWidth(x + i, 1), scaleHeight(y + j, 1)); + } + } + } +} + +uint16_t NV3001BDisplay::getTextWidth(const char* str) { + if (!str) return 0; + + uint16_t len = 0; + while (str[len] && str[len] != '\n' && str[len] != '\r') len++; + return (uint16_t)((len * 6 * textPixelScaleX(text_size)) / DISPLAY_SCALE_X); +} + +void NV3001BDisplay::endFrame() { +} diff --git a/src/helpers/ui/NV3001BDisplay.h b/src/helpers/ui/NV3001BDisplay.h new file mode 100644 index 00000000..98cdaae8 --- /dev/null +++ b/src/helpers/ui/NV3001BDisplay.h @@ -0,0 +1,68 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include + +#ifndef NV3001B_LOGICAL_WIDTH + #define NV3001B_LOGICAL_WIDTH 128 +#endif + +#ifndef NV3001B_LOGICAL_HEIGHT + #define NV3001B_LOGICAL_HEIGHT 64 +#endif + +#ifndef NV3001B_PANEL_WIDTH + #define NV3001B_PANEL_WIDTH 128 +#endif + +#ifndef NV3001B_PANEL_HEIGHT + #define NV3001B_PANEL_HEIGHT 220 +#endif + +#ifndef NV3001B_SPI_HOST + #define NV3001B_SPI_HOST HSPI +#endif + +class NV3001BDisplay : public DisplayDriver { + SPIClass spi; + RefCountedDigitalPin* periph_power; + bool is_on = false; + uint16_t color = 0xffff; + uint8_t text_size = 1; + int cursor_x = 0; + int cursor_y = 0; + + void writeCommand(uint8_t cmd); + void writeBytes(const uint8_t* data, size_t len); + void writeCommandData(uint8_t cmd, const uint8_t* data, size_t len); + void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void writeColor(uint16_t rgb, uint32_t count); + void fillPhysicalRect(int x, int y, int w, int h); + void initPanel(); + void drawChar(int x, int y, char ch); + +public: + NV3001BDisplay(RefCountedDigitalPin* power = nullptr) : + DisplayDriver(NV3001B_LOGICAL_WIDTH, NV3001B_LOGICAL_HEIGHT), spi(NV3001B_SPI_HOST), periph_power(power) { } + + bool begin(); + static const char* driverName() { return "NV3001B"; } + static uint16_t physicalWidth() { return NV3001B_PANEL_WIDTH; } + static uint16_t physicalHeight() { return NV3001B_PANEL_HEIGHT; } + + bool isOn() override { return is_on; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; +}; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp new file mode 100644 index 00000000..37188e9e --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -0,0 +1,66 @@ +#include "HeltecRC32Board.h" + +void HeltecRC32Board::begin() { + ESP32Board::begin(); + + pinMode(PIN_ADC_CTRL, OUTPUT); + digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED); + +#ifdef SENSOR_RST_PIN + pinMode(SENSOR_RST_PIN, OUTPUT); + digitalWrite(SENSOR_RST_PIN, HIGH); +#endif + +#ifdef LED_POWER + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LOW); +#endif + + periph_power.begin(); + vext_power.begin(); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1L << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } +} + +void HeltecRC32Board::powerOff() { + enterDeepSleep(0); +} + +void HeltecRC32Board::onBeforeTransmit() { + digitalWrite(P_LORA_TX_LED, HIGH); +} + +void HeltecRC32Board::onAfterTransmit() { + digitalWrite(P_LORA_TX_LED, LOW); +} + +uint16_t HeltecRC32Board::getBattMilliVolts() { + analogReadResolution(12); + digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED); + delay(10); + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogReadMilliVolts(PIN_VBAT_READ); + } + raw = raw / 8; + + return (adc_mult * raw); +} + +bool HeltecRC32Board::setAdcMultiplier(float multiplier) { + adc_mult = multiplier == 0.0f ? ADC_MULTIPLIER : multiplier; + return true; +} + +const char* HeltecRC32Board::getManufacturerName() const { + return "Heltec RC32"; +} diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h new file mode 100644 index 00000000..256cb964 --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER 4.9f +#endif + +class HeltecRC32Board : public ESP32Board { +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + RefCountedDigitalPin vext_power; + + HeltecRC32Board() : + periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON), + vext_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + void begin(); + void onBeforeTransmit() override; + void onAfterTransmit() override; + void powerOff() override; + uint16_t getBattMilliVolts() override; + bool setAdcMultiplier(float multiplier) override; + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/heltec_rc32/pins_arduino.h b/variants/heltec_rc32/pins_arduino.h new file mode 100644 index 00000000..8ea3a0c1 --- /dev/null +++ b/variants/heltec_rc32/pins_arduino.h @@ -0,0 +1,60 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 18; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 12; +static const uint8_t MISO = 13; +static const uint8_t SCK = 11; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 9; +static const uint8_t BUSY_LoRa = 1; +static const uint8_t DIO1_LoRa = 14; + +#endif diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini new file mode 100644 index 00000000..9284650b --- /dev/null +++ b/variants/heltec_rc32/platformio.ini @@ -0,0 +1,346 @@ +[Heltec_RC32] +extends = esp32_base +board = heltec-rc32 +board_build.partitions = default_16MB.csv +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/heltec_rc32 + -I src/helpers/ui + -D HELTEC_RC32 + -D USE_SX1262 + -D ESP32_CPU_FREQ=160 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=10 + -D P_LORA_RESET=9 + -D P_LORA_BUSY=1 + -D P_LORA_SCLK=11 + -D P_LORA_MISO=13 + -D P_LORA_MOSI=12 + -D LORA_TX_POWER=22 + -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=21 + -D PIN_BOARD_SCL=18 + -D PIN_VEXT_EN=3 + -D PIN_VEXT_EN_ACTIVE=HIGH + -D SENSOR_POWER_CTRL_PIN=46 + -D SENSOR_POWER_ON=HIGH + -D SENSOR_RST_PIN=2 + -D P_LORA_TX_LED=47 + -D PIN_BUZZER=48 + -D PIN_TFT_SCL=17 + -D PIN_TFT_SDA=38 + -D PIN_TFT_CS=39 + -D PIN_TFT_DC=16 + -D PIN_TFT_RST=4 + -D PIN_TFT_EN=6 + -D PIN_TFT_EN_ACTIVE=LOW + -D PIN_TFT_BL=5 + -D PIN_TFT_BL_ACTIVE=HIGH + -D SPI_FREQUENCY=8000000 + -D PIN_GPS_TX=44 + -D PIN_GPS_RX=43 + -D PIN_GPS_EN=45 + -D PIN_GPS_EN_ACTIVE=HIGH + -D PIN_GPS_RESET=40 + -D PIN_GPS_RESET_ACTIVE=LOW + -D PIN_GPS_PPS=41 + -D GPS_BAUD_RATE=9600 + -D ENV_INCLUDE_GPS=1 + -D PIN_ADC_CTRL=15 + -D PIN_VBAT_READ=7 + -D ADC_CTRL_ENABLED=HIGH + -D ADC_MULTIPLIER=4.9 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_rc32> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[Heltec_RC32_with_display] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D HELTEC_RC32_WITH_DISPLAY + -D DISPLAY_CLASS=NV3001BDisplay +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + +lib_deps = + ${Heltec_RC32.lib_deps} + +[env:heltec_rc32_without_display_repeater] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_rc32_without_display_repeater_bridge_espnow] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_without_display_room_server] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_without_display_companion_radio_usb] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_companion_radio_ble] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_companion_radio_wifi] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_sensor] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=21 + -D ENV_PIN_SCL=18 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_repeater] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_rc32_repeater_bridge_espnow] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_room_server] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_terminal_chat] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_rc32_companion_radio_usb] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_companion_radio_ble] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_companion_radio_wifi] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_sensor] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=21 + -D ENV_PIN_SCL=18 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_kiss_modem] +extends = Heltec_RC32 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp new file mode 100644 index 00000000..6c88d2bb --- /dev/null +++ b/variants/heltec_rc32/target.cpp @@ -0,0 +1,45 @@ +#include +#include "target.h" + +HeltecRC32Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi(FSPI); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN, &board.periph_power); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/heltec_rc32/target.h b/variants/heltec_rc32/target.h new file mode 100644 index 00000000..ae692598 --- /dev/null +++ b/variants/heltec_rc32/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +#ifdef DISPLAY_CLASS +#include +#ifdef HELTEC_RC32_WITH_DISPLAY +#include +#else +#include +#endif +#endif + +extern HeltecRC32Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h new file mode 100644 index 00000000..d510ab9b --- /dev/null +++ b/variants/heltec_rc32/variant.h @@ -0,0 +1,51 @@ +#ifndef _VARIANT_HELTEC_RC32_ +#define _VARIANT_HELTEC_RC32_ + +#define BUTTON_PIN 0 + +#define HAS_GPS 1 +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define PIN_GPS_EN 45 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET 40 +#define GPS_RESET_MODE LOW +#define PIN_GPS_PPS 41 + +#define I2C_SCL 18 +#define I2C_SDA 21 +#define SENSOR_INT_PIN 42 +#define SENSOR_RST_PIN 2 +#define SENSOR_POWER_CTRL_PIN 46 +#define SENSOR_POWER_ON HIGH +#define PERIPHERAL_WARMUP_MS 100 + +#define VEXT_ENABLE 3 +#define VEXT_ON_VALUE HIGH + +#define USE_SX1262 +#define LORA_SCK 11 +#define LORA_MISO 13 +#define LORA_MOSI 12 +#define LORA_CS 10 +#define LORA_DIO0 RADIOLIB_NC +#define LORA_DIO1 14 +#define LORA_RESET 9 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 1 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC_CHANNEL_6 +#define ADC_CTRL 15 +#define ADC_CTRL_ENABLED HIGH +#define ADC_MULTIPLIER 4.9 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 + +#endif From 754fcb1fae19c2f4b1af2dfe248a809c68438401 Mon Sep 17 00:00:00 2001 From: Will Dillon Date: Thu, 9 Jul 2026 11:43:20 -0700 Subject: [PATCH 17/48] Update number_allocations.md Adds a small allocation for groupdata packets for the Meshcore firmware for the StreamSensor product. It was previously a LoRaWAN platform, and we've moved to Meshcore. --- docs/number_allocations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 857525d4..84b9cba0 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -17,6 +17,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0000 - 00FF | -reserved for internal use- | | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | +| 0120 - 012F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From d9cd3ea03a41f74a1869210b0c4330d8f92668f2 Mon Sep 17 00:00:00 2001 From: ripplebiz Date: Mon, 13 Jul 2026 19:30:11 +1000 Subject: [PATCH 18/48] Revert "Add StreamSensor allocation for GroupData" --- docs/number_allocations.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 84b9cba0..857525d4 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -17,7 +17,6 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0000 - 00FF | -reserved for internal use- | | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | -| 0120 - 012F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 05d4bd2f7f363f2d69a776ed758838328eea2fa3 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 15 Jul 2026 17:45:24 +0800 Subject: [PATCH 19/48] Add a rotary encoder --- examples/companion_radio/ui-new/UITask.cpp | 10 ++ src/helpers/ui/RotaryInput.h | 18 +++ variants/heltec_rc32/HeltecRC32Board.cpp | 12 +- variants/heltec_rc32/HeltecRC32Board.h | 11 +- .../heltec_rc32/HeltecRC32RotaryInput.cpp | 122 ++++++++++++++++++ variants/heltec_rc32/HeltecRC32RotaryInput.h | 26 ++++ variants/heltec_rc32/platformio.ini | 5 +- variants/heltec_rc32/target.cpp | 5 +- variants/heltec_rc32/target.h | 6 + variants/heltec_rc32/variant.h | 3 - 10 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 src/helpers/ui/RotaryInput.h create mode 100644 variants/heltec_rc32/HeltecRC32RotaryInput.cpp create mode 100644 variants/heltec_rc32/HeltecRC32RotaryInput.h diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 28591cc1..64e61bac 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -750,6 +750,16 @@ void UITask::loop() { c = handleTripleClick(KEY_SELECT); } #endif +#if defined(UI_HAS_ROTARY_INPUT) + if (c == 0) { + RotaryInputEvent ev = rotary_input.poll(); + if (ev == RotaryInputEvent::Next) { + c = checkDisplayOn(KEY_NEXT); + } else if (ev == RotaryInputEvent::Prev) { + c = checkDisplayOn(KEY_PREV); + } + } +#endif #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { int ev = analog_btn.check(); diff --git a/src/helpers/ui/RotaryInput.h b/src/helpers/ui/RotaryInput.h new file mode 100644 index 00000000..2dda695b --- /dev/null +++ b/src/helpers/ui/RotaryInput.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +enum class RotaryInputEvent : uint8_t { + None, + Next, + Prev, +}; + +class RotaryInput { +public: + virtual ~RotaryInput() = default; + + virtual bool begin() = 0; + virtual RotaryInputEvent poll() = 0; + virtual bool isReady() const = 0; +}; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 37188e9e..11be483e 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -1,4 +1,14 @@ #include "HeltecRC32Board.h" +#if defined(UI_HAS_ROTARY_INPUT) +#include "HeltecRC32RotaryInput.h" +#endif + +#if defined(UI_HAS_ROTARY_INPUT) +RotaryInput& HeltecRC32Board::rotaryInput() { + static HeltecRC32RotaryInput input(&periph_power); + return input; +} +#endif void HeltecRC32Board::begin() { ESP32Board::begin(); @@ -17,7 +27,7 @@ void HeltecRC32Board::begin() { #endif periph_power.begin(); - vext_power.begin(); + periph_power.claim(); esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 256cb964..5f3a0adf 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -4,6 +4,9 @@ #include #include #include +#if defined(UI_HAS_ROTARY_INPUT) +#include +#endif #ifndef ADC_MULTIPLIER #define ADC_MULTIPLIER 4.9f @@ -15,13 +18,13 @@ protected: public: RefCountedDigitalPin periph_power; - RefCountedDigitalPin vext_power; - HeltecRC32Board() : - periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON), - vext_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){} void begin(); +#if defined(UI_HAS_ROTARY_INPUT) + RotaryInput& rotaryInput(); +#endif void onBeforeTransmit() override; void onAfterTransmit() override; void powerOff() override; diff --git a/variants/heltec_rc32/HeltecRC32RotaryInput.cpp b/variants/heltec_rc32/HeltecRC32RotaryInput.cpp new file mode 100644 index 00000000..e8b282ff --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32RotaryInput.cpp @@ -0,0 +1,122 @@ +#include "HeltecRC32RotaryInput.h" + +#include + +namespace { +constexpr uint8_t TCA6408_ADDR = 0x20; +constexpr uint8_t TCA6408_INPUT_REG = 0x00; +constexpr uint8_t TCA6408_POLARITY_REG = 0x02; +constexpr uint8_t TCA6408_CONFIG_REG = 0x03; +constexpr uint8_t TCA6408_ROTARY_A_MASK = 0x01; +constexpr uint8_t TCA6408_ROTARY_B_MASK = 0x02; +constexpr uint8_t TCA6408_ROTARY_MASK = TCA6408_ROTARY_A_MASK | TCA6408_ROTARY_B_MASK; +constexpr uint32_t TCA6408_DEBOUNCE_MS = 5; +} + +bool HeltecRC32RotaryInput::begin() { + initialized = true; + ready = false; + input_state = TCA6408_ROTARY_MASK; + active_low_phase = false; + + if (periph_power && !power_claimed) { + periph_power->claim(); + power_claimed = true; + delay(12); + } + + if (!writeRegister(TCA6408_POLARITY_REG, 0x00) || !writeRegister(TCA6408_CONFIG_REG, 0xFF)) { + return false; + } + + uint8_t state = 0; + if (!readInput(state)) { + return false; + } + + input_state = state & TCA6408_ROTARY_MASK; + ready = true; + return true; +} + +RotaryInputEvent HeltecRC32RotaryInput::poll() { + if (!initialized) { + begin(); + return RotaryInputEvent::None; + } + + if (!ready) { + return RotaryInputEvent::None; + } + + uint8_t new_state = 0; + if (!readInput(new_state)) { + return RotaryInputEvent::None; + } + + new_state &= TCA6408_ROTARY_MASK; + RotaryInputEvent event = handleTransition(new_state); + input_state = new_state; + return event; +} + +bool HeltecRC32RotaryInput::writeRegister(uint8_t reg, uint8_t value) { + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; +} + +bool HeltecRC32RotaryInput::readInput(uint8_t& value) { + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(TCA6408_INPUT_REG); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(TCA6408_ADDR, static_cast(1)) != 1) { + return false; + } + + value = Wire.read(); + return true; +} + +RotaryInputEvent HeltecRC32RotaryInput::handleTransition(uint8_t newState) { + uint8_t changed = (input_state ^ newState) & TCA6408_ROTARY_MASK; + RotaryInputEvent event = RotaryInputEvent::None; + bool a_low = (newState & TCA6408_ROTARY_A_MASK) == 0; + bool b_low = (newState & TCA6408_ROTARY_B_MASK) == 0; + + if (!a_low && !b_low) { + active_low_phase = false; + } + + if (!active_low_phase && (changed & TCA6408_ROTARY_A_MASK) && a_low && !b_low) { + event = RotaryInputEvent::Prev; + active_low_phase = true; + } else if (!active_low_phase && (changed & TCA6408_ROTARY_B_MASK) && b_low && !a_low) { + event = RotaryInputEvent::Next; + active_low_phase = true; + } + + if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_A_MASK)) { + bool a_rising = (newState & TCA6408_ROTARY_A_MASK) != 0; + if (a_rising && b_low) { + event = RotaryInputEvent::Prev; + } + } + + if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_B_MASK)) { + bool b_rising = (newState & TCA6408_ROTARY_B_MASK) != 0; + if (b_rising && a_low) { + event = RotaryInputEvent::Next; + } + } + + if (event == RotaryInputEvent::None || (millis() - last_event_ms) < TCA6408_DEBOUNCE_MS) { + return RotaryInputEvent::None; + } + + last_event_ms = millis(); + return event; +} diff --git a/variants/heltec_rc32/HeltecRC32RotaryInput.h b/variants/heltec_rc32/HeltecRC32RotaryInput.h new file mode 100644 index 00000000..36557dca --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32RotaryInput.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class HeltecRC32RotaryInput : public RotaryInput { +public: + explicit HeltecRC32RotaryInput(RefCountedDigitalPin* periphPower = nullptr) : periph_power(periphPower) { } + + bool begin() override; + RotaryInputEvent poll() override; + bool isReady() const override { return ready; } + +private: + bool writeRegister(uint8_t reg, uint8_t value); + bool readInput(uint8_t& value); + RotaryInputEvent handleTransition(uint8_t newState); + + uint8_t input_state = 0x03; + uint32_t last_event_ms = 0; + bool ready = false; + bool initialized = false; + bool power_claimed = false; + bool active_low_phase = false; + RefCountedDigitalPin* periph_power = nullptr; +}; diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 9284650b..545472d8 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -23,8 +23,6 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_BOARD_SDA=21 -D PIN_BOARD_SCL=18 - -D PIN_VEXT_EN=3 - -D PIN_VEXT_EN_ACTIVE=HIGH -D SENSOR_POWER_CTRL_PIN=46 -D SENSOR_POWER_ON=HIGH -D SENSOR_RST_PIN=2 @@ -270,6 +268,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 build_src_filter = ${Heltec_RC32_with_display.build_src_filter} @@ -287,6 +286,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 @@ -308,6 +308,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp index 6c88d2bb..386fc559 100644 --- a/variants/heltec_rc32/target.cpp +++ b/variants/heltec_rc32/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN, &board.periph_power); + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN); EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); #else EnvironmentSensorManager sensors; @@ -26,6 +26,9 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#if defined(UI_HAS_ROTARY_INPUT) + RotaryInput& rotary_input = board.rotaryInput(); +#endif #endif bool radio_init() { diff --git a/variants/heltec_rc32/target.h b/variants/heltec_rc32/target.h index ae692598..04cb6d94 100644 --- a/variants/heltec_rc32/target.h +++ b/variants/heltec_rc32/target.h @@ -10,6 +10,9 @@ #ifdef DISPLAY_CLASS #include +#if defined(UI_HAS_ROTARY_INPUT) +#include +#endif #ifdef HELTEC_RC32_WITH_DISPLAY #include #else @@ -25,6 +28,9 @@ extern EnvironmentSensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; +#if defined(UI_HAS_ROTARY_INPUT) + extern RotaryInput& rotary_input; +#endif #endif bool radio_init(); diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h index d510ab9b..0546fbc4 100644 --- a/variants/heltec_rc32/variant.h +++ b/variants/heltec_rc32/variant.h @@ -22,9 +22,6 @@ #define SENSOR_POWER_ON HIGH #define PERIPHERAL_WARMUP_MS 100 -#define VEXT_ENABLE 3 -#define VEXT_ON_VALUE HIGH - #define USE_SX1262 #define LORA_SCK 11 #define LORA_MISO 13 From 3469e924586151f2209dc5a2d5a353e8959e0081 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 11:37:12 +0800 Subject: [PATCH 20/48] Enable patch reception --- variants/heltec_rc32/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 545472d8..d535fb77 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -55,6 +55,7 @@ build_flags = -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_rc32> + From a472cd7956cb5cb31f650cdb2f4f04d4451ea259 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 15:14:17 +0800 Subject: [PATCH 21/48] Delete duplicate pin definitions --- variants/heltec_rc32/variant.h | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h index 0546fbc4..5f7b8d81 100644 --- a/variants/heltec_rc32/variant.h +++ b/variants/heltec_rc32/variant.h @@ -3,26 +3,11 @@ #define BUTTON_PIN 0 -#define HAS_GPS 1 -#undef GPS_RX_PIN -#undef GPS_TX_PIN -#define GPS_RX_PIN 44 -#define GPS_TX_PIN 43 -#define PIN_GPS_EN 45 -#define GPS_EN_ACTIVE HIGH -#define PIN_GPS_RESET 40 -#define GPS_RESET_MODE LOW -#define PIN_GPS_PPS 41 - #define I2C_SCL 18 #define I2C_SDA 21 #define SENSOR_INT_PIN 42 -#define SENSOR_RST_PIN 2 -#define SENSOR_POWER_CTRL_PIN 46 -#define SENSOR_POWER_ON HIGH #define PERIPHERAL_WARMUP_MS 100 -#define USE_SX1262 #define LORA_SCK 11 #define LORA_MISO 13 #define LORA_MOSI 12 @@ -35,14 +20,10 @@ #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY 1 #define SX126X_RESET LORA_RESET -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define BATTERY_PIN 7 #define ADC_CHANNEL ADC_CHANNEL_6 #define ADC_CTRL 15 -#define ADC_CTRL_ENABLED HIGH -#define ADC_MULTIPLIER 4.9 #define ADC_ATTENUATION ADC_ATTEN_DB_2_5 #endif From 61a8821d3c6f950738c3bd35bd13b0dfbefc6af0 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 17:45:00 +0800 Subject: [PATCH 22/48] Optimize battery voltage reading --- variants/heltec_rc32/HeltecRC32Board.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 11be483e..0517f5ff 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -55,6 +55,7 @@ void HeltecRC32Board::onAfterTransmit() { uint16_t HeltecRC32Board::getBattMilliVolts() { analogReadResolution(12); + analogSetAttenuation(ADC_2_5db); digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED); delay(10); uint32_t raw = 0; @@ -62,6 +63,7 @@ uint16_t HeltecRC32Board::getBattMilliVolts() { raw += analogReadMilliVolts(PIN_VBAT_READ); } raw = raw / 8; + digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED); return (adc_mult * raw); } From b26f53a263c41b1587065a5cfcb9049de3999184 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 17 Jul 2026 15:35:07 +0800 Subject: [PATCH 23/48] Avoid waking display from rotary input --- examples/companion_radio/ui-new/UITask.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 64e61bac..5a23324c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -751,12 +751,12 @@ void UITask::loop() { } #endif #if defined(UI_HAS_ROTARY_INPUT) - if (c == 0) { - RotaryInputEvent ev = rotary_input.poll(); - if (ev == RotaryInputEvent::Next) { - c = checkDisplayOn(KEY_NEXT); - } else if (ev == RotaryInputEvent::Prev) { - c = checkDisplayOn(KEY_PREV); + RotaryInputEvent rotaryEv = rotary_input.poll(); + if (c == 0 && _display != NULL && _display->isOn()) { + if (rotaryEv == RotaryInputEvent::Next) { + c = KEY_NEXT; + } else if (rotaryEv == RotaryInputEvent::Prev) { + c = KEY_PREV; } } #endif From fd7b35f457a580c1e43724135b68a0c1b6d801ba Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 17 Jul 2026 15:50:25 +0800 Subject: [PATCH 24/48] Move the rotation input to target.cpp --- variants/heltec_rc32/HeltecRC32Board.cpp | 10 ---------- variants/heltec_rc32/HeltecRC32Board.h | 6 ------ variants/heltec_rc32/target.cpp | 6 +++++- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 0517f5ff..59920d23 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -1,14 +1,4 @@ #include "HeltecRC32Board.h" -#if defined(UI_HAS_ROTARY_INPUT) -#include "HeltecRC32RotaryInput.h" -#endif - -#if defined(UI_HAS_ROTARY_INPUT) -RotaryInput& HeltecRC32Board::rotaryInput() { - static HeltecRC32RotaryInput input(&periph_power); - return input; -} -#endif void HeltecRC32Board::begin() { ESP32Board::begin(); diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 5f3a0adf..5b2093c8 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -4,9 +4,6 @@ #include #include #include -#if defined(UI_HAS_ROTARY_INPUT) -#include -#endif #ifndef ADC_MULTIPLIER #define ADC_MULTIPLIER 4.9f @@ -22,9 +19,6 @@ public: HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){} void begin(); -#if defined(UI_HAS_ROTARY_INPUT) - RotaryInput& rotaryInput(); -#endif void onBeforeTransmit() override; void onAfterTransmit() override; void powerOff() override; diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp index 386fc559..cc7e4deb 100644 --- a/variants/heltec_rc32/target.cpp +++ b/variants/heltec_rc32/target.cpp @@ -1,5 +1,8 @@ #include #include "target.h" +#if defined(UI_HAS_ROTARY_INPUT) +#include "HeltecRC32RotaryInput.h" +#endif HeltecRC32Board board; @@ -27,7 +30,8 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #if defined(UI_HAS_ROTARY_INPUT) - RotaryInput& rotary_input = board.rotaryInput(); + static HeltecRC32RotaryInput rotaryInputImpl(&board.periph_power); + RotaryInput& rotary_input = rotaryInputImpl; #endif #endif From 975a673efd4fcabc21becb8f92f434b432f43265 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:15:09 +1000 Subject: [PATCH 25/48] add calcMaxPacketMillis --- src/helpers/radiolib/RadioLibWrappers.cpp | 18 ++++++++++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 5e72336c..7146e47b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -228,3 +228,21 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty)); } + +PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) { + // based on RadioLib's calculateTimeOnAir() + uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10); + uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd + + // preamble + syncword + sfd + header + uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4; + + // airtime for max packet at current radio settings + uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT); + // airtime for payload only (no preamble, header or SOF) + uint32_t payload_us = total_us > preamble_us ? total_us - preamble_us : 4000 - preamble_us; // fallback to 4 secs at worst case + // rescale payload_us for max possible CR + if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } + + return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; +} \ No newline at end of file diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3091832f..e7fef910 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,6 +3,11 @@ #include #include +struct PacketMillis { + uint32_t preambleMillis; // preamble-detect -> header-valid deadline + uint32_t payloadMillis; // header-valid -> rx-done deadline +}; + class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; @@ -47,6 +52,7 @@ public: virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } + PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols); virtual int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } From 79ef74ea8def0195ddbe340441fec7ce5c943051 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:33:57 +1000 Subject: [PATCH 26/48] add IRQ timeout logic for SX1262 --- src/helpers/radiolib/CustomSX1262.h | 51 +++++++++++++++++++--- src/helpers/radiolib/CustomSX1262Wrapper.h | 1 + 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ca62fc26..284b79c0 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -3,10 +3,12 @@ #include #include "MeshCore.h" -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSX1262 : public SX1262 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSX1262(Module *mod) : SX1262(mod) { } @@ -99,9 +101,46 @@ class CustomSX1262 : public SX1262 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqStatus(RADIOLIB_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); + _preambleMillis = maxPacketMillis.preambleMillis; + _maxPayloadMillis = maxPacketMillis.payloadMillis; } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 1d103f57..45af4cb4 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -18,6 +18,7 @@ public: ((CustomSX1262 *)_radio)->setBandwidth(bw); ((CustomSX1262 *)_radio)->setCodingRate(cr); updatePreamble(sf); + ((CustomSX1262 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); } bool isReceivingPacket() override { From 84d1e24338138327394a447b7faad27934e4c314 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 17 Jul 2026 07:36:39 -0400 Subject: [PATCH 27/48] nrf52_variants: override shutdownPeripherals instead of powerOff for TechoLite, TInpulse and TechoBoard (protect the gate) --- variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h | 4 ++-- variants/lilygo_techo_card/TechoCardBoard.cpp | 4 ++-- variants/lilygo_techo_card/TechoCardBoard.h | 2 +- variants/lilygo_techo_lite/TechoBoard.h | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 5fafcad8..30bbc3a5 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -47,9 +47,9 @@ public: return "LilyGo T-Impulse-Plus"; } - void powerOff() override { + void shutdownPeripherals() override { // power off system - NRF52Board::powerOff(); + NRF52Board::shutdownPeripherals(); // turn off 3.3v digitalWrite(RT9080_EN, LOW); diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 8143587d..8a3a54b1 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -87,11 +87,11 @@ void TechoCardBoard::turnOffLeds() { } } -void TechoCardBoard::powerOff() { +void TechoCardBoard::shutdownPeripherals() { nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); turnOffLeds(); digitalWrite(PIN_PWR_EN, LOW); - NRF52Board::powerOff(); + NRF52Board::shutdownPeripherals(); } #endif diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 8a2913a6..a2ee3ea1 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -28,7 +28,7 @@ public: return "LilyGo T-Echo Card"; } - void powerOff() override; + void shutdownPeripherals() override; void toggleTorch(); void turnOffLeds(); diff --git a/variants/lilygo_techo_lite/TechoBoard.h b/variants/lilygo_techo_lite/TechoBoard.h index f4c16016..1e5651e7 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -21,8 +21,8 @@ public: return "LilyGo T-Echo Lite"; } - void powerOff() override { - NRF52Board::powerOff(); + void shutdownPeripherals() override { + NRF52Board::shutdownPeripherals(); digitalWrite(PIN_VBAT_MEAS_EN, LOW); #ifdef LED_RED From 0bd871cde6e3256c7de7518c703bb3827d3f9fa2 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:45:31 +1000 Subject: [PATCH 28/48] add IRQ timeout logic for LR11X0 --- src/helpers/radiolib/CustomLR1110.h | 47 ++++++++++++++++++++-- src/helpers/radiolib/CustomLR1110Wrapper.h | 2 + 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 4061c6b1..db0f2af3 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -4,6 +4,10 @@ #include "MeshCore.h" class CustomLR1110 : public LR1110 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: @@ -32,9 +36,46 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } bool isReceiving() { - uint16_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); - return detected; + uint32_t irq = getIrqStatus(); + bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 + bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 + bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 + uint32_t now = millis(); + if (hdrErr) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); + _preambleMillis = maxPacketMillis.preambleMillis; + _maxPayloadMillis = maxPacketMillis.payloadMillis; } uint8_t getSpreadingFactor() const { return spreadingFactor; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index c6b1acb4..981faf09 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -14,6 +14,8 @@ public: ((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setCodingRate(cr); updatePreamble(sf); + ((CustomLR1110 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); + } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } From c644720ea7f969d45731e7cd018f1b57d9186a08 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 17 Jul 2026 08:29:31 -0400 Subject: [PATCH 29/48] uitask: screen and radio poweroff moved to board --- examples/companion_radio/ui-new/UITask.cpp | 3 --- examples/companion_radio/ui-orig/UITask.cpp | 2 -- examples/companion_radio/ui-tiny/UITask.cpp | 2 -- examples/simple_repeater/UITask.cpp | 2 -- 4 files changed, 9 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 28591cc1..403ea463 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -697,9 +697,6 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - // still necessary until all boards are refactored to use poweroff - _display->turnOff(); - radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 34a7342b..b48f6412 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -307,8 +307,6 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - _display->turnOff(); - radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index a6cbe9de..452c02d4 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -566,8 +566,6 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - _display->turnOff(); - radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 17c708e3..6751aad6 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -146,8 +146,6 @@ void UITask::loop() { digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff #endif if (millis() > _powering_off_at) { - _display->turnOff(); - radio_driver.powerOff(); _board->powerOff(); // should not return } } From fb2c61f862fcd4c6e08cf0f882175ca260052b13 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Mon, 22 Jun 2026 16:12:00 +0200 Subject: [PATCH 30/48] fix: prevent ESP32 KISS modem stalls under USB backpressure Switch ESP32-S3 KISS modem environments to HWCDC and move outbound KISS writes to a non-blocking queued frame path so loop() and TX completion keep progressing when the host reads slowly. Add native backpressure regression tests and correct the native_kiss_modem test filter so this suite runs directly with pio test. --- docs/kiss_modem_protocol.md | 14 +- examples/kiss_modem/KissModem.cpp | 199 ++++++++++-- examples/kiss_modem/KissModem.h | 38 ++- examples/kiss_modem/main.cpp | 10 +- platformio.ini | 15 + test/mocks/Arduino.h | 17 + test/mocks/CayenneLPP.h | 11 + test/mocks/Identity.h | 39 +++ test/mocks/Mesh.h | 27 ++ test/mocks/Stream.h | 21 +- test/mocks/Utils.h | 44 +++ test/mocks/helpers/SensorManager.h | 10 + test/test_kiss_modem/test_tx_backpressure.cpp | 294 ++++++++++++++++++ variants/heltec_v4/platformio.ini | 5 + variants/station_g2/platformio.ini | 5 + 15 files changed, 702 insertions(+), 47 deletions(-) create mode 100644 test/mocks/Arduino.h create mode 100644 test/mocks/CayenneLPP.h create mode 100644 test/mocks/Identity.h create mode 100644 test/mocks/Mesh.h create mode 100644 test/mocks/Utils.h create mode 100644 test/mocks/helpers/SensorManager.h create mode 100644 test/test_kiss_modem/test_tx_backpressure.cpp diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 9a996224..3f4dbf9c 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -41,7 +41,7 @@ Maximum unescaped frame size: 512 bytes. | Command | Value | Data | Description | |-------------|--------|--------------------|-------------------------------------------------------------| -| Data | `0x00` | Raw packet | Queue packet for transmission | +| Data | `0x00` | Raw packet | Queue packet for transmission (one pending at a time) | | TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) | | Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) | | SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) | @@ -58,6 +58,12 @@ Maximum unescaped frame size: 512 bytes. Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. +Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07). + +### Host Output Backpressure + +Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; `loop()` never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses. + ### CSMA Behavior The TNC implements p-persistent CSMA for half-duplex operation: @@ -156,15 +162,15 @@ Response codes use the high-bit convention: `response = command | 0x80`. Generic | MacFailed | `0x04` | MAC verification failed | | UnknownCmd | `0x05` | Unknown sub-command | | EncryptFailed | `0x06` | Encryption failed | -| TxBusy | `0x07` | Transmit busy | +| TxBusy | `0x07` | Radio TX busy, or host output queue full | ### Unsolicited Events The TNC sends these SetHardware frames without a preceding request: -**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. +**TxDone (0xF8)**: Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped. -**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame. +**RxMeta (0xF9)**: Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame. ## Data Formats diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index eeab1501..7dcfc8d7 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -22,6 +22,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _getStatsCallback = nullptr; _config = {0, 0, 0, 0, 0}; _signal_report_enabled = true; + resetOutputQueue(); } void KissModem::begin() { @@ -30,37 +31,168 @@ void KissModem::begin() { _rx_active = false; _has_pending_tx = false; _tx_state = TX_IDLE; + resetOutputQueue(); } -void KissModem::writeByte(uint8_t b) { - if (b == KISS_FEND) { - _serial.write(KISS_FESC); - _serial.write(KISS_TFEND); - } else if (b == KISS_FESC) { - _serial.write(KISS_FESC); - _serial.write(KISS_TFESC); - } else { - _serial.write(b); +void KissModem::resetOutputQueue() { + _tx_frame_head = 0; + _tx_frame_tail = 0; + _tx_frame_count = 0; + _tx_busy_error_pending = false; + _tx_done_pending = false; + _tx_done_result = 0; +} + +void KissModem::popTxFrame() { + _tx_frame_head = (uint8_t)((_tx_frame_head + 1) % KISS_TX_FRAME_QUEUE_DEPTH); + _tx_frame_count--; +} + +uint16_t KissModem::appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b) { + if (b == KISS_FEND || b == KISS_FESC) { + if (idx + 2 > max_len) { + return 0; + } + dest[idx++] = KISS_FESC; + dest[idx++] = (b == KISS_FEND) ? KISS_TFEND : KISS_TFESC; + return idx; } + if (idx + 1 > max_len) { + return 0; + } + dest[idx++] = b; + return idx; } -void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(type); +uint16_t KissModem::encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len) { + if (max_len < KISS_FRAME_BOUNDARY_BYTES) { + return 0; + } + + uint16_t idx = 0; + dest[idx++] = KISS_FEND; + + idx = appendEscapedByte(dest, idx, max_len, type); + if (idx == 0) { + return 0; + } + for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); + idx = appendEscapedByte(dest, idx, max_len, data[i]); + if (idx == 0) { + return 0; + } + } + + if (idx + 1 > max_len) { + return 0; } - _serial.write(KISS_FEND); + dest[idx++] = KISS_FEND; + return idx; } -void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(KISS_CMD_SETHARDWARE); - writeByte(sub_cmd); - for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); +bool KissModem::tryFlushFrames() { + while (_tx_frame_count > 0) { + const uint8_t idx = _tx_frame_head; + const uint16_t frame_len = _tx_frame_len[idx]; + uint16_t written_len = _tx_frame_written[idx]; + + if (written_len >= frame_len) { + popTxFrame(); + continue; + } + + const int available = _serial.availableForWrite(); + if (available <= 0) { + return false; + } + + const uint16_t remaining = frame_len - written_len; + const uint16_t chunk_len = (available < (int)remaining) ? (uint16_t)available : remaining; + if (chunk_len == 0) { + return false; + } + + size_t chunk_written = _serial.write(_tx_frame_buf[idx] + written_len, chunk_len); + if (chunk_written == 0) { + return false; + } + + written_len += (uint16_t)chunk_written; + _tx_frame_written[idx] = written_len; + + if (written_len < frame_len) { + return false; + } + + popTxFrame(); } - _serial.write(KISS_FEND); + return true; +} + +bool KissModem::queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error) { + if (_tx_frame_count >= KISS_TX_FRAME_QUEUE_DEPTH && !tryFlushFrames()) { + if (mark_busy_error) { + _tx_busy_error_pending = true; + } + return false; + } + const uint8_t idx = _tx_frame_tail; + uint16_t frame_len = encodeFrame(type, data, len, _tx_frame_buf[idx], sizeof(_tx_frame_buf[idx])); + if (frame_len == 0) { + return false; + } + + _tx_frame_len[idx] = frame_len; + _tx_frame_written[idx] = 0; + _tx_frame_tail = (uint8_t)((_tx_frame_tail + 1) % KISS_TX_FRAME_QUEUE_DEPTH); + _tx_frame_count++; + tryFlushFrames(); + return true; +} + +bool KissModem::queuePendingBusyError() { + if (!_tx_busy_error_pending) { + return true; + } + const uint8_t err = HW_ERR_TX_BUSY; + if (!queueHardwareFrame(HW_RESP_ERROR, &err, 1, false)) { + return false; + } + _tx_busy_error_pending = false; + return true; +} + +bool KissModem::queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error) { + if (len > KISS_MAX_FRAME_SIZE) { + return false; + } + _tx_hw_payload[0] = sub_cmd; + if (len > 0) { + memcpy(_tx_hw_payload + 1, data, len); + } + return queueFrame(KISS_CMD_SETHARDWARE, _tx_hw_payload, len + 1, mark_busy_error); +} + +bool KissModem::queuePendingTxDone() { + if (!_tx_done_pending) { + return true; + } + if (!queueHardwareFrame(HW_RESP_TX_DONE, &_tx_done_result, 1, false)) { + return false; + } + _tx_done_pending = false; + return true; +} + +void KissModem::setTxDonePending(uint8_t result) { + _tx_done_result = result; + _tx_done_pending = true; + _tx_state = TX_DONE_PENDING; +} + +bool KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { + return queueHardwareFrame(sub_cmd, data, len, true); } void KissModem::writeHardwareError(uint8_t error_code) { @@ -68,6 +200,8 @@ void KissModem::writeHardwareError(uint8_t error_code) { } void KissModem::loop() { + tryFlushFrames(); + while (_serial.available()) { uint8_t b = _serial.read(); @@ -106,6 +240,8 @@ void KissModem::loop() { } processTx(); + tryFlushFrames(); + queuePendingBusyError(); } void KissModem::processFrame() { @@ -295,10 +431,7 @@ void KissModem::processTx() { _tx_timer = millis(); _tx_state = TX_SENDING; } else { - uint8_t result = 0x00; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); - _has_pending_tx = false; - _tx_state = TX_IDLE; + setTxDonePending(0x00); } } break; @@ -306,14 +439,15 @@ void KissModem::processTx() { case TX_SENDING: if (_radio.isSendComplete()) { _radio.onSendFinished(); - uint8_t result = 0x01; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); - _has_pending_tx = false; - _tx_state = TX_IDLE; + setTxDonePending(0x01); } else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) { _radio.onSendFinished(); - uint8_t result = 0x00; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); + setTxDonePending(0x00); + } + break; + + case TX_DONE_PENDING: + if (queuePendingTxDone()) { _has_pending_tx = false; _tx_state = TX_IDLE; } @@ -322,8 +456,7 @@ void KissModem::processTx() { } void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { - writeFrame(KISS_CMD_DATA, packet, len); - if (_signal_report_enabled) { + if (queueFrame(KISS_CMD_DATA, packet, len) && _signal_report_enabled) { uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; writeHardwareFrame(HW_RESP_RX_META, meta, 2); } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bbe99d6d..a23e459b 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -13,6 +13,14 @@ #define KISS_MAX_FRAME_SIZE 512 #define KISS_MAX_PACKET_SIZE 255 +#define KISS_FRAME_BOUNDARY_BYTES 2 +#define KISS_TYPE_BYTES 1 +#define KISS_HW_SUBCMD_BYTES 1 +#define KISS_MAX_ESCAPABLE_BYTES (KISS_MAX_FRAME_SIZE + KISS_TYPE_BYTES + KISS_HW_SUBCMD_BYTES) +#define KISS_MAX_ESCAPED_PAYLOAD_SIZE (2 * KISS_MAX_ESCAPABLE_BYTES) +#define KISS_MAX_ENCODED_FRAME_SIZE (KISS_FRAME_BOUNDARY_BYTES + KISS_MAX_ESCAPED_PAYLOAD_SIZE) +#define KISS_TX_FRAME_QUEUE_DEPTH 2 +#define KISS_HW_MAX_PAYLOAD_SIZE (KISS_MAX_FRAME_SIZE + KISS_HW_SUBCMD_BYTES) #define KISS_CMD_DATA 0x00 #define KISS_CMD_TXDELAY 0x01 @@ -94,7 +102,8 @@ enum TxState { TX_WAIT_CLEAR, TX_SLOT_WAIT, TX_DELAY, - TX_SENDING + TX_SENDING, + TX_DONE_PENDING }; class KissModem { @@ -130,10 +139,28 @@ class KissModem { RadioConfig _config; bool _signal_report_enabled; - - void writeByte(uint8_t b); - void writeFrame(uint8_t type, const uint8_t* data, uint16_t len); - void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); + uint8_t _tx_frame_buf[KISS_TX_FRAME_QUEUE_DEPTH][KISS_MAX_ENCODED_FRAME_SIZE]; + uint16_t _tx_frame_len[KISS_TX_FRAME_QUEUE_DEPTH]; + uint16_t _tx_frame_written[KISS_TX_FRAME_QUEUE_DEPTH]; + uint8_t _tx_frame_head; + uint8_t _tx_frame_tail; + uint8_t _tx_frame_count; + bool _tx_busy_error_pending; + bool _tx_done_pending; + uint8_t _tx_done_result; + uint8_t _tx_hw_payload[KISS_HW_MAX_PAYLOAD_SIZE]; + + static uint16_t appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b); + static uint16_t encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len); + void resetOutputQueue(); + void popTxFrame(); + bool tryFlushFrames(); + bool queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error = true); + bool queuePendingBusyError(); + bool queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error); + bool queuePendingTxDone(); + void setTxDonePending(uint8_t result); + bool writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); void writeHardwareError(uint8_t error_code); void processFrame(); void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len); @@ -182,4 +209,5 @@ public: bool isTxBusy() const { return _tx_state != TX_IDLE; } /** True only when radio is actually transmitting; use to skip recvRaw in main loop. */ bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; } + bool isHostOutputBackedUp() const { return _tx_frame_count > 0 || _tx_busy_error_pending || _tx_done_pending; } }; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 7fbcaed1..95a06ee6 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -20,6 +20,8 @@ #define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 #define AGC_RESET_INTERVAL_MS 30000 +#define USB_TX_TIMEOUT_MS 50 +#define USB_TX_BUFFER_SIZE 1024 StdRNG rng; mesh::LocalIdentity identity; @@ -111,6 +113,12 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); +#if defined(ESP32) + Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); +#if ARDUINO_USB_MODE + Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); +#endif +#endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif @@ -126,7 +134,7 @@ void setup() { void loop() { modem->loop(); - if (!modem->isActuallyTransmitting()) { + if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) { if (!modem->isTxBusy()) { if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { radio_driver.resetAGC(); diff --git a/platformio.ini b/platformio.ini index f3ada133..2de71a37 100644 --- a/platformio.ini +++ b/platformio.ini @@ -169,3 +169,18 @@ build_src_filter = +<../src/Packet.cpp> lib_deps = google/googletest @ 1.17.0 + +[env:native_kiss_modem] +platform = native +test_framework = googletest +build_flags = -std=c++17 + -I test/mocks + -I src + -I examples/kiss_modem +test_build_src = yes +test_filter = test_kiss_modem +build_src_filter = + -<*> + +<../examples/kiss_modem/KissModem.cpp> +lib_deps = + google/googletest @ 1.17.0 diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 00000000..77499fe4 --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "Stream.h" + +inline uint32_t g_mock_millis = 0; + +using std::isnan; + +inline uint32_t millis() { + return g_mock_millis; +} + +inline void delay(uint32_t ms) { + g_mock_millis += ms; +} diff --git a/test/mocks/CayenneLPP.h b/test/mocks/CayenneLPP.h new file mode 100644 index 00000000..9d51c0fc --- /dev/null +++ b/test/mocks/CayenneLPP.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class CayenneLPP { +public: + explicit CayenneLPP(size_t) {} + const uint8_t* getBuffer() const { return nullptr; } + uint16_t getSize() const { return 0; } +}; diff --git a/test/mocks/Identity.h b/test/mocks/Identity.h new file mode 100644 index 00000000..5c3d25e9 --- /dev/null +++ b/test/mocks/Identity.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "Utils.h" + +namespace mesh { + +class Identity { +public: + uint8_t pub_key[PUB_KEY_SIZE]; + + Identity() { + std::memset(pub_key, 0, sizeof(pub_key)); + } + + explicit Identity(const uint8_t* src) { + std::memcpy(pub_key, src, PUB_KEY_SIZE); + } + + bool verify(const uint8_t*, const uint8_t*, int) const { + return true; + } +}; + +class LocalIdentity : public Identity { +public: + LocalIdentity() : Identity() {} + + void sign(uint8_t* sig, const uint8_t*, int) const { + std::memset(sig, 0x5A, SIGNATURE_SIZE); + } + + void calcSharedSecret(uint8_t* secret, const uint8_t*) const { + std::memset(secret, 0x11, PUB_KEY_SIZE); + } +}; + +} diff --git a/test/mocks/Mesh.h b/test/mocks/Mesh.h new file mode 100644 index 00000000..b6c263c1 --- /dev/null +++ b/test/mocks/Mesh.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace mesh { + +class Radio { +public: + virtual ~Radio() = default; + virtual bool isReceiving() { return false; } + virtual uint32_t getEstAirtimeFor(uint16_t) { return 10; } + virtual bool startSendRaw(const uint8_t*, uint16_t) { return true; } + virtual bool isSendComplete() { return true; } + virtual void onSendFinished() {} + virtual int16_t getNoiseFloor() { return -120; } +}; + +class MainBoard { +public: + virtual ~MainBoard() = default; + virtual uint16_t getBattMilliVolts() { return 4200; } + virtual float getMCUTemperature() { return 25.0f; } + virtual const char* getManufacturerName() { return "mock-board"; } + virtual void reboot() {} +}; + +} diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a3029..cf33785d 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,23 @@ #pragma once -// Mock Stream class for native testing -// Provides minimal interface needed by Utils.h +#include +#include class Stream { public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual ~Stream() = default; + virtual size_t write(uint8_t) { return 1; } + virtual size_t write(const uint8_t* buffer, size_t size) { + size_t total = 0; + for (size_t i = 0; i < size; i++) { + total += write(buffer[i]); + } + return total; + } + virtual int available() { return 0; } + virtual int availableForWrite() { return 0; } + virtual int read() { return -1; } + virtual void flush() {} + virtual void print(char) {} + virtual void print(const char*) {} }; diff --git a/test/mocks/Utils.h b/test/mocks/Utils.h new file mode 100644 index 00000000..9bb6b060 --- /dev/null +++ b/test/mocks/Utils.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#define PUB_KEY_SIZE 32 +#define PRV_KEY_SIZE 64 +#define SIGNATURE_SIZE 64 +#define CIPHER_MAC_SIZE 16 + +namespace mesh { + +class RNG { +public: + virtual ~RNG() = default; + virtual void random(uint8_t* dest, size_t sz) = 0; +}; + +class Utils { +public: + static void sha256(uint8_t* hash, size_t hash_len, const uint8_t*, int) { + std::memset(hash, 0, hash_len); + } + + static int encryptThenMAC(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) { + int out_len = src_len + CIPHER_MAC_SIZE; + std::memset(dest, 0xAA, CIPHER_MAC_SIZE); + std::memcpy(dest + CIPHER_MAC_SIZE, src, src_len); + return out_len; + } + + static int MACThenDecrypt(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) { + if (src_len < CIPHER_MAC_SIZE) { + return 0; + } + int out_len = src_len - CIPHER_MAC_SIZE; + std::memcpy(dest, src + CIPHER_MAC_SIZE, out_len); + return out_len; + } +}; + +} diff --git a/test/mocks/helpers/SensorManager.h b/test/mocks/helpers/SensorManager.h new file mode 100644 index 00000000..d1a41cb5 --- /dev/null +++ b/test/mocks/helpers/SensorManager.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include "CayenneLPP.h" + +class SensorManager { +public: + virtual ~SensorManager() = default; + virtual bool querySensors(uint8_t, CayenneLPP&) { return false; } +}; diff --git a/test/test_kiss_modem/test_tx_backpressure.cpp b/test/test_kiss_modem/test_tx_backpressure.cpp new file mode 100644 index 00000000..39a5e3b7 --- /dev/null +++ b/test/test_kiss_modem/test_tx_backpressure.cpp @@ -0,0 +1,294 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "KissModem.h" + +static constexpr int TEST_TX_AVAILABLE_BYTES = 4096; +static constexpr size_t TEST_DEFAULT_MAX_WRITE_CHUNK = SIZE_MAX; +static constexpr size_t TEST_PARTIAL_WRITE_CHUNK = 2; +static constexpr int TEST_PARTIAL_WRITE_FLUSH_LOOPS = 3; +static constexpr uint8_t TEST_SNR = 8; +static constexpr uint8_t TEST_RSSI = 200; + +class BlockingStream : public Stream { +public: + void pushRx(const std::vector& bytes) { + std::lock_guard lock(_mutex); + for (uint8_t b : bytes) { + _rx.push(b); + } + } + + void setBlockWrites(bool blocked) { + { + std::lock_guard lock(_mutex); + _block_writes = blocked; + } + _cv.notify_all(); + } + + bool isWriteBlocked() const { + return _entered_block.load(); + } + + size_t writesCount() const { + std::lock_guard lock(_mutex); + return _writes.size(); + } + + std::vector writesSnapshot() const { + std::lock_guard lock(_mutex); + return _writes; + } + + int availableForWrite() override { + std::lock_guard lock(_mutex); + return _block_writes ? 0 : TEST_TX_AVAILABLE_BYTES; + } + + void setMaxWriteChunk(size_t chunk) { + std::lock_guard lock(_mutex); + _max_write_chunk = chunk; + } + + size_t write(const uint8_t* buffer, size_t size) override { + std::unique_lock lock(_mutex); + while (_block_writes) { + _entered_block.store(true); + _cv.wait(lock); + } + const size_t chunk = (size < _max_write_chunk) ? size : _max_write_chunk; + for (size_t i = 0; i < chunk; i++) { + _writes.push_back(buffer[i]); + } + return chunk; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + int available() override { + std::lock_guard lock(_mutex); + return static_cast(_rx.size()); + } + + int read() override { + std::lock_guard lock(_mutex); + if (_rx.empty()) { + return -1; + } + int b = _rx.front(); + _rx.pop(); + return b; + } + +private: + mutable std::mutex _mutex; + std::condition_variable _cv; + std::queue _rx; + std::vector _writes; + bool _block_writes = false; + std::atomic _entered_block = false; + size_t _max_write_chunk = TEST_DEFAULT_MAX_WRITE_CHUNK; +}; + +class FakeRNG : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + for (size_t i = 0; i < sz; i++) { + dest[i] = 0; + } + } +}; + +class FakeRadio : public mesh::Radio { +public: + bool isReceiving() override { return false; } + uint32_t getEstAirtimeFor(uint16_t) override { return 10; } + bool startSendRaw(const uint8_t*, uint16_t) override { + _start_send_count++; + return _start_send_result; + } + bool isSendComplete() override { return _send_complete; } + void onSendFinished() override { _send_finished_count++; } + int16_t getNoiseFloor() override { return -120; } + + void setStartSendResult(bool result) { _start_send_result = result; } + void setSendComplete(bool complete) { _send_complete = complete; } + int startSendCount() const { return _start_send_count; } + int sendFinishedCount() const { return _send_finished_count; } + +private: + bool _start_send_result = true; + bool _send_complete = true; + int _start_send_count = 0; + int _send_finished_count = 0; +}; + +class FakeBoard : public mesh::MainBoard { +public: + uint16_t getBattMilliVolts() override { return 4200; } + float getMCUTemperature() override { return 24.0f; } + const char* getManufacturerName() override { return "test-board"; } + void reboot() override {} +}; + +class FakeSensors : public SensorManager { +public: + bool querySensors(uint8_t, CayenneLPP&) override { return false; } +}; + +class KissModemFixture : public ::testing::Test { +protected: + BlockingStream serial; + mesh::LocalIdentity identity; + FakeRNG rng; + FakeRadio radio; + FakeBoard board; + FakeSensors sensors; + KissModem modem; + + KissModemFixture() + : modem(serial, identity, rng, radio, board, sensors) { + modem.begin(); + } + + static std::vector dataFrame(const std::vector& packet) { + std::vector frame = {KISS_FEND, KISS_CMD_DATA}; + frame.insert(frame.end(), packet.begin(), packet.end()); + frame.push_back(KISS_FEND); + return frame; + } + + void advanceToTxSending() { + modem.loop(); + modem.loop(); + delay((uint32_t)KISS_DEFAULT_TXDELAY * 10); + modem.loop(); + } +}; + +TEST_F(KissModemFixture, PingResponseShouldNotStallLoopUnderTxBackpressure) { + serial.setBlockWrites(true); + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + + auto future = std::async(std::launch::async, [this]() { + modem.loop(); + }); + + auto status = future.wait_for(std::chrono::milliseconds(100)); + EXPECT_EQ(status, std::future_status::ready) << "KissModem::loop blocked in serial write under TX backpressure"; + EXPECT_FALSE(serial.isWriteBlocked()) << "KissModem entered blocking write path"; + + serial.setBlockWrites(false); + future.wait(); + modem.loop(); + EXPECT_GT(serial.writesCount(), 0U) << "KissModem did not flush queued response after backpressure cleared"; +} + +TEST_F(KissModemFixture, PingResponseKeepsStandardKissFraming) { + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + modem.loop(); + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, PingResponseKeepsFramingWithPartialBulkWrites) { + serial.setMaxWriteChunk(TEST_PARTIAL_WRITE_CHUNK); + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + for (int i = 0; i < TEST_PARTIAL_WRITE_FLUSH_LOOPS; i++) { + modem.loop(); + } + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, PacketAndMetaAreQueuedTogetherUnderBackpressure) { + static constexpr uint8_t TEST_PACKET[] = {0x01, 0x02, 0x03}; + + serial.setBlockWrites(true); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET)); + serial.setBlockWrites(false); + modem.loop(); + modem.loop(); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, TEST_PACKET[0], TEST_PACKET[1], TEST_PACKET[2], KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, RadioTxCompletionAdvancesWhileHostOutputIsBackedUp) { + serial.pushRx(dataFrame({0x42})); + advanceToTxSending(); + ASSERT_EQ(radio.startSendCount(), 1); + + serial.setBlockWrites(true); + modem.loop(); + EXPECT_EQ(radio.sendFinishedCount(), 1); + EXPECT_TRUE(modem.isTxBusy()); + + serial.setBlockWrites(false); + modem.loop(); + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_TX_DONE, 0x01, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); + EXPECT_FALSE(modem.isTxBusy()); +} + +TEST_F(KissModemFixture, QueueFullReportsBusyWithoutDroppingQueuedFrames) { + static constexpr uint8_t TEST_PACKET_ONE[] = {0x11, 0x12}; + static constexpr uint8_t TEST_PACKET_TWO[] = {0x21, 0x22}; + + serial.setBlockWrites(true); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_ONE, sizeof(TEST_PACKET_ONE)); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_TWO, sizeof(TEST_PACKET_TWO)); + serial.setBlockWrites(false); + modem.loop(); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, TEST_PACKET_ONE[0], TEST_PACKET_ONE[1], KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_ERROR, HW_ERR_TX_BUSY, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, QueuedEncoderEscapesKissSpecialBytes) { + static constexpr uint8_t TEST_PACKET[] = {KISS_FEND, KISS_FESC, 0x01}; + + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET)); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC, 0x01, KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, MaxPacketWorstCaseEscapingFitsQueuedFrame) { + std::vector packet(KISS_MAX_PACKET_SIZE, KISS_FEND); + std::vector expected = {KISS_FEND, KISS_CMD_DATA}; + for (size_t i = 0; i < packet.size(); i++) { + expected.push_back(KISS_FESC); + expected.push_back(KISS_TFEND); + } + expected.push_back(KISS_FEND); + expected.insert(expected.end(), {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}); + + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, packet.data(), packet.size()); + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index fabf3827..cb76beb3 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -432,5 +432,10 @@ lib_deps = [env:heltec_v4_kiss_modem] extends = Heltec_lora32_v4 +build_unflags = + -DARDUINO_USB_MODE=0 +build_flags = + ${Heltec_lora32_v4.build_flags} + -DARDUINO_USB_MODE=1 build_src_filter = ${Heltec_lora32_v4.build_src_filter} +<../examples/kiss_modem/> diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 6432b523..508ffe4b 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -241,5 +241,10 @@ lib_deps = [env:Station_G2_kiss_modem] extends = Station_G2 +build_unflags = + -DARDUINO_USB_MODE=0 +build_flags = + ${Station_G2.build_flags} + -DARDUINO_USB_MODE=1 build_src_filter = ${Station_G2.build_src_filter} +<../examples/kiss_modem/> From 39ff5b879acadb124ee535fcf2386a83cfff08e0 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Tue, 23 Jun 2026 15:15:13 +0200 Subject: [PATCH 31/48] fix: run KISS modem unit tests in CI via native_kiss_modem env The new test_kiss_modem suite needs KissModem headers and sources that only native_kiss_modem provides. Scope native to test_utils and run both envs in the unit test workflow. --- .github/workflows/run-unit-tests.yml | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index e3af3aaf..5d48f4c6 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -20,7 +20,7 @@ jobs: uses: ./.github/actions/setup-build-environment - name: Run Unit Tests - run: pio test -e native -vv + run: pio test -e native -e native_kiss_modem -vv - name: Upload Test Results # Upload test results even if the test step failed. diff --git a/platformio.ini b/platformio.ini index 2de71a37..cdd67d27 100644 --- a/platformio.ini +++ b/platformio.ini @@ -163,6 +163,7 @@ build_flags = -std=c++17 -I src -I test/mocks test_build_src = yes +test_filter = test_utils build_src_filter = -<*> +<../src/Utils.cpp> From 3b69e7873bd76b52c3d3469d7496ff64a9048db5 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 26 Jun 2026 00:11:33 +0200 Subject: [PATCH 32/48] fix: guard HWCDC serial config behind ARDUINO_USB_MODE setTxTimeoutMs() and setTxBufferSize() are HWCDC-only APIs; calling them on HardwareSerial (UART-bridge boards like T-Beam) fails to compile. Co-Authored-By: Adam Gessaman --- examples/kiss_modem/main.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 95a06ee6..5836a694 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,11 +113,9 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) +#if defined(ESP32) && ARDUINO_USB_MODE Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); -#if ARDUINO_USB_MODE Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); -#endif #endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif From b28bf00348c1cf555a88a5988bd0e889958a2517 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 19 Jul 2026 22:06:36 +1000 Subject: [PATCH 33/48] fix: setMaxPacketMillis() should return void --- src/helpers/radiolib/CustomLR1110.h | 2 +- src/helpers/radiolib/CustomSX1262.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index db0f2af3..87a7005f 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -72,7 +72,7 @@ class CustomLR1110 : public LR1110 { return false; } - PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + void setMaxPacketMillis(PacketMillis maxPacketMillis) { MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); _preambleMillis = maxPacketMillis.preambleMillis; _maxPayloadMillis = maxPacketMillis.payloadMillis; diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 284b79c0..18932488 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -137,7 +137,7 @@ class CustomSX1262 : public SX1262 { return false; } - PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + void setMaxPacketMillis(PacketMillis maxPacketMillis) { MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); _preambleMillis = maxPacketMillis.preambleMillis; _maxPayloadMillis = maxPacketMillis.payloadMillis; From aee53a8c3e3176eb9e26fd781774f198d4bb6f30 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:59:12 +1000 Subject: [PATCH 34/48] NRF52Board - ShutdownPeripherals() housekeeping Improvements: - Add condition to only turn off display if on (avoids wedging the shutdown on some displays) - Reset LoRa radio before calling radio_driver.powerOff() to ensure it is in a known good state (and will accept the command) - Ensure SPI is started so LoRa radio commands can be sent (some shutdowns can happen before this) - Add gate for P_LORA_NSS pin defined before writing to the pin --- src/helpers/NRF52Board.cpp | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 5fb1e55e..23f7cafc 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -314,14 +314,38 @@ float NRF52Board::getMCUTemperature() { void NRF52Board::shutdownPeripherals() { // Power off the display if any #ifdef DISPLAY_CLASS - display.turnOff(); + if (display.isOn()) { + display.turnOff(); + } #endif - + // Prep LoRa radio for power down + #ifdef P_LORA_RESET + digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us) + delayMicroseconds(200); + digitalWrite(P_LORA_RESET, HIGH); + #endif + #if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI) + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio + #endif + #ifdef P_LORA_BUSY + pinMode(P_LORA_BUSY, INPUT); + uint32_t started_at = millis(); + while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready + #endif + #ifdef P_LORA_NSS + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + #endif // Power off LoRa radio_driver.powerOff(); // Keep LoRa inactive during deepsleep - digitalWrite(P_LORA_NSS, HIGH); + #ifdef P_LORA_NSS + digitalWrite(P_LORA_NSS, HIGH); + #endif // Power off GPS if any if(sensors.getLocationProvider() != NULL) { From cc221330ba1dbac71d2214dbcdb3699d74b02e83 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sun, 19 Jul 2026 10:17:08 -0700 Subject: [PATCH 35/48] Fix nRF52 builds: guard SerialEthernetInterface with ETHERNET_ENABLED SerialEthernetInterface.cpp is compiled for every nRF52 target because PlatformIO builds all .cpp files under src/. It includes SerialEthernetInterface.h, which unconditionally pulls in -- a library only present in the RAK4631 Ethernet env's lib_deps. As a result any other nRF52 board (e.g. Heltec T114) fails to build with 'RAK13800_W5100S.h: No such file or directory'. Wrap the contents of both files in '#ifdef ETHERNET_ENABLED' so they compile to empty translation units on non-Ethernet builds. RAK4631 Ethernet envs define ETHERNET_ENABLED and are unaffected. Fixes #2985 --- src/helpers/nrf52/SerialEthernetInterface.cpp | 4 ++++ src/helpers/nrf52/SerialEthernetInterface.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 70891023..36a998a4 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -1,3 +1,5 @@ +#ifdef ETHERNET_ENABLED + #include "SerialEthernetInterface.h" #include "EthernetMac.h" #include @@ -262,3 +264,5 @@ bool SerialEthernetInterface::isConnected() const { void SerialEthernetInterface::loop() { Ethernet.maintain(); } + +#endif // ETHERNET_ENABLED diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 95ce8a52..b8b4e94b 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -1,5 +1,7 @@ #pragma once +#ifdef ETHERNET_ENABLED + #include "helpers/BaseSerialInterface.h" #include #include @@ -76,3 +78,5 @@ class SerialEthernetInterface : public BaseSerialInterface { #define ETHERNET_DEBUG_PRINTLN(...) {} #define ETHERNET_DEBUG_PRINT_IP(...) {} #endif + +#endif // ETHERNET_ENABLED From ae48072ff059c53c94803cd9e6583c763a13a703 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 19:40:49 +1000 Subject: [PATCH 36/48] SX1262: add setter methods for preambleMillis and payloadMillis --- src/helpers/radiolib/CustomSX1262.h | 11 +++++++---- src/helpers/radiolib/CustomSX1262Wrapper.h | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 18932488..103f5167 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -137,10 +137,13 @@ class CustomSX1262 : public SX1262 { return false; } - void setMaxPacketMillis(PacketMillis maxPacketMillis) { - MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); - _preambleMillis = maxPacketMillis.preambleMillis; - _maxPayloadMillis = maxPacketMillis.payloadMillis; + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 45af4cb4..bfea50ec 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -18,7 +18,9 @@ public: ((CustomSX1262 *)_radio)->setBandwidth(bw); ((CustomSX1262 *)_radio)->setCodingRate(cr); updatePreamble(sf); - ((CustomSX1262 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSX1262 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From 121b72712b361eaa5685f8c90144e5fccad5690d Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 19:42:22 +1000 Subject: [PATCH 37/48] LR1110: add setter methods for preambleMillis and payloadMillis --- src/helpers/radiolib/CustomLR1110.h | 13 ++++++++----- src/helpers/radiolib/CustomLR1110Wrapper.h | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 87a7005f..0470e48f 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -71,11 +71,14 @@ class CustomLR1110 : public LR1110 { _activityAt = 0; _headerSeen = false; return false; } - - void setMaxPacketMillis(PacketMillis maxPacketMillis) { - MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); - _preambleMillis = maxPacketMillis.preambleMillis; - _maxPayloadMillis = maxPacketMillis.payloadMillis; + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } uint8_t getSpreadingFactor() const { return spreadingFactor; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 981faf09..44230c61 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -14,8 +14,9 @@ public: ((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setCodingRate(cr); updatePreamble(sf); - ((CustomLR1110 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); - + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR1110 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } From 91e65af9dd8514627b320f19a442100bafb868d7 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 20:28:38 +1000 Subject: [PATCH 38/48] fix: heltec t1: add missing bodmer TFT_eSPI library --- variants/heltec_t1/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index d5c3caf1..eaa8af37 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 debug_tool = jlink upload_protocol = nrfutil From ab5c45f51612c268b7fa4dca423d5b3dab88df8e Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 21:05:12 +1000 Subject: [PATCH 39/48] fix: clear the correct IRQ flag for timeout on preamble detected --- src/helpers/radiolib/CustomSX1262.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 103f5167..15efed5c 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -125,7 +125,7 @@ class CustomSX1262 : public SX1262 { if (preamble) { if (_activityAt == 0) _activityAt = now; if (now - _activityAt > _preambleMillis) { - clearIrqStatus(RADIOLIB_IRQ_PREAMBLE_DETECTED); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); _activityAt = 0; MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); From d2bbe4d8d3629f1d44b27b8af0264799ce54c63e Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:43:25 +1000 Subject: [PATCH 40/48] Fix Xiao nRF52840 repeater compile failure Compile fails due to user_btn not declared in board target. Fixes: - Add MomentaryButton declaration for user_btn in variants/xiao_nrf52/target.cpp - Add momentary button helper include in variants/xiao_nrf52/target.h - Add extern linkage for MomentaryButton -> user_btn in variants/xiao_nrf52/target.h --- variants/xiao_nrf52/XiaoNrf52Board.h | 2 +- variants/xiao_nrf52/target.cpp | 1 + variants/xiao_nrf52/target.h | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index b2638a44..e71e7f65 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -47,7 +47,7 @@ public: #ifdef PIN_USER_BTN // configure button press to wake up when in powered off state - nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); #endif NRF52Board::powerOff(); diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index ab6fe279..def1dbbe 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -4,6 +4,7 @@ #ifdef DISPLAY_CLASS DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); #endif XiaoNrf52Board board; diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index bb3d2a81..20093904 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -11,7 +11,9 @@ #ifdef DISPLAY_CLASS #include + #include extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; #endif extern XiaoNrf52Board board; From 3885c67c8eaf46ce66e28252338df783ca178a95 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 41/48] fix: synchronize BLE receive queue --- src/helpers/BoundedQueue.h | 39 +++++++++++++ src/helpers/esp32/SerialBLEInterface.cpp | 41 ++++++++----- src/helpers/esp32/SerialBLEInterface.h | 10 ++-- .../test_bounded_queue/test_bounded_queue.cpp | 58 +++++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/helpers/BoundedQueue.h create mode 100644 test/test_bounded_queue/test_bounded_queue.cpp diff --git a/src/helpers/BoundedQueue.h b/src/helpers/BoundedQueue.h new file mode 100644 index 00000000..07ecb4ff --- /dev/null +++ b/src/helpers/BoundedQueue.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace mesh { + +template +class BoundedQueue { + static_assert(Capacity > 0, "BoundedQueue capacity must be greater than zero"); + + T _items[Capacity]; + size_t _head = 0; + size_t _size = 0; + +public: + bool push(const T& item) { + if (_size == Capacity) return false; + _items[(_head + _size) % Capacity] = item; + _size++; + return true; + } + + bool pop(T& item) { + if (_size == 0) return false; + item = _items[_head]; + _head = (_head + 1) % Capacity; + _size--; + return true; + } + + void clear() { + _head = 0; + _size = 0; + } + + size_t size() const { return _size; } +}; + +} // namespace mesh diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e..9c81dc7b 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -118,17 +118,30 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat if (len > MAX_FRAME_SIZE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len); - } else if (recv_queue_len >= FRAME_QUEUE_SIZE) { - BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } else { - recv_queue[recv_queue_len].len = len; - memcpy(recv_queue[recv_queue_len].buf, rxValue, len); - recv_queue_len++; + Frame frame = {}; + frame.len = len; + memcpy(frame.buf, rxValue, len); + + portENTER_CRITICAL(&recv_queue_mux); + bool queued = recv_queue.push(frame); + portEXIT_CRITICAL(&recv_queue_mux); + + if (!queued) { + BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); + } } } // ---------- public methods +void SerialBLEInterface::clearBuffers() { + portENTER_CRITICAL(&recv_queue_mux); + recv_queue.clear(); + portEXIT_CRITICAL(&recv_queue_mux); + send_queue_len = 0; +} + void SerialBLEInterface::enable() { if (_isEnabled) return; @@ -202,17 +215,15 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } } - if (recv_queue_len > 0) { // check recv queue - size_t len = recv_queue[0].len; // take from top of queue - memcpy(dest, recv_queue[0].buf, len); - - BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); + Frame frame = {}; + portENTER_CRITICAL(&recv_queue_mux); + bool received = recv_queue.pop(frame); + portEXIT_CRITICAL(&recv_queue_mux); - recv_queue_len--; - for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue - recv_queue[i] = recv_queue[i + 1]; - } - return len; + if (received) { + memcpy(dest, frame.buf, frame.len); + BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); + return frame.len; } if (pServer->getConnectedCount() == 0) deviceConnected = false; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd..e2d600a0 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -1,10 +1,12 @@ #pragma once #include "../BaseSerialInterface.h" +#include "../BoundedQueue.h" #include #include #include #include +#include class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; @@ -24,12 +26,12 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE }; #define FRAME_QUEUE_SIZE 4 - int recv_queue_len; - Frame recv_queue[FRAME_QUEUE_SIZE]; + portMUX_TYPE recv_queue_mux = portMUX_INITIALIZER_UNLOCKED; + mesh::BoundedQueue recv_queue; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; - void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + void clearBuffers(); protected: // BLESecurityCallbacks methods @@ -58,7 +60,7 @@ public: _isEnabled = false; _last_write = 0; last_conn_id = 0; - send_queue_len = recv_queue_len = 0; + send_queue_len = 0; } /** diff --git a/test/test_bounded_queue/test_bounded_queue.cpp b/test/test_bounded_queue/test_bounded_queue.cpp new file mode 100644 index 00000000..3e2e2de7 --- /dev/null +++ b/test/test_bounded_queue/test_bounded_queue.cpp @@ -0,0 +1,58 @@ +#include + +#include + +TEST(BoundedQueue, PreservesOrderAcrossWraparound) { + mesh::BoundedQueue queue; + int value = 0; + + for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(i)); + EXPECT_FALSE(queue.push(4)); + + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(0, value); + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(1, value); + + EXPECT_TRUE(queue.push(4)); + EXPECT_TRUE(queue.push(5)); + + for (int expected = 2; expected < 6; expected++) { + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(expected, value); + } + EXPECT_FALSE(queue.pop(value)); +} + +TEST(BoundedQueue, SurvivesRepeatedFillAndDrainCycles) { + mesh::BoundedQueue queue; + int value = 0; + + for (int cycle = 0; cycle < 1000; cycle++) { + for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(cycle * 4 + i)); + for (int i = 0; i < 4; i++) { + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(cycle * 4 + i, value); + } + } +} + +TEST(BoundedQueue, ClearDropsPendingItems) { + mesh::BoundedQueue queue; + int value = 0; + + EXPECT_TRUE(queue.push(1)); + EXPECT_TRUE(queue.push(2)); + queue.clear(); + + EXPECT_EQ(0u, queue.size()); + EXPECT_FALSE(queue.pop(value)); + EXPECT_TRUE(queue.push(3)); + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(3, value); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 60030469c93c592bddb5f1a4123717484a7db857 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 42/48] fix: enable Heltec V4 USB serial --- boards/heltec_v4.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json index 36cdfc04..0d3a52cd 100644 --- a/boards/heltec_v4.json +++ b/boards/heltec_v4.json @@ -9,7 +9,7 @@ "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], @@ -40,4 +40,4 @@ }, "url": "https://heltec.org/", "vendor": "heltec" -} \ No newline at end of file +} From 9fd6b419e5d134811e1775e7e3d129df9b83f12f Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 43/48] fix: preserve bonded BLE reconnects --- src/helpers/esp32/SerialBLEInterface.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e..c1c04602 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -103,10 +103,9 @@ void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param void SerialBLEInterface::onDisconnect(BLEServer* pServer) { BLE_DEBUG_PRINTLN("onDisconnect()"); + deviceConnected = false; if (_isEnabled) { adv_restart_time = millis() + ADVERT_RESTART_DELAY; - - // loop() will detect this on next loop, and set deviceConnected to false } } @@ -215,8 +214,6 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { return len; } - if (pServer->getConnectedCount() == 0) deviceConnected = false; - if (deviceConnected != oldDeviceConnected) { if (!deviceConnected) { // disconnecting clearBuffers(); From 0de2b4fc5f208489a8523882f050ead033ed3d8b Mon Sep 17 00:00:00 2001 From: =UltimateCodeWarrior Date: Mon, 20 Jul 2026 19:24:38 -0700 Subject: [PATCH 44/48] Move Supreme OLED address to variant config Keep SH1106Display generic by relying on DISPLAY_ADDRESS while the T-Beam Supreme variant declares its 0x3D OLED address. --- src/helpers/ui/SH1106Display.cpp | 15 ++++----------- .../lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 86c6089b..57ccfc2e 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,17 +11,10 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - // Address selection: on some board revisions (notably the LilyGo T-Beam - // Supreme V3) the OLED lives at 0x3D because 0x3C is occupied by a - // magnetometer (QMC6310N). 0x3D is only ever used by the OLED, so prefer it - // when present, otherwise fall back to the standard 0x3C (DISPLAY_ADDRESS). - uint8_t addr = 0; - if (i2c_probe(Wire, 0x3D)) { - addr = 0x3D; - } else if (i2c_probe(Wire, DISPLAY_ADDRESS)) { - addr = DISPLAY_ADDRESS; - } - return addr && display.begin(addr, true); + // Wire must already be initialised by board.begin() before this is called. + // Boards with non-standard SH1106 addresses should define DISPLAY_ADDRESS + // in their variant/platformio configuration. + return i2c_probe(Wire, DISPLAY_ADDRESS) && display.begin(DISPLAY_ADDRESS, true); } void SH1106Display::turnOn() diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 02946156..8bfc4093 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -18,6 +18,7 @@ build_flags = -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D DISPLAY_CLASS=SH1106Display + -D DISPLAY_ADDRESS=0x3D -D LORA_TX_POWER=22 -D P_LORA_TX_LED=6 -D PIN_BOARD_SDA=17 From 4d4d7c37467ce2ad2255f70e53ea40624834393a Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Tue, 21 Jul 2026 07:17:01 +0100 Subject: [PATCH 45/48] refactor: use FreeRTOS BLE receive queue --- src/helpers/BoundedQueue.h | 39 ------------- src/helpers/esp32/SerialBLEInterface.cpp | 16 +---- src/helpers/esp32/SerialBLEInterface.h | 10 +++- .../test_bounded_queue/test_bounded_queue.cpp | 58 ------------------- 4 files changed, 10 insertions(+), 113 deletions(-) delete mode 100644 src/helpers/BoundedQueue.h delete mode 100644 test/test_bounded_queue/test_bounded_queue.cpp diff --git a/src/helpers/BoundedQueue.h b/src/helpers/BoundedQueue.h deleted file mode 100644 index 07ecb4ff..00000000 --- a/src/helpers/BoundedQueue.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include - -namespace mesh { - -template -class BoundedQueue { - static_assert(Capacity > 0, "BoundedQueue capacity must be greater than zero"); - - T _items[Capacity]; - size_t _head = 0; - size_t _size = 0; - -public: - bool push(const T& item) { - if (_size == Capacity) return false; - _items[(_head + _size) % Capacity] = item; - _size++; - return true; - } - - bool pop(T& item) { - if (_size == 0) return false; - item = _items[_head]; - _head = (_head + 1) % Capacity; - _size--; - return true; - } - - void clear() { - _head = 0; - _size = 0; - } - - size_t size() const { return _size; } -}; - -} // namespace mesh diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 9c81dc7b..2dd20b2f 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -123,11 +123,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat frame.len = len; memcpy(frame.buf, rxValue, len); - portENTER_CRITICAL(&recv_queue_mux); - bool queued = recv_queue.push(frame); - portEXIT_CRITICAL(&recv_queue_mux); - - if (!queued) { + if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } } @@ -136,9 +132,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat // ---------- public methods void SerialBLEInterface::clearBuffers() { - portENTER_CRITICAL(&recv_queue_mux); - recv_queue.clear(); - portEXIT_CRITICAL(&recv_queue_mux); + xQueueReset(recv_queue); send_queue_len = 0; } @@ -216,11 +210,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } Frame frame = {}; - portENTER_CRITICAL(&recv_queue_mux); - bool received = recv_queue.pop(frame); - portEXIT_CRITICAL(&recv_queue_mux); - - if (received) { + if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) { memcpy(dest, frame.buf, frame.len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); return frame.len; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index e2d600a0..9fb6adba 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -1,12 +1,12 @@ #pragma once #include "../BaseSerialInterface.h" -#include "../BoundedQueue.h" #include #include #include #include #include +#include class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; @@ -26,8 +26,9 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE }; #define FRAME_QUEUE_SIZE 4 - portMUX_TYPE recv_queue_mux = portMUX_INITIALIZER_UNLOCKED; - mesh::BoundedQueue recv_queue; + StaticQueue_t recv_queue_state; + uint8_t recv_queue_storage[FRAME_QUEUE_SIZE * sizeof(Frame)]; + QueueHandle_t recv_queue; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; @@ -60,6 +61,9 @@ public: _isEnabled = false; _last_write = 0; last_conn_id = 0; + recv_queue = xQueueCreateStatic( + FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state + ); send_queue_len = 0; } diff --git a/test/test_bounded_queue/test_bounded_queue.cpp b/test/test_bounded_queue/test_bounded_queue.cpp deleted file mode 100644 index 3e2e2de7..00000000 --- a/test/test_bounded_queue/test_bounded_queue.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include - -#include - -TEST(BoundedQueue, PreservesOrderAcrossWraparound) { - mesh::BoundedQueue queue; - int value = 0; - - for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(i)); - EXPECT_FALSE(queue.push(4)); - - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(0, value); - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(1, value); - - EXPECT_TRUE(queue.push(4)); - EXPECT_TRUE(queue.push(5)); - - for (int expected = 2; expected < 6; expected++) { - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(expected, value); - } - EXPECT_FALSE(queue.pop(value)); -} - -TEST(BoundedQueue, SurvivesRepeatedFillAndDrainCycles) { - mesh::BoundedQueue queue; - int value = 0; - - for (int cycle = 0; cycle < 1000; cycle++) { - for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(cycle * 4 + i)); - for (int i = 0; i < 4; i++) { - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(cycle * 4 + i, value); - } - } -} - -TEST(BoundedQueue, ClearDropsPendingItems) { - mesh::BoundedQueue queue; - int value = 0; - - EXPECT_TRUE(queue.push(1)); - EXPECT_TRUE(queue.push(2)); - queue.clear(); - - EXPECT_EQ(0u, queue.size()); - EXPECT_FALSE(queue.pop(value)); - EXPECT_TRUE(queue.push(3)); - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(3, value); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} From 78240e9477feab86b2ea1d24073ca580f9e59415 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 21 Jul 2026 19:22:09 +1000 Subject: [PATCH 46/48] * optimised: value-init not needed --- src/helpers/esp32/SerialBLEInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 7084b7d9..6371cf33 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -208,7 +208,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } } - Frame frame = {}; + Frame frame; if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) { memcpy(dest, frame.buf, frame.len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); From a71cc238546e36c5c31c196eb4ec0cf528746017 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 21 Jul 2026 12:23:20 -0700 Subject: [PATCH 47/48] fix(room_server): support RX boosted gain The room server never supported RX boosted gain, while the repeater does. Three consequences on SX1262/SX1268 boards: - boosted gain was never applied to the radio at boot, so a room server ran in power-saving RX mode while an identical repeater ran boosted - a real receive-sensitivity difference with nothing pointing at it - _prefs.rx_boosted_gain was never initialised (the prefs are memset to 0 before defaults are set), so it defaulted to off - 'set radio.rxgain on' replied "Error: unsupported" but CommonCLI had already written the value to prefs and saved them, so a stale setting survived reboot and was never applied Mirror the repeater's implementation: initialise the pref default under the same USE_SX1262/USE_SX1268 and SX126X_RX_BOOSTED_GAIN guards, apply it during radio init, and override setRxBoostedGain() so the CLI reports success and takes effect. Built Heltec_v3_room_server (SX1262) and LilyGo_T3S3_sx1276_room_server (guards compile out cleanly). --- examples/simple_room_server/MyMesh.cpp | 13 +++++++++++++ examples/simple_room_server/MyMesh.h | 1 + 2 files changed, 14 insertions(+) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e80..b6485128 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -658,6 +658,14 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + +#if defined(USE_SX1262) || defined(USE_SX1268) +#ifdef SX126X_RX_BOOSTED_GAIN + _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; +#else + _prefs.rx_boosted_gain = 1; // enabled by default; +#endif +#endif _prefs.radio_fem_rxgain = 1; next_post_idx = 0; @@ -700,6 +708,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); + radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); @@ -807,6 +816,10 @@ void MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); } +bool MyMesh::setRxBoostedGain(bool enable) { + return radio_driver.setRxBoostedGainMode(enable); +} + void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) IdentityStore store(*_fs, ""); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 6bab9dc2..caef6920 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -206,6 +206,7 @@ public: void dumpLogFile() override; void setTxPower(int8_t power_dbm) override; + bool setRxBoostedGain(bool enable) override; void formatNeighborsReply(char *reply) override { strcpy(reply, "not supported"); From 520fa77f4fedfe0d39eb0c472f7c7346f470df82 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 24 Jul 2026 15:31:01 +1200 Subject: [PATCH 48/48] increase tbeam sx1276 max group channels from 8 to 40 --- variants/lilygo_tbeam_SX1276/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index cb25903c..f5f49047 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -45,7 +45,7 @@ build_flags = ${LilyGo_TBeam_SX1276.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=160 - -D MAX_GROUP_CHANNELS=8 + -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128