任务3 平台下发命令控制设备
一、任务描述
本任务基于 STM32F103C8T6 最小系统板、ESP-01S WiFi 模块,依托 MQTT 协议实现OneNET 平台远程下发指令,本地设备解析云端命令并执行对应动作。完成云端远程控制 LED 亮灭、风扇启停,实现云端下发指令→设备接收解析→外设动作响应的双向物联网通信,掌握 MQTT 主题订阅、云端指令解析、设备联动控制的完整流程。
二、任务目标
1. 知识目标
1)理解 MQTT订阅(Subscribe)机制,区分数据上报主题与平台指令下发主题。
2)掌握 OneNET 平台命令下发格式、指令报文解析规则。
3)熟悉 ESP-01S 订阅主题、串口接收云端指令的工作流程。
4)掌握 STM32 字符串解析、指令匹配、IO 与定时器联动控制逻辑。
2. 能力目标
1)能够使用 AT 指令让 ESP-01S 订阅 OneNET 平台命令主题。
2)能够编写串口接收程序,实时获取云端下发的指令数据。
3)能够解析不同指令,分别实现 LED 亮灭、风扇启停 / 调速控制。
4)能够在 OneNET 平台手动下发指令,验证设备远程响应效果。
3. 素质目标
1)培养指令解析严谨、分支逻辑清晰的嵌入式编程习惯。
2)提升双向通信调试、故障定位、软硬件联调能力。
3)建立 “云端下发 - 终端解析 - 设备执行” 的物联网双向控制思维。
三、任务重难点
1. 任务重点
1)OneNET 平台命令下发主题格式与 ESP-01S 主题订阅配置。
2)ESP-01S 订阅主题 AT 指令使用,MQTT 长连接保持。
3)STM32 串口中断 / 轮询方式接收云端指令、字符串解析判断。
4)根据不同指令,分别控制 LED 引脚电平、风扇 PWM 占空比。
5)平台下发指令、设备动作响应的全流程验证。
2. 任务难点
1)MQTT 订阅主题书写错误,导致无法接收平台指令。
2)串口数据接收不完整、指令报文截取与解析出错。
3)多指令分支逻辑编写,区分 LED 指令与风扇指令。
4)网络延迟、指令粘包、长连接掉线等问题排查。
5)云端指令格式不规范,设备无法识别。
四、知识准备
1.MQTT 订阅与双向通信基础
MQTT 协议分为发布和订阅两种核心行为,实现设备与平台双向交互:
- 发布(Publish):设备主动向平台上传温湿度、光照等采集数据(任务 2 已实现)。
- 订阅(Subscribe):设备主动监听指定主题,平台向该主题下发命令时,设备可实时接收。 设备只有成功订阅命令主题,才能收到 OneNET 下发的远程控制指令。
- 通信前提:设备已连上 WiFi、MQTT 服务器连接正常、设备保持在线。
2.OneNET 平台命令下发专用主题
设备必须订阅以下主题,才能接收平台下发的指令,格式固定,需替换为实际产品 ID和设备ID:
$sys/产品ID/设备ID/thing/property/set
该主题为 OneNET 物模型属性设置主题,专门用于平台下发控制指令、修改设备参数,设备订阅此主题后,可接收云端下发的控制报文。
3.基于MQTT协议栈的订阅实现原理
本任务采用嵌入式MQTT协议栈,手动组包、解包实现订阅与数据接收,核心知识点如下:
(1)MQTT 协议包结构体
MQTT_PACKET_STRUCTURE mqtt_packet = {NULL, 0, 0, 0};
该结构体用于管理 MQTT 通信数据包,存放数据缓冲区、数据包长度、偏移等信息,是协议栈组包、解包的基础载体。
- 主题拼接函数 snprintf
通过 snprintf 动态拼接完整订阅主题:
snprintf(topic_buf, sizeof(topic_buf), "$sys/%s/%s/thing/property/set", PROID, DEVICE_NAME);
- 作用:将宏定义的产品 ID (PROID)、设备名称 (DEVICE_NAME) 与固定主题字段拼接,生成完整可订阅主题;
- 优势:代码可复用,更换产品 / 设备时仅修改宏定义即可,无需修改业务逻辑。
(3)MQTT 订阅组包与发送流程
- 调用 MQTT_PacketSubscribe() 函数,按照 MQTT 协议规范组装订阅数据包,指定数据包 ID、QoS 等级、订阅主题;
- 调用 esp01s_SendData(),通过 ESP-01S 将组好的订阅数据包发送至 OneNET 服务器;
- 调用 MQTT_DeleteBuffer() 释放数据包内存,防止内存泄漏。
(4)QoS 服务等级
代码中使用 MQTT_QOS_LEVEL0:
- QoS 0:最多分发一次,消息发送后无需应答,适用于物联网常规控制指令、调试场景;
- 特点:传输速度快、资源占用低,为本任务默认配置。
4.MQTT报文解包与类型判断
设备通过串口接收 ESP-01S 转发的 MQTT 原始数据流后,调用 MQTT_UnPacketRecv() 对数据进行解包,识别报文类型,不同报文执行不同逻辑:
(1)主流报文类型说明
- MQTT_PKT_PUBLISH
平台下发控制指令、主动推送数据时使用该报文类型,是本任务核心接收报文,设备从中提取主题和负载数据。
- MQTT_PKT_PUBACK
设备上报数据后,平台返回的应答报文,代表数据上传成功。
- MQTT_PKT_SUBACK
设备发送订阅请求后,平台返回的订阅应答报文:
接收该报文且解析正常 → 打印 MQTT Subscribe OK,代表订阅主题成功;
解析异常 → 打印 MQTT Subscribe Err,代表订阅失败。
(2)Publish 报文解析函数
MQTT_UnPacketPublish() 用于解析平台下发的控制报文,提取三大关键信息:
- 主题字符串、主题长度;
- 报文负载(JSON 指令数据)、负载长度;
- 消息 QoS 等级、数据包 ID。
5. cJSON 库解析 JSON 指令(核心知识点)
平台下发的控制指令以JSON 格式封装,项目使用开源 cJSON 库完成解析,是嵌入式设备解析 JSON 数据的主流方案。
(1)cJSON 核心函数
- cJSON_Parse():将原始字符串格式的 JSON 数据,解析为 cJSON 结构体对象;
- cJSON_GetObjectItem():根据键名逐层读取 JSON 中的字段内容;
- cJSON_Delete():解析完成后释放 cJSON 对象内存,避免内存泄漏。
(2)分层解析逻辑
平台下发标准 JSON 指令格式示例:
{
"params":{
"LED":true,
"fan":false
}
}
解析流程:
解析根 JSON 对象 raw_json;
逐层获取子对象 params_json(读取 params 字段);
分别读取 led_json(LED 字段)、fan_json(风扇字段);
(3)字段类型判断与设备控制
通过 cJSON->type 判断字段值类型:
- cJSON_True:对应指令为开启,执行 LED 点亮、风扇启动逻辑;
- cJSON_False:对应指令为关闭,执行 LED 熄灭、风扇停止逻辑。
6. 外设联动控制逻辑
解析出指令后,调用底层驱动函数完成硬件控制:
LED 控制:调用 Led_Set() 函数,传入 LED_ON/LED_OFF 实现灯珠亮灭;
风扇控制:
- PWM_SetCompare1(30):设置 PWM 占空比为 30%,风扇以对应转速运转;
- PWM_SetCompare1(0):占空比置 0,风扇停止转动;
- HAL_GPIO_WritePin():配置风扇方向引脚,固定风扇转向。
7.函数整体执行流程
1)调用 OneNET_Subscribe():拼接主题、组装 MQTT 订阅包、发送订阅请求、释放内存;
2)循环接收串口数据,调用 OneNet_RevPro() 解析 MQTT 报文;
3)识别报文类型:订阅应答、数据下发、上传应答;
4)对平台下发的 Publish 报文做 JSON 解析;
5)根据解析结果执行 LED、风扇控制;
6)释放 cJSON 对象,等待下一条指令。
8.代码封装思想
- 功能模块化:将订阅功能封装为独立函数 OneNET_Subscribe(),将数据接收与解析封装为 OneNet_RevPro(),代码结构清晰、便于维护;
- 宏定义解耦:使用 PROID、DEVICE_NAME 宏定义产品 ID 与设备名,提升代码通用性;
- 内存管理:组包、解析完成后主动释放缓冲区与结构体,符合嵌入式系统内存使用规范。
五、硬件连接
ESP-01S模块与主控STM32F103C8T6的引脚连接如表1所示。
表1 ESP-01S模块与主控STM32F103C8T6的引脚连接
|
编号 |
ESP-01S模块 |
主控STM32F103C8T6 |
|
1 |
VCC |
3V3 |
|
2 |
GND |
GND |
|
3 |
TX |
PA3(USART2_RX) |
|
4 |
RX |
PA2(USART2_TX) |
硬件连接电路图如图1所示。
图1 硬件连接电路图
六、任务实现
任务 3.1 添加cJSON文件
1.将CJSON文件夹拷贝到工程根目录。
2.在项目中添加cJSON.c文件,如图2所示。

图2 添加cJSON.c文件
3.将\CJSON\目录添加到Include Paths中。
任务 3.2 修改 onenet.c 代码
1.添加头文件。
#include "cJSON.h"
2.修改OneNet_RevPro()函数,代码如下:
void OneNet_RevPro(unsigned char *cmd)
{
char *req_payload = NULL;
char *cmdid_topic = NULL;
unsigned short topic_len = 0;
unsigned short req_len = 0;
unsigned char qos = 0;
static unsigned short pkt_id = 0;
unsigned char type = 0;
short result = 0;
char *dataPtr = NULL;
char numBuf[10];
int num = 0;
cJSON *raw_json, *params_json, *led_json, *fan_json;
type = MQTT_UnPacketRecv(cmd);
printf("type=%d\n",type);
switch(type)
{
case MQTT_PKT_PUBLISH: //接收的Publish消息
result = MQTT_UnPacketPublish(cmd, &cmdid_topic, &topic_len, &req_payload, &req_len, &qos, &pkt_id);
if(result == 0)
{
char *data_ptr = NULL;
printf( "topic: %s, topic_len: %d, payload: %s, payload_len: %d\r\n",
cmdid_topic, topic_len, req_payload, req_len);
raw_json = cJSON_Parse(req_payload);
params_json = cJSON_GetObjectItem(raw_json,"params");
led_json = cJSON_GetObjectItem(params_json,"LED");
fan_json = cJSON_GetObjectItem(params_json,"fan");
if(led_json != NULL)
{
if(led_json->type == cJSON_True) Led_Set(LED_ON);
else Led_Set(LED_OFF);
}
if(fan_json!= NULL)
{
if(fan_json->type == cJSON_True)
{
PWM_SetCompare1(30); //开启风扇 正转30%
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
}
else
{
PWM_SetCompare1(0); // 停止
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
}
}
cJSON_Delete(raw_json);
}
break;
case MQTT_PKT_PUBACK: //发送Publish消息,平台回复的Ack
if(MQTT_UnPacketPublishAck(cmd) == 0)
printf( "Tips: MQTT Publish Send OK\r\n");
break;
case MQTT_PKT_SUBACK: //发送Subscribe消息的Ack
if(MQTT_UnPacketSubscribe(cmd) == 0)
printf( "Tips: MQTT Subscribe OK\r\n");
else
printf( "Tips: MQTT Subscribe Err\r\n");
break;
default:
result = -1;
break;
}
esp01s_Clear(); //清空缓存
if(result == -1)
return;
dataPtr = strchr(req_payload, ':'); //搜索':'
if(dataPtr != NULL && result != -1) //如果找到了
{
dataPtr++;
while(*dataPtr >= '0' && *dataPtr <= '9') //判断是否是下发的命令控制数据
{
numBuf[num++] = *dataPtr++;
}
numBuf[num] = 0;
num = atoi((const char *)numBuf); //转为数值形式
}
if(type == MQTT_PKT_CMD || type == MQTT_PKT_PUBLISH)
{
MQTT_FreeBuffer(cmdid_topic);
MQTT_FreeBuffer(req_payload);
}
}
任务 3.3 修改main.c 代码
1.在 /* USER CODE BEGIN 1 */后添加代码:
unsigned char *dataPtr = NULL;
unsigned short timeCount = 0; //发送间隔变量
2.在 /* USER CODE END 2 */前添加代码:
OneNET_Subscribe();
3.修改printf函数打印内容。
printf("任务3 平台下发命令控制设备\r\n");
4.修改while(1)的代码:
/* USER CODE BEGIN WHILE */
uint32_t dht_timer = 0;
while (1)
{
dataPtr = esp01s_GetIPD(0);
if(dataPtr != NULL)
OneNet_RevPro(dataPtr); //平台返回数据检测
if(HAL_GetTick() - dht_timer >= 5000) //5秒一次
{
dht_timer = HAL_GetTick();
DHT11_Read_Data(&temp, &humi);
light = Value_GY30();
printf("OneNet_SendData\r\n");
OneNet_SendData(); //发送数据 esp01s_Clear(); //清空数据缓存区
}
printf("temp=%d,humi=%d\n",temp,humi);
HAL_Delay(1); //延时1毫秒
// ===================== OLED =====================
// 第1行:温度
OLED_ShowChinese(1,1, CHN_WEN);
OLED_ShowChinese(1,2, CHN_DU);
OLED_ShowChinese(1,3, CHN_MH);
OLED_ShowChinese(1,6, CHN_SS); // ℃
// 第2行:湿度
OLED_ShowChinese(2,1, CHN_SHI);
OLED_ShowChinese(2,2, CHN_DU);
OLED_ShowChinese(2,3, CHN_MH);
OLED_ShowChinese(2,6, CHN_BF); // %
// 第3行:光照
OLED_ShowChinese(3,1, CHN_GUANG);
OLED_ShowChinese(3,2, CHN_ZHAO);
OLED_ShowChinese(3,3, CHN_MH);
// 第4行:知物学苑
OLED_ShowChinese(4,3, CHN_Z);
OLED_ShowChinese(4,4, CHN_W);
OLED_ShowChinese(4,5, CHN_X);
OLED_ShowChinese(4,6, CHN_Y);
//OLED_ShowString(4,6, "2026.5.9");
if(temp!=0)
OLED_ShowNum(1,6, temp, 2);
if(humi!=0)
OLED_ShowNum(2,6, humi, 2);
if(Value_GY30()!=0)
OLED_ShowNum(3,6,light,4); // 光照一般 0-65535,用4位
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
任务 3.4 编译、下载、查看结果
1.点击编译按钮
,编译程序。
2.点击下载按钮
,下载程序。
3.打开串口调试助手,按stm32f103c8t6主控板上的复位键,在串口调试助手中输出打印信息,如图3所示。

图3 输出打印信息
4.在OneNET平台设备详情页面,选择“设备调试”->”应用模拟器”->“属性设置”,即可远程控制设备上的LED和风扇的开启或关闭,如图4所示。

图4 远程控制设备上的LED和风扇的开启或关闭

459

被折叠的 条评论
为什么被折叠?



