Skip to content

[Bugfix] Incorrect Client connection state tracking and self delete#411

Merged
h2zero merged 1 commit intomasterfrom
bugfix/client-connection-state
Mar 25, 2026
Merged

[Bugfix] Incorrect Client connection state tracking and self delete#411
h2zero merged 1 commit intomasterfrom
bugfix/client-connection-state

Conversation

@h2zero
Copy link
Copy Markdown
Owner

@h2zero h2zero commented Mar 24, 2026

This adds better client state tracking so that functions like NimBLEDevice::getDisconnectedClient get a more accurate state and will not return a connecting client.

This also fixes the client self delete on connection error where function call errors did not delete the client

Fixes #335
Fixes #336

Summary by CodeRabbit

  • Bug Fixes

    • More accurate connection-state tracking for reliable connectivity reporting.
    • Improved connect/disconnect flow and error handling to reduce spurious failures and unexpected connections.
    • Operations gated by connection state (RSSI, peer address, service discovery, MTU/timeout) now obey the updated lifecycle for steadier results.
    • Cleanup and retry behavior adjusted to avoid premature resource deletion during connect attempts.
  • Refactor

    • Internal connection state handling reorganized for clearer, more predictable behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Arr, added a private ConnStatus enum and m_connStatus to NimBLEClient, and converted connection control flow, GAP event handling, and operations gating to use this status (CONNECTING/CONNECTED/DISCONNECTING/DISCONNECTED) instead of relying solely on the connection handle. (50 words)

Changes

Cohort / File(s) Summary
Header — connection state
src/NimBLEClient.h
Add private ConnStatus enum and m_connStatus member to track CONNECTED, DISCONNECTED, CONNECTING, DISCONNECTING states.
Client implementation — lifecycle & GAP handling
src/NimBLEClient.cpp
Refactor connect() to require DISCONNECTED, set CONNECTING, centralize error path to reset to DISCONNECTED and record m_lastErr, move deleteServices() after retry loop, change blocking/MTU timeout checks to use m_connStatus, update disconnect() to set appropriate status, replace handle/isConnected checks with m_connStatus gating, and update GAP event handlers to set m_connStatus.
Device-level client bookkeeping
src/NimBLEDevice.cpp
Use clients' m_connStatus for selection and deletion logic (getDisconnectedClient, deleteClient) replacing prior handle/task-data heuristics; minor preprocessor formatting alignment changes.

Sequence Diagram(s)

sequenceDiagram
    participant App as "App"
    participant Device as "NimBLEDevice"
    participant Client as "NimBLEClient"
    participant GAP as "BLE GAP/Controller"

    App->>Device: request connect client
    Device->>Client: connect(params)
    Client->>Client: set m_connStatus = CONNECTING
    Client->>GAP: gap_connect()
    GAP-->>Client: BLE_GAP_EVENT_CONNECT / BLE_GAP_EVENT_DISCONNECT / error
    alt BLE_GAP_EVENT_CONNECT (success)
      Client->>Client: set m_connStatus = CONNECTED\nstore m_connHandle
      Client-->>Device: notify connected
    else BLE_GAP_EVENT_CONNECT (failure) or error
      Client->>Client: set m_connStatus = DISCONNECTED\nclear m_connHandle
      Client-->>Device: notify failed
    end
    App->>Client: operations (getRssi, retrieveServices, setPeerAddress)
    Client->>Client: gate ops on m_connStatus (e.g., require CONNECTED)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

Arr, a flag hoisted on the BLE mast,
CONNECTING winds that change too fast,
When storms arise ye mark DISCONNECTED shore,
Services kept tidy, callbacks roar,
ConnStatus steers our BLE ship safe once more.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objectives: fixing client connection state tracking and addressing self-delete behavior issues.
Linked Issues check ✅ Passed All code changes directly address the linked issues: implementing m_connStatus enum to prevent getDisconnectedClient from returning connecting clients [#335], and ensuring self-delete executes on all connect failure paths [#336].
Out of Scope Changes check ✅ Passed All changes are scoped to connection state tracking and self-delete logic; no unrelated modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 92.86% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/client-connection-state

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/NimBLEClient.cpp (1)

355-364: ⚠️ Potential issue | 🟠 Major

Don’t strand BLE_HS_ENOTCONN clients in DISCONNECTING.

Arrr, if ble_gap_terminate() reports BLE_HS_ENOTCONN, there may be no follow-up GAP event to move the state again. That leaves a fully disconnected client stuck as busy and can block later connect() or setPeerAddress() calls. Keep m_connStatus as DISCONNECTED for that branch.

🏴‍☠️ Suggested fix
-    m_connStatus = DISCONNECTING;
+    m_connStatus = (rc == BLE_HS_ENOTCONN) ? DISCONNECTED : DISCONNECTING;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 355 - 364, The disconnect path in
NimBLEClient::disconnect() leaves m_connStatus as DISCONNECTING when
ble_gap_terminate() returns BLE_HS_ENOTCONN, which can strand a client; change
the logic so that when rc == BLE_HS_ENOTCONN you set m_connStatus = DISCONNECTED
(and still record m_lastErr as appropriate) instead of DISCONNECTING, while
preserving the existing logging and behavior for other error codes (keep
handling for BLE_HS_EALREADY and non-zero rc unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/NimBLEClient.cpp`:
- Around line 1266-1267: Change NimBLEClient::isConnected() to return true only
when m_connStatus == CONNECTED (not CONNECTING) to ensure established links
only; add a separate predicate (e.g., NimBLEClient::isInUse() or isActive())
that implements the previous “not DISCONNECTED” semantics (or checks
m_connHandle != BLE_HS_CONN_HANDLE_NONE) for reuse/disconnected-client logic,
and update call sites that relied on the old behavior (notably the
BLE_GAP_EVENT_CONNECT handling and the synchronous wait path) to use the new
isInUse()/m_connHandle check where appropriate while leaving event/connection
logic to use the stricter isConnected().

---

Outside diff comments:
In `@src/NimBLEClient.cpp`:
- Around line 355-364: The disconnect path in NimBLEClient::disconnect() leaves
m_connStatus as DISCONNECTING when ble_gap_terminate() returns BLE_HS_ENOTCONN,
which can strand a client; change the logic so that when rc == BLE_HS_ENOTCONN
you set m_connStatus = DISCONNECTED (and still record m_lastErr as appropriate)
instead of DISCONNECTING, while preserving the existing logging and behavior for
other error codes (keep handling for BLE_HS_EALREADY and non-zero rc unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c8e107fa-673d-4350-8007-ea86f1ea35e2

📥 Commits

Reviewing files that changed from the base of the PR and between 74443d9 and 7f99a5d.

📒 Files selected for processing (2)
  • src/NimBLEClient.cpp
  • src/NimBLEClient.h

@h2zero h2zero force-pushed the bugfix/client-connection-state branch from 7f99a5d to ab86717 Compare March 25, 2026 13:13
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/NimBLEClient.cpp (1)

974-982: ⚠️ Potential issue | 🟠 Major

Arr, publish DISCONNECTED before firing the failure/disconnect callbacks.

Right now onConnectFail() / onDisconnect() still run while the client state says the link be alive. If callback code checks isConnected() or kicks off GATT work there, it can observe the stale pre-disconnect state. Move the DISCONNECTED transition before those callbacks so the new state machine stays truthful inside the callback too.

🏴‍☠️ Minimal fix
             pClient->m_terminateFailCount = 0;
             pClient->m_asyncSecureAttempt = 0;
+            pClient->m_connStatus         = DISCONNECTED;
 
             // Don't call the disconnect callback if we are waiting for a connection to complete and it fails
             if (rc == (BLE_HS_ERR_HCI_BASE + BLE_ERR_CONN_ESTABLISHMENT) && pClient->m_config.asyncConnect) {
                 pClient->m_pClientCallbacks->onConnectFail(pClient, rc);
             } else {
                 pClient->m_pClientCallbacks->onDisconnect(pClient, rc);
             }
 
-            pClient->m_connStatus = DISCONNECTED;
             pClient->m_connHandle = BLE_HS_CONN_HANDLE_NONE;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 974 - 982, Move the connection state
update before invoking callbacks: set pClient->m_connStatus = DISCONNECTED and
pClient->m_connHandle = BLE_HS_CONN_HANDLE_NONE prior to calling
pClient->m_pClientCallbacks->onConnectFail(...) or onDisconnect(...).
Specifically, in the block that checks rc == (BLE_HS_ERR_HCI_BASE +
BLE_ERR_CONN_ESTABLISHMENT) && pClient->m_config.asyncConnect, update
m_connStatus and m_connHandle first, then call onConnectFail; otherwise update
them first and then call onDisconnect, so callbacks observe the true
disconnected state.
♻️ Duplicate comments (1)
src/NimBLEClient.cpp (1)

185-189: ⚠️ Potential issue | 🔴 Critical

Arr, split “connected” from “in use.”

isConnected() now returns true for CONNECTING and DISCONNECTING, but the guards here only block CONNECTED / CONNECTING. That leaves a DISCONNECTING client reusable before its old GAP disconnect lands, so the late event can clobber the new attempt or miss the client entirely. Keep isConnected() strict to CONNECTED, and gate reuse / peer-address mutation on m_connStatus != DISCONNECTED (or a dedicated helper).

🏴‍☠️ Local fix
-    if (m_connStatus == CONNECTED || m_connStatus == CONNECTING) {
+    if (m_connStatus != DISCONNECTED) {
         NIMBLE_LOGE(LOG_TAG, "Client already connected/connecting");
         rc = BLE_HS_EREJECT;
         goto error;
     }
...
-    if (m_connStatus == CONNECTED || m_connStatus == CONNECTING) {
+    if (m_connStatus != DISCONNECTED) {
         NIMBLE_LOGE(LOG_TAG, "Cannot set peer address while connected/connecting");
         return false;
     }
...
 bool NimBLEClient::isConnected() const {
-    return m_connStatus != DISCONNECTED;
+    return m_connStatus == CONNECTED;
 }

Also applies to: 580-582, 1265-1266

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 185 - 189, The guard that blocks
reuse/mutation currently checks CONNECTED/CONNECTING but must also block
DISCONNECTING; update the conditional around m_connStatus (the block that logs
"Client already connected/connecting" and returns BLE_HS_EREJECT) to treat any
non-DISCONNECTED state as in-use (i.e., reject when m_connStatus !=
DISCONNECTED) and ensure isConnected() remains strict (return true only for
CONNECTED); apply the same fix to the other similar guards that check
CONNECTED/CONNECTING (all uses that gate reuse or peer-address mutation) so
DISCONNECTING is treated as in-use too.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/NimBLEClient.cpp`:
- Around line 974-982: Move the connection state update before invoking
callbacks: set pClient->m_connStatus = DISCONNECTED and pClient->m_connHandle =
BLE_HS_CONN_HANDLE_NONE prior to calling
pClient->m_pClientCallbacks->onConnectFail(...) or onDisconnect(...).
Specifically, in the block that checks rc == (BLE_HS_ERR_HCI_BASE +
BLE_ERR_CONN_ESTABLISHMENT) && pClient->m_config.asyncConnect, update
m_connStatus and m_connHandle first, then call onConnectFail; otherwise update
them first and then call onDisconnect, so callbacks observe the true
disconnected state.

---

Duplicate comments:
In `@src/NimBLEClient.cpp`:
- Around line 185-189: The guard that blocks reuse/mutation currently checks
CONNECTED/CONNECTING but must also block DISCONNECTING; update the conditional
around m_connStatus (the block that logs "Client already connected/connecting"
and returns BLE_HS_EREJECT) to treat any non-DISCONNECTED state as in-use (i.e.,
reject when m_connStatus != DISCONNECTED) and ensure isConnected() remains
strict (return true only for CONNECTED); apply the same fix to the other similar
guards that check CONNECTED/CONNECTING (all uses that gate reuse or peer-address
mutation) so DISCONNECTING is treated as in-use too.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a5440372-da29-4749-977b-d7f654f814dd

📥 Commits

Reviewing files that changed from the base of the PR and between 7f99a5d and ab86717.

📒 Files selected for processing (2)
  • src/NimBLEClient.cpp
  • src/NimBLEClient.h
✅ Files skipped from review due to trivial changes (1)
  • src/NimBLEClient.h

@h2zero h2zero force-pushed the bugfix/client-connection-state branch from ab86717 to 63dcdf2 Compare March 25, 2026 13:27
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/NimBLEDevice.cpp (1)

362-374: ⚠️ Potential issue | 🟠 Major

Arrr, keep DISCONNECTING on the deferred-delete path.

Once disconnect() succeeds, NimBLEClient::disconnect() moves the client to DISCONNECTING before the GAP disconnect event lands. In that window, Line 367 skips both guarded branches and drops into the raw delete, which can remove the client from m_pClients before the disconnect-completion path runs. Treat DISCONNECTING the same as CONNECTED: arm deleteOnDisconnect, but only call disconnect() if the state is still CONNECTED.

🏴‍☠️ Proposed fix
-            if (clt->m_connStatus == NimBLEClient::CONNECTED) {
+            if (clt->m_connStatus == NimBLEClient::CONNECTED ||
+                clt->m_connStatus == NimBLEClient::DISCONNECTING) {
                 clt->m_config.deleteOnDisconnect = true;
-                if (!clt->disconnect()) {
+                if (clt->m_connStatus == NimBLEClient::CONNECTED && !clt->disconnect()) {
                     break;
                 }
             } else if (pClient->m_connStatus == NimBLEClient::CONNECTING) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEDevice.cpp` around lines 362 - 374, The current removal path
deletes a NimBLEClient prematurely because DISCONNECTING falls through to raw
delete; change the logic in the block that checks clt->m_connStatus so
DISCONNECTING is treated like CONNECTED by setting
clt->m_config.deleteOnDisconnect = true, but only invoke clt->disconnect() when
clt->m_connStatus is actually CONNECTED (do not call disconnect() when already
DISCONNECTING); keep the existing CONNECTING branch (setting deleteOnConnectFail
and calling cancelConnect()) and only delete the object in the final else if
neither CONNECTED/DISCONNECTING nor CONNECTING.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/NimBLEDevice.cpp`:
- Around line 362-374: The current removal path deletes a NimBLEClient
prematurely because DISCONNECTING falls through to raw delete; change the logic
in the block that checks clt->m_connStatus so DISCONNECTING is treated like
CONNECTED by setting clt->m_config.deleteOnDisconnect = true, but only invoke
clt->disconnect() when clt->m_connStatus is actually CONNECTED (do not call
disconnect() when already DISCONNECTING); keep the existing CONNECTING branch
(setting deleteOnConnectFail and calling cancelConnect()) and only delete the
object in the final else if neither CONNECTED/DISCONNECTING nor CONNECTING.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49242007-61f1-4261-ac49-1219a6315540

📥 Commits

Reviewing files that changed from the base of the PR and between ab86717 and 63dcdf2.

📒 Files selected for processing (3)
  • src/NimBLEClient.cpp
  • src/NimBLEClient.h
  • src/NimBLEDevice.cpp
✅ Files skipped from review due to trivial changes (1)
  • src/NimBLEClient.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/NimBLEClient.cpp

@h2zero h2zero force-pushed the bugfix/client-connection-state branch 2 times, most recently from 4f75a21 to 8aecbbf Compare March 25, 2026 15:30
This adds better client state tracking so that functions like NimBLEDevice::getDisconnectedClient get a more accurate state and will not return a connecting client.

This also fixes the client self delete on connection error where function call errors did not delete the client
@h2zero h2zero force-pushed the bugfix/client-connection-state branch from 8aecbbf to c06b3cb Compare March 25, 2026 17:27
@h2zero h2zero merged commit 0dbca80 into master Mar 25, 2026
66 checks passed
@h2zero h2zero deleted the bugfix/client-connection-state branch March 25, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant