Files
hms/apps/miniprogram/src/services/ble/BLEManager.ts
iven fcce2f5c51 fix(mp): 二轮审计修复 — ScrollView嵌套/InputField重建/markdown分组/BLE上限/缓存清理
CRITICAL: ai-report/list PageShell scroll=false 修复双重滚动冲突
HIGH: dialysis/create InputField 提取为独立组件避免 render 销毁重建
MEDIUM: markdownToHtml 连续<li>合并到单个<ul>
MEDIUM: 咨询详情页图片添加 lazyLoad
MEDIUM: BLEManager readings 添加 MAX_LIVE_READINGS=200 上限
MEDIUM: DataBuffer trimToMax 时重建 seenKeys 保持一致性
MEDIUM: auth.ts logout 清理模块级缓存变量
LOW: request.ts safeReLaunch 添加 console.warn + doRefresh 死锁警告注释
2026-05-17 18:54:27 +08:00

357 lines
10 KiB
TypeScript

import Taro from '@tarojs/taro';
import type {
DeviceAdapter,
BLEDevice,
BLEConnection,
BLEConnectionState,
NormalizedReading,
SyncResult,
BLEManagerConfig,
} from './types';
import { DataBuffer } from './DataBuffer';
const DEFAULT_CONFIG: BLEManagerConfig = {
scanTimeout: 10000,
maxReadingsPerSync: 500,
retryCount: 3,
};
const MAX_LIVE_READINGS = 200;
export class BLEManager {
private adapters: DeviceAdapter[] = [];
private connection: BLEConnection | null = null;
private readings: NormalizedReading[] = [];
private dataBuffer: DataBuffer;
private config: BLEManagerConfig;
private scanTimer: ReturnType<typeof setTimeout> | null = null;
private onConnectionChange?: (state: BLEConnectionState) => void;
private onReadings?: (readings: NormalizedReading[]) => void;
private connChangeHandler: ((res: any) => void) | null = null;
private charChangeHandler: ((res: any) => void) | null = null;
constructor(config?: Partial<BLEManagerConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.dataBuffer = new DataBuffer();
this.dataBuffer.restore();
}
/** 注册设备适配器 */
registerAdapter(adapter: DeviceAdapter): void {
this.adapters = [...this.adapters, adapter];
}
/** 设置连接状态回调 */
setOnConnectionChange(cb: (state: BLEConnectionState) => void): void {
this.onConnectionChange = cb;
}
/** 设置读数回调 */
setOnReadings(cb: (readings: NormalizedReading[]) => void): void {
this.onReadings = cb;
}
private updateState(state: BLEConnectionState, error?: string): void {
if (this.connection) {
this.connection = { ...this.connection, state, error };
}
this.onConnectionChange?.(state);
}
/** 匹配设备到适配器 */
private matchAdapter(deviceName: string): DeviceAdapter | undefined {
const lower = deviceName.toLowerCase();
return this.adapters.find((a) =>
a.supportedModels.some((m) => lower.includes(m.toLowerCase())),
);
}
/** 初始化蓝牙适配器 */
async initialize(): Promise<void> {
try {
await Taro.openBluetoothAdapter();
} catch (e: any) {
throw new Error(e.errMsg || '蓝牙初始化失败,请检查蓝牙是否开启');
}
}
/** 扫描 BLE 设备 */
async scanDevices(): Promise<BLEDevice[]> {
await this.initialize();
const discovered = new Map<string, BLEDevice>();
const onFound = (res: any) => {
for (const device of res.devices || []) {
const name = device.name || device.localName || '';
if (!name) continue;
const adapter = this.matchAdapter(name);
if (adapter) {
discovered.set(device.deviceId, {
deviceId: device.deviceId,
name,
RSSI: device.RSSI,
localName: device.localName,
advertisData: device.advertisData,
adapter,
});
}
}
};
Taro.onBluetoothDeviceFound(onFound);
const allServiceUUIDs = this.adapters.flatMap((a) => a.serviceUUIDs);
await Taro.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
services: allServiceUUIDs.length > 0 ? allServiceUUIDs : undefined,
});
return new Promise((resolve) => {
this.scanTimer = setTimeout(async () => {
await this.stopScan();
Taro.offBluetoothDeviceFound(onFound);
resolve(Array.from(discovered.values()));
}, this.config.scanTimeout);
});
}
/** 停止扫描 */
async stopScan(): Promise<void> {
if (this.scanTimer) {
clearTimeout(this.scanTimer);
this.scanTimer = null;
}
try {
await Taro.stopBluetoothDevicesDiscovery();
} catch {
// 忽略停止扫描错误
}
}
/** 连接到设备 */
async connect(device: BLEDevice): Promise<void> {
if (!device.adapter) throw new Error('设备无适配器');
this.connection = {
deviceId: device.deviceId,
state: 'connecting',
adapter: device.adapter,
};
this.updateState('connecting');
this.readings = [];
try {
await Taro.createBLEConnection({
deviceId: device.deviceId,
timeout: 10000,
});
// 移除旧监听器,避免多次 connect 累积
if (this.connChangeHandler) {
Taro.offBLEConnectionStateChange(this.connChangeHandler);
}
if (this.charChangeHandler) {
Taro.offBLECharacteristicValueChange(this.charChangeHandler);
}
// 监听断连
this.connChangeHandler = (res: any) => {
if (res.deviceId === device.deviceId && !res.connected) {
this.updateState('disconnected', '设备断开连接');
this.connection = null;
}
};
Taro.onBLEConnectionStateChange(this.connChangeHandler);
// 发现服务
const servicesRes = await Taro.getBLEDeviceServices({ deviceId: device.deviceId });
const services = servicesRes.services || [];
// 启用通知
for (const { service: svcUUID, characteristic: charUUID } of device.adapter.notifyCharacteristics) {
const svc = services.find((s: any) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()));
if (!svc) continue;
await Taro.getBLEDeviceCharacteristics({
deviceId: device.deviceId,
serviceId: svc.uuid,
});
await Taro.notifyBLECharacteristicValueChange({
deviceId: device.deviceId,
serviceId: svc.uuid,
characteristicId: charUUID,
state: true,
});
}
// 监听数据通知
this.charChangeHandler = (res: any) => {
if (res.deviceId !== device.deviceId) return;
const newReadings = device.adapter!.parseNotification(
res.serviceId,
res.characteristicId,
res.value,
);
if (newReadings.length > 0) {
const combined = [...this.readings, ...newReadings];
this.readings = combined.length > MAX_LIVE_READINGS
? combined.slice(-MAX_LIVE_READINGS)
: combined;
this.dataBuffer.push(newReadings);
this.onReadings?.(newReadings);
}
};
Taro.onBLECharacteristicValueChange(this.charChangeHandler);
this.connection = { ...this.connection, state: 'connected', connectedAt: Date.now() };
this.updateState('connected');
} catch (e: any) {
this.updateState('error', e.errMsg || e.message || '连接失败');
this.connection = null;
throw new Error(e.errMsg || '蓝牙连接失败');
}
}
/** 手动读取特征值 */
async readCharacteristics(): Promise<NormalizedReading[]> {
if (!this.connection || this.connection.state !== 'connected') {
throw new Error('设备未连接');
}
const { deviceId, adapter } = this.connection;
const results: NormalizedReading[] = [];
const servicesRes = await Taro.getBLEDeviceServices({ deviceId });
const services = servicesRes.services || [];
for (const { service: svcUUID, characteristic: charUUID } of adapter.readCharacteristics) {
const svc = services.find((s: any) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()));
if (!svc) continue;
try {
await Taro.readBLECharacteristicValue({
deviceId,
serviceId: svc.uuid,
characteristicId: charUUID,
});
} catch {
// 某些特征值可能不支持读取
}
}
if (results.length > 0) {
this.readings = [...this.readings, ...results];
this.onReadings?.(results);
}
return results;
}
/** 同步收集的读数到后端 */
async syncToServer(uploadFn: (readings: NormalizedReading[]) => Promise<number>): Promise<SyncResult> {
if (!this.connection) {
return { success: false, readingsCount: 0, uploadedCount: 0, error: '未连接设备' };
}
this.updateState('syncing');
const batch = this.dataBuffer.flush();
if (batch.length === 0) {
this.updateState('connected');
return { success: true, readingsCount: 0, uploadedCount: 0 };
}
let lastError: string | undefined;
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
try {
const uploaded = await uploadFn(batch);
this.readings = [];
this.updateState('connected');
return {
success: true,
readingsCount: batch.length,
uploadedCount: uploaded,
};
} catch (e: any) {
lastError = e.message || '上传失败';
// flush 已取出,失败时需要放回
this.dataBuffer.push(batch);
}
}
this.updateState('error', lastError);
return { success: false, readingsCount: batch.length, uploadedCount: 0, error: lastError };
}
/** 断开连接 */
async disconnect(): Promise<void> {
if (!this.connection) return;
const { deviceId } = this.connection;
// 移除 BLE 监听器,防止断开后仍收到回调
if (this.connChangeHandler) {
Taro.offBLEConnectionStateChange(this.connChangeHandler);
this.connChangeHandler = null;
}
if (this.charChangeHandler) {
Taro.offBLECharacteristicValueChange(this.charChangeHandler);
this.charChangeHandler = null;
}
try {
await Taro.closeBLEConnection({ deviceId });
} catch {
// 忽略断连错误
}
this.connection = null;
this.readings = [];
this.updateState('disconnected');
}
/** 关闭蓝牙适配器 */
async destroy(): Promise<void> {
await this.disconnect();
try {
await Taro.closeBluetoothAdapter();
} catch {
// 忽略关闭错误
}
}
/** 获取当前连接信息 */
getConnection(): BLEConnection | null {
return this.connection;
}
/** 获取缓存的读数 */
getCachedReadings(): NormalizedReading[] {
return [...this.readings];
}
/** 清除缓存读数 */
clearReadings(): void {
this.readings = [];
}
/** 启动时检查缓存,有未上传数据则自动重传 */
async flushPendingReadings(
uploadFn: (readings: NormalizedReading[]) => Promise<number>,
): Promise<number> {
const batch = this.dataBuffer.flush();
if (batch.length === 0) return 0;
try {
const uploaded = await uploadFn(batch);
return uploaded;
} catch {
// 失败时放回
this.dataBuffer.push(batch);
return 0;
}
}
}