fix: port T-Beam 1W hardware corrections

Adapt the validated fan thermostat, battery ADC calibration, GPS baud, SX1262 register patch, and 1700us PA ramp behavior from mintylinux/Meshcore-T-beam-1W-Firmware to the current board architecture. Also make Arduino 3 full-build dependency switching reliable after standard ESP32 builds and guard the release hardware contract with regression coverage.
This commit is contained in:
mikecarper committed 2026-08-27 18:58:37 -07:00
1 parent 74002db4e5
commit 4f13bbc1c1
13 files changed
+218 -26

No files matched your search

+2
View File
@@ -25,6 +25,8 @@ venv/
# Script-generated cert bundles (see scripts/generate_cert_bundle.py)
src/certs/x509_crt_bundle.bin
src/helpers/esp32/WebConfigHtml.h
# Generated by pioarduino's component-manager preflight for Arduino 3 builds.
src/idf_component.yml
ssl_certs/cacert.pem
platformio.local.ini
.cursor/*
+13 -4
View File
@@ -2432,15 +2432,24 @@ requires_esp32_arduino3_framework() {
prepare_esp32_arduino3_framework() {
local env_name=$1
local pio_env_name=$2
local arduino3_core_url="https://github.com/espressif/arduino-esp32/releases/download/3.3.11/esp32-core-3.3.11.tar.xz"
local arduino3_libs_url="https://github.com/espressif/arduino-esp32/releases/download/3.3.11/esp32-core-3.3.11-libs.tar.xz"
local pio_core_dir="${PLATFORMIO_CORE_DIR:-${HOME}/.platformio}"
local arduino_manifest="${pio_core_dir}/packages/framework-arduinoespressif32/package.json"
requires_esp32_arduino3_framework "$env_name" "$pio_env_name" || return 0
# PlatformIO's standard ESP32 platform and pioarduino publish incompatible
# Arduino 2.x/3.x cores under the same global package name. `pio run` restores
# the standard core automatically, but pioarduino can otherwise proceed with
# no matching framework after a standard build. Enforce this environment's
# explicit platform_packages pins before SCons starts.
# Arduino 2.x/3.x cores under the same global package name. PlatformIO may
# list both versions side by side but pioarduino only resolves the canonical
# package directory. Replace just that conflicting framework when necessary;
# do not force-reinstall the platform and all of its toolchains.
echo "Ensuring Arduino-ESP32 3.3.11 dependencies for ${env_name}..."
if ! grep -Eq '"version"[[:space:]]*:[[:space:]]*"3\.3\.11"' "$arduino_manifest" 2>/dev/null; then
pio pkg uninstall --global --tool framework-arduinoespressif32 --no-save || true
pio pkg install --global --tool "$arduino3_core_url"
fi
pio pkg install --global --tool "$arduino3_libs_url"
pio pkg install -e "$pio_env_name"
}
+3
View File
@@ -1613,6 +1613,9 @@ void loop() {
#endif
#endif
rtc_clock.tick();
#ifdef TBEAM_1W
board.updateFanControl();
#endif
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
+3
View File
@@ -157,6 +157,9 @@ void loop() {
// GPS acquisition, cache refresh, and delayed shutdown are driven by the
// sensor manager loop even though KISS itself is normally host powered.
sensors.loop();
#ifdef TBEAM_1W
board.updateFanControl();
#endif
if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) {
if (!modem->isTxBusy()) {
+4
View File
@@ -321,6 +321,10 @@ void loop() {
#endif
rtc_clock.tick();
#ifdef TBEAM_1W
board.updateFanControl();
#endif
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
+3
View File
@@ -192,6 +192,9 @@ void loop() {
if (display_ready) ui_task.loop();
#endif
rtc_clock.tick();
#ifdef TBEAM_1W
board.updateFanControl();
#endif
#ifdef HAS_EXTERNAL_WATCHDOG
external_watchdog.loop();
#endif
@@ -13,6 +13,18 @@ public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
protected:
int16_t applyCachedTxPower(int8_t dbm) override {
#ifdef SX126X_PA_RAMP_TIME
// External-PA boards such as the T-Beam 1W require a slower PA ramp. Use
// the same ramp for runtime power changes and post-reset restoration;
// SX1262::setOutputPower() would otherwise restore RadioLib's default.
return ((CustomSX1262 *)_radio)->setTxParams(
dbm, static_cast<uint8_t>(SX126X_PA_RAMP_TIME));
#else
return RadioLibWrapper::applyCachedTxPower(dbm);
#endif
}
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
bool success = ((CustomSX1262 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
&& ((CustomSX1262 *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
+73
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import io
import importlib.util
import json
import os
import random
import struct
@@ -417,6 +418,11 @@ def test_esp32_s3_full_profiles_inherit_dio_boot_mode():
"prepare_esp32_arduino3_framework()", 1
)[1].split("requires_esp32_companion_full_ota_fallback()", 1)[0]
assert "requires_esp32_arduino3_framework" in framework_preflight
assert 'framework-arduinoespressif32/package.json' in framework_preflight
assert '"3\\.3\\.11"' in framework_preflight
assert "pio pkg uninstall --global --tool framework-arduinoespressif32" in framework_preflight
assert 'pio pkg install --global --tool "$arduino3_core_url"' in framework_preflight
assert 'pio pkg install --global --tool "$arduino3_libs_url"' in framework_preflight
assert 'pio pkg install -e "$pio_env_name"' in framework_preflight
build_call = build.split(
'print_build_flags "$pio_env_name" "$env_name"', 1
@@ -459,6 +465,73 @@ def test_esp32_s3_full_profiles_inherit_dio_boot_mode():
assert profile_count == 17
def test_tbeam_1w_release_hardware_fixes_are_preserved():
root = Path(__file__).resolve().parents[2]
variant_root = root / "variants/lilygo_tbeam_1w"
profile = (variant_root / "platformio.ini").read_text(encoding="utf-8")
variant = (variant_root / "variant.h").read_text(encoding="utf-8")
pins = (variant_root / "pins_arduino.h").read_text(encoding="utf-8")
target = (variant_root / "target.cpp").read_text(encoding="utf-8")
board_impl = (variant_root / "TBeam1WBoard.cpp").read_text(
encoding="utf-8"
)
wrapper = (
root / "src/helpers/radiolib/CustomSX1262Wrapper.h"
).read_text(encoding="utf-8")
board_manifest = json.loads(
(root / "boards/t_beam_1w.json").read_text(encoding="utf-8")
)
# Hardware fixes advertised by the T-Beam 1W v1.17.1 release. Keep the
# build flag and header fallback aligned so every role uses 1700 us.
for flag in (
"-D USE_SX1262",
"-D SX126X_REGISTER_PATCH=1",
"-D SX126X_PA_RAMP_TIME=0x06",
"-D SX126X_CURRENT_LIMIT=140",
"-D SX126X_RX_BOOSTED_GAIN=1",
"-D MAX_LORA_TX_POWER=22",
"-D BATT_MIN_MILLIVOLTS=6000",
"-D BATT_MAX_MILLIVOLTS=8400",
"-D GPS_BAUD_RATE=9600",
):
assert flag in profile
assert "#define SX126X_PA_RAMP_TIME 0x06" in variant
assert "RADIOLIB_SX126X_PA_RAMP_1700U" in variant
# This branch has a working board-specific Arduino variant. Protect its
# correct default SPI pins instead of regressing to generic ESP32-S3 pins.
assert board_manifest["build"]["variant"] == "lilygo_tbeam_1w"
assert (
board_manifest["build"]["arduino"]["partitions"]
== "default_16MB.csv"
)
assert "static const uint8_t MISO = 12;" in pins
assert "static const uint8_t SCK = 13;" in pins
# The extended ramp must survive boot, CLI power changes, and recovery.
assert "radio.setTxParams(" in target
assert "SX126X_PA_RAMP_TIME" in target
assert "applyCachedTxPower(int8_t dbm) override" in wrapper
assert "SX126X_PA_RAMP_TIME" in wrapper
# Preserve the hardware-tested battery divider and fan thermostat.
assert "#define BATTERY_PIN 4" in variant
assert "#define BATTERY_SENSE_SAMPLES 30" in variant
assert "#define ADC_MULTIPLIER 2.9333f" in variant
assert "#define FAN_TEMP_ON_C 45.0f" in board_impl
assert "#define FAN_TEMP_OFF_C 41.0f" in board_impl
assert "#define FAN_MIN_RUN_TIME_MS 5000UL" in board_impl
for role_main in (
"examples/companion_radio/main.cpp",
"examples/simple_repeater/main.cpp",
"examples/simple_room_server/main.cpp",
"examples/kiss_modem/main.cpp",
):
source = (root / role_main).read_text(encoding="utf-8")
assert "board.updateFanControl();" in source
def test_release_catalog_resolves_canonical_runtime_aliases():
root = Path(__file__).resolve().parents[2]
provider_path = root / "mesh-america/update-provider-release.py"
+72 -14
View File
@@ -3,6 +3,19 @@
#include <driver/gpio.h>
#include <driver/rtc_io.h>
#ifndef FAN_TEMP_ON_C
#define FAN_TEMP_ON_C 45.0f
#endif
#ifndef FAN_TEMP_OFF_C
#define FAN_TEMP_OFF_C 41.0f
#endif
#ifndef FAN_MIN_RUN_TIME_MS
#define FAN_MIN_RUN_TIME_MS 5000UL
#endif
#ifndef FAN_TEMP_POLL_INTERVAL_MS
#define FAN_TEMP_POLL_INTERVAL_MS 1000UL
#endif
void TBeam1WBoard::begin() {
ESP32Board::begin();
@@ -36,9 +49,15 @@ void TBeam1WBoard::begin() {
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Initialize fan control (on by default - 1W PA can overheat)
// Start with the fan off. updateFanControl() turns it on at the validated
// temperature threshold and applies hysteresis, avoiding the former
// always-on battery drain while still failing safe on an invalid reading.
pinMode(FAN_CTRL_PIN, OUTPUT);
digitalWrite(FAN_CTRL_PIN, HIGH);
fan_running = false;
fan_temperature_valid = false;
fan_started_at_ms = 0;
fan_last_poll_ms = 0;
digitalWrite(FAN_CTRL_PIN, LOW);
}
void TBeam1WBoard::powerCycleRadio() {
@@ -55,25 +74,34 @@ void TBeam1WBoard::powerCycleRadio() {
void TBeam1WBoard::onBeforeTransmit() {
// RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins()
updateFanControl();
digitalWrite(LED_PIN, HIGH); // TX LED on
}
void TBeam1WBoard::onAfterTransmit() {
digitalWrite(LED_PIN, LOW); // TX LED off
updateFanControl();
}
uint16_t TBeam1WBoard::getBattMilliVolts() {
// T-Beam 1W uses 7.4V battery with voltage divider
// ADC reads through divider - adjust multiplier based on actual divider ratio
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogRead(BATTERY_PIN);
// T-Beam 1W uses a 2S battery through the GPIO4 divider. These settings
// match the hardware-tested fork and the Meshtastic board definition.
static bool adc_initialized = false;
if (!adc_initialized) {
pinMode(BATTERY_PIN, INPUT);
analogReadResolution(12);
analogSetPinAttenuation(BATTERY_PIN, ADC_11db);
adc_initialized = true;
}
raw = raw / 8;
// Assuming voltage divider ratio from ADC_MULTIPLIER
// 3.3V reference, 12-bit ADC (4095 max)
return static_cast<uint16_t>((raw * 3300 * ADC_MULTIPLIER) / 4095);
uint32_t raw = 0;
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
raw += analogRead(BATTERY_PIN);
delayMicroseconds(100);
}
raw /= BATTERY_SENSE_SAMPLES;
return static_cast<uint16_t>(
(static_cast<float>(raw) / 4095.0f) * 3300.0f * ADC_MULTIPLIER);
}
const char* TBeam1WBoard::getManufacturerName() const {
@@ -90,7 +118,7 @@ void TBeam1WBoard::powerOff() {
// Turn off LED and fan
digitalWrite(LED_PIN, LOW);
digitalWrite(FAN_CTRL_PIN, LOW);
setFanEnabled(false);
ESP32Board::powerOff();
}
@@ -137,9 +165,39 @@ bool TBeam1WBoard::isLoRaFemLnaEnabled() const {
}
void TBeam1WBoard::setFanEnabled(bool enabled) {
if (fan_running == enabled) return;
fan_running = enabled;
if (enabled) fan_started_at_ms = millis();
digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW);
}
bool TBeam1WBoard::isFanEnabled() const {
return digitalRead(FAN_CTRL_PIN) == HIGH;
return fan_running;
}
void TBeam1WBoard::updateFanControl() {
const uint32_t now = millis();
if (!fan_temperature_valid
|| static_cast<uint32_t>(now - fan_last_poll_ms)
>= FAN_TEMP_POLL_INTERVAL_MS) {
const float temperature_c = getMCUTemperature();
fan_last_poll_ms = now;
if (isnan(temperature_c)) {
// Cooling is safer than silently leaving the 1 W PA unprotected if the
// ESP32 temperature sensor ever reports an invalid sample.
setFanEnabled(true);
return;
}
fan_last_temperature_c = temperature_c;
fan_temperature_valid = true;
}
if (fan_running) {
if (static_cast<uint32_t>(now - fan_started_at_ms) >= FAN_MIN_RUN_TIME_MS
&& fan_last_temperature_c < FAN_TEMP_OFF_C) {
setFanEnabled(false);
}
} else if (fan_last_temperature_c >= FAN_TEMP_ON_C) {
setFanEnabled(true);
}
}
+9 -1
View File
@@ -35,6 +35,11 @@ private:
bool lna_enabled = true;
bool lna_driver_synced = false;
CustomSX1262Wrapper* radio_driver = nullptr;
bool fan_running = false;
bool fan_temperature_valid = false;
uint32_t fan_started_at_ms = 0;
uint32_t fan_last_poll_ms = 0;
float fan_last_temperature_c = 0.0f;
public:
void begin();
@@ -55,7 +60,10 @@ public:
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
// Fan control methods
// Temperature-gated cooling for the external PA. Every T-Beam 1W role
// services this from its main loop; the TX hooks also run it at the points
// where the PA can begin heating.
void updateFanControl();
void setFanEnabled(bool enabled);
bool isFanEnabled() const;
};
+8
View File
@@ -6,6 +6,7 @@ build_flags =
-I variants/lilygo_tbeam_1w
-D TBEAM_1W
-D RECOVERABLE_EXTERNAL_RADIO
-D USE_SX1262
; Radio - SX1262 with high-power PA (32dBm max output)
; Note: Set SX1262 output to 22dBm max, external PA provides additional gain
@@ -28,9 +29,15 @@ build_flags =
-D SX126X_DIO3_TCXO_VOLTAGE=3.0
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_REGISTER_PATCH=1
; LilyGo specifies a PA stabilization time greater than 800us. 0x06 is
; RadioLib's 1700us SX126x ramp setting.
-D SX126X_PA_RAMP_TIME=0x06
; TX power: 22dBm to SX1262, PA module adds ~10dB for 32dBm total
-D LORA_TX_POWER=22
-D MAX_LORA_TX_POWER=22
-D MIN_LORA_TX_POWER=-9
; Battery - 2S 7.4V LiPo (6.0V min, 8.4V max)
-D BATT_MIN_MILLIVOLTS=6000
@@ -49,6 +56,7 @@ build_flags =
-D PIN_GPS_TX=5
-D PIN_GPS_RX=6
-D PIN_GPS_EN=16
-D GPS_BAUD_RATE=9600
-D ENV_INCLUDE_GPS=1
; User interface
+10 -3
View File
@@ -53,12 +53,19 @@ bool radio_init() {
// GPS serial initialized by EnvironmentSensorManager::begin()
bool success = radio.std_init(&spi);
if (success) {
// T-Beam 1W has external PA requiring longer ramp time (>800us recommended)
const int16_t ramp_status = radio.setTxParams(
LORA_TX_POWER, static_cast<uint8_t>(SX126X_PA_RAMP_TIME));
if (ramp_status != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("T-Beam 1W PA ramp configuration failed: %d",
(int)ramp_status);
success = false;
}
}
target_radio_available = success;
if (success) {
board.attachRadioDriver(&radio_driver);
// T-Beam 1W has external PA requiring longer ramp time (>800us recommended)
// RADIOLIB_SX126X_PA_RAMP_800U = 0x05
radio.setTxParams(LORA_TX_POWER, RADIOLIB_SX126X_PA_RAMP_800U);
} else {
MESH_DEBUG_PRINTLN(
"T-Beam 1W radio pins: LDO=%d NSS=%d RST=%d BUSY=%d MISO=%d DIO1=%d CTRL=%d",
+6 -4
View File
@@ -15,7 +15,7 @@
#define PIN_GPS_PPS 7 // GPS PPS output
#define PIN_GPS_EN 16 // GPS wake-up/enable (GPS_EN_PIN in LilyGO code)
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
#define GPS_BAUD_RATE 9600
// Buttons
#define BUTTON_PIN 0 // BUTTON 1 (boot)
@@ -75,7 +75,7 @@
#define BATTERY_PIN 4
#define ADC_CHANNEL ADC1_GPIO4_CHANNEL
#define BATTERY_SENSE_SAMPLES 30
#define ADC_MULTIPLIER 3.0
#define ADC_MULTIPLIER 2.9333f
// NTC temperature sensor
#define NTC_PIN 14
@@ -84,8 +84,10 @@
#define FAN_CTRL_PIN 41
// PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us)
// Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U
#define SX126X_PA_RAMP_US 0x05
// Value 0x06 = RADIOLIB_SX126X_PA_RAMP_1700U
#ifndef SX126X_PA_RAMP_TIME
#define SX126X_PA_RAMP_TIME 0x06
#endif
// Display - SH1106 OLED (128x64)
#define USE_SH1106