Skip to content

Add functional requirements for service & characteristic discovery#47

Merged
itsniper merged 2 commits into
masterfrom
claude/ble-ios-requirements-3r8dat
Jul 15, 2026
Merged

Add functional requirements for service & characteristic discovery#47
itsniper merged 2 commits into
masterfrom
claude/ble-ios-requirements-3r8dat

Conversation

@itsniper

Copy link
Copy Markdown
Member

Summary

The PRD covered scanning/connections (FR-3, FR-8) and reading/writing data (FR-4, FR-7) but skipped the GATT layer in between — service and characteristic discovery and setup, the phase that must happen after a peripheral connects and before any read/write can occur. This PR fills that gap.

Adds a new §10 — Service and Characteristic Discovery and Management to PRD.md, appended after the Logging section with stable numbering (no existing sections renumbered, so the FR-8.5 code references are untouched).

Scope

Discovery and characteristic setup only. The movement of application data over characteristics stays with the command protocol (FR-4) and chunking (FR-7), referenced rather than duplicated.

New requirements (all unchecked — greenfield; the library currently has no GATT layer)

  • FR-10.1 Service Discovery — caller-declared UUID filtering (no unfiltered default), auto-on-connect + on-demand, Sendable snapshots keyed by peripheral id + CBUUID.
  • FR-10.2 Characteristic & Descriptor Discovery — enforce services→characteristics→descriptors ordering, per-service completion tracking, characteristic metadata (properties).
  • FR-10.3 Discovery Lifecycle & Readiness — explicit per-peripheral state machine, progress/error streams, configurable timeout, interrupted/partial-discovery reset, error mapping.
  • FR-10.4 Subscription Management (Notify/Indicate)setNotifyValue (never write CCCD 0x2902 directly), confirm-before-active + enable-before-command sequencing, don't trust isNotifying, notifications are unacknowledged.
  • FR-10.5 GATT Cache Invalidation & Re-discoverydidModifyServices re-discovery, post-DFU full re-discovery, and the documented iOS OS-level caching limitation (Service Changed 0x2A05 + bonding is a peripheral-firmware responsibility, no app-side clear API).
  • FR-10.6 Reconnection & Stale-Reference Handling — re-discover every connection, never reuse CBService/CBCharacteristic across connections, catalog keyed by CBUUID.

Tie-ins to existing sections

  • FR-1.2 — reconnection now notes services/characteristics are re-discovered (→ FR-10.6).
  • FR-4.1 — commands target services/characteristics made available through discovery (→ FR-10).
  • FR-9.2 — added discovery events to the logged-events list.

Grounding

Requirement content is grounded in researched real-world iOS CoreBluetooth pitfalls: filtered vs. unfiltered discovery (battery/perf), the mandatory discovery ordering and per-service callback fan-out, the OS-level GATT cache + Service Changed problem, CCCD-via-setNotifyValue, unacknowledged/lossy notifications, and the requirement never to hold CoreBluetooth attribute objects across connections.

This is a documentation-only change (PRD.md); no code is modified.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hg9mZheWnLsn8fSrB7JZ88


Generated by Claude Code

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hg9mZheWnLsn8fSrB7JZ88

@itsniper itsniper left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: GATT discovery PRD vs architecture findings

Scope is right (services/characteristics only; not a full PRD rewrite). The CoreBluetooth pitfalls coverage (filtered discovery, ordering, CCCD via setNotifyValue, OS GATT cache, no live CB object reuse) is solid and well grounded.

Where this misses the mark relative to the ObjC-vs-ReliaBLE architecture evaluation (docs/investigations/objc-ble-vs-reliable-architecture-2026-07-15.md and the ObjC readiness model):

The critical architectural role of discovery is under-specified

In the production ObjC stack, discovery was not just a catalog API — it was a hard readiness gate on the per-peripheral work queue:

  • queue created suspended
  • unsuspend only after required services/chars exist
  • didModifyServices re-suspends and rediscovers before any further work

ReliaBLE's reliability unit today is connection state. FR-10 correctly introduces discovery state, but it never makes connected ≠ ready-for-GATT an invariant that FR-4/FR-5 must honor. Without that, shipping GATT later recreates the early write/notify races the ObjC gate prevented.

Gaps to close within this PR's GATT scope (not full command-layer PRD)

  1. Readiness gate invariant — FR-10 must require that command/GATT ops wait until ready (and re-wait after re-discovery).
  2. Declare-desired-services API shape — auto-on-connect (FR-10.1.2) needs where/when the app declares the filter (connect options? config? per-peripheral registration?).
  3. Discovery vs ConnectionState — separate stream vs extend connection lifecycle; document that connected is not ready.
  4. State restoration — existing Background.md already says GATT must re-wire on restore; FR-10.6 should cover restore explicitly, not only "new connection".
  5. Re-subscribe after reconnect — FR-10.4 enables notify, FR-10.6 rediscovers, but nothing requires re-enabling subscriptions after drop/restore (notify-only apps break otherwise).
  6. didModifyServices must re-gate work, not only re-run discovery in the background while commands continue.
  7. Partial discovery policy — missing required UUID → hard fail vs partial ready?
  8. Peripheral key identity — "peripheral identifier" must align with existing Peripheral.id (name/localName/cbIdentifier; FR-8.5 still open).

Intentionally out of scope (flag only — post-merge collaboration)

Do not expand this PR into full FR-4/5. Just leave explicit forward references so later work doesn't invent a parallel model:

  • serial-per-peripheral queue + reconnect-and-rerun (ObjC moat)
  • command watchdogs / exactly-once completion
  • demand-connect / idle disconnect (deliberate product fork; optional later)

Recommended build order from the investigation still holds: GATT substrate → serial queue → readiness gate → reconnect-and-rerun → watchdogs → only then deps/priority/chunking.

Inline comments mark the specific FR text to tighten. Happy to iterate on wording in-PR; once merged we can align the rest of the architecture into the PRD.

(Posted as COMMENT rather than REQUEST_CHANGES because GitHub disallows requesting changes on your own PR.)

Comment thread PRD.md
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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readiness as reliability invariant (missing).

This intro correctly positions discovery between connect and data movement, but it still describes discovery as a catalog/setup phase rather than a gate.

Please add an explicit invariant here (or as FR-10.3.x), roughly:

A peripheral is not eligible for command execution or characteristic I/O until discovery has reached a terminal ready state for the app-declared UUID set. connected (FR-1.3.1 / connection streams) is not sufficient.

This is the ObjC "discovery gates the command queue" property that the architecture comparison flagged as the first reliability primitive to land with GATT. Without it, FR-4/5 implementors will race writes against incomplete discovery.

Comment thread PRD.md Outdated

- 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API shape for "caller-declared" services is underspecified.

FR-10.1.1–10.1.2 require a declared UUID set for auto-on-connect discovery, but not where that declaration lives. ReliaBLE's connection model is explicit connect(to:) (not demand-driven), so this needs a deliberate choice, e.g.:

  • per-connect options (desiredServices / desiredCharacteristics), and/or
  • a sticky per-peripheral registration on the manager, and/or
  • config defaults

Also specify: if auto-discovery is enabled and no UUID set was declared, is that a hard error (preferred — matches "no unfiltered default") or a no-op leave-not-ready?

Please nail this at requirement level so implementation doesn't invent three competing APIs.

Comment thread PRD.md Outdated

- 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Ready" needs a hard consumer contract, not just a status bit.

"Report a peripheral as ready only once all requested discoveries have completed" is good, but incomplete without:

  1. Partial discovery policy — if a declared service/characteristic UUID is absent (narrow filter, wrong firmware, stale cache), is that discovery failure (not ready + error) or partial ready with a missing-UUID report? ObjC-style reliability prefers fail-closed for required UUIDs.
  2. Gate on consumers — FR-4 commands / any future read-write-notify API must await/park until ready (and fail fast if discovery failed), not merely observe a stream.
  3. Cross-ref — add an explicit FR-4.1 (or FR-5.2) note that enqueue/execute is gated on FR-10 readiness.

Without (2)–(3), "ready" is informational only and won't prevent the early-I/O races this section exists to stop.

Comment thread PRD.md Outdated

- 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discovery lifecycle vs existing ConnectionState streams.

ReliaBLE already ships connection lifecycle streams (connected / disconnected / reconnecting, etc.). FR-10.3.1–10.3.2 introduce another per-peripheral state machine + streams, but don't define the relationship.

Please specify one of:

  • A) Discovery states are a separate feed (e.g. discoveryStateChanges) and docs must stress connected ≠ ready, or
  • B) Connection lifecycle is extended with discovery phases (connecting → connected → discovering → ready), or
  • C) Hybrid: keep connection stream pure; readiness is a derived gate used internally + exposed as a simple isReady/awaitable.

Architecture eval preference: keep connection recovery observability (a ReliaBLE strength) distinct, and treat readiness as an explicit gate/stream so reconnect ladders don't get conflated with GATT catalog state. Either way, pick it in the PRD so implementers don't fork models.

Comment thread PRD.md Outdated
- 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good hard-reset on mid-discovery disconnect — keep this.

One addition: on unexpected disconnect, also define what happens to any app-visible discovery snapshot (clear immediately vs mark stale until next ready). FR-10.6.2 says discard internal CB refs; the app-facing catalog invalidation timing should be equally explicit to match the existing peripheral-snapshot refresh philosophy.

Comment thread PRD.md Outdated
- 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR-10.4 scope + reconnect re-subscribe gap.

Two issues:

  1. Scope boundary — PR summary says discovery/setup only; enable/disable notify is setup-adjacent and fine, but FR-10.4.4's deep dive into lossy notifications + FR-7 handoff is command/chunking territory. Consider trimming 10.4.4 to a one-liner forward-ref so this section stays discovery/setup-focused.

  2. Missing requirement (in-scope): After reconnect / restore / didModifyServices re-discovery, previously requested subscriptions must be re-armed (or explicitly reported as inactive for the app to re-request). Rediscovery alone does not restore notify/indicate intent. Notify-only apps (FR-4.2.1) break without this.

Also sequence re-subscribe before releasing the readiness gate for commands that depend on notifications (ties to FR-10.4.2).

Comment thread PRD.md Outdated
- 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didModifyServices must re-gate work, not only re-discover.

ObjC re-suspended the peripheral queue on GATT table change, then rediscovered, then resumed. FR-10.5.1 only requires detecting the callback and re-running discovery.

Please require:

  • mark peripheral not ready immediately on didModifyServices
  • fail or park any in-flight discovery-dependent I/O
  • re-run discovery for affected (or all declared) services
  • return to ready only after completion
  • surface the event on the discovery/lifecycle stream

Otherwise commands can execute against a half-invalidated catalog mid-DFU/service-change — exactly the class of bug the readiness gate exists to prevent.

Comment thread PRD.md Outdated

- 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State restoration path is missing.

FR-10.6 covers "every new connection" and disconnect invalidation, but ReliaBLE already has CB state restoration (restoreIdentifier, willRestoreState). Documentation.docc/Topics/Background.md already notes that when GATT lands, restoration must re-wire peripheral GATT state.

Please add something like FR-10.6.3:

  • On central restore, re-associate restored CBPeripherals, discard any prior catalog, re-run discovery for peripherals the app still intends to use, and re-apply subscription intent (see FR-10.4 comment).
  • Restored link ≠ ready; same readiness gate as a fresh connect.

Also: "keyed by peripheral identifier" should explicitly mean ReliaBLE's Peripheral.id model (and note FR-8.5 still incomplete) so implementers don't key only on cbIdentifier inconsistently with the rest of the library.

Comment thread PRD.md Outdated
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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good cross-ref — make the gate explicit.

"Commands target services/characteristics made available through discovery" still allows interpreting discovery as a passive catalog. Please strengthen to something like:

Commands may only execute against a peripheral that is discovery-ready for the targeted service/characteristic UUIDs (FR-10.3). Targeting an undiscovered or not-ready characteristic is an error (or must await readiness — pick one; prefer await-with-timeout for auto-discover, hard error if discovery already failed).

This is the minimal FR-4 tie-in needed so the later command-queue work inherits the gate instead of re-litigating it.

Comment thread PRD.md Outdated

- 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).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-discover on reconnect is correct; incomplete for in-flight work.

Within GATT scope this FR-1.2 addition is right. Please also note (one clause is enough) that re-discovery is part of returning to ready, and that any future command-layer reconnect-and-rerun (FR-4/5; post-merge) depends on this ready transition — so we don't later treat "connected again" as sufficient to resume I/O.

No need to specify the full pause/retry model here.

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hg9mZheWnLsn8fSrB7JZ88

Copy link
Copy Markdown
Member Author

Thanks — this is a great review. Justin confirmed the direction: fold the discovery-relevant bits of the ObjC architecture re-alignment into this PR, keep it scoped to services/characteristics, and leave the full FR-4/FR-5 command-layer alignment to a follow-up. Pushed in d6dac2e. Point-by-point:

Readiness as reliability invariant (§137, and lines 29/52) — Added. The §10 intro now states the connected ≠ ready invariant explicitly, and FR-10.3.6 makes readiness an enforced gate (not just a signal): I/O awaits readiness (bounded by the FR-10.3.3 timeout) or fails fast if discovery already failed. FR-1.2 and FR-4.1 now name the gate as a forward hook so the command layer inherits it instead of re-litigating it.

"Ready" consumer contract + partial-discovery policy (§146) — FR-10.3.5 is now fail-closed: a missing declared/required UUID = discovery failure (not-ready + missing-UUID report). Consumer gating is FR-10.3.6; FR-4.1 carries the cross-ref.

Declared-services API shape (§141) — FR-10.1.2 now requires the app to declare the desired UUID set at/around connect (via connect options and/or a sticky per-peripheral registration, and/or config defaults), fail-closed if absent. I kept the exact API surface at requirement altitude ("finalized in implementation") rather than picking one signature — flagging that as the one genuine open product decision for you to lock when the command layer lands.

Discovery vs ConnectionState (§152) — Went with your preferred split (option A/C): FR-10.3.2 requires readiness on a feed distinct from the connection-state stream, observable + awaitable, and connected must never be surfaced as ready.

Snapshot invalidation on disconnect (§154) — Added to FR-10.3.4 (clear or mark stale, explicit timing).

Re-subscribe after reconnect/restore/modify (§161) — New FR-10.4.5 (subscription intent persistence + re-arm, or report inactive); FR-10.4.2 sequences re-subscribe before releasing the gate for notify-dependent work; trimmed FR-10.4.4 to a forward-ref to FR-7/FR-4.

didModifyServices must re-gate (§164) — FR-10.5.1 rewritten: mark not-ready → park/fail in-flight I/O → re-discover → back to ready → re-arm subscriptions → surface on the stream.

State restoration (§170) — New FR-10.6.3 (willRestoreState: re-associate, discard prior catalog, re-discover, re-apply subscription intent; restored link ≠ ready). Identity keying in FR-10.6.2 now points at Peripheral.id (name → localName → cbIdentifier), not cbIdentifier alone, with the FR-8.5 caveat.

Two notes:

  • Intentionally left in FR-4/FR-5 for the follow-up per your "flag only": serial-per-peripheral queue, exactly-once completion/watchdogs, reconnect-and-rerun of in-flight work, demand-connect/idle-disconnect. FR-1.2 and FR-4.1 leave the forward references so those don't invent a parallel model.
  • Heads-up on the referenced material: the investigation doc (objc-ble-vs-reliable-architecture-2026-07-15.md) and Topics/Background.md aren't committed on this branch, and CB state restoration isn't yet in Sources/ here (this branch's tree is behind the one the investigation was written against). So FR-10.6.3 is written forward-looking — worth confirming it matches the intended restore wiring once that work merges.

Happy to iterate on wording in-thread.


Generated by Claude Code

@itsniper
itsniper merged commit 2142be2 into master Jul 15, 2026
3 checks passed
@itsniper
itsniper deleted the claude/ble-ios-requirements-3r8dat branch July 15, 2026 23:39
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.

2 participants