feat: 蓝牙血压计基础架构 + UI配色体系升级

- 新增 AppColors 配色体系(紫蓝渐变 + 超大圆角 + 漂浮卡片)
- 新增 OmronBleService(BLE 扫描/连接/SFLOAT 协议解析)
- 新增 BpReading 数据模型 + 血压数据自动同步后端
- 新增 DeviceScanPage(血压计扫描绑定页)+ DeviceBindPage(已绑定管理页)
- 侧边栏新增"蓝牙设备"入口,ProfilePage 移除设备管理
- Android/iOS BLE 权限配置(BLUETOOTH_SCAN/CONNECT)
- AppTheme 升级:新阴影系统、新语义色、圆角放大
This commit is contained in:
MingNian
2026-06-09 18:48:58 +08:00
parent f01fc9268d
commit 3964cf2bcb
14 changed files with 1068 additions and 157 deletions

View File

@@ -0,0 +1,165 @@
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 (_) {}
}
}