diff --git a/lib/ble_interface/BLEInterface.cpp b/lib/ble_interface/BLEInterface.cpp index 34d92d2e..e7c1f6e2 100644 --- a/lib/ble_interface/BLEInterface.cpp +++ b/lib/ble_interface/BLEInterface.cpp @@ -10,7 +10,6 @@ #ifdef ARDUINO #include #include -#include #endif using namespace RNS; @@ -1136,12 +1135,13 @@ void BLEInterface::ble_task(void* param) { BLEInterface* self = static_cast(param); Serial.printf("BLE task started on core %d\n", xPortGetCoreID()); - // Subscribe BLE task to Task Watchdog — detects BLE deadlocks - esp_task_wdt_add(NULL); + // NOTE: BLE task is intentionally NOT subscribed to the FreeRTOS Task WDT. + // Blocking NimBLE GATT operations (service discovery, subscribe, read, write) + // each have ~30s internal timeouts. A full connect chain (connect + discover + + // enable notifications + identity read) can legitimately block 30-60s total, + // which exceeds the 30s WDT. NimBLE's own timeouts provide recovery. while (true) { - esp_task_wdt_reset(); - // Run the BLE loop (already has internal mutex protection) self->loop(); diff --git a/lib/ble_interface/platforms/NimBLEPlatform.cpp b/lib/ble_interface/platforms/NimBLEPlatform.cpp index 5b6cc79d..360e16d7 100644 --- a/lib/ble_interface/platforms/NimBLEPlatform.cpp +++ b/lib/ble_interface/platforms/NimBLEPlatform.cpp @@ -11,7 +11,6 @@ #include "Identity.h" #include #include -#include // WiFi coexistence: Check if WiFi is available and connected // This is used to add extra delays before BLE connection attempts @@ -310,7 +309,6 @@ void NimBLEPlatform::shutdown() { " active write operation(s)"); // DELAY RATIONALE: Shutdown wait polling - check every 100ms for write completion delay(100); - esp_task_wdt_reset(); } // Check if we timed out @@ -412,7 +410,6 @@ bool NimBLEPlatform::attemptHostReset() { uint32_t start = millis(); while (!ble_hs_synced() && (millis() - start) < 3000) { delay(50); - esp_task_wdt_reset(); } if (ble_hs_synced()) { @@ -539,7 +536,6 @@ bool NimBLEPlatform::pauseSlaveForMaster() { while (ble_gap_adv_active() && millis() - start < 2000) { // DELAY RATIONALE: Advertising stop polling - check completion every NimBLE scheduler tick (~10ms) delay(10); - esp_task_wdt_reset(); // Feed WDT during blocking wait } if (ble_gap_adv_active()) { @@ -574,7 +570,6 @@ bool NimBLEPlatform::pauseSlaveForMaster() { } // DELAY RATIONALE: Slave state polling - check completion every NimBLE scheduler tick (~10ms) delay(10); - esp_task_wdt_reset(); } WARNING("NimBLEPlatform: Timed out waiting for slave to become idle"); @@ -649,7 +644,6 @@ void NimBLEPlatform::enterErrorRecovery() { uint32_t sync_start = millis(); while (!ble_hs_synced() && (millis() - sync_start) < 5000) { delay(50); - esp_task_wdt_reset(); } if (ble_hs_synced()) { INFO("NimBLEPlatform: Host sync restored after " + @@ -676,7 +670,6 @@ void NimBLEPlatform::enterErrorRecovery() { uint32_t reset_start = millis(); while (!ble_hs_synced() && (millis() - reset_start) < 5000) { delay(50); - esp_task_wdt_reset(); } if (ble_hs_synced()) { INFO("NimBLEPlatform: Host-controller resync after " + @@ -1018,7 +1011,6 @@ void NimBLEPlatform::stopScan() { while (ble_gap_disc_active() && millis() - start < 1000) { // DELAY RATIONALE: Scan stop polling - check completion every NimBLE scheduler tick (~10ms) delay(10); - esp_task_wdt_reset(); } // Transition to IDLE @@ -1141,7 +1133,6 @@ bool NimBLEPlatform::connect(const BLEAddress& address, uint16_t timeout_ms) { while (ble_gap_conn_active() && millis() - start < 1000) { // DELAY RATIONALE: Service discovery polling - check completion per scheduler tick delay(10); - esp_task_wdt_reset(); } if (ble_gap_conn_active()) { ERROR("NimBLEPlatform: GAP connection still active after timeout"); @@ -1329,14 +1320,9 @@ bool NimBLEPlatform::connectNative(const BLEAddress& address, uint16_t timeout_m // discovery) that would deadlock the host task. _native_connect_pending = true; - // Feed WDT before blocking connect — NimBLE connect can take several seconds - // with WiFi coexistence, and the BLE task is subscribed to the 10s WDT - esp_task_wdt_reset(); - // Connect (blocking) — NimBLE handles GAP event management internally bool connected = client->connect(nimAddr, false); // deleteAttributes=false - esp_task_wdt_reset(); // Feed WDT after connect returns _native_connect_pending = false; if (!connected) { @@ -1360,7 +1346,8 @@ bool NimBLEPlatform::connectNative(const BLEAddress& address, uint16_t timeout_m " MTU=" + std::to_string(negotiated_mtu)); // Fire _on_connected from THIS task (BLEInterface loop), not the host task. - // This allows the callback to safely do blocking GATT operations. + // This allows the callback to safely do blocking GATT operations + // (service discovery, notification enable, identity read/write). if (_on_connected) { ConnectionHandle conn = getConnection(conn_handle); _on_connected(conn); @@ -1429,6 +1416,10 @@ bool NimBLEPlatform::requestMTU(uint16_t conn_handle, uint16_t mtu) { } bool NimBLEPlatform::discoverServices(uint16_t conn_handle) { + if (!ble_hs_synced()) { + return false; + } + auto client_it = _clients.find(conn_handle); if (client_it == _clients.end() || !client_it->second) { return false; @@ -1436,7 +1427,7 @@ bool NimBLEPlatform::discoverServices(uint16_t conn_handle) { NimBLEClient* client = client_it->second; - // Get our service + // Get our service — blocking GATT operation NimBLERemoteService* service = client->getService(UUID::SERVICE); if (!service) { ERROR("NimBLEPlatform: Service not found"); @@ -1447,7 +1438,7 @@ bool NimBLEPlatform::discoverServices(uint16_t conn_handle) { return false; } - // Get characteristics + // Get characteristics — each is a blocking GATT operation NimBLERemoteCharacteristic* rxChar = service->getCharacteristic(UUID::RX_CHAR); NimBLERemoteCharacteristic* txChar = service->getCharacteristic(UUID::TX_CHAR); NimBLERemoteCharacteristic* idChar = service->getCharacteristic(UUID::IDENTITY_CHAR); @@ -1507,7 +1498,6 @@ bool NimBLEPlatform::startAdvertising() { uint32_t sync_wait = millis(); while (!ble_hs_synced() && (millis() - sync_wait) < 1000) { delay(50); - esp_task_wdt_reset(); } if (!ble_hs_synced()) { DEBUG("NimBLEPlatform: Host not synced, cannot start advertising"); @@ -1578,7 +1568,6 @@ void NimBLEPlatform::stopAdvertising() { while (ble_gap_adv_active() && millis() - start < 1000) { // DELAY RATIONALE: Loop iteration throttle - prevent tight loop CPU consumption delay(10); - esp_task_wdt_reset(); } // Transition to IDLE @@ -1635,6 +1624,12 @@ void NimBLEPlatform::setIdentityData(const Bytes& identity) { //============================================================================= bool NimBLEPlatform::write(uint16_t conn_handle, const Bytes& data, bool response) { + // Guard against use-after-free: during a host reset, NimBLE invalidates + // client objects on core 0 while we may still hold stale pointers. + if (!ble_hs_synced()) { + return false; + } + auto conn_it = _connections.find(conn_handle); if (conn_it == _connections.end()) { DEBUG("NimBLEPlatform::write: no connection for handle " + std::to_string(conn_handle)); @@ -1695,6 +1690,10 @@ bool NimBLEPlatform::write(uint16_t conn_handle, const Bytes& data, bool respons bool NimBLEPlatform::writeCharacteristic(uint16_t conn_handle, uint16_t char_handle, const Bytes& data, bool response) { + if (!ble_hs_synced()) { + return false; + } + auto client_it = _clients.find(conn_handle); if (client_it == _clients.end() || !client_it->second) { return false; @@ -1723,6 +1722,11 @@ bool NimBLEPlatform::writeCharacteristic(uint16_t conn_handle, uint16_t char_han bool NimBLEPlatform::read(uint16_t conn_handle, uint16_t char_handle, std::function callback) { + if (!ble_hs_synced()) { + if (callback) callback(OperationResult::DISCONNECTED, Bytes()); + return false; + } + auto client_it = _clients.find(conn_handle); if (client_it == _clients.end() || !client_it->second) { if (callback) callback(OperationResult::NOT_FOUND, Bytes()); @@ -1762,6 +1766,10 @@ bool NimBLEPlatform::read(uint16_t conn_handle, uint16_t char_handle, } bool NimBLEPlatform::enableNotifications(uint16_t conn_handle, bool enable) { + if (!ble_hs_synced()) { + return false; + } + auto client_it = _clients.find(conn_handle); if (client_it == _clients.end() || !client_it->second) { return false; @@ -1800,7 +1808,7 @@ bool NimBLEPlatform::enableNotifications(uint16_t conn_handle, bool enable) { } bool NimBLEPlatform::notify(uint16_t conn_handle, const Bytes& data) { - if (!_tx_char) { + if (!ble_hs_synced() || !_tx_char) { return false; } @@ -1809,7 +1817,7 @@ bool NimBLEPlatform::notify(uint16_t conn_handle, const Bytes& data) { } bool NimBLEPlatform::notifyAll(const Bytes& data) { - if (!_tx_char) { + if (!ble_hs_synced() || !_tx_char) { return false; } diff --git a/src/main.cpp b/src/main.cpp index bbdfa67f..440e27de 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -656,9 +656,10 @@ void setup_lvgl_and_ui() { INFO("LVGL initialized"); // Start LVGL on its own FreeRTOS task for responsive UI - // Core 1, priority 2 (higher than default to ensure smooth rendering) - // All LVGL API calls from other code are protected by mutex (LVGL_LOCK) - if (!UI::LVGL::LVGLInit::start_task(2, 1)) { + // Core 1, priority 1 (same as loopTask — round-robin scheduling). + // Previously priority 2, but this starved loopTask of CPU time during + // heavy rendering, causing 30s WDT timeouts on loopTask. + if (!UI::LVGL::LVGLInit::start_task(1, 1)) { ERROR("Failed to start LVGL task!"); while (1) delay(1000); }