feat(voice): migrate to push2talk and serialize websocket text sends

This commit is contained in:
admin
2026-03-01 01:21:18 +08:00
parent 9deeab738e
commit dd108e2658
19 changed files with 891 additions and 224 deletions

View File

@@ -88,7 +88,7 @@ static char *voice_build_start_message(void) {
cJSON *parameters = cJSON_AddObjectToObject(payload, "parameters");
cJSON *upstream = cJSON_AddObjectToObject(parameters, "upstream");
cJSON_AddStringToObject(upstream, "type", "AudioOnly");
cJSON_AddStringToObject(upstream, "mode", "tap2talk");
cJSON_AddStringToObject(upstream, "mode", "push2talk");
cJSON_AddStringToObject(upstream, "audio_format", "raw-opus");
cJSON_AddNumberToObject(upstream, "sample_rate", CONFIG_TQ_VOICE_SAMPLE_RATE);
@@ -113,7 +113,7 @@ static char *voice_build_start_message(void) {
return text;
}
static esp_err_t voice_send_text(const char *text, int len) {
static esp_err_t voice_send_text_immediate(const char *text, int len) {
if (text == NULL || len <= 0) {
return ESP_ERR_INVALID_ARG;
}
@@ -132,6 +132,7 @@ static esp_err_t voice_send_text(const char *text, int len) {
s_voice.ws_connected = false;
s_voice.started = false;
s_voice.tap_active = false;
s_voice.speech_active = false;
s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE;
s_voice.last_event_ms = voice_now_ms();
ESP_LOGW(TAG, "[stage] ws_state_reconciled: transport disconnected");
@@ -156,6 +157,43 @@ static esp_err_t voice_send_text(const char *text, int len) {
return ESP_OK;
}
static esp_err_t voice_send_text(const char *text, int len) {
if (text == NULL || len <= 0) {
return ESP_ERR_INVALID_ARG;
}
QueueHandle_t queue = NULL;
TaskHandle_t ws_text_task = NULL;
bool session_active = false;
if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
queue = s_voice.ws_text_queue;
ws_text_task = s_voice.ws_text_task;
session_active = s_voice.session_active;
voice_unlock();
}
if (queue == NULL || ws_text_task == NULL || !session_active) {
return voice_send_text_immediate(text, len);
}
char *copy = (char *)voice_malloc_prefer_psram((size_t)len + 1);
if (copy == NULL) {
return ESP_ERR_NO_MEM;
}
memcpy(copy, text, (size_t)len);
copy[len] = '\0';
voice_ws_text_msg_t msg = {
.text = copy,
.len = len,
};
if (xQueueSend(queue, &msg, pdMS_TO_TICKS(VOICE_WS_SEND_TIMEOUT_MS)) != pdTRUE) {
free(copy);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
esp_err_t voice_send_directive(const char *action,
const char *directive,
bool with_dialog_id) {
@@ -181,6 +219,68 @@ esp_err_t voice_send_directive(const char *action,
return err;
}
void voice_ws_text_task(void *arg) {
(void)arg;
while (true) {
bool shutdown = false;
QueueHandle_t queue = NULL;
if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
shutdown = s_voice.ws_text_shutdown;
queue = s_voice.ws_text_queue;
voice_unlock();
}
if (shutdown || queue == NULL) {
break;
}
voice_ws_text_msg_t msg = {0};
if (xQueueReceive(queue, &msg, pdMS_TO_TICKS(VOICE_WS_TEXT_QUEUE_RX_TIMEOUT_MS)) != pdTRUE) {
continue;
}
if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
s_voice.ws_text_busy = true;
voice_unlock();
}
esp_err_t err = voice_send_text_immediate(msg.text, msg.len);
if (err != ESP_OK && voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
voice_set_last_error_locked("ws text send failed");
voice_unlock();
}
if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
s_voice.ws_text_busy = false;
voice_unlock();
}
free(msg.text);
}
QueueHandle_t queue = NULL;
if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
queue = s_voice.ws_text_queue;
s_voice.ws_text_busy = false;
voice_unlock();
}
if (queue != NULL) {
voice_ws_text_msg_t msg = {0};
while (xQueueReceive(queue, &msg, 0) == pdTRUE) {
free(msg.text);
}
}
bool delete_with_caps = false;
if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) {
delete_with_caps = s_voice.ws_text_task_with_caps;
s_voice.ws_text_task = NULL;
s_voice.ws_text_task_with_caps = false;
voice_unlock();
}
voice_delete_self_task(delete_with_caps);
}
static void voice_handle_text_chunk(esp_websocket_event_data_t *data) {
if (data->payload_offset == 0) {
free(s_voice.text_agg);
@@ -310,6 +410,7 @@ void voice_websocket_event_handler(void *handler_args,
s_voice.ws_connected = false;
s_voice.started = false;
s_voice.tap_active = false;
s_voice.speech_active = false;
s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE;
s_voice.last_event_ms = voice_now_ms();
voice_unlock();