import 'dart:async'; import 'dart:math'; import 'package:flutter/foundation.dart' show VoidCallback; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../models/ble_device.dart'; import '../models/bp_reading.dart'; class UnsupportedBleDeviceException implements Exception { final String message; const UnsupportedBleDeviceException(this.message); @override String toString() => message; } class HealthBleService { static const bpMeasurementUuid = '00002A35-0000-1000-8000-00805F9B34FB'; static const racpUuid = '00002A52-0000-1000-8000-00805F9B34FB'; BluetoothDevice? _device; StreamSubscription? _connStateSub; StreamSubscription>? _measurementSub; final _connStateController = StreamController.broadcast(); Stream get connectionState => _connStateController.stream; static bool isSupportedHealthDevice(ScanResult result) { return BleDeviceIdentifier.isSupportedScanResult(result); } static BleDeviceType? deviceTypeFromScan(ScanResult result) { return BleDeviceIdentifier.typeFromScan(result); } Future connectAndIdentify({ required BluetoothDevice device, required String name, }) async { final services = await _connectAndDiscover(device); final type = BleDeviceIdentifier.typeFromServices(services); if (type == null) { throw const UnsupportedBleDeviceException('暂不支持该设备'); } return BoundBleDevice( id: device.remoteId.toString(), name: name, type: type, serviceUuid: type.serviceUuid, ); } Future syncBoundDevice( BluetoothDevice device, BoundBleDevice bound, { Duration timeout = const Duration(seconds: 45), VoidCallback? onConnected, VoidCallback? onMeasurementReceived, }) async { final services = await _connectAndDiscover(device); onConnected?.call(); final connectedType = BleDeviceIdentifier.typeFromServices(services); if (connectedType != bound.type) { throw const UnsupportedBleDeviceException('设备类型和绑定信息不一致'); } return switch (bound.type) { BleDeviceType.bloodPressure => BloodPressureSyncResult( device: bound, reading: await _syncBloodPressure( services, onMeasurementReceived: onMeasurementReceived, ).timeout(timeout), ), BleDeviceType.glucose => throw const UnsupportedBleDeviceException( '暂未支持血糖仪数据解析', ), BleDeviceType.weightScale => throw const UnsupportedBleDeviceException( '暂未支持体重秤数据解析', ), BleDeviceType.pulseOximeter => throw const UnsupportedBleDeviceException( '暂未支持血氧仪数据解析', ), }; } Future> _connectAndDiscover( BluetoothDevice device, ) async { await _measurementSub?.cancel(); _measurementSub = null; await _connStateSub?.cancel(); _device = device; _connStateSub = device.connectionState.listen((state) { _connStateController.add(state == BluetoothConnectionState.connected); }); if (device.isConnected) { try { await device.disconnect(); } catch (_) {} } await device.connect( autoConnect: false, timeout: const Duration(seconds: 10), ); try { await device.requestMtu(512); } catch (_) { // Some devices or platforms do not allow MTU negotiation. } final services = await device.discoverServices(); return services; } Future _syncBloodPressure( List services, { VoidCallback? onMeasurementReceived, }) async { BluetoothService? bpService; for (final service in services) { if (service.uuid.str128.toUpperCase() == BleDeviceIdentifier.bloodPressureServiceUuid) { bpService = service; break; } } if (bpService == null) { throw const UnsupportedBleDeviceException('未找到血压服务'); } BluetoothCharacteristic? measurementChar; BluetoothCharacteristic? racpChar; for (final characteristic in bpService.characteristics) { final uuid = characteristic.uuid.str128.toUpperCase(); if (uuid == bpMeasurementUuid) measurementChar = characteristic; if (uuid == racpUuid) racpChar = characteristic; } if (measurementChar == null) { throw const UnsupportedBleDeviceException('未找到血压测量特征'); } final completer = Completer(); final readings = []; Timer? settleTimer; void acceptReading(List raw) { if (raw.isEmpty || completer.isCompleted) return; try { readings.add(_parseBloodPressureReading(raw)); onMeasurementReceived?.call(); settleTimer?.cancel(); settleTimer = Timer(const Duration(milliseconds: 900), () { final reading = _selectCurrentBloodPressureReading(readings); if (!completer.isCompleted) { completer.complete(reading); } }); } catch (e, stack) { if (!completer.isCompleted) completer.completeError(e, stack); } } _measurementSub = measurementChar.onValueReceived.listen( (raw) { acceptReading(raw); }, onError: (Object e, StackTrace stack) { if (!completer.isCompleted) completer.completeError(e, stack); }, ); await _enableMeasurementUpdates(measurementChar); if (racpChar != null) { try { await racpChar.write([0x01, 0x01]); } catch (_) {} } try { return await completer.future; } finally { settleTimer?.cancel(); } } BpReading _selectCurrentBloodPressureReading(List readings) { if (readings.isEmpty) { throw const FormatException('未收到血压数据'); } final now = DateTime.now(); final currentWindowStart = now.subtract(const Duration(minutes: 15)); final currentWindowEnd = now.add(const Duration(minutes: 2)); final timestampedCurrent = readings.where((reading) { if (!reading.hasDeviceTimestamp) return false; return !reading.timestamp.isBefore(currentWindowStart) && !reading.timestamp.isAfter(currentWindowEnd); }).toList(); if (timestampedCurrent.isNotEmpty) { timestampedCurrent.sort((a, b) => b.timestamp.compareTo(a.timestamp)); return timestampedCurrent.first; } final timestamped = readings .where((reading) => reading.hasDeviceTimestamp) .toList(); if (timestamped.isNotEmpty) { timestamped.sort((a, b) => b.timestamp.compareTo(a.timestamp)); return timestamped.first; } return readings.last; } Future _enableMeasurementUpdates( BluetoothCharacteristic characteristic, ) async { final useIndication = characteristic.properties.indicate; await characteristic.setNotifyValue(true, forceIndications: useIndication); } Future disconnect() async { await _measurementSub?.cancel(); _measurementSub = null; await _connStateSub?.cancel(); _connStateSub = null; await _device?.disconnect(); _device = null; _connStateController.add(false); } void dispose() { unawaited(disconnect()); _connStateController.close(); } BpReading _parseBloodPressureReading(List raw) { if (raw.length < 7) { throw const FormatException('血压数据长度不足'); } final flags = raw[0]; final isKpa = (flags & 0x01) != 0; final hasTimestamp = (flags & 0x02) != 0; final hasPulse = (flags & 0x04) != 0; final hasUserId = (flags & 0x08) != 0; final hasStatus = (flags & 0x10) != 0; int systolic = _decodeSFloat(raw[1] | (raw[2] << 8)).round(); int diastolic = _decodeSFloat(raw[3] | (raw[4] << 8)).round(); if (isKpa) { systolic = (systolic * 7.50062).round(); diastolic = (diastolic * 7.50062).round(); } var offset = 7; var timestamp = DateTime.now(); if (hasTimestamp && raw.length >= offset + 7) { timestamp = DateTime( raw[offset] | (raw[offset + 1] << 8), raw[offset + 2], raw[offset + 3], raw[offset + 4], raw[offset + 5], raw[offset + 6], ); offset += 7; } int? pulse; if (hasPulse && raw.length >= offset + 2) { pulse = _decodeSFloat(raw[offset] | (raw[offset + 1] << 8)).round(); offset += 2; } String? userId; if (hasUserId && raw.length > offset) { userId = raw[offset].toString(); offset++; } var hasBodyMovement = false; var hasIrregularPulse = false; if (hasStatus && raw.length >= offset + 2) { final status = raw[offset] | (raw[offset + 1] << 8); hasBodyMovement = (status & 0x0001) != 0; hasIrregularPulse = (status & 0x0800) != 0; } return BpReading( systolic: systolic, diastolic: diastolic, pulse: pulse, timestamp: timestamp, userId: userId, isKpa: isKpa, hasBodyMovement: hasBodyMovement, hasIrregularPulse: hasIrregularPulse, hasDeviceTimestamp: hasTimestamp, ); } static double _decodeSFloat(int raw) { var mantissa = raw & 0x0FFF; if (mantissa & 0x0800 != 0) mantissa |= 0xFFFFF000; var exponent = (raw >> 12) & 0x0F; if (exponent & 0x08 != 0) exponent |= 0xFFFFFFF0; return (mantissa * pow(10, exponent)).toDouble(); } }