第5章〓串口操作和第三方控件的使用

UART接口是嵌入式、物联网开发最重要的接口之一,有着十分广泛的应用。Qt自从5.1版开始提供了串口操作库,大幅降低了串口开发的难度。对于更早版本的Qt,虽然官方没有给出串口支持库,但是有热心的开发者开发了第三方的串口库,如QextSerialPort。虽然是第三方库,但是在功能、性能上均有良好的表现。

事实上,Qt作为一个历史悠久的、开源的平台,有着许多优秀的第三方库,QextSerialPort只是其中之一。Qt的第三方库涉及图形图表、复杂控件、功能增强等许多方面。

在本章的基础知识部分,首先详细介绍了Qt的串口操作类QSerialPort的使用,然后以QUC SDK为例介绍了Qt第三方控件库的使用,最后介绍了窗口菜单的使用。

在实践案例部分,使用本章介绍的知识为V0.1版简易气象站程序增加了下列功能: 

(1) 使用第三方控件库QUC SDK重新改进程序界面。

(2) 使用串口操作类通过串口读取硬件模块的数据,并对数据进行处理和显示。

(3) 增加历史数据曲线功能,方便查看历史数据。

(4) 增加窗口菜单和快捷键。

5.1基础知识
◆




视频讲解


5.1.1Qt串口通信类的使用

Qt 5提供了串口操作相关的类QSerialPortInfo和QSerialPort。使用QSerialPortInfo类可以检测系统的串口信息,如COM号、设备位置、厂商信息等。使用QSerialPort类可以完成串口的具体操作,如打开或关闭串口、读写数据等。在实际应用中,常常先用QSerialPortInfo类检测可用的串口,然后创建QSerialPort类对象来操作串口。要使用这两个类,应在pro文件中添加模块: 



QT += serialport





同时在程序中包含头文件: 



#include <QSerialPort>

#include <QSerialPortInfo>





下面通过一个例程介绍串口的基本操作,如图5.1所示。该程序具有获取串口信息、打开/关闭串口、读写串口数据等功能(参见示例代码\ch5\ch51QSerialPortDemo\)。



图5.1串口操作演示程序界面


1. 获取所有串口设备信息

要获取计算机中所有串口设备的信息,需要调用QSerialPortInfo类的静态成员函数availablePorts()。该函数的原型为: 



static QList <QSerialPortInfo> availablePorts();





该函数会将计算机中的所有串口设备信息保存到QList链表中。链表的每一个元素都是一个QSerialPortInfo类对象,含有串口设备的COM号、描述信息、序列号等信息。使用时可以通过foreach语句将链表中的元素取出并处理。例如,可以将“获取串口”按钮的槽函数修改如下,从而将串口信息显示在组合框控件comboBoxPortList中: 



void MainWindow::on_pushButtonGetPortList_clicked()

{

foreach (QSerialPortInfo info, QSerialPortInfo::availablePorts())

{

ui->comboBoxPortList->addItem(info.portName());

}

}





运行程序并单击“获取串口”按钮,组合框会显示出计算机的所有串口,如图5.2所示。



图5.2获取串口号并显示


如果要显示串口的描述文字,可以将代码修改为: 



foreach (QSerialPortInfo info, QSerialPortInfo::availablePorts())

{

ui->comboBoxPortList->addItem(QString(info.portName() + " " + info.description()));

}





此时的运行结果如图5.3所示。



图5.3获取串口号和描述文字并显示


2. 设置串口参数并打开串口

使用串口前需要对串口的COM号、波特率、奇偶校验等参数进行设置,具体步骤为: 

(1) 定义QSerialPort类对象。

(2) 设置串口的工作参数,如COM号、波特率、奇偶校验等。

(3) 使用open()函数打开串口。

(4) 确认串口已打开。

在本例中,首先为主窗口类增加成员变量: 



private:

QSerialPort *m_port = new QSerialPort();//(1)定义类对象





然后在“打开串口”按钮的槽函数中设置串口的参数并打开串口: 



void MainWindow::on_pushButtonOpenPort_clicked()

{

//(2)设置串口的工作参数

m_port->setPortName(ui->comboBoxPortList->currentText());//选取串口

m_port->setBaudRate(QSerialPort::Baud9600);//设置波特率

m_port->setDataBits(QSerialPort::Data8);//设置数据位数

m_port->setParity(QSerialPort::NoParity);//设置校验类型

m_port->setStopBits(QSerialPort::OneStop);//设置停止位长度

m_port->setFlowControl(QSerialPort::NoFlowControl);//设置流控制



//(3)打开串口

m_port->open(QIODevice::ReadWrite);



//(4)判断串口是否已打开

if (m_port->isOpen())

{

qDebug() << "打开串口成功";

}







else

{

qDebug() << "打开串口失败";

}

}





代码中的波特率(QSerialPort::Baud9600)、数据位数(QSerialPort::Data8)、校验类型(QSerialPort::NoParity)、停止位长度(QSerialPort::OneStop)、流控制(QSerialPort::NoFlowControl)都是QSerialPort类的枚举类型成员。它们的取值在帮助文档中有详细的介绍。isOpen()函数用于判断串口是否已打开。成功打开串口是读写数据、关闭串口、清空缓冲区等操作的前提,因此进行判断非常必要。

3. 串口的读写

QSerialPort类的父类QIODevice提供了公有的write()、read()、readAll()等函数。QSerialPort类继承了这些函数,从而实现串口数据的读写操作。在本例中,在“写数据”按钮的槽函数中调用write()函数向串口写数据(下面均假设串口m_port已打开): 



void MainWindow::on_pushButtonWriteData_clicked()

{

QByteArray data = "This is a test.";

qDebug() << m_port->write(data);

}





调用write()函数后,数据会写入串口缓冲区,等待硬件完成发送操作。write()函数的返回值为实际写出的字节数。在这个例子中,因为data变量的实际长度为15,所以write()函数的返回值为15。

类似地,也可以通过read()(读取指定长度的数据)、readLine()(读一行数据)、readAll()(读取所有数据)等函数读取串口接收缓冲区的内容。例如,在“读数据”按钮的槽函数中,可以调用readAll()函数读取数据: 



void MainWindow::on_pushButtonReadData_clicked()

{

qDebug() << m_port->readAll();

}





4. 关闭串口

串口使用完成后,需要调用close()函数关闭串口,从而释放资源。在本例中,在“关闭串口”的槽函数中完成这一操作: 



void MainWindow::on_pushButtonClosePort_clicked()

{

m_port->close();

}





如果尝试关闭一个没有打开的串口,则会出现错误QSerialPort:: NotOpenError。

5. 清空缓冲区

clear()函数可以根据需要清空输入/输出缓冲区中的内容。默认情况下,该函数会同时清空输入和输出缓冲区。如果使用QSerialPort类的枚举型变量Direction作为参数,则可以有选择地清空输入和输出缓冲区。枚举型变量Direction的定义如下: 



enum Direction

{

Input = 1,

Output = 2,

AllDirections = Input | Output

};





例如,下列第一行代码清空了输入缓冲区,第二行代码同时清空了输入和输出缓冲区: 



m_port->clear(Input);

m_port->clear(AllDirections);





如果清空成功,则clear()函数返回true,否则返回false。




视频讲解


5.1.2Qt的第三方控件库——QUC SDK
1. Qt的第三方库

Qt提供了许多功能强大的库,并保证这些库在不同操作系统下具有一致的行为。但是如果需要实现一些特殊的效果,就需要自行对库进行改进。很多热心的开发者将自己设计的库共享到了网上。下面简单介绍几个常见的Qt第三方库。

(1) QWT全称是Qt Widgets for Technical Applications。这是一个基于LGPL版权协议的开源项目,包含GUI组件和实用类。
QWT不但提供了刻度、滑块、刻度盘、指南针、温度计、旋钮等控件,还可生成各种专业图表。图5.4是使用QWT生成的对数坐标系下的图形。



图5.4QWT生成的图表


(2) QCustomPlot是一个用于绘图和数据可视化的控件包,可以生成美观、高品质的图形,并能将图形导出为多种常见格式,如矢量PDF文件或PNG、JPG、BMP等位图。开发者对代码进行了细致的优化,能支持实时可视化应用。图5.5是使用QCustomPlot生成的曲线。



图5.5QCustomPlot生成的曲线


(3) QextSerialPort是一个广泛应用的第三方串口操作库。支持Qt 2~Qt 5,可以运行在Windows、Linux、macOS X、FreeBSD等系统下。因为Qt 4不支持串口操作,所以QextSerialPort是Qt 4用户为数不多的选择之一。

(4) QUC SDK。SDK(Software Development Kit,软件开发工具包)是编程领域常用的名词,通常指为特定的软件框架、硬件平台、操作系统等开发应用程序时使用的开发工具集合。QUC SDK是一套由国内开发者设计、维护的界面库,有超过60个精美的控件,涵盖了各种仪表盘、进度条、指南针、曲线图、标尺、温度计、导航条、导航栏、高亮按钮、滑动选择器等内容。控件的绝大多数效果只要设置几个属性即可实现。QUC SDK支持MinGW、MSVC、gcc等编译器,可直接集成到Qt Creator中。图5.6展示了QUC SDK提供的部分控件。




图5.6QUC SDK控件示例


由于QUC SDK控件的色彩简洁明快、使用方便,因此本书以QUC SDK为例介绍Qt第三方库的使用方法。

2. QUC SDK的安装

因为QUC SDK的作者没有公开源代码,所以只能访问该项目的GitHub主页下载编译好的库文件。为了便于后续的使用,建议将整个项目下载到本地。

下面是QUC SDK项目中部分文件夹的结构。include文件夹存放了QUC SDK库的头文件,后面会使用到这个文件夹的内容。sdkdemo文件夹存放着示例文件。sdk_V开头的文件夹存放着不同开发环境需要使用的库文件。snap文件夹存放着各种控件的运行截图。



QUC SDK

├─include

├─sdkdemo

├─sdk_V20211010_mingw//MinGW版库文件

│├─qt_5_9_9_mingw53_32

│ ├─qt_5_12_3_mingw73_32

│├─qt_5_12_3_mingw73_64//最终选择的库

│└─…

├─sdk_V20211010_msvc   //MSVC版库文件

│├─qt_5_12_3_msvc2017_32  

│├─qt_5_14_0_msvc2017_64

│└─…

├─sdk_V20220222_static//静态版库文件

│├─qt5_linux_gcc_32

│├─qt5_win_mingw_32

│└─…

└─snap





QUC SDK提供了适用于不同开发环境的库文件,使用时需要根据自己的开发环境进行选择。具体选择标准是: 

(1) 库文件的版本应等于或略低于Qt的版本。

(2) 库文件的编译器版本应符合当前安装的编译器版本。如果使用了错误版本的库文件,会导致程序无法编译。

因为本书安装的Qt版本为5.14.2、编译器为MinGW 7.3.0 64bit,所以选择的库文件为qt_5_12_3_mingw73_64。解压该库文件可以得到quc.dll、libquc.a、qucd.dll、libqucd.a这4个文件。将它们复制到Qt的designer文件夹下即可完成库文件的安装。在第1章介绍Qt的安装时,将Qt安装在D:\Qt\下,这时designer文件夹的路径为: 



D:\Qt\Qt5.14.2\Tools\QtCreator\bin\plugins\designer







图5.7QUC SDK提供的部分控件


完成库文件的安装后,重启Qt Creator就可以在控件列表看到新安装的控件了。QUC SDK提供的所有控件都位于Quc Widgets栏目内,如图5.7所示。


3. QUC SDK的使用

要在项目中使用QUC SDK需要进行简单的配置,具体操作步骤如下(参见示例代码\ch5\ch52QUCDemo\): 

(1) 复制SDK头文件。在项目文件夹下新建子文件夹(如SDK),然后将QUC SDK的include文件夹中的所有文件(均为头文件)复制到SDK文件夹中。

(2) 复制库文件。将库文件解压得到的quc.dll和qucd.dll复制到SDK文件夹中。

(3) 引用库文件。在项目的pro文件末尾增加如下代码: 



INCLUDEPATH += $$PWD/SDK



CONFIG(release, debug|release){

LIBS+= -L$$PWD/SDK/ -lquc

} else {

unix {LIBS  += -L$$PWD/SDK/ -lquc}

else {LIBS  += -L$$PWD/SDK/ -lqucd}

}





代码中的PWD是Present Working Directory(当前工作目录)的缩写,指项目所在的文件夹。SDK指第(1)步中新建的SDK文件夹。如果使用了其他文件夹名称,则应将代码中的SDK替换为所使用的名称。

经过这样的配置,就可以像使用Qt自带控件一样使用QUC SDK的控件了。在示例代码中使用了gaugeArc和gaugeCompassPan两个控件,运行效果如图5.8所示。



图5.8gaugeArc和gaugeCompassPan控件的外观




图5.9gaugeArc控件的部分属性


QUC SDK控件的外观几乎都可以通过属性进行修改。如果要调整gaugeArc控件指针的位置,只要修改控件的value属性即可,如图5.9所示。


在程序运行过程中,也可以在代码中使用控件的setValue()函数修改控件的指针位置,如: 




ui->gaugeArc->setValue(110);

ui->gaugeCompassPan->setValue(190);





QUC SDK并没有给出控件的文档。要全面了解控件的功能,可以查阅SDK文件夹中控件的头文件来了解控件支持的操作。




视频讲解


5.1.3窗口菜单的使用

在程序和用户之间的交互过程中,菜单是极为常用的工具。大多数软件都会在主界面设计一套菜单。像Microsoft Office更是原创性地将常规菜单升级为了Ribbon选项卡,进一步提高了用户操作的效率。Qt作为一款功能完善的开发平台,也能够方便地为窗口添加菜单。下面通过一个例子来学习Qt中菜单的使用方法(见示例代码\ch5\ch53MenuDemo\)。

1. 为窗口添加菜单

基于QMainWindow类的窗口默认带有菜单。如图5.10(a)所示,只要双击窗口左上角的“在这里输入”,便可以进入菜单编辑状态。输入“文件”两个字后按Enter键,Qt Creator会自动创建名为“文件”的菜单并展开,如图5.10(b)所示。双击“文件”菜单中的“在这里输入”可以新增菜单项,双击“添加分隔符”可以添加一个分隔符。图5.11是本例最终完成的菜单。



图5.10添加文件菜单的过程



2. 为菜单添加快捷键

使用快捷键可以大幅提高操作效率。例如,在Windows的记事本中,按Alt+F键会打开“文件”菜单,如图5.12所示。在“文件”菜单中按N键会新建文档,按X键会退出记事本。不过无论是否打开“文件”菜单,都可以通过快捷键Ctrl+N在记事本中新建一个文件。同样都是快捷键,为什么有的要打开菜单才能用,有的不需要打开菜单就能用呢?



图5.11最终完成的菜单




图5.12Windows记事本的部分快捷键




这是因为快捷键有自己的作用范围。对记事本而言,Ctrl+N是全局有效的,哪怕不打开“文件”菜单也能够使用。但是“退出”菜单项的快捷键X是局部有效的,只有在菜单打开以后才能起作用。全局快捷键和局部快捷键的显示位置是不一样的,如图5.12所示。局部快捷键紧跟菜单名称或菜单项名称,常用括号包围起来。而全局快捷键位于菜单项的右侧,通常需要同时使用2个或3个按键。

在Qt中,可以根据需要为菜单和菜单项增加不同作用范围的快捷键。

(1) 为“文件”菜单增加全局快捷键Alt+F。

要为“文件”菜单添加快捷键Alt+F,只要在“文件”二字后面添加“&F”(不含引号)。因为菜单的快捷键一般都放在括号中,所以在“文件”二字后面添加“(&F)”更符合习惯,如图5.13(a)所示。在这种情况下,菜单名会变成“文件(F)”,如图5.13(b)所示。



图5.13为“文件”菜单增加快捷键




图5.14为“退出”菜单项增加
局部有效的快捷键X


(2) 为“退出”菜单项增加局部快捷键X。

要为“退出”菜单项增加局部快捷键X,只要在“退出”两个字后面增加“(&X)”即可,如图5.14所示。


(3) 为“退出”菜单项增加全局快捷键Ctrl+X。

要为菜单项增加全局快捷键,需要使用Action Editor。Action Editor是Qt的菜单编辑器,位于Qt设计界面下方,如图5.15所示。在Action Editor中,详细列出了每个菜单项的名称、显示文字、快捷键、提示文字的信息,可以直接用鼠标和键盘修改。



图5.15Action Editor的位置


要为“退出”菜单项增加全局快捷键Ctrl+X,只需要在Action Editor中双击“退出”按钮对应的行,打开“编辑动作”对话框,如图5.16所示。单击Shortcut文本框,然后同时按键盘上的Ctrl键和X键,从而完成快捷键录入。图5.17是添加了快捷键的菜单截图。



图5.16使用Action Editor为“退出”菜单项
增加全局快捷键Ctrl+X





图5.17添加了快捷键的菜单截图






3. 为菜单增加动作

至此,菜单项已经有了快捷键。但是按这些快捷键并没有任何反应。这是因为还没有为菜单项增加对应的动作,也就是没有完成菜单项的槽函数。

要为菜单增加动作,可以在Action Editor中右击菜单项对应的行,在弹出的菜单中选择“转到槽”,如图5.18所示。在“转到槽”对话框中选择triggered()(即菜单项被触发),并单击OK按钮。系统会自动定位到菜单项的槽函数on_action_3_triggered()。因为退出按钮的功能是关闭窗口,所以只需要调用close()函数即可: 



void MainWindow::on_action_3_triggered()

{

this->close();

}








图5.18“转到槽”菜单项及对话框


再次编译、运行程序,就可以通过快捷键关闭程序了。




视频讲解


5.2实践案例: 简易气象站程序V0.2的实现
◆


下面使用本章所学知识对V0.1版简易气象站程序进行改进,包括使用QUC SDK更新程序界面,增加串口操作和数据读取功能,增加菜单和快捷键。这一版的程序代码见“示例代码\ch5\ch54SimpleWeatherStationV0.2\”。

5.2.1使用QUC SDK升级程序界面

使用QUC SDK升级程序界面的步骤相对简单,只要用新的控件替换掉默认控件即可。图5.19是升级完成的界面。相对于V0.1版程序,主要使用了QUC SDK的GaugeSimple、GaugeCompassPan、NavLabel、ImageSwitch、XSlider、LightPoint、WaveChart这几种控件,并用Tab Widget控件增加了多页面显示功能。表5.1给出了V0.2版程序中使用的QUC SDK控件的信息。界面中使用的Qt自带控件则不再赘述。




表5.1界面中使用的QUC SDK控件


序号控 件 类 型控 件 名 称序号控 件 类 型控 件 名 称


1GaugeSimplegaugeSimpleTemperature5NavLabelnavLabelPressure
2GaugeSimplegaugeSimpleHumidity6NavLabelnavLabelIllumination
3GaugeSimplegaugeSimpleWindSpeed7NavLabelnavLabelAltitude
4GaugeCompassPangaugeCompassPanWindDirection8ImageSwitchimageSwitchAlarm



续表




序号控 件 类 型控 件 名 称序号控 件 类 型控 件 名 称


9ImageSwitchimageSwitchHTTP14LightPointlightPoint
10ImageSwitchimageSwitchTCP15WaveChartwaveChartTemperature
11XSliderxsliderWindSpeedLimit16WaveChartwaveChartHumidity
12XSliderxsliderTemperatureLimit17WaveChartwaveChartWindSpeed
13XSliderxsliderIlluminationLimit18WaveChartwaveChartIllumination




图5.19使用QUC SDK更新后的界面




图5.19(续)


5.2.2串口操作功能的实现

在V0.1版程序中已经设计好了串口操作区的界面。下面利用本章所学的知识完成这部分的功能。

1. 串口信息的读取和显示

在这一版程序中为主窗口类增加了读取串口信息的函数updateSerialInfo()。在主窗口的构造函数中调用该函数,从而在启动后立即读取串口信息。在后续章节中,还会对该函数做进一步改进,从而实现实时更新串口信息的功能。updateSerialInfo()函数的代码如下: 



void MainWindow::updateSerialInfo()

{

ui->comboBoxUart1->clear();

ui->comboBoxUart2->clear();







printLog("检测到串口信息:");



foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts())

{

ui->comboBoxUart1->addItem(info.portName());

ui->comboBoxUart2->addItem(info.portName());

printLog(info.portName(), info.description());

}



ui->comboBoxUart1->model()->sort(0);//对串口列表进行排序

ui->comboBoxUart2->model()->sort(0);

}





图5.20是程序运行后自动检测并显示的串口信息。



图5.20程序运行后自动检测并显示的串口信息


2. 定义串口类对象

在进行串口操作前,首先为主窗口类增加两个QSerialPort类对象,分别对应气象信息串口和风速风向串口: 



private:

QSerialPort *m_serialWeather;

QSerialPort *m_serialWind;





同时在构造函数中为两个对象申请内存: 



m_serialWeather = new QSerialPort();

m_serialWind = new QSerialPort();





3. 打开和关闭串口

程序使用两个按钮分别控制两个串口的打开和关闭,并用主窗口类的成员变量m_nSerialWeatherOpenedFlag和m_nSerialWindOpenedFlag表示串口的打开状态。“打开串口1”按钮(控件名为pushButtonOpenUart1)的槽函数代码如下: 



1void MainWindow::on_pushButtonOpenUart1_clicked()

2{







3if (m_nSerialWeatherOpenedFlag == 0)

4{

5m_serialWeather->setPortName(ui->comboBoxUart1->currentText()); 

6m_serialWeather->setBaudRate(ui->lineEditBaudRate1->text().toInt()); 

7m_serialWeather->setDataBits(QSerialPort::Data8); 

8m_serialWeather->setParity(QSerialPort::NoParity); 

9m_serialWeather->setStopBits(QSerialPort::OneStop); 

10m_serialWeather->setFlowControl(QSerialPort::NoFlowControl); 

11m_serialWeather->open(QIODevice::ReadWrite); 

12

13if (m_serialWeather->isOpen()) 

14{

15printLog("串口1已打开", ui->comboBoxUart1->currentText()); 

16ui->pushButtonOpenUart1->setText("关闭串口1"); 

17m_nSerialWeatherOpenedFlag = 1; 

18} 

19else 

20{

21printLog("串口1打开失败"); 

22} 

23} 

24else 

25{

26printLog("串口1已关闭"); 

27ui->pushButtonOpenUart1->setText("打开串口1"); 

28m_nSerialWeatherOpenedFlag = 0; 

29m_serialWeather->close(); 

30} 

31} 

32}





在上述代码中,首先通过标志位m_nSerialWeatherOpenedFlag判断串口的打开状态(第3行)。如果未打开,则进行打开操作(第5~11行),并判断打开是否成功(第13~22行)。如果已打开,则关闭串口(第26~29行)。

“打开串口2”按钮(控件名为pushButtonOpenUart2)的槽函数与上述代码几乎完全相同,此处不再赘述。

5.2.3GY39模块的数据读取和处理

要想正确获取GY39模块测量的气象信息,需要经过串口数据读取、数据完整性校验、数据解析3个步骤。在程序中,为ClassGY39类增添了3个成员函数readSerialData()、verifySerialData()、parseSerialData()分别实现这些功能。

1. 串口数据读取

readSerialData()函数用于读取串口数据,并对读取的数据进行初步处理。为了让它能配合不同的串口进行工作,将串口类对象的指针作为形参传入。该函数的代码如下: 



1int ClassGY39::readSerialData(QSerialPort *serialPort)

2{

3QByteArray qbaWeatherData = serialPort->readAll();

4if (qbaWeatherData.length() % 24 != 0 || qbaWeatherData.length() == 0)

//检查数据长度

5{

6return -1; //数据长度不正确

7}

8qbaWeatherData = qbaWeatherData.right(24);//取最后一组数据

9

10if (verifySerialData(qbaWeatherData) != 0)//校验数据

11{

12return -2; //数据校验错误

13}

14

15parseSerialData(qbaWeatherData);//解析数据

16return 0;

17}





GY39模块每秒报告一次气象信息,每组气象信息的长度为24字节。由于读取气象信息的频率不确定,因此串口缓冲区内可能会同时包含多组气象信息。为了获取其中最新的一组信息,首先要判断接收到的数据是否是24字节的整数倍(第4行),然后取出最后24字节进行后续处理(第8行)。第10行调用函数verifySerialData()对读取的数据进行校验。如果校验结果正确,则调用函数parseSerialData()进行数据解析(第15行)。

2. 数据完整性校验

GY39模块的数据
校验的步骤是将所有的数据相加,取结果的低8位作为校验位。在下列代码中使用foreach语句分别对光照强度数据以及温度和湿度数据进行了校验。



int ClassGY39::verifySerialData(QByteArray qbaSerialData)

{

unsigned int nSum = 0;//保存求和结果

foreach (char cTmp, qbaSerialData.left(8))//开始校验光照强度数据

{

nSum += cTmp;//进行累加操作

}



if ((nSum % 256) != (unsigned char)qbaSerialData.at(8))//求和结果是否等于校验位

{

return -1; //不等于则返回-1,等于则继续

}



nSum = 0;//求和结果清零

foreach (char cTmp, qbaSerialData.right(15).left(14))//开始校验气象数据

{

nSum += cTmp;//进行累加操作








}


if ((nSum % 256) != (unsigned char)qbaSerialData.at(23)) //求和结果是否等于校验位

{

return -2; //不等于则返回-2,等于则继续

}

return 0;

}





3. 数据解析

parseSerialData()函数负责将原始数据解析成具体的测量结果(计算公式见2.2.4节)。需要特别注意的是,读取的串口数据需要存储在QByteArray变量中,但是QByteArray会将其中的每个元素都作为有符号数处理。在某些特定的情况下,计算结果会出现问题。以下列原始测量数据为例(下画线标出的是照度数据)。



5A 5A 15 04 00 2E 12 EC F9 5A 5A 45 0A 0B 58 00 9A 9E 4E 14 13 00 00 13





由于照度的测量结果总是非负的(气压、湿度等亦同),因此数据的每一字节都代表了一个正数。正常情况下,实际的照度值应该为: 


(0x0024)+(0x2E16)+(0x128)+0xEC
=(00000000B24)+(00101110B16)+(00010010B8)+11101100B
=0+3014656+4608+236
=3019500


但是QByteArray会将每一字节都作为有符号数处理。这就会导致计算结果变为(带下画线的是QByteArray误认的符号位): 


(0x0024)+(0x2E16)+(0x128)+0xEC
=(0000 0000B24)+(0010 1110B16)+(0001 0010B8)+1110 1100B
=0+3 014 656+4 608-20
=3 019 244≠3 019 500


要解决这一问题,可以将QByteArray转换为C语言中常用的unsigned char类型进行计算。

但是对于温度、海拔高度数据,情况又有所不同。这两个数据的测量结果既可以是正数,也可以是负数。所以这两个数据的原始测量结果的最高字节是有符号的,其余字节是无符号的。在计算数据的过程中,不同字节应当按不同的方式来计算。

按照上面的思路,可以得到parseSerialData()的代码: 



int ClassGY39::parseSerialData(QByteArray qbaSerialData)

{

unsigned char *cData = (unsigned char *)qbaSerialData.data();//转换为无符号数组







int nIllumi = (cData[4] << 24) + (cData[5] << 16) + (cData[6] << 8) + cData[7];

//光照数据恒正,采用无符号的数据进行计算

float fTemp = ((qbaSerialData[13] << 8) + cData[14]) / 100.0;

//温度数据高字节采用有符号的数据计算,低字节采用无符号的数据计算

float fPre = ((cData[15] << 24) + (cData[16] << 16) + (cData[17] << 8) + cData[18]) / 100.0 / 1000.0;

int nHum = ((cData[19] << 8) + cData[20]) / 100.0;

int nAlti = (qbaSerialData[21] << 8) + cData[22];



setIllumination(nIllumi);

setTemperature(fTemp);

setPressure(fPre);

setHumidity(nHum);

setAltitude(nAlti);



return 0;

}





5.2.4PR3000模块的数据读取和处理

PR3000模块的情况与GY39模块的情况稍有不同。由于PR3000模块的数据读取流程较为复杂,涉及Modbus协议问询帧和应答帧的交替处理,因此只为ClassPR3000类增加了两个成员函数,即负责数据读取和解析的readSerialData()和负责CRC16校验的crc16Verify()。

1. 串口数据读取和解析

首先为ClassPR3000类增加两个QByteArray类型的成员变量: m_qbaRequestWS和m_qbaRequestWD,分别用于存储风速和风向问询帧: 



private:

QByteArray m_qbaRequestWS = QByteArray::fromHex("010300000001840A");

QByteArray m_qbaRequestWD = QByteArray::fromHex("020300000002C438");





然后完成函数readSerialData(): 



1int ClassPR3000::readSerialData(QSerialPort *serialPort)

2{

3QEventLoop eventLoop; //定义事件循环

4

5serialPort->write(m_qbaRequestWS); //风速部分发送问询帧

6QTimer::singleShot(200, &eventLoop, SLOT(quit()));

7eventLoop.exec();

8QByteArray qbaWSData = serialPort->readAll();

9

10if (0 != crc16Verify(qbaWSData.left(5), qbaWSData.right(2)))

11{







12return -1;

13}

14

15float fWindSpeed = (qbaWSData.at(3) * 256 + qbaWSData.at(4)) / 10.0;

16setWindSpeed(fWindSpeed);

17

18//风向部分与上述风速部分类似,此处略去

19return 0;

20}





因为该函数涉及Modbus协议的通信,所以流程较为复杂。代码第5行发送了风速模块的问询帧。由于模块在执行指令时需要一定的时间,因此在第6行和第7行添加了一个EventLoop(事件循环)。通过事件循环可以临时阻塞当前程序,并在满足一定条件后继续运行程序。关于事件循环的细节将在第6章讲解。当风速模块返回了风速信息后,事件循环自动退出,并运行readAll()函数读取缓冲区内容(第8行)。代码的第10~13行是对风速信息进行CRC16校验。第15行和第16行对校验后的数据进行解析。风向部分的代码与风速部分的代码十分类似,可以参阅示例代码。

2. CRC校验的实现

CRC算法的思路在第1章中已经做了介绍。在实际应用中,CRC16的实现有查表法和实时计算法两种。查表法是提前制作好CRC16的参考表格,校验时根据数据和参考表格经过简单计算迅速得到结果。实时计算法则是严格按照CRC16的算法,根据实际数据进行计算。以下是CRC16实时计算法的代码: 



int ClassPR3000::crc16Verify(QByteArray qbaData, QByteArray qbaCheckSum)

{

quint16 data8, crc16 = 0xFFFF;



for (int i = 0; i < qbaData.size(); i++)

{

data8 = qbaData.at(i) & 0x00FF;

crc16 ^= data8;

for (int j = 0; j < 8; j++)

{

if (crc16 & 0x0001)

{

crc16 >>= 1;

crc16 ^= 0xA001;

}

else

{

crc16 >>= 1;

}

}

}







crc16 = (crc16 >> 8) + (crc16 << 8);



if ((crc16 / 256) == (unsigned char)qbaCheckSum.at(0))

{

if ((crc16 % 256) == (unsigned char)qbaCheckSum.at(1))

{

return 0;

}

}

return 1;

}





5.2.5界面更新函数的进一步修改

在第4章中已经实现了界面更新函数updateUI()。本章因为更换了控件,所以需要对该函数进行更新。更新后的代码如下: 



1void MainWindow::updateUI()

2{

3ui->gaugeSimpleHumidity->setValue(m_GY39Device->getHumidity()); 

4ui->gaugeSimpleTemperature->setValue(m_GY39Device->getTemperature());

5ui->gaugeSimpleWindSpeed->setValue(m_PR3000Device->getWindSpeed());

6ui->gaugeCompassPanWindDirection->setValue(m_PR3000Device->getWindDirection());

7

8ui->navLabelPressure->setText(QString::number(m_GY39Device->getPressure(), 'f', 3));

9ui->navLabelIllumination->setText(QString::number(m_GY39Device->getIllumination()));

10ui->navLabelAltitude->setText(QString::number(m_GY39Device->getAltitude()));

11

12ui->waveChartTemperature->addData(m_GY39Device->getTemperature());

13ui->waveChartHumidity->addData(m_GY39Device->getHumidity());

14ui->waveChartIllumination->addData(m_GY39Device->getIllumination() / 1000);

15ui->waveChartWindSpeed->addData(m_PR3000Device->getWindSpeed());

16}





在第8行中,由于气压以Pa为单位存放在变量中,但是在显示过程中希望以kPa为单位进行显示,因此按照浮点数的形式进行转换,并保留3位小数。历史记录页面中的控件类型为WaveChart。调用该类控件的addData()函数可以将数据添加到控件中(第12~15行)。


5.2.6手动读取数据的实现

单击“读取测量数据”按钮后,程序会读取硬件模块的数据并显示在界面上,还会根据报警开关的状态确定是否开启报警功能。要实现这些功能,需要在按钮的槽函数中加入如下内容: 



1void MainWindow::on_pushButtonGetHardwareData_clicked()

2{

3int nGY39DataValidFlag = -1, nPR3000DataValidFlag = -1;

4if (m_nSerialWeatherOpenedFlag == 1)

5{

6nGY39DataValidFlag = m_GY39Device->readSerialData(m_serialWeather);

7}

8

9if (m_nSerialWindOpenedFlag == 1)

10{

11nPR3000DataValidFlag = m_PR3000Device->readSerialData(m_serialWind);

12}

13

14if ((nGY39DataValidFlag == 0) || (nPR3000DataValidFlag == 0))

15{

16updateUI();

17if (ui->imageSwitchAlarm->getChecked())

18{

19alarm();

20}

21}





在该代码中,第6行和第11行依次读取GY39模块和PR3000模块的数据,并使用变量nGY39DataValidFlag和nPR3000DataValidFlag记录数据是否有效。如果有效,则变量的取值为0。只要有一个模块的数据读取成功,程序就可以更新界面(第16行)。紧接着程序会判断报警功能是否启用(第17行)。如果启用,则调用alarm()函数进行判断和报警(第19行)。

5.2.7菜单功能的实现

虽然V0.2版的程序的功能相对简单,但是仍设计了菜单和快捷键,如图5.21所示。



图5.21程序菜单的内容


在所有的菜单项中,本章可以实现的只有“文件”菜单的“退出”、“数据”菜单的“读取测量数据”、“关于”菜单的“关于”。因为退出功能在前面已经介绍过了,所以此处主要讲解“读取测量数据”菜单项和“关于”菜单项的实现。

在Action Editor中将“读取测量数据”菜单项命名为menuGetHardwareData。由于“读取测量数据”菜单项的功能与“读取测量数据”按钮的功能相同,因此可以在菜单项的槽函数中直接调用“读取测量数据”按钮的槽函数,即



void MainWindow::on_menuGetHardwareData_triggered()

{

on_pushButtonGetHardwareData_clicked();

}





按照习惯,单击“关于”菜单项后会弹出一个介绍程序相关信息的对话框。本例使用Qt消息对话框类QMessageBox的静态函数information()显示一个简易的信息(information)对话框。要实现这一功能,首先要引用头文件: 



#include <QMessageBox>





然后将“关于”菜单项的槽函数修改为: 



void MainWindow::on_menuAbout_triggered()

{

QMessageBox::information(this, "关于","简易气象站V0.2\r\n——金陵科技学院电子信息工程学院");

}





information()函数的第一个参数是父窗体的指针,第二个参数是对话框的标题,第三个参数是对话框的内容。图5.22是“关于”对话框的运行结果。



图5.22气象站程序的“关于”对话框


Qt提供了5种不同类型的对话框。除了上面使用的信息对话框外,还有about(关于)、question(询问)、warning(警告)、critical(关键错误)对话框。这几种对话框的用法大致相同,读者可以顺次尝试。

5.3程序运行结果
◆

本章主要完成了界面的更新和串口数据的读取、处理。下面是这部分功能的测试结果。

(1) 图5.23是程序启动后的界面。程序启动后首先检测计算机的串口,并将串口信息输出在日志区域和串口列表中。在本例中,程序共检测到了4个串口信息,其中有两个来自于USB转接板。此外,程序启动后界面中的各个控件显示默认值,警告图标为绿色。



图5.23程序启动后的界面


(2) 图5.24是打开串口并读取一组数据后,程序的运行结果。选择串口号并打开串口后,程序首先会在日志区域输出打开串口的结果。结果中的“已打开”代表打开成功。单击“读取测量数据”按钮后,程序会通过硬件读取数据并显示在日志区域和控件中。由于报警功能处于关闭状态,虽然风速、温度、照度等数据均超过了限值,但是程序不会报警。



(3) 打开报警功能并重新读取一组测量数据,如图5.25所示。因为风速和照度值均超过了限值,所以程序开始报警。报警的现象与第4章相同,包括红色闪烁的图标和日志文字提示。



图5.24读取并显示一组测量数据



图5.25测量结果超过限值时,程序报警



(4) 切换到历史数据界面,重复进行几次测量,程序自动显示历史记录曲线,如图5.26所示。需要注意的是,QUC SDK无法自动调整纵坐标的显示范围,只能按照事先设定好的范围显示。对于温度这种变化缓慢的数据,其图形经常呈一条直线。要解决这一问题,可以使用变量记录下数据的最大值和最小值,然后调用控件的函数调整纵坐标范围。



图5.26历史数据界面


5.4本章小结
◆


本章介绍了Qt中串口操作的方法、QUC SDK的安装和使用方法、菜单的使用方法3部分内容,同时利用这些知识完成了简易气象站V0.2版的程序,实现了读取硬件数据的功能。在学习了本章的内容后,还可以学习串口蓝牙、串口WiFi等模块的使用方法,并通过Qt编写程序控制这些模块; 也可以试着动手编写一个简单的串口调试助手软件; 如果有绘制图形、曲线的需要,也可以学习QCustomPlot、QWT等库的使用。

扩展阅读: 阿里巴巴——中国重要的开源参与者
◆


近几年,开源在国内异常火热,其全称为开放源代码,最大的特点是开放。任何人都可以得到软件的源代码,并可以修改、学习甚至重新发布代码。百度、阿里巴巴、华为、腾讯、浪潮等公司均是我国重要的开源软件贡献者。

早在2010年时,阿里巴巴的工程师们便在杭州开源了第一个项目——Dubbo。这是一款高性能、轻量级的开源服务框架,可提供面向接口代理的高性能RPC调用、智能容错和负载均衡、服务自动注册和发现。之后几年,阿里巴巴又相继开源了Fastjson、Druid、Sea.js、Arale等项目。

2017年9月,阿里巴巴发起了OpenMessaging项目。这一项目也正式入驻Linux基金会,成为国内首个在全球范围发起的分布式计算领域的国际标准。在随后的一年里,OpenMessaging开源标准社区又吸引了十余家企业的参与,获得了RocketMQ、RabbitMQ和Pulsar等3个消息开源厂商的支持。

迄今为止,阿里巴巴开源项目数已超过2700个,覆盖大数据、云、AI、数据库、中间件、硬件等多个领域。这些开源的项目收获了超过一百万颗星(Star),参与贡献的开发人员达到几万人。阿里巴巴已成为十多个国内外开源基金会重要成员,包括CNCF、MariaDB基金会白金会员。

阿里巴巴开源技术委员会负责人曾说过,“各种成就的背后,离不开每一个开发者的耕耘和创造。我们经常发现,当各种喧嚣归于平静,当各种繁华归于平淡,我们的工程师们依然不变初心,追求着自己的梦想: 通过代码这一种最直接的语言,通过开源这一种最简单的方式,寻找着技术路上的下一个突破点,寻找着技术对于社会创造的更多价值。开源是开发者最大的同心圆,未来,我们希望与更多开源人一起,用技术普惠世界。”