// 服务与特征值操作相关:服务发现、特征值读写、订阅 import { BleService, BleCharacteristic } from '../interface.uts'; import { BLE_SERVICE_PREFIXES } from './bluetooth_manager.uts'; function isBaeService(uuid: string): boolean { if (!uuid) return false; const lower = uuid.toLowerCase(); for (let i = 0; i < BLE_SERVICE_PREFIXES.length; i++) { const prefix = BLE_SERVICE_PREFIXES[i]; if (prefix && lower.startsWith(prefix.toLowerCase())) { return true; } } return false; } function isBleService(uuid: string, prefixes: string[]): boolean { if (!uuid) return false; if (!prefixes || prefixes.length === 0) return false; const lower = uuid.toLowerCase(); for (let i = 0; i < prefixes.length; i++) { const prefix = prefixes[i]; if (!prefix) continue; const prefixLower = prefix.toLowerCase(); if (lower.startsWith(prefixLower)) return true; if (prefixLower.length === 4) { const expanded = `0000${prefixLower}-0000-1000-8000-00805f9b34fb`; if (lower === expanded) return true; } } return false; } function getPrimaryServerCandidate(server: any): any { if (!server) return null; if (typeof server.getPrimaryServices === 'function') return server; if (server.gatt && typeof server.gatt.getPrimaryServices === 'function') return server.gatt; if (server.device && server.device.gatt && typeof server.device.gatt.getPrimaryServices === 'function') { return server.device.gatt; } return server; } function isGattConnected(server: any): boolean { if (!server) return false; if (typeof server.connected === 'boolean') return server.connected; if (server.device && server.device.gatt && typeof server.device.gatt.connected === 'boolean') { return server.device.gatt.connected; } return true; } async function attemptGattConnect(source: any): Promise { if (!source) return null; try { if (typeof source.connect === 'function') { const result = await source.connect(); if (result != null) return result; } } catch (e) { console.warn('[ServiceManager] connect() failed', e); } const deviceGatt = source?.device?.gatt; if (deviceGatt && typeof deviceGatt.connect === 'function') { try { const result = await deviceGatt.connect(); if (result != null) return result; return deviceGatt; } catch (e) { console.warn('[ServiceManager] device.gatt.connect() failed', e); } } const nestedGatt = source?.gatt; if (nestedGatt && typeof nestedGatt.connect === 'function') { try { const result = await nestedGatt.connect(); if (result != null) return result; return nestedGatt; } catch (e) { console.warn('[ServiceManager] nested gatt.connect() failed', e); } } return null; } async function ensureGattServer(server: any, forceReconnect: boolean = false): Promise { let candidate = getPrimaryServerCandidate(server); if (!forceReconnect && isGattConnected(candidate)) { return candidate; } console.log('[ServiceManager] ensureGattServer attempting reconnect'); const connected = await attemptGattConnect(candidate ?? server); if (connected != null) { candidate = getPrimaryServerCandidate(connected); } if (!isGattConnected(candidate) && server && server !== candidate) { const fallback = await attemptGattConnect(server); if (fallback != null) { candidate = getPrimaryServerCandidate(fallback); } } return candidate; } function isDisconnectError(err: any): boolean { if (!err) return false; const message = typeof err.message === 'string' ? err.message.toLowerCase() : ''; return err.name === 'NetworkError' || message.includes('disconnected') || message.includes('connect first'); } // Helper: normalize UUIDs (accept 16-bit like '180F' and expand to full 128-bit) function normalizeUuid(uuid: string): string { if (!uuid) return uuid; const u = uuid.toLowerCase(); // already full form if (u.length === 36 && u.indexOf('-') > 0) return u; // allow forms like '180f' or '0x180f' const hex = u.replace(/^0x/, '').replace(/[^0-9a-f]/g, ''); if (/^[0-9a-f]{4}$/.test(hex)) { return `0000${hex}-0000-1000-8000-00805f9b34fb`; } return uuid; } export class ServiceManager { private services = {}; private characteristics = {}; private characteristicCallbacks = {}; private characteristicListeners = {}; constructor() { } async discoverServices(deviceId: string, server: any): Promise { // 获取设备的 GATT 服务器 console.log(deviceId) // 由外部传入 server if (!server) throw new Error('设备未连接'); try { // Some browsers report a stale server with connected=false; attempt to reconnect const needsReconnect = (server.connected === false) || (server.device && server.device.gatt && server.device.gatt.connected === false); if (needsReconnect) { console.log('[ServiceManager] server disconnected, attempting reconnect'); if (typeof server.connect === 'function') { try { await server.connect(); } catch (connectErr) { console.warn('[ServiceManager] server.connect() failed', connectErr); } } if (server.device && server.device.gatt && typeof server.device.gatt.connect === 'function' && !server.device.gatt.connected) { try { await server.device.gatt.connect(); } catch (gattErr) { console.warn('[ServiceManager] server.device.gatt.connect() failed', gattErr); } } } } catch (reconnectError) { console.warn('[ServiceManager] reconnect attempt encountered error', reconnectError); } const bleServices: BleService[] = []; if (!this.services[deviceId]) this.services[deviceId] = {}; try { console.log('[ServiceManager] discoverServices called for', deviceId) console.log('[ServiceManager] server param:', server) let services = null; let primaryServer = await ensureGattServer(server); if (primaryServer && typeof primaryServer.getPrimaryServices === 'function') { console.log('[ServiceManager]server.getPrimaryServices') try { services = await primaryServer.getPrimaryServices(); console.log('[ServiceManager] got services from primaryServer', services) } catch (primaryError) { if (isDisconnectError(primaryError)) { console.log('[ServiceManager] primary getPrimaryServices failed, retrying after reconnect'); primaryServer = await ensureGattServer(server, true); if (primaryServer && typeof primaryServer.getPrimaryServices === 'function') { services = await primaryServer.getPrimaryServices(); } else { throw primaryError; } } else { throw primaryError; } } } if (!services && server && server.device && server.device.gatt) { const fallbackServer = await ensureGattServer(server.device.gatt, true); if (fallbackServer && typeof fallbackServer.getPrimaryServices === 'function') { console.log('server.device.gatt.getPrimaryServices (fallback)') services = await fallbackServer.getPrimaryServices(); } } if (!services && server && typeof server.connect === 'function') { console.log('other getPrimaryServices') try { const s = await server.connect(); if (s && typeof s.getPrimaryServices === 'function') { services = await s.getPrimaryServices(); } } catch (e) { console.warn('[ServiceManager] server.connect() failed', e) } } console.log('[ServiceManager] services resolved:', services) if (!services) throw new Error('无法解析 GATT services 对象 —— server 参数不包含 getPrimaryServices'); for (let i = 0; i < services.length; i++) { const service = services[i]; const rawUuid = service.uuid; const uuid = normalizeUuid(rawUuid); bleServices.push({ uuid, isPrimary: true }); this.services[deviceId][uuid] = service; // ensure service UUID detection supports standard BLE services like Battery (0x180F) const lower = uuid.toLowerCase(); const isBattery = lower === '0000180f-0000-1000-8000-00805f9b34fb'; if (isBattery || isBaeService(uuid) || isBleService(uuid, BLE_SERVICE_PREFIXES)) { await this.getCharacteristics(deviceId, uuid); } } return bleServices; } catch (err) { console.error('[ServiceManager] discoverServices error:', err) throw err; } } async getCharacteristics(deviceId: string, serviceId: string): Promise { const service = this.services[deviceId]?.[serviceId]; if (!service) throw new Error('服务未找到'); const characteristics = await service.getCharacteristics(); console.log(characteristics) const bleCharacteristics: BleCharacteristic[] = []; if (!this.characteristics[deviceId]) this.characteristics[deviceId] = {}; if (!this.characteristics[deviceId][serviceId]) this.characteristics[deviceId][serviceId] = {}; for (const characteristic of characteristics) { const properties = { read: characteristic.properties.read || false, write: characteristic.properties.write || characteristic.properties.writableAuxiliaries || characteristic.properties.reliableWrite || characteristic.properties.writeWithoutResponse || false, notify: characteristic.properties.notify || false, indicate: characteristic.properties.indicate || false }; console.log(characteristic.properties) console.log(properties) // Construct a BleCharacteristic-shaped object including the required `service` property const bleCharObj = { uuid: characteristic.uuid, service: { uuid: serviceId, isPrimary: true }, properties }; bleCharacteristics.push(bleCharObj); // keep native characteristic reference for read/write/notify operations this.characteristics[deviceId][serviceId][characteristic.uuid] = characteristic; } return bleCharacteristics; } async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: string | ArrayBuffer): Promise { const characteristic = this.characteristics[deviceId]?.[serviceId]?.[characteristicId]; if (!characteristic) throw new Error('特征值未找到'); let buffer; if (typeof data === 'string') { buffer = new TextEncoder().encode(data).buffer; } else { buffer = data; } await characteristic.writeValue(buffer); } async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback): Promise { const characteristic = this.characteristics[deviceId]?.[serviceId]?.[characteristicId]; if (!characteristic) throw new Error('特征值未找到'); if (!characteristic.properties.notify && !characteristic.properties.indicate) { throw new Error('特征值不支持通知'); } if (!this.characteristicCallbacks[deviceId]) this.characteristicCallbacks[deviceId] = {}; if (!this.characteristicCallbacks[deviceId][serviceId]) this.characteristicCallbacks[deviceId][serviceId] = {}; this.characteristicCallbacks[deviceId][serviceId][characteristicId] = callback; try { await characteristic.startNotifications(); } catch (e) { console.error('[ServiceManager] startNotifications failed for', deviceId, serviceId, characteristicId, e); throw e; } const listener = (event) => { const value = event.target.value; let data: Uint8Array; if (value && typeof value.byteOffset === 'number' && typeof value.byteLength === 'number') { data = new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)); } else if (value instanceof ArrayBuffer) { data = new Uint8Array(value.slice(0)); } else { data = new Uint8Array(0); } const cb = this.characteristicCallbacks[deviceId][serviceId][characteristicId]; if (cb) { try { cb(data); } catch (err) { console.warn('[ServiceManager] characteristic notify callback error', err); } } }; // store listener so it can be removed later if (!this.characteristicListeners[deviceId]) this.characteristicListeners[deviceId] = {}; if (!this.characteristicListeners[deviceId][serviceId]) this.characteristicListeners[deviceId][serviceId] = {}; this.characteristicListeners[deviceId][serviceId][characteristicId] = { characteristic, listener }; characteristic.addEventListener('characteristicvaluechanged', listener); console.log('[ServiceManager] subscribeCharacteristic ok:', deviceId, serviceId, characteristicId); } async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise { const entry = this.characteristicListeners[deviceId]?.[serviceId]?.[characteristicId]; if (!entry) return; try { const { characteristic, listener } = entry; characteristic.removeEventListener('characteristicvaluechanged', listener); const gattCandidate = characteristic?.service?.device?.gatt || characteristic?.service?.gatt || characteristic?.service; const shouldStop = !gattCandidate || isGattConnected(gattCandidate); if (shouldStop && typeof characteristic.stopNotifications === 'function') { try { await characteristic.stopNotifications(); } catch (stopError) { if (!isDisconnectError(stopError)) { throw stopError; } console.log('[ServiceManager] stopNotifications ignored disconnect:', deviceId, serviceId, characteristicId); } } console.log('[ServiceManager] unsubscribeCharacteristic ok:', deviceId, serviceId, characteristicId); } catch (e) { console.warn('[ServiceManager] unsubscribeCharacteristic failed for', deviceId, serviceId, characteristicId, e); // ignore } // cleanup delete this.characteristicListeners[deviceId][serviceId][characteristicId]; delete this.characteristicCallbacks[deviceId][serviceId][characteristicId]; } // Read a characteristic value and return ArrayBuffer async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise { const characteristic = this.characteristics[deviceId]?.[serviceId]?.[characteristicId]; if (!characteristic) throw new Error('特征值未找到'); // Web Bluetooth returns a DataView from readValue() const value = await characteristic.readValue(); if (!value) return new ArrayBuffer(0); // DataView.buffer is a shared ArrayBuffer; return a copy slice to be safe try { return value.buffer ? value.buffer.slice(0) : new Uint8Array(value).buffer; } catch (e) { // fallback const arr = new Uint8Array(value); return arr.buffer; } } }