From bd35e3faa963fe42401d8c71d1700c4a6df1cb70 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=AD=99=E6=98=A5=E9=98=B3?= Date: Thu, 4 Sep 2025 22:28:56 +0800 Subject: [PATCH] =?utf8?q?ai=E4=B8=AD=E9=97=B4=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- code/sdk/zhihui/ai/example/ai_at_cmd.c | 262 ++++++++++++++++-- .../sdk/zhihui/ai/src/ai_connection_manager.c | 148 +++++++--- code/sdk/zhihui/ai/src/ai_tts_service.c | 2 +- 3 files changed, 353 insertions(+), 59 deletions(-) diff --git a/code/sdk/zhihui/ai/example/ai_at_cmd.c b/code/sdk/zhihui/ai/example/ai_at_cmd.c index bab86387d..fab2c58bc 100644 --- a/code/sdk/zhihui/ai/example/ai_at_cmd.c +++ b/code/sdk/zhihui/ai/example/ai_at_cmd.c @@ -20,6 +20,7 @@ #include "ai_connection_manager.h" #include "ai_memory_pool.h" #include "ai_services.h" +#include "ai_media.h" #include #include #include @@ -55,7 +56,7 @@ extern "C" { #else #define AI_AT_DEMO_ASR_URL "ws://118.145.158.207:22080/asr/ws" #define AI_AT_DEMO_CHAT_URL "ws://118.145.158.207:22080/chat/ws" -#define AI_AT_DEMO_TTS_URL "ws://118.145.158.207:22080/tts/ws" +#define AI_AT_DEMO_TTS_URL "ws://118.145.158.207:22080/tts/stream" #endif #define AI_AT_DEMO_AUDIO_SIZE 1024 #define AI_AT_DEMO_CHAT_MESSAGE "你好,我是AI助手AT命令测试程序" @@ -79,6 +80,51 @@ typedef struct { } ai_at_test_stats_t; static ai_at_test_stats_t g_test_stats = {0}; +static uint32_t get_timestamp_ms(void); + +/* ================================ 媒体测试相关 ================================ */ + +/* 媒体测试状态 */ +typedef struct { + bool is_testing; + uint32_t start_time; + uint32_t test_duration; // 测试持续时间(秒) + uint32_t record_count; // 录音数据包计数 + uint32_t play_count; // 播放数据包计数 +} ai_media_test_context_t; + +static ai_media_test_context_t g_media_test_ctx = {0}; + +/** + * @brief 媒体测试录音回调函数 - 边录边放 + */ +static void media_test_record_callback(uint8_t *data, uint32_t size) +{ + if (!g_media_test_ctx.is_testing) { + return; + } + + g_media_test_ctx.record_count++; + + // 检查播放状态,如果播放正在进行,直接播放录音数据 + if (ai_media_get_play_status()) { + ai_media_play_buffer(data, size); + g_media_test_ctx.play_count++; + + // 每100个数据包打印一次状态 + if (g_media_test_ctx.record_count % 100 == 0) { + printf("[AI_MEDIA_TEST] 录音包: %u, 播放包: %u\n", + g_media_test_ctx.record_count, g_media_test_ctx.play_count); + } + } + + // 检查测试时间是否到达 + uint32_t current_time = get_timestamp_ms(); + if (current_time - g_media_test_ctx.start_time >= g_media_test_ctx.test_duration * 1000) { + printf("[AI_MEDIA_TEST] 测试时间到达,准备停止测试\n"); + g_media_test_ctx.is_testing = false; + } +} /* ================================ 内部函数 ================================ */ @@ -304,6 +350,18 @@ static uint32_t at_ai_status(uint8_t *para, uint32_t paraLen, char *strCommonCmd const ai_memory_stats_t* mem_stats = ai_memory_pool_get_global_stats(); printf("[AI_AT] 全局状态: %s\n", state_names[global_state]); + + // 显示各服务的详细连接状态 + ai_connection_state_t asr_state = ai_connection_manager_get_connection_state(AI_SERVICE_ASR); + ai_connection_state_t chat_state = ai_connection_manager_get_connection_state(AI_SERVICE_CHAT); + ai_connection_state_t tts_state = ai_connection_manager_get_connection_state(AI_SERVICE_TTS); + + const char* conn_state_names[] = {"断开", "连接中", "已连接", "错误"}; + printf("[AI_AT] 服务状态:\n"); + printf("[AI_AT] ASR: %s (%d)\n", conn_state_names[asr_state], asr_state); + printf("[AI_AT] Chat: %s (%d)\n", conn_state_names[chat_state], chat_state); + printf("[AI_AT] TTS: %s (%d)\n", conn_state_names[tts_state], tts_state); + printf("[AI_AT] 内存使用: %u/%u 字节 (%.1f%%)\n", mem_stats->used_size, mem_stats->total_size, (float)mem_stats->used_size * 100.0f / mem_stats->total_size); @@ -370,30 +428,7 @@ static uint32_t at_ai_asr_test(uint8_t *para, uint32_t paraLen, char *strCommonC return ERRCODE_FAIL; } - // 生成并发送模拟音频数据 - uint8_t *audio_data = malloc(AI_AT_DEMO_AUDIO_SIZE); - if (audio_data == NULL) { - printf("[AI_AT] 音频缓冲区分配失败\n"); - ai_asr_stop(&g_ai_services_context); - g_test_stats.tests_failed++; - return ERRCODE_FAIL; - } - - // 生成模拟音频数据(更真实的模式) - for (int i = 0; i < AI_AT_DEMO_AUDIO_SIZE; i++) { - audio_data[i] = (uint8_t)(i % 256); - } - - printf("[AI_AT] 发送音频数据: %d 字节\n", AI_AT_DEMO_AUDIO_SIZE); - ret = ai_asr_send_audio(&g_ai_services_context, audio_data, AI_AT_DEMO_AUDIO_SIZE); - free(audio_data); - - if (ret != AI_OK) { - printf("[AI_AT] 音频数据发送失败: %d\n", ret); - ai_asr_stop(&g_ai_services_context); - g_test_stats.tests_failed++; - return ERRCODE_FAIL; - } + // 等待指定时间 printf("[AI_AT] 等待ASR处理...\n"); @@ -870,6 +905,172 @@ static uint32_t at_ai_reconnect(uint8_t *para, uint32_t paraLen, char *strCommon } } +/** + * @brief AT^AIMEDIATEST - 媒体服务测试(边录边放) + * @param para 参数:测试持续时间(秒),默认10秒 + * @param paraLen 参数长度 + * @param strCommonCmd 通用命令字符串 + * @return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败 + */ +static uint32_t at_ai_media_test(uint8_t *para, uint32_t paraLen, char *strCommonCmd) +{ + UNUSED(strCommonCmd); + + printf("[AI_AT] 执行 AT^AIMEDIATEST - 媒体服务测试(边录边放)\n"); + + // 检查是否已经在测试中 + if (g_media_test_ctx.is_testing) { + printf("[AI_AT] 媒体测试已在进行中,请先停止当前测试\n"); + return ERRCODE_FAIL; + } + + // 解析测试持续时间参数 + uint32_t test_duration = 10; // 默认10秒 + if (para != NULL && paraLen > 0) { + uint8_t argv[AI_AT_MAX_ARGC][AI_AT_MAX_ARGV_LEN] = { 0 }; + uint32_t argc = 0; + + int ret = ConvertCmdToParam(para, paraLen, argv, &argc); + if (ret == AI_AT_SUCCESS && argc > 0) { + int duration = atoi((char*)argv[0]); + if (duration > 0 && duration <= 300) { // 最大5分钟 + test_duration = (uint32_t)duration; + } else { + printf("[AI_AT] 无效的测试时长: %d (有效范围: 1-300秒)\n", duration); + return ERRCODE_FAIL; + } + } + } + + printf("[AI_AT] 开始媒体测试,持续时间: %u秒\n", test_duration); + + // 初始化媒体测试上下文 + memset(&g_media_test_ctx, 0, sizeof(g_media_test_ctx)); + g_media_test_ctx.is_testing = true; + g_media_test_ctx.start_time = get_timestamp_ms(); + g_media_test_ctx.test_duration = test_duration; + + // 初始化媒体服务 + int32_t init_ret = ai_media_service_init(); + if (init_ret != 0) { + printf("[AI_AT] 媒体服务初始化失败: %d\n", init_ret); + g_media_test_ctx.is_testing = false; + return ERRCODE_FAIL; + } + + // 设置录音回调 + ai_media_set_record_callback(media_test_record_callback); + + // 启动播放服务 + printf("[AI_AT] 启动音频播放服务...\n"); + uint8_t play_format[] = "mp3"; + ai_media_play_start(play_format); + + // 检查播放状态 + if (!ai_media_get_play_status()) { + printf("[AI_AT] 播放服务启动失败\n"); + ai_media_service_deinit(); + g_media_test_ctx.is_testing = false; + return ERRCODE_FAIL; + } + + // 启动录音服务 + printf("[AI_AT] 启动音频录音服务...\n"); + uint8_t record_format[] = "mp3"; + ai_media_record_start(record_format); + + // 检查录音状态 + if (!ai_media_get_record_status()) { + printf("[AI_AT] 录音服务启动失败\n"); + ai_media_play_stop(); + ai_media_service_deinit(); + g_media_test_ctx.is_testing = false; + return ERRCODE_FAIL; + } + + printf("[AI_AT] 媒体测试已启动!录音数据将实时播放\n"); + printf("[AI_AT] 测试将在%u秒后自动停止\n", test_duration); + + // 等待测试完成 + uint32_t check_interval = 1000; // 每秒检查一次 + uint32_t elapsed_time = 0; + + while (g_media_test_ctx.is_testing && elapsed_time < test_duration * 1000) { +#ifndef __TEST_MODE__ + osDelay(check_interval); +#else + usleep(check_interval * 1000); // 测试模式下使用usleep +#endif + elapsed_time += check_interval; + + // 每5秒打印一次状态 + if (elapsed_time % 5000 == 0) { + printf("[AI_AT] 测试进行中... 已录音包: %u, 已播放包: %u, 剩余时间: %u秒\n", + g_media_test_ctx.record_count, g_media_test_ctx.play_count, + (test_duration * 1000 - elapsed_time) / 1000); + } + } + + // 停止测试 + printf("[AI_AT] 停止媒体测试...\n"); + g_media_test_ctx.is_testing = false; + + // 停止录音和播放 + ai_media_record_stop(); + ai_media_play_stop(); + + // 反初始化媒体服务 + ai_media_service_deinit(); + + // 打印测试结果 + printf("[AI_AT] 媒体测试完成!\n"); + printf("[AI_AT] 测试时长: %u秒\n", test_duration); + printf("[AI_AT] 录音数据包: %u\n", g_media_test_ctx.record_count); + printf("[AI_AT] 播放数据包: %u\n", g_media_test_ctx.play_count); + printf("[AI_AT] 数据传输率: %.2f包/秒\n", + (float)g_media_test_ctx.record_count / test_duration); + + return ERRCODE_SUCC; +} + +/** + * @brief AT^AIMEDIASTOP - 停止媒体服务测试 + * @param para 参数(未使用) + * @param paraLen 参数长度 + * @param strCommonCmd 通用命令字符串 + * @return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败 + */ +static uint32_t at_ai_media_stop(uint8_t *para, uint32_t paraLen, char *strCommonCmd) +{ + UNUSED(para); + UNUSED(paraLen); + UNUSED(strCommonCmd); + + printf("[AI_AT] 执行 AT^AIMEDIASTOP - 停止媒体服务测试\n"); + + if (!g_media_test_ctx.is_testing) { + printf("[AI_AT] 当前没有进行中的媒体测试\n"); + return ERRCODE_SUCC; + } + + // 停止测试 + printf("[AI_AT] 强制停止媒体测试...\n"); + g_media_test_ctx.is_testing = false; + + // 停止录音和播放 + ai_media_record_stop(); + ai_media_play_stop(); + + // 反初始化媒体服务 + ai_media_service_deinit(); + + printf("[AI_AT] 媒体测试已停止\n"); + printf("[AI_AT] 录音数据包: %u\n", g_media_test_ctx.record_count); + printf("[AI_AT] 播放数据包: %u\n", g_media_test_ctx.play_count); + + return ERRCODE_SUCC; +} + /* ================================ AT命令表定义 ================================ */ static const at_cmd_table_t g_ai_cmd[] = { @@ -885,6 +1086,8 @@ static const at_cmd_table_t g_ai_cmd[] = { { (char *)"AT^AISTATS", at_ai_stats, (char *)"Show test statistics" }, { (char *)"AT^AICLEANUP", at_ai_cleanup, (char *)"Cleanup AI services" }, { (char *)"AT^AIRECONNECT", at_ai_reconnect, (char *)"service_type (0=ASR,1=Chat,2=TTS)" }, + { (char *)"AT^AIMEDIATEST", at_ai_media_test, (char *)"duration_seconds (default=10, max=300)" }, + { (char *)"AT^AIMEDIASTOP", at_ai_media_stop, (char *)"Stop media test" }, }; /* ================================ 公共接口 ================================ */ @@ -966,11 +1169,16 @@ int main(void) result = at_ai_performance_test(NULL, 0, NULL); printf(" 结果: %s\n", result == ERRCODE_SUCC ? "✅ 成功" : "❌ 失败"); - printf("\n9. 最终统计和清理\n"); + printf("\n9. 测试 AT^AIMEDIATEST=5 - 媒体服务测试(边录边放5秒)\n"); + uint8_t media_para[] = "5"; + result = at_ai_media_test(media_para, sizeof(media_para) - 1, NULL); + printf(" 结果: %s\n", result == ERRCODE_SUCC ? "✅ 成功" : "❌ 失败"); + + printf("\n10. 最终统计和清理\n"); result = at_ai_stats(NULL, 0, NULL); printf(" 统计结果: %s\n", result == ERRCODE_SUCC ? "✅ 成功" : "❌ 失败"); - printf("\n10. 测试 AT^AICLEANUP - 清理AI服务\n"); + printf("\n11. 测试 AT^AICLEANUP - 清理AI服务\n"); result = at_ai_cleanup(NULL, 0, NULL); printf(" 结果: %s\n", result == ERRCODE_SUCC ? "✅ 成功" : "❌ 失败"); diff --git a/code/sdk/zhihui/ai/src/ai_connection_manager.c b/code/sdk/zhihui/ai/src/ai_connection_manager.c index f7d73859d..ea7f6e136 100644 --- a/code/sdk/zhihui/ai/src/ai_connection_manager.c +++ b/code/sdk/zhihui/ai/src/ai_connection_manager.c @@ -34,7 +34,7 @@ static bool g_manager_initialized = false; /* WebSocket连接计数器和限制 */ static int g_active_websocket_count = 0; -#define MAX_CONCURRENT_WEBSOCKETS 4 // 最大并发WebSocket连接数 +#define MAX_CONCURRENT_WEBSOCKETS 3 // 最大并发WebSocket连接数 /* 服务名称映射表 */ static const char* g_service_names[AI_SERVICE_COUNT] = { @@ -365,8 +365,9 @@ ai_error_t ai_connection_manager_start_all(void) g_connection_manager.metrics.last_connect_timestamp = start_time; - /* 并发启动所有连接线程 */ + /* 串行启动所有连接,确保按顺序成功连接 */ AI_LOGI("[DEBUG] AI_SERVICE_COUNT = %d", AI_SERVICE_COUNT); + AI_LOGI("Starting services in serial mode to avoid connection conflicts"); for (int i = 0; i < AI_SERVICE_COUNT; i++) { AI_LOGI("[DEBUG] Creating thread for service %d (%s)...", i, g_service_names[i]); @@ -382,8 +383,47 @@ ai_error_t ai_connection_manager_start_all(void) AI_LOGI("[DEBUG] Thread created successfully for service %s", g_service_names[i]); #ifndef __TEST_MODE__ - /* 给线程一些时间启动,避免竞态条件 */ - osDelay(100); // 100ms延迟 + /* 等待当前服务连接成功后再启动下一个 */ + AI_LOGI("Waiting for %s service to connect before starting next service...", g_service_names[i]); + + // 等待连接建立,最多等待15秒(增加超时时间) + bool connected = false; + uint32_t wait_start = get_current_timestamp(); + + for (int wait_count = 0; wait_count < 150; wait_count++) { + osDelay(100); // 每100ms检查一次 + + ai_connection_state_t state = g_connection_manager.connections[i].state; + if (state == AI_CONN_STATE_CONNECTED) { + connected = true; + uint32_t connect_duration = get_current_timestamp() - wait_start; + AI_LOGI("Service %s connected successfully in %u ms", g_service_names[i], connect_duration); + break; + } else if (state == AI_CONN_STATE_ERROR) { + AI_LOGE("Service %s connection failed with error state", g_service_names[i]); + break; + } + + // 每2秒打印一次状态,减少日志噪音 + if (wait_count % 20 == 0 && wait_count > 0) { + uint32_t elapsed = get_current_timestamp() - wait_start; + AI_LOGI("Waiting for %s connection... (state: %d, elapsed: %u ms, attempt: %d/150)", + g_service_names[i], state, elapsed, wait_count + 1); + } + } + + if (!connected) { + uint32_t total_wait = get_current_timestamp() - wait_start; + AI_LOGE("Service %s failed to connect within %u ms timeout", g_service_names[i], total_wait); + // 继续尝试下一个服务,而不是完全失败 + } else { + // 连接成功后等待1秒,确保连接稳定并避免资源竞争 + AI_LOGI("Service %s connection established, waiting 1s before next service...", g_service_names[i]); + osDelay(1000); + } +#else + /* 测试模式下简单延时 */ + usleep(100000); // 100ms #endif } @@ -875,9 +915,13 @@ static void connection_thread_entry(void *argument) AI_LOGI("[DEBUG] Connection thread started for service %s", g_service_names[service_type]); - while (g_connection_manager.is_running) { + /* 只尝试连接一次,连接成功后线程退出 */ + bool connection_attempted = false; + + while (g_connection_manager.is_running && !connection_attempted) { if (conn->state == AI_CONN_STATE_DISCONNECTED) { /* 尝试连接 */ + connection_attempted = true; // 标记已尝试连接 uint32_t connect_start = get_current_timestamp(); ai_error_t ret = websocket_connect(conn); @@ -888,26 +932,7 @@ static void connection_thread_entry(void *argument) AI_LOGI("Service %s connected in %u ms", g_service_names[service_type], connect_time); - /* 更新连接状态矩阵 */ - switch (service_type) { - case AI_SERVICE_ASR: - g_connection_manager.connection_matrix.asr_connected = 1; - break; - case AI_SERVICE_CHAT: - g_connection_manager.connection_matrix.chat_connected = 1; - break; - case AI_SERVICE_TTS: - g_connection_manager.connection_matrix.tts_connected = 1; - break; - case AI_SERVICE_COUNT: - /* 不应该到达这里 */ - break; - case AI_SERVICE_GLOBAL: - /* 全局服务不需要更新连接矩阵 */ - break; - } - - update_global_state(); + /* 注意:连接状态矩阵将在websocket_on_connected回调中更新 */ #ifndef __TEST_MODE__ /* 释放就绪信号量 */ @@ -916,6 +941,10 @@ static void connection_thread_entry(void *argument) /* 发送连接建立事件 */ send_event(AI_EVENT_CONNECTION_ESTABLISHED, service_type, NULL); + + /* 连接成功,线程任务完成,退出循环 */ + AI_LOGI("Connection thread for service %s completed successfully, exiting", g_service_names[service_type]); + break; } else { conn->retry_count++; g_connection_manager.metrics.retry_count[service_type]++; @@ -928,6 +957,22 @@ static void connection_thread_entry(void *argument) /* 使用新的清理函数替换原有的清理逻辑 */ cleanup_websocket_connection(conn); + /* 确保连接状态矩阵正确反映失败状态 */ + switch (service_type) { + case AI_SERVICE_ASR: + g_connection_manager.connection_matrix.asr_connected = 0; + break; + case AI_SERVICE_CHAT: + g_connection_manager.connection_matrix.chat_connected = 0; + break; + case AI_SERVICE_TTS: + g_connection_manager.connection_matrix.tts_connected = 0; + break; + case AI_SERVICE_COUNT: + case AI_SERVICE_GLOBAL: + break; + } + /* 为了安全,限制重试次数为1次 */ if (conn->retry_count >= 1) { AI_LOGW("Service %s connection disabled for safety after %d attempts", @@ -936,21 +981,28 @@ static void connection_thread_entry(void *argument) /* 设置为永久断开状态,不再重试 */ conn->state = AI_CONN_STATE_ERROR; + /* 更新全局状态 */ + update_global_state(); + /* 发送连接失败事件 */ send_event(AI_EVENT_ERROR_OCCURRED, service_type, &ret); + + /* 连接失败,线程任务完成,退出循环 */ + AI_LOGI("Connection thread for service %s failed after retries, exiting", g_service_names[service_type]); break; } - /* 等待重试间隔 */ + /* 等待重试间隔,然后继续尝试 */ #ifndef __TEST_MODE__ osDelay(g_connection_manager.config.retry_interval_ms); #endif + /* 重置connection_attempted以允许重试 */ + connection_attempted = false; } } else { - /* 连接已建立,等待断开或错误 */ -#ifndef __TEST_MODE__ - osDelay(1000); // 1秒检查一次 -#endif + /* 连接已建立,线程任务完成,退出循环 */ + AI_LOGI("Connection thread for service %s found already connected, exiting", g_service_names[service_type]); + break; } } @@ -1082,7 +1134,21 @@ static void websocket_on_connected(int client) ai_connection_info_t *conn_info = find_connection_by_websocket(client); if (conn_info == NULL) { AI_LOGE("Cannot find connection info for WebSocket client %d", client); - return; + + /* 尝试恢复:检查是否有处于CONNECTING状态的连接 */ + for (int i = 0; i < AI_SERVICE_COUNT; i++) { + if (g_connection_manager.connections[i].state == AI_CONN_STATE_CONNECTING) { + AI_LOGW("Found CONNECTING service %s, assuming this is the connected one", g_service_names[i]); + conn_info = &g_connection_manager.connections[i]; + conn_info->websocket = client; // 更新正确的WebSocket ID + break; + } + } + + if (conn_info == NULL) { + AI_LOGE("No CONNECTING service found, cannot recover"); + return; + } } /* 验证服务类型有效性 */ @@ -1102,6 +1168,26 @@ static void websocket_on_connected(int client) conn_info->state = AI_CONN_STATE_CONNECTED; conn_info->last_activity_time = get_current_timestamp(); + /* 更新连接状态矩阵 */ + switch (conn_info->service_type) { + case AI_SERVICE_ASR: + g_connection_manager.connection_matrix.asr_connected = 1; + break; + case AI_SERVICE_CHAT: + g_connection_manager.connection_matrix.chat_connected = 1; + break; + case AI_SERVICE_TTS: + g_connection_manager.connection_matrix.tts_connected = 1; + break; + case AI_SERVICE_COUNT: + case AI_SERVICE_GLOBAL: + /* 不需要更新连接矩阵 */ + break; + } + + /* 更新全局状态 */ + update_global_state(); + #ifndef __TEST_MODE__ if (conn_info->mutex != NULL) { osMutexRelease(conn_info->mutex); diff --git a/code/sdk/zhihui/ai/src/ai_tts_service.c b/code/sdk/zhihui/ai/src/ai_tts_service.c index bf2fc91ac..7b9a44db0 100644 --- a/code/sdk/zhihui/ai/src/ai_tts_service.c +++ b/code/sdk/zhihui/ai/src/ai_tts_service.c @@ -65,7 +65,7 @@ bool tts_api_ws_url_init(char *url_buf,const char *server_url) } else { - snprintf(url_buf,AI_MAX_URL_LENGTH,"%s?platform=aliyun&noCache=1",server_url); + snprintf(url_buf,AI_MAX_URL_LENGTH,"ws://118.145.158.207:22080/tts/stream?audio_buffer_size=512"); } return true; -- 2.17.1