本章主要学习 lwIP 提供的 MQTT 协议文件使用,通过 MQTT 协议将设备连接到OneNet(中国移动物联网开放平台),实现远程互通。由于 MQTT 协议是基于 TCP 协议实现的,所以我们只需要在单片机端实现 TCP 客户端程序并使用 lwIP 提供的 MQTT 文件来连接OneNet。
本章分为如下几个部分:
55.1 MQTT 协议简介
55.2 硬件设计
55.3 软件设计
55.4 下载验证
55.1 MQTT 协议简介
(1) MQTT 是什么?
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种基于 发布/订阅(Publish/Subscribe) 模式的 轻量级 消息传输协议,专为低带宽、高延迟、不稳定网络的物联网环境设计。该协议构建于 TCP/IP 协议上,由 IBM 在1999 年发布,目前最新版本为 v3.1.1。 MQTT 最大的优点在于可以以极少的代码和有限的带宽,为远程设备提供实时可靠的消息服务。做为一种低开销、低带宽占用的即时通讯协议, MQTT在物联网、小型设备、移动应用等方面有广泛的应用, MQTT 协议属于应用层。
(2) MQTT 协议特点
MQTT 是一个基于客户端与服务器的消息发布/订阅传输协议。 MQTT 协议是轻量、简单开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、医疗设备、智能家居、及一些小型化设备中已广泛使用。
(3) MQTT 协议原理及实现方式
实现 MQTT 协议需要:客户端和服务器端 MQTT 协议中有三种身份:发布者(Publish)、
代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器, 消息发布者可以同时是订阅者,如下图所示。

图 55.1.1 MQTT 订阅和发布过程
MQTT 传输的消息分为:主题(Topic)和消息的内容(payload)两部分:
Topic: 可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容
(payload);
**Payload:**可以理解为消息的内容,是指订阅者具体要使用的内容。
(4)MQTT 与传统 HTTP 等请求/响应模式的区别
传统模式 (Client-Server):客户端直接向服务器的一个特定端点(URL)发送请求,并等待该端点的响应。通信双方紧密耦合,必须同时在线。
发布/订阅模式:
- **发布者 (Publisher):**产生并发送消息的客户端(如您的 ESP32 传感器)。它不关心谁接收消息,只关心把消息发送到哪个主题(Topic);
- **订阅者 (Subscriber):**接收消息的客户端(如您的手机 App 或云端应用)。它不关心消息来自哪里,只关心它订阅了哪些主题;
- **代理 (Broker):**消息中转站(如阿里云物联网平台)。它的核心职责是接收发布者的消息,并根据主题将其转发给所有对应的订阅者。
优势:实现了通信双方的完全解耦。发布者和订阅者互不知晓对方的存在,无需同时在线,只需分别与 Broker 交互即可。这使得系统极具弹性和可扩展性。
55.1.1 MQTT 协议实现原理
(1)客户端与代理服务器建立连接
要在客户端与代理服务端建立一个TCP连接,建立连接的过程是由客户端主动发起的,代理服务一直是处于指定端口的监听状态,当监听到有客户端要接入的时候,就会立刻去处理。客户端在发起连接请求时,携带客户端 ID、账号、密码(无账号密码使用除外,正式项目不会允许这样)、心跳间隔时间等数据。代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接,如果允许则建立会话标识并保存,绑定客户端 ID 与会话,并记录心跳间隔时间(判断是否掉线和启动遗嘱时用)和遗嘱消息等,然后回发连接成功确认消息给客户端,客户端收到连接成功的确认消息后,进入下一步(通常是开始订阅主题,如果不需要订阅则跳过)。如下图所示:

图 55.1.1.1 客户端与代理服务器建立连接示意图
(2)客户端向服务器订阅消息
客户端将需要订阅的主题经过 SUBSCRIBE 报文发送给代理服务,代理服务则将这个主题记录到该客户端 ID 下(以后有这个主题发布就会发送给该客户端),然后回复确认消息SUBACK报文,客户端接到 SUBACK报文后知道已经订阅成功,则处于等待监听代理服务推送的消息,也可以继续订阅其他主题或发布主题,如下图所示:

图 55.1.1.2 客户端向服务器订阅示意图
(3)客户端向代理服务器发送主题
当某一客户端发布一个主题到代理服务后,代理服务先回复该客户端收到主题的确认消息,该客户端收到确认后就可以继续自己的逻辑了。但这时主题消息还没有发给订阅了这个主题的客户端,代理要根据质量级别( QoS)来决定怎样处理这个主题。所以这里充分体现了是MQTT 协议是异步通信模式,不是立即端到端反应的,如下图所示:

图 55.1.1.3 客户端向代理服务器发送主题示意图
如果发布和订阅时的质量级别 QoS 都是至多一次,那代理服务则检查当前订阅这个主题的客户端是否在线,在线则转发一次,收到与否不再做任何处理。这种质量对系统压力最小。
如果发布和订阅时的质量级别 QoS 都是至少一次,那要保证代理服务和订阅的客户端都有成功收到才可以,否则会尝试补充发送(具体机制后面讨论)。这也可能会出现同一主题多次重复发送的情况。这种质量对系统压力较大。
如果发布和订阅时的质量级别 QoS 都是只有一次,那要保证代理服务和订阅的客户端都有成功收到,并只收到一次不会重复发送(具体机制后面讨论)。这种质量对系统压力最大。
55.1.2 配置远程服务器
实验功能简介: 通过lwIP 连接OneNet实现数据上存。首先我们注册OneNet,并开启一个MQTT代理服务,通过ESP32向MQTT代理发布温度信息并实时更新,然后ESP32订阅该主题,ESP32可以收到MQTT发送的温度信息,下面为实现过程,同时关于下面详细说明参开OneNet物模型数据交互章节说明:
(1)注册ONENET,开启一个MQTT服务
第一步:注册ONENET云平台(地址:https://open.iot.10086.cn/ ),点击开发者中心,如下图所示:

第二步:点击**产品开发,**如下图所示:

第三步:下面可以随便选一个,后续根据真实项目选,本次只是一个演示测试,如下图所示:

第四步:按下图选,注意:接入协议选MQTT,数据协议选OneJson,联网方式选WI-FI,如下图所示:

第五步:点击产品开发,如下图所示:

第六步:添加自定义功能点,注意数据类型、取值范围和读写类型的选择,如下图所示:

第七步:点击保存,如下图所示:

第八步:点击关闭,如下图所示:

第九步:点击下一步,然后一直点下一步,如下图所示:

第十步:最终点击发布,如下图所示:

第十一步:回到产品开发页面,可以看到已发布,如下图所示: 在云端添加设备,如下图所示:

第十二步:在设备管理-设备详情里,查询设备所属产品ID和设备秘钥,实验要用到,本例中,设备接入相关参数如下:
(2)通过MQTT.fx,连接Onenet云端,与MQTT代理进行通信
在进行正式实验之前,先使用 MQTT.fx 客户端模拟成另一个设备或云端应用,与MQTT代理进行通信 ,下载MQTT,fx(点击即可跳转到下载地址)工具,下载后安装。
MQTT.fx 是一款基于 Java 开发的跨平台(Windows, macOS, Linux)MQTT 客户端软件。它的核心作用是模拟一个 MQTT 客户端,让我们能够:
1)连接到任何 MQTT 代理(Broker),如公共的 HiveMQ、Mosquitto,或私有的如阿里云物联网平台、EMQX 等;
2)发布消息到任何主题;
3)订阅任何主题并接收消息;
4)可视化整个消息收发过程。
对于项目开发而言,可以将 MQTT.fx 模拟成另一个设备或云端应用,用来测试您的 ESP32 程序是否正确工作。
如何使用 MQTT.fx 测试OneNet项目:
第 1 步:MQTT.fx客户端配置
打开MQTT.fx客户端,进入客户端配置页面

设置Profile Name,设置接入地址与端口,并设置Client ID、User Name与Password,其中参数设置方式如下:
Client ID:设备名称
User Name:产品id
Password:token
设置方法见下面,设置完成后点击右下角OK:

然后点击connect,此时设备在页面处于在线状态。


参数设备名称、产品ID、token获取见下面所示:


Token生成,token的生成及使用工具:点击接入安全认证中下载,同时里面有详细说明:

其中et的获取如下,通过时间戳转换工具,设置过期的时间后转换获取:

第 2 步:开始测试
实验1:测试属性上报:
订阅属性上报结果通知消息为了确保设备上传消息确实被平台所接受处理,设备可以订阅系统 topic 获取属性上报结果消息,属性上报的topic为:sys/产品ID/设别名称/thing/property/post,本次实验如下sys/sq8cUJ2H38/ESP32_01/thing/property/post,设置见下图,可以在页面看到数据更新成功:

可以从MQTT.fx的LOG中看到Publish成功,数据发送成功:

**实验2:**设备属性设置
设备侧需要收到平台下发的数据,需要订阅:sys/产品ID/设别名称/thing/property/set,本次实验如下sys/sq8cUJ2H38/ESP32_01/thing/property/set,设置见下图:

设置完成后,在网页上进行设别调试,如下图所示,上图LOG显示数据收到:

到此我们配置OneNet远程服务器,并通过MQTT.fx与远程服务器进行通信,详细说明可以看OneNet的文档说明。
55.2 硬件设计
- 例程功能
本章实验功能简介:通过lwIP 连接OneNet实现数据上存。首先我们注册OneNet,并开启一个MQTT代理服务,通过ESP32向MQTT代理发布温度信息并实时更新,然后ESP32订阅该主题,ESP32可以收到MQTT发送的温度信息。
- 硬件资源
1) LED 灯
LED-IO1
2) XL9555
IIC_INT-IO0(需在 P5 连接 IO0)
IIC_SDA-IO41
IIC_SCL-IO42
3) SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
4) ESP32-S3 内部 WiFi
- 原理图
本章实验使用的 WiFi 为 ESP32-S3 的片上资源,因此并没有相应的连接原理图。
55.3 软件设计
55.3.1 程序流程图
本实验的程序流程图:

图 55.3.1.1 程序流程图
55.3.2 程序解析
在本章节中,主要关注两个文件: lwip_demo.c 和 lwip_demo.h。 lwip_demo.h 文件主要
定义了配置OneNet MQTT 相关参数。主要关注点是 lwip_demo.c 文件中的函数,在 lwip_demo 函数中,配置了相关的 MQTT 参数,并创建了一个名为mqtt_event_handler 的事件回调函数。这个事件回调函数通过获取 MQTT 事件 ID 来处理连接过程中所需的操作。接下来,将分别详细解释 lwip_demo 函数和 mqtt_event_handler 事件回调函数。首先先看下 lwip_demo.h 文件,定义了配置OneNet MQTT 相关参数:
/* MQTT地址与端口 */
#define HOST_RUL "mqtt://mqtts.heclouds.com"
#define HOST_PORT 1883
/* 根据三元组内容计算得出的数值 */
#define CLIENT_ID "ESP32_01" /* 客户端ID */
#define USER_NAME "sq8cUJ2H38" /* 客户端用户名 */
#define PASSWORD "version=2018-10-31&res=products%2Fsq8cUJ2H38%2Fdevices%2FESP32_01&et=2548944000&method=md5&sign=WbDxXbg%2BPRcCy%2FpoPXJ9xQ%3D%3D" /* 由MQTT_Password工具计算得出的连接密码 */
/* 发布与订阅 */
#define DEVICE_PUBLISH "$sys/" USER_NAME "/" CLIENT_ID "/thing/property/post" /* 发布主题 */
#define DEVICE_SUBSCRIBE "$sys/" USER_NAME "/" CLIENT_ID "/thing/property/set" /* 订阅主题 */
void lwip_demo(void);
接下来看下lwip_demo()函数:
/**
* @brief lwip_demo进程
* @param 无
* @retval 无
*/
void lwip_demo(void)
{
uint8_t temperature = 0; //真实项目可以从传感器中获取
/* 设置MQTT客户端配置 */
esp_mqtt_client_config_t mqtt_cfg =
{
.broker.address.uri = HOST_RUL,
.broker.address.port = HOST_PORT,
.credentials.client_id = CLIENT_ID,
.credentials.username = USER_NAME,
.credentials.authentication.password = PASSWORD,
};
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
if (client == NULL)
{
ESP_LOGE(TAG, "MQTT客户端初始化失败");
return;
}
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_err_t err = esp_mqtt_client_start(client);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "MQTT客户端启动失败,错误码: %d", err);
return;
}
ESP_LOGI(TAG, "MQTT客户端初始化完成");
while(1)
{
if (g_publish_flag == 1)
{
temperature = ((temperature > 100) ? 0:(temperature + 10));
sprintf(mqtt_publish_data,
"{\"id\": \"123\",\"version\": \"1.0\",\"params\": {\"temperature\": {\"value\": %d}}}",
temperature);
/* 定期发布数据 */
int msg_id = esp_mqtt_client_publish(client, DEVICE_PUBLISH, mqtt_publish_data, 0, 1, 0);
if (msg_id < 0)
{
ESP_LOGE(TAG, "MQTT发布失败");
}
else
{
ESP_LOGI(TAG, "MQTT发布成功,msg_id = %d", msg_id);
}
}
vTaskDelay(pdMS_TO_TICKS(10000)); // 每10秒发布一次数据
}
}
这个函数主要负责 MQTT 的连接配置。它首先创建了一个 MQTT 控制块,用于存储配置参数以及发送和接收数据。接着,它定义了一个回调函数,用于处理和响应 MQTT 连接过程中的各种事件。最后,它启动 MQTT 并发送连接请求到服务器。一旦成功连接到 MQTT 服务器,它就可以开始循环发布数据。现在,了解回调函数的工作原理。
int g_publish_flag = 0;/* 发布成功标志位 */
static const char *TAG = "ONENET_MQTT";
char mqtt_publish_data[200] = {'0'};
static const char test_data[] = "{"
"\"id\": \"123\","
"\"version\": \"1.0\","
"\"params\": {"
"\"temperature\": {"
"\"value\": 10"
"}"
"}"
"}";
/**
* @brief 错误日记
* @param message :错误消息
* @param error_code :错误码
* @retval 无
*/
static void log_error_if_nonzero(const char *message, int error_code)
{
if (error_code != 0)
{
ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code);
}
}
/**
* @brief 注册接收MQTT事件的事件处理程序
* @param handler_args:注册到事件的用户数据
* @param base :处理程序的事件库
* @param event_id :接收到的事件的id
* @param event_data :事件的数据
* @retval 无
*/
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id);
esp_mqtt_event_handle_t event = event_data;
esp_mqtt_client_handle_t client = event->client;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED: /* 连接事件 */
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
/* 订阅主题 */
msg_id = esp_mqtt_client_subscribe(client, DEVICE_SUBSCRIBE, 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
/* 发布测试数据 */
msg_id = esp_mqtt_client_publish(client, DEVICE_PUBLISH, test_data, 0, 1, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
g_publish_flag = 1;
break;
case MQTT_EVENT_DISCONNECTED: /* 断开连接事件 */
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
g_publish_flag = 0;
break;
case MQTT_EVENT_SUBSCRIBED: /* 订阅成功事件 */
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED: /* 取消订阅事件 */
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED: /* 发布事件 */
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA: /* 接收数据事件 */
ESP_LOGI(TAG, "MQTT_EVENT_DATA");
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
/* 处理接收到的数据 */
if (event->data_len > 0) {
// 在这里添加处理接收数据的代码
}
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT)
{
log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err);
log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err);
log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno);
ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno));
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
在这个回调函数中,主要处理与 MQTT 的交互过程。当系统接收到 MQTT 服务器的连接应答时,它会发送订阅主题报文。当系统接收到 MQTT 服务器的订阅应答报文时,它会发布一个订阅完成报文。因此,每个状态事件都需要读者根据项目需求进行相应的修改。
55.4 下载验证
程序下载成功后,打开OneNet物联网平台设备管理,可以看到此时的设备处于连接状态,同时收到ESP32发送的temperature数据。

图55.4.1 OneNet物联网平台收到ESP发送的数据

图55.4.2 ESP32收到OneNet物联网平台发送的数据