目錄
一、前言
二、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升級。