简介
本教程中主要讲解使用阿里云Iot监测控制NodeMCU的方法。
设备从MQTT数据上传、监测、控制的全流程如下图所示,本教程仅仅讲解从设备与Aliyun平台之间的交互,业务服务器部分(App开发)会在后面的教程中进行讲解,敬请期待。
友情提醒:多看官方文档,官方文档什么都有。
开发环境
- Arduino IDE
- NodeMCU1.0芯片包
- ArduinoJson库
- PubSubClient库
PubSubClient
作者名:Nick O’Leary
官网地址:https://pubsubclient.knolleary.net/
GitHub:https://github.com/knolleary/pubsubclient/
百度网盘下载: https://pan.baidu.com/s/12MHGbdfiOdwOGip5RMSSEQ 提取码: sizy
如果不知道怎么安装芯片包和导入第三方库,自行research
上一节讲述了NoduMCU通过软串口控制Arduino的例程,这一次笔者将NodeMCU接入AliyunIot平台,通过MQTT来远程监测控制NodeMCU,再通过NodeMCU来控制Arduino。
前述知识
MQTT协议
这里不做过多介绍,笔者默认您已经掌握了该知识,如果对MQTT不够了解,可以参考零基础入门学用物联网 – MQTT基础篇
物模型
物模型是产品数字化的描述,定义了产品的功能,物模型将不同品牌不同品类的产品功能抽象归纳,形成“标准物模型”,便于各方用统一的语言描述、控制、理解产品功能。
物模型由若干条“参数”组成,参数按描述的功能类型不同,又分为属性、方法和事件。
为什么要有物模型?当下的云平台为了兼容不同的通信协议,在通信层之间构建了一个物模型,这才是最重要的事,为了让不同通信协议的设备都可以整合在一个平台内进行运行,因此需要一个云平台们非常需要一个中间层做整合,物模型由此诞生。本例程中采用MQTT模型,其订阅发布的逻辑也因为加了物模型层而有所不同。
虽然在本例程中的原理还是基于订阅发布的物模型,但是订阅发布的主题和要处理的数据已经与传统MQTT意义上的数据有所不同,下面将做简单的讲解。
基于物模型的MQTT协议
MQTT协议的使用方式在物模型的产生后所迭代,下面我举两个例子就可以看出传统MQTT和基于MQTT的物模型之间的差别了。
传统MQTT
在以上图示中一共有三个MQTT客户端。它们分别是汽车,手机和电脑。MQTT服务端在管理MQTT通讯时使用了“主题”来对信息进行管理的。比如上图所示,假设我们需要利用手机和电脑获取汽车的速度,那么我们首先要利用电脑和手机向MQTT服务器订阅主题“汽车速度”。接下来,当汽车客户端向服务端的“汽车速度”主题发布信息后,服务端就会首先检查以下都有哪些客户端订阅了“汽车速度”这一主题的信息。当它发现订阅了该主题的客户端有一个手机和一个电脑,于是服务端就会将刚刚收到的“汽车速度”信息转发给订阅了该主题的手机和电脑客户端。
我们现在做一个假设,汽车发布了多少数据手机端就像看到多少数据。而现在汽车只发布了一个速度的属性,如果这个时候汽车还想把自己车内温度上传上去,那么就要向一个名为“CarTemperture”的主题发布数据,这样一来,一个循环内,小车就要向两个主题发布两次消息,而手机也因此需要多订阅一个主题。在小车向n个主题发布数据的状态下,如果手机端想要全部接收到,难道就要一次一次把所有的主题都订阅了吗?这样来看就有些臃肿了。
而云平台的定位就和我们刚才所述的手机端一样,云平台想要看到设备发布的所有数据,因此不可能让云平台一个一个主题的去订阅,能不能把所有数据全部发送给云平台让云平台自己去解析呢?由此就产生了物模型。
基于物模型的MQTT
物模型的作用就相当于定义了主题名叫“post”,汽车只要把所有的数据放在一起发送给“post”就可以了。
如果您想要将汽车的速度和温度的属性进行上传,那么您可以先告诉云平台你的汽车要上传“速度”和“温度”两个属性,让云平台先了解你将要上传的数据。然后这时汽车就可以对云平台的“post”主题发送特定格式的字符串,这个字符串是云平台规定好的。比如发送如下字符串:
publish topic=/sys/xxx/yyy/thing/event/property/post, payload={"id":123321,"params":{"carTemperture":12312341,"speed":50},"version":"1.0","method":"thing.event.property.post"}
快速上手
下面开始讲解开发步骤,大致流程如下。
注册Aliyun帐号
进入Aliyun平台注册一个帐号,自己操作
在物联网平台创建实例
首先打开控制台;
打开物联网平台;
自行创建一个实例,这里的实例在本例程中你可以当做它是一个MQTT服务器。
创建产品
创建完实例后,进入实例,新建一个产品
创建一个设备,如下图所示。
添加物模型
在产品中,选择功能定义,编辑功能;
选择自定义功能;
创建一个温度属性,如下;
创建设备
切换页面,点击添加设备。
填写设备信息,如下图,这里的NodeMCU-1是笔者创建的产品;
Tip:产品和设备的管理你可以类比以下例子:Iphone 11 PRO是一个产品,我的11和你的11是两个设备。从这里可以看出,同一个产品的设备具有一样的属性,但是同一个产品不同设备属性的值可能不一样,比如说我的11还有100G内存,你的11只有10G内存的,这里的内存就可以当做一个属性。
创建完设备就可以进行代码的开发了。
Arduino IDE代码开发
下面讲解代码部分,代码部分主要分为几个部分:
- wifi连接
- mqtt初始化
- ntp网络时间获取
- 数据上报
- 数据接收
注意事项:
- 这里的数据上报就是对"post"主题发布信息
- 这里的数据接收功能当前只能接收到云平台发送了什么内容,这个内容是需要解析的,在后面的教程中会讲解如何解析数据。
在定义物模型的时候,笔者定义了两个属性,分别是开门时间和开门指令,这两个数据分别演示数据上报和数据下发。在NodeMCU运行的时候,会上传当前时间;而在另外一边,云平台会下发开门指令给NoduMCU。
例程
/* 目的:该例程为NodeMCU连接阿里云Iot平台的例程
* 作者:Zeeland
* 最后修改时间:2021年12月2日 18:33:56
* https://gitee.com/zeeland/projects
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <ESP8266WiFiMulti.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp1.aliyun.com",60*60*8, 30*60*1000);
String currentTime;
/* 设备证书信息*/
#define PRODUCT_KEY "PRODUCT_KEY"
#define DEVICE_NAME "DEVICE_NAME"
#define DEVICE_SECRET "DEVICE_SECRET"
#define REGION_ID "cn-shanghai"
/* Aliyun线上环境域名和端口号,不需要改 */
#define MQTT_SERVER PRODUCT_KEY ".iot-as-mqtt." REGION_ID ".aliyuncs.com"
#define MQTT_PORT 1883
#define MQTT_USRNAME DEVICE_NAME "&" PRODUCT_KEY
#define CLIENT_ID "gmvzwtDHC6.ntance1|securemode=2,signmethod=hmacsha256,timestamp=254604000000|"
// MQTT连接报文参数,请参见MQTT-TCP连接通信文档,文档地址:https://help.aliyun.com/document_detail/73742.html
// 加密明文是参数和对应的值(clientIdesp8266deviceName${deviceName}productKey${productKey}timestamp1234567890)按字典顺序拼接
// 密钥是设备的DeviceSecret
//要使用加密工具,输入以上证书信息加密(时间戳可以省略)
#define MQTT_PASSWD "a29f727768b161f9073e199ab6e37ee0e3e75f1320d0219a0a204b5bbb1420"
// 发送报文的json格式
#define ALINK_BODY_FORMAT "{\"id\":\"123\",\"version\":\"1.0\",\"method\":\"thing.event.property.post\",\"params\":%s}"
// 上报报文主题
#define ALINK_TOPIC_PROP_POST "/sys/" PRODUCT_KEY "/" DEVICE_NAME "/thing/event/property/post"
unsigned long lastMs = 0;
WiFiClient espClient;
PubSubClient client(espClient);
ESP8266WiFiMulti wifiMulti;
unsigned int pwm_r=0,pwm_g=0,pwm_b=0;
int openFlag = 0;
void setup()
{
Serial.begin(9600);
Serial.println("[info] Demo Start");
Serial.print("[info] CLIENT_ID:");Serial.println(CLIENT_ID);
Serial.print("[info] MQTT_USRNAME:");Serial.println(MQTT_USRNAME);
Serial.print("[info] MQTT_PASSWD:");Serial.println(MQTT_PASSWD);
// 连接WIFI
wifiInit();
// 初始化Mqtt服务
mqttServeInit();
// 初始化NTP时间服务
timeClient.begin();
}
void loop()
{
//millis()是系统启动到目前的总时间,以下为5s上传一次数据
if (millis() - lastMs >= 3000)
{
// 获取当前时间
lastMs = millis();
// 检查连接状态
mqttCheckConnect();
timeClient.update();
// 获取当前时间
currentTime = timeClient.getFormattedTime();
Serial.print("[info] now time is :");
Serial.println(currentTime);
// 上报消息
mqttIntervalPost();
// 根据下发的数据进行反馈
work();
}
client.loop();
}
void work() {
if(openFlag ==1){
Serial.println("[info] this is the truly answer!!!");
}
}
// 初始化Mqtt服务
void mqttServeInit() {
// 设置MQTT服务器和端口号
client.setServer(MQTT_SERVER, MQTT_PORT);
// 设置MQTT订阅回调函数
client.setCallback(callback);
}
// 收到信息后的回调函数
void callback(char *topic, byte *payload, unsigned int length)
{
Serial.print("[info]Message arrived,the topic is [");
Serial.print(topic);
Serial.println("] ");
payload[length] = '\0';
const char* json = (char *)payload;
Serial.println("收到的json:");
Serial.println(json);
DynamicJsonDocument doc(1024);
deserializeJson(doc, json);
JsonObject root = doc.as<JsonObject>();
//云端下发的数据只有一个数据点,因此要判断是哪一个数据点下发了数据
if( root["params"].containsKey("openDoor") ) //containsKey方法为判断json对象是否包含指定字段
{
openFlag = root["params"]["openDoor"];
}
}
// 连接wifi
void wifiInit()
{
wifiMulti.addAP("LAPTOP-RIH1JO89 5592", "12345678"); // 将需要连接的一系列WiFi ID和密码输入这里
wifiMulti.addAP("MI 9", "12345678"); // ESP8266-NodeMCU再启动后会扫描当前网络
wifiMulti.addAP("LAPTOP9#337", "xy1229033519"); // 环境查找是否有这里列出的WiFi ID。如果有
Serial.println("[info] Connecting ..."); // 则尝试使用此处存储的密码进行连接。
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { // 此处的wifiMulti.run()是重点。通过wifiMulti.run(),NodeMCU将会在当前
delay(1000); // 环境中搜索addAP函数所存储的WiFi。如果搜到多个存储的WiFi那么NodeMCU
Serial.print(i++); Serial.print(' '); // 将会连接信号最强的那一个WiFi信号。
} // 一旦连接WiFI成功,wifiMulti.run()将会返回“WL_CONNECTED”。这也是
// 此处while循环判断是否跳出循环的条件。
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.print("[info] Connected to ");
Serial.println(WiFi.SSID()); // 通过串口监视器输出连接的WiFi名称
Serial.print("[info] IP address:\t");
Serial.println(WiFi.localIP()); // 通过串口监视器输出ESP8266-NodeMCU的IP
}
// 检查设备与MQTT服务器连接情况
void mqttCheckConnect()
{
while (!client.connected())
{
Serial.println("Connecting to MQTT Server ...");
if (client.connect(CLIENT_ID, MQTT_USRNAME, MQTT_PASSWD))
{
Serial.println("MQTT Connected!");
}
else
{
Serial.print("MQTT Connect err:");
Serial.println(client.state());
delay(5000);
}
}
if (client.connected()){
Serial.println("[info] keeping alive");
}
}
/* 上报消息 */
void mqttIntervalPost()
{
char param[128];
char jsonBuf[128];
// 将current转换为字符数组
const char * temp = currentTime.c_str();
//上传的数据在这里编辑,该例程将上报的数据为当前时间
sprintf(param, "{\"openTime\":\"%s\"}",temp);
sprintf(jsonBuf, ALINK_BODY_FORMAT, param);
Serial.println("[info] 上传的json:");
Serial.println(jsonBuf);
// 上传数据
boolean d = client.publish( ALINK_TOPIC_PROP_POST, jsonBuf);
if(d==1){
Serial.println("[info] 发送成功");
}else{
Serial.println("[info] 发送失败");
}
}
运行之后,NodeMCU首先会连接Wifi,然后连接aliyunIot平台的服务的对应设备,连接成功了之后,将会把当前的时间的数据上报至云服务器;而在NodeMCU接收到Iot平台发送的开门指令之后,Serial也会做出对应的反应。
注意事项:当出现MQTT连不上时,错误返回值2表示客户端标识符不正确, -4表示用户名或者密码错误。 请做以下检查:
- 先检查一下库文件PubSubClient.h文件中定义的 MQTT_MAX_PACKET_SIZE的值, 最好要大于1024, MQTT_KEEPALIVE 大于60;
- 检查一下你的签名和接入参数的设置,可以参考文档 https://help.aliyun.com/document_detail/73742.html?spm=a2c4g.11186623.6.650.3820619bBWPshh 。
官方文档:CONNECT指令中需包含Keep Alive(保活时间)。保活心跳时间取值范围为30至1200秒。如果心跳时间不在此区间内,物联网平台会拒绝连接。建议取值300秒以上。如果网络不稳定,将心跳时间设置高一些。