From: luozheng <1732036298@qq.com> Date: Mon, 8 Sep 2025 05:55:15 +0000 (+0800) Subject: 拆分ln_api.c文件中api函数到多个文件中 X-Git-Tag: leiniao_debug_1.0.0_20250922_020716~92^2~13^2~1 X-Git-Url: http://git.snicker.com.cn/css/static/gitweb.css?a=commitdiff_plain;h=dd9f3d6ca329176c1c605bfebe5dc9495b6909c3;p=mirrors%2FFD230.git 拆分ln_api.c文件中api函数到多个文件中 --- diff --git a/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt b/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt index c165ad435..cb1e08d00 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt +++ b/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt @@ -9,6 +9,14 @@ set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/imsl_system_adapter.c ${CMAKE_CURRENT_SOURCE_DIR}/leiniao_api_at.c ${CMAKE_CURRENT_SOURCE_DIR}/ln_api.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_token.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_speech_chat.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_choose.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_random.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_exit.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_finish.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_device_info.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_ota.c ) # 公共头文件路径(供其他模块引用) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c index 5f5223b92..8f94b4d70 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c @@ -20,18 +20,18 @@ static unsigned long g_ln_send_queue = 0xFFFFFFFF; static unsigned long g_ln_recv_queue = 0xFFFFFFFF; static char *g_device_id = NULL; -static char *g_device_token = NULL;//设备token,服务器获取 +char *g_device_token = NULL;//设备token,服务器获取 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 -static int g_ln_last_package_size = 0; // 最近一次查询到的 OTA 包大小(字节) +int g_ln_last_update_type = -1; // -1:未知 0:可选 1:强制 +char g_ln_last_saved_path[256] = {0}; // 最近一次保存的 OTA 包路径 +char g_ln_last_package_url[512] = {0}; // 最近一次查询到的 OTA 包 URL +char g_ln_last_target_version[64] = {0}; // 最近一次查询到的 OTA 包目标版本 +char g_ln_last_package_md5[65] = {0}; // 最近一次查询到的 OTA 包 MD5 +int g_ln_last_package_size = 0; // 最近一次查询到的 OTA 包大小(字节) /* 获取设备id: 设备唯一标识 */ char *uapi_ln_get_device_id(void) @@ -88,291 +88,8 @@ const char *uapi_ln_ota_get_saved_path(void) return (g_ln_last_saved_path[0] != '\0') ? g_ln_last_saved_path : NULL; } -/* ============================== 雷鸟设备Token获取 ============================== */ - -/* 接口基础配置 - 如需切换环境/密钥,在此调整 */ -#define LN_BASE_URL "https://ambybox-api.test.leiniao.com/ambybox-device-api" -#define LN_INTERFACE_PATH "/v1/auth/getDeviceToken" -#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" -#define LN_STORY_EXIT_INQUIRY_PATH "/v1/story/getExitSleepInquiry" -#define LN_STORY_FINISH_PATH "/v1/story/finishSleepMode" -#define LN_DEVICE_GET_CHARACTER_PATH "/v1/device/getCharacter" -#define LN_DEVICE_GET_USER_ID_PATH "/v1/device/getUserId" -#define LN_OTA_GET_TASK_PATH "/v1/upgrade/getTask" -#define LN_STORY_CHOOSE_URL LN_BASE_URL LN_STORY_CHOOSE_PATH -#define LN_STORY_RANDOM_URL LN_BASE_URL LN_STORY_RANDOM_PATH -#define LN_STORY_EXIT_INQUIRY_URL LN_BASE_URL LN_STORY_EXIT_INQUIRY_PATH -#define LN_STORY_FINISH_URL LN_BASE_URL LN_STORY_FINISH_PATH -#define LN_DEVICE_GET_CHARACTER_URL LN_BASE_URL LN_DEVICE_GET_CHARACTER_PATH -#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) -{ - static const char table[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - if (out == NULL || len == 0) - { - return; - } - /* 使用时间作为随机种子,避免强依赖标准rand种子 */ - uint32_t ts_s = 0; - uapi_get_timestampe_ss(&ts_s); - srand((unsigned int)(ts_s ^ (unsigned int)uapi_tcxo_get_ms())); - for (size_t i = 0; i + 1 < len; i++) - { - out[i] = table[rand() % (sizeof(table) - 1)]; - } - out[len - 1] = '\0'; -} - -static void ln_to_hex_lower(const uint8_t *in, size_t in_len, char *out) -{ - static const char hex[] = "0123456789abcdef"; - for (size_t i = 0; i < in_len; i++) - { - out[i * 2] = hex[(in[i] >> 4) & 0x0F]; - out[i * 2 + 1] = hex[in[i] & 0x0F]; - } - out[in_len * 2] = '\0'; -} - -/* 计算 HMAC-SHA256 并输出小写hex字符串 */ -static int ln_hmac_sha256_hex(const char *key, const char *msg, char *out_hex, size_t out_hex_size) -{ - if (out_hex_size < 65) - { /* 32 bytes -> 64 hex chars + NUL */ - return -1; - } - const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); - if (md_info == NULL) - { - return -1; - } - uint8_t digest[32]; - int ret = mbedtls_md_hmac(md_info, (const unsigned char *)key, strlen(key), - (const unsigned char *)msg, strlen(msg), digest); - if (ret != 0) - { - return -1; - } - ln_to_hex_lower(digest, sizeof(digest), out_hex); - return 0; -} - -/* 发送HTTPS GET请求,携带定制Header,解析返回token */ -static uint32_t ln_get_device_token_http(const char *device_id, - const char *device_iccid, - const char *device_model, - const char *device_version) -{ - if (device_id == NULL || device_iccid == NULL) - { - printf("LN token: invalid parameters\n"); - return ERRCODE_FAIL; - } - - /* 1. 生成时间戳(ms)与nonce */ - uint32_t ts_s = 0; /* epoch seconds */ - uapi_get_timestampe_ss(&ts_s); - char timestamp[32] = {0}; - (void)snprintf(timestamp, sizeof(timestamp), "%u000", ts_s); /* 转毫秒字符串 */ - - char nonce[17] = {0}; /* 12~16位,取固定16以简化 */ - ln_gen_nonce(nonce, sizeof(nonce)); - - /* 2. 计算签名: deviceId:deviceIccid:timestamp:nonce */ - char sign_data[256] = {0}; - (void)snprintf(sign_data, sizeof(sign_data), "%s:%s:%s:%s", device_id, device_iccid, timestamp, nonce); - char signature[65] = {0}; - if (ln_hmac_sha256_hex(LN_SIGN_SECRET, sign_data, signature, sizeof(signature)) != 0) - { - printf("LN token: hmac calc failed\n"); - return ERRCODE_FAIL; - } - - /* 3. 组织自定义请求头 */ - char header_buf[1024] = {0}; - int hdr_len = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\n" - "deviceIccid: %s\r\n" - "timestamp: %s\r\n" - "nonce: %s\r\n" - "signature: %s\r\n" - "deviceModel: %s\r\n" - "deviceVersion: %s\r\n" - "Content-Type: application/json\r\n", - device_id, device_iccid, timestamp, nonce, signature, - (device_model && device_model[0]) ? device_model : "abmybox_1", - (device_version && device_version[0]) ? device_version : "1.0.0"); - if (hdr_len <= 0 || hdr_len >= (int)sizeof(header_buf)) - { - printf("LN token: header build failed\n"); - return ERRCODE_FAIL; - } - - /* 4. 保活CAT1,准备网络 */ - cat1_api_add_cat1_vote(); - cat1_api_wakeup_cat1(1); - cat1_api_keep_wakeup(1); - cat1_api_disable_cat1_cmd(1); - LOS_Msleep(500); - - /* 5. 发起HTTPS GET */ - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; /* https */ - client.remotePort = 443; - HttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = HttpClientPrepare(&clientData, 2048, 4096); - if (prep != HTTP_SUCCESS) - { - printf("LN token: HttpClientPrepare failed %d\n", prep); - return ERRCODE_FAIL; - } - - HTTPC_RESULT res = HttpClientGetRequest(&client, LN_FULL_URL, &clientData); - if (res != HTTP_SUCCESS) - { - printf("LN token: request failed %d\n", res); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - - int code = HttpClientGetResponseCode(&client); - if (code != 200) - { - printf("LN token: HTTP %d\n", code); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - - /* 6. 解析响应JSON */ - if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0) - { - printf("LN token: empty body\n"); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - - int body_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; - if (body_len <= 0) - body_len = (int)strlen(clientData.responseBuf); - - char *json_text = (char *)malloc((size_t)body_len + 1); - if (json_text == NULL) - { - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - memcpy(json_text, clientData.responseBuf, (size_t)body_len); - json_text[body_len] = '\0'; - - cJSON *root = cJSON_Parse(json_text); - if (!root) - { - printf("LN token: JSON parse failed\n"); - free(json_text); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - - cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); - if (errorCode != NULL && !cJSON_IsNull(errorCode)) - { - cJSON *msg = cJSON_GetObjectItem(root, "message"); - printf("LN token: failed, err=%s\n", msg && cJSON_IsString(msg) ? msg->valuestring : "unknown"); - cJSON_Delete(root); - free(json_text); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - - cJSON *data = cJSON_GetObjectItem(root, "data"); - if (data && cJSON_IsObject(data)) - { - cJSON *token = cJSON_GetObjectItem(data, "token"); - cJSON *expire = cJSON_GetObjectItem(data, "expireTime"); - /* --------------------------------- 保存token -------------------------------- */ - if (g_device_token) - { - free(g_device_token); - g_device_token = NULL; - } - if (token && cJSON_IsString(token)) { - size_t tlen = strlen(token->valuestring); - g_device_token = (char *)malloc(tlen + 1); - if (g_device_token) { - memcpy(g_device_token, token->valuestring, tlen + 1); - } - } - if (g_device_token == NULL) { - g_device_token = strdup(""); - } - /* -------------------------------------------------------------------------- */ - // 打印完整的json字符串 - printf("\njson=%s\n",json_text); - if (expire && cJSON_IsNumber(expire)) - { - printf("Token expireTime: %lld\n", (long long)expire->valuedouble); - } - } - else - { - printf("LN token: no data field\n"); - } - - cJSON_Delete(root); - free(json_text); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_SUCC; -} - -/* ============================ 雷鸟设备Token获取 End ============================ */ - -/* ============================ 雷鸟语音对话流 Start ============================ */ - -/* 读取整个文件到内存,返回缓冲区和长度;需调用者 free */ -static uint8_t *ln_read_file_all(const char *path, int *out_len) -{ - if (out_len) *out_len = 0; - if (path == NULL) return NULL; - FILE *fp = fopen(path, "rb"); - if (!fp) return NULL; - fseek(fp, 0, SEEK_END); - long sz = ftell(fp); - fseek(fp, 0, SEEK_SET); - if (sz <= 0 || sz > (10 * 1024 * 1024)) { fclose(fp); return NULL; } - uint8_t *buf = (uint8_t *)malloc((size_t)sz); - if (!buf) { fclose(fp); return NULL; } - size_t n = fread(buf, 1, (size_t)sz, fp); - fclose(fp); - if ((long)n != sz) { free(buf); return NULL; } - if (out_len) *out_len = (int)sz; - return buf; -} - /* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */ -static HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size) +HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size) { if (header_size < 2048) header_size = 2048; if (resp_size < 2048) resp_size = 2048; @@ -389,189 +106,8 @@ static HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_ return ret; } -/* 简单 multipart/form-data 组包(单文件字段 file) */ -static int ln_build_multipart_form(const char *boundary, - const char *filename, - const uint8_t *file_data, - int file_len, - char **out_body, - int *out_len, - char *out_content_type, - int ctype_len) -{ - if (!boundary || !filename || !file_data || file_len <= 0 || !out_body || !out_len) return -1; - char head[512]; - int head_len = snprintf(head, sizeof(head), - "--%s\r\n" - "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n" - "Content-Type: audio/mpeg\r\n\r\n", - boundary, filename); - char tail[128]; - int tail_len = snprintf(tail, sizeof(tail), "\r\n--%s--\r\n", boundary); - int total = head_len + file_len + tail_len; - char *body = (char *)malloc((size_t)total); - if (!body) return -1; - memcpy(body, head, head_len); - memcpy(body + head_len, file_data, (size_t)file_len); - memcpy(body + head_len + file_len, tail, tail_len); - *out_body = body; - *out_len = total; - if (out_content_type && ctype_len > 0) { - snprintf(out_content_type, ctype_len, "multipart/form-data; boundary=%s", boundary); - } - return 0; -} - -/* 发送语音流对话(非分块:一次POST完整multipart;Accept: text/event-stream) */ -static uint32_t ln_speech_chat_stream_http(const char *device_id, - const char *device_token, - const char *audio_path) -{ - if (!device_id || !device_token || !audio_path) return ERRCODE_FAIL; - - /* 读取音频 */ - 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]; - snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", (unsigned)uapi_tcxo_get_ms()); - - /* 组包 */ - char *body = NULL; int body_len = 0; char content_type[96]; - 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; - } - free(audio_buf); - - /* CAT1 保活 */ - cat1_api_add_cat1_vote(); - cat1_api_wakeup_cat1(1); - cat1_api_keep_wakeup(1); - cat1_api_disable_cat1_cmd(0); - LOS_Msleep(300); - - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; /* https */ - client.remotePort = 443; - - /* 自定义头:仅保留自定义业务头,Content-Type 交由 postContentType 指定 */ - char header_buf[512]; - 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)) { 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); - if (prep != HTTP_SUCCESS) { free(body); return ERRCODE_FAIL; } - - /* 设置POST数据与Content-Type(放在Prepare之后,避免被重置) */ - clientData.postBuf = body; - clientData.postBufLen = body_len; - clientData.postContentType = content_type; - /* 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); - HttpClientClse(&client); - free(body); - return ERRCODE_FAIL; - } - - 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); - return ERRCODE_FAIL; - } - - /* 循环接收SSE分片并转投递到接收队列 */ - while (1) { - HTTPC_RESULT rr = HttpClientRecvResponse(&client, &clientData); - if (rr == HTTP_SUCCESS || rr == HTTP_EAGAIN) { - if (clientData.responseBuf && clientData.responseBufLen > 0) { - /* 复制分片数据并投递 */ - do_ln_http_server_recv_data(LN_HTTP_SERVICE_CMD_SSE_RECV, - clientData.responseBuf, - (uint32_t)clientData.responseBufLen); - /* 重置缓冲继续收 */ - HttpClientReset(&clientData); - } else { - LOS_Msleep(20); - } - continue; - } else { - printf("SSE recv end: %d\n", rr); - break; - } - } - - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - free(body); - return ERRCODE_SUCC; -} - -/* ============================ 雷鸟语音对话流 End ============================ */ - -/* ============================ 故事相关接口 Start ============================ */ - // 累积并解析SSE数据(补充实现) -static void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_len, +void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_len, char *acc, int *acc_len, int acc_size) { if (!data || data_len <= 0 || !acc || !acc_len || *acc_len >= acc_size) return; @@ -609,712 +145,6 @@ static void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_l } } -/* 故事-选择故事(SSE,multipart)*/ -static uint32_t ln_story_choose_http(const char *device_id, - const char *device_token, - const char *audio_path) -{ - if (!device_id || !device_token || !audio_path) { - printf("story choose: invalid parameters\n"); - return ERRCODE_FAIL; - } - - /* 读取音频文件 */ - int audio_len = 0; - uint8_t *audio_buf = ln_read_file_all(audio_path, &audio_len); - if (!audio_buf || audio_len <= 0) { - printf("story choose: read audio file failed (path: %s)\n", audio_path); - return ERRCODE_FAIL; - } - - /* 生成 boundary 并构建 multipart 表单数据 */ - char boundary[64] = {0}; - int boundary_len = snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", - (unsigned)uapi_tcxo_get_ms()); - if (boundary_len <= 0 || boundary_len >= (int)sizeof(boundary)) { - printf("story choose: create boundary failed\n"); - free(audio_buf); - return ERRCODE_FAIL; - } - - char *body = NULL; - int body_len = 0; - char content_type[96] = {0}; - if (ln_build_multipart_form(boundary, "story_request.mp3", audio_buf, audio_len, - &body, &body_len, content_type, sizeof(content_type)) != 0) { - printf("story choose: build multipart form failed\n"); - free(audio_buf); - return ERRCODE_FAIL; - } - free(audio_buf); // 音频数据已复制到 body,释放原始缓冲区 - - /* CAT1 网络保活处理 */ - cat1_api_add_cat1_vote();cat1_api_wakeup_cat1(1);cat1_api_keep_wakeup(1);cat1_api_disable_cat1_cmd(0); - LOS_Msleep(300); - - HttpClient client = {0}; - HttpClientData clientData = {0}; - client.isHttp = false; // 使用 HTTPS - client.remotePort = 443; - - /* 设置请求头:精简头部,避免重复(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); - if (prep != HTTP_SUCCESS) { - printf("story choose: prepare HTTP buffer failed (code: %d)\n", prep); - free(body); - return ERRCODE_FAIL; - } - - /* 设置 POST 数据 */ - clientData.postBuf = body; - clientData.postBufLen = body_len; - clientData.postContentType = content_type; - 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); - 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); - // 新增:打印响应体内容 - if (clientData.responseBuf && clientData.responseBufLen > 0) { - char *resp = (char *)malloc(clientData.responseBufLen + 1); - if (resp) { - memcpy(resp, clientData.responseBuf, clientData.responseBufLen); - resp[clientData.responseBufLen] = '\0'; - printf("story choose: server response: %s\n", resp); - free(resp); - } - } - free(body); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - 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 数据累积缓冲区 - int sse_acc_len = 0; - const int MAX_LOOP = 2000; // 约 40 秒超时(20ms * 2000) - while (loop_guard++ < MAX_LOOP) { - HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData); - if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) { - if (clientData.responseBuf && clientData.responseBufLen > 0) { - // 累积并分发 SSE 数据 - ln_sse_accumulate_and_dispatch(LN_HTTP_RECV_STORY_CHOOSE, - clientData.responseBuf, - clientData.responseBufLen, - sse_acc, &sse_acc_len, sizeof(sse_acc)); - HttpClientReset(&clientData); // 重置缓冲区继续接收 - } else { - LOS_Msleep(20); // 无数据时短暂休眠 - } - continue; - } else { - printf("story choose: SSE receive ended (code: %d)\n", recv_res); - break; - } - } - - /* 释放资源 */ - free(body); - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_SUCC; -} - -/* 故事-随机故事(SSE,JSON)*/ -static uint32_t ln_story_random_http(const char *device_id, - const char *device_token, - const char *story_theme /* 可为NULL */) -{ - if (!device_id || !device_token) { - printf("story random: invalid parameters\n"); - return ERRCODE_FAIL; - } - - /* CAT1 网络保活处理 */ - cat1_api_add_cat1_vote();cat1_api_wakeup_cat1(1);cat1_api_keep_wakeup(1);cat1_api_disable_cat1_cmd(0); - LOS_Msleep(200); - - HttpClient client = {0}; - HttpClientData clientData = {0}; - 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: application/json\r\n", - device_id, device_token); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) { - printf("story random: build header failed\n"); - return ERRCODE_FAIL; - } - HttpClientSetCustomHeader(&client, header_buf); - } - - /* 自适应准备 HTTP 缓冲区 */ - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) { - printf("story random: prepare HTTP buffer failed (code: %d)\n", prep); - return ERRCODE_FAIL; - } - - /* 构建 JSON 请求体(增加转义处理,避免格式错误)*/ - char json_body[256] = {0}; - char escaped_theme[256] = {0}; - int json_len = 0; - - // 对故事主题进行JSON转义(处理双引号和反斜杠) - if (story_theme && story_theme[0]) { - int i = 0, j = 0; - while (story_theme[i] && j < sizeof(escaped_theme) - 1) { - if (story_theme[i] == '"') { - escaped_theme[j++] = '\\'; - escaped_theme[j++] = '"'; - } else if (story_theme[i] == '\\') { - escaped_theme[j++] = '\\'; - escaped_theme[j++] = '\\'; - } else { - escaped_theme[j++] = story_theme[i]; - } - i++; - } - escaped_theme[j] = '\0'; - json_len = snprintf(json_body, sizeof(json_body), - "{\"storyTheme\":\"%s\"}", escaped_theme); - } else { - json_len = snprintf(json_body, sizeof(json_body), "{}"); - } - if (json_len <= 0 || json_len >= (int)sizeof(json_body)) { - printf("story random: build JSON body failed\n"); - HttpClientUnprepare(&clientData); - return ERRCODE_FAIL; - } - - /* 设置 POST 数据 */ - clientData.postBuf = json_body; - clientData.postBufLen = json_len; - clientData.postContentType = (char *)"application/json"; - - /* 发送 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) - { - 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"); - } - } - - printf("story random: send POST request success\n"); - - /* 检查 HTTP 响应码 */ - int http_code = HttpClientGetResponseCode(&client); - if (http_code != 200) { - printf("story random: invalid HTTP response code (%d)\n", http_code); - // 新增:打印响应体内容 - if (clientData.responseBuf && clientData.responseBufLen > 0) { - char *resp = (char *)malloc(clientData.responseBufLen + 1); - if (resp) { - memcpy(resp, clientData.responseBuf, clientData.responseBufLen); - resp[clientData.responseBufLen] = '\0'; - printf("story random: server response: %s\n", resp); - free(resp); - } - } - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_FAIL; - } - - 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; - char sse_acc[4096] = {0}; // SSE 数据累积缓冲区 - int sse_acc_len = 0; - const int MAX_LOOP = 2000; // 约 40 秒超时(20ms * 2000) - while (loop_guard++ < MAX_LOOP) - { - HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData); - printf("story random: recv response result: %d\n", recv_res); - if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) - { - if (clientData.responseBuf && clientData.responseBufLen > 0) { - // 累积并分发 SSE 数据 - ln_sse_accumulate_and_dispatch(LN_HTTP_RECV_STORY_RANDOM, - clientData.responseBuf, - clientData.responseBufLen, - sse_acc, &sse_acc_len, sizeof(sse_acc)); - HttpClientReset(&clientData); // 重置缓冲区继续接收 - } else { - LOS_Msleep(20); // 无数据时短暂休眠 - } - continue; - } - else - { - printf("story random: SSE receive ended (code: %d)\n", recv_res); - break; - } - } - - /* 释放资源 */ - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - return ERRCODE_SUCC; -} - -/* 故事-获取退出哄睡询问(JSON)*/ -static uint32_t ln_story_exit_inquiry_http(const char *device_id, - const char *device_token) -{ - if (!device_id || !device_token) return ERRCODE_FAIL; - /* 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}; - client.isHttp = false; client.remotePort = 443; - - /* 普通JSON头 */ - char header_buf[512]; - int hdr = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\ndeviceToken: %s\r\nContent-Type: application/json\r\n", - device_id, device_token); - printf("ln_story_finish_http header_buf: %s\n", header_buf); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL; - HttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) return ERRCODE_FAIL; - - clientData.postBuf = (char *)"{}"; - clientData.postBufLen = 2; - clientData.postContentType = (char *)"application/json"; - - HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_EXIT_INQUIRY_URL, &clientData); - if (res != HTTP_SUCCESS) { printf("story exit inquiry post failed %d\n", res); goto end; } - if (HttpClientGetResponseCode(&client) != 200) { printf("story exit inquiry HTTP %d\n", HttpClientGetResponseCode(&client)); goto end; } - - if (clientData.responseBuf && clientData.responseBufLen > 0) - { - // 解析响应JSON,提取audioUrl等字段 - char *json_text = (char *)malloc(clientData.responseBufLen + 1); - if (json_text) - { - memcpy(json_text, clientData.responseBuf, clientData.responseBufLen); - json_text[clientData.responseBufLen] = '\0'; - cJSON *root = cJSON_Parse(json_text); - if (root) - { - cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); - if (errorCode && !cJSON_IsNull(errorCode)) - { - // 处理认证失败等错误 - printf("[EXIT_INQUIRY] Error: %s\n", cJSON_Print(errorCode)); - } - else - { - cJSON *data = cJSON_GetObjectItem(root, "data"); - if (data && cJSON_IsObject(data)) - { - cJSON *audioUrl = cJSON_GetObjectItem(data, "audioUrl"); - if (audioUrl && cJSON_IsString(audioUrl)) - { - printf("[EXIT_INQUIRY] Play audio: %s\n", audioUrl->valuestring); - printf("[EXIT_INQUIRY] 触发音频播放\n"); - // 触发音频播放(调用播放接口) - // ... - } - } - } - cJSON_Delete(root); - } - free(json_text); - } - // clientData.responseBufLen 表示的是缓冲区的总大小(提前分配的内存大小),而非实际有效的响应数据长度。 - // 实际有效的响应数据长度应使用 clientData.responseContentLen(HTTP 响应体的真实长度)。 - // 当 responseBufLen 大于 responseContentLen 时,responseBuf 中从 responseContentLen 到 responseBufLen 的部分是未使用的内存(可能残留之前的垃圾数据), - // 这些数据被当作有效内容传递并打印,导致乱码。 - int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; - do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_EXIT,clientData.responseBuf,(uint32_t)real_len); - } - -end: - HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC; -} - -/* 故事-确认结束哄睡模式(JSON)*/ -static uint32_t ln_story_finish_http(const char *device_id, - const char *device_token) -{ - if (!device_id || !device_token) return ERRCODE_FAIL; - /* 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}; - client.isHttp = false; client.remotePort = 443; - - char header_buf[512]; int hdr = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\ndeviceToken: %s\r\nContent-Type: application/json\r\n", - device_id, device_token); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL; - HttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) return ERRCODE_FAIL; - - clientData.postBuf = (char *)"{}"; clientData.postBufLen = 2; clientData.postContentType = (char *)"application/json"; - - HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_FINISH_URL, &clientData); - if (res != HTTP_SUCCESS) { printf("story finish post failed %d\n", res); goto end; } - if (HttpClientGetResponseCode(&client) != 200) - { - printf("story finish HTTP %d\n", HttpClientGetResponseCode(&client)); - // 解析非200响应中的错误信息 - if (clientData.responseBuf && clientData.responseBufLen > 0) - { - char *json_text = (char *)malloc(clientData.responseBufLen + 1); - if (json_text) - { - memcpy(json_text, clientData.responseBuf, clientData.responseBufLen); - json_text[clientData.responseBufLen] = '\0'; - cJSON *root = cJSON_Parse(json_text); - if (root) - { - cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); - if (errorCode && cJSON_IsString(errorCode)) - { - printf("[STORY_FINISH] Error: %s\n", errorCode->valuestring); - } - cJSON_Delete(root); - } - free(json_text); - } - } - goto end; - } - - 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_FINISH,clientData.responseBuf,(uint32_t)real_len); - } - -end: - HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC; -} - -/* ============================ 故事相关接口 End ============================ */ - - -/* ============================ 设备信息接口 start ============================ */ -/* 设备-获取角色信息(GET, JSON)*/ -static uint32_t ln_device_get_character_http(const char *device_id, - const char *device_token) -{ - if (!device_id || !device_token) return ERRCODE_FAIL; - - /* 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}; - client.isHttp = false; client.remotePort = 443; - - char header_buf[512]; int hdr = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\n""deviceToken: %s\r\n""Content-Type: application/json\r\n", - device_id, device_token); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL; - HttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) return ERRCODE_FAIL; - - HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_CHARACTER_URL, &clientData); - if (res != HTTP_SUCCESS) { printf("device getCharacter get failed %d\n", res); goto end; } - if (HttpClientGetResponseCode(&client) != 200) - { - printf("device getCharacter HTTP %d\n", HttpClientGetResponseCode(&client)); - // 解析非200响应中的错误信息 - if (clientData.responseBuf && clientData.responseBufLen > 0) - { - char *json_text = (char *)malloc(clientData.responseBufLen + 1); - if (json_text) - { - memcpy(json_text, clientData.responseBuf, clientData.responseBufLen); - json_text[clientData.responseBufLen] = '\0'; - cJSON *root = cJSON_Parse(json_text); - if (root) - { - cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); - if (errorCode && cJSON_IsString(errorCode)) - { - printf("[STORY_FINISH] Error: %s\n", errorCode->valuestring); - } - cJSON_Delete(root); - } - free(json_text); - } - } - goto end; - } - - 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_DEVICE_CHARACTER, clientData.responseBuf, (uint32_t)real_len); - } - -end: - HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC; -} - -/* 设备-获取绑定用户ID(GET, JSON)*/ -static uint32_t ln_device_get_user_id_http(const char *device_id, - const char *device_token) -{ - if (!device_id || !device_token) return ERRCODE_FAIL; - - /* 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}; - client.isHttp = false; client.remotePort = 443; - - char header_buf[512]; int hdr = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\n""deviceToken: %s\r\n""Content-Type: application/json\r\n", - device_id, device_token); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL; - HttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) return ERRCODE_FAIL; - - HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_USER_ID_URL, &clientData); - if (res != HTTP_SUCCESS) { printf("device getUserId get failed %d\n", res); goto end; } - if (HttpClientGetResponseCode(&client) != 200) { - printf("device getUserId HTTP %d\n", HttpClientGetResponseCode(&client)); - // 解析非200响应中的错误信息 - if (clientData.responseBuf && clientData.responseBufLen > 0) - { - char *json_text = (char *)malloc(clientData.responseBufLen + 1); - if (json_text) - { - memcpy(json_text, clientData.responseBuf, clientData.responseBufLen); - json_text[clientData.responseBufLen] = '\0'; - cJSON *root = cJSON_Parse(json_text); - if (root) - { - cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); - if (errorCode && cJSON_IsString(errorCode)) - { - printf("[STORY_FINISH] Error: %s\n", errorCode->valuestring); - } - cJSON_Delete(root); - } - free(json_text); - } - } - goto end; - } - - 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_DEVICE_USER_ID, clientData.responseBuf, (uint32_t)real_len); - } - -end: - HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC; -} -/* ============================ 设备信息接口 End ============================ */ - - - -/* ============================ OTA 升级任务接口 Start ============================ */ -/* 设备-获取OTA升级任务(GET, JSON,带查询参数 deviceId, sourceVersion)*/ -static uint32_t ln_ota_get_task_http(const char *device_id, - const char *device_token, - const char *source_version) -{ - if (!device_id || !device_token || !source_version) return ERRCODE_FAIL; - - /* 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}; - client.isHttp = false; client.remotePort = 443; - - /* 头:与 device info 一致,额外带 model/version 可辅助服务端日志定位 */ - char header_buf[512]; - int hdr = snprintf(header_buf, sizeof(header_buf), - "deviceId: %s\r\n" - "deviceToken: %s\r\n" - "deviceModel: %s\r\n" - "deviceVersion: %s\r\n" - "Content-Type: application/json\r\n", - device_id, device_token, - (uapi_ln_get_device_model() ? uapi_ln_get_device_model() : "abmybox_1"), - (uapi_ln_get_device_version() ? uapi_ln_get_device_version() : "1.0.0")); - if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL; - HttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) return ERRCODE_FAIL; - - /* 拼 GET URL 参数 */ - char url_with_params[512]; - int ulen = snprintf(url_with_params, sizeof(url_with_params), - "%s?deviceId=%s&sourceVersion=%s", - LN_OTA_GET_TASK_URL, device_id, source_version); - if (ulen <= 0 || ulen >= (int)sizeof(url_with_params)) { HttpClientUnprepare(&clientData); return ERRCODE_FAIL; } - - printf("[OTA] GET %s\n", url_with_params); - - HTTPC_RESULT res = HttpClientGetRequest(&client, url_with_params, &clientData); - if (res != HTTP_SUCCESS) { printf("[OTA] getTask failed %d\n", res); goto end; } - - int code = HttpClientGetResponseCode(&client); - printf("[OTA] HTTP %d\n", code); - if (code != 200) { - 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_OTA_TASK, clientData.responseBuf, (uint32_t)real_len); - } - goto end; - } - - 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); - } - -end: - HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC; -} -/* ============================ OTA 升级任务接口 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]) { @@ -1364,160 +194,6 @@ static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]) return 0; } -/* 分片下载:使用 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 || total_size <= 0) - { - return -1; - } - 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); - - FILE *fp = fopen(save_path, "wb"); - if (!fp) - { - printf("[OTA] open file failed: %s\n", save_path); - return -1; - } - - const int chunk_size = 256 * 1024; /* 256KB 分片,兼顾内存与吞吐 */ - int64_t downloaded = 0; - - while (downloaded < total_size) - { - 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); - - HttpClientUnprepare(&clientData); - HttpClientClse(&client); - } - - fclose(fp); - return 0; -} - -/* 仅下载:根据最近一次 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_.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; - 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]; - 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) { diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h index 734aea0aa..4394554e6 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h @@ -8,6 +8,8 @@ extern "C" #include "stdint.h" #include "errcode.h" +/* HTTP types for helper function prototypes */ +#include "netstack/http/include/httpclient.h" typedef struct { @@ -16,6 +18,32 @@ typedef struct uint32_t len; } ln_http_data_mail_t; +/* ============================== 公共宏与URL路径 ============================== */ +/* 接口基础配置 - 如需切换环境/密钥,在此调整 */ +#define LN_BASE_URL "https://ambybox-api.test.leiniao.com/ambybox-device-api" +#define LN_INTERFACE_PATH "/v1/auth/getDeviceToken" +#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 +/* 故事接口路径 */ +#define LN_STORY_CHOOSE_PATH "/v1/story/chooseStory" +#define LN_STORY_RANDOM_PATH "/v1/story/randomStory" +#define LN_STORY_EXIT_INQUIRY_PATH "/v1/story/getExitSleepInquiry" +#define LN_STORY_FINISH_PATH "/v1/story/finishSleepMode" +#define LN_DEVICE_GET_CHARACTER_PATH "/v1/device/getCharacter" +#define LN_DEVICE_GET_USER_ID_PATH "/v1/device/getUserId" +#define LN_OTA_GET_TASK_PATH "/v1/upgrade/getTask" +#define LN_STORY_CHOOSE_URL LN_BASE_URL LN_STORY_CHOOSE_PATH +#define LN_STORY_RANDOM_URL LN_BASE_URL LN_STORY_RANDOM_PATH +#define LN_STORY_EXIT_INQUIRY_URL LN_BASE_URL LN_STORY_EXIT_INQUIRY_PATH +#define LN_STORY_FINISH_URL LN_BASE_URL LN_STORY_FINISH_PATH +#define LN_DEVICE_GET_CHARACTER_URL LN_BASE_URL LN_DEVICE_GET_CHARACTER_PATH +#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" + #define LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN 0x1 /* 获取设备 token */ #define LN_HTTP_SERVICE_CMD_SPEECH_CHAT_STREAM 0x2 /* 语音对话流 */ #define LN_HTTP_SERVICE_CMD_SSE_RECV 0x3 /* 语音对话流分片接收 */ @@ -110,6 +138,57 @@ void uapi_ln_clear_device_token(void); int uapi_ln_ota_get_update_type(void); /* OTA 下载保存的最后一个文件路径(无则返回NULL)*/ const char *uapi_ln_ota_get_saved_path(void); + +/* ============================== 共享全局状态(跨文件) ============================== */ +extern char *g_device_token; +extern int g_ln_last_update_type; +extern char g_ln_last_saved_path[256]; +extern char g_ln_last_package_url[512]; +extern char g_ln_last_target_version[64]; +extern char g_ln_last_package_md5[65]; +extern int g_ln_last_package_size; + +/* ============================== 公共工具函数(供拆分后的模块复用) ============================== */ +HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size); +void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_len, + char *acc, int *acc_len, int acc_size); +/* ============================== 各接口函数声明 ============================== */ +uint32_t ln_get_device_token_http(const char *device_id, + const char *device_iccid, + const char *device_model, + const char *device_version); + +uint32_t ln_speech_chat_stream_http(const char *device_id, + const char *device_token, + const char *audio_path); + +uint32_t ln_story_choose_http(const char *device_id, + const char *device_token, + const char *audio_path); + +uint32_t ln_story_random_http(const char *device_id, + const char *device_token, + const char *story_theme); + +uint32_t ln_story_exit_inquiry_http(const char *device_id, + const char *device_token); + +uint32_t ln_story_finish_http(const char *device_id, + const char *device_token); + +uint32_t ln_device_get_character_http(const char *device_id, + const char *device_token); + +uint32_t ln_device_get_user_id_http(const char *device_id, + const char *device_token); + +uint32_t ln_ota_get_task_http(const char *device_id, + const char *device_token, + const char *source_version); + +uint32_t ln_ota_download_flow_http(const char *device_id, + const char *device_token, + const char *source_version); #ifdef __cplusplus } #endif diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c new file mode 100644 index 000000000..e89a96957 --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c @@ -0,0 +1,136 @@ +#include "ln_api.h" +#include + +// 获取设备角色信息 +uint32_t ln_device_get_character_http(const char *device_id, + const char *device_token) +{ + if (!device_id || !device_token) + return ERRCODE_FAIL; + + /* 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}; + client.isHttp = false; + client.remotePort = 443; + + char header_buf[512]; + int hdr = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\n" + "deviceToken: %s\r\n" + "Content-Type: application/json\r\n", + device_id, device_token); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + return ERRCODE_FAIL; + HttpClientSetCustomHeader(&client, header_buf); + + printf("device getCharacter header %s\n", header_buf); + + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); + if (prep != HTTP_SUCCESS) + { + printf("device getCharacter prepare failed %d\n", prep); + return ERRCODE_FAIL; + } + + HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_CHARACTER_URL, &clientData); + if (res != HTTP_SUCCESS) + { + printf("device getCharacter get failed %d\n", res); + goto end; + } + int code = HttpClientGetResponseCode(&client); + if (code != 200) + { + printf("device getCharacter: HTTP %d\n", 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_DEVICE_CHARACTER, clientData.responseBuf, (uint32_t)real_len); + } + goto end; + } + + 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_DEVICE_CHARACTER, clientData.responseBuf, (uint32_t)real_len); + } + +end: + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} + +// 获取设备绑定用户ID +uint32_t ln_device_get_user_id_http(const char *device_id, + const char *device_token) +{ + if (!device_id || !device_token) + return ERRCODE_FAIL; + + /* 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}; + client.isHttp = false; + client.remotePort = 443; + + char header_buf[512]; + int hdr = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\n" + "deviceToken: %s\r\n" + "Content-Type: application/json\r\n", + device_id, device_token); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + return ERRCODE_FAIL; + HttpClientSetCustomHeader(&client, header_buf); + + printf("device getUserId header %s\n", header_buf); + + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); + if (prep != HTTP_SUCCESS) + { + printf("device getUserId prepare failed %d\n", prep); + return ERRCODE_FAIL; + } + + HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_USER_ID_URL, &clientData); + if (res != HTTP_SUCCESS) + { + printf("device getUserId get failed %d\n", res); + goto end; + } + int code = HttpClientGetResponseCode(&client); + if (code != 200) + { + printf("device getUserId: HTTP %d\n", 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_DEVICE_USER_ID, clientData.responseBuf, (uint32_t)real_len); + } + goto end; + } + + 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_DEVICE_USER_ID, clientData.responseBuf, (uint32_t)real_len); + } + +end: + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c new file mode 100644 index 000000000..9dec066ff --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c @@ -0,0 +1,246 @@ +#include "ln_api.h" +#include + +uint32_t ln_ota_get_task_http(const char *device_id, + const char *device_token, + const char *source_version) +{ + if (!device_id || !device_token || !source_version) + return ERRCODE_FAIL; + + /* 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}; + client.isHttp = false; + client.remotePort = 443; + + /* 头:与 device info 一致,额外带 model/version 可辅助服务端日志定位 */ + char header_buf[512]; + int hdr = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\n" + "deviceToken: %s\r\n" + "deviceModel: %s\r\n" + "deviceVersion: %s\r\n" + "Content-Type: application/json\r\n", + device_id, device_token, + (uapi_ln_get_device_model() ? uapi_ln_get_device_model() : "abmybox_1"), + (uapi_ln_get_device_version() ? uapi_ln_get_device_version() : "1.0.0")); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + return ERRCODE_FAIL; + HttpClientSetCustomHeader(&client, header_buf); + + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); + if (prep != HTTP_SUCCESS) + { + printf("[OTA] getTask prepare failed %d\n", prep); + return ERRCODE_FAIL; + } + + /* 拼 GET URL 参数 */ + char url_with_params[512] = {0}; + int ulen = snprintf(url_with_params, sizeof(url_with_params), + "%s?deviceId=%s&sourceVersion=%s", + LN_OTA_GET_TASK_URL, device_id, source_version); + if (ulen <= 0 || ulen >= (int)sizeof(url_with_params)) + { + HttpClientUnprepare(&clientData); + return ERRCODE_FAIL; + } + + printf("[OTA] GET %s\n", url_with_params); + + HTTPC_RESULT res = HttpClientGetRequest(&client, url_with_params, &clientData); + if (res != HTTP_SUCCESS) + { + printf("[OTA] getTask failed %d\n", res); + goto end; + } + + int code = HttpClientGetResponseCode(&client); + if (code != 200) + { + printf("[OTA] getTask failed %d\n", 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_OTA_TASK, clientData.responseBuf, (uint32_t)real_len); + } + goto end; + } + + 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_OTA_TASK, clientData.responseBuf, (uint32_t)real_len); + } + +end: + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} + +/* 分片下载:使用 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 || total_size <= 0) + { + printf("[OTA] range download: url=%s -> %s, total=%d\n", full_url, save_path, total_size); + return -1; + } + 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); + + FILE *fp = fopen(save_path, "wb"); + if (!fp) + { + printf("[OTA] open file failed: %s\n", save_path); + return -1; + } + + const int chunk_size = 256 * 1024; /* 256KB 分片,兼顾内存与吞吐 */ + int64_t downloaded = 0; + + while (downloaded < total_size) + { + 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)) + { + printf("[OTA] range header failed: %d\n", hdr); + 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) + { + printf("[OTA] range prepare failed: %d\n", prep); + 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); + + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + } + + fclose(fp); + return 0; +} + +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; + } + 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; + 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]; + 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; + } + + 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; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c new file mode 100644 index 000000000..e9c798348 --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c @@ -0,0 +1,246 @@ +#include "ln_api.h" +#include + +/* 读取整个文件到内存,返回缓冲区和长度;需调用者 free */ +static uint8_t *ln_read_file_all_speech_chat(const char *path, int *out_len) +{ + if (out_len) + *out_len = 0; + if (path == NULL) + return NULL; + FILE *fp = fopen(path, "rb"); + if (!fp) + return NULL; + fseek(fp, 0, SEEK_END); + long sz = ftell(fp); + fseek(fp, 0, SEEK_SET); + if (sz <= 0 || sz > (10 * 1024 * 1024)) + { + fclose(fp); + return NULL; + } + uint8_t *buf = (uint8_t *)malloc((size_t)sz); + if (!buf) + { + fclose(fp); + return NULL; + } + size_t n = fread(buf, 1, (size_t)sz, fp); + fclose(fp); + if ((long)n != sz) + { + free(buf); + return NULL; + } + if (out_len) + *out_len = (int)sz; + return buf; +} + +/* 简单 multipart/form-data 组包(单文件字段 file) */ +static int ln_build_multipart_form_speech_chat(const char *boundary, + const char *filename, + const uint8_t *file_data, + int file_len, + char **out_body, + int *out_len, + char *out_content_type, + int ctype_len) +{ + if (!boundary || !filename || !file_data || file_len <= 0 || !out_body || !out_len) + return -1; + char head[512]; + int head_len = snprintf(head, sizeof(head), + "--%s\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n" + "Content-Type: audio/mpeg\r\n\r\n", + boundary, filename); + char tail[128]; + int tail_len = snprintf(tail, sizeof(tail), "\r\n--%s--\r\n", boundary); + int total = head_len + file_len + tail_len; + char *body = (char *)malloc((size_t)total); + if (!body) + return -1; + memcpy(body, head, head_len); + memcpy(body + head_len, file_data, (size_t)file_len); + memcpy(body + head_len + file_len, tail, tail_len); + *out_body = body; + *out_len = total; + if (out_content_type && ctype_len > 0) + { + snprintf(out_content_type, ctype_len, "multipart/form-data; boundary=%s", boundary); + } + return 0; +} + +uint32_t ln_speech_chat_stream_http(const char *device_id, + const char *device_token, + const char *audio_path) +{ + if (!device_id || !device_token || !audio_path) + return ERRCODE_FAIL; + + /* 读取音频 */ + int audio_len = 0; + uint8_t *audio_buf = ln_read_file_all_speech_chat(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]; + snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", (unsigned)uapi_tcxo_get_ms()); + + /* 组包 */ + char *body = NULL; + int body_len = 0; + char content_type[96]; + const char *fname = audio_path; + const char *slash = strrchr(audio_path, '/'); + if (slash && *(slash + 1)) + { + fname = slash + 1; + } + if (ln_build_multipart_form_speech_chat(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; + } + free(audio_buf); + + /* CAT1 保活 */ + cat1_api_add_cat1_vote(); + cat1_api_wakeup_cat1(1); + cat1_api_keep_wakeup(1); + cat1_api_disable_cat1_cmd(0); + LOS_Msleep(300); + + HttpClient client = (HttpClient){0}; + HttpClientData clientData = (HttpClientData){0}; + client.isHttp = false; /* https */ + client.remotePort = 443; + + /* 自定义头:仅保留自定义业务头,Content-Type 交由 postContentType 指定 */ + char header_buf[512]; + 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)) + { + free(body); + return ERRCODE_FAIL; + } + HttpClientSetCustomHeader(&client, header_buf); + printf("chatstream header_buf: %s\n", header_buf); + + /* 准备缓冲区(自适应) */ + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 64 * 1024); + if (prep != HTTP_SUCCESS) + { + free(body); + return ERRCODE_FAIL; + } + + /* 设置POST数据与Content-Type(放在Prepare之后,避免被重置) */ + clientData.postBuf = body; + clientData.postBufLen = body_len; + clientData.postContentType = content_type; + /* 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); + HttpClientClse(&client); + free(body); + return ERRCODE_FAIL; + } + + 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; + do_ln_http_server_recv_data(LN_HTTP_SERVICE_CMD_SSE_RECV, clientData.responseBuf, (uint32_t)real_len); + } + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + free(body); + return ERRCODE_FAIL; + } + + /* 循环接收SSE分片并转投递到接收队列 */ + while (1) + { + HTTPC_RESULT rr = HttpClientRecvResponse(&client, &clientData); + if (rr == HTTP_SUCCESS || rr == HTTP_EAGAIN) + { + if (clientData.responseBuf && clientData.responseBufLen > 0) + { + do_ln_http_server_recv_data(LN_HTTP_SERVICE_CMD_SSE_RECV, clientData.responseBuf, (uint32_t)clientData.responseBufLen); + HttpClientReset(&clientData); + } + else + { + LOS_Msleep(20); + } + continue; + } + else + { + printf("SSE recv end: %d\n", rr); + break; + } + } + + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + free(body); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c new file mode 100644 index 000000000..8dbcdb31f --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c @@ -0,0 +1,265 @@ +#include "ln_api.h" +#include + +/* 读取整个文件到内存,返回缓冲区和长度;需调用者 free */ +static uint8_t *ln_read_file_all_story(const char *path, int *out_len) +{ + if (out_len) + *out_len = 0; + if (path == NULL) + return NULL; + FILE *fp = fopen(path, "rb"); + if (!fp) + return NULL; + fseek(fp, 0, SEEK_END); + long sz = ftell(fp); + fseek(fp, 0, SEEK_SET); + if (sz <= 0 || sz > (10 * 1024 * 1024)) + { + fclose(fp); + return NULL; + } + uint8_t *buf = (uint8_t *)malloc((size_t)sz); + if (!buf) + { + fclose(fp); + return NULL; + } + size_t n = fread(buf, 1, (size_t)sz, fp); + fclose(fp); + if ((long)n != sz) + { + free(buf); + return NULL; + } + if (out_len) + *out_len = (int)sz; + return buf; +} + +/* 简单 multipart/form-data 组包(单文件字段 file) */ +static int ln_build_multipart_form_story(const char *boundary, + const char *filename, + const uint8_t *file_data, + int file_len, + char **out_body, + int *out_len, + char *out_content_type, + int ctype_len) +{ + if (!boundary || !filename || !file_data || file_len <= 0 || !out_body || !out_len) + return -1; + char head[512]; + int head_len = snprintf(head, sizeof(head), + "--%s\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n" + "Content-Type: audio/mpeg\r\n\r\n", + boundary, filename); + char tail[128]; + int tail_len = snprintf(tail, sizeof(tail), "\r\n--%s--\r\n", boundary); + int total = head_len + file_len + tail_len; + char *body = (char *)malloc((size_t)total); + if (!body) + return -1; + memcpy(body, head, head_len); + memcpy(body + head_len, file_data, (size_t)file_len); + memcpy(body + head_len + file_len, tail, tail_len); + *out_body = body; + *out_len = total; + if (out_content_type && ctype_len > 0) + { + snprintf(out_content_type, ctype_len, "multipart/form-data; boundary=%s", boundary); + } + return 0; +} + +uint32_t ln_story_choose_http(const char *device_id, + const char *device_token, + const char *audio_path) +{ + if (!device_id || !device_token || !audio_path) + { + printf("story choose: invalid parameters\n"); + return ERRCODE_FAIL; + } + + /* 读取音频文件 */ + int audio_len = 0; + uint8_t *audio_buf = ln_read_file_all_story(audio_path, &audio_len); + if (!audio_buf || audio_len <= 0) + { + printf("story choose: read audio file failed (path: %s)\n", audio_path); + return ERRCODE_FAIL; + } + + /* 生成 boundary 并构建 multipart 表单数据 */ + char boundary[64] = {0}; + int boundary_len = snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", + (unsigned)uapi_tcxo_get_ms()); + if (boundary_len <= 0 || boundary_len >= (int)sizeof(boundary)) + { + printf("story choose: create boundary failed\n"); + free(audio_buf); + return ERRCODE_FAIL; + } + + char *body = NULL; + int body_len = 0; + char content_type[96] = {0}; + if (ln_build_multipart_form_story(boundary, "story_request.mp3", audio_buf, audio_len, + &body, &body_len, content_type, sizeof(content_type)) != 0) + { + printf("story choose: build multipart form failed\n"); + free(audio_buf); + return ERRCODE_FAIL; + } + free(audio_buf); + + /* CAT1 网络保活处理 */ + cat1_api_add_cat1_vote(); + cat1_api_wakeup_cat1(1); + cat1_api_keep_wakeup(1); + cat1_api_disable_cat1_cmd(0); + LOS_Msleep(300); + + HttpClient client = {0}; + HttpClientData clientData = {0}; + client.isHttp = false; // 使用 HTTPS + client.remotePort = 443; + + /* 设置请求头:精简头部,避免重复(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); + if (prep != HTTP_SUCCESS) + { + printf("story choose: prepare HTTP buffer failed (code: %d)\n", prep); + free(body); + return ERRCODE_FAIL; + } + + /* 设置 POST 数据 */ + clientData.postBuf = body; + clientData.postBufLen = body_len; + clientData.postContentType = content_type; + 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); + 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; + } + + /* 检查 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); + 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); + } + free(body); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + 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}; + int sse_acc_len = 0; + const int MAX_LOOP = 2000; + while (loop_guard++ < MAX_LOOP) + { + HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData); + if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) + { + if (clientData.responseBuf && clientData.responseBufLen > 0) + { + ln_sse_accumulate_and_dispatch(LN_HTTP_RECV_STORY_CHOOSE, + clientData.responseBuf, + clientData.responseBufLen, + sse_acc, &sse_acc_len, sizeof(sse_acc)); + HttpClientReset(&clientData); + } + else + { + LOS_Msleep(20); + } + continue; + } + else + { + printf("story choose: SSE receive ended (code: %d)\n", recv_res); + break; + } + } + + free(body); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_exit.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_exit.c new file mode 100644 index 000000000..9e3193fc4 --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_exit.c @@ -0,0 +1,65 @@ +#include "ln_api.h" +#include + +uint32_t ln_story_exit_inquiry_http(const char *device_id, + const char *device_token) +{ + if (!device_id || !device_token) + return ERRCODE_FAIL; + /* 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}; + client.isHttp = false; + client.remotePort = 443; + + /* 普通JSON头 */ + char header_buf[512]; + int hdr = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\ndeviceToken: %s\r\nContent-Type: application/json\r\n", + device_id, device_token); + printf("ln_story_finish_http header_buf: %s\n", header_buf); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + return ERRCODE_FAIL; + HttpClientSetCustomHeader(&client, header_buf); + + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); + if (prep != HTTP_SUCCESS) + return ERRCODE_FAIL; + + clientData.postBuf = (char *)"{}"; + clientData.postBufLen = 2; + clientData.postContentType = (char *)"application/json"; + + HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_EXIT_INQUIRY_URL, &clientData); + if (res != HTTP_SUCCESS) + { + printf("story exit inquiry post failed %d\n", res); + goto end; + } + if (HttpClientGetResponseCode(&client) != 200) + { + printf("story exit inquiry HTTP %d\n", HttpClientGetResponseCode(&client)); + 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_EXIT, clientData.responseBuf, real_len); + } + goto end; + } + + 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_EXIT, clientData.responseBuf, (uint32_t)real_len); + } + +end: + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_finish.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_finish.c new file mode 100644 index 000000000..f0006fd34 --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_finish.c @@ -0,0 +1,64 @@ +#include "ln_api.h" +#include + +uint32_t ln_story_finish_http(const char *device_id, + const char *device_token) +{ + if (!device_id || !device_token) + return ERRCODE_FAIL; + /* 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}; + client.isHttp = false; + client.remotePort = 443; + + char header_buf[512]; + int hdr = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\ndeviceToken: %s\r\nContent-Type: application/json\r\n", + device_id, device_token); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + return ERRCODE_FAIL; + HttpClientSetCustomHeader(&client, header_buf); + + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); + if (prep != HTTP_SUCCESS) + return ERRCODE_FAIL; + + clientData.postBuf = (char *)"{}"; + clientData.postBufLen = 2; + clientData.postContentType = (char *)"application/json"; + + HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_FINISH_URL, &clientData); + if (res != HTTP_SUCCESS) + { + printf("story finish post failed %d\n", res); + goto end; + } + int code = HttpClientGetResponseCode(&client); + if (code != 200) + { + printf("device getUserId: HTTP %d\n", 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_FINISH, clientData.responseBuf, (uint32_t)real_len); + } + goto end; + } + + 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_FINISH, clientData.responseBuf, (uint32_t)real_len); + } + +end: + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c new file mode 100644 index 000000000..ebf74ccef --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c @@ -0,0 +1,171 @@ +#include "ln_api.h" +#include + +uint32_t ln_story_random_http(const char *device_id, + const char *device_token, + const char *story_theme) +{ + if (!device_id || !device_token) + { + printf("story random: invalid parameters\n"); + return ERRCODE_FAIL; + } + + /* CAT1 网络保活处理 */ + cat1_api_add_cat1_vote(); + cat1_api_wakeup_cat1(1); + cat1_api_keep_wakeup(1); + cat1_api_disable_cat1_cmd(0); + LOS_Msleep(200); + + HttpClient client = {0}; + HttpClientData clientData = {0}; + 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: application/json\r\n", + device_id, device_token); + if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) + { + printf("story random: build header failed\n"); + return ERRCODE_FAIL; + } + HttpClientSetCustomHeader(&client, header_buf); + + /* 自适应准备 HTTP 缓冲区 */ + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); + if (prep != HTTP_SUCCESS) + { + printf("story random: prepare HTTP buffer failed (code: %d)\n", prep); + return ERRCODE_FAIL; + } + + /* 构建 JSON 请求体(增加转义处理,避免格式错误)*/ + char json_body[256] = {0}; + char escaped_theme[256] = {0}; + int json_len = 0; + + if (story_theme && story_theme[0]) + { + int i = 0, j = 0; + while (story_theme[i] && j < (int)sizeof(escaped_theme) - 1) + { + if (story_theme[i] == '"') + { + escaped_theme[j++] = '\\'; + escaped_theme[j++] = '"'; + } + else if (story_theme[i] == '\\') + { + escaped_theme[j++] = '\\'; + escaped_theme[j++] = '\\'; + } + else + { + escaped_theme[j++] = story_theme[i]; + } + i++; + } + escaped_theme[j] = '\0'; + json_len = snprintf(json_body, sizeof(json_body), "{\"storyTheme\":\"%s\"}", escaped_theme); + } + else + { + json_len = snprintf(json_body, sizeof(json_body), "{}"); + } + if (json_len <= 0 || json_len >= (int)sizeof(json_body)) + { + printf("story random: build JSON body failed\n"); + HttpClientUnprepare(&clientData); + return ERRCODE_FAIL; + } + + /* 设置 POST 数据 */ + clientData.postBuf = json_body; + clientData.postBufLen = json_len; + clientData.postContentType = (char *)"application/json"; + + /* 发送 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) + { + printf("story random: send POST request failed (code: %d)\n", res); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + printf("story random: send POST request success\n"); + + /* 检查 HTTP 响应码 */ + int http_code = HttpClientGetResponseCode(&client); + if (http_code != 200) + { + printf("story random: invalid HTTP response code (%d)\n", http_code); + if (clientData.responseBuf && clientData.responseBufLen > 0) + { + char *resp = (char *)malloc(clientData.responseBufLen + 1); + if (resp) + { + memcpy(resp, clientData.responseBuf, clientData.responseBufLen); + resp[clientData.responseBufLen] = '\0'; + printf("story random: server response: %s\n", resp); + free(resp); + } + } + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + 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; + char sse_acc[4096] = {0}; + int sse_acc_len = 0; + const int MAX_LOOP = 2000; + while (loop_guard++ < MAX_LOOP) + { + HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData); + printf("story random: recv response result: %d\n", recv_res); + if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) + { + if (clientData.responseBuf && clientData.responseBufLen > 0) + { + ln_sse_accumulate_and_dispatch(LN_HTTP_RECV_STORY_RANDOM, + clientData.responseBuf, + clientData.responseBufLen, + sse_acc, &sse_acc_len, sizeof(sse_acc)); + HttpClientReset(&clientData); + } + else + { + LOS_Msleep(20); + } + continue; + } + else + { + printf("story random: SSE receive ended (code: %d)\n", recv_res); + break; + } + } + + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c new file mode 100644 index 000000000..60259ad79 --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c @@ -0,0 +1,237 @@ +#include "ln_api.h" +#include +#include +#include +#include "cJSON.h" +/* for HMAC-SHA256 */ +#include "mbedtls/md.h" +/* for MD5 file verify */ +#include "mbedtls/md5.h" + +static void ln_to_hex_lower(const uint8_t *in, size_t in_len, char *out) +{ + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < in_len; i++) + { + out[i * 2] = hex[(in[i] >> 4) & 0x0F]; + out[i * 2 + 1] = hex[in[i] & 0x0F]; + } + out[in_len * 2] = '\0'; +} + +/* 计算 HMAC-SHA256 并输出小写hex字符串 */ +static int ln_hmac_sha256_hex(const char *key, const char *msg, char *out_hex, size_t out_hex_size) +{ + if (out_hex_size < 65) + { /* 32 bytes -> 64 hex chars + NUL */ + return -1; + } + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (md_info == NULL) + { + return -1; + } + uint8_t digest[32]; + int ret = mbedtls_md_hmac(md_info, (const unsigned char *)key, strlen(key), + (const unsigned char *)msg, strlen(msg), digest); + if (ret != 0) + { + return -1; + } + ln_to_hex_lower(digest, sizeof(digest), out_hex); + return 0; +} + +/* 生成[a-zA-Z0-9]随机串 */ +static void ln_gen_nonce(char *out, size_t len) +{ + static const char table[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + if (out == NULL || len == 0) + { + return; + } + /* 使用时间作为随机种子,避免强依赖标准rand种子 */ + uint32_t ts_s = 0; + uapi_get_timestampe_ss(&ts_s); + srand((unsigned int)(ts_s ^ (unsigned int)uapi_tcxo_get_ms())); + for (size_t i = 0; i + 1 < len; i++) + { + out[i] = table[rand() % (sizeof(table) - 1)]; + } + out[len - 1] = '\0'; +} + +uint32_t ln_get_device_token_http(const char *device_id, + const char *device_iccid, + const char *device_model, + const char *device_version) +{ + if (device_id == NULL || device_iccid == NULL) + { + printf("LN token: invalid parameters\n"); + return ERRCODE_FAIL; + } + + /* 1. 生成时间戳(ms)与nonce */ + uint32_t ts_s = 0; /* epoch seconds */ + uapi_get_timestampe_ss(&ts_s); + char timestamp[32] = {0}; + (void)snprintf(timestamp, sizeof(timestamp), "%u000", ts_s); /* 转毫秒字符串 */ + + char nonce[17] = {0}; /* 12~16位,取固定16以简化 */ + ln_gen_nonce(nonce, sizeof(nonce)); + + /* 2. 计算签名: deviceId:deviceIccid:timestamp:nonce */ + char sign_data[256] = {0}; + (void)snprintf(sign_data, sizeof(sign_data), "%s:%s:%s:%s", device_id, device_iccid, timestamp, nonce); + char signature[65] = {0}; + if (ln_hmac_sha256_hex(LN_SIGN_SECRET, sign_data, signature, sizeof(signature)) != 0) + { + printf("LN token: hmac calc failed\n"); + return ERRCODE_FAIL; + } + + /* 3. 组织自定义请求头 */ + char header_buf[1024] = {0}; + int hdr_len = snprintf(header_buf, sizeof(header_buf), + "deviceId: %s\r\n" + "deviceIccid: %s\r\n" + "timestamp: %s\r\n" + "nonce: %s\r\n" + "signature: %s\r\n" + "deviceModel: %s\r\n" + "deviceVersion: %s\r\n" + "Content-Type: application/json\r\n", + device_id, device_iccid, timestamp, nonce, signature, + (device_model && device_model[0]) ? device_model : "abmybox_1", + (device_version && device_version[0]) ? device_version : "1.0.0"); + if (hdr_len <= 0 || hdr_len >= (int)sizeof(header_buf)) + { + printf("LN token: header build failed\n"); + return ERRCODE_FAIL; + } + + /* 4. 保活CAT1,准备网络 */ + cat1_api_add_cat1_vote(); + cat1_api_wakeup_cat1(1); + cat1_api_keep_wakeup(1); + cat1_api_disable_cat1_cmd(1); + LOS_Msleep(500); + + /* 5. 发起HTTPS GET */ + HttpClient client = (HttpClient){0}; + HttpClientData clientData = (HttpClientData){0}; + client.isHttp = false; /* https */ + client.remotePort = 443; + HttpClientSetCustomHeader(&client, header_buf); + + HTTPC_RESULT prep = HttpClientPrepare(&clientData, 2048, 4096); + if (prep != HTTP_SUCCESS) + { + printf("LN token: HttpClientPrepare failed %d\n", prep); + return ERRCODE_FAIL; + } + + HTTPC_RESULT res = HttpClientGetRequest(&client, LN_FULL_URL, &clientData); + if (res != HTTP_SUCCESS) + { + printf("LN token: request failed %d\n", res); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + int code = HttpClientGetResponseCode(&client); + if (code != 200) + { + printf("LN token: HTTP %d\n", code); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + /* 6. 解析响应JSON */ + if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0) + { + printf("LN token: empty body\n"); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + int body_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; + if (body_len <= 0) + body_len = (int)strlen(clientData.responseBuf); + + char *json_text = (char *)malloc((size_t)body_len + 1); + if (json_text == NULL) + { + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + memcpy(json_text, clientData.responseBuf, (size_t)body_len); + json_text[body_len] = '\0'; + + cJSON *root = cJSON_Parse(json_text); + if (!root) + { + printf("LN token: JSON parse failed\n"); + free(json_text); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); + if (errorCode != NULL && !cJSON_IsNull(errorCode)) + { + cJSON *msg = cJSON_GetObjectItem(root, "message"); + printf("LN token: failed, err=%s\n", msg && cJSON_IsString(msg) ? msg->valuestring : "unknown"); + cJSON_Delete(root); + free(json_text); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_FAIL; + } + + cJSON *data = cJSON_GetObjectItem(root, "data"); + if (data && cJSON_IsObject(data)) + { + cJSON *token = cJSON_GetObjectItem(data, "token"); + cJSON *expire = cJSON_GetObjectItem(data, "expireTime"); + if (g_device_token) + { + free(g_device_token); + g_device_token = NULL; + } + if (token && cJSON_IsString(token)) + { + size_t tlen = strlen(token->valuestring); + g_device_token = (char *)malloc(tlen + 1); + if (g_device_token) + { + memcpy(g_device_token, token->valuestring, tlen + 1); + } + } + if (g_device_token == NULL) + { + g_device_token = strdup(""); + } + printf("\njson=%s\n", json_text); + if (expire && cJSON_IsNumber(expire)) + { + printf("Token expireTime: %lld\n", (long long)expire->valuedouble); + } + } + else + { + printf("LN token: no data field\n"); + } + + cJSON_Delete(root); + free(json_text); + HttpClientUnprepare(&clientData); + HttpClientClse(&client); + return ERRCODE_SUCC; +} diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py b/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py index 615281a8f..4869b285e 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py @@ -125,6 +125,10 @@ def print_request_details(url: str, method: str, headers: Dict, data: Optional[D def process_sse_stream(response: requests.Response) -> None: + # 新增:打印完整请求头(包括自动生成的Content-Type) + logger.info("完整请求头(含自动生成部分):") + for k, v in response.request.headers.items(): + logger.info(f" {k}: {v}") """处理SSE流式响应的通用函数""" if response.status_code != 200: logger.error(f"❌ 请求失败,状态码: {response.status_code}") diff --git "a/code/sdk/zhihui/leiniao/\350\256\276\345\244\207API\346\216\245\345\217\243\346\226\207\346\241\243.md" "b/code/sdk/zhihui/leiniao/\350\256\276\345\244\207API\346\216\245\345\217\243\346\226\207\346\241\243.md" new file mode 100644 index 000000000..b3a7ed109 --- /dev/null +++ "b/code/sdk/zhihui/leiniao/\350\256\276\345\244\207API\346\216\245\345\217\243\346\226\207\346\241\243.md" @@ -0,0 +1,1554 @@ +# 设备API接口文档 + +测试服务BaseUrl:https://ambybox-api.test.leiniao.com/ambybox-device-api + +## 1. 获取设备Token接口 + +### 时序图 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + + Note over Device, Server: 设备认证流程 + + Device->>Device: 生成时间戳和随机数 + Device->>Device: 构建签名数据
deviceId:deviceIccid:timestamp:nonce + Device->>Device: 使用HMAC-SHA256生成签名 + + Device->>Server: GET /v1/auth/getDeviceToken
Headers: deviceId, deviceIccid, timestamp, nonce, signature + + alt 签名验证成功 + Server->>Server: 验证时间戳(5分钟内有效) + Server->>Server: 验证签名正确性 + Server->>Server: 生成JWT Token + Server->>Device: 返回设备Token
{"deviceId", "token", "expireTime"} + + Note over Device: Token获取成功,缓存Token + + else 签名验证失败 + Server->>Device: 返回错误
{"errorCode": "INVALID_SIGNATURE"} + Note over Device: 认证失败,重新尝试 + end +``` + +### 接口描述 +设备认证接口,用于获取设备访问令牌。该接口无需认证,但需要进行签名校验。 + +### 请求信息 +- **URL**: `/v1/auth/getDeviceToken` +- **方法**: `GET` +- **Content-Type**: `application/json` + +### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceIccid | String | 是 | 设备物联网卡号 | "12345678901234567890" | +| timestamp | String | 是 | 毫秒时间戳 | "1640995200000" | +| nonce | String | 是 | 随机数,用于防重放攻击 | "abc123" | +| signature | String | 是 | 签名,用于验证请求合法性 | "a1b2c3d4..." | +| deviceModel | String | 否 | 设备型号,默认值: "abmybox_1" | "abmybox_1" | +| deviceVersion | String | 否 | 设备固件版本,默认值: "1.0.0" | "1.0.0" | + +### 签名算法 +签名使用 HMAC-SHA256 算法生成,具体步骤如下: + +1. **构建签名数据**:将参数按以下格式拼接 + ``` + signData = deviceId + ":" + deviceIccid + ":" + timestamp + ":" + nonce + ``` + +2. **生成签名**:使用预共享密钥对签名数据进行 HMAC-SHA256 加密 + ``` + signature = HMAC-SHA256(signSecret, signData) + ``` + +3. **转换格式**:将签名结果转换为十六进制字符串(小写) + +### 签名验证规则 +- 时间戳有效性:请求时间戳与服务器时间差不能超过5分钟 +- 签名正确性:使用相同算法生成的签名必须与请求中的签名一致 +- 防重放攻击:建议客户端每次请求使用不同的随机数 +- 测试签名密钥:vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus= + + +### 响应数据 +**成功响应**: +```json +{ + "errorCode": null, + "message": null, + "data": { + "deviceId": "DEVICE_001", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expireTime": 2592000 + } +} +``` + +**失败响应**: +```json +{ + "errorCode": "INVALID_SIGNATURE", + "message": "签名验证失败", + "data": null +} +``` + +### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| deviceId | String | 设备ID | +| token | String | 设备访问令牌(JWT格式) | +| expireTime | Long | 令牌过期时间(秒,默认30天) | + +### 错误码 +| 错误码 | 描述 | +|--------|------| +| INVALID_SIGNATURE | 签名验证失败 | +| SERVER_ERROR | 服务器内部错误 | + +--- + +## 2. 语音对话接口 + +### 时序图 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + + Note over Device, Server: 流式语音对话流程 + + Device->>Device: 录制用户语音 + Device->>Device: 转换为MP3格式 + + Device->>Server: POST /v1/speechChat/chatStream
Headers: deviceId, deviceToken, Accept: text/event-stream
Form: audio file + + alt Token验证成功 + Server->>Server: 验证设备Token + Server->>Server: 建立SSE连接 + Server->>Server: 语音识别(ASR) + Server->>Server: 意图识别 + + loop 流式响应生成 + Server->>Server: 生成回复片段 + Server->>Server: 文本转语音(TTS) + Server->>Device: SSE事件: 回复片段
{"audioUrl", "textContent", "intention"} + Device->>Device: 播放回复音频片段 + Device->>Device: 显示文本片段 + end + + Server->>Device: SSE连接关闭(对话完成) + + alt 识别到哄睡意图 + Note over Device: intention = "StartSleepMode" + Device->>Device: 准备进入哄睡模式 + end + + else Token验证失败 + Server->>Device: 返回认证错误
{"errorCode": "AUTH_FAILURE"} + Device->>Device: 重新获取Token + end +``` + +### 接口描述 +流式语音对话接口,接收音频文件并通过SSE(Server-Sent Events)流式返回对话结果。 + +### 请求信息 +- **URL**: `/v1/speechChat/chatStream` +- **方法**: `POST` +- **Content-Type**: `multipart/form-data` +- **响应类型**: `text/event-stream` (SSE流式响应) + +### 认证要求 +该接口需要设备Token认证,有以下两种方式传递Token: + +1. **请求头方式**: + ``` + deviceToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` + +2. **查询参数方式**: + ``` + /v1/speechChat/chatStream?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` + +### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | +| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" | +| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" | + +### 请求参数 +| 参数名 | 类型 | 必填 | 描述 | 限制 | +|--------|------|------|------|------| +| file | MultipartFile | 是 | 音频文件 | MP3格式,压缩前应为采样率16k或8k单声道的PCM | + +### Token认证规则 +- Token必须是有效的JWT格式 +- Token必须包含设备ID、设备型号、设备版本等信息 +- Token不能过期(默认有效期30天) +- 请求头中的设备ID不能为空 +- 设备ID必须与Token中的设备ID一致 + +### SSE流式响应数据 +**回复片段事件**: +``` +data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_001.mp3","textContent":"好的,","intention":null}} + +data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_002.mp3","textContent":"我来为你","intention":null}} + +data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_003.mp3","textContent":"讲一个故事","intention":"StartSleepMode"}} +``` + +**失败响应(认证失败)**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +**失败响应(服务异常)**: +```json +{ + "errorCode": "SERVER_ERROR", + "message": "服务器内部错误", + "data": null +} +``` + +### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| audioUrl | String | 回复音频片段文件URL | +| textContent | String | 回复的文本片段内容 | +| intention | String | 用户意图识别结果,通常在第一个片段中返回,可能的值:StartSleepMode(哄我睡觉)等 | + +### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | +| SERVER_ERROR | 服务器内部错误 | + +### SSE协议说明 +- 接口使用SSE(Server-Sent Events)协议进行流式响应 +- 每个回复片段作为一个独立的SSE事件发送 +- 客户端需要实现SSE客户端来接收流式数据 +- 连接会在对话生成完成后自动关闭 +- 客户端应当设置适当的超时时间(建议60秒) + +### 音频数据处理说明 +- 返回的 `audioUrl` 字段是音频片段文件URL +- 音频格式通常为MP3格式 +- 客户端需要将音频文件URL下载后播放 +- 建议客户端实现音频片段的缓存和顺序播放机制 + +### 用户意图说明 +流式语音对话接口会识别用户的意图,目前支持的意图包括: +- **StartSleepMode**:用户表达想要听故事或哄睡的意图 +- 更多意图类型会在后续版本中添加 + +用户意图通常在最后一个或几个SSE事件中返回,客户端应当: +- 监听所有SSE事件中的intention字段 +- 当收到非null的intention时,根据意图类型进行相应处理 +- 收到StartSleepMode意图时,可以调用故事相关的API接口 + +### 使用说明 +- 该接口适用于需要实时响应的语音对话场景 +- 客户端可以边接收边播放,实现流式播放体验 +- 相比普通语音对话接口,流式接口能提供更自然的交互体验 +- 建议在网络状况良好的环境下使用,以确保音频片段的连续播放 +- 对话生成完成后,SSE连接会自动关闭 + + + +## 3. 故事相关接口 + +### 时序图 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + + Note over Device, Server: 哄睡故事完整流程 + + %% 意图触发 + Device->>Server: 语音对话识别到哄睡意图
intention = "StartSleepMode" + + %% 故事选择分支 + alt 用户指定故事类型 + Device->>Device: 录制用户偏好语音 + Device->>Server: POST /v1/story/chooseStory
Headers: Accept: text/event-stream
Form: audio file + else 随机故事 + Device->>Server: POST /v1/story/randomStory
Headers: Accept: text/event-stream
Body: {"storyTheme": "可选主题"} + end + + %% SSE流式响应 + Server->>Server: 建立SSE连接 + Server->>Server: 分析语音/生成故事主题 + Server->>Server: 开始生成故事片段 + + loop 故事片段流式推送 + Server->>Server: 生成故事片段文本 + Server->>Server: 文本转语音 + Server->>Device: SSE事件: 故事片段
{"storyContent", "storyAudioUrl", "storyTheme"} + Device->>Device: 播放音频片段 + Device->>Device: 显示文本内容 + end + + Server->>Device: SSE连接关闭(故事完成) + + %% 退出确认流程 + Device->>Server: POST /v1/story/getExitSleepInquiry
获取退出询问 + Server->>Device: 返回退出询问
{"audioContent", "textContent"} + Device->>Device: 播放退出询问音频 + + Device->>Device: 等待用户确认 + alt 用户确认退出 + Device->>Server: POST /v1/story/finishSleepMode
确认结束哄睡 + Server->>Device: 返回成功响应 + Device->>Device: 退出哄睡模式 + else 用户继续 + Note over Device: 继续哄睡模式或重新开始故事 + end +``` + +### 3.1 智能选择故事 + +#### 接口描述 +智能选择故事接口。用户通过语音表达故事类型偏好,系统会识别故事主题并流式返回相应的故事内容。该接口使用SSE(Server-Sent Events)协议进行流式响应,需要设备Token认证。 + +#### 请求信息 +- **URL**: `/v1/story/chooseStory` +- **方法**: `POST` +- **Content-Type**: `multipart/form-data` +- **响应类型**: `text/event-stream` (SSE流式响应) + +#### 认证要求 +该接口需要设备Token认证,Token通过请求头或查询参数传递。 + +#### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | +| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" | +| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" | + +#### 请求表单参数 +| 参数名 | 类型 | 必填 | 描述 | 限制 | +|--------|------|------|------|------| +| file | MultipartFile | 是 | 语音文件 | MP3格式,压缩前应为采样率16k或8k单声道的PCM | + + +#### SSE流式响应数据 +**故事片段事件**: +``` +data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"从前有一个可爱的小女孩","storyAudioUrl":"https://example.com/story/audio/chunk_001.mp3","storyDuration":null}} + +data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"她住在森林深处的小木屋里","storyAudioUrl":"https://example.com/story/audio/chunk_002.mp3","storyDuration":null}} + +data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"每天都会去采集美丽的花朵","storyAudioUrl":"https://example.com/story/audio/chunk_003.mp3","storyDuration":null}} +``` + +**错误响应(认证失败)**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +**错误响应(故事生成失败)**: +```json +{ + "errorCode": "SERVER_ERROR", + "message": "故事生成失败", + "data": null +} +``` + +#### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| storyTheme | String | 故事主题 | +| storyContent | String | 故事片段文本内容 | +| storyAudioUrl | String | 故事片段音频文件URL | +| storyDuration | Integer | 故事片段时长(秒),流式响应中通常为null | + +#### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | +| SERVER_ERROR | 服务器内部错误 | + +#### SSE协议说明 +- 接口使用SSE(Server-Sent Events)协议进行流式响应 +- 每个故事片段作为一个独立的SSE事件发送 +- 客户端需要实现SSE客户端来接收流式数据 +- 连接会在故事生成完成后自动关闭 +- 客户端应当设置适当的超时时间(建议120秒) + +#### 使用说明 +- 用户可以通过语音表达故事偏好,如"我想听童话故事"、"讲个冒险故事吧"等 +- 系统会分析语音内容,识别故事主题,并实时生成故事片段 +- 每个故事片段包含文本内容和对应的音频URL +- 客户端可以边接收边播放,实现流式播放体验 +- 故事生成完成后,SSE连接会自动关闭 + +### 3.2 随机故事 + +#### 接口描述 +随机故事接口。系统会随机生成一个故事并流式返回给用户。该接口使用SSE(Server-Sent Events)协议进行流式响应,需要设备Token认证。 + +#### 请求信息 +- **URL**: `/v1/story/randomStory` +- **方法**: `POST` +- **Content-Type**: `application/json` +- **响应类型**: `text/event-stream` (SSE流式响应) + +#### 认证要求 +该接口需要设备Token认证,Token通过请求头或查询参数传递。 + +#### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | +| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" | +| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" | + + +#### 请求体参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| storyTheme | String | 否 | 故事主题 | "爱情故事" | + + +示例: +```json +{ + "storyTheme": "爱情故事" +} +``` + + +#### SSE流式响应数据 +**故事片段事件**: +``` +data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"在一个阳光明媚的早晨","storyAudioUrl":"https://example.com/story/audio/chunk_001.mp3","storyDuration":null}} + +data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"小兔子决定去探险","storyAudioUrl":"https://example.com/story/audio/chunk_002.mp3","storyDuration":null}} + +data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"它背上小书包,踏上了未知的旅程","storyAudioUrl":"https://example.com/story/audio/chunk_003.mp3","storyDuration":null}} +``` + +**错误响应(认证失败)**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +**错误响应(故事生成失败)**: +```json +{ + "errorCode": "SERVER_ERROR", + "message": "故事生成失败", + "data": null +} +``` + +#### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| storyTheme | String | 故事主题 | +| storyContent | String | 故事片段文本内容 | +| storyAudioUrl | String | 故事片段音频文件URL | +| storyDuration | Integer | 故事片段时长(秒),流式响应中通常为null | + +#### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | +| SERVER_ERROR | 服务器内部错误 | + +#### SSE协议说明 +- 接口使用SSE(Server-Sent Events)协议进行流式响应 +- 每个故事片段作为一个独立的SSE事件发送 +- 客户端需要实现SSE客户端来接收流式数据 +- 连接会在故事生成完成后自动关闭 +- 客户端应当设置适当的超时时间(建议120秒) + +#### 使用说明 +- 该接口不需要任何参数,系统会自动生成随机主题的故事 +- 适用于用户没有特定偏好,想要听任意故事的场景 +- 可以作为默认的故事推荐方式 +- 客户端可以边接收边播放,实现流式播放体验 +- 故事生成完成后,SSE连接会自动关闭 + +### 3.3 获取退出哄睡询问 + +#### 接口描述 +获取退出哄睡询问接口。在用户要结束哄睡前,返回询问内容用于确认用户是否真的要退出哄睡模式。该接口需要设备Token认证。 + +#### 请求信息 +- **URL**: `/v1/story/getExitSleepInquiry` +- **方法**: `POST` +- **Content-Type**: `application/json` + +#### 认证要求 +该接口需要设备Token认证,Token通过请求头或查询参数传递。 + +#### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | + +#### 请求参数 +无请求参数 + +#### 响应数据 +**成功响应**: +```json +{ + "errorCode": null, + "message": null, + "data": { + "audioUrl": "https://example.com/audio/response.mp3", + "textContent": "你确定要退出哄睡模式吗?如果确定要退出,请说确定", + "intention": "EXIT_SLEEP_MODE" + } +} +``` + +**失败响应(认证失败)**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +#### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| audioUrl | String | 询问音频文件URL | +| textContent | String | 询问的文本内容 | +| intention | String | 用户意图类型,此接口固定返回"EXIT_SLEEP_MODE" | + +#### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | +| SERVER_ERROR | 服务器内部错误 | + +#### 使用说明 +- 该接口用于获取退出哄睡模式的确认询问 +- 返回的音频和文本内容用于向用户确认是否真的要退出哄睡模式 +- 设备播放询问内容后,应等待用户的确认回复 +- 如果用户确认退出,再调用`finishSleepMode`接口正式结束哄睡模式 + +### 3.4 确认结束哄睡模式 + +#### 接口描述 +结束哄睡模式接口。当用户完成哄睡故事播放或主动结束哄睡模式时,调用此接口通知服务器。该接口需要设备Token认证。 + +#### 请求信息 +- **URL**: `/v1/story/finishSleepMode` +- **方法**: `POST` +- **Content-Type**: `application/json` + +#### 认证要求 +该接口需要设备Token认证,Token通过请求头或查询参数传递。 + +#### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | + +#### 请求参数 +无请求参数 + +#### 响应数据 +**成功响应**: +```json +{ + "errorCode": null, + "message": null, + "data": "" +} +``` + +**失败响应(认证失败)**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +#### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| data | String | | + +#### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | +| SERVER_ERROR | 服务器内部错误 | + +#### 使用说明 +- 该接口用于通知服务器哄睡模式已结束 +- 服务器可以记录哄睡会话的结束时间和统计信息 +- 调用此接口后,服务器可能会停止推送哄睡相关的消息 +- 建议在故事播放完成或用户主动停止时调用此接口 + +### 故事接口使用流程 +1. **意图识别**:用户通过语音对话接口表达哄我睡觉的意图,系统返回`StartSleepMode`意图 +2. **故事选择**: + - 如果用户有特定偏好,调用`chooseStory`接口,传入用户的语音描述,获取对应主题的故事流 + - 如果用户没有特定偏好,调用`randomStory`接口获取随机故事流 +3. **SSE连接建立**:客户端建立SSE连接,准备接收流式故事数据 +4. **流式故事播放**: + - 接收到每个故事片段事件后,立即播放对应的音频文件 + - 可以同时显示文本内容,实现边听边看的体验 + - 继续接收后续故事片段,直到故事完整播放完毕 +5. **连接关闭**:故事生成完成后,SSE连接自动关闭 +6. **退出询问**:故事播放完成或用户主动结束时,调用`getExitSleepInquiry`获取二次确认询问 +7. **播放询问**:设备播放退出询问的音频内容,等待用户确认 +8. **确认结束**:用户确认后,调用`finishSleepMode`接口通知服务器正式结束哄睡模式 + +### SSE客户端实现要点 +- **请求头设置**:必须设置`Accept: text/event-stream`和`Cache-Control: no-cache` +- **连接超时**:建议设置120秒超时时间,因为故事生成可能需要较长时间 +- **事件解析**:正确解析SSE事件格式,提取`data:`字段中的JSON数据 +- **错误处理**:处理连接失败、超时、认证失败等异常情况 +- **资源管理**:连接完成后正确释放资源,避免内存泄漏 + + +--- + +## 4. 设备信息接口 + +### 时序图 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + + Note over Device, Server: 设备信息管理流程 + + %% 获取设备角色信息 + Device->>Server: GET /v1/device/getCharacter
Headers: deviceId, deviceToken + + alt 首次开机 + Server->>Device: 返回默认角色信息
{"characterId", "characterName", "characterAvatar", "emotionEmojis"} + Device->>Device: 应用默认角色配置 + else 已有角色配置 + Server->>Device: 返回用户选择的角色信息 + Device->>Device: 应用角色配置 + end + + %% 检查用户绑定状态 + Device->>Server: GET /v1/device/getUserId
获取绑定用户ID + + alt 设备已绑定用户 + Server->>Device: 返回用户ID
{"data": "user_001"} + Device->>Device: 正常运行模式 + else 设备未绑定 + Server->>Device: 返回空值
{"data": null} + Device->>Device: 显示绑定二维码 + + %% 生成绑定二维码 + Device->>Server: POST /v1/qrcode/generate/image
{"content": "绑定链接", "width": 300, "height": 300} + Server->>Device: 返回二维码图片(PNG) + Device->>Device: 显示二维码等待绑定 + end +``` + +### 4.1 获取设备角色信息 + +#### 接口描述 +获取设备角色信息接口。首次开机时返回默认角色,否则返回小程序选择的角色信息。该接口需要设备Token认证。 + +#### 请求信息 +- **URL**: `/v1/device/getCharacter` +- **方法**: `GET` +- **Content-Type**: `application/json` + +#### 认证要求 +该接口需要设备Token认证,Token通过请求头传递。 + +#### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | + +#### 响应数据 +**成功响应**: +```json +{ + "errorCode": null, + "message": null, + "data": { + "characterId": "char_001", + "characterName": "小艾", + "characterAvatar": { + "hairColor": "orange", + "eyesColor": "blue", + "hairStyle": "long-hair", + "screenImage": "https://example.com/avatar.jpg", + "screenImageAsEmoji": false + }, + "emotionEmojis": [ + { + "emotion": "happy", + "emojiUrl": "https://example.com/emoji/happy.png" + }, + { + "emotion": "sad", + "emojiUrl": "https://example.com/emoji/sad.png" + } + ] + } +} +``` + +**失败响应**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +#### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| characterId | String | 角色ID | +| characterName | String | 角色名称 | +| characterAvatar | Object | 角色形象配置 | +| emotionEmojis | Array | 角色表情数组 | + +**characterAvatar字段说明**: + +| 字段名 | 类型 | 描述 | 可选值 | +|--------|------|------|--------| +| hairColor | String | 发色 | orange, red, purple, green, blue, black | +| eyesColor | String | 瞳色 | orange, red, purple, green, blue, yellow | +| hairStyle | String | 发型 | side-parted, slicked-back, long-hair | +| screenImage | String | 屏保图片URL | | +| screenImageAsEmoji | Boolean | 屏保图片是否作为对话表情 | 默认false | + +**emotionEmoji字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| emotion | String | 心情名称 | +| emojiUrl | String | 表情URL | + +#### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | + +### 4.2 获取设备绑定用户ID + +#### 接口描述 +获取设备绑定的用户ID接口。用于检查设备是否已绑定用户,如果未绑定则需要弹出二维码供用户绑定。该接口需要设备Token认证。 + +#### 请求信息 +- **URL**: `/v1/device/getUserId` +- **方法**: `GET` +- **Content-Type**: `application/json` + +#### 认证要求 +该接口需要设备Token认证,Token通过请求头传递。 + +#### 请求头参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | + +#### 响应数据 +**成功响应(已绑定用户)**: +```json +{ + "errorCode": null, + "message": null, + "data": "user_001" +} +``` + +**成功响应(未绑定用户)**: +```json +{ + "errorCode": null, + "message": null, + "data": null +} +``` + +**失败响应**: +```json +{ + "errorCode": "AUTH_FAILURE", + "message": "设备Token无效", + "data": null +} +``` + +#### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| data | String | 用户ID,如果设备未绑定用户则返回null | + +#### 错误码 +| 错误码 | 描述 | +|--------|------| +| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) | + +--- + +## 5. OTA升级任务接口 + +### 时序图 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + + Note over Device, Server: OTA升级流程 + + %% 设备启动检查升级 + Device->>Device: 设备启动 + Device->>Device: 获取当前固件版本 + + Device->>Server: GET /v1/upgrade/getTask
?deviceId=xxx&sourceVersion=1.0.0 + + alt 有可用升级 + Server->>Server: 检查升级策略 + Server->>Server: 查找可用升级包 + Server->>Device: 返回升级任务
{"updateId", "targetVersion", "packageUrl", "packageMd5", "updateType"} + + Device->>Device: 检查升级类型 + + alt 强制升级 (updateType=1) + Device->>Device: 立即开始升级 + else 可选升级 (updateType=0) + Device->>Device: 提示用户选择 + alt 用户同意升级 + Device->>Device: 开始升级 + else 用户拒绝 + Device->>Device: 继续正常运行 + end + end + + %% 执行升级 + opt 开始升级流程 + Device->>Device: 下载升级包 + Device->>Server: 下载 packageUrl + Server->>Device: 返回升级包文件 + + Device->>Device: 校验MD5 + alt MD5校验成功 + Device->>Device: 执行固件升级 + Device->>Device: 重启设备 + Device->>Device: 升级完成,版本更新 + Device->>Server: 重新获取设备Token
(新版本) + else MD5校验失败 + Device->>Device: 升级失败,重新下载 + end + end + + else 无可用升级 + Server->>Device: 返回无升级
{"errorCode": "UPGRADE_NOT_FOUND"} + Device->>Device: 继续正常运行 + end +``` + +### 接口描述 +获取OTA升级任务接口。设备可通过此接口查询是否有可用的固件升级任务,如果有则返回升级包的详细信息。 + +### 请求信息 +- **URL**: `/v1/upgrade/getTask` +- **方法**: `GET` +- **Content-Type**: `application/json` + +### 请求参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" | +| sourceVersion | String | 是 | 设备当前固件版本 | "1.0.0" | + +### 响应数据 +**成功响应(有升级任务)**: +```json +{ + "errorCode": null, + "message": null, + "data": { + "updateId": 123, + "sourceVersion": "1.0.0", + "targetVersion": "1.1.0", + "updateType": 1, + "packageId": 456, + "packageUrl": "https://example.com/firmware/update_1.1.0.bin", + "packageMd5": "a1b2c3d4e5f6789012345678901234567890", + "packageSize": 1048576, + "description": "升级描述" + } +} +``` + +**成功响应(无升级任务)**: +```json +{ + "errorCode": "UPGRADE_NOT_FOUND", + "message": "已是最新版本", + "data": null +} +``` + +**失败响应**: +```json +{ + "errorCode": "SERVER_ERROR", + "message": "服务器异常", + "data": null +} +``` + +### 响应字段说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| updateId | Integer | 升级任务ID | +| sourceVersion | String | 源版本号 | +| targetVersion | String | 目标版本号 | +| updateType | Integer | 升级类型,1:强制升级,0:可选升级 | +| packageId | Integer | 升级包ID | +| packageUrl | String | 升级包下载URL | +| packageMd5 | String | 升级包MD5校验值 | +| packageSize | Long | 升级包大小(字节) | +| description | String | 升级描述 | + +### 错误码 +| 错误码 | 描述 | +|--------|------| +| UPGRADE_NOT_FOUND | 已是最新版本 | +| SERVER_ERROR | 服务器异常 | + +### 升级流程说明 +1. 设备启动时,调用此接口检查是否有可用的升级任务 +2. 如果有升级任务,获取升级包URL和校验信息 +3. 设备下载升级包,使用MD5校验文件完整性 +4. 校验通过后,设备执行固件升级操作 +5. 升级完成后,设备重启并重新获取设备token + +--- + +## 6. WebSocket连接端点 + +### 时序图 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + participant MiniProgram as 小程序端 + + Note over Device,MiniProgram: WebSocket通信流程 + + %% 建立连接 + Device->>Server: WebSocket连接请求
Headers: deviceId, deviceToken + + alt Token验证成功 + Server->>Device: 连接成功(101) + Server->>Server: 记录设备会话 + + %% 心跳保持 + loop 心跳循环 + Device->>Server: ping + Server->>Device: pong + end + + %% 业务消息推送示例 + par 角色切换 + MiniProgram->>Server: 用户在小程序切换角色 + Server->>Device: WebSocket推送
{"command": "CharacterChange", "data": {...}} + Device->>Device: 应用新角色配置 + and 哄睡模式控制 + MiniProgram->>Server: 用户启动哄睡模式 + Server->>Device: WebSocket推送
{"command": "SleepModeStart", "data": {"storyTheme": "..."}} + Device->>Device: 启动哄睡模式 + and 设备绑定通知 + MiniProgram->>Server: 用户绑定设备 + Server->>Device: WebSocket推送
{"command": "DeviceBind", "data": {"userId": "...", "userName": "..."}} + Device->>Device: 更新绑定状态 + and 日程提醒 + Server->>Device: WebSocket推送
{"command": "ScheduleReminder", "data": {"scheduleName": "...", "reminderContent": "..."}} + Device->>Device: 播放提醒内容 + end + + else Token验证失败 + Server->>Device: 连接拒绝(401) + Device->>Device: 重新获取Token后重连 + end + + %% 连接断开 + alt 正常断开 + Device->>Server: 关闭连接 + Server->>Server: 清理设备会话 + else 异常断开 + Server->>Server: 检测到连接断开 + Server->>Server: 清理设备会话 + Device->>Device: 自动重连机制 + end +``` + +### 接口描述 +WebSocket连接端点,用于设备与服务器建立实时双向通信连接。该连接需要设备Token认证。 + +### 连接信息 +- **URL**: `wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket` +- **协议**: `WebSocket` +- **认证**: 设备Token认证 + +### 连接认证 +WebSocket连接需要设备Token认证,有以下两种方式传递Token: + +1. **请求头方式**: + ``` + deviceId: DEVICE_001 + deviceToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + reconnect: 0 + ``` + +2. **查询参数方式**: + ``` + wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket?deviceToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` + +### 连接参数 +| 参数名 | 类型 | 必填 | 描述 | 示例 | +|--------|------|------|------|------| +| deviceId | String | 是 | 设备唯一标识符(请求头) | "DEVICE_001" | +| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." | +| reconnect | Integer | 否 | 重连次数,设备启动时设置为0,断线重连递增 | 0 | + +### 连接状态码 +| 状态码 | 描述 | +|--------|------| +| 400 | 设备ID为空 | +| 401 | 设备Token无效或过期 | +| 101 | 正常连接 | + + +### 连接流程 +1. 客户端携带设备Token发起WebSocket连接 +2. 服务器验证Token有效性和设备ID匹配性 +3. 连接建立成功,设备会话被管理器记录 +4. 客户端可发送心跳消息保持连接活跃 +5. 连接断开时,设备会话从管理器中移除 + +### 错误处理 +- **认证失败**:Token无效或设备ID不匹配,连接被拒绝 +- **传输错误**:连接出现异常时,服务器会主动关闭连接 +- **心跳超时**:长时间无消息交互时,连接可能被服务器关闭 + + + +### 客户端心跳消息 +**客户端发送心跳**: +``` +ping +``` + +**服务器响应心跳**: +``` +pong +``` + +### 服务端业务消息 + +服务端通过WebSocket连接推送业务消息给设备端,消息采用JSON格式,消息结构如下: + +```json +{ + "deviceId": "设备ID", + "timestamp": 1640995200, + "messageId": "消息ID,用于设备去重防重放", + "command": "命令名称", + "data": { + // 具体的消息数据,根据不同命令名称而变化 + } +} +``` + +#### 消息结构说明 +| 字段名 | 类型 | 描述 | +|--------|------|------| +| deviceId | String | 设备唯一标识符 | +| timestamp | Long | 时间戳,单位:秒 | +| messageId | String | 消息ID,用于设备去重防重放 | +| command | String | 命令名称,见下方支持的命令列表 | +| data | Object | 消息体,不同命令对应不同的数据结构 | + +支持的命令名称如下: + +#### 1. 角色切换 (CharacterChange) +**命令描述**:通知设备切换到指定角色 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_001", + "command": "CharacterChange", + "data": { + "characterId": "char_001", + "characterName": "小艾", + "characterAvatar": { + "hairColor": "orange", + "eyesColor": "blue", + "hairStyle": "long-hair", + "screenImage": "https://example.com/avatar.jpg", + "screenImageAsEmoji": false + }, + "emotionEmojis": [ + { + "emotion": "happy", + "emojiUrl": "https://example.com/emoji/happy.png" + }, + { + "emotion": "sad", + "emojiUrl": "https://example.com/emoji/sad.png" + } + ] + } +} +``` + +**data字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| characterId | String | 角色ID | +| characterName | String | 角色名称 | +| characterAvatar | Object | 角色形象配置 | +| emotionEmojis | Array | 角色表情数组 | + +**characterAvatar字段说明**: + +| 字段名 | 类型 | 描述 | 可选值 | +|--------|------|------|--------| +| hairColor | String | 发色 | orange, red, purple, green, blue, black | +| eyesColor | String | 瞳色 | orange, red, purple, green, blue, yellow | +| hairStyle | String | 发型 | side-parted, slicked-back, long-hair | +| screenImage | String | 屏保图片URL | | +| screenImageAsEmoji | Boolean | 屏保图片是否作为对话表情 | 默认false | + +**emotionEmoji字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| emotion | String | 心情名称 | +| emojiUrl | String | 表情图片URL | + +#### 2. 角色配置更改 (CharacterConfigChange) +**命令描述**:更新当前角色的配置信息 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_002", + "command": "CharacterConfigChange", + "data": { + "characterId": "char_001", + "characterName": "小艾", + "characterAvatar": { + "hairColor": "blue", + "eyesColor": "green", + "hairStyle": "side-parted", + "screenImage": "https://example.com/new_avatar.jpg", + "screenImageAsEmoji": true + }, + "emotionEmojis": [ + { + "emotion": "excited", + "emojiUrl": "https://example.com/emoji/excited.png" + } + ] + } +} +``` + +**data字段说明**:与角色切换命令相同 + +#### 3. 哄睡模式开始 (SleepModeStart) +**命令描述**:通知设备启动哄睡模式 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_003", + "command": "SleepModeStart", + "data": { + "storyTheme": "爱情故事" + } +} +``` + +**data字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| storyTheme | String | 故事主题 | + +设备收到此命令后,使用推送的主题调用randomStory接口,播放故事。 + + +#### 4. 哄睡模式结束 (SleepModeEnd) +**命令描述**:通知设备结束哄睡模式 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_004", + "command": "SleepModeEnd", + "data": {} +} +``` + +**data字段说明**:无特殊字段 + +设备收到改命令后,需要停止播放故事。 + + +#### 6. 哄睡模式故事内容 + +设备收到`SleepModeStart` 命令后,需要调用`/v1/story/randomStory`接口获取故事内容。 + + + +#### 7. 设备绑定 (DeviceBind) +**命令描述**:通知设备已被用户绑定 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_007", + "command": "DeviceBind", + "data": { + "userId": "user_001", + "userName": "张三", + "bindTime": 1640995200 + } +} +``` + +**data字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| userId | String | 用户ID | +| userName | String | 用户名称 | +| bindTime | Long | 绑定时间戳,单位:秒 | + +#### 8. 设备解绑 (DeviceUnbind) +**命令描述**:通知设备已被用户解绑 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_008", + "command": "DeviceUnbind", + "data": { + "userId": "user_001", + "reason": "用户主动解绑", + "unbindTime": 1640995200 + } +} +``` + +**data字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| userId | String | 用户ID | +| reason | String | 解绑原因 | +| unbindTime | Long | 解绑时间戳,单位:秒 | + +#### 9. 日程提醒 (ScheduleReminder) +**命令描述**:推送日程提醒信息 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_009", + "command": "ScheduleReminder", + "data": { + "scheduleId": 123, + "deviceId": "DEVICE_001", + "scheduleName": "开会提醒", + "reminderContent": "您有一个会议即将开始,请准备参加", + "reminderAudioUrl": "https://example.com/reminder/reminder_001.mp3", + "enableVoice": 1, + "enableVibration": 1 + } +} +``` + +**data字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| scheduleId | Integer | 日程ID | +| deviceId | String | 设备ID | +| scheduleName | String | 日程名称 | +| reminderContent | String | 提醒内容 | +| reminderAudioUrl | String | 提醒音频文件URL | +| enableVoice | Integer | 是否启用语音,1:启用,0:未启用 | +| enableVibration | Integer | 是否启用震动,1:启用,0:未启用 | + +#### 10. 日程提醒配置 (ScheduleReminderConfig) +**命令描述**:配置日程提醒相关设置 +**消息示例**: +```json +{ + "deviceId": "DEVICE_001", + "timestamp": 1640995200, + "messageId": "msg_010", + "command": "ScheduleReminderConfig", + "data": { + "scheduleName": "七点起床" + } +} +``` + +**data字段说明**: + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| scheduleName | String | 日程名称 | + + + +## 7. 二维码生成接口 + +### 接口描述 +生成二维码图片接口,根据提供的内容生成PNG格式的二维码图片并直接返回图片二进制数据。该接口无需认证。 + +### 请求信息 +- **URL**: `/v1/qrcode/generate/image` +- **方法**: `POST` +- **Content-Type**: `application/json` + +### 请求参数 +| 参数名 | 类型 | 必填 | 描述 | 限制 | 默认值 | +|--------|------|------|------|------|--------| +| content | String | 是 | 二维码内容 | 不能为空 | - | +| width | Integer | 否 | 图片宽度 | 100-1000像素 | 300 | +| height | Integer | 否 | 图片高度 | 100-1000像素 | 300 | +| withLogo | Boolean | 否 | 是否包含Logo | - | false | + +### 请求示例 +```json +{ + "content": "https://example.com/device/bind?deviceId=DEVICE_001", + "width": 300, + "height": 300, + "withLogo": false +} +``` + +### 响应数据 +**成功响应**: +- **Content-Type**: `image/png` +- **Content-Disposition**: `inline; filename=qrcode.png` +- **响应体**: PNG格式的二维码图片二进制数据 + +**失败响应**: +- **400 Bad Request**: 请求参数无效(如content为空) +- **500 Internal Server Error**: 服务器内部错误 + +### 响应头说明 +| 头字段名 | 值 | 描述 | +|---------|-----|------| +| Content-Type | image/png | 响应内容类型为PNG图片 | +| Content-Length | 图片字节数 | 图片文件大小 | +| Content-Disposition | inline; filename=qrcode.png | 内联显示,文件名为qrcode.png | + +### 参数验证规则 +- **content**: 不能为空或空字符串 +- **width**: 如果提供,必须在100-1000像素范围内,否则使用默认值300 +- **height**: 如果提供,必须在100-1000像素范围内,否则使用默认值300 +- **withLogo**: 可选参数,默认为false + +### 使用场景 +此接口主要用于: +1. 设备绑定:生成包含设备ID的绑定二维码 +2. 设备信息展示:生成设备相关信息的二维码 +3. 通用二维码生成:根据任意文本内容生成二维码 + +### 客户端处理说明 +- 响应为二进制图片数据,可直接显示或保存 +- 建议客户端设置合理的超时时间 +- 可通过Content-Length头获取图片大小信息 + +--- + +## 8. 系统整体时序图 + +### 设备完整生命周期流程 + +```mermaid +sequenceDiagram + participant Device as 设备端 + participant Server as 服务端 + participant User as 用户 + participant MiniProgram as 小程序端 + + Note over Device,MiniProgram: 设备完整生命周期流程 + + %% 设备启动和认证 + rect rgb(240, 248, 255) + Note over Device, Server: 1. 设备启动和认证阶段 + Device->>Device: 设备开机启动 + Device->>Server: 获取设备Token (签名认证) + Server->>Device: 返回JWT Token + Device->>Server: 建立WebSocket连接 + Server->>Device: 连接成功 + end + + %% 设备初始化 + rect rgb(245, 255, 250) + Note over Device, Server: 2. 设备初始化阶段 + Device->>Server: 检查OTA升级 + alt 有升级 + Server->>Device: 返回升级包信息 + Device->>Device: 执行升级 + else 无升级 + Server->>Device: 无升级任务 + end + + Device->>Server: 获取设备角色信息 + Server->>Device: 返回角色配置 + Device->>Device: 应用角色设置 + + Device->>Server: 检查用户绑定状态 + alt 未绑定 + Server->>Device: 返回未绑定 + Device->>Server: 生成绑定二维码 + Server->>Device: 返回二维码图片 + Device->>Device: 显示二维码等待绑定 + else 已绑定 + Server->>Device: 返回用户ID + Device->>Device: 进入正常运行模式 + end + end + + %% 用户交互阶段 + rect rgb(255, 250, 240) + Note over Device, MiniProgram: 3. 用户交互阶段 + + par 语音对话 + User->>Device: 语音输入 + Device->>Server: 语音对话接口 + Server->>Device: 返回回复和意图识别 + Device->>User: 播放回复音频 + + alt 识别到哄睡意图 + Device->>Server: 请求故事接口(SSE) + loop 故事流式推送 + Server->>Device: 推送故事片段 + Device->>User: 播放故事音频 + end + Device->>Server: 结束哄睡模式 + end + + and 小程序控制 + MiniProgram->>Server: 角色切换/哄睡控制 + Server->>Device: WebSocket推送命令 + Device->>Device: 执行相应操作 + + and 系统提醒 + Server->>Device: WebSocket推送日程提醒 + Device->>User: 播放提醒内容 + end + end + + %% 运行维护 + rect rgb(248, 248, 255) + Note over Device, Server: 4. 运行维护阶段 + loop 持续运行 + Device->>Server: 心跳保持 + Server->>Device: 心跳响应 + end + + opt 定期检查升级 + Device->>Server: 检查OTA升级 + Server->>Device: 升级任务响应 + end + end +``` + +## 9. 通用说明 + +### 设备认证流程 +1. 设备首次启动时,使用签名算法调用获取设备Token接口 +2. 获取到Token后,后续所有API调用都需要携带该Token +3. Token过期后,需要重新获取新的Token + +### 安全注意事项 +- 签名密钥和Token密钥必须安全保存,不能泄露 +- 时间戳验证可以防止重放攻击 +- 使用HTTPS协议传输,确保数据安全 +- WebSocket连接建议使用WSS协议确保通信安全 + +### 开发调试 +- 签名算法实现时注意字符串拼接的顺序和分隔符 +- 时间戳必须是毫秒级别 +- WebSocket连接测试时注意Token的有效性 +- 建议实现心跳机制保持连接活跃 + +### 时序图说明 +本文档包含以下时序图: +1. **设备认证流程**:展示设备如何获取访问令牌 +2. **语音对话流程**:展示语音交互的完整过程 +3. **哄睡故事流程**:展示从意图识别到故事播放的完整流程 +4. **设备信息管理**:展示设备角色配置和用户绑定流程 +5. **WebSocket通信**:展示实时通信和命令推送流程 +6. **OTA升级流程**:展示固件升级的完整过程 +7. **系统整体流程**:展示设备完整生命周期的综合时序图 + +每个时序图都详细描述了参与方之间的交互序列,有助于开发者理解系统架构和实现细节。