一.前言
- 閒了好久沒更新博客,偷懶的我就拿了去年暑假備戰電賽時做的一個串口版“示波器”來充充水。拋磚引玉哈。
- 因爲以前發過一篇主題一樣的文章,所以就當該文就相當於上版的升級版(其實兩者沒啥關係,僅是主題一樣,以前那篇是單純爲賽題弄的,這篇更有通用性)
- 是當時爲電賽準備的(吐槽一下,原本我組準備的電源題,怎麼想着也會用上adc、pwm啥的吧,結果19年的A題一出來真是讓我一言難盡啊)
- 實現的功能有:自定義數據格式,多個數據圖線顯示,自動和手動兩種模式,顯示鼠標當前的位置
- 因爲時間有點久,很多註釋沒寫到還請諒解。
二.效果介紹
- 因爲一直宅在家,又沒帶板子回來,所以就只能看看界面了,紅色字體爲說明
三.軟件思路介紹
- 大概思路,串口接收數據,然後按照自定義格式解析數據,最後繪畫波形圖,因爲總代碼有點多,就不全貼出來,有空我上傳到github上去,也可以評論發郵箱號我來發給你。
- 這裏用到到繪畫波形圖的庫是qcustomplot,不清楚的可以百度一下,它的官網介紹得挺詳細的。
- 先是定義一個Handle類裏有串口的自動檢測,即如果檢測有串口出現會自動加到串口端口的欄中,和串口數據的接收、數據解析和曲線圖的繪畫(放在一個線程裏執行,防止界面顯示卡死)代碼如下,先是頭文件:
#include <QObject>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
#include "qcustomplot.h"
#include <QVector>
#define MAX_COUNT 1500 // x軸可顯示的最大範圍
#define cout qDebug() << __LINE__ << ":"
class Handle : public QObject
{
Q_OBJECT
public:
explicit Handle(QCustomPlot *P,QObject *parent = nullptr);
signals:
void finish(); // 數據解析完成時發出信號
public slots:
void getValidPort(QComboBox* p); // 獲取有效串口端口
void getText(QString t);
void analyData(); // 解析數據
void setFlag(QString b = "[",QString m=",",QString e="]"){
begin = b;mid = m; end = e;
} // 自定義數據格式,分隔符
void clear(); // 數據清空
void setAuto(bool e){ // 是否設置爲自動模式
isAuto = e;
}
void setRun(bool e){ // 是否設置爲運行
isRun = e;
}
void setOpen(bool e){ // 是否打開串口
isPortOpen = e;
}
bool getOpen(){return isPortOpen;} // 獲取串口打開狀態
bool getRun(){return isRun;} // 獲取串口是否在運行
bool getAuto(){return isAuto;} // 獲取是否爲自動模式
private:
QString begin,mid,end; // 自定義數據格式
QString text;
bool isPortOpen; // 標識串口端口是否打開
QStringList list;
QCustomPlot *Plot; // 圖層
QVector <QCPItemTracer *> trace; // 曲線上的跟隨點
QVector <QCPItemText * > Ptext; // 每條曲線上的顯示的文字
QVector <QVector<double>> YData; // Y軸數據,可具有多個數據曲線
QVector <double> XData; // x軸數據
QVector<QPen> pen; // 曲線顏色的區分
int Clk,xMid; // x軸的刻度
int countLine; // 曲線的數量
int firstLine;
bool isAuto,isRun; // 標識是否自動,是否在運行
};
- 再貼點串口數據解析函數的代碼
/*
* 串口數據的解析
*/
void Handle::analyData()
{
// 進行數據提取,比如begin="[",mid=",",end="]",串口數據爲,30][10,20,30][...
//
int _end = text.indexOf(begin);
text.remove(0,_end+1); // 移除前面多餘的數據,比如移除,30][,還剩10,20,30][...
if( _end < 0 || !isRun){
return ;
}
// 將數據分段,比如分出10,20,30]和下一段[...
QStringList _be = text.split(begin,QString::SkipEmptyParts);
for(auto _b:_be){
int _e = _b.indexOf(end); // 找出數據段裏的]的位置
if(_e > 0){
QStringList _list = _b.left(_e).split(mid,QString::SkipEmptyParts); // 將數據10,20,30中的各個數據分離出來
countLine = 0; // 統計一段數據裏的數據個數
for(auto i:list){ // 分離出的數據放入各個曲線數據裏
YData[countLine].push_back(i.toDouble());
if(YData[countLine].count() >= MAX_COUNT){
YData[countLine].pop_front();
}
countLine ++;
}
XData.push_back(Clk++);
if(XData.count() >= MAX_COUNT){
XData.pop_front();
}
for(int _i = firstLine; _i < countLine && _i < 6; _i ++){
firstLine = countLine;
Plot->addGraph();
Plot->graph(_i)->setPen(pen.at(_i)); // 給數據曲線上色
Plot->graph(_i)->setVisible(true); // 曲線顯示
trace[_i] = new QCPItemTracer(Plot); // 生成一個跟隨點
trace[_i]->setPen(pen.at(_i)); // 設置點的顏色
trace[_i]->setSize(10); // 設置點的大小
trace[_i]->setBrush(QBrush(pen.at(_i).color()));
trace[_i]->setGraph(Plot->graph(_i)); // 設置在該曲線顯示
trace[_i]->setStyle(QCPItemTracer::tsCircle); // 圓形點
trace[_i]->setInterpolating(true);
Ptext[_i] = new QCPItemText(Plot); // 跟隨點的文本信息
Ptext[_i]->setPen(pen.at(_i));
Ptext[_i]->setText(QString("Graph:%1").arg(_i));
Ptext[_i]->setRotation(4);
Ptext[_i]->setTextAlignment(Qt::AlignRight | Qt::AlignBottom|Qt::AlignJustify);
Ptext[_i]->setPositionAlignment(Qt::AlignRight | Qt::AlignBottom|Qt::AlignJustify);
Ptext[_i]->position->setType(QCPItemPosition::ptPlotCoords);
Ptext[_i]->setFont(QFont("Helvetica [Cronyx]", 12));
Ptext[_i]->setPadding(QMargins(8, 0, 0, 0));
YData[_i].resize(MAX_COUNT);
}
for(int _i = 0; _i < countLine; _i ++){
Plot->graph(_i)->setData(XData,YData[_i]);
if(Clk <= xMid){
Plot->xAxis->setRange(0,Clk);
}
else{
Plot->xAxis->setRange(Clk-xMid,Clk);
}
Ptext[_i]->position->setCoords(Clk,YData[_i].last()); // 文本顯示的位置
Ptext[_i]->setText(QString("%1:(%2,%3)").arg(_i+1).arg(Clk).arg(YData[_i].last())); // 文本內容
trace[_i]->setGraphKey(Clk); // 跟隨點的位置
if(isAuto){ // 是否自動適配縮放
Plot->yAxis->rescale(true);
}
}
}
}
Plot->replot(); // 圖層更新
if(text.length() > 200){ // 判斷串口接收數據是否過多,過多久丟棄
text.clear();
}
emit finish();
}
- 然後在widget類中,這裏比較簡單,開了個線程給Handle類,然後綁定各種信號與槽,先上頭文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
#include <QWidget>
#include <QDebug>
#include <QMessageBox>
#include "qcustomplot.h"
#include <QVector>
#include <QTimer>
#include <QPen>
#include <QMouseEvent>
#include <QToolTip>
#include "handle.h"
#include <QThread>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
void goClear(); // 清除
void autoHandle(); // 沒用上
signals:
void analy(QString t);
void clearT();
void getValidP(QComboBox* p);
void startA();
private slots:
void on_pushOpen_clicked();
void on_pushClear_clicked();
void on_pushRun_clicked();
void on_pushAuto_clicked();
private:
Ui::Widget *ui;
Handle *hand;
QCustomPlot *Plot;
QSerialPort *SerialPort;
QThread *thread;
};
#endif // WIDGET_H
- widget類構造函數裏的各種信號與槽的綁定
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
SerialPort = new QSerialPort(this);
Plot = ui->widget;
Plot->setMouseTracking(true); // 使能圖層鼠標跟隨
// 開啓圖層上的圖層刻度可手動調動
Plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
connect(Plot->xAxis, SIGNAL(rangeChanged(QCPRange)), Plot->xAxis2, SLOT(setRange(QCPRange)));
connect(Plot->yAxis, SIGNAL(rangeChanged(QCPRange)), Plot->yAxis2, SLOT(setRange(QCPRange)));
connect(Plot,&QCustomPlot::mouseMove,[=](QMouseEvent *e){
double _x = Plot->xAxis->pixelToCoord(e->x()),_y = Plot->yAxis->pixelToCoord(e->y());
ui->labelWhere->setText(QString("(%1,%2)").arg(_x).arg(_y));
});
connect(Plot,&QCustomPlot::plottableClick,[=](
QCPAbstractPlottable * plottable,
int ,
QMouseEvent * ){
ui->labelGraph->setText(plottable->name());
});
ui->tabWidget->setTabText(0,"Gui");
ui->tabWidget->setTabText(1,"Data");
hand = new Handle(Plot);
thread = new QThread(this);
hand->moveToThread(thread);
// 獲取自定義數據
hand->setFlag(ui->lineEditBegin->text(),ui->lineEditMid->text(),ui->lineEditEnd->text());
// 綁定清除按鍵的響應
connect(this,&Widget::clearT,hand,&Handle::clear,Qt::QueuedConnection);
// 有串口信息時,將數據給Handle類處理
connect(this,&Widget::analy,hand,&Handle::getText,Qt::QueuedConnection);
// 綁定獲取有效串口端口信息
connect(this,&Widget::getValidP,hand,&Handle::getValidPort,Qt::QueuedConnection);
// 這個也沒用上,還請注意
connect(hand,&Handle::finish,this,&Widget::autoHandle,Qt::QueuedConnection);
connect(SerialPort,&QSerialPort::readyRead,[=](){
emit analy(SerialPort->readAll());
});
connect(SerialPort,&QSerialPort::errorOccurred,[=](QSerialPort::SerialPortError e){
cout << "error";
if(e == QSerialPort::DeviceNotFoundError){
SerialPort->close();
hand->setOpen(false);
on_pushClear_clicked();
}
});
thread->start(); // 開啓線程
goClear();
}
四.完結
注:這個程序有個小問題,就是當窗口全屏時,就會發生數據丟失加劇的現象。當時的我本想試着再加一個線程來專門處理曲線繪畫的,後來發現數據丟失的現象更嚴重,就沒加那個線程了。數據丟失是由下位機發信息太過於頻繁,比如頻率大於1000HZ時,頻率降低可解決。