Skip to content

Support multiple characteristics with the same UUID on Apple#1218

Draft
twyatt wants to merge 4 commits into
mainfrom
cursor/apple-same-uuid-characteristics-f76b
Draft

Support multiple characteristics with the same UUID on Apple#1218
twyatt wants to merge 4 commits into
mainfrom
cursor/apple-same-uuid-characteristics-f76b

Conversation

@twyatt

@twyatt twyatt commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

Fixes #1016 (bug originally reported via #875).

Core Bluetooth — unlike Android — does not provide instance IDs for services/characteristics, so previously the Apple implementation fell back to object reference (===) equality for DiscoveredService/DiscoveredCharacteristic/DiscoveredDescriptor. That prevented reliably distinguishing (and associating I/O events with) multiple characteristics that share the same UUID, causing operations (e.g. reads) to stall indefinitely (#875).

Solution

Apple platforms only (Android already relies on system-provided instance IDs):

  • Artificial instance IDs generated during service discovery: when the discovered services are cached (Connection.discoverServices), each service is assigned an instanceId equal to its position in the cached service array, and each characteristic an instanceId equal to its position within its parent service's characteristic array.
  • Equality mirrors Android: PlatformDiscoveredService/PlatformDiscoveredCharacteristic/PlatformDiscoveredDescriptor equals/hashCode now compare UUIDs + instance IDs (matching the Android implementation) instead of object references. Two different characteristics with the same UUID now compare as not equal, while re-wraps of the same underlying characteristic (e.g. across service re-discovery) compare as equal.
  • Characteristic change/error events are tracked by reference against the cached service array: PeripheralDelegate now resolves the CBCharacteristic from didUpdateValueForCharacteristic (by object reference) to the corresponding PlatformDiscoveredCharacteristic from the most recent service discovery, so isAssociatedWith performs an exact (instance-aware) match. If the characteristic cannot be found in the cached services (e.g. event received while a service re-discovery is in-flight), it falls back to UUID-only association (equivalent to the forceCharacteristicEqualityByUuid workaround behavior) with a warning logged.
  • ObservationEvent.CharacteristicChange/Error had their characteristic type widened from PlatformDiscoveredCharacteristic to Characteristic (internal API) to allow the UUID-only fallback; other platforms are unaffected.

This also removes the force-unwraps of the weakly-referenced CBCharacteristic.service/CBDescriptor.characteristic back-pointers in Profile.kt (parent wrappers are passed down during discovery instead).

Notes

  • forceCharacteristicEqualityByUuid (@ObsoleteKableApi, annotated "will be removed after Support multiple characteristics with the same UUID #1016 is fixed") is intentionally left in place; it can be removed in a follow-up PR once this fix is validated.
  • Outgoing operations (write/read/notify) already resolve DiscoveredCharacteristic arguments by reference to the underlying CBCharacteristic (obtain in Profile.kt), so no change was needed there; this PR fixes the event-association side (which previously relied on wrapper equality).

Testing

  • New ProfileTests cover instance ID assignment, equality of same-UUID characteristics (within a service and across same-UUID services), stability across re-wraps, and findCharacteristic reference lookup (built on CBMutableService/CBMutableCharacteristic). Placed in macosTest (rather than appleTest) because the CBMutable* initializers are unavailable on watchOS.
  • New commonTest ObservationEventTests cover UUID-based event association (isAssociatedWith).
  • CI is green: assemble + check on macOS (including macosArm64Test and iosSimulatorArm64Test) pass, along with all other platform jobs. Also verified locally (Linux host): compileKotlinJvm, compileKotlinJs, compileKotlinWasmJs, compileAndroidMain, jvmTest, testAndroidHostTest, lintKotlin, jvmApiCheck, and klibApiCheck.
  • Not tested against physical peripherals; would benefit from validation against the Fix iOS Bluetooth read operation suspension issue #875 scenario (two services with the same UUID, each containing same-UUID characteristics).
Open in Web Open in Cursor 

cursoragent and others added 4 commits July 16, 2026 09:22
Core Bluetooth (unlike Android) does not provide instance ids for
services/characteristics, so artificial instance ids are generated
during service discovery (as positions within the cached service
array). Discovered service/characteristic/descriptor equality is now
based on UUIDs + instance ids (mirroring the Android implementation),
rather than object reference.

Characteristic change/error events are resolved (by object reference)
against the cached discovered services, falling back to UUID-only
association when the characteristic cannot be found.

Fixes #1016

Co-authored-by: Travis Wyatt <travis.i.wyatt@gmail.com>
Co-authored-by: Travis Wyatt <travis.i.wyatt@gmail.com>
Co-authored-by: Travis Wyatt <travis.i.wyatt@gmail.com>
CBMutableService/CBMutableCharacteristic initializers are unavailable
(prohibited) on watchOS, so the tests cannot compile in appleTest.

Co-authored-by: Travis Wyatt <travis.i.wyatt@gmail.com>
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.

Support multiple characteristics with the same UUID

2 participants