Skip to content

fix: scope BLE service-completeness check to services we actually implement - #116

Merged
gdoumen merged 1 commit into
mainfrom
fix/neo-bike-plus-expected-services-filter
Aug 10, 2026
Merged

fix: scope BLE service-completeness check to services we actually implement#116
gdoumen merged 1 commit into
mainfrom
fix/neo-bike-plus-expected-services-filter

Conversation

@gdoumen

@gdoumen gdoumen commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Follow-up to fix: BLE service-completeness check false-fails on custom vendor UUID families #115, which made the announced-vs-discovered service comparison tolerant of same-family custom UUID drift (TICKR FIT, HRM Pro+). A third, structurally different production log surfaced: Tacx NEO Bike Plus announces [1818, 1826, 1000]. 1000 is a bare, standalone entry that doesn't correspond to any real GATT service number in a range we recognize — most likely a firmware/advertising-packet-size shorthand or scan-library artifact for the FE031000-... custom service also visible in discovery (the digits line up exactly), but the device is a smart trainer, not something with a custom Tacx/Wahoo protocol we implement for it — nothing we implement ever needed that UUID in the first place.
  • Confirmed via the user this was not a transient registration race: the device never connected on BLE in production, and the user eventually switched to ANT+.
  • checkAnnouncedServices() (src/ble/base/peripheral.ts) now filters the announced list down to services BleInterface.getSupportedServices() (new method, src/ble/base/interface.ts) actually cares about — the same list already announced upfront to bindings like WebBluetooth's optionalServices, sourced from every registered sensor implementation's declared services — before checking completeness. An announced UUID no sensor of ours ever uses is now simply irrelevant, not a completeness gap.
  • Combined with fix: BLE service-completeness check false-fails on custom vendor UUID families #115's isSameServiceFamily() leniency, this still requires an exact (or same-family) match for any service we actually implement against — the original MRK-R15-D829 rower FTMS registration race is unaffected, since FTMS is squarely in the expected-services set.
  • DirectConnectInterface (WiFi/mDNS, src/direct-connect/base/interface.ts) gets a trivial getSupportedServices() stub to satisfy the now-extended shared IBleInterface contract — it has no BLE-style completeness check of its own.

What changed

  • src/ble/types.ts: IBleInterface.getSupportedServices(): string[] added to the contract.
  • src/ble/base/interface.ts: BleInterface.getSupportedServices() returns the cached expectedServices field (the same value last announced to bindings).
  • src/ble/base/peripheral.ts: checkAnnouncedServices() filters announced to entries matching (via isSameServiceFamily) something in getSupportedServices() before checking completeness.
  • src/direct-connect/base/interface.ts: trivial getSupportedServices() stub (returns []).
  • Tests: interface.unit.test.ts (new case for the getter), peripheral.unit.test.ts (NEO Bike Plus regression repro using the real production log UUIDs, plus an "expected custom UUID with a genuinely different base still fails" case isolating that scenario from the new filtering).

Test plan

  • npm test — 1165 passed, 35 skipped (pre-existing skips, no change), 0 failed
  • npx tsc -p tsconfig.esm.json --noEmit — clean
  • Real-device pairing test: a device that only advertises services none of our sensors implement (in addition to its real ones) connects successfully
  • Real-device regression check: FTMS/rower power-on-race retry still works as before

…lement

The prior commit made the announced-vs-discovered comparison tolerant of
same-family custom UUID drift, but a device can also advertise a service
UUID that isn't part of any implemented sensor's family at all - e.g. Tacx
NEO Bike Plus announces a bare "1000" fragment (not a real SIG service
number, most likely a firmware/advertising-packet-size shorthand or
scan-library artifact for its FE03xxxx custom service) alongside its two
real, correctly-discovered standard services. That device never connected
in production - confirmed not a transient registration race.

checkAnnouncedServices() now filters the announced list down to services
BleInterface.getSupportedServices() actually cares about (the same list
already announced upfront to bindings like WebBluetooth's optionalServices,
sourced from every registered sensor implementation's declared services)
before checking completeness - an announced UUID no sensor of ours ever
uses is simply irrelevant, not a completeness gap. Combined with the
existing isSameServiceFamily() leniency, this still requires an exact (or
same-family) match for any service we actually implement against - e.g. the
MRK-R15-D829 rower's genuine FTMS registration race is unaffected, since
FTMS is squarely in the expected-services set.

DirectConnectInterface (WiFi/mDNS) picks up a trivial getSupportedServices()
stub to satisfy the now-extended shared IBleInterface contract - it has no
BLE-style completeness check of its own.
@sonarqubecloud

Copy link
Copy Markdown

@gdoumen
gdoumen marked this pull request as ready for review August 10, 2026 22:04
@gdoumen
gdoumen merged commit 0b44909 into main Aug 10, 2026
4 checks passed
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.

1 participant