Repository navigation
Normalize BleDevice.services to 128-bit UUIDs - #346
Merged
fotiDim merged 3 commits intoOct 16, 2026
Merged
Conversation
fotiDim
force-pushed
the
refactor/normalize-device-services
branch
2 times, most recently
from
October 15, 2026 21:44
3382722 to
4500535
Compare
**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.
fotiDim
force-pushed
the
refactor/normalize-device-services
branch
from
October 15, 2026 21:45
4500535 to
2412be7
Compare
…ease 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.
rohitsangwan01
approved these changes
Oct 16, 2026
fotiDim
added a commit
that referenced
this pull request
Oct 16, 2026
#345) * Add ScanFilter.withServiceData to match advertising Service Data (#57) `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. * Refactor the scan filter path and pin its semantics with a shared fixture 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. * Address scan-filter review feedback - 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 (#346) * 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Possibly breaking change — stacked on #345. Base is
feat/scan-filter-service-dataso the diff below is only this change; retarget tomainonce #345 lands (or drop this PR if you want the normalization later).Problem
BleDevice.serviceshad a different shape depending on the platform:180D(CoreBluetooth preserves the advertised width)0000180d-0000-1000-8000-00805f9b34fbSo a value read on iOS/macOS did not compare equal to the same service read on Android, and did not key the same map entry. This is the same class of inconsistency that case-insensitive device IDs fixed in 3.0.0, and it is what made Apple's service filter compare a short advertised UUID against the full form the Dart layer sends (worked around locally in #345, fixed at the source here).
BleDevice.serviceDatakeys already had a canonical form;servicesdid not.Change
BleDevicenormalisesservicesin its constructor, exactly like theserviceDatakeys:180D),0x-prefixed and mixed-case forms remain accepted as input, everywhere.UniversalBleScanResult.toBleDevice) no longer parses services itself either, so a malformed advertisement value can no longer throwFormatExceptionand fail a scan on any platform (previously both that conversion andservicesMatchcould throw).toValidUUIDList()calls.Migration
Code that stored or compared a short form read from Apple must switch to the 128-bit form, or parse with
BleUuidParserfirst. Nothing else changes:serviceskeeps its type and order (the list is still mutable).Tests
test/ble_device_services_test.dart— 16 cases: short/0x/32-bit/mixed-case expansion, already-normalised values, malformed entries kept verbatim, empty list,copyWith,serviceDatakey symmetry, cross-form filter matching (short advertised vs full filter and vice versa), and the platform-channel conversion (short normalised, malformed kept verbatim without throwing, absent becomes empty).flutter test(282),flutter test --platform chrome(239),flutter analyzeclean.Docs
AGENTS.mdgains an "Advertisement Field Normalization" section next to the device ID rules, since this is the second cross-platform canonical form.CHANGELOG.mdentry under the unreleased3.1.0section, flagged as (possibly) breaking.README.mdnotes thatservicesis emitted as 128-bit lower-case UUIDs.Versioning
Kept in the unreleased
3.1.0line rather than a separate4.0.0. The change alters emitted values, so it is flagged as breaking in the changelog even though the version stays a minor bump.