From cc0c663ddb36baa5db1dbe3b13e72687fb6db74c Mon Sep 17 00:00:00 2001 From: luozheng <1732036298@qq.com> Date: Mon, 8 Sep 2025 09:53:01 +0800 Subject: [PATCH] =?utf8?q?ota=E4=B8=8B=E8=BD=BD=E6=B7=BB=E5=8A=A0range?= =?utf8?q?=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- code/sdk/zhihui/leiniao/imsl_ln/ln_api.c | 365 +++++++++++++++-------- code/sdk/zhihui/leiniao/imsl_ln/ln_api.h | 17 ++ 2 files changed, 251 insertions(+), 131 deletions(-) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c index e5ad23206..5f5223b92 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c @@ -31,6 +31,7 @@ static char g_ln_last_saved_path[256] = {0}; // 最近一次保存的 OTA 包 static char g_ln_last_package_url[512] = {0}; // 最近一次查询到的 OTA 包 URL static char g_ln_last_target_version[64] = {0}; // 最近一次查询到的 OTA 包目标版本 static char g_ln_last_package_md5[65] = {0}; // 最近一次查询到的 OTA 包 MD5 +static int g_ln_last_package_size = 0; // 最近一次查询到的 OTA 包大小(字节) /* 获取设备id: 设备唯一标识 */ char *uapi_ln_get_device_id(void) @@ -95,6 +96,8 @@ const char *uapi_ln_ota_get_saved_path(void) #define LN_FULL_URL LN_BASE_URL LN_INTERFACE_PATH #define LN_CHAT_PATH "/v1/speechChat/chatStream" #define LN_CHAT_URL LN_BASE_URL LN_CHAT_PATH +/* 提取基础域名用于 Host 头(SNI/Host 一致性)*/ +#define LN_BASE_HOST "ambybox-api.test.leiniao.com" /* 故事接口路径 */ #define LN_STORY_CHOOSE_PATH "/v1/story/chooseStory" #define LN_STORY_RANDOM_PATH "/v1/story/randomStory" @@ -430,6 +433,7 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id, int audio_len = 0; uint8_t *audio_buf = ln_read_file_all(audio_path, &audio_len); if (!audio_buf) { printf("chatstream: read audio failed\n"); return ERRCODE_FAIL; } + printf("chatstream: audio length = %d\n", audio_len); /* boundary */ char boundary[64]; @@ -437,7 +441,9 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id, /* 组包 */ char *body = NULL; int body_len = 0; char content_type[96]; - if (ln_build_multipart_form(boundary, "audio.mp3", audio_buf, audio_len, + const char *fname = audio_path; const char *slash = strrchr(audio_path, '/'); + if (slash && *(slash + 1)) { fname = slash + 1; } + if (ln_build_multipart_form(boundary, fname && *fname ? fname : "audio.mp3", audio_buf, audio_len, &body, &body_len, content_type, sizeof(content_type)) != 0) { free(audio_buf); return ERRCODE_FAIL; } @@ -455,7 +461,7 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id, client.isHttp = false; /* https */ client.remotePort = 443; - /* 自定义头:deviceId/deviceToken/Accept (Content-Type 由 postContentType 指定,不放在自定义头里) */ + /* 自定义头:仅保留自定义业务头,Content-Type 交由 postContentType 指定 */ char header_buf[512]; int hdr = snprintf(header_buf, sizeof(header_buf), "deviceId: %s\r\n" @@ -465,6 +471,9 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id, device_id, device_token); if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) { free(body); return ERRCODE_FAIL; } HttpClientSetCustomHeader(&client, header_buf); + + /* 关键调试:打印请求头,便于对比Python行为 */ + printf("chatstream header_buf: %s\n", header_buf); /* 准备缓冲区(自适应) */ HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 64 * 1024); @@ -474,8 +483,41 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id, clientData.postBuf = body; clientData.postBufLen = body_len; clientData.postContentType = content_type; - - HTTPC_RESULT res = HttpClientPostRequest(&client, LN_CHAT_URL, &clientData); + /* SSE 场景:响应通常为 Transfer-Encoding: chunked,打开分块标志以避免长度校验问题 */ + clientData.isChunked = true; + + /* 发送请求:对易抖动错误进行有限次重试 */ + HTTPC_RESULT res = HTTP_EUNKOWN; + for (int attempt = 1; attempt <= 3; attempt++) { + res = HttpClientPostRequest(&client, LN_CHAT_URL, &clientData); + printf("chatstream: post attempt %d result %d\n", attempt, res); + if (res == HTTP_SUCCESS) { + break; + } + if (res == HTTP_ECONN || res == HTTP_ECLSD || res == HTTP_ERECV || res == HTTP_ETIMEOUT) { + /* 连接/收发超时类错误,短暂休眠后重建会话重试 */ + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + LOS_Msleep(200); + client = (HttpClient){0}; + clientData = (HttpClientData){0}; + client.isHttp = false; + client.remotePort = 443; + HttpClientSetCustomHeader(&client, header_buf); + HTTPC_RESULT prep2 = ln_http_prepare_auto(&clientData, 2048, 64 * 1024); + if (prep2 != HTTP_SUCCESS) { + printf("chatstream: prepare failed on retry %d, code %d\n", attempt, prep2); + break; + } + clientData.postBuf = body; + clientData.postBufLen = body_len; + clientData.postContentType = content_type; + clientData.isChunked = true; + continue; + } + /* 其他错误不再重试 */ + break; + } if (res != HTTP_SUCCESS) { printf("chatstream: post failed %d\n", res); HttpClientUnprepare(&clientData); @@ -487,6 +529,10 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id, int code = HttpClientGetResponseCode(&client); if (code != 200) { printf("chatstream: HTTP %d\n", code); + if (clientData.responseBuf && clientData.responseBufLen > 0) { + int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; + printf("chatstream: resp body: %.*s\n", real_len, clientData.responseBuf); + } HttpClientUnprepare(&clientData); HttpClientClse(&client); free(body); @@ -611,23 +657,21 @@ static uint32_t ln_story_choose_http(const char *device_id, client.isHttp = false; // 使用 HTTPS client.remotePort = 443; - /* 设置请求头(包含Content-Type,保持与Python一致) */ - { - char header_buf[1024]; - int hdr = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\n" - "deviceToken: %s\r\n" - "Accept: text/event-stream\r\n" - "Cache-Control: no-cache\r\n" - "Content-Type: %s\r\n", - device_id, device_token, content_type); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) { - printf("story choose: build header failed\n"); - free(body); - return ERRCODE_FAIL; - } - HttpClientSetCustomHeader(&client, header_buf); + /* 设置请求头:精简头部,避免重复(Host/Connection 交由库设置) */ + char header_buf[1024]; + int hdr = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\n" + "deviceToken: %s\r\n" + "Accept: text/event-stream\r\n" + "Cache-Control: no-cache\r\n", + device_id, device_token); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) { + printf("story choose: build header failed\n"); + free(body); + return ERRCODE_FAIL; } + HttpClientSetCustomHeader(&client, header_buf); + printf("story choose header_buf: %s\n", header_buf); /* 自适应准备 HTTP 缓冲区 */ HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); @@ -641,33 +685,59 @@ static uint32_t ln_story_choose_http(const char *device_id, clientData.postBuf = body; clientData.postBufLen = body_len; clientData.postContentType = content_type; - - /* 发送 POST 请求 */ - HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_CHOOSE_URL, &clientData); - if (res != HTTP_SUCCESS) { - if (res == HTTP_EAGAIN) { - /* 驱动状态机直到成功或失败 */ - while (1) { - HTTPC_RESULT rrp = HttpClientRecvResponse(&client, &clientData); - if (rrp == HTTP_SUCCESS) { break; } - if (rrp == HTTP_EAGAIN) { continue; } - printf("story choose post failed %d\n", rrp); - free(body); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - } else { - printf("story choose: send POST request failed (code: %d)\n", res); - free(body); + clientData.isChunked = true; + + /* 发送请求:对易抖动错误进行有限次重试 */ + HTTPC_RESULT res = HTTP_EUNKOWN; + for (int attempt = 1; attempt <= 3; attempt++) { + res = HttpClientPostRequest(&client, LN_STORY_CHOOSE_URL, &clientData); + printf("story choose: post attempt %d result %d\n", attempt, res); + if (res == HTTP_SUCCESS) { + break; + } + if (res == HTTP_ECONN || res == HTTP_ECLSD || res == HTTP_ERECV || res == HTTP_ETIMEOUT) { + /* 连接/收发超时类错误,短暂休眠后重建会话重试 */ HttpClientUnprepare(&clientData); HttpClientClse(&client); - return ERRCODE_FAIL; + LOS_Msleep(200); + client = (HttpClient){0}; + clientData = (HttpClientData){0}; + client.isHttp = false; + client.remotePort = 443; + HttpClientSetCustomHeader(&client, header_buf); + HTTPC_RESULT prep2 = ln_http_prepare_auto(&clientData, 2048, 64 * 1024); + if (prep2 != HTTP_SUCCESS) { + printf("story choose: prepare failed on retry %d, code %d\n", attempt, prep2); + break; + } + clientData.postBuf = body; + clientData.postBufLen = body_len; + clientData.postContentType = content_type; + clientData.isChunked = true; + continue; + } + /* 其他错误不再重试 */ + break; + } + printf("story choose: send POST request result: %d\n", res); + if (res != HTTP_SUCCESS) { + printf("story choose: send POST request failed (code: %d)\n", res); + free(body); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + else + { + if (client.socket == -1) + { + printf("[ln_story_choose_http] 错误:POST请求返回成功,但socket无效\n"); } } /* 检查 HTTP 响应码 */ int http_code = HttpClientGetResponseCode(&client); + printf("story choose: send POST request result: %d\n", res); if (http_code != 200) { printf("story choose: invalid HTTP response code (%d)\n", http_code); // 新增:打印响应体内容 @@ -686,6 +756,12 @@ static uint32_t ln_story_choose_http(const char *device_id, return ERRCODE_FAIL; } + printf("story choose: send POST request success\n"); + if (clientData.responseBuf && clientData.responseBufLen > 0) { + int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; + do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_CHOOSE, clientData.responseBuf, real_len); + } + /* 循环接收 SSE 分块数据(带累积处理)*/ int loop_guard = 0; char sse_acc[4096] = {0}; // SSE 数据累积缓冲区 @@ -798,39 +874,21 @@ static uint32_t ln_story_random_http(const char *device_id, clientData.postBufLen = json_len; clientData.postContentType = (char *)"application/json"; - - /* 打印发送给服务端的请求数据(关键调试信息)*/ - printf("story random: request details:\n"); - printf(" URL:%s\n", LN_STORY_RANDOM_URL); // 从Python的RANDOM_STORY_URL提取 - printf(" Headers:\n"); - printf(" deviceId: %s\n", device_id); - printf(" deviceToken: %s\n", device_token); - printf(" Accept: text/event-stream\n"); - printf(" Cache-Control: no-cache\n"); - printf(" Content-Type: application/json\n"); - printf(" Body: %s (length: %d)\n", json_body, json_len); - - /* 发送 POST 请求 */ HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_RANDOM_URL, &clientData); printf("story random: send POST request result: %d\n", res); - if (res != HTTP_SUCCESS) { - if (res == HTTP_EAGAIN) { - /* 驱动状态机直到成功或失败 */ - while (1) { - HTTPC_RESULT rrp = HttpClientRecvResponse(&client, &clientData); - if (rrp == HTTP_SUCCESS) { break; } - if (rrp == HTTP_EAGAIN) { continue; } - printf("story random post failed %d\n", rrp); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - } else { - printf("story random: send POST request failed (code: %d)\n", res); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; + if (res != HTTP_SUCCESS) + { + printf("story random: send POST request failed (code: %d)\n", res); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + else + { + if (client.socket == -1) + { + printf("[ln_speech_chat_stream_http] 错误:POST请求返回成功,但socket无效\n"); } } @@ -856,6 +914,10 @@ static uint32_t ln_story_random_http(const char *device_id, } printf("story random: HTTP response code (%d)\n", http_code); + if (clientData.responseBuf && clientData.responseBufLen > 0) { + int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; + do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_RANDOM, clientData.responseBuf, real_len); + } /* 循环接收 SSE 分块数据(带累积处理)*/ int loop_guard = 0; @@ -1302,69 +1364,117 @@ static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]) return 0; } -/* 简单下载器:支持 http/https 全URL,保存到 save_path */ -static int ln_download_file_to_path(const char *full_url, const char *save_path) +/* 分片下载:使用 Range 头按固定分片大小请求,边下边写并上报进度 */ +static int ln_download_file_to_path_range(const char *full_url, const char *save_path, int total_size) { - if (!full_url || !save_path) + if (!full_url || !save_path || total_size <= 0) { return -1; } - printf("[OTA] start download: url=%s -> %s\n", full_url, save_path); + printf("[OTA] range download: url=%s -> %s, total=%d\n", full_url, save_path, total_size); /* CAT1 网络保活处理 */ cat1_api_add_cat1_vote(); cat1_api_wakeup_cat1(1); cat1_api_keep_wakeup(1); cat1_api_disable_cat1_cmd(0); - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - - /* 判断协议 */ - bool is_https = (strstr(full_url, "https://") == full_url); - client.isHttp = is_https ? false : true; - client.remotePort = is_https ? 443 : 80; - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 256 * 1024); - if (prep != HTTP_SUCCESS) + FILE *fp = fopen(save_path, "wb"); + if (!fp) { + printf("[OTA] open file failed: %s\n", save_path); return -1; } - HTTPC_RESULT res = HttpClientGetRequest(&client, full_url, &clientData); - if (res != HTTP_SUCCESS) - { - printf("[OTA] download request failed: %d\n", res); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return -1; - } + const int chunk_size = 256 * 1024; /* 256KB 分片,兼顾内存与吞吐 */ + int64_t downloaded = 0; - int code = HttpClientGetResponseCode(&client); - if (code != 200) + while (downloaded < total_size) { - printf("[OTA] download HTTP code=%d\n", code); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return -1; - } + int start = (int)downloaded; + int remaining = total_size - start; + int this_chunk = remaining > chunk_size ? chunk_size : remaining; + int end = start + this_chunk - 1; + + HttpClient client = (HttpClient){0}; + HttpClientData clientData = (HttpClientData){0}; + + /* 判断协议 */ + bool is_https = (strstr(full_url, "https://") == full_url); + client.isHttp = is_https ? false : true; + client.remotePort = is_https ? 443 : 80; + + /* 自定义 Range 头 */ + char header_buf[256]; + int hdr = snprintf(header_buf, sizeof(header_buf), "Range: bytes=%d-%d\r\n", start, end); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + { + fclose(fp); + return -1; + } + HttpClientSetCustomHeader(&client, header_buf); + + /* 预留足够的响应缓冲区(分片大小+余量)*/ + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, this_chunk + 16 * 1024); + if (prep != HTTP_SUCCESS) + { + fclose(fp); + return -1; + } + + HTTPC_RESULT res = HttpClientGetRequest(&client, full_url, &clientData); + if (res != HTTP_SUCCESS) + { + printf("[OTA] range request failed: %d\n", res); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + fclose(fp); + return -1; + } + + int code = HttpClientGetResponseCode(&client); + if (!(code == 206 || code == 200)) + { + printf("[OTA] range HTTP code=%d\n", code); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + fclose(fp); + return -1; + } + + if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0) + { + printf("[OTA] range empty body\n"); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + fclose(fp); + return -1; + } + + int buf_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; + size_t written = fwrite(clientData.responseBuf, 1, (size_t)buf_len, fp); + if ((int)written != buf_len) + { + printf("[OTA] range fwrite failed\n"); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + fclose(fp); + return -1; + } + + downloaded += buf_len; + printf("[OTA] range downloaded: %d/%d\n", downloaded, total_size); + + /* 进度上报 */ + int percent = (int)((downloaded * 100) / total_size); + if (percent > 100) percent = 100; + char prog[128]; + int pn = snprintf(prog, sizeof(prog), "{\"status\":%d,\"progress\":%d}", OTA_DOWNLOAD_STATUS_PROGRESS, percent); + if (pn > 0) do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, prog, (uint32_t)pn); - if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0) - { - printf("[OTA] download empty body\n"); HttpClientUnprepare(&clientData); HttpClientClse(&client); - return -1; - } - - int content_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; - int ret = ln_write_buffer_to_file(save_path, clientData.responseBuf, content_len, clientData.responseBufLen); - if (ret == 0) { - printf("[OTA] download saved: %s (%d bytes)\n", save_path, content_len); - } else { - printf("[OTA] download save failed\n"); } - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ret; + fclose(fp); + return 0; } /* 仅下载:根据最近一次 getTask 的记录字段执行下载,不再主动请求任务 */ @@ -1387,7 +1497,11 @@ static uint32_t ln_ota_download_flow_http(const char *device_id, strncpy(g_ln_last_saved_path, save_path, sizeof(g_ln_last_saved_path) - 1); printf("[OTA] download only: version=%s, url=%s -> %s\n", g_ln_last_target_version, g_ln_last_package_url, save_path); - int dres = ln_download_file_to_path(g_ln_last_package_url, save_path); + int dres; + if (g_ln_last_package_size > 0) + { + dres = ln_download_file_to_path_range(g_ln_last_package_url, save_path, g_ln_last_package_size); + } if (dres != 0) { char msg[256]; @@ -1448,6 +1562,7 @@ static void ln_handle_ota_task_response(const char *json_buf, int json_len) cJSON *updateType = cJSON_GetObjectItem(data, "updateType"); cJSON *packageUrl = cJSON_GetObjectItem(data, "packageUrl"); cJSON *packageMd5 = cJSON_GetObjectItem(data, "packageMd5"); + cJSON *packageSize = cJSON_GetObjectItem(data, "packageSize"); if (!(targetVersion && cJSON_IsString(targetVersion) && updateType && (cJSON_IsNumber(updateType) || cJSON_IsString(updateType)))) { printf("[OTA] task fields invalid\n"); @@ -1470,6 +1585,7 @@ static void ln_handle_ota_task_response(const char *json_buf, int json_len) memset(g_ln_last_target_version, 0, sizeof(g_ln_last_target_version)); // 目标版本 memset(g_ln_last_package_url, 0, sizeof(g_ln_last_package_url)); // 升级包URL memset(g_ln_last_package_md5, 0, sizeof(g_ln_last_package_md5)); // 升级包MD5 + g_ln_last_package_size = 0; if (targetVersion && cJSON_IsString(targetVersion)) { strncpy(g_ln_last_target_version, targetVersion->valuestring, sizeof(g_ln_last_target_version) - 1); } @@ -1479,6 +1595,9 @@ static void ln_handle_ota_task_response(const char *json_buf, int json_len) if (packageMd5 && cJSON_IsString(packageMd5)) { strncpy(g_ln_last_package_md5, packageMd5->valuestring, sizeof(g_ln_last_package_md5) - 1); } + if (packageSize && cJSON_IsNumber(packageSize)) { + g_ln_last_package_size = (int)packageSize->valuedouble; + } if (ut == 1) { @@ -1501,22 +1620,6 @@ static void ln_handle_ota_task_response(const char *json_buf, int json_len) cJSON_Delete(root); free(text); } -/* OTA下载状态枚举(对应时序图中的下载阶段)*/ -typedef enum { - OTA_DOWNLOAD_STATUS_DOWNLOADING = 0, /* 下载中 */ - OTA_DOWNLOAD_STATUS_COMPLETED = 1, /* 下载完成 */ - OTA_DOWNLOAD_STATUS_FAILED = 2, /* 下载失败 */ - OTA_DOWNLOAD_STATUS_PROGRESS = 3 /* 进度更新 */ -} OtaDownloadStatus; - -/* OTA下载消息结构体(简化版)*/ -typedef struct { - OtaDownloadStatus status; /* 下载状态 */ - int progress; /* 进度(0-100) */ - char file_path[256]; /* 文件路径 */ - char error_msg[128]; /* 错误信息 */ - char md5[65]; /* MD5(来自任务)*/ -} OtaDownloadMsg; /* 解析OTA下载消息JSON(简化实现)*/ static int parse_ota_download_msg(const char *json_str, OtaDownloadMsg *msg) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h index 07f69aa80..734aea0aa 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h @@ -70,6 +70,23 @@ typedef struct char story_theme[64]; /* 为空则随机 */ } ln_story_random_req_t; +/* OTA下载状态枚举(对应时序图中的下载阶段)*/ +typedef enum { + OTA_DOWNLOAD_STATUS_DOWNLOADING = 0, /* 下载中 */ + OTA_DOWNLOAD_STATUS_COMPLETED = 1, /* 下载完成 */ + OTA_DOWNLOAD_STATUS_FAILED = 2, /* 下载失败 */ + OTA_DOWNLOAD_STATUS_PROGRESS = 3 /* 进度更新 */ +} OtaDownloadStatus; + +/* OTA下载消息结构体(简化版)*/ +typedef struct { + OtaDownloadStatus status; /* 下载状态*/ + int progress; /* 进度(0-100),仅状态为PROGRESS时有效 */ + char file_path[256]; /* 文件路径,仅状态为COMPLETED时有效 */ + char error_msg[128]; /* 错误信息,仅状态为FAILED时有效 */ + char md5[65]; /* MD5(来自任务),仅状态为COMPLETED时有效 */ +} OtaDownloadMsg; + /* 初始化 LN HTTP 服务 */ errcode_t uapi_ln_http_server_init(void); /* 通用发送接口:向 LN HTTP 发送队列投递消息 */ -- 2.17.1