模块3 WIFI物联网通信-项目2 数据上报下发-任务3 平台下发命令控制设备

任务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 通信数据包,存放数据缓冲区、数据包长度、偏移等信息,是协议栈组包、解包的基础载体。

  1. 主题拼接函数 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和风扇的开启或关闭

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值