目录
安装mqttx(云端部署)
安装mosquitto(本地部署)
编程,连接wifi
编程,连接mqtt,实现数据接收
实际效果展示:
附录:mqtt介绍
工作流程简述:
工作流程具体介绍:
1. 建立连接(CONNECT/CONNACK)
2. 订阅主题(SUBSCRIBE/SUBACK)
3. 发布消息(PUBLISH)
4. 心跳保持(PINGREQ/PINGRESP)
5. 断开连接(DISCONNECT)
主要特点
QoS(服务质量)等级
MQTT协议组成
看一眼课设要求,只需要连接MQTT并且下发显示英文字符串就行。下面开干。
首先老师给了一个mqtt的broker ip地址,这个很简单只要安装mqttx就直接能用。
当然由于我一开始不知道有mqttx,于是使用了Mosquitto。这个可以在本地部署mqtt的broker服务器,我也成功了。这两个我都会描述一下。
安装mqttx(云端部署)
前往官网MQTTX 下载
下载后,点击new connection,然后填写自己的配置。这里是学校老师给的。
连接成功。
安装mosquitto(本地部署)
先安装mosquitto,前往官网下载安装:Download | Eclipse Mosquitto,安装后进入文件夹,可以看到mosquitto.conf
添加监听窗口和基础设置。
listener 1885
allow_anonymous false
password_file ./passwd
persistence true
persistence_location ./mosquitto_data/
打开命令提示符(CMD),并切换到 Mosquitto 的安装目录。例如,如果 Mosquitto 安装在 C:\Program Files\mosquitto
,则运行:
cd C:\Program Files\mosquitto
创建密码文件。然后输入密码。
mosquitto_passwd -c ./passwd iot040
然后,使用以下命令启动 Mosquitto Broker:
mosquitto -v -c mosquitto.conf
这将根据 mosquitto.conf
文件中的配置启动 Mosquitto。
现在已经成功在本地搭建服务器了,套接字用的1885。
测试如下:
订阅主题(终端1)
mosquitto_sub -h localhost -t "test/topic" -v
-h localhost
:连接本地服务器-t "test/topic"
:订阅主题-v
:显示详细消息
发布消息(终端2)
mosquitto_pub -h localhost -t "test/topic" -m "Hello MQTT"
-m "Hello MQTT"
:消息内容
预期结果
终端1会立即收到消息:
test/topic Hello MQTT
在成功后,可以使用图形化客户端,比如:
- MQTT.fx(Windows/macOS/Linux)
- MQTT Explorer(跨平台)
连接配置:
- Broker Address:
localhost
- Port:
1883
- 用户名/密码:按照配置的来
编程,连接wifi
最后我的整个工程会开源到github。git还是很方便的。本课设采用ESP-IDF库编程,这是基于freertos的。
首先是wifi的初始化,
初始化需要执行以下操作:
1.创建FreeRTOS事件组用于同步
2.初始化ESP32的网络接口
3.配置并启动WiFi连接
4.阻塞等待连接成功或失败
先定义wifi的基本配置。
// 添加WiFi配置
#define WIFI_SSID "" //wifi名称
#define WIFI_PASSWORD "" //wifi密码
#define WIFI_MAXIMUM_RETRY 5 //最大重试次数
#define WIFI_CONNECTED_BIT BIT0 //WiFi连接成功标志
#define WIFI_FAIL_BIT BIT1 //WiFi连接失败标志
// FreeRTOS事件组,用于WiFi连接
EventGroupHandle_t s_wifi_event_group; //FreeRTOS事件组句柄,同步WiFi连接状态,允许程序等待WiFi状态变化,而不是持续轮询
int s_retry_num = 0;//跟踪WiFi连接尝试次数
按顺序编写wifi的初始化。这里面绝大多数函数都是库里面自带的。主要是#include "esp_wifi.h"。
// 初始化WiFi连接
void wifi_init_sta(void)
{
// 创建事件组,用于等待WiFi连接
s_wifi_event_group = xEventGroupCreate();
// 初始化网络接口
ESP_ERROR_CHECK(esp_netif_init());//初始化ESP32的TCP/IP协议栈
ESP_ERROR_CHECK(esp_event_loop_create_default());//创建一个默认的事件循环
esp_netif_create_default_wifi_sta();//创建WiFi站点(STA)模式的网络接口
// 初始化WiFi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// 注册WiFi事件处理函数
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&wifi_event_handler,
NULL,
&instance_got_ip));
// 配置WiFi
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PASSWORD,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false},
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
/* 等待WiFi连接或达到最大尝试次数 */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
// 标记变量为有意未使用,防止报警告
(void)bits;
}
然后编写WIFI事件处理函数,逻辑如下:
这个函数不是直接被调用的,而是通过ESP-IDF的事件系统自动触发,当执行esp_wifi_start()后:
WiFi驱动启动并触发WIFI_EVENT_STA_START事件
事件系统调用wifi_event_handler,传入此事件
处理函数检测到是启动事件,执行esp_wifi_connect()开始连接
如果连接成功,TCP/IP栈获取IP地址并触发IP_EVENT_STA_GOT_IP事件
事件系统再次调用wifi_event_handler,这次传入IP事件
处理函数设置WIFI_CONNECTED_BIT标志,通知主线程连接成功
// WIFI事件处理函数
void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
// 1. WiFi驱动启动完成事件
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
// WiFi已初始化完成,开始尝试连接到配置的AP
esp_wifi_connect();
}
// 2. WiFi断开连接事件(初次连接失败或运行中断开)
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
// 检查是否超过最大重试次数
if (s_retry_num < WIFI_MAXIMUM_RETRY)
{
// 未超过重试上限:增加计数并尝试重新连接
esp_wifi_connect();
s_retry_num++;
// ESP_LOGI(WIFI_TAG, "重试连接WiFi... (%d/%d)", s_retry_num, WIFI_MAXIMUM_RETRY);
}
else
{
// 超过重试上限:设置失败标志位,允许主程序继续执行
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
// ESP_LOGE(WIFI_TAG, "WiFi连接失败,已达最大重试次数");
}
}
// 3. 成功获取IP地址事件(连接成功的最终标志)
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
// 转换事件数据(包含IP地址信息)
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
// 标记变量为有意未使用(避免编译警告)
(void)event;
// 重置重试计数器,为后续可能的断线重连做准备
s_retry_num = 0;
// 设置连接成功标志位,允许主程序继续执行
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
// ESP_LOGI(WIFI_TAG, "成功连接WiFi,IP地址: "IPSTR, IP2STR(&event->ip_info.ip));
}
}
然后在main.c调用wifi_init_sta();即可接上wifi。
// WiFi连接
wifi_init_sta();
编程,连接mqtt,实现数据接收
先定义需要的变量。
其中esp_mqtt_client_handle_t 是ESP-IDF框架中定义的类型,实际上是一个指向MQTT客户端内部结构的指针,每个客户端一个,初始化为NULL说明此时没有连接任何客户端。
esp_mqtt_client_handle_t的使用流程:
创建: 通过esp_mqtt_client_init()函数创建:
mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
操作: 用于所有MQTT API调用:
// 订阅主题
esp_mqtt_client_subscribe(mqtt_client, MQTT_TOPIC, 0);
// 发布消息
esp_mqtt_client_publish(mqtt_client, MQTT_PUBLISH_TOPIC, "消息内容", 0, 1, 0);
状态检查: 在发送消息前检查连接:if (mqtt_client != NULL && mqtt_connected) {
// 执行MQTT操作
}
// MQTT配置 - 使用提供的服务器信息
#define MQTT_BROKER_URL "" //broker的url,比如mqtt://211.81.51.133:1885
#define MQTT_USERNAME "" //用户名
#define MQTT_PASSWORD "" //密码
#define MQTT_CLIENT_ID "" //CLIENT_ID
#define MQTT_TOPIC "top040" //订阅的主题
#define MQTT_PUBLISH_TOPIC "top039"// 发送的主题
// MQTT状态和消息缓冲区
esp_mqtt_client_handle_t mqtt_client = NULL; // ESP-IDF框架中定义的类型,MQTT客户端句柄(指针),指向MQTT客户端内部结构
bool mqtt_connected = false; // 跟踪MQTT客户端的连接状态
char mqtt_message[256] = {0};//存储从MQTT服务器接收到的最新消息内容
bool new_message_received = false; // 标记是否接收到新的MQTT消息(需要处理)
初始化:各个功能已经写注释里了。
// 初始化MQTT客户端
void mqtt_app_start(void)
{
// 配置结构体创建
esp_mqtt_client_config_t mqtt_cfg = {
.broker = {
.address = {
.uri = MQTT_BROKER_URL, // 服务器地址
},
.verification = {
.skip_cert_common_name_check = true, // 非TLS连接
}},
.credentials = {
.username = MQTT_USERNAME, // MQTT服务器认证用户名
.authentication = {
.password = MQTT_PASSWORD, // MQTT服务器认证密码
},
.client_id = MQTT_CLIENT_ID, // 客户端唯一标识符
}};
// 客户端初始化
mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
// 注册事件处理函数
esp_mqtt_client_register_event(mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); // ESP_EVENT_ANY_ID表示注册所有MQTT事件(连接、断开、收发消息等),当MQTT事件发生时,系统会自动调用mqtt_event_handler
esp_mqtt_client_start(mqtt_client); // 客户端启动
}
来吧,写回调函数,就是每个事件干什么事情。同上,也是自动调用。至此结束了。
// 回调函数指针
void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data;
esp_mqtt_client_handle_t client = event->client;
switch (event->event_id)
{
case MQTT_EVENT_CONNECTED:
ESP_LOGI(MQTT_TAG, "MQTT已连接");
mqtt_connected = true;
// 订阅主题
esp_mqtt_client_subscribe(client, MQTT_TOPIC, 0);
ESP_LOGI(MQTT_TAG, "已订阅主题: %s", MQTT_TOPIC);
// 发布连接成功消息
esp_mqtt_client_publish(client, MQTT_PUBLISH_TOPIC, "ESP32已上线", 0, 1, 0);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(MQTT_TAG, "MQTT已断开连接");
mqtt_connected = false;
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(MQTT_TAG, "MQTT订阅成功,msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(MQTT_TAG, "MQTT取消订阅,msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(MQTT_TAG, "MQTT消息已发布,msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(MQTT_TAG, "MQTT收到数据:");
ESP_LOGI(MQTT_TAG, "主题: %.*s", event->topic_len, event->topic);
ESP_LOGI(MQTT_TAG, "数据: %.*s", event->data_len, event->data);
// 将收到的消息复制到缓冲区
if (event->data_len < sizeof(mqtt_message) - 1)
{
memcpy(mqtt_message, event->data, event->data_len);
mqtt_message[event->data_len] = '\0'; // 确保字符串结束符
new_message_received = true; // 设置新消息标志
}
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(MQTT_TAG, "MQTT错误");
break;
default:
ESP_LOGI(MQTT_TAG, "其他MQTT事件 id:%d", event->event_id);
break;
}
}
搞定!下面在主函数里面调用初始化就好了。
//初始化
mqtt_app_start();
发送消息:
// 发送消息
if (mqtt_connected) {
// 准备消息内容
char message[100];
// 方式1:简单文本消息
strcpy(message, "Hello from ESP32");
// 方式2:格式化消息
sprintf(message, "Sensor reading: %.2f", sensor_value);
// 方式3:JSON格式消息(推荐用于复杂数据)
sprintf(message, "{\"device\":\"ESP32\",\"value\":%.2f,\"status\":\"%s\"}",
sensor_value, status ? "ON" : "OFF");
// 发送消息
esp_mqtt_client_publish(mqtt_client, MQTT_PUBLISH_TOPIC, message, 0, 1, 0);
/*参数说明:mqtt_client: MQTT客户端句柄
MQTT_PUBLISH_TOPIC: 发布主题
message: 消息内容
0: 消息长度(0表示自动计算)
1: QoS级别(0=最多一次,1=至少一次,2=只有一次)
0: 消息保留标志(0=不保留,1=保留)*/
ESP_LOGI(TAG, "已发送MQTT消息: %s", message);
}
接收消息有2种方式,一种是在while循环里,通过轮询。
// 在主循环中
while (1) {
// 检查是否收到新消息
if (new_message_received) {
// 重置消息标志
new_message_received = false;
// 处理收到的消息
ESP_LOGI(TAG, "收到MQTT消息: %s", mqtt_message);
// 解析和处理消息...
if (strcmp(mqtt_message, "LED_ON") == 0) {
led_set_state(true);
} else if (strcmp(mqtt_message, "LED_OFF") == 0) {
led_set_state(false);
}
// 可选:显示消息到LCD
clear_text_area(lcd_buffer, LCD_H_RES, LCD_V_RES);
draw_string(lcd_buffer, 0, 0, "MQTT消息", 0xFFFF, LCD_H_RES);
draw_string(lcd_buffer, 0, FONT_HEIGHT + 2, mqtt_message, 0xFFFF, LCD_H_RES);
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, LCD_H_RES, LCD_V_RES, lcd_buffer);
}
// 其他任务处理...
vTaskDelay(10 / portTICK_PERIOD_MS);
}
第二种是直接在MQTT事件处理函数中处理(适用于需要立即响应的场景)
在mqtt_event_handler函数的MQTT_EVENT_DATA部分添加处理逻辑:
case MQTT_EVENT_DATA:
ESP_LOGI(MQTT_TAG, "MQTT收到数据: %.*s", event->data_len, event->data);
// 将收到的消息复制到缓冲区
if (event->data_len < sizeof(mqtt_message) - 1) {
memcpy(mqtt_message, event->data, event->data_len);
mqtt_message[event->data_len] = '\0'; // 确保字符串结束符
// 立即处理特定命令
if (strcmp(mqtt_message, "EMERGENCY_STOP") == 0) {
// 紧急处理,不等待主循环
system_emergency_stop();
}
// 设置标志供主循环处理
new_message_received = true;
}
break;
实际效果展示:
初始化
接收消息
发送消息:
首先需要创建一个订阅,确保主题和宏定义的MQTT_PUBLISH_TOPIC一样。
然后在程序里面发送,就可以收到了!
附录:mqtt介绍
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种轻量级的发布/订阅模式消息传输协议,专为低带宽、高延迟或不稳定的网络环境设计。是应用层协议,类似HTTP。QTT已成为物联网领域最流行的通信协议之一,被广泛应用于各种智能设备和系统中。
工作流程简述:
如图所示:
发布者(Publisher):左侧的冰箱代表一个物联网设备,它通过MQTT协议向中间的MQTT Broker(代理服务器)发布(Publish)消息。通常消息包含传感器数据、状态信息等。
代理服务器(MQTT Broker):MQTT Broker负责接收和分发消息。它起到“中间人”的作用,不需要设备之间直接通信。
订阅者(Subscriber):右侧的手机、电脑和服务器代表多个订阅者。它们向MQTT Broker订阅(Subscribe)自己感兴趣的主题(Topic)。Topic是消息的分类标识,采用层级结构(如"sensor/temperature")。
一旦有消息发布到Broker,Broker会把消息分发给所有已经订阅该主题的设备。
MQTT通过“发布-订阅”模式,发布者只负责发送消息到Broker,订阅者只需从Broker接收感兴趣的数据。这样可以实现高效、灵活的物联网通信架构。
工作流程具体介绍:
1. 建立连接(CONNECT/CONNACK)
步骤说明:
- 客户端发送CONNECT
-
- 携带参数:
-
-
ClientID
:客户端唯一标识(如device123
)Clean Session
:是否清除历史会话(true
=重新开始,false
=恢复之前订阅)- 可选的
Username/Password
(若Broker启用认证)
-
- 服务端回复CONNACK
-
- 返回状态码:
-
-
0
:连接成功4
:无效用户名/密码5
:未授权(如客户端无权限连接)
-
2. 订阅主题(SUBSCRIBE/SUBACK)
步骤说明:
- 客户端发送SUBSCRIBE
-
- 指定订阅的 主题过滤器(如
sensor/#
匹配所有传感器主题) - 设置 QoS级别(0/1/2,决定消息传递质量)
- 指定订阅的 主题过滤器(如
- 服务端回复SUBACK
-
- 确认订阅成功,或返回失败原因(如主题格式无效)
3. 发布消息(PUBLISH)
步骤说明:
- 发布者发送PUBLISH
-
Topic
:消息分类(如home/temperature
)Message
:实际数据(如{"value":25.5}
)QoS
:发布时的服务质量Retain
:是否保留消息(新订阅者立即收到最后一条)
- Broker转发消息
-
- 根据订阅者的QoS级别,可能需确认(QoS≥1时)
QoS处理差异:
- QoS 0:Broker直接转发,无确认。
- QoS 1:Broker存储消息直到收到订阅者的
PUBACK
。 - QoS 2:四次握手确保消息不重复(PUBREC→PUBREL→PUBCOMP)。
4. 心跳保持(PINGREQ/PINGRESP)
作用:
- 在长连接无数据交互时(如
Keep Alive
时间内),通过心跳包保持连接活跃。 - 若Broker未及时响应
PINGRESP
,客户端会判定连接断开并重连。
5. 断开连接(DISCONNECT)
行为说明:
- 客户端主动断开时发送
DISCONNECT
,Broker释放该客户端的资源。 - 若未发送
DISCONNECT
直接断开,Broker会根据遗嘱消息(Last Will)通知其他客户端。
主要特点
- 轻量级:协议头小,消息精简,适合带宽受限环境
- 发布/订阅模式:解耦消息生产者和消费者
- 低功耗:适合电池供电的物联网设备
- 支持QoS:提供三种消息服务质量等级
- 支持TCP/IP:运行在TCP协议之上
QoS(服务质量)等级
MQTT定义了三种QoS级别:
- QoS 0(最多一次):发后即忘(无确认)。消息发送一次,不保证送达。
- QoS 1(至少一次):至少一次(需PUBACK)。确保消息送达,但可能有重复。
- QoS 2(恰好一次):恰好一次(4次握手)。确保消息只送达一次。
MQTT协议组成
MQTT协议由以下核心组件构成:
组件 | 说明 |
固定头部(Fixed Header) | 每个MQTT报文都以一个固定头部开始,包含了报文类型和标志位等重要信息。 |
可变报头(Variable Header) | 可选字段,如报文标识符(Packet ID)、主题名长度等 |
有效载荷(Payload) | 实际传输的数据(如消息内容),在PUBLISH报文中最常见 |
核心控制报文类型:
报文类型 | 值 | 用途 |
CONNECT | 1 | 客户端连接服务器(发送用户名/密码、Clean Session标志等) |
CONNACK | 2 | 服务器响应连接(返回状态码,如0=成功,5=认证失败) |
PUBLISH | 3 | 发布消息到指定主题(含QoS、Retain标志) |
SUBSCRIBE | 8 | 客户端订阅主题(可指定多个主题及QoS) |
UNSUBSCRIBE | 10 | 取消订阅 |
PINGREQ | 12 | 客户端心跳请求(保持长连接) |
PINGRESP | 13 | 服务器心跳响应 |
DISCONNECT | 14 | 客户端主动断开连接 |