Files
AI-Health/health_app/lib/models/ble_device.dart
MingNian 4507083f3f refactor: 重构蓝牙设备页与新增设备页的扫描-连接-录入流程
- 通用 BLE 健康设备识别(血压计/血糖仪/体重秤/血氧仪 标准 service UUID)
- 蓝牙设备页静默自动同步:仅匹配已绑定的设备名+类型,收到数据后变绿
- 新增设备页:扫描所有支持类型设备 → 选连接 → 绑定 + 同步首次数据
- 修复 read 缓存导致的重复录入循环(删除主动 read,依赖 indicate 推送)
- 修复 isConnected 缓存状态导致连接异常(强制先 disconnect 再 connect)
- 录入弹窗:血压心率同等级展示、3 秒自动关闭/手动确认
- 顺手更新本机开发 IP 与清理过时设计文档
2026-06-28 22:53:35 +08:00

188 lines
5.6 KiB
Dart

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