Initial commit of akmon project
This commit is contained in:
139
uni_modules/ak-sbsrv/utssdk/web/bluetooth_manager.uts
Normal file
139
uni_modules/ak-sbsrv/utssdk/web/bluetooth_manager.uts
Normal file
@@ -0,0 +1,139 @@
|
||||
// H5平台 Web Bluetooth 设备扫描实现
|
||||
import { DeviceManager } from './device-manager.uts';
|
||||
import { ServiceManager } from './service-manager.uts';
|
||||
import type { BleDevice, BleOptions, BleConnectOptionsExt, BleDataReceivedCallback, BleConnectionStateChangeCallback } from '../interface.uts'
|
||||
|
||||
const DEFAULT_OPTIONAL_SERVICES = [
|
||||
'00001800-0000-1000-8000-00805f9b34fb', // GAP
|
||||
'0000180a-0000-1000-8000-00805f9b34fb', // Device Information
|
||||
'0000180f-0000-1000-8000-00805f9b34fb', // Battery
|
||||
'00001812-0000-1000-8000-00805f9b34fb', // Human Interface Device
|
||||
'0000fe59-0000-1000-8000-00805f9b34fb', // Nordic DFU / vendor specific
|
||||
'6e400001-b5a3-f393-e0a9-e50e24dcca9e', // Nordic UART primary service
|
||||
'6e400010-b5a3-f393-e0a9-e50e24dcca9e', // Custom health service (observed on Android)
|
||||
'6e400020-b5a3-f393-e0a9-e50e24dcca9e' // Additional vendor service
|
||||
];
|
||||
|
||||
export const BLE_SERVICE_PREFIXES = [
|
||||
'6e4000',
|
||||
'0000180f',
|
||||
'00001812',
|
||||
'0000fe59'
|
||||
];
|
||||
|
||||
function normalizeServiceUuid(uuid: string): string {
|
||||
if (!uuid) return uuid;
|
||||
let u = uuid.trim().toLowerCase();
|
||||
if (u.startsWith('0x')) {
|
||||
u = u.slice(2);
|
||||
}
|
||||
if (/^[0-9a-f]{4}$/.test(u)) {
|
||||
return `0000${u}-0000-1000-8000-00805f9b34fb`;
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
function mergeOptionalServices(userServices?: string[]): string[] {
|
||||
const set = new Set<string>();
|
||||
DEFAULT_OPTIONAL_SERVICES.forEach((svc) => set.add(normalizeServiceUuid(svc)));
|
||||
if (userServices != null) {
|
||||
for (let i = 0; i < userServices.length; i++) {
|
||||
const normalized = normalizeServiceUuid(userServices[i]);
|
||||
if (normalized != null && normalized !== '') {
|
||||
set.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(set);
|
||||
}
|
||||
|
||||
// 实例化各个管理器
|
||||
const deviceManager = new DeviceManager();
|
||||
const serviceManager = new ServiceManager();
|
||||
|
||||
// 导出简化接口
|
||||
export const scanDevices = async (options?: { optionalServices?: string[] }) => {
|
||||
const mergedOptions = options != null ? { ...options } : {};
|
||||
mergedOptions.optionalServices = mergeOptionalServices(options?.optionalServices ?? []);
|
||||
return deviceManager.startScan(mergedOptions);
|
||||
};
|
||||
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 getKnownDevices = () => Object.keys((deviceManager as any).devices || {})
|
||||
export const discoverServices = async (deviceId: string) => {
|
||||
// 获取 server 实例
|
||||
const server = deviceManager.getServer(deviceId)
|
||||
if (!server) throw new Error(`设备未连接: ${deviceId}`)
|
||||
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) => {
|
||||
console.log('[bluetooth_manager] subscribeCharacteristic called:', deviceId, serviceId, characteristicId)
|
||||
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, callback);
|
||||
}
|
||||
export const unsubscribeCharacteristic = async (deviceId: string, serviceId: string, characteristicId: string) => serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
|
||||
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 };
|
||||
};
|
||||
237
uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
Normal file
237
uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
Normal file
@@ -0,0 +1,237 @@
|
||||
// 设备管理相关:扫描、连接、断开、重连
|
||||
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 key = this.resolveDeviceKey(deviceId)
|
||||
console.log(key,deviceId)
|
||||
if (!key) {
|
||||
// better debugging: include a short sample of known device keys
|
||||
const known = Object.keys(this.devices || {}).slice(0, 20)
|
||||
throw new Error(`设备未找到: ${deviceId}; 已知设备: ${known.join(',')}`)
|
||||
}
|
||||
const device = this.devices[key]
|
||||
const server = await device.gatt.connect();
|
||||
this.servers[key] = server;
|
||||
this.connectionStates[key] = BLE_CONNECTION_STATE.CONNECTED;
|
||||
this.reconnectAttempts = 0;
|
||||
this.emitConnectionStateChange(key, 'connected');
|
||||
// 监听物理断开
|
||||
if (device.gatt) {
|
||||
device.gatt.onconnectionstatechanged = null;
|
||||
device.gatt.onconnectionstatechanged = () => {
|
||||
if (!device.gatt.connected) {
|
||||
this.emitConnectionStateChange(key, '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 key = this.resolveDeviceKey(deviceId)
|
||||
if (!key) throw new Error('设备未找到')
|
||||
const device = this.devices[key]
|
||||
try {
|
||||
if (device.gatt && device.gatt.connected) {
|
||||
device.gatt.disconnect();
|
||||
}
|
||||
delete this.servers[key];
|
||||
this.connectionStates[key] = BLE_CONNECTION_STATE.DISCONNECTED;
|
||||
this.emitConnectionStateChange(key, '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) {
|
||||
const key = this.resolveDeviceKey(deviceId)
|
||||
const idKey = key ?? deviceId
|
||||
this.connectionStates[idKey] = BLE_CONNECTION_STATE.DISCONNECTED;
|
||||
this.emitConnectionStateChange(idKey, 'disconnected');
|
||||
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect(idKey);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve a device key used in this.devices map from a given deviceId.
|
||||
// Accepts formatted IDs (with ':'), raw ids or case-insensitive hex strings.
|
||||
private resolveDeviceKey(deviceId: string): string | null {
|
||||
// Accept either a string id or an object that contains the id (UTSJSONObject or plain object)
|
||||
if (deviceId == null) return null
|
||||
let idCandidate: any = deviceId
|
||||
if (typeof deviceId !== 'string') {
|
||||
try {
|
||||
// UTSJSONObject has getString
|
||||
if (typeof (deviceId as any).getString === 'function') {
|
||||
const got = (deviceId as any).getString('deviceId') || (deviceId as any).getString('device_id') || (deviceId as any).getString('id')
|
||||
if (got) idCandidate = got
|
||||
} else if (typeof deviceId === 'object') {
|
||||
const got = (deviceId as any).deviceId || (deviceId as any).device_id || (deviceId as any).id
|
||||
if (got) idCandidate = got
|
||||
}
|
||||
} catch (e) { /* ignore extraction errors */ }
|
||||
}
|
||||
if (!idCandidate) return null
|
||||
if (this.devices[idCandidate]) return idCandidate
|
||||
const normalize = (s: string) => (s || '').toString().replace(/:/g, '').toUpperCase()
|
||||
const target = normalize(idCandidate)
|
||||
for (const k in this.devices) {
|
||||
if (k === deviceId) return k
|
||||
const dev = this.devices[k]
|
||||
try {
|
||||
if (dev && dev.id && normalize(dev.id) === target) return k
|
||||
} catch (e) { }
|
||||
if (normalize(k) === target) return k
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// Public helper to obtain the GATT server for a device id using flexible matching
|
||||
getServer(deviceId: string): any | null {
|
||||
const key = this.resolveDeviceKey(deviceId)
|
||||
if (!key) return null
|
||||
return this.servers[key] ?? null
|
||||
}
|
||||
}
|
||||
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;
|
||||
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