refactor: 重构蓝牙设备页与新增设备页的扫描-连接-录入流程
- 通用 BLE 健康设备识别(血压计/血糖仪/体重秤/血氧仪 标准 service UUID) - 蓝牙设备页静默自动同步:仅匹配已绑定的设备名+类型,收到数据后变绿 - 新增设备页:扫描所有支持类型设备 → 选连接 → 绑定 + 同步首次数据 - 修复 read 缓存导致的重复录入循环(删除主动 read,依赖 indicate 推送) - 修复 isConnected 缓存状态导致连接异常(强制先 disconnect 再 connect) - 录入弹窗:血压心率同等级展示、3 秒自动关闭/手动确认 - 顺手更新本机开发 IP 与清理过时设计文档
This commit is contained in:
187
health_app/lib/models/ble_device.dart
Normal file
187
health_app/lib/models/ble_device.dart
Normal file
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
|
||||
import 'bp_reading.dart';
|
||||
|
||||
enum BleDeviceType { bloodPressure, glucose, weightScale, pulseOximeter }
|
||||
|
||||
enum SupportedMetric { bloodPressure, heartRate, glucose, weight, spo2 }
|
||||
|
||||
extension BleDeviceTypeX on BleDeviceType {
|
||||
String get code => switch (this) {
|
||||
BleDeviceType.bloodPressure => 'bloodPressure',
|
||||
BleDeviceType.glucose => 'glucose',
|
||||
BleDeviceType.weightScale => 'weightScale',
|
||||
BleDeviceType.pulseOximeter => 'pulseOximeter',
|
||||
};
|
||||
|
||||
String get label => switch (this) {
|
||||
BleDeviceType.bloodPressure => '血压计',
|
||||
BleDeviceType.glucose => '血糖仪',
|
||||
BleDeviceType.weightScale => '体重秤',
|
||||
BleDeviceType.pulseOximeter => '血氧仪',
|
||||
};
|
||||
|
||||
String get serviceUuid => switch (this) {
|
||||
BleDeviceType.bloodPressure => BleDeviceIdentifier.bloodPressureServiceUuid,
|
||||
BleDeviceType.glucose => BleDeviceIdentifier.glucoseServiceUuid,
|
||||
BleDeviceType.weightScale => BleDeviceIdentifier.weightScaleServiceUuid,
|
||||
BleDeviceType.pulseOximeter => BleDeviceIdentifier.pulseOximeterServiceUuid,
|
||||
};
|
||||
|
||||
List<SupportedMetric> get metrics => switch (this) {
|
||||
BleDeviceType.bloodPressure => [
|
||||
SupportedMetric.bloodPressure,
|
||||
SupportedMetric.heartRate,
|
||||
],
|
||||
BleDeviceType.glucose => [SupportedMetric.glucose],
|
||||
BleDeviceType.weightScale => [SupportedMetric.weight],
|
||||
BleDeviceType.pulseOximeter => [
|
||||
SupportedMetric.spo2,
|
||||
SupportedMetric.heartRate,
|
||||
],
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
BleDeviceType.bloodPressure => Icons.speed_rounded,
|
||||
BleDeviceType.glucose => Icons.water_drop_rounded,
|
||||
BleDeviceType.weightScale => Icons.monitor_weight_outlined,
|
||||
BleDeviceType.pulseOximeter => Icons.air_rounded,
|
||||
};
|
||||
|
||||
static BleDeviceType? fromCode(String? code) {
|
||||
for (final type in BleDeviceType.values) {
|
||||
if (type.code == code) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension SupportedMetricX on SupportedMetric {
|
||||
String get code => switch (this) {
|
||||
SupportedMetric.bloodPressure => 'BloodPressure',
|
||||
SupportedMetric.heartRate => 'HeartRate',
|
||||
SupportedMetric.glucose => 'Glucose',
|
||||
SupportedMetric.weight => 'Weight',
|
||||
SupportedMetric.spo2 => 'SpO2',
|
||||
};
|
||||
|
||||
String get label => switch (this) {
|
||||
SupportedMetric.bloodPressure => '血压',
|
||||
SupportedMetric.heartRate => '心率',
|
||||
SupportedMetric.glucose => '血糖',
|
||||
SupportedMetric.weight => '体重',
|
||||
SupportedMetric.spo2 => '血氧',
|
||||
};
|
||||
}
|
||||
|
||||
class BoundBleDevice {
|
||||
final String id;
|
||||
final String name;
|
||||
final BleDeviceType type;
|
||||
final String serviceUuid;
|
||||
final DateTime? lastSyncAt;
|
||||
|
||||
const BoundBleDevice({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.serviceUuid,
|
||||
this.lastSyncAt,
|
||||
});
|
||||
|
||||
List<SupportedMetric> get metrics => type.metrics;
|
||||
|
||||
BoundBleDevice copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
BleDeviceType? type,
|
||||
String? serviceUuid,
|
||||
DateTime? lastSyncAt,
|
||||
}) {
|
||||
return BoundBleDevice(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
serviceUuid: serviceUuid ?? this.serviceUuid,
|
||||
lastSyncAt: lastSyncAt ?? this.lastSyncAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type.code,
|
||||
'serviceUuid': serviceUuid,
|
||||
'lastSyncAt': lastSyncAt?.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
static BoundBleDevice? fromJson(Map<String, dynamic> json) {
|
||||
final type = BleDeviceTypeX.fromCode(json['type']?.toString());
|
||||
final id = json['id']?.toString();
|
||||
if (type == null || id == null || id.isEmpty) return null;
|
||||
|
||||
final lastSyncRaw = json['lastSyncAt']?.toString();
|
||||
return BoundBleDevice(
|
||||
id: id,
|
||||
name: json['name']?.toString() ?? type.label,
|
||||
type: type,
|
||||
serviceUuid: json['serviceUuid']?.toString() ?? type.serviceUuid,
|
||||
lastSyncAt: lastSyncRaw == null || lastSyncRaw.isEmpty
|
||||
? null
|
||||
: DateTime.tryParse(lastSyncRaw)?.toLocal(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BleDeviceIdentifier {
|
||||
static const glucoseServiceUuid = '00001808-0000-1000-8000-00805F9B34FB';
|
||||
static const bloodPressureServiceUuid =
|
||||
'00001810-0000-1000-8000-00805F9B34FB';
|
||||
static const weightScaleServiceUuid = '0000181D-0000-1000-8000-00805F9B34FB';
|
||||
static const pulseOximeterServiceUuid =
|
||||
'00001822-0000-1000-8000-00805F9B34FB';
|
||||
|
||||
static BleDeviceType? typeFromScan(ScanResult result) {
|
||||
return typeFromUuidStrings(
|
||||
result.advertisementData.serviceUuids.map((uuid) => uuid.str128),
|
||||
);
|
||||
}
|
||||
|
||||
static BleDeviceType? typeFromServices(List<BluetoothService> services) {
|
||||
return typeFromUuidStrings(services.map((service) => service.uuid.str128));
|
||||
}
|
||||
|
||||
static BleDeviceType? typeFromUuidStrings(Iterable<String> uuids) {
|
||||
final normalized = uuids.map((uuid) => uuid.toUpperCase()).toSet();
|
||||
if (normalized.contains(bloodPressureServiceUuid)) {
|
||||
return BleDeviceType.bloodPressure;
|
||||
}
|
||||
if (normalized.contains(glucoseServiceUuid)) {
|
||||
return BleDeviceType.glucose;
|
||||
}
|
||||
if (normalized.contains(weightScaleServiceUuid)) {
|
||||
return BleDeviceType.weightScale;
|
||||
}
|
||||
if (normalized.contains(pulseOximeterServiceUuid)) {
|
||||
return BleDeviceType.pulseOximeter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool isSupportedScanResult(ScanResult result) {
|
||||
return typeFromScan(result) != null;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class HealthBleSyncResult {
|
||||
const HealthBleSyncResult({required this.device});
|
||||
|
||||
final BoundBleDevice device;
|
||||
}
|
||||
|
||||
class BloodPressureSyncResult extends HealthBleSyncResult {
|
||||
const BloodPressureSyncResult({required super.device, required this.reading});
|
||||
|
||||
final BpReading reading;
|
||||
}
|
||||
Reference in New Issue
Block a user