Keep T-Beam companion available without radio

This commit is contained in:
mikecarper committed 2026-08-15 22:34:45 -07:00
1 parent 7afa6b11a8
commit bcedfdd019
11 files changed
+310 -53

No files matched your search

+8 -8
View File
@@ -1960,12 +1960,12 @@ is_nrf52_companion_radio_full_target() {
}
requires_esp32_companion_full_ota_fallback() {
# Some classic ESP32 companions cannot hold their configured high-capacity
# contact, channel, and offline-queue tables together with LoRa OTA in
# Some ESP32 companions cannot hold their configured high-capacity contact,
# channel, and offline-queue tables together with BLE, WiFi, and LoRa OTA in
# internal DRAM. Keep their ordinary high-capacity image unchanged and emit
# a separately named FULL OTA image with the companion default capacities.
case "${1,,}" in
heltec_v2_companion_radio_wifi|lilygo_tlora_v2_1_1_6_companion_radio_wifi|meshadventurer_sx1262_companion_radio_usb|meshadventurer_sx1268_companion_radio_usb) return 0 ;;
heltec_v2_companion_radio_wifi|lilygo_tbeam_1w_companion_radio_wifi|lilygo_tlora_v2_1_1_6_companion_radio_wifi|meshadventurer_sx1262_companion_radio_usb|meshadventurer_sx1268_companion_radio_usb) return 0 ;;
*) return 1 ;;
esac
}
@@ -2180,7 +2180,7 @@ apply_esp32_full_size_profile() {
fi
if requires_esp32_companion_full_ota_fallback "$env_name"; then
append_platformio_build_unflags "-DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=128"
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16"
fi
}
@@ -2382,11 +2382,11 @@ apply_companion_radio_full_profile() {
append_platformio_build_unflags "-DOTA_FOLDER_SERIAL"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UOTA_FOLDER_SERIAL -DWIFI_OTA_SEEDER=1"
# BLE + WiFi exhaust internal DRAM on the two high-capacity classic ESP32
# recipes. Use the same measured-safe tables as their existing FULL OTA
# fallback without changing ordinary USB/BLE/WiFi companion builds.
# BLE + WiFi exhaust internal DRAM on these high-capacity ESP32 recipes. Use
# measured-safe tables for FULL OTA without changing ordinary USB/BLE/WiFi
# companion builds.
if requires_esp32_companion_full_ota_fallback "$pio_env_name"; then
append_platformio_build_unflags "-DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=128"
append_platformio_build_unflags "-DMAX_CONTACTS=350 -DMAX_CONTACTS=160 -DMAX_GROUP_CHANNELS=40 -DOFFLINE_QUEUE_SIZE=256 -DOFFLINE_QUEUE_SIZE=128"
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=8 -DOFFLINE_QUEUE_SIZE=16"
fi
+49 -20
View File
@@ -1293,6 +1293,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
clearTerminalTrace();
#endif
saved_radio_apply_pending = false;
_radio_available = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
command_radio_apply_pending = false;
@@ -1373,7 +1374,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
#endif
}
void MyMesh::begin(bool has_display) {
void MyMesh::begin(bool has_display, bool radio_available) {
_radio_available = radio_available;
setRadioAvailable(radio_available);
BaseChatMesh::begin();
const bool is_new_install = !_store->loadMainIdentity(self_id)
@@ -1487,19 +1490,7 @@ void MyMesh::begin(bool has_display) {
_store->saveChannels(this);
}
saved_radio_apply_pending = !applySavedRadioParams();
if (!saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
}
const bool fem_gain_changed = board.canControlLoRaFemLna()
&& board.isLoRaFemLnaEnabled() != (_prefs.radio_fem_rxgain != 0);
if (board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain) && fem_gain_changed) {
_radio->recalibrateNoiseFloor();
}
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
configureRadioFromPrefs();
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
applyMQTTDefaults(&_mqtt_prefs);
@@ -1539,7 +1530,42 @@ void MyMesh::begin(bool has_display) {
#endif
}
void MyMesh::configureRadioFromPrefs() {
if (!_radio_available) {
saved_radio_apply_pending = false;
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("Radio unavailable: companion services are running in recovery mode");
return;
}
saved_radio_apply_pending = !applySavedRadioParams();
if (!saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
}
const bool fem_gain_changed = board.canControlLoRaFemLna()
&& board.isLoRaFemLnaEnabled() != (_prefs.radio_fem_rxgain != 0);
if (board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain) && fem_gain_changed) {
_radio->recalibrateNoiseFloor();
}
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}
void MyMesh::activateRadio() {
if (_radio_available) return;
_radio_available = true;
setRadioAvailable(true);
configureRadioFromPrefs();
MESH_DEBUG_PRINTLN("Radio recovery completed; mesh transport is active");
}
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
if (!_radio_available) return mesh::RadioParamApplyResult::FAILED;
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0
@@ -2045,7 +2071,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
snprintf(reply, 160, "Error: TX power must be -9 to %d", MAX_LORA_TX_POWER);
} else {
_prefs.tx_power_dbm = static_cast<int8_t>(parsed);
radio_driver.setTxPower(_prefs.tx_power_dbm);
if (_radio_available) radio_driver.setTxPower(_prefs.tx_power_dbm);
savePrefs();
strcpy(reply, "OK");
}
@@ -2071,7 +2097,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
} else {
if (strcmp(key, "radio.rxgain") == 0) {
_prefs.rx_boosted_gain = enabled;
radio_driver.setRxBoostedGainMode(enabled);
if (_radio_available) radio_driver.setRxBoostedGainMode(enabled);
} else {
_prefs.client_repeat = enabled;
}
@@ -2837,7 +2863,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else {
_prefs.tx_power_dbm = power;
savePrefs();
radio_driver.setTxPower(_prefs.tx_power_dbm);
if (_radio_available) radio_driver.setTxPower(_prefs.tx_power_dbm);
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
@@ -3510,7 +3536,7 @@ bool MyMesh::applyAndSaveFemRxGain(bool enabled) {
const bool changed = board.isLoRaFemLnaEnabled() != enabled;
if (!board.setLoRaFemLnaEnabled(enabled)) return false;
if (changed) _radio->recalibrateNoiseFloor();
if (changed && _radio_available) _radio->recalibrateNoiseFloor();
_prefs.radio_fem_rxgain = enabled ? 1 : 0;
_prefs.radio_fem_rxgain_override = 1;
savePrefs();
@@ -3636,7 +3662,8 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
return false;
}
if (!radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
if (_radio_available
&& !radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
strcpy(reply, "Error: radio busy; retry");
return false;
}
@@ -4846,7 +4873,9 @@ bool MyMesh::advert() {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
if (radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()) return true;
if (_radio_available
&& (radio_driver.isWatchdogObserving()
|| radio_driver.isCalibratingNoiseFloor())) return true;
return (_serial != NULL && _serial->hasPendingIO())
|| (_iter_started && _serial != NULL && _serial->isConnected())
|| command_radio_apply_pending
+5 -1
View File
@@ -112,7 +112,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost
public:
MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
void begin(bool has_display);
void begin(bool has_display, bool radio_available = true);
void activateRadio();
bool isRadioReady() const { return _radio_available; }
void startInterface(BaseSerialInterface &serial);
const char *getNodeName();
@@ -311,6 +313,7 @@ private:
void updateGpsTelemetryPolicy();
mesh::RadioParamApplyResult tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr);
bool applySavedRadioParams();
void configureRadioFromPrefs();
void finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t repeat);
void cancelPendingRadioParamApply();
void servicePendingRadioParamApply();
@@ -373,6 +376,7 @@ private:
char _terminal_trace_target[32];
#endif
bool saved_radio_apply_pending;
bool _radio_available;
unsigned long radio_apply_retry_at;
uint8_t radio_apply_failures;
bool command_radio_apply_pending;
+58 -1
View File
@@ -6,6 +6,9 @@
#ifdef ESP32_PLATFORM
#include "esp_bt.h"
#endif
#ifdef TBEAM_1W
#include <esp_random.h>
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
@@ -122,6 +125,29 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
/* END GLOBAL OBJECTS */
#ifdef TBEAM_1W
static bool companion_radio_available = true;
static unsigned long companion_radio_retry_at = 0;
static const unsigned long COMPANION_RADIO_RETRY_MS = 60000UL;
static void serviceCompanionRadioRecovery() {
if (companion_radio_available
|| (long)(millis() - companion_radio_retry_at) < 0) return;
companion_radio_retry_at = millis() + COMPANION_RADIO_RETRY_MS;
Serial.println("Radio recovery probe starting");
board.powerCycleRadio();
if (!radio_init()) {
Serial.println("Radio recovery probe failed; companion services remain available");
return;
}
companion_radio_available = true;
the_mesh.activateRadio();
Serial.println("Radio recovered; mesh transport is active");
}
#endif
#ifdef ESP32_PLATFORM
static int8_t applied_power_saving = -1;
static int8_t attempted_power_saving = -1;
@@ -705,17 +731,39 @@ void setup() {
#endif
int radioinit_attempts = 0;
while (!radio_init()) {
bool radio_available = false;
while (!(radio_available = radio_init())) {
++radioinit_attempts;
MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts);
if (radioinit_attempts >= 3) {
#if defined(TBEAM_1W)
// Continue into a recovery-capable Companion instead of trapping native
// USB in a reboot loop. The main loop retries the radio independently.
Serial.println("Radio unavailable; starting display, USB, BLE, and WiFi recovery services");
break;
#else
MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting");
board.reboot();
#endif
}
delay(500);
}
#ifdef TBEAM_1W
companion_radio_available = radio_available;
companion_radio_retry_at = millis() + COMPANION_RADIO_RETRY_MS;
fast_rng.begin(radio_available ? radio_driver.getRngSeed() : esp_random());
#ifdef DISPLAY_CLASS
if (!radio_available && disp != NULL) {
disp->startFrame();
disp->drawTextCentered(disp->width() / 2, 20, "Radio unavailable");
disp->drawTextCentered(disp->width() / 2, 40, "Starting WiFi...");
disp->endFrame();
}
#endif
#else
fast_rng.begin(radio_driver.getRngSeed());
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
@@ -748,6 +796,8 @@ void setup() {
#else
false
#endif
,
radio_available
);
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
@@ -758,6 +808,8 @@ void setup() {
#else
false
#endif
,
radio_available
);
#elif defined(ESP32)
SPIFFS.begin(true);
@@ -768,6 +820,8 @@ void setup() {
#else
false
#endif
,
radio_available
);
#else
#error "need to define filesystem"
@@ -874,6 +928,9 @@ void loop() {
board.feedWatchdog();
#endif
the_mesh.loop();
#ifdef TBEAM_1W
serviceCompanionRadioRecovery();
#endif
#if defined(ENABLE_USB_INTERFACE)
serviceUsbTerminal();
#endif
+44 -3
View File
@@ -35,6 +35,28 @@ static uint32_t scaleCADDelayForQueue(uint32_t normal_delay, int ready_count,
return scaled < floor ? floor : scaled;
}
void Dispatcher::setRadioAvailable(bool available) {
if (radio_available == available) return;
radio_available = available;
if (!dispatcher_started || !radio_available) return;
const unsigned long now = _ms->getMillis();
radio_nonrx_start = now;
next_floor_calib_time = now;
next_agc_reset_time = now;
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
#ifdef RADIO_LIVENESS_SOFT_ONLY
last_radio_activity_ms = now;
#else
radio_liveness.begin(now);
nonrx_soft_recovery_attempted = false;
#endif
MESH_DEBUG_PRINTLN("Dispatcher: radio transport activated");
}
void Dispatcher::begin() {
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
@@ -49,10 +71,17 @@ void Dispatcher::begin() {
tx_budget_ms = (unsigned long)(duty_cycle_window_ms * duty_cycle);
last_budget_update = _ms->getMillis();
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
const unsigned long now = _ms->getMillis();
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
dispatcher_started = true;
if (radio_available) {
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
} else {
prev_isrecv_mode = false;
last_observed_radio_irq = 0;
MESH_DEBUG_PRINTLN("Dispatcher: starting without a radio transport");
}
#ifdef RADIO_LIVENESS_SOFT_ONLY
last_radio_activity_ms = now;
#else
@@ -267,6 +296,14 @@ uint32_t Dispatcher::getRadioWatchdogMillis() const {
#endif
void Dispatcher::loop() {
if (!radio_available) {
// Keep packet-manager cleanup alive, but never touch an uninitialized or
// physically unavailable radio. sendPacket() rejects new outbound work
// while this state is active, so no queue can silently fill up.
releaseDroppedOutbound();
return;
}
if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
_radio->setCADEnabled(getCADEnabled());
@@ -713,6 +750,10 @@ void Dispatcher::releasePacket(Packet* packet) {
}
bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (!radio_available) {
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): radio unavailable", getLogDateTime());
return false;
}
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
return false;
+6
View File
@@ -245,6 +245,8 @@ class Dispatcher {
unsigned long radio_nonrx_start;
unsigned long next_floor_calib_time, next_agc_reset_time;
bool prev_isrecv_mode;
bool radio_available;
bool dispatcher_started;
bool outbound_radio_retry_pending;
bool outbound_radio_retry_used;
#ifndef RADIO_LIVENESS_SOFT_ONLY
@@ -286,6 +288,8 @@ protected:
_err_flags = 0;
radio_nonrx_start = 0;
prev_isrecv_mode = true;
radio_available = true;
dispatcher_started = false;
tx_budget_ms = 0;
last_budget_update = 0;
duty_cycle_window_ms = 3600000;
@@ -346,6 +350,8 @@ protected:
}
bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis);
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
void setRadioAvailable(bool available);
bool isRadioAvailable() const { return radio_available; }
public:
void begin();
+23 -5
View File
@@ -585,17 +585,31 @@ bool WebConfigServer::startSetupMode(char reply[]) {
// AP_STA (not pure AP) so the WiFi scan for the SSID picker works while
// the AP is up. STA stays unconnected - the bridge won't touch WiFi
// while wifi_ssid is empty, and `start webconfig ap` requires it stopped.
WiFi.mode(WIFI_AP_STA);
bool mode_ok = WiFi.mode(WIFI_AP_STA);
// Setup mode has no login. Drop any STA association so the open setup API is
// reachable only from the setup AP, not from the operator's LAN.
WiFi.setAutoReconnect(false);
WiFi.disconnect(false, true);
bool disconnect_ok = WiFi.disconnect(false, true);
delay(100);
snprintf(_ap_ssid, sizeof(_ap_ssid), "MeshCore-Setup-%02X%02X", _pub_key[0], _pub_key[1]);
bool ap_ok = false;
for (uint8_t attempt = 1; attempt <= 6 && !ap_ok; ++attempt) {
#ifdef WEBCONFIG_AP_PASSWORD
bool ap_ok = WiFi.softAP(_ap_ssid, WEBCONFIG_AP_PASSWORD);
ap_ok = WiFi.softAP(_ap_ssid, WEBCONFIG_AP_PASSWORD);
#else
bool ap_ok = WiFi.softAP(_ap_ssid);
ap_ok = WiFi.softAP(_ap_ssid);
#endif
if (ap_ok) break;
Serial.printf(
"WebConfig AP attempt %u failed: mode_ok=%d disconnect_ok=%d mode=%d heap=%u largest=%u\n",
(unsigned)attempt, mode_ok, disconnect_ok, (int)WiFi.getMode(),
(unsigned)ESP.getFreeHeap(),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
WiFi.softAPdisconnect(true);
delay(250);
mode_ok = WiFi.mode(WIFI_AP_STA);
}
if (!ap_ok) {
if (!promote_lan) WiFi.mode(WIFI_OFF);
strcpy(reply, "Err: failed to start AP");
@@ -782,7 +796,11 @@ void WebConfigServer::tick(uint32_t now) {
} else if (_connect_deadline && (int32_t)(now - _connect_deadline) >= 0) {
Serial.printf("WebConfig: WiFi '%s' unavailable; opening setup AP\n", _wifi_ssid);
const bool retry_saved_wifi = _wifi_ssid[0] != 0;
WiFi.disconnect(true);
// Keep the WiFi driver running while changing from STA to AP+STA.
// Powering it off here and starting an AP immediately can race the
// asynchronous ESP32-S3 netif teardown and make softAP() return false.
WiFi.disconnect(false, false);
delay(50);
_mode = MODE_OFF;
_connect_deadline = 0;
char ignored[160];
@@ -9,6 +9,7 @@ public:
class TestRadio : public mesh::Radio {
public:
int begins = 0;
uint8_t pending_rx[MAX_TRANS_UNIT];
int pending_rx_len = 0;
int send_starts = 0;
@@ -26,6 +27,7 @@ public:
uint8_t sent_raw[3][MAX_TRANS_UNIT] = {};
int sent_len[3] = {};
void begin() override { begins++; }
int recvRaw(uint8_t* dest, int max_len) override {
if (pending_rx_len == 0) return 0;
int len = pending_rx_len < max_len ? pending_rx_len : max_len;
@@ -118,8 +120,37 @@ public:
bool parse(mesh::Packet* packet, const uint8_t* raw, int len) {
return tryParsePacket(packet, raw, len);
}
void makeRadioAvailable(bool available) { setRadioAvailable(available); }
};
TEST(Dispatcher, UnavailableRadioDoesNotBlockStartupAndRejectsSends) {
RxReservePacketManager manager(4, 1);
TestClock clock;
TestRadio radio;
TestDispatcher dispatcher(radio, clock, manager);
dispatcher.makeRadioAvailable(false);
dispatcher.begin();
dispatcher.loop();
EXPECT_EQ(0, radio.begins);
EXPECT_EQ(0, radio.cad_set_calls);
mesh::Packet* packet = dispatcher.obtainNewPacket();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
packet->payload[0] = 0x42;
packet->payload_len = 1;
EXPECT_FALSE(dispatcher.sendPacket(packet, 0));
EXPECT_EQ(4, manager.getFreeCount());
EXPECT_EQ(packet, dispatcher.failed_packet);
dispatcher.makeRadioAvailable(true);
EXPECT_EQ(1, radio.begins);
clock.now = 1;
dispatcher.loop();
EXPECT_EQ(1, radio.cad_set_calls);
}
TEST(Packet, ReadFromRejectsTruncatedHeadersAndPaths) {
mesh::Packet packet;
const uint8_t one_byte[] = {ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT)};
+52 -10
View File
@@ -1,14 +1,33 @@
#include "TBeam1WBoard.h"
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <driver/gpio.h>
#include <driver/rtc_io.h>
void TBeam1WBoard::begin() {
ESP32Board::begin();
// Power on radio module (must be done before radio init)
// ESP32Board::enterDeepSleep() holds NSS high. The LilyGo factory firmware
// can also hold the dedicated radio LDO off. These pad holds survive wake
// and can survive a subsequent software reset, so release them on every
// startup before RadioLib tries to select the SX1262.
esp_err_t nss_hold_result = rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
esp_err_t dio_result = rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
esp_err_t power_hold_result = gpio_hold_dis((gpio_num_t)SX126X_POWER_EN);
MESH_DEBUG_PRINTLN("T-Beam 1W pad recovery: NSS=%d DIO1=%d LDO=%d",
nss_hold_result, dio_result, power_hold_result);
// The microSD card and radio share this SPI bus. Keep the card deselected
// before RadioLib starts; a floating SD CS can make the card drive MISO and
// make the SX1262 appear absent (all reads return 0xFF).
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
// A software reset can leave the external radio regulator enabled while the
// ESP32 and SX1262 are in different states. Force a real regulator cycle so
// flashing or rebooting does not require removing USB/battery power before
// RadioLib probes the chip.
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
radio_powered = true;
delay(10); // Allow radio to power up
powerCycleRadio();
// GPIO21/CTRL is initialized by RadioLib with the normal RXEN mode table.
// Do not drive it independently during board startup.
@@ -22,6 +41,18 @@ void TBeam1WBoard::begin() {
digitalWrite(FAN_CTRL_PIN, HIGH);
}
void TBeam1WBoard::powerCycleRadio() {
digitalWrite(SX126X_POWER_EN, LOW);
radio_powered = false;
delay(100);
int power_off_level = digitalRead(SX126X_POWER_EN);
digitalWrite(SX126X_POWER_EN, HIGH);
radio_powered = true;
delay(100);
MESH_DEBUG_PRINTLN("T-Beam 1W radio LDO cycle: off=%d on=%d",
power_off_level, digitalRead(SX126X_POWER_EN));
}
void TBeam1WBoard::onBeforeTransmit() {
// RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins()
digitalWrite(LED_PIN, HIGH); // TX LED on
@@ -66,16 +97,26 @@ void TBeam1WBoard::powerOff() {
void TBeam1WBoard::attachRadioDriver(CustomSX1262Wrapper* driver) {
radio_driver = driver;
// radio.std_init() installs the normal RXEN table, whose RX state is on.
// A setting saved while the radio was unavailable still needs to be pushed
// into that fresh table when the radio later recovers.
lna_driver_synced = lna_enabled;
}
bool TBeam1WBoard::setLoRaFemLnaEnabled(bool enable) {
// The normal/default ON state is already installed by radio.std_init().
// Avoid touching GPIO21 or reconfiguring the radio during boot unless the
// saved setting actually requests a different state.
if (lna_enabled == enable) {
return radio_driver != nullptr;
if (radio_driver == nullptr) {
// Preserve the requested setting while booting in radio-unavailable mode.
// It will be applied after a later successful probe.
lna_enabled = enable;
lna_driver_synced = false;
return true;
}
// The normal/default ON state is already installed by radio.std_init().
// Avoid touching GPIO21 or reconfiguring the radio unless the desired table
// differs from the one currently installed in the radio driver.
if (lna_driver_synced && lna_enabled == enable) return true;
if (radio_driver == nullptr || !radio_driver->setExternalRxLnaEnabled(enable)) {
return false;
}
@@ -83,11 +124,12 @@ bool TBeam1WBoard::setLoRaFemLnaEnabled(bool enable) {
// This is the requested RX-mode state. TX and standby always force CTRL low
// regardless of this preference, protecting both the LNA and PA paths.
lna_enabled = enable;
lna_driver_synced = true;
return true;
}
bool TBeam1WBoard::canControlLoRaFemLna() const {
return radio_driver != nullptr;
return true;
}
bool TBeam1WBoard::isLoRaFemLnaEnabled() const {
+2
View File
@@ -33,6 +33,7 @@ class TBeam1WBoard : public ESP32Board {
private:
bool radio_powered = false;
bool lna_enabled = true;
bool lna_driver_synced = false;
CustomSX1262Wrapper* radio_driver = nullptr;
public:
@@ -42,6 +43,7 @@ public:
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override;
void powerOff() override;
void powerCycleRadio();
void attachRadioDriver(CustomSX1262Wrapper* driver);
bool setLoRaFemLnaEnabled(bool enable) override;
+32 -5
View File
@@ -1,4 +1,6 @@
#include <Arduino.h>
#include <esp_random.h>
#include <helpers/ESP32TrueRandom.h>
#include "target.h"
TBeam1WBoard board;
@@ -9,6 +11,8 @@ TBeam1WBoard board;
#endif
static SPIClass spi;
static bool target_peripherals_initialized = false;
static bool target_radio_available = false;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
@@ -26,25 +30,48 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
#endif
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
if (!target_peripherals_initialized) {
fallback_clock.begin();
rtc_clock.begin(Wire);
// Initialize SPI for radio
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
// Initialize the shared SPI bus once. Repeated radio recovery probes only
// reset and initialize the SX1262 itself.
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
target_peripherals_initialized = true;
}
// GPS serial initialized by EnvironmentSensorManager::begin()
bool success = radio.std_init(&spi);
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",
digitalRead(SX126X_POWER_EN), digitalRead(P_LORA_NSS),
digitalRead(P_LORA_RESET), digitalRead(P_LORA_BUSY),
digitalRead(P_LORA_MISO), digitalRead(P_LORA_DIO_1),
digitalRead(SX126X_RXEN));
}
return success;
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
if (target_radio_available) {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}
class ESP32IdentityRNG : public mesh::RNG {
public:
void random(uint8_t* dest, size_t sz) override {
esp_fill_random(dest, sz);
mesh::mixESP32TrueRandom(dest, sz);
}
} rng;
return mesh::LocalIdentity(&rng);
}