@@ -904,6 +904,52 @@ void BLECharacteristicCallbacks::onWrite(BLECharacteristic *pCharacteristic, esp
904904
905905#if defined(CONFIG_NIMBLE_ENABLED)
906906
907+ /* *
908+ * @brief Process a deferred write callback.
909+ *
910+ * This function is called as a FreeRTOS task to execute the onWrite callback
911+ * after the write response has been sent to the client. This maintains backwards
912+ * compatibility with Bluedroid, where the write response is sent before the
913+ * onWrite callback is invoked.
914+ *
915+ * The delay is based on the connection interval to ensure the write response
916+ * packet has been transmitted over the air before the callback executes.
917+ *
918+ * See: https://github.com/espressif/arduino-esp32/issues/11938
919+ */
920+ void BLECharacteristic::processDeferredWriteCallback (void *pvParameters) {
921+ DeferredWriteCallback *pCallback = (DeferredWriteCallback *)pvParameters;
922+
923+ // Get connection parameters to calculate appropriate delay
924+ ble_gap_conn_desc desc;
925+ int rc = ble_gap_conn_find (pCallback->conn_handle , &desc);
926+
927+ if (rc == 0 ) {
928+ // Connection interval is in units of 1.25ms
929+ // Wait for at least one connection interval to ensure the write response
930+ // has been transmitted. Add a small buffer for processing.
931+ uint16_t intervalMs = (desc.conn_itvl * 125 ) / 100 ; // Convert to milliseconds
932+ uint16_t delayMs = intervalMs + 5 ; // Add 5ms buffer
933+
934+ log_v (" Deferring write callback by %dms (conn_interval=%d units, %dms)" , delayMs, desc.conn_itvl , intervalMs);
935+ vTaskDelay (pdMS_TO_TICKS (delayMs));
936+ } else {
937+ // If we can't get connection parameters, use a conservative default
938+ // Most connections use 7.5-30ms intervals, so 50ms should be safe
939+ log_w (" Could not get connection parameters, using default 50ms delay" );
940+ vTaskDelay (pdMS_TO_TICKS (50 ));
941+ }
942+
943+ // Call the onWrite callback now that the response has been transmitted
944+ pCallback->pCharacteristic ->m_pCallbacks ->onWrite (pCallback->pCharacteristic , &pCallback->desc );
945+
946+ // Free the allocated memory
947+ delete pCallback;
948+
949+ // Delete this one-shot task
950+ vTaskDelete (NULL );
951+ }
952+
907953int BLECharacteristic::handleGATTServerEvent (uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
908954 const ble_uuid_t *uuid;
909955 int rc;
@@ -955,7 +1001,29 @@ int BLECharacteristic::handleGATTServerEvent(uint16_t conn_handle, uint16_t attr
9551001 rc = ble_gap_conn_find (conn_handle, &desc);
9561002 assert (rc == 0 );
9571003 pCharacteristic->setValue (buf, len);
958- pCharacteristic->m_pCallbacks ->onWrite (pCharacteristic, &desc);
1004+
1005+ // Defer the onWrite callback to maintain backwards compatibility with Bluedroid.
1006+ // In Bluedroid, the write response is sent BEFORE the onWrite callback is invoked.
1007+ // In NimBLE, the response is sent implicitly when this function returns.
1008+ // By deferring the callback to a separate task with a delay based on the connection
1009+ // interval, we ensure the response packet is transmitted before the callback executes.
1010+ // See: https://github.com/espressif/arduino-esp32/issues/11938
1011+ DeferredWriteCallback *pCallback = new DeferredWriteCallback ();
1012+ pCallback->pCharacteristic = pCharacteristic;
1013+ pCallback->desc = desc;
1014+ pCallback->conn_handle = conn_handle;
1015+
1016+ // Create a one-shot task to execute the callback after the response is transmitted
1017+ // Using priority 1 (low priority) and sufficient stack for callback operations
1018+ // Note: Stack must be large enough to handle notify() calls from within onWrite()
1019+ xTaskCreate (
1020+ processDeferredWriteCallback,
1021+ " BLEWriteCB" ,
1022+ 4096 , // Stack size - increased to handle notify() operations
1023+ pCallback,
1024+ 1 , // Priority (low)
1025+ NULL // Task handle (not needed for one-shot task)
1026+ );
9591027
9601028 return 0 ;
9611029 }
0 commit comments