利用多线程,但比之前更专业!子线程负责串口和图片文件的读写操作,主线程负责UI界面的更新!!!
main.cpp
#include "mywidget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MyWidget w;
w.show();
return a.exec();
}
mywidget.h
#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QWidget>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QThread>
//包含自定义类的头文件
#include "mythread.h"
namespace Ui {
class MyWidget;
}
class MyWidget : public QWidget
{
Q_OBJECT
public:
explicit MyWidget(QWidget *parent = 0);
~MyWidget();
void detectSerial();//探测系统可用的串口列表
void serialDisplay(QByteArray tmp);//显示串口数据(如何显示、显示什么自己定义)
//处理关闭窗口/应用程序的操作
//主要是为了结束子线程
void dealClose();
private slots:
//串口选择下拉框槽函数
void on_comboBox_SerialNum_currentIndexChanged(const QString &arg1);
private:
Ui::MyWidget *ui;
//7. 涉及到子线程的成员尽量声明成指针类型
//如果声明成对象,在主线程构造函数初始化的时候默认把它当做主线程对象
//从而产生跨线程调用对象的错误
MyThread *myT;//自定义对象指针--将要放入子线程
QThread *thread;//子线程--负责串口数据的读取
signals:
void initUart(QSerialPortInfo info);//发送给子线程的串口初始化信号
};
#endif // MYWIDGET_H
mywidget.cpp
#include "mywidget.h"
#include "ui_mywidget.h"
MyWidget::MyWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::MyWidget)
{
//对于子线程的东西(将被移入子线程的自定义对象以及线程对象),最好定义为指针
myT = new MyThread;//将被子线程处理的自定义对象不能在主线程初始化的时候指定父对象
thread = new QThread(this);//初始化子线程线程
myT->moveToThread(thread);//将自定义对象移交给子线程,从此子线程控制他的成员函数
//启动子线程,但是没有启动真正的子线程处理函数,
//只是让子线程对象开始监控移交给他的相关对象
thread->start();
//绑定/连接关闭应用程序窗口的信号和主线程的dealClose槽函数
connect(this, &MyWidget::destroyed, this, &MyWidget::dealClose);
ui->setupUi(this);//绘制界面
detectSerial();//探测当前系统可用的串口列表
}
//析构函数
MyWidget::~MyWidget()
{
delete ui;
}
void MyWidget::dealClose()
{
if(thread->isRunning() == false)
{
return;
}
//2.如果调用的是子线程的函数(对象已被放入子线程,其成员函数均在子线程)
//需要在子线程退出的之前调用
myT->setFlag(true);//更新子线程的isStop标志--结束子线程的处理函数
//3.退出子线程要显示的调用这两个函数,否则主线程退出但子线程还在运行
thread->quit();
//回收资源
thread->wait();
//4. 将要被放入子线程的对象在主线程初始化(构造)的时候不能指定父对象,且需要在子线程结束以后显示delete
delete myT;
}
void MyWidget::detectSerial()
{
//1. 绑定信号和曹的时候,如果带参数,在QT5中可以直接给出信号和槽函数名即可
//但是,如果所传递的参数类型是未注册(非本地默认识别的可传递类型)的,需要在绑定之前进行注册
qRegisterMetaType<QSerialPortInfo>("QSerialPortInfo");
//连接子线程的isDone信号到主线程的serialDisplay槽函数,显示串口接收到的数据
connect(myT,&MyThread::isDone,this,&MyWidget::serialDisplay);
//连接主线程的initUart信号到子线程的initSerial槽函数,开始串口初始化
connect(this,&MyWidget::initUart,myT,&MyThread::initSerial);
//获取可用串口列表
QList<QSerialPortInfo> infos = QSerialPortInfo::availablePorts();
if(infos.isEmpty())//系统无可用串口
{
ui->comboBox_SerialNum->addItem("无效");//在串口选择下拉框显示“无效”
return;
}
ui->comboBox_SerialNum->addItem("串口");//如果有可用串口则在串口选择下拉框显示“串口”
//将每个可用串口号作为一个条目添加到串口选择下拉框
foreach (QSerialPortInfo info, infos) {
ui->comboBox_SerialNum->addItem(info.portName());
}
}
//处理串口选择下拉框被点击
void MyWidget::on_comboBox_SerialNum_currentIndexChanged(const QString &arg1)
{
QSerialPortInfo info;
QList<QSerialPortInfo> infos = QSerialPortInfo::availablePorts();//获取可用串口列表
//遍历链表
int i = 0;
foreach (info, infos) {
//如果在下拉框里选择的串口在系统可用串口链表里找到就跳出循环--表示能够操作该串口
if(info.portName() == arg1) break;
i++;
}
if(i != infos.size ()){//can find----没有遍历到可用串口链表尾部
ui->label->setText("[已开启]");
emit initUart(info);//发送串口初始化信号
}
else
{
ui->label->setText("[出错]");//没有找到可用串口
}
}
//将包头数据显示到UI的文本编辑框--前提是接收到来自子线程的isDone信号
void MyWidget::serialDisplay(QByteArray tmp)
{
//操作头部数据
QString str_display;
QString str = tmp.toHex().data();//转换为16进制,再转为char*
str = str.toUpper();//转换为大写形式
str_display.clear();
for(int i = 0;i < str.length();i+= 2)
{
QString st = str.mid(i,2);//取出16进制的char*中的两个字符(就是一个完整的16进制数字)
str_display += st;//累加到str_display
str_display += " ";//相邻两个16进制数据之间添加一个空格,方便显示
}
//在接受文本编辑框接显示收到的包头数据
ui->textEdit_Recv->insertPlainText(str_display);
}
mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QDebug>
#include <QFile>
#include <QDataStream>
#include <QByteArray>
#include <QThread>
//一个包的长度信息
#define REAL_LEN 64//有效数据最大长度
#define HDR_LEN 8//包头长度(固定不变)
//处理照片数据的状态标志(目前没用到--留待以后扩展)
//照片数据分为三部分(begin + 有效数据 +end)
#define BEGIN 0x01//开始处理照片--接收到含有begin的包
#define END 0x02//照片数据接收完毕--接收到含有end的包
#define READING 0x03//正在处理照片数据--照片的实际有效数据
//读串口的状态标志--每个包分为两部分读取(包头+有效数据)
#define READ_HDR 0x01//读取包头
#define READ_DATA 0x02//读取有效数据
class MyThread : public QObject
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = 0);
void initSerial(QSerialPortInfo info);//串口初始化
void readSerial();//子线程真正的处理函数--在没有结束程序的时候是死循环
void setFlag(bool flag);//更新isStop标志
//读取指定长度的数据,但是返回的是本次读取到的实际字节数
//实际读取的字节数可能比length小
int readFrameData(unsigned int length);
void handleHead(); //处理包头数据
void handlePhoto();//处理图片的有效数据
signals:
void isDone(QByteArray tmp);//接收到完整的包头以后给主线程发送的信号--携带包头数据
public slots:
private:
bool isStop;//控制子线程的处理函数是否结束
//8. IODevice及其子类对象都不应该跨线程调用
//所以子线程声明/初始化该类对象的时候需要显示指定其父对象
//通常其父对象就是将要被放入子线程的对象,所以只需要在构造函数中将其父对象指定为this指针即可
QSerialPort *serial;//串口对象指针
QFile dst;//目标文件--图片文件
unsigned int cnt_read;//总的已读取的字节数(只针对完整读取指定长度的数据包)
unsigned int cnt_need;//还需要的字节数(只针对完整读取指定长度的数据包)
unsigned int data_need;//每一个包的有效数据的总长度
unsigned int cnt;//串口接收到的所有有效数据的字节数(针对读取图片的整个过程)
QByteArray read_data;//总的已读取的数据(只针对完整读取指定长度的数据包)
//缓冲区,当读取的有效数据累计满32*64的时候在进行写入文件操作,减少写文件的次数,以提高速度
QByteArray my_stream;
int photo_state;//读取图片的操作状态(暂时没用到)--对应BEGIN/READING/END
int serial_state;//读取串口的状态(读取包头/读取有效数据--对应READ_HDR/READ_DATA)
//为了以后扩展系统--一个协调器可能接收来自多个终端节点的数据
int node_type;//节点类型(判断当前数据包是哪一种类型的节点发过来的)
int node_id;//节点序号(同类节点有可能有多个)
unsigned int seqnb;//当前数据包的序号
unsigned int pkt_cnt;//读取到图片有效数据的包的个数--满32个就进行一次写入操作
bool FAIL_FLAG;//读取的数据包是否发生丢失或者重复
};
#endif // MYTHREAD_H
mythread.cpp
#include "mythread.h"
#include <QDateTime>
MyThread::MyThread(QObject *parent) : QObject(parent)
{
//记录读取状态相关变量的初始化
cnt_read = 0;
cnt_need = 0;
photo_state = BEGIN;//默认状态是开始读取图片(暂时没用到)
serial_state = READ_HDR;//默认状态是读取包头的状态
data_need = 0;
node_id = 0;
node_type = 0;
cnt = 0;
seqnb = 0;
FAIL_FLAG = false;//默认没有失败
pkt_cnt = 0;
isStop = false;//默认是false--不退出子线程处理函数
}
void MyThread::initSerial(QSerialPortInfo info)
{
serial = new QSerialPort(this);
if(serial->isOpen())//先关闭
serial->close();
serial->setPort(info);//设置串口号--就是从下拉框选择的串口号
serial->open(QIODevice::ReadWrite); //读写打开
serial->setBaudRate(QSerialPort::Baud115200); //波特率
serial->setDataBits(QSerialPort::Data8); //数据位
serial->setParity(QSerialPort::NoParity); //无奇偶校验
serial->setStopBits(QSerialPort::OneStop); //停止位
serial->setFlowControl(QSerialPort::NoFlowControl); //无控制
readSerial();//实际意义的子线程处理函数
}
//将QByteArray类型的数据转换为int类型
int bytesToInt(QByteArray bytes) {
int addr = bytes[0] & 0x000000FF;
addr |= ((bytes[1] << 8) & 0x0000FF00);
addr |= ((bytes[2] << 16) & 0x00FF0000);
addr |= ((bytes[3] << 24) & 0xFF000000);
return addr;
}
//读取指定长度串口数据(实际不一定能读取指定的长度)
//实际读取到的字节数 小于等于 给定的长度
int MyThread::readFrameData(unsigned int length)
{
QByteArray tmp_data;//存放本次读取到的临时数据
int cnt_tmp;//存放本次读取到的临时长度
cnt_need = length - cnt_read;//更新当前还需要读取的字节数
tmp_data = serial->read(cnt_need);//本次读取的数据
if(tmp_data.isEmpty())//如果读取空内容直接返回-1表示读取错误
return -1;
cnt_tmp = tmp_data.length();//获取本次读取到的字节数
cnt_read += cnt_tmp;//已经读取的总字节数(只针对读取一个指定长度的数据包)
read_data += tmp_data;//已经读取的数据(只针对读取一个指定长度的数据包)
return cnt_tmp;//返回本次读取的实际字节数
}
//处理包头
void MyThread::handleHead()
{
static unsigned int seq_old = 0;//记录上一次读取到的数据包的序号
//能够执行这个函数说明已经读完了指定包头长度的数据(表示读完了包头),要进行第二部分指定长度的数据读取了(有效数据的读取)
//将记录读取状态的相关变量清零
cnt_read = 0;
cnt_need = 0;
//读完头部数据下一个状态应该是读取有效数据--切换状态
serial_state = READ_DATA;
//获取包头里的有用信息
node_type = bytesToInt(read_data.mid(0,1));//节点类型
node_id = bytesToInt(read_data.mid(1,1));//节点id
data_need = bytesToInt(read_data.mid(7,1));//有效数据的长度--很重要
seqnb = bytesToInt(read_data.mid(6,1))*256 + bytesToInt(read_data.mid(5,1));//该数据包的序号--也很重要
//如果该数据包序号和上一个数据包序号相等--读重复了
if(seqnb == seq_old)
{
FAIL_FLAG = true;//重复--读失败的标志置位--该数据包将不会写入图片文件
qDebug()<<"seqnb err..********************************";
}
else if(seqnb - seq_old > 1)//如果该数据包序号比上一个数据包序号大于等于2,表示中间丢了数据包
{
//虽然读取失败了,但是该数据包是需要写入图片文件的,所以失败标志复位--要写入该数据包到图片文件
FAIL_FLAG = false;
//计算丢失的数据包个数
int need_pkt = seqnb - seq_old;
QByteArray errData(64*(need_pkt-1),0);//构造一个和丢失的所有数据包总和一样大、全0的数据包
my_stream += errData;//将上述构造的数据包加到缓冲区
dst.write(my_stream);//将缓冲区所有数据写入图片文件
my_stream.clear(); //清空缓冲区
pkt_cnt = 0;//记录缓冲区是否满32*64的变量清零--表示重新计数(此时缓冲区已经没有数据了)
}
else {//既没有重复读取也没有丢失数据包
FAIL_FLAG = false;
}
seq_old = seqnb;//更新旧的数据包序号
//发送信号给主线程--更新UI界面(携带包头数据)
emit isDone(read_data);
//清空读取到的数据(只针对读取一个指定长度的数据包)
read_data.clear();
}
//处理图片实际数据
void MyThread::handlePhoto()
{
//能够执行这个函数说明已经读完了一个数据包的效数据,要进行下一个数据包的数据读取了(又要从包头开始读取)
//记录读取状态的变量清零--以便下一个指定长度的读取
cnt_read = 0;
cnt_need = 0;
serial_state = READ_HDR;//更新串口读取状态--立马要读取下一个数据包的包头了
//if(photo_state == END && strcmp("end\r\n",str_tmp) == 0)
//if(strcmp("end",str_tmp) == 0)
if(read_data.contains("end"))//如果读取到的有效数据包含end--表示读取到图片结束
{
if(dst.isOpen())//关闭图片文件
dst.close();
//打印调试信息
qDebug()<<"end";
qDebug()<<cnt;
//清空相关缓冲区和--准备读取下一个数据包了
//(只针对读取一个指定长度的数据包)已读取的数据清空
read_data.clear();
//缓冲区清空
my_stream.clear();
photo_state = BEGIN;//更新图片的读取状态(暂时没用到)
data_need = 0;//图片的有效数据长度清零
cnt = 0;//读取到的所有有效数据清零
return ;//直接返回--后面的没必要再执行了
}
//if(photo_state == BEGIN && strcmp("begin\r\n",str_tmp) == 0)
//if(strcmp("begin\r\n",str_tmp) == 0)
if(read_data.contains("begin"))//如果读取到的有效数据包含begin--表示准备开始读取图片
{
//将系统当前时间作为图片的文件名
QDateTime time = QDateTime::currentDateTime();//获取系统现在的时间
//设置显示格式,注意QFile的文件名不能有:(冒号)等特殊字符
QString str = time.toString("yyyy-MM-dd-hh-mm-ss");
str += ".jpg";
qDebug()<<str;
if(dst.isOpen())
dst.close();
dst.setFileName(str);//将时间作为文件名
//QDir::currentPath()
bool isOK = dst.open(QIODevice::WriteOnly|QIODevice::Append); //打开文件
if(isOK == false)//打开失败
{
qDebug()<<"dst.open err";
// //this->close();
}
//清空相关缓冲区和--准备读取下一个数据包了
read_data.clear();
data_need = 0;
photo_state = READING;//更新图片读取状态(暂时没用到)
return ;//直接返回--后面的没必要再执行了
}
//if(photo_state == READING)//读取图片的实际有效数据
{
cnt += read_data.length();//累加图片的有效数据
//如果重复了--不会写入当前数据包到图片文件
if(FAIL_FLAG == true)
{
//清空已读取到的数据(但不清空缓冲区--实际不重复的有效数据没有满32*64)--准备读取下一个数据包的数据
read_data.clear();
data_need = 0;//有效数据长度清零
return;//直接返回
}
// //qDebug()<<"==========>"<<seqnb;
pkt_cnt++;//记录不重复的有效数据包的个数
my_stream += read_data;//不重复的有效数据包累加到缓冲区
//如果当前数据包的有效长度小于给定的有效数据包的长度的宏定义--表示是最后一个数据包
if(data_need < REAL_LEN)
{
photo_state = END;//更新图片读取状态(暂时没用到)
dst.write(my_stream);//将当前缓冲区的数据写入图片文件
my_stream.clear();//清空缓冲区
pkt_cnt = 0;//记录不重复有效数据包的个数清零
}
//还没有读取完毕,但是缓冲区已经满了32*64(这里的64是一个数据包里有效数据的最大长度--对应宏定义REAL_LEN)
if(pkt_cnt == 32)
{
pkt_cnt = 0;//记录不重复有效数据包的个数清零
dst.write(my_stream);//将当前缓冲区的数据写入图片文件
my_stream.clear();//清空缓冲区
}
//已读取的数据清空(只针对读取一个指定长度的数据包)
read_data.clear();
data_need = 0;//图片的有效数据长度清零
}
}
//读取串口数据--真正子线程处理函数
void MyThread::readSerial()
{
int ret = 0;//记录每次实际读取的字节数
while(!isStop)
{
//while(this->serial->waitForReadyRead(10))
//一定要调用这个函数,否则串口不会发出readyRead信号(或者说即使发了也没有去捕获),也就什么都不能读取了
if(this->serial->waitForReadyRead(10) == false)
qDebug() << "子线程号:========================"<< QThread::currentThread() ;
if(serial->bytesAvailable() >= 1)//有可读数据再去读
{
switch(serial_state)
{
case READ_HDR://读包头
ret = readFrameData(HDR_LEN);//目标是读取HDR_LEN个字节的数据
if(ret == -1)//读取错误
{
return;
}
if(HDR_LEN == cnt_read)//已读取到的字节数和目标长度相等
handleHead();//处理头部数据
break;
case READ_DATA://读有效数据
if(node_type == 1)//如果是图像节点的数据(不再判断节点--目前只有一个节点的数据)
{
ret = readFrameData(data_need);//读取有效长度(从包头获取)的数据
if(ret == -1)//读取错误
{
return;
}
if(data_need == cnt_read)//已读取到的字节数和目标长度相等
handlePhoto();//处理有效数据
}
break;
default:
break;
}
}
}
}
void MyThread::setFlag(bool flag)
{
isStop = flag;
//5. 子线程中声明、初始化的对象在子线程中析构,
//利用deleteLater该函数可很好解决多线程释放对象
serial->deleteLater();
qDebug() << "stop";
}