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;