57. ESP32的WiFi功能

一、ESP32简介

  ESP32 是乐鑫公司(ESPRESSIF)继 ESP8266 芯片后推出的又一款集成集成了 Wi-Fi 和蓝牙功能的低成本、低功耗的微控制器。它是一个集成天线和射频巴伦、功率放大器、低噪声放大器、滤波器和电源管理模块。整个解决方案占用的印刷电路板面积最少。该板采用台积电 40nm 低功耗技术的 2.4GHz 双模 Wi-Fi 和蓝牙芯片,功率和射频性能最佳,安全可靠,可扩展到各种应用。 ESP32 指的是 ESP32 裸芯片。但是,“ESP32” 一词通常指 ESP32 系列芯片及开发板。

  ESP32 模块的 WIFI 功能支持三种工作模式,分别为:STA、AP、STA + AP。

  • STA 模式:在此模式下,ESP32 模块可连接其他设备提供的无线网络,例如通过 WIFI 连接至路由器,从而可以访问互联网,进而实现手机或电脑通过互联网实现对设备的远程控制。
  • AP 模式:该模式为默认的模式,在此模式下,ESP32 模块将作为热点供其他设备连接,从而让手机或电脑直接与模块进行通讯,实现局域网的无线控制。
  • STA + AP 模式:该模式为 STA 模式与 AP 模式共存的一种模式,ESP32 模块既能连接至其他设备提供的无线网络,又能作为热点,供其他设备连接,以实现广域网与局域网的无缝切换,方便操作使用。

  除了上述的三种工作模式外,ESP32 模块在进行 UDP 连接或作为 TCP 客户端连接时,能够进入透传模式,进入透传模式后,ESP32 将会原封不动地把从 TCP 服务器或其他 UDP 终端接收到的消息,通过 UART 发送至与之连接的设备。

 参考文档链接如下:https://docs.ai-thinker.com/wifi

二、AT指令简介

  ESP32 可以通过 AT 指令进行配置和控制,使其能够连接到 WiFi 网络,建立 TCP/IP 连接,甚至作为服务器或客户端进行数据传输。AT 指令是应用于终端设备与 PC 应用之间连接与通信的指令集,它以 "AT" 作为起始符,后面跟随特定的指令代码和参数,以新的一行(CRLF)为结尾。输入的每条命名都会返回 OK 或者 ERROR 的响应。

2.1、AT指令类型

  AT 指令可以细分为四种类型,如下表所示:

AT指令分类

  具体的 AT 指令,我们可以在官网提供的文档中查看:https://docs.espressif.com/projects/esp-at/zh_CN/latest/esp32/AT_Command_Set/index.html

基础AT命令集

Wi-FI-AT命令集

TCP-IP-AT命令集

2.2、烧入AT固件

  要想使用 AT 指令,首先需要往 ESP32 模块中烧入固件,我们可以从乐鑫的官网下载 FLASH 烧入工具和固件。FLASH 烧入工具网址链接: https://www.espressif.com.cn/zh-hans/support/download/other-tool。BIN 文件网址链接:https://www.espressif.com.cn/zh-hans/support/download/at

选择模组

  点击 OK 按钮后,进入下载界面,我们需要下载的固件是在解压的固件包中的 【factory】子文件夹下。

选择固件下载

下载的固件

  使用乐鑫 ESP32 DOWNLOAD TOOL V3.9.2 向 ESP32 开发板烧录固件时,点击 “ERASE” 后,显示 “等待上电同步”,这时按开发板上的 “BOOT” 按钮 1 秒左右即可;ERASE 完成后,点击 “START”,再次出现 “等待上电同步”,同样按开发板上的 “BOOT” 按钮 1 秒左右即可;

所有 AT 命令均为串行执行,每次只执行一条命令。因此,在使用 AT 命令时,应等待上一条命令执行完毕后,再发送下一条命令。如果上一条命名未执行完毕,又发送新的命令,则会返回 busy p...

ESP32 开发板的 D16(RX,接 STM32 的 TX) 和 D17(TX,接 STM32 的 RX)才是发送 AT 指令的串口引脚。

三、ESP32初始化

UART_HandleTypeDef *pg_uart_esp32_handler;
UART_FrameData_t *pg_uart_esp32_frameData;

/**
 * @brief ESP32初始化函数
 * 
 * @param huart 串口句柄
 * @param frameData 串口接收数据帧
 */
void ESP32_Init(UART_HandleTypeDef *huart, UART_FrameData_t *frameData)
{
    pg_uart_esp32_handler = huart;
    pg_uart_esp32_frameData = frameData;
}
/**
 * @brief ESP32发送AT指令函数
 * 
 * @param cmd 待发送的AT指令
 * @param ack 期待的应答结果
 * @param timeOut 等待超时时间
 * @return true 应答成功
 * @return false 应答失败
 */
bool ESP32_SendAtCmd(char *cmd, char *ack, uint32_t timeOut)
{
    UART_ClearFrameData(pg_uart_esp32_frameData);                               // 清除串口的帧数据
    printf("cmd:%s\r\n", cmd);                                                  // 打印AT指令
    UART_Printf(pg_uart_esp32_handler, "%s\r\n", cmd);                          // 发送AT指令
  
    if ((ack == NULL) || (timeOut == 0))
    {
        return 0;
    }
    else
    {
        while (timeOut > 0)
        {
            if (pg_uart_esp32_frameData->finsh)                                 // 判断是否接收完成
            {
                if (strstr((char *)pg_uart_esp32_frameData->data, ack) != NULL) // 获取数据帧中是否包含期待的应答结果
                {
                    return true;
                }
                else
                {
                    UART_ClearFrameData(pg_uart_esp32_frameData);               // 如果没有,则清空数据帧,等待下次接收
                }
            }
            timeOut--;
            HAL_Delay(1);
        }
        return false;
    }
}
/**
 * @brief ESP32 WIFI功能初始化函数
 * 
 * @param huart 串口句柄
 * @param frameData 串口接收数据帧
 */
void ESP32_WiFi_Init(void)
{
    ESP32_ExitUnvarnished();                                                    // 退出透传模式
    ESP32_SendAtCmd("AT+SAVETRANSLINK=0", "OK", 500);                           // 关闭WIFI上电进入透传模式
  
   
    if (ESP32_SendAtCmd("AT+RST", "OK", 3000))                                  // 重启WIFI芯片
    {
        printf("重启WIFI芯片成功\r\n");
        HAL_Delay(3000);
    }

    if (ESP32_SendAtCmd("AT+CWINIT=0", "OK", 500))                              // 初始化WIFI驱动程序
    {
        printf("初始化WIFI驱动程序成功\r\n");
    }
    
    if (ESP32_SendAtCmd("AT+RESTORE", "ready", 3000))                           // 恢复出场设置
    {
        printf("恢复出场设置成功\r\n");
    }
   
    if (ESP32_SendAtCmd("AT", "OK", 500))                                       // 发送AT指令,等待是否回复OK
    {
        printf("AT指令测试成功\r\n");
    }
}
/**
 * @brief ESP32进入透传函数
 * 
 */
void ESP32_WiFi_EnterUnvarnished(void)
{
    if (ESP32_SendAtCmd("AT+CIPMODE=1", "OK", 500))
    {
        printf("设置最大连接数成功");
    }

    if (ESP32_SendAtCmd("AT+CIPSEND", ">", 500))
    {
        printf("进入透传模式成功\r\n");
    }
}
/**
 * @brief ESP32退出透传函数
 * 
 */
void ESP32_ExitUnvarnished(void)
{
    HAL_Delay(20);
    UART_Printf(pg_uart_esp32_handler, "+++");
    HAL_Delay(20);

    printf("退出透传模式成功\r\n");
}

四、ESP32连接WiFi

/**
 * @brief 设置ESP32工作模式函数
 * 
 * @param mode 1: Station模式; 2: AP模式; 3: AP+Station模式
 */
void ESP32_WiFi_SetMode(uint8_t mode)
{
    char cmd[15] = {0};
    char *modeStr[] = {"Station模式", "AP模式", "AP+Station模式"};

    if (mode < 1 || mode > 3)
    {
        printf("参数错误,工作模式设置失败\r\n");
        return;
    }

    sprintf(cmd, "AT+CWMODE=%d", mode);
    if (ESP32_SendAtCmd(cmd, "OK", 500))
    {
        printf("%s设置成功\r\n", modeStr[mode - 1]);
    }
}
/**
 * @brief ESP32连接WIFI函数
 * 
 * @param ssid  WIFI名称
 * @param pwd WIFI密码
 */
void ESP32_WiFi_Connect(char *ssid, char *pwd)
{
    char cmd[64] = {0};

    ESP32_WiFi_SetMode(1);                                                      // 设置工作模式为Station模式
  
    sprintf(cmd, "AT+CWJAP=\"%s\",\"%s\"", ssid, pwd);
    if (ESP32_SendAtCmd(cmd, "WIFI GOT IP", 10000))
    {
        printf("WIFI连接成功\r\n");
    }
}
/**
 * @brief ESP32获取IP地址函数
 * 
 * @param ip IP地址,需要16字节内存空间
 * @return uint8_t 0: 获取IP地址成功; 1: 获取IP地址失败;
 */
void ESP32_WiFi_GetIp(char *ip)
{
    char *p_start;
    char *p_end;
  
    if (ESP32_SendAtCmd("AT+CIFSR", "OK", 500))
    {
        printf("获取IP地址成功\r\n");
    }
  
    p_start = strstr((const char *)pg_uart_esp32_frameData->data, "\"");
    p_end = strstr(p_start + 1, "\"");
    *p_end = '\0';
    sprintf(ip, "%s", p_start + 1);
}

五、开启TCP服务器

/**
 * @brief ESP32设置是否多连接函数
 * 
 * @param mode 0: 单连接模式; 1: 多连接模式;
 */
void ESP32_WiFi_SetConnectionCount(uint8_t mode)
{
    char cmd[15] = {0};
    char *modeStr[] = {"单连接模式", "多连接模式",};

    if (mode != 0 && mode != 1)
    {
        printf("参数错误,连接模式设置失败\r\n");
        return;
    }

    sprintf(cmd, "AT+CIPMUX=%d", mode);
    if (ESP32_SendAtCmd(cmd, "OK", 500))                                        // 设置是否多连接
    {
        printf("%s设置成功\r\n", modeStr[mode]);
    }
}
/**
 * @brief 开启TCP服务器
 * 
 * @param port 端口号
 */
void ESP32_WiFi_StartTcpServer(uint16_t port)
{
    char cmd[30] = {0};
    ESP32_WiFi_SetConnectionCount(1);                                           // 开启多连接

    if (ESP32_SendAtCmd("AT+CIPDINFO=1", "OK", 500))                            // 设置IPD消息格式
    {
        printf("设置IPD消息格式成功\r\n");
    }

    sprintf(cmd, "AT+CIPSERVER=1,%d,\"TCP\"", port);
    if (ESP32_SendAtCmd(cmd, "OK", 3000))                                       // 开启TCP服务器
    {
        printf("开启TCP服务器成功\r\n");
    }
}
/**
 * @brief ESP32读取TCP通信发送过来的数据
 * 
 * @param id 对方的id
 * @param ip 对方的ip地址
 * @param port 对方的端口号
 * @param data 对方要发送的数据
 * @param length 对方发送的数据长度
 */
void ESP32_WiFi_ReadTcpData(uint16_t *id, char ip[], uint16_t *port, char *data, uint16_t *length)
{
    if (pg_uart_esp32_frameData->finsh)
    {
        if (strstr((const char *)pg_uart_esp32_frameData->data, "+IPD"))        // 收到TCP传输的数据
        {
            // 格式类型: \r\n+IPD,第几个连接,数据长度,"IP地址",端口号:数据\r\n,
            memset(data, 0, strlen(data));
            sscanf((const char *)pg_uart_esp32_frameData->data, "%*[\r\n]+IPD,%hd,%hd,\"%[^\"]\",%hd", id, length, ip, port);
            strtok((char *)pg_uart_esp32_frameData->data, ":");
            memcpy(data, strtok(NULL, ":"), *length);
        }
  
        UART_ClearFrameData(pg_uart_esp32_frameData);
    }
}
/**
 * @brief ESP32通过TCP通信发送数据
 * 
 * @param id 对方的id
 * @param data 对方要发送的数据
 * @param length 对方发送的数据长度
 */
void ESP32_WiFi_SendTcpData(uint16_t id, char *data, uint16_t length)
{
    char cmd[30] = {0};

    if (length <= 0 || strcmp(data, "") == 0)
    {
        return;
    }

    sprintf(cmd, "AT+CIPSEND=%d,%d", id, length);
    if (ESP32_SendAtCmd(cmd, "OK", 500))
    {
        if (ESP32_SendAtCmd(data, "OK", 3000))
        {
            printf("发送TCP数据成功\r\n");
        } 
    }
}

六、连接TCP服务器

/**
 * @brief ESP32连接TCP服务器
 * 
 * @param server_ip TCP服务器IP地址
 * @param server_port TCP服务器端口号
 */
void ESP32_WiFi_ConnectTcpServer(char *server_ip, char *server_port)
{
    char cmd[128] = {0};
  
    sprintf(cmd, "AT+CIPSTART=\"TCP\",\"%s\",%s", server_ip, server_port);
    if (ESP32_SendAtCmd(cmd, "CONNECT", 5000))
    {
        printf("连接TCP服务器成功\r\n");
    }
}

七、MQTT协议

7.1、什么是MQTT协议

  MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,主要用于物联网(IoT)设备之间的通信。它由 IBM 在 1999 年提出,现已成为物联网通信的重要标准之一。

【1】、MQTT 协议的主要组件

  • MQTT 客户端
    • 发布者(Publisher):发送消息的客户端。
    • 订阅者(Subscriber):接收消息的客户端。
  • MQTT 代理(Broker)
    • 服务器端组件,负责接收所有客户端的消息,并将这些消息转发给符合条件的订阅者。
    • 管理客户端的连接、订阅请求和消息传递。

【2】、MQTT 协议的关键特性

  • 发布/订阅通信模式
    • 与传统的客户端-服务器模型不同,发布/订阅模型允许消息的发送者(发布者)和接收者(订阅者)不必知道对方的存在。
    • 代理负责消息的路由和分发。
  • 消息服务质量(QoS)
    • QoS 0(至多一次):消息可能会丢失,但不重复。
    • QoS 1(至少一次):消息至少到达一次,可能会重复。
    • QoS 2(确保一次):消息确保到达一次,不会丢失或重复。
  • 主题(Topic)
    • 用于定义消息的路由路径。
    • 主题可以使用层次结构,例如 /home/temperature
  • 保留消息
    • 允许客户端发送一条消息到代理,代理将此消息保留,并在新的订阅者订阅该主题时立即发送给它。
  • 会话(Session)
    • MQTT 客户端与代理之间的连接可以有一个会话,会话可以持续一段时间,即使客户端断开连接。
    • 会话可以存储状态信息,如订阅和未确认的消息。

【3】、MQTT 协议的工作流程

  • 连接:客户端向代理发送连接请求,代理响应连接确认。
  • 订阅:客户端向代理发送订阅请求,指定感兴趣的主题。
  • 发布:客户端向代理发送消息,指定主题。
  • 消息传递:代理根据订阅信息,将消息转发给相应的客户端。
  • 断开连接:客户端发送断开连接请求,代理确认并关闭连接。

【4】、MQTT 协议的优点

  • 低带宽消耗:消息头部小,适合在带宽有限的网络中使用。
  • 低延迟:设计用于快速传递消息,适用于实时应用。
  • 可扩展性:支持大量的客户端和主题。
  • 可靠性:通过不同的 QoS 级别提供不同程度的消息可靠性。

【5】、MQTT 协议的缺点

  • 不支持消息的有序传递:在某些情况下,消息可能不会按照发送顺序到达。
  • 安全性问题:标准 MQTT 协议不提供加密,需要通过 SSL/TLS 或其他安全措施来增强安全性。

7.2、MQTT报文

  有关 MQTT 各种报文的详解,我们可以参考以下网站:hhttps://mcxiaoke.gitbooks.io/mqtt-cn/content/

7.2.1、连接报文

/**
 * @brief 构建MQTT连接包
 * 
 * @param mqtt_message 保存构建的MQTT连接包
 * @param client_id 客户端id
 * @param username 用户名
 * @param password 密码
 * @return uint16_t 构建的MQTT连接报文长度
 * 
 * @note MQTT连接包格式如下:
 *      固定报头: 报文类型 (10) 剩余长度=可变报头+负载 (??)
 *      可变报头:
 *          协议名: 协议名长度 (00 04) MQTT (4D 51 54 54)
 *          协议版本: (04)
 *          连接标志: (C2)
 *          保活时间: (?? ??)
 *      有效载荷:
 *          客户端ID:客户端ID长度 (?? ??) 客户端ID (client_id)
 *          用户名: 用户名长度 (?? ??) 用户名 (username)
 *          密码: 密码长度 (?? ??) 密码 (password)
 */
uint16_t MQTT_ConnectMessage(uint8_t*mqtt_message,char *client_id,char *username,char *password)
{
    uint16_t client_id_length = strlen(client_id);
    uint16_t username_length = strlen(username);
    uint16_t password_length = strlen(password);
    uint16_t remain_length = 0;
    uint16_t index = 0;

    // MQTT连接报文类型
    mqtt_message[index++] = 0x10;                                               // MQTT Message Type CONNECT

    // 剩余长度=可变报头长度(10)+客户端ID长度(2)+客户端ID(client_id_length)
    remain_length = 10 + 2 + client_id_length;
    // 用户名长度(2)+用户名(username_length)
    remain_length = (username_length > 0) ? (remain_length + 2 + username_length) : remain_length;
    // 密码长度(2)+密码(password_length)
    remain_length = (password_length > 0) ? (remain_length + 2 + password_length) : remain_length;

    // 循环处理固定报文中的剩余长度字节,字节量根据剩余字节的真实长度变化
    do {
        int temp = remain_length % 128;                                         // 剩余长度取余
        remain_length = remain_length / 128;                                    // 剩余长度取整
        (remain_length > 0) ? (temp |= 0x80) : temp;                            // 按协议要求位7置位
        mqtt_message[index++] = temp;                                           // 剩余长度字节记录一个数据
    } while (remain_length > 0);                                                // 如果remain_length大于0,再次进入循环

    // 协议名长度:00 04
    mqtt_message[index + 0] = 0x00;                                             // Protocol Name Length MSB 
    mqtt_message[index + 1] = 0x04;                                             // Protocol Name Length LSB 

    // 协议名:MQTT   
    mqtt_message[index + 2] = 0x4D;                                             // ASCII Code for M  
    mqtt_message[index + 3] = 0x51;                                             // ASCII Code for Q  
    mqtt_message[index + 4] = 0x54;                                             // ASCII Code for T  
    mqtt_message[index + 5] = 0x54;                                             // ASCII Code for T 

    // 协议版本
    mqtt_message[index + 6] = 0x04;                                             // MQTT Protocol version = 4   

    //  连接标志
    mqtt_message[index + 7] = 0xC2;                                             // conn flags 

    // 保活时间
    mqtt_message[index + 8] = 0x00;                                             // Keep-alive Time Length MSB  
    mqtt_message[index + 9] = 0x64;                                             // Keep-alive Time Length LSB  

    // 客户端ID长度
    mqtt_message[index + 10] = client_id_length / 256;                          // Client ID length MSB  
    mqtt_message[index + 11] = client_id_length % 256;                          // Client ID length LSB

    // 客户端ID
    for(uint8_t i = 0; i < client_id_length; i++)
    {
        mqtt_message[index + 12 + i] = client_id[i];
    }

    if(username_length > 0)
    {
        // 用户名长度  
        mqtt_message[index + 12 + client_id_length] = username_length / 256;    // username length MSB  
        mqtt_message[index + 13 + client_id_length] = username_length % 256;    // username length LSB

        // 用户名
        for(uint8_t i = 0; i < username_length ; i++)
        {
            mqtt_message[index + 14 + client_id_length + i] = username[i];  
        }
    }

    if(password_length > 0)
    {
        // 密码长度
        mqtt_message[index + 14 + client_id_length + username_length] = password_length / 256;  // password length MSB  
        mqtt_message[index + 15 + client_id_length + username_length] = password_length % 256;  // password length LSB  

        // 密码
        for(uint8_t i = 0; i < password_length; i++)
        {
            mqtt_message[index + 16 + client_id_length + username_length + i] = password[i]; 
        }
        index = index + 16 + client_id_length + username_length + password_length; 
    }

    return index;
}

7.2.2、发布消息报文

/**
 * @brief 通过MQTT向云平台发布信息
 * 
 * @param mqtt_message 保存构建的MQTT发布数据包
 * @param topic 发布主题
 * @param message 发布消息
 * @param udp 重发标志, 0: 表示这是客户端或服务端第一次请求发送这个PUBLISH报文; 1: 表示这可能是一个早前报文请求的重发
 * @param QoS 发布质量, 0: 最多分发一次; 1: 至少分发一次; 2: 只分发一次
 * @param retain 是否保留消息, 0: 不保留 1: 保留
 * @return uint16_t 构建的MQTT推送数据包长度
 * 
 * @note MQTT发送包格式如下:
 *      固定报头:报文类型 (3?) 剩余长度=可变报头+负载 (??)
 *      可变报头:
 *          主题: 主题长度 (?? ??) + 主题 (topic: /sys/{ProductKey}/{deviceName}/thing/event/property/post)
 *          报文标识符: 等级1或等级2有,等级0没有报文标识符
 *      有效载荷:
 *          消息:message (JSON格式数据)
 */
uint16_t MQTT_PublishMessage(uint8_t * mqtt_message, char * topic, char * message, uint8_t udp, uint8_t QoS, uint8_t retain)
{
    static uint16_t id = 0;
    uint16_t topic_length = strlen(topic);
    uint16_t message_length = strlen(message);
    uint16_t index = 0;
    uint16_t remain_length = 0;

    // MQTT发布报文类型
    mqtt_message[index++] = 0x30 | (udp << 3) | (QoS << 2) | (retain);          // MQTT Message Type PUBLISH

    // 剩余长度=可变报头长度(主题名长度(2) + 主题长度(topic_length) + 报文标识符长度(0或2))+ 有效载荷长度(消息长度)
    remain_length = 2 + topic_length + (QoS ? 2 : 0) + message_length;

    // 循环处理固定报文中的剩余长度字节,字节量根据剩余字节的真实长度变化
    do {
        int temp = remain_length % 128;                                         // 剩余长度取余
        remain_length = remain_length / 128;                                    // 剩余长度取整
        (remain_length > 0) ? (temp |= 0x80) : temp;                            // 按协议要求位7置位
        mqtt_message[index++] = temp;                                           // 剩余长度字节记录一个数据
    } while (remain_length > 0);                                                // 如果remain_length大于0,再次进入循环

    // 主题长度
    mqtt_message[index++] = (0xff00 & topic_length) >> 8;
    mqtt_message[index++] = 0xff & topic_length;

    // 主题
    for(uint8_t i = 0; i < topic_length; i++)
    {
        mqtt_message[index + i] = topic[i];
    }
    index += topic_length;

    // 报文标识符,等级0没有
    if(QoS)
    {
        mqtt_message[index++] = (0xff00 & id) >> 8;
        mqtt_message[index++] = 0xff & id;
        id++;
    }

    // 消息
    for(uint8_t i = 0; i < message_length; i++)
    {
        mqtt_message[index + i] = message[i];
    }
    index += message_length;

    return index;
}

7.3、定时发送心跳包

  在客户端和服务端长时间没有相互发送数据的情况下,我们需要一种机制来判断连接是否依然存在。直接发送任何数据包可以实现这一点,但为了效率和简洁,通常发送一个空包,这个就是 心跳包。心跳包类似心跳,每隔固定时间发送一次,通知服务器客户端依然活着。它是一种保持长连接的机制,包的内容没有特别规定,通常是很小的包或仅包含包头的空包。

  这里,我们需要定时发送 心跳包0xC0 0x00),如果发送成功,服务器会返回 心跳响应0xD0 0x00)。

/**
 * @brief ESP32发送MQTT心跳包
 * 
 * @return true 心跳包发送成功,不需要断网重连
 * @return false 心跳包发送失败,需要断网重连
 */
bool ESP32_WiFi_MQTT_KeepAlive(void)
{
    uint8_t i = 0;
    uint8_t message[2] = {0xC0, 0x00};

    for (i = 0; i < 5; i++)
    {
        HAL_UART_Transmit(pg_uart_esp32_handler, message, 2, 0xFFFF);
        if (pg_uart_esp32_frameData->finsh == 1)
        {
            if (pg_uart_esp32_frameData->data[0] == 0xD0 && pg_uart_esp32_frameData->data[1] == 0x00)
            {
                break;
            }
        }
        UART_ClearFrameData(pg_uart_esp32_frameData);
        HAL_Delay(1000);
    }

    UART_ClearFrameData(pg_uart_esp32_frameData);

    return (i == 5) ? false : true;
}

八、CJson的使用

  JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,它是 JavaScript 的子集,易于人阅读和编写。JSON 是一个对象,由一个大括号表示。其中,括号中描述对象的属性,通过键值对来描述对象的属性,键与值之间使用冒号连接,多个键值对之间使用逗号分隔。键值对的键 应使用引号引住,键值对的值,可以是 JS 中的任意类型的数据。

{ 
    "method": "thing.event.property.post",
    "id": "1", 
    "params": 
    { 
        "CurrentHumidity": 70.5, 
        "CurrentTemperature": 26.5 
    }, 
    "version": "1.0.0" 
}

  CJSON 是一个轻量级的、用于处理 JSON 数据的 C 语言库。它提供了简单而直观的 API,使得在 C 程序中处理 JSON 数据变得相对容易。它的官方连接如下:https://github.com/DaveGamble/cJSON。这里,我们只需要其中的 cJSON.ccJSON.h 文件即可。

  我们可以通过 cJSON_CreateObject() 函数 创建一个空的 JSON 对象,该函数返回 cJSON 的指针。

CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);

  创建 JSON 对象后,我们可以通过下列的一系列函数来添加对应类型的键值对到 JSON 对象中。该系列函数 返回一个指向新添加的 JSON 元素的指针。新返回的这个元素包含了添加的键值对。如果添加失败,返回 NULL。该系类函数的第一个参数 cJSON * const object 指向了你 要添加键值对的 JSON 对象。第二个参数 const char * const name 表示你 要添加的键值对的键值。如果有 第三个参数,则表示了你要 添加的键值对对应的值

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_CreateArray() 等一系列方法创建 对应的数据类型的 JSON 的数组对象。该函数返回指向 JSON 数组的指针。如果有参数,第一个参数 是你要 创建对应类型的数组第二个参数数组中元素的个数

CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);

  然后通过 cJSON_AddItemToArray() 方法 将元素添加到 JSON 数组中。该函数返回一个布尔值表示成功或失败。如果成功添加元素,返回 true,否则返回 false。该函数的 CJSON *array 参数是一个指向 JSON 数组的指针,表示你要 往哪个数组中添加元素。参数 cJSON *ite 表示了你要 添加的键值对,该键值对可以是任何 JSON 数据类型,比如字符串、数字、数组、对象等。

CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);

  我们可以通过如下的一系列方法 创建对应的 JSON 格式的数据类型

CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);

接着,我们调用 cJSON_AddItemToObject() 方法。该函数可以 将 JSON 任意数据类型的键值对添加到 JSON 对象中。该函数返回一个布尔值表示成功或失败。如果成功添加元素,返回 true,否则返回 false。该函数的第一个参数 cJSON * const object指向了你 要添加键值对的 JSON 对象。第二个参数 const char * string 表示你要 添加的键值对的键值。如果有 第三个参数,则表示了你 要添加的键值对,该键值对可以是任何 JSON 数据类型,比如字符串、数字、数组、对象等。

CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item)

  在创建完成 JSON 对象后,我们可以调用 cJSON_Print() 方法 将 JSON 对象转换为字符串。该函数的 返回值 是一个 指向字符数组的指针,表示包含 JSON 元素内容的字符串。该函数的参数 const cJSON *item 是一个指向 cJSON 元素的指针,表示你要 将哪个 JSON 元素转换成字符串

CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);

  需要注意的是,这个 返回的字符串是在堆上动态分配的,所以在使用完毕后,需要负责释放内存以防止内存泄漏。我们可以使用 cJSON_Free() 函数即可释放使用 cJSON_Print() 转换后的 JSON 格式的字符串的内存。该函数的参数 void *object 是一个 转换后的 JSON 格式的字符串的指针

CJSON_PUBLIC(void) cJSON_free(void *object)

  在释放 JSON 格式的字符串之后,我们还需要使用 cJSON_Delete() 函数释放 JSON 对象的内存控件,它的的参数 void *object 是一个指向 cJSON 元素的指针。

CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);

九、ESP32连接阿里云

9.1、阿里云新建产品

  阿里云的物联网平台的网址如下:https://iot.console.aliyun.com/lk/summary/new

进入公共实例

创建产品

确认产品信息

添加设备

确认设备信息

9.2、查看MQTT连接参数

查看MQTT连接参数

9.3、查看上传阿里云的JSON格式

  上传阿里云的具体的 JSON 格式,我们可以通过在设备模拟器或在线调试的方式查看。

设备模拟器

  为了方便查看 JSON 格式,我们可以在网站上查找一些 JSON 在线解析的网站使用,比如如下网站:https://www.json.cn/jsononline/ 等。

JSON在线解析

9.4、main()函数

#define WIFI_SSID               "HUAWEI-1AA2CE"                                 // WiFi名字
#define WIFI_PWD                "12345678"                                      // WiFi密码

// 阿里云的账号CLIENT_ID和PASSWODR密码过一段时间就自动变
#define CLIEND_ID               "h716BiondGQ.D001|securemode=2,signmethod=hmacsha256,timestamp=1721656592189|"
#define USERNAME                "D001&h716BiondGQ"
#define PASSWORD                "1be2f6ff8f32e36c60ecebab397102475ec7d09dd8f72b9d7b4a9a23f1dd2855"

#define MQTT_HOST_URL           "iot-06z00c5zp62mf64.mqtt.iothub.aliyuncs.com"
#define PORT                    "1883"

// 属性上报主题: /sys/${productKey}/${deviceName}/thing/event/property/post
#define PUSBLISH_TOPIC          "/sys/h716BiondGQ/D001/thing/event/property/post"
uint8_t mqtt_message[512];
int main(void)
{
    char ip[16];
    uint16_t length = 0;
    uint8_t old_tick = 0, current_tick = 0;

    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init();
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

    UART_Init(&g_usart1_handle, USART1, 115200);
    UART_Init(&g_usart2_handle, USART2, 115200);

    Timer_Base_Init(&g_timer6_handle, TIM6, 8399, 9999);

    ESP32_Init(&g_usart2_handle, &g_usart2_frame_data);
    ESP32_WiFi_Init();
  
    printf("\r\nJoining to AP...\r\n");

    ESP32_WiFi_Connect(WIFI_SSID, WIFI_PWD);                                    // 设置WIFI要连接的AP
    printf("\r\n");

    ESP32_WiFi_GetIp(ip);                                                       // 获取IP地址
    printf("IP: %s\r\n\r\n", ip);

    ESP32_WiFi_SetConnectionCount(0);                                           // 设置单连接
    ESP32_WiFi_ConnectTcpServer(MQTT_HOST_URL, PORT);                           // 连接TCP服务器
    ESP32_WiFi_EnterUnvarnished();                                              // 进入透传模式

    __HAL_TIM_CLEAR_IT(&g_timer6_handle, TIM_IT_UPDATE);                          // 清除更新中断标志位
    HAL_TIM_Base_Start_IT(&g_timer6_handle);                                      // 使能更新中断,并启动计数器
  
    length = MQTT_ConnectMessage(mqtt_message, CLIEND_ID, USERNAME, PASSWORD);
    HAL_UART_Transmit(&g_usart2_handle, mqtt_message, length, 0xFFFF);

    memset(mqtt_message, 0, sizeof(mqtt_message));
    UART_ClearFrameData(pg_uart_esp32_frameData);                               // 重新开始接收新的一帧数据

    old_tick = g_timer_1ms_ticks;
  
    while (1)
    {
        current_tick = g_timer_1ms_ticks;
        if ((current_tick - old_tick) != 0)
        {
            // 创建 JSON 对象
            cJSON *root = cJSON_CreateObject();

            // 添加基本键值对
            cJSON_AddStringToObject(root, "method", "thing.event.property.post");
            cJSON_AddNumberToObject(root, "id", 1);
            // cJSON_AddStringToObject(root, "id", "1");

            // 创建嵌套的JSON对象
            cJSON *location = cJSON_CreateObject();
            cJSON_AddNumberToObject(location, "Longitude", 30);
            cJSON_AddNumberToObject(location, "Latitude", 30);
            cJSON_AddNumberToObject(location, "Altitude", 30);
            cJSON_AddNumberToObject(location, "CoordinateSystem", 1);

            // 添加嵌套的 JSON 对象
            cJSON *params = cJSON_CreateObject();
            cJSON_AddNumberToObject(params, "CurrentTemperature", 24);
            cJSON_AddNumberToObject(params, "CurrentHumidity", 70);
            cJSON_AddItemToObject(params, "GeoLocation", location);
            cJSON_AddItemToObject(root, "params", params);

            // 添加基本的JSON键值对
            cJSON_AddStringToObject(root, "version", "1.0");

            // 将JSON对象转换为字符串
            char *jsonString = cJSON_Print(root);

            memset(mqtt_message, 0, sizeof(mqtt_message));
            length = MQTT_PublishMessage(mqtt_message, PUSBLISH_TOPIC, jsonString, 0, 0, 0);
            HAL_UART_Transmit(pg_uart_esp32_handler, mqtt_message, length, 0xFFFF);
  
            memset(mqtt_message, 0, sizeof(mqtt_message));
            UART_ClearFrameData(pg_uart_esp32_frameData);                       // 重新开始接收新的一帧数据

            // 释放内存
            cJSON_Delete(root);
            cJSON_free(jsonString);

            // 发送完消息后,心跳的定时器清零,重新计数
            old_tick =  0;
            current_tick = 0;
            g_timer_1ms_ticks = 0;
        }

        if (g_timer_1ms_ticks >= 30)
        {
            // 如果发送心跳包失败,则重新连接
            if (!ESP32_WiFi_MQTT_KeepAlive())
            {
                memset(mqtt_message, 0, sizeof(mqtt_message));
                length = MQTT_ConnectMessage(mqtt_message, CLIEND_ID, USERNAME, PASSWORD);
                HAL_UART_Transmit(pg_uart_esp32_handler, mqtt_message, length, 0xFFFF);

                memset(mqtt_message, 0, sizeof(mqtt_message));
                UART_ClearFrameData(pg_uart_esp32_frameData);
            }
            old_tick =  0;
            current_tick = 0;
            g_timer_1ms_ticks = 0;
        }
  
    }
  
    return 0;
}

  定时器定时功能初始化函数:

TIM_HandleTypeDef g_timer6_handle;

/**
 * @brief 定时器定时功能初始化函数
 * 
 * @param htim 定时器句柄
 * @param TIMx 定时器寄存器基地址,可选值: TIMx, x可选范围: 1 ~ 14
 * @param prescaler 预分频系数,可选值: 0 ~ 65535
 * @param period 自动重装载值,可选值: 0 ~ 65535
 * 
 * @note 默认为向上计数模式
 */
void TIM_Base_Init(TIM_HandleTypeDef *htim, TIM_TypeDef *TIMx, uint16_t prescaler, uint16_t period)
{
    htim->Instance = TIMx;                                                      // 定时器寄存器基地址
    htim->Init.CounterMode = TIM_COUNTERMODE_UP;                                // 计数模式
    htim->Init.Prescaler = prescaler;                                           // 预分频系数
    htim->Init.Period = period;                                                 // 自动重装载值
    HAL_TIM_Base_Init(htim);
}

  基本定时器底层初始化函数:

/**
 * @brief 基本定时器底层初始化函数
 * 
 * @param htim 定时器句柄
 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        __HAL_RCC_TIM6_CLK_ENABLE();                                            // 使能定时器6的时钟

        HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);                                      // 使能定时器6中断
        HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 4, 0);                              // 设置中断优先级
    }
}

  定时器 6 中断服务函数:

uint32_t g_timer_1ms_ticks;

/**
 * @brief 定时器更新中断回调函数
 * 
 * @param htim 定时器句柄
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        g_timer_1ms_ticks++;
    }
}
posted @ 2025-04-01 20:01  星光映梦  阅读(940)  评论(0)    收藏  举报