Skip to content

Phase 3: FR-10 GATT discovery and readiness on Peripheral #52

Description

@itsniper

Goal

Implement service/characteristic discovery as a reliability gate on Peripheral: connected ≠ ready; sticky UUID filter; subscriptions as intent; re-discover on reconnect/restore/modify.

PRD: FR-10 (all), FR-1.2 discovery re-run tie-in, FR-2.3.3, FR-9.2 discovery events, FR-3.3 (peripheral delegate in actor), FR-11.4 (discover-to-ready joins the single ensure-linked path)
Depends on: #50 (handle API), #51 (work-driven connection lifecycle)
Blocks: FR-4/5 commands (must not ship before this)

High-level requirements

  • Sticky discovery filter on Peripheral; fail-closed if auto-discovery/work needs discovery with empty filter (FR-10.1)
  • Filtered service/char discovery; services → characteristics → descriptors ordering (FR-10.1–10.2)
  • Per-peripheral discovery state machine; catalog-ready default (FR-10.3.7)
  • Distinct readiness stream/await from connection state (FR-10.3.2); enforced gate (FR-10.3.6)
  • Discovery timeout; mid-disconnect hard reset; fail-closed missing UUIDs (FR-10.3.3–3.5)
  • Sendable service/characteristic snapshots; never live CB objects
  • Subscriptions via setNotifyValue only; confirm-before-active; intent re-arm after reconnect/restore/didModifyServices (FR-10.4)
  • didModifyServices → not ready → re-discover → re-arm → event (FR-10.5)
  • Re-discover every connection; discard CB refs on disconnect; restore re-discover + re-subscribe (FR-10.6)
  • Document OS GATT cache / Service Changed limitation (FR-10.5.3)
  • Wire restore path to attach peripheral delegate when GATT lands (called out in Background docs today)

Constraints inherited from the work-driven lifecycle (#51)

These are load-bearing for every sub-issue and must be settled before 3a starts.

  • Discovery must be demand-aware. handleDidConnect starts the idle grace timer immediately when a peripheral has no demand (no work lease, no manual-connect hold). Two decisions follow:
    • Discovery in flight must itself constitute demand (hold a work lease, or an equivalent suppressor) so idle teardown cannot fire mid-discovery.
    • A link with no demand should not trigger auto-discovery at all — the Tier-0 grace-window blip and a restored link with no persisted hold are both about to be torn down, and discovering them is pure battery cost (NFR-3.2).
  • Discovery timeout vs. idle interval. ReliaBLEConfig.idleDisconnectInterval defaults to 5s globally. A discovery timeout at or above that value is a footgun unless discovery suppresses idle per the point above. Document the interaction.
  • Readiness needs a completion-carrying primitive. acquireWorkLease returns once a connect has been issued, not completed, and gives holders no success/failure signal — the connection-state stream is the only channel. FR-10.3.6's awaitable readiness and FR-4.1's bounded wait cannot be layered on that shape as-is; an await-linked primitive (or per-command failure delivery) is required. Do not let ensureLinked's fire-and-forget shape get inherited by the readiness gate.
  • Stranded-demand failures. reevaluateLink failures raised in delegate context publish .failed(reason: .notFound) on the connection-state stream and retain demand deliberately (PRD FR-4.4). Phase 3 must define how readiness and, later, commands react — treat it as a work/command failure decided independently of lease or hold lifetime.
  • Invalidation now includes .poweredOff. invalidatePeripherals() drops live CBPeripheral references on power-off as well as resetting/unsupported/unauthorized. Catalog discard and the not-ready transition (FR-10.6.2) must hook that path, not just disconnect.
  • Restore intent is concrete. "Peripherals the app still intends to use" (FR-10.6.3) = ids with a rehydrated persisted manual-connect hold. Work leases never survive process death, so at launch a hold is the only source of intent.

Out of scope

  • Full command protocol FR-4/5 (follow-on phase)
  • FR-5.2 prioritization/deps beyond what's needed to gate I/O
  • App domain framing/CRC

Acceptance criteria

  • Sticky filter API on Peripheral; empty filter + auto-discovery = hard error
  • After connect, peripheral reaches catalog-ready only when declared UUIDs discovered (or fails closed)
  • Readiness observable/awaitable and distinct from ConnectionState; connectedready
  • Discovery holds demand for its duration; no idle teardown can fire mid-discovery
  • No auto-discovery on a link with zero demand
  • Unexpected disconnect and radio invalidation both invalidate the discovery catalog; next connection re-discovers
  • didModifyServices re-gates and re-discovers
  • Subscription intent re-armed after reconnect/restore/modify (or explicitly reported inactive)
  • State restoration re-runs discovery and re-applies intent for peripherals with a rehydrated hold (FR-10.6.3)
  • No public CB GATT objects; unit tests with mocks; DocC + FR-10.5.3 limitation documented
  • PRD FR-1.2 discovery re-run tie-in satisfied for link recovery paths that exist

Sub-issues

Planning/execution breakdown lives in child issues: #61 filter + discovery + catalogs, #62 ready gate SM, #63 subscriptions + intent re-arm, #64 modify services / restore rediscovery.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions