【esp32s3】4 - 从零开始入门 MQTT
一、MQTT 科普
1、MQTT 全称
- MQTT 是什么?
MQTT(Message Queuing Telemetry Transport
)是一种轻量级的 物联网(IoT)通信协议 ,专为低带宽、高延迟或不稳定的网络环境设计。以下是核心特性:
MQTT协议核心特点
-
发布/订阅模型
- 解耦 消息生产者(
Publisher
)和消费者(Subscriber
),通过主题(Topic
)进行消息路由
- 解耦 消息生产者(
-
极简协议头
- 最小仅需2字节头部开销(对比HTTP的数百字节)
-
三种QoS等级
QoS 0
: 最多一次(可能丢包) # 适用于频繁多次的传感器数据QoS 1
: 至少一次(可能重复) # 默认级别QoS 2
: 恰好一次(可靠传输) # 不允许重复的付款场景
-
遗嘱消息(
Last Will
)- 设备异常离线时自动发布预设消息
2、数据通信的本质
- wifi,ip,tcp,mqtt,http,直接的关系什么?
- 我可以和以前学过的uart,modbus,寄存器数据作类比吗?分别对应哪些部分?
- 我知道所有数据其实都是0101的数据,然后根据规定划分为一个字节一个字节的十六进制数据,然后再根据协议划分不同数据类型。请问如何从这个角度理解本质?
- 可以使用示波器抓取到0101数据吗?可以用监听软件查看十六进制数据吗?
- 所以实际发送的数据是在mqtt的载荷吗? 然后mqtt本身的内容又是在tcp协议的数据包中吗? 层层嵌套吗?
让我们用嵌入式开发的思维来解构这些协议的本质关系,我会用寄存器级别的类比帮你建立认知框架:
(1)协议栈层级对应表
OSI模型层级 | 无线通信实例 | 有线通信类比 | 数据单元特征 |
---|---|---|---|
物理层 | WiFi射频调制 | UART电平信号 | 0101比特流 |
数据链路层 | 802.11 MAC帧 | Modbus帧结构 | 带CRC的十六进制数据块 |
网络层 | IP报文 | - | 包含源/目的地址的二进制包 |
传输层 | TCP分段 | Modbus TCP | 带序列号的可靠数据段 |
应用层 | MQTT/HTTP | Modbus功能码 | 人类可读的语义化指令 |
(2)二进制视角下的协议解剖
以 WiFi发送MQTT消息 为例,展示数据封装本质:
[IEEE 802.11 MAC头][IP头][TCP头][MQTT PUBLISH (发布)][Payload (载荷)]
└─射频调制→01流 └─0xC0A80101等IP地址 └─端口1883 └─Topic+Msg (主题+消息)
对比 UART传输Modbus:
[起始位][设备地址][功能码][寄存器地址][数据][CRC][停止位]
└─电平信号 → 0x01 0x03 0x0001 0x0002 0xCRC16
除非自己使用4G模块,TCP直连IP端口,否者不需要也不想再去看帧头帧尾数据帧的定义了。
(3)捕获原始数据
1. 示波器捕获0101数据的本质方法
- SPI/I2C/UART等低速总线的 高低电平 信号
- WiFi/蓝牙等高速的 射频 信号
2. 十六进制数据监听对比
- 电脑串口监听工具
AccessPort
- 网络封包分析工具
WireShark
(4)WireShark 分析工具
推荐教程:【2025最新】超详细Wireshark安装保姆级教程+Wireshark抓包(网络分析)!
官网下载最新版本:https://www.wireshark.org/download/
导航:Index of /download/win32/all-versions/Wireshark-win32-3.6.24.exe
- 安装一直下一步,安装后打开软件和终端,输入指令和过滤。查看效果:
ping www.baidu.com // 指令
// 157.148.69.151 是终端显示的目标IP
// 192.168.1.42 是本地路由器分配的局域网IP
ip.addr == 157.148.69.151 and icmp // 过滤器
- 数据太多太杂,需要准备好常用的 过滤器方法 !
- 其他不多赘述了,拿两张图留个大概印象。
3、MQTT 客户端与服务器
(1)MQTTX 与 EMQX 工具
推荐工具一:命令行 + 文本配置
Windows10上安装Mosquitto的步骤(win10、win11 安装mqtt)
【MQTT】mosquitto 的 “下载、交叉编译、使用” 详细教程,手把手搭建一个MQTT Broker
推荐工具二:可视化操作界面
客户端测试工具:MQTTX Web 或 MQTTX
服务器测试工具:EMQX-5.0.26服务Windows上安装使用 下载 EMQX 开源版 window版 v5.3.1
- 官网去掉了
EMQX
开源版图标入口,可以直接通过网址进入www.emqx.com/zh/downloads/broker
window
版本最新只v5.3.1
,更加新的版本只有linux和mac- 官网教程:MQTT 协议快速入门 2025:基础知识和实用教程
mqtt的分为客户端和服务器, 客户端与客户端和服务器之前靠订阅与发布达到解耦效果,不需要知道接收/发生方,只需要知道主题即可(主题名称可包含/字符,看起来类似路径格式,本质是名字,参考spiffs文件名,该名字甚至至此通配符+与#)和服务器的地址与端口即可。然后除了原本的数据传输外,mqtt还传输其他设置位和做到服务器用于识别设备和确保收发的其他机制?
(2)MQTT 基础架构
MQTT采用 发布/订阅(Pub/Sub)模式 ,其核心组件为:
-
Broker(服务器)
- 负责消息路由和分发的中央枢纽,所有通信都通过Broker中转。
- 不直接存储消息(除非配置持久化),仅根据主题匹配转发。
- 典型Broker实现:
Mosquitto
、EMQX
。
-
Client(客户端)
- 数量不限,可以是 发布者(Publisher)、订阅者(Subscriber) 或两者兼具。
- 示例场景:
- 传感器(
Publisher
)发布温度数据 →Broker
→ 手机App(Subscriber
)接收告警。 - 两个客户端可通过订阅相同主题实现双向通信。
- 传感器(
- 关键解耦特性:
- 空间解耦:客户端彼此无需知道对方IP或身份。
- 时间解耦:发布者和订阅者无需同时在线(需Broker支持持久会话)。
- 同步解耦:消息异步传递,不阻塞发送方。
(3)主题(Topic)
- 主题的作用
- 相当于消息的“分类标签”,Broker根据主题将消息路由给订阅者。
- 订阅者只需关注感兴趣的主题,无需与发布者直接交互。
- 主题命名规则
- 层级结构:用
/
分隔的多级字符串,如home/floor1/temperature
。 - 大小写敏感:
home/temp
与Home/Temp
视为不同主题。 - 允许字符:Unicode字母、数字及
_
、-
、/
等,避免空格和特殊符号。 - 长度限制:通常不超过65535字节(具体取决于Broker实现)。
- 层级结构:用
- 通配符
- 单级通配符
+
:匹配单一级别。
例:home/+/temperature
可匹配home/floor1/temperature
,但不可匹配home/floor1/room2/temperature
。 - 多级通配符
#
:匹配后续所有级别(必须放在末尾)。
例:home/#
可匹配home/floor1
和home/floor2/room3/light
。
- 单级通配符
- 注意事项
- 避免歧义:如
sensor/data
和sensor_data
易混淆,建议统一分隔符。 - 前缀分类:按业务划分,如
company/device/type
(tencent/weather/temperature
)。 - 保留主题:以
$
开头的主题通常为Broker内部使用(如$SYS/broker/load
)。
- 避免歧义:如
- 特殊主题
$SYS
- Broker通过
$SYS
主题发布系统信息(需配置开启),例如:$SYS/broker/clients/connected
:当前连接客户端数。$SYS/broker/uptime
:Broker运行时长。
- Broker通过
(4)客户端(Client)
- 客户端名称 (Client Name)
- 作用:本地标识,用于日志记录和调试
- 填写规范:任意有意义的字符串,如"ESP32_Floor1_Sensor"
- 特殊性:不会传输到Broker,仅本地使用
- ClientID
- 核心作用:Broker识别客户端的唯一标识
- 填写规范:
- 必须唯一,建议包含设备MAC地址
- 例:“ESP32_” + MAC地址后6位
- 高级特性:
Clean Session=false
(清除会话=假)时可实现持久会话
- 服务器地址 (Broker Address)
- 作用:MQTT代理服务器的网络位置
- 填写规范:
- IP格式:
192.168.1.100
- 域名:
broker.emqx.io
- 本地调试可用:localhost或127.0.0.1
- IP格式:
- 调试技巧:用
ping
命令测试连通性
- 端口 (Port)
- 标准端口:
- TCP Port:
1883
- WebSocket Port:
8083
- SSL/TLS Port:
8883
- Secure WebSocket Port:
8084
- TCP Port:
- 标准端口:
- 协议前缀说明
mqtt://
:标准MQTT协议(TCP层)- 示例:
mqtt://broker.hivemq.com:1883
- 示例:
ws://
:WebSocket明文传输- 示例:
ws://test.mosquitto.org:8083/mqtt
- 示例:
wss://
:WebSocket加密传输(相当于HTTPS)- 示例:
wss://your-broker.com:8084/mqtt
- 示例:
- Path (WebSocket特有)
- 作用:指定
MQTT over WebSocket
的访问路径 - 典型值:
/mqtt
- 示例:
wss://broker.example.com:8083/mqtt
- 作用:指定
(5)遗嘱(Last Will and Testament / LWT)
- 🌟 核心定义
遗嘱是客户端在连接时预先设定的应急消息,当客户端异常断开(非主动DISCONNECT)时,由Broker自动发布到指定主题。
- 🔧 工作机制
-
遗嘱注册
客户端在CONNECT
报文中携 -
触发条件
满足以下任意一条即触发:- TCP连接意外中断(网络故障)
- 心跳(Keepalive)超时未响应
- 客户端强制终止进程
-
消息发布
Broker立即以原始客户端身份发布遗嘱消息,流程完全自动化。
-
-
🚨 关键特性
特性 说明 即时性 通常在Broker检测到异常后秒级触发 身份继承 消息保留发布者的 ClientID
,订阅者可精准定位故障设备QoS保障 支持0/1/2级服务质量,确保消息可靠投递 保留消息联动 若设 will_retain=1
,遗嘱消息会成为主题的最后一个保留消息
- 💡 典型应用场景
- 设备离线报警
- 集群状态同步
网关设备通过遗嘱快速通知其他节点自身故障 - 资源清理
服务端收到遗嘱后触发自动化回收流程
4、WireShark 捕获分析
(1)发布(Publisher)订阅(Subscriber)
免费的公共 MQTT 服务器
Broker: broker.emqx.io
-
连接公共服务器
- 安装
MQTTX
,打开软件,创建客户端,连接官方提供的公共服务器,两种方式都可以连接上:- 前缀
mqtt://
+ TCP端口1883
- 前缀
ws://
+ WS端口8083
+ 路径 Path/mqtt
- 前缀
- 安装
- 然后测试主题的
订阅与发布
(收发)功能,使用通配符#
可以看到其他人发布的主题内容。
- 使用监听软件可以看到收发的原始十六进制数据,如果想分析MQTT协议帧头帧尾可以看这里。
-
连接本地服务器
- 下载
EMQX
,本地运行登录。切换到安装目录的bin
文件下,使用管理员终端调用cmd
。.\emqx.cmd install
将发行版安装为 Windows 服务.\emqx.cmd start
启动服务和 Erlang 节点.\emqx.cmd stop
停止服务和 Erlang 节点.\emqx.cmd restart
运行停止命令和启动命令.\emqx.cmd uninstall
卸载服务并终止正在运行的节点.\emqx.cmd ping
检查节点是否正在运行- 登录网址:
http://localhost:18083/
或http://127.0.0.1:18083/
或http://局域网IP:18083/
- 默认用户名:
admin
;默认密码:public
; 跳过修改密码。
- 下载
- 照着上面的例子,连接本地服务器,不同的就是服务器地址不一样。
- 服务器上有很多功能选项,可以监控主题,订阅,客户端,日志等内容,这里不赘述了。
- 至少目前对于 MQTT 的最基础的
连接 / 订阅 / 发布
功能掌握了。
(2)QoS 等级
📦 生活化比喻:
-
QoS0
(最多一次)→ 普通平邮- 快递员把信扔进邮箱就走,
不确认
你是否收到 - 示例:温度传感器周期性上报环境数据
- 快递员把信扔进邮箱就走,
-
QoS1
(至少一次)→ 挂号信+签收单- 快递员必须等你签收后才离开,但可能
重复
投递(网络重传时) - 示例:智能门锁状态上报
- 快递员必须等你签收后才离开,但可能
-
QoS2
(恰好一次)→ 公证处托管寄送- 快递员把信交公证处→你
确认
要收→公证处销毁备份 - 示例:金融终端交易指令
- 快递员把信交公证处→你
⚡ 关键差异总结:
- 报文数量:QoS0(1) < QoS1(2) < QoS2(4)
- 消息标识:QoS0无
MessageID
,QoS1/2有 - 重传机制:仅QoS1可能出现重复(
PUBLISH
重发) - 状态保存:QoS2需要Broker临时存储消息
💡 物联网场景:
- 传感器数据用QoS0(可容忍丢失)
- 设备状态用QoS1(平衡可靠性)
- 支付指令用QoS2(严格防重)
(Ⅰ)QoS 0
- 使用
Wireshark
捕捉到:
39 13:01:00.152893 192.168.1.42 44.232.241.40 MQTT 109 Publish Message [testtopic/himsad]
51 13:01:00.408882 44.232.241.40 192.168.1.42 TCP 60 1883 → 61513 [ACK] Seq=1 Ack=56 Win=4 Len=0
- 🔍 数据包逐帧分析
帧编号 | 时间戳 | 源地址 | 目标地址 | 协议 | 长度 | 描述 |
---|---|---|---|---|---|---|
39 | 13:01:00.152893 | 192.168.1.42 | 44.232.241.40 | MQTT | 109 | QoS0 PUBLISH [testtopic/himsad] |
51 | 13:01:00.408882 | 44.232.241.40 | 192.168.1.42 | TCP | 60 | TCP层ACK确认(非MQTT协议层) |
- 📜 MQTT QoS0 序列图
(Ⅱ)QoS 1
- 使用
Wireshark
捕捉到:
70 13:26:51.270187 192.168.1.42 44.232.241.40 MQTT 111 Publish Message (id=17659) [testtopic/himsad]
72 13:26:51.486878 44.232.241.40 192.168.1.42 MQTT 60 Publish Ack (id=17659)
73 13:26:51.541934 192.168.1.42 44.232.241.40 TCP 54 61696 → 1883 [ACK] Seq=58 Ack=7 Win=1029 Len=0
- 🔍 数据包逐帧分析
帧编号 | 时间戳 | 源地址 | 目标地址 | 协议 | 长度 | 关键信息 |
---|---|---|---|---|---|---|
70 | 13:26:51.270187 | 192.168.1.42 | 44.232.241.40 | MQTT | 111 | PUBLISH (QoS1, MessageID=17659) |
72 | 13:26:51.486878 | 44.232.241.40 | 192.168.1.42 | MQTT | 60 | PUBACK (对应ID=17659) |
73 | 13:26:51.541934 | 192.168.1.42 | 44.232.241.40 | TCP | 54 | TCP层ACK(确认PUBACK的传输) |
- 📜 MQTT QoS1 序列图
(Ⅲ)QoS 2
- 使用
Wireshark
捕捉到:
24 13:31:38.474955 192.168.1.42 44.232.241.40 MQTT 111 Publish Message (id=17661) [testtopic/himsad]
25 13:31:38.694727 44.232.241.40 192.168.1.42 MQTT 60 Publish Received (id=17661)
26 13:31:38.695360 192.168.1.42 44.232.241.40 MQTT 59 Publish Release (id=17661)
28 13:31:38.926004 44.232.241.40 192.168.1.42 MQTT 60 Publish Complete (id=17661)
30 13:31:38.967413 192.168.1.42 44.232.241.40 TCP 54 61696 → 1883 [ACK] Seq=63 Ack=13 Win=1029 Len=0
- 🔍 数据包逐帧分析
帧编号 | 时间戳 | 源地址 | 目标地址 | 协议 | 长度 | 关键信息 |
---|---|---|---|---|---|---|
24 | 13:31:38.474955 | 192.168.1.42 | 44.232.241.40 | MQTT | 111 | PUBLISH (QoS2, MID=17661) |
25 | 13:31:38.694727 | 44.232.241.40 | 192.168.1.42 | MQTT | 60 | PUBREC (确认收到) |
26 | 13:31:38.695360 | 192.168.1.42 | 44.232.241.40 | MQTT | 59 | PUBREL (释放消息) |
28 | 13:31:38.926004 | 44.232.241.40 | 192.168.1.42 | MQTT | 60 | PUBCOMP (最终确认) |
30 | 13:31:38.967413 | 192.168.1.42 | 44.232.241.40 | TCP | 54 | TCP ACK (确认PUBCOMP传输) |
- 📜 MQTT QoS2 序列图
- ⚙️ 协议交互详解
-
PUBLISH (QoS2)
- 携带消息ID(17661),Broker必须持久化消息但暂不投递
-
PUBREC (Receive Acknowledgement)
- Broker确认收到消息,承诺会处理
- 此时消息处于"半持久化"状态
-
PUBREL (Release)
- 客户端确认已准备好让Broker释放消息
- 如果此时断连,Broker会保留消息直到重新连接
-
PUBCOMP (Complete)
- Broker确认完成最终处理,可安全删除消息副本
- 保证严格一次(Exactly-Once)投递
-
TCP ACK
- 仅确认
PUBCOMP
报文的传输可靠性
- 仅确认
- 🕒 时序特征分析
阶段 | 耗时 | 说明 |
---|---|---|
PUBLISH→PUBREC | 219.772ms | Broker处理消息的延迟 |
PUBREL→PUBCOMP | 230.644ms | Broker完成持久化的时间 |
全程总耗时 | 492.458ms | 包含所有协议层和TCP确认 |
(3)心跳包
1546 13:32:38.699463 192.168.1.42 44.232.241.40 MQTT 56 Ping Request
1548 13:32:38.920869 44.232.241.40 192.168.1.42 MQTT 60 Ping Response
1549 13:32:38.976712 192.168.1.42 44.232.241.40 TCP 54 61696 → 1883 [ACK] Seq=65 Ack=15 Win=1029 Len=0
- 📜 心跳包序列图
- 🔍 心跳包工作原理
-
触发条件:
- 客户端在CONNECT报文指定
Keepalive
时间(例如60秒) - 若在此期间没有数据包交换,客户端必须发送PINGREQ
- 客户端在CONNECT报文指定
-
交互过程:
客户端 -> Broker: PINGREQ (0xC0) Broker -> 客户端: PINGRESP (0xD0)
-
TCP层确认:
- 帧1549是TCP对PINGRESP的传输层确认(与MQTT协议无关)
- ⏱ 您案例中的时序分析
帧编号 | 方向 | 类型 | 时间差 |
---|---|---|---|
1546 | Client → Broker | PINGREQ | - |
1548 | Broker → Client | PINGRESP | 221.406ms |
1549 | Client → Broker | TCP ACK | 55.843ms |
总计 | 277.249ms |
这个响应时间(221ms)可以反映Broker的当前负载状态
- ⚠️ 心跳包的核心作用
功能 | 说明 |
---|---|
连接保活 | 防止NAT超时或防火墙断开连接 |
服务端存活检测 | 超过1.5倍Keepalive时间未收到心跳,Broker会断开连接 |
网络质量监测 | Ping往返时间(RTT)可作为网络延迟指标(您的案例中RTT=221ms) |
(4)客户端申请连接
- 捕获的是 MQTT协议的完整连接建立过程,包含TCP三次握手和 MQTT
CONNECT
/CONNACK
交互。这是MQTT会话的初始化阶段:
162 13:49:44.623661 192.168.1.42 44.232.241.40 TCP 66 61807 → 1883 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
166 13:49:44.837480 44.232.241.40 192.168.1.42 TCP 66 1883 → 61807 [SYN, ACK] Seq=0 Ack=1 Win=2048 Len=0 MSS=1440 SACK_PERM=1 WS=512
167 13:49:44.837568 192.168.1.42 44.232.241.40 TCP 54 61807 → 1883 [ACK] Seq=1 Ack=1 Win=263424 Len=0
168 13:49:44.838043 192.168.1.42 44.232.241.40 MQTT 88 Connect Command
169 13:49:45.059595 44.232.241.40 192.168.1.42 TCP 60 1883 → 61807 [ACK] Seq=1 Ack=35 Win=2048 Len=0
171 13:49:45.292108 44.232.241.40 192.168.1.42 MQTT 78 Connect Ack
172 13:49:45.337502 192.168.1.42 44.232.241.40 TCP 54 61807 → 1883 [ACK] Seq=35 Ack=25 Win=263424 Len=0
- 📜 连接建立序列图
- 🔍 关键阶段拆解
- TCP三次握手(帧162-167)
帧编号 | 方向 | 标志位 | 关键参数 |
---|---|---|---|
162 | Client → Broker | SYN | MSS=1460, WS=256 |
166 | Broker → Client | SYN+ACK | MSS=1440, WS=512 |
167 | Client → Broker | ACK | Win=263424 |
- MQTT连接协商(帧168-172)
帧编号 | 协议 | 报文类型 | 关键信息 |
---|---|---|---|
168 | MQTT | CONNECT | 包含协议版本、Keepalive等参数 |
171 | MQTT | CONNACK | 返回连接状态码 |
169/172 | TCP | ACK | 传输层确认 |
(5)客户端申请断开
- 捕获的是 MQTT协议的优雅断开连接过程,包含协议层
DISCONNECT
和TCP
四次挥手。这是客户端主动发起的连接终止流程:
54 14:03:01.461420 192.168.1.42 44.232.241.40 MQTT 58 Disconnect Req
55 14:03:01.461632 192.168.1.42 44.232.241.40 TCP 54 61807 → 1883 [FIN, ACK] Seq=5 Ack=1 Win=1029 Len=0
62 14:03:01.729858 44.232.241.40 192.168.1.42 TCP 60 1883 → 61807 [FIN, ACK] Seq=1 Ack=6 Win=4 Len=0
63 14:03:01.729929 192.168.1.42 44.232.241.40 TCP 54 61807 → 1883 [ACK] Seq=6 Ack=2 Win=1029 Len=0
- 📜 断开连接序列图
- 🔍 关键帧分析
帧编号 | 方向 | 协议 | 关键动作 | 说明 |
---|---|---|---|---|
54 | Client → Broker | MQTT | DISCONNECT | 协议层断开请求 |
55 | Client → Broker | TCP | FIN+ACK | 传输层连接终止 |
62 | Broker → Client | TCP | FIN+ACK | Broker确认终止 |
63 | Client → Broker | TCP | ACK | 最终确认 |
二、阿里云 IoT MQTT
1、ESP32 测试 MQTT
- 常规先创建一个组件,在组件中编写测试程序:
- 为组件添加依赖:不要添加错了!!!我找半天才找到正确的依赖名称。
PRIV_REQUIRES mqtt # 添加私有依赖
- 为组件添加
Kconfig
文件,方便设置连接参数:
rsource "../components/mqtt_test/Kconfig" # 相对路径
menu "MQTT Client Configuration"config MQTT_BROKER_ADDRESS_URIstring "MQTT Broker URI"default "mqtt://broker-cn.emqx.io"helpComplete MQTT broker URI (e.g. mqtt://, mqtts://, ws://, wss://)Note: TLS requires mqtts:// prefix and certificate configurationconfig MQTT_BROKER_ADDRESS_PORTint "Broker Port Number"default 1883range 1 65535helpStandard ports:- TCP Port:1883- WebSocket Port:8083- SSL/TLS Port:8883- WebSocket Secure Port:8084- QUIC Port:14567config MQTT_BROKER_ADDRESS_PATHstring "WebSocket Path (WS/WSS only)"default ""helpLeave empty for TCP connections. WS/WSS requires path like "/mqtt"config MQTT_CREDENTIALS_CLIENT_IDstring "Client Identifier"default "esp_mqttx_c9067c5e"helpMust be unique per broker.config MQTT_CREDENTIALS_USERNAMEstring "Authentication Username"default "esp32_user"helpLeave empty if broker doesn't require authenticationconfig MQTT_CREDENTIALS_PASSWORDstring "Authentication Password"default "esp32_pass"helpPlaintext password (consider enabling TLS)config MQTT_TOPIC_PUBLISHstring "Default Publish Topic"default "/topic/esp32_c9067c5p"helpTopic for publishing messages.config MQTT_TOPIC_SUBSCRIBEstring "Default Subscribe Topic"default "/topic/esp32_c9067c5s"helpTopic for receiving messages. config MQTT_QOS_LEVELint "Default QoS Level"default 1range 0 2helpQuality of Service level:0 - At most once1 - At least once (default)2 - Exactly onceconfig MQTT_KEEPALIVE_SECint "Keepalive Period (seconds)"default 120range 30 900helpPing interval to maintain connection. Should be < broker's timeout setting
endmenu
- 编译成功后添加生成的宏定义到代码中:
#include <stdio.h>
#include <string.h>
#include "mqtt_test.h"#include "mqtt_client.h" // PRIV_REQUIRES mqtt # 添加私有依赖 不是 esp-mqtt,也不是esp_mqtt,也不是mqtt/esp-mqtt !!!!
#include "esp_log.h"#define MQTT_BROKER_ADDRESS_URI CONFIG_MQTT_BROKER_ADDRESS_URI // TCP MQTT 完整的 MQTT 代理 URI "mqtt://emqx@192.168.1.42" //
#define MQTT_BROKER_ADDRESS_PORT CONFIG_MQTT_BROKER_ADDRESS_PORT // TCP 端口
#define MQTT_BROKER_ADDRESS_PATH CONFIG_MQTT_BROKER_ADDRESS_PATH // TCP 不需要路径#define MQTT_CREDENTIALS_CLIENT_ID CONFIG_MQTT_CREDENTIALS_CLIENT_ID // 服务器识别客户端的唯一标识
#define MQTT_CREDENTIALS_USERNAME CONFIG_MQTT_CREDENTIALS_USERNAME // 用户名
#define MQTT_CREDENTIALS_PASSWORD CONFIG_MQTT_CREDENTIALS_PASSWORD // 用户密码#define MQTT_TOPIC_PUBLISH CONFIG_MQTT_TOPIC_PUBLISH // 发布主题
#define MQTT_TOPIC_SUBSCRIBE CONFIG_MQTT_TOPIC_SUBSCRIBE // 订阅主题static const char *TAG = "mqtt_test.c";static esp_mqtt_client_handle_t mqtt_handle = NULL; // 句柄, 是 esp_mqtt_client 的指针类型
- 编写mqtt初始化配置内容:
/*** @brief 初始化并启动MQTT客户端连接** @details 完成MQTT客户端核心配置和连接启动,包含:* - Broker服务器URI/端口/路径(WS专用)配置* - 客户端认证信息设置(ClientID/用户名密码)* - 自动注册全局事件回调* - 触发TCP连接建立** @note 关键配置说明:* 1. .set_null_client_id=false 强制使用显式客户端ID* 2. WS协议需单独设置.path字段* 3. 密码字段采用明文传输,建议配合TLS使用** @warning 需提前确保:* - 已配置有效的MQTT_BROKER_*系列宏定义* - 网络连接已就绪(Wi-Fi/Ethernet)** @see mqtt_event_callback 关联事件处理函数*/
void mqtt_start(void)
{esp_mqtt_client_config_t mqtt_cfg = {0}; // 默认初始化宏定义mqtt_cfg.broker.address.uri = MQTT_BROKER_ADDRESS_URI, // 完整的 MQTT 代理 URImqtt_cfg.broker.address.path = MQTT_BROKER_ADDRESS_PATH, // WS 端口专属的路径设置mqtt_cfg.broker.address.port = MQTT_BROKER_ADDRESS_PORT, // 符合 uri 的端口mqtt_cfg.credentials.client_id = MQTT_CREDENTIALS_CLIENT_ID, // 唯一标识mqtt_cfg.credentials.set_null_client_id = false, // 禁止自动idmqtt_cfg.credentials.username = MQTT_CREDENTIALS_USERNAME, // 用户名mqtt_cfg.credentials.authentication.password = MQTT_CREDENTIALS_PASSWORD, // 认证密码mqtt_handle = esp_mqtt_client_init(&mqtt_cfg); // 根据结构体初始化 mqttesp_mqtt_client_register_event(mqtt_handle, ESP_EVENT_ANY_ID, mqtt_event_callback, NULL); // 为mqtt的循环事件绑定回调函数esp_mqtt_client_start(mqtt_handle); // 启动 mqtt
}
- 编写事件回调函数:
- 获取回调数据前确保数据有效性,有些事件触发时貌似没有数据!
/*** @brief MQTT客户端事件回调处理函数** @details 处理所有MQTT客户端生命周期事件,包括连接状态、订阅响应和数据接收。* 采用事件驱动模型,通过 switch-case 分发不同事件类型,典型处理场景:* - 连接成功时自动订阅预设主题* - 解析接收到的MQTT消息内容和主题* - 记录关键事件日志用于调试** @param event_handler_arg 用户上下文参数(未使用)* @param event_base 事件基础类型(应为MQTT_EVENTS)* @param event_id 具体事件ID,见 esp_mqtt_event_id_t 枚举* @param event_data 事件数据载荷,需转型为 esp_mqtt_event_handle_t** @note 1. QoS1级别订阅需等待SERVER_ACK* 2. 事件处理应保持快速返回,避免阻塞事件循环* 3. 数据事件中payload可能不完整(分片情况需检查data->total_len)** @see esp_mqtt_event_id_t*/
static void mqtt_event_callback(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{esp_mqtt_event_handle_t data = (esp_mqtt_event_handle_t)event_data; // 获取数据句柄switch (event_id){case MQTT_EVENT_CONNECTED: // 服务器连接成功ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");esp_mqtt_client_subscribe_single(mqtt_handle, MQTT_TOPIC_SUBSCRIBE, 1); // 订阅主题break;case MQTT_EVENT_DISCONNECTED: // 服务器连接断开ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");break;case MQTT_EVENT_PUBLISHED: // 话题发布成功 ACK 返回ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED");break;case MQTT_EVENT_SUBSCRIBED: // 话题订阅成功 ACK 返回ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED");break;case MQTT_EVENT_DATA: // 订阅的主题收到数据ESP_LOGI(TAG, "MQTT_EVENT_DATA");if (data != NULL) {char *temp = malloc(data->data_len+1);memcpy(temp, data->data, data->data_len); // 提取内容并拷贝temp[data->data_len] = '\0';ESP_LOGI(TAG, "topic: %s", data->topic);ESP_LOGI(TAG, "payload: %s", temp);esp_mqtt_client_publish(mqtt_handle, MQTT_TOPIC_PUBLISH, temp, data->data_len, 1, 0); // 发布主题消息free(temp);}break;default:break;}
}
- 主函数就先启用wifi,等 ip 获取到后再启用 mqtt :
wifi_stack_init_with_auto_mode(); // 初始化WiFi网络栈并配置指定工作模式
vTaskDelay(10000 / portTICK_PERIOD_MS); // 简单延时10秒等待网络连接
mqtt_start(); // 初始化并启动MQTT客户端连接
- 编译下载监视:运行成功,打印内容,代表公共服务器连接成功:
I (3673) esp_netif_handlers: sta ip: 192.168.1.40, mask: 255.255.255.0, gw: 192.168.1.1
I (3673) wifi_use.c: got ip:192.168.1.40
I (10693) main_task: Returned from app_main()
I (10723) wifi:<ba-add>idx:1 (ifx:0, 10:55:38:e4:18:ea), tid:1, ssn:8, winSize:64
I (12153) mqtt_test.c: MQTT_EVENT_CONNECTED
I (14613) mqtt_test.c: MQTT_EVENT_SUBSCRIBED
- 检查订阅与发布的主题名,还有服务器的url与端口,打开 MQTTX 测试:
- esp32正常从服务器订阅到另一个客户端发布的主题,并将内容拷贝发布到另一个主题,再被PC的客户端订阅,实现两个客户端的通信。
I (12153) mqtt_test.c: MQTT_EVENT_CONNECTED
I (14613) mqtt_test.c: MQTT_EVENT_SUBSCRIBED
I (245223) mqtt_test.c: MQTT_EVENT_DATA
I (245223) mqtt_test.c: topic: /topic/esp32_c9067c5s
I (245223) mqtt_test.c: payload: {"msg": "hel666lo","him": "2"
}
I (245543) mqtt_test.c: MQTT_EVENT_PUBLISHED
- 将代码中服务器的 URI 修改为本地局域网的MQTT服务器,效果一致。不赘述了。
2、连接 阿里云 IoT
注册登录账号: 阿里云物联网平台控制台
下载
证书
,获悉设备ID
,用户名
,密码格式
: MQTT-TLS连接通信
获取
域名
格式: 查看和配置实例终端节点信息(Endpoint)
获取域名
参数: 支持的地域
计算
密码
:如何计算MQTT签名参数
图一:
图二:
图三:
图四:
-
控制台首页就有非常详细的指引和视频介绍,免费的公共实例足够测试。
-
第一次打开页面对一些名词感到疑惑,简单理解:
产品
:一个系列,包含很多设备设备
:单个设备,代表单个客户端物模型
:描述设备和读写的参数/数据三元组
:ProductKey
(产品密钥)、DeviceName
(设备名称)、DeviceSecret
(设备密钥)CA根证书
:阿里云物联网平台自签名证书,连接服务器时 TSL 所需
-
MQTT 连接时客户端所需的
设备ID,用户名,密码
由三元组
按规定格式生成。 -
Link SDK
是阿里云提供的单片机等开发平台的驱动库,包含MQTT驱动和证书加密等驱动。我使用的ESP32中本来就具备这些库,所以不需要移植。 -
我们只需要根据
三元组
生成设备ID,用户名,密码
填入 ESP32 的 MQTT 初始化结构体,还有填入CA根证书
即可。别忘了 还有域名
和端口号
!
1. 域名
- 别忘记还有加密 MQTT 的前缀
mqtts://
2. 端口号 和 CA根证书
- 建议下载
Link SDK
,把LinkSDK\external\ali_ca_cert.c
里面写好的格式扣过来。
3. 设备ID 和 用户名
- 设备ID 和 用户名 比较简单,直接拼凑字符串即可
4. 密码
- 密码 较麻烦一点,需要组合
字符串
,再根据设备密钥
和指定加密算法
计算的HEX
值,再将HEX
值转为字符串作为密码。
5. 代码实战
- 添加宏定义参数和
Kconfig
文件
rsource "../components/iot_test/Kconfig" # 相对路径
menu "Aliyun IoT Platform Configuration"config ALIYUN_DEVICE_NAMEstring "Device Name"default "EJyGG3cj5gR352vWfhu5"helpUnique device identifier in Aliyun IoT PlatformFormat requirements: 1-32 characters, alphanumeric + underscoreconfig ALIYUN_PRODUCT_KEYstring "Product Key"default "k0fxx47Q86H"helpProduct identifier issued by Aliyun IoT PlatformCase-sensitive 11 character stringconfig ALIYUN_DEVICE_SECRETstring "Device Secret"default "f6226009d975015ebdb926bebba2a1ee"helpDevice authentication key (32 character hex string)WARNING: Should be securely stored in productionconfig ALIYUN_REGION_IDstring "Region ID"default "cn-shanghai"helpAliyun data center region:- cn-shanghai (East China 2)- cn-qingdao (North China 1)- cn-beijing (North China 2)- cn-shenzhen (South China 1)- ap-southeast-1 (Singapore)config ALIYUN_MQTT_PORTint "MQTT Secure Port"default 8883range 1024 65535helpStandard ports:- 1883 (Standard MQTT)- 443 (HTTPS Tunnel)- 80 (WebSocket)Note: Aliyun requires TLS for productionconfig ALIYUN_KEEPALIVEint "MQTT Keepalive (seconds)"default 60range 30 300helpRecommended 60-120s for Aliyun IoT PlatformMust be shorter than server-side timeout
endmenu
- 添加头文件和组件依赖
#include <stdio.h>
#include <string.h>
#include "iot_test.h"#include "esp_log.h"
#include "esp_wifi.h" // 需要添加依赖 PRIV_REQUIRES esp_wifi
#include "mqtt_client.h" // PRIV_REQUIRES mqtt # 添加私有依赖 不是 esp-mqtt,也不是esp_mqtt,也不是mqtt/esp-mqtt !!!!
#include "mbedtls/md5.h" // 需要添加依赖 PRIV_REQUIRES mbedtls
#include "mbedtls/md.h"/* 设备证书 一机一密 */
#define DEVICE_NAME CONFIG_ALIYUN_DEVICE_NAME // 设备名称
#define PRODUCT_KEY CONFIG_ALIYUN_PRODUCT_KEY // 产品密钥
#define DEVICE_SECRET CONFIG_ALIYUN_DEVICE_SECRET // 设备密钥#define REGION_ID CONFIG_ALIYUN_REGION_ID // 华东 2 上海
#define ALIOT_MQTT_URL "mqtts://" PRODUCT_KEY ".iot-as-mqtt." REGION_ID ".aliyuncs.com" // 阿里 IoT 域名 (旧版公共实例终端节点信息)
#define ALIOT_MQTT_PORT CONFIG_ALIYUN_MQTT_PORT // TCP 端口号static const char *TAG = "iot_test.c";
- 添加 CA根证书,从官方 SDK 代码截取
/* 阿里云物联网平台自签名证书 */
#ifndef ALIYUN_IOT_S1
#define ALIYUN_IOT_S1 \"-----BEGIN CERTIFICATE-----\r\n" \"MIID3zCCAsegAwIBAgISfiX6mTa5RMUTGSC3rQhnestIMA0GCSqGSIb3DQEBCwUA\r\n" \"MHcxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFu\r\n" \"Z3pob3UxEzARBgNVBAoMCkFsaXl1biBJb1QxEDAOBgNVBAsMB1Jvb3QgQ0ExGzAZ\r\n" \"BgNVBAMMEkFsaXl1biBJb1QgUm9vdCBDQTAgFw0yMzA3MDQwNjM2NThaGA8yMDUz\r\n" \"MDcwNDA2MzY1OFowdzELMAkGA1UEBhMCQ04xETAPBgNVBAgMCFpoZWppYW5nMREw\r\n" \"DwYDVQQHDAhIYW5nemhvdTETMBEGA1UECgwKQWxpeXVuIElvVDEQMA4GA1UECwwH\r\n" \"Um9vdCBDQTEbMBkGA1UEAwwSQWxpeXVuIElvVCBSb290IENBMIIBIjANBgkqhkiG\r\n" \"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoK//6vc2oXhnvJD7BVhj6grj7PMlN2N4iNH4\r\n" \"GBmLmMdkF1z9eQLjksYc4Zid/FX67ypWFtdycOei5ec0X00m53Gvy4zLGBo2uKgi\r\n" \"T9IxMudmt95bORZbaph4VK82gPNU4ewbiI1q2loRZEHRdyPORTPpvNLHu8DrYBnY\r\n" \"Vg5feEYLLyhxg5M1UTrT/30RggHpaa0BYIPxwsKyylQ1OskOsyZQeOyPe8t8r2D4\r\n" \"RBpUGc5ix4j537HYTKSyK3Hv57R7w1NzKtXoOioDOm+YySsz9sTLFajZkUcQci4X\r\n" \"aedyEeguDLAIUKiYicJhRCZWljVlZActorTgjCY4zRajodThrQIDAQABo2MwYTAO\r\n" \"BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkWHoKi2h\r\n" \"DlS1/rYpcT/Ue+aKhP8wHwYDVR0jBBgwFoAUkWHoKi2hDlS1/rYpcT/Ue+aKhP8w\r\n" \"DQYJKoZIhvcNAQELBQADggEBADrrLcBY7gDXN8/0KHvPbGwMrEAJcnF9z4MBxRvt\r\n" \"rEoRxhlvRZzPi7w/868xbipwwnksZsn0QNIiAZ6XzbwvIFG01ONJET+OzDy6ZqUb\r\n" \"YmJI09EOe9/Hst8Fac2D14Oyw0+6KTqZW7WWrP2TAgv8/Uox2S05pCWNfJpRZxOv\r\n" \"Lr4DZmnXBJCMNMY/X7xpcjylq+uCj118PBobfH9Oo+iAJ4YyjOLmX3bflKIn1Oat\r\n" \"vdJBtXCj3phpfuf56VwKxoxEVR818GqPAHnz9oVvye4sQqBp/2ynrKFxZKUaJtk0\r\n" \"7UeVbtecwnQTrlcpWM7ACQC0OO0M9+uNjpKIbksv1s11xu0=\r\n" \"-----END CERTIFICATE-----\r\n"
#endifstatic const char *ali_ca_cert = ALIYUN_IOT_S1; // 阿里云物联网平台自签名证书
- 添加密码的加密算法打包计算函数,使用 ESP-IDF 库函数
- 注意:需要打开
menuconfig
配置菜单修改 启用 项:Component config -> mbedTLS -> Elliptic Curve Ciphers -> Elliptic Curve DSA
- 注意:需要打开
/*** @brief 计算字符串的HMAC-MD5哈希值** @param[in] key 用于HMAC计算的密钥字符串(必须以\0结尾)* @param[in] content 待计算的内容字符串(必须以\0结尾)* @param[out] output 输出缓冲区(必须至少分配16字节空间)** @note* 1. 使用mbedtls密码库实现,适用于嵌入式系统* 2. 该函数线程安全但不重入(因使用堆内存)* 3. 典型应用场景:* - MQTT密码生成* - 固件完整性校验* - 安全认证令牌生成** @warning* 1. 不进行输入参数有效性检查(调用方需确保)* 2. 在FreeRTOS中建议使用静态内存版本* */
static void sign_hmac_md5(char *key, char *content, unsigned char *output)
{mbedtls_md_context_t md5_ctx;const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_MD5); // 获取摘要信息mbedtls_md_init(&md5_ctx); // 此函数初始化消息摘要上下文,而不将其绑定到特定的消息摘要算法mbedtls_md_setup(&md5_ctx, md_info, 1);mbedtls_md_hmac_starts(&md5_ctx, (const unsigned char *)key, strlen(key));mbedtls_md_hmac_update(&md5_ctx, (const unsigned char *)content, strlen(content));mbedtls_md_hmac_finish(&md5_ctx, output);mbedtls_md_free(&md5_ctx);
}
- 拷贝上次MQTT的回调函数处理
/*** @brief MQTT客户端事件回调处理函数** @details 处理所有MQTT客户端生命周期事件,包括连接状态、订阅响应和数据接收。* 采用事件驱动模型,通过 switch-case 分发不同事件类型,典型处理场景:* - 连接成功时自动订阅预设主题* - 解析接收到的MQTT消息内容和主题* - 记录关键事件日志用于调试** @param event_handler_arg 用户上下文参数(未使用)* @param event_base 事件基础类型(应为MQTT_EVENTS)* @param event_id 具体事件ID,见 esp_mqtt_event_id_t 枚举* @param event_data 事件数据载荷,需转型为 esp_mqtt_event_handle_t** @note 1. QoS1级别订阅需等待SERVER_ACK* 2. 事件处理应保持快速返回,避免阻塞事件循环* 3. 数据事件中payload可能不完整(分片情况需检查data->total_len)** @see esp_mqtt_event_id_t*/
static void aliot_mqtt_event_callback(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{esp_mqtt_event_handle_t data = (esp_mqtt_event_handle_t)event_data; // 获取数据句柄switch (event_id){case MQTT_EVENT_CONNECTED: // 服务器连接成功ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");//esp_mqtt_client_subscribe_single(mqtt_handle, MQTT_TOPIC_SUBSCRIBE, 1); // 订阅主题break;case MQTT_EVENT_DISCONNECTED: // 服务器连接断开ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");break;case MQTT_EVENT_PUBLISHED: // 话题发布成功 ACK 返回ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED");break;case MQTT_EVENT_SUBSCRIBED: // 话题订阅成功 ACK 返回ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED");break;case MQTT_EVENT_DATA: // 订阅的主题收到数据ESP_LOGI(TAG, "MQTT_EVENT_DATA");if (data != NULL){char *temp = malloc(data->data_len + 1);memcpy(temp, data->data, data->data_len); // 提取内容并拷贝temp[data->data_len] = '\0';ESP_LOGI(TAG, "topic: %s", data->topic);ESP_LOGI(TAG, "payload: %s", temp);//esp_mqtt_client_publish(mqtt_handle, MQTT_TOPIC_PUBLISH, temp, data->data_len, 1, 0); // 发布主题消息free(temp);}break;default:break;}
}
- 拷贝上次初始化处理,根据要求计算
设备ID,用户名,密码
/*** @brief 初始化并启动MQTT客户端连接** @details 完成MQTT客户端核心配置和连接启动,包含:* - Broker服务器URI/端口/路径(WS专用)配置* - 客户端认证信息设置(ClientID/用户名密码)* - 自动注册全局事件回调* - 触发TCP连接建立** @note 关键配置说明:* 1. .set_null_client_id=false 强制使用显式客户端ID* 2. WS协议需单独设置.path字段* 3. 密码字段采用明文传输,建议配合TLS使用** @warning 需提前确保:* - 已配置有效的MQTT_BROKER_*系列宏定义* - 网络连接已就绪(Wi-Fi/Ethernet)** @see mqtt_event_callback 关联事件处理函数*/
void aliot_mqtt_start(void)
{/** mqttClientId: clientId+"|securemode=3,signmethod=hmacmd5,timestamp=132323232|"* mqttUsername: deviceName+"&"+productKey* mqttPassword: sign_hmac(deviceSecret,"clientId12345deviceNamedeviceproductKeypktimestamp789").toHexString();*//* 使用 mac 作为设备id */uint8_t mac[6] = {0};char clientId[32] = {0};esp_wifi_get_mac(WIFI_IF_STA, mac); // 获取 macsnprintf(clientId, sizeof(clientId),"%02X%02X%02X%02X%02X%02X",mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);/* 生成带参数的 设备ID */char mqttClientId[128] = {0};snprintf(mqttClientId, sizeof(mqttClientId),"%s|securemode=2,signmethod=hmacmd5|",clientId);/* 加密前的密码 */char cmd5[128] = {0};snprintf(cmd5, sizeof(cmd5),"clientId%sdeviceName%sproductKey%s",clientId, DEVICE_NAME, PRODUCT_KEY);/* 使用 MD5 将密码加密 */uint8_t password_hex[16] = {0};char password_str[33] = {0};sign_hmac_md5(DEVICE_SECRET, cmd5, password_hex); // 计算字符串的 HMAC-MD5 哈希值snprintf(password_str, sizeof(password_str),"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",password_hex[0], password_hex[1], password_hex[2], password_hex[3], password_hex[4], password_hex[5], password_hex[6], password_hex[7],password_hex[8], password_hex[9], password_hex[10], password_hex[11], password_hex[12], password_hex[13], password_hex[14], password_hex[15]);esp_mqtt_client_config_t mqtt_cfg = {0}; // 默认初始化宏定义mqtt_cfg.broker.address.uri = ALIOT_MQTT_URL; // 完整的 MQTT 代理 URImqtt_cfg.broker.address.port = ALIOT_MQTT_PORT; // 符合 uri 的端口mqtt_cfg.credentials.client_id = mqttClientId; // 唯一标识mqtt_cfg.credentials.set_null_client_id = false; // 禁止自动idmqtt_cfg.credentials.username = DEVICE_NAME "&" PRODUCT_KEY; // 用户名mqtt_cfg.credentials.authentication.password = password_str; // 认证密码/* Component config -> mbedTLS -> Elliptic Curve Ciphers -> Elliptic Curve DSA* 启用ECDSA。需要使用ECDSA-xxx TLS密码套件。 */mqtt_cfg.broker.verification.certificate = ali_ca_cert; // 证书mqtt_handle = esp_mqtt_client_init(&mqtt_cfg); // 根据结构体初始化 mqttesp_mqtt_client_register_event(mqtt_handle, ESP_EVENT_ANY_ID, aliot_mqtt_event_callback, NULL); // 为mqtt的循环事件绑定回调函数esp_mqtt_client_start(mqtt_handle); // 启动 mqtt
}
- 编译下载监视打印以下代表成功,阿里云也看到设备在线。
I (3943) wifi_use.c: got ip:192.168.1.40
I (10653) main_task: Returned from app_main()
I (12983) iot_test.c: MQTT_EVENT_CONNECTED
三、JSON 数据解析
推荐笔记: cJSON使用详细教程 | 一个轻量级C语言JSON解析器
- 前言:
- ESP32 中自带了
cJSON
库, 它巧妙的使用链式的方式实现解析. 学习它的实现逻辑, 吸收总结, 日后在自己的代码中运用相同思维, 是必要的.- 初学入门时学习的
STM32
类的驱动库, 学习了很多枚举/结构体/指针的使用方法, 现在应该多学习递归/链式/动态的使用方法了.
1、JSON 语法规则
{// 基本类型"string_example": "Hello World", // String类型"number_integer": 42, // Number类型(整数)"number_float": 3.1415926, // Number类型(浮点数)"boolean_true": true, // Boolean类型"boolean_false": false, // Boolean类型"null_value": null, // Null类型// 复合类型"object_example": { // Object类型"nested_string": "value","nested_number": 123,"nested_object": {"deep_key": "deep_value"}},"array_example": [ // Array类型"text", // 字符串元素100, // 数字元素false, // 布尔元素null, // null元素{"mixed_type": true}, // 对象元素["sub_array"] // 数组嵌套]
}
- 看起来非常像
python
中的 字典 类型, 使用键值对
方式代表索引数据, 键名必须是字符串, 值为常规类型.
1. 基本数据类型
类型 | 描述 | 示例 | 注意事项 |
---|---|---|---|
String | UTF-8编码的Unicode字符串 | "name": "张三" | 必须双引号包裹 |
Number | 双精度浮点数(包含整数) | "age": 25 | 不支持NaN /Infinity |
Boolean | 逻辑值 | "active": true | 仅接受true /false |
Null | 空值 | "address": null | 必须小写 |
2. 复合数据类型
类型 | 描述 | 示例 | 技术要点 |
---|---|---|---|
Object | 键值对集合(无序) | {"user": {"id": 1}} | 键必须是字符串 |
Array | 值的有序列表 | "tags": ["A", 250] | 可混合不同类型元素 |
2、cJSON 库
- 从 git 拉取仓库, 里面只需要源码
cJSON.c
和cJSON.h
文件, 其他是测试文件之类的东西.
git clone https://github.com/DaveGamble/cJSON.git
- 从头文件
cJSON.h
开始看. 略过开头的开源协议声明和pc/linux编译环境兼容性定义. 和 库版本定义:
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 18/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
(1)类型定义
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
宏定义 | 值(二进制) | 说明 | 内存表现(C语言层面) |
---|---|---|---|
cJSON_Invalid | 0 | 无效JSON项(解析错误或未初始化) | 无类型标识 |
cJSON_False | 1 (0001 ) | 布尔值false | int 存储为0 |
cJSON_True | 2 (0010 ) | 布尔值true | int 存储为1 |
cJSON_NULL | 4 (0100 ) | JSON的null 值 | 空指针(NULL ) |
cJSON_Number | 8 (1000 ) | 数值类型(整数/浮点数) | double 类型存储 |
cJSON_String | 16 (0001 0000 ) | 字符串类型 | char* 动态分配(需free ) |
cJSON_Array | 32 (0010 0000 ) | JSON数组 | 链表结构(cJSON* 链表头) |
cJSON_Object | 64 (0100 0000 ) | JSON对象(键值对集合) | 链表存储(键值对链表) |
cJSON_Raw | 128 (1000 0000 ) | 原始JSON字符串(未解析的内容) | char* 存储原始文本 |
- 库函数根据不同标记位执行不同操作:
- 常规数值类型就赋值存储
- 字符串类型需要申请空间拷贝字符串
- 数组或对象类型需要申请结构体空间,递归填充链式
Raw
是将部分重复固定的JSON
内容打包输入,比如物联网中通信版本等固定信息。
(2)对象结构体
/* The cJSON structure: */
typedef struct cJSON
{/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */struct cJSON *next;struct cJSON *prev;/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */struct cJSON *child;/* The type of the item, as above. */int type;/* The item's string, if type==cJSON_String and type == cJSON_Raw */char *valuestring;/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */int valueint;/* The item's number, if type==cJSON_Number */double valuedouble;/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */char *string;
} cJSON;
成员名 | 类型 | 作用域 | 详细说明 |
---|---|---|---|
next /prev | struct cJSON* | 数组/对象元素 | 双向链表指针,用于遍历同级元素(数组元素或对象键值对) |
child | struct cJSON* | 数组/对象容器 | 指向当前项的子元素链表头(数组的子项或对象的第一个键值对) |
type | int | 所有项 | 存储类型标记(cJSON_Number 等)及标志位(cJSON_IsReference 等) |
valuestring | char* | String /Raw 类型 | 存储字符串值或原始JSON文本(需malloc 分配,由cJSON_Delete 释放) |
valueint | int | Number 类型(已废弃) | 整数缓存(不推荐直接使用,可能丢失精度) |
valuedouble | double | Number 类型 | 实际存储所有数值(整数和浮点统一用此字段) |
string | char* | 对象键名 | 当前项在父对象中的键名(如{"key":value} 中的"key" ) |
-
每个
键值对
都是一个cJSON
结构体:string
代表键值对
键名,type
代表键值对
类型,相当于标志着这个结构体的意义:- 字符类型,就用
valuestring
操作 字符串 - 常规类型,就用
valuedouble
/valueint
操作 实数 - 复合类型,就用
child
指代 数组/对象 第一个元素,可以根据链式递归访问每一个元素。可以类比C语言
数组,名字就是第一个元素的地址,如何可以索引便利所有元素。
- 字符类型,就用
next
代表同级的下一个元素,直到最后一个元素时,下一个就是NULL
,代表结束,尾巴,不循环。prev
代表同级的上一个元素,直到第一个元素时,会再循环指向最后一个元素,代表可以快速索引到最后一个元素,而不是靠next
一直找下一个找到NULL
为止。同时也代表可以一直循环,不会找到NULL
。
-
链式的关键就是:
next
/prev
构成的横向循环关联,和child
构成的纵向单调向下关联。
(3)内存申请 接口定义
typedef struct cJSON_Hooks
{/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */void *(CJSON_CDECL *malloc_fn)(size_t sz);void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
成员 | 签名要求 | 调用约定 | 默认实现 |
---|---|---|---|
malloc_fn | 等效于malloc(size_t) | CJSON_CDECL | 标准库malloc |
free_fn | 等效于free(void*) | CJSON_CDECL | 标准库free |
- 允许用户替换
cJSON
默认的内存管理机制, 嵌入式平台默认情况就是使用malloc
和free
- 有些操作系统可以使用使用不同接口用来指定片外内存申请空间,比如
ESP32
- 或pc或linux平台需要特别指定接口。
- 有些操作系统可以使用使用不同接口用来指定片外内存申请空间,比如
3、cJSON 接口 API
(1)cJSON 解析
1. 主要 API
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
2. 示意流程图:
- 注意:
JSON
本身最外围是一个花括号,所以本身就一个复合对象类型,所以调用parse_value
后第一次必定是进入对象类型,然后循环遍历和递归解析所有元素。
3. 链式核心逻辑
parse_value
内调用各个类型的解析函数parse_object
/parse_array
等, 下面选择典型的对象类型:
static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer)
{/*** 省略其他部分 ***//* allocate next item */cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); // 申请对象 `键值对` 结构体空间if (new_item == NULL) // 申请成功{goto fail; /* allocation failure */}/* attach next item to list */if (head == NULL) // 头节点:为空{/* start the linked list */current_item = head = new_item; // 尾节点 = 头节点 = 新节点}else // 头节点:不为空{/* add to the end and advance */current_item->next = new_item; // 尾节点的下一个 = 新节点new_item->prev = current_item; // 新节点的上一个 = 尾节点current_item = new_item; // 尾节点 = 新节点}/*** 省略其他部分 ***/ if (head != NULL) { // 如果存在头节点,将头节点和尾节点循环关联head->prev = current_item; // 头节点的上一个 = 尾节点}item->type = cJSON_Object; // 指定类型item->child = head; // 指定这一级键值对的头节点,赋值给上一级/*** 省略其他部分 ***/
}
-
current_item
翻译作当前节点,最新节点,就是最新添加的节点,也就是最后一个节点,放在最后面的节点 —— 尾节点。 -
head
头节点 最后循环指向 尾节点,且会赋值给上一级的item->child
-
对象/数据类型 总结:
{}
数据没有键值对
情况下,那child
,next
,prev
都为NULL
。{“a”=1}
数据只有一个键值对
情况下,那child
,prev
都是头节点,这个头节点的next
为NULL
,它既是头节点,也是尾节点,所以prev
循环指向自己。{...}
数据有多个键值对
情况下,那child
是头节点,每个节点prev
指上一个,每个next
指向下一个,头节点的上一个快速索引尾节点,尾节点的next
是NULL
。- 尝试在代码中调试时,可以看链式指针的指向理解
{"name": "Foo", // ← item1.prev = item3"age": 30, // ← item1.next → item2, item2.prev → item1"active": true // ← item2.next → item3, item3.prev → item2
} // item3.next = NULL
(2)cJSON 获取
- 所以在调用
cJSON
解析函数后, 会一次性解析完字符串内容,并完成所有结构体的空间申请和填写。 - 后续获取只需要使用获取
API
查询键名和键值,查询过程中并不额外申请内存空间。(大概) - 内部自动判断类型和内容,符合才返回,否者为空。
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
- 获取的逻辑就是
next
遍历链表, 直到遇到符合 字符串参数的键名 或是NULL
- 下面是遍历对象的函数
get_object_item
, 遍历数组的函数get_array_item
类似, 不赘述了.
static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive)
{cJSON *current_element = NULL;if ((object == NULL) || (name == NULL)){return NULL;}current_element = object->child; // 获取头指针if (case_sensitive) // 区分大小写的判断{while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) // 比对字符串是否有效且完全相等{current_element = current_element->next; // 不符合就递增下一个节点}}else // 不区分大小写的判断{while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) // 打包函数,内部进行不区分大小写字符串的判断{current_element = current_element->next; // 不符合就递增下一个节点}}if ((current_element == NULL) || (current_element->string == NULL)) { // 最后找到的是 NULL 代表没有符合return NULL; // 返回空}return current_element; // 否者代表找到, 返回找到的节点
}
(3)cJSON 打印
- 打印时需要获取分配的存放字符串空间, 可以选择让程序自动计算, 如果条件宽裕的话.
- 保险起见避免溢出, 或单片机受限, 还是推荐使用预分配的方式.
- 注意:cJSON在估计它将使用多少内存方面并不总是100%准确,因此为了安全起见,请比实际需要多分配5个字节
- 和解析流程类似, 内部有
print_value
, 然后内部调用print_object
/print_array
等处理函数. 这里不赘述了.
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
(4)cJSON 释放
- 解析时会一次性申请完结构体的空间, 使用完后要释放掉.
- 打印时,如果使用动态分配,也会一次性申请完字符串的空间,使用完后要释放掉.
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
- 从下列函数实现就知道, 释放空间是沿着链表顺序释放下去,如果有复合类型就递归函数.
- 因此调用释放时, 只需要调用一次, 传入最顶部对象的头节点指针.
- 不能传入中间节点的指针, 不然会出现释放不完全的情况, 如果二次调用且传入不正确, 会因为链表已经被破坏而跑飞代码!!!
/* Delete a cJSON structure. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item)
{cJSON *next = NULL;while (item != NULL) // 当前节点有效{next = item->next; // 横向获取下一个节点if (!(item->type & cJSON_IsReference) && (item->child != NULL)) // 纵向有子链表{cJSON_Delete(item->child); // 递归删除子链表}if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) // 有申请 键值 字符串空间{global_hooks.deallocate(item->valuestring); // 释放 键值 字符串空间item->valuestring = NULL;}if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) // 有申请 键名 字符串空间{global_hooks.deallocate(item->string); // 释放 键名 字符串空间item->string = NULL;}global_hooks.deallocate(item); // 删除节点本身item = next; // 更新到下一个节点, 以此循环}
}
(5)cJSON 创建
- 和 解析流程类似, 不同的是人为 创建 和 添加 节点.
- 大部分接口都是内部相互调用, 可以选择封装度最高的, 省事, 不满足要求也可以自己调用各个独立接口.
具体不赘述了, 直接用到时再说再查,只要掌握链表的工作原理就好, 详情看下一章节的代码实例.
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Helper functions for creating and adding items to an object at the same time.* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
4. cJSON 实践
(1)创建组件添加依赖
- 创建组件,添加依赖和头文件,还有定义参考标准JSON字符串内容
#include <stdio.h>
#include <stdlib.h>
#include "cJSON_test.h"#include "cJSON.h" // 需要添加依赖 PRIV_REQUIRES json
#include "esp_log.h"static const char *TAG = "cJSON_test.c";static const char *cJSON_test_str = // 测试字符串
#if (1) // 手动添加折叠
"{"// 基本类型"\"string_example\" : \"Hello World\"," // String类型"\"number_integer\" : 42," // Number类型(整数)"\"number_float\" : 3.1415926," // Number类型(浮点数)"\"boolean_true\" : true," // Boolean类型"\"boolean_false\" : false," // Boolean类型"\"null_value\" : null," // Null类型// 复合类型"\"object_example\" : {" // Object类型"\"nested_string\" : \"value\",""\"nested_number\" : 123,""\"nested_object\" : {""\"deep_key\" : \"deep_value\"""}""},""\"array_example\" : [" // Array类型"\"text\"," // 字符串元素"100," // 数字元素"false," // 布尔元素"null," // null元素"{\"mixed_type\" : true}," // 对象元素"[\"sub_array\"]" // 数组嵌套"]"
"}";
#endif
(2)解析 与 获取
- 然后就是测试解析功能和获取功能,解析只需要一个函数
cJSON_Parse
即可,最后再释放掉cJSON_Delete
重点是中间的获取功能。详情看下面代码中的注释
/*** @brief 解析JSON字符串并提取各类型数据** @param json_string 需要解析的标准JSON格式字符串(需确保格式正确,不含注释)** @note 函数会递归处理嵌套的JSON对象和数组结构* @warning 传入的字符串必须是有效的JSON格式(RFC 8259标准),且:* - 不能包含注释(原始JSON标准不支持注释)* - 字符串需要以NULL结尾* - 内存由调用方管理** @par 异常处理:* - 解析失败会通过stderr输出错误位置* - 类型不匹配会跳过该字段** @see cJSON_Parse()* @see cJSON_Delete()* * */
void parse_json_test(void)
{const char *json_string = cJSON_test_str;// 1. 解析JSON字符串cJSON *root = cJSON_Parse(json_string); // 提供一个JSON块,这将返回一个可以查询的cJSON对象。if (root == NULL){const char *error_ptr = cJSON_GetErrorPtr(); // 用于分析失败的解析。这将返回指向解析错误的指针。if (error_ptr != NULL){ESP_LOGI(TAG, "Error parsing JSON before: %s\n", error_ptr);}return;}// 2. 解析基本类型cJSON *string_example = cJSON_GetObjectItemCaseSensitive(root, "string_example"); // 获取 get 对象项 ObjectItem 区分大小写 CaseSensitiveif (cJSON_IsString(string_example) && (string_example->valuestring != NULL)) // 键值对类型为字符串,字符串内容有效{ESP_LOGI(TAG, "String value: %s\n", string_example->valuestring); }cJSON *number_int = cJSON_GetObjectItemCaseSensitive(root, "number_integer"); // 同上if (cJSON_IsNumber(number_int)) // 键值对类型为数值{ESP_LOGI(TAG, "Integer value: %d\n", number_int->valueint);}cJSON *number_float = cJSON_GetObjectItemCaseSensitive(root, "number_float"); // 同上if (cJSON_IsNumber(number_float)) // 同上{ESP_LOGI(TAG, "Float value: %f\n", number_float->valuedouble);}cJSON *bool_true = cJSON_GetObjectItemCaseSensitive(root, "boolean_true"); // 同上if (cJSON_IsBool(bool_true)) // 键值对类型为布尔{ESP_LOGI(TAG, "Boolean true: %s\n", cJSON_IsTrue(bool_true) ? "true" : "false"); // 判断布尔类型}cJSON *bool_false = cJSON_GetObjectItemCaseSensitive(root, "boolean_false"); // 同上if (cJSON_IsBool(bool_false)) // 同上{ESP_LOGI(TAG, "Boolean false: %s\n", cJSON_IsTrue(bool_false) ? "true" : "false"); // 同上}cJSON *null_value = cJSON_GetObjectItemCaseSensitive(root, "null_value"); // 同上if (cJSON_IsNull(null_value)) // 键值对类型为null{ESP_LOGI(TAG, "Null value detected\n");}// 3. 解析复合类型 - 对象cJSON *object_example = cJSON_GetObjectItemCaseSensitive(root, "object_example"); // 同上if (cJSON_IsObject(object_example)) // 键值对类型为对象{ESP_LOGI(TAG, "\nParsing nested object:\n");cJSON *nested_string = cJSON_GetObjectItemCaseSensitive(object_example, "nested_string"); // 同上if (cJSON_IsString(nested_string)) // 同上{ESP_LOGI(TAG, " nested_string: %s\n", nested_string->valuestring);}cJSON *nested_number = cJSON_GetObjectItemCaseSensitive(object_example, "nested_number"); // 同上if (cJSON_IsNumber(nested_number)) // 同上{ESP_LOGI(TAG, " nested_number: %d\n", nested_number->valueint);}// 深度嵌套对象cJSON *nested_object = cJSON_GetObjectItemCaseSensitive(object_example, "nested_object"); // 同上if (cJSON_IsObject(nested_object)) // 同上{cJSON *deep_key = cJSON_GetObjectItemCaseSensitive(nested_object, "deep_key"); // 同上if (cJSON_IsString(deep_key)) // 同上{ESP_LOGI(TAG, " deep_key: %s\n", deep_key->valuestring);}}}// 4. 解析复合类型 - 数组cJSON *array_example = cJSON_GetObjectItemCaseSensitive(root, "array_example"); // 同上if (cJSON_IsArray(array_example)) // 同上{ESP_LOGI(TAG, "\nParsing array elements:\n");cJSON *array_item = NULL;int array_index = 0;cJSON_ArrayForEach(array_item, array_example) // 宏定义 for 循环, 有点类似 python 的写法, 循环遍历{ESP_LOGI(TAG, " [%d] ", array_index++); // 打印索引,在数组中,元素类型杂糅,且没有键名可以查找,只能用索引查找,遍历所有if (cJSON_IsString(array_item)){ESP_LOGI(TAG, "String: %s\n", array_item->valuestring);}else if (cJSON_IsNumber(array_item)){ESP_LOGI(TAG, "Number: %d\n", array_item->valueint);}else if (cJSON_IsBool(array_item)){ESP_LOGI(TAG, "Boolean: %s\n", cJSON_IsTrue(array_item) ? "true" : "false");}else if (cJSON_IsNull(array_item)){ESP_LOGI(TAG, "NULL value\n");}else if (cJSON_IsObject(array_item)){ESP_LOGI(TAG, "Object -> ");cJSON *mixed_type = cJSON_GetObjectItemCaseSensitive(array_item, "mixed_type");if (cJSON_IsBool(mixed_type)){ESP_LOGI(TAG, "mixed_type: %s\n", cJSON_IsTrue(mixed_type) ? "true" : "false");}}else if (cJSON_IsArray(array_item)){ESP_LOGI(TAG, "Sub-array -> ");cJSON *sub_item = cJSON_GetArrayItem(array_item, 0);if (cJSON_IsString(sub_item)){ESP_LOGI(TAG, "[0]: %s\n", sub_item->valuestring);}}}}// 清理内存cJSON_Delete(root); // 删除cJSON实体和所有子实体。
}
- 打印结果如下
I (10719) cJSON_test.c: String value: Hello WorldI (10719) cJSON_test.c: Integer value: 42I (10719) cJSON_test.c: Float value: 3.141593I (10719) cJSON_test.c: Boolean true: trueI (10729) cJSON_test.c: Boolean false: falseI (10729) cJSON_test.c: Null value detectedI (10729) cJSON_test.c:
Parsing nested object:I (10739) cJSON_test.c: nested_string: valueI (10739) cJSON_test.c: nested_number: 123I (10749) cJSON_test.c: deep_key: deep_valueI (10749) cJSON_test.c:
Parsing array elements:I (10759) cJSON_test.c: [0]
I (10759) cJSON_test.c: String: textI (10759) cJSON_test.c: [1]
I (10759) cJSON_test.c: Number: 100I (10769) cJSON_test.c: [2]
I (10769) cJSON_test.c: Boolean: falseI (10779) cJSON_test.c: [3]
I (10779) cJSON_test.c: NULL valueI (10779) cJSON_test.c: [4]
I (10779) cJSON_test.c: Object ->
I (10789) cJSON_test.c: mixed_type: trueI (10789) cJSON_test.c: [5]
I (10789) cJSON_test.c: Sub-array ->
I (10799) cJSON_test.c: [0]: sub_array
(3)创建 与 打印
- 创建需要注意添加主体,父子级键的顺序,容易复制粘贴看漏。
- 记得最后申请打印的字符串用完后要释放,创建的cJSON的对象也要释放。
/*** @brief 动态构建测试用JSON数据结构(包含完整类型示例)** @return 返回构建好的cJSON对象指针,调用方必须使用cJSON_Delete()释放内存** @note 此函数专门用于:* - 单元测试JSON序列化功能* - 演示cJSON对象构建最佳实践* - 生成设备配置模板数据** @warning 返回的对象指针必须检查NULL值!** @see cJSON_CreateObject()* @see cJSON_AddItemToArray()* @see cJSON_AddNumberToObject()* @see cJSON_Print()***/
void create_json_test(void)
{ESP_LOGI(TAG, "%s\n", cJSON_test_str); // 打印对比// 所有 cJSON_Create***() 函数都会申请一个结构体空间// 所有 cJSON_Add******() 函数内部都会调用 cJSON_Create***() 函数申请空间// 所有创建行为都会申请空间,并按添加顺序依次链式起来,最后不用时释放空间也是根据链式依次进行// 创建根对象cJSON *root = cJSON_CreateObject(); // 创建头指针,指向头结点,申请一个结构体空间// 添加基本类型cJSON_AddStringToObject(root, "string_example", "Hello World"); // 对象没有节点,第一个默认为首节点,添加在 child 属性内,并且首节点的 prev 是本身, next 是 nullcJSON_AddNumberToObject(root, "number_integer", 42); // 对象已经有首节点,通过首节点->prev获取到最后一个,添加在最后一个的 next 中,更新首节点的 prevcJSON_AddNumberToObject(root, "number_float", 3.1415926); // 依次类推,根据不同类型调用不同 apicJSON_AddTrueToObject(root, "boolean_true"); // 添加键值对类型布尔真cJSON_AddFalseToObject(root, "boolean_false"); // 添加键值对类型布尔假cJSON_AddNullToObject(root, "null_value"); // 添加键值对类型null// 添加对象类型cJSON *object_example = cJSON_CreateObject(); // 创建对象cJSON_AddStringToObject(object_example, "nested_string", "value"); // 在对象内添加键值对类型字符串cJSON_AddNumberToObject(object_example, "nested_number", 123); // 在对象内添加键值对类型数值cJSON *nested_object = cJSON_CreateObject(); // 同上cJSON_AddStringToObject(nested_object, "deep_key", "deep_value"); // 同上cJSON_AddItemToObject(object_example, "nested_object", nested_object); // 将对象添加在对象cJSON_AddItemToObject(root, "object_example", object_example); // 将对象添加在对象// 添加数组类型cJSON *array_example = cJSON_CreateArray(); // 创建数组cJSON_AddItemToArray(array_example, cJSON_CreateString("text")); // 创建一个只有键值没有键名的项添加到数组中cJSON_AddItemToArray(array_example, cJSON_CreateNumber(100)); // 同上cJSON_AddItemToArray(array_example, cJSON_CreateFalse()); // 同上cJSON_AddItemToArray(array_example, cJSON_CreateNull()); // 同上cJSON *mixed_object = cJSON_CreateObject(); // 同上cJSON_AddTrueToObject(mixed_object, "mixed_type"); // 同上cJSON_AddItemToArray(array_example, mixed_object); // 将对象添加到数组中cJSON *sub_array = cJSON_CreateArray(); // 同上cJSON_AddItemToArray(sub_array, cJSON_CreateString("sub_array")); // 同上cJSON_AddItemToArray(array_example, sub_array); // 将数组添加到数组中cJSON_AddItemToObject(root, "array_example", array_example); // 将数组添加到对象中// 打印JSONchar *json_str = cJSON_Print(root); // 获取字符串,动态分配空间if (json_str){ESP_LOGI(TAG, "%s\n", json_str);cJSON_free(json_str); // 释放空间}// 释放内存cJSON_Delete(root); // 释放空间
}
- 最后打印如下:
I (10799) cJSON_test.c: {"string_example" : "Hello World","number_integer" : 42,"number_float" : 3.1415926,"boolean_true" : true,"boolean_false" : false,"null_value" : null,"object_example" : {"nested_string" : "value","nested_number" : 123,"nested_object" : {"deep_key" : "deep_value"}},"array_example" : ["text",100,false,null,{"mixed_type" : true},["sub_array"]]}
I (10839) cJSON_test.c: {"string_example": "Hello World","number_integer": 42,"number_float": 3.1415926,"boolean_true": true,"boolean_false": false,"null_value": null,"object_example": {"nested_string": "value","nested_number": 123,"nested_object": {"deep_key": "deep_value"}},"array_example": ["text", 100, false, null, {"mixed_type": true}, ["sub_array"]]
}
四、阿里云 IoT 物模型
1. 闪灯测试
- 创建一个闪灯组件,方便测试。直接贴出内容:
- 值得注意是:我发现组件之间的编译会出现问题,检查了好久,发现重新编译时只删除
build
文件是不够的,需要删除managed_components
和dependencies.lock
,切记。
#include <stdio.h>
#include "WS2812_RGB_LED.h"// 使用指令添加下载 idf.py add-dependency "espressif/led_strip^3.0.1~1"
#include "led_strip.h" // 需要添加依赖 PRIV_REQUIRES led_strip
#include "esp_log.h"
#include "sdkconfig.h"#define WS2812_GPIO CONFIG_WS2812_GPIO // 指定引脚static const char *TAG = "WS2812_RGB_LED.c";
static led_strip_handle_t led_strip; // 组件句柄
static uint8_t s_led_state = 0; // 代表当前 led 状态/*** @brief 初始化WS2812 RGB LED控制* @note 使用RMT外设驱动LED灯带,配置GPIO和LED数量* @return void*/
void ws2812_rgb_led_init(void)
{/* LED带初始化与GPIO和像素数 */led_strip_config_t strip_config = {.strip_gpio_num = WS2812_GPIO,.max_leds = 1, // 船上至少有一个LED};led_strip_rmt_config_t rmt_config = {.resolution_hz = 10 * 1000 * 1000, // 10MHz.flags.with_dma = false,};ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));/* 设置所有LED关闭以清除所有像素 */led_strip_clear(led_strip);
}/*** @brief 控制WS2812 LED亮灭状态* @param led_state LED状态 true-亮 false-灭* @note 亮时设置为RGB(16,16,16)的颜色,灭时关闭所有LED* @return void*/
void ws2812_rgb_led_set_state(bool led_state)
{/* 如果可寻址LED已启用 */s_led_state = led_state;if (s_led_state){/* 为每种颜色设置使用RGB的LED像素从0(0%)到255 (100%) */led_strip_set_pixel(led_strip, 0, 16, 16, 16);/* 刷新条带发送数据 */led_strip_refresh(led_strip);ESP_LOGI(TAG, "led state true!");}else{/* 设置所有LED关闭以清除所有像素 */led_strip_clear(led_strip);ESP_LOGI(TAG, "led state false!");}
}/*** @brief 获取WS2812 LED当前状态* @note 通过读取当前LED颜色值判断状态* @return bool true-LED亮 false-LED灭*/
bool ws2812_rgb_led_get_state(void)
{return (s_led_state);
}
2. 物模型测试
-
注意物模型设置是在产品页面查看:
-
设备页面查看的是数据:
-
留坑…
五、阿里云 IoT 空中升级 OTA
- 留坑…