- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
245 lines
8.5 KiB
Dart
245 lines
8.5 KiB
Dart
import 'dart:async';
|
||
import 'dart:math';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||
import '../models/bp_reading.dart';
|
||
|
||
/// 欧姆龙蓝牙血压计 BLE 服务
|
||
class OmronBleService {
|
||
static const bpServiceUuid = '00001810-0000-1000-8000-00805F9B34FB';
|
||
static const bpMeasurementUuid = '00002A35-0000-1000-8000-00805F9B34FB';
|
||
static const bpFeatureUuid = '00002A49-0000-1000-8000-00805F9B34FB';
|
||
static const dateTimeUuid = '00002A08-0000-1000-8000-00805F9B34FB';
|
||
static const intermediateCuffUuid = '00002A36-0000-1000-8000-00805F9B34FB';
|
||
static const racpUuid = '00002A52-0000-1000-8000-00805F9B34FB';
|
||
|
||
BluetoothDevice? _device;
|
||
StreamSubscription<List<int>>? _measurementSub;
|
||
StreamSubscription<BluetoothConnectionState>? _connStateSub;
|
||
final _readingsController = StreamController<BpReading>.broadcast();
|
||
final _connStateController = StreamController<bool>.broadcast();
|
||
Stream<BpReading> get readings => _readingsController.stream;
|
||
Stream<bool> get connectionState => _connStateController.stream;
|
||
|
||
bool get isConnected => _device?.isConnected ?? false;
|
||
|
||
/// 直接重连已知设备(通过MAC地址)
|
||
Future<bool> reconnectByMac(String mac) async {
|
||
debugPrint('[BLE] 直连设备: $mac');
|
||
final bonded = await FlutterBluePlus.bondedDevices;
|
||
BluetoothDevice? target;
|
||
for (final d in bonded) {
|
||
if (d.remoteId.toString() == mac) {
|
||
target = d;
|
||
break;
|
||
}
|
||
}
|
||
if (target == null) {
|
||
debugPrint('[BLE] 未在已配对设备中找到 $mac');
|
||
return false;
|
||
}
|
||
try {
|
||
await connect(target);
|
||
return true;
|
||
} catch (e) {
|
||
debugPrint('[BLE] 直连失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static bool isBpDevice(ScanResult r) {
|
||
final name = [
|
||
r.advertisementData.advName,
|
||
r.device.platformName,
|
||
].where((n) => n.isNotEmpty).join(' ').toUpperCase();
|
||
return name.contains('OMRON') ||
|
||
name.contains('HEM') ||
|
||
name.contains('BLESMART') ||
|
||
name.contains('J735') ||
|
||
name.contains('BPM') ||
|
||
name.contains('BP');
|
||
}
|
||
|
||
/// 连接设备并订阅测量数据
|
||
/// 设备连接后会自动发送存储的最后一次测量数据
|
||
Future<void> connect(BluetoothDevice device) async {
|
||
_device = device;
|
||
debugPrint('[BLE] ═══ 连接: "${device.platformName}" ═══');
|
||
|
||
_connStateSub = device.connectionState.listen((s) {
|
||
debugPrint('[BLE] 连接状态: $s');
|
||
_connStateController.add(s == BluetoothConnectionState.connected);
|
||
});
|
||
|
||
// 1. GATT连接
|
||
await device.connect(autoConnect: false);
|
||
debugPrint('[BLE] ① 已连接, MTU=${device.mtuNow}');
|
||
try { await device.requestMtu(512); } catch (_) {}
|
||
|
||
// 2. 发现服务
|
||
final services = await device.discoverServices();
|
||
debugPrint('[BLE] ② 发现${services.length}个服务');
|
||
|
||
// 遍历所有服务/特征
|
||
BluetoothService? bpSvc;
|
||
BluetoothCharacteristic? measChar, racpChar;
|
||
for (final s in services) {
|
||
debugPrint('[BLE] 服务: ${s.uuid.str128}');
|
||
for (final c in s.characteristics) {
|
||
final p = <String>[];
|
||
if (c.properties.read) p.add('R');
|
||
if (c.properties.write) p.add('W');
|
||
if (c.properties.notify) p.add('N');
|
||
if (c.properties.indicate) p.add('I');
|
||
debugPrint('[BLE] ${c.uuid.str128} [$p]');
|
||
for (final d in c.descriptors) {
|
||
debugPrint('[BLE] desc: ${d.uuid.str128}');
|
||
}
|
||
}
|
||
if (s.uuid.str128.toUpperCase() == bpServiceUuid.toUpperCase()) bpSvc = s;
|
||
}
|
||
|
||
if (bpSvc == null) throw Exception('未找到血压服务0x1810');
|
||
for (final c in bpSvc.characteristics) {
|
||
final u = c.uuid.str128.toUpperCase();
|
||
if (u == bpMeasurementUuid.toUpperCase()) measChar = c;
|
||
if (u == racpUuid.toUpperCase()) racpChar = c;
|
||
}
|
||
if (measChar == null) throw Exception('未找到0x2A35');
|
||
debugPrint('[BLE] ③ 0x2A35: indicate=${measChar.properties.indicate} read=${measChar.properties.read}');
|
||
debugPrint('[BLE] RACP: ${racpChar != null ? "有" : "无"}');
|
||
|
||
// ★ 3. 先订阅数据流(在配置CCCD之前!)
|
||
debugPrint('[BLE] ④ 订阅lastValueStream...');
|
||
_measurementSub = measChar.lastValueStream.listen(
|
||
(raw) {
|
||
debugPrint('[BLE] ★★★ 收到数据! ${raw.length}字节 ★★★');
|
||
debugPrint('[BLE] HEX: ${raw.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||
_parseReading(raw);
|
||
},
|
||
onError: (e) => debugPrint('[BLE] 流错误: $e'),
|
||
cancelOnError: false,
|
||
);
|
||
|
||
// 4. 配置CCCD
|
||
final isIndicate = measChar.properties.indicate;
|
||
final cccdVal = isIndicate ? [0x02, 0x00] : [0x01, 0x00];
|
||
debugPrint('[BLE] ⑤ 配置CCCD → ${isIndicate ? "Indicate" : "Notify"}...');
|
||
var ok = false;
|
||
for (final d in measChar.descriptors) {
|
||
if (d.uuid.str128 == '00002902-0000-1000-8000-00805F9B34FB') {
|
||
final before = await d.read();
|
||
await d.write(cccdVal);
|
||
final after = await d.read();
|
||
ok = after.length >= 2 && after[0] == cccdVal[0] && after[1] == cccdVal[1];
|
||
debugPrint('[BLE] ⑤ CCCD: $before → $after ${ok ? "OK" : "FAIL"}');
|
||
break;
|
||
}
|
||
}
|
||
if (!ok) {
|
||
await measChar.setNotifyValue(true, forceIndications: isIndicate);
|
||
debugPrint('[BLE] ⑤ 降级setNotifyValue, isNotifying=${measChar.isNotifying}');
|
||
}
|
||
|
||
// 5. 主动读取缓存数据(设备存储的最后一次测量)
|
||
debugPrint('[BLE] ⑥ 主动读取0x2A35...');
|
||
if (measChar.properties.read) {
|
||
try {
|
||
final data = await measChar.read();
|
||
if (data.isNotEmpty) {
|
||
debugPrint('[BLE] ⑥ 读到${data.length}字节 HEX=${data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||
_parseReading(data);
|
||
} else {
|
||
debugPrint('[BLE] ⑥ 空数据');
|
||
}
|
||
} catch (e) {
|
||
debugPrint('[BLE] ⑥ 读取失败: $e');
|
||
}
|
||
}
|
||
|
||
// 6. RACP请求存储记录
|
||
if (racpChar != null) {
|
||
debugPrint('[BLE] ⑦ RACP请求存储记录...');
|
||
try {
|
||
await racpChar.write([0x01, 0x01]); // ReportStoredRecords, All
|
||
debugPrint('[BLE] ⑦ RACP已发送');
|
||
} catch (e) {
|
||
debugPrint('[BLE] ⑦ RACP失败: $e');
|
||
}
|
||
}
|
||
|
||
debugPrint('[BLE] ═══ 连接完成, 等待数据 ═══');
|
||
}
|
||
|
||
Future<void> disconnect() async {
|
||
_connStateSub?.cancel();
|
||
_connStateSub = null;
|
||
_measurementSub?.cancel();
|
||
_measurementSub = null;
|
||
await _device?.disconnect();
|
||
_device = null;
|
||
debugPrint('[BLE] 已断开');
|
||
}
|
||
|
||
void dispose() {
|
||
disconnect();
|
||
_readingsController.close();
|
||
_connStateController.close();
|
||
}
|
||
|
||
// ── 解析 BLE 测量数据 ──
|
||
|
||
void _parseReading(List<int> raw) {
|
||
debugPrint('[BLE] 解析: flags=0x${raw[0].toRadixString(16)}, len=${raw.length}');
|
||
if (raw.length < 7) { debugPrint('[BLE] 太短,跳过'); return; }
|
||
|
||
final flags = raw[0];
|
||
final isKpa = (flags & 0x01) != 0;
|
||
final hasTs = (flags & 0x02) != 0;
|
||
final hasPulse = (flags & 0x04) != 0;
|
||
final hasUid = (flags & 0x08) != 0;
|
||
final hasStatus = (flags & 0x10) != 0;
|
||
|
||
int sys = _decodeSFloat(raw[1] | (raw[2] << 8)).round();
|
||
int dia = _decodeSFloat(raw[3] | (raw[4] << 8)).round();
|
||
if (isKpa) { sys = (sys * 7.50062).round(); dia = (dia * 7.50062).round(); }
|
||
|
||
int off = 7;
|
||
DateTime ts = DateTime.now();
|
||
if (hasTs && raw.length >= off + 7) {
|
||
ts = DateTime(raw[off] | (raw[off + 1] << 8), raw[off + 2], raw[off + 3],
|
||
raw[off + 4], raw[off + 5], raw[off + 6]);
|
||
off += 7;
|
||
}
|
||
|
||
int? pulse;
|
||
if (hasPulse && raw.length >= off + 2) {
|
||
pulse = _decodeSFloat(raw[off] | (raw[off + 1] << 8)).round();
|
||
off += 2;
|
||
}
|
||
|
||
String? uid;
|
||
if (hasUid && raw.length > off) { uid = raw[off].toString(); off++; }
|
||
|
||
bool bm = false, ir = false;
|
||
if (hasStatus && raw.length >= off + 2) {
|
||
int s = raw[off] | (raw[off + 1] << 8);
|
||
bm = (s & 0x0001) != 0;
|
||
ir = (s & 0x0800) != 0;
|
||
}
|
||
|
||
final r = BpReading(systolic: sys, diastolic: dia, pulse: pulse, timestamp: ts,
|
||
userId: uid, isKpa: isKpa, hasBodyMovement: bm, hasIrregularPulse: ir);
|
||
debugPrint('[BLE] ★ $sys/$dia mmHg, pulse=$pulse ★');
|
||
_readingsController.add(r);
|
||
}
|
||
|
||
static double _decodeSFloat(int raw) {
|
||
int m = raw & 0x0FFF;
|
||
if (m & 0x0800 != 0) m |= 0xFFFFF000;
|
||
int e = (raw >> 12) & 0x0F;
|
||
if (e & 0x08 != 0) e |= 0xFFFFFFF0;
|
||
return (m * pow(10, e)).toDouble();
|
||
}
|
||
}
|