Initial commit
This commit is contained in:
91
uni_modules/ak-sbsrv/utssdk/web/bluetooth_manager.uts
Normal file
91
uni_modules/ak-sbsrv/utssdk/web/bluetooth_manager.uts
Normal file
@@ -0,0 +1,91 @@
|
||||
// H5平台 Web Bluetooth 设备扫描实现
|
||||
import { DeviceManager } from './device-manager.uts';
|
||||
import { ServiceManager } from './service-manager.uts';
|
||||
import { BleDataProcessor } from '../data-processor.uts';
|
||||
import * as BleUtils from '../ble-utils.uts';
|
||||
import type { BleDevice, BleOptions, BleConnectOptionsExt, BleDataReceivedCallback, BleConnectionStateChangeCallback } from '../interface.uts'
|
||||
|
||||
export const BLE_SERVICE_PREFIXES = ['bae']; // 这里写你的实际前缀
|
||||
|
||||
// 实例化各个管理器
|
||||
const deviceManager = new DeviceManager();
|
||||
const serviceManager = new ServiceManager();
|
||||
const dataProcessor = BleDataProcessor.getInstance();
|
||||
|
||||
// 导出简化接口
|
||||
export const scanDevices = async (options?: { optionalServices?: string[] }) => deviceManager.startScan(options);
|
||||
export const connectDevice = async (deviceId: string, options?: BleConnectOptionsExt) => deviceManager.connectDevice(deviceId, options);
|
||||
export const disconnectDevice = async (deviceId: string) => deviceManager.disconnectDevice(deviceId);
|
||||
export const getConnectedDevices = () => deviceManager.getConnectedDevices();
|
||||
export const discoverServices = async (deviceId: string) => {
|
||||
// 获取 server 实例
|
||||
const server = deviceManager.servers[deviceId];
|
||||
if (!server) throw new Error('设备未连接');
|
||||
return serviceManager.discoverServices(deviceId, server);
|
||||
};
|
||||
export const getCharacteristics = async (deviceId: string, serviceId: string) => serviceManager.getCharacteristics(deviceId, serviceId);
|
||||
export const writeCharacteristic = async (deviceId: string, serviceId: string, characteristicId: string, data: string | ArrayBuffer) => serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, data);
|
||||
export const subscribeCharacteristic = async (deviceId: string, serviceId: string, characteristicId: string, callback) => serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, callback);
|
||||
export const readCharacteristic = async (deviceId: string, serviceId: string, characteristicId: string) => serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
|
||||
export const sendCommand = async (deviceId: string, serviceId: string, writeCharId: string, notifyCharId: string, command: string, params: any = null, timeout: number = 5000) => dataProcessor.sendAndReceive(deviceId, serviceId, writeCharId, notifyCharId, command, params, timeout);
|
||||
// Event adapter helpers: translate DeviceManager callbacks into payload objects
|
||||
export const onDeviceFound = (listener) => deviceManager.onDeviceFound((device) => {
|
||||
try { listener({ device }); } catch (e) { /* ignore listener errors */ }
|
||||
});
|
||||
|
||||
export const onScanFinished = (listener) => deviceManager.onScanFinished(() => {
|
||||
try { listener({}); } catch (e) {}
|
||||
});
|
||||
|
||||
export const onConnectionStateChange = (listener) => deviceManager.onConnectionStateChange((deviceId, state) => {
|
||||
try { listener({ device: { deviceId }, state }); } catch (e) {}
|
||||
});
|
||||
|
||||
/**
|
||||
* 自动连接并初始化蓝牙设备,获取可用serviceId、writeCharId、notifyCharId
|
||||
* @param deviceId 设备ID
|
||||
* @returns {Promise<{serviceId: string, writeCharId: string, notifyCharId: string}>}
|
||||
*/
|
||||
export const autoConnect = async (deviceId: string): Promise<{serviceId: string, writeCharId: string, notifyCharId: string}> => {
|
||||
// 1. 连接设备
|
||||
await connectDevice(deviceId);
|
||||
|
||||
// 2. 服务发现
|
||||
const services = await discoverServices(deviceId);
|
||||
if (!services || services.length === 0) throw new Error('未发现服务');
|
||||
|
||||
// 3. 获取私有serviceId(优先bae前缀或通过dataProcessor模板)
|
||||
let serviceId = '';
|
||||
for (const s of services) {
|
||||
if (s.uuid && BLE_SERVICE_PREFIXES.some(prefix => s.uuid.startsWith(prefix))) {
|
||||
serviceId = s.uuid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!serviceId) {
|
||||
// 可扩展:通过dataProcessor获取模板serviceId
|
||||
serviceId = services[0].uuid;
|
||||
}
|
||||
|
||||
// 4. 获取特征值
|
||||
const characteristics = await getCharacteristics(deviceId, serviceId);
|
||||
if (!characteristics || characteristics.length === 0) throw new Error('未发现特征值');
|
||||
|
||||
// 5. 找到write和notify特征
|
||||
let writeCharId = '';
|
||||
let notifyCharId = '';
|
||||
for (const c of characteristics) {
|
||||
if (!writeCharId && (c.properties.write || c.properties.writeWithoutResponse)) writeCharId = c.uuid;
|
||||
if (!notifyCharId && (c.properties.notify || c.properties.indicate)) notifyCharId = c.uuid;
|
||||
}
|
||||
if (!writeCharId || !notifyCharId) throw new Error('未找到可用的写/通知特征');
|
||||
|
||||
// 6. 注册notification
|
||||
await subscribeCharacteristic(deviceId, serviceId, notifyCharId, (data) => {
|
||||
// 可在此处分发/处理notification
|
||||
// console.log('Notification:', data);
|
||||
});
|
||||
|
||||
// 7. 返回结果
|
||||
return { serviceId, writeCharId, notifyCharId };
|
||||
};
|
||||
188
uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
Normal file
188
uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
Normal file
@@ -0,0 +1,188 @@
|
||||
// 设备管理相关:扫描、连接、断开、重连
|
||||
import { BleDevice, BLE_CONNECTION_STATE } from '../interface.uts';
|
||||
import type { BleConnectOptionsExt } from '../interface.uts';
|
||||
|
||||
export class DeviceManager {
|
||||
private devices = {};
|
||||
private servers = {};
|
||||
private connectionStates = {};
|
||||
private allowedServices = {};
|
||||
private reconnectAttempts: number = 0;
|
||||
private maxReconnectAttempts: number = 5;
|
||||
private reconnectDelay: number = 2000;
|
||||
private reconnectTimeoutId: number = 0;
|
||||
private autoReconnect: boolean = false;
|
||||
private connectionStateChangeListeners: Function[] = [];
|
||||
|
||||
private deviceFoundListeners: ((device: BleDevice) => void)[] = [];
|
||||
private scanFinishedListeners: (() => void)[] = [];
|
||||
|
||||
onDeviceFound(listener: (device: BleDevice) => void) {
|
||||
this.deviceFoundListeners.push(listener);
|
||||
}
|
||||
onScanFinished(listener: () => void) {
|
||||
this.scanFinishedListeners.push(listener);
|
||||
}
|
||||
private emitDeviceFound(device: BleDevice) {
|
||||
for (const listener of this.deviceFoundListeners) {
|
||||
try { listener(device); } catch (e) {}
|
||||
}
|
||||
}
|
||||
private emitScanFinished() {
|
||||
for (const listener of this.scanFinishedListeners) {
|
||||
try { listener(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
async startScan(options?: { optionalServices?: string[] } ): Promise<void> {
|
||||
if (!navigator.bluetooth) throw new Error('Web Bluetooth API not supported');
|
||||
try {
|
||||
const scanOptions: any = { acceptAllDevices: true };
|
||||
// allow callers to request optionalServices (required by Web Bluetooth to access custom services)
|
||||
if (options && Array.isArray(options.optionalServices) && options.optionalServices.length > 0) {
|
||||
scanOptions.optionalServices = options.optionalServices;
|
||||
}
|
||||
// Log the exact options passed to requestDevice for debugging optionalServices propagation
|
||||
try {
|
||||
console.log('[DeviceManager] requestDevice options:', JSON.stringify(scanOptions));
|
||||
} catch (e) {
|
||||
console.log('[DeviceManager] requestDevice options (raw):', scanOptions);
|
||||
}
|
||||
const device = await navigator.bluetooth.requestDevice(scanOptions);
|
||||
try {
|
||||
console.log('[DeviceManager] requestDevice result:', device);
|
||||
} catch (e) {
|
||||
console.log('[DeviceManager] requestDevice result (raw):', device);
|
||||
}
|
||||
if (device) {
|
||||
console.log(device)
|
||||
// 格式化 deviceId 为 MAC 地址格式
|
||||
const formatDeviceId = (id: string): string => {
|
||||
// 如果是12位16进制字符串(如 'AABBCCDDEEFF'),转为 'AA:BB:CC:DD:EE:FF'
|
||||
if (/^[0-9A-Fa-f]{12}$/.test(id)) {
|
||||
return id.match(/.{1,2}/g)!.join(":").toUpperCase();
|
||||
}
|
||||
// 如果是base64,无法直接转MAC,保留原样
|
||||
// 你可以根据实际情况扩展此处
|
||||
return id;
|
||||
};
|
||||
const isConnected = !!this.servers[device.id];
|
||||
const formattedId = formatDeviceId(device.id);
|
||||
const bleDevice = { deviceId: formattedId, name: device.name, connected: isConnected };
|
||||
this.devices[formattedId] = device;
|
||||
this.emitDeviceFound(bleDevice);
|
||||
}
|
||||
this.emitScanFinished();
|
||||
} catch (e) {
|
||||
this.emitScanFinished();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
onConnectionStateChange(listener: (deviceId: string, state: string) => void) {
|
||||
this.connectionStateChangeListeners.push(listener);
|
||||
}
|
||||
private emitConnectionStateChange(deviceId: string, state: string) {
|
||||
for (const listener of this.connectionStateChangeListeners) {
|
||||
try {
|
||||
listener(deviceId, state);
|
||||
} catch (e) {
|
||||
// 忽略单个回调异常
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<boolean> {
|
||||
this.autoReconnect = options?.autoReconnect ?? false;
|
||||
try {
|
||||
const device = this.devices[deviceId];
|
||||
if (!device) throw new Error('设备未找到');
|
||||
const server = await device.gatt.connect();
|
||||
this.servers[deviceId] = server;
|
||||
this.connectionStates[deviceId] = BLE_CONNECTION_STATE.CONNECTED;
|
||||
this.reconnectAttempts = 0;
|
||||
this.emitConnectionStateChange(deviceId, 'connected');
|
||||
// 监听物理断开
|
||||
if (device.gatt) {
|
||||
device.gatt.onconnectionstatechanged = null;
|
||||
device.gatt.onconnectionstatechanged = () => {
|
||||
if (!device.gatt.connected) {
|
||||
this.emitConnectionStateChange(deviceId, 'disconnected');
|
||||
}
|
||||
};
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
return this.scheduleReconnect(deviceId);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectDevice(deviceId: string): Promise<void> {
|
||||
const device = this.devices[deviceId];
|
||||
if (!device) throw new Error('设备未找到');
|
||||
try {
|
||||
if (device.gatt && device.gatt.connected) {
|
||||
device.gatt.disconnect();
|
||||
}
|
||||
delete this.servers[deviceId];
|
||||
this.connectionStates[deviceId] = BLE_CONNECTION_STATE.DISCONNECTED;
|
||||
this.emitConnectionStateChange(deviceId, 'disconnected');
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
getConnectedDevices(): BleDevice[] {
|
||||
const connectedDevices: BleDevice[] = [];
|
||||
for (const deviceId in this.servers) {
|
||||
const device = this.devices[deviceId];
|
||||
if (device) {
|
||||
connectedDevices.push({ deviceId: device.id, name: device.name || '未知设备', connected: true });
|
||||
}
|
||||
}
|
||||
return connectedDevices;
|
||||
}
|
||||
|
||||
handleDisconnect(deviceId: string) {
|
||||
this.connectionStates[deviceId] = BLE_CONNECTION_STATE.DISCONNECTED;
|
||||
this.emitConnectionStateChange(deviceId, 'disconnected');
|
||||
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(deviceId: string): Promise<boolean> {
|
||||
this.reconnectAttempts++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.reconnectTimeoutId = setTimeout(() => {
|
||||
this.connectDevice(deviceId, { autoReconnect: true })
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, this.reconnectDelay);
|
||||
});
|
||||
}
|
||||
|
||||
cancelReconnect() {
|
||||
if (this.reconnectTimeoutId) {
|
||||
clearTimeout(this.reconnectTimeoutId);
|
||||
this.reconnectTimeoutId = 0;
|
||||
}
|
||||
this.autoReconnect = false;
|
||||
this.reconnectAttempts = 0;
|
||||
}
|
||||
|
||||
setMaxReconnectAttempts(attempts: number) {
|
||||
this.maxReconnectAttempts = attempts;
|
||||
}
|
||||
|
||||
setReconnectDelay(delay: number) {
|
||||
this.reconnectDelay = delay;
|
||||
}
|
||||
|
||||
isDeviceConnected(deviceId: string): boolean {
|
||||
return !!this.servers[deviceId];
|
||||
}
|
||||
}
|
||||
136
uni_modules/ak-sbsrv/utssdk/web/dfu_manager.uts
Normal file
136
uni_modules/ak-sbsrv/utssdk/web/dfu_manager.uts
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as BluetoothManager from './bluetooth_manager.uts'
|
||||
|
||||
// 默认 Nordic DFU UUIDs (web 模式也可用,如设备使用自定义请传入 options)
|
||||
const DFU_SERVICE_UUID = '00001530-1212-EFDE-1523-785FEABCD123'
|
||||
const DFU_CONTROL_POINT_UUID = '00001531-1212-EFDE-1523-785FEABCD123'
|
||||
const DFU_PACKET_UUID = '00001532-1212-EFDE-1523-785FEABCD123'
|
||||
|
||||
export class WebDfuManager {
|
||||
// startDfu: deviceId, firmwareBytes (Uint8Array), options
|
||||
// options: { serviceId?, writeCharId?, notifyCharId?, chunkSize?, onProgress?, onLog?, useNordic?, controlParser?, controlTimeout? }
|
||||
async startDfu(deviceId: string, firmwareBytes: Uint8Array, options?: any): Promise<void> {
|
||||
options = options || {};
|
||||
// 1. ensure connected and discover services
|
||||
let svcInfo;
|
||||
if (options.serviceId && options.writeCharId && options.notifyCharId) {
|
||||
svcInfo = { serviceId: options.serviceId, writeCharId: options.writeCharId, notifyCharId: options.notifyCharId };
|
||||
} else {
|
||||
svcInfo = await BluetoothManager.autoConnect(deviceId);
|
||||
}
|
||||
const serviceId = svcInfo.serviceId;
|
||||
const writeCharId = svcInfo.writeCharId;
|
||||
const notifyCharId = svcInfo.notifyCharId;
|
||||
|
||||
const chunkSize = options.chunkSize ?? 20;
|
||||
|
||||
// control parser
|
||||
const controlParser = options.controlParser ?? (options.useNordic ? this._nordicControlParser.bind(this) : this._defaultControlParser.bind(this));
|
||||
|
||||
// subscribe notifications on control/notify char
|
||||
let finalizeSub;
|
||||
let resolved = false;
|
||||
const promise = new Promise<void>(async (resolve, reject) => {
|
||||
const cb = (payload) => {
|
||||
try {
|
||||
const data = payload.data instanceof Uint8Array ? payload.data : new Uint8Array(payload.data);
|
||||
options.onLog?.('control notify: ' + Array.from(data).join(','));
|
||||
const parsed = controlParser(data);
|
||||
if (!parsed) return;
|
||||
if (parsed.type === 'progress' && parsed.progress != null) {
|
||||
if (options.useNordic && svcInfo && svcInfo.totalBytes) {
|
||||
const percent = Math.floor((parsed.progress / svcInfo.totalBytes) * 100);
|
||||
options.onProgress?.(percent);
|
||||
} else {
|
||||
options.onProgress?.(parsed.progress);
|
||||
}
|
||||
} else if (parsed.type === 'success') {
|
||||
resolved = true;
|
||||
resolve();
|
||||
} else if (parsed.type === 'error') {
|
||||
reject(parsed.error ?? new Error('DFU device error'));
|
||||
}
|
||||
} catch (e) {
|
||||
options.onLog?.('control handler error: ' + e);
|
||||
}
|
||||
};
|
||||
await BluetoothManager.subscribeCharacteristic(deviceId, serviceId, notifyCharId, cb);
|
||||
finalizeSub = async () => { try { await BluetoothManager.subscribeCharacteristic(deviceId, serviceId, notifyCharId, () => {}); } catch(e){} };
|
||||
// write firmware in chunks
|
||||
try {
|
||||
let offset = 0;
|
||||
const total = firmwareBytes.length;
|
||||
// attach totalBytes for nordic if needed
|
||||
svcInfo.totalBytes = total;
|
||||
while (offset < total) {
|
||||
const end = Math.min(offset + chunkSize, total);
|
||||
const slice = firmwareBytes.subarray(offset, end);
|
||||
// writeValue accepts ArrayBuffer
|
||||
await BluetoothManager.writeCharacteristic(deviceId, serviceId, writeCharId, slice.buffer);
|
||||
offset = end;
|
||||
// optimistic progress
|
||||
options.onProgress?.(Math.floor((offset / total) * 100));
|
||||
await this._sleep(options.chunkDelay ?? 6);
|
||||
}
|
||||
|
||||
// send validate/activate command to control point (placeholder)
|
||||
try {
|
||||
await BluetoothManager.writeCharacteristic(deviceId, serviceId, writeCharId, new Uint8Array([0x04]).buffer);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// wait for control success or timeout
|
||||
const timeoutMs = options.controlTimeout ?? 20000;
|
||||
const t = setTimeout(() => {
|
||||
if (!resolved) reject(new Error('DFU control timeout'));
|
||||
}, timeoutMs);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await promise;
|
||||
} finally {
|
||||
// unsubscribe notifications
|
||||
try { await BluetoothManager.unsubscribeCharacteristic(deviceId, serviceId, notifyCharId); } catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
_sleep(ms: number) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
_defaultControlParser(data: Uint8Array) {
|
||||
if (!data || data.length === 0) return null;
|
||||
if (data.length >= 2) {
|
||||
const maybeProgress = data[1];
|
||||
if (maybeProgress >= 0 && maybeProgress <= 100) return { type: 'progress', progress: maybeProgress };
|
||||
}
|
||||
const op = data[0];
|
||||
if (op === 0x01) return { type: 'success' };
|
||||
if (op === 0xFF) return { type: 'error', error: data };
|
||||
return { type: 'info' };
|
||||
}
|
||||
|
||||
_nordicControlParser(data: Uint8Array) {
|
||||
if (!data || data.length === 0) return null;
|
||||
const op = data[0];
|
||||
// 0x11 = Packet Receipt Notification
|
||||
if (op === 0x11 && data.length >= 3) {
|
||||
const lsb = data[1];
|
||||
const msb = data[2];
|
||||
const received = (msb << 8) | lsb;
|
||||
return { type: 'progress', progress: received };
|
||||
}
|
||||
// 0x10 = Response
|
||||
if (op === 0x10 && data.length >= 3) {
|
||||
const resultCode = data[2];
|
||||
if (resultCode === 0x01) return { type: 'success' };
|
||||
return { type: 'error', error: { resultCode } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const dfuManager = new WebDfuManager();
|
||||
46
uni_modules/ak-sbsrv/utssdk/web/index.uts
Normal file
46
uni_modules/ak-sbsrv/utssdk/web/index.uts
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as BluetoothManager from './bluetooth_manager.uts';
|
||||
|
||||
export const bluetoothService = {
|
||||
scanDevices: BluetoothManager.scanDevices,
|
||||
connectDevice: BluetoothManager.connectDevice,
|
||||
disconnectDevice: BluetoothManager.disconnectDevice,
|
||||
getConnectedDevices: BluetoothManager.getConnectedDevices,
|
||||
discoverServices: BluetoothManager.discoverServices,
|
||||
// compatibility aliases used by app code
|
||||
getServices: BluetoothManager.discoverServices,
|
||||
getCharacteristics: BluetoothManager.getCharacteristics,
|
||||
readCharacteristic: BluetoothManager.readCharacteristic,
|
||||
writeCharacteristic: BluetoothManager.writeCharacteristic,
|
||||
subscribeCharacteristic: BluetoothManager.subscribeCharacteristic,
|
||||
unsubscribeCharacteristic: BluetoothManager.unsubscribeCharacteristic,
|
||||
sendCommand: BluetoothManager.sendCommand,
|
||||
onConnectionStateChange: BluetoothManager.onConnectionStateChange,
|
||||
// 兼容旧接口,如有 readCharacteristic 可补充
|
||||
};
|
||||
|
||||
// Provide a minimal EventEmitter-style `.on(eventName, handler)` to match app code
|
||||
// Supported events: 'deviceFound', 'scanFinished', 'connectionStateChanged'
|
||||
bluetoothService.on = function(eventName: string, handler: Function) {
|
||||
if (!eventName || typeof handler !== 'function') return;
|
||||
switch (eventName) {
|
||||
case 'deviceFound':
|
||||
return BluetoothManager.onDeviceFound(handler);
|
||||
case 'scanFinished':
|
||||
return BluetoothManager.onScanFinished(handler);
|
||||
case 'connectionStateChanged':
|
||||
return BluetoothManager.onConnectionStateChange(handler);
|
||||
default:
|
||||
// no-op for unsupported events
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Backwards-compat: getAutoBleInterfaces expected by pages -> maps to autoConnect
|
||||
if (!bluetoothService.getAutoBleInterfaces) {
|
||||
bluetoothService.getAutoBleInterfaces = function(deviceId: string) {
|
||||
return BluetoothManager.autoConnect(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
import { dfuManager as webDfuManager } from './dfu_manager.uts'
|
||||
export const dfuManager = webDfuManager;
|
||||
186
uni_modules/ak-sbsrv/utssdk/web/service-manager.uts
Normal file
186
uni_modules/ak-sbsrv/utssdk/web/service-manager.uts
Normal file
@@ -0,0 +1,186 @@
|
||||
// 服务与特征值操作相关:服务发现、特征值读写、订阅
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user