From 9a94e8bd9f2d007aea0e8e28a17068d906a718c9 Mon Sep 17 00:00:00 2001 From: Austin Lane Date: Fri, 20 Feb 2026 19:07:18 -0500 Subject: [PATCH 01/11] Add T-Beam BPF (144-148 Mhz LoRa) --- boards/t-beam-bpf.json | 39 +++++++++++++ src/mesh/RF95Interface.cpp | 9 +++ src/platform/esp32/architecture.h | 3 + variants/esp32s3/t-beam-bpf/pins_arduino.h | 26 +++++++++ variants/esp32s3/t-beam-bpf/platformio.ini | 23 ++++++++ variants/esp32s3/t-beam-bpf/variant.h | 65 ++++++++++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 boards/t-beam-bpf.json create mode 100644 variants/esp32s3/t-beam-bpf/pins_arduino.h create mode 100644 variants/esp32s3/t-beam-bpf/platformio.ini create mode 100644 variants/esp32s3/t-beam-bpf/variant.h diff --git a/boards/t-beam-bpf.json b/boards/t-beam-bpf.json new file mode 100644 index 000000000..2d7889b49 --- /dev/null +++ b/boards/t-beam-bpf.json @@ -0,0 +1,39 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_BPF", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-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": "t-beam-bpf" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo TBeam-BPF", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 0c12401ca..5421d9638 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -113,6 +113,11 @@ void RF95Interface::setTransmitEnable(bool txon) /// \return true if initialisation succeeded. bool RF95Interface::init() { +#ifdef RF95_POWER_EN + digitalWrite(RF95_POWER_EN, HIGH); + pinMode(RF95_POWER_EN, OUTPUT); +#endif + RadioLibInterface::init(); #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) @@ -335,6 +340,10 @@ bool RF95Interface::sleep() setStandby(); // First cancel any active receiving/sending lora->sleep(); +#ifdef RF95_POWER_EN + digitalWrite(RF95_POWER_EN, LOW); +#endif + #ifdef RF95_FAN_EN digitalWrite(RF95_FAN_EN, 0); #endif diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 7aee45f81..e9aa6f3fe 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -194,6 +194,9 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_BEAM_1W) #define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT +// Awaiting protobuf merge +// #elif defined(T_BEAM_BPF) +// #define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-bpf/pins_arduino.h b/variants/esp32s3/t-beam-bpf/pins_arduino.h new file mode 100644 index 000000000..39d98bbaa --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// UART1 (qwiic) +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// I2C for OLED and sensors +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI mapped to Radio/SD +static const uint8_t SS = 15; // LoRa CS +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; + +// SD Card CS +#define SDCARD_CS 10 + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t-beam-bpf/platformio.ini b/variants/esp32s3/t-beam-bpf/platformio.ini new file mode 100644 index 000000000..8358d33b5 --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/platformio.ini @@ -0,0 +1,23 @@ +; LilyGo T-Beam-BPF (144-148Mhz) +[env:t-beam-bpf] +custom_meshtastic_hw_model = 124 +custom_meshtastic_hw_model_slug = TBEAM_BPF +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-Beam BPF +custom_meshtastic_images = tbeam-1w.svg +custom_meshtastic_tags = LilyGo + +extends = esp32s3_base +board = t-beam-bpf +board_build.partitions = default_16MB.csv +board_check = true + +lib_deps = + ${esp32s3_base.lib_deps} + +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/t-beam-bpf + -D T_BEAM_BPF diff --git a/variants/esp32s3/t-beam-bpf/variant.h b/variants/esp32s3/t-beam-bpf/variant.h new file mode 100644 index 000000000..9b40baea0 --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/variant.h @@ -0,0 +1,65 @@ +// LilyGo T-Beam-BPF variant.h +// Configuration based on LilyGO utilities.h and RF documentation + +// TODO Lock to 2M (144mhz) ham "region" +// #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_HAM_2M + +// I2C for OLED display (SH1106 at 0x3C) +#define I2C_SDA 8 +#define I2C_SCL 9 + +// GPS - Quectel L76K +#define GPS_RX_PIN 5 +#define GPS_TX_PIN 6 +#define GPS_1PPS_PIN 7 +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 + +// Buttons +#define BUTTON_PIN 0 // BUTTON 1 +#define ALT_BUTTON_PIN 3 // BUTTON 2 + +// SPI (shared by LoRa and SD) +#define SPI_MOSI 11 +#define SPI_SCK 12 +#define SPI_MISO 13 +#define SPI_CS 10 + +// SD Card +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +// LoRa Radio - SX1278 144-148Mhz +#define USE_RF95 +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 1 +#define LORA_RESET 18 +#define LORA_IRQ 14 +#define LORA_DIO0 LORA_IRQ +#define LORA_DIO1 21 +#define RF95_RXEN 39 // LNA enable - HIGH during RX + +// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()! +// GPIO 16 powers the SX1278 via LDO +#define RF95_POWER_EN 16 + +// "+27dBm"? PA! Investigate further (poorly documented) +// LilyGo Docs specify SX1278 power must be capped at 10 +#define RF95_MAX_POWER 10 +// TODO map PA output curve +// #define TX_GAIN_LORA 17 + +// Display - SH1106 OLED (128x64) +#define USE_SH1106 +#define OLED_WIDTH 128 +#define OLED_HEIGHT 64 + +// 32768 Hz crystal present +#define HAS_32768HZ 1 + +// PMU +#define HAS_AXP2101 +// #define PMU_IRQ 4 // Leave disabled for now From 1d1ca1053bbe097c5358660d84b3d69c60ec4d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 26 Mar 2026 19:33:41 +0100 Subject: [PATCH 02/11] minor correction to fix compiler warnings --- src/platform/esp32/architecture.h | 5 ++--- variants/esp32s3/t-beam-bpf/variant.h | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index e9aa6f3fe..fcfb88bcf 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -194,9 +194,8 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_BEAM_1W) #define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT -// Awaiting protobuf merge -// #elif defined(T_BEAM_BPF) -// #define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF +#elif defined(T_BEAM_BPF) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-bpf/variant.h b/variants/esp32s3/t-beam-bpf/variant.h index 9b40baea0..8c0647dfe 100644 --- a/variants/esp32s3/t-beam-bpf/variant.h +++ b/variants/esp32s3/t-beam-bpf/variant.h @@ -28,7 +28,7 @@ // SD Card #define HAS_SDCARD #define SDCARD_USE_SPI1 -#define SDCARD_CS SPI_CS +// #define SDCARD_CS SPI_CS (already defined in pins_arduino.h) // LoRa Radio - SX1278 144-148Mhz #define USE_RF95 From 64f415dc0f55d42b72698c99ccf0da3d4e4e9cfd Mon Sep 17 00:00:00 2001 From: Austin Lane Date: Fri, 20 Feb 2026 19:07:18 -0500 Subject: [PATCH 03/11] Add T-Beam BPF (144-148 Mhz LoRa) --- boards/t-beam-bpf.json | 39 +++++++++++++ src/mesh/RF95Interface.cpp | 9 +++ src/platform/esp32/architecture.h | 3 + variants/esp32s3/t-beam-bpf/pins_arduino.h | 26 +++++++++ variants/esp32s3/t-beam-bpf/platformio.ini | 23 ++++++++ variants/esp32s3/t-beam-bpf/variant.h | 65 ++++++++++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 boards/t-beam-bpf.json create mode 100644 variants/esp32s3/t-beam-bpf/pins_arduino.h create mode 100644 variants/esp32s3/t-beam-bpf/platformio.ini create mode 100644 variants/esp32s3/t-beam-bpf/variant.h diff --git a/boards/t-beam-bpf.json b/boards/t-beam-bpf.json new file mode 100644 index 000000000..2d7889b49 --- /dev/null +++ b/boards/t-beam-bpf.json @@ -0,0 +1,39 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_BPF", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-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": "t-beam-bpf" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo TBeam-BPF", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index b3aa72f7a..cb9e4a09b 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -113,6 +113,11 @@ void RF95Interface::setTransmitEnable(bool txon) /// \return true if initialisation succeeded. bool RF95Interface::init() { +#ifdef RF95_POWER_EN + digitalWrite(RF95_POWER_EN, HIGH); + pinMode(RF95_POWER_EN, OUTPUT); +#endif + RadioLibInterface::init(); #if defined(RADIOMASTER_900_BANDIT_NANO) || defined(RADIOMASTER_900_BANDIT) @@ -336,6 +341,10 @@ bool RF95Interface::sleep() setStandby(); // First cancel any active receiving/sending lora->sleep(); +#ifdef RF95_POWER_EN + digitalWrite(RF95_POWER_EN, LOW); +#endif + #ifdef RF95_FAN_EN digitalWrite(RF95_FAN_EN, 0); #endif diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 30398a675..1bbca50e7 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -194,6 +194,9 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_BEAM_1W) #define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT +// Awaiting protobuf merge +// #elif defined(T_BEAM_BPF) +// #define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-bpf/pins_arduino.h b/variants/esp32s3/t-beam-bpf/pins_arduino.h new file mode 100644 index 000000000..39d98bbaa --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// UART1 (qwiic) +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// I2C for OLED and sensors +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI mapped to Radio/SD +static const uint8_t SS = 15; // LoRa CS +static const uint8_t MOSI = 11; +static const uint8_t MISO = 13; +static const uint8_t SCK = 12; + +// SD Card CS +#define SDCARD_CS 10 + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t-beam-bpf/platformio.ini b/variants/esp32s3/t-beam-bpf/platformio.ini new file mode 100644 index 000000000..8358d33b5 --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/platformio.ini @@ -0,0 +1,23 @@ +; LilyGo T-Beam-BPF (144-148Mhz) +[env:t-beam-bpf] +custom_meshtastic_hw_model = 124 +custom_meshtastic_hw_model_slug = TBEAM_BPF +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-Beam BPF +custom_meshtastic_images = tbeam-1w.svg +custom_meshtastic_tags = LilyGo + +extends = esp32s3_base +board = t-beam-bpf +board_build.partitions = default_16MB.csv +board_check = true + +lib_deps = + ${esp32s3_base.lib_deps} + +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/t-beam-bpf + -D T_BEAM_BPF diff --git a/variants/esp32s3/t-beam-bpf/variant.h b/variants/esp32s3/t-beam-bpf/variant.h new file mode 100644 index 000000000..9b40baea0 --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/variant.h @@ -0,0 +1,65 @@ +// LilyGo T-Beam-BPF variant.h +// Configuration based on LilyGO utilities.h and RF documentation + +// TODO Lock to 2M (144mhz) ham "region" +// #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_HAM_2M + +// I2C for OLED display (SH1106 at 0x3C) +#define I2C_SDA 8 +#define I2C_SCL 9 + +// GPS - Quectel L76K +#define GPS_RX_PIN 5 +#define GPS_TX_PIN 6 +#define GPS_1PPS_PIN 7 +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 + +// Buttons +#define BUTTON_PIN 0 // BUTTON 1 +#define ALT_BUTTON_PIN 3 // BUTTON 2 + +// SPI (shared by LoRa and SD) +#define SPI_MOSI 11 +#define SPI_SCK 12 +#define SPI_MISO 13 +#define SPI_CS 10 + +// SD Card +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +// LoRa Radio - SX1278 144-148Mhz +#define USE_RF95 +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 1 +#define LORA_RESET 18 +#define LORA_IRQ 14 +#define LORA_DIO0 LORA_IRQ +#define LORA_DIO1 21 +#define RF95_RXEN 39 // LNA enable - HIGH during RX + +// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()! +// GPIO 16 powers the SX1278 via LDO +#define RF95_POWER_EN 16 + +// "+27dBm"? PA! Investigate further (poorly documented) +// LilyGo Docs specify SX1278 power must be capped at 10 +#define RF95_MAX_POWER 10 +// TODO map PA output curve +// #define TX_GAIN_LORA 17 + +// Display - SH1106 OLED (128x64) +#define USE_SH1106 +#define OLED_WIDTH 128 +#define OLED_HEIGHT 64 + +// 32768 Hz crystal present +#define HAS_32768HZ 1 + +// PMU +#define HAS_AXP2101 +// #define PMU_IRQ 4 // Leave disabled for now From 4271481300210610bdd85a6de25f3d5a50a89d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 26 Mar 2026 19:33:41 +0100 Subject: [PATCH 04/11] minor correction to fix compiler warnings --- src/platform/esp32/architecture.h | 5 ++--- variants/esp32s3/t-beam-bpf/variant.h | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 1bbca50e7..d2f7d78b8 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -194,9 +194,8 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_BEAM_1W) #define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT -// Awaiting protobuf merge -// #elif defined(T_BEAM_BPF) -// #define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF +#elif defined(T_BEAM_BPF) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_BPF #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-bpf/variant.h b/variants/esp32s3/t-beam-bpf/variant.h index 9b40baea0..8c0647dfe 100644 --- a/variants/esp32s3/t-beam-bpf/variant.h +++ b/variants/esp32s3/t-beam-bpf/variant.h @@ -28,7 +28,7 @@ // SD Card #define HAS_SDCARD #define SDCARD_USE_SPI1 -#define SDCARD_CS SPI_CS +// #define SDCARD_CS SPI_CS (already defined in pins_arduino.h) // LoRa Radio - SX1278 144-148Mhz #define USE_RF95 From b365c94981281325a11afe6f6c04fcabbd6825af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 21 Apr 2026 15:13:43 +0200 Subject: [PATCH 05/11] Add ITU regions for this device and make GPS work. --- src/Power.cpp | 16 +++++++ src/graphics/draw/MenuHandler.cpp | 9 ++++ src/mesh/MeshRadio.h | 2 +- src/mesh/NodeDB.cpp | 7 +++ src/mesh/RadioInterface.cpp | 68 ++++++++++++++++++++++++++- variants/esp32s3/t-beam-bpf/variant.h | 7 +-- 6 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 26b961525..301eeed07 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1239,6 +1239,22 @@ bool Power::axpChipInit() PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist PMU->disablePowerOutput(XPOWERS_VBACKUP); + } else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) { + // T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08): + // DCDC1 -> ESP32 + OLED 3V3 (always on, protected) + // ALDO2 -> MicroSD 3V3 (OFF at reset, must enable) + // ALDO4 -> L76K GNSS 3V3 (OFF at reset, must enable) + // ALDO1/3, BLDO1/2, DLDO1 -> user headers / unused at boot, leave at reset defaults. + // LoRa power is outside the PMU (external P-MOSFET switched by RF95_POWER_EN / IO16). + PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO4); + + PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + PMU->enablePowerOutput(XPOWERS_ALDO2); + + // Make sure nothing's driving into an unused rail + PMU->disablePowerOutput(XPOWERS_DCDC5); + PMU->disablePowerOutput(XPOWERS_DLDO1); } // disable all axp chip interrupt diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e92ba4839..304cefd44 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -112,6 +112,14 @@ void menuHandler::OnboardMessage() void menuHandler::LoraRegionPicker(uint32_t duration) { +#ifdef HAS_HAM_2M_ONLY + // Hardware is restricted to the amateur 2m band — offer only the two 2m regions + // so the user cannot pick a sub-GHz region the RF path cannot emit or receive. + static const LoraRegionOption regionOptions[] = { + {"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M}, + {"ITU2/3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M}, + }; +#else static const LoraRegionOption regionOptions[] = { {"Back", OptionsAction::Back}, {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, @@ -141,6 +149,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration) {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, }; +#endif constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); static std::array regionLabels{}; diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index 089b4b189..017a3a207 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -26,9 +26,9 @@ struct RegionProfile { extern const RegionProfile PROFILE_STD; extern const RegionProfile PROFILE_EU868; extern const RegionProfile PROFILE_UNDEF; +extern const RegionProfile PROFILE_HAM; // extern const RegionProfile PROFILE_LITE; // extern const RegionProfile PROFILE_NARROW; -// extern const RegionProfile PROFILE_HAM; // Map from old region names to new region enums struct RegionInfo { diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 6e57e89f6..3d7f6a32a 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1124,6 +1124,13 @@ void NodeDB::installDefaultDeviceState() memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr)); owner.has_is_unmessagable = true; owner.is_unmessagable = false; + +#ifdef HAS_HAM_2M_ONLY + // Ham-band-only hardware defaults to licensed operation. The user can still flip this off later + // (e.g. a commercial operator on an adjacent allocation who wants to keep encryption on) — we + // only set the default here, not on every boot. + owner.is_licensed = true; +#endif } // We reserve a few nodenums for future use diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index b7b672d8b..cafc7ca4d 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -54,6 +54,7 @@ static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtas const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0}; const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0}; const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0}; +const RegionProfile PROFILE_HAM = {PRESETS_STD, 0, 0, false, true, 0, 0, 0, 0}; #define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \ { \ @@ -223,6 +224,19 @@ const RegionInfo regions[] = { */ RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD), + /* + ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz. + Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this + via its own PA curve; the field here is just the legal upper bound. + */ + RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM), + + /* + ITU Region 2 (Americas) and Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz. + Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators. + */ + RDEF(ITU23_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ @@ -505,6 +519,23 @@ std::unique_ptr initLoRa() rebootAtMsec = millis() + 5000; } } + + // Hardware/region crosscheck for the amateur 2m band: ham-only boards must run a 2m region, + // and boards without 2m support must not run one. In either mismatch, drop to UNSET so the + // first-start picker runs and the user re-selects a legal region for the hardware. + const bool is2mRegion = config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M || + config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M; +#ifdef HAS_HAM_2M_ONLY + const bool mismatch = !is2mRegion && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET; +#else + const bool mismatch = is2mRegion; +#endif + if (mismatch) { + LOG_WARN("Saved region incompatible with this hardware's RF path. Revert to unset"); + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + nodeDB->saveToDisk(SEGMENT_CONFIG); + } + return rIf; } @@ -787,7 +818,14 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo const RegionInfo *newRegion = getRegion(loraConfig.region); // If you are not licensed, you can't use ham regions. - if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) { + // Exception: on hardware that can *only* operate on a ham band (e.g. T-Beam BPF), the user has + // no other region to choose, so allow unlicensed selection — a commercial operator on adjacent + // frequencies can still use the band plan and keep encryption enabled. + bool allowUnlicensedHam = false; +#ifdef HAS_HAM_2M_ONLY + allowUnlicensedHam = true; +#endif + if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed && !allowUnlicensedHam) { char err_string[160]; snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name); LOG_ERROR("%s", err_string); @@ -796,6 +834,34 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo return false; } + const bool is2mRegion = loraConfig.region == meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M || + loraConfig.region == meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M; + +#ifdef HAS_HAM_2M_ONLY + // This hardware's front-end / band-pass filter only passes 144-148 MHz. Any other region + // selection would key the radio on a frequency the RF path cannot emit or receive. + if (!is2mRegion && loraConfig.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + char err_string[160]; + snprintf(err_string, sizeof(err_string), "Region %s not supported: this hardware is 2m-only", newRegion->name); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + return false; + } +#else + // Conversely, the 2m ham regions are illegal RF output for hardware not designed for that band + // (e.g. selecting ITU23_2M on a 915 MHz node would transmit at ~3x the expected frequency with + // an untuned antenna and filter). Refuse the selection entirely. + if (is2mRegion) { + char err_string[160]; + snprintf(err_string, sizeof(err_string), "Region %s requires 2m-band hardware", newRegion->name); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + return false; + } +#endif + return true; } diff --git a/variants/esp32s3/t-beam-bpf/variant.h b/variants/esp32s3/t-beam-bpf/variant.h index 8c0647dfe..1b1524145 100644 --- a/variants/esp32s3/t-beam-bpf/variant.h +++ b/variants/esp32s3/t-beam-bpf/variant.h @@ -1,14 +1,15 @@ // LilyGo T-Beam-BPF variant.h // Configuration based on LilyGO utilities.h and RF documentation -// TODO Lock to 2M (144mhz) ham "region" -// #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_HAM_2M +// Hardware is restricted to the amateur 2m band (144-148 MHz). +#define HAS_HAM_2M_ONLY 1 // I2C for OLED display (SH1106 at 0x3C) #define I2C_SDA 8 #define I2C_SCL 9 -// GPS - Quectel L76K +// GPS - Quectel L76K. Per schematic sheet 7: + #define GPS_RX_PIN 5 #define GPS_TX_PIN 6 #define GPS_1PPS_PIN 7 From 5fa9b05d708b2f924273a8470f8afc4e407b8764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 21 Apr 2026 17:26:57 +0200 Subject: [PATCH 06/11] Switch pin after defining it as output Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/mesh/RF95Interface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index be948526f..9ea470365 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -114,8 +114,8 @@ void RF95Interface::setTransmitEnable(bool txon) bool RF95Interface::init() { #ifdef RF95_POWER_EN - digitalWrite(RF95_POWER_EN, HIGH); pinMode(RF95_POWER_EN, OUTPUT); + digitalWrite(RF95_POWER_EN, HIGH); #endif RadioLibInterface::init(); From a4eb6b0b6398ed5bef9fe5c043fae692f0ef5a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 21 Apr 2026 17:32:40 +0200 Subject: [PATCH 07/11] Lora CS is indeed 1, SD Card CS is 10 --- variants/esp32s3/t-beam-bpf/pins_arduino.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/esp32s3/t-beam-bpf/pins_arduino.h b/variants/esp32s3/t-beam-bpf/pins_arduino.h index 39d98bbaa..195ef2896 100644 --- a/variants/esp32s3/t-beam-bpf/pins_arduino.h +++ b/variants/esp32s3/t-beam-bpf/pins_arduino.h @@ -15,7 +15,7 @@ static const uint8_t SDA = 8; static const uint8_t SCL = 9; // Default SPI mapped to Radio/SD -static const uint8_t SS = 15; // LoRa CS +static const uint8_t SS = 1; // LoRa CS static const uint8_t MOSI = 11; static const uint8_t MISO = 13; static const uint8_t SCK = 12; From 39908bfbd2aa6902eedb1f5739d8f9e815f94362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 21 Apr 2026 21:22:33 +0200 Subject: [PATCH 08/11] Include the back option. --- src/graphics/draw/MenuHandler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 304cefd44..5b5200ff4 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -116,8 +116,9 @@ void menuHandler::LoraRegionPicker(uint32_t duration) // Hardware is restricted to the amateur 2m band — offer only the two 2m regions // so the user cannot pick a sub-GHz region the RF path cannot emit or receive. static const LoraRegionOption regionOptions[] = { + {"Back", OptionsAction::Back}, {"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M}, - {"ITU2/3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M}, + {"ITU23_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M}, }; #else static const LoraRegionOption regionOptions[] = { From 76a79dd8ec218d781a3f7f1e8c85ea1ac5aa4ef8 Mon Sep 17 00:00:00 2001 From: vidplace7 Date: Mon, 18 May 2026 15:47:14 -0400 Subject: [PATCH 09/11] Fix compilation with pioarduino (USB_MODE) --- boards/t-beam-bpf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boards/t-beam-bpf.json b/boards/t-beam-bpf.json index 2d7889b49..50afb7900 100644 --- a/boards/t-beam-bpf.json +++ b/boards/t-beam-bpf.json @@ -9,7 +9,7 @@ "-DBOARD_HAS_PSRAM", "-DLILYGO_TBEAM_BPF", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], From 1d595beab7fa63a11a6c95ae7004b3834a502ecf Mon Sep 17 00:00:00 2001 From: vidplace7 Date: Thu, 21 May 2026 18:13:05 -0400 Subject: [PATCH 10/11] Default ham to narrow_fast --- src/mesh/MeshRadio.h | 2 +- src/mesh/RadioInterface.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index a0090673d..b54b072a2 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -46,7 +46,7 @@ extern const RegionProfile PROFILE_EU868; extern const RegionProfile PROFILE_UNDEF; extern const RegionProfile PROFILE_LITE; extern const RegionProfile PROFILE_NARROW; -extern const RegionProfile PROFILE_HAM; +extern const RegionProfile PROFILE_HAM; // Map from old region names to new region enums struct RegionInfo { diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 8f910822f..3ff995680 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -231,13 +231,13 @@ const RegionInfo regions[] = { Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this via its own PA curve; the field here is just the legal upper bound. */ - RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM), + RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST)), /* ITU Region 2 (Americas) and Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz. Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators. */ - RDEF(ITU23_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM), + RDEF(ITU23_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST)), /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. From 2e5f44db16a5fe9c6d237d4f3905af2eff8de9af Mon Sep 17 00:00:00 2001 From: vidplace7 Date: Thu, 21 May 2026 18:34:16 -0400 Subject: [PATCH 11/11] Default PROFILE_HAM to slot 17 This is an appropriate default in the USA but not the EU. The slot override really should follow the region itself, not the regionprofile. --- src/mesh/RadioInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 3ff995680..18cff3628 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -56,7 +56,7 @@ const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1 const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1, 0}; const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10, 0}; const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1, 1}; -const RegionProfile PROFILE_HAM = {PRESETS_NARROW, 0, 0, false, true, 0, 1, 1, 1}; +const RegionProfile PROFILE_HAM = {PRESETS_NARROW, 0, 0, false, true, 0, 1, 1, 17}; #define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset) \ { \