diff --git a/PRD.md b/PRD.md index ed926b2..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. +- 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. +- 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,10 +122,65 @@ 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, readiness transitions, 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). + +**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 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 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: 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). 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. 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: 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: 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, 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 1. Modern Swift 6 Architecture: