Initial commit of akmon project
This commit is contained in:
356
uni_modules/ak-sbsrv/utssdk/web/service-manager.uts
Normal file
356
uni_modules/ak-sbsrv/utssdk/web/service-manager.uts
Normal file
@@ -0,0 +1,356 @@
|
||||
// 服务与特征值操作相关:服务发现、特征值读写、订阅
|
||||
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<any | null> {
|
||||
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<any> {
|
||||
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<BleService[]> {
|
||||
// 获取设备的 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<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;
|
||||
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<void> {
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user