Repository navigation
Add ScanFilter.withServiceData to match advertising Service Data (#57) - #345
Conversation
`ScanFilter.withServices` only matches the advertised service UUID list, so a device that carries a service UUID only inside its advertising Service Data — beacons like Eddystone (0xFEAA) or the ATC_MiThermometer (0x181A) — was never discovered, even though its bytes were already reported in `BleDevice.serviceData` (#57). Add an opt-in `ScanFilter.withServiceData` that matches the service UUIDs found in the Service Data fields (0x16/0x20/0x21). UUIDs are accepted in short, 0x-prefixed or full 128-bit form, in any case, and the new condition stays in OR relation with the others. Defaults to empty, so existing scans (and the native filter paths) behave exactly as before. Android and Apple cannot express this as a platform filter — a controller-level scan filter or `scanForPeripherals(withServices:)` drops the packet before the plugin is notified — so opting in switches those platforms to the software filter path, as already done for name prefixes. Web throws `notSupported`, since the browser chooser cannot filter on Service Data. Linux matches in Dart. Also: - Linux: re-evaluate the filter against the device's current properties instead of the snapshot taken when BlueZ added it, and keep listening to devices that did not match at that point. A device that only advertises its Service Data in a later advertisement was previously dropped permanently. - Apple: compare service UUIDs in 128-bit form on the custom filter path, since `CBUUID` equality does not match a 16-bit advertised UUID against the full form the API sends. - Append `withServiceData` last on the pigeon model to keep positional encoding stable, and document `BleDevice.serviceData` in the README.
…ture
Follow-up to the Service Data filter work, addressing the drift and duplication
the four filter implementations had accumulated.
One entry point per platform
- Android and Apple kept the filter plus two normalised UUID lists as parallel
state that the plugin had to populate and clear by hand; both are now derived
by a single `configure(...)` call, so a new filter dimension cannot be left
half-wired.
- Windows set `setScanFilter` mutated accumulating globals and relied on the
caller to reset first. It now assigns a single value.
Strict predicates, one combination point
- Each per-dimension check now answers only "does this dimension match?", so the
OR chain no longer repeats `has…Filter &&` four times per language, and the
redundant "empty filter list returns true" branches are gone. The allowance
for a filter with no conditions lives in exactly one place per implementation.
- The Dart exclusion filter relies on the opposite (AND) semantics, so it now
guards its manufacturer dimension explicitly, like its service dimension
already did.
Shared conformance fixture
- `test/fixtures/scan_filter_cases.tsv` is the single description of the
semantics, consumed by the Dart, Kotlin and C++ implementations
(20 cases: each dimension, the OR relation, empty filters, and that Service
Data and advertised services stay distinct).
- To make the C++ side testable, the matching rules moved out of the
flutter/winrt-typed file into `windows/src/scan_filter_matcher.{h,cpp}`, with
`universal_ble_filter_util.cpp` reduced to a type adapter. The new
`windows/test/scan_filter_matcher_test.cpp` runs in the same ctest job as the
existing tests and needs neither Flutter nor WinRT, so it also runs on the
Linux CI runner.
- Swift has no test target (the plugin sources need the Flutter module), so it
is the one implementation not driven by the fixture; its predicate shape was
aligned by hand.
Smaller cleanups
- Linux: filter and emit through one `_reportIfAccepted` helper instead of
building the device twice at two different points.
- Web: validation (`_validateScanFilter`) split from translation, and the
exclusion-filter mapping extracted, in what had grown into a 100-line function.
- Tests: a fresh filter util per test instead of one mutable instance shared by
the whole file.
|
Pushed a second commit to this PR: a refactor of the filter path that came out of reviewing the first one. Reviewing the two commits separately is easier — the feature is unchanged by it ( One entry point per platform. Android and Apple kept the filter plus two normalised UUID lists as parallel state the plugin had to populate and clear by hand; an Strict predicates. Each per-dimension check now answers only "does this dimension match?", so the four Shared conformance fixture. To make C++ testable at all, the matching rules moved out of the flutter/winrt-typed file into Not covered: Swift has no test target (the plugin sources need the Flutter module), so Apple is the one implementation not driven by the fixture; its predicate shape was aligned by hand. Same caveat on #346. Also: a fresh filter util per test in Verified: |
- Fix vacuous matchedScanFilter assertion in manufacturer_data_filter_test
(assign scanFilter so the empty-filter path is actually exercised)
- Drop the unused ToLower helper from scan_filter_matcher.cpp and the
unused ToBytes helper plus <unordered_set> include from
universal_ble_filter_util.cpp
- Remove the no-op `.also { it.first() }` in the Kotlin conformance test
- List serviceData among the filter conditions in the Scan Filter README intro
- Revert the incidental CocoaPods version bump in example/macos/Podfile.lock
* Normalize BleDevice.services to 128-bit UUIDs on every platform **Breaking:** `BleDevice.services` had a different shape per platform. Apple reported 16-bit advertised service UUIDs in their short form (`180D`) while Android, Windows, Linux and Web reported the full 128-bit form, so a value read on one platform did not compare equal (or key the same map entry) on another — the same inconsistency the case-insensitive device IDs fixed in 3.0.0, and what made the Apple service filter compare short against full. `BleDevice` now normalises `services` in its constructor, exactly like the `serviceData` keys, so every emitted value is a full 128-bit lower-case UUID. Entries that are not valid UUIDs are kept verbatim rather than dropped, and the matching helpers skip the unparseable ones instead of throwing, so malformed advertisement data can no longer fail a scan. Callers that stored or compared a short form from Apple must use the 128-bit form (short and `0x`-prefixed forms remain accepted as *input* everywhere). The scan filter paths lose their now-redundant per-site `toValidUUIDList()` calls on the device side. Also documents the invariant in AGENTS.md, next to the device ID rules, since this is the second field with a cross-platform canonical form. * Address review: harden malformed-service handling and align 3.1.0 release docs - Convert platform-channel scan results without a throwing UUID parse, so malformed advertised services are kept verbatim by BleDevice instead of failing the scan on Android/Apple/Windows. - Keep the normalised services list growable, preserving the previous ability to mutate it. - Document the 128-bit canonical form in the README. - Add regression tests for the platform-channel conversion path. * Document BleDevice.services normalisation in the 2.x -> 3.x migration skill
Closes #57 (the remaining half of it).
Problem
#57 asks for the advertising Service Data of a specific UUID (0x181A) and reports that
ScanFilter(withServices: ["181A"])discovers nothing, even though nRF Connect andbleaksee the UUID.The data half already landed in #207 —
BleDevice.serviceDatareports the payloads on every platform. What was still missing is the filter half: every filter path matched only the advertised service UUID list (0x02/0x03,0x04/0x05,0x06/0x07), never the Service Data fields (0x16/0x20/0x21). A beacon that carries its UUID only in Service Data — Eddystone (0xFEAA), the ATC_MiThermometer in the issue (0x181A), BLE Mesh provisioning (0x1827) — was therefore invisible to any service filter.Change
Opt-in
ScanFilter.withServiceData, matching the service UUIDs found in the advertising Service Data:181A),0x-prefixed or full 128-bit form, any case (normalised by the existingBleUuidParser→toValidUUIDList()).ORrelation withwithServices/withManufacturerData/withNamePrefix; empty list = ignored.BleDevice.serviceData.Platform behaviour:
usesCustomFilters)nilservices + software filternotSupported— the browser chooser cannot filter on Service DataWhy the software path: a controller-level
ScanFilter(Android) orscanForPeripherals(withServices:)(Apple) discards such a device before the plugin is notified, so the filter cannot be applied later. That is exactly why this is opt-in rather than folded intowithServices.Also fixed along the way
services: [], thenUnhandledDevicePropertyChanged … ServiceData). Now re-evaluated on current properties, and devices stay subscribed so they can start matching later.Set<CBUUID>equality did not match a 16-bit advertised UUID against the full 128-bit form the API sends —withServicescould not filter 16-bit services when the custom filter path was active.Compatibility
No breaking change to the public API: the new field is optional with an empty default,
withServicesandBleDevice.servicesare untouched, and no exported interface gained a member (the Web guard lives in the web implementation instead of adding a capability flag to the exportedUniversalBlePlatform).Pigeon encodes positionally, so
withServiceDatais appended last onUniversalScanFilter. Native rebuild after upgrading, as usual for a schema change.Tests
test/scan_filter_test.dart— service-data matching: short/0x/full/case variants, non-matching and empty data, OR relation, empty filter, malformed device keys, and that unsetwithServiceDatakeeps the old behaviour.test/scan_filter_channel_test.dart— the filter reaches the platform as a canonical 128-bit lowercase UUID.android/src/test/.../UniversalBleFilterUtilTest.kt—usesCustomFilters(), beacon-without-advertised-services regression, and that service-data matching does not leak from/into the advertised service list.android/src/test/.../UniversalBleHelperTest.kt—ScanResult.serviceDataUuids/serviceDataparsing.Verified locally:
flutter analyzeclean,flutter test(249),flutter test --platform chromefor the touched tests, Android:universal_ble:testDebugUnitTest(50),flutter build macos --debug(compiles the Swift changes). Windows C++ compiles in thewindowsCI job.Versioning
Bumped
pubspec.yamlto 3.1.0 with the CHANGELOG entry (additive feature → minor). Happy to drop the bump and let you cut the release instead.Manual verification still worth doing on real hardware: an advertiser that carries 0x181A only in Service Data, on Android (
AndroidOptions(legacy: true)for legacy BLE 4.x advertisers) and Apple. I could not run that here.