From 9d060f9f40a822a67fd8cad2983f5ce1441b5861 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 25 Feb 2026 11:16:30 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/Kconfig.projbuild | 16 +- main/control_plane/src/rest_server.c | 126 +++++++++ main/domain/src/voice_interaction.c | 390 ++++++++++++++++++++++++--- main/platform/src/voice_audio.c | 112 +++++++- sdkconfig.defaults | 26 +- 5 files changed, 612 insertions(+), 58 deletions(-) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index a91d39b..12ff8e6 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -107,17 +107,17 @@ config TQ_VOICE_HEARTBEAT_SEC config TQ_VOICE_I2C_PORT int "Voice I2C port" range 0 1 - default 0 + default 1 config TQ_VOICE_I2C_SDA_PIN int "Voice I2C SDA GPIO" range 0 48 - default 17 + default 4 config TQ_VOICE_I2C_SCL_PIN int "Voice I2C SCL GPIO" range 0 48 - default 18 + default 5 config TQ_VOICE_CODEC_ADDR int "ES8311 I2C address" @@ -132,27 +132,27 @@ config TQ_VOICE_I2S_PORT config TQ_VOICE_I2S_MCLK_PIN int "Voice I2S MCLK GPIO" range -1 48 - default 16 + default 39 config TQ_VOICE_I2S_BCLK_PIN int "Voice I2S BCLK GPIO" range 0 48 - default 9 + default 40 config TQ_VOICE_I2S_WS_PIN int "Voice I2S WS GPIO" range 0 48 - default 45 + default 41 config TQ_VOICE_I2S_DOUT_PIN int "Voice I2S DOUT GPIO" range 0 48 - default 8 + default 38 config TQ_VOICE_I2S_DIN_PIN int "Voice I2S DIN GPIO" range 0 48 - default 10 + default 42 config TQ_VOICE_CODEC_PA_PIN int "ES8311 PA control GPIO" diff --git a/main/control_plane/src/rest_server.c b/main/control_plane/src/rest_server.c index afc4b99..b76b2e3 100644 --- a/main/control_plane/src/rest_server.c +++ b/main/control_plane/src/rest_server.c @@ -30,6 +30,8 @@ static const char *s_web_index = " button { border:0; border-radius:8px; padding:8px 12px; cursor:pointer; background:#0b66d0; color:#fff; }\n" " button.alt { background:#5f6f82; }\n" " button.warn { background:#cf2f2f; }\n" + " button.talk { background:#198754; touch-action:none; user-select:none; }\n" + " button.talk.active { background:#d68910; }\n" " pre { background:#0f1720; color:#d6e2f5; border-radius:8px; padding:10px; overflow:auto; min-height:120px; }\n" " .small { font-size: 12px; color:#6a778a; }\n" " label { font-size:13px; color:#44566c; display:block; margin: 10px 0 4px 0; }\n" @@ -102,6 +104,12 @@ static const char *s_web_index = " \n" "
Submit prompt only: ESP32-S3 calls DashScope Z-Image, downloads PNG, rasterizes and prints.
\n" "\n" + " \n" + "
\n" + " \n" + "
\n" + "
点击一次开始语音输入,再点击一次结束(自动建立语音会话)。
\n" + "\n" "
Tip: connect printer first, then submit print jobs.
\n" "
Ready.
\n" " \n" @@ -121,7 +129,10 @@ static const char *s_web_index = " const imgScaleToWidthEl = document.getElementById('imgScaleToWidth');\n" " const imgInvertEl = document.getElementById('imgInvert');\n" " const imgPromptExtendEl = document.getElementById('imgPromptExtend');\n" + " const btnVoiceTalkEl = document.getElementById('btnVoiceTalk');\n" " const thermalPromptSuffix = 'black and white line art, pure white background, high contrast, clean edges, minimal texture, no dark background, no heavy shadows, centered subject, thermal printer friendly';\n" + " let voiceTalkHolding = false;\n" + " let voiceTalkBusy = false;\n" "\n" " function log(obj) {\n" " if (typeof obj === 'string') {\n" @@ -147,6 +158,97 @@ static const char *s_web_index = " return n;\n" " }\n" "\n" + " function sleep(ms) {\n" + " return new Promise((resolve) => setTimeout(resolve, ms));\n" + " }\n" + "\n" + " function setVoiceTalkButtonState(active) {\n" + " if (active) {\n" + " btnVoiceTalkEl.classList.add('active');\n" + " btnVoiceTalkEl.textContent = '点击结束对话';\n" + " } else {\n" + " btnVoiceTalkEl.classList.remove('active');\n" + " btnVoiceTalkEl.textContent = '点击开始对话';\n" + " }\n" + " }\n" + "\n" + " function syncVoiceTalkUi(st) {\n" + " const active = !!(st && st.voice_tap_active);\n" + " voiceTalkHolding = active;\n" + " setVoiceTalkButtonState(active);\n" + " }\n" + "\n" + " function voiceReady(st) {\n" + " return !!(st && st.voice_session_active && st.voice_ws_connected && st.voice_started);\n" + " }\n" + "\n" + " async function ensureVoiceSessionReady() {\n" + " let st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (voiceReady(st)) {\n" + " return st;\n" + " }\n" + "\n" + " await callApi('/v1/voice/session/start', 'POST', {});\n" + " const deadline = Date.now() + 10000;\n" + " while (Date.now() < deadline) {\n" + " await sleep(250);\n" + " st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (voiceReady(st)) {\n" + " return st;\n" + " }\n" + " }\n" + "\n" + " throw new Error('voice session is not ready, please retry');\n" + " }\n" + "\n" + " async function beginVoiceTalk() {\n" + " if (voiceTalkBusy) {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " }\n" + "\n" + " voiceTalkBusy = true;\n" + " try {\n" + " let st = await ensureVoiceSessionReady();\n" + " if (st && st.voice_tap_active) {\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " }\n" + " await callApi('/v1/voice/tap/start', 'POST', {});\n" + " st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " } finally {\n" + " voiceTalkBusy = false;\n" + " }\n" + " }\n" + "\n" + " async function endVoiceTalk() {\n" + " if (voiceTalkBusy) {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " }\n" + "\n" + " voiceTalkBusy = true;\n" + " try {\n" + " let st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (!(st && st.voice_tap_active)) {\n" + " return st;\n" + " }\n" + " await callApi('/v1/voice/tap/cancel', 'POST', {});\n" + " st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " return st;\n" + " } finally {\n" + " voiceTalkBusy = false;\n" + " }\n" + " }\n" + "\n" " function buildImagePromptRequest() {\n" " const prompt = imagePromptEl.value.trim();\n" " if (!prompt) {\n" @@ -222,6 +324,30 @@ static const char *s_web_index = " const req = buildImagePromptRequest();\n" " return callApi('/v1/print/image', 'POST', req);\n" " });\n" + "\n" + " btnVoiceTalkEl.addEventListener('click', () => run(async () => {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (st && st.voice_tap_active) {\n" + " return endVoiceTalk();\n" + " }\n" + " return beginVoiceTalk();\n" + " }));\n" + " window.addEventListener('blur', () => run(async () => {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " if (st && st.voice_tap_active) {\n" + " return endVoiceTalk();\n" + " }\n" + " return st;\n" + " }));\n" + " setVoiceTalkButtonState(false);\n" + " (async () => {\n" + " try {\n" + " const st = await callApi('/v1/voice/status', 'GET');\n" + " syncVoiceTalkUi(st);\n" + " } catch (_) {}\n" + " })();\n" " \n" "\n" "\n"; diff --git a/main/domain/src/voice_interaction.c b/main/domain/src/voice_interaction.c index e59945c..aa92fbc 100644 --- a/main/domain/src/voice_interaction.c +++ b/main/domain/src/voice_interaction.c @@ -13,12 +13,14 @@ #include "esp_log.h" #include "esp_mac.h" #include "esp_random.h" +#include "esp_heap_caps.h" #include "esp_system.h" #include "esp_timer.h" #include "esp_websocket_client.h" #include "esp_opus_dec.h" #include "esp_opus_enc.h" #include "freertos/FreeRTOS.h" +#include "freertos/idf_additions.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "voice_audio.h" @@ -32,7 +34,8 @@ #define VOICE_SAMPLE_BYTES 2 #define VOICE_STATUS_LOCK_TIMEOUT_MS 1000 #define VOICE_WS_SEND_TIMEOUT_MS 1000 -#define VOICE_UPLINK_TASK_STACK 8192 +#define VOICE_WS_TASK_STACK 24576 +#define VOICE_UPLINK_TASK_STACK 32768 #define VOICE_HEARTBEAT_TASK_STACK 4096 #define VOICE_START_CONNECT_TIMEOUT_MS 8000 #define VOICE_AUDIO_IO_TIMEOUT_MS 1200 @@ -45,6 +48,8 @@ typedef struct { TaskHandle_t uplink_task; TaskHandle_t heartbeat_task; + bool uplink_task_with_caps; + bool heartbeat_task_with_caps; void *opus_enc; void *opus_dec; @@ -69,6 +74,7 @@ typedef struct { bool started; bool tap_active; bool stop_requested; + bool ws_low_stack_warned; voice_dialog_state_t dialog_state; @@ -112,6 +118,143 @@ static void voice_unlock(void) { } } +static void voice_log_status_snapshot(const char *stage) { + if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + ESP_LOGW(TAG, "[stage] %s: status snapshot lock timeout", stage); + return; + } + + ESP_LOGI(TAG, + "[stage] %s: active=%d ws=%d started=%d tap=%d state=%s up=%" PRIu32 " down=%" PRIu32 " err=%s", + stage, + s_voice.session_active, + s_voice.ws_connected, + s_voice.started, + s_voice.tap_active, + voice_interaction_dialog_state_str(s_voice.dialog_state), + s_voice.upstream_packets, + s_voice.downstream_packets, + s_voice.last_error[0] != '\0' ? s_voice.last_error : "-"); + voice_unlock(); +} + +static void voice_log_heap_snapshot(const char *stage) { + size_t free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + size_t free_spiram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + size_t largest_8bit = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); + size_t largest_internal = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + size_t largest_spiram = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + ESP_LOGI(TAG, + "[stage] %s heap: free8=%u free_internal=%u free_spiram=%u largest8=%u largest_internal=%u largest_spiram=%u", + stage, + (unsigned)free_8bit, + (unsigned)free_internal, + (unsigned)free_spiram, + (unsigned)largest_8bit, + (unsigned)largest_internal, + (unsigned)largest_spiram); +} + +static void *voice_malloc_prefer_psram(size_t size) { + void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (ptr == NULL) { + ptr = malloc(size); + } + return ptr; +} + +static void *voice_calloc_prefer_psram(size_t n, size_t size) { + void *ptr = heap_caps_calloc(n, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (ptr == NULL) { + ptr = calloc(n, size); + } + return ptr; +} + +static void *voice_realloc_prefer_psram(void *ptr, size_t size) { + void *new_ptr = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (new_ptr == NULL) { + new_ptr = realloc(ptr, size); + } + return new_ptr; +} + +static BaseType_t voice_create_task_prefer_psram(TaskFunction_t task_fn, + const char *name, + uint32_t stack_size, + UBaseType_t priority, + TaskHandle_t *out_task, + bool *out_with_caps) { + if (out_with_caps != NULL) { + *out_with_caps = false; + } + + BaseType_t rc = pdFAIL; + +#if defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) && \ + defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && \ + (configSUPPORT_STATIC_ALLOCATION == 1) + rc = xTaskCreatePinnedToCoreWithCaps(task_fn, + name, + (configSTACK_DEPTH_TYPE)stack_size, + NULL, + priority, + out_task, + tskNO_AFFINITY, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (rc == pdPASS) { + if (out_with_caps != NULL) { + *out_with_caps = true; + } + ESP_LOGI(TAG, "[stage] task_create_psram_ok: name=%s stack=%u", name, (unsigned)stack_size); + return pdPASS; + } + ESP_LOGW(TAG, "[stage] task_create_psram_failed: name=%s stack=%u", name, (unsigned)stack_size); +#endif + + rc = xTaskCreate(task_fn, + name, + (configSTACK_DEPTH_TYPE)stack_size, + NULL, + priority, + out_task); + if (rc == pdPASS) { + ESP_LOGI(TAG, "[stage] task_create_internal_ok: name=%s stack=%u", name, (unsigned)stack_size); + } else { + ESP_LOGE(TAG, "[stage] task_create_internal_failed: name=%s stack=%u", name, (unsigned)stack_size); + } + return rc; +} + +static void voice_delete_task(TaskHandle_t task, bool with_caps) { + if (task == NULL) { + return; + } + +#if (configSUPPORT_STATIC_ALLOCATION == 1) + if (with_caps) { + vTaskDeleteWithCaps(task); + return; + } +#else + (void)with_caps; +#endif + vTaskDelete(task); +} + +static void voice_delete_self_task(bool with_caps) { +#if (configSUPPORT_STATIC_ALLOCATION == 1) + if (with_caps) { + vTaskDeleteWithCaps(NULL); + return; + } +#else + (void)with_caps; +#endif + vTaskDelete(NULL); +} + static void voice_make_uuid(char out[40]) { uint8_t raw[16]; esp_fill_random(raw, sizeof(raw)); @@ -317,6 +460,7 @@ static esp_err_t voice_send_text(const char *text, int len) { voice_unlock(); if (ws == NULL || !connected) { + ESP_LOGW(TAG, "[stage] ws_send_text skipped: ws=%p connected=%d len=%d", (void *)ws, connected, len); return ESP_ERR_INVALID_STATE; } @@ -325,8 +469,10 @@ static esp_err_t voice_send_text(const char *text, int len) { len, pdMS_TO_TICKS(VOICE_WS_SEND_TIMEOUT_MS)); if (ret < 0) { + ESP_LOGW(TAG, "[stage] ws_send_text failed: len=%d", len); return ESP_FAIL; } + ESP_LOGI(TAG, "[stage] ws_send_text ok: len=%d", len); return ESP_OK; } @@ -345,6 +491,12 @@ static esp_err_t voice_send_directive(const char *action, } esp_err_t err = voice_send_text(text, (int)strlen(text)); + ESP_LOGI(TAG, + "[stage] directive: action=%s directive=%s with_dialog_id=%d result=%s", + action, + directive, + with_dialog_id, + esp_err_to_name(err)); cJSON_free(text); return err; } @@ -385,7 +537,7 @@ static void voice_handle_downstream_packet(const uint8_t *data, size_t len) { esp_audio_err_t ret = esp_opus_dec_decode(opus_dec, &raw, &out, &dec_info); if (ret == ESP_AUDIO_ERR_BUFF_NOT_ENOUGH) { if (out.needed_size > pcm_buf_size) { - uint8_t *new_buf = (uint8_t *)realloc(pcm_buf, out.needed_size); + uint8_t *new_buf = (uint8_t *)voice_realloc_prefer_psram(pcm_buf, out.needed_size); if (new_buf == NULL) { ESP_LOGE(TAG, "No memory to extend pcm rx buffer to %u", out.needed_size); break; @@ -441,6 +593,33 @@ static const char *voice_get_json_str(cJSON *obj, const char *name) { return NULL; } +static bool voice_get_json_bool(cJSON *obj, const char *name, bool *out_value) { + if (obj == NULL || name == NULL || out_value == NULL) { + return false; + } + cJSON *item = cJSON_GetObjectItemCaseSensitive(obj, name); + if (!cJSON_IsBool(item)) { + return false; + } + *out_value = cJSON_IsTrue(item); + return true; +} + +static void voice_log_final_text(const char *stage, const char *text) { + if (stage == NULL || text == NULL || text[0] == '\0') { + return; + } + size_t len = strlen(text); + const int preview = 240; + ESP_LOGI(TAG, + "[stage] %s: len=%u text=%.*s%s", + stage, + (unsigned)len, + preview, + text, + (len > (size_t)preview) ? "..." : ""); +} + static void voice_handle_output_event(cJSON *output) { const char *event_name = voice_get_json_str(output, "event"); if (event_name == NULL) { @@ -448,6 +627,29 @@ static void voice_handle_output_event(cJSON *output) { } const char *dialog_id = voice_get_json_str(output, "dialog_id"); + const char *state = NULL; + if (strcmp(event_name, "DialogStateChanged") == 0) { + state = voice_get_json_str(output, "state"); + } + ESP_LOGI(TAG, + "[stage] ws_event_output: event=%s dialog_id=%s state=%s", + event_name, + dialog_id != NULL ? dialog_id : "-", + state != NULL ? state : "-"); + + bool finished = false; + bool has_finished = voice_get_json_bool(output, "finished", &finished); + if (has_finished && finished) { + if (strcmp(event_name, "SpeechContent") == 0) { + voice_log_final_text("asr_final_text", voice_get_json_str(output, "text")); + } else if (strcmp(event_name, "RespondingContent") == 0) { + const char *final_text = voice_get_json_str(output, "text"); + if (final_text == NULL || final_text[0] == '\0') { + final_text = voice_get_json_str(output, "spoken"); + } + voice_log_final_text("response_final_text", final_text); + } + } if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { if (dialog_id != NULL) { @@ -458,8 +660,8 @@ static void voice_handle_output_event(cJSON *output) { if (strcmp(event_name, "Started") == 0) { s_voice.started = true; s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE; + ESP_LOGI(TAG, "[stage] session_started: dialog_id=%s", s_voice.dialog_id); } else if (strcmp(event_name, "DialogStateChanged") == 0) { - const char *state = voice_get_json_str(output, "state"); if (state != NULL) { if (strcmp(state, "Listening") == 0) { s_voice.dialog_state = VOICE_DIALOG_STATE_LISTENING; @@ -469,16 +671,22 @@ static void voice_handle_output_event(cJSON *output) { s_voice.dialog_state = VOICE_DIALOG_STATE_RESPONDING; } } + ESP_LOGI(TAG, + "[stage] dialog_state_changed: state=%s current=%s", + state != NULL ? state : "-", + voice_interaction_dialog_state_str(s_voice.dialog_state)); } else if (strcmp(event_name, "SpeechEnded") == 0) { - s_voice.tap_active = false; + ESP_LOGI(TAG, "[stage] speech_ended: keep tap_active=%d for continuous rounds", s_voice.tap_active); } else if (strcmp(event_name, "Stopped") == 0) { s_voice.started = false; s_voice.tap_active = false; s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE; + ESP_LOGI(TAG, "[stage] session_stopped_by_server"); } else if (strcmp(event_name, "Error") == 0) { const char *error_msg = voice_get_json_str(output, "error_message"); if (error_msg != NULL) { voice_set_last_error_locked(error_msg); + ESP_LOGW(TAG, "[stage] server_error: %s", error_msg); } s_voice.tap_active = false; } @@ -512,6 +720,10 @@ static void voice_handle_text_message(const char *text, size_t len) { cJSON *status_code = cJSON_GetObjectItemCaseSensitive(header, "status_code"); cJSON *status_msg = cJSON_GetObjectItemCaseSensitive(header, "status_message"); if (cJSON_IsNumber(status_code) && status_code->valueint >= 400) { + ESP_LOGW(TAG, + "[stage] ws_header_error: status_code=%d status_message=%s", + status_code->valueint, + (cJSON_IsString(status_msg) && status_msg->valuestring != NULL) ? status_msg->valuestring : "-"); if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { if (cJSON_IsString(status_msg) && status_msg->valuestring != NULL) { voice_set_last_error_locked(status_msg->valuestring); @@ -527,6 +739,7 @@ static void voice_handle_text_message(const char *text, size_t len) { voice_handle_output_event(output); } else if (header_event != NULL && strcmp(header_event, "task-failed") == 0) { const char *error_msg = voice_get_json_str(header, "error_message"); + ESP_LOGW(TAG, "[stage] ws_task_failed: %s", error_msg != NULL ? error_msg : "task failed"); if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_set_last_error_locked(error_msg != NULL ? error_msg : "task failed"); voice_unlock(); @@ -546,8 +759,9 @@ static void voice_handle_text_chunk(esp_websocket_event_data_t *data) { return; } - s_voice.text_agg = (uint8_t *)calloc(1, (size_t)data->payload_len + 1); + s_voice.text_agg = (uint8_t *)voice_calloc_prefer_psram(1, (size_t)data->payload_len + 1); if (s_voice.text_agg == NULL) { + ESP_LOGW(TAG, "[stage] text_agg_alloc_failed: len=%d", data->payload_len); return; } s_voice.text_agg_size = (size_t)data->payload_len; @@ -588,8 +802,9 @@ static void voice_handle_binary_chunk(esp_websocket_event_data_t *data) { return; } - s_voice.bin_agg = (uint8_t *)malloc((size_t)data->payload_len); + s_voice.bin_agg = (uint8_t *)voice_malloc_prefer_psram((size_t)data->payload_len); if (s_voice.bin_agg == NULL) { + ESP_LOGW(TAG, "[stage] bin_agg_alloc_failed: len=%d", data->payload_len); return; } s_voice.bin_agg_size = (size_t)data->payload_len; @@ -627,10 +842,16 @@ static void voice_websocket_event_handler(void *handler_args, (void)handler_args; (void)base; + UBaseType_t ws_hwm = uxTaskGetStackHighWaterMark(NULL); + if (!s_voice.ws_low_stack_warned && ws_hwm < 256) { + s_voice.ws_low_stack_warned = true; + ESP_LOGW(TAG, "[stage] ws_task_low_stack: hwm_words=%u", (unsigned)ws_hwm); + } + esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; switch ((esp_websocket_event_id_t)event_id) { case WEBSOCKET_EVENT_CONNECTED: { - ESP_LOGI(TAG, "websocket connected"); + ESP_LOGI(TAG, "[stage] ws_connected"); if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { s_voice.ws_connected = true; s_voice.last_event_ms = voice_now_ms(); @@ -643,13 +864,16 @@ static void voice_websocket_event_handler(void *handler_args, voice_unlock(); } if (start_msg != NULL) { - (void)voice_send_text(start_msg, (int)strlen(start_msg)); + esp_err_t send_err = voice_send_text(start_msg, (int)strlen(start_msg)); + ESP_LOGI(TAG, "[stage] start_message_sent: result=%s", esp_err_to_name(send_err)); cJSON_free(start_msg); + } else { + ESP_LOGE(TAG, "[stage] start_message_build_failed"); } break; } case WEBSOCKET_EVENT_DISCONNECTED: - ESP_LOGW(TAG, "websocket disconnected"); + ESP_LOGW(TAG, "[stage] ws_disconnected"); if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { s_voice.ws_connected = false; s_voice.started = false; @@ -670,6 +894,7 @@ static void voice_websocket_event_handler(void *handler_args, } break; case WEBSOCKET_EVENT_ERROR: + ESP_LOGW(TAG, "[stage] ws_error"); if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_set_last_error_locked("websocket error"); s_voice.last_event_ms = voice_now_ms(); @@ -685,8 +910,19 @@ static void voice_uplink_task(void *arg) { (void)arg; const int frame_ms = CONFIG_TQ_VOICE_OPUS_FRAME_MS; + bool low_stack_warned = false; + UBaseType_t start_hwm = uxTaskGetStackHighWaterMark(NULL); + ESP_LOGI(TAG, "[stage] uplink_task_started: stack_hwm_words=%u", (unsigned)start_hwm); while (true) { + if (!low_stack_warned) { + UBaseType_t hwm = uxTaskGetStackHighWaterMark(NULL); + if (hwm < 256) { + low_stack_warned = true; + ESP_LOGW(TAG, "[stage] uplink_task_low_stack: hwm_words=%u", (unsigned)hwm); + } + } + bool active = false; bool connected = false; bool started = false; @@ -762,11 +998,14 @@ static void voice_uplink_task(void *arg) { } } + bool delete_with_caps = false; if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + delete_with_caps = s_voice.uplink_task_with_caps; s_voice.uplink_task = NULL; + s_voice.uplink_task_with_caps = false; voice_unlock(); } - vTaskDelete(NULL); + voice_delete_self_task(delete_with_caps); } static void voice_heartbeat_task(void *arg) { @@ -789,17 +1028,21 @@ static void voice_heartbeat_task(void *arg) { } if (connected && started) { + ESP_LOGI(TAG, "[stage] heartbeat_send"); (void)voice_send_directive("continue-task", "HeartBeat", true); } vTaskDelay(pdMS_TO_TICKS(CONFIG_TQ_VOICE_HEARTBEAT_SEC * 1000)); } + bool delete_with_caps = false; if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + delete_with_caps = s_voice.heartbeat_task_with_caps; s_voice.heartbeat_task = NULL; + s_voice.heartbeat_task_with_caps = false; voice_unlock(); } - vTaskDelete(NULL); + voice_delete_self_task(delete_with_caps); } static esp_err_t voice_setup_opus(char *err, size_t err_len) { @@ -841,8 +1084,8 @@ static esp_err_t voice_setup_opus(char *err, size_t err_len) { return ESP_FAIL; } - s_voice.pcm_tx_buf = (int16_t *)malloc((size_t)s_voice.opus_enc_in_size); - s_voice.opus_tx_buf = (uint8_t *)malloc((size_t)s_voice.opus_enc_out_size); + s_voice.pcm_tx_buf = (int16_t *)voice_malloc_prefer_psram((size_t)s_voice.opus_enc_in_size); + s_voice.opus_tx_buf = (uint8_t *)voice_malloc_prefer_psram((size_t)s_voice.opus_enc_out_size); if (s_voice.pcm_tx_buf == NULL || s_voice.opus_tx_buf == NULL) { voice_fill_err(err, err_len, "no memory for opus tx buffers"); return ESP_ERR_NO_MEM; @@ -870,7 +1113,7 @@ static esp_err_t voice_setup_opus(char *err, size_t err_len) { rx_pcm_size = 2048; } - s_voice.pcm_rx_buf = (uint8_t *)malloc(rx_pcm_size); + s_voice.pcm_rx_buf = (uint8_t *)voice_malloc_prefer_psram(rx_pcm_size); if (s_voice.pcm_rx_buf == NULL) { voice_fill_err(err, err_len, "no memory for opus rx buffer"); return ESP_ERR_NO_MEM; @@ -931,8 +1174,10 @@ esp_err_t voice_interaction_init(void) { } esp_err_t voice_interaction_start(char *err, size_t err_len) { + ESP_LOGI(TAG, "[stage] session_start_enter"); if (!s_voice.ready) { voice_fill_err(err, err_len, "voice interaction not initialized"); + ESP_LOGW(TAG, "[stage] session_start_abort: not initialized"); return ESP_ERR_INVALID_STATE; } @@ -940,17 +1185,28 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { strlen(CONFIG_TQ_VOICE_WORKSPACE_ID) == 0 || strlen(CONFIG_TQ_VOICE_APP_ID) == 0) { voice_fill_err(err, err_len, "voice API key/workspace/app_id is missing"); + ESP_LOGW(TAG, "[stage] session_start_abort: config missing"); return ESP_ERR_INVALID_STATE; } + ESP_LOGI(TAG, + "[stage] session_config_ok: ws_uri=%s sample_rate=%d frame_ms=%d bitrate_kbps=%d ws_stack=%d", + CONFIG_TQ_VOICE_WS_URI, + CONFIG_TQ_VOICE_SAMPLE_RATE, + CONFIG_TQ_VOICE_OPUS_FRAME_MS, + CONFIG_TQ_VOICE_OPUS_BITRATE_KBPS, + VOICE_WS_TASK_STACK); if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_fill_err(err, err_len, "voice lock timeout"); + ESP_LOGW(TAG, "[stage] session_start_abort: lock timeout"); return ESP_ERR_TIMEOUT; } bool already_active = s_voice.session_active; voice_unlock(); if (already_active) { + ESP_LOGI(TAG, "[stage] session_start_skip: already active"); + voice_log_status_snapshot("already_active"); return ESP_OK; } @@ -962,12 +1218,16 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { .input_gain_db = (float)CONFIG_TQ_VOICE_CODEC_MIC_GAIN_DB, }; + ESP_LOGI(TAG, "[stage] audio_open_begin"); ESP_RETURN_ON_ERROR(voice_audio_open(&audio_cfg, err, err_len), TAG, "voice audio open failed"); + ESP_LOGI(TAG, "[stage] audio_open_ok"); + ESP_LOGI(TAG, "[stage] opus_setup_begin"); esp_err_t opus_setup_err = voice_setup_opus(err, err_len); if (opus_setup_err != ESP_OK) { ESP_LOGE(TAG, "voice opus setup failed: %s", esp_err_to_name(opus_setup_err)); goto exit_err; } + ESP_LOGI(TAG, "[stage] opus_setup_ok"); if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_make_uuid(s_voice.task_id); @@ -979,7 +1239,14 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE; s_voice.stop_requested = false; s_voice.tap_active = false; + s_voice.ws_low_stack_warned = false; + s_voice.uplink_task_with_caps = false; + s_voice.heartbeat_task_with_caps = false; s_voice.last_event_ms = voice_now_ms(); + ESP_LOGI(TAG, + "[stage] session_context_prepared: task_id=%s device_uuid=%s", + s_voice.task_id, + s_voice.device_uuid); voice_unlock(); } @@ -987,7 +1254,7 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { .uri = CONFIG_TQ_VOICE_WS_URI, .task_name = "voice_ws", .task_prio = 5, - .task_stack = 8192, + .task_stack = VOICE_WS_TASK_STACK, .buffer_size = 2048, .network_timeout_ms = 10000, .reconnect_timeout_ms = 3000, @@ -999,42 +1266,54 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { #endif }; + ESP_LOGI(TAG, "[stage] ws_init_begin"); esp_websocket_client_handle_t ws = esp_websocket_client_init(&ws_cfg); if (ws == NULL) { voice_fill_err(err, err_len, "websocket init failed"); + ESP_LOGE(TAG, "[stage] ws_init_failed"); goto exit_err; } + ESP_LOGI(TAG, "[stage] ws_init_ok"); size_t auth_len = strlen(CONFIG_TQ_VOICE_API_KEY) + 8; char *auth_value = (char *)malloc(auth_len); if (auth_value == NULL) { voice_fill_err(err, err_len, "no memory for auth header"); + ESP_LOGE(TAG, "[stage] ws_auth_header_alloc_failed"); esp_websocket_client_destroy(ws); goto exit_err; } snprintf(auth_value, auth_len, "Bearer %s", CONFIG_TQ_VOICE_API_KEY); esp_websocket_client_append_header(ws, "Authorization", auth_value); free(auth_value); + ESP_LOGI(TAG, "[stage] ws_auth_header_set"); + ESP_LOGI(TAG, "[stage] ws_register_events_begin"); esp_err_t reg_err = esp_websocket_register_events(ws, WEBSOCKET_EVENT_ANY, voice_websocket_event_handler, NULL); if (reg_err != ESP_OK) { voice_fill_err(err, err_len, "websocket register events failed"); + ESP_LOGE(TAG, "[stage] ws_register_events_failed: %s", esp_err_to_name(reg_err)); esp_websocket_client_destroy(ws); goto exit_err; } + ESP_LOGI(TAG, "[stage] ws_register_events_ok"); + ESP_LOGI(TAG, "[stage] ws_start_begin"); esp_err_t ws_start_err = esp_websocket_client_start(ws); if (ws_start_err != ESP_OK) { voice_fill_err(err, err_len, "websocket start failed"); + ESP_LOGE(TAG, "[stage] ws_start_failed: %s", esp_err_to_name(ws_start_err)); esp_websocket_client_destroy(ws); goto exit_err; } + ESP_LOGI(TAG, "[stage] ws_start_ok"); if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_fill_err(err, err_len, "voice lock timeout"); + ESP_LOGE(TAG, "[stage] session_start_abort: lock timeout after ws_start"); esp_websocket_client_stop(ws); esp_websocket_client_destroy(ws); goto exit_err; @@ -1044,31 +1323,44 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { s_voice.ws_connected = false; s_voice.started = false; voice_unlock(); + voice_log_status_snapshot("session_marked_active"); + voice_log_heap_snapshot("before_create_uplink"); - BaseType_t ok = xTaskCreate(voice_uplink_task, - "voice_uplink", - VOICE_UPLINK_TASK_STACK, - NULL, - 5, - &s_voice.uplink_task); + ESP_LOGI(TAG, "[stage] uplink_task_create_begin"); + BaseType_t ok = voice_create_task_prefer_psram(voice_uplink_task, + "voice_uplink", + VOICE_UPLINK_TASK_STACK, + 5, + &s_voice.uplink_task, + &s_voice.uplink_task_with_caps); if (ok != pdPASS) { voice_fill_err(err, err_len, "create uplink task failed"); + ESP_LOGE(TAG, "[stage] uplink_task_create_failed"); + voice_log_heap_snapshot("uplink_create_failed"); (void)voice_interaction_stop(NULL, 0); return ESP_ERR_NO_MEM; } + ESP_LOGI(TAG, "[stage] uplink_task_create_ok"); + voice_log_heap_snapshot("after_create_uplink"); - ok = xTaskCreate(voice_heartbeat_task, - "voice_hb", - VOICE_HEARTBEAT_TASK_STACK, - NULL, - 4, - &s_voice.heartbeat_task); + ESP_LOGI(TAG, "[stage] heartbeat_task_create_begin"); + ok = voice_create_task_prefer_psram(voice_heartbeat_task, + "voice_hb", + VOICE_HEARTBEAT_TASK_STACK, + 4, + &s_voice.heartbeat_task, + &s_voice.heartbeat_task_with_caps); if (ok != pdPASS) { voice_fill_err(err, err_len, "create heartbeat task failed"); + ESP_LOGE(TAG, "[stage] heartbeat_task_create_failed"); + voice_log_heap_snapshot("heartbeat_create_failed"); (void)voice_interaction_stop(NULL, 0); return ESP_ERR_NO_MEM; } + ESP_LOGI(TAG, "[stage] heartbeat_task_create_ok"); + voice_log_heap_snapshot("after_create_heartbeat"); + ESP_LOGI(TAG, "[stage] wait_ws_connected_begin: timeout_ms=%d", VOICE_START_CONNECT_TIMEOUT_MS); int64_t deadline = voice_now_ms() + VOICE_START_CONNECT_TIMEOUT_MS; while (voice_now_ms() < deadline) { bool connected = false; @@ -1084,6 +1376,7 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_fill_err(err, err_len, "voice lock timeout"); + ESP_LOGE(TAG, "[stage] wait_ws_connected_abort: lock timeout"); (void)voice_interaction_stop(NULL, 0); return ESP_ERR_TIMEOUT; } @@ -1091,13 +1384,18 @@ esp_err_t voice_interaction_start(char *err, size_t err_len) { voice_unlock(); if (!connected) { voice_fill_err(err, err_len, "websocket connect timeout"); + ESP_LOGE(TAG, "[stage] wait_ws_connected_timeout"); + voice_log_status_snapshot("wait_ws_connected_timeout"); (void)voice_interaction_stop(NULL, 0); return ESP_ERR_TIMEOUT; } + ESP_LOGI(TAG, "[stage] wait_ws_connected_ok"); + voice_log_status_snapshot("session_start_exit_ok"); return ESP_OK; exit_err: + ESP_LOGE(TAG, "[stage] session_start_exit_err: %s", (err != NULL && err[0] != '\0') ? err : "unknown"); voice_release_runtime_resources(); voice_audio_close(); return ESP_FAIL; @@ -1106,17 +1404,21 @@ exit_err: esp_err_t voice_interaction_stop(char *err, size_t err_len) { (void)err; (void)err_len; + ESP_LOGI(TAG, "[stage] session_stop_enter"); if (!s_voice.ready) { + ESP_LOGW(TAG, "[stage] session_stop_abort: not initialized"); return ESP_ERR_INVALID_STATE; } if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + ESP_LOGW(TAG, "[stage] session_stop_abort: lock timeout"); return ESP_ERR_TIMEOUT; } if (!s_voice.session_active) { voice_unlock(); + ESP_LOGI(TAG, "[stage] session_stop_skip: not active"); return ESP_OK; } @@ -1124,34 +1426,37 @@ esp_err_t voice_interaction_stop(char *err, size_t err_len) { bool ws_connected = s_voice.ws_connected; TaskHandle_t uplink_task = s_voice.uplink_task; TaskHandle_t heartbeat_task = s_voice.heartbeat_task; + bool uplink_task_with_caps = s_voice.uplink_task_with_caps; + bool heartbeat_task_with_caps = s_voice.heartbeat_task_with_caps; esp_websocket_client_handle_t ws = s_voice.ws; s_voice.tap_active = false; voice_unlock(); + voice_log_status_snapshot("session_stop_requested"); if (ws_connected) { (void)voice_send_directive("finish-task", "Stop", true); } - if (uplink_task != NULL) { - vTaskDelete(uplink_task); - } - if (heartbeat_task != NULL) { - vTaskDelete(heartbeat_task); - } + voice_delete_task(uplink_task, uplink_task_with_caps); + voice_delete_task(heartbeat_task, heartbeat_task_with_caps); if (ws != NULL) { + ESP_LOGI(TAG, "[stage] ws_stop_destroy_begin"); esp_websocket_unregister_events(ws, WEBSOCKET_EVENT_ANY, voice_websocket_event_handler); esp_websocket_client_stop(ws); esp_websocket_client_destroy(ws); + ESP_LOGI(TAG, "[stage] ws_stop_destroy_ok"); } if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { s_voice.ws = NULL; s_voice.uplink_task = NULL; s_voice.heartbeat_task = NULL; + s_voice.uplink_task_with_caps = false; + s_voice.heartbeat_task_with_caps = false; s_voice.ws_connected = false; s_voice.started = false; s_voice.session_active = false; @@ -1160,19 +1465,28 @@ esp_err_t voice_interaction_stop(char *err, size_t err_len) { s_voice.dialog_id[0] = '\0'; voice_unlock(); } + voice_log_status_snapshot("session_stop_cleared"); voice_release_runtime_resources(); voice_audio_close(); + ESP_LOGI(TAG, "[stage] session_stop_exit_ok"); return ESP_OK; } esp_err_t voice_interaction_tap_start(char *err, size_t err_len) { + ESP_LOGI(TAG, "[stage] tap_start_enter"); if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_fill_err(err, err_len, "voice lock timeout"); + ESP_LOGW(TAG, "[stage] tap_start_abort: lock timeout"); return ESP_ERR_TIMEOUT; } if (!s_voice.session_active || !s_voice.ws_connected || !s_voice.started) { + ESP_LOGW(TAG, + "[stage] tap_start_abort: session_active=%d ws_connected=%d started=%d", + s_voice.session_active, + s_voice.ws_connected, + s_voice.started); voice_unlock(); voice_fill_err(err, err_len, "voice session not ready"); return ESP_ERR_INVALID_STATE; @@ -1181,21 +1495,30 @@ esp_err_t voice_interaction_tap_start(char *err, size_t err_len) { bool listening = (s_voice.dialog_state == VOICE_DIALOG_STATE_LISTENING); s_voice.tap_active = true; voice_unlock(); + ESP_LOGI(TAG, "[stage] tap_start_marked_active: listening=%d", listening); if (!listening) { (void)voice_send_directive("continue-task", "RequestToSpeak", true); } + voice_log_status_snapshot("tap_start_exit_ok"); return ESP_OK; } esp_err_t voice_interaction_tap_cancel(char *err, size_t err_len) { + ESP_LOGI(TAG, "[stage] tap_cancel_enter"); if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_fill_err(err, err_len, "voice lock timeout"); + ESP_LOGW(TAG, "[stage] tap_cancel_abort: lock timeout"); return ESP_ERR_TIMEOUT; } if (!s_voice.session_active || !s_voice.ws_connected || !s_voice.started) { + ESP_LOGW(TAG, + "[stage] tap_cancel_abort: session_active=%d ws_connected=%d started=%d", + s_voice.session_active, + s_voice.ws_connected, + s_voice.started); voice_unlock(); voice_fill_err(err, err_len, "voice session not ready"); return ESP_ERR_INVALID_STATE; @@ -1203,6 +1526,7 @@ esp_err_t voice_interaction_tap_cancel(char *err, size_t err_len) { s_voice.tap_active = false; voice_unlock(); + voice_log_status_snapshot("tap_cancel_marked_inactive"); return voice_send_directive("continue-task", "CancelSpeech", true); } diff --git a/main/platform/src/voice_audio.c b/main/platform/src/voice_audio.c index 76bd22d..7e06594 100644 --- a/main/platform/src/voice_audio.c +++ b/main/platform/src/voice_audio.c @@ -22,6 +22,9 @@ #endif static const char *TAG = "voice_audio"; +static const uint32_t VOICE_CODEC_I2C_PROBE_TIMEOUT_MS = 50; + +#define ES8311_CODEC_ALT_ADDR ((uint8_t)(ES8311_CODEC_DEFAULT_ADDR + 0x02)) #ifdef CONFIG_TQ_VOICE_CODEC_PA_PIN_REVERSED #define VOICE_CODEC_PA_REVERSED true @@ -56,6 +59,66 @@ static void fill_err(char *err, size_t err_len, const char *msg) { } } +static bool voice_audio_i2c_probe_addr(uint8_t addr_8bit) { +#ifdef VOICE_USE_IDF_I2C_MASTER + if (s_i2c_bus == NULL || addr_8bit == 0) { + return false; + } + uint16_t addr_7bit = (uint16_t)(addr_8bit >> 1); + return i2c_master_probe(s_i2c_bus, addr_7bit, (int)VOICE_CODEC_I2C_PROBE_TIMEOUT_MS) == ESP_OK; +#else + if (addr_8bit == 0) { + return false; + } + + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + if (cmd == NULL) { + return false; + } + + i2c_master_start(cmd); + i2c_master_write_byte(cmd, addr_8bit, true); + i2c_master_stop(cmd); + + esp_err_t err = i2c_master_cmd_begin(CONFIG_TQ_VOICE_I2C_PORT, + cmd, + pdMS_TO_TICKS(VOICE_CODEC_I2C_PROBE_TIMEOUT_MS)); + i2c_cmd_link_delete(cmd); + return err == ESP_OK; +#endif +} + +static uint8_t voice_audio_resolve_codec_addr(uint8_t preferred_addr) { + uint8_t candidates[3] = {preferred_addr, ES8311_CODEC_DEFAULT_ADDR, ES8311_CODEC_ALT_ADDR}; + for (size_t i = 0; i < 3; ++i) { + uint8_t addr = candidates[i]; + bool duplicate = false; + for (size_t j = 0; j < i; ++j) { + if (candidates[j] == addr) { + duplicate = true; + break; + } + } + if (duplicate) { + continue; + } + + if (voice_audio_i2c_probe_addr(addr)) { + return addr; + } + } + + ESP_LOGW(TAG, + "ES8311 not found on I2C(port=%d,sda=%d,scl=%d), tried addrs:0x%02x/0x%02x/0x%02x", + CONFIG_TQ_VOICE_I2C_PORT, + CONFIG_TQ_VOICE_I2C_SDA_PIN, + CONFIG_TQ_VOICE_I2C_SCL_PIN, + (unsigned)preferred_addr, + (unsigned)ES8311_CODEC_DEFAULT_ADDR, + (unsigned)ES8311_CODEC_ALT_ADDR); + return preferred_addr; +} + static esp_err_t voice_audio_init_i2c(void) { #ifdef VOICE_USE_IDF_I2C_MASTER if (s_i2c_bus != NULL) { @@ -71,7 +134,15 @@ static esp_err_t voice_audio_init_i2c(void) { .flags.enable_internal_pullup = true, }; - return i2c_new_master_bus(&bus_cfg, &s_i2c_bus); + esp_err_t err = i2c_new_master_bus(&bus_cfg, &s_i2c_bus); + if (err == ESP_OK) { + ESP_LOGI(TAG, + "voice i2c ready: port=%d sda=%d scl=%d", + CONFIG_TQ_VOICE_I2C_PORT, + CONFIG_TQ_VOICE_I2C_SDA_PIN, + CONFIG_TQ_VOICE_I2C_SCL_PIN); + } + return err; #else i2c_config_t i2c_cfg = { .mode = I2C_MODE_MASTER, @@ -83,7 +154,15 @@ static esp_err_t voice_audio_init_i2c(void) { }; ESP_RETURN_ON_ERROR(i2c_param_config(CONFIG_TQ_VOICE_I2C_PORT, &i2c_cfg), TAG, "i2c_param_config failed"); - return i2c_driver_install(CONFIG_TQ_VOICE_I2C_PORT, i2c_cfg.mode, 0, 0, 0); + esp_err_t err = i2c_driver_install(CONFIG_TQ_VOICE_I2C_PORT, i2c_cfg.mode, 0, 0, 0); + if (err == ESP_OK) { + ESP_LOGI(TAG, + "voice i2c ready: port=%d sda=%d scl=%d", + CONFIG_TQ_VOICE_I2C_PORT, + CONFIG_TQ_VOICE_I2C_SDA_PIN, + CONFIG_TQ_VOICE_I2C_SCL_PIN); + } + return err; #endif } @@ -136,9 +215,27 @@ static esp_err_t voice_audio_build_codec_dev(void) { return ESP_OK; } + uint8_t codec_addr = voice_audio_resolve_codec_addr((uint8_t)CONFIG_TQ_VOICE_CODEC_ADDR); + if (codec_addr != (uint8_t)CONFIG_TQ_VOICE_CODEC_ADDR) { + ESP_LOGW(TAG, + "override ES8311 I2C addr 0x%02x -> 0x%02x", + (unsigned)CONFIG_TQ_VOICE_CODEC_ADDR, + (unsigned)codec_addr); + } + ESP_LOGI(TAG, + "voice codec cfg: i2c_port=%d addr=0x%02x i2s_port=%d mclk=%d bclk=%d ws=%d dout=%d din=%d", + CONFIG_TQ_VOICE_I2C_PORT, + (unsigned)codec_addr, + CONFIG_TQ_VOICE_I2S_PORT, + CONFIG_TQ_VOICE_I2S_MCLK_PIN, + CONFIG_TQ_VOICE_I2S_BCLK_PIN, + CONFIG_TQ_VOICE_I2S_WS_PIN, + CONFIG_TQ_VOICE_I2S_DOUT_PIN, + CONFIG_TQ_VOICE_I2S_DIN_PIN); + audio_codec_i2c_cfg_t i2c_cfg = { .port = (uint8_t)CONFIG_TQ_VOICE_I2C_PORT, - .addr = (uint8_t)CONFIG_TQ_VOICE_CODEC_ADDR, + .addr = codec_addr, #ifdef VOICE_USE_IDF_I2C_MASTER .bus_handle = s_i2c_bus, #endif @@ -180,7 +277,8 @@ static esp_err_t voice_audio_build_codec_dev(void) { s_codec_if = es8311_codec_new(&codec_cfg); if (s_codec_if == NULL) { - return ESP_ERR_NO_MEM; + ESP_LOGE(TAG, "es8311_codec_new failed, check I2C wiring/pull-up/address"); + return ESP_FAIL; } esp_codec_dev_cfg_t dev_cfg = { @@ -230,7 +328,11 @@ esp_err_t voice_audio_open(const voice_audio_open_config_t *cfg, char *err, size esp_err_t init_err = voice_audio_prepare(cfg); if (init_err != ESP_OK) { - fill_err(err, err_len, "voice audio peripheral init failed"); + if (init_err == ESP_FAIL) { + fill_err(err, err_len, "es8311 init failed, check i2c wiring/address"); + } else { + fill_err(err, err_len, "voice audio peripheral init failed"); + } xSemaphoreGive(s_lock); return init_err; } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 9ef8d5e..883fbf0 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -22,27 +22,29 @@ CONFIG_SPIRAM_SPEED_80M=y CONFIG_SPIRAM_BOOT_INIT=y CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y -CONFIG_TQ_VOICE_API_KEY="" +CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y +CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y +CONFIG_TQ_VOICE_API_KEY="sk-7a50eca6856d4afb968ac3bf512f6d1b" CONFIG_TQ_VOICE_WS_URI="wss://dashscope.aliyuncs.com/api-ws/v1/inference" -CONFIG_TQ_VOICE_WORKSPACE_ID="" -CONFIG_TQ_VOICE_APP_ID="" +CONFIG_TQ_VOICE_WORKSPACE_ID="llm-igyrxzdvbwja5gyt" +CONFIG_TQ_VOICE_APP_ID="fe83b756f6634dcc842c6e7f356c0cf8" CONFIG_TQ_VOICE_USER_ID="esp32-user" CONFIG_TQ_VOICE_DEVICE_UUID="" -CONFIG_TQ_VOICE_TTS_VOICE="" +CONFIG_TQ_VOICE_TTS_VOICE="longling_v3" CONFIG_TQ_VOICE_SAMPLE_RATE=16000 CONFIG_TQ_VOICE_OPUS_FRAME_MS=100 CONFIG_TQ_VOICE_OPUS_BITRATE_KBPS=32 CONFIG_TQ_VOICE_HEARTBEAT_SEC=50 -CONFIG_TQ_VOICE_I2C_PORT=0 -CONFIG_TQ_VOICE_I2C_SDA_PIN=17 -CONFIG_TQ_VOICE_I2C_SCL_PIN=18 +CONFIG_TQ_VOICE_I2C_PORT=1 +CONFIG_TQ_VOICE_I2C_SDA_PIN=4 +CONFIG_TQ_VOICE_I2C_SCL_PIN=5 CONFIG_TQ_VOICE_CODEC_ADDR=48 CONFIG_TQ_VOICE_I2S_PORT=0 -CONFIG_TQ_VOICE_I2S_MCLK_PIN=16 -CONFIG_TQ_VOICE_I2S_BCLK_PIN=9 -CONFIG_TQ_VOICE_I2S_WS_PIN=45 -CONFIG_TQ_VOICE_I2S_DOUT_PIN=8 -CONFIG_TQ_VOICE_I2S_DIN_PIN=10 +CONFIG_TQ_VOICE_I2S_MCLK_PIN=39 +CONFIG_TQ_VOICE_I2S_BCLK_PIN=40 +CONFIG_TQ_VOICE_I2S_WS_PIN=41 +CONFIG_TQ_VOICE_I2S_DOUT_PIN=38 +CONFIG_TQ_VOICE_I2S_DIN_PIN=42 CONFIG_TQ_VOICE_CODEC_PA_PIN=48 CONFIG_TQ_VOICE_CODEC_OUT_VOL=70 CONFIG_TQ_VOICE_CODEC_MIC_GAIN_DB=24