From 4f13bbc1c13b0ab5f35fe29c9cccc7fdfe415759 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 27 Aug 2026 18:40:47 -0700 Subject: [PATCH] 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. --- .gitignore | 2 + build.sh | 17 ++++- examples/companion_radio/main.cpp | 3 + examples/kiss_modem/main.cpp | 3 + examples/simple_repeater/main.cpp | 4 + examples/simple_room_server/main.cpp | 3 + src/helpers/radiolib/CustomSX1262Wrapper.h | 12 +++ tools/mota/test_mota.py | 73 ++++++++++++++++++ variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 86 ++++++++++++++++++---- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 10 ++- variants/lilygo_tbeam_1w/platformio.ini | 8 ++ variants/lilygo_tbeam_1w/target.cpp | 13 +++- variants/lilygo_tbeam_1w/variant.h | 10 ++- 13 files changed, 218 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 7980a05b5..f17f4df47 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/build.sh b/build.sh index fce925239..e36c51c6b 100755 --- a/build.sh +++ b/build.sh @@ -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" } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index da78e3402..26f4b418e 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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 diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index fd2ac2655..5efdf0911 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -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()) { diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index f85d2b285..58d87bb4a 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -321,6 +321,10 @@ void loop() { #endif rtc_clock.tick(); +#ifdef TBEAM_1W + board.updateFanControl(); +#endif + #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index bc6bb81b1..552e1f567 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -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 diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index bf517a41a..17897812f 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -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(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 diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py index 9ef2e8f54..e63484160 100644 --- a/tools/mota/test_mota.py +++ b/tools/mota/test_mota.py @@ -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" diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index d791dccdf..cc8da3080 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -3,6 +3,19 @@ #include #include +#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((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( + (static_cast(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(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(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); + } } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index 84dd3bca3..9f1bb428c 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -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; }; diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index f231673ec..2d78a9ba6 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -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 diff --git a/variants/lilygo_tbeam_1w/target.cpp b/variants/lilygo_tbeam_1w/target.cpp index aa38eea12..d57e3f4e0 100644 --- a/variants/lilygo_tbeam_1w/target.cpp +++ b/variants/lilygo_tbeam_1w/target.cpp @@ -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(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", diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index d86080590..764f41949 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -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