ESP32应用——HTTP client(ESP-IDF框架)

发布于:2025-08-20 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

一、前言

二、URL

2.1 URL简介

2.2 URL示例

三、HTTP

3.1 HTTP协议概述

3.2 HTTP的工作原理

3.2.1 HTTP 请求-响应流程

3.2.2 HTTP 请求结构

3.2.3 HTTP请求方法

3.2.4 HTTP响应结构

3.2.5 HTTP状态码

四、ESP HTTP 客户端流程

五、ESP HTTP 客户端实战解析

5.1 服务器为设备提供的HTTP服务

① 下载设备配置:

② 上报设备使用记录:

③ 上报设备状态:

④ 查询设备升级固件信息:

⑤ 下载设备固件:

5.2 HTTP客户端初始化函数

5.3 通用的URL构建函数

5.4 通用的HTTP请求函数

5.5 通用的JSON响应解析函数

5.6 HTTP事件处理函数

5.7 下载设备配置

5.8 查询设备升级固件信息

5.9 下载设备固件

5.10 上报设备使用记录

5.11 定时上报设备状态

5.12 设置服务器IP并尝试连接管理终端

结语


一、前言

        最近做的项目需要使用HTTP协议和服务器进行通信,进行定时上报设备状态以及下载设备固件等操作,第一次接触到HTTP,所以写个博客,为后面的OTA升级打下基础。


二、URL

2.1 URL简介

        URL(统一资源定位符)是互联网上用于定位和访问各种资源的标准方式。它由多个部分组成,包括协议(如HTTP、HTTPS)、主机名、端口号、路径、查询参数等。这些元素共同构成了一个完整的URL地址。
        URL的一般语法格式为:

protocol :// hostname[:port] / path / [;parameters][?query]#fragment

● protocol(协议)
        
指定使用的传输协议,最常用的是HTTP协议,protocol常见的名称如下:
        ① http 通过HTTP协议访问资源。格式 http://
        ② https 通过完全的HTTPS协议访问资源。格式 https://
        ③ ftp 通过FTP协议访问资源。格式 ftp://
● hostname(主机名)
        
存放资源的服务器的域名系统(DNS)主机名或IP地址。有时在主机名前也可以包含连接到服务器所需的用户名和密码(格式:username:password@hostname)
● port(端口号)
        
HTTP默认工作在TCP协议的80端口(TCP协议是传输层,HTTP协议是应用层,所以说HTTP协议是工作在TCP协议之上的);HTTPS默认工作在TCP协议的443端口。
● path(路径)
        
由0或多个"/"符号隔开的字符串,一般用来表示主机上的一个目录或文件地址。
● parameters(参数)
       
可选。用于指定特殊参数。
● query(查询)
       
可选。用于给动态网页(如使用CGI、ISAPI、PHP、JSP、ASP、NET等技术制作的网页)传递参数,可有多个参数,参数之间用"&"符号隔开,每个参数的名和值用"="符号隔开。
● fragment(信息片段)
        
字符串,用于指定网络资源中的片段。例如一个网页中有多个名词解释,可以用fragment直接定位到某一名词解释。

2.2 URL示例

        假设现在有个服务:
        方法:GET
        协议:HTTP
        主机域名:office.sophymedical.com
        端口号:11125
        路径:/device/download_config
        需要的参数:名:sn;值:字符串

        现在构造URL如下:http://office.sophymedical.com:11125/device/download_config?sn=DROUK0LezZH,将它复制到浏览器中,可以获得数据如下:

        上面就是访问到的资源,是个JSON数据格式的文本。注意:浏览器地址栏访问默认是GET请求,如果要发送其他请求如POST,需要设置表单method="post",但无法直接在地址栏发起POST请求。


三、HTTP

3.1 HTTP协议概述

        HTTP(超文本传输协议) 是一种用作获取诸如 HTML 文档这类资源的协议。它是 Web 上进行任何数据交换的基础,同时,也是一种客户端—服务器(client-server)协议。完整网页文档通常由文本、布局描述、图片、视频、脚本等资源构成。HTTP 是万维网(WWW)的基础,支持网页浏览、文件下载、API 调用等应用场景。

3.2 HTTP的工作原理

        HTTP 使用客户端-服务器模型,通过请求-响应的方式传输数据。它的核心功能是客户端向服务器发送请求,服务器返回响应。

3.2.1 HTTP 请求-响应流程

● 客户端:向服务器发送 HTTP 请求(如 GET/index.html
服务器:处理请求并返回 HTTP 响应(如 200 OK 和网页内容)

3.2.2 HTTP 请求结构

        HTTP请求由以下部分组成:
● 请求行:包括请求方法(如 GET、POST)、请求资源(如 /index.html)和协议版本(如 HTTP/1.1)
请求头:包含附加信息(如 Host、User-Agent、Accept)
请求体:可选。用于传输数据(如 POST 请求的表单数据)

        请求头里的内容由键值对组成,每行一对。用于通知服务器有关于客户端请求的信息,比如上面的 Content-Length: 16 表示请求体(请求数据)的长度为16字节。典型的请求头有:
● User-Agent:产生请求的浏览器类型。(手机、PC等)
● Accept:客户端可识别的内容类型列表
● Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机
● Content-Type:内容类型(请求数据的或响应数据的类型,比如我要使用 PUT 方法,上传JSON格式的数据,那么内容类型可写为:application/json

3.2.3 HTTP请求方法

        上面提到了 PUT 方法,HTTP/1.1协议中共定义了八种方法(也叫做“动作”)来以不同的方式操作指定的资源:
● GET
        从服务器获取资源。用于请求数据而不对数据进行更改。例如,从服务器获取网页、图片、二进制文件等。
● POST
        向服务器发送数据以创建新资源。常用于提交表单数据或上传文件。发送的数据包含在请求体中。
● PUT
        向服务器发送数据以更新现有资源。如果资源不存在,则创建新的资源。与 POST 不同,PUT 通常是幂等的,即多次执行相同的 PUT 请求不会产生不同的结果。
● DELETE
        从服务器删除指定的资源。请求中包含要删除的资源标识符。
● PATCH
        对资源进行部分修改。与 PUT 类似,但 PATCH 只更改部分数据而不是替换整个资源。
● HEAD
        类似于 GET,但服务器只返回响应的头部,不返回实际数据。用于检查资源的元数据(例如,检查资源是否存在,查看响应的头部信息)。
● OPTIONS
        返回服务器支持的 HTTP 方法。用于检查服务器支持哪些请求方法,通常用于跨域资源共享(CORS)的预检请求。
● TRACE
        回显服务器收到的请求,主要用于诊断。客户端可以查看请求在服务器中的处理路径。
CONNECT
        建立一个到服务器的隧道,通常用于 HTTPS 连接。客户端可以通过该隧道发送加密的数据。

        后面给的示例里只用到 GET、PUT和POST这三种方法。

3.2.4 HTTP响应结构

        HTTP响应由以下部分组成:
状态行:包括协议版本(如 HTTP/1.1)、状态码(如200、404)和状态消息(如 OK、NOT FOUND)
响应头:包含附加信息(如 Content-Type、Content-Length)
响应体:包含实际数据

3.2.5 HTTP状态码

        所有HTTP响应的第一行都是状态行,依次是当前HTTP版本号,3位数字组成的状态代码,以及描述状态的短语,彼此由空格分隔。
        状态码的第一个数字代表当前响应的类型:
1XX 消息——请求已被服务器接收,继续处理
2XX 成功——请求已成功被服务器接收、理解并接受
3XX 重定向——需要后续操作才能完成这一请求
4XX 请求错误——请求含有词法错误或无法被执行
5XX 服务器错误——服务器正在处理某个正确请求时发生错误


四、ESP HTTP 客户端流程

        esp_http_client 提供了一组API,用于从ESP-IDF应用程序中发起HTTP/HTTPS请求,具体的使用步骤如下:

● 首先调用 esp_http_client_init(),创建一个 esp_http_client_handle_t 实例,即基于给定的 esp_http_client_config_t 结构体配置创建HTTP客户端句柄。此函数必须第一个被调用。若用户未明确定义参数的配置值,则使用默认值。

● 其次调用 esp_http_client_perform(),执行 esp_http_client 的所有操作:包括打开连接、交换数据、关闭连接(如需要),同时在当前任务完成前阻塞该任务。所有相关的事件(在 esp_http_client_config_t 中指定)将通过事件处理程序被调用。

● 最后调用 esp_http_client_cleanup() 来关闭连接(如有),并释放所有分配给HTTP客户端实例的内存。此函数必须在操作完成后最后一个被调用。


五、ESP HTTP 客户端实战解析

        在此之前,建议把上面的HTTP和URL的知识看懂,后面写代码的时候才不会一知半解。我们现在服务器有以下5个服务:

5.1 服务器为设备提供的HTTP服务

① 下载设备配置:

请求结构:
        方法:GET
        URL:
http://office.sophymedical.com:11125/device/download_config
        请求参数:

响应结构:
        响应码:200(成功)
        响应体类型:application/json
        响应体:

② 上报设备使用记录:

请求结构:
        方法:POST
        URL:
http://office.sophymedical.com:11125/device/usage_logs
        请求体类型:application/json
        请求体:


响应结构:
        响应码:200(成功)
        响应体类型:application/json
        响应体:

③ 上报设备状态:

请求结构:
        方法:PUT
        URL:
http://office.sophymedical.com:11125/device/status
        请求体类型:application/json
        请求体:


响应结构:
        响应码:200(成功)
        响应体类型:application/json
        响应体:

④ 查询设备升级固件信息:

请求结构:
        方法:GET
        URL:
http://office.sophymedical.com:11125/device/query_latest_firmware_version
        请求参数:

响应结构:
        响应码:200(成功)
        响应体类型:application/json
        响应体:

⑤ 下载设备固件:

请求结构:
        方法:GET
        URL:
http://office.sophymedical.com:11125/device/download_firmware
        请求参数:

响应结构:
        响应码:200(成功)
        响应体类型:application/octet-stream

        响应体:二进制数据流(.bin文件,用于固件升级)

5.2 HTTP客户端初始化函数

        首先,定义了一些指向cJSON类型的指针,用于后面定时上报设备状态的时候构造JSON数据。
        然后确保NVS里存储IP地址的命名空间存在,后续构造URL的时候是用的服务器的IP地址来构造的,后续需要将要连接的服务器IP地址存储到NVS里(非易失性存储器)。
        最后创建一个定时器,用于定时上报设备状态(如果创建一个任务太耗资源),绑定的定时器回调函数见目录 5.11 定时上报设备状态 ,使能自动重装载,即周期性定时器。最后的信号量可以忽略,是用于通知UI层的,为了实现UI层和底层分离,本篇只解析HTTP底层代码。

        这个初始化只是做了一下准备工作,还没见到ESP-IDF封装的HTTP相关函数。

static TimerHandle_t status_report_timer = NULL; // 设备状态上报定时器句柄
SemaphoreHandle_t g_http_gui_semaphore = NULL;   //HTTP GUI信号量

// JSON对象指针声明
static cJSON *root = NULL;
static cJSON *device = NULL;
static cJSON *channel = NULL;
static cJSON *audio = NULL;
static cJSON *light = NULL;
static cJSON *electrical = NULL;

// 管理终端IP地址字符串
static char manage_ip_str[16] = {0}; // 格式: xxx.xxx.xxx.xxx

/**
 * @brief 初始化HTTP模块
 */
void app_http_init(void)
{
    // 初始化JSON对象指针
    root = NULL;
    device = NULL;
    channel = NULL;
    audio = NULL;
    light = NULL;
    electrical = NULL;

    // 确保NVS命名空间存在
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE_HTTP, NVS_READWRITE, &nvs_handle);
    if (err == ESP_ERR_NVS_NOT_FOUND)
    {
        // 命名空间不存在,需要创建
        ESP_LOGI(HTTP_TAG, "NVS命名空间不存在,正在创建...");
        // 关闭当前句柄
        nvs_close(nvs_handle);
        // 重新以读写模式打开,这将创建命名空间
        err = nvs_open(NVS_NAMESPACE_HTTP, NVS_READWRITE, &nvs_handle);
        if (err != ESP_OK)
        {
            ESP_LOGE(HTTP_TAG, "创建NVS命名空间失败: %s", esp_err_to_name(err));
        }
        else
        {
            ESP_LOGI(HTTP_TAG, "NVS命名空间创建成功");
            nvs_close(nvs_handle);
        }
    }
    else if (err == ESP_OK)
    {
        // 命名空间已存在,关闭句柄
        nvs_close(nvs_handle);
    }

    // 清空存储IP地址的字符串
    memset(manage_ip_str, 0, sizeof(manage_ip_str));

    // 创建设备状态上报定时器(初始不启动)
    status_report_timer = xTimerCreate(
        "StatusReportTimer",
        pdMS_TO_TICKS(REPORT_INTERVAL_DEF_MS), // 默认使用预设的上报间隔
        pdTRUE,                                // 自动重载
        (void *)0,                             // 定时器ID
        status_report_timer_callback           // 回调函数
    );

    // 创建与UI层通信的信号量
    g_http_gui_semaphore = xSemaphoreCreateBinary();
}

5.3 通用的URL构建函数

        这个函数用于构建完整的URL。
        在目录 5.4 通用的HTTP请求函数 里,发起HTTP请求之前,会调用这个函数构建完整的URL。函数的入参由不同的服务请求函数传入,因为每个服务的相对路径都不同,比如下载设备配置,相对路径为:device/download_config
        注意:由于是malloc分配的内存,需要调用者手动释放内存,否则会导致内存泄漏。

/**
 * 构建完整URL
 * @param path 相对路径(所有接口都会传入正确格式的路径)
 * @return char* 完整URL(需要调用者释放内存)
 */
static char *build_full_url(const char *path)
{
    // 基础URL前缀
    const char *url_prefix = "http://";
    // 计算所需内存大小
    size_t prefix_len = strlen(url_prefix);
    // 这里的manage_ip_str在目录5.11里会设置
    size_t ip_len = strlen(manage_ip_str);
    // 相对路径
    size_t path_len = strlen(path);

    // 检查管理IP是否为空
    if (ip_len == 0)
    {
        ESP_LOGE(HTTP_TAG, "管理终端IP地址未设置,无法构建URL");
        return NULL;
    }

    // 使用存储的管理IP构建URL
    size_t url_len = prefix_len + ip_len + path_len + 1; // +1 for \0
    // 分配内存
    char *full_url = (char *)malloc(url_len);
    if (!full_url)
    {
        ESP_LOGE(HTTP_TAG, "内存分配失败");
        return NULL;
    }

    // 构建完整URL: http://xxx.xxx.xxx.xxx/path
    strcpy(full_url, url_prefix);

    strcat(full_url, manage_ip_str);

    strcat(full_url, path);

    ESP_LOGI(HTTP_TAG, "构建URL: %s", full_url);
    return full_url;
}

5.4 通用的HTTP请求函数

        这里开始看到了ESP HTTP客户端流程提到的函数。来解析一下这个函数:
        ① 首先根据传入的相对路径构建完整的URL。
        ② 配置结构体成员,第二个成员变量 event_handler 指向HTTP事件处理函数,见目录 5.6 HTTP事件处理函数 ,第三个成员变量用于存放响应体(JSON格式的数据)。这里需要纠正一下,这个请求函数只适用于下载设备固件之外的四个服务,因为下载设备固件返回的是二进制流,会定义另一个HTTP响应缓冲区。见目录 5.9 下载设备固件 
        ③ 设置请求方法,如果是POST和PUT方法先设置请求头。
        调用 esp_http_client_perform 函数执行请求。此函数会阻塞直到整个HTTP事务完成(包括DNS解析、TCP连接、请求发送、响应接受),也就是说 ret 被赋值的时候,整个HTTP流程就结束了,如果成功的话,缓冲区里已经有响应数据了。
        ⑤ 打印响应报文的状态码、响应长度等调试信息。
        ⑥ 别忘了调用 esp_http_client_cleanup 函数清理资源。

#define MAX_HTTP_OUTPUT_BUFFER 2048                        // HTTP响应缓冲区大小
char response_buffer[MAX_HTTP_OUTPUT_BUFFER + 1] = {0};    // HTTP响应缓冲区

/**
 * 统一的HTTP请求接口
 * @param path 请求路径(相对路径)
 * @param post_data POST/PUT请求体数据(GET时传NULL)
 * @param method 请求方法(HTTP_GET/HTTP_POST/HTTP_PUT)
 * @return esp_err_t 执行结果
 */
static esp_err_t http_rest_request(const char *path, const char *post_data, http_method_t method)
{
    esp_err_t ret = ESP_OK;
    // 构建完整URL
    char *full_url = build_full_url(path);
    if (!full_url)
    {
        return ESP_FAIL;
    }

    // 基础配置
    esp_http_client_config_t config = {
        .url = full_url,
        .event_handler = _http_event_handler, // 使用统一的事件处理器,见目录5.6
        .user_data = response_buffer,         // JSON响应数据存入buffer
        .disable_auto_redirect = true,        // 禁用重定向,避免未知错误发生
    };
    // 初始化客户端
    esp_http_client_handle_t client = esp_http_client_init(&config);

    // 根据请求类型设置参数
    switch (method)
    {
    case HTTP_GET:
        esp_http_client_set_method(client, HTTP_METHOD_GET);
        break;
    case HTTP_POST:
    case HTTP_PUT:
        // 设置请求头:请求体内容为JSON格式的数据
        esp_http_client_set_header(client, "Content-Type", "application/json");
        // 如果有请求体数据
        if (post_data && strlen(post_data) > 0)
        {
            // 设置POST数据,此函数必须在 `esp_http_client_perform` 之前调用。
            esp_http_client_set_post_field(client, post_data, strlen(post_data));
        }
        // 设置请求方法
        esp_http_client_set_method(client, (method == HTTP_POST) ? HTTP_METHOD_POST : HTTP_METHOD_PUT);
        break;
    }

    // 执行请求
    ret = esp_http_client_perform(client);
    if (ret == ESP_OK)
    {
        ESP_LOGI(HTTP_TAG, "HTTP %s 状态码 = %d, 内容长度 = %lld",
                 (method == HTTP_GET) ? "GET" : (method == HTTP_POST) ? "POST" : "PUT",
                 esp_http_client_get_status_code(client),
                 esp_http_client_get_content_length(client));

        ESP_LOGI(HTTP_TAG, "响应内容: %s", response_buffer);
    }
    else
    {
        ESP_LOGE(HTTP_TAG, "HTTP请求失败: %s", esp_err_to_name(ret));
    }

    // 清理资源
    esp_http_client_cleanup(client);
    free(full_url); // 释放动态分配的URL内存
    return ret;
}

5.5 通用的JSON响应解析函数

        见上一篇博客:cJSON库应用这个函数一般是那四个服务(排除掉下载设备固件)调用统一的HTTP请求函数后,用于解析响应数据的。
        值得注意的是,解析函数内部获取 data 节点的函数 cJSON_GetObjectItem(root, "data"); 返回的是cJSON指针类型,因此解析函数的第二个参数需要传入一个二级指针。如果传入的是一个一级指针(cJSON *data),函数内部会获得指针的副本,然后修改副本的值(如data = new_address;),但这样不会影响调用方的原始指针,因而获取不到想要的结果。

/**
 * 通用的JSON响应解析函数 - 检查响应是否成功
 * @param json_str JSON字符串
 * @param data 出参,指向cJSON对象的指针
 * @return esp_err_t 执行结果
 */
esp_err_t parse_response_json(const char *json_str, cJSON **data)
{
    cJSON *root = cJSON_Parse(json_str);
    if (root == NULL)
    {
        ESP_LOGE(HTTP_TAG, "JSON解析失败: %s", cJSON_GetErrorPtr());
        return ESP_FAIL;
    }
    // 检查code字段是否为200(表示成功)
    cJSON *code = cJSON_GetObjectItem(root, "code");
    if (!code || !cJSON_IsNumber(code) || code->valueint != 200)
    {
        // 尝试获取错误信息
        cJSON *msg = cJSON_GetObjectItem(root, "msg");
        if (msg && cJSON_IsString(msg))
        {
            ESP_LOGE(HTTP_TAG, "API请求失败: %s", msg->valuestring);
        }
        else
        {
            ESP_LOGE(HTTP_TAG, "API请求失败: 响应码非200");
        }
        cJSON_Delete(root);
        return ESP_FAIL;
    }
    // 更新data指针
    *data = cJSON_GetObjectItem(root, "data");
    return ESP_OK;
}

5.6 HTTP事件处理函数

        在目录 5.4 通用的HTTP请求函数 里,调用 esp_http_client_perform 函数后会阻塞当前任务,这个函数结束(返回)了,整个 HTTP 流程也结束了,但是我们没有看到数据的接收或事件的处理,这些都在调用 esp_http_client_init 函数时传入的 esp_http_client_config_t 类型的结构体里的 event_handler 成员变量指向的事件处理函数里实现。
        event_handler 成员变量是一个函数指针,类型为 http_event_handle_cb,具体定义如下:

typedef esp_err_t (*http_event_handle_cb)(esp_http_client_event_t *evt);

        可以看到事件回调函数的入参为指向 esp_http_client_event_t 结构体的指针,该结构体定义如下:
        如果事件ID为 HTTP_EVENT_ON_DATA,那么数据是放在 evt->data 里的。而 user_data 是初始化client的时候传入的缓冲区,因为如果数据比较大,HTTP一次性可能传输不完,每一次调用事件处理函数要传输响应体时,data成员指向传输的数据。

/**
 * @brief      HTTP Client events data
 */
typedef struct esp_http_client_event {
    esp_http_client_event_id_t event_id;    // 事件ID
    esp_http_client_handle_t client;        // 句柄
    void *data;                             // 事件数据缓存
    int data_len;                           // 事件数据长度
    void *user_data;                        // 用户数据
    char *header_key;                       // http头密钥
    char *header_value;                     // http请求头
} esp_http_client_event_t;

        我们实现事件处理回调函数的时候,都是通过判断事件ID来进行不同的处理,事件ID枚举定义如下:

/**
 * @brief   HTTP Client events id
 */
typedef enum {
    HTTP_EVENT_ERROR = 0,       // 执行期间出现任何错误时,会发生此事件
    HTTP_EVENT_ON_CONNECTED,    // HTTP连接到服务器
    HTTP_EVENT_HEADERS_SENT,    // 发送请求头
    HTTP_EVENT_HEADER_SENT = HTTP_EVENT_HEADERS_SENT, 
    HTTP_EVENT_ON_HEADER,       // 接收到响应头
    HTTP_EVENT_ON_DATA,         // 接收到响应体
    HTTP_EVENT_ON_FINISH,       // HTTP会话完成
    HTTP_EVENT_DISCONNECTED,    // HTTP断开事件
    HTTP_EVENT_REDIRECT,        // 拦截HTTP重定向,以便手动处理
} esp_http_client_event_id_t;

        下面就是结合五个HTTP服务实现的事件处理函数:重点看接收到 HTTP_EVENT_ON_HEADER 和 HTTP_EVENT_ON_DATA 事件的处理,一个是接收到响应头,一个是接收到响应体。

// 固件下载相关变量
static FILE *firmware_file = NULL;     // 固件文件指针
static char firmware_path[128] = {0};  // 固件文件路径
static uint32_t firmware_size = 0;     // 固件文件大小
static uint32_t firmware_received = 0; // 已接收的固件数据大小
static char firmware_md5[33] = {0};    // 服务器返回的固件MD5值(32个字符+结束符)
static mbedtls_md5_context md5_ctx;    // MD5计算上下文

/**
 * @brief HTTP事件处理函数
 * @param evt HTTP事件结构体指针
 * @return esp_err_t 错误码
 */
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    static char *output_buffer;                // 用于存储响应体的缓冲区
    static int output_len;                     // 存储读取的字节数
    static bool is_json_response = false;      // 标记是否为JSON响应
    static char content_type_buffer[64] = {0}; // 保存Content-Type值的缓冲区

    switch (evt->event_id)
    {
    case HTTP_EVENT_ERROR: // HTTP事件错误
        // 直接返回
        break;
    case HTTP_EVENT_ON_CONNECTED: // HTTP事件连接成功
        // 连接时重置Content-Type标记和缓冲区
        is_json_response = false;
        memset(content_type_buffer, 0, sizeof(content_type_buffer));
        break;
    case HTTP_EVENT_HEADER_SENT: // HTTP事件头信息发送事件
        // 直接返回
        break;
    case HTTP_EVENT_ON_HEADER: // HTTP事件头信息接收事件
        // 保存Content-Type响应头
        if (strcmp(evt->header_key, "Content-Type") == 0 || strcmp(evt->header_key, "Content-type") == 0)
        {
            ESP_LOGI(HTTP_TAG, "Content-Type: %s", evt->header_value);
            // 保存Content-Type值
            strncpy(content_type_buffer, evt->header_value, sizeof(content_type_buffer) - 1);
            content_type_buffer[sizeof(content_type_buffer) - 1] = '\0'; // 确保字符串结束

            // 判断是否为JSON响应
            is_json_response = (strstr(content_type_buffer, "application/json") != NULL);
            ESP_LOGI(HTTP_TAG, "响应类型: %s", is_json_response ? "JSON" : "二进制");

            // 如果是二进制响应,准备文件操作
            if (!is_json_response)
            {
                // 关闭可能已经打开的文件
                if (firmware_file != NULL)
                {
                    fclose(firmware_file);
                    firmware_file = NULL;
                }

                // 确保固件路径已设置,在目录 5.9 下载设备固件里设置固件路径
                if (strlen(firmware_path) == 0)
                {
                    ESP_LOGE(HTTP_TAG, "固件路径未设置,无法创建文件");
                }
                else
                {
                    // 打开文件准备写入
                    firmware_file = fopen(firmware_path, "wb");
                    ESP_LOGI(HTTP_TAG, "已创建固件文件: %s", firmware_path);
                    firmware_received = 0;

                    // 初始化MD5计算
                    mbedtls_md5_init(&md5_ctx);
                    mbedtls_md5_starts(&md5_ctx);
                }
            }
        }
        else if (strcmp(evt->header_key, "Content-Length") == 0)
        {
            // 保存内容长度
            firmware_size = atoi(evt->header_value);
            ESP_LOGI(HTTP_TAG, "Content-Length: %d字节", firmware_size);
        }
        break;
    case HTTP_EVENT_ON_DATA: // HTTP事件数据接收事件
        // 清理缓冲区以便处理新的请求
        if (output_len == 0 && evt->user_data)
        {
            memset(evt->user_data, 0, is_json_response ? MAX_HTTP_OUTPUT_BUFFER : MAX_HTTP_FILE_RESPONSE_BUFFER); // 根据响应类型清理缓冲区
        }
        // 处理分块和非分块响应
        if (evt->user_data)
        {
            // 处理非JSON响应            
            if (!is_json_response)
            {
                if (firmware_file != NULL)
                {
                    // 写入文件
                    size_t written = fwrite(evt->data, 1, evt->data_len, firmware_file);
                    if (written != evt->data_len)
                    {
                        ESP_LOGE(HTTP_TAG, "写入固件文件失败: %d/%d字节", written, evt->data_len);
                        esp_http_client_close(evt->client); // 结束http会话,标记为失败
                    }
                    else
                    {
                        mbedtls_md5_update(&md5_ctx, (const unsigned char *)evt->data, evt->data_len); // 更新MD5计算
                        firmware_received += written;                                                  // 更新已接收的字节数
                        ESP_LOGI(HTTP_TAG, "写入固件总计: %d/%d字节, 速度:%.2fKB/s, 进度: %d%%", firmware_received, firmware_size, (float)written / 1024, (firmware_received * 100) / firmware_size);
                    }
                }
                else
                {
                    ESP_LOGI(HTTP_TAG, "接收到二进制数据: %d字节,但打开文件失败", evt->data_len);
                    esp_http_client_close(evt->client); // 结束http会话,标记为失败
                }
            }
            // 处理JSON响应
            else
            {
                char *user_data_buf = (char *)evt->user_data;
                size_t available_space = MAX_HTTP_OUTPUT_BUFFER - output_len - 1;
                int copy_len = MIN(evt->data_len, available_space);
                if (copy_len > 0)
                {
                    // JSON响应处理 - 复制到缓冲区
                    memcpy(user_data_buf + output_len, evt->data, copy_len);
                    output_len += copy_len;
                    user_data_buf[output_len] = '\0'; // 确保字符串结束
                    ESP_LOGI(HTTP_TAG, "接收到JSON数据: %d字节", copy_len);
                }
                else if (available_space == 0)
                {
                    ESP_LOGW(HTTP_TAG, "用户数据缓冲区已满,无法复制更多数据");
                }
            }
        }
        else
        {
            // 当未提供user_data时的原始处理
            if (!esp_http_client_is_chunked_response(evt->client))
            {
                int content_len = esp_http_client_get_content_length(evt->client);
                if (output_buffer == NULL)
                {
                    output_buffer = (char *)calloc(content_len + 1, sizeof(char));
                    output_len = 0;
                    if (output_buffer == NULL)
                    {
                        ESP_LOGE(HTTP_TAG, "为输出缓冲区分配内存失败");
                        return ESP_FAIL;
                    }
                }
                int copy_len = MIN(evt->data_len, (content_len - output_len));
                if (copy_len)
                {
                    memcpy(output_buffer + output_len, evt->data, copy_len);
                    output_len += copy_len;
                }
            }
        }
        break;
    case HTTP_EVENT_ON_FINISH: // HTTP会话完成事件
        // 会话完成时记录响应类型
        ESP_LOGI(HTTP_TAG, "HTTP会话完成,响应类型: %s", is_json_response ? "JSON" : "二进制");

        // 如果有打开的固件文件,关闭它
        if (firmware_file != NULL)
        {
            fclose(firmware_file);
            firmware_file = NULL;
            ESP_LOGI(HTTP_TAG, "固件文件已保存: %s, 大小: %d字节", firmware_path, firmware_received);
        }

        if (output_buffer != NULL)
        {
            free(output_buffer);
            output_buffer = NULL;
        }
        output_len = 0;
        break;
    case HTTP_EVENT_DISCONNECTED: // HTTP事件断开连接事件
        // 连接断开时重置响应类型标记
        is_json_response = false;
        memset(content_type_buffer, 0, sizeof(content_type_buffer));

        // 如果有打开的固件文件,关闭它
        if (firmware_file != NULL)
        {
            fclose(firmware_file);
            firmware_file = NULL;
            ESP_LOGW(HTTP_TAG, "连接断开,固件文件已关闭: %s, 已接收: %d字节", firmware_path, firmware_received);

            // 释放MD5资源
            mbedtls_md5_free(&md5_ctx);
        }
        if (output_buffer != NULL)
        {
            free(output_buffer);
            output_buffer = NULL;
        }
        output_len = 0;
        break;
    }
    return ESP_OK;
}

5.7 下载设备配置

        下面开始就是那五个服务了,ESP-IDF的HTTP client组件就差不多讲完了,后面都是些数据的处理和构造,这五个服务的数据构造供大家参考,具体还得看你的业务需求。重点可以看看 目录 5.9 下载设备固件 ,因为下载设备固件获得的响应体类型是二进制数据,但是将二进制数据写入文件是在事件处理函数里进行的。

/**
 * 下载设备配置
 * @return esp_err_t 执行结果
 */
esp_err_t download_device_config(void)
{
    char device_sn[16];
    app_storage_get_info(APP_STORAGE_LOCAL_SERIAL_NUMBER, device_sn); // 获取设备SN
    // 构建请求路径
    char path[64] = {0};
    snprintf(path, sizeof(path), "/device/download_config?sn=%s", device_sn);
    // 发送GET请求
    esp_err_t result = http_rest_request(path, NULL, HTTP_GET);
    if (result != ESP_OK)
    {
        ESP_LOGE(HTTP_TAG, "下载设备配置失败: %s", esp_err_to_name(result));
        return result;
    }

    // 解析响应数据
    cJSON *data = NULL;
    cJSON *root_json = NULL;
    result = parse_response_json(response_buffer, &data);
    if (result != ESP_OK || data == NULL)
    {
        ESP_LOGE(HTTP_TAG, "解析设备配置响应失败");
        return ESP_FAIL;
    }
    // 获取root对象用于后续释放
    root_json = cJSON_Parse(response_buffer);
    // 处理配置数据
    strcpy(device_config.name, cJSON_GetObjectItem(data, "name")->valuestring);
    device_config.heartbeat = cJSON_GetObjectItem(data, "heartbeat")->valueint;
    strcpy(device_config.timestamp, cJSON_GetObjectItem(data, "timestamp")->valuestring); // 2025-05-15T18:03:25.867439
    app_time_date_t time_date;
    time_date.date.year = atoi(device_config.timestamp + 1); // 跳过第一个字符,从年的第一个数字开始如(2025:025)
    time_date.date.month = atoi(device_config.timestamp + 5);
    time_date.date.day = atoi(device_config.timestamp + 8);
    time_date.time.hour = atoi(device_config.timestamp + 11);
    time_date.time.minute = atoi(device_config.timestamp + 14);
    time_date.time.second = atoi(device_config.timestamp + 17); // 跳过小数点

    // 将本机信息的设备名称设置成从管理终端获取到的设备名称
    app_storage_set_info(APP_STORAGE_LOCAL_NAME, device_config.name);
    cJSON_Delete(root_json);
    return ESP_OK;
}

5.8 查询设备升级固件信息

/**
 * @brief 查询设备最新固件版本
 * @param model 设备型号
 * @param hardware 硬件版本
 * @param firmware 当前固件版本
 * @param firmware_info 固件信息结构体指针
 * @note 此函数会查询指定设备的最新固件版本信息,包括固件版本号和MD5值。
 *       调用此函数后,固件信息会存储在firmware_info结构体中。
 * @return esp_err_t 执行结果
 */
esp_err_t query_latest_firmware_version(const char *model, const char *hardware, const char *firmware, firmware_info_t *firmware_info)
{
    // 参数检查
    if (!model || !hardware || !firmware)
    {
        ESP_LOGE(HTTP_TAG, "查询固件版本参数错误:参数不能为空");
        return ESP_ERR_INVALID_ARG;
    }
    // 检查是否连接上管理终端
    if (!app_wifi_get_status() || !is_connected_manage)
    {
        ESP_LOGW(HTTP_TAG, "未连接上管理终端,尝试重新连接");
        return ESP_ERR_NOT_FOUND;
    }
    // 构建请求路径
    char path[128] = {0};
    snprintf(path, sizeof(path), "/device/query_latest_firmware_version?model=%s&hardware=%s&firmware=%s", model, hardware, firmware);

    // 发送GET请求
    esp_err_t result = http_rest_request(path, NULL, HTTP_GET);
    if (result != ESP_OK)
    {
        ESP_LOGE(HTTP_TAG, "查询最新固件版本失败: %s", esp_err_to_name(result));
        return result;
    }

    // 解析响应数据
    cJSON *data = NULL;
    cJSON *root_json = NULL;
    result = parse_response_json(response_buffer, &data);
    if (result != ESP_OK)
    {
        ESP_LOGE(HTTP_TAG, "解析固件版本响应失败");
        return ESP_FAIL;
    }
    // 获取root对象用于后续释放
    root_json = cJSON_Parse(response_buffer);
    if (data && cJSON_IsNull(data))
    {
        ESP_LOGE(HTTP_TAG, "未发现新固件");
        // 未发现新固件
        cJSON_Delete(root_json);
        return ESP_FAIL;
    }
    strcpy(firmware_info->firmware, cJSON_GetObjectItem(data, "firmware")->valuestring); // 存储固件版本号
    strcpy(firmware_info->md5, cJSON_GetObjectItem(data, "md5")->valuestring);           // 存储MD5值

    // 保存MD5值到全局变量,用于后续下载固件时校验
    strncpy(firmware_md5, firmware_info->md5, sizeof(firmware_md5) - 1);
    firmware_md5[sizeof(firmware_md5) - 1] = '\0'; // 确保字符串结束
    ESP_LOGI(HTTP_TAG, "获取到固件MD5值: %s", firmware_md5);

    // 释放JSON对象
    cJSON_Delete(root_json);
    return ESP_OK;
}

5.9 下载设备固件

        可以重点看看这个函数,因为响应类型是二进制流,因此不使用通用的HTTP请求函数。

#define MAX_HTTP_FILE_RESPONSE_BUFFER (1024 * 10)                   // HTTP文件响应缓冲区大小(需将CONFIG_LWIP_TCP_WND_DEFAULT调整为对应大小)
char file_response_buffer[MAX_HTTP_FILE_RESPONSE_BUFFER + 1] = {0}; // HTTP文件响应缓冲区

/**
 * @brief 下载设备固件
 * @param model 设备型号
 * @param hardware 硬件版本
 * @param firmware 当前固件版本
 * @param new_firmware 新固件版本
 * @return esp_err_t 执行结果
 * @note 此函数会下载指定设备的固件到SD卡中,下载完成后会验证MD5值,如果MD5值不匹配,则会删除下载的固件文件。
 *       此函数不使用http_rest_request函数请求,因为固件下载是一个较大文件,需要指定块大小提高下载速度。
 */
esp_err_t download_device_firmware(const char *model, const char *hardware, const char *firmware, const char *new_firmware)
{
    // 参数检查
    if (!model || !hardware || !firmware)
    {
        ESP_LOGE(HTTP_TAG, "下载固件参数错误:参数不能为空");
        return ESP_ERR_INVALID_ARG;
    }

    // 构建请求路径
    char path[128] = {0};
    snprintf(path, sizeof(path), "/device/download_firmware?model=%s&hardware=%s&firmware=%s", model, hardware, firmware);

    ESP_LOGI(HTTP_TAG, "开始下载固件,请求路径: %s", path);
    // 构建完整URL
    char *full_url = build_full_url(path);
    if (!full_url)
    {
        return ESP_FAIL;
    }
    // 确保SD卡已挂载(在app_audio_player_init<main.c>时已经挂载,此处无需再次挂载)
    // esp_err_t ret = ph_sd_card_init();
    // if (ret != ESP_OK)
    // {
    //     ESP_LOGE(HTTP_TAG, "SD卡挂载失败: %s", esp_err_to_name(ret));
    //     free(full_url);
    //     return ret;
    // }

    // 创建固件存储目录
    char *firmware_dir = (char *)malloc(strlen(FIRMWARE_BASE_DIR) + strlen(new_firmware) + 10); // 预留10个字符用于路径
    memset(firmware_dir, 0, strlen(FIRMWARE_BASE_DIR) + strlen(new_firmware) + 10);
    sprintf(firmware_dir, "%s%s/", FIRMWARE_BASE_DIR, new_firmware);
    struct stat st;
    if (stat(firmware_dir, &st) != 0)
    {
        // 目录不存在,创建目录
        if (mkdir(firmware_dir, ACCESSPERMS) != 0) // 赋予所有用户有读、写、执行权限
        {
            ESP_LOGE(HTTP_TAG, "创建固件目录失败: %s", firmware_dir);
            free(full_url);
            free(firmware_dir);
            return ESP_FAIL;
        }
        ESP_LOGI(HTTP_TAG, "已创建固件目录: %s", firmware_dir);
    }

    // 根据固件版本号生成文件路径
    memset(firmware_path, 0, sizeof(firmware_path));
    snprintf(firmware_path, sizeof(firmware_path), "%sfirmware_%s%s", firmware_dir, new_firmware, FIRMWARE_PACKAGE_SUFFIX);
    free(firmware_dir);
    ESP_LOGI(HTTP_TAG, "固件将保存到: %s", firmware_path);

    // 重置固件接收状态
    firmware_received = 0;
    firmware_size = 0;

    // 基础配置
    esp_http_client_config_t config = {
        .url = full_url,
        .event_handler = _http_event_handler, // 使用统一的事件处理器
        .user_data = file_response_buffer,    // 响应数据存入buffer
        .disable_auto_redirect = true,
        .buffer_size = MAX_HTTP_FILE_RESPONSE_BUFFER, // 设置缓冲区大小,保证下载速度
    };

    // 初始化客户端
    esp_http_client_handle_t client = esp_http_client_init(&config);
    if (!client)
    {
        ESP_LOGE(HTTP_TAG, "HTTP客户端初始化失败");
        free(full_url);
        return ESP_FAIL;
    }

    // 设置GET方法
    esp_http_client_set_method(client, HTTP_METHOD_GET);

    // 执行请求
    esp_err_t ret = esp_http_client_perform(client);
    if (ret == ESP_OK)
    {
        int status_code = esp_http_client_get_status_code(client);
        int content_length = esp_http_client_get_content_length(client);

        if (status_code == 200)
        {
            ESP_LOGI(HTTP_TAG, "固件下载成功,数据长度: %d 字节", content_length);
            ESP_LOGI(HTTP_TAG, "固件已保存到: %s", firmware_path);

            // 检查固件文件是否存在
            if (ph_sd_card_file_exists(firmware_path))
            {
                ESP_LOGI(HTTP_TAG, "固件文件已成功保存到SD卡");
            }
            else
            {
                ESP_LOGE(HTTP_TAG, "固件文件未找到,保存可能失败");
                ret = ESP_FAIL;
            }
            // 验证文件大小是否与Content-Length一致
            if (firmware_size > 0 && firmware_received != firmware_size)
            {
                ESP_LOGW(HTTP_TAG, "固件文件大小不匹配: 接收 %d字节, 预期 %d字节", firmware_received, firmware_size);
                ret = ESP_FAIL;
            }
            // md5校验
            // 计算并验证MD5
            if (strlen(firmware_md5) > 0)
            {
                unsigned char md5_digest[16];
                char calculated_md5[33] = {0};

                // 完成MD5计算
                mbedtls_md5_finish(&md5_ctx, md5_digest);
                mbedtls_md5_free(&md5_ctx);

                // 将MD5二进制值转换为十六进制字符串
                for (int i = 0; i < 16; i++)
                {
                    sprintf(&calculated_md5[i * 2], "%02x", md5_digest[i]);
                }

                ESP_LOGI(HTTP_TAG, "计算的固件MD5值: %s", calculated_md5);

                // 比较MD5值
                if (strcasecmp(calculated_md5, firmware_md5) == 0)
                {
                    ESP_LOGI(HTTP_TAG, "固件MD5校验成功");
                    ret = ESP_OK;
                }
                else
                {
                    ESP_LOGE(HTTP_TAG, "固件MD5校验失败: 期望值=%s, 计算值=%s", firmware_md5, calculated_md5);
                    ret = ESP_FAIL;
                    // 删除校验失败的固件文件
                    if (ph_sd_card_file_exists(firmware_path))
                    {
                        if (remove(firmware_path) == 0)
                        {
                            ESP_LOGI(HTTP_TAG, "已删除校验失败的固件文件: %s", firmware_path);
                        }
                        else
                        {
                            ESP_LOGE(HTTP_TAG, "删除校验失败的固件文件失败: %s", firmware_path);
                        }
                    }
                }
            }
            else
            {
                ESP_LOGW(HTTP_TAG, "未收到固件MD5值,跳过MD5校验");
                mbedtls_md5_free(&md5_ctx);
                ret = ESP_FAIL;
            }
        }
        else
        {
            ESP_LOGE(HTTP_TAG, "固件下载失败: 服务器返回非200状态码");
            ret = ESP_FAIL;
        }
    }
    else
    {
        ESP_LOGE(HTTP_TAG, "固件下载请求失败: %s", esp_err_to_name(ret));
    }

    // 清理资源
    esp_http_client_cleanup(client);
    free(full_url);
    return ret;
}

5.10 上报设备使用记录

/**
 * 上报设备使用记录
 * @return esp_err_t 执行结果
 */
esp_err_t report_usage_logs(void)
{
    esp_err_t result = ESP_OK;
    // 检查是否有使用记录需要上报
    app_usage_log_t *logs = NULL;
    uint16_t count = 0;
    char temp[8];
    app_storage_log_get_all(&logs, &count);
    if (count > 0)
    {
        // 构建设备
        root = cJSON_CreateObject();
        // 设置设备序列号
        cJSON_AddStringToObject(root, "sn", "DROUK0LezZ0");
        // 构建JSON数组
        cJSON *logs_array = cJSON_CreateArray();
        for (uint16_t i = 0; i < count; i++)
        {
            cJSON *log_item = cJSON_CreateObject();
            cJSON_AddNumberToObject(log_item, "sequence", logs[i].id);
            cJSON_AddStringToObject(log_item, "channel", logs[i].channel == CHANNEL_A ? "A" : "B");
            sprintf(temp, "p%02d", logs[i].plan);
            cJSON_AddStringToObject(log_item, "plan", temp);
            cJSON_AddStringToObject(log_item, "start_time", logs[i].start_time);
            cJSON_AddNumberToObject(log_item, "duration", logs[i].duration);
            cJSON_AddItemToArray(logs_array, log_item);
        }
        cJSON_AddItemToObject(root, "usageLogs", logs_array);
        // 转换为字符串
        char *json_str = cJSON_PrintUnformatted(root);
        if (!json_str)
        {
            ESP_LOGE(HTTP_TAG, "JSON序列化失败");
            return ESP_FAIL;
        }
        // 打印JSON数据
        ESP_LOGI(HTTP_TAG, "上报的JSON数据: %s", json_str);

        // 发送HTTP请求
        result = http_rest_request("/device/usage_logs", json_str, HTTP_POST);
        // 释放JSON对象
        cJSON_Delete(root);
        // 释放内存
        free(logs);
        // 释放内存
        free(json_str);

        // 检查HTTP请求是否成功
        if (result == ESP_OK)
        {
            // 检查响应是否符合成功标准(code=200)
            cJSON *data = NULL;
            cJSON *root_json = NULL;
            result = parse_response_json(response_buffer, &data);
            if (result == ESP_OK)
            {
                ESP_LOGI(HTTP_TAG, "设备使用记录上报成功");
                root_json = cJSON_Parse(response_buffer);
            }
            // 清除已上报的使用记录
            app_storage_log_clear_all();
            if (root_json) {
                cJSON_Delete(root_json);
            }
        }
    }
    else
    {
        ESP_LOGI(HTTP_TAG, "没有需要上报的使用记录");
    }
    return result;
}

5.11 定时上报设备状态

/**
 * @brief 定时上报设备状态回调函数
 * @param xTimer 定时器句柄
 */
static void status_report_timer_callback(TimerHandle_t xTimer)
{
    char temp[18];
    ESP_LOGI(HTTP_TAG, "设备状态定时上报触发");

    // 使用全局JSON对象变量
    root = cJSON_CreateObject();
    device = cJSON_CreateObject();
    cJSON_AddItemToObject(root, "device", device);

    app_storage_local_info_t local_info;
    memset(&local_info, 0, sizeof(local_info)); // 确保初始化
    esp_err_t err = app_storage_get_all_info(&local_info);
    cJSON_AddStringToObject(device, "model", local_info.name);
    cJSON_AddStringToObject(device, "sn", local_info.serial_number);
    cJSON_AddNumberToObject(device, "battery", ph_battery_power_control_get_battery_soc());
    cJSON_AddStringToObject(device, "hardware", local_info.hardware_version);
    cJSON_AddStringToObject(device, "firmware", local_info.firmware_version);
    cJSON_AddStringToObject(device, "mac", local_info.mac);
    cJSON_AddStringToObject(device, "ip", local_info.ip);

    // 处理通道A
    cJSON *channel_a = cJSON_CreateObject();
    cJSON *audio_a = cJSON_CreateObject();
    cJSON *light_a = cJSON_CreateObject();
    cJSON *electrical_a = cJSON_CreateObject();
    cJSON_AddItemToObject(channel_a, "audio", audio_a);           // 添加audio子对象
    cJSON_AddItemToObject(channel_a, "light", light_a);           // 添加light子对象
    cJSON_AddItemToObject(channel_a, "electrical", electrical_a); // 添加electrical子对象

    app_state_channel_status_t *channel_status_a = app_state_get_channel_info(APP_STATE_CHANNEL_A); // 获取通道A的信息
    if (channel_status_a != NULL)
    {
        cJSON_AddStringToObject(channel_a, "channel", "A"); // 标记通道A
        // 判断治疗状态(运行、空闲或暂停)
        cJSON_AddStringToObject(channel_a, "status", channel_status_a->status == APP_STATE_STATUS_RUNNING ? "Work" : (channel_status_a->status == APP_STATE_STATUS_IDLE ? "Idle" : "Pause"));
        sprintf(temp, "p%02d", channel_status_a->plan);                             // 格式化方案编号
        cJSON_AddStringToObject(channel_a, "plan", temp);                           // 添加方案编号
        cJSON_AddNumberToObject(channel_a, "duration", channel_status_a->duration); // 添加治疗持续时间

        cJSON_AddBoolToObject(audio_a, "connected", channel_status_a->audio_state);         // 添加声连接状态
        cJSON_AddNumberToObject(audio_a, "level", channel_status_a->audio_value);           // 添加声值
        cJSON_AddBoolToObject(light_a, "connected", channel_status_a);                      // 添加光连接状态
        cJSON_AddNumberToObject(light_a, "level", channel_status_a->light_value);           // 添加光值
        cJSON_AddBoolToObject(electrical_a, "connected", channel_status_a->electric_state); // 添加电连接状态
        cJSON_AddNumberToObject(electrical_a, "level", channel_status_a->electric_value);   // 添加电值
    }
    cJSON *channels_array = cJSON_CreateArray();
    cJSON_AddItemToArray(channels_array, channel_a);
    cJSON_AddItemToObject(root, "channels", channels_array);

    // 调用设备状态上报函数,上报通道A状态
    esp_err_t ret = report_device_status(root);
    if (ret != ESP_OK)
    {
        connect_manage_attempts++;
        if (connect_manage_attempts == 2 && is_connected_manage) // 连续2次失败后,将管理IP状态置为未连接
        {
            // 便携机 连接失败,将管理IP状态置为未连接
        }
        if (connect_manage_attempts >= MAX_RETRY_ATTEMPTS && is_connected_manage) // 连接失败达到最大重试次数后,将管理IP状态置为错误
        {
            // 停止定时器
            xTimerStop(status_report_timer, 0);
            is_connected_manage = false; // 标记为未连接状态
            connect_manage_attempts = 0; // 重置重试次数
            // 便携机 连接失败,将管理IP状态置为错误
        }
        ESP_LOGE(HTTP_TAG, "设备状态上报失败: %s", esp_err_to_name(ret));
    }
    else
    {
        connect_manage_attempts = 0; // 上报成功,重置重试次数
        is_connected_manage = true;  // 标记为已连接状态
        ESP_LOGI(HTTP_TAG, "设备状态上报成功");
    }
    // 释放JSON对象
    cJSON_Delete(root);
}

/**
 * 上报设备状态
 * @param status 设备状态结构体指针
 * @return esp_err_t 执行结果
 */
esp_err_t report_device_status(const cJSON *json_data)
{
    if (json_data == NULL)
    {
        ESP_LOGE(HTTP_TAG, "JSON数据指针为空");
        return ESP_ERR_INVALID_ARG;
    }
    esp_err_t result = ESP_OK;

    // 转换为字符串 - 使用静态缓冲区而不是动态分配
    static char json_buffer[1024]; // 确保足够大以容纳JSON字符串
    char *json_str = cJSON_PrintUnformatted(json_data);
    if (json_str)
    {
        strncpy(json_buffer, json_str, sizeof(json_buffer) - 1);
        json_buffer[sizeof(json_buffer) - 1] = '\0'; // 确保字符串结束
        // 打印JSON数据
        ESP_LOGI(HTTP_TAG, "上报的JSON数据: %s", json_buffer);
        free(json_str); // 释放临时字符串

        // 发送HTTP请求
        result = http_rest_request("/device/status", json_buffer, HTTP_PUT);
    }
    else
    {
        ESP_LOGE(HTTP_TAG, "JSON序列化失败");
        return ESP_FAIL;
    }

    // 检查HTTP请求是否成功
    if (result == ESP_OK)
    {
        // 检查响应是否符合成功标准(code=200)
        cJSON *data = NULL;
        cJSON *root_json = NULL;
        result = parse_response_json(response_buffer, &data);
        if (result == ESP_OK)
        {
            ESP_LOGI(HTTP_TAG, "设备状态上报成功");
            root_json = cJSON_Parse(response_buffer);
        }
        if (root_json) {
            cJSON_Delete(root_json);
        }
    }
    return result;
}

5.12 设置服务器IP并尝试连接管理终端

        这里就是与FreeRTOS相关的了,获得IP地址后调用第一个函数去连接管理终端。然后会创建一个任务,这个任务实际上就做两件事,即调用 目录 5.7 下载设备配置 和 目录 5.10 上报设备使用记录 这两个函数,然后根据他们两的返回值来判断是否成功与服务器通信。并没有“连接”这一说,只是说能否获取服务器的资源,你IP地址不对,自然无法与服务器通信。主要通过全局变量来标记是否连接上管理终端。可以学习一下这个思路。

static bool is_connected_manage = false;    // 标记是否已连接到管理终端
static uint8_t connect_manage_attempts = 0; // 连接管理终端尝试次数

/**
 * @brief 设置管理终端IP地址并存储到NVS
 * @param ip IP地址数组指针
 * @return esp_err_t 执行结果
 */
esp_err_t app_http_set_manage_ip(uint8_t *ip)
{
    if (ip == NULL)
    {
        ESP_LOGE(HTTP_TAG, "IP地址为空");
        return ESP_ERR_INVALID_ARG;
    }

    // 将IP地址转换为字符串格式
    char ip_str[16] = {0};
    snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);

    // 更新当前使用的IP地址字符串
    strncpy(manage_ip_str, ip_str, sizeof(manage_ip_str) - 1);
    manage_ip_str[sizeof(manage_ip_str) - 1] = '\0'; // 确保字符串结束
    ESP_LOGI(HTTP_TAG, "管理终端IP地址已设置并存储: %s", manage_ip_str);
    // 启动下载配置及使用记录上报任务
    // 如果句柄为空才创建、防止多次创建
    if(!http_network_access_task_handle)
    {
        xTaskCreate(network_access_task, "HTTPNetworkAccessTask", 4096, NULL, 8, &http_network_access_task_handle);
    }
    return ESP_OK;
}

/**
 * @brief 入网成功任务函数
 * @param param 回调参数
 * 成功连接上管理端后被调用,执行设备配置下载和使用日志上报
 */
void network_access_task(void *pvParameters)
{
    for (;;)
    {
        // 下载设备配置请求
        esp_err_t download_config_ret = download_device_config();
        if (download_config_ret != ESP_OK)
        {
            connect_manage_attempts++;
            ESP_LOGE(HTTP_TAG, "下载设备配置失败: %s", esp_err_to_name(download_config_ret));
        }
        if (device_config.heartbeat != 0)
        {
            // 更新定时器周期(更新时会自动启动定时器)
            if (xTimerChangePeriod(status_report_timer, pdMS_TO_TICKS(device_config.heartbeat * 1000), 100) != pdPASS)
            {
                ESP_LOGE(HTTP_TAG, "更新设备状态上报定时器周期失败,使用默认间隔: %d ms");
            }
            else
            {
                ESP_LOGI(HTTP_TAG, "设备状态上报定时器已更新,间隔: %d ms", device_config.heartbeat * 1000);
            }
            // 暂停定时器
            xTimerStop(status_report_timer, 0);
        }

        // 上报使用日志请求
        esp_err_t report_logs_ret = report_usage_logs();
        if (report_logs_ret != ESP_OK)
            connect_manage_attempts++;
        if (download_config_ret != ESP_OK || report_logs_ret != ESP_OK)
        {
            if(report_logs_ret != ESP_OK) {
                ESP_LOGE(HTTP_TAG, "上报使用日志失败: %s", esp_err_to_name(report_logs_ret));
            }else if(download_config_ret != ESP_OK) {
                ESP_LOGE(HTTP_TAG, "下载设备配置失败:%s", esp_err_to_name(download_config_ret));
            }
            if (connect_manage_attempts >= MAX_RETRY_ATTEMPTS)
            {
                // 便携机 连接失败,将管理IP状态置为错误
                xSemaphoreGive(g_http_gui_semaphore);
                ESP_LOGE(HTTP_TAG, "达到最大重试次数");
                is_connected_manage = false;
                connect_manage_attempts = 0;
                ESP_LOGI(HTTP_TAG, "达到最大重试次数,立即退出任务");
                http_network_access_task_handle = NULL;
                vTaskDelete(NULL);
            }
            vTaskDelay(1000 / portTICK_PERIOD_MS); // 等待5秒后重试
        }
        else // 下载配置和上报使用记录都成功
        {
            // 启动设备状态上报定时器
            if (status_report_timer != NULL)
            {
                if (xTimerStart(status_report_timer, 0) != pdPASS)
                {
                    ESP_LOGE(HTTP_TAG, "启动设备状态上报定时器失败");
                    xTimerStart(status_report_timer, 0);
                }
            }
            else
            {
                ESP_LOGE(HTTP_TAG, "设备状态上报定时器未初始化");
            }
            is_connected_manage = true;
            connect_manage_attempts = 0;
            // 便携机 连接成功,将管理IP状态置为已连接
            xSemaphoreGive(g_http_gui_semaphore);
            // 上报成功后,立即退出任务
            ESP_LOGI(HTTP_TAG, "使用日志上报成功,立即退出任务");
            http_network_access_task_handle = NULL;
            vTaskDelete(NULL);
        }
    }
}

结语

        后续更新UDP组播广播和OTA升级。


网站公告

今日签到

点亮在社区的每一天
去签到