{
VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
printf("[LN][AUDIO_PAUSE]\n");
- do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_PAUSE, NULL, 0);
+ // do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_PAUSE, NULL, 0);
+ uapi_ln_audio_download_is_cancel(1);
return ERRCODE_SUCC;
}
VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
printf("[LN][AUDIO_RESUME]\n");
do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_RESUME, NULL, 0);
+ uapi_ln_audio_download_is_cancel(0);
return ERRCODE_SUCC;
}
return g_ln_device_user_id;
}
+/* 获取设备角色id */
+const char *uapi_ln_get_character_id(void)
+{
+ return g_ln_character_id;
+}
+
+/* 获取设备角色名称 */
+const char *uapi_ln_get_character_name(void)
+{
+ return g_ln_character_name;
+}
+
+/* 获取设备角色眼睛形状 */
+const char *uapi_ln_get_character_eyes_shape(void)
+{
+ return g_ln_avatar_eyes_shape;
+}
+
+/* 获取设备角色头发颜色 */
+const char *uapi_ln_get_character_hair_color(void)
+{
+ return g_ln_avatar_hair_color;
+}
+
+/* 获取设备角色眼睛颜色 */
+const char *uapi_ln_get_character_eyes_color(void)
+{
+ return g_ln_avatar_eyes_color;
+}
+
+/* 获取设备角色头发样式 */
+const char *uapi_ln_get_character_hair_style(void)
+{
+ return g_ln_avatar_hair_style;
+}
+
+/* 获取设备角色屏幕图片 */
+const char *uapi_ln_get_character_screen_image(void)
+{
+ return g_ln_avatar_screen_image;
+}
+
+/* 获取设备角色屏幕图片是否为表情,默认false */
+int uapi_ln_get_character_screen_image_as_emoji(void)
+{
+ return g_ln_avatar_screen_image_as_emoji;
+}
+
/* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */
HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size)
{
mbedtls_md5_free(&ctx);
return -1;
}
- unsigned char buf[4096];
+ /* 使用堆缓冲,避免在调用栈上占用4KB,降低线程峰值栈使用 */
+ const size_t kBufSize = 4096;
+ unsigned char *buf = (unsigned char *)malloc(kBufSize);
+ if (!buf)
+ {
+ fclose(fp);
+ mbedtls_md5_free(&ctx);
+ return -1;
+ }
size_t n;
- while ((n = fread(buf, 1, sizeof(buf), fp)) > 0)
+ while ((n = fread(buf, 1, kBufSize, fp)) > 0)
{
if (mbedtls_md5_update(&ctx, buf, n) != 0)
{
fclose(fp);
+ free(buf);
mbedtls_md5_free(&ctx);
return -1;
}
}
fclose(fp);
+ free(buf);
if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0)
{
mbedtls_md5_free(&ctx);
do
{
if (g_ln_audio_download_cancel) {
- printf("[SSE] download canceled by user\n");
+ printf("download canceled by user\n");
break;
}
HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &client_data);
(void)ln_audio_player_write((const uint8_t*)client_data.responseBuf, (uint32_t)client_data.contentBlockLen);
}
+ else
+ {
+ /* EAGAIN 或 chunk 分界但无数据时,让出CPU避免忙等 */
+ LOS_Msleep(10);
+ }
}
else
{
int success_count = 0;
int error_count = 0;
+ if(g_ln_audio_download_cancel)//如果之前中断了播放,新的对话就恢复播放
+ {
+ uapi_ln_audio_download_is_cancel(0);
+ }
+
while (fgets(line, sizeof(line), fp))
{
if (g_ln_audio_download_cancel) {
while (1)
{
+ /* 每轮重置 msgsize,兼容底层可能改写该值的实现 */
+ msgsize = sizeof(ln_http_data_mail_t);
ret = osal_msg_queue_read_copy(g_ln_send_queue, (void *)&mail, &msgsize,
OSAL_MSGQ_WAIT_FOREVER);
if (ret != OSAL_SUCCESS)
{
+ LOS_Msleep(100);
continue;
}
if (mail.cmd == LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN)
while (1)
{
+ /* 某些OSAL实现会在read后改写msgsize,循环开始时重置,避免下次读取异常 */
+ msgsize = sizeof(ln_http_data_mail_t);
ret = osal_msg_queue_read_copy(g_ln_recv_queue, (void *)&mail, &msgsize,
OSAL_MSGQ_WAIT_FOREVER);
if (ret != OSAL_SUCCESS)
{
+ printf("[LN][RECV] queue read error=0x%x\n", ret);
+ LOS_Msleep(100);
continue;
}
tag = "STORY_FINISH";
} else if (mail.cmd == LN_HTTP_RECV_DEVICE_CHARACTER) {
tag = "DEVICE_CHARACTER";
+ //解析角色配置信息
+ if(mail.data)
+ {
+ ln_parse_character_config(mail.data, (int)mail.len);
+ }
} else if (mail.cmd == LN_HTTP_RECV_DEVICE_USER_ID) {
tag = "DEVICE_USER_ID";
} else if (mail.cmd == LN_HTTP_RECV_OTA_TASK) {
/* 在收到 OTA_DOWNLOAD_MSG 后,解析状态并回调处理(进度/完成/失败) */
if (mail.data)
{
- OtaDownloadMsg m = {0};
- if (parse_ota_download_msg(mail.data, &m) == 0) {
- handle_ota_download_msg(&m);
- } else {
- printf("OTA_DOWNLOAD_MSG: parse failed\n");
+ /* 改为堆分配,进一步降低接收线程的峰值栈占用 */
+ OtaDownloadMsg *msg_ptr = (OtaDownloadMsg *)malloc(sizeof(OtaDownloadMsg));
+ if (msg_ptr)
+ {
+ memset(msg_ptr, 0, sizeof(OtaDownloadMsg));
+ if (parse_ota_download_msg(mail.data, msg_ptr) == 0) {
+ handle_ota_download_msg(msg_ptr);
+ } else {
+ printf("OTA_DOWNLOAD_MSG: parse failed\n");
+ }
+ free(msg_ptr);
+ }
+ else
+ {
+ printf("OTA_DOWNLOAD_MSG: alloc failed\n");
}
}
}
uint32_t uapi_ln_is_device_bound(void);
/* 获取设备绑定用户ID */
const char* uapi_ln_get_user_id(void);
+/* 获取设备角色id */
+const char *uapi_ln_get_character_id(void);
+/* 获取设备角色名称 */
+const char *uapi_ln_get_character_name(void);
+/* 获取设备角色眼睛形状 */
+const char *uapi_ln_get_character_eyes_shape(void);
+/* 获取设备角色头发颜色 */
+const char *uapi_ln_get_character_hair_color(void);
+/* 获取设备角色眼睛颜色 */
+const char *uapi_ln_get_character_eyes_color(void);
+/* 获取设备角色头发样式 */
+const char *uapi_ln_get_character_hair_style(void);
+/* 获取设备角色屏幕图片 */
+const char *uapi_ln_get_character_screen_image(void);
+/* 获取设备角色屏幕图片是否为表情 */
+int uapi_ln_get_character_screen_image_as_emoji(void);
+
/* ============================== 共享全局状态(跨文件) ============================== */
extern char *g_device_token;
extern char g_ln_last_target_version[64];
extern char g_ln_last_package_md5[65];
extern int g_ln_last_package_size;
+/* 设备角色信息(由 ln_parse_character_config 填充)*/
+extern char g_ln_character_id[64]; //角色id
+extern char g_ln_character_name[64]; //角色名称
+extern char g_ln_avatar_eyes_shape[64]; //眼睛形状
+extern char g_ln_avatar_hair_color[64]; //头发颜色
+extern char g_ln_avatar_eyes_color[64]; //眼睛颜色
+extern char g_ln_avatar_hair_style[64]; //发型
+extern char g_ln_avatar_screen_image[256]; //屏幕图片
+extern int g_ln_avatar_screen_image_as_emoji; //屏幕图片是否为表情
/* ============================== 公共工具函数(供拆分后的模块复用) ============================== */
uint32_t uapi_ln_refresh_device_token(void);
uint32_t uapi_ln_device_get_user_id(LnDeviceBindStatus *out_status, char *out_user_id, uint32_t out_user_id_len);
+//解析设备角色配置
+void ln_parse_character_config(const char *json_buf, int json_len);
/* ============================== 各接口函数声明 ============================== */
uint32_t ln_get_device_token_http(const char *device_id,
const char *device_iccid,
#include <string.h>
#include <cJSON.h>
+/* =================== 设备角色信息存储 =================== */
+char g_ln_character_id[64] = {0};
+char g_ln_character_name[64] = {0};
+char g_ln_avatar_hair_color[64] = {0};
+char g_ln_avatar_eyes_color[64] = {0};
+char g_ln_avatar_hair_style[64] = {0};
+char g_ln_avatar_eyes_shape[64] = {0};
+char g_ln_avatar_screen_image[256] = {0};
+int g_ln_avatar_screen_image_as_emoji = 0;
+
+/* 安全写入帮助函数 */
+static inline void ln_safe_strcpy(char *dst, size_t dst_size, const char *src)
+{
+ if (!dst || dst_size == 0) return;
+ if (src == NULL) {
+ dst[0] = '\0';
+ return;
+ }
+ strncpy(dst, src, dst_size - 1);
+ dst[dst_size - 1] = '\0';
+}
+
+/* 解析设备角色配置 JSON 并保存到全局变量 */
+void ln_parse_character_config(const char *json_buf, int json_len)
+{
+ if (!json_buf || json_len <= 0)
+ {
+ printf("[DEVICE_CHARACTER] invalid json input\n");
+ return;
+ }
+
+ char *text = (char *)malloc((size_t)json_len + 1);
+ if (!text) return;
+ memcpy(text, json_buf, (size_t)json_len);
+ text[json_len] = '\0';
+
+ cJSON *root = cJSON_Parse(text);
+ if (!root)
+ {
+ printf("[DEVICE_CHARACTER] json parse failed\n");
+ free(text);
+ return;
+ }
+
+ cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+ if (errorCode && !cJSON_IsNull(errorCode))
+ {
+ printf("[DEVICE_CHARACTER] server error: %s\n",
+ cJSON_IsString(errorCode) ? errorCode->valuestring : "unknown");
+ cJSON_Delete(root);
+ free(text);
+ return;
+ }
+
+ cJSON *data = cJSON_GetObjectItem(root, "data");
+ if (!data || !cJSON_IsObject(data))
+ {
+ printf("[DEVICE_CHARACTER] no data object\n");
+ cJSON_Delete(root);
+ free(text);
+ return;
+ }
+
+ /* 基础字段 */
+ cJSON *cid = cJSON_GetObjectItem(data, "characterId");
+ cJSON *cname = cJSON_GetObjectItem(data, "characterName");
+ ln_safe_strcpy(g_ln_character_id, sizeof(g_ln_character_id), (cid && cJSON_IsString(cid)) ? cid->valuestring : "");
+ ln_safe_strcpy(g_ln_character_name, sizeof(g_ln_character_name), (cname && cJSON_IsString(cname)) ? cname->valuestring : "");
+
+ /* 头像配置 */
+ cJSON *avatar = cJSON_GetObjectItem(data, "characterAvatar");
+ if (avatar && cJSON_IsObject(avatar))
+ {
+ cJSON *hairColor = cJSON_GetObjectItem(avatar, "hairColor");
+ cJSON *eyesColor = cJSON_GetObjectItem(avatar, "eyesColor");
+ cJSON *hairStyle = cJSON_GetObjectItem(avatar, "hairStyle");
+ cJSON *eyesShape = cJSON_GetObjectItem(avatar, "eyesShape");
+ cJSON *screenImage = cJSON_GetObjectItem(avatar, "screenImage");
+ cJSON *screenImageAsEmoji = cJSON_GetObjectItem(avatar, "screenImageAsEmoji");
+
+ ln_safe_strcpy(g_ln_avatar_hair_color, sizeof(g_ln_avatar_hair_color), (hairColor && cJSON_IsString(hairColor)) ? hairColor->valuestring : "");
+ ln_safe_strcpy(g_ln_avatar_eyes_color, sizeof(g_ln_avatar_eyes_color), (eyesColor && cJSON_IsString(eyesColor)) ? eyesColor->valuestring : "");
+ ln_safe_strcpy(g_ln_avatar_hair_style, sizeof(g_ln_avatar_hair_style), (hairStyle && cJSON_IsString(hairStyle)) ? hairStyle->valuestring : "");
+ ln_safe_strcpy(g_ln_avatar_eyes_shape, sizeof(g_ln_avatar_eyes_shape), (eyesShape && cJSON_IsString(eyesShape)) ? eyesShape->valuestring : "");
+ ln_safe_strcpy(g_ln_avatar_screen_image, sizeof(g_ln_avatar_screen_image), (screenImage && cJSON_IsString(screenImage)) ? screenImage->valuestring : "");
+ g_ln_avatar_screen_image_as_emoji = (screenImageAsEmoji && cJSON_IsBool(screenImageAsEmoji)) ? (cJSON_IsTrue(screenImageAsEmoji) ? 1 : 0) : 0;
+ }
+ else
+ {
+ /* 缺省时清空 */
+ g_ln_avatar_hair_color[0] = '\0';
+ g_ln_avatar_eyes_color[0] = '\0';
+ g_ln_avatar_hair_style[0] = '\0';
+ g_ln_avatar_eyes_shape[0] = '\0';
+ g_ln_avatar_screen_image[0] = '\0';
+ g_ln_avatar_screen_image_as_emoji = 0;
+ }
+
+ printf("[DEVICE_CHARACTER] parsed: id=%s\n name=%s\n hairColor=%s\n eyesColor=%s\n hairStyle=%s\n eyesShape=%s\n screenImage=%s\n asEmoji=%d\n",
+ g_ln_character_id, g_ln_character_name,
+ g_ln_avatar_hair_color, g_ln_avatar_eyes_color,
+ g_ln_avatar_hair_style, g_ln_avatar_eyes_shape,
+ g_ln_avatar_screen_image, g_ln_avatar_screen_image_as_emoji);
+
+ cJSON_Delete(root);
+ free(text);
+}
+
// 获取设备角色信息
uint32_t ln_device_get_character_http(const char *device_id,
const char *device_token)
printf("chatstream header_buf: %s\n", header_buf);
/* 准备缓冲区(自适应) */
- HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE);
+ HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE*2);
if (prep != HTTP_SUCCESS)
{
free(body);
}
total_written += written;
}
+ else
+ {
+ /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */
+ LOS_Msleep(10);
+ }
}
else
{
}
total_written += written;
}
+ else
+ {
+ /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */
+ LOS_Msleep(10);
+ }
}
else
{
}
total_written += written;
}
+ else
+ {
+ /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */
+ LOS_Msleep(10);
+ }
}
else
{
import random
import string
import logging
+import uuid
+import base64
from enum import Enum
# 配置日志
# 常量定义
class Constants(Enum):
- # WS_BASE_URL = "wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket"
- WS_BASE_URL = "ws://127.0.0.1:8765" # 临时本地测试,到时候需要替换为正式环境
+ WS_BASE_URL = "wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket"
+ # WS_BASE_URL = "ws://127.0.0.1:8765" # 临时本地测试,到时候需要替换为正式环境
TOKEN_API_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken"
UPGRADE_TEST_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/upgrade/getTask"
SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
HEARTBEAT_TIMEOUT = 10 # 心跳超时(秒)
CONNECT_TIMEOUT = 20 # 连接超时(秒)
MAX_RECONNECT_DELAY = 60 # 最大重连延迟(秒)
+ AUDIO_CHUNK_SIZE = 8 * 1024 # 音频分片大小(8KB)
class DeviceWebSocketClient:
"SleepModeEnd": self.handle_sleep_mode_end,
"DeviceBind": self.handle_device_bind,
"DeviceUnbind": self.handle_device_unbind,
- "ScheduleReminder": self.handle_schedule_reminder
+ "ScheduleReminder": self.handle_schedule_reminder,
+ "VoiceChatResponse": self.handle_voice_chat_response # 新增语音响应处理器
}
# 线程相关
self.reconnect_count = 0
self._start_heartbeat()
- # 发送上线通知(修正缩进和变量名)
+ # 发送上线通知
online_msg = json.dumps({"type": "online", "deviceId": self.device_id, "timestamp": int(time.time())})
self.ws.send(online_msg)
print(f"📤 已发送上线消息: {online_msg}")
self.connected = False
logger.info("WebSocket客户端已关闭")
- # 命令处理器实现
+ # 新增:语音对话分片发送功能
+ def send_voice_chat_request(self, audio_file_path: str) -> bool:
+ """
+ 发送语音对话请求(支持分片传输)
+
+ Args:
+ audio_file_path: 音频文件本地路径
+
+ Returns:
+ 是否成功启动分片发送(不保证所有分片都发送成功)
+ """
+ try:
+ # 1. 读取音频文件二进制数据
+ with open(audio_file_path, 'rb') as f:
+ audio_bytes = f.read()
+ total_size = len(audio_bytes)
+ if total_size == 0:
+ logger.error("音频文件为空,无法发送")
+ return False
+
+ # 2. 生成对话唯一标识(UUIDv4)
+ chat_id = str(uuid.uuid4())
+ logger.info(f"开始处理语音对话,chatId: {chat_id},文件大小: {total_size}字节")
+
+ # 3. 分片配置(8KB原始数据/分片)
+ chunk_size = Constants.AUDIO_CHUNK_SIZE.value
+ total_chunks = (total_size + chunk_size - 1) // chunk_size # 向上取整计算总分片数
+
+ # 4. 循环发送分片
+ for chunk_idx in range(total_chunks):
+ # 计算当前分片的起止位置
+ start = chunk_idx * chunk_size
+ end = min(start + chunk_size, total_size)
+ current_chunk = audio_bytes[start:end]
+
+ # 5. 音频分片Base64编码
+ audio_base64 = base64.b64encode(current_chunk).decode('utf-8')
+
+ # 6. 构造分片消息
+ message = {
+ "deviceId": self.device_id,
+ "timestamp": int(time.time()),
+ "messageId": f"voice_{chat_id}_{chunk_idx}", # 分片唯一ID
+ "command": "VoiceChatRequest",
+ "data": {
+ "audioBase64": audio_base64,
+ "chatId": chat_id,
+ "finished": (chunk_idx == total_chunks - 1) # 最后一个分片标记为完成
+ }
+ }
+
+ # 7. 发送分片(调用现有发送方法)
+ if not self.send_message(json.dumps(message)):
+ logger.error(f"第{chunk_idx + 1}/{total_chunks}个分片发送失败,终止传输")
+ return False
+
+ logger.info(f"已发送分片 {chunk_idx + 1}/{total_chunks} "
+ f"(大小: {len(current_chunk)}字节,是否最后一片: {message['data']['finished']})")
+
+ # 可选:添加微小延迟避免服务器压力
+ time.sleep(0.05)
+
+ logger.info(f"所有分片发送完成,共{total_chunks}个分片,chatId: {chat_id}")
+ return True
+
+ except FileNotFoundError:
+ logger.error(f"音频文件不存在: {audio_file_path}")
+ except Exception as e:
+ logger.error(f"语音分片发送失败: {str(e)}")
+ return False
+
+ # 新增:语音对话响应处理
+ def handle_voice_chat_response(self, data: Dict[str, Any]) -> None:
+ """处理服务端返回的语音对话响应"""
+ logger.info("收到语音对话响应 (VoiceChatResponse)")
+ response_data = data.get("data", {})
+ chat_id = response_data.get("chatId")
+ finished = response_data.get("finished", False)
+ content_data = response_data.get("data", {})
+
+ logger.info(f"对话ID: {chat_id}")
+ logger.info(f"是否处理完成: {finished}")
+ logger.info(f"文本响应: {content_data.get('textContent', '无')}")
+ logger.info(f"音频响应URL: {content_data.get('audioUrl', '无')}")
+
+ # 原有命令处理器实现
def handle_character_change(self, data: Dict[str, Any]) -> None:
logger.info("收到角色切换命令")
command_data = data.get("data", {})
return
try:
+ # 示例:连接成功后发送测试音频(替换为实际音频文件路径)
+ time.sleep(2) # 等待连接稳定
+ # ws_client.send_voice_chat_request("test_audio.mp3") # 取消注释并替换为实际音频路径
+
while True:
time.sleep(1)
except KeyboardInterrupt: