Fix ESP32 Full idle radio teardown and compact AP startup

This commit is contained in:
mikecarper committed 2026-10-06 11:46:10 -07:00
1 parent f1ffe53e8a
commit d0859f925d
29 files changed
+1724 -144

No files matched your search

+47 -11
View File
@@ -25,6 +25,9 @@ void companionPairingUiHilProbe();
#include "esp_pm.h"
#include "esp_sleep.h"
#include "esp_system.h"
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
#include <esp_bt_main.h>
#endif
#if defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE
#define COMPANION_IDF_PM_AVAILABLE 1
#else
@@ -3001,6 +3004,37 @@ public:
};
static CompanionWirelessBackend companion_wireless;
#if defined(ESP32_PLATFORM) && defined(ENABLE_USB_INTERFACE) \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& !defined(ETHERNET_ENABLED) && !defined(SERIAL_RX)
static bool companionNativeUsbLightSleepReady() {
// Full compiles every transport, but a compiled driver is not an active
// receiver. Preserve live services and pending replies while permitting
// the same short sleep slice when those services have been turned off.
if (companion_wireless.enabled() != 0 || interface_manager.hasPendingIO()
|| board.isOTAUpdateRunning() || board.isRadioTestActive()
|| sensors.gpsUsesSerialUart(1)) return false;
#if defined(BLE_PIN_CODE) && !CONFIG_IDF_TARGET_ESP32C6
// Stopped advertising is not a stopped controller. Manual light sleep
// requires the RF driver to be disabled, including during BLE teardown.
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED)
return false;
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
if (esp_bluedroid_get_status() == ESP_BLUEDROID_STATUS_ENABLED) return false;
#endif
#endif
#if defined(ESP32) && defined(WIFI_SSID)
// Catch an unfinished teardown even if the logical WiFi owner is off.
wifi_mode_t mode = WIFI_MODE_NULL;
const esp_err_t result = esp_wifi_get_mode(&mode);
if (result == ESP_OK) return mode == WIFI_MODE_NULL;
return result == ESP_ERR_WIFI_NOT_INIT || result == ESP_ERR_WIFI_NOT_STARTED;
#else
return true;
#endif
}
#endif
bool handleCompanionWirelessCommand(const char* command, char* reply, size_t size,
CompanionWirelessSource source) {
#if defined(BLE_PIN_CODE)
@@ -3698,8 +3732,10 @@ void loop() {
// The native-USB-only light-sleep path below bypasses ESP32Board::sleep.
can_sleep = can_sleep && !mesh::isUsbLoggingEnabled();
#endif
#if defined(NRF52_PLATFORM) \
|| (defined(ESP32_PLATFORM) && defined(ENABLE_USB_INTERFACE))
#if defined(ESP32_PLATFORM) && defined(ENABLE_USB_INTERFACE)
const bool usb_sleep_held = board.isUsbSleepHeld();
can_sleep = can_sleep && !usb_sleep_held;
#elif defined(NRF52_PLATFORM)
can_sleep = can_sleep && !board.isUsbHostConnected();
#endif
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
@@ -3714,22 +3750,22 @@ void loop() {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#elif defined(ESP32_PLATFORM)
#if COMPANION_IDF_PM_AVAILABLE
// Yield long enough for ESP-IDF automatic light sleep to enter when no
// driver holds a power-management lock.
vTaskDelay(pdMS_TO_TICKS(10));
#elif defined(ENABLE_USB_INTERFACE) \
#if defined(ENABLE_USB_INTERFACE) \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& !defined(BLE_PIN_CODE) && !defined(WIFI_SSID) \
&& !defined(ETHERNET_ENABLED) && !defined(SERIAL_RX)
// The stock Arduino core has no automatic light sleep. A short timer
// slice gives native-USB-only battery builds real light sleep without
// Automatic light sleep is disabled for native USB, even in a PM-enabled
// core. A short timer slice gives idle battery builds real light sleep without
// delaying radio, GPS, button, or newly attached USB work by more than the
// normal 10 ms loop cadence. can_sleep already proved no USB host is up.
if (esp_sleep_enable_timer_wakeup(10000ULL) != ESP_OK
if (!companionNativeUsbLightSleepReady()
|| esp_sleep_enable_timer_wakeup(10000ULL) != ESP_OK
|| esp_light_sleep_start() != ESP_OK) {
vTaskDelay(pdMS_TO_TICKS(10));
}
#elif COMPANION_IDF_PM_AVAILABLE
// Yield long enough for ESP-IDF automatic light sleep to enter when no
// driver holds a power-management lock.
vTaskDelay(pdMS_TO_TICKS(10));
#else
// Connected transports need their own modem sleep and must retain the
// normal FreeRTOS idle behavior.
+137 -9
View File
@@ -1,6 +1,11 @@
#include "MyMesh.h"
#if defined(ESP32_PLATFORM)
#include <helpers/esp32/BootFileSystem.h>
#if defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(LIGHTWEIGHT_WIFI_OTA) || (defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA))
#include <WiFi.h>
#include <esp_wifi.h>
#endif
#endif
#include <helpers/UsbLogging.h>
#include <helpers/HilStartupTrace.h>
@@ -10920,6 +10925,29 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
}
}
void MyMesh::suspendUnconfiguredSetupBridges() {
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
#ifdef WITH_MQTT_BRIDGE
if (_cli.getObserverPrefs()->wifi_ssid[0]) return;
#else
char ssid[33] = {};
WebConfigServer::loadStandaloneWiFi(ssid, sizeof(ssid), nullptr, 0);
if (ssid[0]) return;
#endif
_unconfigured_setup_espnow_suspended = true;
#ifdef WITH_ESPNOW_BRIDGE
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
if (espnow_bridge.isRunning()) espnow_bridge.end();
#else
if (bridge.isRunning()) bridge.end();
#endif
#endif
#ifdef WITH_MQTT_BRIDGE
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
#endif
#endif
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
if (_cli.getBoard()->isOTAUpdateRunning()) {
strcpy(reply, "Err: OTA server is running - 'stop ota' first");
@@ -10930,6 +10958,23 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
: "Err: webconfig already running");
return true;
}
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) {
strcpy(reply, "Error: WiFi disabled; use set wifi on or set 2.4ghz on");
return true;
}
#ifdef WITH_MQTT_BRIDGE
// A live MQTT worker cannot be rolled back immediately if its stop times
// out. Require an explicit stop before a portal that would take its radio.
if ((isMqttBridgeRunning() || isMqttBridgeStopping()) && (force_ap
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
|| _cli.getObserverPrefs()->wifi_ssid[0] == 0
#endif
)) {
strcpy(reply, isMqttBridgeStopping() ? "Err: MQTT bridge is stopping - retry shortly"
: "Err: MQTT bridge is running - 'set bridge off' first");
return true;
}
#endif
if (!_webconfig) {
void* mqtt_prefs = nullptr;
bool owns_wifi = true;
@@ -10947,21 +10992,38 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
}
}
const bool setup_was_suspended = _unconfigured_setup_espnow_suspended;
#ifdef WITH_ESPNOW_BRIDGE
const bool espnow_was_running = isEspNowBridgeRunning();
#endif
suspendUnconfiguredSetupBridges();
#ifdef WITH_MQTT_BRIDGE
_webconfig->updateWiFiOwnership(!mqtt_bridge
|| (!mqtt_bridge->isRunning() && !mqtt_bridge->isStopping()));
#endif
bool started;
if (force_ap) {
#ifdef WITH_MQTT_BRIDGE
if (mqtt_bridge && mqtt_bridge->isRunning()) {
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
return true;
started = _webconfig->startSetupMode(reply);
} else {
started = _webconfig->startAutoMode(reply);
}
if (!started) {
// A failed portal must not consume a live operator-started transport.
// Restore actual pre-start state, never the saved first-boot defaults.
_unconfigured_setup_espnow_suspended = setup_was_suspended;
bool restored = true;
#ifdef WITH_ESPNOW_BRIDGE
if (espnow_was_running && !isEspNowBridgeRunning()) {
restored = setEspNowBridgeState(true) && restored;
}
#endif
_webconfig->startSetupMode(reply);
} else {
_webconfig->startAutoMode(reply);
_unconfigured_setup_espnow_suspended = setup_was_suspended;
if (!restored) {
const size_t used = strlen(reply);
snprintf(reply + used, 160 - used, "; bridge resume failed");
}
}
return true;
}
@@ -12918,6 +12980,16 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
// the loop until reboot - otherwise a packet still queued here (busy /
// duty-limited channel) is lost when the flash spins the loop and reboots.
drainOutbound(OTA_TX_DRAIN_TIMEOUT_MS);
const bool mqtt_was_running = mqtt_bridge && mqtt_bridge->isRunning();
#ifdef WITH_ESPNOW_BRIDGE
const bool espnow_was_running = espnow_bridge.isRunning();
#endif
auto resume_paused_bridges = [&]() {
if (mqtt_was_running) setMqttBridgeState(true);
#ifdef WITH_ESPNOW_BRIDGE
if (espnow_was_running) setEspNowBridgeState(true);
#endif
};
setBridgeState(false);
// TODO: Replace this timed settle with a task-exit/join barrier once MQTT
// teardown can prove that the idle task has reclaimed the worker resources.
@@ -12930,13 +13002,13 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
if (mqtt_bridge && !mqtt_bridge->canFlashAfterStop()) {
mesh::usbConsolePort().printf("OTA: aborted, MQTT stop did not complete cleanly - resuming bridge\r\n");
otaAlert("OTA aborted: MQTT stop unclean, bridge resumed");
setBridgeState(true);
resume_paused_bridges();
} else if (!_cli.getBoard()->otaFromManifest(ota_resolve_base(_ota_update_channel), getFirmwareVer(), false, ota_reply)) {
mesh::usbConsolePort().printf("OTA: aborted, resuming bridge - %s\r\n", ota_reply);
char ota_alert_msg[160];
snprintf(ota_alert_msg, sizeof(ota_alert_msg), "OTA aborted: %s", ota_reply);
otaAlert(ota_alert_msg);
setBridgeState(true);
resume_paused_bridges();
}
// Success path: otaFromManifest() flashes and reboots into the new image
// (never returns), so there is no in-boot "success" alert - the START alert
@@ -12964,6 +13036,9 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
#endif
// is pending dirty contacts write needed?
#if defined(ESP32_PLATFORM)
serviceIdleWiFi();
#endif
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
const bool saved = acl.save(_fs);
if (saved) {
@@ -13546,9 +13621,59 @@ bool MyMesh::startNeighborDiscover(char* reply) {
#endif // WITH_MQTT_NEIGHBORS
// To check if there is pending work
#if defined(ESP32_PLATFORM)
void MyMesh::serviceIdleWiFi() {
#if (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(LIGHTWEIGHT_WIFI_OTA) || (defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA))) \
&& (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW) \
&& (!defined(MESH_ESPNOW_RADIO) || !MESH_ESPNOW_RADIO)
if (_cli.getBoard()->isOTAUpdateRunning()) return;
#ifdef WITH_WEBCONFIG
if (isWebConfigActive()) return;
#endif
#ifdef WITH_MQTT_BRIDGE
if (_ota_update_at) return;
if (mqtt_bridge && (mqtt_bridge->isRunning() || mqtt_bridge->isStopping())) return;
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (isEspNowBridgeRunning()) return;
#endif
// MQTT shutdown and WebConfig-to-OTA handoff retain STA intentionally.
// Reclaim it only after the last live owner is gone, using Arduino first
// so its private initialized/started cache agrees with the SDK driver.
if (WiFi.getMode() == WIFI_OFF && WiFi.channel() > 0
&& !WiFi.mode(WIFI_STA)) return;
wifi_mode_t sdk_mode = WIFI_MODE_NULL;
if (WiFi.getMode() == WIFI_OFF && esp_wifi_get_mode(&sdk_mode) != ESP_OK) return;
WiFi.setAutoReconnect(false);
WiFi.disconnect(false, false);
if (!WiFi.mode(WIFI_OFF)) return;
// A pure IDF ESP-NOW owner never created an Arduino facade, making OFF a
// no-op there. The ownership checks above also protect that startup path.
if (esp_wifi_get_mode(&sdk_mode) == ESP_OK) {
esp_wifi_stop();
esp_wifi_deinit();
}
#endif
}
#endif
bool MyMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (hasPendingOtaApply()) return true;
#ifdef WITH_WEBCONFIG
if (isWebConfigActive()) return true;
#endif
#if defined(ESP32_PLATFORM) \
&& (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(LIGHTWEIGHT_WIFI_OTA) || (defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA))) \
&& (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW) \
&& (!defined(MESH_ESPNOW_RADIO) || !MESH_ESPNOW_RADIO)
// Cleanup may fail while the Arduino facade already reports OFF. Require
// the live SDK driver to be gone before permitting manual light sleep.
wifi_mode_t sdk_mode = WIFI_MODE_NULL;
if (esp_wifi_get_mode(&sdk_mode) != ESP_ERR_WIFI_NOT_INIT) return true;
#endif
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_local_cli_output.busy()) return true;
#endif
@@ -13556,6 +13681,9 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
const AbstractBridge* active_bridge = activeBridge();
if (active_bridge && active_bridge->isRunning()) return true;
#ifdef WITH_MQTT_BRIDGE
if (mqtt_bridge && mqtt_bridge->isStopping()) return true;
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
if (isRs232BridgeRunning()) return true;
#endif
+19
View File
@@ -621,6 +621,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
// Retry ESP-NOW without taking over an active OTA session. MQTT creates its
// WiFi task asynchronously, so it must first associate on the shared channel.
bool startSharedEspNowBridgeIfReady() {
#ifdef WITH_WEBCONFIG
if (_unconfigured_setup_espnow_suspended) return false;
#endif
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
ESPNowBridge& transport = espnow_bridge;
if (!_prefs.espnow_bridge_enabled) return false;
@@ -650,10 +653,17 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
#endif
#ifdef WITH_WEBCONFIG
WebConfigServer* _webconfig = nullptr;
// A first-boot setup session may pause saved bridge intent for this boot.
// Its retry loop must not revive ESP-NOW after the AP window ends.
bool _unconfigured_setup_espnow_suspended = false;
void suspendUnconfiguredSetupBridges();
bool _wc_batch_active = false;
bool _wc_restart_pending = false;
uint8_t _wc_slot_restart_mask = 0;
#endif
#if defined(ESP32_PLATFORM)
void serviceIdleWiFi();
#endif
#if defined(WITH_MQTT_NEIGHBORS)
// Neighbor-scope discovery: a snapshot of the neighbor table overlaid with an
@@ -1393,6 +1403,9 @@ public:
}
if (espnow_bridge.isRunning()) return true;
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
_unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
#ifdef WITH_MQTT_BRIDGE
if (mqtt_bridge && mqtt_bridge->isRunning()) {
@@ -1450,6 +1463,9 @@ public:
// A standalone bridge shares its WiFi driver with the browser uploader.
// Preserve saved intent, but do not restart it until OTA has stopped.
if (enable && _cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
if (enable) _unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
#endif
// Disabling an already-absent heap-backed bridge is successful and must
@@ -1547,6 +1563,9 @@ public:
#else
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_RS232_BRIDGE)
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
_unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
#endif
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
+127 -11
View File
@@ -1,6 +1,11 @@
#include "MyMesh.h"
#if defined(ESP32_PLATFORM)
#include <helpers/esp32/BootFileSystem.h>
#if defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(LIGHTWEIGHT_WIFI_OTA) || (defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA))
#include <WiFi.h>
#include <esp_wifi.h>
#endif
#endif
#include <helpers/UsbLogging.h>
#if defined(ENABLE_OTA)
@@ -1948,6 +1953,25 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
}
}
void MyMesh::suspendUnconfiguredSetupBridges() {
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
#ifdef WITH_MQTT_BRIDGE
if (_cli.getObserverPrefs()->wifi_ssid[0]) return;
#else
char ssid[33] = {};
WebConfigServer::loadStandaloneWiFi(ssid, sizeof(ssid), nullptr, 0);
if (ssid[0]) return;
#endif
_unconfigured_setup_espnow_suspended = true;
#ifdef WITH_ESPNOW_BRIDGE
if (espnow_bridge.isRunning()) espnow_bridge.end();
#endif
#ifdef WITH_MQTT_BRIDGE
if (bridge && bridge->isRunning()) bridge->end();
#endif
#endif
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
if (_cli.getBoard()->isOTAUpdateRunning()) {
strcpy(reply, "Err: OTA server is running - 'stop ota' first");
@@ -1958,6 +1982,21 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
: "Err: webconfig already running");
return true;
}
if (mesh::wireless::control().blocked(mesh::wireless::WiFi)) {
strcpy(reply, "Error: WiFi disabled; use set wifi on or set 2.4ghz on");
return true;
}
#ifdef WITH_MQTT_BRIDGE
if ((isMqttBridgeRunning() || isMqttBridgeStopping()) && (force_ap
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
|| _cli.getObserverPrefs()->wifi_ssid[0] == 0
#endif
)) {
strcpy(reply, isMqttBridgeStopping() ? "Err: MQTT bridge is stopping - retry shortly"
: "Err: MQTT bridge is running - 'set bridge off' first");
return true;
}
#endif
if (!_webconfig) {
void* mqtt_prefs = nullptr;
bool owns_wifi = true;
@@ -1974,21 +2013,36 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
}
}
const bool setup_was_suspended = _unconfigured_setup_espnow_suspended;
#ifdef WITH_ESPNOW_BRIDGE
const bool espnow_was_running = isEspNowBridgeRunning();
#endif
suspendUnconfiguredSetupBridges();
#ifdef WITH_MQTT_BRIDGE
_webconfig->updateWiFiOwnership(!bridge
|| (!bridge->isRunning() && !bridge->isStopping()));
#endif
bool started;
if (force_ap) {
#ifdef WITH_MQTT_BRIDGE
if (bridge && bridge->isRunning()) {
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
return true;
started = _webconfig->startSetupMode(reply);
} else {
started = _webconfig->startAutoMode(reply);
}
if (!started) {
_unconfigured_setup_espnow_suspended = setup_was_suspended;
bool restored = true;
#ifdef WITH_ESPNOW_BRIDGE
if (espnow_was_running && !isEspNowBridgeRunning()) {
restored = setEspNowBridgeState(true) && restored;
}
#endif
_webconfig->startSetupMode(reply);
} else {
_webconfig->startAutoMode(reply);
_unconfigured_setup_espnow_suspended = setup_was_suspended;
if (!restored) {
const size_t used = strlen(reply);
snprintf(reply + used, 160 - used, "; bridge resume failed");
}
}
return true;
}
@@ -2780,6 +2834,9 @@ void MyMesh::loop() {
// had deliberately stopped.
mesh::usbConsolePort().printf("OTA: starting update\r\n");
const bool bridge_was_running = bridge && bridge->isRunning();
#ifdef WITH_ESPNOW_BRIDGE
const bool espnow_was_running = espnow_bridge.isRunning();
#endif
drainOutbound(OTA_TX_DRAIN_TIMEOUT_MS);
// A previously timed-out stop leaves the lifecycle dirty even though the
@@ -2787,11 +2844,15 @@ void MyMesh::loop() {
// later request must not bypass the teardown barrier merely because there
// is no active task left to stop this time.
bool may_flash = !bridge || bridge->canFlashAfterStop();
if (bridge_was_running) {
if (bridge_was_running
#ifdef WITH_ESPNOW_BRIDGE
|| espnow_was_running
#endif
) {
setBridgeState(false);
// OTA must not write after a forced/timed-out MQTT shutdown: its TLS/heap
// ownership is uncertain until a subsequent clean start/stop cycle.
may_flash = bridge && bridge->canFlashAfterStop();
may_flash = !bridge || bridge->canFlashAfterStop();
if (!may_flash) {
mesh::usbConsolePort().printf("OTA: aborted, MQTT stop did not complete cleanly\r\n");
} else {
@@ -2811,9 +2872,12 @@ void MyMesh::loop() {
// Successful otaFromManifest() reboots and never returns. Restore only a
// bridge that was running before this attempt; leave an intentionally
// stopped bridge stopped after any OTA refusal or download failure.
if (!may_flash && bridge_was_running) {
if (!may_flash) {
mesh::usbConsolePort().printf("OTA: resuming bridge\r\n");
setBridgeState(true);
if (bridge_was_running) setMqttBridgeState(true);
#ifdef WITH_ESPNOW_BRIDGE
if (espnow_was_running) setEspNowBridgeState(true);
#endif
}
}
#endif
@@ -2837,6 +2901,10 @@ void MyMesh::loop() {
}
}
#if defined(ESP32_PLATFORM)
serviceIdleWiFi();
#endif
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
// update uptime
@@ -2981,14 +3049,62 @@ uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const {
}
// To check if there is pending work
#if defined(ESP32_PLATFORM)
void MyMesh::serviceIdleWiFi() {
#if (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(LIGHTWEIGHT_WIFI_OTA) || (defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA))) \
&& (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW) \
&& (!defined(MESH_ESPNOW_RADIO) || !MESH_ESPNOW_RADIO)
if (_cli.getBoard()->isOTAUpdateRunning()) return;
#ifdef WITH_WEBCONFIG
if (isWebConfigActive()) return;
#endif
#ifdef WITH_MQTT_BRIDGE
if (_ota_update_at) return;
if (bridge && (bridge->isRunning() || bridge->isStopping())) return;
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (isEspNowBridgeRunning()) return;
#endif
if (WiFi.getMode() == WIFI_OFF && WiFi.channel() > 0
&& !WiFi.mode(WIFI_STA)) return;
wifi_mode_t sdk_mode = WIFI_MODE_NULL;
if (WiFi.getMode() == WIFI_OFF && esp_wifi_get_mode(&sdk_mode) != ESP_OK) return;
WiFi.setAutoReconnect(false);
WiFi.disconnect(false, false);
if (!WiFi.mode(WIFI_OFF)) return;
if (esp_wifi_get_mode(&sdk_mode) == ESP_OK) {
esp_wifi_stop();
esp_wifi_deinit();
}
#endif
}
#endif
bool MyMesh::hasPendingWork() const {
if (isDualRadioActive()) return true;
if (hasPendingOtaApply()) return true;
#ifdef WITH_WEBCONFIG
if (isWebConfigActive()) return true;
#endif
#if defined(ESP32_PLATFORM) \
&& (defined(WITH_WEBCONFIG) || defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(LIGHTWEIGHT_WIFI_OTA) || (defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA))) \
&& (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW) \
&& (!defined(MESH_ESPNOW_RADIO) || !MESH_ESPNOW_RADIO)
// Never sleep over an unowned driver whose cleanup failed or whose Arduino
// facade no longer reflects the live SDK state.
wifi_mode_t sdk_mode = WIFI_MODE_NULL;
if (esp_wifi_get_mode(&sdk_mode) != ESP_ERR_WIFI_NOT_INIT) return true;
#endif
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_local_cli_output.busy()) return true;
#endif
#if defined(WITH_BRIDGE)
if (bridge && bridge->isRunning()) return true; // bridge needs WiFi radio, can't sleep
#ifdef WITH_MQTT_BRIDGE
if (bridge && bridge->isStopping()) return true;
#endif
#if defined(WITH_ESPNOW_BRIDGE)
if (espnow_bridge.isRunning()) return true;
#endif
+12
View File
@@ -320,10 +320,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
#endif
#ifdef WITH_WEBCONFIG
WebConfigServer* _webconfig = nullptr;
bool _unconfigured_setup_espnow_suspended = false;
void suspendUnconfiguredSetupBridges();
bool _wc_batch_active = false;
bool _wc_restart_pending = false;
uint8_t _wc_slot_restart_mask = 0;
#endif
#if defined(ESP32_PLATFORM)
void serviceIdleWiFi();
#endif
void addPost(ClientInfo* client, const char* postData);
bool applySavedRadioParams();
@@ -579,6 +584,9 @@ public:
// MQTT's WiFi task associates asynchronously. ESP-NOW must wait for that
// station interface so it can share the AP-selected channel safely.
bool startSharedEspNowBridgeIfReady() {
#ifdef WITH_WEBCONFIG
if (_unconfigured_setup_espnow_suspended) return false;
#endif
if (espnow_bridge.isRunning()) return true;
if (!_prefs.espnow_bridge_enabled) return false;
// Keep the browser uploader's bridge pause in effect until OTA stops.
@@ -661,6 +669,10 @@ public:
return !espnow_bridge.isRunning();
}
if (espnow_bridge.isRunning()) return true;
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
_unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
if (bridge && bridge->isRunning()) {
startSharedEspNowBridgeIfReady();