零基础入门学用物联网(ESP8266) 第二部分 MQTT基础篇(三)

参考教程:https://www.bilibili.com/video/BV1L7411c7jw/?spm_id_from=333.1387.favlist.content.click

七、ESP8266发布MQTT消息

(1)ESP8266发布MQTT消息,可通过调用PubSubClient类的成员函数publish实现,函数参数依次为发布主题及发布信息(数据类型均为字符数组首地址),如发送成功,该函数返回True,否则返回False。

(2)ESP8266发布MQTT消息示例程序:

①程序框图:

②程序代码:

cpp 复制代码
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <Ticker.h>
 
// 设置Wi-Fi接入信息
const char* ssid = "Zevalin_Computer";
const char* password = "00114514";
const char* mqttServer = "test.mosquitto.org";   //如无法使用,可更换为其它公用MQTT服务端地址
 
Ticker ticker;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
 
int count;    // Ticker计数用变量
 
void setup() {
  Serial.begin(9600);
  WiFi.mode(WIFI_STA);  //设置ESP8266工作模式为无线终端模式
  connectWifi();        //连接Wi-Fi
  
  mqttClient.setServer(mqttServer, 1883);  //设置MQTT服务端和端口号
  connectMQTTServer();  //连接MQTT服务端
 
  ticker.attach(1, tickerCount);    //每隔1秒,count计数加1
}
 
void loop() { 
  if (mqttClient.connected()) { // 如果开发板成功连接服务器
    // 每隔3秒钟发布一次信息
    if (count >= 3){
      pubMQTTmsg();
      count = 0;
    }    
    // 保持心跳
    mqttClient.loop();
  }
  else {                    // 如果开发板未能成功连接服务器
    connectMQTTServer();    // 不断尝试连接服务器
  }
}
 
void tickerCount(){
  count++;
}
 
void connectMQTTServer(){
  // 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
  String clientId = "esp8266-" + WiFi.macAddress();
 
  // 连接MQTT服务端
  if (mqttClient.connect(clientId.c_str())) { 
    Serial.println("MQTT Server Connected.");
    Serial.println("Server Address: ");
    Serial.println(mqttServer);
    Serial.println("ClientId:");
    Serial.println(clientId);
  }
  else {
    Serial.print("MQTT Server Connect Failed. Client State:");
    Serial.println(mqttClient.state());
    delay(3000);
  }   
}
 
void pubMQTTmsg(){  // 发布信息
  static int value; // 客户端发布信息的内容之一
 
  // 建立发布主题,主题名称以Taichi-Maker-为前缀,后面添加设备的MAC地址
  // 这么做是为确保不同用户进行MQTT信息发布时,ESP8266客户端名称各不相同
  String topicString = "Taichi-Maker-Pub-" + WiFi.macAddress();
  char publishTopic[topicString.length() + 1];   //创建字符数组(末位为'/0')
  strcpy(publishTopic, topicString.c_str());     //字符串拷贝到字符数组中
 
  // 建立发布信息,信息内容以Hello World为起始,后面添加发布次数
  String messageString = "Hello World " + String(value++); 
  char publishMsg[messageString.length() + 1];   //创建字符数组(末位为'/0')
  strcpy(publishMsg, messageString.c_str());     //字符串拷贝到字符数组中
  
  // ESP8266向主题发布信息
  if(mqttClient.publish(publishTopic, publishMsg)) {
    Serial.println("Publish Topic:");Serial.println(publishTopic);
    Serial.println("Publish message:");Serial.println(publishMsg);    
  }
  else {
    Serial.println("Message Publish Failed."); 
  }
}
 
void connectWifi(){   // ESP8266连接Wi-Fi
  WiFi.begin(ssid, password);
 
  //等待Wi-Fi连接,成功连接后输出成功信息
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi Connected!");  
  Serial.println(""); 
}

八、ESP8266订阅MQTT消息

1、订阅单个主题

(1)ESP8266订阅主题,同样可通过调用PubSubClient类的成员函数实现,其中相关的成员函数如下:

①subscribe函数:向MQTT服务端订阅主题,函数参数为所订阅的主题(类型为字符数组首地址),订阅成功则返回True,否则返回False。

②setCallback函数:设置MQTT订阅回调函数,也就是设置收到订阅的MQTT信息后执行的处理函数,其函数参数为函数指针。函数指针指向的处理函数,其函数参数必须依次为主题(类型为字符数组首地址)、信息内容(类型为字节数组首地址)和信息长度(类型为无符号整型)。

③loop函数:该函数除了发送心跳信息以外,还会主动检查网络缓冲区是否有新收到的MQTT信息,如果有,则调用setCallback函数注册的回调函数。

(2)ESP8266订阅单个主题示例程序:

①程序框图:

②程序代码:

cpp 复制代码
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
 
// 设置Wi-Fi接入信息
const char* ssid = "Zevalin_Computer";
const char* password = "00114514";
const char* mqttServer = "test.mosquitto.org";   //如无法使用,可更换为其它公用MQTT服务端地址
 
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
 
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);     // 设置板上LED引脚为输出模式
  digitalWrite(LED_BUILTIN, HIGH);  // 启动后关闭板上LED
  Serial.begin(9600);               // 启动串口通讯
  
  WiFi.mode(WIFI_STA);
  connectWifi();
  
  // 设置MQTT服务端和端口号
  mqttClient.setServer(mqttServer, 1883);
  // 设置MQTT订阅回调函数(收到订阅的信息后执行的函数)
  mqttClient.setCallback(receiveCallback);
  // 连接MQTT服务端
  connectMQTTserver();
}
 
void loop() {
  if (mqttClient.connected()) { // 如果开发板成功连接服务器
    mqttClient.loop();          // 发送心跳信息,并判断是否收到MQTT信息,如有则调用处理函数
  }
  else {                        // 如果开发板未能成功连接服务器
    connectMQTTserver();        // 不断尝试连接服务器
  }
}
 
// 连接MQTT服务端并订阅信息
void connectMQTTserver(){
  // 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
  String clientId = "esp8266-" + WiFi.macAddress();
 
  // 连接MQTT服务端
  if (mqttClient.connect(clientId.c_str())) { 
    Serial.println("MQTT Server Connected.");
    Serial.println("Server Address:");
    Serial.println(mqttServer);
    Serial.println("ClientId: ");
    Serial.println(clientId);
    subscribeTopic(); // 订阅指定主题
  }
  else {
    Serial.print("MQTT Server Connect Failed. Client State:");
    Serial.println(mqttClient.state());
    delay(5000);
  }   
}
// 收到信息后的回调函数(函数参数依次为主题、信息内容和信息长度,顺序是固定的)
void receiveCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message Received ["); Serial.print(topic); Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);  //以字符的形式打印信息内容数组的每一个字节数据
  }
  Serial.println("");
  Serial.print("Message Length(Bytes) "); Serial.println(length);
 
  if ((char)payload[0] == '1') {     // 如果收到的信息以"1"为开头
    digitalWrite(BUILTIN_LED, LOW);  // 点亮LED
    Serial.println("LED ON");
  }
  else {                           
    digitalWrite(BUILTIN_LED, HIGH); // 熄灭LED
    Serial.println("LED OFF");
  }
}
 
// 订阅指定主题
void subscribeTopic(){
  String topicString = "Taichi-Maker-Sub-" + WiFi.macAddress();
  char subTopic[topicString.length() + 1];  
  strcpy(subTopic, topicString.c_str());
  
  // 订阅主题,并通过串口监视器输出是否成功订阅主题以及订阅的主题名称
  if(mqttClient.subscribe(subTopic)){
    Serial.println("Subscrib Topic:");
    Serial.println(subTopic);
  }
  else {
    Serial.print("Subscribe Fail...");
  }  
}
 
void connectWifi(){
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000); Serial.print(".");
  }
  Serial.println(""); Serial.println("WiFi Connected!");   Serial.println(""); 
}

2、订阅多个主题

(1)ESP8266订阅多个主题,其实就是多次订阅单个主题,或者使用单级通配符或多级通配符订阅主题。

(2)ESP8266订阅多个主题示例程序与ESP8266订阅单个主题示例程序类似,区别在于自定义函数subscribeTopic的实现。

cpp 复制代码
void subscribeTopic(){
  // 构建主题1的字符数组
  String topicString1 = "Taichi-Maker-Sub-" + WiFi.macAddress()+"/+/data";
  char subTopic1[topicString1.length() + 1];  
  strcpy(subTopic1, topicString1.c_str());

  // 构建主题2的字符数组
  String topicString2 = "Taichi-Maker-Sub-" + WiFi.macAddress()+"/sensor/#";
  char subTopic2[topicString2.length() + 1];  
  strcpy(subTopic2, topicString2.c_str());
  
  // 订阅主题1,并通过串口监视器输出是否成功订阅主题1以及订阅的主题1名称
  if(mqttClient.subscribe(subTopic1)){
    Serial.println("Subscrib Topic:");
    Serial.println(subTopic1);
  }
  else {
    Serial.print("Subscribe Fail...");
  }  
 
  // 订阅主题2,并通过串口监视器输出是否成功订阅主题2以及订阅的主题2名称
  if(mqttClient.subscribe(subTopic2)){
    Serial.println("Subscrib Topic:");
    Serial.println(subTopic2);
  }
  else {
    Serial.print("Subscribe Fail...");
  }  
}

九、QoS服务质量等级

1、服务质量等级的概念

(1)一个物联网系统中有些信息非常重要们需要确保这类重要信息可以准确无误地被发送和接收,而有些信息则相对不那么重要,这类信息如果在传输中丢失不会影响系统的运行。MQTT服务质量(Quality of Service,缩写QoS)正是用于告知物联网系统,哪些信息是重要信息,需要准确无误的传输,而哪些信息不那么重要,即使丢失也没有问题。

(2)MQTT协议有三种服务质量级别,不同的服务质量级别意味着不同的MQTT传输流程,对于较为重要的MQTT消息,通常选择QoS>0的服务级别(即QoS为1或2)。

①QoS = 0------最多发一次

②QoS = 1------最少发一次

③QoS = 2------保证收一次

这里提到的"发"与"收"有两种可能:一种是MQTT客户端发布消息时,将消息发送给MQTT服务端;一种是MQTT客户端订阅了某一主题消息后,MQTT服务端将消息发送给MQTT客户端

为了避免造成混淆,在本节教程后面的描述中将使用"发送端"来描述发送MQTT消息的设备,而使用"接收端"来描述接收MQTT消息的设备

2、不同的服务质量等级

(1)QoS = 0------最多发一次:

①0是服务质量QoS的最低级别。

②当QoS为0级时,MQTT协议并不保证所有信息都能得以传输,也就是说,QoS=0的情况下,MQTT服务端和MQTT客户端不会对消息传输是否成功进行确认和检查,发送端一旦发送完消息后,就完成任务了,消息能否成功传输全看网络环境是否稳定。

③在网络环境稳定的情况下,信息传输一般是不会出现问题的,但是在环境不稳定的情况下,可能会在传输过程中出现MQTT消息丢失的情况。

(2)QoS = 1------最少发一次:

①当QoS级别为1时,发送端在消息发送完成后,会等待接收端的确认,接收端成功接收消息后,会发送一条确认报文PUBACK给发送端,如果发送端收到了这条PUBACK确认报文,那么它就知道消息已经成功接收。

②PUBACK报文的内容只有一个packetId------报文标识符。

③假如过了一段时间后,发送端没有收到PUBACK报文,那么发送端会再次发送消息,然后再次等待接收端的PUBACK确认报文。需要注意的是,当发送端重复发送一条消息时,PUBLISH报文中的dupFlag会被设置为True,这是为了告诉接收端,此消息为重复发送的消息。

(3)QoS = 2------保证收一次:

①当QoS级别为2时,发送端在消息发送完成后,会等待接收端的应答,接收端成功接收消息后,会发送一条应答报文PUBREC给发送端,如果发送端收到了这条PUBREC确认报文,那么它就知道消息已经成功接收。

②发送端收到PUBREC报文后,会把此报文进行存储,并且返回PUBREL报文作为应答,当接收端收到PUBREL报文后,会应答发送端一条PUBCOMP报文,至此,一次QoS=2的MQTT消息传输就结束了。

③关于QoS=2的MQTT服务端内部控制机制,在实际开发MQTT物联网的过程中基本不会涉及到,所以这里也不详细展开了。

3、服务质量等级的设置

(1)发布消息时设置:客户端发布信息时,PUBLISH数据包中专有一个信息为qos,该信息正是用于设置客户端发布MQTT消息的QoS等级。

(2)订阅消息时设置:在客户端订阅MQTT主题时,SUBSCRIBE数据包中有一个信息用于设置订阅主题的QoS级别,客户端正是通过该主题来设置订阅主题的QoS级别的。

(3)客户端连接服务端时设置:要想实现QoS>0的MQTT通讯,客户端在连接服务端时必须要将CONNECT报文中的cleanSession设置为False。

4、服务质量降级

(1)如下图所示,假如客户端A发布到主题1的消息采用QoS = 2,然而客户端B订阅主题1采用QoS = 1,在这种情况下,服务端会使用较低级别来提供服务,虽然客户端A发送到主题1的消息采用QoS=2,但是服务端发送主题1的消息给B时采用QoS=1,这是因为客户端B在订阅主题1时采用QoS=1。

(2)如下图所示,假如客户端A发布到主题1的消息采用QoS = 0,然而客户端B订阅主题1采用QoS = 1,在这种情况下,服务端会使用较低级别来提供服务,虽然客户端B订阅主题1消息采用QoS=1,但是由于客户端A发送主题1消息采用QoS=0,所以服务端发送消息给客户端B采用QoS=0。

5、服务质量等级的注意事项

(1)如果想在MQTT通讯中实现服务质量等级为1级(QoS=1),要分别对消息的发布端和接收端进行相应的设置,以下是具体需要采取的措施:

①接收端连接服务端时cleanSession设置为false。

②接收端订阅主题时QoS=1。

③发布端发布消息时QoS=1。

(2)如果想在MQTT通讯中实现服务质量等级为2级(QoS=2),要分别对消息的发布端和接收端进行相应的设置,以下是具体需要采取的措施:

①接收端连接服务端时cleanSession设置为false。

②接收端订阅主题时QoS=2。

③发布端发布消息时QoS=2。

(3)由于QoS1和QoS2都能确保客户端接收到消息,但是QoS1所占用的资源较QoS2占用资源更小,因此在网络资源较为珍贵的环境下,建议使用QoS1传输重要信息。

6、ESP8266 QoS应用

(1)PubSubClient库截至目前最高只支持1级QoS订阅和1级QoS发布,本节仅介绍如何使用ESP8266订阅和接收QoS=1的MQTT消息。

(2)先前介绍了connect函数的其中一种形式,在调用PubSubClient类的connect函数连接MQTT服务端时,唯一的函数参数为客户端ID,不过connect函数还有第二种形式,该形式下需要传入的函数参数不仅有客户端ID,还有cleanSession等配置参数,具体可见以下示例代码,其中其它配置参数在后续会有介绍,本节暂不展开。

需要注意的是,QoS=1的核心是"最少发一次",如果先前MQTT客户端与MQTT服务端连接时使用的是持久会话(cleanSession = False),那么当MQTT客户端离线后,MQTT服务端会为其保存所有其有订阅但未确认的QoS=1信息(这个"保存"与保留消息的性质不同),待MQTT客户端重新上电并再次连接(客户端ID与之前一致,且使用持久会话),MQTT服务端会立即将这些离线期间积压的消息发送给MQTT客户端

(3)先前介绍了subscribe函数的其中一种形式,在调用PubSubClient类的subscribe函数订阅主题时,唯一的函数参数为主题字符数组首地址,同样的,subscribe函数也有第二种形式,该形式下需要传入的函数参数不仅有主题字符数组首地址,还有订阅主题的QoS等级(数据类型为整型),具体可见以下示例代码。

(4)使用ESP8266订阅和接收QoS=1的MQTT消息示例代码:

cpp 复制代码
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
 
// 设置Wi-Fi接入信息
const char* ssid = "Zevalin_Computer";
const char* password = "00114514";
const char* mqttServer = "test.mosquitto.org";   //如无法使用,可更换为其它公用MQTT服务器地址

WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
 
const int subQoS = 1;     // 客户端订阅主题时使用的QoS级别(截止2025,不支持QoS = 2)
const bool cleanSession = false;     // 清除会话(如QoS>0,必须要设为false)
 
const char* willTopic = "willTopic"; // 遗嘱主题名称
const char* willMsg = "willMsg";     // 遗嘱主题信息
const int willQos = 0;               // 遗嘱QoS
const int willRetain = false;        // 遗嘱保留

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);     // 设置板上LED引脚为输出模式
  digitalWrite(LED_BUILTIN, HIGH);  // 启动后关闭板上LED
  Serial.begin(9600);               // 启动串口通讯
  
  WiFi.mode(WIFI_STA);
  connectWifi();
  
  // 设置MQTT服务器和端口号
  mqttClient.setServer(mqttServer, 1883);
  // 设置MQTT订阅回调函数(收到订阅的信息后执行的函数)
  mqttClient.setCallback(receiveCallback);
  // 连接MQTT服务器
  connectMQTTserver();
}
 
void loop() {
  if (mqttClient.connected()) { // 如果开发板成功连接服务器
    mqttClient.loop();          // 发送心跳信息,并判断是否收到MQTT信息,如有则调用处理函数
  }
  else {                        // 如果开发板未能成功连接服务器
    connectMQTTserver();        // 不断尝试连接服务器
  }
}
 
// 连接MQTT服务器并订阅信息
void connectMQTTserver(){
  // 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
  String clientId = "esp8266-" + WiFi.macAddress();

  /* connect函数的"完全体"形式
  boolean connect(const char* id, const char* user, 
                  const char* pass, const char* willTopic, 
                  uint8_t willQos, boolean willRetain, 
                  const char* willMessage, boolean cleanSession);             
                  */

  // 连接MQTT服务端
  if (mqttClient.connect(clientId.c_str(), NULL, NULL, willTopic, willQos, willRetain, willMsg, cleanSession)) { 
    Serial.println("MQTT Server Connected.");
    Serial.println("Server Address:");
    Serial.println(mqttServer);
    Serial.println("ClientId: ");
    Serial.println(clientId);
    subscribeTopic(); // 订阅指定主题
  }
  else {
    Serial.print("MQTT Server Connect Failed. Client State:");
    Serial.println(mqttClient.state());
    delay(5000);
  }   
}
 
// 收到信息后的回调函数(函数参数依次为主题、信息内容和信息长度,顺序是固定的)
void receiveCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message Received [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);  //以字符的形式打印信息内容数组的每一个字节数据
  }
  Serial.println("");
  Serial.print("Message Length(Bytes) ");
  Serial.println(length);
 
  if ((char)payload[0] == '1') {     // 如果收到的信息以"1"为开头
    digitalWrite(BUILTIN_LED, LOW);  // 点亮LED
    Serial.println("LED ON");
  }
  else {                           
    digitalWrite(BUILTIN_LED, HIGH); // 熄灭LED
    Serial.println("LED OFF");
  }
}
 
// 订阅指定主题
void subscribeTopic(){
  // 构建主题的字符数组
  String topicString = "Taichi-Maker-Sub-" + WiFi.macAddress()+"/+/data";
  char subTopic[topicString.length() + 1];  
  strcpy(subTopic, topicString.c_str());
  
  // 采用QoS=1订阅主题,并通过串口监视器输出是否成功订阅主题以及订阅的主题名称
  if(mqttClient.subscribe(subTopic, subQoS)){
    Serial.println("Subscrib Topic:");
    Serial.println(subTopic);
  }
  else{
    Serial.print("Subscribe Fail...");
  }  
}
 
void connectWifi(){
  WiFi.begin(ssid, password);
 
  //等待Wi-Fi连接,成功连接后输出成功信息
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi Connected!");  
  Serial.println(""); 
}
相关推荐
llilian_162 小时前
ptp从时钟 ptp授时模块 如何挑选PTP从时钟授时协议模块 ptp从时钟模块
数据库·功能测试·单片机·嵌入式硬件·测试工具
Truffle7电子3 小时前
STM32理论 —— FreeRTOS:中断管理、列表
stm32·单片机·嵌入式硬件·rtos
Hello_Embed3 小时前
嵌入式上位机开发入门(二):常用 API
笔记·stm32·嵌入式·信息与通信
Zevalin爱灰灰4 小时前
零基础入门学用物联网(ESP8266) 第二部分 MQTT基础篇(四)
单片机·物联网·mqtt·嵌入式·esp8266
贺小涛4 小时前
STM32学习
stm32·单片机·学习
凉、介4 小时前
SylixOS 多核启动
服务器·笔记·学习·嵌入式·sylixos
银月光科技4 小时前
细分市场带动下 UV LED行业发展潜力巨大
人工智能·物联网·uv
DA02214 小时前
系统移植-STM32MP1_TF-A概述
单片机·系统移植·stm32mp1
2301_822782826 小时前
C语言利用EasyX实现图形化界面的小游戏
c语言·单片机·图形化界面·lcd菜单·接口实现