【后端修复】 - 删除diet/consultation agent假工具声明 - 修复DayOfWeek实体注释 - 修复Vision API content序列化 - cleanup_service级联删除修复 - 用药提醒时区偏差修复 - 统一DateTime处理(UtcNow+8) - 新增UTC DateTime JSON转换器 【前端UI重构】 - 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景) - 登录页重设计 - 首页重设计(透明顶栏、渐变背景、胶囊输入区) - 聊天卡片加白蓝边框、渐变标题 - 侧边栏重构(渐变背景、合并顶部、删除底部设置) - 确认卡片可编辑字段恢复 - 所有子页面加返回按钮 - catch异常加日志 - 删除后refresh provider缓存
166 lines
5.0 KiB
Dart
166 lines
5.0 KiB
Dart
import 'dart:async';
|
||
import 'dart:math';
|
||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||
import '../models/bp_reading.dart';
|
||
|
||
/// 欧姆龙蓝牙血压计 BLE 服务
|
||
class OmronBleService {
|
||
static const bpServiceUuid = '00001810-0000-1000-8000-00805F9B34FB';
|
||
static const bpMeasurementUuid = '00002A35-0000-1000-8000-00805F9B34FB';
|
||
static const bpFeatureUuid = '00002A49-0000-1000-8000-00805F9B34FB';
|
||
static const dateTimeUuid = '00002A08-0000-1000-8000-00805F9B34FB';
|
||
|
||
BluetoothDevice? _device;
|
||
StreamSubscription<List<int>>? _subscription;
|
||
final _readingsController = StreamController<BpReading>.broadcast();
|
||
Stream<BpReading> get readings => _readingsController.stream;
|
||
|
||
bool get isConnected => _device?.isConnected ?? false;
|
||
|
||
/// 扫描血压计设备
|
||
Stream<ScanResult> scan({Duration timeout = const Duration(seconds: 15)}) async* {
|
||
await FlutterBluePlus.startScan(timeout: timeout);
|
||
await for (final list in FlutterBluePlus.scanResults) {
|
||
for (final r in list) {
|
||
yield r;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 判断是否为血压计设备
|
||
static bool isBpDevice(ScanResult r) {
|
||
final name = [
|
||
r.advertisementData.localName,
|
||
r.device.platformName,
|
||
].where((n) => n.isNotEmpty).join(' ').toUpperCase();
|
||
return name.contains('OMRON') ||
|
||
name.contains('HEM') ||
|
||
name.contains('BLESMART') ||
|
||
name.contains('J735') ||
|
||
name.contains('BPM') ||
|
||
name.contains('BP');
|
||
}
|
||
|
||
/// 连接设备并订阅测量数据
|
||
Future<void> connect(BluetoothDevice device) async {
|
||
_device = device;
|
||
await device.connect(autoConnect: false);
|
||
await device.discoverServices();
|
||
|
||
final services = await device.discoverServices();
|
||
final bpService = services.firstWhere(
|
||
(s) => s.uuid.str128.toUpperCase() == bpServiceUuid.toUpperCase(),
|
||
);
|
||
|
||
final measurementChar = bpService.characteristics.firstWhere(
|
||
(c) => c.uuid.str128.toUpperCase() == bpMeasurementUuid.toUpperCase(),
|
||
);
|
||
|
||
await measurementChar.setNotifyValue(true);
|
||
_subscription = measurementChar.lastValueStream.listen(_parseReading);
|
||
await _syncTime(bpService);
|
||
}
|
||
|
||
/// 断开连接
|
||
Future<void> disconnect() async {
|
||
_subscription?.cancel();
|
||
_subscription = null;
|
||
await _device?.disconnect();
|
||
_device = null;
|
||
}
|
||
|
||
void dispose() {
|
||
disconnect();
|
||
_readingsController.close();
|
||
}
|
||
|
||
// ── 解析 BLE 测量数据(Bluetooth SIG BP Service 1.1) ──
|
||
|
||
void _parseReading(List<int> raw) {
|
||
if (raw.length < 7) return;
|
||
|
||
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();
|
||
}
|
||
|
||
int offset = 7;
|
||
DateTime timestamp = DateTime.now();
|
||
|
||
if (hasTimestamp && raw.length >= offset + 7) {
|
||
int year = raw[offset] | (raw[offset + 1] << 8);
|
||
int month = raw[offset + 2];
|
||
int day = raw[offset + 3];
|
||
int hour = raw[offset + 4];
|
||
int minute = raw[offset + 5];
|
||
int second = raw[offset + 6];
|
||
timestamp = DateTime(year, month, day, hour, minute, second);
|
||
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 += 1;
|
||
}
|
||
|
||
bool bodyMovement = false, irregularPulse = false;
|
||
if (hasStatus && raw.length >= offset + 2) {
|
||
int status = raw[offset] | (raw[offset + 1] << 8);
|
||
bodyMovement = (status & 0x0001) != 0;
|
||
irregularPulse = (status & 0x0800) != 0;
|
||
}
|
||
|
||
_readingsController.add(BpReading(
|
||
systolic: systolic,
|
||
diastolic: diastolic,
|
||
pulse: pulse,
|
||
timestamp: timestamp,
|
||
userId: userId,
|
||
isKpa: isKpa,
|
||
hasBodyMovement: bodyMovement,
|
||
hasIrregularPulse: irregularPulse,
|
||
));
|
||
}
|
||
|
||
/// SFLOAT IEEE-11073 16-bit 解码
|
||
static double _decodeSFloat(int raw) {
|
||
int mantissa = raw & 0x0FFF;
|
||
if (mantissa & 0x0800 != 0) mantissa |= 0xFFFFF000;
|
||
int exponent = (raw >> 12) & 0x0F;
|
||
if (exponent & 0x08 != 0) exponent |= 0xFFFFFFF0;
|
||
return (mantissa * pow(10, exponent)).toDouble();
|
||
}
|
||
|
||
/// 同步当前时间到设备
|
||
Future<void> _syncTime(BluetoothService service) async {
|
||
try {
|
||
final dtChar = service.characteristics.firstWhere(
|
||
(c) => c.uuid.str128.toUpperCase() == dateTimeUuid.toUpperCase(),
|
||
);
|
||
final now = DateTime.now();
|
||
await dtChar.write([
|
||
now.year & 0xFF, now.year >> 8,
|
||
now.month, now.day,
|
||
now.hour, now.minute, now.second,
|
||
]);
|
||
} catch (e) { print('[BLE]读取数据失败: $e'); }
|
||
}
|
||
}
|