Canvas 将 Min-Max 心率采样绘成连续包络波形

心率记录需要同时返回 BPM 和一秒采样波形。设备端每秒处理 500 个 ADC 样本,将它们分成 10 个桶,每个桶输出一个 8-bit 最小值和最大值。这样一条记录会携带 20 个波形字节,主机端再把它显示在实时心率曲线下方。
这类数据不能直接按 20 个普通采样点连线。20 个字节实际代表 10 组范围,每组范围覆盖一段原始样本。最终图表采用“一个桶一个 x 坐标”的包络绘制,再叠加一条中心线,既保留极值,也避免把 Min-Max 数据误读成带有明确先后顺序的波形点。
Min-Max 桶语义
协议中的波形部分由 10 组连续的 [min,max] 组成。每组覆盖 50 个新样本,桶边界保留一个重叠样本,用于保持边沿信息。min 和 max 只描述这段样本的范围,协议没有携带极值出现的原始索引。
主机端为每个桶建立一条记录:
| 字段 | 含义 |
|---|---|
timestamp |
桶中心对应的时间 |
min |
该桶的 8-bit 最小值 |
max |
该桶的 8-bit 最大值 |
value |
(min + max) / 2,用于中心线 |
如果把每组 [min,max] 拆成两个有序时间点,页面必须猜测极值先后。上升段、下降段和重复量化值都会让这条猜测产生凹刺或不自然的平台。包络绘制直接使用协议提供的范围信息,避免引入这层猜测。
桶中心坐标与 15 秒窗口
记录时间戳表示这一秒波形的结束位置。第 pairIndex 个桶的中心时间计算为:
const bucketTimestamp = recordTimestamp - 1 +
(pairIndex + 0.5) / 10;10 个桶均匀落在 [recordTimestamp - 1, recordTimestamp] 内。每个桶只有一个 x 坐标,min 和 max 共同描述这个位置的上下边界。
调试器只保留最近 15 秒,也就是 150 个桶。单条读取、批量读取和连续读取都进入同一个 appendHeartRateSignalRecord() 数据入口。记录用 id:timestamp 去重,Canvas 重绘通过一个待处理的 requestAnimationFrame 合并,避免一次批量响应触发多次完整绘制。
包络路径与中心线
Canvas 先把所有桶的 min 和 max 映射到动态 8-bit 纵轴。包络路径分两段建立:第一段从左到右连接每个桶的 max,第二段从右到左连接每个桶的 min,最后闭合路径并填充淡色区域。
ctx.beginPath();
for (let index = start; index < end; index++) {
const bucket = buckets[index];
const x = xForTimestamp(bucket.timestamp);
const maxY = yForValue(bucket.max, min, max, area);
if (index === start) {
ctx.moveTo(x, maxY);
} else {
ctx.lineTo(x, maxY);
}
}
for (let index = end - 1; index >= start; index--) {
const bucket = buckets[index];
const x = xForTimestamp(bucket.timestamp);
const minY = yForValue(bucket.min, min, max, area);
ctx.lineTo(x, minY);
}
ctx.closePath();
ctx.fill();包络上下边界使用较浅的青绿色描边,中心线使用桶的中点:
const value = (bucket.min + bucket.max) / 2;
const y = yForValue(value, min, max, area);包络路径覆盖相邻桶之间的整个区间,所以画面不会出现独立柱子之间的空白。中心线提供清晰的趋势轮廓,淡色区域继续保留每个桶的极值范围。截图中的上下边界、中心线和右侧 0 到 255 刻度共同表达了同一组采样数据。
8-bit 量化的视觉表现
设备端把 12-bit ADC 极值量化为 8-bit:有效 ADC 值执行 sample >> 4,超出 12-bit 范围的值饱和到 255。量化会让相邻桶出现相同的最小值或最大值,包络边界可能形成短平台,中心线也会保留阶梯状变化。
这种平台属于量化后的数据特征。它与多个采样点被错误放到同一 x 坐标的情况不同。包络图把一个桶的范围画在一个时间位置,中心线只使用一个中点值,因此图形结构和数据结构保持一致。
上位机数据流与显示分工
实时心率区域和同步采样区域使用两套历史数据:
- 上方红色曲线保存 BPM、MAD、1 分钟均值和有效性状态。
- 下方青绿色图形保存最近 15 秒的 Min-Max 桶。
- 心率记录解析器从记录偏移 9 处读取 20 个波形字节。
- 单条记录和批量记录共享同一套桶转换与绘制逻辑。
- 原始 ADC 调试指令继续保留,便于对照未经 Min-Max 处理的采样数据。
这样的分工让 BPM 趋势和采样形状各自使用合适的纵轴。BPM 图固定业务范围,采样图使用 8-bit 动态范围,两个图不会因为量纲不同互相压缩。
数据边界与验证结果
Min-Max 包络能保证每个桶的极值进入画面,却不能恢复极值在 50 个原始样本中的准确先后。如果产品需要一条严格对应原始时间顺序的折线,协议还需要传输极值索引或保留有序采样点。当前协议只提供范围信息,包络加中心线是与协议语义一致的显示方式。
这次实现完成了三层校验:协议记录长度和 20 字节波形解析通过静态测试;调试器内联 JavaScript 通过语法检查;实机页面确认实时心率和同步波形上下排列,波形窗口稳定显示最近 15 秒。最终图形保持了极值信息,也让时间轴、包络范围和趋势线各自承担清晰的显示职责。