前言
前一篇文章講到了利用C#窗體應用程序來設計上位機的界面軟件,但是這樣的界面軟件還不夠通用,只能定製化運行,圖形界面的代碼和上位機初步處理數據的代碼耦合性較高。所以這篇文章講了利用QT Creator將上位機部分的代碼編寫成QT類庫,從而和圖形界面解耦合,如果使用不同的軟件開發圖形界面,都可以使用到這個類庫。
創建一個QT程序
- 首先我們需要創建一個QT程序來當做這個類庫的主窗體,打開Qt Creator後,選擇工具欄"文件"->“新建文件或項目”->“項目”->“Application”->“Qt Widgets Application”;
- 單擊"Choose"後,重新命名,選擇文件存放路徑,點擊"下一步";
- 根據自己需要選擇"Kits",點擊"下一步";然後命名各個文件名和基類名,點擊"下一步";最後點擊"完成"就可以生成一個Qt主程序了。
創建QT類庫
- 首先右鍵項目欄中的QT項目名,選擇"添加新文件",接着選擇"C++"->“C++ Class”,點擊"Choose";
- 給類名取名,選擇要包含的基類,點擊"下一步";
- 可以看到該C++類就被添加到主項目裏了,最後點擊"完成"。頭文件就被放到主項目的Headers文件夾下,.cpp文件就放到Sources文件夾下了。
編寫QT類庫
- 我們在剛創建的頭文件的類裏創建各個變量和函數,以光開關模塊爲例,代碼如下所示。
#ifndef BOTDR_H
#define BOTDR_H
#include <QMainWindow>
#include <QObject> //Qt鏈接函數所用
#include <QtSerialPort/QSerialPort> //Qt串口類
#include <QtSerialPort/QSerialPortInfo>
#include <QDebug> //qDebug所用
#include <QtDebug>
#include <QThread> //Qt多進程所用
class BOTDR : public QThread
{
Q_OBJECT
public:
explicit BOTDR();
~BOTDR();
bool ComOpen(int BaudRate); //Qt串口打開函數
bool ComClose(); //Qt串口關閉函數
bool SendData(int *frame, int length); //Qt串口發送數據函數
bool OpticalSwitchSetting(int ChannelNumber); //光開關下發命令函數
int CalBIP8(int *data); //異或校驗碼函數
struct QTARGC //用來顯示下位機參數的結構體
{
int Module; //各個模塊的標識碼
int Optical_Rx; //存放下位機返回的通道號參數
bool IsSuccess;
bool IsUpdate;
int Error;
int Warning;
int ErrorExtense;
};
QTARGC QtArgc;
signals:
void readyRead(); //信號函數
void DataRead();
public slots:
void ReceivedData(); //Qt串口接收數據函數,也是事件函數,由信號函數觸發
private:
QSerialPort* MySerial; //實例化一個私有串口
QByteArray txdata; //串口接收的QByteArray
const int OpticalSwitch = 0x03;
protected:
void run();
};
#endif // BOTDR_H
- 接着我們在創建的.cpp文件裏編寫類先關的函數。
#include "botdr.h"
BOTDR::BOTDR()
{
//構造函數初始化串口和參數
MySerial = new QSerialPort();
QtArgc.Optical_Rx = 0;
}
BOTDR::~BOTDR()
{
//析構函數關閉串口
BOTDR::ComClose();
}
bool BOTDR::ComOpen(int BaudRate)
{
//查詢當前PC內有多少可用的串口,全部放入列表Ports
QList<QSerialPortInfo> Ports = QSerialPortInfo::availablePorts();
int a = Ports.length();
if (a>1)
{
//如果有新插入的設備則MySerial選擇對應的端口
MySerial->setPortName(Ports[a-1].portName());
}
else
{
MySerial->setPortName(Ports[0].portName()) ; //默認條件下則選擇端口1
}
MySerial->setBaudRate(BaudRate,QSerialPort::AllDirections);//設置波特率和讀寫方向
MySerial->setDataBits(QSerialPort::Data8); //數據位爲8位
MySerial->setFlowControl(QSerialPort::NoFlowControl);//無流控制
MySerial->setParity(QSerialPort::NoParity); //無校驗位
MySerial->setStopBits(QSerialPort::OneStop); //一位停止位
//按照以上設置打開MySerial串口
if (MySerial->open(QIODevice::ReadWrite))
{
qDebug() <<MySerial->portName()<<"串口打開成功!";
return true;
}
else
{
qDebug() <<"串口打開失敗!";
return false;
}
}
bool BOTDR::ComClose()
{
if(MySerial->isOpen()==true)
{
//清除串口數據並關閉
MySerial->clear();
MySerial->close();
}
return true;
}
bool BOTDR::SendData(int* frame, int length)
{
//將原本的int數組內的數據轉換到QByteArray內
for(int i=0;i<length;i++)
{
txdata[i] = frame[i];
}
//串口發送數據
if(MySerial->write(txdata)== length)
{
qDebug()<<"命令發送成功!";
//串口發出數據後,串口如果準備完畢,就鏈接到數據接收函數,時刻監聽串口
QObject::connect(MySerial,SIGNAL(readyRead()), this,SLOT(ReceivedData()));
return true;
}
else
return false;
}
void BOTDR::ReceivedData()
{
QtArgc.IsSuccess =false;
QtArgc.IsUpdate = false;
QtArgc.Module = 0;
QByteArray data = MySerial->readAll(); //將串口緩衝區內所有數據放入QByteArray類型的data內
int length = data.length();
if(length>0) //當數據不爲空時
{
//數據轉化爲無符號十六進制(因爲QByteArray會把每個字節數據當成有符號的byte型,但我這裏接收到數據是無符號的byte,所以要做個轉換,不然接收到的數據就是錯誤的;所以請根據自行需求加不加這段代碼)
int buff[30] ={0};
for(int i=0;i<length;i++)
{
if(data[i]&128)
{
buff[i]= 128+(data[i]&127);
}
else
{
buff[i] = data[i];
}
}
//根據接收到的數據進行初步的處理
if((buff[0]-65+'A') =='D')
{
//標識本次數據更新屬於哪個模塊
QtArgc.Module = buff[1];
if(buff[1]==OpticalSwitch)
{
QtArgc.Optical_Rx = buff[2];
QtArgc.IsUpdate = true;
}
}
else if((buff[0]-65+'A')=='S')
{
QtArgc.IsSuccess = true;
}
else if((buff[0]-65+'A')=='W')
{
QtArgc.IsSuccess = false;
QtArgc.Warning = buff[1];
}
else if((buff[0]-65+'A')=='E')
{
QtArgc.IsSuccess = false;
QtArgc.Error = buff[1];
if(length > 2)
{
QtArgc.ErrorExtense = buff[2];
}
}
}
// else
// qDebug()<<"數據接收失敗!";
}
//光開關命令下發函數
bool BOTDR::OpticalSwitchSetting(int ChannelNumber)
{
if(MySerial->isOpen()) //檢查串口是否打開
{
if(ChannelNumber>=0 && ChannelNumber<5)
{
int frame[8] = { 0xfb, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xfe };
frame[4] = ChannelNumber;
frame[1] = BOTDR::CalBIP8(frame);
//發送數據命令幀
if(BOTDR::SendData(frame,8)==true)
{
return true;
}
else
return false;
}
else
return false;
}
else
return false;
}
//異或校驗函數
int BOTDR::CalBIP8(int *data)
{
int bip16 = char(data[0] ^ (data[1] & 0x00) ^ data[2] ^ data[3] ^ data[4] ^ data[5] ^ data[6] ^ data[7]);
int bip8 = char(((bip16 & 0xf0) >> 4) ^ (bip16 & 0x0f));
return bip8;
}
void BOTDR::run()
{
//只有當更新和成功標誌位置true後才發射信號函數
while(QtArgc.IsUpdate!=true && QtArgc.IsSuccess !=true)
{}
emit DataRead(); //發射信號函數,進而觸發讀取通道號的函數
}
調用QT類庫
- 類庫裏的函數和變量設置好了之後,我們就可以使用類庫了,這裏我們不在UI界面做設計,只在mainwindow.cpp裏簡單調用類庫。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//調用類庫
com = new BOTDR(); //示例化類
connect(com, SIGNAL(DataRead()),SLOT(DisplayArgc()));
com->start();
//串口打開,光開關設置參數
if(com->ComOpen(9600) == true)
{
com->OpticalSwitchSetting(3);
}
}
void MainWindow::DisplayArgc()
{
if(com->QtArgc.IsSuccess == true)
{
qDebug()<<"本次操作成功";
}
else if(com->QtArgc.IsUpdate == true)
{
switch(com->QtArgc.Module)
{
case 0x03:
{
//OpticalSwitch
qDebug()<<"當前通道號爲"<<com->QtArgc.Optical_Rx;
break;
}
default:
break;
}
}
else
{
qDebug()<<"差錯代碼:"<<com->QtArgc.Error;
qDebug()<<"差錯代碼擴展:"<<com->QtArgc.ErrorExtense;
qDebug()<<"告警代碼:"<<com->QtArgc.Warning;
}
}
- 串口打開後,光開關設置參數,調用類庫裏相應的函數,就數據命令幀下發到下位機,下位機接收後由外部設備光開關操作後,返回當前通道號給上位機,上位機接收函數ReceivedData()接收後,將其參數存放對應位置,並更新標誌位,等待界面軟件讀取。
優化
爲了進一步優化讀取通道號的邏輯,可進行Qt線程的鏈接函數。
- 在頭文件的類裏新增信號函數
void DataRead()
;調用類庫時新增語connect(com,SIGNAL(DataRead()),SLOT(DisplayArgc()));
,即通過信號函數DataRead()來觸發讀取通道號的顯示函數DisplayArgc(),該函數定義在mainwindow類裏,後續可由界面軟件函數替代。 - 那麼信號函數DataRead()什麼時候開始運行,可以由一個Qt線程來控制。在頭文件里加一個受保護的線程函數,
protected: void run();
,並由調用類庫時的語句com->start();
來控制該線程運行;接着在.cpp文件裏編寫該函數,只有當串口數據接收函數ReceivedData()內的標誌位更新了才發射信號函數(這裏有需求的可在run()裏增加等待時間跳出) - 讀取通道號的函數DisplayArgc()被觸發後,就會根據標誌位來顯示當前通道號
通道號設置爲3,點擊Qt左下角的"運行",最後運行結果如下: