1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html 4)对正点原子Linux感兴趣的同学可以加群讨论:935446741
第二十四章 智能家居物联网项目
本章介绍使用Qt开发智能家居中的一个物联应用。简单直白的说就是通过云服务器远程控制设备(与设备通信等)。本章可以直接做毕设,是毕设物联网项目的一大福音!本章将实现远程点亮开发板LED作为一个项目实例。 在生活中,我们可能使用过WIFI智能插座这款产品。智能家居中常用来控制电器开关。比如远程开热水器,远程打开空调,窗帘等等。这些WIFI智能插座的原理就是将WIFI插座注册到云服务器上,然后通过手机的APP来访问云服务器,然后控制WIFI插座。嗯,原理我们懂了。本章就是模仿?不,或者说是直接开发这样的一个项目。包括WIFI连网,注册到云服务器上,编写Qt UI通过网络来与云服务器通信,然后再下发指令到这个连网的设备,与之通信。恩本章的流程就是这些,带着这个项目流程,然后一步步看编者是如何通过Qt实现的吧! 本章需要读者对正点原子的wifi模块ATK-ESP8266串口转WIFI有一定的了解。正点原子提供了STM32与ESP8266模块通信的例程,如果学习过STM32与ESP8266模块通信的例程的内容,理解起来则会更容易。建议参考文档:ATK-ESP8266 WIFI用户手册_V1.x.pdf及原子云平台API文档V1.2.pdf。
24.1 项目硬件
?必备硬件 本章需要正点原子ATK-ESP8266串口转WIFI模块(免费接入原子云)。另外还需要加上一个USB-TTL模块,外加一根T口USB连接线,可接入PC(电脑)调试。
T口连接线连接USB-TTL模块再连接ATK-ESP8266模块到PC(电脑),用于在PC(电脑)上直接使用串口调试/测试此模块。
这里可能会有部分读者会问是否可以用其他WIFI模块,比如正点原子Linux USB WIFI模块,或者直接使用开发板联网接入到云设备呢?答案是不可以的!只有一些特定的设备,需要刷能接入云的固件才能接入服务器。
又有读者问是否可以直接购买一些WIFI插座来使用呢?答案是不可以的!因为这些WIFI插座也是一样,也是刷了固件,而且这些设备是连接到阿里云的。需要与特定的手机APP结合使用才能注册到阿里云服务器。也就是不能拿来二次开发了!
恰好我们正点原子有物联网模块ESP8266与4G DTU模块。本章主要讲解如何通过正点原子的串口转WIFI ESP8266模块来开发一个物联网的项目应用!
?可选配件 本项目可以在正点原子I.MX6U ALPHA | mini开发板直接使用,下图为正点原子ALPHA开发板在底板上预留的ATK MODULE接口(串口接口)上的接法图。
特别提醒:如果反复实验不正确时,因为ATK-MODULE这个接口,KEY和LED脚如果有其他程序在使用,那么很可能会影响ESP8266模块的功能。刚好那两个脚接到了ESP8266的烧录固件IO-0脚与复位RST脚上,所以我们可以用杜邦线将模块重新连接到这个座子上,只接VCC、GND、TX和RX脚即可! 24.2 测试WIFI模块 要实现物联网功能,需要使用正点原子的ATK-ESP8266 WIFI模块。首先我们先测试正点原子的ATK-ESP8266 WIFI模块是否正常使用,及能否正常连接原子云服务器。ATK-ESP8266 WIFI用户手册_V1.3.pdf手册第2.2.3小节硬件连接,将ATK-ESP8266 WIFI模块连接到PC(电脑),再查阅ATK-ESP8266 WIFI用户手册_V1.3.pdf手册的第2.2.9.1小节,注册原子云帐号后,添加设备,然后按2.2.9.1小节测试连接本地WIFI(自己的路由器发出的WIFI,注意不要使用中文名或者有空格的WIFI,确保路由器的WIFI能上网!)。请自行完成并成功连接到原子云。 24.3 WIFI模块连接原子云 请先测试个人的ATK-ESP8266模块是否正常使用,及正常连接云。原子云的设备需要先分好组,各个设备命名如下。注意需要和编者命名的名字一样,也就是至少有一个分组及一个名字为“客厅灯”的设备,并记住编号及密码(密码由云生成,默认“12345678”)。
源码路径4/01_smarthome/esp8266/esp8266.cpp,内容如下。默认使用的WIFI模块串口通信波特率为115200。在Ubuntu上设试WIFI模块时,一般串口名称为“ttyUSB0”,默认是没有权限访问这个/dev/ttyUSB0设备的。所以我们需要使用下面的指令修改权限。(注意:本例适用于I.MX6U Linux开发板与Ubuntu,Windows不作测试!)。
sudo chmod 777 /dev/ttyUSB0
修改完成后查看源码内容如下。先看源码,不要急着运行!
1 #include "esp8266.h"
2 #include <unistd.h>
3 #include <QDebug>
4
5 Esp82266::Esp82266(QWidget *parent)
6 {
7 Q_UNUSED(parent)
8
9 serialPort = new QSerialPort(this);
10
11
12 timer = new QTimer();
13
14
15 led = new Led(this);
16
17
18 #if __arm__
19 serialPort->setPortName("ttymxc2");
20 #else
21 serialPort->setPortName("ttyUSB0");
22 #endif
23
24
25 serialPort->setBaudRate(115200);
26
27
28 serialPort->setDataBits(QSerialPort::Data8);
29
30
31 serialPort->setParity(QSerialPort::NoParity);
32
33
34 serialPort->setStopBits(QSerialPort::OneStop);
35
36
37 serialPort->setFlowControl(QSerialPort::NoFlowControl);
38
39 if (!serialPort->open(QIODevice::ReadWrite))
40 qDebug()<<"串口无法打开!可能正在被使用!"<<endl;
41 else {
42 qDebug()<<"串口打开成功!"<<endl;
43 }
44
45
46 connectToClound();
47
48 connect(serialPort, SIGNAL(readyRead()),
49 this, SLOT(serialPortReadyRead()));
50
51 connect(timer, SIGNAL(timeout()),
52 this, SLOT(onTimerTimeOut()));
53 }
54
55 void Esp82266::serialPortReadyRead()
56 {
57
58 QByteArray buf = serialPort->readAll();
59
60 QString temp = QString(buf);
61 readData.append(temp);
62 qDebug()<<temp<<endl;
63
64 if (readData.contains("ready")) {
65
66 sendCmdToEsp8266("AT+CWMODE=1");
67 readData.clear();
68 }
69
70 if (readData.contains("OK") && readData.contains("AT+CWMODE")) {
71 qDebug()<<"设置STA模式成功"<<endl;
72 sendCmdToEsp8266("AT+CWJAP=\"ALIENTEK-YF\",\"15902020353\"");
73 qDebug()<<"开始连接WIFI"<<endl;
74 readData.clear();
75 }
76
77 if (temp.contains("WIFI GOT IP")) {
78 qDebug()<<"连接WIFI成功"<<endl;
79 sleep(2);
80
81 sendCmdToEsp8266("AT+ATKCLDSTA=\"02314701717851074890\",\"12345678\"");
82 qDebug()<<"开始连接原子云请等待"<<endl;
83 }
84
85 if (temp.contains("CLOUD CONNECTED")) {
86 qDebug()<<"连接原子云成功"<<endl;
87 sleep(2);
88
89 timer->start(15000);
90 }
91
92 if (temp == "开")
93 led->setLedState(true);
94 else if (temp == "关")
95 led->setLedState(false);
96
97 }
98
99 Esp82266::~Esp82266()
100 {
101 serialPort->close();
102 delete timer;
103 timer = nullptr;
104 }
105
106 void Esp82266::sendCmdToEsp8266(QString cmd)
107 {
108 cmd = cmd + "\r\n";
109
110 QByteArray data = cmd.toUtf8();
111 serialPort->write(data);
112 }
113
114 void Esp82266::connectToClound()
115 {
116
118 sendCmdToEsp8266("AT+RST");
119 }
120
121 void Esp82266::sleep(int second)
122 {
123 usleep(second * 1000000);
124 }
125
126 void Esp82266::sendTextMessage(QString message)
127 {
128 serialPort->write(message.toLatin1());
129 }
130
131 void Esp82266::onTimerTimeOut()
132 {
133 sendTextMessage("online");
134 qDebug()<<"发送设备在线心跳包"<<endl;
135 }
查看源码,我们可以知道以下重要内容。
第72行,“ALIENTEK-YF”是编者处的路由器发出的WIFI热点名称,密码是“15902020353”。请修改为自己的WIFI名称及连接密码!没有路由器,用手机开热点也可以。
第81行,是原子云上的“客厅灯”设备的编号“02314701717851074890”,及密码“12345678”,请填写自己的设备编号,及密码,注意这个设备需要命名为“客厅灯”,后面程序需要使用到它!按照上面的程序就容易地可以连接上原子云了。也无需熟读ATK-ESP8266 WIFI用户手册_V1.3.pdf手册。
24.4 智能家居物联UI界面开发
项目路径为4/01_smarthome/01_smarthome/01_smarthome.pro,先看项目界面。项目界面如下,采用暗黑主题设计,结合黄色作为亮色,让用户一目了然。界面编者从一些智能家居界面中找到灵感的,编写设计完成的效果不错!请自行查阅源码,掌握了本教程前面第七章的内容,就可以理解这个界面是如何设计的。
24.5 原子云API接口 我们想要与原子云通信,那么必须先了解原子云平台的API接口。请参阅原子云平台API文档V1.2.pdf文档。原子云平台API写的非常详细了,请自行翻阅。需要我们从原子云平台了解原子云API的通信流程。 下图是原子云平台API的使用流程图。
我们写Qt应用就应该重点放在HTTPS与WebSocket方向上。查阅原子云平台API可以知道,下面是重点!一些帐号信息,与设备信息是通过HTTPS协议接口获取的,通信用WebSocket协议接口。那么我们就按原子云平台的协议流程编写应用程序。
源码路径为4/01_smarthome/webapi/webapi.cpp。内容如下。
1 #include "webapi.h"
2 #include <QUuid>
3 #include <QRegularExpression>
4
5 Webapi::Webapi(QObject *parent)
6 {
7 this->setParent(parent);
8
9 groupID.clear();
10 deviceID.clear();
11 deviceNumber.clear();
12
13 timer = new QTimer();
14 connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
15
16 networkAccessManager = new QNetworkAccessManager(this);
17
18 orgURL = "https://cloud.alientek.com/api/orgs";
19
20 api_token = "bf591984c8fa417584d18f6328e0ef73";
21
22
23 getOrgURL();
24
25 QUuid uuid = QUuid::createUuid();
26 random_token = uuid.toString();
27
28 webSocket = new QWebSocket();
29
30 QSslConfiguration config;
31 config.setPeerVerifyMode(QSslSocket::VerifyNone);
32 config.setProtocol(QSsl::TlsV1SslV3);
33 webSocket->setSslConfiguration(config);
34
35 connect(webSocket, SIGNAL(connected()),
36 this, SLOT(webSocketConnected()));
37 connect(webSocket, SIGNAL(binaryMessageReceived(QByteArray)),
38 this, SLOT(onBinaryMessageReceived(QByteArray)));
39 }
40
41 Webapi::~Webapi()
42 {
43 delete timer;
44 delete webSocket;
45 webSocket = nullptr;
46 }
47
48 void Webapi::getOrgURL()
49 {
50 getDataFromWeb(QUrl(orgURL));
51 }
52
53
54 void Webapi::getGroupListUrl()
55 {
56 getDataFromWeb(QUrl(groupListUrl));
57 }
58
59
60 void Webapi::getDevOfGroupUrl()
61 {
62 getDataFromWeb(QUrl(devOfGroupUrl));
63 }
64
65
66 void Webapi::getConStateUrl()
67 {
68 getDataFromWeb(QUrl(conStateUrl));
69 }
70
71
72 void Webapi::getDataFromWeb(QUrl url)
73 {
74
75 QNetworkRequest networkRequest;
76
77
78 QSslConfiguration config;
79 config.setPeerVerifyMode(QSslSocket::VerifyNone);
80 config.setProtocol(QSsl::TlsV1SslV3);
81 networkRequest.setSslConfiguration(config);
82
83
84 networkRequest.setUrl(url);
85
86
87 networkRequest.setHeader(QNetworkRequest::ContentTypeHeader,
88 "application/json;charset=UTF-8");
89
90
91 networkRequest.setRawHeader("token", api_token.toLatin1());
92
93 QNetworkReply *newReply =
94 networkAccessManager->get(networkRequest);
95
96 connect(newReply, SIGNAL(finished()),
97 this, SLOT(replyFinished()));
98 connect(newReply, SIGNAL(readyRead()),
99 this, SLOT(readyReadData()));
100
101 }
102 void Webapi::replyFinished()
103 {
104 QNetworkReply *reply = (QNetworkReply *)sender();
105
106 if (reply->url() == QUrl(orgURL)) {
107
108 getID(dataString, reply);
109 }
110
111 if (reply->url() == QUrl(groupListUrl)) {
112
113 getID(dataString, reply);
114
115
116 if (!timer->isActive())
117 timer->start(2000);
118 }
119
120
121 if (reply->url() == QUrl(devOfGroupUrl)) {
122 getID(dataString, reply);
123 getNumber(dataString);
124 getName(dataString);
125 }
126
127
128 if (reply->url() == QUrl(conStateUrl)) {
129 getConnectState(dataString);
130 }
131
132 reply->deleteLater();
133 reply = nullptr;
134 }
135 void Webapi::readyReadData()
136 {
137 QNetworkReply *reply = (QNetworkReply *)sender();
138 QByteArray data = reply->readAll();
139 dataString = QString(data);
140 qDebug()<<dataString<<endl;
141 }
142
143
144 void Webapi::getID(QString data, QNetworkReply *reply)
145 {
146
147 QRegularExpression pattern("\"id\":(\\d+)");
148
149 QRegularExpressionMatchIterator i = pattern.globalMatch(data);
150 while (i.hasNext()) {
151 QRegularExpressionMatch match = i.next();
152 if (match.hasMatch()) {
153 if (reply->url() == QUrl(orgURL)) {
154 org_id = match.captured(1);
155 groupListUrl = "https://cloud.alientek.com/api/orgs/"
156 + org_id + "/grouplist";
157 getGroupListUrl();
158
159 webSocket->open(QUrl(QString("wss://cloud.alientek.com/connection/%1/org/%2?token=%3")
160 .arg(api_token).arg(org_id).arg(random_token)));
161 }
162
163 if (reply->url() == QUrl(groupListUrl)) {
164 group_id = match.captured(1);
165
166 groupID.append(group_id);
167 qDebug()<<"组ID:"<<group_id<<endl;
168
169 }
170
171 if (reply->url() == QUrl(devOfGroupUrl)) {
172 device_id = match.captured(1);
173
174 deviceID.append(device_id);
175 qDebug()<<"设备ID:"<<device_id<<endl;
176 }
177 }
178 }
179 }
180
181 void Webapi::getNumber(QString data)
182 {
183 QRegularExpression pattern("\"number\":\"(\\d+)\"");
184
185 QRegularExpressionMatchIterator i = pattern.globalMatch(data);
186 while (i.hasNext()) {
187 QRegularExpressionMatch match = i.next();
188 if (match.hasMatch()) {
189 device_number = match.captured(1);
190 deviceNumber.append(device_number);
191 qDebug()<<"设备编号:"<<device_number<<endl;
192 }
193 }
194 }
195
196 void Webapi::getName(QString data)
197 {
198
199 QRegularExpression pattern("\"name\":\"([\u4e00-\u9fa5]*)");
200
201 QRegularExpressionMatchIterator i = pattern.globalMatch(data);
202 while (i.hasNext()) {
203 QRegularExpressionMatch match = i.next();
204 if (match.hasMatch()) {
205 device_name = match.captured(1);
206 deviceName.append(device_name);
207 qDebug()<<"设备名称:"<<device_name<<endl;
208 }
209 }
210 }
211
212
213 void Webapi::getConnectState(QString data)
214 {
215 QString pattern = "\"data\":\"(\\S*)\"";
216 QRegularExpression regularExpression(pattern);
217 QRegularExpressionMatch match = regularExpression.match(data, 0);
218 if(match.hasMatch()) {
219 qDebug()<<"设备连接状态"<<match.captured(1);
220 deviceConnectState.append(match.captured(1));
221 }
222 }
223
224 void Webapi::webSocketConnected()
225 {
226 qDebug()<<"webSocket连接原子云成功"<<endl;
227 }
228
229 void Webapi::onBinaryMessageReceived(QByteArray str)
230 {
231
232 QString temp(str);
233 if (temp.contains("online")) {
234 for (int i = 0; i < deviceNumber.count() ; i++) {
235 if (temp.contains(deviceNumber[i])) {
236
237 emit deviceStateChanged(deviceName[i] + "|在线");
238 qDebug()<<deviceName[i] + "|在线"<<endl;
239 break;
240 }
241 }
242 }
243 }
244
245
246 void Webapi::sleep(double second)
247 {
248 usleep(second * 1000000);
249 }
250
251 void Webapi::onTimerTimeOut()
252 {
253 static int i = 0;
254 if (i < groupID.count()) {
255
256 devOfGroupUrl = "https://cloud.alientek.com/api/orgs/"
257 + org_id + "/groups/"
258 + groupID[i] + "/devices";
259 dataString.clear();
260 getDevOfGroupUrl();
261 } else if (i >= groupID.count()
262 && i < groupID.count() + deviceID.count() ) {
263 timer->start(1000);
264 conStateUrl = "https://cloud.alientek.com/api/orgs/"
265 + org_id + "/devicestate/"
266 + deviceID[i - groupID.count()];
267 getConStateUrl();
268
269 } else {
270
271 for (int j = 0; j < deviceNumber.count(); j++) {
272 QByteArray cmd;
273 cmd[0] = 0x01;
274 sendCmd(deviceNumber[j], cmd);
275 }
276
277 timer->stop();
278 }
279
280 i++;
281 }
282
283
284 void Webapi::sendCmd(QString number, QByteArray cmd)
285 {
286 QStringList list = number.split("");
287 for (int i = 0; i < list.count(); i++) {
288 if (!list[i].isEmpty()) {
289 cmd.append(list[i]);
290 }
291 }
292
293 webSocket->sendBinaryMessage(cmd);
294 }
295
296
297 void Webapi::sendCmdMessage(QString number,
298 QByteArray cmd, QString message)
299 {
300 QStringList list = number.split("");
301 for (int i = 0; i < list.count(); i++) {
302 if (!list[i].isEmpty()) {
303 cmd.append(list[i]);
304 }
305 }
306
307 cmd.append(message);
308
309 webSocket->sendBinaryMessage(cmd);
310 }
311
312 void Webapi::whichDeviceNameSendCmd(QString name,
313 QString message) {
314
315 for (int i = 0; i < deviceName.count(); i++) {
316 if (name == deviceName[i]) {
317 QByteArray cmd;
318 cmd[0] = 0x03;
319 sendCmdMessage(deviceNumber[i], cmd, message);
320 break;
321 }
322 }
323 }
第20行,需要填写自己的原子云平台帐号api_token信息,请在原子云》帐号信息中查看!
剩余的代码都按照原子云平台API文档编写,首先是通过网络请求networkRequest,访问需要访问的地址,然后通过网络回应对象newReply来接收网络回复的结果。结果是JSION格式的文本,编者使用正则表达式提取回复的内容,作为下一个地址的参数,如此反复,就可以将原子云服务器的帐号下的设备信息提取出来。
第159行,提取出来的信息转交webSocket对象,让webSocket获取原子云平台的鉴权,就可以实现通信了。
流程都是按照原子云平台API文档的走,剩下的就是webSocket通信了,与TCP,UDP的socket通信相似,这里就不多解释了,和第十一章的TCP/UDP Socket通信内容相似。重点是流程,再参考代码看。
24.6 物联网项目综合测试
打开4/01_smarthome/01_smarthome/01_smarthome.pro项目,此项目为智能家居物联网UI界面控制端。 打开4/01_smarthome/esp8266/esp8266.pro项目,此项目设备端(被控端)。 打开上面两个项目如下。
项目文件夹下内容解释: 01_smarthome项目下: ?webapi文件夹为原子云平台的应用程序,主要用来与原子云通信。 ?Headers文件夹为界面设计的头文件。 ?Sources文件夹为界面设计的源文件。 esp8266项目下: ?led文件夹为I.MX6U开发板控制LED的接口程序。 ?Headers文件夹为esp8266通信的头文件。 ?Sources文件夹为esp8266通信的源文件(使用串口通信)。 24.6.1 Ubuntu上运行 运行esp8266.pro项目前请确认ESP8266 WIFI模块已经使用USB-TTL模块通过T口USB连接线连接到Ubuntu上,并赋予/dev/ttyUSB0权限才能访问这个串口。运行后串口终端打印信息如下。若模块已经连接上原子云,重新运行程序时,需要将模块断电复位才能再次通信! 注意,需要修改程序中的个人的本地WIFI帐号及密码,以及原子云上的设备的编号及密码,特别注意,这个设备必须在一个新增的分组下。设备在分组才能被后面的原子云API接口获取到。 运行esp8266.pro项目,Qt Creator的应用程序窗口输出如下。连接原子云成功后就会启用定时器,每15s向原子云服务器发送一次心跳包,webSocket应用程序收到后就会显示此设备在线。
运行01_smarthome.pro项目前请确认原子云》帐号信息处的API TOKEN信息,请填写自己的原子云帐号API TOKEN。否则您将访问到编者的原子云帐号的设备信息。
点击“客厅”开关按钮,可以看到esp8266设备应用程序收到开关信息。这个信息是通过UI界面应用程序发送到原子云服务器,然后原子云服务器转发给WIFI模块设备的。
24.6.2 ALPHA/Mini开发板运行 正点原子的ALPHA/Mini Linux开发板交运行上面的01_smarthome项目和esp8266项目后,运行的结果与Ubuntu上面的是一样的,注意,主控开发板需要先连网(通过网线|USB WIFI模块|4G模块|SDIO WIFI模块),可以将正点原子的ESP8266 WIFI插在同一块开发板上进行实验,这样,点击UI界面上的开关按钮,就相当于发信息到原子云上,原子云再转发给ESP8266 WIFI模块,进而控制开发板上的LED。这样就实现了将开发板实现连接到原子云里了。
|