Skip to content

feat: follow the controller's Bluetooth-then-Wi-Fi sequence - #32

Merged
lemarier merged 6 commits into
mainfrom
lemarier/onboarding-follow-the-controllers-bluetooth-then
Sep 25, 2026
Merged

lemarier merged 6 commits into
mainfrom
lemarier/onboarding-follow-the-controllers-bluetooth-then

Conversation

@lemarier

@lemarier lemarier commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

The app did not follow the order the controller firmware supports since origin89hq/firmware#161. It connected over Bluetooth before the pairing window was open, but a unit whose station has joined advertises only while the window is open, so re-pairing such a unit failed before the window instructions appeared. After Pair, a unit that already holds a network was sent to the network form even though its station starts on its own. A Bluetooth drop while the station started ended with "connection lost" instead of looking for the unit on Wi-Fi.

Now:

  • Pairing shows the window instructions first and opens Bluetooth only after the person confirms the window is open. The instructions mention the 120 s power-on window on a unit no phone has paired with.
  • After Pair, a unit that holds a network and passphrase goes straight to watching that join and then moves to Wi-Fi, with nothing written. "Choose another network" stays available.
  • When Bluetooth drops during the join watch, the flow looks for the unit on Wi-Fi (DNS-SD, then the kept address) every 2 s for up to 30 s and continues there. If that fails, the join shows as unknown with the Wi-Fi reason and offers to check again over Wi-Fi or Bluetooth.
  • When a network write loses its answer with the Bluetooth link, the flow reads the section over Wi-Fi. If it holds the written SSID at a newer version, the write counts; otherwise the write fails as before.
  • A suspend or reset during the Wi-Fi search ends it with nothing left open, and a search that ends without the unit reports why.
  • Bench: pair follows the join of a held network (--watch SECONDS) and, with --ssid, writes a network on the pairing connection; clear writes no network.

For #28. Its acceptance names an iPhone run, which is still to do; both cases passed on board A from the macOS bench (below).

Validation

just check passes: format check, Rust tests, 155 SetupKit tests, the unsigned simulator build and the bench build. New tests cover the window-first open and its failures, a held network after Pair (also when the first read comes after a reconnect) and the cases that still go to the form, the Wi-Fi search after a drop (found, not found within the limit, no candidate, suspended, reset before it starts), and a lost write answer (confirmed, not held, unit not found, refusal not searched).

Board A, controller from firmware main (2ecada9), comms 0.0.0+g56488aca (#161), macOS bench over the Mac's Bluetooth:

  • Re-pair a unit holding wasabi: bench pair --window-open paired in 2.3 s, went to written(1) with nothing written, lost Bluetooth 4.2 s after Pair as the station started, found the unit over DNS-SD 11.5 s later and read the join over the WebSocket at 192.168.0.180.
  • Onboard a unit with no network (cleared with bench clear, epoch raised so the power-on window opens): bench pair --window-open --ssid wasabi paired, wrote version 3 with its answer 110 ms later, lost Bluetooth 4 s after the write and continued over Wi-Fi 10.6 s after the drop, joined.

The radio reported on the section version in both runs, so the held-network watch reads the right version. The lost-write path did not trigger: the SetConfig answer arrived before the station started. Not run: the same two cases on an iPhone. Found on the way: board A advertises nothing over BLE with no network and the window closed (origin89hq/firmware#166).

Copilot AI lite review requested due to automatic review settings September 25, 2026 11:56
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: b940d119-28d7-4527-8eeb-12a5cbe07202

📥 Commits

Reviewing files that changed from the base of the PR and between adf14c5 and 3972ce7.

📒 Files selected for processing (2)
  • apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/KeptEnrolmentTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The setup flow opens Bluetooth after pairing-window confirmation. After pairing, it can watch a controller’s held network, recover a Wi-Fi connection after Bluetooth disconnects, and confirm eligible lost network-write responses by reading back settings. The app and SetupBench display updated network, pairing, and connection status.

Priority: ➖ Normal

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

Merge Risk: 🔵 Low · up to 3972c

A narrow Wi-Fi status message can remain stale in some direct-search flows; the main pairing and recovery races are resolved, so merge is reasonable with bounded follow-up.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 3972c

The new recovery path can treat a network write as successful after reading a newer configuration with the requested network name. That does not necessarily prove this phone’s write took effect if another authorized writer changed the controller meanwhile. The potential impact is bounded to the controller being configured, and the flow retains identity and retry controls.

Retained concerns

  • Medium · security · inferred: Lost-answer recovery may attribute another authorized writer’s newer, same-SSID configuration to this setup flow’s write and advance to the written state.
Security review details

Security Blast Radius

  • inferred — The write-attribution scenario requires a competing writer with access to the same controller; its supported exposure is that controller’s network configuration, not a demonstrated cross-controller or service-wide path.

Security Findings and Attack Paths

  • inferred — If this flow loses its write answer and another authorized writer advances the section to the same SSID, the recovery predicate can mark this flow’s write successful without establishing which settings or passphrase took effect.

Trust Boundaries and Controls

  • observed — Recovery carries the selected device ID into a resumed client and rejects failed discovery or enrolment before reading network settings. Mismatched kept addresses are cleared. Enforcement inside the underlying controller session was not directly verified.

Resilience and Maintainability Implications

  • observed — Recovery is limited to dropped or timed-out SSID writes; an unconfirmed read takes the failure path, while search and suspension use cancellation and generation guards.

Hardening Proposals

  • proposed — Confirm an uncertain write with a controller-provided write identifier or stronger configuration correlation, and exercise a competing same-SSID write before treating version advancement as attribution.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 80 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: enforcing the controller's Bluetooth-then-Wi-Fi sequence.
Description check ✅ Passed The description explains the problem, resulting behavior, affected bench flow, validation results, and remaining uncertainty. It also identifies the outstanding iPhone acceptance run and the untrigger…
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.
  • Fix all pre-merge checks with AI

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

@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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
apps/ios/SetupBench/Sources/SetupBench/main.swift-379-380 (1)

379-380: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

watchJoin can stop on a brief .connectionLost before the Wi-Fi search starts.

In SetupFlow.runWatch, the catch path sets join = .connectionLost. It then awaits close() and sets join = .waiting only after that. It sets isSwitchingToWiFi = true before the close. This predicate returns true for .connectionLost without checking isSwitchingToWiFi. A 50 ms poll can land during the Bluetooth close. If it does, pair and write call flow.reset() and cancel the Wi-Fi search. The just bench pair path added by this PR then never shows the Wi-Fi handover.

Proposed fix
-      case .failed, .noAnswer, .connectionLost: return true
+      case .failed, .noAnswer: return true
+      // A search on Wi-Fi follows a lost Bluetooth link.
+      case .connectionLost: return !flow.isSwitchingToWiFi
apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift-753-756 (1)

753-756: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Track a completed Wi-Fi search separately from wifiUnavailable.

When reachWiFi reaches its 30-second deadline without a DNS-SD or stored-address candidate, it returns nil and continueOverWiFi sets join to .connectionLost. It does not update wifiUnavailable, so SetupView shows only the Bluetooth message.

wifiUnavailable also persists across searches. A previous address attempt can therefore make a later search appear to have failed after 30 seconds. The direct watchJoinAgain → connect() path does not itself set .connectionLost when its single search fails, but the same stale-value problem occurs when continueOverWiFi completes a search.

Add search-specific state. Set it only when reachWiFi actually reaches its deadline, clear it when a new search starts or succeeds, and use it instead of wifiUnavailable != nil for the 30-second text. Keep wifiUnavailable only for the optional failure reason.

Suggested fix
+ public private(set) var wifiSearchFailed = false
  public private(set) var wifiUnavailable: WiFiUnavailable?

  private func reachWiFi(deviceID: String, _ operation: Int) async -> WiFiSession? {
    transportActive = true
    isSwitchingToWiFi = true
+   wifiSearchFailed = false
    defer { isSwitchingToWiFi = false }
    let deadline = clock.now + Self.wifiLimit
    ...
    if generation == operation {
      SetupLog.flow.notice("the controller was not found on Wi-Fi")
      transportActive = false
+     wifiSearchFailed = !Task.isCancelled && clock.now >= deadline
    }
    return nil
  }

Use flow.wifiSearchFailed for the connection-lost 30-second message, and show wifiUnavailable.reason only when that value is non-nil.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: b849ad7b-8eab-44a8-8895-affba16036f8

📥 Commits

Reviewing files that changed from the base of the PR and between b77d40f and 1cb4a5d.

📒 Files selected for processing (7)
  • apps/ios/Origin89/SetupView.swift
  • apps/ios/SetupBench/Sources/SetupBench/main.swift
  • apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/ControllerMismatchTests.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/SetupFlowTests.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/WiFiFlowTests.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/WiFiHandoverTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment thread apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift
@lemarier

Copy link
Copy Markdown
Contributor Author

Bench on board A, controller flashed from firmware main (2ecada9), comms 0.0.0+g56488aca (#161's image). The unit held the network wasabi and had no enrolled client. Its device secret was replaced first (new device id 343199df…) because no copy of the previous setup code was left.

just bench pair --window-open --watch 90 right after a reboot (power-on window) passed the re-pair case:

  • Window step first, then Bluetooth connect, Discover, Pair, Hello, read: 2.3 s.
  • The held network went straight to written(1); WifiStatus reported the radio on section version 1, so the held-network watch reads the right version.
  • The Bluetooth link dropped 4.2 s after Pair, when the station started. That answers the open question in Onboarding: follow the controller's Bluetooth-then-Wi-Fi sequence #28: an open link does not survive the station starting.
  • The Wi-Fi search found the unit over DNS-SD on its third browse, 11.5 s after the drop, and Discover and Hello succeeded at 192.168.0.180 over /km43. WifiStatus over Wi-Fi then reported the join.

Not yet run: the unit-without-network case (needs the network cleared and a write, where the write's answer may now be lost as the station starts) and an iPhone run of either case.

@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

Caution

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

⚠️ Outside diff range comments (1)

🟠 Major · Preserve held-network detection across a reconnect. · SetupFlow.swift:521

apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift:521
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve held-network detection across a reconnect.

If Bluetooth drops after Pair and Hello but before readNetwork() returns, fail leaves resume as .readNetwork. A successful retry calls proceed, which calls readNetwork() with afterPair == false. The controller can then report its held SSID, but the flow enters .editingNetwork instead of watching the join. Keep the post-pair detection pending until a network read succeeds, including after a reconnect.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: a65a70fe-0d10-4d2b-bc87-c43761078a18

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb4a5d and 75168b1.

📒 Files selected for processing (3)
  • apps/ios/SetupBench/Sources/SetupBench/main.swift
  • apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/WiFiHandoverTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment thread apps/ios/SetupBench/Sources/SetupBench/main.swift

@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.

Caution

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

⚠️ Outside diff range comments (1)

🟠 Major · Check the operation before entering .openWindow. · SetupFlow.swift:339-340

apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift:339-340
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check the operation before entering .openWindow.

If suspend() runs while connect() awaits clearExcludedPeers(), suspension advances generation and sets .suspended. When the await returns, this branch can replace .suspended with .openWindow. Check generation == operation after the awaited cleanup and before this branch.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: 0f542418-2ca2-4555-9987-e2d846e59264

📥 Commits

Reviewing files that changed from the base of the PR and between 75168b1 and 7ba6400.

📒 Files selected for processing (3)
  • apps/ios/SetupBench/Sources/SetupBench/main.swift
  • apps/ios/SetupKit/Sources/SetupKit/SetupFlow.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/WiFiFlowTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

@lemarier
lemarier merged commit c20167e into main Sep 25, 2026
2 checks passed
@lemarier
lemarier deleted the lemarier/onboarding-follow-the-controllers-bluetooth-then branch September 25, 2026 15:24
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.

2 participants