一、專案配置
按常規新建一個Quick空專案後,我們需要對專案內容稍微改造、規劃下。
首先根據我們的需要在.pro檔內添加必要的模組,其中quick就是qml了,core那是核心模組,必須的,network是網絡模組,MQTT需要使用網絡。
然後就是為QML檔和圖片檔各自建立一個資原始檔,這樣編譯的時候會把這些資源帶上,否則的話打包釋出的時候你需要把QML檔和圖片檔放到指定資料夾內,這在安卓裏就不方便了。
最後就是如何載入前端QML檔的問題了,如下圖所示,後端透過QML載入引擎QQmlApplicationEngine把QML主檔載入進來就能顯示界面了,下一行是前端到後端的設定介面名稱,這樣在QML檔裏就可以用theMainInterface這個名稱參照後端的函數了,完成開關、調速等動作。
在新工程預設的檔裏,載入前端檔這一步是在main.c檔裏完成的,我們這裏為了整體前後端的互動,特意建了一個MainInterface的類,在主函數中直接定義這個類的變量即可,這樣整個工程結構比較清晰。
二、MQTT連線
QT標準居里沒有mqtt,需要自己單獨下載GitCode - 開發者的程式碼家園,我的專案裏已經整合了,只要右鍵添加進來即可,這裏主要是要寫個自己的MQTT管理類,把狀態保活、話題訂閱等任務內部處理掉,就是BaseMqtt類了,裏面還有個內容是網域名稱解析需要處理。
#include "BaseMqtt.h"
BaseMqtt::BaseMqtt(QObject *parent) : QObject(parent)
{
isConnected=false;
checkTimer = new QTimer(this);
checkTimer->setInterval(1*100);//心跳檢測
checkTimer->start();
m_heartTime=0;
connect(checkTimer, SIGNAL(timeout()),this,SLOT(slotCheckTimeout()));
m_mqttClient=nullptr;
m_hostAddress="";
}
BaseMqtt::~BaseMqtt(void)
{
qDebug()<<"~BaseMqtt, hostName="<<m_connectParam.hostName;
}
void BaseMqtt::slotLookUpHost(QHostInfo info)
{
if(info.error() == QHostInfo::NoError)
{
foreach (QHostAddress address, info.addresses())
{
m_hostAddress=address.toString();
qDebug()<<m_connectParam.hostName<<" mqtt found ip= "<<m_hostAddress;
break;
}
}
}
void BaseMqtt::slotCheckTimeout(void)
{
m_heartTime++;
if(m_hostAddress.isEmpty())
{
if(m_heartTime==0)
{
qDebug("mqtt start dns!");
QHostInfo::lookupHost(m_connectParam.hostName, this, SLOT(slotLookUpHost(QHostInfo)));
}
return;
}
if(m_mqttClient==nullptr)
{
if(!m_connectParam.certPath.isEmpty())//使用SSL連線
{
QFile file;
QByteArray client_key_text, client_crt_text, root_crt_text;
QString certPath=m_connectParam.certPath;
QSslSocket ssl_socket;
file.setFileName(certPath+"/client.key");
file.open(QIODevice::ReadOnly | QIODevice::Text);
client_key_text = file.readAll();
file.close();
file.setFileName(certPath+"/client.crt");
file.open(QIODevice::ReadOnly | QIODevice::Text);
client_crt_text = file.readAll();
file.close();
file.setFileName(certPath+"/rootCA.crt");
file.open(QIODevice::ReadOnly | QIODevice::Text);
root_crt_text = file.readAll();
file.close();
QSslKey client_key(client_key_text, QSsl::Rsa);
QSslCertificate client_crt(client_crt_text);
ssl_socket.setPrivateKey(client_key);
ssl_socket.setLocalCertificate(client_crt);
QSslConfiguration ssl_config=QSslConfiguration::defaultConfiguration();
ssl_config.setCaCertificates(QSslCertificate::fromData(root_crt_text)); //QSslCertificate::fromPath(certPath+"/rootCA.crt");
ssl_config.setPrivateKey(ssl_socket.privateKey());
ssl_config.setLocalCertificate(ssl_socket.localCertificate());
ssl_config.setPeerVerifyMode(QSslSocket::QueryPeer);
ssl_config.setPeerVerifyDepth(10);
ssl_config.setProtocol(QSsl::TlsV1_2);
m_mqttClient = new QMQTT::Client(m_hostAddress, m_connectParam.hostPort, ssl_config, true, this);
// qDebug()<<"\n###SSL PrivateKey="<<ssl_config.privateKey();
// qDebug()<<"###SSL Certificate="<<ssl_config.localCertificate();
// qDebug()<<"###SSL rootCA="<<ssl_config.caCertificates();
// qDebug()<<"hostName="<<m_hostAddress<<", hostPort="<<m_connectParam.hostPort;
// qDebug()<<"userName="<<m_connectParam.userName<<", passWord="<<m_connectParam.passWord<<", clientID="<<m_connectParam.clientID;
}
else//普通連線
{
QHostAddress host(m_hostAddress);
m_mqttClient = new QMQTT::Client(host, m_connectParam.hostPort, this);
}
signalSlotInit();
m_mqttClient->setUsername(m_connectParam.userName);
m_mqttClient->setPassword(m_connectParam.passWord);
m_mqttClient->setClientId(m_connectParam.clientID);
m_mqttClient->setAutoReconnect(true);
m_mqttClient->setCleanSession(true);
m_mqttClient->setKeepAlive(30);
m_mqttClient->connectToHost();
}
else if(this->mqttIsConnected())
{
for(auto iter : m_subTopicList)//訂閱話題
{
if(iter.isSubed==false)
{
mqttSubscribeMessage(iter.subTopic, iter.qos);
break;
}
}
if(m_heartTime 0==0)//保持連線
{
// qDebug()<<"mqtt send keep";
mqttPublishMessage("dev/sub/data1", QByteArray("heart"));
}
}
else
{
// qDebug()<<"BaseMqtt no connected!";
}
}
void BaseMqtt::mqttConnect(QString hostName, u16 hostPort, QString userName, QByteArray passWord, QString clientID, QString certPath)
{
clientID=clientID+QString("_")+takeHostMac().remove(":");
m_connectParam.hostName=hostName;
m_connectParam.hostPort=hostPort;
m_connectParam.userName=userName;
m_connectParam.passWord=passWord;
m_connectParam.clientID=clientID;
m_connectParam.certPath=certPath;
u8 *pData=(u8*)hostName.toUtf8().data();
if(pData[0]>='0' && pData[0]<='9')//判斷是否為網域名稱,使用網域名稱時 網域名稱的第一個字元不能是數碼
{
m_hostAddress=hostName;
}
}
bool BaseMqtt::mqttIsConnected(void)
{
// if(m_mqttClient!=nullptr)
// return m_mqttClient->isConnectedToHost();
return isConnected;
}
void BaseMqtt::mqttPublishMessage(QString topicFilter, QByteArray msgBa)
{
if(m_mqttClient==nullptr || m_mqttClient->isConnectedToHost()==false)
return;
QMQTT::Message message;
message.setTopic(topicFilter);
message.setPayload(msgBa);
m_mqttClient->publish(message);
}
void BaseMqtt::mqttPingresp(void)
{
// m_mqttClient->pingresp();
}
void BaseMqtt::mqttSubscribeMessage(QString topicFilter, quint8 qos)
{
if(m_mqttClient==nullptr)
return;
m_mqttClient->subscribe(topicFilter, qos);
}
void BaseMqtt::mqttUnsubscribeMessage(QString topicFilter)
{
if(m_mqttClient==nullptr)
return;
m_mqttClient->unsubscribe(topicFilter);
}
void BaseMqtt::signalSlotInit(void)
{
connect(m_mqttClient, SIGNAL(connected()), this, SLOT(slotMqttConnected()));
connect(m_mqttClient, SIGNAL(disconnected()), this, SLOT(slotMqttDisconnected()));
connect(m_mqttClient, SIGNAL(error(QMQTT::ClientError)), this, SLOT(slotMqttError(QMQTT::ClientError)));
connect(m_mqttClient, SIGNAL(pingresp()), this, SLOT(slotMqttPingresp()));
connect(m_mqttClient, SIGNAL(published(QMQTT::Message,quint16)), this, SLOT(slotMqttPuslished(QMQTT::Message,quint16)));
connect(m_mqttClient, SIGNAL(received(QMQTT::Message)), this, SLOT(slotMqttReceived(QMQTT::Message)));
connect(m_mqttClient, SIGNAL(subscribed(QString,quint8)), this, SLOT(slotMqttSubscribed(QString,quint8)));
connect(m_mqttClient, SIGNAL(unsubscribed(QString)), this, SLOT(slotMqttUnsubscribed(QString)));
}
void BaseMqtt::mqttAddTopic(QString topic, u8 qos)
{
for(auto iter : m_subTopicList)
{
if(iter.subTopic==topic)
{
qDebug()<<"have the same topic="<<topic;
return;
}
}
SubTopicStruct tag_subTopic;
tag_subTopic.subTopic=topic;
tag_subTopic.isSubed=false;
tag_subTopic.qos=qos;
m_subTopicList.append(tag_subTopic);
qDebug()<<"mqttAddTopic="<<topic;
}
void BaseMqtt::mqttDelTopic(QString topic)
{
int i=0;
for(auto iter : m_subTopicList)
{
if(topic==iter.subTopic)
{
if(iter.isSubed==true)
{
this->mqttUnsubscribeMessage(topic);
}
m_subTopicList.removeAt(i);
qDebug()<<"remove topic="<<topic;
return;
}
i++;
}
}
void BaseMqtt::slotMqttConnected(void)
{
isConnected=true;
emit sigMqttConnected();
qDebug()<<"clientId="<<m_mqttClient->clientId()<<"connected";
}
void BaseMqtt::slotMqttDisconnected(void)
{
isConnected=false;
int nSize=m_subTopicList.size();
for(int i=0; i<nSize; i++)
{
m_subTopicList[i].isSubed=false;
}
qDebug()<<"clientId="<<m_mqttClient->clientId()<<"disconnected";
emit sigMqttDisconnected();
}
void BaseMqtt::slotMqttError(const QMQTT::ClientError error)
{
qDebug()<<"clientId="<<m_mqttClient->clientId()<<"slotMqttError="<<error;
}
void BaseMqtt::slotMqttPingresp(void)
{
// qDebug("BaseMqtt::slotMqttPingresp");
}
void BaseMqtt::slotMqttPuslished(const QMQTT::Message &message, quint16 msgid)
{
msgid=message.id();
msgid=msgid;
// qDebug("BaseMqtt::slotMqttPuslished, msgid=%d ", msgid);
// qDebug()<<"msg="<<message.payload().toHex();
}
void BaseMqtt::slotMqttReceived(const QMQTT::Message &message)
{
emit sigtMqttReceived(message);
}
void BaseMqtt::slotMqttSubscribed(const QString &topic, const quint8 qos)
{
int i=0;
// qDebug()<<"slotMqttSubscribed, topic="<<topic<<", qos="<<qos;
for(auto iter : m_subTopicList)
{
if(iter.subTopic==topic)
{
m_subTopicList[i].isSubed=true;
break;
}
i++;
}
emit sigMqttSubscribed(topic, qos);
}
void BaseMqtt::slotMqttUnsubscribed(const QString &topic)
{
int i=0;
for(auto iter : m_subTopicList)
{
if(iter.subTopic==topic)
{
m_subTopicList[i].isSubed=false;
break;
}
i++;
}
emit sigMqttUnsubscribed(topic);
}
QString BaseMqtt::takeHostMac(void)
{
DrvCommon drv_com;
return drv_com.takeRandMac();
}
對於套用層就很簡單了,就是建立物件、連線和添加訂閱話題即可。其中有個槽函數slotMqttReceived就是用來接收器材發來的數據的。
三、數據解析
數據解析跟嵌入式端是差不多的,下面是程式碼,像剝洋蔥一樣,尋找幀頭、校驗、根據命令類別執行解析。
void MainInterface::slotMqttReceived(const QMQTT::Message &message)
{
QByteArray msg_ba=message.payload();
u8 *pData=(u8*)msg_ba.data();
// qDebug()<<"mqtt recv="<<msg_ba.toHex(':');
u8 head[2]={0xAA, 0x55};
pData=drv_com.memstr(pData, msg_ba.size(), head, 2);
if(pData!=nullptr)
{
u16 total_len=pData[2]<<8 | pData[3];
u16 crcValue=pData[total_len]<<8 | pData[total_len+1];
if(crcValue==drv_com.crc16(pData, total_len))
{
pData+=4;
u32 device_sn=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3];
pData+=4;
m_currDevSn=device_sn;
u8 cmd_type=pData[0];
pData+=1;
qDebug("recv device_sn=X, cmd_type=%d", device_sn, cmd_type);
m_keepTime=m_secCounts;
switch(cmd_type)
{
case AIR_CMD_HEART:
{
break;
}
case AIR_CMD_DATA:
{
int temp=pData[0]<<8|pData[1];//溫度 原始數據
float temp_f=(temp-1000)/10.f;//溫度浮點數據
pData+=2;
int humi=pData[0]<<8|pData[1];
float humi_f=humi/10.f;
pData+=2;
int pm25=pData[0]<<8|pData[1];
pData+=2;
u8 speed=pData[0];
pData+=1;
u8 state=pData[0];
pData+=1;
qDebug("temp_f=%.1f C, humi_f=%.1f%%, pm25=%d ug/m3, speed=%d, state=%d", temp_f, humi_f, pm25, speed,state);
QString dev_sn_str=QString::asprintf("X", device_sn);
QString temp_str=QString::asprintf("%.0f", temp_f);
QString humi_str=QString::asprintf("%.0f", humi_f);
QString pm25_str=QString::asprintf("d", pm25);
int alarm_level=0;
if(pm25<20)alarm_level=0;
else if(pm25<30)alarm_level=1;
else alarm_level=2;
emit siqUpdateSensorValues(dev_sn_str, temp_str, humi_str, pm25_str);
emit siqUpdateAlarmLevel(alarm_level);
emit siqUpdateSwitchState(state);
break;
}
case AIR_CMD_SET_SPEED:
{
break;
}
}
}
}
}
這裏我們需要把數據送到前端去顯示,所以定義了幾個訊號內容,如下圖所示,從上到下依次是狀態數據,汙染等級和開關狀態,這些數據都是器材端發送上來的,透過後端處理加工後發到前端顯示。這裏對於汙染等級的數值可以自訂,我這邊為了方便測試是20、30兩個分界線,小米的凈化器應該是30和80兩條線。
四、數據更新
對於前端顯示,這裏先提一下如何接收後端發來的數據的,如下圖所示。以Connections物件為基礎,設定它的內容target為theMainInterface,這個其實就是我們載入QML檔時候設定的前後端互動名稱,這裏用上了,相當於是訊號發射者;訊號接收器就是C++檔裏定義的訊號函數前面加個on,然後首字母改成大寫就可以了,這裏是s改為S,這樣這裏就能接收到後端發送過來的傳感器數據了,很簡單吧。至於如何顯示,放到前端部份再講解。
五、數據發送
數據發送底層就是跟嵌入式端一樣,組合後透過mqtt發送出去就行了,有點區別就是這時候要帶上目標的序列號dev_sn,這樣帶有序列號的話題器材端才能收到數據。
void MainInterface::airSendLevel(u32 dev_sn, int cmd_type, u8 *cmd_buff, u16 cmd_len)
{
u8 make_buff[500]={0};
u16 make_len=0;
make_buff[make_len++]=0xAA;
make_buff[make_len++]=0x55;
make_buff[make_len++]=0;
make_buff[make_len++]=0;
make_buff[make_len++]=dev_sn>>24;
make_buff[make_len++]=dev_sn>>16;
make_buff[make_len++]=dev_sn>>8;
make_buff[make_len++]=dev_sn;
make_buff[make_len++]=cmd_type;
memcpy(&make_buff[make_len], cmd_buff, cmd_len);
make_len+=cmd_len;
make_buff[2]=make_len>>8;
make_buff[3]=make_len;
u16 crcValue=drv_com.crc16(make_buff, make_len);
make_buff[make_len++]=crcValue>>8;
make_buff[make_len++]=crcValue;
QByteArray msg_ba((char*)make_buff, make_len);
QString topic=QString::asprintf("air/dev/sub/X", dev_sn);
if(m_mqttClient)
{
m_mqttClient->mqttPublishMessage(topic, msg_ba);
}
}
六、指令下發
在套用層,主要就是開關和調速兩個功能,這裏要看下這兩個函數的定義,比較特別,在表頭檔定義的函數名稱前多了Q_INVOKABLE,添加了這個關鍵字後,這個函數就可以在QML檔裏直接呼叫了,是不是很方便。置於函數的內容應該比較簡單了,就是組合下報文給底層函數發送出去就行了,這裏的命令定義跟嵌入式端是一樣的,兩邊有改動的話一定要及時同步,不然就亂了。對於調速設定,函數的輸入是0~1的浮點數,當小於0.1的時候會強制等於0.1,這樣調速最低時才不會停掉,只是慢速轉動。
對於這裏的後端,總的來說沒什麽難點,主要還是做好前後端數據互動的準備。這裏有個細節提一下,細心的也會發現,有個定時器槽函數slotCheckTimeout(),雖然這裏沒什麽用,但是在其他有復雜任務或者多執行緒的時候很有用,它可以定時執行任務,相當於局部的main函數了,以後有機會再慢慢學習。
本專案的交流QQ群:701889554