Repository navigation
Pairing 2/6: bind scans to the selected band #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1924,6 +1924,17 @@ class ScanAcceptPolicy { | |
| } | ||
| return null; | ||
| } | ||
|
|
||
| /// Whether a scan result may be the band the user chose: the remote id | ||
| /// [wantRemoteId] when one was named, and no advertised generation other | ||
| /// than [wantGeneration]. A name-only or 16-bit-only match names no | ||
| /// generation, so it may still be either and passes. | ||
| static bool fitsChoice(String remoteId, Iterable<String> serviceUuids, | ||
| {String? wantRemoteId, String? wantGeneration}) { | ||
| if (wantRemoteId != null && remoteId != wantRemoteId) return false; | ||
| final adv = accepts(serviceUuids); | ||
| return wantGeneration == null || adv == null || adv == wantGeneration; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Wrong-generation bands pass selection When a caller selects a generation but the advertisement has no recognized generation and GATT discovery identifies a different one, Reject advertisements with no recognized generation when one was selected, and verify the discovered generation matches before saving the pairing. Prompt for AI agentsIn `lib/ble/ble_state.dart` at line 1936:
**Wrong-generation bands pass selection**
When a caller selects a generation but the advertisement has no recognized generation and GATT discovery identifies a different one, `ScanAcceptPolicy.fitsChoice` treats a missing advertised generation as a match, and the pairing flow does not compare the generation found during discovery with the selected generation before saving; the user is paired with the wrong-generation band.
Reject advertisements with no recognized generation when one was selected, and verify the discovered generation matches before saving the pairing. |
||
| } | ||
| } | ||
|
|
||
| /// Which connect order a link gets, decided BEFORE discovery has run. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; | |
| import 'package:lucide_icons_flutter/lucide_icons.dart'; | ||
| import 'package:provider/provider.dart'; | ||
|
|
||
| import '../../ble/adapters/_registry.dart' show BandEntry; | ||
| import '../../ble/band_status_l10n.dart' show localizedBandStatus; | ||
| import '../../ble/ble_state.dart'; | ||
| import '../../l10n/app_localizations.dart'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High · Pairing ignores selected band When a user opens pairing from a device-picker entry on a platform that does not use AccessorySetupKit, Pass the selected Also at Prompt for AI agentsIn `lib/ui2/onboarding/pairing.dart` at line 21:
**Pairing ignores selected band**
When a user opens pairing from a device-picker entry on a platform that does not use AccessorySetupKit, `DevicePickerScreen._openEntry` pushes `PairingScreen` without the selected entry or remote ID, so `PairingScreen` passes null filters to `AppState.scanForBand` and accepts any nearby WHOOP; `pairWith` can persist a different band than the user selected.
Pass the selected `BandEntry` and remote ID from `DevicePickerScreen._openEntry` into `PairingScreen` so the scan filters by the user's choice.
Also at `lib/ui2/onboarding/pairing.dart:119`, `lib/ui2/onboarding/pairing.dart:142-143`.From ### 4.7 Capability wired into one call path but not all N |
||
|
|
@@ -110,7 +111,12 @@ class PairingScreen extends StatefulWidget { | |
| /// Walk past pairing and open the app anyway. Supplied by the router. | ||
| final VoidCallback? onSkip; | ||
|
|
||
| const PairingScreen({super.key, this.onSkip}); | ||
| /// Android and older iOS bind scans to this choice. On iOS 18+ the | ||
| /// AccessorySetupKit sheet chooses; no post-connect choice check runs. | ||
| final BandEntry? entry; | ||
| final String? remoteId; | ||
|
|
||
| const PairingScreen({super.key, this.onSkip, this.entry, this.remoteId}); | ||
|
|
||
| @override | ||
| State<PairingScreen> createState() => _PairingScreenState(); | ||
|
|
@@ -132,7 +138,8 @@ class _PairingScreenState extends State<PairingScreen> { | |
| if (await app.accessorySetupSupported()) { | ||
| await app.pairViaAccessorySetup(); | ||
| } else { | ||
| final found = await app.scanForBand(); | ||
| final found = await app.scanForBand( | ||
| remoteId: widget.remoteId, generation: widget.entry?.id); | ||
| if (found == null) { | ||
| if (mounted) setState(() => _phase = PairPhase.notFound); | ||
| return; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // WHY: the scan must never substitute a nearby band for the tapped one. | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:openstrap_edge/ble/ble_state.dart'; | ||
|
|
||
| void main() { | ||
| test('choice binds both id and generation, with independent wildcards', () { | ||
| expect(ScanAcceptPolicy.fitsChoice('picked', const ['fd4b0001'], wantRemoteId: 'picked', wantGeneration: 'gen5'), isTrue); | ||
| expect(ScanAcceptPolicy.fitsChoice('other', const ['fd4b0001'], wantRemoteId: 'picked', wantGeneration: 'gen5'), isFalse); | ||
| expect(ScanAcceptPolicy.fitsChoice('picked', const ['61080001'], wantRemoteId: 'picked', wantGeneration: 'gen5'), isFalse); | ||
| expect(ScanAcceptPolicy.fitsChoice('picked', const ['61080001'], wantRemoteId: 'picked'), isTrue); | ||
| expect(ScanAcceptPolicy.fitsChoice('other', const ['fd4b0001'], wantGeneration: 'gen5'), isTrue); | ||
| expect(ScanAcceptPolicy.fitsChoice('other', const ['61080001']), isTrue); | ||
| // A name-only match has no model evidence; discovery still has to identify it. | ||
| expect(ScanAcceptPolicy.fitsChoice('picked', const [], wantRemoteId: 'picked', wantGeneration: 'gen5'), isTrue); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Conflicting advertisements are accepted
When an advertisement contains service UUIDs for both supported generations and the selected generation's UUID is encountered first,
ScanAcceptPolicy.acceptsreturns only the first recognized generation, sofitsChoicemisses the conflicting UUID and accepts an ambiguous result; the scan can select a band advertising both generations.Inspect all recognized generation UUIDs and reject advertisements that identify more than one generation.
Also at
lib/ble/ble_state.dart:1936.Prompt for AI agents