${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
)
# 公共头文件路径(供其他模块引用)
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)
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("<null>");
- }
- /* -------------------------------------------------------------------------- */
- // 打印完整的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;
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;
}
}
-/* 故事-选择故事(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])
{
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_<ver>.bin */
- char save_path[256];
- snprintf(save_path, sizeof(save_path), LN_OTA_SAVE_DIR "/upgrade_%s.bin", g_ln_last_target_version);
- memset(g_ln_last_saved_path, 0, sizeof(g_ln_last_saved_path));
- strncpy(g_ln_last_saved_path, save_path, sizeof(g_ln_last_saved_path) - 1);
-
- printf("[OTA] download only: version=%s, url=%s -> %s\n", g_ln_last_target_version, g_ln_last_package_url, save_path);
- int dres;
- 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)
{
#include "stdint.h"
#include "errcode.h"
+/* HTTP types for helper function prototypes */
+#include "netstack/http/include/httpclient.h"
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 /* 语音对话流分片接收 */
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
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+// 获取设备角色信息
+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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+/* 读取整个文件到内存,返回缓冲区和长度;需调用者 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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+/* 读取整个文件到内存,返回缓冲区和长度;需调用者 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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+
+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;
+}
--- /dev/null
+#include "ln_api.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#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("<null>");
+ }
+ 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;
+}
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}")
--- /dev/null
+# 设备API接口文档\r
+\r
+测试服务BaseUrl:https://ambybox-api.test.leiniao.com/ambybox-device-api\r
+\r
+## 1. 获取设备Token接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ \r
+ Note over Device, Server: 设备认证流程\r
+ \r
+ Device->>Device: 生成时间戳和随机数\r
+ Device->>Device: 构建签名数据<br/>deviceId:deviceIccid:timestamp:nonce\r
+ Device->>Device: 使用HMAC-SHA256生成签名\r
+ \r
+ Device->>Server: GET /v1/auth/getDeviceToken<br/>Headers: deviceId, deviceIccid, timestamp, nonce, signature\r
+ \r
+ alt 签名验证成功\r
+ Server->>Server: 验证时间戳(5分钟内有效)\r
+ Server->>Server: 验证签名正确性\r
+ Server->>Server: 生成JWT Token\r
+ Server->>Device: 返回设备Token<br/>{"deviceId", "token", "expireTime"}\r
+ \r
+ Note over Device: Token获取成功,缓存Token\r
+ \r
+ else 签名验证失败\r
+ Server->>Device: 返回错误<br/>{"errorCode": "INVALID_SIGNATURE"}\r
+ Note over Device: 认证失败,重新尝试\r
+ end\r
+```\r
+\r
+### 接口描述\r
+设备认证接口,用于获取设备访问令牌。该接口无需认证,但需要进行签名校验。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/auth/getDeviceToken`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceIccid | String | 是 | 设备物联网卡号 | "12345678901234567890" |\r
+| timestamp | String | 是 | 毫秒时间戳 | "1640995200000" |\r
+| nonce | String | 是 | 随机数,用于防重放攻击 | "abc123" |\r
+| signature | String | 是 | 签名,用于验证请求合法性 | "a1b2c3d4..." |\r
+| deviceModel | String | 否 | 设备型号,默认值: "abmybox_1" | "abmybox_1" |\r
+| deviceVersion | String | 否 | 设备固件版本,默认值: "1.0.0" | "1.0.0" |\r
+\r
+### 签名算法\r
+签名使用 HMAC-SHA256 算法生成,具体步骤如下:\r
+\r
+1. **构建签名数据**:将参数按以下格式拼接\r
+ ```\r
+ signData = deviceId + ":" + deviceIccid + ":" + timestamp + ":" + nonce\r
+ ```\r
+\r
+2. **生成签名**:使用预共享密钥对签名数据进行 HMAC-SHA256 加密\r
+ ```\r
+ signature = HMAC-SHA256(signSecret, signData)\r
+ ```\r
+\r
+3. **转换格式**:将签名结果转换为十六进制字符串(小写)\r
+\r
+### 签名验证规则\r
+- 时间戳有效性:请求时间戳与服务器时间差不能超过5分钟\r
+- 签名正确性:使用相同算法生成的签名必须与请求中的签名一致\r
+- 防重放攻击:建议客户端每次请求使用不同的随机数\r
+- 测试签名密钥:vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=\r
+\r
+\r
+### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": {\r
+ "deviceId": "DEVICE_001",\r
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",\r
+ "expireTime": 2592000\r
+ }\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+ "errorCode": "INVALID_SIGNATURE",\r
+ "message": "签名验证失败",\r
+ "data": null\r
+}\r
+```\r
+\r
+### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| deviceId | String | 设备ID |\r
+| token | String | 设备访问令牌(JWT格式) |\r
+| expireTime | Long | 令牌过期时间(秒,默认30天) |\r
+\r
+### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| INVALID_SIGNATURE | 签名验证失败 |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+---\r
+\r
+## 2. 语音对话接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ \r
+ Note over Device, Server: 流式语音对话流程\r
+ \r
+ Device->>Device: 录制用户语音\r
+ Device->>Device: 转换为MP3格式\r
+ \r
+ Device->>Server: POST /v1/speechChat/chatStream<br/>Headers: deviceId, deviceToken, Accept: text/event-stream<br/>Form: audio file\r
+ \r
+ alt Token验证成功\r
+ Server->>Server: 验证设备Token\r
+ Server->>Server: 建立SSE连接\r
+ Server->>Server: 语音识别(ASR)\r
+ Server->>Server: 意图识别\r
+ \r
+ loop 流式响应生成\r
+ Server->>Server: 生成回复片段\r
+ Server->>Server: 文本转语音(TTS)\r
+ Server->>Device: SSE事件: 回复片段<br/>{"audioUrl", "textContent", "intention"}\r
+ Device->>Device: 播放回复音频片段\r
+ Device->>Device: 显示文本片段\r
+ end\r
+ \r
+ Server->>Device: SSE连接关闭(对话完成)\r
+ \r
+ alt 识别到哄睡意图\r
+ Note over Device: intention = "StartSleepMode"\r
+ Device->>Device: 准备进入哄睡模式\r
+ end\r
+ \r
+ else Token验证失败\r
+ Server->>Device: 返回认证错误<br/>{"errorCode": "AUTH_FAILURE"}\r
+ Device->>Device: 重新获取Token\r
+ end\r
+```\r
+\r
+### 接口描述\r
+流式语音对话接口,接收音频文件并通过SSE(Server-Sent Events)流式返回对话结果。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/speechChat/chatStream`\r
+- **方法**: `POST`\r
+- **Content-Type**: `multipart/form-data`\r
+- **响应类型**: `text/event-stream` (SSE流式响应)\r
+\r
+### 认证要求\r
+该接口需要设备Token认证,有以下两种方式传递Token:\r
+\r
+1. **请求头方式**:\r
+ ```\r
+ deviceToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+ ```\r
+\r
+2. **查询参数方式**:\r
+ ```\r
+ /v1/speechChat/chatStream?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+ ```\r
+\r
+### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" |\r
+| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" |\r
+\r
+### 请求参数\r
+| 参数名 | 类型 | 必填 | 描述 | 限制 |\r
+|--------|------|------|------|------|\r
+| file | MultipartFile | 是 | 音频文件 | MP3格式,压缩前应为采样率16k或8k单声道的PCM |\r
+\r
+### Token认证规则\r
+- Token必须是有效的JWT格式\r
+- Token必须包含设备ID、设备型号、设备版本等信息\r
+- Token不能过期(默认有效期30天)\r
+- 请求头中的设备ID不能为空\r
+- 设备ID必须与Token中的设备ID一致\r
+\r
+### SSE流式响应数据\r
+**回复片段事件**:\r
+```\r
+data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_001.mp3","textContent":"好的,","intention":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_002.mp3","textContent":"我来为你","intention":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_003.mp3","textContent":"讲一个故事","intention":"StartSleepMode"}}\r
+```\r
+\r
+**失败响应(认证失败)**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+**失败响应(服务异常)**:\r
+```json\r
+{\r
+ "errorCode": "SERVER_ERROR",\r
+ "message": "服务器内部错误",\r
+ "data": null\r
+}\r
+```\r
+\r
+### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| audioUrl | String | 回复音频片段文件URL |\r
+| textContent | String | 回复的文本片段内容 |\r
+| intention | String | 用户意图识别结果,通常在第一个片段中返回,可能的值:StartSleepMode(哄我睡觉)等 |\r
+\r
+### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+### SSE协议说明\r
+- 接口使用SSE(Server-Sent Events)协议进行流式响应\r
+- 每个回复片段作为一个独立的SSE事件发送\r
+- 客户端需要实现SSE客户端来接收流式数据\r
+- 连接会在对话生成完成后自动关闭\r
+- 客户端应当设置适当的超时时间(建议60秒)\r
+\r
+### 音频数据处理说明\r
+- 返回的 `audioUrl` 字段是音频片段文件URL\r
+- 音频格式通常为MP3格式\r
+- 客户端需要将音频文件URL下载后播放\r
+- 建议客户端实现音频片段的缓存和顺序播放机制\r
+\r
+### 用户意图说明\r
+流式语音对话接口会识别用户的意图,目前支持的意图包括:\r
+- **StartSleepMode**:用户表达想要听故事或哄睡的意图\r
+- 更多意图类型会在后续版本中添加\r
+\r
+用户意图通常在最后一个或几个SSE事件中返回,客户端应当:\r
+- 监听所有SSE事件中的intention字段\r
+- 当收到非null的intention时,根据意图类型进行相应处理\r
+- 收到StartSleepMode意图时,可以调用故事相关的API接口\r
+\r
+### 使用说明\r
+- 该接口适用于需要实时响应的语音对话场景\r
+- 客户端可以边接收边播放,实现流式播放体验\r
+- 相比普通语音对话接口,流式接口能提供更自然的交互体验\r
+- 建议在网络状况良好的环境下使用,以确保音频片段的连续播放\r
+- 对话生成完成后,SSE连接会自动关闭\r
+\r
+\r
+\r
+## 3. 故事相关接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ \r
+ Note over Device, Server: 哄睡故事完整流程\r
+ \r
+ %% 意图触发\r
+ Device->>Server: 语音对话识别到哄睡意图<br/>intention = "StartSleepMode"\r
+ \r
+ %% 故事选择分支\r
+ alt 用户指定故事类型\r
+ Device->>Device: 录制用户偏好语音\r
+ Device->>Server: POST /v1/story/chooseStory<br/>Headers: Accept: text/event-stream<br/>Form: audio file\r
+ else 随机故事\r
+ Device->>Server: POST /v1/story/randomStory<br/>Headers: Accept: text/event-stream<br/>Body: {"storyTheme": "可选主题"}\r
+ end\r
+ \r
+ %% SSE流式响应\r
+ Server->>Server: 建立SSE连接\r
+ Server->>Server: 分析语音/生成故事主题\r
+ Server->>Server: 开始生成故事片段\r
+ \r
+ loop 故事片段流式推送\r
+ Server->>Server: 生成故事片段文本\r
+ Server->>Server: 文本转语音\r
+ Server->>Device: SSE事件: 故事片段<br/>{"storyContent", "storyAudioUrl", "storyTheme"}\r
+ Device->>Device: 播放音频片段\r
+ Device->>Device: 显示文本内容\r
+ end\r
+ \r
+ Server->>Device: SSE连接关闭(故事完成)\r
+ \r
+ %% 退出确认流程\r
+ Device->>Server: POST /v1/story/getExitSleepInquiry<br/>获取退出询问\r
+ Server->>Device: 返回退出询问<br/>{"audioContent", "textContent"}\r
+ Device->>Device: 播放退出询问音频\r
+ \r
+ Device->>Device: 等待用户确认\r
+ alt 用户确认退出\r
+ Device->>Server: POST /v1/story/finishSleepMode<br/>确认结束哄睡\r
+ Server->>Device: 返回成功响应\r
+ Device->>Device: 退出哄睡模式\r
+ else 用户继续\r
+ Note over Device: 继续哄睡模式或重新开始故事\r
+ end\r
+```\r
+\r
+### 3.1 智能选择故事\r
+\r
+#### 接口描述\r
+智能选择故事接口。用户通过语音表达故事类型偏好,系统会识别故事主题并流式返回相应的故事内容。该接口使用SSE(Server-Sent Events)协议进行流式响应,需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/chooseStory`\r
+- **方法**: `POST`\r
+- **Content-Type**: `multipart/form-data`\r
+- **响应类型**: `text/event-stream` (SSE流式响应)\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" |\r
+| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" |\r
+\r
+#### 请求表单参数\r
+| 参数名 | 类型 | 必填 | 描述 | 限制 |\r
+|--------|------|------|------|------|\r
+| file | MultipartFile | 是 | 语音文件 | MP3格式,压缩前应为采样率16k或8k单声道的PCM |\r
+\r
+\r
+#### SSE流式响应数据\r
+**故事片段事件**:\r
+```\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"从前有一个可爱的小女孩","storyAudioUrl":"https://example.com/story/audio/chunk_001.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"她住在森林深处的小木屋里","storyAudioUrl":"https://example.com/story/audio/chunk_002.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"每天都会去采集美丽的花朵","storyAudioUrl":"https://example.com/story/audio/chunk_003.mp3","storyDuration":null}}\r
+```\r
+\r
+**错误响应(认证失败)**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+**错误响应(故事生成失败)**:\r
+```json\r
+{\r
+ "errorCode": "SERVER_ERROR",\r
+ "message": "故事生成失败",\r
+ "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| storyTheme | String | 故事主题 |\r
+| storyContent | String | 故事片段文本内容 |\r
+| storyAudioUrl | String | 故事片段音频文件URL |\r
+| storyDuration | Integer | 故事片段时长(秒),流式响应中通常为null |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### SSE协议说明\r
+- 接口使用SSE(Server-Sent Events)协议进行流式响应\r
+- 每个故事片段作为一个独立的SSE事件发送\r
+- 客户端需要实现SSE客户端来接收流式数据\r
+- 连接会在故事生成完成后自动关闭\r
+- 客户端应当设置适当的超时时间(建议120秒)\r
+\r
+#### 使用说明\r
+- 用户可以通过语音表达故事偏好,如"我想听童话故事"、"讲个冒险故事吧"等\r
+- 系统会分析语音内容,识别故事主题,并实时生成故事片段\r
+- 每个故事片段包含文本内容和对应的音频URL\r
+- 客户端可以边接收边播放,实现流式播放体验\r
+- 故事生成完成后,SSE连接会自动关闭\r
+\r
+### 3.2 随机故事\r
+\r
+#### 接口描述\r
+随机故事接口。系统会随机生成一个故事并流式返回给用户。该接口使用SSE(Server-Sent Events)协议进行流式响应,需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/randomStory`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+- **响应类型**: `text/event-stream` (SSE流式响应)\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" |\r
+| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" |\r
+\r
+\r
+#### 请求体参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| storyTheme | String | 否 | 故事主题 | "爱情故事" |\r
+\r
+\r
+示例:\r
+```json\r
+{\r
+ "storyTheme": "爱情故事"\r
+}\r
+```\r
+\r
+\r
+#### SSE流式响应数据\r
+**故事片段事件**:\r
+```\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"在一个阳光明媚的早晨","storyAudioUrl":"https://example.com/story/audio/chunk_001.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"小兔子决定去探险","storyAudioUrl":"https://example.com/story/audio/chunk_002.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"它背上小书包,踏上了未知的旅程","storyAudioUrl":"https://example.com/story/audio/chunk_003.mp3","storyDuration":null}}\r
+```\r
+\r
+**错误响应(认证失败)**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+**错误响应(故事生成失败)**:\r
+```json\r
+{\r
+ "errorCode": "SERVER_ERROR",\r
+ "message": "故事生成失败",\r
+ "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| storyTheme | String | 故事主题 |\r
+| storyContent | String | 故事片段文本内容 |\r
+| storyAudioUrl | String | 故事片段音频文件URL |\r
+| storyDuration | Integer | 故事片段时长(秒),流式响应中通常为null |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### SSE协议说明\r
+- 接口使用SSE(Server-Sent Events)协议进行流式响应\r
+- 每个故事片段作为一个独立的SSE事件发送\r
+- 客户端需要实现SSE客户端来接收流式数据\r
+- 连接会在故事生成完成后自动关闭\r
+- 客户端应当设置适当的超时时间(建议120秒)\r
+\r
+#### 使用说明\r
+- 该接口不需要任何参数,系统会自动生成随机主题的故事\r
+- 适用于用户没有特定偏好,想要听任意故事的场景\r
+- 可以作为默认的故事推荐方式\r
+- 客户端可以边接收边播放,实现流式播放体验\r
+- 故事生成完成后,SSE连接会自动关闭\r
+\r
+### 3.3 获取退出哄睡询问\r
+\r
+#### 接口描述\r
+获取退出哄睡询问接口。在用户要结束哄睡前,返回询问内容用于确认用户是否真的要退出哄睡模式。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/getExitSleepInquiry`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 请求参数\r
+无请求参数\r
+\r
+#### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": {\r
+ "audioUrl": "https://example.com/audio/response.mp3",\r
+ "textContent": "你确定要退出哄睡模式吗?如果确定要退出,请说确定",\r
+ "intention": "EXIT_SLEEP_MODE"\r
+ }\r
+}\r
+```\r
+\r
+**失败响应(认证失败)**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| audioUrl | String | 询问音频文件URL |\r
+| textContent | String | 询问的文本内容 |\r
+| intention | String | 用户意图类型,此接口固定返回"EXIT_SLEEP_MODE" |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### 使用说明\r
+- 该接口用于获取退出哄睡模式的确认询问\r
+- 返回的音频和文本内容用于向用户确认是否真的要退出哄睡模式\r
+- 设备播放询问内容后,应等待用户的确认回复\r
+- 如果用户确认退出,再调用`finishSleepMode`接口正式结束哄睡模式\r
+\r
+### 3.4 确认结束哄睡模式\r
+\r
+#### 接口描述\r
+结束哄睡模式接口。当用户完成哄睡故事播放或主动结束哄睡模式时,调用此接口通知服务器。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/finishSleepMode`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 请求参数\r
+无请求参数\r
+\r
+#### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": ""\r
+}\r
+```\r
+\r
+**失败响应(认证失败)**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| data | String | |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### 使用说明\r
+- 该接口用于通知服务器哄睡模式已结束\r
+- 服务器可以记录哄睡会话的结束时间和统计信息\r
+- 调用此接口后,服务器可能会停止推送哄睡相关的消息\r
+- 建议在故事播放完成或用户主动停止时调用此接口\r
+\r
+### 故事接口使用流程\r
+1. **意图识别**:用户通过语音对话接口表达哄我睡觉的意图,系统返回`StartSleepMode`意图\r
+2. **故事选择**:\r
+ - 如果用户有特定偏好,调用`chooseStory`接口,传入用户的语音描述,获取对应主题的故事流\r
+ - 如果用户没有特定偏好,调用`randomStory`接口获取随机故事流\r
+3. **SSE连接建立**:客户端建立SSE连接,准备接收流式故事数据\r
+4. **流式故事播放**:\r
+ - 接收到每个故事片段事件后,立即播放对应的音频文件\r
+ - 可以同时显示文本内容,实现边听边看的体验\r
+ - 继续接收后续故事片段,直到故事完整播放完毕\r
+5. **连接关闭**:故事生成完成后,SSE连接自动关闭\r
+6. **退出询问**:故事播放完成或用户主动结束时,调用`getExitSleepInquiry`获取二次确认询问\r
+7. **播放询问**:设备播放退出询问的音频内容,等待用户确认\r
+8. **确认结束**:用户确认后,调用`finishSleepMode`接口通知服务器正式结束哄睡模式\r
+\r
+### SSE客户端实现要点\r
+- **请求头设置**:必须设置`Accept: text/event-stream`和`Cache-Control: no-cache`\r
+- **连接超时**:建议设置120秒超时时间,因为故事生成可能需要较长时间\r
+- **事件解析**:正确解析SSE事件格式,提取`data:`字段中的JSON数据\r
+- **错误处理**:处理连接失败、超时、认证失败等异常情况\r
+- **资源管理**:连接完成后正确释放资源,避免内存泄漏\r
+\r
+\r
+---\r
+\r
+## 4. 设备信息接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ \r
+ Note over Device, Server: 设备信息管理流程\r
+ \r
+ %% 获取设备角色信息\r
+ Device->>Server: GET /v1/device/getCharacter<br/>Headers: deviceId, deviceToken\r
+ \r
+ alt 首次开机\r
+ Server->>Device: 返回默认角色信息<br/>{"characterId", "characterName", "characterAvatar", "emotionEmojis"}\r
+ Device->>Device: 应用默认角色配置\r
+ else 已有角色配置\r
+ Server->>Device: 返回用户选择的角色信息\r
+ Device->>Device: 应用角色配置\r
+ end\r
+ \r
+ %% 检查用户绑定状态\r
+ Device->>Server: GET /v1/device/getUserId<br/>获取绑定用户ID\r
+ \r
+ alt 设备已绑定用户\r
+ Server->>Device: 返回用户ID<br/>{"data": "user_001"}\r
+ Device->>Device: 正常运行模式\r
+ else 设备未绑定\r
+ Server->>Device: 返回空值<br/>{"data": null}\r
+ Device->>Device: 显示绑定二维码\r
+ \r
+ %% 生成绑定二维码\r
+ Device->>Server: POST /v1/qrcode/generate/image<br/>{"content": "绑定链接", "width": 300, "height": 300}\r
+ Server->>Device: 返回二维码图片(PNG)\r
+ Device->>Device: 显示二维码等待绑定\r
+ end\r
+```\r
+\r
+### 4.1 获取设备角色信息\r
+\r
+#### 接口描述\r
+获取设备角色信息接口。首次开机时返回默认角色,否则返回小程序选择的角色信息。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/device/getCharacter`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": {\r
+ "characterId": "char_001",\r
+ "characterName": "小艾",\r
+ "characterAvatar": {\r
+ "hairColor": "orange",\r
+ "eyesColor": "blue",\r
+ "hairStyle": "long-hair",\r
+ "screenImage": "https://example.com/avatar.jpg",\r
+ "screenImageAsEmoji": false\r
+ },\r
+ "emotionEmojis": [\r
+ {\r
+ "emotion": "happy",\r
+ "emojiUrl": "https://example.com/emoji/happy.png"\r
+ },\r
+ {\r
+ "emotion": "sad",\r
+ "emojiUrl": "https://example.com/emoji/sad.png"\r
+ }\r
+ ]\r
+ }\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| characterId | String | 角色ID |\r
+| characterName | String | 角色名称 |\r
+| characterAvatar | Object | 角色形象配置 |\r
+| emotionEmojis | Array | 角色表情数组 |\r
+\r
+**characterAvatar字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 | 可选值 |\r
+|--------|------|------|--------|\r
+| hairColor | String | 发色 | orange, red, purple, green, blue, black |\r
+| eyesColor | String | 瞳色 | orange, red, purple, green, blue, yellow |\r
+| hairStyle | String | 发型 | side-parted, slicked-back, long-hair |\r
+| screenImage | String | 屏保图片URL | |\r
+| screenImageAsEmoji | Boolean | 屏保图片是否作为对话表情 | 默认false |\r
+\r
+**emotionEmoji字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| emotion | String | 心情名称 |\r
+| emojiUrl | String | 表情URL |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+\r
+### 4.2 获取设备绑定用户ID\r
+\r
+#### 接口描述\r
+获取设备绑定的用户ID接口。用于检查设备是否已绑定用户,如果未绑定则需要弹出二维码供用户绑定。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/device/getUserId`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 响应数据\r
+**成功响应(已绑定用户)**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": "user_001"\r
+}\r
+```\r
+\r
+**成功响应(未绑定用户)**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": null\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+ "errorCode": "AUTH_FAILURE",\r
+ "message": "设备Token无效",\r
+ "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| data | String | 用户ID,如果设备未绑定用户则返回null |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+\r
+---\r
+\r
+## 5. OTA升级任务接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ \r
+ Note over Device, Server: OTA升级流程\r
+ \r
+ %% 设备启动检查升级\r
+ Device->>Device: 设备启动\r
+ Device->>Device: 获取当前固件版本\r
+ \r
+ Device->>Server: GET /v1/upgrade/getTask<br/>?deviceId=xxx&sourceVersion=1.0.0\r
+ \r
+ alt 有可用升级\r
+ Server->>Server: 检查升级策略\r
+ Server->>Server: 查找可用升级包\r
+ Server->>Device: 返回升级任务<br/>{"updateId", "targetVersion", "packageUrl", "packageMd5", "updateType"}\r
+ \r
+ Device->>Device: 检查升级类型\r
+ \r
+ alt 强制升级 (updateType=1)\r
+ Device->>Device: 立即开始升级\r
+ else 可选升级 (updateType=0)\r
+ Device->>Device: 提示用户选择\r
+ alt 用户同意升级\r
+ Device->>Device: 开始升级\r
+ else 用户拒绝\r
+ Device->>Device: 继续正常运行\r
+ end\r
+ end\r
+ \r
+ %% 执行升级\r
+ opt 开始升级流程\r
+ Device->>Device: 下载升级包\r
+ Device->>Server: 下载 packageUrl\r
+ Server->>Device: 返回升级包文件\r
+ \r
+ Device->>Device: 校验MD5\r
+ alt MD5校验成功\r
+ Device->>Device: 执行固件升级\r
+ Device->>Device: 重启设备\r
+ Device->>Device: 升级完成,版本更新\r
+ Device->>Server: 重新获取设备Token<br/>(新版本)\r
+ else MD5校验失败\r
+ Device->>Device: 升级失败,重新下载\r
+ end\r
+ end\r
+ \r
+ else 无可用升级\r
+ Server->>Device: 返回无升级<br/>{"errorCode": "UPGRADE_NOT_FOUND"}\r
+ Device->>Device: 继续正常运行\r
+ end\r
+```\r
+\r
+### 接口描述\r
+获取OTA升级任务接口。设备可通过此接口查询是否有可用的固件升级任务,如果有则返回升级包的详细信息。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/upgrade/getTask`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+### 请求参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| sourceVersion | String | 是 | 设备当前固件版本 | "1.0.0" |\r
+\r
+### 响应数据\r
+**成功响应(有升级任务)**:\r
+```json\r
+{\r
+ "errorCode": null,\r
+ "message": null,\r
+ "data": {\r
+ "updateId": 123,\r
+ "sourceVersion": "1.0.0",\r
+ "targetVersion": "1.1.0",\r
+ "updateType": 1,\r
+ "packageId": 456,\r
+ "packageUrl": "https://example.com/firmware/update_1.1.0.bin",\r
+ "packageMd5": "a1b2c3d4e5f6789012345678901234567890",\r
+ "packageSize": 1048576,\r
+ "description": "升级描述"\r
+ }\r
+}\r
+```\r
+\r
+**成功响应(无升级任务)**:\r
+```json\r
+{\r
+ "errorCode": "UPGRADE_NOT_FOUND",\r
+ "message": "已是最新版本",\r
+ "data": null\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+ "errorCode": "SERVER_ERROR",\r
+ "message": "服务器异常",\r
+ "data": null\r
+}\r
+```\r
+\r
+### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| updateId | Integer | 升级任务ID |\r
+| sourceVersion | String | 源版本号 |\r
+| targetVersion | String | 目标版本号 |\r
+| updateType | Integer | 升级类型,1:强制升级,0:可选升级 |\r
+| packageId | Integer | 升级包ID |\r
+| packageUrl | String | 升级包下载URL |\r
+| packageMd5 | String | 升级包MD5校验值 |\r
+| packageSize | Long | 升级包大小(字节) |\r
+| description | String | 升级描述 |\r
+\r
+### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| UPGRADE_NOT_FOUND | 已是最新版本 |\r
+| SERVER_ERROR | 服务器异常 |\r
+\r
+### 升级流程说明\r
+1. 设备启动时,调用此接口检查是否有可用的升级任务\r
+2. 如果有升级任务,获取升级包URL和校验信息\r
+3. 设备下载升级包,使用MD5校验文件完整性\r
+4. 校验通过后,设备执行固件升级操作\r
+5. 升级完成后,设备重启并重新获取设备token\r
+\r
+---\r
+\r
+## 6. WebSocket连接端点\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ participant MiniProgram as 小程序端\r
+ \r
+ Note over Device,MiniProgram: WebSocket通信流程\r
+ \r
+ %% 建立连接\r
+ Device->>Server: WebSocket连接请求<br/>Headers: deviceId, deviceToken\r
+ \r
+ alt Token验证成功\r
+ Server->>Device: 连接成功(101)\r
+ Server->>Server: 记录设备会话\r
+ \r
+ %% 心跳保持\r
+ loop 心跳循环\r
+ Device->>Server: ping\r
+ Server->>Device: pong\r
+ end\r
+ \r
+ %% 业务消息推送示例\r
+ par 角色切换\r
+ MiniProgram->>Server: 用户在小程序切换角色\r
+ Server->>Device: WebSocket推送<br/>{"command": "CharacterChange", "data": {...}}\r
+ Device->>Device: 应用新角色配置\r
+ and 哄睡模式控制\r
+ MiniProgram->>Server: 用户启动哄睡模式\r
+ Server->>Device: WebSocket推送<br/>{"command": "SleepModeStart", "data": {"storyTheme": "..."}}\r
+ Device->>Device: 启动哄睡模式\r
+ and 设备绑定通知\r
+ MiniProgram->>Server: 用户绑定设备\r
+ Server->>Device: WebSocket推送<br/>{"command": "DeviceBind", "data": {"userId": "...", "userName": "..."}}\r
+ Device->>Device: 更新绑定状态\r
+ and 日程提醒\r
+ Server->>Device: WebSocket推送<br/>{"command": "ScheduleReminder", "data": {"scheduleName": "...", "reminderContent": "..."}}\r
+ Device->>Device: 播放提醒内容\r
+ end\r
+ \r
+ else Token验证失败\r
+ Server->>Device: 连接拒绝(401)\r
+ Device->>Device: 重新获取Token后重连\r
+ end\r
+ \r
+ %% 连接断开\r
+ alt 正常断开\r
+ Device->>Server: 关闭连接\r
+ Server->>Server: 清理设备会话\r
+ else 异常断开\r
+ Server->>Server: 检测到连接断开\r
+ Server->>Server: 清理设备会话\r
+ Device->>Device: 自动重连机制\r
+ end\r
+```\r
+\r
+### 接口描述\r
+WebSocket连接端点,用于设备与服务器建立实时双向通信连接。该连接需要设备Token认证。\r
+\r
+### 连接信息\r
+- **URL**: `wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket`\r
+- **协议**: `WebSocket`\r
+- **认证**: 设备Token认证\r
+\r
+### 连接认证\r
+WebSocket连接需要设备Token认证,有以下两种方式传递Token:\r
+\r
+1. **请求头方式**:\r
+ ```\r
+ deviceId: DEVICE_001\r
+ deviceToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+ reconnect: 0\r
+ ```\r
+\r
+2. **查询参数方式**:\r
+ ```\r
+ wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket?deviceToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+ ```\r
+\r
+### 连接参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符(请求头) | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| reconnect | Integer | 否 | 重连次数,设备启动时设置为0,断线重连递增 | 0 |\r
+\r
+### 连接状态码\r
+| 状态码 | 描述 |\r
+|--------|------|\r
+| 400 | 设备ID为空 |\r
+| 401 | 设备Token无效或过期 |\r
+| 101 | 正常连接 |\r
+\r
+\r
+### 连接流程\r
+1. 客户端携带设备Token发起WebSocket连接\r
+2. 服务器验证Token有效性和设备ID匹配性\r
+3. 连接建立成功,设备会话被管理器记录\r
+4. 客户端可发送心跳消息保持连接活跃\r
+5. 连接断开时,设备会话从管理器中移除\r
+\r
+### 错误处理\r
+- **认证失败**:Token无效或设备ID不匹配,连接被拒绝\r
+- **传输错误**:连接出现异常时,服务器会主动关闭连接\r
+- **心跳超时**:长时间无消息交互时,连接可能被服务器关闭\r
+\r
+\r
+\r
+### 客户端心跳消息\r
+**客户端发送心跳**:\r
+```\r
+ping\r
+```\r
+\r
+**服务器响应心跳**:\r
+```\r
+pong\r
+```\r
+\r
+### 服务端业务消息\r
+\r
+服务端通过WebSocket连接推送业务消息给设备端,消息采用JSON格式,消息结构如下:\r
+\r
+```json\r
+{\r
+ "deviceId": "设备ID",\r
+ "timestamp": 1640995200,\r
+ "messageId": "消息ID,用于设备去重防重放",\r
+ "command": "命令名称",\r
+ "data": {\r
+ // 具体的消息数据,根据不同命令名称而变化\r
+ }\r
+}\r
+```\r
+\r
+#### 消息结构说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| deviceId | String | 设备唯一标识符 |\r
+| timestamp | Long | 时间戳,单位:秒 |\r
+| messageId | String | 消息ID,用于设备去重防重放 |\r
+| command | String | 命令名称,见下方支持的命令列表 |\r
+| data | Object | 消息体,不同命令对应不同的数据结构 |\r
+\r
+支持的命令名称如下:\r
+\r
+#### 1. 角色切换 (CharacterChange)\r
+**命令描述**:通知设备切换到指定角色\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_001",\r
+ "command": "CharacterChange",\r
+ "data": {\r
+ "characterId": "char_001",\r
+ "characterName": "小艾",\r
+ "characterAvatar": {\r
+ "hairColor": "orange",\r
+ "eyesColor": "blue",\r
+ "hairStyle": "long-hair",\r
+ "screenImage": "https://example.com/avatar.jpg",\r
+ "screenImageAsEmoji": false\r
+ },\r
+ "emotionEmojis": [\r
+ {\r
+ "emotion": "happy",\r
+ "emojiUrl": "https://example.com/emoji/happy.png"\r
+ },\r
+ {\r
+ "emotion": "sad",\r
+ "emojiUrl": "https://example.com/emoji/sad.png"\r
+ }\r
+ ]\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| characterId | String | 角色ID |\r
+| characterName | String | 角色名称 |\r
+| characterAvatar | Object | 角色形象配置 |\r
+| emotionEmojis | Array | 角色表情数组 |\r
+\r
+**characterAvatar字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 | 可选值 |\r
+|--------|------|------|--------|\r
+| hairColor | String | 发色 | orange, red, purple, green, blue, black |\r
+| eyesColor | String | 瞳色 | orange, red, purple, green, blue, yellow |\r
+| hairStyle | String | 发型 | side-parted, slicked-back, long-hair |\r
+| screenImage | String | 屏保图片URL | |\r
+| screenImageAsEmoji | Boolean | 屏保图片是否作为对话表情 | 默认false |\r
+\r
+**emotionEmoji字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| emotion | String | 心情名称 |\r
+| emojiUrl | String | 表情图片URL |\r
+\r
+#### 2. 角色配置更改 (CharacterConfigChange)\r
+**命令描述**:更新当前角色的配置信息\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_002",\r
+ "command": "CharacterConfigChange",\r
+ "data": {\r
+ "characterId": "char_001",\r
+ "characterName": "小艾",\r
+ "characterAvatar": {\r
+ "hairColor": "blue",\r
+ "eyesColor": "green",\r
+ "hairStyle": "side-parted",\r
+ "screenImage": "https://example.com/new_avatar.jpg",\r
+ "screenImageAsEmoji": true\r
+ },\r
+ "emotionEmojis": [\r
+ {\r
+ "emotion": "excited",\r
+ "emojiUrl": "https://example.com/emoji/excited.png"\r
+ }\r
+ ]\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:与角色切换命令相同\r
+\r
+#### 3. 哄睡模式开始 (SleepModeStart)\r
+**命令描述**:通知设备启动哄睡模式\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_003",\r
+ "command": "SleepModeStart",\r
+ "data": {\r
+ "storyTheme": "爱情故事"\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| storyTheme | String | 故事主题 |\r
+\r
+设备收到此命令后,使用推送的主题调用randomStory接口,播放故事。\r
+\r
+\r
+#### 4. 哄睡模式结束 (SleepModeEnd)\r
+**命令描述**:通知设备结束哄睡模式\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_004",\r
+ "command": "SleepModeEnd",\r
+ "data": {}\r
+}\r
+```\r
+\r
+**data字段说明**:无特殊字段\r
+\r
+设备收到改命令后,需要停止播放故事。\r
+\r
+\r
+#### 6. 哄睡模式故事内容\r
+\r
+设备收到`SleepModeStart` 命令后,需要调用`/v1/story/randomStory`接口获取故事内容。\r
+\r
+\r
+\r
+#### 7. 设备绑定 (DeviceBind)\r
+**命令描述**:通知设备已被用户绑定\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_007",\r
+ "command": "DeviceBind",\r
+ "data": {\r
+ "userId": "user_001",\r
+ "userName": "张三",\r
+ "bindTime": 1640995200\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| userId | String | 用户ID |\r
+| userName | String | 用户名称 |\r
+| bindTime | Long | 绑定时间戳,单位:秒 |\r
+\r
+#### 8. 设备解绑 (DeviceUnbind)\r
+**命令描述**:通知设备已被用户解绑\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_008",\r
+ "command": "DeviceUnbind",\r
+ "data": {\r
+ "userId": "user_001",\r
+ "reason": "用户主动解绑",\r
+ "unbindTime": 1640995200\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| userId | String | 用户ID |\r
+| reason | String | 解绑原因 |\r
+| unbindTime | Long | 解绑时间戳,单位:秒 |\r
+\r
+#### 9. 日程提醒 (ScheduleReminder)\r
+**命令描述**:推送日程提醒信息\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_009",\r
+ "command": "ScheduleReminder",\r
+ "data": {\r
+ "scheduleId": 123,\r
+ "deviceId": "DEVICE_001",\r
+ "scheduleName": "开会提醒",\r
+ "reminderContent": "您有一个会议即将开始,请准备参加",\r
+ "reminderAudioUrl": "https://example.com/reminder/reminder_001.mp3",\r
+ "enableVoice": 1,\r
+ "enableVibration": 1\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| scheduleId | Integer | 日程ID |\r
+| deviceId | String | 设备ID |\r
+| scheduleName | String | 日程名称 |\r
+| reminderContent | String | 提醒内容 |\r
+| reminderAudioUrl | String | 提醒音频文件URL |\r
+| enableVoice | Integer | 是否启用语音,1:启用,0:未启用 |\r
+| enableVibration | Integer | 是否启用震动,1:启用,0:未启用 |\r
+\r
+#### 10. 日程提醒配置 (ScheduleReminderConfig)\r
+**命令描述**:配置日程提醒相关设置\r
+**消息示例**:\r
+```json\r
+{\r
+ "deviceId": "DEVICE_001",\r
+ "timestamp": 1640995200,\r
+ "messageId": "msg_010",\r
+ "command": "ScheduleReminderConfig",\r
+ "data": {\r
+ "scheduleName": "七点起床"\r
+ }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| scheduleName | String | 日程名称 |\r
+\r
+\r
+\r
+## 7. 二维码生成接口\r
+\r
+### 接口描述\r
+生成二维码图片接口,根据提供的内容生成PNG格式的二维码图片并直接返回图片二进制数据。该接口无需认证。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/qrcode/generate/image`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+\r
+### 请求参数\r
+| 参数名 | 类型 | 必填 | 描述 | 限制 | 默认值 |\r
+|--------|------|------|------|------|--------|\r
+| content | String | 是 | 二维码内容 | 不能为空 | - |\r
+| width | Integer | 否 | 图片宽度 | 100-1000像素 | 300 |\r
+| height | Integer | 否 | 图片高度 | 100-1000像素 | 300 |\r
+| withLogo | Boolean | 否 | 是否包含Logo | - | false |\r
+\r
+### 请求示例\r
+```json\r
+{\r
+ "content": "https://example.com/device/bind?deviceId=DEVICE_001",\r
+ "width": 300,\r
+ "height": 300,\r
+ "withLogo": false\r
+}\r
+```\r
+\r
+### 响应数据\r
+**成功响应**:\r
+- **Content-Type**: `image/png`\r
+- **Content-Disposition**: `inline; filename=qrcode.png`\r
+- **响应体**: PNG格式的二维码图片二进制数据\r
+\r
+**失败响应**:\r
+- **400 Bad Request**: 请求参数无效(如content为空)\r
+- **500 Internal Server Error**: 服务器内部错误\r
+\r
+### 响应头说明\r
+| 头字段名 | 值 | 描述 |\r
+|---------|-----|------|\r
+| Content-Type | image/png | 响应内容类型为PNG图片 |\r
+| Content-Length | 图片字节数 | 图片文件大小 |\r
+| Content-Disposition | inline; filename=qrcode.png | 内联显示,文件名为qrcode.png |\r
+\r
+### 参数验证规则\r
+- **content**: 不能为空或空字符串\r
+- **width**: 如果提供,必须在100-1000像素范围内,否则使用默认值300\r
+- **height**: 如果提供,必须在100-1000像素范围内,否则使用默认值300\r
+- **withLogo**: 可选参数,默认为false\r
+\r
+### 使用场景\r
+此接口主要用于:\r
+1. 设备绑定:生成包含设备ID的绑定二维码\r
+2. 设备信息展示:生成设备相关信息的二维码\r
+3. 通用二维码生成:根据任意文本内容生成二维码\r
+\r
+### 客户端处理说明\r
+- 响应为二进制图片数据,可直接显示或保存\r
+- 建议客户端设置合理的超时时间\r
+- 可通过Content-Length头获取图片大小信息\r
+\r
+---\r
+\r
+## 8. 系统整体时序图\r
+\r
+### 设备完整生命周期流程\r
+\r
+```mermaid\r
+sequenceDiagram\r
+ participant Device as 设备端\r
+ participant Server as 服务端\r
+ participant User as 用户\r
+ participant MiniProgram as 小程序端\r
+ \r
+ Note over Device,MiniProgram: 设备完整生命周期流程\r
+ \r
+ %% 设备启动和认证\r
+ rect rgb(240, 248, 255)\r
+ Note over Device, Server: 1. 设备启动和认证阶段\r
+ Device->>Device: 设备开机启动\r
+ Device->>Server: 获取设备Token (签名认证)\r
+ Server->>Device: 返回JWT Token\r
+ Device->>Server: 建立WebSocket连接\r
+ Server->>Device: 连接成功\r
+ end\r
+ \r
+ %% 设备初始化\r
+ rect rgb(245, 255, 250)\r
+ Note over Device, Server: 2. 设备初始化阶段\r
+ Device->>Server: 检查OTA升级\r
+ alt 有升级\r
+ Server->>Device: 返回升级包信息\r
+ Device->>Device: 执行升级\r
+ else 无升级\r
+ Server->>Device: 无升级任务\r
+ end\r
+ \r
+ Device->>Server: 获取设备角色信息\r
+ Server->>Device: 返回角色配置\r
+ Device->>Device: 应用角色设置\r
+ \r
+ Device->>Server: 检查用户绑定状态\r
+ alt 未绑定\r
+ Server->>Device: 返回未绑定\r
+ Device->>Server: 生成绑定二维码\r
+ Server->>Device: 返回二维码图片\r
+ Device->>Device: 显示二维码等待绑定\r
+ else 已绑定\r
+ Server->>Device: 返回用户ID\r
+ Device->>Device: 进入正常运行模式\r
+ end\r
+ end\r
+ \r
+ %% 用户交互阶段\r
+ rect rgb(255, 250, 240)\r
+ Note over Device, MiniProgram: 3. 用户交互阶段\r
+ \r
+ par 语音对话\r
+ User->>Device: 语音输入\r
+ Device->>Server: 语音对话接口\r
+ Server->>Device: 返回回复和意图识别\r
+ Device->>User: 播放回复音频\r
+ \r
+ alt 识别到哄睡意图\r
+ Device->>Server: 请求故事接口(SSE)\r
+ loop 故事流式推送\r
+ Server->>Device: 推送故事片段\r
+ Device->>User: 播放故事音频\r
+ end\r
+ Device->>Server: 结束哄睡模式\r
+ end\r
+ \r
+ and 小程序控制\r
+ MiniProgram->>Server: 角色切换/哄睡控制\r
+ Server->>Device: WebSocket推送命令\r
+ Device->>Device: 执行相应操作\r
+ \r
+ and 系统提醒\r
+ Server->>Device: WebSocket推送日程提醒\r
+ Device->>User: 播放提醒内容\r
+ end\r
+ end\r
+ \r
+ %% 运行维护\r
+ rect rgb(248, 248, 255)\r
+ Note over Device, Server: 4. 运行维护阶段\r
+ loop 持续运行\r
+ Device->>Server: 心跳保持\r
+ Server->>Device: 心跳响应\r
+ end\r
+ \r
+ opt 定期检查升级\r
+ Device->>Server: 检查OTA升级\r
+ Server->>Device: 升级任务响应\r
+ end\r
+ end\r
+```\r
+\r
+## 9. 通用说明\r
+\r
+### 设备认证流程\r
+1. 设备首次启动时,使用签名算法调用获取设备Token接口\r
+2. 获取到Token后,后续所有API调用都需要携带该Token\r
+3. Token过期后,需要重新获取新的Token\r
+\r
+### 安全注意事项\r
+- 签名密钥和Token密钥必须安全保存,不能泄露\r
+- 时间戳验证可以防止重放攻击\r
+- 使用HTTPS协议传输,确保数据安全\r
+- WebSocket连接建议使用WSS协议确保通信安全\r
+\r
+### 开发调试\r
+- 签名算法实现时注意字符串拼接的顺序和分隔符\r
+- 时间戳必须是毫秒级别\r
+- WebSocket连接测试时注意Token的有效性\r
+- 建议实现心跳机制保持连接活跃\r
+\r
+### 时序图说明\r
+本文档包含以下时序图:\r
+1. **设备认证流程**:展示设备如何获取访问令牌\r
+2. **语音对话流程**:展示语音交互的完整过程\r
+3. **哄睡故事流程**:展示从意图识别到故事播放的完整流程\r
+4. **设备信息管理**:展示设备角色配置和用户绑定流程\r
+5. **WebSocket通信**:展示实时通信和命令推送流程\r
+6. **OTA升级流程**:展示固件升级的完整过程\r
+7. **系统整体流程**:展示设备完整生命周期的综合时序图\r
+\r
+每个时序图都详细描述了参与方之间的交互序列,有助于开发者理解系统架构和实现细节。\r