Skip to content

Fix: onDisconnect not fired when peer powers off with 0x3E disconnect reason#1151

Draft
Copilot wants to merge 2 commits into
masterfrom
copilot/fix-peripheral-disconnect-event
Draft

Fix: onDisconnect not fired when peer powers off with 0x3E disconnect reason#1151
Copilot wants to merge 2 commits into
masterfrom
copilot/fix-peripheral-disconnect-event

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 14, 2026

In 2.5.0, the connection retry logic for BLE_ERR_CONN_ESTABLISHMENT (0x3E) incorrectly activates when a previously-established connection drops with that reason code — which happens when the peer powers off within the first 6 connection events (the BLE LL "connection establishment phase"). This suppresses onDisconnect entirely, instead calling onConnectFail after up to 60+ seconds of retries (2 retries × 30s timeout). Applications with reconnect logic in onDisconnect never recover.

Changes

  • src/NimBLEClient.cpp — In BLE_GAP_EVENT_DISCONNECT, capture wasConnected = (m_connStatus == CONNECTED) before any state mutation:
    • Retry on 0x3E only when !wasConnected — i.e., the connection was never confirmed at the host level. A 0x3E on an established connection is a genuine disconnect, not a retryable establishment failure.
    • Call onDisconnect (not onConnectFail) when wasConnected && rc == 0x3E, so the semantics match what the application observes: a working connection that went away.
    • Apply the same isConnectFail flag consistently to the deleteOnConnectFail / deleteOnDisconnect deletion logic.
// Before fix: retried on 0x3E regardless of whether connection was established
if (rc == connEstablishFailReason && pClient->m_connectFailRetryCount < pClient->m_config.connectFailRetries) { ... }

// After fix: only retry when the connection was never established
const bool wasConnected = (pClient->m_connStatus == CONNECTED);
if (rc == connEstablishFailReason && !wasConnected &&
    pClient->m_connectFailRetryCount < pClient->m_config.connectFailRetries) { ... }

const bool isConnectFail = (rc == connEstablishFailReason && !wasConnected);
if (isConnectFail) {
    pClient->m_pClientCallbacks->onConnectFail(pClient, rc);
} else {
    pClient->m_pClientCallbacks->onDisconnect(pClient, rc);  // now fires correctly for 0x3E post-connect
}

… after established connection

When a BLE connection was established (BLE_GAP_EVENT_CONNECT with status 0) and then
disconnects with BLE_ERR_CONN_ESTABLISHMENT (0x3E) - which can happen when the peer
powers off within the first 6 connection events - the 2.5.0 retry logic incorrectly
suppressed onDisconnect and called onConnectFail after retries (up to 60+ seconds later).

Fix: only retry on 0x3E if the connection was never established (m_connStatus != CONNECTED).
When the connection was previously established, call onDisconnect (not onConnectFail) so
user reconnect logic fires correctly.

Agent-Logs-Url: https://github.com/h2zero/NimBLE-Arduino/sessions/ca79e82b-2ceb-4ae8-83d3-c6344ed13cea

Co-authored-by: h2zero <32826625+h2zero@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix peripheral not receiving disconnect event on sleep in 2.5.0 Fix: onDisconnect not fired when peer powers off with 0x3E disconnect reason May 15, 2026
Copilot finished work on behalf of h2zero May 15, 2026 00:22
Copilot AI requested a review from h2zero May 15, 2026 00:22
@h2zero
Copy link
Copy Markdown
Owner

h2zero commented May 15, 2026

@copilot how does this address the fact the server device doesn't know about the disconnect?
Also the 0x3E error should not occur unless the connection was not confirmed in the first place.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 15, 2026

@copilot how does this address the fact the server device doesn't know about the disconnect?
Also the 0x3E error should not occur unless the connection was not confirmed in the first place.

The fix addresses the client-side (NimBLEClient) scenario where the peripheral devices are running NimBLEClient and connecting to the central hub. It does not change NimBLEServer's disconnect handling (which already calls onDisconnect for all reasons including 0x3E).

Regarding 0x3E after a confirmed connection: looking at the NimBLE controller code (ble_ll_conn.c), BLE_GAP_EVENT_CONNECT (status=0) is fired by the HCI connection complete event at line 2914 while the LL connection is still in BLE_LL_CONN_STATE_CREATED (line 2814) — before any data PDU is exchanged. The state only transitions to BLE_LL_CONN_STATE_ESTABLISHED when the first packet is received (line 3478). The 0x3E timeout fires if no PDU arrives in the first 6 connection events (line 3055-3057), meaning 0x3E in the disconnect handler always follows a BLE_GAP_EVENT_CONNECT with status=0, so wasConnected is always true at that point.

In 2.4.0, 0x3E called onDisconnect (no retry). In 2.5.0, the new retry code suppresses onDisconnect and calls onConnectFail after retries — breaking any user reconnect logic that lives in onDisconnect. My fix restores the 2.4.0 behaviour: when BLE_GAP_EVENT_CONNECT was already received (wasConnected = true), treat subsequent 0x3E as a disconnect and call onDisconnect.

If the peripheral devices are instead running NimBLEServer (accepting inbound connections from the central), their disconnect handling is unaffected by this PR — could you point me at the specific server-side path that's missing the disconnect event? I reviewed NimBLEServer::handleGapEvent and couldn't find a code path that would drop the 0x3E disconnect without calling onDisconnect.

Copilot finished work on behalf of h2zero May 15, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Peripheral not receiving disconnect when central is put to sleep on 2.5.0

2 participants