Skip to content

GATTServerConnection.close() does not invoke GATTServer.onDisconnect #1633

@stc1988

Description

@stc1988

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:

connection.close();

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:

  1. accept a temporary peripheral connection from iOS;
  2. pair and record the peer address;
  3. call connection.close();
  4. wait for onDisconnect;
  5. 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:

  1. Mark the connection as closing.
  2. Call ble_gap_terminate().
  3. Keep the connection in server->connections.
  4. When NimBLE reports the disconnect, invoke GATTServer.onDisconnect(connection).
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions