Files
AI-Health/health_app/lib/services/omron_ble_service.dart
MingNian 4d213b5a44 feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
2026-06-20 20:41:42 +08:00

245 lines
8.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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