// src/profiles/commands.ts import { contractError } from '../backend-contract/errors' import type { CharacteristicPath, CharacteristicProperties, GattDatabaseSnapshot } from '../backend-contract/gatt' import type { BackendIdentity } from '../backend-contract/identity' import type { PublicOperationOptions, SubscriptionOptions, WritePolicy, WriteReceipt } from '../backend-contract/operations' import type { OwnedBytes, Uuid } from '../backend-contract/primitives' import type { Connection, DiscoveredGattDatabase, Subscription } from '../manager' type CurrentCharacteristicPath = CharacteristicPath< Attachment, string, string, string, string, 'current' > /** A duplicate-safe selector for an attribute in one discovered GATT database. */ export interface CharacteristicSelector { readonly serviceUuid: Uuid readonly characteristicUuid: Uuid readonly serviceOccurrence: string | null readonly characteristicOccurrence: string | null } export interface CharacteristicSelectorOptions { readonly serviceOccurrence?: string readonly characteristicOccurrence?: string } export function characteristicSelector( serviceUuid: Uuid, characteristicUuid: Uuid, options: CharacteristicSelectorOptions = {} ): CharacteristicSelector { return { serviceUuid, characteristicUuid, serviceOccurrence: options.serviceOccurrence ?? null, characteristicOccurrence: options.characteristicOccurrence ?? null } } /** Thin public command for discovery; cancellation and deadlines stay with the public connection primitive. */ export function discoverGatt>( connection: Connection, options: PublicOperationOptions ): Promise> { return connection.discover(options) } /** * Resolves one exact characteristic path from an ordered snapshot. UUID-only * lookup is intentionally rejected when an occurrence is duplicated. */ export async function resolveCharacteristicPath( snapshot: GattDatabaseSnapshot, selector: CharacteristicSelector ): Promise> { const candidates = snapshot.characteristics.filter(characteristic => matchesSelector(characteristic.path, selector)) if (candidates.length === 0) { throw contractError('gatt.not-found', 'gatt', 'profiles.resolve-characteristic-path') } if (candidates.length > 1) { throw contractError('gatt.ambiguous-path', 'gatt', 'profiles.resolve-characteristic-path') } const candidate = candidates[0] if (candidate === undefined) { throw contractError('lifecycle.invariant-violation', 'core', 'profiles.resolve-characteristic-path.selection') } return candidate.path } export async function readCharacteristic>( database: DiscoveredGattDatabase, selector: CharacteristicSelector, options: PublicOperationOptions ): Promise { const snapshot = await database.snapshot() const path = await resolveCharacteristicPath(snapshot, selector) assertRequiredProperty(snapshot, path, 'read', 'profiles.read-characteristic') return database.read(path, options) } export async function writeCharacteristic>( database: DiscoveredGattDatabase, selector: CharacteristicSelector, value: Readonly, options: WritePolicy ): Promise> { const snapshot = await database.snapshot() const path = await resolveCharacteristicPath(snapshot, selector) assertRequiredProperty( snapshot, path, options.mode === 'without-response' ? 'writeWithoutResponse' : 'writeWithResponse', 'profiles.write-characteristic' ) return database.write(path, value, options) } export async function subscribeCharacteristic>( database: DiscoveredGattDatabase, selector: CharacteristicSelector, options: SubscriptionOptions ): Promise> { const snapshot = await database.snapshot() const path = await resolveCharacteristicPath(snapshot, selector) assertRequiredProperty(snapshot, path, 'notify', 'profiles.subscribe-characteristic') return database.subscribe(path, options) } function assertRequiredProperty( snapshot: GattDatabaseSnapshot, path: CurrentCharacteristicPath, property: keyof CharacteristicProperties, operation: string ): void { const record = snapshot.characteristics.find( characteristic => characteristic.path.serviceUuid === path.serviceUuid && characteristic.path.characteristicUuid === path.characteristicUuid && String(characteristic.path.serviceOccurrence) === String(path.serviceOccurrence) && String(characteristic.path.characteristicOccurrence) === String(path.characteristicOccurrence) ) if (record === undefined) { throw contractError('gatt.not-found', 'gatt', operation) } if (record.properties[property] !== true) { throw contractError('gatt.property-not-supported', 'gatt', operation) } } function matchesSelector( path: CurrentCharacteristicPath, selector: CharacteristicSelector ): boolean { return ( path.serviceUuid === selector.serviceUuid && path.characteristicUuid === selector.characteristicUuid && (selector.serviceOccurrence === null || String(path.serviceOccurrence) === selector.serviceOccurrence) && (selector.characteristicOccurrence === null || String(path.characteristicOccurrence) === selector.characteristicOccurrence) ) }