雷鸟:ota文件OTA升级任务接口at验证,ota文件已经可以下载,还差下载进度 2197/target/3 2204/1 2204/head 2205/target/1
authorluozheng <1732036298@qq.com>
Thu, 4 Sep 2025 09:58:23 +0000 (17:58 +0800)
committerluozheng <1732036298@qq.com>
Thu, 4 Sep 2025 09:58:23 +0000 (17:58 +0800)
code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api.h

index 488bc5a..5564caa 100644 (file)
@@ -624,6 +624,18 @@ static uint32_t ln_ota_get_task_at(uint8_t *para, uint32_t paraLen, char *strCom
     return ERRCODE_SUCC;
 }
 
+/* 
+ * 职责单一:仅触发“根据最近一次getTask结果下载并校验MD5”的动作
+ * 不负责:查询任务/升级/重启/重取token
+ */
+static uint32_t ln_ota_download_at(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
+    printf("[LN][OTA_DOWNLOAD]\n");
+    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD, NULL, 0);
+    return ERRCODE_SUCC;
+}
+
 
 static const at_cmd_table_t g_ln_api_cmd[] = {
     /* ----------------------------------- VAD ---------------------------------- */
@@ -647,6 +659,7 @@ static const at_cmd_table_t g_ln_api_cmd[] = {
     {(char *)"AT^LN_DEV_CHARACTER", ln_device_get_character_at, (char *)"获取设备角色信息(JSON,GET): AT^LN_DEV_CHARACTER"},
     {(char *)"AT^LN_DEV_USERID", ln_device_get_userid_at, (char *)"获取设备绑定用户ID(JSON,GET): AT^LN_DEV_USERID"},
     {(char *)"AT^LN_OTA_GET_TASK", ln_ota_get_task_at, (char *)"获取OTA升级任务(JSON,GET): AT^LN_OTA_GET_TASK"},
+    {(char *)"AT^LN_OTA_DOWNLOAD", ln_ota_download_at, (char *)"下载OTA升级包(JSON,GET->下载): AT^LN_OTA_DOWNLOAD"},
 };
 
 at_cmd_table_t *get_leiniao_api_cmd_table(uint32_t *cmd_count)
index eb8055e..e5ad232 100644 (file)
@@ -3,6 +3,8 @@
 #include "netstack/http/include/httpclient.h"
 /* for HMAC-SHA256 */
 #include "mbedtls/md.h"
+/* for MD5 file verify */
+#include "mbedtls/md5.h"
 /* for JSON parsing */
 #include "cJSON.h"
 #include <thread_init.h>
@@ -23,6 +25,13 @@ static char *g_device_iccid = NULL;
 static char *g_device_model = NULL;
 static char *g_device_version = NULL;
 
+/* 记录最近一次 OTA 查询的升级类型与保存路径 */
+static int g_ln_last_update_type = -1;          // -1:未知 0:可选 1:强制
+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
+
 /* 获取设备id: 设备唯一标识 */
 char *uapi_ln_get_device_id(void)
 {
@@ -41,7 +50,7 @@ char *uapi_ln_get_device_model(void)
     g_device_model = "abmybox_1";
     return g_device_model;
 }
-/* 获取设备version */
+/* 获取设备固件版本version */
 char *uapi_ln_get_device_version(void)
 {
     g_device_version = "1.0.0";
@@ -67,6 +76,16 @@ void uapi_ln_clear_device_token(void)
         g_device_token = NULL;
     }
 }
+/* 提供给外部查询最近一次 OTA 查询到的升级类型 */
+int uapi_ln_ota_get_update_type(void)
+{
+    return g_ln_last_update_type;
+}
+/* 提供给外部查询最近一次保存的 OTA 包路径 */
+const char *uapi_ln_ota_get_saved_path(void)
+{
+    return (g_ln_last_saved_path[0] != '\0') ? g_ln_last_saved_path : NULL;
+}
 
 /* ============================== 雷鸟设备Token获取 ============================== */
 
@@ -92,6 +111,9 @@ void uapi_ln_clear_device_token(void)
 #define LN_DEVICE_GET_USER_ID_URL LN_BASE_URL LN_DEVICE_GET_USER_ID_PATH
 #define LN_OTA_GET_TASK_URL LN_BASE_URL LN_OTA_GET_TASK_PATH
 #define LN_SIGN_SECRET "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
+/* OTA 包保存目录(最终文件仍将放在 /user/ 下)*/
+#define LN_OTA_SAVE_DIR "/user"
+
 
 /* 生成[a-zA-Z0-9]随机串 */
 static void ln_gen_nonce(char *out, size_t len)
@@ -1189,6 +1211,10 @@ static uint32_t ln_ota_get_task_http(const char *device_id,
     }
 
     if (clientData.responseBuf && clientData.responseBufLen > 0) {
+        /* 关键打印:返回的响应数据 */
+        printf("[OTA] getTask response: %.*s\n",
+               clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen,
+               clientData.responseBuf);
         int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
         do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_TASK, clientData.responseBuf, (uint32_t)real_len);
     }
@@ -1200,6 +1226,399 @@ end:
 
 
 
+/* ============================ OTA 包下载实现 Start ============================ */
+/* 将响应缓冲写入文件 */
+static int ln_write_buffer_to_file(const char *save_path, const char *buf, int content_len, int buf_len)
+{
+    if (save_path == NULL || buf == NULL)
+    {
+        return -1;
+    }
+    printf("[OTA] write file: path=%s, content_len=%d, buf_len=%d\n", save_path, content_len, buf_len);
+    int actual_size = content_len > 0 ? content_len : buf_len;
+    if (actual_size <= 0)
+    {
+        printf("[OTA] write file failed: invalid size\n");
+        return -1;
+    }
+    FILE *fp = fopen(save_path, "wb");
+    if (fp == NULL)
+    {
+        printf("[OTA] write file failed: open error\n");
+        return -1;
+    }
+    size_t written = fwrite(buf, 1, (size_t)actual_size, fp);
+    fclose(fp);
+    printf("[OTA] write file done: written=%zu\n", written);
+    return (written == (size_t)actual_size) ? 0 : -1;
+}
+
+/* 计算文件MD5(小写hex)*/
+static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33])
+{
+    if (file_path == NULL || out_hex == NULL)
+    {
+        return -1;
+    }
+    unsigned char md5_ctx_buf[16] = {0};
+    mbedtls_md5_context ctx;
+    mbedtls_md5_init(&ctx);
+    if (mbedtls_md5_starts(&ctx) != 0)
+    {
+        mbedtls_md5_free(&ctx);
+        return -1;
+    }
+    FILE *fp = fopen(file_path, "rb");
+    if (!fp)
+    {
+        mbedtls_md5_free(&ctx);
+        return -1;
+    }
+    unsigned char buf[4096];
+    size_t n;
+    while ((n = fread(buf, 1, sizeof(buf), fp)) > 0)
+    {
+        if (mbedtls_md5_update(&ctx, buf, n) != 0)
+        {
+            fclose(fp);
+            mbedtls_md5_free(&ctx);
+            return -1;
+        }
+    }
+    fclose(fp);
+    if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0)
+    {
+        mbedtls_md5_free(&ctx);
+        return -1;
+    }
+    mbedtls_md5_free(&ctx);
+    static const char hex[] = "0123456789abcdef";
+    for (int i = 0; i < 16; i++)
+    {
+        out_hex[i * 2] = hex[(md5_ctx_buf[i] >> 4) & 0x0F];
+        out_hex[i * 2 + 1] = hex[md5_ctx_buf[i] & 0x0F];
+    }
+    out_hex[32] = '\0';
+    return 0;
+}
+
+/* 简单下载器:支持 http/https 全URL,保存到 save_path */
+static int ln_download_file_to_path(const char *full_url, const char *save_path)
+{
+    if (!full_url || !save_path)
+    {
+        return -1;
+    }
+    printf("[OTA] start download: url=%s -> %s\n", full_url, save_path);
+
+    /* 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)
+    {
+        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;
+    }
+
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("[OTA] download HTTP code=%d\n", code);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return -1;
+    }
+
+    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;
+}
+
+/* 仅下载:根据最近一次 getTask 的记录字段执行下载,不再主动请求任务 */
+static uint32_t ln_ota_download_flow_http(const char *device_id,
+                                          const char *device_token,
+                                          const char *source_version)
+{
+    (void)device_id;
+    (void)device_token;
+    (void)source_version;
+    if (g_ln_last_target_version[0] == '\0' || g_ln_last_package_url[0] == '\0')
+    {
+        printf("[OTA] no cached task to download, please GET_TASK first\n");
+        return ERRCODE_FAIL;
+    }
+    /* 保存路径:LN_OTA_SAVE_DIR/upgrade_<ver>.bin */
+    char save_path[256];
+    snprintf(save_path, sizeof(save_path), LN_OTA_SAVE_DIR "/upgrade_%s.bin", g_ln_last_target_version);
+    memset(g_ln_last_saved_path, 0, sizeof(g_ln_last_saved_path));
+    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);
+    if (dres != 0)
+    {
+        char msg[256];
+        int n = snprintf(msg, sizeof(msg), "{\"status\":%d,\"error\":\"download failed\"}", 2);
+        if (n > 0) do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, msg, (uint32_t)n);
+        return ERRCODE_FAIL;
+    }
+
+    /* 发送完成消息(不在此处做MD5,保持AT^LN_OTA_DOWNLOAD单一职责:下载)*/
+    char done[512];
+    int m = snprintf(done, sizeof(done), "{\"status\":%d,\"filePath\":\"%s\",\"md5\":\"%s\"}", 1, save_path, g_ln_last_package_md5);
+    if (m > 0) do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, done, (uint32_t)m);
+    return ERRCODE_SUCC;
+}
+
+
+/* 在接收线程内使用:解析 OTA 任务 JSON,决定是否触发下载 */
+static void ln_handle_ota_task_response(const char *json_buf, int json_len)
+{
+    if (!json_buf || json_len <= 0)
+    {
+        printf("[OTA] handle ota task: invalid json\n");
+        return;
+    }
+    char *text = (char *)malloc((size_t)json_len + 1);
+    if (!text) return;
+    memcpy(text, json_buf, (size_t)json_len);
+    text[json_len] = '\0';
+
+    cJSON *root = cJSON_Parse(text);
+    if (!root)
+    {
+        printf("[OTA] handle ota task: json parse failed\n");
+        free(text);
+        return;
+    }
+
+    cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+    if (errorCode && cJSON_IsString(errorCode))
+    {
+        /* 无升级或服务端错误 */
+        printf("[OTA] task errorCode=%s\n", errorCode->valuestring);
+        cJSON_Delete(root);
+        free(text);
+        return;
+    }
+
+    cJSON *data = cJSON_GetObjectItem(root, "data");
+    if (!data || !cJSON_IsObject(data))
+    {
+        printf("[OTA] task data missing\n");
+        cJSON_Delete(root);
+        free(text);
+        return;
+    }
+
+    cJSON *targetVersion = cJSON_GetObjectItem(data, "targetVersion");
+    cJSON *updateType = cJSON_GetObjectItem(data, "updateType");
+    cJSON *packageUrl  = cJSON_GetObjectItem(data, "packageUrl");
+    cJSON *packageMd5  = cJSON_GetObjectItem(data, "packageMd5");
+    if (!(targetVersion && cJSON_IsString(targetVersion) && updateType && (cJSON_IsNumber(updateType) || cJSON_IsString(updateType))))
+    {
+        printf("[OTA] task fields invalid\n");
+        cJSON_Delete(root);
+        free(text);
+        return;
+    }
+
+    /* 兼容 updateType 为数字或字符串的两种情况 */
+    int ut = -1;
+    if (cJSON_IsNumber(updateType)) ut = (int)updateType->valuedouble;
+    else if (cJSON_IsString(updateType)) ut = atoi(updateType->valuestring);
+    g_ln_last_update_type = ut;
+
+    printf("[OTA] task parsed: targetVersion=%s updateType=%d url=%s md5=%s\n",
+           targetVersion->valuestring, ut,
+           (packageUrl && cJSON_IsString(packageUrl)) ? packageUrl->valuestring : "<null>",
+           (packageMd5 && cJSON_IsString(packageMd5)) ? packageMd5->valuestring : "<null>");
+    /* 记录必要字段供后续 AT^LN_OTA_DOWNLOAD 使用 */
+    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
+    if (targetVersion && cJSON_IsString(targetVersion)) {
+        strncpy(g_ln_last_target_version, targetVersion->valuestring, sizeof(g_ln_last_target_version) - 1);
+    }
+    if (packageUrl && cJSON_IsString(packageUrl)) {
+        strncpy(g_ln_last_package_url, packageUrl->valuestring, sizeof(g_ln_last_package_url) - 1);
+    }
+    if (packageMd5 && cJSON_IsString(packageMd5)) {
+        strncpy(g_ln_last_package_md5, packageMd5->valuestring, sizeof(g_ln_last_package_md5) - 1);
+    }
+
+    if (ut == 1)
+    {
+        /* 强制升级:直接触发下载 */
+        printf("[OTA] force upgrade: trigger download(强制升级:直接触发下载)\n");
+        do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD, NULL, 0);
+    }
+    else if (ut == 0)
+    {
+        /* 可选升级:此处仅提示,实际是否下载由上层UI决定(例如用户确认后再触发)*/
+        printf("[OTA] optional upgrade: waiting for user confirmation(可选升级:等待用户确认)\n");
+        /* 如需自动下载,可取消下面注释 */
+        // do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD, NULL, 0);
+    }
+    else
+    {
+        printf("[OTA] unknown updateType, skip\n");
+    }
+
+    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)
+{
+    if (!json_str || !msg) return -1;
+    cJSON *root = cJSON_Parse(json_str);
+    if (!root) return -1;
+
+    memset(msg, 0, sizeof(*msg));
+
+    cJSON *status = cJSON_GetObjectItem(root, "status");
+    if (status && cJSON_IsNumber(status)) {
+        msg->status = (OtaDownloadStatus)status->valueint;
+    }
+    cJSON *progress = cJSON_GetObjectItem(root, "progress");
+    if (progress && cJSON_IsNumber(progress)) {
+        msg->progress = progress->valueint;
+        if (msg->progress < 0) msg->progress = 0;
+        if (msg->progress > 100) msg->progress = 100;
+    }
+    cJSON *filePath = cJSON_GetObjectItem(root, "filePath");
+    if (filePath && cJSON_IsString(filePath)) {
+        snprintf(msg->file_path, sizeof(msg->file_path), "%s", filePath->valuestring);
+    }
+    cJSON *error = cJSON_GetObjectItem(root, "error");
+    if (error && cJSON_IsString(error)) {
+        snprintf(msg->error_msg, sizeof(msg->error_msg), "%s", error->valuestring);
+    }
+    cJSON *md5 = cJSON_GetObjectItem(root, "md5");
+    if (md5 && cJSON_IsString(md5)) {
+        snprintf(msg->md5, sizeof(msg->md5), "%s", md5->valuestring);
+    }
+
+    cJSON_Delete(root);
+    return 0;
+}
+
+/* 校验文件MD5是否匹配(不区分大小写),返回1=通过,0=不通过,-1=出错 */
+static int ln_ota_is_md5_ok(const char *file_path, const char *expected_md5)
+{
+    if (file_path == NULL || expected_md5 == NULL || expected_md5[0] == '\0')
+    {
+        printf("[OTA] md5 verify: invalid args\n");
+        return -1;
+    }
+    char calc[33] = {0};
+    if (ln_calc_file_md5_lower_hex(file_path, calc) != 0)
+    {
+        printf("[OTA] md5 verify: calc failed for %s\n", file_path);
+        return -1;
+    }
+    /* 规范化期望md5为小写并截断为32字符 */
+    char expect_lower[33] = {0};
+    size_t elen = strlen(expected_md5);
+    size_t copy_len = elen > 32 ? 32 : elen;
+    for (size_t i = 0; i < copy_len; i++)
+    {
+        char ch = expected_md5[i];
+        if (ch >= 'A' && ch <= 'Z') ch = (char)(ch - 'A' + 'a');
+        expect_lower[i] = ch;
+    }
+    expect_lower[32] = '\0';
+
+    int ok = (strcmp(calc, expect_lower) == 0) ? 1 : 0;
+    printf("[OTA] md5 verify result=%s, calc=%s, expect=%s\n", ok ? "OK" : "FAIL", calc, expect_lower);
+    return ok;
+}
+
+/* OTA下载消息处理(对接时序图中的后续流程)*/
+static void handle_ota_download_msg(const OtaDownloadMsg *msg)
+{
+    if (!msg) return;
+    switch (msg->status) {
+        case OTA_DOWNLOAD_STATUS_PROGRESS:
+            printf("[OTA_DOWNLOAD] 下载进度: %d%%\n", msg->progress);
+            break;
+        case OTA_DOWNLOAD_STATUS_COMPLETED:
+            printf("[OTA_DOWNLOAD] 下载完成,文件路径: %s\n", msg->file_path);
+            if (msg->file_path[0] && msg->md5[0]) {
+                printf("[OTA_DOWNLOAD] 开始MD5校验: %s\n", msg->md5);
+                int md5_ok = ln_ota_is_md5_ok(msg->file_path, msg->md5);
+                if (md5_ok == 1) {
+                    printf("[OTA_DOWNLOAD] MD5校验成功,准备重启升级\n");
+                    /* 外部可在此触发升级动作 */
+                } else if (md5_ok == 0) {
+                    printf("[OTA_DOWNLOAD] MD5校验失败,建议重新下载\n");
+                } else {
+                    printf("[OTA_DOWNLOAD] MD5校验出错\n");
+                }
+            }
+            break;
+        case OTA_DOWNLOAD_STATUS_FAILED:
+            printf("[OTA_DOWNLOAD] 下载失败: %s\n", msg->error_msg);
+            break;
+        default:
+            printf("[OTA_DOWNLOAD] 未知状态: %d\n", msg->status);
+            break;
+    }
+}
+
+/* ============================ OTA 包下载实现 End ============================ */
+
 // 发送队列
 static void ln_http_server_send_task(void const *arg)
 {
@@ -1220,7 +1639,7 @@ static void ln_http_server_send_task(void const *arg)
         {
             if (mail.data != NULL && mail.len == sizeof(ln_device_token_req_t))
             {
-                printf("LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN\n");
+                printf("LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN:获取雷鸟设备Token\n");
                 ln_device_token_req_t req;
                 memcpy(&req, mail.data, sizeof(req));
                 ln_get_device_token_http(req.device_id, req.device_iccid, req.device_model, req.device_version);
@@ -1239,7 +1658,7 @@ static void ln_http_server_send_task(void const *arg)
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_CHOOSE)
         {
             if (mail.data && mail.len == sizeof(ln_story_choose_req_t)) {
-                printf("LN_HTTP_SERVICE_CMD_STORY_CHOOSE\n");
+                printf("LN_HTTP_SERVICE_CMD_STORY_CHOOSE:选择故事\n");
                 ln_story_choose_req_t req; memcpy(&req, mail.data, sizeof(req));
                 ln_story_choose_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), req.audio_path);
             }
@@ -1247,36 +1666,41 @@ static void ln_http_server_send_task(void const *arg)
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_RANDOM)
         {
             if (mail.data && mail.len == sizeof(ln_story_random_req_t)) {
-                printf("LN_HTTP_SERVICE_CMD_STORY_RANDOM\n");
+                printf("LN_HTTP_SERVICE_CMD_STORY_RANDOM:随机故事\n");
                 ln_story_random_req_t req; memcpy(&req, mail.data, sizeof(req));
                 ln_story_random_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), req.story_theme[0] ? req.story_theme : NULL);
             }
         }
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_EXIT_INQUIRY)
         {
-            printf("LN_HTTP_SERVICE_CMD_STORY_EXIT_INQUIRY\n");
+            printf("LN_HTTP_SERVICE_CMD_STORY_EXIT_INQUIRY:获取退出哄睡询问\n");
             ln_story_exit_inquiry_http(uapi_ln_get_device_id(), uapi_ln_get_device_token());
         }
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_FINISH)
         {
-            printf("LN_HTTP_SERVICE_CMD_STORY_FINISH\n");
+            printf("LN_HTTP_SERVICE_CMD_STORY_FINISH:确认结束哄睡\n");
             ln_story_finish_http(uapi_ln_get_device_id(), uapi_ln_get_device_token());
         }
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_GET_CHARACTER)
         {
-            printf("LN_HTTP_SERVICE_CMD_GET_CHARACTER\n");
+            printf("LN_HTTP_SERVICE_CMD_GET_CHARACTER:获取设备角色信息\n");
             ln_device_get_character_http(uapi_ln_get_device_id(), uapi_ln_get_device_token());
         }
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_GET_USER_ID)
         {
-            printf("LN_HTTP_SERVICE_CMD_GET_USER_ID\n");
+            printf("LN_HTTP_SERVICE_CMD_GET_USER_ID:获取设备绑定用户ID\n");
             ln_device_get_user_id_http(uapi_ln_get_device_id(), uapi_ln_get_device_token());
         }
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_OTA_GET_TASK)
         {
-            printf("LN_HTTP_SERVICE_CMD_OTA_GET_TASK\n");
+            printf("LN_HTTP_SERVICE_CMD_OTA_GET_TASK:获取OTA升级任务\n");
             ln_ota_get_task_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), uapi_ln_get_device_version());
         }
+        else if (mail.cmd == LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD)
+        {
+            printf("LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD:下载OTA升级包\n");
+            ln_ota_download_flow_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), uapi_ln_get_device_version());
+        }
         if (mail.data != NULL)
         {
             printf("free(mail.data)\n");
@@ -1299,38 +1723,58 @@ static void ln_http_server_recv_task(void const *arg)
                                        OSAL_MSGQ_WAIT_FOREVER);
         if (ret != OSAL_SUCCESS)
         {
+            printf("[RECV_TASK] 消息读取失败: %d\n", ret);
             continue;
         }
-        if (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV
-            || mail.cmd == LN_HTTP_RECV_STORY_CHOOSE
-            || mail.cmd == LN_HTTP_RECV_STORY_RANDOM
-            || mail.cmd == LN_HTTP_RECV_STORY_EXIT
-            || mail.cmd == LN_HTTP_RECV_STORY_FINISH
-            || mail.cmd == LN_HTTP_RECV_DEVICE_CHARACTER
-            || mail.cmd == LN_HTTP_RECV_DEVICE_USER_ID
-            || mail.cmd == LN_HTTP_RECV_OTA_TASK)
-        {
-            if (mail.data && mail.len)
-            {
-                /* 分类打印,便于区分不同来源 */
-                const char *tag = (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV) ? "SPEECH_CHAT" :
-                                  (mail.cmd == LN_HTTP_RECV_STORY_CHOOSE) ? "STORY_CHOOSE" :
-                                  (mail.cmd == LN_HTTP_RECV_STORY_RANDOM) ? "STORY_RANDOM" :
-                                  (mail.cmd == LN_HTTP_RECV_STORY_EXIT)   ? "STORY_EXIT"   :
-                                  (mail.cmd == LN_HTTP_RECV_STORY_FINISH) ? "STORY_FINISH" :
-                                  (mail.cmd == LN_HTTP_RECV_DEVICE_CHARACTER) ? "DEVICE_CHARACTER" :
-                                  (mail.cmd == LN_HTTP_RECV_DEVICE_USER_ID)  ? "DEVICE_USER_ID" :
-                                  (mail.cmd == LN_HTTP_RECV_OTA_TASK) ? "OTA_TASK" : "UNKNOWN";
-                printf("[%s] SSE raw len=%u\n", tag, mail.len);
-                /* 确保可打印:追加\0 */
-                char *buf = (char *)malloc(mail.len + 1);
-                if (buf) {
-                    memcpy(buf, mail.data, mail.len);
-                    buf[mail.len] = '\0';
-                    printf("[%s] SSE raw data=%s\n", tag, buf);
-                    free(buf);
+
+        // 消息有效性检查
+        if (!mail.data || mail.len <= 0) 
+        { 
+            printf("[RECV_TASK] 无效消息: data=%p, len=%u\n", mail.data, mail.len);
+            continue;
+        }
+
+        // 分类处理消息
+        const char *tag = "UNKNOWN";
+        if (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV) {
+            tag = "SPEECH_CHAT";
+        } else if (mail.cmd == LN_HTTP_RECV_STORY_CHOOSE) {
+            tag = "STORY_CHOOSE";
+        } else if (mail.cmd == LN_HTTP_RECV_STORY_RANDOM) {
+            tag = "STORY_RANDOM";
+        } else if (mail.cmd == LN_HTTP_RECV_STORY_EXIT) {
+            tag = "STORY_EXIT";
+        } else if (mail.cmd == LN_HTTP_RECV_STORY_FINISH) {
+            tag = "STORY_FINISH";
+        } else if (mail.cmd == LN_HTTP_RECV_DEVICE_CHARACTER) {
+            tag = "DEVICE_CHARACTER";
+        } else if (mail.cmd == LN_HTTP_RECV_DEVICE_USER_ID) {
+            tag = "DEVICE_USER_ID";
+        } else if (mail.cmd == LN_HTTP_RECV_OTA_TASK) {
+            tag = "OTA_TASK";
+        } else if (mail.cmd == LN_HTTP_RECV_OTA_DOWNLOAD_MSG) {
+            tag = "OTA_DOWNLOAD_MSG";
+        }
+
+        printf("[%s] 数据长度: %u\n", tag, mail.len);
+        /* 确保可打印:追加\0 */
+        char *buf = (char *)malloc(mail.len + 1);
+        if (buf) {
+            memcpy(buf, mail.data, mail.len);
+            buf[mail.len] = '\0';
+            printf("[%s] 原始数据:%s\n", tag, buf);
+            /* 在收到 OTA_TASK 后,根据升级策略决定是否触发下载 */
+            if (mail.cmd == LN_HTTP_RECV_OTA_TASK) {
+                ln_handle_ota_task_response(buf, (int)mail.len);
+            }
+            /* 在收到 OTA_DOWNLOAD_MSG 后,解析状态并回调处理(进度/完成/失败) */
+            if (mail.cmd == LN_HTTP_RECV_OTA_DOWNLOAD_MSG) {
+                OtaDownloadMsg m = {0};
+                if (parse_ota_download_msg(buf, &m) == 0) {
+                    handle_ota_download_msg(&m);
                 }
             }
+            free(buf);
         }
     }
 }
index 6ecfa2a..07f69aa 100644 (file)
@@ -39,6 +39,9 @@ typedef struct
 #define LN_HTTP_SERVICE_CMD_OTA_GET_TASK 0x50 /* 获取OTA升级任务(JSON, GET,带查询参数)*/
 /* OTA 升级相关接收命令 */
 #define LN_HTTP_RECV_OTA_TASK 0x60 /* OTA 升级任务 接收 */
+/* OTA 下载命令与回执 */
+#define LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD 0x51 /* 触发 OTA 包下载(根据 getTask 的返回 packageUrl)*/
+#define LN_HTTP_RECV_OTA_DOWNLOAD_MSG 0x61   /* 可选:OTA 下载控制消息(JSON)接收)*/
 
 typedef struct
 {
@@ -86,6 +89,10 @@ char *uapi_ln_get_device_version(void);
 char *uapi_ln_get_device_token(void);
 /* 清除设备token */
 void uapi_ln_clear_device_token(void);
+/* OTA 查询结果:获取最近一次 getTask 的升级类型(-1:未知 0:可选 1:强制)*/
+int uapi_ln_ota_get_update_type(void);
+/* OTA 下载保存的最后一个文件路径(无则返回NULL)*/
+const char *uapi_ln_ota_get_saved_path(void);
 #ifdef __cplusplus
 }
 #endif