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

目錄

一、前言

二、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.pswp.cn/news/919677.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/919677.shtml
英文地址,請注明出處:http://en.pswp.cn/news/919677.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

動學學深度學習07-現代卷積神經網絡

動學學深度學習pytorch 參考地址&#xff1a;https://zh.d2l.ai/ 文章目錄動學學深度學習pytorch1-第07章-現代卷積神經網絡1. AlexNet1.1 AlexNet 的核心貢獻是什么&#xff1f;1.2 AlexNet 與 LeNet 的主要區別有哪些&#xff1f;1.3 為什么 AlexNet 需要 GPU 訓練&#xff1…

詳細講解Java中的反射和經典面試題(保姆級別)

1.1 反射的概述&#xff1a;專業的解釋&#xff08;了解一下&#xff09;&#xff1a;是在運行狀態中&#xff0c;對于任意一個類&#xff0c;都能夠知道這個類的所有屬性和方法&#xff1b;對于任意一個對象&#xff0c;都能夠調用它的任意屬性和方法&#xff1b;這種動態獲取…

MyCAT完整實驗報告

MyCAT完整實驗報告 ? 前言 剛剛看了一下前面的那篇MyCAT的文章 感覺有一些問題 所以拿出一篇文章再說一下 單獨構建了完整的實驗環境 這樣會全面一點 ? 安裝MyCAT #跳過? 主從配置 #不多追溯 因為我們選擇的主從 也可以做雙主機 但我們后邊再說? 環境搭建 一、環境規劃 服務…

機器翻譯論文閱讀方法:頂會(ACL、EMNLP)論文解析技巧

更多內容請見: 機器翻譯修煉-專欄介紹和目錄 文章目錄 一、論文選擇:快速判斷論文價值 1.1 關注核心會議與子領域 1.2 篩選標準 1.3 預讀篩選 1.4 快速定位關鍵信息 二、精讀解析 2.1 問題定義(5分鐘) 2.2 方法解剖(15分鐘) 2.3 實驗深挖(20分鐘) 2.4 批判性思考(10分…

Transformer模型實戰篇

引入 基于Transformers的NLP解決方案的步驟如下&#xff1a;&#xff08;以文本分類為例&#xff09; 導入相關包&#xff0c;General&#xff0c;可以詢問ai需要導什么包加載數據集&#xff0c;Data_loader&#xff0c;Datasets數據集劃分&#xff0c;測試機&#xff0c;驗證集…

深入(流批【牛批】框架)Flink的機制

flink本身是專注有狀態的無限流處理&#xff0c;有限流處理【batch批次】是無限流處理的一中特殊情況&#xff01;應用場景實時ETL 集成流計算現有的諸多數據通道和SQL靈活的加工能力&#xff0c;對流式數據進行實時清洗、歸并和結構化 處理&#xff1b;同時&#xff0c;對離線…

Git 2.15.0 64位安裝步驟Windows詳細教程從下載到驗證(附安裝包下載)

一、下載后雙擊運行 安裝包下載&#xff1a;https://pan.quark.cn/s/7200b32a1ecf&#xff0c;找到下載好的文件&#xff1a;?Git-2.15.0-64-bit.exe?雙擊這個文件&#xff0c;就會彈出安裝向導窗口&#xff0c;點 ??“Next”&#xff08;下一步&#xff09;?? 二、選擇…

在職老D滲透日記day23:sqli-labs靶場通關(第29關-31關)http參數過濾

5.29.第29關 http參數過濾 閉合5.29.1.手動注入&#xff08;1&#xff09;判斷注入類型、注入點閉合&#xff08;2&#xff09;有回顯&#xff0c;優先用聯合查詢注入&#xff0c;判讀字段數?id1&id2 order by 3 -- ?id1&id2 order by 4 --&#xff08;3&#xff09;…

Spring Boot整合Amazon SNS實戰:郵件訂閱通知系統開發

Spring Boot整合Amazon SNS實戰引言配置服務總結新用戶可獲得高達 200 美元的服務抵扣金 亞馬遜云科技新用戶可以免費使用亞馬遜云科技免費套餐&#xff08;Amazon Free Tier&#xff09;。注冊即可獲得 100 美元的服務抵扣金&#xff0c;在探索關鍵亞馬遜云科技服務時可以再額…

LeetCode_動態規劃1

動態規劃1.動態規劃總結1.1 01背1.1.1 二維數組1.1.2 一維數組1.2 完全背包2.斐波那契數(力扣509)3.爬樓梯(力扣70)4.使用最小花費爬樓梯(力扣746)5.不同路徑(力扣62)6.不同路徑 II(力扣63)7.整數拆分(力扣343)8.不同的二叉搜索樹(力扣96)9.分割等和子集(力扣416)10.最后一塊石…

【STM32】HAL庫中的實現(九):SPI(串行外設接口)

SPI 接口通信原理 SPI&#xff08;Serial Peripheral Interface&#xff09;是全雙工主從通信協議&#xff0c;特點是&#xff1a; 信號線功能SCK串行時鐘MOSI主設備輸出&#xff0c;從設備輸入MISO主設備輸入&#xff0c;從設備輸出CS&#xff08;NSS&#xff09;片選信號&am…

Git常用操作大全(附git操作命令)

Git常用操作大全 一、基礎配置 1.1 設置用戶名和郵箱 git config --global user.name "你的名字" git config --global user.email "你的郵箱"1.2 查看配置 git config --list二、倉庫管理 2.1 初始化本地倉庫 git init2.2 克隆遠程倉庫 git clone <倉庫…

詳解flink table api基礎(三)

文章目錄1.使用flink的原因&#xff1a;2. Flink支持兩種模式&#xff1a;3. flink table api工作原理&#xff1a;4. Flink table api 使用5. select語句&flink table api&#xff1a;6. 使用flink table api 創建table7. 使用flink table api 寫流式數據輸出到表或sink8.…

Vue2+Vue3前端開發_Day5

參考課程: 【黑馬程序員 Vue2Vue3基礎入門到實戰項目】 [https://www.bilibili.com/video/BV1HV4y1a7n4] ZZHow(ZZHow1024) 自定義指令 基本語法&#xff08;全局 & 局部注冊&#xff09; 介紹&#xff1a;自己定義的指令&#xff0c;可以封裝一些 DOM 操作&#xff0c…

機器學習--決策樹2

目錄 第一代裁判&#xff1a;ID3 與信息增益的 “偏愛” 第二代裁判&#xff1a;C4.5 用 “增益率” 找平衡 第三代裁判&#xff1a;CART 的 “基尼指數” 新思路 遇到連續值&#xff1f;先 “砍幾刀” 再說 給決策樹 “減肥”&#xff1a;剪枝的學問 動手試試&#xff1…

yggjs_react使用教程 v0.1.1

yggjs_react是一個用于快速創建React項目的工具&#xff0c;它集成了Vite、TypeScript、Zustand和React Router等現代前端技術棧&#xff0c;幫助開發者快速搭建高質量的React應用。 快速入門 快速入門部分將指導您如何安裝yggjs_react工具、創建新項目并啟動開發服務器。 安…

vulhub可用的docker源

這一塊不太容易找&#xff0c;我試了好幾個源&#xff0c;下面是20250820測試可用源 編輯方法sudo mkdir -p /etc/docker sudo vim /etc/docker/daemon.json 配置內容 [1] {"registry-mirrors" : ["https://docker.registry.cyou", "https://docker-…

基于YOLOv8-SEAttention與LLMs融合的農作物害蟲智能診斷與防控決策系統

1. 引言 1.1 研究背景與意義 農作物蟲害是制約農業產量與質量的重要因素。據FAO報告&#xff0c;全球每年因病蟲害造成的糧食損失高達 20%–40%。傳統人工巡查與經驗診斷具有時效性差、成本高與專業人才不足等缺陷。近年來&#xff0c;計算機視覺特別是目標檢測技術在農業檢測…

從零開始構建GraphRAG紅樓夢知識圖譜問答項目(三)

文章結尾有CSDN官方提供的學長的聯系方式&#xff01;&#xff01; 歡迎關注B站從零開始構建一個基于GraphRAG的紅樓夢項目 第三集01 搭建后端服務 創建一個python文件server.py 完整源碼放到文章最后了。 1.1 graphrag 相關導入 # GraphRAG 相關導入 from graphrag.query.cont…

S32K328(Arm Cortex-M7)適配CmBacktrace錯誤追蹤

CmBacktrace 相當于重寫了hard_fault函數&#xff0c;在hard_fault函數里面去分析SCB寄存器的信息和堆棧信息&#xff0c;然后把這些信息打印出來(或者寫到flash)&#xff1b;通過使用串口輸出產生hard_fault的堆棧信息&#xff0c;然后利用addr2line工具反推出具體的代碼執行函…