树莓派4实时绘制生命体征数据的优化方案咨询
生命体征数据绘制卡顿优化方案
我使用4GB版树莓派4,搭配Raspberry Pi OS Bullseye、Django、Channels WebSocket以及Chromium+Canvas JavaScript,通过UART连接生命体征模块绘制人体生命体征数据,但每秒发送1600条数据导致信号卡顿,求更优的绘制方式。
环境信息
- Raspberry PI OS (2022-09-26)
- Python 3.9.2
- Django Channels + Daphne 4.0.0
原代码
asgi.py
from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack from monitor.routing import websocket_urlpatters application = ProtocolTypeRouter({ 'http': get_asgi_application(), 'websocket': AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) })
consumers.py
from channels.generic.websocket import WebsocketConsumer from json import dumps class MonitorConsumer(WebsocketConsumer): def connect(self): self.accept() while True: data = serial.read()#Serial data uart read self.send(dumps({'value':data}) def disconnect(self): self.disconnect()
routing.py
from django.urls import re_path from monitor.consumers import MonitorConsumer websocket_urlpatterns = [ re_path(r'ws/socket/', MonitorConsumer.as_asgi()) ]
HTML
<canvas id="ecg-wave" width="565" height="70"></canvas>
JavaScript
const ecgIWave = document.getElementById('ecg-wave'); const ecgICtx = ecgIWave.getContext('2d'); const graphWidth = ecgIWave.width; const graphHeight = ecgIWave.height; let ecgIX = 0, ecgIpX = 0, ecgIY = graphHeight, ecgIpY = graphHeight; ecgICtx.setTransform(1, 0, 0, -1, 0, graphHeight); const scanBarWidth = 10; const step = 0.75; const ecg_step = 0.25; let ecgIData = 0; function drawEcgI() { ecgIX += ecg_step; ecgICtx.clearRect(ecgIX, 0, scanBarWidth, graphHeight); ecgICtx.beginPath(); ecgICtx.moveTo(ecgIpX, ecgIpY); ecgICtx.lineJoin= 'round'; ecgIY = ecgIData/(4096/(graphHeight/2)); ecgICtx.lineTo(ecgIX, ecgIY); ecgICtx.stroke(); ecgIpX = ecgIX; ecgIpY = ecgIY; if (ecgIpX > graphWidth) { ecgIX = ecgIpX = -ecg_step; } } const URL = `ws://${window.location.host}/ws/socket/`; function startButton() { let socket = new WebSocket(URL); socket.onmessage = function(e) { let data = JSON.parse(e.data); ecgIData = data.value; requestAnimationFrame(drawEcgI); } }
优化方案
后端优化(减少WebSocket传输开销)
- 批量发送数据:不再单条发送,累积一定数量数据后打包推送,降低WebSocket连接的频繁交互开销。
- 异步处理串口读取:避免阻塞Channels的事件循环,用异步任务或线程处理串口数据读取。
- 简化数据格式:减少序列化开销,批量传输比单条JSON序列化更高效。
修改后的consumers.py示例:
import asyncio from channels.generic.websocket import AsyncWebsocketConsumer import serial import json # 根据实际硬件配置调整串口参数 ser = serial.Serial('/dev/ttyAMA0', 9600, timeout=0.01) class MonitorConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() # 启动异步任务持续读取串口数据 self.read_task = asyncio.create_task(self.read_serial_data()) async def disconnect(self, close_code): # 断开连接时取消异步任务 self.read_task.cancel() await super().disconnect(close_code) async def read_serial_data(self): data_batch = [] while True: try: data_byte = ser.read() if data_byte: # 将字节转为整数(根据模块输出格式调整) data_value = int.from_bytes(data_byte, byteorder='little') data_batch.append(data_value) # 每累积16条数据发送一次(对应10ms间隔,匹配1600条/秒的采样率) if len(data_batch) >= 16: await self.send(text_data=json.dumps({'values': data_batch})) data_batch = [] # 短暂休眠,避免占用过多CPU资源 await asyncio.sleep(0.001) except asyncio.CancelledError: break
前端优化(提升Canvas绘制效率)
- 固定帧率绘制:用
requestAnimationFrame维持60fps的稳定绘制频率,不随数据接收频率波动。 - 批量绘制路径:一次处理多条数据点,减少Canvas的绘制调用次数。
- 优化画布重置:避免频繁局部清空,改用全画布清空+位置重置的方式,减少绘制操作复杂度。
修改后的JavaScript示例:
const ecgIWave = document.getElementById('ecg-wave'); const ecgICtx = ecgIWave.getContext('2d'); const graphWidth = ecgIWave.width; const graphHeight = ecgIWave.height; let ecgIX = 0, ecgIpX = 0, ecgIY = graphHeight/2, ecgIpY = graphHeight/2; ecgICtx.setTransform(1, 0, 0, -1, 0, graphHeight); const step = 0.75; // 预计算缩放比例,避免重复计算 const scaleFactor = (graphHeight/2) / 4096; let dataQueue = []; let isDrawing = false; // 固定帧率的绘制循环 function drawLoop() { if (dataQueue.length === 0) { requestAnimationFrame(drawLoop); return; } ecgICtx.beginPath(); ecgICtx.moveTo(ecgIpX, ecgIpY); ecgICtx.lineJoin = 'round'; // 批量绘制队列中的数据点 while (dataQueue.length > 0 && ecgIX < graphWidth) { const data = dataQueue.shift(); ecgIY = data * scaleFactor; ecgIX += step; ecgICtx.lineTo(ecgIX, ecgIY); ecgIpX = ecgIX; ecgIpY = ecgIY; } ecgICtx.stroke(); // 画布满了之后重置 if (ecgIX >= graphWidth) { ecgICtx.clearRect(0, 0, graphWidth, graphHeight); ecgIX = ecgIpX = 0; ecgIpY = graphHeight/2; } requestAnimationFrame(drawLoop); } const URL = `ws://${window.location.host}/ws/socket/`; function startButton() { const socket = new WebSocket(URL); socket.onmessage = function(e) { const data = JSON.parse(e.data); // 把批量数据加入绘制队列 dataQueue.push(...data.values); // 首次收到数据时启动绘制循环 if (!isDrawing) { isDrawing = true; drawLoop(); } }; socket.onerror = function(err) { console.error('WebSocket连接错误:', err); isDrawing = false; }; }
内容的提问来源于stack exchange,提问作者Elmer
相关产品推荐
相关产品推荐

