当前位置: 首页 > news >正文

成都网站设计招聘全球招商网

成都网站设计招聘,全球招商网,用iPhone做网站服务器,响应式培训网站模板目录 一、前言 二、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 客户端实战解析…

目录

一、前言

二、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,                             // 定时器IDstatus_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构建URLsize_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/pathstrcpy(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;// 构建完整URLchar *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;    // 事件IDesp_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.867439app_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);// 构建完整URLchar *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校验// 计算并验证MD5if (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);// 处理通道AcJSON *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升级。

http://www.dtcms.com/a/579146.html

相关文章:

  • 如何用Postman测试API?
  • 学校网站建设目的是什么微网站开发平台案例
  • 学校网站建设问卷调查表如何选择佛山网站建设
  • shell(2)--shell脚本的编写
  • Linux挂载磁盘方法
  • 河南企业网官方网站昆明今天刚刚发生的新闻
  • 空间坐标系转换矩阵计算
  • 城市建设理论研究收录网站阿里服务器租用价格表
  • 研究生论文阅读指南:整理的心法
  • 合肥网站建设渠道建安培训官网
  • 专业的ai软著材料选哪家公司
  • 网站产品优化描述网站开发都有什么类别
  • 【Vue基础】--变化检测机制
  • 哪些网站可以做移动端模板新乡网站建设制作
  • 九章算MDPI解读【姜黄素】广西医大陶人川、雍翔智课题组:姜黄素对放疗/化疗引起的口腔黏膜炎的影响
  • 网站建设整改报告php创建网页
  • 网站制作应用网站开发调查问卷题
  • 网站的宽度广州网站开发服务
  • LangChain的核心组件之Models使用手册
  • zencart网站搬家plm项目管理系统
  • 缔客网络上海响应式网站建设建网站 行业 销售额
  • 做盗市相关网站如何在线上推广产品
  • iMX6ULL使用GUI Guider开发LVGL
  • 新乡移动网站建设制作视频特效
  • 丹阳市住房建设管理局网站wordpress怎么挂广告
  • 已经有域名 怎么做网站关键词首页优化
  • 无锡网站建设公司排名高端网站建设哪家公司好
  • 做网站的图片尺寸怎么设定dede wap网站
  • 龙虎榜——20251106
  • 深圳市做网站建设平台设计是做什么的