From 2cc9441f0a76d1e83da9fb7df26e61688f8b111a Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Tue, 3 Mar 2026 18:34:40 -0500 Subject: [PATCH] BLE stability: desync connect cooldown prevents crash-on-connect Add 30-second cooldown after NimBLE host desync recovery before allowing new connection attempts. During desync, client->connect() blocks waiting for a host-task completion event that never arrives, causing WDT crashes. The cooldown skips connection attempts while the host is desynced or recently recovered. Also adds ESP reset reason logging at boot to diagnose crash types (WDT, panic, brownout, etc.) in soak test logs. Soak test results: Run 3 (before) had 17 reboots in ~4 hours with a 12-crash-in-14-minutes loop. Run 4 (after) has 1 early reboot then 19+ hours of continuous uptime with the same desync frequency. Co-Authored-By: Claude Opus 4.6 --- .../platforms/NimBLEPlatform.cpp | 9 ++++++++ lib/ble_interface/platforms/NimBLEPlatform.h | 2 ++ src/main.cpp | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/lib/ble_interface/platforms/NimBLEPlatform.cpp b/lib/ble_interface/platforms/NimBLEPlatform.cpp index f04ffe61..b12744e7 100644 --- a/lib/ble_interface/platforms/NimBLEPlatform.cpp +++ b/lib/ble_interface/platforms/NimBLEPlatform.cpp @@ -859,6 +859,7 @@ bool NimBLEPlatform::startScan(uint16_t duration_ms) { (reset_reason != 0 ? " (nimble_reason=" + std::to_string(reset_reason) + ")" : "")); _host_desync_since = 0; _host_reset_attempts = 0; + _last_desync_recovery = millis(); // Start cooldown before allowing connections } // Log GAP hardware state before checking @@ -1014,6 +1015,14 @@ bool NimBLEPlatform::isScanning() const { bool NimBLEPlatform::connect(const BLEAddress& address, uint16_t timeout_ms) { NimBLEAddress nimAddr = toNimBLE(address); + // Skip connections during desync cooldown — connecting while the NimBLE + // stack is recovering from a desync can hang client->connect() (the host + // task can't process the completion event), leading to WDT crashes. + if (_host_desync_since != 0 || (_last_desync_recovery > 0 && millis() - _last_desync_recovery < DESYNC_CONNECT_COOLDOWN_MS)) { + DEBUG("NimBLEPlatform: Skipping connect during desync cooldown"); + return false; + } + // Rate limit connections to avoid overwhelming the BLE stack // Non-blocking: return false if too soon, caller can retry later static unsigned long last_connect_time = 0; diff --git a/lib/ble_interface/platforms/NimBLEPlatform.h b/lib/ble_interface/platforms/NimBLEPlatform.h index 97ff2330..fbe88f5e 100644 --- a/lib/ble_interface/platforms/NimBLEPlatform.h +++ b/lib/ble_interface/platforms/NimBLEPlatform.h @@ -279,11 +279,13 @@ private: uint8_t _conn_establish_fail_count = 0; // rc=574 connection establishment failures unsigned long _last_full_recovery_time = 0; unsigned long _host_desync_since = 0; // millis() when host first lost sync (0 = synced) + unsigned long _last_desync_recovery = 0; // millis() when last desync recovered (for connect cooldown) uint8_t _host_reset_attempts = 0; // ble_hs_sched_reset attempts since last sync static constexpr uint8_t SCAN_FAIL_RECOVERY_THRESHOLD = 10; static constexpr uint8_t LIGHTWEIGHT_RESET_MAX_FAILS = 3; static constexpr uint8_t CONN_ESTABLISH_FAIL_THRESHOLD = 5; static constexpr unsigned long FULL_RECOVERY_COOLDOWN_MS = 60000; // 60 seconds + static constexpr unsigned long DESYNC_CONNECT_COOLDOWN_MS = 30000; // Don't connect for 30s after desync recovery static constexpr unsigned long HOST_DESYNC_REBOOT_MS = 60000; // Reboot after 60s desync (no connections) bool recoverBLEStack(); bool attemptHostReset(); diff --git a/src/main.cpp b/src/main.cpp index 3cbad586..578dc56f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1196,6 +1196,29 @@ void setup() { INFO("╚══════════════════════════════════════╝"); INFO(""); + // Log ESP reset reason — helps diagnose crashes in soak tests + { + esp_reset_reason_t reason = esp_reset_reason(); + const char* reason_str = "UNKNOWN"; + switch (reason) { + case ESP_RST_POWERON: reason_str = "POWERON"; break; + case ESP_RST_SW: reason_str = "SOFTWARE"; break; + case ESP_RST_PANIC: reason_str = "PANIC"; break; + case ESP_RST_INT_WDT: reason_str = "INT_WDT"; break; + case ESP_RST_TASK_WDT: reason_str = "TASK_WDT"; break; + case ESP_RST_WDT: reason_str = "WDT"; break; + case ESP_RST_DEEPSLEEP: reason_str = "DEEPSLEEP"; break; + case ESP_RST_BROWNOUT: reason_str = "BROWNOUT"; break; + case ESP_RST_SDIO: reason_str = "SDIO"; break; + default: break; + } + if (reason != ESP_RST_POWERON) { + WARNING("Reset reason: " + std::string(reason_str) + " (" + std::to_string(reason) + ")"); + } else { + INFO("Reset reason: " + std::string(reason_str)); + } + } + // Check for LXST crash breadcrumb from previous boot { Preferences _dbg;