// src/backends/bluez/bluez-runtime-models.ts import { advertisementMatchesFilter, deviceIdentity, type AdvertisementObservation, type OwnerScanOptions } from '../../backend-contract/advertisement' import type { PeerAddressDescriptor } from '../../backend-contract/backend' import { contractError, type CleanupFailure, type CleanupRecord } from '../../backend-contract/errors' import type { OperationOptions, OperationTerminalRecord } from '../../backend-contract/operations' import { canonicalUuid, monotonicTimestamp, type BackendInstanceId, type LeaseId, type PeerId, type ScanSessionId, type Uuid } from '../../backend-contract/primitives' import { BLUEZ_DEVICE_INTERFACE, BLUEZ_GATT_CHARACTERISTIC_INTERFACE, BLUEZ_GATT_DESCRIPTOR_INTERFACE, BLUEZ_GATT_SERVICE_INTERFACE, type BluezManagedInterface, type BluezManagedObject, type BluezVariant } from './bluez-dbus-contract' import { BluezObjectStore } from './bluez-object-store' import type { BluezConnection } from './bluez-backend-handles' import { planBluezScan } from './bluez-scan-planner' import { trustedServiceUuidFilter } from '../scan-planning/service-uuid-scan-planner' import type { BluezGattCharacteristicRecord, BluezGattDescriptorRecord, BluezGattServiceRecord, BluezGattSnapshotRecord } from './bluez-runtime-types' import type { BluezConnectionRecord } from './bluez-runtime-types' export function createPendingConnectionRecord(devicePath: string, peerId: PeerId): BluezConnectionRecord { return { devicePath, peerId, connection: null, leases: new Set(), databases: new Set(), pendingOperations: new Map(), state: 'connecting', active: false, physicalLinkMayExist: false, ownerLeaseId: null, nextDatabaseGeneration: 1, currentDatabase: null, transition: null, disconnection: null, disconnectRequested: false, disconnectMethod: null, pendingConnectors: 0, orphanCleanupScheduled: false } } export function requireRecordConnection(record: BluezConnectionRecord): BluezConnection { if (record.connection === null) { throw contractError('lifecycle.invariant-violation', 'connection', 'bluez.connection.record') } return record.connection } export function requireOwnerLease(record: BluezConnectionRecord): LeaseId { if (record.ownerLeaseId === null) { throw contractError('ownership.denied', 'connection', 'bluez.gatt.owner-lease') } return record.ownerLeaseId } export function successfulTerminal(options: OperationOptions): OperationTerminalRecord { return Object.freeze({ correlation: options.correlation, outcome: 'succeeded', cause: null }) } export function scanFilterVariant(options: OwnerScanOptions): BluezVariant { const effectiveFilter = trustedServiceUuidFilter(options, planBluezScan, 'bluez.scan') const filter: Record = { DuplicateData: { signature: 'b', value: options.duplicatePolicy === 'all' }, Transport: { signature: 's', value: 'le' } } if (effectiveFilter.serviceUuids.length > 0) { filter.UUIDs = { signature: 'as', value: Object.freeze(effectiveFilter.serviceUuids.map(String)) } } if (effectiveFilter.localNamePrefix !== null) { filter.Pattern = { signature: 's', value: effectiveFilter.localNamePrefix } } return Object.freeze({ signature: 'a{sv}', value: Object.freeze(filter) }) } export function scanSignature(options: OwnerScanOptions): string { return JSON.stringify({ queryDigest: options.query?.digest ?? null, planDigest: options.plan?.queryDigest ?? null, serviceUuids: options.filter.serviceUuids.map(String), localNamePrefix: options.filter.localNamePrefix, duplicatePolicy: options.duplicatePolicy, timestampPolicy: options.timestampPolicy }) } export function widenedDiscoveryFilterVariant(options: OwnerScanOptions): BluezVariant { const filter: Record = { DuplicateData: { signature: 'b', value: options.duplicatePolicy === 'all' }, Transport: { signature: 's', value: 'le' } } return Object.freeze({ signature: 'a{sv}', value: Object.freeze(filter) }) } export function scanFilterObservesAddress( options: OwnerScanOptions, target: PeerAddressDescriptor ): boolean { const filter = trustedServiceUuidFilter(options, planBluezScan, 'bluez.scan') if (filter.serviceUuids.length > 0) { return false } const pattern = filter.localNamePrefix if (pattern === null || pattern.length === 0) { return true } return target.address.toLowerCase().startsWith(pattern.toLowerCase()) } export function createObservation( store: BluezObjectStore, path: string, peerId: PeerId, backendInstanceId: BackendInstanceId, scanSessionId: ScanSessionId, observedAt: number, ingressOrdinal: number ): AdvertisementObservation { const absent = Object.freeze({ state: 'absent', reason: 'not provided by BlueZ ObjectManager', provenance: 'not-provided' }) const localName = store.optionalStringProperty(path, BLUEZ_DEVICE_INTERFACE, 'Alias') const rssi = store.optionalNumberProperty(path, BLUEZ_DEVICE_INTERFACE, 'RSSI') const txPower = store.optionalNumberProperty(path, BLUEZ_DEVICE_INTERFACE, 'TxPower') const uuids = hasProperty(store, path, BLUEZ_DEVICE_INTERFACE, 'UUIDs') ? store.stringsProperty(path, BLUEZ_DEVICE_INTERFACE, 'UUIDs').map(canonicalUuid) : null return Object.freeze({ device: deviceIdentity(peerId, backendInstanceId, bluezAddress(store, path)), provenance: 'platform-derived', sourceTimestamp: absent, receivedAtMonotonicMs: monotonicTimestamp(observedAt), ingressOrdinal, scanSessionId, localName: localName === null ? absent : Object.freeze({ state: 'present', value: localName, provenance: 'observed' }), rssi: rssi === null ? absent : Object.freeze({ state: 'present', value: rssi, provenance: 'observed' }), txPower: txPower === null ? absent : Object.freeze({ state: 'present', value: txPower, provenance: 'observed' }), connectable: absent, appearance: absent, serviceUuids: uuids === null ? absent : Object.freeze({ state: 'present', value: Object.freeze(uuids), provenance: 'observed' }), solicitedServiceUuids: absent, overflowServiceUuids: absent, serviceData: absent, manufacturerData: absent, rawRecord: absent, scanResponseRecord: absent }) } export function matchesScan( options: OwnerScanOptions, observation: AdvertisementObservation ): boolean { return advertisementMatchesFilter(options.filter, observation) } function bluezAddress(store: BluezObjectStore, path: string) { const value = store.optionalStringProperty(path, BLUEZ_DEVICE_INTERFACE, 'Address') if (value === null) { return null } const addressType = store.optionalStringProperty(path, BLUEZ_DEVICE_INTERFACE, 'AddressType') if (addressType === 'public') { return Object.freeze({ value, type: 'public' }) } return Object.freeze({ value, type: 'random' }) } export function advertisementSize(observation: AdvertisementObservation): number { let size = 64 if (observation.localName.state === 'present') { size += observation.localName.value.length } if (observation.serviceUuids.state === 'present') { size += observation.serviceUuids.value.length * 36 } return size } export function createGattSnapshot( objects: readonly BluezManagedObject[], devicePath: string ): BluezGattSnapshotRecord { const services: BluezGattServiceRecord[] = [] const serviceUuidsByPath = new Map() for (const object of objects) { const service = findInterface(object.interfaces, BLUEZ_GATT_SERVICE_INTERFACE) if (service !== null && stringVariant(service, 'Device') === devicePath) { serviceUuidsByPath.set(object.path, canonicalUuid(stringVariant(service, 'UUID'))) } } for (const object of objects) { const service = findInterface(object.interfaces, BLUEZ_GATT_SERVICE_INTERFACE) const serviceUuid = serviceUuidsByPath.get(object.path) if (service === null || serviceUuid === undefined) { continue } const primary = booleanVariant(service, 'Primary') const includedObjectPaths = objectPathArrayVariant(service, 'Includes') const characteristics: BluezGattCharacteristicRecord[] = [] for (const characteristicObject of objects) { const characteristic = findInterface(characteristicObject.interfaces, BLUEZ_GATT_CHARACTERISTIC_INTERFACE) if (characteristic === null || stringVariant(characteristic, 'Service') !== object.path) { continue } const descriptors: BluezGattDescriptorRecord[] = [] for (const descriptorObject of objects) { const descriptor = findInterface(descriptorObject.interfaces, BLUEZ_GATT_DESCRIPTOR_INTERFACE) if (descriptor === null || stringVariant(descriptor, 'Characteristic') !== characteristicObject.path) { continue } descriptors.push( Object.freeze({ objectPath: descriptorObject.path, uuid: canonicalUuid(stringVariant(descriptor, 'UUID')) }) ) } characteristics.push( Object.freeze({ objectPath: characteristicObject.path, uuid: canonicalUuid(stringVariant(characteristic, 'UUID')), flags: Object.freeze(stringsVariant(characteristic, 'Flags')), descriptors: Object.freeze(descriptors.sort((left, right) => left.objectPath.localeCompare(right.objectPath))) }) ) } services.push( Object.freeze({ objectPath: object.path, uuid: serviceUuid, primary, // BlueZ may legitimately list an Includes entry whose object lives outside // this device's resolved snapshot. The public included-service link is // advisory {uuid, occurrence} metadata scoped to this database, so an // unresolvable entry degrades to omission instead of failing discovery. includedServices: Object.freeze( includedObjectPaths.flatMap(objectPath => { const uuid = serviceUuidsByPath.get(objectPath) return uuid === undefined ? [] : [{ objectPath, uuid }] }) ), characteristics: Object.freeze( characteristics.sort((left, right) => left.objectPath.localeCompare(right.objectPath)) ) }) ) } return Object.freeze({ services: Object.freeze(services.sort((left, right) => left.objectPath.localeCompare(right.objectPath))) }) } export async function captureCleanup( cleanup: Promise, resourceKind: string, operation: string ): Promise { try { return await cleanup } catch (error) { console.error(`[BluezBackendRuntime.${operation}] Cleanup rejected:`, error) return Object.freeze({ state: 'release-failed', failures: Object.freeze([cleanupFailure(resourceKind, operation)]) }) } } export function cleanupFailure(resourceKind: string, operation: string): CleanupFailure { return Object.freeze({ resourceKind, error: Object.freeze({ code: 'platform.failure', domain: 'cleanup', operation, platform: null, retryability: 'never' }) }) } function hasProperty(store: BluezObjectStore, path: string, interfaceName: string, property: string): boolean { return Object.prototype.hasOwnProperty.call(store.properties(path, interfaceName), property) } function findInterface( interfaces: readonly BluezManagedInterface[], interfaceName: string ): BluezManagedInterface | null { return interfaces.find(entry => entry.name === interfaceName) ?? null } function stringVariant(entry: BluezManagedInterface, property: string): string { const variant = entry.properties[property] if (variant === undefined || (variant.signature !== 's' && variant.signature !== 'o')) { throw contractError('protocol.malformed', 'gatt', `bluez.object-manager.${entry.name}.${property}`) } return variant.value } function stringsVariant(entry: BluezManagedInterface, property: string): readonly string[] { const variant = entry.properties[property] if (variant === undefined || variant.signature !== 'as') { throw contractError('protocol.malformed', 'gatt', `bluez.object-manager.${entry.name}.${property}`) } return [...variant.value] } function objectPathArrayVariant(entry: BluezManagedInterface, property: string): readonly string[] { const variant = entry.properties[property] if (variant === undefined) return Object.freeze([]) if (variant.signature !== 'ao') { throw contractError('protocol.malformed', 'gatt', `bluez.object-manager.${entry.name}.${property}`) } return [...variant.value] } function booleanVariant(entry: BluezManagedInterface, property: string): boolean { const variant = entry.properties[property] if (variant === undefined || variant.signature !== 'b') { throw contractError('protocol.malformed', 'gatt', `bluez.object-manager.${entry.name}.${property}`) } return variant.value }