Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
{
"id": "ak-sbsrv",
"displayName": "多蓝牙设备连接管理插件",
"version": "1.0.0",
"description": "支持多蓝牙设备连接管理的插件,可扫描、连接、发送数据等",
"keywords": [
"蓝牙",
"设备管理",
"BLE",
"多设备",
"连接管理"
],
"repository": "",
"engines": {
"HBuilderX": "^4.0.0",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"type": "uts-plugin",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "设备蓝牙"
},
"npmurl": "",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,115 @@
# ak-sbsrv
## 介绍
ak-sbsrv 是一个多蓝牙设备连接管理插件,基于 uni-app-x UTS 开发,支持同时连接和管理多个蓝牙设备。
主要特点:
- 支持多蓝牙设备的扫描、连接和通信
- 支持多种蓝牙协议BLE、SLE、BR/EDR等
- 支持多种数据格式JSON、XML、RAW等
- 统一接口多平台支持目前已支持H5平台
## 平台支持
- H5 (Chrome、Edge、Safari等支持Web Bluetooth API的现代浏览器)
- Android (开发中)
- iOS (开发中)
> H5端需要在支持Web Bluetooth API的浏览器及安全上下文HTTPS或localhost中使用
## 安装使用
1. 在插件市场下载或通过HBuilderX导入本插件
2. 导入到项目中
## API列表
### 基础功能
#### 扫描设备
```js
import { bluetoothService } from '@/uni_modules/ak-sbsrv/utssdk/web/index.uts';
// 扫描设备
const result = await bluetoothService.scanDevices();
console.log('扫描到设备:', result.devices);
```
#### 连接设备
```js
// 连接设备
await bluetoothService.connectDevice(deviceId);
```
#### 断开连接
```js
// 断开连接
await bluetoothService.disconnectDevice(deviceId);
```
#### 发送数据
```js
// 发送数据
await bluetoothService.sendData({
deviceId: '设备ID',
serviceId: '服务UUID',
characteristicId: '特征值UUID',
data: '要发送的数据',
format: 2 // 2代表RAW格式
});
```
### 事件监听
#### 监听连接状态变化
```js
// 监听连接状态变化
bluetoothService.onConnectionStateChange((deviceId, state) => {
console.log(`设备 ${deviceId} 连接状态变为: ${state}`);
// state: 0-断开1-连接中2-已连接3-断开中
});
```
#### 监听数据接收
```js
// 监听数据接收
bluetoothService.onDataReceived((payload) => {
console.log('收到数据:', payload);
});
```
#### 监听错误
```js
// 监听错误
bluetoothService.onError((error) => {
console.error('蓝牙错误:', error);
});
```
### 其他API
- `getConnectedDevices()` - 获取已连接设备列表
- `getConnectionState(deviceId)` - 获取指定设备的连接状态
- `listenCharacteristicNotify(deviceId, serviceId, characteristicId)` - 监听特征值通知
## 示例项目
参见仓库中的 [control.uvue](pages/control.uvue) 页面了解完整的使用示例。
## 常见问题
1. **H5中无法扫描到设备**
请确保浏览器支持Web Bluetooth API且页面在HTTPS或localhost环境下运行。
2. **扫描后无法连接设备?**
请确保设备在可连接范围内,且蓝牙服务已打开。
3. **发送数据失败?**
请检查serviceId和characteristicId是否正确以及特征值是否支持写入。
## 更新日志
### 1.0.0 (2025-04-24)
- 支持Web平台的蓝牙设备扫描、连接和数据收发
- 支持多设备同时连接管理
- 实现事件监听机制

View File

@@ -0,0 +1,283 @@
import type {
BleDevice,
BleConnectionState,
BleEvent,
BleEventCallback,
BleEventPayload,
BleScanResult,
BleConnectOptionsExt,
AutoBleInterfaces,
BleDataPayload,
SendDataPayload,
BleOptions,
MultiProtocolDevice,
ScanHandler,
BleProtocolType,
ScanDevicesOptions
} from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
// Shape used when callers register plain objects as handlers. Using a named
// type keeps member access explicit so the code generator emits valid Kotlin
// member references instead of trying to access properties on Any.
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
// 设备上下文
class DeviceContext {
device : BleDevice;
protocol : BleProtocolType;
state : BleConnectionState;
handler : ProtocolHandler;
constructor(device : BleDevice, protocol : BleProtocolType, handler : ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0; // DISCONNECTED
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>(); // key: deviceId|protocol
// Single active protocol handler (no multi-protocol registration)
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
// 事件监听注册表
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
let defaultBluetoothService: BluetoothService | null = null;
function emit(event : BleEvent, payload : BleEventPayload) {
if (event == 'connectionStateChanged') {
console.log('[AKBLE][LOG] bluetooth_manager.uts emit connectionStateChanged', payload)
}
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try { cb(payload); } catch (e) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler, bluetoothService?: BluetoothService) {
super(bluetoothService);
this._raw = raw ?? null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices == 'function') {
await rawTyped.scanDevices(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect == 'function') {
await rawTyped.connect(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect == 'function') {
await rawTyped.disconnect(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData == 'function') {
await rawTyped.sendData(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect == 'function') {
return await rawTyped.autoConnect(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' };
}
}
// Strong runtime detector for plain object handlers (no Type Predicate)
// Note: the UTS bundler doesn't support TypeScript type predicates (x is T),
// and it doesn't accept the 'unknown' type. This returns a boolean and
// callers must cast the value to RawProtocolHandler after the function
// returns true.
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object') return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] == 'function') return true;
if (typeof r['connect'] == 'function') return true;
if (typeof r['disconnect'] == 'function') return true;
if (typeof r['sendData'] == 'function') return true;
if (typeof r['autoConnect'] == 'function') return true;
if (typeof r['protocol'] == 'string') return true;
return false;
}
export const registerProtocolHandler = (handler : any) => {
if (handler == null) return;
// Determine protocol value defensively. Default to 'standard' when unknown.
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = handler as ProtocolHandler;
} else if (isRawProtocolHandler(handler)) {
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler, defaultBluetoothService);
(activeHandler as ProtocolHandler).protocol = proto;
} else {
console.warn('[AKBLE] registerProtocolHandler: unsupported handler type, ignoring', handler);
return;
}
activeProtocol = proto;
}
export const scanDevices = async (options ?: ScanDevicesOptions) : Promise<void> => {
ensureDefaultProtocolHandler();
console.log('[AKBLE] start scan', options)
// Determine which protocols to run: either user-specified or all registered
// Single active handler flow
if (activeHandler == null) {
console.log('[AKBLE] no active scan handler registered')
return
}
const handler = activeHandler as ProtocolHandler;
const scanOptions : ScanDevicesOptions = {
onDeviceFound: (device : BleDevice) => emit('deviceFound', { event: 'deviceFound', device }),
onScanFinished: () => emit('scanFinished', { event: 'scanFinished' })
}
try {
await handler.scanDevices(scanOptions)
} catch (e) {
console.warn('[AKBLE] scanDevices handler error', e)
}
}
export const connectDevice = async (deviceId : string, protocol : BleProtocolType, options ?: BleConnectOptionsExt) : Promise<void> => {
const handler = activeHandler;
if (handler == null) throw new Error('No protocol handler');
const device : BleDevice = { deviceId, name: '', rssi: 0 }; // 可扩展
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2; // CONNECTED
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
console.log(deviceMap)
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
}
export const disconnectDevice = async (deviceId : string, protocol : BleProtocolType) : Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null) return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
deviceMap.delete(getDeviceKey(deviceId, protocol));
}
export const sendData = async (payload : SendDataPayload, options ?: BleOptions) : Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null) throw new Error('Device not connected');
// copy to local non-null variable so generator can smart-cast across awaits
const deviceCtx = ctx as DeviceContext;
if (deviceCtx.handler == null) throw new Error('sendData not supported for this protocol');
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data });
}
export const getConnectedDevices = () : MultiProtocolDevice[] => {
const result : MultiProtocolDevice[] = [];
deviceMap.forEach((ctx : DeviceContext) => {
const dev : MultiProtocolDevice = {
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
};
result.push(dev);
});
return result;
}
export const getConnectionState = (deviceId : string, protocol : BleProtocolType) : BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null) return 0;
return ctx.state;
}
export const on = (event : BleEvent, callback : BleEventCallback) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
}
export const off = (event : BleEvent, callback ?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
} else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
}
function getDeviceKey(deviceId : string, protocol : BleProtocolType) : string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId : string, protocol : BleProtocolType, options ?: BleConnectOptionsExt) : Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null) throw new Error('autoConnect not supported for this protocol');
const device : BleDevice = { deviceId, name: '', rssi: 0 };
// safe call - handler.autoConnect exists on ProtocolHandler
return await handler.autoConnect(device, options) as AutoBleInterfaces;
}
function ensureDefaultProtocolHandler(): void {
if (activeHandler != null) return;
const service = defaultBluetoothService;
if (service == null) return;
try {
const _dm = DeviceManager.getInstance();
const _raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions) => {
try {
const scanOptions = options != null ? options : {} as ScanDevicesOptions;
_dm.startScan(scanOptions);
} catch (e) {
console.warn('[AKBLE] DeviceManager.startScan failed', e);
}
return Promise.resolve();
},
connect: (device, options?: BleConnectOptionsExt) => {
return _dm.connectDevice(device.deviceId, options);
},
disconnect: (device) => {
return _dm.disconnectDevice(device.deviceId);
},
autoConnect: (device, options?: any) => {
const result: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
return Promise.resolve(result);
}
};
const _wrapper = new ProtocolHandlerWrapper(_raw, service);
activeHandler = _wrapper;
activeProtocol = _raw.protocol as BleProtocolType;
console.log('[AKBLE] default protocol handler (BluetoothService-backed) registered', activeProtocol);
} catch (e) {
console.warn('[AKBLE] failed to register default protocol handler', e);
}
}
export const setDefaultBluetoothService = (service: BluetoothService) => {
defaultBluetoothService = service;
ensureDefaultProtocolHandler();
};

View File

@@ -0,0 +1,5 @@
{
"dependencies": [
]
}

View File

@@ -0,0 +1,311 @@
import type { BleDevice, BleOptions, BleConnectionState, BleConnectionStateChangeCallback } from '../interface.uts'
import type { BleConnectOptionsExt } from '../interface.uts'
import type { ScanDevicesOptions } from '../interface.uts';
import Context from "android.content.Context";
import BluetoothAdapter from "android.bluetooth.BluetoothAdapter";
import BluetoothManager from "android.bluetooth.BluetoothManager";
import BluetoothDevice from "android.bluetooth.BluetoothDevice";
import BluetoothGatt from "android.bluetooth.BluetoothGatt";
import BluetoothGattCallback from "android.bluetooth.BluetoothGattCallback";
import ScanCallback from "android.bluetooth.le.ScanCallback";
import ScanResult from "android.bluetooth.le.ScanResult";
import ScanSettings from "android.bluetooth.le.ScanSettings";
import Handler from "android.os.Handler";
import Looper from "android.os.Looper";
import ContextCompat from "androidx.core.content.ContextCompat";
import PackageManager from "android.content.pm.PackageManager";
// 定义 PendingConnect 类型和实现类
interface PendingConnect {
resolve: () => void;
reject: (err?: any) => void; // Changed to make err optional
timer?: number;
}
class PendingConnectImpl implements PendingConnect {
resolve: () => void;
reject: (err?: any) => void; // Changed to make err optional
timer?: number;
constructor(resolve: () => void, reject: (err?: any) => void, timer?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
// 引入全局回调管理
import { gattCallback } from './service_manager.uts'
const pendingConnects = new Map<string, PendingConnect>();
const STATE_DISCONNECTED = 0;
const STATE_CONNECTING = 1;
const STATE_CONNECTED = 2;
const STATE_DISCONNECTING = 3;
export class DeviceManager {
private static instance: DeviceManager | null = null;
private devices = new Map<string, BleDevice>();
private connectionStates = new Map<string, BleConnectionState>();
private connectionStateChangeListeners: BleConnectionStateChangeCallback[] = []
private gattMap = new Map<string, BluetoothGatt | null>();
private scanCallback: ScanCallback | null = null
private isScanning: boolean = false
private constructor() {}
static getInstance(): DeviceManager {
if (DeviceManager.instance == null) {
DeviceManager.instance = new DeviceManager();
}
return DeviceManager.instance!;
}
startScan(options: ScanDevicesOptions): void {
console.log('ak startscan now')
const adapter = this.getBluetoothAdapter();
if (adapter == null) {
throw new Error('未找到蓝牙适配器');
}
if (!adapter.isEnabled) {
// 尝试请求用户开启蓝牙
try {
adapter.enable(); // 直接调用,无需可选链和括号
} catch (e) {
// 某些设备可能不支持 enable
}
setTimeout(() => {
if (!adapter.isEnabled) {
throw new Error('蓝牙未开启');
}
}, 1500);
throw new Error('正在开启蓝牙,请重试');
}
const foundDevices = this.devices; // 直接用全局 devices
class MyScanCallback extends ScanCallback {
private foundDevices: Map<string, BleDevice>;
private onDeviceFound: (device: BleDevice) => void;
constructor(foundDevices: Map<string, BleDevice>, onDeviceFound: (device: BleDevice) => void) {
super();
this.foundDevices = foundDevices;
this.onDeviceFound = onDeviceFound;
}
override onScanResult(callbackType: Int, result: ScanResult): void {
const device = result.getDevice();
if (device != null) {
const deviceId = device.getAddress();
let bleDevice = foundDevices.get(deviceId);
if (bleDevice == null) {
bleDevice = {
deviceId,
name: device.getName() ?? 'Unknown',
rssi: result.getRssi(),
lastSeen: Date.now()
};
foundDevices.set(deviceId, bleDevice);
this.onDeviceFound(bleDevice);
} else {
// 更新属性(已确保 bleDevice 非空)
bleDevice.rssi = result.getRssi();
bleDevice.name = device.getName() ?? bleDevice.name;
bleDevice.lastSeen = Date.now();
}
}
}
override onScanFailed(errorCode: Int): void {
console.log('ak scan fail')
}
}
this.scanCallback = new MyScanCallback(foundDevices, options.onDeviceFound ?? (() => {}));
const scanner = adapter.getBluetoothLeScanner();
if (scanner == null) {
throw new Error('无法获取扫描器');
}
const scanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
scanner.startScan(null, scanSettings, this.scanCallback);
this.isScanning = true;
// 默认10秒后停止扫描
new Handler(Looper.getMainLooper()).postDelayed(() => {
if (this.isScanning && this.scanCallback != null) {
scanner.stopScan(this.scanCallback);
this.isScanning = false;
// this.devices = foundDevices;
if (options.onScanFinished != null) options.onScanFinished?.invoke();
}
}, 40000);
}
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
console.log('[AKBLE] connectDevice called, deviceId:', deviceId, 'options:', options, 'connectionStates:')
const adapter = this.getBluetoothAdapter();
if (adapter == null) {
console.error('[AKBLE] connectDevice failed: 蓝牙适配器不可用')
throw new Error('蓝牙适配器不可用');
}
const device = adapter.getRemoteDevice(deviceId);
if (device == null) {
console.error('[AKBLE] connectDevice failed: 未找到设备', deviceId)
throw new Error('未找到设备');
}
this.connectionStates.set(deviceId, STATE_CONNECTING);
console.log('[AKBLE] connectDevice set STATE_CONNECTING, deviceId:', deviceId, 'connectionStates:')
this.emitConnectionStateChange(deviceId, STATE_CONNECTING);
const activity = UTSAndroid.getUniActivity();
const timeout = options?.timeout ?? 15000;
const key = `${deviceId}|connect`;
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
console.error('[AKBLE] connectDevice 超时:', deviceId)
pendingConnects.delete(key);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
this.gattMap.set(deviceId, null);
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
reject(new Error('连接超时'));
}, timeout);
// 创建一个适配器函数来匹配类型签名
const resolveAdapter = () => {
console.log('[AKBLE] connectDevice resolveAdapter:', deviceId)
resolve();
};
const rejectAdapter = (err?: any) => {
console.error('[AKBLE] connectDevice rejectAdapter:', deviceId, err)
reject(err);
};
pendingConnects.set(key, new PendingConnectImpl(resolveAdapter, rejectAdapter, timer));
try {
console.log('[AKBLE] connectGatt 调用前:', deviceId)
const gatt = device.connectGatt(activity, false, gattCallback);
this.gattMap.set(deviceId, gatt);
console.log('[AKBLE] connectGatt 调用后:', deviceId, gatt)
} catch (e) {
console.error('[AKBLE] connectGatt 异常:', deviceId, e)
clearTimeout(timer);
pendingConnects.delete(key);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
this.gattMap.set(deviceId, null);
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
reject(e);
}
});
}
// 统一分发连接回调(应在 gattCallback.onConnectionStateChange 内调用)
static handleConnectionStateChange(deviceId: string, newState: number, error?: any) {
console.log('[AKBLE] handleConnectionStateChange:', deviceId, 'newState:', newState, 'error:', error, 'pendingConnects:')
const key = `${deviceId}|connect`;
const cb = pendingConnects.get(key);
if (cb != null) {
// 修复 timer 的空安全问题,使用临时变量
const timerValue = cb.timer;
if (timerValue != null) {
clearTimeout(timerValue);
}
// 修复 error 处理
if (newState == STATE_CONNECTED) {
console.log('[AKBLE] handleConnectionStateChange: 连接成功', deviceId)
cb.resolve();
} else {
// 正确处理可空值
const errorToUse = error != null ? error : new Error('连接断开');
console.error('[AKBLE] handleConnectionStateChange: 连接失败', deviceId, errorToUse)
cb.reject(errorToUse);
}
pendingConnects.delete(key);
} else {
console.warn('[AKBLE] handleConnectionStateChange: 未找到 pendingConnects', deviceId, newState)
}
}
async disconnectDevice(deviceId: string, isActive: boolean = true): Promise<void> {
console.log('[AKBLE] disconnectDevice called, deviceId:', deviceId, 'isActive:', isActive)
let gatt = this.gattMap.get(deviceId);
if (gatt != null) {
gatt.disconnect();
gatt.close();
// gatt=null;
this.gattMap.set(deviceId, null);
this.connectionStates.set(deviceId, STATE_DISCONNECTED);
console.log('[AKBLE] disconnectDevice set STATE_DISCONNECTED, deviceId:', deviceId, 'connectionStates:')
this.emitConnectionStateChange(deviceId, STATE_DISCONNECTED);
return;
} else {
console.log('[AKBLE] disconnectDevice: gatt is null, deviceId:', deviceId)
return;
}
}
async reconnectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
let attempts = 0;
const maxAttempts = options?.maxAttempts ?? 3;
const interval = options?.interval ?? 3000;
while (attempts < maxAttempts) {
try {
await this.disconnectDevice(deviceId, false);
await this.connectDevice(deviceId, options);
return;
} catch (e) {
attempts++;
if (attempts >= maxAttempts) throw new Error('重连失败');
// 修复 setTimeout 问题,使用旧式 Promise + setTimeout 解决
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, interval);
});
}
}
}
getConnectedDevices(): BleDevice[] {
// 创建一个空数组来存储结果
const result: BleDevice[] = [];
// 遍历 devices Map 并检查连接状态
this.devices.forEach((device, deviceId) => {
if (this.connectionStates.get(deviceId) == STATE_CONNECTED) {
result.push(device);
}
});
return result;
}
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
console.log('[AKBLE][LOG] onConnectionStateChange 注册, 当前监听数:', this.connectionStateChangeListeners.length + 1, listener)
this.connectionStateChangeListeners.push(listener)
}
protected emitConnectionStateChange(deviceId: string, state: BleConnectionState) {
console.log('[AKBLE][LOG] emitConnectionStateChange', deviceId, state, 'listeners:', this.connectionStateChangeListeners.length, 'connectionStates:', this.connectionStates)
for (const listener of this.connectionStateChangeListeners) {
try {
console.log('[AKBLE][LOG] emitConnectionStateChange 调用 listener', listener)
listener(deviceId, state)
} catch (e) {
console.error('[AKBLE][LOG] emitConnectionStateChange listener error', e)
}
}
}
getGattInstance(deviceId: string): BluetoothGatt | null {
return this.gattMap.get(deviceId) ?? null;
}
private getBluetoothAdapter(): BluetoothAdapter | null {
const context = UTSAndroid.getAppContext();
if (context == null) return null;
const manager = context?.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager;
return manager.getAdapter();
}
/**
* 获取指定ID的设备如果存在
*/
public getDevice(deviceId: string): BleDevice | null {
console.log(deviceId,this.devices)
return this.devices.get(deviceId) ?? null;
}
}

View File

@@ -0,0 +1,670 @@
import { BleService } from '../interface.uts'
import type { WriteCharacteristicOptions, DfuOptions, ControlParserResult } from '../interface.uts'
import { DeviceManager } from './device_manager.uts'
import { ServiceManager } from './service_manager.uts'
import BluetoothGatt from 'android.bluetooth.BluetoothGatt'
import BluetoothGattCharacteristic from 'android.bluetooth.BluetoothGattCharacteristic'
import BluetoothGattDescriptor from 'android.bluetooth.BluetoothGattDescriptor'
import UUID from 'java.util.UUID'
// @ts-ignore missing ambient declaration provided by native bridge
import UTSError from "io.dcloud.uts.UTSError";
// 通用 Nordic DFU UUIDs (常见设备可能使用这些;如厂商自定义请替换)
const DFU_SERVICE_UUID = '0000fe59-0000-1000-8000-00805f9b34fb'
const DFU_CONTROL_POINT_UUID = '8ec90001-f315-4f60-9fb8-838830daea50'
const DFU_PACKET_UUID = '8ec90002-f315-4f60-9fb8-838830daea50'
type DfuSession = {
resolve : () => void;
reject : (err ?: any) => void;
onProgress ?: (p : number) => void;
onLog ?: (s : string) => void;
controlParser ?: (data : Uint8Array) => ControlParserResult | null;
// Nordic 专用字段
bytesSent ?: number;
totalBytes ?: number;
useNordic ?: boolean;
// PRN (packet receipt notification) support
prn ?: number;
packetsSincePrn ?: number;
prnResolve ?: () => void;
prnReject ?: (err ?: any) => void;
}
export class DfuManager {
// 会话表,用于把 control-point 通知路由到当前 DFU 流程
private sessions : Map<string, DfuSession> = new Map();
// 简化:只实现最基本的 GATT-based DFU 上传逻辑,需按设备协议调整 control point 的命令/解析
// Emit a DFU lifecycle event for a session. name should follow Nordic listener names
private _emitDfuEvent(deviceId : string, name : string, payload ?: any) {
console.log('[DFU][Event]', name, deviceId, payload ?? '');
const s = this.sessions.get(deviceId);
if (s == null) return;
if (typeof s.onLog == 'function') {
try {
const logFn = s.onLog as (msg : string) => void;
logFn(`[${name}] ${payload != null ? JSON.stringify(payload) : ''}`);
} catch (e) { }
}
if (name == 'onProgress' && typeof s.onProgress == 'function' && typeof payload == 'number') {
try { s.onProgress(payload); } catch (e) { }
}
}
async startDfu(deviceId : string, firmwareBytes : Uint8Array, options ?: DfuOptions) : Promise<void> {
console.log('startDfu 0')
const deviceManager = DeviceManager.getInstance();
const serviceManager = ServiceManager.getInstance();
console.log('startDfu 1')
const gatt : BluetoothGatt | null = deviceManager.getGattInstance(deviceId);
console.log('startDfu 2')
if (gatt == null) throw new Error('Device not connected');
console.log('[DFU] startDfu start deviceId=', deviceId, 'firmwareBytes=', firmwareBytes != null ? firmwareBytes.length : 0, 'options=', options);
try {
console.log('[DFU] requesting high connection priority for', deviceId);
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
} catch (e) {
console.warn('[DFU] requestConnectionPriority failed', e);
}
// 发现服务并特征
// ensure services discovered before accessing GATT; serviceManager exposes Promise-based API
await serviceManager.getServices(deviceId, null);
console.log('[DFU] services ensured for', deviceId);
const dfuService = gatt.getService(UUID.fromString(DFU_SERVICE_UUID));
if (dfuService == null) throw new Error('DFU service not found');
const controlChar = dfuService.getCharacteristic(UUID.fromString(DFU_CONTROL_POINT_UUID));
const packetChar = dfuService.getCharacteristic(UUID.fromString(DFU_PACKET_UUID));
console.log('[DFU] dfuService=', dfuService != null ? dfuService.getUuid().toString() : null, 'controlChar=', controlChar != null ? controlChar.getUuid().toString() : null, 'packetChar=', packetChar != null ? packetChar.getUuid().toString() : null);
if (controlChar == null || packetChar == null) throw new Error('DFU characteristics missing');
// Allow caller to request a desired MTU via options for higher throughput
const desiredMtu = (options != null && typeof options.mtu == 'number') ? options.mtu : 247;
try {
console.log('[DFU] requesting MTU=', desiredMtu, 'for', deviceId);
await this._requestMtu(gatt, desiredMtu, 8000);
console.log('[DFU] requestMtu completed for', deviceId);
} catch (e) {
console.warn('[DFU] requestMtu failed or timed out, continue with default.', e);
}
const mtu = desiredMtu; // 假定成功或使用期望值
const chunkSize = Math.max(20, mtu - 3);
// small helper to convert a byte (possibly signed) to a two-digit hex string
const byteToHex = (b : number) => {
const v = (b < 0) ? (b + 256) : b;
let s = v.toString(16);
if (s.length < 2) s = '0' + s;
return s;
};
// Parameterize PRN window and timeout via options early so they are available
// for session logging. Defaults: prn = 12 packets, prnTimeoutMs = 10000 ms
let prnWindow = 0;
if (options != null && typeof options.prn == 'number') {
prnWindow = Math.max(0, Math.floor(options.prn));
}
const prnTimeoutMs = (options != null && typeof options.prnTimeoutMs == 'number') ? Math.max(1000, Math.floor(options.prnTimeoutMs)) : 8000;
const disablePrnOnTimeout = !(options != null && options.disablePrnOnTimeout == false);
// 订阅 control point 通知并将通知路由到会话处理器
const controlHandler = (data : Uint8Array) => {
// 交给会话处理器解析并触发事件
try {
const hexParts: string[] = [];
for (let i = 0; i < data.length; i++) {
const v = data[i] as number;
hexParts.push(byteToHex(v));
}
const hex = hexParts.join(' ');
console.log('[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data), 'hex=', hex);
} catch (e) {
console.log('[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data));
}
this._handleControlNotification(deviceId, data);
};
console.log('[DFU] subscribing control point for', deviceId);
await serviceManager.subscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, controlHandler);
console.log('[DFU] subscribeCharacteristic returned for', deviceId);
// 保存会话回调(用于 waitForControlEvent; 支持 Nordic 模式追踪已发送字节
this.sessions.set(deviceId, {
resolve: () => { },
reject: (err ?: any) => {console.log(err) },
onProgress: null,
onLog: null,
controlParser: (data : Uint8Array) => this._defaultControlParser(data),
bytesSent: 0,
totalBytes: firmwareBytes.length,
useNordic: options != null && options.useNordic == true,
prn: null,
packetsSincePrn: 0,
prnResolve: null,
prnReject: null
});
console.log('[DFU] session created for', deviceId, 'totalBytes=', firmwareBytes.length);
console.log('[DFU] DFU session details:', { deviceId: deviceId, totalBytes: firmwareBytes.length, chunkSize: chunkSize, prnWindow: prnWindow, prnTimeoutMs: prnTimeoutMs });
// wire options callbacks into the session (if provided)
const sessRef = this.sessions.get(deviceId);
if (sessRef != null) {
sessRef.onProgress = (options != null && typeof options.onProgress == 'function') ? options.onProgress : null;
sessRef.onLog = (options != null && typeof options.onLog == 'function') ? options.onLog : null;
}
// emit initial lifecycle events (Nordic-like)
this._emitDfuEvent(deviceId, 'onDeviceConnecting', null);
// 写入固件数据(非常保守的实现:逐包写入并等待短延迟)
// --- PRN setup (optional, Nordic-style flow) ---
// Parameterize PRN window and timeout via options: options.prn, options.prnTimeoutMs
// Defaults were set earlier; build PRN payload using arithmetic to avoid
// bitwise operators which don't map cleanly to generated Kotlin.
if (prnWindow > 0) {
try {
// send Set PRN to device (format: [OP_CODE_SET_PRN, prn LSB, prn MSB])
// WARNING: Ensure your device uses the same opcode/format; change if needed.
const prnLsb = prnWindow % 256;
const prnMsb = Math.floor(prnWindow / 256) % 256;
const prnPayload = new Uint8Array([0x02, prnLsb, prnMsb]);
await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, prnPayload, null);
const sess0 = this.sessions.get(deviceId);
if (sess0 != null) {
sess0.useNordic = true;
sess0.prn = prnWindow;
sess0.packetsSincePrn = 0;
sess0.controlParser = (data : Uint8Array) => this._nordicControlParser(data);
}
console.log('[DFU] Set PRN sent (prn=', prnWindow, ') for', deviceId);
} catch (e) {
console.warn('[DFU] Set PRN failed (continuing without PRN):', e);
const sessFallback = this.sessions.get(deviceId);
if (sessFallback != null) sessFallback.prn = 0;
}
} else {
console.log('[DFU] PRN disabled (prnWindow=', prnWindow, ') for', deviceId);
}
// 写入固件数据(逐包写入并根据 options.waitForResponse 选择是否等待响应)
let offset = 0;
const total = firmwareBytes.length;
this._emitDfuEvent(deviceId, 'onDfuProcessStarted', null);
this._emitDfuEvent(deviceId, 'onUploadingStarted', null);
// Track outstanding write operations when using fire-and-forget mode so we can
// log and throttle if the Android stack becomes overwhelmed.
let outstandingWrites = 0;
// read tuning parameters from options in a safe, generator-friendly way
let configuredMaxOutstanding = 16;
let writeSleepMs = 0;
let writeRetryDelay = 20;
let writeMaxAttempts = 6;
let writeGiveupTimeout = 15000;
let drainOutstandingTimeout = 3000;
let failureBackoffMs = 0;
// throughput measurement
let throughputWindowBytes = 0;
let lastThroughputTime = Date.now();
function _logThroughputIfNeeded(force ?: boolean) {
try {
const now = Date.now();
const elapsed = now - lastThroughputTime;
if (force == true || elapsed >= 1000) {
const bytes = throughputWindowBytes;
const bps = Math.floor((bytes * 1000) / Math.max(1, elapsed));
// reset window
throughputWindowBytes = 0;
lastThroughputTime = now;
const human = `${bps} B/s`;
console.log('[DFU] throughput:', human, 'elapsedMs=', elapsed);
const s = this.sessions.get(deviceId);
if (s != null && typeof s.onLog == 'function') {
try { s.onLog?.invoke('[DFU] throughput: ' + human); } catch (e) { }
}
}
} catch (e) { }
}
function _safeErr(e ?: any) {
try {
if (e == null) return '';
if (typeof e == 'string') return e;
try { return JSON.stringify(e); } catch (e2) { }
try { return (e as any).toString(); } catch (e3) { }
return '';
} catch (e4) { return ''; }
}
try {
if (options != null) {
try {
if (options.maxOutstanding != null) {
const parsed = Math.floor(options.maxOutstanding as number);
if (!isNaN(parsed) && parsed > 0) configuredMaxOutstanding = parsed;
}
} catch (e) { }
try {
if (options.writeSleepMs != null) {
const parsedWs = Math.floor(options.writeSleepMs as number);
if (!isNaN(parsedWs) && parsedWs >= 0) writeSleepMs = parsedWs;
}
} catch (e) { }
try {
if (options.writeRetryDelayMs != null) {
const parsedRetry = Math.floor(options.writeRetryDelayMs as number);
if (!isNaN(parsedRetry) && parsedRetry >= 0) writeRetryDelay = parsedRetry;
}
} catch (e) { }
try {
if (options.writeMaxAttempts != null) {
const parsedAttempts = Math.floor(options.writeMaxAttempts as number);
if (!isNaN(parsedAttempts) && parsedAttempts > 0) writeMaxAttempts = parsedAttempts;
}
} catch (e) { }
try {
if (options.writeGiveupTimeoutMs != null) {
const parsedGiveupTimeout = Math.floor(options.writeGiveupTimeoutMs as number);
if (!isNaN(parsedGiveupTimeout) && parsedGiveupTimeout > 0) writeGiveupTimeout = parsedGiveupTimeout;
}
} catch (e) { }
try {
if (options.drainOutstandingTimeoutMs != null) {
const parsedDrain = Math.floor(options.drainOutstandingTimeoutMs as number);
if (!isNaN(parsedDrain) && parsedDrain >= 0) drainOutstandingTimeout = parsedDrain;
}
} catch (e) { }
}
if (configuredMaxOutstanding < 1) configuredMaxOutstanding = 1;
if (writeSleepMs < 0) writeSleepMs = 0;
} catch (e) { }
const maxOutstandingCeiling = configuredMaxOutstanding;
let adaptiveMaxOutstanding = configuredMaxOutstanding;
const minOutstandingWindow = 1;
while (offset < total) {
const end = Math.min(offset + chunkSize, total);
const slice = firmwareBytes.subarray(offset, end);
// Decide whether to wait for response per-chunk. Default to false for high throughput.
// Generator-friendly: avoid 'undefined' and use explicit boolean check.
let finalWaitForResponse = false;
if (options != null) {
try {
const maybe = options.waitForResponse;
if (maybe == true) finalWaitForResponse = true;
} catch (e) { finalWaitForResponse = false; }
}
const writeOpts: WriteCharacteristicOptions = {
waitForResponse: finalWaitForResponse,
retryDelayMs: writeRetryDelay,
maxAttempts: writeMaxAttempts,
giveupTimeoutMs: writeGiveupTimeout,
forceWriteTypeNoResponse: finalWaitForResponse == false
};
console.log('[DFU] writing packet chunk offset=', offset, 'len=', slice.length, 'waitForResponse=', finalWaitForResponse, 'outstanding=', outstandingWrites);
// Fire-and-forget path: do not await the write if waitForResponse == false.
if (finalWaitForResponse == false) {
if (failureBackoffMs > 0) {
console.log('[DFU] applying failure backoff', failureBackoffMs, 'ms before next write for', deviceId);
await this._sleep(failureBackoffMs);
failureBackoffMs = Math.floor(failureBackoffMs / 2);
}
while (outstandingWrites >= adaptiveMaxOutstanding) {
await this._sleep(Math.max(1, writeSleepMs));
}
// increment outstanding counter and kick the write without awaiting.
outstandingWrites = outstandingWrites + 1;
// fire-and-forget: start the write but don't await its Promise
const writeOffset = offset;
serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts).then((res) => {
outstandingWrites = Math.max(0, outstandingWrites - 1);
if (res == true) {
if (adaptiveMaxOutstanding < maxOutstandingCeiling) {
adaptiveMaxOutstanding = Math.min(maxOutstandingCeiling, adaptiveMaxOutstanding + 1);
}
if (failureBackoffMs > 0) failureBackoffMs = Math.floor(failureBackoffMs / 2);
}
// log occasional completions
if ((outstandingWrites & 0x1f) == 0) {
console.log('[DFU] write completion callback, outstandingWrites=', outstandingWrites, 'adaptiveWindow=', adaptiveMaxOutstanding, 'device=', deviceId);
}
// detect write failure signaled by service manager
if (res !== true) {
adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2));
failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay)));
console.error('[DFU] writeCharacteristic returned false for device=', deviceId, 'offset=', writeOffset, 'adaptiveWindow now=', adaptiveMaxOutstanding);
try { this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: 'write returned false' }); } catch (e) { }
}
}).catch((e) => {
outstandingWrites = Math.max(0, outstandingWrites - 1);
adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2));
failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay)));
console.warn('[DFU] fire-and-forget write failed for device=', deviceId, e, 'adaptiveWindow now=', adaptiveMaxOutstanding);
try {
const errMsg ='[DFU] fire-and-forget write failed for device=';
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: errMsg });
} catch (e2) { }
});
// account bytes for throughput
throughputWindowBytes += slice.length;
_logThroughputIfNeeded(false);
} else {
console.log('[DFU] awaiting write for chunk offset=', offset);
try {
const writeResult = await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts);
if (writeResult !== true) {
console.error('[DFU] writeCharacteristic(await) returned false at offset=', offset, 'device=', deviceId);
try { this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: 'write returned false' }); } catch (e) { }
// abort DFU by throwing
throw new Error('write failed');
}
} catch (e) {
console.error('[DFU] awaiting write failed at offset=', offset, 'device=', deviceId, e);
try {
const errMsg = '[DFU] awaiting write failed ';
this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: errMsg });
} catch (e2) { }
throw e;
}
// account bytes for throughput
throughputWindowBytes += slice.length;
_logThroughputIfNeeded(false);
}
// update PRN counters and wait when window reached
const sessAfter = this.sessions.get(deviceId);
if (sessAfter != null && sessAfter.useNordic == true && typeof sessAfter.prn == 'number' && (sessAfter.prn ?? 0) > 0) {
sessAfter.packetsSincePrn = (sessAfter.packetsSincePrn ?? 0) + 1;
if ((sessAfter.packetsSincePrn ?? 0) >= (sessAfter.prn ?? 0) && (sessAfter.prn ?? 0) > 0) {
// wait for PRN (device notification) before continuing
try {
console.log('[DFU] reached PRN window, waiting for PRN for', deviceId, 'packetsSincePrn=', sessAfter.packetsSincePrn, 'prn=', sessAfter.prn);
await this._waitForPrn(deviceId, prnTimeoutMs);
console.log('[DFU] PRN received, resuming transfer for', deviceId);
} catch (e) {
console.warn('[DFU] PRN wait failed/timed out, continuing anyway for', deviceId, e);
if (disablePrnOnTimeout) {
console.warn('[DFU] disabling PRN waits after timeout for', deviceId);
sessAfter.prn = 0;
}
}
// reset counter
sessAfter.packetsSincePrn = 0;
}
}
offset = end;
// 如果启用 nordic 模式,统计已发送字节
const sess = this.sessions.get(deviceId);
if (sess != null && typeof sess.bytesSent == 'number') {
sess.bytesSent = (sess.bytesSent ?? 0) + slice.length;
}
// 简单节流与日志,避免过快。默认睡眠非常短以提高吞吐量; 可在设备上调节
console.log('[DFU] wrote chunk for', deviceId, 'offset=', offset, '/', total, 'chunkSize=', slice.length, 'bytesSent=', sess != null ? sess.bytesSent : null, 'outstanding=', outstandingWrites);
// emit upload progress event (percent) if available
if (sess != null && typeof sess.bytesSent == 'number' && typeof sess.totalBytes == 'number') {
const p = Math.floor((sess.bytesSent / sess.totalBytes) * 100);
this._emitDfuEvent(deviceId, 'onProgress', p);
}
// yield to event loop and avoid starving the Android BLE stack
await this._sleep(Math.max(0, writeSleepMs));
}
// wait for outstanding writes to drain before continuing with control commands
if (outstandingWrites > 0) {
const drainStart = Date.now();
while (outstandingWrites > 0 && (Date.now() - drainStart) < drainOutstandingTimeout) {
await this._sleep(Math.max(0, writeSleepMs));
}
if (outstandingWrites > 0) {
console.warn('[DFU] outstandingWrites remain after drain timeout, continuing with', outstandingWrites);
} else {
console.log('[DFU] outstandingWrites drained before control phase');
}
}
this._emitDfuEvent(deviceId, 'onUploadingCompleted', null);
// force final throughput log before activate/validate
_logThroughputIfNeeded(true);
// 发送 activate/validate 命令到 control point需根据设备协议实现
// 下面为占位:请替换为实际的 opcode
// 发送 validate/activate 命令到 control point需根据设备协议实现
try {
// control writes: pass undefined options explicitly to satisfy the generator/typechecker
const activatePayload = new Uint8Array([0x04]);
console.log('[DFU] sending activate/validate payload=', Array.from(activatePayload));
await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, activatePayload, null);
console.log('[DFU] activate/validate write returned for', deviceId);
} catch (e) {
console.warn('[DFU] activate/validate write failed (ignored) for', deviceId, e);
}
console.log('[DFU] sent control activate/validate command to control point for', deviceId);
this._emitDfuEvent(deviceId, 'onValidating', null);
// 等待 control-point 返回最终结果(成功或失败),超时可配置
try {
const timeout = 20000;
console.log('[DFU] waiting for control result (timeout=', timeout, ') for', deviceId);
await this._waitForControlResult(deviceId, timeout);
console.log('[DFU] control result resolved for', deviceId);
} catch (err) {
// 清理订阅后抛出
try { await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID); } catch (e) { }
this.sessions.delete(deviceId);
throw err;
}
// 取消订阅
try {
await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID);
} catch (e) { }
console.log('[DFU] unsubscribed control point for', deviceId);
// 清理会话
this.sessions.delete(deviceId);
console.log('[DFU] session cleared for', deviceId);
return;
}
async _requestMtu(gatt : BluetoothGatt, mtu : number, timeoutMs : number) : Promise<void> {
return new Promise<void>((resolve, reject) => {
// 在当前项目BluetoothGattCallback.onMtuChanged 未被封装;简单发起请求并等待短超时
try {
const ok = gatt.requestMtu(Math.floor(mtu) as Int);
if (!ok) {
return reject(new Error('requestMtu failed'));
}
} catch (e) {
return reject(e);
}
// 无 callback 监听时退回,等待一小段时间以便成功
setTimeout(() => resolve(), Math.min(2000, timeoutMs));
});
}
_sleep(ms : number) {
return new Promise<void>((r) => { setTimeout(() => { r() }, ms); });
}
_waitForPrn(deviceId : string, timeoutMs : number) : Promise<void> {
const session = this.sessions.get(deviceId);
if (session == null) return Promise.reject(new Error('no dfu session'));
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
// timeout waiting for PRN
// clear pending handlers
session.prnResolve = null;
session.prnReject = null;
reject(new Error('PRN timeout'));
}, timeoutMs);
const prnResolve = () => {
clearTimeout(timer);
resolve();
};
const prnReject = (err ?: any) => {
clearTimeout(timer);
reject(err);
};
session.prnResolve = prnResolve;
session.prnReject = prnReject;
});
}
// 默认 control point 解析器(非常通用的尝试解析:如果设备发送 progress byte 或成功码)
_defaultControlParser(data : Uint8Array) : ControlParserResult | null {
// 假设协议:第一个字节为 opcode, 第二字节可为状态或进度
if (data == null || data.length == 0) return null;
const op = data[0];
// 常见 Nordic: 0x10 = Response? 0x60/0x20 etc - 具体需按设备协议调整
// 这里做保守解析:若长度>=2 并且第二字节为 progress (0-100) 则返回 progress
if (data.length >= 2) {
const maybeProgress = data[1];
if (maybeProgress >= 0 && maybeProgress <= 100) {
return { type: 'progress', progress: maybeProgress };
}
}
// 若找到明显的 success opcode (示例 0x01) 或 error 0xFF
if (op == 0x01) return { type: 'success' };
if (op == 0xFF) return { type: 'error', error: data };
// vendor-specific opcode example: 0x60 may mean 'upload progress' for some firmwares
if (op == 0x60 && data.length >= 2) {
return { type: 'progress', progress: data[1] };
}
return { type: 'info' };
}
// Nordic DFU control-parser支持 Response and Packet Receipt Notification
_nordicControlParser(data : Uint8Array) : ControlParserResult | null {
// Nordic opcodes (简化):
// - 0x10 : Response (opcode, requestOp, resultCode)
// - 0x11 : Packet Receipt Notification (opcode, value LSB, value MSB)
if (data == null || data.length == 0) return null;
const op = data[0];
if (op == 0x11 && data.length >= 3) {
// packet receipt notif: bytes received (little endian)
const lsb = data[1];
const msb = data[2];
const received = (msb << 8) | lsb;
// Return received bytes as progress value; parser does not resolve device-specific session here.
return { type: 'progress', progress: received };
}
// Nordic vendor-specific progress opcode (example 0x60)
if (op == 0x60 && data.length >= 2) {
return { type: 'progress', progress: data[1] };
}
// Response: check result code for success (0x01 may indicate success in some stacks)
if (op == 0x10 && data.length >= 3) {
const requestOp = data[1];
const resultCode = data[2];
// Nordic resultCode 0x01 = SUCCESS typically
if (resultCode == 0x01) return { type: 'success' };
else return { type: 'error', error: { requestOp, resultCode } };
}
return null;
}
_handleControlNotification(deviceId : string, data : Uint8Array) {
const session = this.sessions.get(deviceId);
if (session == null) {
console.warn('[DFU] control notification received but no session for', deviceId, 'data=', Array.from(data));
return;
}
try {
// human readable opcode mapping
let opcodeName = 'unknown';
switch (data[0]) {
case 0x10: opcodeName = 'Response'; break;
case 0x11: opcodeName = 'PRN'; break;
case 0x60: opcodeName = 'VendorProgress'; break;
case 0x01: opcodeName = 'SuccessOpcode'; break;
case 0xFF: opcodeName = 'ErrorOpcode'; break;
}
console.log('[DFU] _handleControlNotification deviceId=', deviceId, 'opcode=0x' + data[0].toString(16), 'name=', opcodeName, 'raw=', Array.from(data));
const parsed = session.controlParser != null ? session.controlParser(data) : null;
if (session.onLog != null) session.onLog('DFU control notify: ' + Array.from(data).join(','));
console.log('[DFU] parsed control result=', parsed);
if (parsed == null) return;
if (parsed.type == 'progress' && parsed.progress != null) {
// 如果在 nordic 模式 parsed.progress 可能是已接收字节数,则转换为百分比
if (session.useNordic == true && session.totalBytes != null && session.totalBytes > 0) {
const percent = Math.floor((parsed.progress / session.totalBytes) * 100);
session.onProgress?.(percent);
// If we have written all bytes locally, log that event
if (session.bytesSent != null && session.totalBytes != null && session.bytesSent >= session.totalBytes) {
console.log('[DFU] all bytes written locally for', deviceId, 'bytesSent=', session.bytesSent, 'total=', session.totalBytes);
// emit uploading completed once
this._emitDfuEvent(deviceId, 'onUploadingCompleted', null);
}
// If a PRN wait is pending, resolve it (PRN indicates device received packets)
if (typeof session.prnResolve == 'function') {
try { session.prnResolve(); } catch (e) { }
session.prnResolve = null;
session.prnReject = null;
session.packetsSincePrn = 0;
}
} else {
const progress = parsed.progress
if (progress != null) {
console.log('[DFU] progress for', deviceId, 'progress=', progress);
session.onProgress?.(progress);
// also resolve PRN if was waiting (in case device reports numeric progress)
if (typeof session.prnResolve == 'function') {
try { session.prnResolve(); } catch (e) { }
session.prnResolve = null;
session.prnReject = null;
session.packetsSincePrn = 0;
}
}
}
} else if (parsed.type == 'success') {
console.log('[DFU] parsed success for', deviceId, 'resolving session');
session.resolve();
// Log final device-acknowledged success
console.log('[DFU] device reported DFU success for', deviceId);
this._emitDfuEvent(deviceId, 'onDfuCompleted', null);
} else if (parsed.type == 'error') {
console.error('[DFU] parsed error for', deviceId, parsed.error);
session.reject(parsed.error ?? new Error('DFU device error'));
this._emitDfuEvent(deviceId, 'onError', parsed.error ?? {});
} else {
// info - just log
}
} catch (e) {
session.onLog?.('control parse error: ' + e);
console.error('[DFU] control parse exception for', deviceId, e);
}
}
_waitForControlResult(deviceId : string, timeoutMs : number) : Promise<void> {
const session = this.sessions.get(deviceId);
if (session == null) return Promise.reject(new Error('no dfu session'));
return new Promise<void>((resolve, reject) => {
// wrap resolve/reject to clear timer
const timer = setTimeout(() => {
// 超时
console.error('[DFU] _waitForControlResult timeout for', deviceId);
reject(new Error('DFU control timeout'));
}, timeoutMs);
const origResolve = () => {
clearTimeout(timer);
console.log('[DFU] _waitForControlResult resolved for', deviceId);
resolve();
};
const origReject = (err ?: any) => {
clearTimeout(timer);
console.error('[DFU] _waitForControlResult rejected for', deviceId, 'err=', err);
reject(err);
};
// replace session handlers temporarily (guard nullable)
if (session != null) {
session.resolve = origResolve;
session.reject = origReject;
}
});
}
}
export const dfuManager = new DfuManager();

View File

@@ -0,0 +1,238 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback, BleProtocolType, BluetoothService as BluetoothServiceContract } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
const serviceManager = ServiceManager.getInstance();
class AndroidBluetoothService implements BluetoothServiceContract {
scanDevices(options?: ScanDevicesOptions | null): Promise<void> {
return BluetoothManager.scanDevices(options);
}
async connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt | null): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.connectDevice(deviceId, proto, options ?? null);
}
async disconnectDevice(deviceId: string, protocol?: string): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.disconnectDevice(deviceId, proto);
}
getConnectedDevices(): MultiProtocolDevice[] {
return BluetoothManager.getConnectedDevices();
}
on(event: BleEvent | string, callback: BleEventCallback): void {
BluetoothManager.on(event as BleEvent, callback);
}
off(event: BleEvent | string, callback?: BleEventCallback | null): void {
BluetoothManager.off(event as BleEvent, callback ?? null);
}
getServices(deviceId: string): Promise<BleService[]> {
return new Promise((resolve, reject) => {
serviceManager.getServices(deviceId, (list, err) => {
console.log('getServices:', list, err);
if (err != null) {
reject(err);
} else {
resolve((list as BleService[]) ?? []);
}
});
});
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return new Promise((resolve, reject) => {
console.log(deviceId, serviceId);
serviceManager.getCharacteristics(deviceId, serviceId, (list, err) => {
if (err != null) {
reject(err);
} else {
resolve((list as BleCharacteristic[]) ?? []);
}
});
});
}
/**
* 自动发现服务和特征返回可用的写入和通知特征ID
* @param deviceId 设备ID
* @returns {Promise<AutoBleInterfaces>}
*/
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
// 1. 获取服务列表
const services = await this.getServices(deviceId);
if (services == null || services.length == 0) throw new Error('未发现服务');
// 2. 选择目标服务优先bae前缀可根据需要调整
let serviceId = '';
for (let i = 0; i < services.length; i++) {
const s = services[i];
const uuidCandidate: string | null = (s.uuid != null ? s.uuid : null)
const uuid: string = uuidCandidate != null ? uuidCandidate : ''
// prefer regex test to avoid nullable receiver calls in generated Kotlin
if (/^bae/i.test(uuid)) {
serviceId = uuid
break;
}
}
console.log(serviceId)
if (serviceId == null || serviceId == '') serviceId = services[0].uuid;
// 3. 获取特征列表
const characteristics = await this.getCharacteristics(deviceId, serviceId);
console.log(characteristics)
if (characteristics == null || characteristics.length == 0) throw new Error('未发现特征值');
// 4. 筛选write和notify特征
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
console.log(c)
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse==true)) writeCharId = c.uuid;
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) notifyCharId = c.uuid;
}
console.log(serviceId, writeCharId, notifyCharId);
if ((writeCharId == null || writeCharId == '') || (notifyCharId == null || notifyCharId == '')) throw new Error('未找到合适的写入或通知特征');
console.log(serviceId, writeCharId, notifyCharId);
// // 发现服务和特征后
const deviceManager = DeviceManager.getInstance();
console.log(deviceManager);
const device = deviceManager.getDevice(deviceId);
console.log(deviceId,device)
device!.serviceId = serviceId;
device!.writeCharId = writeCharId;
device!.notifyCharId = notifyCharId;
console.log(device);
return { serviceId, writeCharId, notifyCharId };
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
const payload = value instanceof Uint8Array ? value : new Uint8Array(value);
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, payload, options);
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
async autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
async subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export class BluetoothServiceShape implements BluetoothServiceContract {
scanDevices(options?: ScanDevicesOptions | null): Promise<void> {
return BluetoothManager.scanDevices(options);
}
async connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt | null): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.connectDevice(deviceId, proto, options ?? null);
}
async disconnectDevice(deviceId: string, protocol?: string): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.disconnectDevice(deviceId, proto);
}
getConnectedDevices(): MultiProtocolDevice[] {
return BluetoothManager.getConnectedDevices();
}
on(event: BleEvent | string, callback: BleEventCallback): void {
BluetoothManager.on(event as BleEvent, callback);
}
off(event: BleEvent | string, callback?: BleEventCallback | null): void {
BluetoothManager.off(event as BleEvent, callback ?? null);
}
getServices(deviceId: string): Promise<BleService[]> {
return new Promise((resolve, reject) => {
serviceManager.getServices(deviceId, (list, err) => {
console.log('getServices:', list, err);
if (err != null) {
reject(err);
} else {
resolve((list as BleService[]) ?? []);
}
});
});
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return new Promise((resolve, reject) => {
console.log(deviceId, serviceId);
serviceManager.getCharacteristics(deviceId, serviceId, (list, err) => {
if (err != null) {
reject(err);
} else {
resolve((list as BleCharacteristic[]) ?? []);
}
});
});
}
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const services = await this.getServices(deviceId);
if (services == null || services.length == 0) throw new Error('未发现服务');
let serviceId = '';
for (let i = 0; i < services.length; i++) {
const s = services[i];
const uuidCandidate: string | null = (s.uuid != null ? s.uuid : null)
const uuid: string = uuidCandidate != null ? uuidCandidate : ''
if (/^bae/i.test(uuid)) {
serviceId = uuid
break;
}
}
console.log(serviceId)
if (serviceId == null || serviceId == '') serviceId = services[0].uuid;
const characteristics = await this.getCharacteristics(deviceId, serviceId);
if (characteristics == null || characteristics.length == 0) throw new Error('未发现特征值');
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse==true)) writeCharId = c.uuid;
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) notifyCharId = c.uuid;
}
if ((writeCharId == null || writeCharId == '') || (notifyCharId == null || notifyCharId == '')) throw new Error('未找到合适的写入或通知特征');
const deviceManager = DeviceManager.getInstance();
const device = deviceManager.getDevice(deviceId);
device!.serviceId = serviceId;
device!.writeCharId = writeCharId;
device!.notifyCharId = notifyCharId;
return { serviceId, writeCharId, notifyCharId };
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
const payload = value instanceof Uint8Array ? value : new Uint8Array(value);
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, payload, options);
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
async autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
async subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
const bluetoothServiceInstance = new BluetoothServiceShape();
BluetoothManager.setDefaultBluetoothService(bluetoothServiceInstance);
export const bluetoothService: BluetoothServiceContract = bluetoothServiceInstance;
export function getBluetoothService(): BluetoothServiceShape {
return bluetoothServiceInstance;
}
// Ensure protocol handlers are registered when this module is imported.
// import './protocol_registry.uts';

View File

@@ -0,0 +1,815 @@
import type { BleService, BleCharacteristic, BleDataReceivedCallback, BleCharacteristicProperties, WriteCharacteristicOptions, ByteArray } from '../interface.uts';
import BluetoothGatt from "android.bluetooth.BluetoothGatt";
import BluetoothGattService from "android.bluetooth.BluetoothGattService";
import BluetoothGattCharacteristic from "android.bluetooth.BluetoothGattCharacteristic";
import BluetoothGattDescriptor from "android.bluetooth.BluetoothGattDescriptor";
import BluetoothGattCallback from "android.bluetooth.BluetoothGattCallback";
import UUID from "java.util.UUID";
import { DeviceManager } from './device_manager.uts';
import { AkBleErrorImpl, AkBluetoothErrorCode } from '../unierror.uts';
import { AutoDiscoverAllResult } from '../interface.uts';
// 补全UUID格式将短格式转换为标准格式
function getFullUuid(shortUuid: string): string {
return `0000${shortUuid}-0000-1000-8000-00805f9b34fb`;
}
const deviceWriteQueues = new Map<string, Promise<void>>();
function enqueueDeviceWrite<T>(deviceId: string, work: () => Promise<T>): Promise<T> {
const previous = deviceWriteQueues.get(deviceId) ?? Promise.resolve();
const next = (async (): Promise<T> => {
try {
await previous;
}
catch (e: any) { /* ignore previous rejection to keep queue alive */ }
return await work();
})();
const queued = next.then(() => { }, () => { });
deviceWriteQueues.set(deviceId, queued);
return next.finally(() => {
if (deviceWriteQueues.get(deviceId) == queued) {
deviceWriteQueues.delete(deviceId);
}
});
}
function createCharProperties(props: number): BleCharacteristicProperties {
const result: BleCharacteristicProperties = {
read: false,
write: false,
notify: false,
indicate: false,
canRead: false,
canWrite: false,
canNotify: false,
writeWithoutResponse: false
};
result.read = (props & BluetoothGattCharacteristic.PROPERTY_READ) !== 0;
result.write = (props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0;
result.notify = (props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0;
result.indicate = (props & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0;
result.writeWithoutResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0;
result.canRead = result.read;
const writeWithoutResponse = result.writeWithoutResponse!;
result.canWrite = (result.write != null && result.write) || (writeWithoutResponse != null && writeWithoutResponse);
result.canNotify = result.notify;
return result;
}
// 定义 PendingCallback 类型和实现类
interface PendingCallback {
resolve: (data: any) => void;
reject: (err?: any) => void;
timer?: number; // Changed from any to number
}
class PendingCallbackImpl implements PendingCallback {
override resolve: (data: any) => void;
override reject: (err?: any) => void;
override timer?: number; // Changed from any to number
constructor(resolve: (data: any) => void, reject: (err?: any) => void, timer?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
// 全局回调管理(必须在类外部声明)
let pendingCallbacks: Map<string, PendingCallback>;
let notifyCallbacks: Map<string, BleDataReceivedCallback>;
// 在全局范围内初始化
pendingCallbacks = new Map<string, PendingCallback>();
notifyCallbacks = new Map<string, BleDataReceivedCallback>();
// 服务发现等待队列deviceId -> 回调数组
const serviceDiscoveryWaiters = new Map<string, ((services: BleService[] | null, error?: Error) => void)[]>();
// 服务发现状态deviceId -> 是否已发现
const serviceDiscovered = new Map<string, boolean>();
// 服务发现重试deviceId -> 尝试次数
const serviceDiscoveryAttempts = new Map<string, number>();
const SERVICE_DISCOVERY_MAX_RETRIES = 3;
const SERVICE_DISCOVERY_RETRY_DELAY_MS = 600;
// 特征发现等待队列deviceId|serviceId -> 回调数组
const characteristicDiscoveryWaiters = new Map<string, ((characteristics: BleCharacteristic[] | null, error?: Error) => void)[]>();
class GattCallback extends BluetoothGattCallback {
constructor() {
super();
}
override onServicesDiscovered(gatt: BluetoothGatt, status: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:112', 'ak onServicesDiscovered',gatt);
const deviceId = gatt.getDevice().getAddress();
if (status == BluetoothGatt.GATT_SUCCESS) {
const attempt = serviceDiscoveryAttempts.get(deviceId) ?? 0;
const services = gatt.getServices();
const result: BleService[] = [];
let size = 0;
if (services != null) {
const servicesList = services;
size = servicesList.size;
for (let i = 0; i < size; i++) {
const service = servicesList.get(i as Int);
if (service != null) {
const bleService: BleService = {
uuid: service.getUuid().toString(),
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
result.push(bleService);
}
}
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:133', '[ServiceManager] onServicesDiscovered size=', size, 'attempt=', attempt, 'device=', deviceId);
if (result.length == 0 && attempt < SERVICE_DISCOVERY_MAX_RETRIES) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:135', '[ServiceManager] services empty after discovery, retrying', deviceId, 'nextAttempt=', attempt + 1);
serviceDiscoveryAttempts.set(deviceId, attempt + 1);
serviceDiscovered.delete(deviceId);
setTimeout(() => {
try {
const dm = DeviceManager.getInstance();
const currentGatt = dm.getGattInstance(deviceId);
const target = currentGatt != null ? currentGatt : gatt;
if (target != null) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:144', '[ServiceManager] retry discoverServices for', deviceId);
target.discoverServices();
}
}
catch (e: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:148', '[ServiceManager] retry discoverServices failed', e);
}
}, SERVICE_DISCOVERY_RETRY_DELAY_MS);
return;
}
let finalResult: BleService[] | null = result;
if (result.length == 0) {
const cached = ServiceManager.getInstance().getCachedServices(deviceId);
if (cached != null && cached.length > 0) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:154', '[ServiceManager] discovery returned empty, using cached services for', deviceId, 'len=', cached.length);
finalResult = cached;
} else {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:154', '[ServiceManager] discovery returned empty after retries for', deviceId);
serviceDiscoveryAttempts.delete(deviceId);
const waitersFail = serviceDiscoveryWaiters.get(deviceId);
if (waitersFail != null && waitersFail.length > 0) {
for (let i = 0; i < waitersFail.length; i++) {
const cb = waitersFail[i];
if (cb != null) {
cb(null, new Error('服务发现返回空列表'));
}
}
serviceDiscoveryWaiters.delete(deviceId);
}
return;
}
}
serviceDiscoveryAttempts.delete(deviceId);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:154', `服务发现成功: ${deviceId}, services=${finalResult != null ? finalResult.length : 0}`);
serviceDiscovered.set(deviceId, true);
ServiceManager.getInstance().handleServicesDiscovered(deviceId, finalResult ?? []);
const waiters = serviceDiscoveryWaiters.get(deviceId);
if (waiters != null && waiters.length > 0) {
for (let i = 0; i < waiters.length; i++) {
const cb = waiters[i];
if (cb != null) {
cb(finalResult ?? [], null);
}
}
serviceDiscoveryWaiters.delete(deviceId);
}
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:166', `服务发现失败: ${deviceId}, status: ${status}`);
serviceDiscoveryAttempts.delete(deviceId);
// 失败时也要通知等待队列
const waiters = serviceDiscoveryWaiters.get(deviceId);
if (waiters != null && waiters.length > 0) {
for (let i = 0; i < waiters.length; i++) {
const cb = waiters[i];
if (cb != null) {
cb(null, new Error('服务发现失败'));
}
}
serviceDiscoveryWaiters.delete(deviceId);
}
}
}
override onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int): void {
const deviceId = gatt.getDevice().getAddress();
if (newState == BluetoothGatt.STATE_CONNECTED) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:182', `设备已连接: ${deviceId}`);
ServiceManager.getInstance().resetDiscoveryState(deviceId);
DeviceManager.handleConnectionStateChange(deviceId, 2, null); // 2 = STATE_CONNECTED
}
else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:186', `设备已断开: ${deviceId}`);
serviceDiscovered.delete(deviceId);
serviceDiscoveryAttempts.delete(deviceId);
ServiceManager.getInstance().handleDisconnected(deviceId);
DeviceManager.handleConnectionStateChange(deviceId, 0, null); // 0 = STATE_DISCONNECTED
}
}
override onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:194', 'ak onCharacteristicChanged');
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|notify`;
const callback = notifyCallbacks.get(key);
const value = characteristic.getValue();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:201', '[onCharacteristicChanged]', key, value);
// Check for PING packets (0xAA 0x04 0x00 ...)
if (value != null && value.size >= 4) {
const arr = new Uint8Array(value.size);
for (let i = 0 as Int; i < value.size; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
if (arr[0] === 0xAA && arr[2] === 0x00) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:201', '[BLE] PING packet detected:', Array.from(arr));
}
}
if (callback != null && value != null) {
const valueLength = value.size;
const arr = new Uint8Array(valueLength);
for (let i = 0 as Int; i < valueLength; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
// 保存接收日志
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:210', `
INSERT INTO ble_data_log (device_id, service_id, char_id, direction, data, timestamp)
VALUES ('${deviceId}', '${serviceId}', '${charId}', 'recv', '${Array.from(arr).join(',')}', ${Date.now()})
`);
callback(arr);
}
}
override onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:219', 'ak onCharacteristicRead', status);
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|read`;
const pending = pendingCallbacks.get(key);
const value = characteristic.getValue();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:226', '[onCharacteristicRead]', key, 'status=', status, 'value=', value);
if (pending != null) {
try {
const timer = pending.timer;
if (timer != null) {
clearTimeout(timer);
pending.timer = null;
}
pendingCallbacks.delete(key);
if (status == BluetoothGatt.GATT_SUCCESS && value != null) {
const valueLength = value.size;
const arr = new Uint8Array(valueLength);
for (let i = 0 as Int; i < valueLength; i++) {
const v = value[i as Int];
arr[i] = v != null ? v : 0;
}
// debug: log raw bytes and decoded string (helpful on Android native path)
try {
const hex = Array.from(arr).map((b): string => b.toString(16).padStart(2, '0')).join(' ');
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:246', '[ServiceManager] onCharacteristicRead raw hex:', hex, 'len=', arr.length, 'key=', key);
try {
const decoded = new TextDecoder().decode(arr);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:249', '[ServiceManager] onCharacteristicRead decoded string:', decoded);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:250', '[ServiceManager] decode error', e);
}
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:251', '[ServiceManager] failed to log read buffer', e);
}
// resolve with ArrayBuffer
pending.resolve(arr.buffer as ArrayBuffer);
}
else {
pending.reject(new Error('Characteristic read failed'));
}
}
catch (e: any) {
try {
pending.reject(e);
}
catch (e2: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:259', e2);
}
}
}
}
override onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int): void {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:265', 'ak onCharacteristicWrite', status);
const deviceId = gatt.getDevice().getAddress();
const serviceId = characteristic.getService().getUuid().toString();
const charId = characteristic.getUuid().toString();
const key = `${deviceId}|${serviceId}|${charId}|write`;
const pending = pendingCallbacks.get(key);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:271', '[onCharacteristicWrite]', key, 'status=', status);
if (pending != null) {
try {
const timer = pending.timer;
if (timer != null) {
clearTimeout(timer);
}
pendingCallbacks.delete(key);
if (status == BluetoothGatt.GATT_SUCCESS) {
pending.resolve('ok');
}
else {
pending.reject(new Error('Characteristic write failed'));
}
}
catch (e: any) {
try {
pending.reject(e);
}
catch (e2: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:285', e2);
}
}
}
}
}
// 导出单例实例供外部使用
export const gattCallback = new GattCallback();
export class ServiceManager {
private static instance: ServiceManager | null = null;
private services = new Map<string, BleService[]>();
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
private deviceManager = DeviceManager.getInstance();
private constructor() { }
static getInstance(): ServiceManager {
if (ServiceManager.instance == null) {
ServiceManager.instance = new ServiceManager();
}
return ServiceManager.instance!;
}
public resetDiscoveryState(deviceId: string, clearServiceCache: boolean = false): void {
serviceDiscoveryAttempts.delete(deviceId);
serviceDiscovered.delete(deviceId);
serviceDiscoveryWaiters.delete(deviceId);
if (clearServiceCache == true) {
this.services.delete(deviceId);
}
}
public handleServicesDiscovered(deviceId: string, services: BleService[]): void {
this.services.set(deviceId, services);
}
public getCachedServices(deviceId: string): BleService[] | null {
const cached = this.services.get(deviceId);
return cached != null ? cached : null;
}
public handleDisconnected(deviceId: string): void {
this.resetDiscoveryState(deviceId);
const keysToRemove: string[] = [];
this.characteristics.forEach((_value, key) => {
if (key != null && key.indexOf(deviceId + '|') == 0) {
keysToRemove.push(key);
}
});
for (let i = 0; i < keysToRemove.length; i++) {
this.characteristics.delete(keysToRemove[i]);
}
}
getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): any | Promise<BleService[]> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts', 'ak start getservice', deviceId);
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) {
if (callback != null) {
callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
}
return Promise.reject(new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts', 'ak serviceDiscovered', gatt);
// 如果服务已发现,直接返回
if (serviceDiscovered.get(deviceId) == true) {
const services = gatt.getServices();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:333', services);
const result: BleService[] = [];
if (services != null) {
const servicesList = services;
const size = servicesList.size;
if (size > 0) {
for (let i = 0 as Int; i < size; i++) {
const service = servicesList != null ? servicesList.get(i) : servicesList[i];
if (service != null) {
const bleService: BleService = {
uuid: service.getUuid().toString(),
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
result.push(bleService);
if (bleService.uuid == getFullUuid('0001')) {
const device = this.deviceManager.getDevice(deviceId);
if (device != null) {
device.serviceId = bleService.uuid;
this.getCharacteristics(deviceId, device.serviceId!, (chars, err) => {
if (err == null && chars != null) {
const writeChar = chars.find((c): boolean => c.uuid == getFullUuid('0010'));
const notifyChar = chars.find((c): boolean => c.uuid == getFullUuid('0011'));
if (writeChar != null)
device.writeCharId = writeChar.uuid;
if (notifyChar != null)
device.notifyCharId = notifyChar.uuid;
}
});
}
}
}
}
}
}
if (callback != null) {
callback(result, null);
}
return Promise.resolve(result);
}
// 未发现则发起服务发现并加入等待队列
if (!serviceDiscoveryWaiters.has(deviceId)) {
console.log('ak should start serviceDiscoveryWaiters')
serviceDiscoveryWaiters.set(deviceId, []);
gatt.discoverServices();
}
return new Promise<BleService[]>((resolve, reject) => {
const cb = (services: BleService[] | null, error?: Error) => {
if (error != null)
reject(error);
else
resolve(services ?? []);
if (callback != null)
callback(services, error);
};
const arr = serviceDiscoveryWaiters.get(deviceId);
if (arr != null)
arr.push(cb);
});
}
getCharacteristics(deviceId: string, serviceId: string, callback: (characteristics: BleCharacteristic[] | null, error?: Error) => void): void {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
return callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", ""));
// 如果服务还没发现,等待服务发现后再查特征
if (serviceDiscovered.get(deviceId) !== true) {
// 先注册到服务发现等待队列
this.getServices(deviceId, (services, err) => {
if (err != null) {
callback(null, err);
}
else {
this.getCharacteristics(deviceId, serviceId, callback);
}
});
return;
}
// 服务已发现,正常获取特征
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
return callback(null, new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", ""));
const chars = service.getCharacteristics();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:403', chars);
const result: BleCharacteristic[] = [];
if (chars != null) {
const characteristicsList = chars;
const size = characteristicsList.size;
const bleService: BleService = {
uuid: serviceId,
isPrimary: service.getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY
};
for (let i = 0 as Int; i < size; i++) {
const char = characteristicsList != null ? characteristicsList.get(i as Int) : characteristicsList[i];
if (char != null) {
const props = char.getProperties();
try {
const charUuid = char.getUuid() != null ? char.getUuid().toString() : '';
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:418', '[ServiceManager] characteristic uuid=', charUuid);
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:419', '[ServiceManager] failed to read char uuid', e);
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:420', props);
const bleCharacteristic: BleCharacteristic = {
uuid: char.getUuid().toString(),
service: bleService,
properties: createCharProperties(props)
};
result.push(bleCharacteristic);
}
}
}
callback(result, null);
}
public async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|read`;
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:441', key);
return new Promise<ArrayBuffer>((resolve, reject) => {
const timer = setTimeout(() => {
pendingCallbacks.delete(key);
reject(new AkBleErrorImpl(AkBluetoothErrorCode.ConnectionTimeout, "Connection timeout", ""));
}, 5000);
const resolveAdapter = (data: any) => { __f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:447', 'read resolve:', data); resolve(data as ArrayBuffer); };
const rejectAdapter = (err?: any) => { reject(new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Unknown error occurred", "")); };
pendingCallbacks.set(key, new PendingCallbackImpl(resolveAdapter, rejectAdapter, timer));
if (gatt.readCharacteristic(char) == false) {
clearTimeout(timer);
pendingCallbacks.delete(key);
reject(new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Unknown error occurred", ""));
}
else {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:456', 'read should be succeed', key);
}
});
}
public async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: Uint8Array, options?: WriteCharacteristicOptions): Promise<boolean> {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:462', '[writeCharacteristic] deviceId:', deviceId, 'serviceId:', serviceId, 'characteristicId:', characteristicId, 'data:', data);
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:465', '[writeCharacteristic] gatt is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
}
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:470', '[writeCharacteristic] service is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
}
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:475', '[writeCharacteristic] characteristic is null');
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
}
const key = `${deviceId}|${serviceId}|${characteristicId}|write`;
const wantsNoResponse = options != null && options.waitForResponse == false;
let retryMaxAttempts = 20;
let retryDelay = 100;
let giveupTimeout = 20000;
if (options != null) {
try {
if (options.maxAttempts != null) {
const parsedAttempts = Math.floor(options.maxAttempts as number);
if (!isNaN(parsedAttempts) && parsedAttempts > 0)
retryMaxAttempts = parsedAttempts;
}
}
catch (e: any) { }
try {
if (options.retryDelayMs != null) {
const parsedDelay = Math.floor(options.retryDelayMs as number);
if (!isNaN(parsedDelay) && parsedDelay >= 0)
retryDelay = parsedDelay;
}
}
catch (e: any) { }
try {
if (options.giveupTimeoutMs != null) {
const parsedGiveup = Math.floor(options.giveupTimeoutMs as number);
if (!isNaN(parsedGiveup) && parsedGiveup > 0)
giveupTimeout = parsedGiveup;
}
}
catch (e: any) { }
}
const gattInstance = gatt;
const executeWrite = (): Promise<boolean> => {
return new Promise<boolean>((resolve, _reject) => {
const initialTimeout = Math.max(giveupTimeout + 5000, 10000);
let timer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:510', '[writeCharacteristic] timeout');
resolve(false);
}, initialTimeout);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:513', '[writeCharacteristic] initial timeout set to', initialTimeout, 'ms for', key);
const resolveAdapter = (data: any) => {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:515', '[writeCharacteristic] resolveAdapter called');
resolve(true);
};
const rejectAdapter = (err?: any) => {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:519', '[writeCharacteristic] rejectAdapter called', err);
resolve(false);
};
pendingCallbacks.set(key, new PendingCallbackImpl(resolveAdapter, rejectAdapter, timer));
const byteArray = new ByteArray(data.length as Int);
for (let i = 0 as Int; i < data.length; i++) {
byteArray[i] = data[i].toByte();
}
const forceWriteTypeNoResponse = options != null && options.forceWriteTypeNoResponse == true;
let usesNoResponse = forceWriteTypeNoResponse || wantsNoResponse;
try {
if (usesNoResponse == false) {
const props = char.getProperties();
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:532', '[writeCharacteristic] characteristic properties mask=', props);
usesNoResponse = (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0;
}
if (usesNoResponse) {
try {
char.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
}
catch (e: any) { }
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:537', '[writeCharacteristic] using WRITE_TYPE_NO_RESPONSE');
}
else {
try {
char.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
}
catch (e: any) { }
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:540', '[writeCharacteristic] using WRITE_TYPE_DEFAULT');
}
}
catch (e: any) {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:543', '[writeCharacteristic] failed to inspect/set write type', e);
}
const maxAttempts = retryMaxAttempts;
function attemptWrite(att: Int): void {
try {
let setOk = true;
try {
const setRes = char.setValue(byteArray);
if (typeof setRes == 'boolean' && setRes == false) {
setOk = false;
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:553', '[writeCharacteristic] setValue returned false for', key, 'attempt', att);
}
}
catch (e: any) {
setOk = false;
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:557', '[writeCharacteristic] setValue threw for', key, 'attempt', att, e);
}
if (setOk == false) {
if (att >= maxAttempts) {
try {
clearTimeout(timer);
}
catch (e: any) { }
pendingCallbacks.delete(key);
resolve(false);
return;
}
setTimeout(() => { attemptWrite((att + 1) as Int); }, retryDelay);
return;
}
try {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:570', '[writeCharacteristic] attempt', att, 'calling gatt.writeCharacteristic');
const r = gattInstance.writeCharacteristic(char);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:572', '[writeCharacteristic] attempt', att, 'result=', r);
if (r == true) {
if (usesNoResponse) {
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:575', '[writeCharacteristic] WRITE_TYPE_NO_RESPONSE success for', key);
try {
clearTimeout(timer);
}
catch (e: any) { }
pendingCallbacks.delete(key);
resolve(true);
return;
}
try {
clearTimeout(timer);
}
catch (e: any) { }
const extra = 20000;
timer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:585', '[writeCharacteristic] timeout after write initiated');
resolve(false);
}, extra);
const pendingEntry = pendingCallbacks.get(key);
if (pendingEntry != null)
pendingEntry.timer = timer;
return;
}
}
catch (e: any) {
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:593', '[writeCharacteristic] attempt', att, 'exception when calling writeCharacteristic', e);
}
if (att < maxAttempts) {
const nextAtt = (att + 1) as Int;
setTimeout(() => { attemptWrite(nextAtt); }, retryDelay);
return;
}
if (usesNoResponse) {
try {
clearTimeout(timer);
}
catch (e: any) { }
pendingCallbacks.delete(key);
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:603', '[writeCharacteristic] all attempts failed with WRITE_NO_RESPONSE for', key);
resolve(false);
return;
}
try {
clearTimeout(timer);
}
catch (e: any) { }
const giveupTimeoutLocal = giveupTimeout;
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:609', '[writeCharacteristic] all attempts failed; waiting for late callback up to', giveupTimeoutLocal, 'ms for', key);
const giveupTimer = setTimeout(() => {
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:612', '[writeCharacteristic] giveup timeout expired for', key);
resolve(false);
}, giveupTimeoutLocal);
const pendingEntryAfter = pendingCallbacks.get(key);
if (pendingEntryAfter != null)
pendingEntryAfter.timer = giveupTimer;
}
catch (e: any) {
clearTimeout(timer);
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:620', '[writeCharacteristic] Exception in attemptWrite', e);
resolve(false);
}
}
try {
attemptWrite(1 as Int);
}
catch (e: any) {
clearTimeout(timer);
pendingCallbacks.delete(key);
__f__('error', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:630', '[writeCharacteristic] Exception before attempting write', e);
resolve(false);
}
});
};
return enqueueDeviceWrite(deviceId, executeWrite);
}
public async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|notify`;
notifyCallbacks.set(key, onData);
if (gatt.setCharacteristicNotification(char, true) == false) {
notifyCallbacks.delete(key);
throw new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Failed to unsubscribe characteristic", "");
}
else {
// 写入 CCCD 描述符,启用 notify
const descriptor = char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (descriptor != null) {
// 设置描述符值
const value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
descriptor.setValue(value);
const writedescript = gatt.writeDescriptor(descriptor);
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:660', 'subscribeCharacteristic: CCCD written for notify', writedescript);
}
else {
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:662', 'subscribeCharacteristic: CCCD descriptor not found!');
}
__f__('log', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:664', 'subscribeCharacteristic ok!!');
}
}
public async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
const gatt = this.deviceManager.getGattInstance(deviceId);
if (gatt == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.DeviceNotFound, "Device not found", "");
const service = gatt.getService(UUID.fromString(serviceId));
if (service == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.ServiceNotFound, "Service not found", "");
const char = service.getCharacteristic(UUID.fromString(characteristicId));
if (char == null)
throw new AkBleErrorImpl(AkBluetoothErrorCode.CharacteristicNotFound, "Characteristic not found", "");
const key = `${deviceId}|${serviceId}|${characteristicId}|notify`;
notifyCallbacks.delete(key);
if (gatt.setCharacteristicNotification(char, false) == false) {
throw new AkBleErrorImpl(AkBluetoothErrorCode.UnknownError, "Failed to unsubscribe characteristic", "");
}
}
// 自动发现所有服务和特征
public async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
const services = await this.getServices(deviceId, null) as BleService[];
const allCharacteristics: BleCharacteristic[] = [];
for (const service of services) {
await new Promise<void>((resolve, reject) => {
this.getCharacteristics(deviceId, service.uuid, (chars, err) => {
if (err != null)
reject(err);
else {
if (chars != null)
allCharacteristics.push(...chars);
resolve(void 0);
}
});
});
}
return { services, characteristics: allCharacteristics } as AutoDiscoverAllResult;
}
// 自动订阅所有支持 notify/indicate 的特征
public async subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
const { services, characteristics } = await this.autoDiscoverAll(deviceId);
for (const char of characteristics) {
if (char.properties.notify || char.properties.indicate) {
try {
await this.subscribeCharacteristic(deviceId, char.service.uuid, char.uuid, onData);
}
catch (e: any) {
// 可以选择忽略单个特征订阅失败
__f__('warn', 'at uni_modules/ak-sbsrv/utssdk/app-android/service_manager.uts:710', `订阅特征 ${char.uuid} 失败:`, e);
}
}
}
}
}
//# sourceMappingURL=service_manager.uts.map

View File

@@ -0,0 +1,281 @@
import type {
BleDevice,
BleConnectionState,
BleEvent,
BleEventCallback,
BleEventPayload,
BleConnectOptionsExt,
AutoBleInterfaces,
SendDataPayload,
BleOptions,
MultiProtocolDevice,
BleProtocolType,
ScanDevicesOptions
} from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
class DeviceContext {
device: BleDevice;
protocol: BleProtocolType;
state: BleConnectionState;
handler: ProtocolHandler;
constructor(device: BleDevice, protocol: BleProtocolType, handler: ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0;
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>();
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
let defaultBluetoothService: BluetoothService | null = null;
let connectionHooked = false;
function emit(event: BleEvent, payload: BleEventPayload) {
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try { cb(payload); } catch (e) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler, bluetoothService?: BluetoothService) {
super(bluetoothService);
this._raw = raw ?? null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices == 'function') {
await rawTyped.scanDevices(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect == 'function') {
await rawTyped.connect(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect == 'function') {
await rawTyped.disconnect(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData == 'function') {
await rawTyped.sendData(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect == 'function') {
return await rawTyped.autoConnect(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' };
}
}
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object') return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] == 'function') return true;
if (typeof r['connect'] == 'function') return true;
if (typeof r['disconnect'] == 'function') return true;
if (typeof r['sendData'] == 'function') return true;
if (typeof r['autoConnect'] == 'function') return true;
if (typeof r['protocol'] == 'string') return true;
return false;
}
function ensureConnectionHook() {
if (connectionHooked) return;
connectionHooked = true;
const dm = DeviceManager.getInstance();
dm.onConnectionStateChange((deviceId, state) => {
let handled = false;
deviceMap.forEach((ctx) => {
if (ctx.device.deviceId == deviceId) {
ctx.state = state;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol: ctx.protocol, state });
handled = true;
}
});
if (!handled) {
emit('connectionStateChanged', { event: 'connectionStateChanged', device: { deviceId, name: '', rssi: 0 }, protocol: activeProtocol, state });
}
});
}
export const registerProtocolHandler = (handler: any) => {
if (handler == null) return;
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = handler as ProtocolHandler;
} else if (isRawProtocolHandler(handler)) {
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler, defaultBluetoothService);
(activeHandler as ProtocolHandler).protocol = proto;
} else {
console.warn('[AKBLE][Harmony] registerProtocolHandler unsupported handler', handler);
return;
}
activeProtocol = proto;
ensureConnectionHook();
}
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
ensureDefaultProtocolHandler();
if (activeHandler == null) {
console.log('[AKBLE][Harmony] no active scan handler');
return;
}
const handler = activeHandler as ProtocolHandler;
const original = options ?? null;
const scanOptions: ScanDevicesOptions = {} as ScanDevicesOptions;
if (original != null) {
if (original.protocols != null) scanOptions.protocols = original.protocols;
if (original.optionalServices != null) scanOptions.optionalServices = original.optionalServices;
if (original.timeout != null) scanOptions.timeout = original.timeout;
}
const userFound = original?.onDeviceFound ?? null;
scanOptions.onDeviceFound = (device: BleDevice) => {
emit('deviceFound', { event: 'deviceFound', device });
if (userFound != null) {
try { userFound(device); } catch (err) { }
}
};
const userFinished = original?.onScanFinished ?? null;
scanOptions.onScanFinished = () => {
emit('scanFinished', { event: 'scanFinished' });
if (userFinished != null) {
try { userFinished(); } catch (err) { }
}
};
try {
await handler.scanDevices(scanOptions);
} catch (e) {
console.warn('[AKBLE][Harmony] scan handler error', e);
}
}
export const connectDevice = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<void> => {
const handler = activeHandler;
if (handler == null) throw new Error('No protocol handler');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2;
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
}
export const disconnectDevice = async (deviceId: string, protocol: BleProtocolType): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null) return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
deviceMap.delete(getDeviceKey(deviceId, protocol));
}
export const sendData = async (payload: SendDataPayload, options?: BleOptions): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null) throw new Error('Device not connected');
if (ctx.handler == null) throw new Error('sendData not supported for this protocol');
await ctx.handler.sendData(ctx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: ctx.device, protocol: payload.protocol, data: payload.data });
}
export const getConnectedDevices = (): MultiProtocolDevice[] => {
const result: MultiProtocolDevice[] = [];
deviceMap.forEach((ctx: DeviceContext) => {
result.push({
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
});
});
return result;
}
export const getConnectionState = (deviceId: string, protocol: BleProtocolType): BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null) return 0;
return ctx.state;
}
export const on = (event: BleEvent, callback: BleEventCallback) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
}
export const off = (event: BleEvent, callback?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
} else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
}
function getDeviceKey(deviceId: string, protocol: BleProtocolType): string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null) throw new Error('autoConnect not supported for this protocol');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
return await handler.autoConnect(device, options) as AutoBleInterfaces;
}
function ensureDefaultProtocolHandler(): void {
if (activeHandler != null) return;
const service = defaultBluetoothService;
if (service == null) return;
try {
const dm = DeviceManager.getInstance();
const raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions) => dm.startScan(options ?? {} as ScanDevicesOptions),
connect: (device, options?: BleConnectOptionsExt) => dm.connectDevice(device.deviceId, options),
disconnect: (device) => dm.disconnectDevice(device.deviceId),
autoConnect: () => Promise.resolve({ serviceId: '', writeCharId: '', notifyCharId: '' })
};
const wrapper = new ProtocolHandlerWrapper(raw, service);
activeHandler = wrapper;
activeProtocol = raw.protocol as BleProtocolType;
ensureConnectionHook();
console.log('[AKBLE][Harmony] default protocol handler registered', activeProtocol);
} catch (e) {
console.warn('[AKBLE][Harmony] register default protocol handler failed', e);
}
}
export const setDefaultBluetoothService = (service: BluetoothService) => {
defaultBluetoothService = service;
ensureDefaultProtocolHandler();
};

View File

@@ -0,0 +1,6 @@
{
"dependencies": [
"@ohos.bluetooth.ble",
"@ohos.base"
]
}

View File

@@ -0,0 +1,280 @@
import type { BleDevice, BleConnectOptionsExt, BleConnectionState, BleConnectionStateChangeCallback, ScanDevicesOptions } from '../interface.uts';
import ble from '@ohos.bluetooth.ble';
import type { BusinessError } from '@ohos.base';
type PendingConnect = {
resolve: () => void;
reject: (err?: any) => void;
timer?: number;
};
function now(): number {
return Date.now();
}
export class DeviceManager {
private static instance: DeviceManager | null = null;
private central: any | null = null;
private devices = new Map<string, BleDevice>();
private connectionStates = new Map<string, BleConnectionState>();
private connectionListeners: BleConnectionStateChangeCallback[] = [];
private pendingConnects = new Map<string, PendingConnect>();
private scanOptions: ScanDevicesOptions | null = null;
private scanTimer: number | null = null;
private gattMap = new Map<string, any>();
private scanning: boolean = false;
private eventsBound: boolean = false;
private constructor() {}
static getInstance(): DeviceManager {
if (DeviceManager.instance == null) {
DeviceManager.instance = new DeviceManager();
}
return DeviceManager.instance!;
}
private ensureCentral(): any {
if (this.central != null) return this.central;
try {
this.central = ble.createBluetoothCentralManager();
this.bindCentralEvents();
} catch (e) {
console.warn('[AKBLE][Harmony] createBluetoothCentralManager failed', e);
throw e;
}
return this.central!;
}
private bindCentralEvents() {
if (this.eventsBound) return;
this.eventsBound = true;
const central = this.central;
if (central == null) return;
try {
central.on('scanResult', (result: any) => {
try { this.handleScanResult(result); } catch (e) { console.warn('[AKBLE][Harmony] scanResult handler error', e); }
});
} catch (e) {
console.warn('[AKBLE][Harmony] central.on scanResult failed', e);
}
try {
central.on('bleDeviceFind', (result: any) => {
try { this.handleScanResult(result); } catch (e) { console.warn('[AKBLE][Harmony] bleDeviceFind handler error', e); }
});
} catch (e) {
/* optional */
}
try {
central.on('BLEConnectionStateChange', (state: any) => {
try { this.handleConnectionEvent(state); } catch (err) { console.warn('[AKBLE][Harmony] connection event error', err); }
});
} catch (e) {
console.warn('[AKBLE][Harmony] central.on connection change failed', e);
}
}
private handleScanResult(result: any) {
const list: any[] = result?.devices ?? result ?? [];
if (!Array.isArray(list)) return;
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item == null) continue;
const deviceId: string = item.deviceId ?? item.device?.deviceId ?? '';
if (!deviceId) continue;
const name: string = item.name ?? item.deviceName ?? 'Unknown';
const rssi: number = item.rssi ?? item.RSSI ?? 0;
let device = this.devices.get(deviceId);
if (device == null) {
device = { deviceId, name, rssi, lastSeen: now() };
this.devices.set(deviceId, device);
} else {
device.name = name;
device.rssi = rssi;
device.lastSeen = now();
}
const cb = this.scanOptions?.onDeviceFound;
if (cb != null) {
try { cb(device); } catch (e) { console.warn('[AKBLE][Harmony] onDeviceFound error', e); }
}
}
}
private handleConnectionEvent(evt: any) {
const deviceId: string = evt?.deviceId ?? evt?.device?.deviceId ?? '';
if (!deviceId) return;
const connected = evt?.state === ble.BLEConnectionState.STATE_CONNECTED || evt?.connected === true;
const state: BleConnectionState = connected ? 2 : 0;
this.connectionStates.set(deviceId, state);
const pending = this.pendingConnects.get(deviceId);
if (pending != null) {
this.pendingConnects.delete(deviceId);
if (pending.timer != null) clearTimeout(pending.timer);
if (connected) {
try { pending.resolve(); } catch (e) { }
} else {
try { pending.reject(new Error('连接断开')); } catch (e) { }
}
}
for (let i = 0; i < this.connectionListeners.length; i++) {
const listener = this.connectionListeners[i];
try { listener(deviceId, state); } catch (e) { console.warn('[AKBLE][Harmony] listener error', e); }
}
}
async startScan(options: ScanDevicesOptions): Promise<void> {
const central = this.ensureCentral();
this.scanOptions = options ?? {} as ScanDevicesOptions;
if (this.scanning) {
await this.stopScan();
}
this.scanning = true;
if (this.scanTimer != null) {
clearTimeout(this.scanTimer);
this.scanTimer = null;
}
return new Promise<void>((resolve, reject) => {
try {
const filter = { interval: 0 } as any;
const res = central.startScan?.(filter) ?? central.startScan?.();
if (this.scanOptions?.timeout != null && this.scanOptions.timeout > 0) {
this.scanTimer = setTimeout(() => {
this.stopScanInternal();
}, this.scanOptions.timeout);
}
if (res instanceof Promise) {
res.then(() => resolve()).catch((err: BusinessError) => {
this.scanning = false;
reject(err);
});
} else {
resolve();
}
} catch (e) {
this.scanning = false;
reject(e);
}
});
}
async stopScan(): Promise<void> {
this.stopScanInternal();
return Promise.resolve();
}
private stopScanInternal() {
if (!this.scanning) return;
this.scanning = false;
try {
this.central?.stopScan?.();
} catch (e) {
console.warn('[AKBLE][Harmony] stopScan failed', e);
}
if (this.scanTimer != null) {
clearTimeout(this.scanTimer);
this.scanTimer = null;
}
const finished = this.scanOptions?.onScanFinished;
this.scanOptions = null;
if (finished != null) {
try { finished(); } catch (e) { console.warn('[AKBLE][Harmony] onScanFinished error', e); }
}
}
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
this.ensureCentral();
const timeout = options?.timeout ?? 15000;
if (this.connectionStates.get(deviceId) == 2) {
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingConnects.delete(deviceId);
reject(new Error('连接超时'));
}, timeout);
this.pendingConnects.set(deviceId, { resolve, reject, timer });
try {
let gatt = this.gattMap.get(deviceId);
if (gatt == null) {
gatt = ble.createGattClientDevice(deviceId);
this.gattMap.set(deviceId, gatt);
}
const connectRes = gatt.connect?.(true);
if (connectRes instanceof Promise) {
connectRes.then(() => {
clearTimeout(timer);
this.pendingConnects.delete(deviceId);
this.connectionStates.set(deviceId, 2);
resolve();
}).catch((err: BusinessError) => {
clearTimeout(timer);
this.pendingConnects.delete(deviceId);
reject(err);
});
} else {
// Some implementations return immediate state; rely on connection event to resolve.
}
} catch (e) {
clearTimeout(timer);
this.pendingConnects.delete(deviceId);
reject(e);
}
});
}
async disconnectDevice(deviceId: string): Promise<void> {
const gatt = this.gattMap.get(deviceId);
if (gatt == null) return;
try {
const res = gatt.disconnect?.();
if (res instanceof Promise) {
await res;
}
} catch (e) {
console.warn('[AKBLE][Harmony] disconnect failed', e);
}
this.connectionStates.set(deviceId, 0);
for (let i = 0; i < this.connectionListeners.length; i++) {
const listener = this.connectionListeners[i];
try { listener(deviceId, 0); } catch (err) { console.warn('[AKBLE][Harmony] disconnect listener error', err); }
}
}
reconnectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
const attempts = options?.maxAttempts ?? 3;
const interval = options?.interval ?? 3000;
let count = 0;
const attempt = (): Promise<void> => {
return this.connectDevice(deviceId, options).catch((err) => {
count++;
if (count >= attempts) throw err;
return new Promise<void>((resolve) => {
setTimeout(() => resolve(attempt()), interval);
});
});
};
return attempt();
}
getConnectedDevices(): BleDevice[] {
const result: BleDevice[] = [];
this.devices.forEach((device, id) => {
if (this.connectionStates.get(id) == 2) {
result.push(device);
}
});
return result;
}
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
this.connectionListeners.push(listener);
}
getDevice(deviceId: string): BleDevice | null {
return this.devices.get(deviceId) ?? null;
}
getGatt(deviceId: string): any | null {
return this.gattMap.get(deviceId) ?? null;
}
}

View File

@@ -0,0 +1,9 @@
import type { DfuManagerType, DfuOptions } from '../interface.uts';
class HarmonyDfuManager implements DfuManagerType {
async startDfu(_deviceId: string, _firmwareBytes: Uint8Array, _options?: DfuOptions): Promise<void> {
throw new Error('鸿蒙平台暂未实现 DFU 功能');
}
}
export const dfuManager = new HarmonyDfuManager();

View File

@@ -0,0 +1,90 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback, BleProtocolType, BluetoothService as BluetoothServiceContract } from '../interface.uts';
const serviceManager = ServiceManager.getInstance();
class HarmonyBluetoothService implements BluetoothServiceContract {
scanDevices(options?: ScanDevicesOptions | null): Promise<void> {
return BluetoothManager.scanDevices(options ?? null);
}
async connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt | null): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.connectDevice(deviceId, proto, options ?? null);
}
async disconnectDevice(deviceId: string, protocol?: string): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.disconnectDevice(deviceId, proto);
}
getConnectedDevices(): MultiProtocolDevice[] {
return BluetoothManager.getConnectedDevices();
}
on(event: BleEvent | string, callback: BleEventCallback): void {
BluetoothManager.on(event as BleEvent, callback);
}
off(event: BleEvent | string, callback?: BleEventCallback | null): void {
BluetoothManager.off(event as BleEvent, callback ?? null);
}
getServices(deviceId: string): Promise<BleService[]> {
return serviceManager.getServices(deviceId);
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return serviceManager.getCharacteristics(deviceId, serviceId);
}
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const services = await this.getServices(deviceId);
if (services.length == 0) throw new Error('未发现服务');
let targetService = services[0].uuid;
for (let i = 0; i < services.length; i++) {
const uuid = services[i].uuid ?? '';
if (/^bae/i.test(uuid)) {
targetService = uuid;
break;
}
}
const characteristics = await this.getCharacteristics(deviceId, targetService);
if (characteristics.length == 0) throw new Error('未发现特征值');
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse == true)) {
writeCharId = c.uuid;
}
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) {
notifyCharId = c.uuid;
}
}
if (writeCharId == '' || notifyCharId == '') throw new Error('未找到合适的写入或通知特征');
return { serviceId: targetService, writeCharId, notifyCharId };
}
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, value, options);
}
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export class BluetoothServiceShape extends HarmonyBluetoothService {}
const bluetoothServiceInstance = new BluetoothServiceShape();
BluetoothManager.setDefaultBluetoothService(bluetoothServiceInstance);
export const bluetoothService: BluetoothServiceContract = bluetoothServiceInstance;
export function getBluetoothService(): BluetoothServiceShape {
return bluetoothServiceInstance;
}
export { dfuManager } from './dfu_manager.uts';

View File

@@ -0,0 +1,344 @@
import type { BleService, BleCharacteristic, BleCharacteristicProperties, WriteCharacteristicOptions, AutoDiscoverAllResult, BleDataReceivedCallback } from '../interface.uts';
import type { BleDevice } from '../interface.uts';
import ble from '@ohos.bluetooth.ble';
import type { BusinessError } from '@ohos.base';
import { DeviceManager } from './device_manager.uts';
type PendingRead = {
resolve: (data: ArrayBuffer) => void;
reject: (err?: any) => void;
timer?: number;
};
type CharacteristicChange = {
serviceUuid?: string;
characteristicUuid?: string;
value?: ArrayBuffer | Uint8Array | number[];
};
function toArrayBuffer(value: ArrayBuffer | Uint8Array | number[] | null | undefined): ArrayBuffer {
if (value == null) return new ArrayBuffer(0);
if (value instanceof ArrayBuffer) return value;
if (value instanceof Uint8Array) {
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
}
if (Array.isArray(value)) {
const buf = new Uint8Array(value.length);
for (let i = 0; i < value.length; i++) buf[i] = value[i] ?? 0;
return buf.buffer;
}
return new ArrayBuffer(0);
}
function toUint8Array(value: ArrayBuffer | Uint8Array | number[] | null | undefined): Uint8Array {
if (value instanceof Uint8Array) {
return value;
}
return new Uint8Array(toArrayBuffer(value));
}
function buildProperties(raw: any): BleCharacteristicProperties {
const props = raw?.properties ?? raw ?? {};
const read = props.read === true;
const write = props.write === true || props.writeWithoutResponse === true;
const notify = props.notify === true;
const indicate = props.indicate === true;
const writeNoRsp = props.writeWithoutResponse === true || props.writeNoResponse === true;
return {
read,
write,
notify,
indicate,
writeWithoutResponse: writeNoRsp,
canRead: read,
canWrite: write || writeNoRsp,
canNotify: notify || indicate
};
}
export class ServiceManager {
private static instance: ServiceManager | null = null;
private services = new Map<string, BleService[]>();
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
private pendingReads = new Map<string, PendingRead>();
private notifyCallbacks = new Map<string, BleDataReceivedCallback>();
private boundGattDevices = new Set<string>();
private deviceManager = DeviceManager.getInstance();
private constructor() {}
static getInstance(): ServiceManager {
if (ServiceManager.instance == null) {
ServiceManager.instance = new ServiceManager();
}
return ServiceManager.instance!;
}
private getGattOrThrow(deviceId: string): any {
const gatt = this.deviceManager.getGatt(deviceId);
if (gatt == null) throw new Error('设备未连接');
return gatt;
}
private cacheServices(deviceId: string, services: any[]): BleService[] {
const list: BleService[] = [];
for (let i = 0; i < services.length; i++) {
const svc = services[i];
if (svc == null) continue;
const uuid = svc.uuid ?? svc.serviceUuid ?? '';
if (!uuid) continue;
list.push({ uuid, isPrimary: svc.isPrimary === true });
}
this.services.set(deviceId, list);
return list;
}
private async ensureServices(deviceId: string, gatt: any): Promise<BleService[]> {
const cached = this.services.get(deviceId);
if (cached != null && cached.length > 0) return cached;
try {
await gatt.discoverServices?.();
} catch (e) {
console.warn('[AKBLE][Harmony] discoverServices failed', e);
}
let services: any[] = [];
try {
services = gatt.getServices?.() ?? [];
} catch (e) {
console.warn('[AKBLE][Harmony] getServices failed', e);
}
if (!Array.isArray(services)) services = [];
return this.cacheServices(deviceId, services);
}
private cacheCharacteristics(deviceId: string, serviceId: string, chars: any[]): BleCharacteristic[] {
const list: BleCharacteristic[] = [];
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
if (ch == null) continue;
const uuid = ch.uuid ?? ch.characteristicUuid ?? '';
if (!uuid) continue;
list.push({
uuid,
service: { uuid: serviceId, isPrimary: true },
properties: buildProperties(ch)
});
}
let map = this.characteristics.get(deviceId);
if (map == null) {
map = new Map<string, BleCharacteristic[]>();
this.characteristics.set(deviceId, map);
}
map.set(serviceId, list);
return list;
}
private async ensureCharacteristics(deviceId: string, serviceId: string, gatt: any): Promise<BleCharacteristic[]> {
const perDevice = this.characteristics.get(deviceId);
const cached = perDevice != null ? perDevice.get(serviceId) : null;
if (cached != null && cached.length > 0) return cached;
let list: any[] = [];
try {
list = gatt.getCharacteristics?.(serviceId) ?? [];
} catch (e) {
console.warn('[AKBLE][Harmony] getCharacteristics failed', e);
}
if (!Array.isArray(list)) list = [];
return this.cacheCharacteristics(deviceId, serviceId, list);
}
private bindGattListener(deviceId: string, gatt: any) {
if (this.boundGattDevices.has(deviceId)) return;
this.boundGattDevices.add(deviceId);
try {
gatt.on?.('characteristicChange', (change: CharacteristicChange) => {
try { this.handleCharacteristicChange(deviceId, change); } catch (err) { console.warn('[AKBLE][Harmony] notify handler error', err); }
});
} catch (e) {
console.warn('[AKBLE][Harmony] bind characteristicChange failed', e);
}
}
private pendingKey(deviceId: string, serviceId: string, characteristicId: string): string {
return `${deviceId}|${serviceId}|${characteristicId}|read`;
}
private notifyKey(deviceId: string, serviceId: string, characteristicId: string): string {
return `${deviceId}|${serviceId}|${characteristicId}`;
}
private handleCharacteristicChange(deviceId: string, change: CharacteristicChange) {
const serviceId = change?.serviceUuid ?? '';
const characteristicId = change?.characteristicUuid ?? '';
if (!serviceId || !characteristicId) return;
const buffer = toArrayBuffer(change?.value);
const pending = this.pendingReads.get(this.pendingKey(deviceId, serviceId, characteristicId));
if (pending != null) {
this.pendingReads.delete(this.pendingKey(deviceId, serviceId, characteristicId));
if (pending.timer != null) clearTimeout(pending.timer);
try { pending.resolve(buffer); } catch (e) { }
}
const cb = this.notifyCallbacks.get(this.notifyKey(deviceId, serviceId, characteristicId));
if (cb != null) {
try { cb(toUint8Array(buffer)); } catch (e) { }
}
}
async getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): Promise<BleService[]> {
const gatt = this.getGattOrThrow(deviceId);
this.bindGattListener(deviceId, gatt);
try {
const services = await this.ensureServices(deviceId, gatt);
if (callback != null) callback(services, null);
return services;
} catch (err) {
if (callback != null) callback(null, err as Error);
throw err;
}
}
async getCharacteristics(deviceId: string, serviceId: string, callback?: (list: BleCharacteristic[] | null, error?: Error) => void): Promise<BleCharacteristic[]> {
const gatt = this.getGattOrThrow(deviceId);
this.bindGattListener(deviceId, gatt);
try {
await this.ensureServices(deviceId, gatt);
const list = await this.ensureCharacteristics(deviceId, serviceId, gatt);
if (callback != null) callback(list, null);
return list;
} catch (err) {
if (callback != null) callback(null, err as Error);
throw err;
}
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
const gatt = this.getGattOrThrow(deviceId);
this.bindGattListener(deviceId, gatt);
return new Promise<ArrayBuffer>((resolve, reject) => {
const key = this.pendingKey(deviceId, serviceId, characteristicId);
const timer = setTimeout(() => {
this.pendingReads.delete(key);
reject(new Error('读取超时'));
}, 10000);
this.pendingReads.set(key, { resolve, reject, timer });
try {
const result = gatt.readCharacteristicValue?.({ serviceUuid: serviceId, characteristicUuid: characteristicId });
if (result instanceof Promise) {
result.then((value: any) => {
const buf = toArrayBuffer(value?.value ?? value);
const pending = this.pendingReads.get(key);
if (pending != null) {
this.pendingReads.delete(key);
if (pending.timer != null) clearTimeout(pending.timer);
try { pending.resolve(buf); } catch (e) { }
}
}).catch((err: BusinessError) => {
this.pendingReads.delete(key);
clearTimeout(timer);
reject(err);
});
} else if (result != null) {
const buf = toArrayBuffer((result as any)?.value ?? result);
this.pendingReads.delete(key);
clearTimeout(timer);
resolve(buf);
}
} catch (e) {
this.pendingReads.delete(key);
clearTimeout(timer);
reject(e);
}
});
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
const gatt = this.getGattOrThrow(deviceId);
const payload = value instanceof Uint8Array ? value : new Uint8Array(value ?? new ArrayBuffer(0));
const writeType = options?.forceWriteTypeNoResponse === true || options?.waitForResponse === false
? ble.GattWriteType?.WRITE_TYPE_NO_RESPONSE ?? 1
: ble.GattWriteType?.WRITE_TYPE_DEFAULT ?? 0;
try {
const res = gatt.writeCharacteristicValue?.({
serviceUuid: serviceId,
characteristicUuid: characteristicId,
value: payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength),
writeType
});
if (res instanceof Promise) {
await res;
}
return true;
} catch (e) {
console.warn('[AKBLE][Harmony] writeCharacteristic failed', e);
throw e;
}
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleDataReceivedCallback): Promise<void> {
const gatt = this.getGattOrThrow(deviceId);
this.bindGattListener(deviceId, gatt);
const key = this.notifyKey(deviceId, serviceId, characteristicId);
this.notifyCallbacks.set(key, callback);
try {
const res = gatt.setCharacteristicValueChangeNotification?.({
serviceUuid: serviceId,
characteristicUuid: characteristicId,
enable: true
});
if (res instanceof Promise) {
await res;
}
} catch (e) {
this.notifyCallbacks.delete(key);
console.warn('[AKBLE][Harmony] enable notify failed', e);
throw e;
}
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
const gatt = this.getGattOrThrow(deviceId);
this.bindGattListener(deviceId, gatt);
this.notifyCallbacks.delete(this.notifyKey(deviceId, serviceId, characteristicId));
try {
const res = gatt.setCharacteristicValueChangeNotification?.({
serviceUuid: serviceId,
characteristicUuid: characteristicId,
enable: false
});
if (res instanceof Promise) {
await res;
}
} catch (e) {
console.warn('[AKBLE][Harmony] disable notify failed', e);
}
}
async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
const gatt = this.getGattOrThrow(deviceId);
this.bindGattListener(deviceId, gatt);
const services = await this.ensureServices(deviceId, gatt);
const characteristics: BleCharacteristic[] = [];
for (let i = 0; i < services.length; i++) {
const svc = services[i];
const list = await this.ensureCharacteristics(deviceId, svc.uuid, gatt);
for (let j = 0; j < list.length; j++) {
characteristics.push(list[j]);
}
}
return { services, characteristics };
}
async subscribeAllNotifications(deviceId: string, callback: BleDataReceivedCallback): Promise<void> {
const { services, characteristics } = await this.autoDiscoverAll(deviceId);
for (let i = 0; i < characteristics.length; i++) {
const ch = characteristics[i];
if (ch.properties != null && (ch.properties.notify || ch.properties.indicate)) {
try {
await this.subscribeCharacteristic(deviceId, ch.service.uuid, ch.uuid, callback);
} catch (e) {
console.warn('[AKBLE][Harmony] subscribeAll skip', e);
}
}
}
}
}

View File

@@ -0,0 +1,279 @@
import type {
BleDevice,
BleConnectionState,
BleEvent,
BleEventCallback,
BleEventPayload,
BleScanResult,
BleConnectOptionsExt,
AutoBleInterfaces,
BleDataPayload,
SendDataPayload,
BleOptions,
MultiProtocolDevice,
ScanHandler,
BleProtocolType,
ScanDevicesOptions
} from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
class DeviceContext {
device: BleDevice;
protocol: BleProtocolType;
state: BleConnectionState;
handler: ProtocolHandler;
constructor(device: BleDevice, protocol: BleProtocolType, handler: ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0;
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>();
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
let defaultBluetoothService: BluetoothService | null = null;
function emit(event: BleEvent, payload: BleEventPayload) {
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try { cb(payload); } catch (e) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler, bluetoothService?: BluetoothService) {
super(bluetoothService);
this._raw = raw ?? null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices == 'function') {
await rawTyped.scanDevices(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect == 'function') {
await rawTyped.connect(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect == 'function') {
await rawTyped.disconnect(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData == 'function') {
await rawTyped.sendData(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect == 'function') {
return await rawTyped.autoConnect(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' };
}
}
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object') return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] == 'function') return true;
if (typeof r['connect'] == 'function') return true;
if (typeof r['disconnect'] == 'function') return true;
if (typeof r['sendData'] == 'function') return true;
if (typeof r['autoConnect'] == 'function') return true;
if (typeof r['protocol'] == 'string') return true;
return false;
}
export const registerProtocolHandler = (handler: any) => {
if (handler == null) return;
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = handler as ProtocolHandler;
} else if (isRawProtocolHandler(handler)) {
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler, defaultBluetoothService);
(activeHandler as ProtocolHandler).protocol = proto;
} else {
console.warn('[AKBLE] registerProtocolHandler: unsupported handler type, ignoring', handler);
return;
}
activeProtocol = proto;
}
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
ensureDefaultProtocolHandler();
if (activeHandler == null) {
console.log('[AKBLE] no active scan handler registered');
return;
}
const handler = activeHandler as ProtocolHandler;
const original = options ?? null;
const scanOptions: ScanDevicesOptions = {} as ScanDevicesOptions;
if (original != null) {
if (original.protocols != null) scanOptions.protocols = original.protocols;
if (original.optionalServices != null) scanOptions.optionalServices = original.optionalServices;
if (original.timeout != null) scanOptions.timeout = original.timeout;
}
const userFound = original?.onDeviceFound ?? null;
scanOptions.onDeviceFound = (device: BleDevice) => {
emit('deviceFound', { event: 'deviceFound', device });
if (userFound != null) {
try { userFound(device); } catch (err) { }
}
};
const userFinished = original?.onScanFinished ?? null;
scanOptions.onScanFinished = () => {
emit('scanFinished', { event: 'scanFinished' });
if (userFinished != null) {
try { userFinished(); } catch (err) { }
}
};
try {
await handler.scanDevices(scanOptions);
} catch (e) {
console.warn('[AKBLE] scanDevices handler error', e);
}
}
export const connectDevice = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<void> => {
const handler = activeHandler;
if (handler == null) throw new Error('No protocol handler');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2;
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
}
export const disconnectDevice = async (deviceId: string, protocol: BleProtocolType): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null) return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
deviceMap.delete(getDeviceKey(deviceId, protocol));
}
export const sendData = async (payload: SendDataPayload, options?: BleOptions): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null) throw new Error('Device not connected');
const deviceCtx = ctx as DeviceContext;
if (deviceCtx.handler == null) throw new Error('sendData not supported for this protocol');
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data });
}
export const getConnectedDevices = (): MultiProtocolDevice[] => {
const result: MultiProtocolDevice[] = [];
deviceMap.forEach((ctx: DeviceContext) => {
const dev: MultiProtocolDevice = {
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
};
result.push(dev);
});
return result;
}
export const getConnectionState = (deviceId: string, protocol: BleProtocolType): BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null) return 0;
return ctx.state;
}
export const on = (event: BleEvent, callback: BleEventCallback) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
}
export const off = (event: BleEvent, callback?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
} else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
}
function getDeviceKey(deviceId: string, protocol: BleProtocolType): string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null) throw new Error('autoConnect not supported for this protocol');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
return await handler.autoConnect(device, options) as AutoBleInterfaces;
}
function ensureDefaultProtocolHandler(): void {
if (activeHandler != null) return;
const service = defaultBluetoothService;
if (service == null) return;
try {
const _dm = DeviceManager.getInstance();
const _raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions) => {
try {
const scanOptions = options != null ? options : {} as ScanDevicesOptions;
_dm.startScan(scanOptions);
} catch (e) {
console.warn('[AKBLE] DeviceManager.startScan failed', e);
}
return Promise.resolve();
},
connect: (device, options?: BleConnectOptionsExt) => {
return _dm.connectDevice(device.deviceId, options);
},
disconnect: (device) => {
return _dm.disconnectDevice(device.deviceId);
},
autoConnect: (device, _options?: any) => {
const result: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
return Promise.resolve(result);
}
};
const _wrapper = new ProtocolHandlerWrapper(_raw, service);
activeHandler = _wrapper;
activeProtocol = _raw.protocol as BleProtocolType;
console.log('[AKBLE] default protocol handler registered', activeProtocol);
} catch (e) {
console.warn('[AKBLE] failed to register default protocol handler', e);
}
}
export const setDefaultBluetoothService = (service: BluetoothService) => {
defaultBluetoothService = service;
ensureDefaultProtocolHandler();
};

View File

@@ -0,0 +1,5 @@
{
"dependencies": [
]
}

View File

@@ -0,0 +1,348 @@
import type { BleDevice, BleConnectOptionsExt, BleConnectionState, BleConnectionStateChangeCallback, ScanDevicesOptions } from '../interface.uts';
import { CBCentralManager, CBPeripheral, CBService, CBCharacteristic, CBCentralManagerDelegate, CBPeripheralDelegate, CBManagerState, CBUUID } from 'CoreBluetooth';
import { NSObject, NSDictionary, NSNumber, NSError, NSUUID } from 'Foundation';
import { DispatchQueue } from 'Dispatch';
import { ServiceManager } from './service_manager.uts';
type PendingConnect = {
resolve: () => void;
reject: (err?: any) => void;
timer?: number;
};
class PendingConnectImpl implements PendingConnect {
resolve: () => void;
reject: (err?: any) => void;
timer?: number;
constructor(resolve: () => void, reject: (err?: any) => void, timer?: number) {
this.resolve = resolve;
this.reject = reject;
this.timer = timer;
}
}
class CentralDelegate extends NSObject implements CBCentralManagerDelegate, CBPeripheralDelegate {
private owner: DeviceManager;
constructor(owner: DeviceManager) {
super();
this.owner = owner;
}
override centralManagerDidUpdateState(central: CBCentralManager): void {
this.owner.handleCentralStateUpdate(central.state);
}
override centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: NSDictionary<any, any>, RSSI: NSNumber): void {
this.owner.handleDiscovered(peripheral, RSSI);
}
override centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral): void {
this.owner.handleConnected(peripheral);
}
override centralManager(central: CBCentralManager, didFailToConnectPeripheral peripheral: CBPeripheral, error: NSError | null): void {
this.owner.handleConnectFailed(peripheral, error);
}
override centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError | null): void {
this.owner.handleDisconnected(peripheral, error);
}
override peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError | null): void {
ServiceManager.getInstance().handleServicesDiscovered(peripheral, error);
}
override peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError | null): void {
ServiceManager.getInstance().handleCharacteristicsDiscovered(peripheral, service, error);
}
override peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError | null): void {
ServiceManager.getInstance().handleCharacteristicValueUpdated(peripheral, characteristic, error);
}
override peripheral(peripheral: CBPeripheral, didWriteValueForCharacteristic characteristic: CBCharacteristic, error: NSError | null): void {
ServiceManager.getInstance().handleCharacteristicWrite(peripheral, characteristic, error);
}
override peripheral(peripheral: CBPeripheral, didUpdateNotificationStateForCharacteristic characteristic: CBCharacteristic, error: NSError | null): void {
ServiceManager.getInstance().handleNotificationState(peripheral, characteristic, error);
}
}
export class DeviceManager {
private static instance: DeviceManager | null = null;
private central: CBCentralManager | null = null;
private delegate: CentralDelegate | null = null;
private queue: DispatchQueue | null = null;
private devices = new Map<string, BleDevice>();
private peripherals = new Map<string, CBPeripheral>();
private connectionStates = new Map<string, BleConnectionState>();
private connectionStateChangeListeners: BleConnectionStateChangeCallback[] = [];
private pendingConnects = new Map<string, PendingConnect>();
private centralState: number = CBManagerState.unknown;
private scanOptions: ScanDevicesOptions | null = null;
private scanTimer: number | null = null;
private isScanning: boolean = false;
private pendingScan: boolean = false;
private pendingScanOptions: ScanDevicesOptions | null = null;
private constructor() {}
static getInstance(): DeviceManager {
if (DeviceManager.instance == null) {
DeviceManager.instance = new DeviceManager();
}
return DeviceManager.instance!;
}
private ensureCentral(): CBCentralManager {
if (this.central != null) return this.central!;
if (this.queue == null) {
this.queue = DispatchQueue.main;
}
this.delegate = new CentralDelegate(this);
this.central = new CBCentralManager(delegate = this.delegate!, queue = this.queue);
if (this.central != null) {
this.centralState = this.central!.state;
}
return this.central!;
}
handleCentralStateUpdate(state: number) {
this.centralState = state;
if (state == CBManagerState.poweredOn) {
if (this.pendingScan) {
const opts = this.pendingScanOptions ?? {} as ScanDevicesOptions;
this.pendingScan = false;
this.pendingScanOptions = null;
this.beginScan(opts);
}
} else if (state == CBManagerState.poweredOff) {
this.stopScanInternal();
}
}
startScan(options: ScanDevicesOptions): void {
const central = this.ensureCentral();
const opts = options ?? {} as ScanDevicesOptions;
this.scanOptions = opts;
if (this.centralState != CBManagerState.poweredOn) {
this.pendingScan = true;
this.pendingScanOptions = opts;
console.warn('[AKBLE][iOS] Bluetooth not powered on yet, waiting for state update');
return;
}
this.beginScan(opts, central);
}
private beginScan(options: ScanDevicesOptions, central?: CBCentralManager | null) {
const mgr = central ?? this.central ?? this.ensureCentral();
if (this.isScanning) {
this.stopScanInternal();
}
const serviceIds = options.optionalServices ?? null;
let serviceUUIDs: CBUUID[] | null = null;
if (serviceIds != null && serviceIds.length > 0) {
serviceUUIDs = [];
for (let i = 0; i < serviceIds.length; i++) {
const sid = serviceIds[i];
try {
const uuid = CBUUID.UUIDWithString(sid);
if (uuid != null) serviceUUIDs.push(uuid!);
} catch (e) {
console.warn('[AKBLE][iOS] invalid service uuid', sid, e);
}
}
if (serviceUUIDs.length == 0) serviceUUIDs = null;
}
try {
mgr.scanForPeripherals(withServices = serviceUUIDs, options = null);
this.isScanning = true;
if (options.timeout != null && options.timeout > 0) {
this.scanTimer = setTimeout(() => {
this.stopScanInternal();
}, options.timeout);
}
} catch (e) {
console.error('[AKBLE][iOS] scanForPeripherals failed', e);
}
}
stopScan(): void {
this.stopScanInternal();
}
private stopScanInternal() {
if (!this.isScanning) return;
try {
this.central?.stopScan();
} catch (e) {
console.warn('[AKBLE][iOS] stopScan failed', e);
}
this.isScanning = false;
if (this.scanTimer != null) {
clearTimeout(this.scanTimer);
this.scanTimer = null;
}
const finished = this.scanOptions?.onScanFinished;
this.scanOptions = null;
if (finished != null) {
try { finished(); } catch (e) { }
}
}
handleDiscovered(peripheral: CBPeripheral, RSSI: NSNumber) {
const deviceId = peripheral.identifier.UUIDString;
let rssiValue = 0;
if (RSSI != null) {
try {
rssiValue = RSSI.intValue;
} catch (e) {
rssiValue = Number(RSSI);
}
}
let bleDevice = this.devices.get(deviceId);
if (bleDevice == null) {
bleDevice = { deviceId, name: peripheral.name ?? 'Unknown', rssi: rssiValue, lastSeen: Date.now() };
this.devices.set(deviceId, bleDevice);
} else {
bleDevice.rssi = rssiValue;
bleDevice.name = peripheral.name ?? bleDevice.name;
bleDevice.lastSeen = Date.now();
}
this.peripherals.set(deviceId, peripheral);
peripheral.delegate = this.delegate;
const onFound = this.scanOptions?.onDeviceFound;
if (onFound != null) {
try { onFound(bleDevice); } catch (e) { }
}
}
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
const central = this.ensureCentral();
const timeout = options?.timeout ?? 15000;
const peripheral = this.obtainPeripheral(deviceId, central);
if (peripheral == null) {
throw new Error('未找到设备');
}
this.connectionStates.set(deviceId, 1);
this.emitConnectionStateChange(deviceId, 1);
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingConnects.delete(deviceId);
this.connectionStates.set(deviceId, 0);
this.emitConnectionStateChange(deviceId, 0);
reject(new Error('连接超时'));
}, timeout);
const resolveAdapter = () => {
clearTimeout(timer);
this.pendingConnects.delete(deviceId);
resolve();
};
const rejectAdapter = (err?: any) => {
clearTimeout(timer);
this.pendingConnects.delete(deviceId);
reject(err);
};
this.pendingConnects.set(deviceId, new PendingConnectImpl(resolveAdapter, rejectAdapter, timer));
try {
peripheral.delegate = this.delegate;
central.connect(peripheral = peripheral, options = null);
} catch (e) {
clearTimeout(timer);
this.pendingConnects.delete(deviceId);
this.connectionStates.set(deviceId, 0);
this.emitConnectionStateChange(deviceId, 0);
reject(e);
}
});
}
async disconnectDevice(deviceId: string): Promise<void> {
const central = this.ensureCentral();
const peripheral = this.peripherals.get(deviceId);
if (peripheral == null) {
return;
}
try {
central.cancelPeripheralConnection(peripheral = peripheral);
} catch (e) {
console.warn('[AKBLE][iOS] cancelPeripheralConnection failed', e);
}
this.connectionStates.set(deviceId, 0);
this.emitConnectionStateChange(deviceId, 0);
}
handleConnected(peripheral: CBPeripheral) {
const deviceId = peripheral.identifier.UUIDString;
const pending = this.pendingConnects.get(deviceId);
if (pending != null) {
const timer = pending.timer;
if (timer != null) clearTimeout(timer);
try { pending.resolve(); } catch (e) { }
this.pendingConnects.delete(deviceId);
}
this.connectionStates.set(deviceId, 2);
this.emitConnectionStateChange(deviceId, 2);
this.peripherals.set(deviceId, peripheral);
peripheral.delegate = this.delegate;
try {
peripheral.discoverServices(serviceUUIDs = null);
} catch (e) {
console.warn('[AKBLE][iOS] discoverServices failed', e);
}
}
handleConnectFailed(peripheral: CBPeripheral, error: NSError | null) {
const deviceId = peripheral.identifier.UUIDString;
const pending = this.pendingConnects.get(deviceId);
if (pending != null) {
const timer = pending.timer;
if (timer != null) clearTimeout(timer);
try { pending.reject(error ?? new Error('连接失败')); } catch (e) { }
this.pendingConnects.delete(deviceId);
}
this.connectionStates.set(deviceId, 0);
this.emitConnectionStateChange(deviceId, 0);
}
handleDisconnected(peripheral: CBPeripheral, _error: NSError | null) {
const deviceId = peripheral.identifier.UUIDString;
this.connectionStates.set(deviceId, 0);
this.emitConnectionStateChange(deviceId, 0);
}
getConnectedDevices(): BleDevice[] {
const result: BleDevice[] = [];
this.devices.forEach((device, deviceId) => {
if (this.connectionStates.get(deviceId) == 2) {
result.push(device);
}
});
return result;
}
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
this.connectionStateChangeListeners.push(listener);
}
private emitConnectionStateChange(deviceId: string, state: BleConnectionState) {
for (let i = 0; i < this.connectionStateChangeListeners.length; i++) {
const listener = this.connectionStateChangeListeners[i];
try { listener(deviceId, state); } catch (e) { }
}
}
getPeripheral(deviceId: string): CBPeripheral | null {
return this.peripherals.get(deviceId) ?? null;
}
private obtainPeripheral(deviceId: string, central: CBCentralManager): CBPeripheral | null {
let peripheral = this.peripherals.get(deviceId) ?? null;
if (peripheral != null) return peripheral;
try {
const uuid = new NSUUID(UUIDString = deviceId);
const list = central.retrievePeripherals(withIdentifiers = [uuid]);
if (list != null && list.length > 0) {
peripheral = list[0];
if (peripheral != null) {
this.peripherals.set(deviceId, peripheral!);
peripheral!.delegate = this.delegate;
}
}
} catch (e) {
console.warn('[AKBLE][iOS] retrievePeripherals failed', e);
}
return peripheral;
}
}

View File

@@ -0,0 +1,9 @@
import type { DfuManagerType, DfuOptions } from '../interface.uts';
class IOSDfuManager implements DfuManagerType {
async startDfu(_deviceId: string, _firmwareBytes: Uint8Array, _options?: DfuOptions): Promise<void> {
throw new Error('iOS 平台暂未实现 DFU 功能');
}
}
export const dfuManager = new IOSDfuManager();

View File

@@ -0,0 +1,106 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback, BleProtocolType, BluetoothService as BluetoothServiceContract } from '../interface.uts';
const serviceManager = ServiceManager.getInstance();
class IOSBluetoothService implements BluetoothServiceContract {
scanDevices(options?: ScanDevicesOptions | null): Promise<void> {
return BluetoothManager.scanDevices(options ?? null);
}
async connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt | null): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.connectDevice(deviceId, proto, options ?? null);
}
async disconnectDevice(deviceId: string, protocol?: string): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.disconnectDevice(deviceId, proto);
}
getConnectedDevices(): MultiProtocolDevice[] {
return BluetoothManager.getConnectedDevices();
}
on(event: BleEvent | string, callback: BleEventCallback): void {
BluetoothManager.on(event as BleEvent, callback);
}
off(event: BleEvent | string, callback?: BleEventCallback | null): void {
BluetoothManager.off(event as BleEvent, callback ?? null);
}
getServices(deviceId: string): Promise<BleService[]> {
return new Promise((resolve, reject) => {
serviceManager.getServices(deviceId, (list, err) => {
if (err != null) {
reject(err);
} else {
resolve(list ?? []);
}
});
});
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return new Promise((resolve, reject) => {
serviceManager.getCharacteristics(deviceId, serviceId, (list, err) => {
if (err != null) {
reject(err);
} else {
resolve(list ?? []);
}
});
});
}
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const services = await this.getServices(deviceId);
if (services.length == 0) throw new Error('未发现服务');
let targetService = services[0].uuid;
for (let i = 0; i < services.length; i++) {
const uuid = services[i].uuid ?? '';
if (/^bae/i.test(uuid)) {
targetService = uuid;
break;
}
}
const characteristics = await this.getCharacteristics(deviceId, targetService);
if (characteristics.length == 0) throw new Error('未发现特征值');
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse == true)) {
writeCharId = c.uuid;
}
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) {
notifyCharId = c.uuid;
}
}
if (writeCharId == '' || notifyCharId == '') throw new Error('未找到合适的写入或通知特征');
return { serviceId: targetService, writeCharId, notifyCharId };
}
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, value, options);
}
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export class BluetoothServiceShape extends IOSBluetoothService {}
const bluetoothServiceInstance = new BluetoothServiceShape();
BluetoothManager.setDefaultBluetoothService(bluetoothServiceInstance);
export const bluetoothService: BluetoothServiceContract = bluetoothServiceInstance;
export function getBluetoothService(): BluetoothServiceShape {
return bluetoothServiceInstance;
}
export { dfuManager } from './dfu_manager.uts';

View File

@@ -0,0 +1,483 @@
import type { BleService, BleCharacteristic, BleDataReceivedCallback, BleCharacteristicProperties, WriteCharacteristicOptions, AutoDiscoverAllResult } from '../interface.uts';
import { CBPeripheral, CBService, CBCharacteristic, CBCharacteristicWriteType } from 'CoreBluetooth';
import { Data, NSError } from 'Foundation';
import { DeviceManager } from './device_manager.uts';
function toUint8Array(value: Uint8Array | ArrayBuffer): Uint8Array {
if (value instanceof Uint8Array) return value;
return new Uint8Array(value);
}
function dataToUint8Array(data: Data | null): Uint8Array {
if (data == null) return new Uint8Array(0);
const base64 = data.base64EncodedString(options = 0);
if (base64 == null) return new Uint8Array(0);
const raw = atob(base64);
const out = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i++) {
out[i] = raw.charCodeAt(i) & 0xff;
}
return out;
}
function uint8ArrayToData(bytes: Uint8Array): Data {
if (bytes.length == 0) {
return new Data();
}
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
const data = new Data(base64Encoded = base64);
return data != null ? data! : new Data();
}
function iterateNSArray<T>(collection: any, handler: (item: T | null) => void) {
if (collection == null) return;
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
handler(collection[i] as T);
}
return;
}
try {
const count = collection.count as number;
if (typeof count === 'number') {
for (let i = 0; i < count; i++) {
const item = collection.objectAtIndex(i);
handler(item as T);
}
return;
}
} catch (e) { }
try {
const len = collection.length as number;
if (typeof len === 'number') {
for (let i = 0; i < len; i++) {
handler(collection[i] as T);
}
}
} catch (e2) { }
}
type PendingCallback = {
resolve: (data: any) => void;
reject: (err?: any) => void;
timer?: number;
};
function makeCharProperties(flags: number): BleCharacteristicProperties {
const read = (flags & 0x02) != 0;
const write = (flags & 0x08) != 0;
const notify = (flags & 0x10) != 0;
const indicate = (flags & 0x20) != 0;
const writeNoRsp = (flags & 0x04) != 0;
return {
read,
write,
notify,
indicate,
writeWithoutResponse: writeNoRsp,
canRead: read,
canWrite: write || writeNoRsp,
canNotify: notify || indicate
};
}
function getCharPropertiesValue(characteristic: CBCharacteristic): number {
try {
const anyProps = characteristic.properties as any;
if (anyProps != null && anyProps.rawValue != null) {
return Number(anyProps.rawValue);
}
} catch (e) { }
try {
return Number((characteristic as any).properties);
} catch (e2) { }
return 0;
}
export class ServiceManager {
private static instance: ServiceManager | null = null;
private services = new Map<string, BleService[]>();
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
private serviceWaiters = new Map<string, ((list: BleService[] | null, error?: Error) => void)[]>();
private characteristicWaiters = new Map<string, ((list: BleCharacteristic[] | null, error?: Error) => void)[]>();
private pendingReads = new Map<string, PendingCallback>();
private pendingWrites = new Map<string, PendingCallback>();
private notifyCallbacks = new Map<string, BleDataReceivedCallback>();
private deviceManager = DeviceManager.getInstance();
private constructor() {}
static getInstance(): ServiceManager {
if (ServiceManager.instance == null) {
ServiceManager.instance = new ServiceManager();
}
return ServiceManager.instance!;
}
resetDiscoveryState(deviceId: string) {
this.services.delete(deviceId);
this.characteristics.forEach((_value, key) => {
if (key.startsWith(deviceId + '|')) {
this.characteristics.delete(key);
}
});
}
handleServicesDiscovered(peripheral: CBPeripheral, error: NSError | null) {
const deviceId = peripheral.identifier.UUIDString;
if (error != null) {
const err = new Error('服务发现失败: ' + error.localizedDescription);
this.resolveServiceWaiters(deviceId, null, err);
return;
}
const list: BleService[] = [];
const native = peripheral.services;
iterateNSArray<CBService>(native, (svc) => {
if (svc == null) return;
const uuid = svc.UUID.UUIDString;
list.push({ uuid, isPrimary: svc.isPrimary });
});
this.services.set(deviceId, list);
this.resolveServiceWaiters(deviceId, list, null);
}
handleCharacteristicsDiscovered(peripheral: CBPeripheral, service: CBService, error: NSError | null) {
const deviceId = peripheral.identifier.UUIDString;
const serviceId = service.UUID.UUIDString;
const key = this.characteristicKey(deviceId, serviceId);
if (error != null) {
const err = new Error('特征发现失败: ' + error.localizedDescription);
this.resolveCharacteristicWaiters(key, null, err);
return;
}
const list: BleCharacteristic[] = [];
const chars = service.characteristics;
iterateNSArray<CBCharacteristic>(chars, (ch) => {
if (ch == null) return;
const propsValue = getCharPropertiesValue(ch);
const props = makeCharProperties(propsValue);
list.push({
uuid: ch.UUID.UUIDString,
service: { uuid: serviceId, isPrimary: service.isPrimary },
properties: props
});
});
let map = this.characteristics.get(deviceId);
if (map == null) {
map = new Map<string, BleCharacteristic[]>();
this.characteristics.set(deviceId, map);
}
map.set(serviceId, list);
this.resolveCharacteristicWaiters(key, list, null);
}
handleCharacteristicValueUpdated(peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError | null) {
const deviceId = peripheral.identifier.UUIDString;
const serviceId = characteristic.service.UUID.UUIDString;
const charId = characteristic.UUID.UUIDString;
const notifyKey = this.notifyKey(deviceId, serviceId, charId);
const readKey = this.operationKey(deviceId, serviceId, charId, 'read');
if (error != null) {
const pending = this.pendingReads.get(readKey);
if (pending != null) {
this.pendingReads.delete(readKey);
try { pending.reject(error); } catch (e) { }
}
return;
}
const bytes = dataToUint8Array(characteristic.value);
const pending = this.pendingReads.get(readKey);
if (pending != null) {
this.pendingReads.delete(readKey);
if (pending.timer != null) clearTimeout(pending.timer);
try { pending.resolve(bytes.buffer as ArrayBuffer); } catch (e) { }
}
const cb = this.notifyCallbacks.get(notifyKey);
if (cb != null) {
try { cb(bytes); } catch (e) { }
}
}
handleCharacteristicWrite(peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError | null) {
const deviceId = peripheral.identifier.UUIDString;
const serviceId = characteristic.service.UUID.UUIDString;
const charId = characteristic.UUID.UUIDString;
const writeKey = this.operationKey(deviceId, serviceId, charId, 'write');
const pending = this.pendingWrites.get(writeKey);
if (pending == null) return;
this.pendingWrites.delete(writeKey);
if (pending.timer != null) clearTimeout(pending.timer);
if (error != null) {
try { pending.reject(error); } catch (e) { }
} else {
try { pending.resolve(true); } catch (e) { }
}
}
handleNotificationState(peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError | null) {
if (error != null) {
console.warn('[AKBLE][iOS] notify state change error', error.localizedDescription);
}
}
getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): Promise<BleService[]> {
const cached = this.services.get(deviceId);
if (cached != null && cached.length > 0) {
if (callback != null) callback(cached, null);
return Promise.resolve(cached);
}
return new Promise((resolve, reject) => {
this.enqueueServiceWaiter(deviceId, (list, err) => {
if (err != null || list == null) {
if (callback != null) callback(null, err ?? new Error('服务获取失败'));
reject(err ?? new Error('服务获取失败'));
} else {
if (callback != null) callback(list, null);
resolve(list);
}
});
const peripheral = this.deviceManager.getPeripheral(deviceId);
if (peripheral == null) {
const err = new Error('设备未连接');
this.resolveServiceWaiters(deviceId, null, err);
return;
}
try {
peripheral.discoverServices(serviceUUIDs = null);
} catch (e) {
const err = e instanceof Error ? e : new Error('服务发现失败');
this.resolveServiceWaiters(deviceId, null, err);
}
});
}
getCharacteristics(deviceId: string, serviceId: string, callback?: (list: BleCharacteristic[] | null, error?: Error) => void): Promise<BleCharacteristic[]> {
const cached = this.characteristics.get(deviceId)?.get(serviceId) ?? null;
if (cached != null && cached.length > 0) {
if (callback != null) callback(cached, null);
return Promise.resolve(cached);
}
return new Promise((resolve, reject) => {
const key = this.characteristicKey(deviceId, serviceId);
this.enqueueCharacteristicWaiter(key, (list, err) => {
if (err != null || list == null) {
if (callback != null) callback(null, err ?? new Error('特征获取失败'));
reject(err ?? new Error('特征获取失败'));
} else {
if (callback != null) callback(list, null);
resolve(list);
}
});
const peripheral = this.deviceManager.getPeripheral(deviceId);
if (peripheral == null) {
const err = new Error('设备未连接');
this.resolveCharacteristicWaiters(key, null, err);
return;
}
const service = this.findNativeService(peripheral, serviceId);
if (service == null) {
const err = new Error('未找到服务');
this.resolveCharacteristicWaiters(key, null, err);
return;
}
try {
peripheral.discoverCharacteristics(characteristicUUIDs = null, forService = service);
} catch (e) {
const err = e instanceof Error ? e : new Error('特征发现失败');
this.resolveCharacteristicWaiters(key, null, err);
}
});
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
const peripheral = this.deviceManager.getPeripheral(deviceId);
if (peripheral == null) throw new Error('设备未连接');
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
if (characteristic == null) throw new Error('未找到特征值');
return new Promise<ArrayBuffer>((resolve, reject) => {
const key = this.operationKey(deviceId, serviceId, characteristicId, 'read');
const timer = setTimeout(() => {
this.pendingReads.delete(key);
reject(new Error('读取超时'));
}, 10000);
this.pendingReads.set(key, { resolve, reject, timer });
try {
peripheral.readValueForCharacteristic(characteristic);
} catch (e) {
clearTimeout(timer);
this.pendingReads.delete(key);
reject(e);
}
});
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
const peripheral = this.deviceManager.getPeripheral(deviceId);
if (peripheral == null) throw new Error('设备未连接');
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
if (characteristic == null) throw new Error('未找到特征值');
const payload = uint8ArrayToData(toUint8Array(value));
const waitForResponse = options?.waitForResponse ?? true;
const key = this.operationKey(deviceId, serviceId, characteristicId, 'write');
if (!waitForResponse) {
try {
peripheral.writeValue(payload, forCharacteristic = characteristic, type = CBCharacteristicWriteType.withoutResponse);
return true;
} catch (e) {
throw e;
}
}
return new Promise<boolean>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingWrites.delete(key);
reject(new Error('写入超时'));
}, options?.giveupTimeoutMs ?? 10000);
this.pendingWrites.set(key, { resolve, reject, timer });
try {
peripheral.writeValue(payload, forCharacteristic = characteristic, type = CBCharacteristicWriteType.withResponse);
} catch (e) {
clearTimeout(timer);
this.pendingWrites.delete(key);
reject(e);
}
});
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleDataReceivedCallback): Promise<void> {
const peripheral = this.deviceManager.getPeripheral(deviceId);
if (peripheral == null) throw new Error('设备未连接');
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
if (characteristic == null) throw new Error('未找到特征值');
const key = this.notifyKey(deviceId, serviceId, characteristicId);
this.notifyCallbacks.set(key, callback);
try {
peripheral.setNotifyValue(true, forCharacteristic = characteristic);
} catch (e) {
this.notifyCallbacks.delete(key);
throw e;
}
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
const peripheral = this.deviceManager.getPeripheral(deviceId);
if (peripheral == null) return;
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
if (characteristic == null) return;
const key = this.notifyKey(deviceId, serviceId, characteristicId);
this.notifyCallbacks.delete(key);
try {
peripheral.setNotifyValue(false, forCharacteristic = characteristic);
} catch (e) {
console.warn('[AKBLE][iOS] unsubscribe failed', e);
}
}
async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
const services = await this.getServices(deviceId);
const allCharacteristics: BleCharacteristic[] = [];
for (let i = 0; i < services.length; i++) {
const svc = services[i];
const chars = await this.getCharacteristics(deviceId, svc.uuid);
for (let j = 0; j < chars.length; j++) {
allCharacteristics.push(chars[j]);
}
}
return { services, characteristics: allCharacteristics };
}
async subscribeAllNotifications(deviceId: string, callback: BleDataReceivedCallback): Promise<void> {
const services = await this.getServices(deviceId);
for (let i = 0; i < services.length; i++) {
const svc = services[i];
const chars = await this.getCharacteristics(deviceId, svc.uuid);
for (let j = 0; j < chars.length; j++) {
const charDef = chars[j];
if (charDef.properties != null && (charDef.properties.notify || charDef.properties.indicate)) {
try {
await this.subscribeCharacteristic(deviceId, svc.uuid, charDef.uuid, callback);
} catch (e) {
console.warn('[AKBLE][iOS] subscribeAllNotifications failed', svc.uuid, charDef.uuid, e);
}
}
}
}
}
private enqueueServiceWaiter(deviceId: string, waiter: (list: BleService[] | null, error?: Error) => void) {
let queue = this.serviceWaiters.get(deviceId);
if (queue == null) {
queue = [];
this.serviceWaiters.set(deviceId, queue);
}
queue.push(waiter);
}
private enqueueCharacteristicWaiter(key: string, waiter: (list: BleCharacteristic[] | null, error?: Error) => void) {
let queue = this.characteristicWaiters.get(key);
if (queue == null) {
queue = [];
this.characteristicWaiters.set(key, queue);
}
queue.push(waiter);
}
private resolveServiceWaiters(deviceId: string, list: BleService[] | null, error: Error | null) {
const queue = this.serviceWaiters.get(deviceId);
if (queue == null) return;
this.serviceWaiters.delete(deviceId);
for (let i = 0; i < queue.length; i++) {
const waiter = queue[i];
try { waiter(list, error ?? undefined); } catch (e) { }
}
}
private resolveCharacteristicWaiters(key: string, list: BleCharacteristic[] | null, error: Error | null) {
const queue = this.characteristicWaiters.get(key);
if (queue == null) return;
this.characteristicWaiters.delete(key);
for (let i = 0; i < queue.length; i++) {
const waiter = queue[i];
try { waiter(list, error ?? undefined); } catch (e) { }
}
}
private findNativeService(peripheral: CBPeripheral, serviceId: string): CBService | null {
const services = peripheral.services;
let found: CBService | null = null;
iterateNSArray<CBService>(services, (svc) => {
if (found != null) return;
if (svc != null && svc.UUID.UUIDString == serviceId) found = svc;
});
if (found != null) return found;
return null;
}
private findNativeCharacteristic(peripheral: CBPeripheral, serviceId: string, characteristicId: string): CBCharacteristic | null {
const service = this.findNativeService(peripheral, serviceId);
if (service == null) return null;
const chars = service.characteristics;
let found: CBCharacteristic | null = null;
iterateNSArray<CBCharacteristic>(chars, (ch) => {
if (found != null) return;
if (ch != null && ch.UUID.UUIDString == characteristicId) found = ch;
});
if (found != null) return found;
return null;
}
private notifyKey(deviceId: string, serviceId: string, charId: string): string {
return `${deviceId}|${serviceId}|${charId}|notify`;
}
private operationKey(deviceId: string, serviceId: string, charId: string, op: string): string {
return `${deviceId}|${serviceId}|${charId}|${op}`;
}
private characteristicKey(deviceId: string, serviceId: string): string {
return `${deviceId}|${serviceId}`;
}
}

View File

@@ -0,0 +1,504 @@
// 蓝牙相关接口和类型定义
// 基础设备信息类型
export type BleDeviceInfo = {
deviceId : string;
name : string;
RSSI ?: number;
connected ?: boolean;
// 新增
serviceId ?: string;
writeCharId ?: string;
notifyCharId ?: string;
}
export type AutoDiscoverAllResult = {
services : BleService[];
characteristics : BleCharacteristic[];
}
// 服务信息类型
export type BleServiceInfo = {
uuid : string;
isPrimary : boolean;
}
// 特征值属性类型
export type BleCharacteristicProperties = {
read : boolean;
write : boolean;
notify : boolean;
indicate : boolean;
writeWithoutResponse ?: boolean;
canRead ?: boolean;
canWrite ?: boolean;
canNotify ?: boolean;
}
// 特征值信息类型
export type BleCharacteristicInfo = {
uuid : string;
serviceId : string;
properties : BleCharacteristicProperties;
}
// 错误状态码
export enum BleErrorCode {
UNKNOWN_ERROR = 0,
BLUETOOTH_UNAVAILABLE = 1,
PERMISSION_DENIED = 2,
DEVICE_NOT_CONNECTED = 3,
SERVICE_NOT_FOUND = 4,
CHARACTERISTIC_NOT_FOUND = 5,
OPERATION_TIMEOUT = 6
}
// 命令类型
export enum CommandType {
BATTERY = 1,
DEVICE_INFO = 2,
CUSTOM = 99,
TestBatteryLevel = 0x01
}
// 错误接口
export type BleError {
errCode : number;
errMsg : string;
errSubject ?: string;
}
// 连接选项
export type BleConnectOptions = {
deviceId : string;
timeout ?: number;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 断开连接选项
export type BleDisconnectOptions = {
deviceId : string;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 获取特征值选项
export type BleCharacteristicOptions = {
deviceId : string;
serviceId : string;
characteristicId : string;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 写入特征值选项
export type BleWriteOptions = {
deviceId : string;
serviceId : string;
characteristicId : string;
value : Uint8Array;
writeType ?: number;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// Options for writeCharacteristic helper
export type WriteCharacteristicOptions = {
waitForResponse ?: boolean;
maxAttempts ?: number;
retryDelayMs ?: number;
giveupTimeoutMs ?: number;
forceWriteTypeNoResponse ?: boolean;
}
// 通知特征值回调函数
export type BleNotifyCallback = (data : Uint8Array) => void;
// 通知特征值选项
export type BleNotifyOptions = {
deviceId : string;
serviceId : string;
characteristicId : string;
state ?: boolean; // true: 启用通知false: 禁用通知
onCharacteristicValueChange : BleNotifyCallback;
success ?: (result : any) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 获取服务选项
export type BleDeviceServicesOptions = {
deviceId : string;
success ?: (result : BleServicesResult) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 获取特征值选项
export type BleDeviceCharacteristicsOptions = {
deviceId : string;
serviceId : string;
success ?: (result : BleCharacteristicsResult) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 蓝牙扫描选项
export type BluetoothScanOptions = {
services ?: string[];
timeout ?: number;
onDeviceFound ?: (device : BleDeviceInfo) => void;
success ?: (result : BleScanResult) => void;
fail ?: (error : BleError) => void;
complete ?: (result : any) => void;
}
// 扫描结果
// 服务结果
export type BleServicesResult = {
services : BleServiceInfo[];
errMsg ?: string;
}
// 特征值结果
export type BleCharacteristicsResult = {
characteristics : BleCharacteristicInfo[];
errMsg ?: string;
}
// 定义连接状态枚举
export enum BLE_CONNECTION_STATE {
DISCONNECTED = 0,
CONNECTING = 1,
CONNECTED = 2,
DISCONNECTING = 3
}
// 电池状态类型定义
export type BatteryStatus = {
batteryLevel : number; // 电量百分比
isCharging : boolean; // 充电状态
}
// 蓝牙服务接口类型定义 - 转换为type类型
export type BleService = {
uuid : string;
isPrimary : boolean;
}
// 蓝牙特征值接口定义 - 转换为type类型
export type BleCharacteristic = {
uuid : string;
service : BleService;
properties : BleCharacteristicProperties;
}
// PendingPromise接口定义
export interface PendingCallback {
resolve : (data : any) => void;
reject : (err ?: any) => void;
timer ?: number;
}
// 蓝牙相关接口和类型定义
export type BleDevice = {
deviceId : string;
name : string;
rssi ?: number;
lastSeen ?: number; // 新增
// 新增
serviceId ?: string;
writeCharId ?: string;
notifyCharId ?: string;
}
// BLE常规选项
export type BleOptions = {
timeout ?: number;
success ?: (result : any) => void;
fail ?: (error : any) => void;
complete ?: () => void;
}
export type BleConnectionState = number; // 0: DISCONNECTED, 1: CONNECTING, 2: CONNECTED, 3: DISCONNECTING
export type BleConnectOptionsExt = {
timeout ?: number;
services ?: string[];
requireResponse ?: boolean;
autoReconnect ?: boolean;
maxAttempts ?: number;
interval ?: number;
};
// 回调函数类型
export type BleDeviceFoundCallback = (device : BleDevice) => void;
export type BleConnectionStateChangeCallback = (deviceId : string, state : BleConnectionState) => void;
export type BleDataPayload = {
deviceId : string;
serviceId ?: string;
characteristicId ?: string;
data : string | ArrayBuffer;
format ?: number; // 0: JSON, 1: XML, 2: RAW
}
export type BleDataSentCallback = (payload : BleDataPayload, success : boolean, error ?: BleError) => void;
export type BleErrorCallback = (error : BleError) => void;
// 健康数据类型定义
export enum HealthDataType {
HEART_RATE = 1,
BLOOD_OXYGEN = 2,
TEMPERATURE = 3,
STEP_COUNT = 4,
SLEEP_DATA = 5,
HEALTH_DATA = 6
}
// Shared health notification payloads used by protocol handler and UI consumers
export type HealthSubscription = {
serviceUuid : string
charUuid : string
}
export type WbbpPacket = {
cmd : number
seq : number
payload : Uint8Array
}
export type HealthData = {
type : 'heart' | 'spo2' | 'steps' | 'raw'
cmd ?: number
seq ?: number
timestamp ?: number
heartRate ?: number
spo2 ?: number
pulse ?: number
steps ?: number
distance ?: number
calories ?: number
quality ?: number
activity ?: number
raw ?: Uint8Array
payload?: Uint8Array;
data?: Uint8Array;
}
// Platform-specific services should be imported from per-platform entrypoints
// (e.g. './app-android/index.uts' or './web/index.uts').
// Avoid re-exporting platform modules at the SDK root to prevent bundlers
// Platform-specific services should be imported from per-platform entrypoints
// (e.g. './app-android/index.uts' or './web/index.uts').
// Avoid re-exporting platform modules at the SDK root to prevent bundlers
// from pulling android.* symbols into web bundles.
// If a typed ambient reference is required, declare the shape here instead of importing implementation.
// Example lightweight typed placeholder (do not import platform code here):
// export type BluetoothService = any; // platform-specific implementation exported from platform index files
// ====== 新增多协议、统一事件、协议适配、状态管理支持 ======
export type BleProtocolType =
| 'standard'
| 'custom'
| 'health'
| 'ibeacon'
| 'mesh';
export type BleEvent =
| 'deviceFound'
| 'scanFinished'
| 'connectionStateChanged'
| 'dataReceived'
| 'dataSent'
| 'error'
| 'servicesDiscovered'
| 'connected' // 新增
| 'disconnected'; // 新增
// 事件回调参数
export type BleEventPayload = {
event : BleEvent;
device ?: BleDevice;
protocol ?: BleProtocolType;
state ?: BleConnectionState;
data ?: ArrayBuffer | string | object;
format ?: string;
error ?: BleError;
extra ?: any;
}
// 事件回调函数
export type BleEventCallback = (payload : BleEventPayload) => void;
// 多协议设备信息(去除交叉类型,直接展开字段)
export type MultiProtocolDevice = {
deviceId : string;
name : string;
rssi ?: number;
protocol : BleProtocolType;
};
export type ScanDevicesOptions = {
protocols ?: BleProtocolType[];
optionalServices ?: string[];
timeout ?: number;
onDeviceFound ?: (device : BleDevice) => void;
onScanFinished ?: () => void;
};
// Named payload type used by sendData
export type SendDataPayload = {
deviceId : string;
serviceId ?: string;
characteristicId ?: string;
data : string | ArrayBuffer;
format ?: number;
protocol : BleProtocolType;
}
// 协议处理器接口(为 protocol-handler 适配器预留)
export type ScanHandler = {
protocol : BleProtocolType;
scanDevices ?: (options : ScanDevicesOptions) => Promise<void>;
connect : (device : BleDevice, options ?: BleConnectOptionsExt) => Promise<void>;
disconnect : (device : BleDevice) => Promise<void>;
// Optional: send arbitrary data via the protocol's write characteristic
sendData ?: (device : BleDevice, payload : SendDataPayload, options ?: BleOptions) => Promise<void>;
// Optional: try to connect and discover service/characteristic ids for this device
autoConnect ?: (device : BleDevice, options ?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
// 自动发现服务和特征返回类型
export type AutoBleInterfaces = {
serviceId : string;
writeCharId : string;
notifyCharId : string;
}
export type ResponseCallbackEntry = {
cb : (data : Uint8Array) => boolean | void;
multi : boolean;
};
// Result returned by a DFU control parser. Use a plain string `type` to keep
// the generated Kotlin simple and avoid inline union types which the generator
// does not handle well.
export type ControlParserResult = {
type : string; // e.g. 'progress', 'success', 'error', 'info'
progress ?: number;
error ?: any;
}
// DFU types
export type DfuOptions = {
mtu ?: number;
useNordic ?: boolean;
// If true, the DFU upload will await a write response per-packet. Set false to use
// WRITE_NO_RESPONSE (fire-and-forget) for higher throughput. Default: false.
waitForResponse ?: boolean;
// Maximum number of outstanding NO_RESPONSE writes to allow before throttling.
// This implements a simple sliding window. Default: 32.
maxOutstanding ?: number;
// Per-chunk sleep (ms) to yield to event loop / Android BLE stack. Default: 2.
writeSleepMs ?: number;
// Retry delay (ms) used by the Android write helper when gatt.writeCharacteristic
// returns false. Smaller values can improve throughput on congested stacks.
writeRetryDelayMs ?: number;
// Maximum number of immediate write attempts before falling back to the give-up timeout.
writeMaxAttempts ?: number;
// Timeout (ms) to wait for a late onCharacteristicWrite callback after all retries fail.
writeGiveupTimeoutMs ?: number;
// Packet Receipt Notification (PRN) window size in packets. If set, DFU
// manager will send a Set PRN command to the device and wait for PRN
// notifications after this many packets. Default: 12.
prn ?: number;
// Timeout (ms) to wait for a PRN notification once the window is reached.
// Default: 10000 (10s).
prnTimeoutMs ?: number;
// When true, disable PRN waits automatically after the first timeout to prevent
// repeated long stalls on devices that do not send PRNs. Default: true.
disablePrnOnTimeout ?: boolean;
// Time (ms) to wait for outstanding fire-and-forget writes to drain before issuing
// the activate/validate control command. Default: 3000.
drainOutstandingTimeoutMs ?: number;
controlTimeout ?: number;
onProgress ?: (percent : number) => void;
onLog ?: (message : string) => void;
controlParser ?: (data : Uint8Array) => ControlParserResult | null;
}
export type DfuManagerType = {
startDfu : (deviceId : string, firmwareBytes : Uint8Array, options ?: DfuOptions) => Promise<void>;
}
// Lightweight runtime / UTS shims and missing types
// These are conservative placeholders to satisfy typings used across platform files.
// UTSJSONObject: bundler environments used by the build may not support
// TypeScript-style index signatures in this .uts context. Use a conservative
// 'any' alias so generated code doesn't rely on unsupported syntax while
// preserving a usable type at the source level.
export type UTSJSONObject = any;
// ByteArray / Int are used in the Android platform code to interop with Java APIs.
// Define minimal aliases so source can compile. Runtime uses Uint8Array and number.
export type ByteArray = any; // runtime will use Java byte[] via UTS bridge; keep as any here
// Callback types used by service_manager and index wrappers
export type BleDataReceivedCallback = (data: Uint8Array) => void;
export type BleScanResult = {
deviceId: string;
name?: string;
rssi?: number;
advertising?: any;
};
// Minimal UI / framework placeholders (some files reference these in types only)
export type ComponentPublicInstance = any;
export type UniElement = any;
export type UniPage = any;
// Platform service contract (actual implementations live in per-platform entrypoints).
// Provide a lightweight interface so source-level code can rely on concrete method
// names and signatures without emitting duplicate runtime classes from this shared file.
export interface BluetoothService {
// Event emitter style
on(event: BleEvent | string, callback: BleEventCallback): void;
off(event: BleEvent | string, callback?: BleEventCallback): void;
// Scanning / discovery
scanDevices(options?: ScanDevicesOptions): Promise<void>;
// Connection management
connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt): Promise<void>;
disconnectDevice(deviceId: string, protocol?: string): Promise<void>;
getConnectedDevices(): MultiProtocolDevice[];
// Services / characteristics
getServices(deviceId: string): Promise<BleService[]>;
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]>;
// Read / write / notify
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer>;
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean>;
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleNotifyCallback): Promise<void>;
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void>;
// Convenience helpers
getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces>;
}
// Runtime protocol handler base class. Exporting a concrete class ensures the
// generator emits a resolvable runtime type that platform handlers can extend.
// Source-level code can still use the ScanHandler type for typing.
// Runtime ProtocolHandler is implemented in `protocol_handler.uts`.
// Keep the public typing in this file minimal to avoid duplicate runtime
// declarations. Consumers that need the runtime class should import it from
// './protocol_handler.uts'.

View File

@@ -0,0 +1,286 @@
import type {
BleDevice,
BleConnectionState,
BleEvent,
BleEventCallback,
BleEventPayload,
BleConnectOptionsExt,
AutoBleInterfaces,
BleDataPayload,
SendDataPayload,
BleOptions,
MultiProtocolDevice,
BleProtocolType,
ScanDevicesOptions
} from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
class DeviceContext {
device: BleDevice;
protocol: BleProtocolType;
state: BleConnectionState;
handler: ProtocolHandler;
constructor(device: BleDevice, protocol: BleProtocolType, handler: ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0;
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>();
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
let defaultBluetoothService: BluetoothService | null = null;
let connectionHooked = false;
function emit(event: BleEvent, payload: BleEventPayload) {
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try { cb(payload); } catch (e) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler, bluetoothService?: BluetoothService) {
super(bluetoothService);
this._raw = raw ?? null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices == 'function') {
await rawTyped.scanDevices(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect == 'function') {
await rawTyped.connect(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect == 'function') {
await rawTyped.disconnect(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData == 'function') {
await rawTyped.sendData(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect == 'function') {
return await rawTyped.autoConnect(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' };
}
}
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object') return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] == 'function') return true;
if (typeof r['connect'] == 'function') return true;
if (typeof r['disconnect'] == 'function') return true;
if (typeof r['sendData'] == 'function') return true;
if (typeof r['autoConnect'] == 'function') return true;
if (typeof r['protocol'] == 'string') return true;
return false;
}
function ensureConnectionHook() {
if (connectionHooked) return;
connectionHooked = true;
const dm = DeviceManager.getInstance();
dm.onConnectionStateChange((deviceId, state) => {
let matched = false;
deviceMap.forEach((ctx) => {
if (ctx.device.deviceId == deviceId) {
ctx.state = state;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol: ctx.protocol, state });
matched = true;
}
});
if (!matched) {
emit('connectionStateChanged', { event: 'connectionStateChanged', device: { deviceId, name: '', rssi: 0 }, protocol: activeProtocol, state });
}
});
}
export const registerProtocolHandler = (handler: any) => {
if (handler == null) return;
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = handler as ProtocolHandler;
} else if (isRawProtocolHandler(handler)) {
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler, defaultBluetoothService);
(activeHandler as ProtocolHandler).protocol = proto;
} else {
console.warn('[AKBLE] registerProtocolHandler: unsupported handler type, ignoring', handler);
return;
}
activeProtocol = proto;
ensureConnectionHook();
}
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
ensureDefaultProtocolHandler();
if (activeHandler == null) {
console.log('[AKBLE] no active scan handler registered');
return;
}
const handler = activeHandler as ProtocolHandler;
const original = options ?? null;
const scanOptions: ScanDevicesOptions = {} as ScanDevicesOptions;
if (original != null) {
if (original.protocols != null) scanOptions.protocols = original.protocols;
if (original.optionalServices != null) scanOptions.optionalServices = original.optionalServices;
if (original.timeout != null) scanOptions.timeout = original.timeout;
}
const userFound = original?.onDeviceFound ?? null;
scanOptions.onDeviceFound = (device: BleDevice) => {
emit('deviceFound', { event: 'deviceFound', device });
if (userFound != null) {
try { userFound(device); } catch (err) { }
}
};
const userFinished = original?.onScanFinished ?? null;
scanOptions.onScanFinished = () => {
emit('scanFinished', { event: 'scanFinished' });
if (userFinished != null) {
try { userFinished(); } catch (err) { }
}
};
try {
await handler.scanDevices(scanOptions);
} catch (e) {
console.warn('[AKBLE] scanDevices handler error', e);
}
}
export const connectDevice = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<void> => {
const handler = activeHandler;
if (handler == null) throw new Error('No protocol handler');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2;
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
}
export const disconnectDevice = async (deviceId: string, protocol: BleProtocolType): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null) return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
deviceMap.delete(getDeviceKey(deviceId, protocol));
}
export const sendData = async (payload: SendDataPayload, options?: BleOptions): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null) throw new Error('Device not connected');
const deviceCtx = ctx as DeviceContext;
if (deviceCtx.handler == null) throw new Error('sendData not supported for this protocol');
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data });
}
export const getConnectedDevices = (): MultiProtocolDevice[] => {
const result: MultiProtocolDevice[] = [];
deviceMap.forEach((ctx: DeviceContext) => {
const dev: MultiProtocolDevice = {
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
};
result.push(dev);
});
return result;
}
export const getConnectionState = (deviceId: string, protocol: BleProtocolType): BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null) return 0;
return ctx.state;
}
export const on = (event: BleEvent, callback: BleEventCallback) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
}
export const off = (event: BleEvent, callback?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
} else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
}
function getDeviceKey(deviceId: string, protocol: BleProtocolType): string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null) throw new Error('autoConnect not supported for this protocol');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
return await handler.autoConnect(device, options) as AutoBleInterfaces;
}
function ensureDefaultProtocolHandler(): void {
if (activeHandler != null) return;
const service = defaultBluetoothService;
if (service == null) return;
try {
const dm = DeviceManager.getInstance();
const raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions) => {
return dm.startScan(options ?? {} as ScanDevicesOptions);
},
connect: (device, options?: BleConnectOptionsExt) => dm.connectDevice(device.deviceId, options),
disconnect: (device) => dm.disconnectDevice(device.deviceId),
autoConnect: () => Promise.resolve({ serviceId: '', writeCharId: '', notifyCharId: '' })
};
const wrapper = new ProtocolHandlerWrapper(raw, service);
activeHandler = wrapper;
activeProtocol = raw.protocol as BleProtocolType;
ensureConnectionHook();
console.log('[AKBLE] default protocol handler registered', activeProtocol);
} catch (e) {
console.warn('[AKBLE] failed to register default protocol handler', e);
}
}
export const setDefaultBluetoothService = (service: BluetoothService) => {
defaultBluetoothService = service;
ensureDefaultProtocolHandler();
};

View File

@@ -0,0 +1,5 @@
{
"dependencies": [
]
}

View File

@@ -0,0 +1,250 @@
import type { BleDevice, BleConnectOptionsExt, BleConnectionState, BleConnectionStateChangeCallback, ScanDevicesOptions } from '../interface.uts';
declare const wx: any;
type PendingConnect = {
resolve: () => void;
reject: (err?: any) => void;
timer?: number;
};
function now(): number {
return Date.now();
}
export class DeviceManager {
private static instance: DeviceManager | null = null;
private devices = new Map<string, BleDevice>();
private connectionStates = new Map<string, BleConnectionState>();
private connectionListeners: BleConnectionStateChangeCallback[] = [];
private pendingConnects = new Map<string, PendingConnect>();
private scanOptions: ScanDevicesOptions | null = null;
private scanTimer: number | null = null;
private adapterReady: boolean = false;
private adapterPromise: Promise<void> | null = null;
private discoveryActive: boolean = false;
private deviceFoundRegistered: boolean = false;
private connectionEventRegistered: boolean = false;
private constructor() {}
static getInstance(): DeviceManager {
if (DeviceManager.instance == null) {
DeviceManager.instance = new DeviceManager();
}
return DeviceManager.instance!;
}
private ensureAdapter(): Promise<void> {
if (this.adapterReady) return Promise.resolve();
if (this.adapterPromise != null) return this.adapterPromise!;
this.adapterPromise = new Promise<void>((resolve, reject) => {
wx.openBluetoothAdapter({
success: () => {
this.adapterReady = true;
this.adapterPromise = null;
this.ensureEventHandlers();
resolve();
},
fail: (err: any) => {
this.adapterPromise = null;
reject(err ?? new Error('openBluetoothAdapter failed'));
}
});
});
return this.adapterPromise!;
}
private ensureEventHandlers() {
if (!this.deviceFoundRegistered) {
this.deviceFoundRegistered = true;
wx.onBluetoothDeviceFound((res: any) => {
try { this.handleDeviceFound(res); } catch (e) { }
});
}
if (!this.connectionEventRegistered) {
this.connectionEventRegistered = true;
wx.onBLEConnectionStateChange((res: any) => {
try { this.handleConnectionState(res); } catch (e) { }
});
}
}
startScan(options: ScanDevicesOptions): Promise<void> {
return this.ensureAdapter().then(() => {
return this.beginScan(options ?? {} as ScanDevicesOptions);
});
}
private beginScan(options: ScanDevicesOptions): Promise<void> {
if (this.discoveryActive) {
this.stopScanInternal();
}
this.scanOptions = options;
const services = options.optionalServices ?? null;
return new Promise<void>((resolve, reject) => {
wx.startBluetoothDevicesDiscovery({
services: services ?? undefined,
allowDuplicatesKey: false,
success: () => {
this.discoveryActive = true;
if (options.timeout != null && options.timeout > 0) {
this.scanTimer = setTimeout(() => {
this.stopScanInternal();
}, options.timeout);
}
resolve();
},
fail: (err: any) => {
this.discoveryActive = false;
reject(err ?? new Error('startBluetoothDevicesDiscovery failed'));
}
});
});
}
stopScan(): void {
this.stopScanInternal();
}
private stopScanInternal() {
if (!this.discoveryActive) return;
this.discoveryActive = false;
try {
wx.stopBluetoothDevicesDiscovery({});
} catch (e) { }
if (this.scanTimer != null) {
clearTimeout(this.scanTimer);
this.scanTimer = null;
}
const finished = this.scanOptions?.onScanFinished;
this.scanOptions = null;
if (finished != null) {
try { finished(); } catch (e) { }
}
}
private handleDeviceFound(res: any) {
const list: any[] = res?.devices ?? [];
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item == null) continue;
const deviceId = item.deviceId ?? item.deviceId ?? '';
if (!deviceId) continue;
const name = item.name ?? item.localName ?? 'Unknown';
const rssi = item.RSSI ?? item.rssi ?? 0;
let device = this.devices.get(deviceId);
if (device == null) {
device = { deviceId, name, rssi, lastSeen: now() };
this.devices.set(deviceId, device);
} else {
device.name = name;
device.rssi = rssi;
device.lastSeen = now();
}
const cb = this.scanOptions?.onDeviceFound;
if (cb != null) {
try { cb(device); } catch (e) { }
}
}
}
private handleConnectionState(res: any) {
const deviceId = res?.deviceId ?? '';
if (!deviceId) return;
const connected = res?.connected === true;
const state: BleConnectionState = connected ? 2 : 0;
this.connectionStates.set(deviceId, state);
const pending = this.pendingConnects.get(deviceId);
if (pending != null) {
this.pendingConnects.delete(deviceId);
if (pending.timer != null) clearTimeout(pending.timer);
if (connected) {
try { pending.resolve(); } catch (e) { }
} else {
try { pending.reject(new Error('连接断开')); } catch (e) { }
}
}
for (let i = 0; i < this.connectionListeners.length; i++) {
const listener = this.connectionListeners[i];
try { listener(deviceId, state); } catch (e) { }
}
}
connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
return this.ensureAdapter().then(() => {
const timeout = options?.timeout ?? 15000;
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingConnects.delete(deviceId);
reject(new Error('连接超时'));
}, timeout);
this.pendingConnects.set(deviceId, { resolve, reject, timer });
wx.createBLEConnection({
deviceId,
timeout,
success: () => {
this.pendingConnects.delete(deviceId);
clearTimeout(timer);
this.connectionStates.set(deviceId, 2);
resolve();
},
fail: (err: any) => {
this.pendingConnects.delete(deviceId);
clearTimeout(timer);
reject(err ?? new Error('createBLEConnection failed'));
}
});
});
});
}
disconnectDevice(deviceId: string): Promise<void> {
return new Promise((resolve, reject) => {
wx.closeBLEConnection({
deviceId,
success: () => {
this.connectionStates.set(deviceId, 0);
resolve();
},
fail: (err: any) => {
reject(err ?? new Error('closeBLEConnection failed'));
}
});
});
}
reconnectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
const attempts = options?.maxAttempts ?? 3;
const interval = options?.interval ?? 3000;
let count = 0;
const attempt = (): Promise<void> => {
return this.connectDevice(deviceId, options).catch((err) => {
count++;
if (count >= attempts) throw err;
return new Promise<void>((resolve) => {
setTimeout(() => resolve(attempt()), interval);
});
});
};
return attempt();
}
getConnectedDevices(): BleDevice[] {
const result: BleDevice[] = [];
this.devices.forEach((device, id) => {
if (this.connectionStates.get(id) == 2) {
result.push(device);
}
});
return result;
}
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
this.connectionListeners.push(listener);
}
getDevice(deviceId: string): BleDevice | null {
return this.devices.get(deviceId) ?? null;
}
}

View File

@@ -0,0 +1,9 @@
import type { DfuManagerType, DfuOptions } from '../interface.uts';
class MpWeixinDfuManager implements DfuManagerType {
async startDfu(_deviceId: string, _firmwareBytes: Uint8Array, _options?: DfuOptions): Promise<void> {
throw new Error('小程序平台暂未实现 DFU 功能');
}
}
export const dfuManager = new MpWeixinDfuManager();

View File

@@ -0,0 +1,90 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback, BleProtocolType, BluetoothService as BluetoothServiceContract } from '../interface.uts';
const serviceManager = ServiceManager.getInstance();
class MpWeixinBluetoothService implements BluetoothServiceContract {
scanDevices(options?: ScanDevicesOptions | null): Promise<void> {
return BluetoothManager.scanDevices(options ?? null);
}
async connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt | null): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.connectDevice(deviceId, proto, options ?? null);
}
async disconnectDevice(deviceId: string, protocol?: string): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.disconnectDevice(deviceId, proto);
}
getConnectedDevices(): MultiProtocolDevice[] {
return BluetoothManager.getConnectedDevices();
}
on(event: BleEvent | string, callback: BleEventCallback): void {
BluetoothManager.on(event as BleEvent, callback);
}
off(event: BleEvent | string, callback?: BleEventCallback | null): void {
BluetoothManager.off(event as BleEvent, callback ?? null);
}
getServices(deviceId: string): Promise<BleService[]> {
return serviceManager.getServices(deviceId);
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return serviceManager.getCharacteristics(deviceId, serviceId);
}
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const services = await this.getServices(deviceId);
if (services.length == 0) throw new Error('未发现服务');
let serviceId = services[0].uuid;
for (let i = 0; i < services.length; i++) {
const uuid = services[i].uuid ?? '';
if (/^bae/i.test(uuid)) {
serviceId = uuid;
break;
}
}
const characteristics = await this.getCharacteristics(deviceId, serviceId);
if (characteristics.length == 0) throw new Error('未发现特征值');
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse == true)) {
writeCharId = c.uuid;
}
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) {
notifyCharId = c.uuid;
}
}
if (writeCharId == '' || notifyCharId == '') throw new Error('未找到合适的写入或通知特征');
return { serviceId, writeCharId, notifyCharId };
}
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, value, options);
}
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export class BluetoothServiceShape extends MpWeixinBluetoothService {}
const bluetoothServiceInstance = new BluetoothServiceShape();
BluetoothManager.setDefaultBluetoothService(bluetoothServiceInstance);
export const bluetoothService: BluetoothServiceContract = bluetoothServiceInstance;
export function getBluetoothService(): BluetoothServiceShape {
return bluetoothServiceInstance;
}
export { dfuManager } from './dfu_manager.uts';

View File

@@ -0,0 +1,268 @@
import type { BleService, BleCharacteristic, BleDataReceivedCallback, BleCharacteristicProperties, WriteCharacteristicOptions, AutoDiscoverAllResult } from '../interface.uts';
import type { BleDevice } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
declare const wx: any;
type PendingRead = {
resolve: (data: ArrayBuffer) => void;
reject: (err?: any) => void;
timer?: number;
};
function toUint8Array(buffer: ArrayBuffer): Uint8Array {
return new Uint8Array(buffer ?? new ArrayBuffer(0));
}
function toArrayBuffer(bytes: Uint8Array | ArrayBuffer): ArrayBuffer {
return bytes instanceof Uint8Array ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : (bytes ?? new ArrayBuffer(0));
}
function makeProperties(item: any): BleCharacteristicProperties {
const props = item?.properties ?? {};
const read = props.read === true;
const write = props.write === true;
const notify = props.notify === true;
const indicate = props.indicate === true;
const writeNoRsp = props.writeNoResponse === true || props.writeWithoutResponse === true;
return {
read,
write,
notify,
indicate,
writeWithoutResponse: writeNoRsp,
canRead: read,
canWrite: write || writeNoRsp,
canNotify: notify || indicate
};
}
export class ServiceManager {
private static instance: ServiceManager | null = null;
private services = new Map<string, BleService[]>();
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
private pendingReads = new Map<string, PendingRead>();
private notifyCallbacks = new Map<string, BleDataReceivedCallback>();
private listenersRegistered: boolean = false;
private deviceManager = DeviceManager.getInstance();
private constructor() {}
static getInstance(): ServiceManager {
if (ServiceManager.instance == null) {
ServiceManager.instance = new ServiceManager();
}
return ServiceManager.instance!;
}
private ensureListeners() {
if (this.listenersRegistered) return;
this.listenersRegistered = true;
wx.onBLECharacteristicValueChange((res: any) => {
try { this.handleNotify(res); } catch (e) { }
});
}
private cacheKey(deviceId: string, serviceId: string): string {
return `${deviceId}|${serviceId}`;
}
private notifyKey(deviceId: string, serviceId: string, characteristicId: string): string {
return `${deviceId}|${serviceId}|${characteristicId}`;
}
async getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): Promise<BleService[]> {
const cached = this.services.get(deviceId);
if (cached != null && cached.length > 0) {
if (callback != null) callback(cached, null);
return cached;
}
return new Promise((resolve, reject) => {
wx.getBLEDeviceServices({
deviceId,
success: (res: any) => {
const list: BleService[] = [];
const services: any[] = res?.services ?? [];
for (let i = 0; i < services.length; i++) {
const svc = services[i];
if (svc == null) continue;
list.push({ uuid: svc.uuid, isPrimary: svc.isPrimary === true });
}
this.services.set(deviceId, list);
if (callback != null) callback(list, null);
resolve(list);
},
fail: (err: any) => {
const error = err ?? new Error('getBLEDeviceServices failed');
if (callback != null) callback(null, error);
reject(error);
}
});
});
}
async getCharacteristics(deviceId: string, serviceId: string, callback?: (list: BleCharacteristic[] | null, error?: Error) => void): Promise<BleCharacteristic[]> {
const map = this.characteristics.get(deviceId);
const cached = map != null ? map.get(serviceId) : null;
if (cached != null && cached.length > 0) {
if (callback != null) callback(cached, null);
return cached;
}
return new Promise((resolve, reject) => {
wx.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res: any) => {
const list: BleCharacteristic[] = [];
const chars: any[] = res?.characteristics ?? [];
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
if (ch == null) continue;
list.push({
uuid: ch.uuid,
service: { uuid: serviceId, isPrimary: true },
properties: makeProperties(ch)
});
}
let mapRef = this.characteristics.get(deviceId);
if (mapRef == null) {
mapRef = new Map<string, BleCharacteristic[]>();
this.characteristics.set(deviceId, mapRef);
}
mapRef.set(serviceId, list);
if (callback != null) callback(list, null);
resolve(list);
},
fail: (err: any) => {
const error = err ?? new Error('getBLEDeviceCharacteristics failed');
if (callback != null) callback(null, error);
reject(error);
}
});
});
}
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
this.ensureListeners();
return new Promise<ArrayBuffer>((resolve, reject) => {
const key = `${deviceId}|${serviceId}|${characteristicId}|read`;
const timer = setTimeout(() => {
this.pendingReads.delete(key);
reject(new Error('读取超时'));
}, 10000);
this.pendingReads.set(key, { resolve, reject, timer });
wx.readBLECharacteristicValue({
deviceId,
serviceId,
characteristicId,
fail: (err: any) => {
this.pendingReads.delete(key);
clearTimeout(timer);
reject(err ?? new Error('readBLECharacteristicValue failed'));
}
});
});
}
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
const buffer = toArrayBuffer(value);
return new Promise<boolean>((resolve, reject) => {
wx.writeBLECharacteristicValue({
deviceId,
serviceId,
characteristicId,
value: buffer,
fail: (err: any) => {
reject(err ?? new Error('writeBLECharacteristicValue failed'));
},
success: () => {
resolve(true);
}
});
});
}
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleDataReceivedCallback): Promise<void> {
this.ensureListeners();
const key = this.notifyKey(deviceId, serviceId, characteristicId);
this.notifyCallbacks.set(key, callback);
return new Promise<void>((resolve, reject) => {
wx.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId,
state: true,
success: () => resolve(),
fail: (err: any) => {
this.notifyCallbacks.delete(key);
reject(err ?? new Error('notifyBLECharacteristicValueChange failed'));
}
});
});
}
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
const key = this.notifyKey(deviceId, serviceId, characteristicId);
this.notifyCallbacks.delete(key);
return new Promise<void>((resolve, reject) => {
wx.notifyBLECharacteristicValueChange({
deviceId,
serviceId,
characteristicId,
state: false,
success: () => resolve(),
fail: (err: any) => {
reject(err ?? new Error('notifyBLECharacteristicValueChange disable failed'));
}
});
});
}
async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
const services = await this.getServices(deviceId);
const allChars: BleCharacteristic[] = [];
for (let i = 0; i < services.length; i++) {
const svc = services[i];
const chars = await this.getCharacteristics(deviceId, svc.uuid);
for (let j = 0; j < chars.length; j++) {
allChars.push(chars[j]);
}
}
return { services, characteristics: allChars };
}
async subscribeAllNotifications(deviceId: string, callback: BleDataReceivedCallback): Promise<void> {
const services = await this.getServices(deviceId);
for (let i = 0; i < services.length; i++) {
const svc = services[i];
const chars = await this.getCharacteristics(deviceId, svc.uuid);
for (let j = 0; j < chars.length; j++) {
const ch = chars[j];
if (ch.properties != null && (ch.properties.notify || ch.properties.indicate)) {
try {
await this.subscribeCharacteristic(deviceId, svc.uuid, ch.uuid, callback);
} catch (e) { }
}
}
}
}
private handleNotify(res: any) {
const deviceId = res?.deviceId ?? '';
const serviceId = res?.serviceId ?? '';
const characteristicId = res?.characteristicId ?? '';
const key = `${deviceId}|${serviceId}|${characteristicId}|read`;
const buffer: ArrayBuffer = res?.value ?? new ArrayBuffer(0);
const pending = this.pendingReads.get(key);
if (pending != null) {
this.pendingReads.delete(key);
if (pending.timer != null) clearTimeout(pending.timer);
try { pending.resolve(buffer); } catch (e) { }
}
const notifyKey = this.notifyKey(deviceId, serviceId, characteristicId);
const cb = this.notifyCallbacks.get(notifyKey);
if (cb != null) {
try { cb(toUint8Array(buffer)); } catch (e) { }
}
}
}

View File

@@ -0,0 +1,977 @@
// Minimal ProtocolHandler runtime class used by pages and components.
// This class adapts the platform `BluetoothService` to a small protocol API
// expected by pages: setConnectionParameters, initialize, testBatteryLevel,
// testVersionInfo. Implemented conservatively to avoid heavy dependencies.
import type { BluetoothService, AutoBleInterfaces, BleService, BleCharacteristic, BleProtocolType, BleDevice, ScanDevicesOptions, BleConnectOptionsExt, SendDataPayload, BleOptions, HealthSubscription, WbbpPacket, HealthData } from './interface.uts'
export type PingResult = {
seq : number;
latencyMs : number;
payload : Uint8Array;
raw : Uint8Array;
ok : boolean;
}
type PendingPingRequest = {
seq : number;
resolve : (result : PingResult) => void;
reject : (reason ?: any) => void;
startedAt : number;
timer : number | null;
}
export class ProtocolHandler {
// bluetoothService may be omitted for lightweight wrappers; allow null
bluetoothService : BluetoothService | null = null
protocol : BleProtocolType = 'standard'
deviceId : string | null = null
serviceId : string | null = null
writeCharId : string | null = null
notifyCharId : string | null = null
healthSubscriptions : HealthSubscription[] = []
initialized : boolean = false
controlResolved : boolean = false
pingSequence : number = 1
pendingPingRequests : PendingPingRequest[] = []
// Accept an optional BluetoothService-like object so wrapper subclasses can call
// `super()` without forcing a runtime instance. Allow broader inputs to satisfy
// legacy callers that pass platform-specific subclasses at runtime.
constructor(bluetoothService ?: BluetoothService | any) {
if (bluetoothService != null) {
this.bluetoothService = bluetoothService as BluetoothService
}
}
// Store active health subscriptions so we can unsubscribe later
// onData will be called with a parsed HealthData object
async subscribeHealthNotifications(onData : (data : HealthData) => void) {
if (this.deviceId == null) throw new Error('deviceId not set')
if (this.bluetoothService == null) throw new Error('bluetoothService not set')
const dev = '' + this.deviceId
const bsvc = this.bluetoothService
const self = this
function wrapHealthPacket(data: HealthData): HealthData {
// Since healthble.uvue handles both direct property access and method calls,
// we can just return the data as-is
return data
}
const deliver = (payload : HealthData) => {
try {
onData(wrapHealthPacket(payload))
} catch (deliverError) {
console.warn('[ProtocolHandler] health notify delivery error', deliverError)
}
}
// candidate UUIDs per protocol doc
const CUSTOM_HEALTH_SERVICE = '6e400010-b5a3-f393-e0a9-e50e24dcca9e'
const CUSTOM_HEART = '6e400011-b5a3-f393-e0a9-e50e24dcca9e'
const CUSTOM_STEP = '6e400012-b5a3-f393-e0a9-e50e24dcca9e'
const CUSTOM_SLEEP = '6e400013-b5a3-f393-e0a9-e50e24dcca9e'
// also consider standard characteristics
const STD_HEART_CHAR = '00002a37-0000-1000-8000-00805f9b34fb' // 2A37
const STD_SPO2_CHAR = '00002a5f-0000-1000-8000-00805f9b34fb' // 2A5F (if present)
function xorCheck(buf : Uint8Array) : boolean {
if (buf == null || buf.length < 4) return false
let x = 0
for (let i = 1; i < buf.length - 1; i++) x ^= buf[i]
return (x & 0xff) == buf[buf.length - 1]
}
function parseWbbp(buf : Uint8Array) : WbbpPacket | null {
// STX LEN CMD SEQ DATA.. CRC(1 or 2 bytes)
if (buf == null || buf.length < 5) return null
if (buf[0] != 0xAA) return null
const len = buf[1]
const minTotal = len + 3 // STX + LEN + LEN bytes + CRC(1)
const altTotal = len + 4 // allow CRC(2) variant observed on newer devices
if (buf.length < minTotal) return null
const dataEnd = 2 + len // index after CMD+SEQ+payload
const crcLength = buf.length - dataEnd
if (crcLength < 1) return null
if (crcLength == 1) {
// original XOR checksum validation
let x = 0
for (let i = 1; i < dataEnd; i++) {
x ^= buf[i]
}
if ((x & 0xff) != buf[buf.length - 1]) {
console.warn('[ProtocolHandler] WBBP checksum mismatch (xor)', { len, data: Array.from(buf) })
return null
}
} else {
// two-byte checksum present accept packet even without knowing CRC16 polynomial
if (buf.length != altTotal) {
console.warn('[ProtocolHandler] WBBP packet length unexpected', { len, total: buf.length })
return null
}
}
const cmd = buf[2]
const seq = buf[3]
const payloadLength = Math.max(0, len - 2)
const payload = buf.slice(4, 4 + payloadLength)
return { cmd, seq, payload }
}
function parseHeartStandard(buf : Uint8Array) {
// Bluetooth SIG Heart Rate Measurement (2A37)
if (buf == null || buf.length < 2) return null
const flags = buf[0]
const hrFormat16 = (flags & 0x01) != 0
let hr = 0
if (!hrFormat16) {
hr = buf[1]
} else {
hr = buf[1] | (buf[2] << 8)
}
return { heartRate: hr }
}
// subscribe helper that registers a callback and stores subscription info
async function subscribeChar(svc : string, char : string) {
try {
console.log('[ProtocolHandler] subscribing to', svc, char)
await bsvc.subscribeCharacteristic(dev, svc, char, (data : Uint8Array) => {
try {
// try WBBP parsing first
console.log(char,data)
const pkt = parseWbbp(data) as WbbpPacket | null
if (pkt != null) {
const cmd = pkt.cmd
const seq = pkt.seq
const p = pkt.payload
if (cmd == 0x00) {
self.handlePingNotify(seq, data, p)
return
} else if (cmd == 0x10) {
// heart rate packet per doc: timestamp(4) + heart(1) + quality(1)
if (p.length >= 6) {
const ts = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]
const heart = p[4]
const quality = p[5]
deliver({ type: 'heart', cmd, seq, timestamp: ts, heartRate: heart, quality })
return
}
} else if (cmd == 0x14) {
// (subscription recorded later)
if (p.length >= 8) {
const ts = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]
const spo2 = p[4]
const quality = p[5]
const pulse = p[6]
deliver({ type: 'spo2', cmd, seq, timestamp: ts, spo2, pulse, quality })
return
}
} else if (cmd == 0x11) {
// step count: timestamp(4) + steps(4) + distance(2) + calories(2) + activity(1)
if (p.length >= 13) {
const ts = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]
const steps = (p[4] << 24) | (p[5] << 16) | (p[6] << 8) | p[7]
const distance = (p[8] << 8) | p[9]
const calories = (p[10] << 8) | p[11]
const activity = p[12]
deliver({ type: 'steps', cmd, seq, timestamp: ts, steps, distance, calories, activity })
return
}
}
}
// if not WBBP or parsing failed, try standard heart/spo2 formats
const stdHeart = parseHeartStandard(data)
if (stdHeart != null) {
const heartRate = (stdHeart as UTSJSONObject)['heartRate'] as number
const data : HealthData = { type: 'heart', heartRate: heartRate }
deliver(data)
return
}
// fallback: raw notify
deliver({ type: 'raw', raw: data })
} catch (e) { console.warn('[ProtocolHandler] health notify handler error', e) }
})
// remember to unsubscribe later
self.healthSubscriptions.push({ serviceUuid: svc, charUuid: char })
console.log('[ProtocolHandler] subscribeChar registered', svc, char)
} catch (e) { console.warn('[ProtocolHandler] subscribeChar failed', svc, char, e) }
}
// try subscribe to custom health service chars first
try {
const allSvcs = await bsvc.getServices(dev)
console.log('[ProtocolHandler] allServices', allSvcs)
const svcSet = new Set(allSvcs.map((s : BleService) => ('' + s.uuid).toLowerCase()))
if (svcSet.has(CUSTOM_HEALTH_SERVICE) || Array.from(svcSet).some((u) => (u as string).indexOf('6e400010') != -1)) {
console.log('[ProtocolHandler] going to subscribe', CUSTOM_HEALTH_SERVICE, CUSTOM_HEART, CUSTOM_STEP, CUSTOM_SLEEP)
// prefer custom UUIDs
await subscribeChar(CUSTOM_HEALTH_SERVICE, CUSTOM_HEART)
await subscribeChar(CUSTOM_HEALTH_SERVICE, CUSTOM_STEP)
await subscribeChar(CUSTOM_HEALTH_SERVICE, CUSTOM_SLEEP)
} else {
// fallback: try standard heart and spo2 characteristics across services
for (let i = 0; i < allSvcs.length; i++) {
const s = allSvcs[i]
if (s == null) continue
const sUuid = '' + s.uuid
const chars = await bsvc.getCharacteristics(dev, sUuid)
for (let j = 0; j < chars.length; j++) {
const c = chars[j]
if (c == null || c.uuid == null) continue
const id = ('' + c.uuid).toLowerCase()
if (id == STD_HEART_CHAR || id.indexOf('2a37') != -1) {
await subscribeChar(sUuid, c.uuid)
}
if (id == STD_SPO2_CHAR || id.indexOf('2a5f') != -1) {
await subscribeChar(sUuid, c.uuid)
}
}
}
}
} catch (e) { console.warn('[ProtocolHandler] subscribeHealthNotifications failed', e) }
// If we didn't find standard/custom health characteristics, try a conservative
// fallback: subscribe to any notify/indicate-capable characteristic so we can
// receive vendor-specific notifies. Cap the number to avoid too many subscriptions.
if (this.healthSubscriptions.length == 0) {
console.log('[ProtocolHandler] no known health chars found — attempting auto-subscribe to notify-capable characteristics')
try {
let autoCount = 0
const svcList = await bsvc.getServices(dev)
for (let si = 0; si < svcList.length; si++) {
if (autoCount >= 6) break
const s = svcList[si]
if (s == null) continue
const sUuid = '' + s.uuid
const chars = await bsvc.getCharacteristics(dev, sUuid)
for (let ci = 0; ci < chars.length; ci++) {
if (autoCount >= 6) break
const c = chars[ci]
if (c == null) continue
try {
const _propsAny = c.properties
if (c.properties != null && (_propsAny.notify || _propsAny.indicate)) {
await subscribeChar(sUuid, c.uuid)
autoCount++
console.log('[ProtocolHandler] auto-subscribed to', sUuid, c.uuid)
}
} catch (e) { console.warn('[ProtocolHandler] auto-subscribe char failed', sUuid, c.uuid, e) }
}
}
} catch (e) { console.warn('[ProtocolHandler] auto-subscribe notify-capable failed', e) }
}
console.log('[ProtocolHandler] subscribeHealthNotifications complete, subscriptions=', this.healthSubscriptions.length)
}
handlePingNotify(seq : number, raw : Uint8Array, payload : Uint8Array) : void {
try {
console.log('[ProtocolHandler] PING received', { seq, raw: Array.from(raw) })
} catch (e) { }
for (let i = 0; i < this.pendingPingRequests.length; i++) {
const entry = this.pendingPingRequests[i]
if (entry == null) continue
if (entry.seq != seq) continue
if (entry.timer != null) {
clearTimeout(entry.timer as number)
entry.timer = null
}
this.pendingPingRequests.splice(i, 1)
const latency = Math.max(0, Date.now() - entry.startedAt)
try {
const payloadCopy = payload != null ? payload.slice(0) : new Uint8Array(0)
const rawCopy = raw != null ? raw.slice(0) : new Uint8Array(0)
entry.resolve({ ok: true, seq, latencyMs: latency, payload: payloadCopy, raw: rawCopy })
} catch (resolveError) {
console.warn('[ProtocolHandler] ping resolve error', resolveError)
}
return
}
// no pending ping matched — still log for diagnostics
try {
console.warn('[ProtocolHandler] ping notification with no pending request', { seq })
} catch (e) { }
}
removePendingPing(entry : PendingPingRequest | null) : void {
if (entry == null) return
if (entry.timer != null) {
clearTimeout(entry.timer as number)
entry.timer = null
}
const idx = this.pendingPingRequests.indexOf(entry)
if (idx != -1) {
this.pendingPingRequests.splice(idx, 1)
}
}
rejectPendingPings(reason : string) : void {
const err = new Error(reason)
for (let i = 0; i < this.pendingPingRequests.length; i++) {
const entry = this.pendingPingRequests[i]
if (entry == null) continue
if (entry.timer != null) {
clearTimeout(entry.timer!!)
entry.timer = null
}
try {
entry.reject(err)
} catch (rejectError) {
console.warn('[ProtocolHandler] ping reject error', rejectError)
}
}
this.pendingPingRequests = []
}
async unsubscribeHealthNotifications() {
if (this.deviceId == null) return
if (this.bluetoothService == null) return
const dev = '' + this.deviceId
const bsvc = this.bluetoothService
for (let i = 0; i < this.healthSubscriptions.length; i++) {
const s = this.healthSubscriptions[i]
try { await bsvc.unsubscribeCharacteristic(dev, s.serviceUuid, s.charUuid) } catch (e) { console.warn('[ProtocolHandler] unsubscribe failed', s, e) }
}
this.healthSubscriptions = []
this.rejectPendingPings('notifications cancelled')
}
setConnectionParameters(deviceId : string, serviceId : string, writeCharId : string, notifyCharId : string) {
const previousDevice = this.deviceId
if (previousDevice != null && previousDevice != deviceId) {
this.rejectPendingPings('device changed')
}
this.deviceId = deviceId
this.serviceId = serviceId
this.writeCharId = writeCharId
this.notifyCharId = notifyCharId
const hasService = serviceId != null && serviceId != ''
const hasWrite = writeCharId != null && writeCharId != ''
const hasNotify = notifyCharId != null && notifyCharId != ''
this.controlResolved = hasService && hasWrite && hasNotify
if (!this.controlResolved) {
this.pingSequence = 1
}
}
nextPingSequence() : number {
const seq = this.pingSequence & 0xff
this.pingSequence = (this.pingSequence + 1) & 0xff
if (this.pingSequence == 0) this.pingSequence = 1
return seq == 0 ? 1 : seq
}
buildVendorPacket(cmd : number, seq : number, data ?: number[]) : Uint8Array {
const payload = data != null ? data : [] as number[]
const len = 2 + payload.length
const arr : number[] = []
arr.push(len)
arr.push(cmd & 0xff)
arr.push(seq & 0xff)
for (let i = 0; i < payload.length; i++) arr.push(payload[i] & 0xff)
let crc = 0
for (let i = 0; i < arr.length; i++) crc ^= (arr[i] & 0xff)
const pkt = new Uint8Array(arr.length + 2)
pkt[0] = 0xAA
for (let i = 0; i < arr.length; i++) pkt[i + 1] = arr[i] & 0xff
pkt[pkt.length - 1] = crc & 0xff
return pkt
}
async prepareControlChannel(force : boolean = false) : Promise<AutoBleInterfaces | null> {
return await this.ensureControlChannel(force)
}
async ensureControlChannel(force : boolean = false) : Promise<AutoBleInterfaces | null> {
const deviceIdLocal = this.deviceId
const bsvc = this.bluetoothService
if (deviceIdLocal == null || deviceIdLocal == '') return null
if (bsvc == null) throw new Error('bluetoothService not set')
if (!force) {
const hasService = this.serviceId != null && this.serviceId != ''
const hasWrite = this.writeCharId != null && this.writeCharId != ''
const hasNotify = this.notifyCharId != null && this.notifyCharId != ''
if (hasService && hasWrite && hasNotify) {
return { serviceId: this.serviceId ?? '', writeCharId: this.writeCharId ?? '', notifyCharId: this.notifyCharId ?? '' }
}
}
let resolved : AutoBleInterfaces | null = null
try {
const candidate = await bsvc.getAutoBleInterfaces(deviceIdLocal)
if (candidate != null && candidate.serviceId != null && candidate.serviceId != '' && candidate.writeCharId != null && candidate.writeCharId != '' && candidate.notifyCharId != null && candidate.notifyCharId != '') {
resolved = candidate
}
} catch (autoErr) {
console.warn('[ProtocolHandler] getAutoBleInterfaces failed', autoErr)
}
if (resolved == null) {
resolved = await this.findVendorControlChannel(deviceIdLocal)
}
if (resolved != null) {
this.serviceId = '' + resolved.serviceId
this.writeCharId = '' + resolved.writeCharId
this.notifyCharId = '' + resolved.notifyCharId
this.controlResolved = true
return { serviceId: this.serviceId ?? '', writeCharId: this.writeCharId ?? '', notifyCharId: this.notifyCharId ?? '' }
}
return null
}
async findVendorControlChannel(deviceId : string) : Promise<AutoBleInterfaces | null> {
const bsvc = this.bluetoothService
if (bsvc == null) return null
try {
const services = await bsvc.getServices(deviceId)
if (services == null || services.length == 0) return null
const UART_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
const UART_TX = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'
const UART_RX = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
const UART_CTRL = '6e400004-b5a3-f393-e0a9-e50e24dcca9e'
let chosenService = ''
for (let i = 0; i < services.length; i++) {
const svc = services[i]
if (svc == null || svc.uuid == null) continue
const su = ('' + svc.uuid).toLowerCase()
if (su.indexOf('6e400001') != -1 || su == UART_SERVICE) {
chosenService = '' + svc.uuid
break
}
}
if (chosenService == '') {
const fallbackSvc = services[0]
if (fallbackSvc != null && fallbackSvc.uuid != null) chosenService = '' + fallbackSvc.uuid
}
if (chosenService == '') return null
const chars = await bsvc.getCharacteristics(deviceId, chosenService)
if (chars == null || chars.length == 0) return null
let ctrlCandidate = ''
let txCandidate = ''
let notifyCandidate = ''
let fallbackWrite = ''
let fallbackNotify = ''
for (let i = 0; i < chars.length; i++) {
const c = chars[i]
if (c == null || c.uuid == null) continue
const idLower = ('' + c.uuid).toLowerCase()
if (idLower == UART_CTRL) ctrlCandidate = '' + c.uuid
if (idLower == UART_TX) txCandidate = '' + c.uuid
if (idLower == UART_RX) notifyCandidate = '' + c.uuid
if (fallbackWrite == '' && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse == true)) fallbackWrite = '' + c.uuid
if (fallbackNotify == '' && c.properties != null && (c.properties.notify || c.properties.indicate)) fallbackNotify = '' + c.uuid
}
let writeChar = ctrlCandidate != '' ? ctrlCandidate : txCandidate
if (writeChar == '' && fallbackWrite != '') writeChar = fallbackWrite
let notifyChar = notifyCandidate != '' ? notifyCandidate : fallbackNotify
if (writeChar == '' || notifyChar == '') return null
return { serviceId: chosenService, writeCharId: writeChar, notifyCharId: notifyChar }
} catch (err) {
console.warn('[ProtocolHandler] findVendorControlChannel failed', err)
}
return null
}
async sendPing(timeoutMs : number = 3000) : Promise<PingResult> {
if (timeoutMs <= 0) timeoutMs = 3000
const deviceIdLocal = this.deviceId
const bsvc = this.bluetoothService
if (deviceIdLocal == null || deviceIdLocal == '') throw new Error('deviceId not set')
if (bsvc == null) throw new Error('bluetoothService not set')
const channel = await this.ensureControlChannel(false)
if (channel == null || channel.serviceId == null || channel.serviceId == '' || channel.writeCharId == null || channel.writeCharId == '') {
throw new Error('control channel unavailable')
}
const seq = this.nextPingSequence()
const frame = this.buildVendorPacket(0x00, seq, [0x00])
const startedAt = Date.now()
const self = this
return await new Promise<PingResult>((resolve, reject) => {
const entry : PendingPingRequest = { seq, resolve, reject, startedAt, timer: null }
const timeoutHandle = setTimeout(() => {
self.removePendingPing(entry)
reject(new Error('ping timeout'))
}, timeoutMs)
entry.timer = timeoutHandle as number
self.pendingPingRequests.push(entry)
const serviceUuid = channel.serviceId
const writeUuid = channel.writeCharId
;(async () => {
try {
const ok = await bsvc.writeCharacteristic(deviceIdLocal, serviceUuid, writeUuid, frame, { waitForResponse: true })
if (!ok) {
self.removePendingPing(entry)
reject(new Error('ping write failed'))
}
} catch (err) {
self.removePendingPing(entry)
reject(err)
}
})()
})
}
// initialize: optional setup, returns a Promise that resolves when ready
async initialize() : Promise<void> {
// Simple async initializer — keep implementation minimal and generator-friendly.
try {
// If bluetoothService exposes any protocol-specific setup, call it here.
this.initialized = true
return
} catch (e) {
throw e
}
}
// Protocol lifecycle / operations — default no-ops so generated code has
// concrete member references and platform-specific handlers can override.
async scanDevices(options ?: ScanDevicesOptions) : Promise<void> { return; }
async connect(device : BleDevice, options ?: BleConnectOptionsExt) : Promise<void> { return; }
async disconnect(device : BleDevice) : Promise<void> { return; }
async sendData(device : BleDevice, payload ?: SendDataPayload, options ?: BleOptions) : Promise<void> { return; }
async autoConnect(device : BleDevice, options ?: BleConnectOptionsExt) : Promise<AutoBleInterfaces> { return { serviceId: '', writeCharId: '', notifyCharId: '' }; }
// Example: testBatteryLevel will attempt to read the battery characteristic
// if write/notify-based protocol is not available. Returns number percentage.
async testBatteryLevel() : Promise<number> {
if (this.deviceId == null) throw new Error('deviceId not set')
// copy to local so Kotlin generator can smart-cast the value across awaits
const deviceId = this.deviceId
// try reading standard Battery characteristic (180F -> 2A19)
if (this.bluetoothService == null) throw new Error('bluetoothService not set')
const services = await this.bluetoothService.getServices(deviceId)
let found : BleService | null = null
for (let i = 0; i < services.length; i++) {
const s = services[i]
const uuidCandidate : string | null = (s != null && s.uuid != null ? s.uuid : null)
const uuid = uuidCandidate != null ? ('' + uuidCandidate).toLowerCase() : ''
if (uuid.indexOf('180f') != -1) { found = s; break }
}
if (found == null) {
// fallback: if writeCharId exists and notify available use protocol (not implemented)
return -1
}
const foundUuid = found!.uuid
const charsRaw = await this.bluetoothService.getCharacteristics(deviceId, foundUuid)
const chars : BleCharacteristic[] = charsRaw
const batChar = chars.find((c : BleCharacteristic) => ((c.properties != null && c.properties.read) || (c.uuid != null && ('' + c.uuid).toLowerCase().includes('2a19'))))
if (batChar == null) return -1
const buf = await this.bluetoothService.readCharacteristic(deviceId, foundUuid, batChar.uuid)
const arr = new Uint8Array(buf)
try {
const hex = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(' ')
console.log('[ProtocolHandler] testBatteryLevel raw hex:', hex, 'len=', arr.length)
} catch (e) { }
if (arr.length > 0) {
console.log('[ProtocolHandler] testBatteryLevel parsed battery:', arr[0])
return arr[0]
}
return -1
}
// Best-effort time synchronization on connect.
// Try writing standard Current Time Service (CTS: 0x1805 / char 0x2A2B).
// Keep H5-compatible: return Promise<string> with 'timeSynced' or 'timeFailed' or ''.
async synchronizeOnConnect() : Promise<string> {
const deviceId = this.deviceId
const bsvc = this.bluetoothService
if (deviceId == null || bsvc == null) return Promise.resolve('')
const dev = '' + deviceId
try {
console.log('[ProtocolHandler] synchronizeOnConnect: attempting CTS time write')
const svcs = await bsvc.getServices(dev)
for (let i = 0; i < svcs.length; i++) {
const s = svcs[i]
if (s == null || s.uuid == null) continue
const su = ('' + s.uuid).toLowerCase()
if (su.indexOf('1805') == -1 && su.indexOf('current') == -1) continue
const svcUuid = '' + s.uuid
try {
const chars = await bsvc.getCharacteristics(dev, svcUuid)
for (let j = 0; j < chars.length; j++) {
const c = chars[j]
if (c == null || c.uuid == null) continue
const id = ('' + c.uuid).toLowerCase()
if (id.indexOf('2a2b') != -1) {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
const hours = now.getHours()
const minutes = now.getMinutes()
const seconds = now.getSeconds()
const jsDay = now.getDay()
const dayOfWeek = jsDay == 0 ? 7 : jsDay
const payload = new Uint8Array(10)
payload[0] = year & 0xff
payload[1] = (year >> 8) & 0xff
payload[2] = month
payload[3] = day
payload[4] = hours
payload[5] = minutes
payload[6] = seconds
payload[7] = dayOfWeek
payload[8] = 0 // fractions256
payload[9] = 1 // adjust reason: manual update
try {
await bsvc.writeCharacteristic(dev, svcUuid, c.uuid, payload, { waitForResponse: true })
console.log('[ProtocolHandler] CTS time write succeeded')
return 'timeSynced'
} catch (e) {
console.warn('[ProtocolHandler] CTS write failed', e)
// try other chars/services
}
}
}
} catch (e) {
console.warn('[ProtocolHandler] error enumerating CTS chars', e)
}
}
console.log('[ProtocolHandler] CTS not available or writes failed')
return 'timeFailed'
} catch (e) {
console.warn('[ProtocolHandler] synchronizeOnConnect unexpected error', e)
return ''
}
}
// testVersionInfo: try to read Device Information characteristics or return empty
async testVersionInfo(hw : boolean) : Promise<string> {
// copy to local so Kotlin generator can smart-cast the value across awaits
console.log('testVersionInfo:',hw,this.deviceId,this.bluetoothService)
const deviceId = this.deviceId
if (deviceId == null) return ''
// Device Information service 180A, characteristics: 2A26 (SW), 2A27 (HW) sometimes
if (this.bluetoothService == null) return ''
// Add delay to allow service discovery to complete on Android reconnection
await new Promise<void>((resolve) => { setTimeout(() => resolve(), 1000) })
const _services = await this.bluetoothService.getServices(deviceId)
const services2 : BleService[] = _services
console.log('[ProtocolHandler] services2 length:', services2.length)
for (let i = 0; i < services2.length; i++) {
const s = services2[i]
console.log('[ProtocolHandler] service', i, 'uuid:', s?.uuid)
}
let found2 : BleService | null = null
for (let i = 0; i < services2.length; i++) {
const s = services2[i]
const uuidCandidate : string | null = (s != null && s.uuid != null ? s.uuid : null)
const uuid = uuidCandidate != null ? ('' + uuidCandidate).toLowerCase() : ''
if (uuid.indexOf('180a') != -1) { found2 = s; break }
}
console.log('testVersionInfo 1:',hw)
// If Device Information service exists, locate candidate characteristics.
// Do NOT return early if the service is missing — fallbacks (including UART) exist below.
let found2Uuid : string | null = null
let chars : BleCharacteristic[] = []
let target : BleCharacteristic | null = null
if (found2 != null) {
console.log('testVersionInfo 2:',found2)
const found2NonNull = found2 as BleService
found2Uuid = '' + found2NonNull.uuid
try {
console.log('testVersionInfo 2:',found2)
const _chars = await this.bluetoothService.getCharacteristics(deviceId, found2Uuid)
console.log('ak _chars:', _chars)
chars = _chars as BleCharacteristic[]
target = chars.find((c) => {
const id = ('' + c.uuid).toLowerCase()
console.log('[ProtocolHandler] checking char uuid:', c.uuid, 'id:', id, 'hw:', hw)
if (hw) {
const match = id.includes('2a27') || id.includes('hardware')
console.log('[ProtocolHandler] hw match:', match)
return match
}
const match1 = id.includes('2a26') || id.includes('firmware')
const match2 = id.includes('2a28') || id.includes('software')
console.log('[ProtocolHandler] sw match1:', match1, 'match2:', match2)
return match1 || match2
}) as BleCharacteristic | null
} catch (e) {
console.warn('[ProtocolHandler] failed to get DeviceInfo chars', e)
chars = []
target = null
}
}
// debug: log which characteristic was selected and its uuid (may be null)
console.log('[ProtocolHandler] testVersionInfo selected characteristic:', target != null ? target.uuid : null, 'hw=', hw)
// If we found the Device Information service, log available characteristics to help
// diagnose Android path where IDs or properties may differ from Web shapes.
if (found2Uuid != null) {
try {
console.log('[ProtocolHandler] DeviceInfo service UUID=', found2Uuid, 'chars.length=', chars.length)
for (let i = 0; i < chars.length; i++) {
const c = chars[i]
try {
console.log('[ProtocolHandler] char:', i, 'uuid=', c.uuid, 'props=', c.properties)
} catch (e) { }
}
} catch (e) { }
}
// alias bluetoothService to local variable for inner functions
const bsvc = this.bluetoothService
// local non-null device id for inner functions (we already guard above)
const dev = deviceId as string
// helper: treat short / non-printable results as invalid version strings
function isPrintableVersion(arr : Uint8Array) : boolean {
if (arr == null || arr.length == 0) return false
// reject obvious single-byte numeric measurements (battery etc.)
if (arr.length == 1) return false
let printable = 0
for (let i = 0; i < arr.length; i++) {
const b = arr[i]
if (b >= 0x20 && b <= 0x7e) printable++
}
// accept if at least 50% printable characters
if ((printable / arr.length) >= 0.5) return true
// also accept common version-like ASCII (e.g. "1.0.3", "v2.1") even if ratio is low
try {
const s = new TextDecoder().decode(arr).trim()
if (s.length >= 2 && /^[vV]?\d[\d\.\- ]+$/.test(s)) return true
} catch (e) { /* ignore decode errors */ }
return false
}
async function tryReadChar(serviceUuid : string, charUuid : string) : Promise<string> {
await new Promise<void>((resolve) => { setTimeout(() => resolve(), 1000) })
try {
// avoid accidentally reading battery characteristic
const low = ('' + charUuid).toLowerCase()
if (low.indexOf('2a19') != -1) {
console.log('[ProtocolHandler] skipping battery char 2A19')
return ''
}
const buf = await bsvc!.readCharacteristic(dev, serviceUuid, charUuid)
// Prefer strong typing: treat buf as ArrayBuffer/Uint8Array primarily.
try { console.log('[ProtocolHandler] raw readCharacteristic buf typeof=', typeof buf) } catch (e) { }
let arr : Uint8Array = new Uint8Array(0)
try {
// Primary, strongly-typed paths
if (typeof ArrayBuffer !== 'undefined' && buf instanceof ArrayBuffer) {
arr = new Uint8Array(buf as ArrayBuffer)
} else if (Array.isArray(buf)) {
arr = new Uint8Array(buf as number[])
} else {
// leave arr empty for downstream diagnostics
console.log('ak:',arr)
arr = new Uint8Array(0)
}
} catch (e) { arr = new Uint8Array(0) }
try { console.log('[ProtocolHandler] normalized read buffer len=', arr.length, 'svc=', serviceUuid, 'char=', charUuid) } catch (e) { }
// If nothing was read, dump wrapper diagnostics to help identify platform shapes
if (arr.length == 0) {
console.log(arr)
}
try {
const hex = Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(' ')
console.log('[ProtocolHandler] read buffer raw hex:', hex, 'len=', arr.length, 'svc=', serviceUuid, 'char=', charUuid)
} catch (e) { console.warn('[ProtocolHandler] failed to stringify buffer', e) }
if (isPrintableVersion(arr)) {
return new TextDecoder().decode(arr)
}
// Fallback: sometimes version strings are short (e.g. "1.0" or "v2.1")
// and may fail the printable-ratio heuristic. Try decoding and match
// a version-like regexp before giving up.
try {
const dec = new TextDecoder().decode(arr).trim()
if (dec.length >= 2 && /^[vV]?\d[\d\.\- ]+$/.test(dec)) {
console.log('[ProtocolHandler] tryReadChar regex-fallback decoded:', dec, 'svc=', serviceUuid, 'char=', charUuid)
return dec
}
} catch (e) { /* ignore */ }
return ''
} catch (e) {
console.warn('[ProtocolHandler] read failed for', serviceUuid, charUuid, e)
return ''
}
}
// First attempt: read the initially selected target characteristic (only if service UUID known)
let result = ''
if (target != null && found2Uuid != null) {
result = await tryReadChar(found2Uuid, target.uuid)
if (result != null && result.length > 0) return result
}
// If the direct target failed or none selected, attempt a conservative read of
// the first few characteristics in the Device Information service (Android
// devices sometimes put version strings in non-standard characteristics).
if ((target == null || (result == null || result.length == 0)) && found2Uuid != null && chars.length > 0) {
try {
const tryCount = Math.min(4, chars.length)
for (let i = 0; i < tryCount; i++) {
const c = chars[i]
if (c == null || c.uuid == null) continue
try {
console.log('[ProtocolHandler] device-info conservative read attempt for char', c.uuid)
const attempt = await tryReadChar(found2Uuid!, c.uuid)
if (attempt != null && attempt.length > 0) return attempt
} catch (e) { console.warn('[ProtocolHandler] conservative read failed for', c.uuid, e) }
}
} catch (e) { console.warn('[ProtocolHandler] conservative device-info read loop failed', e) }
}
// Second attempt: search other readable characteristics in the same Device Information service
try {
for (let i = 0; i < chars.length; i++) {
const c = chars[i]
if (c == null) continue
const id = ('' + c.uuid).toLowerCase()
if (target != null && id == ('' + target.uuid).toLowerCase()) continue
// prefer readable properties when available
if (c.properties != null && (c.properties.read == true || c.properties.canRead == true)) {
const attempt = await tryReadChar(found2Uuid!, c.uuid)
if (attempt != null && attempt.length > 0) return attempt
}
}
} catch (e) { console.warn('[ProtocolHandler] fallback scan in service failed', e) }
// Final fallback: scan all services for likely version characteristics (conservative limit)
try {
const svcList = await bsvc!.getServices(dev)
let attempts = 0
for (let si = 0; si < svcList.length; si++) {
if (attempts >= 6) break // limit to avoid long blocking
const s = svcList[si]
if (s == null) continue
const sUuid = ('' + s.uuid)
const charsAll = await bsvc!.getCharacteristics(dev, sUuid)
for (let ci = 0; ci < charsAll.length; ci++) {
const c = charsAll[ci]
if (c == null) continue
const id = ('' + c.uuid).toLowerCase()
if (id.indexOf('2a26') != -1 || id.indexOf('2a27') != -1 || id.indexOf('2a28') != -1 || ('' + c.uuid).toLowerCase().includes('firmware') || ('' + c.uuid).toLowerCase().includes('software') || ('' + c.uuid).toLowerCase().includes('hardware')) {
const attempt = await tryReadChar(sUuid, c.uuid)
if (attempt != null && attempt.length > 0) return attempt
}
}
}
} catch (e) { console.warn('[ProtocolHandler] global fallback scan failed', e) }
// Final final fallback: aggressively (but with a strict cap) try any readable characteristic
try {
const svcList2 = await bsvc!.getServices(dev)
let attempts2 = 0
for (let si = 0; si < svcList2.length; si++) {
if (attempts2 >= 8) break
const s = svcList2[si]
if (s == null) continue
const sUuid = ('' + s.uuid)
const charsAll = await bsvc!.getCharacteristics(dev, sUuid)
for (let ci = 0; ci < charsAll.length; ci++) {
if (attempts2 >= 8) break
const c = charsAll[ci]
if (c == null) continue
const id = ('' + c.uuid).toLowerCase()
if (id.indexOf('2a19') != -1) continue // skip battery
// prefer readable properties if available, otherwise try cautiously
if (c.properties != null && (c.properties.read == true || c.properties.canRead == true)) {
attempts2++
const attempt = await tryReadChar(sUuid, c.uuid)
if (attempt != null && attempt.length > 0) return attempt
}
}
}
} catch (e) { console.warn('[ProtocolHandler] aggressive fallback failed', e) }
// if all attempts failed, return empty string
// Vendor-specific fallback: use custom UART-like service to request device info
try {
// Known custom primary service and chars
const UART_SERVICE = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
const UART_TX = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' // write without response
const UART_RX = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' // notify/read
const UART_CTRL = '6e400004-b5a3-f393-e0a9-e50e24dcca9e' // control write (with response)
// find the UART service
const all = await bsvc!.getServices(deviceId)
let uartSvc : BleService | null = null
for (let i = 0; i < all.length; i++) {
const s = all[i]
if (s != null && (('' + s.uuid).toLowerCase().indexOf('6e400001') != -1 || ('' + s.uuid).toLowerCase() == UART_SERVICE)) { uartSvc = s; break }
}
const nonNullUartSvc = uartSvc
if (nonNullUartSvc != null) {
const sUuid = '' + nonNullUartSvc.uuid
// identify write and notify chars (prefer CTRL then TX for write)
let writeChar = UART_CTRL
let notifyChar = UART_RX
// verify existence
const chars = await bsvc!.getCharacteristics(deviceId, sUuid)
const charSet = new Set(chars.map(c => ('' + c.uuid).toLowerCase()))
if (!charSet.has(UART_CTRL) && !charSet.has(UART_TX)) {
// no usable write char
console.log('[ProtocolHandler] UART service present but no write char found')
} else {
if (!charSet.has(UART_CTRL)) writeChar = UART_TX
if (!charSet.has(UART_RX)) notifyChar = Array.from(charSet)[0] as string // pick first as fallback
const self = this
// subscribe to notify and wait for response
let resolved = false
const notifyPromise = new Promise<Uint8Array | null>((resolve : (value : Uint8Array | null) => void, reject : (reason ?: any) => void) => {
const timeout = setTimeout(() => {
if (!resolved) { resolved = true; resolve(null) }
}, 3000)
const callback = (data : Uint8Array) => {
if (resolved) return
resolved = true
clearTimeout(timeout)
resolve(data)
}
bsvc!.subscribeCharacteristic(deviceId, sUuid, notifyChar, callback).then(() => {
// subscription succeeded
}).catch((e) => {
clearTimeout(timeout)
if (!resolved) { resolved = true; resolve(null) }
})
})
// write the device info request
const pkt = self.buildVendorPacket(0x01, 0x01, [0x00]) // CMD_DEVICE_INFO
try {
await bsvc!.writeCharacteristic(deviceId, sUuid, writeChar, pkt, { waitForResponse: true })
} catch (e) { console.warn('[ProtocolHandler] UART write failed', e) }
const notifyData = await notifyPromise
try {
await bsvc!.unsubscribeCharacteristic(deviceId, sUuid, notifyChar)
} catch (e) { }
if (notifyData != null) {
// parse response: expect packet starting with 0xAA
if (notifyData.length >= 4 && notifyData[0] == 0xAA) {
// strip STX,LEN,CMD,SEQ and CRC
const len = notifyData[1]
const cmd = notifyData[2]
const seq = notifyData[3]
const dataBytes = notifyData.slice(4, notifyData.length - 1)
// try decode data to string
try {
// Use TextDecoder which handles Uint8Array directly and is
// safer than spreading into String.fromCharCode for large arrays.
const decoded = new TextDecoder().decode(dataBytes)
if (decoded != null && decoded.length > 0 && decoded.trim().length > 0) {
console.log('[ProtocolHandler] UART notify decoded:', decoded)
return decoded
}
} catch (e) { console.warn('[ProtocolHandler] failed to decode UART notify', e) }
}
}
}
}
} catch (e) { console.warn('[ProtocolHandler] UART fallback failed', e) }
return ''
}
}

View File

@@ -0,0 +1,34 @@
// Minimal error definitions used across the BLE module.
// Keep this file small and avoid runtime dependencies; it's mainly for typing and
// simple runtime error construction used by native platform code.
export enum AkBluetoothErrorCode {
UnknownError = 0,
DeviceNotFound = 1,
ServiceNotFound = 2,
CharacteristicNotFound = 3,
ConnectionTimeout = 4,
Unspecified = 99
}
export class AkBleErrorImpl extends Error {
public code: AkBluetoothErrorCode;
public detail: any|null;
constructor(code: AkBluetoothErrorCode, message?: string, detail: any|null = null) {
super(message ?? AkBleErrorImpl.defaultMessage(code));
this.name = 'AkBleError';
this.code = code;
this.detail = detail;
}
static defaultMessage(code: AkBluetoothErrorCode) {
switch (code) {
case AkBluetoothErrorCode.DeviceNotFound: return 'Device not found';
case AkBluetoothErrorCode.ServiceNotFound: return 'Service not found';
case AkBluetoothErrorCode.CharacteristicNotFound: return 'Characteristic not found';
case AkBluetoothErrorCode.ConnectionTimeout: return 'Connection timed out';
case AkBluetoothErrorCode.UnknownError: default: return 'Unknown Bluetooth error';
}
}
}
export default AkBleErrorImpl;

View 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 };
};

View 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
}
}

View 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();

View 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;

View 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;
}
}
}