智能家居入门6:最新ONENET物联网开放平台
前言
ONENET云平台更新之后,已经将以前的多协议接入这部分融合进了物联网开放平台,以前在多协议接入里创建的产品和设备,虽然还可以用,但实际上已经过时了,因为新用户根本没有多协议接入这个选项。我的这系列前几篇文章有的也已经失去了实际意义,但是使用mqtt服务器的这两篇(入门三,入门四)仍然有效!因为它们使用mqtt服务器和onenet没关系。本篇文章给想接入最新版onenet的小伙伴提供参考。
须知:新版的http协议接入已经跟之前的完全不一样了,看了官方文档和亲身实践,http协议只能上传数据至服务器,已经不能从服务器获取数据了,所以对于需要双向控制的智能家居而言已经没有啥价值了,本文使用MQTT协议接入,响应快、可靠度高,程序的健壮性也已经测试,在板端跑了几天也没有出问题!
本文下位机(旭日x3派运行QT)与服务器的通信使用mqtt协议,而上位机(微信小程序)与服务器的通信使用的是标准http方法!这是因为官方文档中,服务器的API接口就是使用标准HTTP方法实现资源CURD操作,这个在本文第四节有介绍,如果有小伙伴在微信小程序端也使用mqtt协议与服务器通信的话,可以互相交流一下。
***最重要的参考就是官方文档:https://open.iot.10086.cn/doc/v5/fuse/detail/1464
QT端的mqtt报文构建直接使用DS小龙哥的代码
微信小程序在之前的基础上改改即可
最终现象
linux QT智能家居演示
一、ONENET云平台创建产品与设备
这部分直接以视频的形式给出:
需要注意的点:
①数据协议:
这里选择的是OneJson,当然也可以选择数据流,但是它们对应的发送数据和接收数据的topic是不一样的,所以如果想省事直接使用本文代码,那就选择OneJson。
②新版三元组:
按照视频创建完毕后,进入设备管理的详情中,可以看到后续会用到的三个参数:
二、使用MQTT客户端软件测试
2.1 测试前的准备
①token:
产品与设备创建完成之后,按照文档指示,需要计算token,在连接时会用到:
这里的clienid和username就是前面截图中包含的两个参数:设备名称、产品ID,这里的password就是使用官方软件计算生成的token。
接下来视频演示如何计算token:
重点如下:
纠错:上图中的时间过小,在后面多加随便一个数字既可:2810295937232。
②OneJson数据协议对应的发布、订阅topic:
文档中有明确给出OneJson数据协议(物模型)的发布和订阅topic,如果数据协议选择数据流的小伙伴,在文档的这个界面往后翻翻就可以看到对应的。
在连上服务器之后,对这两个topic操作就可以上传和接收数据啦!
2.2 测试
下载客户端软件,提取码:q1a1,这是DS小龙哥开源的,填入自己的信息:
①数据上传测试:
点击登录即可连接上服务器,然后点击发布主题就可以把数据发布到特定的物模型中,可以在onenet云平台中观察是否成功:
②数据下发测试:
onenet中进入设备调试界面,将fan_ctl设置为true,然后点击属性期望值设置。客户端软件同样连接服务器之后,点击订阅主题,观察是否收到服务器下发的消息:
这样双向测试就算完成了,接下来就是下位机与上位机的代码简介。
三、LInux QT代码
既然上面的MQTT客户端软件已经可以实现连接服务器、发布话题、订阅话题了,那说明在自己的界面中编写代码是可行的。如果不知道如何设计界面可以参考本系列第五篇文章。
项目的目录结构如下:
其中mqtt这两个文件是DS小龙哥开源的,具体哪篇文章我也找不见了,我加了一个信号来解析接收到的服务器下发的json数据。这个mqtt代码的好处就是短小精悍,完全够用,避免了使用官方库麻烦的缺点,当然喜欢使用官方qmqtt库的小伙伴可以参考网上的教程进行编译和移植,然后自己编写相关逻辑。
由于找不到原文章,这里就给出原博主的代码:
mqtt.cpp
#include "mqtt.h"
//连接成功服务器回应 20 02 00 00
//客户端主动断开连接 e0 00
const quint8 parket_connetAck[] = {0x20,0x02,0x00,0x00};
const quint8 parket_disconnet[] = {0xe0,0x00};
const quint8 parket_heart[] = {0xc0,0x00};
const quint8 parket_heart_reply[] = {0xc0,0x00};
const quint8 parket_subAck[] = {0x90,0x03};
MQTT_WorkClass::~MQTT_WorkClass()
{
qDebug()<<"析构函数---TCP";
}
void MQTT_WorkClass::run()
{
qDebug()<<"执行:run";
if(timer)
{
delete timer;
timer=nullptr;
}
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(EndEvenLoop()));
socket_type=0;
//连接到服务器
ConnectMqttServer(m_ip,m_port);
//开始事件循环
StartEvenLoop();
//初始化mqtt协议
MQTT_Init();
//连接mqtt协议
if(MQTT_Connect(m_MQTT_ClientID.toUtf8().data(),m_MQTT_UserName.toUtf8().data(),m_MQTT_PassWord.toUtf8().data()))
{
LogSend("MQTT服务器登录失败.\n");
}
else
{
LogSend("MQTT服务器登录成功.\n");
}
}
void MQTT_WorkClass::MQTT_Init(void)
{
//缓冲区赋值
mqtt_rxbuf = _mqtt_rxbuf;
mqtt_rxlen = sizeof(_mqtt_rxbuf);
mqtt_txbuf = _mqtt_txbuf;
mqtt_txlen = sizeof(_mqtt_txbuf);
memset(mqtt_rxbuf,0,mqtt_rxlen);
memset(mqtt_txbuf,0,mqtt_txlen);
}
/*
函数功能: 登录服务器
函数返回值: 0表示成功 1表示失败
*/
quint8 MQTT_WorkClass::MQTT_Connect(char *ClientID,char *Username,char *Password)
{
quint8 i,j;
int ClientIDLen = strlen(ClientID);
int UsernameLen = strlen(Username);
int PasswordLen = strlen(Password);
int DataLen;
mqtt_txlen=0;
//可变报头+Payload 每个字段包含两个字节的长度标识
DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2);
//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT
//剩余长度(不包括固定头部)
do
{
quint8 encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
//可变报头
//协议名
mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB
mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB
mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M
mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q
mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T
mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T
//协议级别
mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04)
//连接标志
mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags
mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB
mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间
mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen);
mqtt_txlen += ClientIDLen;
if(UsernameLen > 0)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen);
mqtt_txlen += UsernameLen;
}
if(PasswordLen > 0)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen);
mqtt_txlen += PasswordLen;
}
//清空数据
memset(mqtt_rxbuf,0,mqtt_rxlen);
ReadData.clear();
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
//开始事件循环
StartEvenLoop();
if(ReadData.length()==0)
{
//开始事件循环
StartEvenLoop();
}
memcpy((char *)mqtt_rxbuf,ReadData.data(),ReadData.length());
//CONNECT
if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功
{
return 0;//连接成功
}
return 1;
}
/*
函数功能: MQTT订阅/取消订阅数据打包函数
函数参数:
topic 主题
qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次
whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)
返回值: 0表示成功 1表示失败
*/
quint8 MQTT_WorkClass::MQTT_SubscribeTopic(char *topic,quint8 qos,quint8 whether)
{
quint8 i,j;
mqtt_txlen=0;
int topiclen = strlen(topic);
int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度
//固定报头
//控制报文类型
if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅
else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅
//剩余长度
do
{
quint8 encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
//可变报头
mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB
mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB
//有效载荷
mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen);
mqtt_txlen += topiclen;
if(whether)
{
mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别
}
ReadData.clear();
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
//开始事件循环
StartEvenLoop();
if(ReadData.length()==0)
{
//开始事件循环
StartEvenLoop();
}
memcpy((char *)mqtt_rxbuf,ReadData.data(),ReadData.length());
if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功
{
return 0;//订阅成功
}
return 1; //失败
}
//MQTT发布数据打包函数
//topic 主题
//message 消息
//qos 消息等级
quint8 MQTT_WorkClass::MQTT_PublishData(char *topic, char *message, quint8 qos)
{
int topicLength = strlen(topic);
int messageLength = strlen(message);
static quint16 id=0;
int DataLen;
mqtt_txlen=0;
//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度
//QOS为0时没有标识符
//数据长度 主题名 报文标识符 有效载荷
if(qos) DataLen = (2+topicLength) + 2 + messageLength;
else DataLen = (2+topicLength) + messageLength;
//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH
//剩余长度
do
{
quint8 encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题
mqtt_txlen += topicLength;
//报文标识符
if(qos)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(id);
mqtt_txbuf[mqtt_txlen++] = BYTE0(id);
id++;
}
memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength);
mqtt_txlen += messageLength;
ReadData.clear();
MQTT_SendBuf(mqtt_txbuf,mqtt_txlen);
//开始事件循环
StartEvenLoop();
return mqtt_txlen;
}
void MQTT_WorkClass::MQTT_SentHeart(void)
{
MQTT_SendBuf((quint8 *)parket_heart,sizeof(parket_heart));
}
void MQTT_WorkClass::MQTT_Disconnect(void)
{
MQTT_SendBuf((quint8 *)parket_disconnet,sizeof(parket_disconnet));
}
void MQTT_WorkClass::MQTT_SendBuf(quint8 *buf,quint16 len)
{
if(socket_type)
{
// qDebug()<<"len:"<<len;
// for(int i=0;i<len;i++)
// {
// qDebug("%#x ",buf[i]);
// }
LocalTcpClientSocket->write((const char *)buf,len);
}
}
//客户端模式:创建客户端
void MQTT_WorkClass::ConnectMqttServer(QString ip,quint16 port)
{
if(LocalTcpClientSocket)
{
LocalTcpClientSocket->close();
delete LocalTcpClientSocket;
LocalTcpClientSocket=nullptr;
}
/*1. 创建本地客户端TCP套接字*/
LocalTcpClientSocket = new QTcpSocket;
/*2. 设置服务器IP地址*/
QHostAddress FarServerAddr(ip);
/*3. 连接客户端的信号槽*/
connect(LocalTcpClientSocket,SIGNAL(connected()),this,SLOT(LocalTcpClientConnectedSlot()));
connect(LocalTcpClientSocket,SIGNAL(disconnected()),this,SLOT(LocalTcpClientDisconnectedSlot()));
connect(LocalTcpClientSocket,SIGNAL(readyRead()),this,SLOT(LocalTcpClientReadDtatSlot()));
connect(LocalTcpClientSocket,SIGNAL(bytesWritten(qint64)),this,SLOT(LocalTcpClientBytesWrittenSlot(qint64)));
/*4. 尝试连接服务器主机*/
LocalTcpClientSocket->connectToHost(FarServerAddr,port);
}
void MQTT_WorkClass::Set_MQTT_Addr(QString ip,quint16 port,QString MQTT_ClientID,QString MQTT_UserName,QString MQTT_PassWord)
{
m_ip=ip;
m_port=port;
m_MQTT_ClientID=MQTT_ClientID;
m_MQTT_UserName=MQTT_UserName;
m_MQTT_PassWord=MQTT_PassWord;
}
//客户端模式:响应连接上服务器之后的操作
void MQTT_WorkClass::LocalTcpClientConnectedSlot()
{
socket_type=1;
//通知外部
emit MQTT_ConnectState(socket_type);
//结束事件循环
EndEvenLoop();
}
//客户端模式:断开服务器
void MQTT_WorkClass::LocalTcpClientDisconnectedSlot()
{
socket_type=0;
//通知外部
emit MQTT_ConnectState(socket_type);
}
//客户端模式:读取服务器发过来的数据
void MQTT_WorkClass::LocalTcpClientReadDtatSlot()
{
ReadData=LocalTcpClientSocket->readAll();
//qDebug()<<"读取服务器发过来的数据:"<<ReadData.length();
//qDebug()<<"读取服务器发过来的数据:"<<ReadData;
//这里发布一个信号,这样在外面就可以连接槽函数,解析数据做出动作
emit ReceiveWechatData(ReadData);
EndEvenLoop(); //退出事件循环
}
//客户端模式:数据发送成功
void MQTT_WorkClass::LocalTcpClientBytesWrittenSlot(qint64 byte)
{
LogSend(QString("数据发送成功:%1\n").arg(byte));
EndEvenLoop(); //退出事件循环
}
//订阅主题
void MQTT_WorkClass::slot_SubscribeTopic(QString topic)
{
if(MQTT_SubscribeTopic(topic.toUtf8().data(),0,1))
{
LogSend(QString("主题订阅失败.\n"));
}
else
{
LogSend(QString("主题订阅成功.\n"));
}
}
//发布消息
void MQTT_WorkClass::slot_PublishData(QString topic,QString message)
{
MQTT_PublishData(topic.toUtf8().data(),message.toUtf8().data(),0);
}
void MQTT_WorkClass::EndEvenLoop()
{
//停止定时器
timer->stop();
//先退出事件循环
loop.exit();
//qDebug()<<"退出事件循环";
}
//开始事件循环
void MQTT_WorkClass::StartEvenLoop()
{
//qDebug()<<"开始事件循环";
timer->start(5000);
loop.exec();
}
//断开连接
void MQTT_WorkClass::slot_tcp_close()
{
if(socket_type)
{
timer->stop();
loop.exit();
LocalTcpClientSocket->close();
}
}
mqtt.h
#ifndef XL_MQTT_H
#define XL_MQTT_H
extern "C"
{
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
}
#include <iostream>
#include <QWidget>
#include <QTcpServer>
#include <QHostInfo> //获取计算机网络信息
#include <QUdpSocket>
#include <QtNetwork>
#include <QHostInfo>
#include <QDebug>
#include <QTcpSocket>
#include <QHostAddress>
#include <QDebug>
#include <QMessageBox>
#include <QLineEdit>
#include <QHBoxLayout>
#include <QComboBox>
#include <QFile>
#include <QTimer>
#include <QScrollBar>
#define BYTE0(dwTemp) (*(char *)(&dwTemp))
#define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3))
typedef enum
{
//名字 值 报文流动方向 描述
M_RESERVED1 =0 , // 禁止 保留
M_CONNECT , // 客户端到服务端 客户端请求连接服务端
M_CONNACK , // 服务端到客户端 连接报文确认
M_PUBLISH , // 两个方向都允许 发布消息
M_PUBACK , // 两个方向都允许 QoS 1消息发布收到确认
M_PUBREC , // 两个方向都允许 发布收到(保证交付第一步)
M_PUBREL , // 两个方向都允许 发布释放(保证交付第二步)
M_PUBCOMP , // 两个方向都允许 QoS 2消息发布完成(保证交互第三步)
M_SUBSCRIBE , // 客户端到服务端 客户端订阅请求
M_SUBACK , // 服务端到客户端 订阅请求报文确认
M_UNSUBSCRIBE , // 客户端到服务端 客户端取消订阅请求
M_UNSUBACK , // 服务端到客户端 取消订阅报文确认
M_PINGREQ , // 客户端到服务端 心跳请求
M_PINGRESP , // 服务端到客户端 心跳响应
M_DISCONNECT , // 客户端到服务端 客户端断开连接
M_RESERVED2 , // 禁止 保留
}_typdef_mqtt_message;
class MQTT_WorkClass:public QObject
{
Q_OBJECT
public:
QTimer *timer=nullptr;
MQTT_WorkClass(QObject* parent=nullptr):QObject(parent){}
~MQTT_WorkClass();
//用户名初始化
void OneNet_LoginInit(char *ProductKey,char *DeviceName,char *DeviceSecret);
//MQTT协议相关函数声明
quint8 MQTT_PublishData(char *topic, char *message, quint8 qos);
quint8 MQTT_SubscribeTopic(char *topic,quint8 qos,quint8 whether);
void MQTT_Init(void);
quint8 MQTT_Connect(char *ClientID,char *Username,char *Password);
void MQTT_SentHeart(void);
void MQTT_Disconnect(void);
void MQTT_SendBuf(quint8 *buf,quint16 len);
void ConnectMqttServer(QString ip,quint16 port);
void Set_MQTT_Addr(QString ip,quint16 port,QString MQTT_ClientID,QString MQTT_UserName,QString MQTT_PassWord);
void StartEvenLoop();
public slots:
void EndEvenLoop();
void run();
void LocalTcpClientConnectedSlot();
void LocalTcpClientDisconnectedSlot();
void LocalTcpClientReadDtatSlot();
void LocalTcpClientBytesWrittenSlot(qint64 byte);
//订阅主题
void slot_SubscribeTopic(QString topic);
//发布消息
void slot_PublishData(QString topic,QString message);
//断开连接
void slot_tcp_close();
signals:
void LogSend(QString text);
void MQTT_ConnectState(bool state);
void ReceiveWechatData(const QByteArray& data);
private:
quint8 *mqtt_rxbuf;
quint8 *mqtt_txbuf;
quint16 mqtt_rxlen;
quint16 mqtt_txlen;
quint8 _mqtt_txbuf[256];//发送数据缓存区
quint8 _mqtt_rxbuf[256];//接收数据缓存区
QTcpSocket *LocalTcpClientSocket=nullptr;
QString m_ip;
quint16 m_port;
bool socket_type=0; //这是网络的状态: 1表示已经连接 0表示未连接
QString m_MQTT_ClientID;
QString m_MQTT_UserName;
QString m_MQTT_PassWord;
QEventLoop loop;
QByteArray ReadData;
};
#endif
接下来给出代码中比较重要的部分,比如连接服务器、发布数据、订阅话题并解析:
mainwindow.cpp(注意这里不全,给大家一个参考,重要的代码全给出了)
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("智能家居——MQTT");
ui->radioButton_2->setChecked(1);
/**** 连接服务器 ****/
mqttClient = new MQTT_WorkClass();
// 设置MQTT服务器的地址和其他参数
mqttClient->Set_MQTT_Addr(serverIp, serverPort, clientId, username, password);
// 连接到MQTT服务器
mqttClient->run();
/**** 订阅主题 ****/
mqttClient->slot_SubscribeTopic(topicToSubscribe);
/**** 两个pushbutton的信号与槽连接 ****/
connect(ui->pushButton,SIGNAL(clicked()),this,SLOT(on_pushButton_clicked()));
connect(ui->pushButton_2,SIGNAL(clicked()),this,SLOT(on_pushButton_2_clicked()));
/**** 定时器(刷新日期时间、上传数据)与日期时间 ****/
dateTimeEdit = new QDateTimeEdit(QDateTime::currentDateTime(),this);
dateTimeEdit->setGeometry(10,400, 200, 40);
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MainWindow::updateDateTimeEdit);
timer->start(1000);
/**** 五个radiobutton的信号与槽连接 ****/
connect(ui->radioButton,SIGNAL(toggled(bool)), this, SLOT(onRadioButtonToggled(bool)));
connect(ui->radioButton_2,SIGNAL(toggled(bool)), this, SLOT(onRadioButton2Toggled(bool)));
connect(ui->radioButton_3,SIGNAL(toggled(bool)), this, SLOT(onRadioButton3Toggled(bool)));
connect(ui->radioButton_4,SIGNAL(toggled(bool)), this, SLOT(onRadioButton4Toggled(bool)));
connect(ui->radioButton_5,SIGNAL(toggled(bool)), this, SLOT(onRadioButton5Toggled(bool)));
//断线重连槽函数
connect(mqttClient,SIGNAL(MQTT_ConnectState(bool)),this,SLOT(reconnect(bool)));
//接收到服务器下发数据的重写槽函数
connect(mqttClient,SIGNAL(ReceiveWechatData(QByteArray)),this,SLOT(receiveWechatData(QByteArray)));
}
//断线重连槽函数
void MainWindow::reconnect(bool)
{
mqttClient = new MQTT_WorkClass();
// 设置MQTT服务器的地址和其他参数
mqttClient->Set_MQTT_Addr(serverIp, serverPort, clientId, username, password);
// 连接到MQTT服务器
mqttClient->run();
// 订阅主题
mqttClient->slot_SubscribeTopic(topicToSubscribe);
}
//定时器,槽函数
void MainWindow::updateDateTimeEdit() {
static int send_flag = 0;
dateTimeEdit->setDateTime(QDateTime::currentDateTime());
//每隔8s上传一次数据
if(send_flag == 8)
{
/**** 发布消息到主题 ****/
QMap<QString, double> dataMap;
dataMap["temp"] = 40.5; // 温度
dataMap["humi"] = 70; //湿度
dataMap["ch4"] = 12; //甲烷
// 调用函数创建JSON对象
QJsonObject rootObject = createJsonObj(dataMap);
//将QJsonObject转换为QString
QString jsonString = convertObjectToJsonString(rootObject);
mqttClient->slot_PublishData(topicToPublish, jsonString);
send_flag = 0;
qDebug() << "数据上传成功" << endl;
//本地刷新显示
QString temp_show = QString::number(dataMap["temp"]) + "℃";
QString humi_show = QString::number(dataMap["hdmi"]) + "%rh";
QString ch4_show = QString::number(dataMap["ch4"]) + "ppm";
ui->label_6->setText(temp_show);
ui->label_8->setText(humi_show);
ui->label_10->setText(ch4_show);
}
send_flag++;
}
//解析服务器下发数据,槽函数
void MainWindow::receiveWechatData(QByteArray data)
{
//qDebug()<<"读取服务器发过来的数据:"<< data;
// 找到JSON对象的起始位置(第一个'{'符号)
int startPos = data.indexOf('{');
if (startPos != -1) {
// 删除起始位置之前的所有字符
data = data.mid(startPos);
} else {
qDebug() << "未找到JSON对象的起始位置";
//return -1; // 如果找不到起始位置,返回错误代码
}
parseJsonAndAction(data);
}
//将QJsonObject转换为QString,函数
QString MainWindow::convertObjectToJsonString(const QJsonObject& object) {
// 将QJsonObject转换为QJsonDocument
QJsonDocument jsonDoc(object);
// 将QJsonDocument转换为QString,使用Compact模式
return jsonDoc.toJson(QJsonDocument::Compact);
}
//组织上传数据,函数
QJsonObject MainWindow::createJsonObj(const QMap<QString, double>& dataMap) {
QJsonObject rootObject;
// 设置id字段
rootObject["id"] = "123";
// 创建params对象
QJsonObject paramsObject;
// 遍历数据映射,添加每个数据字段
for (auto it = dataMap.constBegin(); it != dataMap.constEnd(); ++it) {
QJsonObject valueObj;
valueObj["value"] = it.value(); // 设置数值
paramsObject[it.key()] = valueObj; // 添加到params对象
}
// 将params对象添加到rootObject
rootObject["params"] = paramsObject;
return rootObject;
}
//解析服务器下发指令并动作,函数
void MainWindow::parseJsonAndAction(const QByteArray& jsonData) {
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
if (jsonDoc.isNull()) {
qDebug() << "JSON解析失败";
return;
}
// 确保这是一个对象
if (!jsonDoc.isObject()) {
qDebug() << "JSON不是一个对象";
return;
}
// 获取JSON对象
QJsonObject jsonObject = jsonDoc.object();
// 进入params对象
if (jsonObject.contains("params"))
{
QJsonObject paramsObject = jsonObject["params"].toObject();
QDateTime currentDateTime = QDateTime::currentDateTime();
// 格式化日期时间为字符串,例如:"2024-08-16 18:32:10"
QString dateTimeStr = currentDateTime.toString("yyyy-MM-dd HH:mm:ss");
// 提取led_ctl的值
if (paramsObject.contains("led_ctl"))
{
bool ledCtlValue = paramsObject["led_ctl"].toBool();
qDebug() << "led_ctl的值:" << ledCtlValue;
if(ledCtlValue == 0)
{
QString message = dateTimeStr + " - 手机控制关闭客厅灯";
ui->textBrowser->append(message);
ui->radioButton->setText("客厅灯|离线");
ui->radioButton->setChecked(0);
}
else if(ledCtlValue == 1)
{
QString message = dateTimeStr + " - 手机控制打开客厅灯";
ui->textBrowser->append(message);
ui->radioButton->setText("客厅灯|在线");
ui->radioButton->setChecked(1);
}
} else
{
qDebug() << "params中不包含led_ctl";
}
if (paramsObject.contains("fan_ctl"))
{
bool fanCtlValue = paramsObject["fan_ctl"].toBool();
qDebug() << "fan_ctl的值:" << fanCtlValue;
if(fanCtlValue ==0)
{
QString message = dateTimeStr + " - 手机控制关闭空调";
ui->textBrowser->append(message);
ui->radioButton_3->setText("空调|离线");
ui->radioButton_3->setChecked(0);
}
else if(fanCtlValue == 1)
{
QString message = dateTimeStr + " - 手机控制打开空调";
ui->textBrowser->append(message);
ui->radioButton_3->setText("空调|在线");
ui->radioButton_3->setChecked(1);
}
} else
{
qDebug() << "params中不包含fan_ctl";
}
if (paramsObject.contains("curtain"))
{
int curtainCtlValue = paramsObject["curtain"].toInt();
qDebug() << "curtain的值:" << curtainCtlValue;
if(curtainCtlValue >=0)
{
QString message = dateTimeStr + " - 手机控制窗帘";
ui->textBrowser->append(message);
QString cl = "窗帘打开程度:" + QString::number(curtainCtlValue);
ui->label_14->setText(cl);
qDebug() << "窗帘打开程度:" << curtainCtlValue << endl;
}
} else
{
qDebug() << "params中不包含curtain";
}
}
else
{
qDebug() << "JSON对象中不包含params";
}
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QRadioButton>
#include <QDebug>
#include <QTimer>
//日期时间
#include <QDateTimeEdit>
#include <QTimeEdit>
#include <QDateEdit>
//tcp客户端通过http协议连接云服务器
#include <QJsonObject>
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonValue>
#include <MQTT/mqtt.h>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
QString serverIp = "183.230.40.96"; // 替换为服务器IP
quint16 serverPort = 1883; // 替换为服务器端口
QString clientId = "dht11"; // 替换为设备名称
QString username = "bs2u21MIHC"; // 替换为产品ID
QString password = "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign=dkKx5uuWp0sMqet7BJGa2w%3D%3D"; // 替换为token
QString topicToSubscribe = "$sys/bs2u21MIHC/dht11/thing/property/set"; // 替换为要订阅的主题
QString topicToPublish = "$sys/bs2u21MIHC/dht11/thing/property/post"; // 替换为要发布消息的主题
//三个处理函数
QString convertObjectToJsonString(const QJsonObject& object);
QJsonObject createJsonObj(const QMap<QString, double>& dataMap);
void parseJsonAndAction(const QByteArray& jsonData);
private:
Ui::MainWindow *ui;
QDateTimeEdit *dateTimeEdit;
QTimer *timer;
MQTT_WorkClass *mqttClient;
private slots:
void onRadioButtonToggled(bool checked);//五个radiobutton,控制模式和显示状态
void onRadioButton2Toggled(bool checked);
void onRadioButton3Toggled(bool checked);
void onRadioButton4Toggled(bool checked);
void onRadioButton5Toggled(bool checked);
void updateDateTimeEdit();//刷新日期时间,上传数据至云服务器
void on_pushButton_clicked();//连接服务器
void on_pushButton_2_clicked();
//断线重连
void reconnect(bool);
//接收服务器下发的数据
void receiveWechatData(QByteArray);
};
#endif // MAINWINDOW_H
代码中注释很全,集成到自己的项目中是很容易的,主要是ui界面设计不是纯代码所以即使我给出了完整代码也没用,很多东西对不上,花点时间集成mqtt相关代码到自己的项目里就好啦!这部分完成之后可以先进行测试,看看是否可以将数据上传至服务器,并且能否订阅到服务器下发的数据,测试方法与第二节一样。
四、onenet 云端API介绍及微信小程序代码
4.1 onenet API介绍
通过阅读onenet物联网开放平台的文档,发现OneNET API提供产品、设备、服务等云端API,使用标准HTTP方法实现资源CURD操作,其中URL中的中文参数使用UTF-8编码。具体的大家可以去看文档,写的很清楚,所以微信小程序使用这种方式接入服务器。
从文档中的接口列表可以看到物模型使用的接口:
具体的使用方式如下:
①设备属性最新数据查询(微信小程序从服务器获取数据):
文档往下翻可以看到使用示例,微信小程序中参考这种格式即可:
②设备属性期望设置(微信小程序发布数据到服务器):
文档往下翻可以看到使用示例,微信小程序中参考这种格式即可:
有了上面这两个示例,就可以在微信小程序中进行代码的编写了!
4.2 微信小程序代码
index.js
Page({
data: {
temp:0,
humi:0,
gas_ch4:0,
fumes:0,
leds:false,
fen:false,
water:false
},
/* 获取OneNET云平台设备数据 */
getinfo() {
/* 发起 HTTPS 网络请求 获取设备参数 */
wx.request({
/* 接口地址、产品ID、设备名 */
url: "https://iot-api.heclouds.com/thingmodel/query-device-property?product_id=466832eFSH&device_name=dht11",
header: {
/* token */
"authorization": "version=2018-10-31&res=products%2F466832eFSH%2Fdevices%2Fdht11&et=281029593723&method=md5&sign"
},
method: "GET", /* HTTP 请求方法:获取 */
success: res => {
console.log("获取成功", res)
this.setData({
humi: res.data.data[4].value,
temp: res.data.data[6].value,
gas_ch4: res.data.data[0].value,
fumes: res.data.data[3].value,
})
}
});
},
//下发指令:led
onledsChange(event){
const that = this
console.log(event.detail.value);
const sw = event.detail.value
that.setData({leds:sw})
if(sw){
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-property',
method: 'POST', /* HTTP 请求方法:上传 */
header: {
/* token */
"authorization": "version=2018-10-31&res=products%2F466832eFSH%2Fdevices%2Fdht11&et=281029593723&method=md5&sign"
},
data: {
"product_id": "466832eFSH",
"device_name": "dht11",
"params": {
"led_ctl": true /* 控制板端LED */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
}else{
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-desired-property',
method: 'POST',
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2F466832eFSH%2Fdevices%2Fdht11&et=281029593723&method=md5&sign"
},
data: {
"product_id": "466832eFSH",
"device_name": "dht11",
"params": {
"led_ctl": false /* 控制板端LED */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
}
},
onLoad() {
var that = this
setInterval(function(){
that.getinfo()
},5000)
}
})
上面的代码也不是完整的,有了思路和格式就是非常大的帮助,大家可以参照这样的发布与请求数据的格式将代码集成到自己的项目中,进行自己的界面设计,注意替换产品ID、设备名称、鉴权信息(token)等。
五、板端运行
这里也和本系列的第五篇文章一样,这个开发板能直接运行QT,所以不需要交叉编译,只需要在虚拟机中测试完成之后,把项目复制到板端就可以运行!对于其它开发板的小伙伴,就参考正点原子的交叉编译吧。
本文到这就结束了,智能家居系列应该就更到这了,说不定等下次onenet更新之后,我又会再更,由于网上现在对新版onenet的接入说明还是很少的,本篇文章提供了完整的思路的请求格式,打通了下位机与微信小程序,还是比较有参考价值的,最最最重要的是自己要去看官方的文档,配合博主的介绍,很容易就能看懂,接下来就是代码实现,多看看就可以看懂。
对于不想使用linux开发板的小伙伴当然也可以使用stm32配合esp8266实现一样的功能,这方面很早就有up实现了,也是新版onenet,不过实现方式大部分不体现在代码端了,是将固件烧入esp8266,这样直接使用内置的AT指令,提供参数就可以直接连接服务器了,不像以前还需要mqtt的库,现在也不需要啦,大家加油!