Files
AI-Health/health_app/lib/services/omron_ble_service.dart
MingNian d102205b18 feat: 蓝牙血压计BLE数据同步 + 设备管理页重构 + 健康记录滑动删除 + 医生端合并设计文档
- BLE: 修复Omron设备Indication模式连接(先订阅后配CCCD + forceIndications) + 直连重连
- BLE: 设备断连实时检测(connectionState流→Provider→UI)
- 修复: 血压数据上报后端枚举不匹配(Device→DeviceSync)导致500
- 新增: DELETE /api/health-records/{id} 后端接口
- 设备管理页: 白色主题重设计 + 直连重连 + 实时状态 + Toast通知
- 设备扫描页: 白色主题 + 扫描动画居中 + 已连接等待页面
- 健康记录: 左滑删除(Dismissible) + 同时清理_allRecords和_filtered
- 导航: 修复自定义路由栈下Navigator.pop黑屏bug
- 文档: 医生端合并App完整设计文档(已确认版)
2026-06-11 22:11:02 +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.localName,
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();
}
}