From 98c38fc2a6d893cffca460bc12306f19a482b67c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 05:35:16 +0000 Subject: [PATCH 1/2] Add functional requirements for service & characteristic discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PRD covered scanning/connections (FR-3, FR-8) and reading/writing data (FR-4, FR-7) but not the GATT layer in between. Add a new §10, "Service and Characteristic Discovery and Management", covering service and characteristic/descriptor discovery, the discovery lifecycle and readiness state machine, subscription (notify/indicate) management, GATT cache invalidation and re-discovery (Service Changed / didModifyServices), and stale-reference/reconnect handling. Scope is discovery and characteristic setup only; value read/write is deferred to FR-4 (command protocol) and FR-7 (chunking) via cross-refs. Requirements are grounded in researched iOS CoreBluetooth pitfalls (filtered discovery, ordering constraints, OS-level GATT caching, CCCD handling, unacknowledged notifications, never reusing CB objects across connections). Also tie the new section into existing requirements: FR-1.2 (reconnection triggers re-discovery), FR-4.1 (commands target discovered characteristics), and FR-9.2 (log discovery events). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Hg9mZheWnLsn8fSrB7JZ88 --- PRD.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/PRD.md b/PRD.md index ed926b2..313680a 100644 --- a/PRD.md +++ b/PRD.md @@ -26,7 +26,7 @@ ReliaBLE is a Swift package that provides a reliable, modern, yet easy to use in 1. Reliability of Communication: - FR-1.1: Implement error detection and correction mechanisms for each BLE transaction. -- FR-1.2: Ensure automatic reconnection attempts with exponential backoff for failed connections. +- FR-1.2: Ensure automatic reconnection attempts with exponential backoff for failed connections. On reconnection, services and characteristics must be re-discovered rather than reused from the prior connection (see FR-10.6). - FR-1.3: Provide status updates on connection stability and data transmission integrity. - FR-1.3.1: Provide status updates on connection stability (e.g. connected, disconnected, reconnecting). - FR-1.3.2: Provide status updates on data transmission integrity. @@ -49,7 +49,7 @@ ReliaBLE is a Swift package that provides a reliable, modern, yet easy to use in 4. Command Style Protocol for Interacting with Peripherals: -- FR-4.1: Define a flexible command protocol where the content and functionality of commands are provided by the integrating app. +- FR-4.1: Define a flexible command protocol where the content and functionality of commands are provided by the integrating app. Commands target services and characteristics made available through discovery (see FR-10), referenced by UUID (CBUUID). - FR-4.2: Support various command types: - FR-4.2.1: Notify-only (peripherals notify with updates). @@ -122,10 +122,54 @@ ReliaBLE is a Swift package that provides a reliable, modern, yet easy to use in - Connection/disconnection events - Command successes or failures - ✅ Scanning start/stop + - Service/characteristic discovery events (discovery start/completion/failure, GATT table changes, subscription state changes) - Security events (e.g., encryption initiation or failure) - Data chunking operations +10. Service and Characteristic Discovery and Management: + +The layer between establishing a connection (FR-3, FR-5) and reading or writing data +(FR-4, FR-7). After a peripheral connects, its GATT services and characteristics must be +discovered before any interaction, and that discovered view must be kept correct across +firmware changes and reconnections. This section covers discovery and characteristic setup +only; the movement of application data over characteristics is covered by the command +protocol (FR-4) and chunking (FR-7). + +- FR-10.1: Service Discovery: + - FR-10.1.1: Provide an API to discover services on a connected peripheral, requiring the integrating app to declare the specific service UUIDs of interest. Do not expose unfiltered "discover all services" as the default path — Apple explicitly discourages it because enumerating an entire remote GATT table negatively affects battery life and time-to-ready. + - FR-10.1.2: Support both automatic discovery on connection (using a caller-declared set of desired services/characteristics) and explicit, on-demand discovery invoked by the integrating app. + - FR-10.1.3: Expose discovered services to the integrating app as Sendable value snapshots keyed by peripheral identifier and service UUID (CBUUID), mirroring the existing peripheral-snapshot pattern. Never surface live CoreBluetooth objects. + +- FR-10.2: Characteristic and Descriptor Discovery: + - FR-10.2.1: Provide an API to discover characteristics for a discovered service, allowing the integrating app to declare the characteristic UUIDs of interest. Enforce CoreBluetooth's ordering constraint: services are discovered before characteristics, and characteristics before descriptors. + - FR-10.2.2: Track per-service discovery completion, correctly handling the separate completion callback fired for each service, and report a peripheral as "ready" only once all requested discoveries have completed. + - FR-10.2.3: Optionally discover descriptors for a characteristic on demand. Descriptor discovery is not required to enable notifications (see FR-10.4.1). + - FR-10.2.4: Expose discovered characteristics as Sendable value snapshots including metadata: UUID (CBUUID), properties (read, write, write-without-response, notify, indicate), and, when discovered, descriptors. + +- FR-10.3: Discovery Lifecycle and Readiness: + - FR-10.3.1: Model discovery as an explicit, per-peripheral state machine driven entirely by delegate callbacks. Never assume synchronous availability of services or characteristics after initiating discovery. + - FR-10.3.2: Provide callbacks/streams (mirroring the asynchronous event pattern of FR-2.3) reporting discovery progress, completion, and errors. + - FR-10.3.3: Enforce a configurable discovery timeout so that a stalled or interrupted discovery surfaces as an error rather than an indefinite wait. + - FR-10.3.4: Handle interrupted or partial discovery: if a peripheral disconnects mid-discovery, treat it as a hard reset of the discovery state — discard partial results and any pending discovery bookkeeping — and re-run discovery on the next connection (see FR-10.6.1). + - FR-10.3.5: Handle non-nil errors returned in each discovery callback, mapping CoreBluetooth errors into the library's error type. Distinguish common failure causes where feasible (e.g., characteristic-not-found from an overly narrow filter or a stale cache, versus disconnection during discovery). + +- FR-10.4: Characteristic Subscription Management (Notify/Indicate): + - FR-10.4.1: Provide an API to enable and disable notifications or indications on a characteristic using the platform's subscribe mechanism. Never write the Client Characteristic Configuration Descriptor (CCCD, 0x2902) directly — CoreBluetooth manages the CCCD write internally and forbids writing it directly. + - FR-10.4.2: Confirm the subscription state-change callback before treating a subscription as active, and sequence subscription-enable before issuing any command that causes the peripheral to emit data, so that initial notifications are not lost. Delivery of the notified values is covered by FR-4.2.1 (notify-only) and FR-2.3.2 (data-received streams). + - FR-10.4.3: Expose subscription state and its changes to the integrating app. Do not rely solely on the platform's `isNotifying` flag, which can lag the actual state. + - FR-10.4.4: Account for the fact that notifications are unacknowledged at the GATT layer and may be lost or reordered above the link layer. Application-layer framing/sequencing for multi-packet notification streams is handled per FR-7 (chunking and reassembly). + +- FR-10.5: GATT Cache Invalidation and Re-discovery: + - FR-10.5.1: Detect and surface peripheral GATT-table changes reported by the platform (the "did modify services" callback) and automatically re-run discovery for the affected services. + - FR-10.5.2: Treat reconnection following a peripheral firmware update or DFU as requiring full re-discovery; do not trust a previously cached view of services and characteristics. + - FR-10.5.3: Document the iOS OS-level GATT caching limitation: there is no public API to clear the cache. Reliable cross-connection refresh requires the peripheral to implement the Service Changed characteristic (0x2A05) within the Generic Attribute Service (0x1801) and to bond; otherwise the only fallback is user-driven (toggle Bluetooth, or "Forget This Device"). This is a peripheral-firmware responsibility that the library cannot fully control, and the limitation must be communicated to integrating apps. + +- FR-10.6: Reconnection and Stale-Reference Handling: + - FR-10.6.1: Re-discover services and characteristics on every new connection. Never reuse a service or characteristic reference obtained from a prior connection (see FR-1.2 for reconnection). + - FR-10.6.2: Internally discard all live CoreBluetooth service/characteristic references on disconnect and reset the discovery state. The app-facing catalog must be keyed by UUID (CBUUID), not by held CoreBluetooth objects. This is consistent with the library's existing invalidation/refresh behavior on Bluetooth power cycling. + + ### Non-Functional Requirements 1. Modern Swift 6 Architecture: From d6dac2eaa4a9aef1ac627c22e7a8939793e7fa05 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 23:24:43 +0000 Subject: [PATCH 2/2] Strengthen FR-10 with discovery readiness-gate invariant Incorporate PR review feedback aligning the services/characteristics requirements with the ObjC BLE architecture re-alignment. Discovery is now specified as a reliability gate, not just a catalog: - Add the "connected != ready" invariant and an enforced readiness gate (FR-10.3.6): characteristic I/O / command execution must await readiness (bounded by the discovery timeout) or fail fast if discovery failed. - Expose readiness via a feed distinct from ConnectionState (FR-10.3.2). - Fail-closed partial-discovery policy for missing declared UUIDs (FR-10.3.5); invalidate app-visible snapshot on disconnect (FR-10.3.4). - Mandatory declared-UUID set with hard error if absent (FR-10.1.2). - Subscription intent persistence / re-arm after reconnect, restoration, or didModifyServices (FR-10.4.5); trim FR-10.4.4 to a forward-ref. - didModifyServices must re-gate work, not only re-discover (FR-10.5.1). - Add state restoration handling (FR-10.6.3) and align catalog keying to the Peripheral.id identity model, noting FR-8.5 (FR-10.6.2). Tie-ins: FR-1.2 and FR-4.1 now name the readiness gate so the future command layer inherits it. Command-queue/pause-rerun mechanics remain out of scope (follow-up PRD alignment); referenced only as forward hooks. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Hg9mZheWnLsn8fSrB7JZ88 --- PRD.md | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/PRD.md b/PRD.md index 313680a..8c95f36 100644 --- a/PRD.md +++ b/PRD.md @@ -26,7 +26,7 @@ ReliaBLE is a Swift package that provides a reliable, modern, yet easy to use in 1. Reliability of Communication: - FR-1.1: Implement error detection and correction mechanisms for each BLE transaction. -- FR-1.2: Ensure automatic reconnection attempts with exponential backoff for failed connections. On reconnection, services and characteristics must be re-discovered rather than reused from the prior connection (see FR-10.6). +- FR-1.2: Ensure automatic reconnection attempts with exponential backoff for failed connections. On reconnection, services and characteristics must be re-discovered rather than reused, as part of returning to a discovery-*ready* state (FR-10.6, FR-10.3); a re-established link alone is not sufficient to resume characteristic I/O. Any future command-layer reconnect-and-rerun (FR-4/FR-5) depends on this ready transition rather than treating "connected again" as enough. - FR-1.3: Provide status updates on connection stability and data transmission integrity. - FR-1.3.1: Provide status updates on connection stability (e.g. connected, disconnected, reconnecting). - FR-1.3.2: Provide status updates on data transmission integrity. @@ -49,7 +49,7 @@ ReliaBLE is a Swift package that provides a reliable, modern, yet easy to use in 4. Command Style Protocol for Interacting with Peripherals: -- FR-4.1: Define a flexible command protocol where the content and functionality of commands are provided by the integrating app. Commands target services and characteristics made available through discovery (see FR-10), referenced by UUID (CBUUID). +- FR-4.1: Define a flexible command protocol where the content and functionality of commands are provided by the integrating app. Commands target services and characteristics by UUID (CBUUID), and may only execute against a peripheral that is discovery-*ready* for the targeted UUIDs (FR-10.3): targeting an undiscovered or not-ready characteristic must await readiness (bounded by a timeout) while discovery is in progress, or fail fast when discovery has already failed. This gate is inherited from FR-10 so the command queue does not re-litigate it. - FR-4.2: Support various command types: - FR-4.2.1: Notify-only (peripherals notify with updates). @@ -122,7 +122,7 @@ ReliaBLE is a Swift package that provides a reliable, modern, yet easy to use in - Connection/disconnection events - Command successes or failures - ✅ Scanning start/stop - - Service/characteristic discovery events (discovery start/completion/failure, GATT table changes, subscription state changes) + - Service/characteristic discovery events (discovery start/completion/failure, readiness transitions, GATT table changes, subscription state changes) - Security events (e.g., encryption initiation or failure) - Data chunking operations @@ -136,38 +136,49 @@ firmware changes and reconnections. This section covers discovery and characteri only; the movement of application data over characteristics is covered by the command protocol (FR-4) and chunking (FR-7). +**Discovery is a reliability gate, not merely a catalog.** A peripheral being connected +(FR-1.3.1) is not sufficient for interaction: it is not eligible for command execution or +characteristic I/O until discovery has reached a terminal *ready* state for the app-declared +UUID set. This "connected ≠ ready" invariant (defined in FR-10.3) is the discovery-layer +foundation that the command queue (FR-4, FR-5) and reconnect-and-rerun behavior are expected +to build on; those command-layer mechanics are out of scope for this section and referenced +only where the gate must be honored. + - FR-10.1: Service Discovery: - FR-10.1.1: Provide an API to discover services on a connected peripheral, requiring the integrating app to declare the specific service UUIDs of interest. Do not expose unfiltered "discover all services" as the default path — Apple explicitly discourages it because enumerating an entire remote GATT table negatively affects battery life and time-to-ready. - - FR-10.1.2: Support both automatic discovery on connection (using a caller-declared set of desired services/characteristics) and explicit, on-demand discovery invoked by the integrating app. - - FR-10.1.3: Expose discovered services to the integrating app as Sendable value snapshots keyed by peripheral identifier and service UUID (CBUUID), mirroring the existing peripheral-snapshot pattern. Never surface live CoreBluetooth objects. + - FR-10.1.2: Support both automatic discovery on connection and explicit, on-demand discovery. The integrating app must declare the desired service/characteristic UUID set that discovery targets; this declaration is supplied at or around connection (e.g., via `connect` options and/or a sticky per-peripheral registration on the manager) and/or as configuration defaults. The exact API surface is finalized in implementation, but the declaration is mandatory: if automatic discovery is requested with no declared UUID set, that is a hard error (fail-closed), not a silent leave-not-ready — consistent with the "no unfiltered default" rule (FR-10.1.1). + - FR-10.1.3: Expose discovered services to the integrating app as Sendable value snapshots keyed by peripheral identifier (per the identity model in FR-10.6.2) and service UUID (CBUUID), mirroring the existing peripheral-snapshot pattern. Never surface live CoreBluetooth objects. - FR-10.2: Characteristic and Descriptor Discovery: - FR-10.2.1: Provide an API to discover characteristics for a discovered service, allowing the integrating app to declare the characteristic UUIDs of interest. Enforce CoreBluetooth's ordering constraint: services are discovered before characteristics, and characteristics before descriptors. - - FR-10.2.2: Track per-service discovery completion, correctly handling the separate completion callback fired for each service, and report a peripheral as "ready" only once all requested discoveries have completed. + - FR-10.2.2: Track per-service discovery completion, correctly handling the separate completion callback fired for each service, and drive the peripheral to the *ready* state (FR-10.3) only once all requested discoveries have completed. - FR-10.2.3: Optionally discover descriptors for a characteristic on demand. Descriptor discovery is not required to enable notifications (see FR-10.4.1). - FR-10.2.4: Expose discovered characteristics as Sendable value snapshots including metadata: UUID (CBUUID), properties (read, write, write-without-response, notify, indicate), and, when discovered, descriptors. - FR-10.3: Discovery Lifecycle and Readiness: - FR-10.3.1: Model discovery as an explicit, per-peripheral state machine driven entirely by delegate callbacks. Never assume synchronous availability of services or characteristics after initiating discovery. - - FR-10.3.2: Provide callbacks/streams (mirroring the asynchronous event pattern of FR-2.3) reporting discovery progress, completion, and errors. + - FR-10.3.2: Expose discovery/readiness state through a feed distinct from the connection-state stream (FR-1.3.1 / `ConnectionState`), so that connection-recovery observability (a library strength) and GATT readiness are not conflated. Readiness must be observable and awaitable (e.g., a discovery/readiness state stream and/or an awaitable ready signal), reporting progress, completion, and errors. `connected` must never be presented to the app as `ready`. - FR-10.3.3: Enforce a configurable discovery timeout so that a stalled or interrupted discovery surfaces as an error rather than an indefinite wait. - - FR-10.3.4: Handle interrupted or partial discovery: if a peripheral disconnects mid-discovery, treat it as a hard reset of the discovery state — discard partial results and any pending discovery bookkeeping — and re-run discovery on the next connection (see FR-10.6.1). - - FR-10.3.5: Handle non-nil errors returned in each discovery callback, mapping CoreBluetooth errors into the library's error type. Distinguish common failure causes where feasible (e.g., characteristic-not-found from an overly narrow filter or a stale cache, versus disconnection during discovery). + - FR-10.3.4: Handle interrupted or partial discovery: if a peripheral disconnects mid-discovery, treat it as a hard reset of the discovery state — discard partial results and any pending discovery bookkeeping — and re-run discovery on the next connection (see FR-10.6.1). On unexpected disconnect, the app-visible discovery snapshot must be invalidated (cleared, or marked stale until the next ready) with explicit, documented timing, matching the existing peripheral-snapshot refresh philosophy. + - FR-10.3.5: Partial-discovery policy is fail-closed. If a declared (required) service or characteristic UUID is absent — narrow filter, wrong firmware, or a stale cache — discovery is a failure: the peripheral does not become ready and the missing UUID(s) are reported. Map non-nil errors from each discovery callback into the library's error type, distinguishing common causes where feasible (e.g., missing-UUID versus disconnection during discovery). + - FR-10.3.6: Readiness is an enforced gate, not merely an observable signal. Characteristic I/O and command execution (FR-4, and any future read/write/notify API) against a peripheral must wait until it is ready for the targeted UUIDs: await readiness (bounded by the FR-10.3.3 timeout) while discovery is pending or in progress, and fail fast if discovery has already failed. The full command-queue behavior that consumes this gate is specified in FR-4/FR-5 (out of scope here); this requirement only fixes the gate contract those layers must honor. - FR-10.4: Characteristic Subscription Management (Notify/Indicate): - FR-10.4.1: Provide an API to enable and disable notifications or indications on a characteristic using the platform's subscribe mechanism. Never write the Client Characteristic Configuration Descriptor (CCCD, 0x2902) directly — CoreBluetooth manages the CCCD write internally and forbids writing it directly. - - FR-10.4.2: Confirm the subscription state-change callback before treating a subscription as active, and sequence subscription-enable before issuing any command that causes the peripheral to emit data, so that initial notifications are not lost. Delivery of the notified values is covered by FR-4.2.1 (notify-only) and FR-2.3.2 (data-received streams). + - FR-10.4.2: Confirm the subscription state-change callback before treating a subscription as active, and sequence subscription-enable before issuing any command that causes the peripheral to emit data, so that initial notifications are not lost. Where a peripheral's readiness depends on active subscriptions, re-subscribe (FR-10.4.5) must complete before the readiness gate (FR-10.3.6) is released for notification-dependent work. - FR-10.4.3: Expose subscription state and its changes to the integrating app. Do not rely solely on the platform's `isNotifying` flag, which can lag the actual state. - - FR-10.4.4: Account for the fact that notifications are unacknowledged at the GATT layer and may be lost or reordered above the link layer. Application-layer framing/sequencing for multi-packet notification streams is handled per FR-7 (chunking and reassembly). + - FR-10.4.4: Notifications are unacknowledged at the GATT layer and may be lost or reordered above the link layer; application-layer framing/sequencing of multi-packet notification streams is out of scope for this section and handled per FR-7 (chunking and reassembly) and the command protocol (FR-4). + - FR-10.4.5: The library tracks requested subscription intent per characteristic. After reconnection, state restoration (FR-10.6.3), or `didModifyServices` re-discovery (FR-10.5.1), previously requested subscriptions must be re-armed — or explicitly reported as inactive for the app to re-request. Re-discovery alone does not restore notify/indicate intent, and notify-only integrations (FR-4.2.1) break without this. - FR-10.5: GATT Cache Invalidation and Re-discovery: - - FR-10.5.1: Detect and surface peripheral GATT-table changes reported by the platform (the "did modify services" callback) and automatically re-run discovery for the affected services. + - FR-10.5.1: On a peripheral GATT-table change (the `didModifyServices` callback), detection alone is insufficient. The library must immediately mark the peripheral *not ready* (re-gating work per FR-10.3.6), park or fail any in-flight discovery-dependent I/O, re-run discovery for the affected (or all declared) services, return to *ready* only after that completes, re-arm subscription intent (FR-10.4.5), and surface the event on the discovery/readiness stream. - FR-10.5.2: Treat reconnection following a peripheral firmware update or DFU as requiring full re-discovery; do not trust a previously cached view of services and characteristics. - FR-10.5.3: Document the iOS OS-level GATT caching limitation: there is no public API to clear the cache. Reliable cross-connection refresh requires the peripheral to implement the Service Changed characteristic (0x2A05) within the Generic Attribute Service (0x1801) and to bond; otherwise the only fallback is user-driven (toggle Bluetooth, or "Forget This Device"). This is a peripheral-firmware responsibility that the library cannot fully control, and the limitation must be communicated to integrating apps. -- FR-10.6: Reconnection and Stale-Reference Handling: - - FR-10.6.1: Re-discover services and characteristics on every new connection. Never reuse a service or characteristic reference obtained from a prior connection (see FR-1.2 for reconnection). - - FR-10.6.2: Internally discard all live CoreBluetooth service/characteristic references on disconnect and reset the discovery state. The app-facing catalog must be keyed by UUID (CBUUID), not by held CoreBluetooth objects. This is consistent with the library's existing invalidation/refresh behavior on Bluetooth power cycling. +- FR-10.6: Reconnection, State Restoration, and Stale-Reference Handling: + - FR-10.6.1: Re-discover services and characteristics on every new connection. Never reuse a service or characteristic reference obtained from a prior connection. Re-discovery is part of returning to the *ready* state (FR-10.3.6): a re-established link is not sufficient to resume characteristic I/O until discovery completes (see FR-1.2). + - FR-10.6.2: Internally discard all live CoreBluetooth service/characteristic references on disconnect and reset the discovery state. The app-facing catalog must be keyed by the library's peripheral identity model (`Peripheral.id`, which resolves name → localName → `cbIdentifier`), not by held CoreBluetooth objects and not by `cbIdentifier` alone — so discovery keys stay consistent with the rest of the library. (Stable identity from manufacturer data, FR-8.5, is still open; this keying must adopt whatever FR-8.5 settles on.) This is consistent with the library's existing invalidation/refresh behavior on Bluetooth power cycling. + - FR-10.6.3: On CoreBluetooth central state restoration (`willRestoreState`), re-associate the restored peripherals, discard any prior discovery catalog, re-run discovery for peripherals the app still intends to use, and re-apply subscription intent (FR-10.4.5). A restored link is not ready: the same readiness gate (FR-10.3.6) as a fresh connection applies. ### Non-Functional Requirements