Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9a94e8b
Add T-Beam BPF (144-148 Mhz LoRa)
vidplace7 Feb 21, 2026
1d1ca10
minor correction to fix compiler warnings
caveman99 Mar 26, 2026
64f415d
Add T-Beam BPF (144-148 Mhz LoRa)
vidplace7 Feb 21, 2026
4271481
minor correction to fix compiler warnings
caveman99 Mar 26, 2026
2bd1faa
Merge branch 't-beam-bpf' of https://github.com/vidplace7/meshtastic-…
caveman99 Apr 21, 2026
51a5957
Merge branch 'develop' into t-beam-bpf
caveman99 Apr 21, 2026
0d08680
Merge branch 't-beam-bpf' of https://github.com/vidplace7/meshtastic-…
caveman99 Apr 21, 2026
b365c94
Add ITU regions for this device and make GPS work.
caveman99 Apr 21, 2026
cccb181
Merge branch 'develop' into t-beam-bpf
caveman99 Apr 21, 2026
5fa9b05
Switch pin after defining it as output
caveman99 Apr 21, 2026
a4eb6b0
Lora CS is indeed 1, SD Card CS is 10
caveman99 Apr 21, 2026
273bcdc
Merge branch 't-beam-bpf' of https://github.com/vidplace7/meshtastic-…
caveman99 Apr 21, 2026
bcceff6
Merge branch 'develop' into t-beam-bpf
caveman99 Apr 21, 2026
39908bf
Include the back option.
caveman99 Apr 21, 2026
da23659
Merge branch 'develop' into t-beam-bpf
vidplace7 May 18, 2026
76a79dd
Fix compilation with pioarduino (USB_MODE)
vidplace7 May 18, 2026
f382718
Merge branch 'develop' into t-beam-bpf
vidplace7 May 21, 2026
1d595be
Default ham to narrow_fast
vidplace7 May 21, 2026
2e5f44d
Default PROFILE_HAM to slot 17
vidplace7 May 21, 2026
ff1f02d
Merge branch 'develop' into t-beam-bpf
vidplace7 May 26, 2026
1997cbc
Move T-Beam-BPF work from #9703 at vidplace7/t-beam-bpf
vidplace7 May 26, 2026
2b5e2b9
Merge branch 'develop' into t-beam-bpf
vidplace7 May 27, 2026
aad1a27
Fix for ITU 2/3 split
vidplace7 May 27, 2026
4a73482
Add ITU region options to MenuAction enum
vidplace7 May 27, 2026
f0a67c5
Add HAS_HAM_2M definition to variant headers for 2M support
vidplace7 May 27, 2026
957b568
Merge branch 'develop' into t-beam-bpf
vidplace7 May 30, 2026
8ef885a
Re-add PROFILE_HAM regionprofile
vidplace7 May 30, 2026
94b343d
Trunk fmt
vidplace7 May 30, 2026
0bce1e9
Initial default slots
vidplace7 May 30, 2026
a01937d
Merge branch 'develop' into t-beam-bpf
vidplace7 Jun 1, 2026
7eb5582
Merge branch 'develop' into t-beam-bpf
vidplace7 Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions boards/t-beam-bpf.json
Original file line number Diff line number Diff line change
@@ -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"
}
16 changes: 16 additions & 0 deletions src/Power.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion src/graphics/draw/MenuHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ 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 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_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
};
#else
static const LoraRegionOption regionOptions[] = {
{"Back", OptionsAction::Back},
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
Expand Down Expand Up @@ -206,8 +216,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
{"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863},
{"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865},
{"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},

{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
{"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
};
#endif

constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
static std::array<const char *, regionCount> regionLabels{};
Expand Down
3 changes: 3 additions & 0 deletions src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ enum MenuAction {
SET_REGION_BR_902,
SET_REGION_EU_866,
SET_REGION_NARROW_868,
SET_REGION_ITU1_2M,
SET_REGION_ITU2_2M,
SET_REGION_ITU3_2M,
// Device Roles
SET_ROLE_CLIENT,
SET_ROLE_CLIENT_MUTE,
Expand Down
15 changes: 15 additions & 0 deletions src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,18 @@ void InkHUD::MenuApplet::execute(MenuItem item)
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868);
break;

case SET_REGION_ITU1_2M:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M);
break;

case SET_REGION_ITU2_2M:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M);
break;

case SET_REGION_ITU3_2M:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M);
break;

// Roles
case SET_ROLE_CLIENT:
applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT);
Expand Down Expand Up @@ -1469,6 +1481,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT));
items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT));
items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT));
items.push_back(MenuItem("ITU1_2M (144-146)", MenuAction::SET_REGION_ITU1_2M, MenuPage::EXIT));
items.push_back(MenuItem("ITU2_2M (144-148)", MenuAction::SET_REGION_ITU2_2M, MenuPage::EXIT));
items.push_back(MenuItem("ITU3_2M (144-148)", MenuAction::SET_REGION_ITU3_2M, MenuPage::EXIT));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;

Expand Down
2 changes: 1 addition & 1 deletion src/mesh/MeshRadio.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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 {
Expand Down
7 changes: 7 additions & 0 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/mesh/RF95Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
90 changes: 89 additions & 1 deletion src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10};
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1};
const RegionProfile PROFILE_HAM = {PRESETS_NARROW, 0, 0, false, true, 0, 1, 1};

#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
override_slot) \
Expand Down Expand Up @@ -226,6 +227,35 @@ const RegionInfo regions[] = {
*/
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),

/*
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.

Default slot: 9 (144.531 MHz)
https://www.iaru-r1.org/wp-content/uploads/2020/12/VHF-Bandplan.pdf
*/
RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST), 9),

/*
ITU Region 2 (Americas) 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.

Default slot: 17 (145.031 MHz)
https://www.arrl.org/band-plan
*/
RDEF(ITU2_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST), 17),

/*
ITU Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz.
Typical admin rules allow well above 30 dBm for licensed operators.

Default slot: 11 (144.656 MHz)
https://www.iaru.org/wp-content/uploads/2020/01/R3-004-IARU-Region-3-Bandplan-rev.2.pdf
https://www.wia.org.au/members/bandplans/data/documents/WIA%20Australian%20Band%20Plan%202026.pdf
*/
RDEF(ITU3_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM, PRESET(NARROW_FAST), 11),

/*
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
*/
Expand Down Expand Up @@ -536,6 +566,28 @@ std::unique_ptr<RadioInterface> 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_ITU2_2M ||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M;
#ifdef HAS_HAM_2M_ONLY
const bool mismatch = !is2mRegion && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET;
#elif defined(HAS_HAM_2M) && !defined(HAS_HAM_2M_ONLY)
// If hardware specifies HAS_HAM_2M without HAS_HAM_2M_ONLY, it supports 2m but isn't restricted to it.
// In this case, we allow any region.
const bool mismatch = false;
#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;
}

Expand Down Expand Up @@ -835,7 +887,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);
Expand All @@ -844,6 +903,35 @@ 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_ITU2_2M ||
loraConfig.region == meshtastic_Config_LoRaConfig_RegionCode_ITU3_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 ITU2_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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/platform/esp32/architecture.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions variants/esp32/tlora_v2_1_16/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#define ADC_CHANNEL ADC_CHANNEL_7
#define BATTERY_SENSE_SAMPLES 30

// A variant has 2M support
#define HAS_HAM_2M 1

// ratio of voltage divider = 2.0 (R42=100k, R43=100k)
#define ADC_MULTIPLIER 2

Expand Down
26 changes: 26 additions & 0 deletions variants/esp32s3/t-beam-bpf/pins_arduino.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h

#include <stdint.h>

#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 */
23 changes: 23 additions & 0 deletions variants/esp32s3/t-beam-bpf/platformio.ini
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading