Skip to content

fix(ble): keep pairing PIN dialog open on first connect (#2057)#2113

Open
bruschill wants to merge 2 commits into
meshtastic:mainfrom
bruschill:fix/ble-pairing-pin-dialog-2057
Open

fix(ble): keep pairing PIN dialog open on first connect (#2057)#2113
bruschill wants to merge 2 commits into
meshtastic:mainfrom
bruschill:fix/ble-pairing-pin-dialog-2057

Conversation

@bruschill

@bruschill bruschill commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What changed?

Fixes #2057 — the custom-PIN BLE pairing dialog opening and then immediately closing before the user can enter the PIN.

  • Gate connect completion on real bonding. BLEConnection.didDiscoverCharacteristicsFor no longer resolves the connect step immediately after issuing subscriptions. It now waits for the FROMNUM notify subscription to confirm (peripheral(_:didUpdateNotificationStateFor:error:)), which is the definitive "bonding succeeded" signal and — on a first-ever connection — does not fire until the user dismisses the pairing sheet.
  • Give first-time pairing enough time. AccessoryManager.connect Step 1 uses a 90s window when the peripheral is not yet known-bonded; already-bonded radios and non-BLE transports keep the fast 5s timeout so a dead/out-of-range radio still fails quickly. The hint is seeded from preferredPeripheralId so existing users don't pay the long window after upgrading.
  • Classify notify-state errors. A genuine auth/encryption failure (wrong or cancelled PIN) fails the connect cleanly with the existing "check the BLE PIN" message; benign per-characteristic errors (e.g. a characteristic that doesn't support notifications) are ignored so they can't fail an otherwise-good connect.
  • Fail fast on mid-pairing link drop. BLEConnection.disconnect now resumes a still-suspended connect continuation, so if the link drops during pairing (iOS commonly delivers a cancelled sheet as a peripheral disconnect rather than a characteristic error) Step 1 fails immediately instead of stalling out the full timeout.
  • Persist a self-healing paired-peripheral hint in UserDefaults (added/removed as bonds succeed or tear down mid-pairing).

Why did it change?

The actor-based transport rewrite runs the connect handshake as fixed hard-timeout steps and, on timeout, tears down the CoreBluetooth peripheral via cancelPeripheralConnection. Because the old flow resolved Step 1 before bonding completed and never waited for the pairing sheet, a 5s (or, later, 30s) timeout could cancel the connection while the system PIN sheet was still displayed — which dismisses the sheet. Pre-rewrite the connection-teardown timer was invalidated at didConnect, before the pairing-triggering encrypted access, so the sheet survived; this restores that behavior in the new architecture.

How is this tested?

  • New unit tests (MeshtasticTests/BLEPairingHintTests.swift, Swift Testing) — all green:
    • BLEPairingFailureTests: BLEConnection.isPairingFailure(_:) classification (auth/encryption CBATTError and CBError.encryptionTimedOut/peerRemovedPairingInformation → failure; benign errors → not a failure).
    • PairedPeripheralHintTests (.serialized): the UserDefaults paired-hint helpers (remember/forget/idempotency/sorted storage).
    • xcodebuild test on iPhone 16 Pro Max sim: 10/10 passing.
  • Manual on-device plan (PIN-secured firmware): fresh pair with slow PIN entry, wrong/cancelled PIN, already-bonded fast reconnect, dead/out-of-range fast-fail, forget-and-repair. The actor delegate-callback flow isn't unit-testable without a CoreBluetooth mock harness the project doesn't currently have.

Screenshots/Videos (when applicable)

N/A — behavior change to the BLE connect handshake; no UI changes.

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/. No Meshtastic/Views/** or @Model changes, so no doc update is required — adding the skip-docs-check label.
  • I have tested the change to ensure that it works as intended.

Summary by CodeRabbit

  • Bug Fixes

    • Improved Bluetooth connection reliability by waiting for the required notification subscription to be fully confirmed before completing setup.
    • Improved handling of pairing failures and disconnects during the connection handshake, including proper recovery when setup is still in progress.
    • Extended the initial Bluetooth connection timeout for first-time (not-yet-bonded) devices to reduce connection failures.
  • Improvements

    • Persist and manage a list of paired Bluetooth device identifiers to guide future connection behavior.
    • Added documentation describing the BLE pairing PIN/notification handshake sequence and timeout differences.
    • Added focused tests validating pairing-hint behavior and pairing failure classification.

)

The connect handshake ran with hard step timeouts and tore down the
CoreBluetooth peripheral on expiry. On a first-ever connection iOS presents
the pairing PIN sheet during characteristic subscription, but the old flow
resolved Step 1 immediately and a 5s timeout could cancelPeripheralConnection
while the sheet was up, dismissing it before the user could enter the PIN.

- Gate connect completion on the FROMNUM notify subscription confirming (the
  definitive bonding-complete signal) instead of resolving optimistically.
- Give Step 1 a 90s window for a first-time BLE bond; already-bonded radios
  (and non-BLE transports) keep the fast 5s fail so dead/out-of-range devices
  still fail quickly. Seeds the hint from preferredPeripheralId to avoid an
  upgrade slowdown.
- Classify notify-state errors: real auth/encryption failures fail the connect
  cleanly (with the existing check-your-PIN message); benign per-characteristic
  errors are ignored.
- Resume a suspended connect continuation on disconnect so a link drop during
  pairing (e.g. user cancels the sheet) fails fast instead of stalling.
- Persist bonded peripheral UUIDs in UserDefaults as a self-healing hint.

Adds unit tests for the paired-peripheral hint helpers and the pairing-failure
error classifier.
@bruschill bruschill added the skip-docs-check Use this label to skip the automatic docs audit label Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

BLE connection setup now uses an adaptive first-pairing timeout and waits for CoreBluetooth confirmation that FROMNUM notifications are enabled. Pairing hints, migration state, failure handling, tests, and developer documentation are updated.

Changes

BLE pairing flow

Layer / File(s) Summary
Pairing hint and adaptive timeout
Meshtastic/Extensions/UserDefaults.swift, Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift, MeshtasticTests/BLEPairingHintTests.swift
Persists paired peripheral UUID hints, migrates the legacy preferred peripheral, selects the initial BLE timeout, and tests hint storage behavior.
Notification-gated BLE handshake
Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift, MeshtasticTests/BLEPairingHintTests.swift, docs/developer/transport.md
Delays completion until FROMNUM notification confirmation, handles pairing failures and disconnects, forwards callbacks, reads initial RSSI, tests error classification, and documents the flow.

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

Sequence Diagram(s)

sequenceDiagram
  participant AccessoryManager
  participant BLEConnection
  participant CoreBluetooth
  participant UserDefaults
  AccessoryManager->>BLEConnection: Start Step 1 with adaptive timeout
  BLEConnection->>CoreBluetooth: Discover characteristics and subscribe to FROMNUM
  CoreBluetooth-->>BLEConnection: Confirm notification state
  BLEConnection->>UserDefaults: Remember paired peripheral
  BLEConnection-->>AccessoryManager: Continue connection process
Loading

Suggested reviewers: thebentern

Poem

A rabbit watched the blue lights glow,
As pairing found its steadier flow.
“Wait for the signal, then hop through—”
The timeout stretched, the hints stayed true.
🐇✨ Connection blooms anew.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the BLE pairing PIN dialog fix and matches the PR's main change.
Description check ✅ Passed The description covers What changed, Why, How tested, screenshots, and checklist items with enough detail.
Linked Issues check ✅ Passed The changes directly address #2057 by keeping first-time BLE pairing open until bonding confirms.
Out of Scope Changes check ✅ Passed The added tests and developer docs are supporting changes for the pairing fix, with no obvious unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@Meshtastic/Accessory/Accessory` Manager/AccessoryManager+Connect.swift:
- Around line 51-57: Update the preferred-device fallback used by knownBonded
and isFirstTimeBLEBond so it is consumed only once or invalidated after a
pairing failure. Ensure subsequent reconnects after iOS pairing data is removed
are treated as first-time BLE bonds, allowing the full PIN-entry timeout;
preserve the existing five-second timeout for genuinely bonded devices.

In `@Meshtastic/Accessory/Transports/Bluetooth` Low Energy/BLEConnection.swift:
- Around line 305-308: Update docs/developer/transport.md to document
BLEConnection’s notification-gated handshake, pairing timeout behavior, and
paired-hint lifecycle. Ensure the documentation reflects the current transport
implementation and satisfies the guideline requiring transport Swift changes to
update the developer transport documentation.

In `@MeshtasticTests/BLEPairingHintTests.swift`:
- Around line 25-28: Update the reset helper in BLEPairingHintTests to preserve
the existing UserDefaults.pairedPeripheralIds value and restore it after each
test via defer or equivalent teardown. Keep the initial clean-state reset while
ensuring the suite leaves the original UUID list unchanged.
🪄 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 Plus

Run ID: 96f49be5-c3df-487c-b2a5-05157e5f0632

📥 Commits

Reviewing files that changed from the base of the PR and between e531f15 and 786355d.

📒 Files selected for processing (4)
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
  • Meshtastic/Accessory/Transports/Bluetooth Low Energy/BLEConnection.swift
  • Meshtastic/Extensions/UserDefaults.swift
  • MeshtasticTests/BLEPairingHintTests.swift

Comment thread Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift Outdated
Comment thread MeshtasticTests/BLEPairingHintTests.swift Outdated
… migration

Address CodeRabbit review on meshtastic#2113:
- Convert the legacy preferredPeripheralId fallback into a one-time migration
  guarded by migratedPreferredPeripheralPairing, so a bond removed in iOS
  Settings self-heals back to the long pairing window instead of being pinned
  to the fast reconnect timeout.
- Restore original pairedPeripheralIds after each test (struct -> final class
  with init/deinit teardown).
- Document the notification-gated handshake, pairing timeout, and paired-hint
  lifecycle in docs/developer/transport.md.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@docs/developer/transport.md`:
- Around line 82-90: Regenerate the bundled HTML documentation from the updated
transport guide by running scripts/build-docs.sh with Meshtastic/Resources/docs
as the output directory, then include all resulting generated HTML changes in
the PR.
🪄 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 Plus

Run ID: a9279ced-0934-4954-af61-ca88a6ea4c24

📥 Commits

Reviewing files that changed from the base of the PR and between 786355d and 76310a2.

📒 Files selected for processing (4)
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift
  • Meshtastic/Extensions/UserDefaults.swift
  • MeshtasticTests/BLEPairingHintTests.swift
  • docs/developer/transport.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • Meshtastic/Extensions/UserDefaults.swift
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift

Comment on lines +82 to +90
### BLE Pairing PIN Handshake

A first-ever connection to an encrypted radio makes iOS present a 6-digit pairing PIN sheet. `BLEConnection` gates connect-completion on that bond so the sheet is not torn down before the user can respond:

- **Notification-gated handshake.** After the required characteristics are discovered, `BLEConnection` does *not* resolve the connect step immediately. It subscribes to the `FROMNUM` notify characteristic (always notify-capable and encrypted) and holds the connect continuation open until `didUpdateNotificationState` confirms the subscription. On a first-ever connection that CoreBluetooth callback does not fire until the user dismisses the pairing sheet, so the connection stays alive while the PIN is entered.
- **Pairing timeout.** `AccessoryManager+Connect` selects the Step 1 timeout based on whether the peripheral is already bonded: a first-time BLE bond gets a long window (90s) so there is time to read and type the PIN, while already-bonded peripherals and non-BLE transports keep the fast reconnect timeout (5s) so a dead/out-of-range radio still fails quickly.
- **Pairing-failure classification.** A wrong or cancelled PIN surfaces as a `CBATTError` (insufficient authentication/encryption/authorization) or a `CBError` (`encryptionTimedOut`, `peerRemovedPairingInformation`). `BLEConnection.isPairingFailure(_:)` distinguishes these bond failures from benign per-characteristic errors (e.g. "notify not supported") so only real failures fail the connect. Cancelling the sheet often arrives as a plain peripheral disconnect, so `disconnect` also resumes any suspended connect continuation to fail Step 1 fast instead of waiting out the full window.
- **Paired-hint lifecycle.** The set of bonded peripheral UUIDs is persisted in `UserDefaults.pairedPeripheralIds`. A confirmed subscription calls `rememberPairedPeripheral`; a bond failure or a teardown while still awaiting confirmation calls `forgetPairedPeripheral`, so a bond the user removes in iOS Settings self-heals back to the long pairing window on the next attempt. The legacy `preferredPeripheralId` is migrated into this list exactly once (guarded by `migratedPreferredPeripheralPairing`) so upgrading users skip the long window on their first reconnect without permanently pinning the fast timeout.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if the bundled HTML was regenerated and included in the recent commits.

if git log -15 --name-only | grep -qi "Meshtastic/Resources/docs/.*\.html"; then
  echo "HTML files are included in the PR."
else
  echo "Warning: No generated HTML docs found in the recent commits."
fi

Repository: meshtastic/Meshtastic-Apple

Length of output: 226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== status =="
git status --short

echo
echo "== changed files =="
git diff --name-only HEAD~1..HEAD 2>/dev/null || true

echo
echo "== docs html changes in recent diff =="
git diff --name-only HEAD~1..HEAD 2>/dev/null | grep -E '^Meshtastic/Resources/docs/.*\.html$' || true

echo
echo "== transport markdown change =="
git diff --name-only HEAD~1..HEAD 2>/dev/null | grep -E '^docs/developer/transport\.md$' || true

Repository: meshtastic/Meshtastic-Apple

Length of output: 481


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'HEAD: '; git rev-parse --short HEAD
printf 'Branch: '; git branch --show-current || true

echo "Changed files against merge-base with origin/main (if available):"
base=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null || true)
if [ -n "${base:-}" ]; then
  git diff --name-only "$base"..HEAD | sed 's#^`#-` #'
else
  echo "(no base found)"
fi

echo
echo "Generated docs output present in diff?"
if [ -n "${base:-}" ]; then
  git diff --name-only "$base"..HEAD | grep -E '^Meshtastic/Resources/docs/.*\.html$' || echo "(none)"
fi

echo
echo "Transport markdown present in diff?"
if [ -n "${base:-}" ]; then
  git diff --name-only "$base"..HEAD | grep -E '^docs/developer/transport\.md$' || echo "(none)"
fi

Repository: meshtastic/Meshtastic-Apple

Length of output: 630


Regenerate the bundled HTML docs. docs/developer/transport.md changed, but Meshtastic/Resources/docs wasn’t updated. Run bash scripts/build-docs.sh --output Meshtastic/Resources/docs and include the generated HTML in this PR.

🤖 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 `@docs/developer/transport.md` around lines 82 - 90, Regenerate the bundled
HTML documentation from the updated transport guide by running
scripts/build-docs.sh with Meshtastic/Resources/docs as the output directory,
then include all resulting generated HTML changes in the PR.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-docs-check Use this label to skip the automatic docs audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞 [Bug]: not able to enter bluetooth pin (dialogue automatically closes)

1 participant