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 死锁警告注释
357 lines
10 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|