Build environment: macOS
Moddable SDK version: 8.2.0 + #1625
Target device: Moddable Six
Summary
On ESP32, calling GATTServerConnection.close() terminates the BLE link, but the corresponding GATTServer.onDisconnect callback is not invoked.
Expected Behavior
GATTServer.onDisconnect(connection) should be invoked once whenever an established server connection ends, regardless of who initiated the disconnection:
- the remote peer disconnects;
- the BLE stack reports a link loss;
- the application calls
GATTServerConnection.close().
GATTServerConnection.close() requests a BLE disconnection, but the physical link termination is asynchronous. The callback should therefore be delivered when NimBLE reports the disconnect event, not synchronously from close().
This gives applications one consistent place to:
- update connection state;
- release per-connection resources;
- restart advertising;
- begin a BLE role transition after the previous link has ended.
When the application calls:
the ESP32 implementation immediately removes and frees the native connection record:
|
void xs_gattserverconnection_close(xsMachine *the) |
|
{ |
|
BLEGATTServerConnection connection = xsmcGetHostDataValidate(xsThis, xs_gattserverconnection_destructor); |
|
BLEServer server = connection->server; |
|
ble_gap_terminate(connection->conn_handle, BLE_ERR_REM_USER_CONN_TERM); |
|
|
|
xsmcSetHostData(xsThis, C_NULL); |
|
|
|
if (connection == server->connections) |
|
server->connections = connection->next; |
|
else { |
|
BLEGATTServerConnection walker = server->connections; |
|
while (walker->next) |
|
walker = walker->next; |
|
walker->next = connection->next; |
|
} |
|
|
|
c_free(connection); |
|
} |
Later, when NimBLE reports the actual disconnect, deliverDisconnect() cannot find the connection and returns without invoking onDisconnect:
|
static void deliverDisconnect(void *theIn, void *refcon, uint8_t *message, uint16_t messageLength) |
|
{ |
|
xsMachine *the = theIn; |
|
BLEServer server = refcon; |
|
struct ble_gap_conn_desc *conn = (struct ble_gap_conn_desc *)message; |
|
BLEGATTServerConnection connection = findConnection(server, conn->conn_handle); |
|
if (C_NULL == connection) |
|
return; // ready called close on connection instance |
|
|
As a result, remotely initiated disconnections invoke onDisconnect, while locally initiated disconnections through connection.close() do not.
Application Impact
An application that waits for onDisconnect before starting its next operation remains blocked after calling connection.close().
For example, an Apple Media Service accessory may:
- accept a temporary peripheral connection from iOS;
- pair and record the peer address;
- call
connection.close();
- wait for
onDisconnect;
- connect back to the same peer as a GATT client.
Step 4 never occurs with the current ESP32 implementation. A timer can work around this behavior, but it does not confirm that the previous BLE link has actually ended.
Proposed SDK Behavior
xs_gattserverconnection_close() should request termination without removing the connection record:
- Mark the connection as closing.
- Call
ble_gap_terminate().
- Keep the connection in
server->connections.
- When NimBLE reports the disconnect, invoke
GATTServer.onDisconnect(connection).
- Invalidate the JavaScript host data and free the native record after the callback.
The closing flag should make repeated calls to close() harmless while termination is pending.
Example structure:
struct BLEGATTServerConnectionRecord {
struct BLEGATTServerConnectionRecord *next;
struct BLEServerRecord *server;
xsSlot *obj;
uint16_t conn_handle;
uint16_t maximumWrite;
uint8_t closing;
};
Example close behavior:
void xs_gattserverconnection_close(xsMachine *the)
{
BLEGATTServerConnection connection =
xsmcGetHostDataValidate(xsThis, xs_gattserverconnection_destructor);
if (connection->closing)
return;
connection->closing = 1;
ble_gap_terminate(
connection->conn_handle,
BLE_ERR_REM_USER_CONN_TERM
);
}
The existing deliverDisconnect() path can then remain responsible for callback delivery, list removal, host-data invalidation, and freeing the connection.
I’m using this approach and it seems to work well, but I’m not very familiar with the native implementation side, so I’m not confident whether this is the right approach.
Other information
appleMediaService.zip
This is the AMS application that reproduces the issue. It’s possible that my implementation is incorrect, but there are two other issues besides the one discussed here:
- The device cannot be discovered from the iOS Bluetooth settings screen, so I connect to it using an app such as nRF Connect.
- After pairing with the BLE server, onSecure is not triggered, so I added an encrypted Battery Service to force encryption.
Build environment: macOS
Moddable SDK version: 8.2.0 + #1625
Target device: Moddable Six
Summary
On ESP32, calling
GATTServerConnection.close()terminates the BLE link, but the correspondingGATTServer.onDisconnectcallback is not invoked.Expected Behavior
GATTServer.onDisconnect(connection)should be invoked once whenever an established server connection ends, regardless of who initiated the disconnection:GATTServerConnection.close().GATTServerConnection.close()requests a BLE disconnection, but the physical link termination is asynchronous. The callback should therefore be delivered when NimBLE reports the disconnect event, not synchronously fromclose().This gives applications one consistent place to:
When the application calls:
the ESP32 implementation immediately removes and frees the native connection record:
moddable/modules/io/ble/server/esp32/bleserver.c
Lines 1129 to 1147 in 52fe713
Later, when NimBLE reports the actual disconnect,
deliverDisconnect()cannot find the connection and returns without invokingonDisconnect:moddable/modules/io/ble/server/esp32/bleserver.c
Lines 863 to 871 in 52fe713
As a result, remotely initiated disconnections invoke
onDisconnect, while locally initiated disconnections throughconnection.close()do not.Application Impact
An application that waits for
onDisconnectbefore starting its next operation remains blocked after callingconnection.close().For example, an Apple Media Service accessory may:
connection.close();onDisconnect;Step 4 never occurs with the current ESP32 implementation. A timer can work around this behavior, but it does not confirm that the previous BLE link has actually ended.
Proposed SDK Behavior
xs_gattserverconnection_close()should request termination without removing the connection record:ble_gap_terminate().server->connections.GATTServer.onDisconnect(connection).The closing flag should make repeated calls to
close()harmless while termination is pending.Example structure:
Example close behavior:
The existing
deliverDisconnect()path can then remain responsible for callback delivery, list removal, host-data invalidation, and freeing the connection.I’m using this approach and it seems to work well, but I’m not very familiar with the native implementation side, so I’m not confident whether this is the right approach.
Other information
appleMediaService.zip
This is the AMS application that reproduces the issue. It’s possible that my implementation is incorrect, but there are two other issues besides the one discussed here: