Skip to content

[Feat] Wait for connections to establish before callback, retry on fail#413

Merged
h2zero merged 1 commit intomasterfrom
feat/conn-reattempt
Mar 27, 2026
Merged

[Feat] Wait for connections to establish before callback, retry on fail#413
h2zero merged 1 commit intomasterfrom
feat/conn-reattempt

Conversation

@h2zero
Copy link
Copy Markdown
Owner

@h2zero h2zero commented Mar 26, 2026

  • Delay onConnect until connection is likely established using a timer (7x conn interval), with early completion on peer activity.
  • Suppress onDisconnect for 0x3e and route terminal 0x3e failures to onConnectFail.
  • Add configurable 0x3e reconnect attempts via setConnectRetries or client config (default 2).
  • Retry logic suppresses callbacks while retrying; callbacks fire only on final outcome.
  • Refactor connect-start path into a shared helper used by initial connect and retries.

Summary by CodeRabbit

  • New Features

    • Public setting to configure automatic connection-establishment retries (default 2, clamped to max 7).
    • Connection finalization is deferred until link parameters are negotiated; user on-connect is invoked only after establishment is confirmed.
  • Bug Fixes

    • Suppress user callbacks during intermediate automated retries to avoid premature notifications.
    • Treat already-exchanged MTU as non‑fatal.
    • Improved disconnect/retry and post‑connect event handling; extended connect wait to accommodate negotiated intervals.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 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 — Adds a delayed “connect established” stage with per-client timer and pending flag, extracts GAP connect into startConnectionAttempt(...), implements configurable retries for connection-establishment failures (0x3e), and adjusts GAP/MTU/disconnect handling and client lookup. (≤50 words)

Changes

Cohort / File(s) Summary
Connection Control Flow
src/NimBLEClient.cpp
Extracted GAP connect logic into startConnectionAttempt(const ble_addr_t*); connect() now resets pending/retry state, delegates to helper, and extends blocking wait timeout; removed immediate onConnect() after wait.
Connect‑Established Timer & Completion
src/NimBLEClient.cpp, src/NimBLEClient.h
Added m_connectEstablishedTimer, m_connectCallbackPending, m_connectFailRetryCount; new startConnectEstablishedTimer(...), connectEstablishedTimerCb(...), and completeConnectEstablished() to delay final onConnect() until stability timer fires.
Retry Handling on Disconnect
src/NimBLEClient.cpp, src/NimBLEClient.h
On disconnect with establishment-failure (BLE_HS_ERR_HCI_BASE + BLE_ERR_CONN_ESTABLISHMENT) will clear handle, increment retry count, re-enter CONNECTING and re-issue startConnectionAttempt(...) up to configured connectFailRetries, suppressing user callbacks during retries; added public setConnectRetries(uint8_t).
GAP Event & Disconnect Handling
src/NimBLEClient.cpp
GAP event handling updated to support delayed completion; opportunistically calls completeConnectEstablished() on multiple post-connect events with conn-handle gating; changed disconnect client lookup to prefer NimBLEDevice::getClientByHandle(...); stops timer and clears pending state on disconnect.
MTU & Error Handling
src/NimBLEClient.cpp
Treats BLE_HS_EALREADY from MTU exchange as non-fatal; refactored error/cleanup flows around MTU and failed connect completion.
Config & Public API
src/NimBLEClient.h
Added Config::connectFailRetries : 3 {2} and public setConnectRetries(uint8_t); made Config default constructor explicit and member-initialized bitfields; declared new timer/helper methods.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Client as NimBLEClient
    participant Stack as BLE_Stack
    participant Timer as ConnectEstTimer

    App->>Client: connect(address,...)
    activate Client
    Client->>Client: reset pending & retry state
    Client->>Stack: startConnectionAttempt()
    deactivate Client

    Stack->>Client: BLE_GAP_EVENT_CONNECT (success)
    activate Client
    Client->>Client: set m_connectCallbackPending = true
    Client->>Timer: startConnectEstablishedTimer(conn_itvl)
    Client->>Stack: exchangeMTU() (fire-and-forget)
    deactivate Client

    Stack->>Client: subsequent GAP events (notify/mtu/encrypt/identity/phy/others)
    activate Client
    Client->>Client: completeConnectEstablished()
    Client->>App: onConnect(this)
    Client->>Timer: stop timer
    deactivate Client

    alt Disconnect with establishment-fail (0x3e)
        Stack->>Client: BLE_GAP_EVENT_DISCONNECT (0x3e)
        activate Client
        alt retries left
            Client->>Client: increment retry count
            Client->>Stack: startConnectionAttempt() (retry)
        else
            Client->>App: onConnectFail()/onDisconnect
        end
        deactivate Client
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

⚓ Yarrr, a handshake waits upon the tide,
A wee timer keeps the shaky greet aside,
Retries be mustered when the link be mean,
Callbacks held fast till the path be clean,
Hoist — the client steadies, connection seen!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: delayed connection callback and retry logic on connection establishment failures.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/conn-reattempt

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.

🧹 Nitpick comments (1)
src/NimBLEClient.cpp (1)

72-73: Arr, a wee formatting barnacle on the hull!

Matey, line 73 be havin' inconsistent indentation - it starts with 4 spaces instead of 6 spaces like the other member initializers. Not a functional issue, but it be lookin' a bit scruffy.

🧹 Proposed fix for alignment
       m_connStatus{DISCONNECTED},
       m_connectCallbackPending{false},
-    m_connectFailRetryCount{0},
+      m_connectFailRetryCount{0},
 # if MYNEWT_VAL(BLE_EXT_ADV)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 72 - 73, The member initializer list in
NimBLEClient.cpp has inconsistent indentation: m_connectCallbackPending is
indented with 6 spaces but m_connectFailRetryCount starts with 4, making the
initializer list misaligned; update the whitespace so m_connectFailRetryCount
aligns with the other initializers in the constructor initializer list (adjust
indentation to match m_connectCallbackPending and the surrounding initializers).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NimBLEClient.cpp`:
- Around line 72-73: The member initializer list in NimBLEClient.cpp has
inconsistent indentation: m_connectCallbackPending is indented with 6 spaces but
m_connectFailRetryCount starts with 4, making the initializer list misaligned;
update the whitespace so m_connectFailRetryCount aligns with the other
initializers in the constructor initializer list (adjust indentation to match
m_connectCallbackPending and the surrounding initializers).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 264a0adc-72f6-4813-a616-f23b74a59e20

📥 Commits

Reviewing files that changed from the base of the PR and between 5d48f94 and 98c5653.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch from 98c5653 to cf5ac86 Compare March 26, 2026 20:56
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.h (1)

104-111: ⚠️ Potential issue | 🟡 Minor

Aye, the retry default should anchor itself to the Config struct, not just the constructor, ye scallywag!

Line 110 currently leaves connectFailRetryCount without a default member initializer. While the constructor seeds m_config.connectFailRetryCount = 1 (line 85), a caller who builds a fresh Config{} and passes it to setConfig() will hand over a zero retry budget, silently disabling retries. The fix is to add a default member initializer so the default survives any way the struct is constructed:

     struct Config {
         uint8_t deleteCallbacks : 1;     // Delete the callback object when the client is deleted.
         uint8_t deleteOnDisconnect : 1;  // Delete the client when disconnected.
         uint8_t deleteOnConnectFail : 1; // Delete the client when a connection attempt fails.
         uint8_t asyncConnect : 1;        // Connect asynchronously.
         uint8_t exchangeMTU : 1;         // Exchange MTU after connection.
-        uint8_t connectFailRetryCount;   // Number of retries for 0x3e (connection establishment) failures.
+        uint8_t connectFailRetryCount = 1; // Number of retries for 0x3e failures; 0 disables retries.
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.h` around lines 104 - 111, The Config struct's
connectFailRetryCount lacks a default member initializer so constructing
Config{} yields zero retries; update the struct definition (struct Config) to
give connectFailRetryCount a default value (e.g., = 1) so any caller who creates
a Config or passes it to setConfig() gets the intended default retry count and
you no longer rely solely on m_config being set in the NimBLEClient constructor.
🤖 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.h`:
- Around line 104-111: The Config struct's connectFailRetryCount lacks a default
member initializer so constructing Config{} yields zero retries; update the
struct definition (struct Config) to give connectFailRetryCount a default value
(e.g., = 1) so any caller who creates a Config or passes it to setConfig() gets
the intended default retry count and you no longer rely solely on m_config being
set in the NimBLEClient constructor.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2dfddf2-9f2c-43e1-9675-84e28df9903c

📥 Commits

Reviewing files that changed from the base of the PR and between 98c5653 and cf5ac86.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch from cf5ac86 to 7a51823 Compare March 27, 2026 12:50
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

🤖 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.h`:
- Around line 127-129: An armed connect-established callout started by
startConnectEstablishedTimer is not being stopped on all terminal
BLE_GAP_EVENT_CONNECT paths, which can later invoke
connectEstablishedTimerCb/completeConnectEstablished on a stale this; update the
BLE_GAP_EVENT_CONNECT handling in NimBLEClient.cpp to call ble_npl_callout_stop
(the same call used on disconnect paths) before any early return/termination,
ensuring the callout is always stopped and that any pointer/state is nulled or
guarded to prevent callbacks after the client is torn down.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb7355a6-be4a-4966-9463-531cea0b1d8f

📥 Commits

Reviewing files that changed from the base of the PR and between cf5ac86 and 7a51823.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch from 7a51823 to 4a138e3 Compare March 27, 2026 13:40
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.

🧹 Nitpick comments (2)
src/NimBLEClient.h (2)

104-110: Make the retry default opt-in, matey.

Arr, Line 110 changes behavior for callers that never touch Config: a 0x3e now gets one hidden reattempt before the final failure path runs. That be a noticeable compatibility shift for synchronous connect() and for apps timing onConnectFail(). I’d default this to 0 and let applications opt in if they want automatic retries.

⚓ Proposed tweak
-        uint8_t connectFailRetryCount{1}; // Number of retries for 0x3e (connection establishment) failures.
+        uint8_t connectFailRetryCount{0}; // Number of retries for 0x3e (connection establishment) failures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.h` around lines 104 - 110, The Config struct in
NimBLEClient.h currently sets connectFailRetryCount{1}, which silently changes
default behavior; change the default to 0 (connectFailRetryCount{0}) so retries
are opt-in, update the inline comment to reflect "default 0, opt-in for
automatic retries" and ensure any code that relies on an implicit retry (e.g.,
in connect() or onConnectFail() flows) continues to read this field so callers
can opt-in explicitly via Config::connectFailRetryCount when they want retries.

124-134: Update the public callback docs for this new phase, matey.

Arr, these helpers add a second “connected but not yet reported” state, but NimBLEClientCallbacks::onConnect, onConnectFail, and onDisconnect still read like the old immediate GAP-event flow. Since completeConnectEstablished() in src/NimBLEClient.cpp:943-974 now defers onConnect(), and terminal 0x3e is routed through onConnectFail(), the header should say that plainly so downstream users do not debug the wrong ordering.

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

In `@src/NimBLEClient.h` around lines 124 - 134, Update the public callback
documentation in NimBLEClient.h to reflect the new “connected but not yet
reported” phase introduced by completeConnectEstablished(): state that onConnect
is deferred until completeConnectEstablished()/connectEstablishedTimerCb signals
the connection is fully established, that terminal disconnect reason 0x3e (and
similar terminal cases handled in handleGapEvent/completeConnectEstablished)
will be reported via onConnectFail rather than a normal onConnect flow, and
clarify ordering semantics for NimBLEClientCallbacks::onConnect, onConnectFail,
and onDisconnect so downstream users understand the deferred-reporting and
failure routing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NimBLEClient.h`:
- Around line 104-110: The Config struct in NimBLEClient.h currently sets
connectFailRetryCount{1}, which silently changes default behavior; change the
default to 0 (connectFailRetryCount{0}) so retries are opt-in, update the inline
comment to reflect "default 0, opt-in for automatic retries" and ensure any code
that relies on an implicit retry (e.g., in connect() or onConnectFail() flows)
continues to read this field so callers can opt-in explicitly via
Config::connectFailRetryCount when they want retries.
- Around line 124-134: Update the public callback documentation in
NimBLEClient.h to reflect the new “connected but not yet reported” phase
introduced by completeConnectEstablished(): state that onConnect is deferred
until completeConnectEstablished()/connectEstablishedTimerCb signals the
connection is fully established, that terminal disconnect reason 0x3e (and
similar terminal cases handled in handleGapEvent/completeConnectEstablished)
will be reported via onConnectFail rather than a normal onConnect flow, and
clarify ordering semantics for NimBLEClientCallbacks::onConnect, onConnectFail,
and onDisconnect so downstream users understand the deferred-reporting and
failure routing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c83c501-0b35-4ea0-ab77-44af1dec208b

📥 Commits

Reviewing files that changed from the base of the PR and between 7a51823 and 4a138e3.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch 2 times, most recently from 531a15e to df98835 Compare March 27, 2026 15:00
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: 2

🤖 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 958-969: completeConnectEstablished currently invokes
m_pClientCallbacks->onConnect inline while GAP handlers (callers that continue
to dereference pClient after this helper returns) may still be running; this can
cause use-after-free if onConnect disconnects or deletes the client. Change
completeConnectEstablished (and any similar helper) to not call onConnect
inline: clear m_connectCallbackPending, stop the connect timer, then schedule a
deferred invocation (e.g., use an existing ble_npl_callout or post a task/event)
that runs after the current GAP event unwinds to call
m_pClientCallbacks->onConnect(this) and perform
NimBLEUtils::taskRelease(*m_pTaskData,0) there; alternatively update all callers
that dereference pClient after completeConnectEstablished returns to avoid
touching pClient when it returns true. Ensure the deferred path cleans up the
same state and preserves the current semantics of m_connectCallbackPending.
- Around line 1045-1059: The blocking connect() wait can return early while
startConnectionAttempt()/delayed-establish retries are still in-flight; update
connect() to extend or restart its wait whenever a retry path is initiated
(detect when startConnectionAttempt(...) returns 0 or when you set m_connStatus
back to CONNECTING and m_connectFailRetryCount is incremented) so that it waits
for the final outcome signaled by completeConnectEstablished()/onConnect() or
the ultimate failure; implement this by resetting/recomputing the
condition-variable timeout (or setting a pending-retry flag) inside the retry
branch that currently returns 0, and ensure connect() checks that flag (or
inspects m_connectFailRetryCount vs m_config.connectFailRetries and
BLE_HS_CONN_HANDLE_NONE) to block until the retry sequence and the post-connect
grace window complete.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 234fccd5-824b-4d66-8d06-09f0aa9d1c08

📥 Commits

Reviewing files that changed from the base of the PR and between 4a138e3 and df98835.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch from df98835 to cc8486e Compare March 27, 2026 16:05
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

🤖 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 991-998: The m_config.connectFailRetries field is only 3 bits
(0–7) but setConnectRetries currently allows 8, which will overflow; change the
clamp to cap at 7 (use std::min<uint8_t>(numRetries, 7U)) inside
NimBLEClient::setConnectRetries and update the function comment to state "Max is
7, Default is 2" so documentation matches the actual storage limit; ensure you
reference/set m_config.connectFailRetries only after clamping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e6bf5a3-cd01-490a-9163-609fc0520132

📥 Commits

Reviewing files that changed from the base of the PR and between 4a138e3 and cc8486e.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch from cc8486e to 7ad0dfc Compare March 27, 2026 16:15
@h2zero h2zero changed the title [Feat] Wait for connections to establish before callback and reattempt [Feat] Wait for connections to establish before callback , retry on fail Mar 27, 2026
@h2zero h2zero changed the title [Feat] Wait for connections to establish before callback , retry on fail [Feat] Wait for connections to establish before callback, retry on fail Mar 27, 2026
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 (2)
src/NimBLEClient.cpp (2)

1073-1089: ⚠️ Potential issue | 🟠 Major

Arr, the new 0x3e connect-fail path skips deleteOnConnectFail for async callers.

Lines 1073-1077 now surface terminal 0x3e as onConnectFail, but Lines 1081-1089 only self-delete off deleteOnDisconnect. In async mode (pTaskData == nullptr), setSelfDelete(false, true) no longer works for this failure path.

🛠️ Proposed fix
-            if (pClient->m_config.deleteOnDisconnect) {
+            const bool shouldDelete = pClient->m_config.deleteOnDisconnect ||
+                                      (rc == connEstablishFailReason && pClient->m_config.deleteOnConnectFail);
+            if (shouldDelete) {
                 // If we are set to self delete on disconnect but we have a task waiting on the connection
                 // completion we will set the flag to delete on connect fail instead of deleting here
                 // to prevent segmentation faults or double deleting
                 if (pTaskData != nullptr && rc == connEstablishFailReason) {
                     pClient->m_config.deleteOnConnectFail = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 1073 - 1089, The connect-fail path (rc ==
connEstablishFailReason) triggers onConnectFail but the subsequent self-delete
logic only checks m_config.deleteOnDisconnect, so async callers (pTaskData ==
nullptr) can't use setSelfDelete(false, true); update the post-disconnect
handling in NimBLEClient.cpp to honor m_config.deleteOnConnectFail for the
connEstablishFailReason case: if rc == connEstablishFailReason and
pClient->m_config.deleteOnConnectFail is true and pTaskData is nullptr then call
NimBLEDevice::deleteClient(pClient); keep the existing behavior that when
pTaskData != nullptr and rc == connEstablishFailReason you set
pClient->m_config.deleteOnConnectFail = true instead of deleting immediately.

1005-1008: ⚠️ Potential issue | 🔴 Critical

Arr, refresh the waiting task after rebinding the disconnect client.

Disconnect events are already special-cased because arg can point at the wrong client. pTaskData is copied before Lines 1015-1022 fix up pClient, so Lines 1396-1398 can release the wrong waiter—or never release the real blocking connect() at all.

🛠️ Proposed fix
             if (pClient == nullptr) {
                 NIMBLE_LOGE(LOG_TAG, "Disconnected client not found, conn_handle=%d", event->disconnect.conn.conn_handle);
                 return 0;
             }
+
+            pTaskData = pClient->m_pTaskData;

Also applies to: 1015-1022

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

In `@src/NimBLEClient.cpp` around lines 1005 - 1008, The code copies pTaskData
from pClient before the rebinding logic that can replace pClient when handling a
disconnect, which can cause the wrong waiter to be signaled; in
NimBLEClient::handleGapEvent adjust the order so pTaskData is refreshed after
any rebinding/realignment of pClient (i.e., move or duplicate the assignment of
NimBLETaskData* pTaskData = pClient->m_pTaskData to occur after the block that
fixes up pClient or re-read pTaskData right before the code that releases the
connect waiter), and ensure you null-check pClient and pTaskData before using
them so the real blocking connect() waiter is always released for the correct
client.
♻️ Duplicate comments (1)
src/NimBLEClient.cpp (1)

296-299: ⚠️ Potential issue | 🔴 Critical

Arr, this wait budget still only covers one connect attempt.

startConnectionAttempt() reuses m_connectTimeout on every 0x3e retry, but Lines 297-299 only budget one m_connectTimeout. With retries enabled, a blocking connect() can time out and cancel a live retry, or hit the Line 301 success fallback before completeConnectEstablished() fires.

🛠️ Proposed fix
-    // Wait for the connect timeout time +1 second +retry time for the connection to complete
+    const uint32_t attempts           = static_cast<uint32_t>(m_config.connectFailRetries) + 1U;
+    const uint32_t establishWindowMs  = connIntervalToMs(m_connParams.itvl_max) * 7U;
+    // Wait for all connection attempts plus the post-connect establishment window.
     if (!NimBLEUtils::taskWait(
             taskData,
-            m_connectTimeout + 1000 + connIntervalToMs(m_connParams.itvl_max) * 7 * m_config.connectFailRetries)) {
+            attempts * (static_cast<uint32_t>(m_connectTimeout) + establishWindowMs) + 1000U)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 296 - 299, The wait in
NimBLEUtils::taskWait only budgets a single m_connectTimeout so it can abort
while startConnectionAttempt is performing multiple 0x3e retry connection
attempts; update the computed timeout passed to NimBLEUtils::taskWait to include
the retry budget (e.g., multiply or add m_connectTimeout for each retry in
m_config.connectFailRetries) in addition to the existing
connIntervalToMs(m_connParams.itvl_max) * 7 * m_config.connectFailRetries and
+1000 so the wait covers every connect attempt and only falls back to the Line
301 success case after completeConnectEstablished can fire.
🤖 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.h`:
- Line 111: The default retry count for the new 0x3e flow is wrong: change the
initializer for the bitfield connectFailRetries in class/struct NimBLEClient
from 2 to 1 so callers that never call setConnectRetries() get a single retry as
intended; update any adjacent comment to reflect "1 retry" if present and ensure
setConnectRetries() still overrides this default behavior.

---

Outside diff comments:
In `@src/NimBLEClient.cpp`:
- Around line 1073-1089: The connect-fail path (rc == connEstablishFailReason)
triggers onConnectFail but the subsequent self-delete logic only checks
m_config.deleteOnDisconnect, so async callers (pTaskData == nullptr) can't use
setSelfDelete(false, true); update the post-disconnect handling in
NimBLEClient.cpp to honor m_config.deleteOnConnectFail for the
connEstablishFailReason case: if rc == connEstablishFailReason and
pClient->m_config.deleteOnConnectFail is true and pTaskData is nullptr then call
NimBLEDevice::deleteClient(pClient); keep the existing behavior that when
pTaskData != nullptr and rc == connEstablishFailReason you set
pClient->m_config.deleteOnConnectFail = true instead of deleting immediately.
- Around line 1005-1008: The code copies pTaskData from pClient before the
rebinding logic that can replace pClient when handling a disconnect, which can
cause the wrong waiter to be signaled; in NimBLEClient::handleGapEvent adjust
the order so pTaskData is refreshed after any rebinding/realignment of pClient
(i.e., move or duplicate the assignment of NimBLETaskData* pTaskData =
pClient->m_pTaskData to occur after the block that fixes up pClient or re-read
pTaskData right before the code that releases the connect waiter), and ensure
you null-check pClient and pTaskData before using them so the real blocking
connect() waiter is always released for the correct client.

---

Duplicate comments:
In `@src/NimBLEClient.cpp`:
- Around line 296-299: The wait in NimBLEUtils::taskWait only budgets a single
m_connectTimeout so it can abort while startConnectionAttempt is performing
multiple 0x3e retry connection attempts; update the computed timeout passed to
NimBLEUtils::taskWait to include the retry budget (e.g., multiply or add
m_connectTimeout for each retry in m_config.connectFailRetries) in addition to
the existing connIntervalToMs(m_connParams.itvl_max) * 7 *
m_config.connectFailRetries and +1000 so the wait covers every connect attempt
and only falls back to the Line 301 success case after
completeConnectEstablished can fire.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff5bd3f7-2f27-4e1b-a74f-2cda7512feda

📥 Commits

Reviewing files that changed from the base of the PR and between cc8486e and 7ad0dfc.

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

@h2zero h2zero force-pushed the feat/conn-reattempt branch 3 times, most recently from 0b8a4eb to c6901eb Compare March 27, 2026 20:00
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.

🧹 Nitpick comments (1)
src/NimBLEClient.h (1)

166-168: Optional hardening: initialize new state fields in-class too.

Arr matey, constructor init is present, but in-class defaults would guard future constructor additions from accidental uninitialized state.

🛠️ Suggested refactor
-    bool                              m_connectCallbackPending;
-    uint8_t                           m_connectFailRetryCount;
+    bool                              m_connectCallbackPending{false};
+    uint8_t                           m_connectFailRetryCount{0};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.h` around lines 166 - 168, Initialize the new member fields
directly in-class to harden against future constructor changes: set
m_connectEstablishedTimer to {} (already shown), and add explicit in-class
defaults for m_connectCallbackPending (e.g., = false) and
m_connectFailRetryCount (e.g., = 0) so the members m_connectEstablishedTimer,
m_connectCallbackPending and m_connectFailRetryCount in the NimBLEClient class
are always initialized even if constructors change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NimBLEClient.h`:
- Around line 166-168: Initialize the new member fields directly in-class to
harden against future constructor changes: set m_connectEstablishedTimer to {}
(already shown), and add explicit in-class defaults for m_connectCallbackPending
(e.g., = false) and m_connectFailRetryCount (e.g., = 0) so the members
m_connectEstablishedTimer, m_connectCallbackPending and m_connectFailRetryCount
in the NimBLEClient class are always initialized even if constructors change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d13f98c-509f-4346-9748-30b08608fe59

📥 Commits

Reviewing files that changed from the base of the PR and between 0b8a4eb and c6901eb.

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

* Delay onConnect until connection is likely established using a timer (7x conn interval), with early completion on peer activity.
* Suppress onDisconnect for 0x3e and route terminal 0x3e failures to onConnectFail.
* Add configurable 0x3e reconnect attempts via `setConnectRetries` or client config (default 2).
* Retry logic suppresses callbacks while retrying; callbacks fire only on final outcome.
* Refactor connect-start path into a shared helper used by initial connect and retries.
@h2zero h2zero force-pushed the feat/conn-reattempt branch from c6901eb to 882ff11 Compare March 27, 2026 20:44
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.

🧹 Nitpick comments (1)
src/NimBLEClient.cpp (1)

954-958: Arr, yer defensive codin' be adequate but could use a wee polish!

The ble_npl_time_ms_to_ticks return code ain't bein' checked, matey. While yer initialization of waitTicks = 1 and the subsequent zero-check provide a safe fallback, checkin' the return code would be more shipshape for diagnosing any conversion failures in rough seas.

🔧 Optional: Check conversion return code
     ble_npl_time_t waitTicks = 1;
-    ble_npl_time_ms_to_ticks(waitMs, &waitTicks);
+    if (ble_npl_time_ms_to_ticks(waitMs, &waitTicks) != 0) {
+        NIMBLE_LOGW(LOG_TAG, "Failed to convert %lu ms to ticks, using 1 tick", (unsigned long)waitMs);
+        waitTicks = 1;
+    }
     if (waitTicks == 0) {
         waitTicks = 1;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NimBLEClient.cpp` around lines 954 - 958, The code calls
ble_npl_time_ms_to_ticks(waitMs, &waitTicks) but doesn't check its return value;
update the call site to capture the function's return code, validate it (e.g.,
on error fall back to a safe value like 1), and log or propagate the error as
appropriate; specifically modify the block around ble_npl_time_ms_to_ticks to
store its return, check for non-success, set waitTicks = 1 if conversion failed
or returned 0, and emit an error/debug message referencing waitMs and the
failing return code so failures are diagnosable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/NimBLEClient.cpp`:
- Around line 954-958: The code calls ble_npl_time_ms_to_ticks(waitMs,
&waitTicks) but doesn't check its return value; update the call site to capture
the function's return code, validate it (e.g., on error fall back to a safe
value like 1), and log or propagate the error as appropriate; specifically
modify the block around ble_npl_time_ms_to_ticks to store its return, check for
non-success, set waitTicks = 1 if conversion failed or returned 0, and emit an
error/debug message referencing waitMs and the failing return code so failures
are diagnosable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df133a01-4422-41d5-a5cc-0241ca63e8fd

📥 Commits

Reviewing files that changed from the base of the PR and between c6901eb and 882ff11.

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

@h2zero h2zero merged commit afe1221 into master Mar 27, 2026
66 checks passed
@h2zero h2zero deleted the feat/conn-reattempt branch March 27, 2026 21:56
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.

1 participant