Files
akbleserver/uni_modules/ak-sbsrv/utssdk/web/service-manager.uts
2026-03-16 10:37:46 +08:00

186 lines
8.5 KiB
Plaintext

// 服务与特征值操作相关:服务发现、特征值读写、订阅
import { BleService, BleCharacteristic } from '../interface.uts';
import { BLE_SERVICE_PREFIXES } from './bluetooth_manager.uts';
// 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<BleService[]> {
// 获取设备的 GATT 服务器
console.log(deviceId)
// 由外部传入 server
if (!server) throw new Error('设备未连接');
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;
// Typical case: server is a BluetoothRemoteGATTServer and has getPrimaryServices
if (server && typeof server.getPrimaryServices === 'function') {
console.log('server.getPrimaryServices')
services = await server.getPrimaryServices();
}
if (server && server.gatt && typeof server.gatt.getPrimaryServices === 'function') {
// sometimes a BluetoothDevice object is passed instead of the server
console.log('server.gatt.getPrimaryServices')
services = await server.gatt.getPrimaryServices();
}
if (server && server.device && server.device.gatt && typeof server.device.gatt.getPrimaryServices === 'function') {
console.log('server.device.gatt.getPrimaryServices')
services = await server.device.gatt.getPrimaryServices();
} else {
console.log('other getPrimaryServices')
// Last resort: if server is a wrapper with a connect method, try to ensure connected
if (server && typeof server.connect === 'function') {
console.log('[ServiceManager] attempting to connect via server.connect()')
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<BleCharacteristic[]> {
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<void> {
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<void> {
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;
await characteristic.startNotifications();
const listener = (event) => {
const value = event.target.value;
const data = new Uint8Array(value.buffer);
const cb = this.characteristicCallbacks[deviceId][serviceId][characteristicId];
if (cb) {
cb({ deviceId, serviceId, characteristicId, data });
}
};
// 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);
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
const entry = this.characteristicListeners[deviceId]?.[serviceId]?.[characteristicId];
if (!entry) return;
try {
const { characteristic, listener } = entry;
characteristic.removeEventListener('characteristicvaluechanged', listener);
await characteristic.stopNotifications();
} catch (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<ArrayBuffer> {
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;
}
}
}