diff --git a/build.sh b/build.sh index e9db657fc..b39d13b7f 100755 --- a/build.sh +++ b/build.sh @@ -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 diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ca95099ec..fc5a5d5f0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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(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 diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index c471ce7f1..ce2bbc133 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index a1298d7c1..517a2bd10 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -6,6 +6,9 @@ #ifdef ESP32_PLATFORM #include "esp_bt.h" #endif +#ifdef TBEAM_1W +#include +#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 diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index f0948f013..17d28fe9e 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -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; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index a48700a68..0c5ec830b 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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(); diff --git a/src/helpers/esp32/WebConfigServer.cpp b/src/helpers/esp32/WebConfigServer.cpp index 68d34d6a7..159842d3a 100644 --- a/src/helpers/esp32/WebConfigServer.cpp +++ b/src/helpers/esp32/WebConfigServer.cpp @@ -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]; diff --git a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp index 1a2086a92..825e5e571 100644 --- a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp +++ b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp @@ -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)}; diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index f9a6b16c3..d791dccdf 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -1,14 +1,33 @@ #include "TBeam1WBoard.h" #include +#include +#include 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 { diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index 5362601af..ef20bb092 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -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; diff --git a/variants/lilygo_tbeam_1w/target.cpp b/variants/lilygo_tbeam_1w/target.cpp index 669fdf804..3e9ddaae6 100644 --- a/variants/lilygo_tbeam_1w/target.cpp +++ b/variants/lilygo_tbeam_1w/target.cpp @@ -1,4 +1,6 @@ #include +#include +#include #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); }