QtCharts绘制动态心电图[2]——利用队列进行实时绘制

开发环境 : VS2017 社区版+QT 5.11.2 +插件qt-vsaddin-msvc2017-2.2.2

文章目录

  • 提要
  • 准备工作
    • 方法一 自定义环形队列(以前的方法)
      • 环形队列介绍
      • 环形队列的实现
    • 方法二 利用QT自带的队列(推荐)
  • 实时心电图绘制-简单例子
    • 初始化心电图
    • 模拟数据到来以及存入队列
    • 绘制心电图
  • 更多的优化-针对问题2的解决办法

QtCharts绘制动态心电图[2]——利用队列进行实时绘制_第1张图片

提要

在上一篇文章中QtCharts绘制动态心电图[1]——初步应用中已经实现的心电图的动态实现,但是在实际情况中,数据通过串口或者网口传来,如果到来一次数据绘制一次,有可能有以下两种情况:

  1. 数据来没绘制,下一次数据就到来,开始下一次数据绘制
  2. 数据绘制太快,等待时间绘制数据为0,影响整个图形的表达
  • 针对问题1,每次接收到数据就应该加入到缓存中,绘图就从缓存中进行读取绘制
  • 针对问题2,在问题一的基础上,需要动态判断缓存中数据个数,根据剩余个数进行绘制,即:
    剩余数据过多 绘制速度大于读取速度
    剩余数据过少 绘制速度小于读取速度

准备工作

方法一 自定义环形队列(以前的方法)

环形队列介绍

其实这里不想介绍了。网上有很多细致的讲解。。。

环形队列的实现

因为之前在STM32开发项目中也使用到了环形队列,所以这里环形队列的实现还是借鉴之前的实现方法。

主要是参考了这个网址STM32的串口软件FIFO

环形队列的声明

	//******** fifo相关 ***********
	typedef struct {
		qint16  data_buf[FIFO_BUFFER_SIZE]; // FIFO buffer 16位有符号数
		quint16 i_first;                    // index of oldest data byte in buffer
		quint16 i_last;                     // index of newest data byte in buffer
		quint16 num_bytes;                  // number of bytes currently in buffer
	}sw_fifo_typedef;
	sw_fifo_typedef ecg_data_fifo = { {0}, 0, 0, 0 }; // declare a receive software buffer

								//------------- fifo中标志位的配置 ---------------
	quint8 fifo_not_empty_flag; // this flag is automatically set and cleared by the software buffer
	quint8 fifo_full_flag;      // this flag is automatically set and cleared by the software buffer
	quint8 fifo_ovf_flag;       // this flag is not automatically cleared by the software buffer
	 

往队列中写入数据函数

//***********  fifo环形队列相关 开始******************
void RealTimeEcg::ecgDataFifoIn(qint16 inputData) {
	//
		  /* Explicitly clear the source of interrupt if necessary */

	if (ecg_data_fifo.num_bytes == FIFO_BUFFER_SIZE) {      // if the sw buffer is full
		fifo_ovf_flag = 1;                     // set the overflow flag

	//TODO fifo_ovf_flag,这个标志影响是否还写入,或者是清空已有数据,然后重新谢写入?
	}
	else if (ecg_data_fifo.num_bytes < FIFO_BUFFER_SIZE) { // if there's room in the sw buffer
		ecg_data_fifo.data_buf[ecg_data_fifo.i_last] = inputData;/* enter pointer to UART rx hardware buffer here */ // store the received data as the newest data element in the sw buffer
		/

		ecg_data_fifo.i_last++;                              // increment the index of the most recently added element
		ecg_data_fifo.num_bytes++;                           // increment the bytes counter

	}
	if (ecg_data_fifo.num_bytes == FIFO_BUFFER_SIZE) {      // if sw buffer just filled up
		fifo_full_flag = 1;
		// set the  FIFO full flag
	}
	if (ecg_data_fifo.i_last == FIFO_BUFFER_SIZE) {         // if the index has reached the end of the buffer,
		ecg_data_fifo.i_last = 0;                            // roll over the index counter
	}
	fifo_not_empty_flag = 1;                 // set received-data flag
} 

接下来是读取队列中的数据

//如果在环形队列中没有数据,返回为0
qint16 RealTimeEcg::ecgDataFifoOut() {
	//

	  /* Explicitly clear the source of interrupt if necessary */
	qint16 outputData = 0;
	if (ecg_data_fifo.num_bytes == FIFO_BUFFER_SIZE) { // if the sw buffer is full
		fifo_full_flag = 0;               // clear the buffer full flag because we are about to make room
	}

	if (ecg_data_fifo.num_bytes > 0) {                 // if data exists in the sw buffer

		outputData = ecg_data_fifo.data_buf[ecg_data_fifo.i_first]; // place oldest data element in the buffer

		ecg_data_fifo.i_first++;                        // increment the index of the oldest element
		ecg_data_fifo.num_bytes--;                      // decrement the bytes counter
	}
	if (ecg_data_fifo.i_first == FIFO_BUFFER_SIZE) {   // if the index has reached the end of the buffer,
		ecg_data_fifo.i_first = 0;                      // roll over the index counter
	}
	if (ecg_data_fifo.num_bytes == 0) {                // if no more data exists
		fifo_not_empty_flag = 0;          // clear flag
	}
	return outputData;
}

方法二 利用QT自带的队列(推荐)

开发开发着就发现,其实QT有自带的队列,使用也要简便的多,不用去考虑那么多的细节实现。并且在实际情况中,并不会耗尽所有资源区。如果考虑实时性更好,就会考虑使用快、慢速两种状态对队列内数据进行绘制,保证未绘制数据控制在一定范围内,也就是控制延迟时间在一定范围内

//QT中队列的定义
QQueue <qint16>m_EcgShortQueue;//中间是数据类型

//队列插入一个数
m_EcgShortQueue.enqueue(tempInt16);

//取出一个数
drawPoint = m_EcgShortQueue.dequeue();

可以看出QQueue的使用还是很简单方便的。
但是需要注意QQueue 的本质其实是一个list,所以在使用dequeue取出一个数的时候,必须要判断m_EcgShortQueue的size是否为0,不然程序会直接崩溃。(QT的list经常出现这些问题。。。基本每个使用list的地方都要记得判断)

if (m_EcgShortQueue.size() != 0)
	{
		drawPoint = m_EcgShortQueue.dequeue();
	}

实时心电图绘制-简单例子

初始化心电图

相对于前一篇,这里初始化就添加600个点(可能全部是是0),之后就不用判断是否是第一次进行数据绘制,直接替换对应点,减少系统消耗

     ecgSeries->clear();
	//提前添加完点,就不用考虑是否是第一次绘制,只用替换现有点的数据
	for (int i = 0; i < AXIS_X_MAX_COUNTS; i++)
	{
		//ecgPointBuffer.append(QPointF(i, 0));
		*ecgSeries << QPointF(i, 0);
	}

模拟数据到来以及存入队列

在实际的工作中,一般是一定时间收到一个数据包,所以这里我们模拟500ms收到一个包

//模拟TCP数据到来的定时器
	ecgWaveReadTimer = new QTimer(this);
	connect(ecgWaveReadTimer, SIGNAL(timeout()), this, SLOT(oneTimeOutWriteToFifoAction()));
	ecgWaveReadTimer->start(500);//500ms执行一次

然后是槽函数,将读出的数据加入队列中

//模拟500ms收到一个数据包 以后这部分就可以是TCP或者是串口接收到一次数据就加入一次缓存队列
void RealTimeEcg::oneTimeOutWriteToFifoAction() {
	//需要从上一次队尾开始读取
	for (int i = originListIndex; i < (125 + originListIndex); i++)
	{
		qint16 tempInt16 = originList.at(i).toInt();
		m_EcgShortQueue.enqueue(tempInt16);
	}
	originListIndex += 125;
	//如果剩下的数据不足以支撑下一次数据读取,就停止定时器
	if ((originListIndex + 125) >= originListSize)
	{
		ecgWaveReadTimer->stop();
	}
}

绘制心电图

相对于前一篇,阅读源码发现了series的replace函数,所以这里对于数据的更新做了一点小修改,以为我们在初始化折线图的时候就添加600个点,所以这里我们只需要使用replace函数替换更改的点就可以了。

和之前类似,我们先初始化绘制定时器

	//******绘制折线定时器***********
	ecgWaveDrawTimer = new QTimer(this);
	ecgWaveDrawTimer->setInterval(DRAW_ECG_INTERVAL);
	ecgWaveDrawTimer->setTimerType(Qt::PreciseTimer);//精确
	connect(ecgWaveDrawTimer, SIGNAL(timeout()), this, SLOT(oneTimeOutReadFromFifoAction()));
	ecgWaveDrawTimer->start();

然后我们定义绘制函数。

//********* 读取队列中的数据,重复绘制数据 ****************
void RealTimeEcg::oneTimeOutReadFromFifoAction() {//一直进行绘制,不会停止,除非手动停止
	qDebug() << QString("fifo中剩余的数据个数%1").arg(m_EcgShortQueue.size());
	//环形队列如果内部没有数据的时候,fifoOut一直为0
	qint16 drawPoint = 0;
	if (axis_x_counts == AXIS_X_MAX_COUNTS)
	{
		axis_x_counts = 0;
	}
	if (m_EcgShortQueue.size() != 0)
	{
		drawPoint = m_EcgShortQueue.dequeue();
	}
	ecgSeries->replace(axis_x_counts, QPointF(axis_x_counts, drawPoint));//只替换当前数据点,不替换所有的数据
	axis_x_counts++;

}

更多的优化-针对问题2的解决办法

如果数据量很大,再加上Qt的定时器不是特别精确,采用这个简单案例的方案会导致缓存数据越来越多,所以本方法采用的是直接舍弃前面几秒的数据。这样就会造成部分数据丢失。

之前采用的优化方案是根据缓存数量修改Timer的间隔时间,保证所有数据都能进行绘制,具体方案如下:

  1. 当队列缓存数据数量小于一个值的时候,设置心电定时器间隔时间长一点,达到缓慢绘制效果
  2. 当队列缓存数据数量大于一个值的时候,设置心电定时器间隔时间短一点,达到快速绘制效果

但是其实现实中还有很多的问题,比如8ms进行一次绘制非常占用资源,比如QtCharts快速绘制的时候非常占用资源,如果需要快速显示多个心电窗格(比如大量心电数据的回看)等待时间非常长, 请看下一篇文章《QtCharts绘制动态心电图[3]——实时绘制的优化处理》(不知道多久能够完成- - 主要是要避开涉及项目的内容)。

程序下载链接

说明:程序提供的下载(本程序是利用VS2017+Qt 5.11.2进行编写,建议用相同环境打开,不同的Qt版本可能会导致编译不成功,最近时间挺紧张,后面有时间才可能再考虑转换成纯Qt版本)

你可能感兴趣的:(QT,QT,心电图,实时,队列,动态)