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
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.
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
Peripheral; fail-closed if auto-discovery/work needs discovery with empty filter (FR-10.1)setNotifyValueonly; confirm-before-active; intent re-arm after reconnect/restore/didModifyServices(FR-10.4)didModifyServices→ not ready → re-discover → re-arm → event (FR-10.5)Constraints inherited from the work-driven lifecycle (#51)
These are load-bearing for every sub-issue and must be settled before 3a starts.
handleDidConnectstarts the idle grace timer immediately when a peripheral has no demand (no work lease, no manual-connect hold). Two decisions follow:ReliaBLEConfig.idleDisconnectIntervaldefaults 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.acquireWorkLeasereturns 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 letensureLinked's fire-and-forget shape get inherited by the readiness gate.reevaluateLinkfailures 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..poweredOff.invalidatePeripherals()drops liveCBPeripheralreferences 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.Out of scope
Acceptance criteria
Peripheral; empty filter + auto-discovery = hard errorConnectionState;connected≠readydidModifyServicesre-gates and re-discoversSub-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.