You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

树莓派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传输开销)

  1. 批量发送数据:不再单条发送,累积一定数量数据后打包推送,降低WebSocket连接的频繁交互开销。
  2. 异步处理串口读取:避免阻塞Channels的事件循环,用异步任务或线程处理串口数据读取。
  3. 简化数据格式:减少序列化开销,批量传输比单条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绘制效率)

  1. 固定帧率绘制:用requestAnimationFrame维持60fps的稳定绘制频率,不随数据接收频率波动。
  2. 批量绘制路径:一次处理多条数据点,减少Canvas的绘制调用次数。
  3. 优化画布重置:避免频繁局部清空,改用全画布清空+位置重置的方式,减少绘制操作复杂度。

修改后的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 13:41:01