目录
一、前言
最近做的项目需要使用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升级。