语音对话
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 =
|
||||
" </div>\n"
|
||||
" <div class='small'>Submit prompt only: ESP32-S3 calls DashScope Z-Image, downloads PNG, rasterizes and prints.</div>\n"
|
||||
"\n"
|
||||
" <label>Voice Tap2Talk</label>\n"
|
||||
" <div class='row'>\n"
|
||||
" <button id='btnVoiceTalk' class='talk'>点击开始对话</button>\n"
|
||||
" </div>\n"
|
||||
" <div class='small'>点击一次开始语音输入,再点击一次结束(自动建立语音会话)。</div>\n"
|
||||
"\n"
|
||||
" <div class='small'>Tip: connect printer first, then submit print jobs.</div>\n"
|
||||
" <pre id='out'>Ready.</pre>\n"
|
||||
" </div>\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"
|
||||
" </script>\n"
|
||||
"</body>\n"
|
||||
"</html>\n";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user