char *json_string = cJSON_Print(push_data);
if (json_string)
{
- printf("[JSON_PARSE] pushing data to interface_type=%d: %s\n", interface_type, json_string);
- do_ln_http_server_recv_data(interface_type, json_string, strlen(json_string));
+ // printf("[JSON_PARSE] pushing data to interface_type=%d: %s\n", interface_type, json_string);
+ // [JSON_PARSE] pushing data to interface_type=3: {
+ // "textContent": "在剧中,她曾利用香料和药材来达到自己的目的,比如制作迷香等。"
+ // }
+ // 解析textContent中字符串发送给ui
+ cJSON *textContent = cJSON_GetObjectItem(push_data, "textContent");
+ if (textContent && cJSON_IsString(textContent))
+ {
+ printf("[JSON_PARSE] textContent: %s\n", textContent->valuestring);
+ do_ln_http_server_recv_data(interface_type, textContent->valuestring, strlen(textContent->valuestring));
+ }
+ else
+ {
+ printf("[JSON_PARSE] no textContent found\n");
+ }
free(json_string);
}
cJSON_Delete(push_data);
const char *tag = "UNKNOWN";
if (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV) {
tag = "SPEECH_CHAT";
+ //推送给ui
} else if (mail.cmd == LN_HTTP_RECV_STORY_CHOOSE) {
tag = "STORY_CHOOSE";
+ //推送给ui
} else if (mail.cmd == LN_HTTP_RECV_STORY_RANDOM) {
tag = "STORY_RANDOM";
+ //推送给ui
} else if (mail.cmd == LN_HTTP_RECV_STORY_EXIT) {
tag = "STORY_EXIT";
} else if (mail.cmd == LN_HTTP_RECV_STORY_FINISH) {
#include "ln_api.h"
LnCharacterConfig g_ln_character_config = {0};
+ScheduleReminderConfig g_schedule_reminder_config = {0};
// 安全字符串复制函数
static void ln_safe_strcpy(char *dest, size_t dest_size, const char *src)
printf(" enableVoice: %d\n", g_schedule_reminder.enableVoice);
printf(" enableVibration: %d\n", g_schedule_reminder.enableVibration);
+ // 清理资源
+ cJSON_Delete(root);
+ free(text);
+}
+
+/* 解析日程提醒配置JSON并保存到全局结构体 */
+void ln_parse_schedule_reminder_config(const char *json_buf, int json_len)
+{
+ if (!json_buf || json_len <= 0)
+ {
+ printf("[SCHEDULE_REMINDER_CONFIG] 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';
+
+ // 解析JSON根对象
+ cJSON *root = cJSON_Parse(text);
+ if (!root)
+ {
+ printf("[SCHEDULE_REMINDER_CONFIG] json parse failed\n");
+ free(text);
+ return;
+ }
+
+ // 检查错误码
+ cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+ if (errorCode && !cJSON_IsNull(errorCode))
+ {
+ printf("[SCHEDULE_REMINDER_CONFIG] server error: %s\n",
+ cJSON_IsString(errorCode) ? errorCode->valuestring : "unknown");
+ cJSON_Delete(root);
+ free(text);
+ return;
+ }
+
+ // 获取data对象
+ cJSON *data = cJSON_GetObjectItem(root, "data");
+ if (!data || !cJSON_IsObject(data))
+ {
+ printf("[SCHEDULE_REMINDER_CONFIG] no data object\n");
+ cJSON_Delete(root);
+ free(text);
+ return;
+ }
+
+ // 解析日程名称字段
+ cJSON *scheduleName = cJSON_GetObjectItem(data, "scheduleName");
+ ln_safe_strcpy(g_schedule_reminder_config.scheduleName,
+ sizeof(g_schedule_reminder_config.scheduleName),
+ (scheduleName && cJSON_IsString(scheduleName)) ? scheduleName->valuestring : "");
+
+ // 打印解析结果
+ printf("[SCHEDULE_REMINDER_CONFIG] parsed successfully:\n");
+ printf(" scheduleName: %s\n", g_schedule_reminder_config.scheduleName);
+
// 清理资源
cJSON_Delete(root);
free(text);
import random
import string
import logging
-import uuid
-import base64
from enum import Enum
# 配置日志
HEARTBEAT_TIMEOUT = 10 # 心跳超时(秒)
CONNECT_TIMEOUT = 20 # 连接超时(秒)
MAX_RECONNECT_DELAY = 60 # 最大重连延迟(秒)
- AUDIO_CHUNK_SIZE = 8 * 1024 # 音频分片大小(8KB)
class DeviceWebSocketClient:
"DeviceBind": self.handle_device_bind,
"DeviceUnbind": self.handle_device_unbind,
"ScheduleReminder": self.handle_schedule_reminder,
- "VoiceChatResponse": self.handle_voice_chat_response # 新增语音响应处理器
+ "Pong": self.handle_pong # 添加Pong命令处理器
}
# 线程相关
try:
logger.info(f"收到消息: {message}")
- # 处理心跳响应
- if message == "pong":
- self.last_heartbeat_response = time.time()
- logger.info("收到心跳响应")
- return
-
- # 处理业务消息
+ # 解析JSON消息
data = json.loads(message)
command = data.get("command")
self.heartbeat_monitor_thread.join(timeout=1)
def _heartbeat_loop(self) -> None:
- """心跳发送循环"""
+ """心跳发送循环,按照协议格式发送JSON格式的Ping消息"""
while self.connected and self.running:
try:
- self.ws.send("ping")
- logger.info("发送心跳 ping")
+ # 生成符合协议要求的Ping消息
+ ping_msg = {
+ "deviceId": self.device_id,
+ "timestamp": int(time.time()),
+ "messageId": f"ping_{int(time.time())}_{random.randint(1000, 9999)}",
+ "command": "Ping",
+ "data": None
+ }
+ self.ws.send(json.dumps(ping_msg))
+ logger.info(f"发送心跳 Ping: {ping_msg}")
time.sleep(Constants.HEARTBEAT_INTERVAL.value)
except Exception as e:
logger.error(f"发送心跳出错: {str(e)}")
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", {})
def handle_schedule_reminder(self, data: Dict[str, Any]) -> None:
logger.info("收到日程提醒命令")
+
+ def handle_pong(self, data: Dict[str, Any]) -> None:
+ """处理服务端返回的Pong响应"""
+ self.last_heartbeat_response = time.time()
+ logger.info(f"收到心跳响应 Pong: {data}")
def get_valid_device_token(device_id: str) -> Tuple[Optional[str], str]:
def main() -> NoReturn:
- device_id = "DEVICE_PC_TEST_001".strip()
+ device_id = "DEVICE_PC_TEST_002".strip()
if not device_id:
logger.error("deviceId不能为空")
return
return
try:
- # 示例:连接成功后发送测试音频(替换为实际音频文件路径)
- time.sleep(2) # 等待连接稳定
- # ws_client.send_voice_chat_request("test_audio.mp3") # 取消注释并替换为实际音频路径
-
while True:
time.sleep(1)
except KeyboardInterrupt: