diff --git a/boards/t-beam-bpf.json b/boards/t-beam-bpf.json new file mode 100644 index 000000000..50afb7900 --- /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=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": "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/Power.cpp b/src/Power.cpp index 43e9f1059..21fd2b29a 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1362,6 +1362,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 279339acd..42c8a18e0 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -176,6 +176,15 @@ 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[] = { + {"Back", OptionsAction::Back}, + {"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M}, + {"ITU23_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}, @@ -208,6 +217,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration) {"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 e2c053a8b..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/NodeDB.cpp b/src/mesh/NodeDB.cpp index 9c1b85dad..cff160ae9 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1520,6 +1520,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/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 43149ef8b..48850ad96 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 + pinMode(RF95_POWER_EN, OUTPUT); + digitalWrite(RF95_POWER_EN, HIGH); +#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/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index d4eeed089..18cff3628 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -56,6 +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, 17}; #define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset) \ { \ @@ -225,6 +226,19 @@ const RegionInfo regions[] = { */ RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST)), + /* + 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, 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, PRESET(NARROW_FAST)), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ @@ -535,6 +549,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; } @@ -834,7 +865,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); @@ -843,6 +881,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/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index e4ec807f8..ab5e08f5b 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -198,6 +198,8 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_BEAM_1W) #define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT +#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..195ef2896 --- /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 = 1; // 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..1b1524145 --- /dev/null +++ b/variants/esp32s3/t-beam-bpf/variant.h @@ -0,0 +1,66 @@ +// LilyGo T-Beam-BPF variant.h +// Configuration based on LilyGO utilities.h and RF documentation + +// 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. Per schematic sheet 7: + +#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 (already defined in pins_arduino.h) + +// 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