Flutter中ECG心电图绘制:BLE设备数据解析与绘图方法问询
解析BLE ECG数据并绘制心电图的方法
一、数据解析步骤
首先要明确设备的BLE通信协议(有厂商文档优先参考),无文档时按通用逻辑推测:
确定字节序
BLE设备常用小端(Little-Endian)或大端(Big-Endian)字节序,两种转换逻辑如下:- 小端:低字节在前,高字节在后,比如字节对
C8 44转换为0x44C8 - 大端:高字节在前,低字节在后,比如字节对
C8 44转换为0xC844
- 小端:低字节在前,高字节在后,比如字节对
拆分数据字段
你收到的6字节数据C8-44-80-8E-37-C9,常见拆分方式有两种:- 方式1:3组16位数据(2字节/组):
[C8,44],[80,8E],[37,C9] - 方式2:4组12位数据(3字节拆为2组12位):
[C84],[480],[8E3],[7C9](从左到右取每12位)
- 方式1:3组16位数据(2字节/组):
转换为有符号十进制值
ECG采样值通常是有符号整数,转换规则:- 16位有符号:若十六进制值≥
0x8000,则用值 - 65536得到负数;否则直接转十进制。 - 12位有符号:若十六进制值≥
0x800,则用值 - 4096得到负数;否则直接转十进制。
- 16位有符号:若十六进制值≥
校准为实际mV值
ECG采样值是放大后的数字量,需按设备增益系数转换为毫伏值。比如增益为0.01mV/LSB时,实际ECG值 = 采样十进制值 × 0.01。
以方式1小端转换为例:
- 第一组
C8 44→ 小端转0x44C8= 17608(正数) - 第二组
80 8E→ 小端转0x8E80= 36480 → 36480 - 65536 = -29056(负数) - 第三组
37 C9→ 小端转0xC937= 51511 → 51511 - 65536 = -14025(负数)
校准后对应176.08mV、-290.56mV、-140.25mV,符合ECG典型数值范围。
另外,BPM心率可能在单独的BLE特征中,或藏在数据的某一字段里,可通过静止/运动状态下的数值变化判断。
二、Flutter中数据解析代码示例
基于flutter_blue_plus这类BLE插件的特征回调,编写解析逻辑:
// 原始数据为List<int>类型 void parseEcgData(List<int> rawData) { List<int> ecgSamples = []; // 按2字节一组拆分(小端字节序) for (int i = 0; i < rawData.length; i += 2) { if (i + 1 >= rawData.length) break; // 小端转换:低字节rawData[i],高字节rawData[i+1] int value = (rawData[i+1] << 8) | rawData[i]; // 转换为16位有符号整数 if (value >= 0x8000) { value -= 0x10000; } ecgSamples.add(value); } // 校准为mV值(示例增益0.01mV/LSB) List<double> ecgMv = ecgSamples.map((v) => v * 0.01).toList(); print('ECG采样值(mV): $ecgMv'); }
三、绘制ECG心电图的两种方式
1. 使用charts_flutter快速绘制折线图
适合静态或半实时展示:
import 'package:charts_flutter/flutter.dart' as charts; // 数据模型 class EcgData { final int time; final double value; EcgData(this.time, this.value); } // 构建图表 Widget buildEcgChart(List<double> ecgValues) { List<EcgData> chartData = []; for (int i = 0; i < ecgValues.length; i++) { chartData.add(EcgData(i, ecgValues[i])); } return charts.LineChart( [ charts.Series<EcgData, int>( id: 'ECG', colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, domainFn: (EcgData data, _) => data.time, measureFn: (EcgData data, _) => data.value, data: chartData, ) ], animate: false, behaviors: [ charts.ChartTitle('ECG心电图', behaviorPosition: charts.BehaviorPosition.top), charts.ChartTitle('时间(采样点)', behaviorPosition: charts.BehaviorPosition.bottom), charts.ChartTitle('电压(mV)', behaviorPosition: charts.BehaviorPosition.start), ], ); }
2. 自定义CustomPainter实现实时滚动绘制
适合动态展示最新采样点:
class EcgPainter extends CustomPainter { final List<double> ecgValues; final double width; final double height; final double scaleX; final double scaleY; EcgPainter({ required this.ecgValues, required this.width, required this.height, this.scaleX = 5.0, this.scaleY = 0.1, }); @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = Colors.blue ..strokeWidth = 2.0 ..style = PaintingStyle.stroke; Path path = Path(); if (ecgValues.isEmpty) return; // 起始点:x=0,y=画布中心 - 第一个值×缩放系数 double startY = height / 2 - ecgValues[0] * scaleY; path.moveTo(0, startY); for (int i = 1; i < ecgValues.length; i++) { double x = i * scaleX; double y = height / 2 - ecgValues[i] * scaleY; path.lineTo(x, y); } canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant EcgPainter oldDelegate) { return oldDelegate.ecgValues != ecgValues; } } // 使用自定义Painter Widget buildCustomEcgChart(List<double> ecgValues) { return SizedBox( width: 300, height: 200, child: CustomPaint( painter: EcgPainter( ecgValues: ecgValues, width: 300, height: 200, ), ), ); }
四、注意事项
- 若解析数值不合理,可切换字节序或数据拆分方式,结合设备状态变化验证字段用途。
- 实时绘制时,限制采样点数量(比如保留最近200个点),避免卡顿。
- 做好BLE连接异常处理,应对数据丢失或断连场景。
内容的提问来源于stack exchange,提问作者user23651217
相关产品推荐
相关产品推荐

