Skip to content

fix(ble): serialize connection parameter updates - #8

Merged
jeremiah-k merged 3 commits into
fix/nimble-connection-parameter-serialization-reviewfrom
fix/nimble-connection-parameter-serialization
Aug 29, 2026
Merged

jeremiah-k merged 3 commits into
fix/nimble-connection-parameter-serialization-reviewfrom
fix/nimble-connection-parameter-serialization

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

This PR introduces a mutex-serialized “single lane” controller for BLE GAP connection-parameter updates, ensuring only one NimBLE connection-parameter procedure is in flight at a time. While an update is active, incoming mode requests (high-throughput vs low-power) are coalesced and the next submission is deferred until NimBLE reports completion; session teardown resets controller state to prevent stale callbacks from affecting a new connection. For CONFIG_IDF_TARGET_ESP32, the controller workflow is compiled out and the new request/service hooks are no-ops.

Key changes

Features

  • Added meshtastic::bluetooth::ConnectionParamsUpdateController (src/nimble/ConnectionParamsUpdateController.h)
    • Defines ConnectionParamsMode (NONE, HIGH_THROUGHPUT, LOW_POWER) and a ConnectionParamsRequest token (connHandle, mode)
    • Coalesces/replaces pending mode requests while an update is in-flight
    • Uses a per-session “generation” token to retire queued/in-flight work across reconnects
    • Applies interval satisfaction logic to suppress redundant submissions when peer-selected params already match the requested profile
    • Implements a single-lane in-flight timeout plus backoff/quiet-period scheduling (wrap-safe using Throttle::remainingTimespanMs)
    • Provides reset() and a PIO_UNIT_TESTING-only resetForTest(beforeLock) to validate atomicity
  • Updated src/nimble/NimbleBluetooth.cpp to route connection-parameter sequencing through the controller on supported targets
    • onConfigStart() requests HIGH_THROUGHPUT; onConfigComplete() requests LOW_POWER via requestConnectionParams(mode)
    • Main loop servicing: runOnce() now returns updateConnectionParams(), which calls connectionParamsController.servicePending(...) when enabled
    • NimBLE callback rewiring:
      • onConnect captures the active session via onBleConnected(connHandle)
      • onConnParamsUpdate records completion/status via onConnectionParamsUpdate(connHandle, interval, status)
    • Session teardown: resetBleSessionState() clears controller-held state via resetConnectionParams()
  • Added comprehensive unit tests for controller behavior (test/test_ble_connection_params/test_main.cpp)
    • Covers coalescing, peer-satisfied updates, retry/backoff after submission rejection, lane-timeout selection, quiet-period behavior, generation retirement, disconnected handling, and wrap-safe timing
  • Added Throttle::remainingTimespanMs(...) helper (src/mesh/Throttle.h) for remaining-time/wrap-safe scheduling

Fixes

  • Prevents overlapping GAP “connection params update” submissions by ensuring NimBLE requests are serialized through a single controller lane (next request is only admitted after completion is recorded).
  • Ensures successful updates release the lane only when safe (correct connection handle, success status, and satisfaction conditions), and prevents ambiguous failures from prematurely clearing the active request.
  • Avoids applying completions from retired sessions by retiring/ignoring work via the controller generation token.
  • Ensures controller reset/disconnect discards in-flight/queued work so a new connection cannot be affected by stale callbacks.

Refactors

  • Refactored NimBLE connection-parameter flow to separate “requesting desired mode” from “submitting the GAP procedure” via the controller.
  • Rewired callback responsibilities so NimBLE completion reporting updates controller state instead of directly applying parameter changes.

Breaking changes / migration

No external API changes are expected for normal operation; this is an internal sequencing change plus new unit coverage. Note that for CONFIG_IDF_TARGET_ESP32, the controller-based connection-parameter request/service path is currently compiled out (requests become no-ops).

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

Adds a generation-aware BLE connection-parameter controller, integrates it with NimBLE callbacks and configuration flow, handles retries and session resets, and adds comprehensive controller tests.

Changes

BLE connection parameter orchestration

Layer / File(s) Summary
Connection parameter controller state machine
src/nimble/ConnectionParamsUpdateController.h, src/mesh/Throttle.h
Defines connection modes, synchronized session state, satisfaction checks, request queuing, completion handling, timeouts, retries, and generation-based reset behavior.
Bluetooth connection-parameter integration
src/nimble/NimbleBluetooth.cpp
Routes configuration changes and NimBLE callbacks through the controller, submits profile-specific parameters, schedules pending work, and resets controller state during teardown.
Controller behavior validation
test/test_ble_connection_params/test_main.cpp
Adds Unity tests for sequencing, completion, retries, timeouts, mode replacement, quiet periods, backoff, and millisecond wrap safety.
Session and test harness validation
test/test_ble_connection_params/test_main.cpp, test/native-suite-count
Tests reset atomicity, retired generations, handle filtering, disconnected behavior, and registers the additional native suite.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: thebentern, jp-bennett, nomdetom

Sequence Diagram(s)

sequenceDiagram
  participant NimBLECallback
  participant BluetoothPhoneAPI
  participant ConnectionParamsUpdateController
  participant BLEServer
  NimBLECallback->>BluetoothPhoneAPI: Report connection
  BluetoothPhoneAPI->>ConnectionParamsUpdateController: Admit connection and obtain generation
  BluetoothPhoneAPI->>ConnectionParamsUpdateController: Queue requested mode
  BluetoothPhoneAPI->>ConnectionParamsUpdateController: Service pending request
  ConnectionParamsUpdateController-->>BluetoothPhoneAPI: Return connection-parameter request
  BluetoothPhoneAPI->>BLEServer: Submit requestConnParams
  NimBLECallback->>BluetoothPhoneAPI: Report update completion
  BluetoothPhoneAPI->>ConnectionParamsUpdateController: Record interval and status
Loading

Poem

A rabbit queues packets in flight,
With modes hopping left, then right.
NimBLE calls back,
Sessions stay on track,
And retries land just right.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is missing and none of the required template sections or attestations were filled in. Replace the placeholder text with the required template sections, complete the attestations, and note testing and any affected devices.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: serializing BLE connection parameter updates.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nimble-connection-parameter-serialization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 2 times, most recently from 65b0999 to 500e3e7 Compare July 27, 2026 23:25
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from 437bbc5 to dafa583 Compare July 27, 2026 23:28
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 2 times, most recently from 914ac5d to d30c67e Compare July 28, 2026 01:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
test/test_ble_connection_params/test_main.cpp (1)

72-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reused output parameter reduces readability.

retired is reused as the output parameter for the unrelated beginNext call at Line 86 after already serving as the original in-flight request snapshot. Harmless here since the call returns false, but a dedicated scratch variable would make the test easier to follow.

Separately, this test only covers the reused-handle completion case where both sessions request the same mode (INITIAL_HIGH_THROUGHPUT); it doesn't cover a stale completion for a different mode than the new session's in-flight request, which is the scenario flagged in src/nimble/ConnectionParamsUpdateController.h (Lines 80-93).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_ble_connection_params/test_main.cpp` around lines 72 - 92, Improve
test_retired_session_rejects_stale_submission_and_wrong_handle_completion by
using a dedicated scratch ConnectionParamsRequest for the final beginNext(false)
check instead of overwriting retired. Configure retired and current with
different update modes, using the existing request fields and mode constants, so
the reused-handle completion exercises a stale completion whose mode differs
from the new session’s in-flight request. Preserve the assertions that stale
submissions and completions do not resurrect or incorrectly consume the retired
request.
src/nimble/NimbleBluetooth.cpp (1)

440-487: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated/diverging connection-parameter magic numbers between INITIAL_HIGH_THROUGHPUT and requestHighThroughputConnection.

Line 469 inlines requestConnParams(request.connHandle, 6, 12, 0, 200) — same min/max interval and latency as requestHighThroughputConnection() (Line 509: 6, 12, 0, 600), but with a different, undocumented supervision timeout (200 vs 600). Unlike the other two modes, this case isn't routed through a dedicated helper with the descriptive parameter-rationale comment block used elsewhere, so the discrepancy looks accidental rather than a deliberate design choice.

Consider extracting a shared helper (parameterized by timeout, or with a documented reason for the 200 vs 600 difference) to avoid future drift between these two similar requests.

♻️ Suggested consolidation
-        case ConnectionParamsMode::INITIAL_HIGH_THROUGHPUT:
-            LOG_INFO("BLE requestInitialHighThroughputConnection");
-            accepted = bleServer->requestConnParams(request.connHandle, 6, 12, 0, 200);
-            break;
+        case ConnectionParamsMode::INITIAL_HIGH_THROUGHPUT:
+            // NOTE: uses a shorter supervision timeout than steady-state high-throughput mode
+            // to detect a bad initial connection faster. See requestHighThroughputConnection().
+            accepted = requestInitialHighThroughputConnection(request.connHandle);
+            break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nimble/NimbleBluetooth.cpp` around lines 440 - 487, Consolidate the
duplicated connection-parameter values used by updateConnectionParams for
INITIAL_HIGH_THROUGHPUT and requestHighThroughputConnection into a shared helper
or shared parameter definition. Preserve the intended supervision-timeout
difference only if deliberate, and document its rationale; otherwise use one
consistent timeout. Keep the existing mode-specific behavior while preventing
future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nimble/NimbleBluetooth.cpp`:
- Around line 211-216: Track the active connection-parameter update generation
when setting requestedMode and updateInProgress, and pass or validate it in
onConnectionParamsUpdate/onUpdateComplete. Reject completion callbacks whose
generation no longer matches the active request, mirroring the stale-generation
handling in onSubmissionRejected(), while preserving wakeForConnectionParams()
only for valid completions.

---

Nitpick comments:
In `@src/nimble/NimbleBluetooth.cpp`:
- Around line 440-487: Consolidate the duplicated connection-parameter values
used by updateConnectionParams for INITIAL_HIGH_THROUGHPUT and
requestHighThroughputConnection into a shared helper or shared parameter
definition. Preserve the intended supervision-timeout difference only if
deliberate, and document its rationale; otherwise use one consistent timeout.
Keep the existing mode-specific behavior while preventing future drift.

In `@test/test_ble_connection_params/test_main.cpp`:
- Around line 72-92: Improve
test_retired_session_rejects_stale_submission_and_wrong_handle_completion by
using a dedicated scratch ConnectionParamsRequest for the final beginNext(false)
check instead of overwriting retired. Configure retired and current with
different update modes, using the existing request fields and mode constants, so
the reused-handle completion exercises a stale completion whose mode differs
from the new session’s in-flight request. Preserve the assertions that stale
submissions and completions do not resurrect or incorrectly consume the retired
request.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c9cd24a0-3c50-4c8c-97b3-8b366af37a67

📥 Commits

Reviewing files that changed from the base of the PR and between dafa583 and d30c67e.

📒 Files selected for processing (4)
  • src/nimble/ConnectionParamsUpdateController.h
  • src/nimble/NimbleBluetooth.cpp
  • test/native-suite-count
  • test/test_ble_connection_params/test_main.cpp

Comment thread src/nimble/NimbleBluetooth.cpp Outdated
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from d30c67e to 77969c4 Compare July 28, 2026 10:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pr_enforce_labels.yml:
- Line 13: Remove the job-level `if: false` guard from the PR label enforcement
workflow, either deleting the disabled job/workflow if it is no longer needed or
replacing the guard with an explicit repository variable condition that can
re-enable enforcement.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 32a45417-2990-482d-af5f-b14847ef83c6

📥 Commits

Reviewing files that changed from the base of the PR and between d30c67e and 77969c4.

📒 Files selected for processing (7)
  • .coderabbit.yaml
  • .github/workflows/pr_enforce_labels.yml
  • .gitignore
  • src/nimble/ConnectionParamsUpdateController.h
  • src/nimble/NimbleBluetooth.cpp
  • test/native-suite-count
  • test/test_ble_connection_params/test_main.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/native-suite-count
  • src/nimble/ConnectionParamsUpdateController.h
  • test/test_ble_connection_params/test_main.cpp
  • src/nimble/NimbleBluetooth.cpp

Comment thread .github/workflows/pr_enforce_labels.yml Outdated
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 2 times, most recently from 81125c5 to baaaa62 Compare July 28, 2026 10:24
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from 0fb8390 to d2b11d5 Compare July 28, 2026 10:26
@jeremiah-k

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nimble/NimbleBluetooth.cpp`:
- Around line 467-470: Extract the inline INITIAL_HIGH_THROUGHPUT request from
the connection-parameter handling into a documented
requestInitialHighThroughputConnection helper, matching the existing
high-throughput helpers. Preserve the parameters (6, 12, 0, 200) and document
that the 2-second supervision timeout intentionally detects initial-connection
failures faster than the regular 6-second mode; update the case to call this
helper.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 87a418a3-62f5-4d3c-9d51-d7ca273d5944

📥 Commits

Reviewing files that changed from the base of the PR and between d2b11d5 and baaaa62.

📒 Files selected for processing (4)
  • src/nimble/ConnectionParamsUpdateController.h
  • src/nimble/NimbleBluetooth.cpp
  • test/native-suite-count
  • test/test_ble_connection_params/test_main.cpp

Comment thread src/nimble/NimbleBluetooth.cpp Outdated
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 2 times, most recently from 27e5a6e to 902345e Compare July 28, 2026 17:41
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from d2b11d5 to a8623a6 Compare July 28, 2026 17:44
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from 902345e to aa6f88f Compare July 28, 2026 19:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pr_enforce_labels.yml:
- Line 3: Update the workflow_dispatch configuration to require a pull request
number input, then modify the label-fetching logic to use that input and
retrieve the PR labels through the GitHub API instead of relying on
context.payload.pull_request. Preserve the existing required-label enforcement
behavior for manually dispatched runs.

In `@src/nimble/ConnectionParamsUpdateController.h`:
- Around line 46-60: Update request() so the satisfied-mode early return only
clears desiredMode when no different pending mode exists; preserve an existing
non-NONE desiredMode instead of overwriting it. Keep the current validation and
new-mode assignment behavior unchanged for requests that are not already
satisfied.
- Around line 106-119: Update onUpdateComplete() to release the update lane when
status != 0 by clearing updateInProgress and requestedMode before returning. If
immediate release is not valid, add a bounded Throttle-based staleness timeout
that clears the lane without accepting arbitrary peer-initiated events, while
preserving the existing success-path completion logic.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 008357ae-251f-4240-b644-746a0ae40c3c

📥 Commits

Reviewing files that changed from the base of the PR and between baaaa62 and aa6f88f.

📒 Files selected for processing (7)
  • .coderabbit.yaml
  • .github/workflows/pr_enforce_labels.yml
  • .gitignore
  • src/nimble/ConnectionParamsUpdateController.h
  • src/nimble/NimbleBluetooth.cpp
  • test/native-suite-count
  • test/test_ble_connection_params/test_main.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/native-suite-count

Comment thread .github/workflows/pr_enforce_labels.yml
Comment thread src/nimble/ConnectionParamsUpdateController.h Outdated
Comment thread src/nimble/ConnectionParamsUpdateController.h Outdated
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from aa6f88f to 7a8b4b7 Compare July 28, 2026 19:23
@jeremiah-k

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a serialized, generation-aware controller for BLE connection parameter updates on NimBLE/ESP32, routing all high-throughput/low-power mode switches through a single main-task lane and backing it with unit tests.

Sequence diagram for serialized BLE connection-parameter updates

sequenceDiagram
    actor User
    participant BluetoothPhoneAPI
    participant ConnectionParamsUpdateController as Controller
    participant NimbleBluetoothServerCallback as ServerCallback
    participant BLEServer

    User->>BluetoothPhoneAPI: onConfigStart()
    BluetoothPhoneAPI->>BluetoothPhoneAPI: requestConnectionParams(HIGH_THROUGHPUT)
    BluetoothPhoneAPI->>Controller: request(currentGeneration(), HIGH_THROUGHPUT)

    loop main_task
        BluetoothPhoneAPI->>BluetoothPhoneAPI: updateConnectionParams()
        BluetoothPhoneAPI->>Controller: beginNext()
        alt [request available]
            BluetoothPhoneAPI->>BLEServer: requestConnParams(connHandle, ...)
            alt [submission rejected]
                BluetoothPhoneAPI->>Controller: onSubmissionRejected(request)
            end
        end
    end

    BLEServer-->>ServerCallback: onConnParamsUpdate(connHandle, interval, status)
    ServerCallback->>BluetoothPhoneAPI: onConnectionParamsUpdate(connHandle, interval, status)
    BluetoothPhoneAPI->>Controller: onUpdateComplete(connHandle, interval, status)
    alt [Controller returns true]
        BluetoothPhoneAPI->>BluetoothPhoneAPI: wakeForConnectionParams()
    end

    User->>BluetoothPhoneAPI: onConfigComplete()
    BluetoothPhoneAPI->>BluetoothPhoneAPI: requestConnectionParams(LOW_POWER)
    BluetoothPhoneAPI->>Controller: request(currentGeneration(), LOW_POWER)
    note over BluetoothPhoneAPI,Controller: New request coalesces and waits until previous update completes
Loading

File-Level Changes

Change Details Files
Serialize BLE connection-parameter updates through a controller integrated into BluetoothPhoneAPI and Nimble callbacks.
  • Include ConnectionParamsUpdateController in NimbleBluetooth and alias its types in the translation unit.
  • Add a ConnectionParamsUpdateController member to BluetoothPhoneAPI plus helper methods to react to connection, parameter update, and reset events.
  • Change BluetoothPhoneAPI::runOnce to drive connection-parameter updates via updateConnectionParams, waking the main loop when controller state changes.
  • Replace direct high-throughput/low-power update calls in onConfigStart/onConfigComplete with mode requests on the controller.
src/nimble/NimbleBluetooth.cpp
src/nimble/ConnectionParamsUpdateController.h
Implement ConnectionParamsUpdateController to coalesce and gate ESP32 GAP connection-parameter procedures.
  • Define ConnectionParamsMode and ConnectionParamsRequest to represent desired profiles and lane submissions.
  • Track connection handle, current interval, desired/requested modes, in-flight state, and a generation counter under a mutex.
  • Provide request/beginNext APIs that skip redundant updates when the peer already satisfies the desired mode and ensure only one active update.
  • Handle submission rejection and onUpdateComplete callbacks by checking handle/generation, updating interval, clearing completed modes, and signaling when more work is pending.
  • Reset controller state and retire older generations on BLE session reset or reconnect.
src/nimble/ConnectionParamsUpdateController.h
Route Nimble server callbacks through the controller and adjust BLE server API usage.
  • On connect, notify BluetoothPhoneAPI so the controller can start a new generation and capture the connection handle.
  • Add onConnParamsUpdate callback to forward NimBLE connection parameter updates (handle, interval, status) into the controller.
  • Replace direct BLEServer::updateConnParams calls with requestConnParams that return a success flag used by the controller retry logic.
  • Reset controller state as part of resetBleSessionState to avoid stale in-flight requests across sessions.
src/nimble/NimbleBluetooth.cpp
Add unit tests to validate controller behavior across edge cases and update native test suite count.
  • Create a new test suite for ConnectionParamsUpdateController covering peer-initiated updates, redundant updates, coalescing of desired modes, submission retries, handle reuse, generation mismatches, failures, and disconnected behavior.
  • Assert that requests are correctly shaped (handle, mode, generation) and that controller methods return expected booleans for each scenario.
  • Update native-suite-count to reflect the new BLE connection params test suite.
test/test_ble_connection_params/test_main.cpp
test/native-suite-count

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The interval thresholds used in ConnectionParamsUpdateController::isSatisfied() (e.g., <= 12, >= 24) should be tied to shared named constants or derived from the actual requestHighThroughputConnection/requestLowerPowerConnection parameters to avoid drift if the connection parameter presets are changed later.
  • In ConnectionParamsUpdateController::onUpdateComplete, when status != 0 you leave updateInProgress set and never release the lane, which can permanently block future requests after an asynchronous failure; consider explicitly clearing updateInProgress (and perhaps requestedMode) on non-zero status and deciding whether to re-queue or drop the desired mode.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The interval thresholds used in `ConnectionParamsUpdateController::isSatisfied()` (e.g., `<= 12`, `>= 24`) should be tied to shared named constants or derived from the actual `requestHighThroughputConnection`/`requestLowerPowerConnection` parameters to avoid drift if the connection parameter presets are changed later.
- In `ConnectionParamsUpdateController::onUpdateComplete`, when `status != 0` you leave `updateInProgress` set and never release the lane, which can permanently block future requests after an asynchronous failure; consider explicitly clearing `updateInProgress` (and perhaps `requestedMode`) on non-zero status and deciding whether to re-queue or drop the desired mode.

## Individual Comments

### Comment 1
<location path="src/nimble/ConnectionParamsUpdateController.h" line_range="99-108" />
<code_context>
+     * request token. Treating every same-handle callback as our completion can therefore release the lane on Android's own
+     * priority update and submit a second peripheral procedure while controller work is still active.
+     */
+    bool onUpdateComplete(uint16_t connHandle, uint16_t interval, uint8_t status)
+    {
+        std::lock_guard<std::mutex> guard(stateMutex);
+        if (connHandle != connectionHandle) {
+            return false;
+        }
+
+        if (status == 0) {
+            currentInterval = interval;
+        }
+        if (!updateInProgress || status != 0 || !isSatisfied(requestedMode, currentInterval)) {
+            return false;
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** Async failures keep the controller lane permanently blocked

In the non-zero `status` path, we return without clearing `updateInProgress` or `requestedMode`, so a failed controller/peer update can leave the lane stuck “in progress” and block `beginNext()` from ever admitting new requests. Please handle the failure case explicitly by resetting `updateInProgress` (and likely `requestedMode`) and deciding whether `desiredMode` should remain pending for retry or be dropped, similar to `onSubmissionRejected()`.
</issue_to_address>

### Comment 2
<location path="test/test_ble_connection_params/test_main.cpp" line_range="152-21" />
<code_context>
+    TEST_ASSERT_FALSE(controller.onUpdateComplete(1, 12, 0));
+}
+
+static void test_retired_session_request_cannot_target_reconnect()
+{
+    ConnectionParamsUpdateController controller;
+    controller.onConnected(1);
+    const uint32_t retiredGeneration = controller.currentGeneration();
+
+    controller.reset();
+    controller.onConnected(1);
+
+    TEST_ASSERT_FALSE(controller.request(retiredGeneration, ConnectionParamsMode::LOW_POWER));
+    TEST_ASSERT_FALSE(controller.beginNext().has_value());
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for `reset()` while an update is in progress to cover mid-flight retirement

The existing tests only cover retirement before a new submission cycle starts. Please also add coverage for calling `reset()` while `updateInProgress` is true: after `onConnected`, queue a mode and call `beginNext()` (so `updateInProgress = true`), then call `reset()`. Finally, invoke `onSubmissionRejected` / `onUpdateComplete` with the old `ConnectionParamsRequest` and assert they return `false` and neither re-open the lane nor emit new requests. This will lock in the behavior for in-flight requests from a retired session.

Suggested implementation:

```cpp
    ConnectionParamsUpdateController controller;
    controller.onConnected(1);
    TEST_ASSERT_TRUE(controller.request(controller.currentGeneration(), ConnectionParamsMode::HIGH_THROUGHPUT));
    TEST_ASSERT_TRUE(controller.beginNext().has_value());

    TEST_ASSERT_FALSE(controller.onUpdateComplete(1, 12, 1));
    TEST_ASSERT_FALSE(controller.beginNext().has_value());
    TEST_ASSERT_FALSE(controller.onUpdateComplete(1, 12, 0));
}

static void test_reset_mid_flight_retires_in_flight_requests()
{
    ConnectionParamsUpdateController controller;

    // Start a session and enqueue a request
    controller.onConnected(1);
    const uint32_t generation = controller.currentGeneration();

    TEST_ASSERT_TRUE(controller.request(generation, ConnectionParamsMode::HIGH_THROUGHPUT));

    // Begin processing the next request so that an update is in progress
    auto inFlightOpt = controller.beginNext();
    TEST_ASSERT_TRUE(inFlightOpt.has_value());
    const ConnectionParamsRequest inFlight = *inFlightOpt;

    // Reset the controller while the update is in progress
    controller.reset();

    // Callbacks for the old, in-flight request from the retired session must be ignored
    TEST_ASSERT_FALSE(controller.onSubmissionRejected(inFlight));
    TEST_ASSERT_FALSE(controller.onUpdateComplete(inFlight));

    // No new work should be emitted and the lane must remain closed
    TEST_ASSERT_FALSE(controller.beginNext().has_value());
}

using meshtastic::bluetooth::ConnectionParamsMode;
using meshtastic::bluetooth::ConnectionParamsRequest;
using meshtastic::bluetooth::ConnectionParamsUpdateController;

```

The above edit assumes the following overloads exist:
- `bool ConnectionParamsUpdateController::onSubmissionRejected(const ConnectionParamsRequest &request);`
- `bool ConnectionParamsUpdateController::onUpdateComplete(const ConnectionParamsRequest &request);`

If, instead, `onSubmissionRejected` / `onUpdateComplete` use a different signature (e.g. individual fields such as handle, generation, interval, latency, status flags), you should:
1. Replace the calls:

   - `controller.onSubmissionRejected(inFlight);`
   - `controller.onUpdateComplete(inFlight);`

   with calls that match your actual API, using the values from `inFlight` (and any other appropriate constants) to represent the “old” request.

2. If there is a separate observable side-effect for “re-opening the lane” (e.g. a callback, flag, or counter), add assertions after `reset()` / the callback invocations to confirm that side-effect does not occur, in addition to the `beginNext()` assertions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/nimble/ConnectionParamsUpdateController.h Outdated
Comment thread test/test_ble_connection_params/test_main.cpp Outdated
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from 7a8b4b7 to a5b1394 Compare July 28, 2026 20:09
@jeremiah-k

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nimble/ConnectionParamsUpdateController.h`:
- Around line 112-134: Update onUpdateComplete() to handle status == 0 callbacks
that leave isSatisfied(requestedMode, currentInterval) false: clear
requestedMode and updateInProgress while preserving desiredMode, then release
the blocking Throttle-based timeout using the existing time-based fallback
pattern so beginNext() can retry.
- Around line 105-111: Shorten the documentation comment immediately preceding
onUpdateComplete to no more than two lines while retaining only the essential
rationale for recording the negotiated interval and avoiding premature request
release; remove the extended explanation and any wording that restates the
implementation.

In `@src/nimble/NimbleBluetooth.cpp`:
- Around line 456-475: The request admission and GAP submission in the
connection-parameter scheduling flow must be atomic with session lifecycle
changes. Replace the separate connectionParamsController.beginNext(), mode
dispatch, and onSubmissionRejected() sequence with a controller-owned submission
operation that validates the request and invokes
requestHighThroughputConnection() or requestLowerPowerConnection() while
synchronized against onConnected() and reset(), preventing stale handle reuse
and untracked in-flight updates; add a regression test covering
disconnect/reset/reconnect handle reuse.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 477dd298-75bf-4b6e-a144-0f4ff4841682

📥 Commits

Reviewing files that changed from the base of the PR and between 21a0675 and a5b1394.

📒 Files selected for processing (4)
  • src/nimble/ConnectionParamsUpdateController.h
  • src/nimble/NimbleBluetooth.cpp
  • test/native-suite-count
  • test/test_ble_connection_params/test_main.cpp

Comment thread src/nimble/ConnectionParamsUpdateController.h Outdated
Comment thread src/nimble/ConnectionParamsUpdateController.h Outdated
Comment thread src/nimble/NimbleBluetooth.cpp Outdated
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 2 times, most recently from a93ebdb to e166df8 Compare July 28, 2026 21:07
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Changed-file structure =='
ast-grep outline src/nimble/NimbleBluetooth.cpp --items all --match 'BluetoothPhoneAPI|onDisconnect|onConnect|requestConnectionParams|resetBleSessionState|resetConnectionParams' || true
ast-grep outline src/nimble/ConnectionParamsUpdateController.h --items all --match 'ConnectionParamsUpdateController|recordUpdate|onConnected|request|reset' || true

echo '== Controller integration and generation captures =='
rg -n -C 6 --glob 'src/nimble/NimbleBluetooth.cpp' \
  'onBleConnected\s*\(|onConnectionParamsUpdate\s*\(|resetConnectionParams\s*\(|resetBleSessionState\s*\(|requestConnectionParams\s*\(|connectionParamsGeneration|currentGeneration\s*\(' \
  src/nimble/NimbleBluetooth.cpp || true

echo '== Controller callback/reset conditions =='
rg -n -C 6 --glob 'src/nimble/ConnectionParamsUpdateController.h' \
  'void\s+(onConnected|recordUpdate|reset)\s*\(|uint32_t\s+currentGeneration\s*\(|bool\s+request\s*\(|generation|connectionHandle|status' \
  src/nimble/ConnectionParamsUpdateController.h || true

echo '== PR diff for relevant paths =='
git diff --unified=20 HEAD^ HEAD -- src/nimble/NimbleBluetooth.cpp src/nimble/ConnectionParamsUpdateController.h || true

Length of output: 30422


🤖 web_search executed:

In ESP-IDF v5.5.4 components/bt/host/nimble/nimble/nimble/host/src/ble_gap.c, what is the ordering between an active connection-parameter-update completion event and a disconnect event? Cite the relevant functions or code paths. Also confirm the fields provided by BLE_GAP_EVENT_CONN_UPDATE in the corresponding ESP-IDF v5.5.4 ble_gap.h event payload.

Length of output: 12004


@jeremiah-k You’re right. I verified both points:

  • BLE_GAP_EVENT_CONN_UPDATE provides a connection handle and status, not an epoch, so inFlightGeneration would not prove callback provenance after handle reuse.
  • In the pinned NimBLE disconnect path, ble_gap_conn_broken() removes the update entry and calls ble_gap_update_notify() before it emits BLE_GAP_EVENT_DISCONNECT. Your onDisconnect() then reaches resetBleSessionState() → resetConnectionParams(), retiring the session afterward.
  • The separate admission-token issue is fixed: onBleConnected() captures the generation returned by onConnected(), and requestConnectionParams() uses that captured atomic rather than re-reading the controller generation immediately before request().

Keeping successful matching-handle peer updates able to refresh currentInterval is therefore the correct behavior. I’m withdrawing the inFlightGeneration concern.

🐇🧠


✏️ Learnings added
Learnt from: jeremiah-k
URL: https://github.com/jeremiah-k/meshtastic-firmware/pull/8

Timestamp: 2026-07-29T22:59:05.475Z
Learning: For the ESP-IDF v5.5.4 pinned NimBLE host used by Meshtastic firmware, `ble_gap_conn_broken()` removes an active connection-parameter update and emits its `BLE_GAP_EVENT_CONN_UPDATE` completion via `ble_gap_update_notify()` before delivering `BLE_GAP_EVENT_DISCONNECT`. In `src/nimble/NimbleBluetooth.cpp`, disconnect handling resets `ConnectionParamsUpdateController` from the disconnect callback; do not require an epoch on `BLE_GAP_EVENT_CONN_UPDATE`, because its payload only identifies the connection by handle and status, and peer-initiated successful updates must still refresh the current interval.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

This chat is LLM-generated and may be inaccurate.

@jeremiah-k

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nimble/ConnectionParamsUpdateController.h`:
- Around line 120-140: Remove the unreachable reset assignments to requestedMode
and stateStartedAtMs inside the waitMs != 0 branch, since both are overwritten
immediately by the subsequent update flow. Preserve the remaining throttle check
and the final requestedMode, updateInProgress, and stateStartedAtMs assignments
in the surrounding controller logic.

In `@test/test_ble_connection_params/test_main.cpp`:
- Around line 140-152: Update
test_latest_mode_already_satisfied_at_timeout_needs_no_submission so its final
serviceAt call occurs at or after the actual lane expiry: LOW_POWER_SETTLE_MS
plus UPDATE_LANE_TIMEOUT_MS, accounting for the timing used by the
HIGH_THROUGHPUT request. Keep the existing no-submission assertion, ensuring
execution reaches the expiry-time isSatisfied check rather than the
lane-still-active early return.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 65f8d2fc-8d65-43f1-ba48-effabdec3213

📥 Commits

Reviewing files that changed from the base of the PR and between 0066705 and 43e7a97.

📒 Files selected for processing (5)
  • src/mesh/Throttle.h
  • src/nimble/ConnectionParamsUpdateController.h
  • src/nimble/NimbleBluetooth.cpp
  • test/native-suite-count
  • test/test_ble_connection_params/test_main.cpp

Comment thread src/nimble/ConnectionParamsUpdateController.h
Comment thread test/test_ble_connection_params/test_main.cpp
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 2 times, most recently from b185a90 to 43ae0e1 Compare July 30, 2026 01:41
@jeremiah-k

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from 43ae0e1 to 79522c6 Compare July 30, 2026 11:18
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from 0066705 to 42b530d Compare July 30, 2026 11:18
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from 79522c6 to 49a7049 Compare July 30, 2026 12:51
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from 42b530d to 505f330 Compare July 30, 2026 19:47
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from 49a7049 to 5d005e7 Compare July 30, 2026 19:47
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from 505f330 to ee53b73 Compare July 31, 2026 12:34
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch 7 times, most recently from dc65b34 to a857cca Compare July 31, 2026 21:02
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization-review branch from ee53b73 to 09dadfc Compare August 1, 2026 13:03
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from a857cca to 6c6b98e Compare August 1, 2026 13:03
Route initial, high-throughput, and low-power connection parameter changes through one controller lane. Coalesce the latest desired mode while an update is active and submit it only after NimBLE reports the previous procedure complete.

Guard session transitions with one mutex and require callers to present the session generation when queueing a mode. A callback from a retired connection therefore cannot queue low-power parameters for a newly connected phone, even when ESP32 reuses the connection handle.

Use fully qualified controller types at their call sites rather than file-local aliases. This keeps the commit self-contained when neighboring BLE fixes add constants to the same namespace block.

This avoids overlapping GAP update requests during back-to-back configuration handshakes, which the ESP32 host rejects as already in progress and which preceded an ld_acl controller assertion in hardware logs. Reset all lane state with the BLE session and keep controller calls on the existing main-task path.
Treat peer-selected connection parameters as observed link state instead of assuming every successful callback belongs to the firmware request. Release the single GAP lane only when the interval satisfies the active mode and doing so cannot immediately launch an opposite pending mode; ambiguous callbacks remain bounded by NimBLE's deadline plus cleanup margin.

Submit under the session lock so disconnect or handle reuse cannot retarget the call, retry synchronous rejection after a bounded delay, and remove the redundant connection-time request so the central can establish its initial priority. Tests cover peer updates, wrong handles, failures, lane timeout, session retirement, atomic submission, and millisecond wraparound.
Serialize firmware-initiated connection-parameter procedures on newer ESP32-family controllers and preserve only the latest requested mode while a GAP update is active.

Carry the connection generation captured by the NimBLE connect callback into main-task configuration requests so work from a retired BLE session cannot be admitted against a reconnect.

Compile the lane and its state out of original ESP32 builds because locally initiated parameter procedures can panic inside the closed controller. Android already selects High and later Balanced priority, so the central owns connection timing on that target.

Retain bounded rejected-submission backoff, wrap-safe lane recovery, and conservative callback correlation. An already-satisfied newest mode now cancels any older opposite request, preventing a deferred low-power transition from leaking into a new configuration phase.
@jeremiah-k
jeremiah-k force-pushed the fix/nimble-connection-parameter-serialization branch from 6c6b98e to 3c0624c Compare August 1, 2026 23:12
@jeremiah-k
jeremiah-k merged commit 8ee17f9 into fix/nimble-connection-parameter-serialization-review Aug 29, 2026
3 checks passed
@jeremiah-k
jeremiah-k deleted the fix/nimble-connection-parameter-serialization branch August 29, 2026 11:37
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