refactor: 重构蓝牙设备页与新增设备页的扫描-连接-录入流程
- 通用 BLE 健康设备识别(血压计/血糖仪/体重秤/血氧仪 标准 service UUID) - 蓝牙设备页静默自动同步:仅匹配已绑定的设备名+类型,收到数据后变绿 - 新增设备页:扫描所有支持类型设备 → 选连接 → 绑定 + 同步首次数据 - 修复 read 缓存导致的重复录入循环(删除主动 read,依赖 indicate 推送) - 修复 isConnected 缓存状态导致连接异常(强制先 disconnect 再 connect) - 录入弹窗:血压心率同等级展示、3 秒自动关闭/手动确认 - 顺手更新本机开发 IP 与清理过时设计文档
This commit is contained in:
@@ -1,55 +1,63 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/ble_device.dart';
|
||||
import '../models/bp_reading.dart';
|
||||
import '../services/health_ble_service.dart';
|
||||
import 'auth_provider.dart';
|
||||
import 'data_providers.dart';
|
||||
import '../models/bp_reading.dart';
|
||||
import '../services/omron_ble_service.dart';
|
||||
|
||||
/// BLE 服务单例
|
||||
final omronBleServiceProvider = Provider<OmronBleService>((ref) {
|
||||
return OmronBleService();
|
||||
const _boundDevicesKey = 'bound_ble_devices';
|
||||
const _readingFingerprintsKey = 'ble_reading_fingerprints';
|
||||
const _legacyBpMacKey = 'bp_device_mac';
|
||||
const _legacyBpNameKey = 'bp_device_name';
|
||||
const _legacyBpLastSyncKey = 'bp_last_sync';
|
||||
|
||||
final healthBleServiceProvider = Provider<HealthBleService>((ref) {
|
||||
final service = HealthBleService();
|
||||
ref.onDispose(service.dispose);
|
||||
return service;
|
||||
});
|
||||
|
||||
/// 设备绑定状态
|
||||
// Keep the old provider name as a compatibility alias while the feature is
|
||||
// being migrated away from the Omron-specific naming.
|
||||
final omronBleServiceProvider = healthBleServiceProvider;
|
||||
|
||||
class DeviceBindState {
|
||||
final bool isBound;
|
||||
final String? mac;
|
||||
final String? name;
|
||||
final String? lastSync;
|
||||
final List<BoundBleDevice> devices;
|
||||
final bool isConnected;
|
||||
final BpReading? lastReading;
|
||||
|
||||
const DeviceBindState({
|
||||
this.isBound = false,
|
||||
this.mac,
|
||||
this.name,
|
||||
this.lastSync,
|
||||
this.isConnected = false,
|
||||
this.lastReading,
|
||||
});
|
||||
const DeviceBindState({this.devices = const [], this.isConnected = false});
|
||||
|
||||
DeviceBindState copyWith({
|
||||
bool? isBound,
|
||||
String? mac,
|
||||
String? name,
|
||||
String? lastSync,
|
||||
bool? isConnected,
|
||||
BpReading? lastReading,
|
||||
}) => DeviceBindState(
|
||||
isBound: isBound ?? this.isBound,
|
||||
mac: mac ?? this.mac,
|
||||
name: name ?? this.name,
|
||||
lastSync: lastSync ?? this.lastSync,
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
lastReading: lastReading ?? this.lastReading,
|
||||
);
|
||||
bool get isBound => devices.isNotEmpty;
|
||||
|
||||
DeviceBindState copyWith({List<BoundBleDevice>? devices, bool? isConnected}) {
|
||||
return DeviceBindState(
|
||||
devices: devices ?? this.devices,
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
);
|
||||
}
|
||||
|
||||
List<BoundBleDevice> devicesOfType(BleDeviceType type) {
|
||||
return devices.where((device) => device.type == type).toList();
|
||||
}
|
||||
|
||||
BoundBleDevice? findById(String id) {
|
||||
for (final device in devices) {
|
||||
if (device.id == id) return device;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceBindNotifier extends Notifier<DeviceBindState> {
|
||||
StreamSubscription? _connSub;
|
||||
StreamSubscription<bool>? _connSub;
|
||||
|
||||
@override
|
||||
DeviceBindState build() {
|
||||
ref.onDispose(() => _connSub?.cancel());
|
||||
_loadBinding();
|
||||
_listenConnection();
|
||||
return const DeviceBindState();
|
||||
@@ -57,52 +65,182 @@ class DeviceBindNotifier extends Notifier<DeviceBindState> {
|
||||
|
||||
void _listenConnection() {
|
||||
_connSub?.cancel();
|
||||
_connSub = ref.read(omronBleServiceProvider).connectionState.listen((connected) {
|
||||
_connSub = ref.read(healthBleServiceProvider).connectionState.listen((
|
||||
connected,
|
||||
) {
|
||||
if (state.isConnected != connected) {
|
||||
state = state.copyWith(isConnected: connected);
|
||||
}
|
||||
if (!connected && state.isBound) {
|
||||
// 设备断开,但保持绑定信息(下次可以重连)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadBinding() async {
|
||||
final db = ref.read(localDbProvider);
|
||||
final mac = await db.read('bp_device_mac');
|
||||
final name = await db.read('bp_device_name');
|
||||
final lastSync = await db.read('bp_last_sync');
|
||||
if (mac != null && mac.isNotEmpty) {
|
||||
state = state.copyWith(isBound: true, mac: mac, name: name, lastSync: lastSync);
|
||||
await _migrateLegacyBloodPressureDevice();
|
||||
final raw = await db.read(_boundDevicesKey);
|
||||
final devices = _decodeDevices(raw);
|
||||
state = state.copyWith(devices: devices);
|
||||
}
|
||||
|
||||
Future<void> _migrateLegacyBloodPressureDevice() async {
|
||||
final db = ref.read(localDbProvider);
|
||||
final existing = await db.read(_boundDevicesKey);
|
||||
final legacyMac = await db.read(_legacyBpMacKey);
|
||||
if (existing != null || legacyMac == null || legacyMac.isEmpty) return;
|
||||
|
||||
final legacyName = await db.read(_legacyBpNameKey);
|
||||
final migrated = BoundBleDevice(
|
||||
id: legacyMac,
|
||||
name: legacyName?.isNotEmpty == true ? legacyName! : '血压计',
|
||||
type: BleDeviceType.bloodPressure,
|
||||
serviceUuid: BleDeviceType.bloodPressure.serviceUuid,
|
||||
);
|
||||
await db.write(_boundDevicesKey, jsonEncode([migrated.toJson()]));
|
||||
await db.delete(_legacyBpMacKey);
|
||||
await db.delete(_legacyBpNameKey);
|
||||
await db.delete(_legacyBpLastSyncKey);
|
||||
}
|
||||
|
||||
List<BoundBleDevice> _decodeDevices(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return [];
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) return [];
|
||||
return decoded
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => BoundBleDevice.fromJson(Map<String, dynamic>.from(item)),
|
||||
)
|
||||
.whereType<BoundBleDevice>()
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> bind(String mac, String name) async {
|
||||
Future<void> _saveDevices(List<BoundBleDevice> devices) async {
|
||||
final db = ref.read(localDbProvider);
|
||||
await db.write('bp_device_mac', mac);
|
||||
await db.write('bp_device_name', name);
|
||||
state = state.copyWith(isBound: true, mac: mac, name: name);
|
||||
await db.write(
|
||||
_boundDevicesKey,
|
||||
jsonEncode(devices.map((device) => device.toJson()).toList()),
|
||||
);
|
||||
state = state.copyWith(devices: devices);
|
||||
}
|
||||
|
||||
Future<void> unbind() async {
|
||||
final db = ref.read(localDbProvider);
|
||||
await db.delete('bp_device_mac');
|
||||
await db.delete('bp_device_name');
|
||||
await db.delete('bp_last_sync');
|
||||
state = const DeviceBindState();
|
||||
Future<void> bind(BoundBleDevice device) async {
|
||||
final devices = [...state.devices];
|
||||
final index = devices.indexWhere((item) => item.id == device.id);
|
||||
if (index >= 0) {
|
||||
devices[index] = device.copyWith(lastSyncAt: devices[index].lastSyncAt);
|
||||
} else {
|
||||
devices.add(device);
|
||||
}
|
||||
await _saveDevices(devices);
|
||||
}
|
||||
|
||||
void setConnected(bool connected) {
|
||||
state = state.copyWith(isConnected: connected);
|
||||
Future<void> unbind(String id) async {
|
||||
await _saveDevices(
|
||||
state.devices.where((device) => device.id != id).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onReading(BpReading reading) async {
|
||||
final db = ref.read(localDbProvider);
|
||||
final lastSync = '${reading.timestamp.month}/${reading.timestamp.day} ${reading.timestamp.hour}:${reading.timestamp.minute.toString().padLeft(2, '0')}';
|
||||
await db.write('bp_last_sync', lastSync);
|
||||
state = state.copyWith(lastSync: lastSync, lastReading: reading);
|
||||
Future<void> recordSuccessfulSync({
|
||||
required BoundBleDevice device,
|
||||
required BpReading reading,
|
||||
}) async {
|
||||
await _rememberReadingFingerprint(device, reading);
|
||||
final syncedAt = DateTime.now();
|
||||
final devices = state.devices.map((item) {
|
||||
if (item.id != device.id) return item;
|
||||
return item.copyWith(lastSyncAt: syncedAt);
|
||||
}).toList();
|
||||
await _saveDevices(devices);
|
||||
ref.invalidate(latestHealthProvider);
|
||||
}
|
||||
|
||||
Future<bool> isDuplicateReading(
|
||||
BoundBleDevice device,
|
||||
BpReading reading,
|
||||
) async {
|
||||
final fingerprints = await _loadFingerprints();
|
||||
final now = DateTime.now();
|
||||
final pruned = _pruneFingerprints(fingerprints, now);
|
||||
if (pruned.length != fingerprints.length) {
|
||||
await _saveFingerprints(pruned);
|
||||
}
|
||||
|
||||
final key = _readingFingerprint(device, reading);
|
||||
final lastSeenRaw = pruned[key];
|
||||
if (lastSeenRaw == null) return false;
|
||||
final lastSeen = DateTime.tryParse(lastSeenRaw)?.toLocal();
|
||||
if (lastSeen == null) return false;
|
||||
if (reading.hasDeviceTimestamp) return true;
|
||||
return now.difference(lastSeen).inSeconds < 10;
|
||||
}
|
||||
|
||||
Future<void> _rememberReadingFingerprint(
|
||||
BoundBleDevice device,
|
||||
BpReading reading,
|
||||
) async {
|
||||
final now = DateTime.now();
|
||||
final fingerprints = _pruneFingerprints(await _loadFingerprints(), now);
|
||||
fingerprints[_readingFingerprint(device, reading)] = now
|
||||
.toUtc()
|
||||
.toIso8601String();
|
||||
await _saveFingerprints(fingerprints);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _loadFingerprints() async {
|
||||
final db = ref.read(localDbProvider);
|
||||
final raw = await db.read(_readingFingerprintsKey);
|
||||
if (raw == null || raw.isEmpty) return {};
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) return {};
|
||||
return decoded.map(
|
||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||
);
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveFingerprints(Map<String, String> fingerprints) async {
|
||||
final db = ref.read(localDbProvider);
|
||||
await db.write(_readingFingerprintsKey, jsonEncode(fingerprints));
|
||||
}
|
||||
|
||||
Map<String, String> _pruneFingerprints(
|
||||
Map<String, String> fingerprints,
|
||||
DateTime now,
|
||||
) {
|
||||
final pruned = <String, String>{};
|
||||
for (final entry in fingerprints.entries) {
|
||||
final seenAt = DateTime.tryParse(entry.value)?.toLocal();
|
||||
if (seenAt == null) continue;
|
||||
if (now.difference(seenAt).inHours <= 24) {
|
||||
pruned[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
return pruned;
|
||||
}
|
||||
|
||||
String _readingFingerprint(BoundBleDevice device, BpReading reading) {
|
||||
final timePart = reading.hasDeviceTimestamp
|
||||
? reading.timestamp.toUtc().toIso8601String()
|
||||
: 'no-device-time';
|
||||
return [
|
||||
device.id,
|
||||
device.type.code,
|
||||
timePart,
|
||||
reading.systolic,
|
||||
reading.diastolic,
|
||||
reading.pulse ?? 0,
|
||||
].join('|');
|
||||
}
|
||||
}
|
||||
|
||||
final omronDeviceProvider = NotifierProvider<DeviceBindNotifier, DeviceBindState>(DeviceBindNotifier.new);
|
||||
final omronDeviceProvider =
|
||||
NotifierProvider<DeviceBindNotifier, DeviceBindState>(
|
||||
DeviceBindNotifier.new,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user