import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../core/app_module_visuals.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 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 => HealthMetricVisuals.bloodPressureIcon, BleDeviceType.glucose => HealthMetricVisuals.glucoseIcon, BleDeviceType.weightScale => Icons.monitor_weight_outlined, BleDeviceType.pulseOximeter => HealthMetricVisuals.spo2Icon, }; 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 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 toJson() => { 'id': id, 'name': name, 'type': type.code, 'serviceUuid': serviceUuid, 'lastSyncAt': lastSyncAt?.toUtc().toIso8601String(), }; static BoundBleDevice? fromJson(Map 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 services) { return typeFromUuidStrings(services.map((service) => service.uuid.str128)); } static BleDeviceType? typeFromUuidStrings(Iterable 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; }