fix: 移除 BLE 蓝牙依赖,消除 CoreLocation API 引用
- 注释 flutter_blue_plus 和 permission_handler 依赖 - 删除蓝牙设备页、BLE service、omron provider 等 6 个源文件 - 移除路由和设备设置入口 - 清理 2 个 BLE 相关测试文件 - 解决 Transporter 报 NSLocationWhenInUseUsageDescription 缺失 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,323 +0,0 @@
|
||||
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<BluetoothConnectionState>? _connStateSub;
|
||||
StreamSubscription<List<int>>? _measurementSub;
|
||||
final _connStateController = StreamController<bool>.broadcast();
|
||||
|
||||
Stream<bool> get connectionState => _connStateController.stream;
|
||||
|
||||
static bool isSupportedHealthDevice(ScanResult result) {
|
||||
return BleDeviceIdentifier.isSupportedScanResult(result);
|
||||
}
|
||||
|
||||
static BleDeviceType? deviceTypeFromScan(ScanResult result) {
|
||||
return BleDeviceIdentifier.typeFromScan(result);
|
||||
}
|
||||
|
||||
static bool isSyncImplemented(BleDeviceType type) {
|
||||
return type == BleDeviceType.bloodPressure;
|
||||
}
|
||||
|
||||
Future<BoundBleDevice> 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<HealthBleSyncResult> 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('设备类型和绑定信息不一致');
|
||||
}
|
||||
if (!isSyncImplemented(bound.type)) {
|
||||
throw UnsupportedBleDeviceException('${bound.type.label}数据同步暂未开通');
|
||||
}
|
||||
|
||||
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<List<BluetoothService>> _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<BpReading> _syncBloodPressure(
|
||||
List<BluetoothService> 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<BpReading>();
|
||||
final readings = <BpReading>[];
|
||||
Timer? settleTimer;
|
||||
|
||||
void acceptReading(List<int> 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<BpReading> 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<void> _enableMeasurementUpdates(
|
||||
BluetoothCharacteristic characteristic,
|
||||
) async {
|
||||
final useIndication = characteristic.properties.indicate;
|
||||
await characteristic.setNotifyValue(true, forceIndications: useIndication);
|
||||
}
|
||||
|
||||
Future<void> 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<int> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user