feat(voice): add on-device Miao Zhang Gui wake word

Add an optional local wake response prompt configuration. The prompt asset is intentionally not included yet.
This commit is contained in:
chenjintao
2026-08-10 16:54:42 +08:00
parent d9660164a6
commit e5c1564130
21 changed files with 2010 additions and 56 deletions
@@ -0,0 +1,976 @@
#include "domain.h"
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_mn_iface.h"
#include "esp_mn_models.h"
#include "esp_mn_speech_commands.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/idf_additions.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "model_path.h"
#include "platform.h"
#include "sdkconfig.h"
#ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES
#define CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES 8192
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY
#define CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY 7
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_TASK_CORE
#define CONFIG_TQ_LOCAL_COMMAND_TASK_CORE 0
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS
#define CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS 1000
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS
#define CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS 6000
#endif
#ifndef CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000
#define CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000 1200
#endif
#define DOMAIN_LOCAL_COMMAND_MODEL_PARTITION "model"
#define DOMAIN_LOCAL_COMMAND_MODEL_NAME "mn7_cn"
#define DOMAIN_LOCAL_COMMAND_TEXT "喵掌柜"
#define DOMAIN_LOCAL_COMMAND_GRAPH_TEXT "miao zhang gui"
#define DOMAIN_LOCAL_COMMAND_PCM_QUEUE_LEN 8U
#define DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES 512U
#define DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS 100U
#define DOMAIN_LOCAL_COMMAND_LOCK_TIMEOUT_MS 100U
#define DOMAIN_LOCAL_COMMAND_STACK_MIN_BYTES 2048U
#define DOMAIN_LOCAL_COMMAND_STATS_INTERVAL_MS 2000U
#define DOMAIN_LOCAL_COMMAND_STOP_BIT BIT0
#define DOMAIN_LOCAL_COMMAND_STOPPED_BIT BIT1
#define DOMAIN_LOCAL_COMMAND_RUNNING_BIT BIT2
#define DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT BIT3
#if CONFIG_TQ_LOCAL_COMMAND_TASK_CORE < 0
#define DOMAIN_LOCAL_COMMAND_TASK_CORE tskNO_AFFINITY
#else
#define DOMAIN_LOCAL_COMMAND_TASK_CORE CONFIG_TQ_LOCAL_COMMAND_TASK_CORE
#endif
typedef struct {
uint16_t sample_count;
int16_t samples[DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES];
} domain_local_command_pcm_frame_t;
typedef struct {
uint32_t received_frames;
uint32_t processed_chunks;
uint32_t dropped_frames;
uint32_t timeout_count;
uint32_t queue_high_water;
uint32_t max_inference_us;
} domain_local_command_pipeline_stats_t;
typedef struct {
SemaphoreHandle_t lock;
EventGroupHandle_t events;
QueueHandle_t pcm_queue;
TaskHandle_t task;
srmodel_list_t *models;
const esp_mn_iface_t *multinet_iface;
model_iface_data_t *multinet_data;
bool commands_allocated;
int multinet_chunk_samples;
int16_t *inference_pcm;
domain_local_command_event_callback_t callback;
void *callback_user_data;
bool started;
bool stop_requested;
bool suspended;
bool detection_running;
bool reset_pending;
uint32_t control_generation;
uint32_t applied_generation;
uint32_t detection_generation;
uint32_t detection_count;
uint32_t task_stack_high_water_bytes;
} domain_local_command_state_t;
static const char *TAG = "domain_local_cmd";
static domain_local_command_state_t s_local_command;
static StaticTask_t s_local_command_task_buffer;
static StackType_t *s_local_command_task_stack;
static portMUX_TYPE s_pcm_ingress_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_pcm_ingress_enabled;
static domain_local_command_pipeline_stats_t s_pipeline_stats;
static bool local_command_lock(void)
{
if (s_local_command.lock == NULL) {
return false;
}
TickType_t ticks = pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_LOCK_TIMEOUT_MS);
return xSemaphoreTake(s_local_command.lock, ticks > 0U ? ticks : 1U) == pdTRUE;
}
static void local_command_unlock(void)
{
xSemaphoreGive(s_local_command.lock);
}
static uint32_t local_command_default_timeout(int timeout_ms)
{
return timeout_ms == 0 ? CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS : (uint32_t)timeout_ms;
}
static uint32_t local_command_remaining_ms(TickType_t started, uint32_t timeout_ms)
{
const uint32_t elapsed_ms = (uint32_t)((xTaskGetTickCount() - started) * portTICK_PERIOD_MS);
return elapsed_ms >= timeout_ms ? 0U : timeout_ms - elapsed_ms;
}
static void local_command_set_pcm_admission(bool enabled)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
s_pcm_ingress_enabled = enabled;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static uint32_t local_command_dropped_pcm_frames(void)
{
uint32_t dropped = 0U;
portENTER_CRITICAL(&s_pcm_ingress_lock);
dropped = s_pipeline_stats.dropped_frames;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
return dropped;
}
static void local_command_note_dropped_pcm(void)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
++s_pipeline_stats.dropped_frames;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static void local_command_note_queued_pcm(uint32_t queue_depth)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
++s_pipeline_stats.received_frames;
if (queue_depth > s_pipeline_stats.queue_high_water) {
s_pipeline_stats.queue_high_water = queue_depth;
}
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static void local_command_note_processed_chunk(uint32_t inference_us, bool timed_out)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
++s_pipeline_stats.processed_chunks;
if (timed_out) {
++s_pipeline_stats.timeout_count;
}
if (inference_us > s_pipeline_stats.max_inference_us) {
s_pipeline_stats.max_inference_us = inference_us;
}
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static domain_local_command_pipeline_stats_t local_command_pipeline_stats(void)
{
domain_local_command_pipeline_stats_t stats;
portENTER_CRITICAL(&s_pcm_ingress_lock);
stats = s_pipeline_stats;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
return stats;
}
static void local_command_reset_pipeline_stats(void)
{
portENTER_CRITICAL(&s_pcm_ingress_lock);
memset(&s_pipeline_stats, 0, sizeof(s_pipeline_stats));
portEXIT_CRITICAL(&s_pcm_ingress_lock);
}
static void local_command_pcm_observer(const int16_t *pcm,
size_t samples,
uint32_t sample_rate_hz,
void *user_data)
{
(void)user_data;
bool admitted = false;
portENTER_CRITICAL(&s_pcm_ingress_lock);
admitted = s_pcm_ingress_enabled;
portEXIT_CRITICAL(&s_pcm_ingress_lock);
if (!admitted || pcm == NULL || samples == 0U) {
return;
}
if (sample_rate_hz != 16000U || samples > DOMAIN_LOCAL_COMMAND_PCM_MAX_SAMPLES ||
s_local_command.pcm_queue == NULL) {
local_command_note_dropped_pcm();
return;
}
domain_local_command_pcm_frame_t frame = {
.sample_count = (uint16_t)samples,
};
memcpy(frame.samples, pcm, samples * sizeof(int16_t));
if (xQueueSend(s_local_command.pcm_queue, &frame, 0U) != pdTRUE) {
local_command_note_dropped_pcm();
} else {
local_command_note_queued_pcm(
(uint32_t)uxQueueMessagesWaiting(s_local_command.pcm_queue));
}
}
static void local_command_drain_pcm_queue(void)
{
domain_local_command_pcm_frame_t frame;
if (s_local_command.pcm_queue == NULL) {
return;
}
while (xQueueReceive(s_local_command.pcm_queue, &frame, 0U) == pdTRUE) {
}
}
static esp_err_t local_command_configure_model(void)
{
s_local_command.models = esp_srmodel_init(DOMAIN_LOCAL_COMMAND_MODEL_PARTITION);
if (s_local_command.models == NULL || s_local_command.models->num <= 0) {
ESP_LOGE(TAG,
"ESP-SR model partition unavailable: partition=%s action=full_flash_required",
DOMAIN_LOCAL_COMMAND_MODEL_PARTITION);
return ESP_ERR_NOT_FOUND;
}
if (esp_srmodel_exists(s_local_command.models,
(char *)DOMAIN_LOCAL_COMMAND_MODEL_NAME) < 0) {
ESP_LOGE(TAG, "Chinese MultiNet model unavailable: model=%s",
DOMAIN_LOCAL_COMMAND_MODEL_NAME);
return ESP_ERR_NOT_FOUND;
}
s_local_command.multinet_iface = esp_mn_handle_from_name(
(char *)DOMAIN_LOCAL_COMMAND_MODEL_NAME);
const esp_mn_iface_t *iface = s_local_command.multinet_iface;
if (iface == NULL || iface->create == NULL || iface->destroy == NULL ||
iface->get_samp_rate == NULL || iface->get_samp_chunksize == NULL ||
iface->detect == NULL || iface->get_results == NULL || iface->clean == NULL ||
iface->check_speech_command == NULL || iface->set_det_threshold == NULL) {
return ESP_ERR_NOT_SUPPORTED;
}
s_local_command.multinet_data = iface->create(
DOMAIN_LOCAL_COMMAND_MODEL_NAME,
CONFIG_TQ_LOCAL_COMMAND_DETECTION_TIMEOUT_MS);
if (s_local_command.multinet_data == NULL) {
return ESP_ERR_NO_MEM;
}
if (iface->get_samp_rate(s_local_command.multinet_data) != 16000) {
return ESP_ERR_NOT_SUPPORTED;
}
s_local_command.multinet_chunk_samples =
iface->get_samp_chunksize(s_local_command.multinet_data);
if (s_local_command.multinet_chunk_samples <= 0 ||
s_local_command.multinet_chunk_samples > INT16_MAX) {
return ESP_ERR_INVALID_SIZE;
}
if (iface->check_speech_command(s_local_command.multinet_data,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT) == 0) {
ESP_LOGE(TAG, "MultiNet command pronunciation is not tokenizable: %s",
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT);
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = esp_mn_commands_alloc(iface, s_local_command.multinet_data);
if (err != ESP_OK) {
return err;
}
s_local_command.commands_allocated = true;
err = esp_mn_commands_clear();
if (err == ESP_OK) {
err = esp_mn_commands_phoneme_add(
DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT);
}
if (err != ESP_OK) {
return err;
}
esp_mn_error_t *command_errors = esp_mn_commands_update();
if (command_errors != NULL) {
ESP_LOGE(TAG, "MultiNet rejected local command graph: invalid_count=%d",
command_errors->num);
return ESP_ERR_INVALID_ARG;
}
const float detection_threshold =
(float)CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000 / 10000.0f;
(void)iface->set_det_threshold(s_local_command.multinet_data,
detection_threshold);
const esp_mn_phrase_t *command = esp_mn_commands_get_from_index(0);
if (command == NULL || command->command_id != DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI ||
command->string == NULL || command->phonemes == NULL ||
strcmp(command->string, DOMAIN_LOCAL_COMMAND_GRAPH_TEXT) != 0 ||
strcmp(command->phonemes, DOMAIN_LOCAL_COMMAND_GRAPH_TEXT) != 0 ||
esp_mn_commands_get_from_index(1) != NULL) {
return ESP_ERR_INVALID_RESPONSE;
}
s_local_command.inference_pcm = calloc(
(size_t)s_local_command.multinet_chunk_samples,
sizeof(int16_t));
if (s_local_command.inference_pcm == NULL) {
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG,
"Local command model ready: model=%s phrase=%s command=%s samples=%d "
"rate=16000 threshold_x10000=%u",
DOMAIN_LOCAL_COMMAND_MODEL_NAME,
DOMAIN_LOCAL_COMMAND_TEXT,
DOMAIN_LOCAL_COMMAND_GRAPH_TEXT,
s_local_command.multinet_chunk_samples,
(unsigned)CONFIG_TQ_LOCAL_COMMAND_DETECTION_THRESHOLD_X10000);
return ESP_OK;
}
static void local_command_release_model(void)
{
if (s_local_command.commands_allocated) {
(void)esp_mn_commands_free();
s_local_command.commands_allocated = false;
}
if (s_local_command.multinet_iface != NULL &&
s_local_command.multinet_data != NULL) {
s_local_command.multinet_iface->destroy(s_local_command.multinet_data);
}
s_local_command.multinet_data = NULL;
s_local_command.multinet_iface = NULL;
if (s_local_command.models != NULL) {
esp_srmodel_deinit(s_local_command.models);
}
s_local_command.models = NULL;
free(s_local_command.inference_pcm);
s_local_command.inference_pcm = NULL;
s_local_command.multinet_chunk_samples = 0;
}
static bool local_command_apply_control(size_t *fill_samples)
{
bool stop_requested = true;
bool suspended = true;
bool changed = false;
bool reset_pending = false;
bool applied = false;
uint32_t generation = 0U;
if (local_command_lock()) {
stop_requested = s_local_command.stop_requested;
suspended = s_local_command.suspended;
generation = s_local_command.control_generation;
changed = s_local_command.applied_generation != generation;
reset_pending = s_local_command.reset_pending;
local_command_unlock();
}
if (stop_requested) {
return false;
}
if (!changed) {
return !suspended;
}
if (reset_pending) {
s_local_command.multinet_iface->clean(s_local_command.multinet_data);
}
local_command_drain_pcm_queue();
*fill_samples = 0U;
if (!local_command_lock()) {
return false;
}
if (s_local_command.stop_requested) {
local_command_unlock();
return false;
}
if (generation == s_local_command.control_generation) {
applied = true;
s_local_command.applied_generation = generation;
s_local_command.detection_running = !s_local_command.suspended;
s_local_command.reset_pending = false;
suspended = s_local_command.suspended;
xEventGroupClearBits(s_local_command.events,
DOMAIN_LOCAL_COMMAND_RUNNING_BIT |
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT);
xEventGroupSetBits(s_local_command.events,
suspended ? DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT :
DOMAIN_LOCAL_COMMAND_RUNNING_BIT);
}
local_command_unlock();
if (applied) {
ESP_LOGI(TAG,
"Local command detection %s: control_generation=%u queue_depth=%u dropped_total=%u",
suspended ? "suspended" : "running",
(unsigned)generation,
s_local_command.pcm_queue != NULL ?
(unsigned)uxQueueMessagesWaiting(s_local_command.pcm_queue) : 0U,
(unsigned)local_command_dropped_pcm_frames());
}
return !suspended;
}
static void local_command_publish_detection(float probability)
{
domain_local_command_event_callback_t callback = NULL;
void *callback_user_data = NULL;
domain_local_command_event_t event = {
.type = DOMAIN_LOCAL_COMMAND_EVENT_DETECTED,
.command_id = DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI,
};
if (probability > 0.0f) {
event.confidence_x10000 = probability >= 0.9999f ?
9999U : (uint32_t)(probability * 10000.0f + 0.5f);
}
if (!local_command_lock()) {
return;
}
if (!s_local_command.started || s_local_command.stop_requested ||
s_local_command.suspended) {
local_command_unlock();
return;
}
s_local_command.suspended = true;
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
local_command_set_pcm_admission(false);
++s_local_command.control_generation;
s_local_command.applied_generation = s_local_command.control_generation;
event.detection_generation = ++s_local_command.detection_generation;
++s_local_command.detection_count;
callback = s_local_command.callback;
callback_user_data = s_local_command.callback_user_data;
xEventGroupClearBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_RUNNING_BIT);
xEventGroupSetBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT);
local_command_unlock();
const domain_local_command_pipeline_stats_t stats = local_command_pipeline_stats();
ESP_LOGI(TAG,
"Local command detected: phrase=%s confidence_x10000=%u generation=%u "
"received=%u processed=%u drops=%u queue_high=%u inference_max_us=%u",
DOMAIN_LOCAL_COMMAND_TEXT,
(unsigned)event.confidence_x10000,
(unsigned)event.detection_generation,
(unsigned)stats.received_frames,
(unsigned)stats.processed_chunks,
(unsigned)stats.dropped_frames,
(unsigned)stats.queue_high_water,
(unsigned)stats.max_inference_us);
if (callback != NULL) {
callback(&event, callback_user_data);
}
}
static void local_command_task(void *arg)
{
(void)arg;
size_t fill_samples = 0U;
uint32_t frame_count = 0U;
bool stack_reported = false;
bool stack_warned = false;
TickType_t stats_last_log_tick = xTaskGetTickCount();
uint32_t previous_received_frames = 0U;
uint32_t previous_processed_chunks = 0U;
uint32_t previous_dropped_frames = 0U;
uint32_t previous_timeout_count = 0U;
for (;;) {
const bool detection_running = local_command_apply_control(&fill_samples);
bool stop_requested = true;
if (local_command_lock()) {
stop_requested = s_local_command.stop_requested;
local_command_unlock();
}
if (stop_requested) {
break;
}
if (!detection_running) {
(void)ulTaskNotifyTake(pdTRUE,
pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS));
continue;
}
domain_local_command_pcm_frame_t frame;
if (xQueueReceive(s_local_command.pcm_queue,
&frame,
pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_QUEUE_WAIT_MS)) != pdTRUE) {
continue;
}
if (!local_command_apply_control(&fill_samples)) {
continue;
}
size_t offset = 0U;
while (offset < frame.sample_count) {
const size_t needed = (size_t)s_local_command.multinet_chunk_samples - fill_samples;
const size_t available = (size_t)frame.sample_count - offset;
const size_t copy_samples = available < needed ? available : needed;
memcpy(s_local_command.inference_pcm + fill_samples,
frame.samples + offset,
copy_samples * sizeof(int16_t));
fill_samples += copy_samples;
offset += copy_samples;
if (fill_samples != (size_t)s_local_command.multinet_chunk_samples) {
continue;
}
const TickType_t inference_started_tick = xTaskGetTickCount();
const esp_mn_state_t state = s_local_command.multinet_iface->detect(
s_local_command.multinet_data,
s_local_command.inference_pcm);
const uint64_t inference_elapsed_us =
(uint64_t)(xTaskGetTickCount() - inference_started_tick) *
(uint64_t)portTICK_PERIOD_MS * 1000U;
const uint32_t inference_us = inference_elapsed_us > UINT32_MAX ?
UINT32_MAX : (uint32_t)inference_elapsed_us;
fill_samples = 0U;
++frame_count;
local_command_note_processed_chunk(
inference_us, state == ESP_MN_STATE_TIMEOUT);
/* MultiNet can consume a full core while the PCM queue is nonempty.
* Give the pinned core's idle task a bounded scheduling point so
* continuous playback-time inference cannot trip the task WDT. */
vTaskDelay(1);
if (state == ESP_MN_STATE_TIMEOUT) {
s_local_command.multinet_iface->clean(s_local_command.multinet_data);
} else if (state == ESP_MN_STATE_DETECTED) {
esp_mn_results_t *results = s_local_command.multinet_iface->get_results(
s_local_command.multinet_data);
const bool accepted = results != NULL && results->num > 0 &&
results->command_id[0] == DOMAIN_LOCAL_COMMAND_MIAO_ZHANG_GUI;
const float probability = accepted ? results->prob[0] : 0.0f;
s_local_command.multinet_iface->clean(s_local_command.multinet_data);
if (accepted) {
local_command_publish_detection(probability);
fill_samples = 0U;
break;
}
ESP_LOGW(TAG, "Ignore unexpected MultiNet result");
} else if (state != ESP_MN_STATE_DETECTING) {
ESP_LOGE(TAG, "Invalid MultiNet state: %d", (int)state);
if (local_command_lock()) {
s_local_command.stop_requested = true;
local_command_unlock();
}
break;
}
const TickType_t now = xTaskGetTickCount();
if ((now - stats_last_log_tick) >=
pdMS_TO_TICKS(DOMAIN_LOCAL_COMMAND_STATS_INTERVAL_MS)) {
const domain_local_command_pipeline_stats_t stats =
local_command_pipeline_stats();
const uint32_t queue_depth = s_local_command.pcm_queue != NULL ?
(uint32_t)uxQueueMessagesWaiting(s_local_command.pcm_queue) : 0U;
ESP_LOGI(TAG,
"Local command pipeline: received=%u processed=%u drops=%u timeouts=%u "
"queue=%u high=%u inference_max_us=%u",
(unsigned)(stats.received_frames - previous_received_frames),
(unsigned)(stats.processed_chunks - previous_processed_chunks),
(unsigned)(stats.dropped_frames - previous_dropped_frames),
(unsigned)(stats.timeout_count - previous_timeout_count),
(unsigned)queue_depth,
(unsigned)stats.queue_high_water,
(unsigned)stats.max_inference_us);
previous_received_frames = stats.received_frames;
previous_processed_chunks = stats.processed_chunks;
previous_dropped_frames = stats.dropped_frames;
previous_timeout_count = stats.timeout_count;
stats_last_log_tick = now;
}
if ((frame_count % 100U) == 0U) {
const uint32_t high_water = (uint32_t)uxTaskGetStackHighWaterMark(NULL);
if (local_command_lock()) {
s_local_command.task_stack_high_water_bytes = high_water;
local_command_unlock();
}
const uint32_t required =
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES / 4U >
DOMAIN_LOCAL_COMMAND_STACK_MIN_BYTES ?
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES / 4U :
DOMAIN_LOCAL_COMMAND_STACK_MIN_BYTES;
if (!stack_reported) {
stack_reported = true;
ESP_LOGI(TAG,
"Task stack headroom=%u required=%u configured=%u bytes",
(unsigned)high_water,
(unsigned)required,
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES);
}
if (!stack_warned && high_water < required) {
stack_warned = true;
ESP_LOGW(TAG,
"Local command task stack margin low: high_water=%u required=%u configured=%u",
(unsigned)high_water,
(unsigned)required,
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES);
}
}
}
}
if (local_command_lock()) {
s_local_command.task_stack_high_water_bytes =
(uint32_t)uxTaskGetStackHighWaterMark(NULL);
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
local_command_unlock();
}
xEventGroupClearBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_RUNNING_BIT);
xEventGroupSetBits(s_local_command.events,
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT |
DOMAIN_LOCAL_COMMAND_STOPPED_BIT);
vTaskSuspend(NULL);
abort();
}
static esp_err_t local_command_reserve_task_stack(void)
{
if (s_local_command_task_stack != NULL) {
return ESP_OK;
}
#if (configSUPPORT_STATIC_ALLOCATION == 1) && \
((defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \
(defined(CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY) && CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY))
s_local_command_task_stack = (StackType_t *)heap_caps_malloc(
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (s_local_command_task_stack != NULL) {
ESP_LOGI(TAG,
"Task stack reserved in PSRAM (%uB)",
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES);
return ESP_OK;
}
#endif
ESP_LOGE(TAG,
"PSRAM task stack reservation failed bytes=%u internal_free=%u "
"internal_largest=%u psram_free=%u psram_largest=%u",
(unsigned)CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES,
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
return ESP_ERR_NO_MEM;
}
static void local_command_cleanup_after_stop(void)
{
local_command_set_pcm_admission(false);
local_command_release_model();
if (local_command_lock()) {
s_local_command.callback = NULL;
s_local_command.callback_user_data = NULL;
s_local_command.started = false;
s_local_command.stop_requested = false;
s_local_command.suspended = true;
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
s_local_command.control_generation = 0U;
s_local_command.applied_generation = 0U;
local_command_unlock();
}
}
esp_err_t domain_runtime_local_command_start(
domain_local_command_event_callback_t callback,
void *user_data)
{
if (callback == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_local_command.lock == NULL) {
s_local_command.lock = xSemaphoreCreateMutex();
}
if (s_local_command.events == NULL) {
s_local_command.events = xEventGroupCreate();
}
if (s_local_command.lock == NULL || s_local_command.events == NULL) {
return ESP_ERR_NO_MEM;
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
if (s_local_command.started) {
const bool same = s_local_command.callback == callback &&
s_local_command.callback_user_data == user_data;
local_command_unlock();
return same ? ESP_OK : ESP_ERR_INVALID_STATE;
}
s_local_command.callback = callback;
s_local_command.callback_user_data = user_data;
s_local_command.stop_requested = false;
s_local_command.suspended = false;
s_local_command.detection_running = false;
s_local_command.reset_pending = false;
s_local_command.control_generation = 1U;
s_local_command.applied_generation = 0U;
local_command_unlock();
local_command_reset_pipeline_stats();
esp_err_t err = local_command_reserve_task_stack();
if (err != ESP_OK) {
local_command_cleanup_after_stop();
return err;
}
if (s_local_command.pcm_queue == NULL) {
s_local_command.pcm_queue = xQueueCreateWithCaps(
DOMAIN_LOCAL_COMMAND_PCM_QUEUE_LEN,
sizeof(domain_local_command_pcm_frame_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
if (s_local_command.pcm_queue == NULL) {
local_command_cleanup_after_stop();
return ESP_ERR_NO_MEM;
}
(void)xQueueReset(s_local_command.pcm_queue);
err = local_command_configure_model();
if (err != ESP_OK) {
local_command_cleanup_after_stop();
return err;
}
err = platform_multimodal_audio_register_pcm_observer(
local_command_pcm_observer,
NULL);
if (err != ESP_OK) {
local_command_cleanup_after_stop();
return err;
}
local_command_set_pcm_admission(true);
xEventGroupClearBits(s_local_command.events,
DOMAIN_LOCAL_COMMAND_STOP_BIT |
DOMAIN_LOCAL_COMMAND_STOPPED_BIT |
DOMAIN_LOCAL_COMMAND_RUNNING_BIT |
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT);
if (!local_command_lock()) {
(void)platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer, NULL,
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
local_command_cleanup_after_stop();
return ESP_ERR_TIMEOUT;
}
s_local_command.started = true;
local_command_unlock();
/* PSRAM-backed static stack. This inference-only task never initiates flash
* writes; OTA stops local wake before image installation. Its worst case is
* a 1026-byte PCM frame, MultiNet inference, and one bounded typed callback. */
TaskHandle_t created = xTaskCreateStaticPinnedToCore(
local_command_task,
"domain_cmd",
CONFIG_TQ_LOCAL_COMMAND_TASK_STACK_BYTES,
NULL,
CONFIG_TQ_LOCAL_COMMAND_TASK_PRIORITY,
s_local_command_task_stack,
&s_local_command_task_buffer,
DOMAIN_LOCAL_COMMAND_TASK_CORE);
if (created == NULL) {
(void)platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer, NULL,
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
local_command_cleanup_after_stop();
return ESP_ERR_NO_MEM;
}
if (local_command_lock()) {
s_local_command.task = created;
local_command_unlock();
} else {
vTaskDelete(created);
(void)platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer, NULL,
CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
local_command_cleanup_after_stop();
return ESP_ERR_TIMEOUT;
}
EventBits_t bits = xEventGroupWaitBits(
s_local_command.events,
DOMAIN_LOCAL_COMMAND_RUNNING_BIT | DOMAIN_LOCAL_COMMAND_STOPPED_BIT,
pdFALSE,
pdFALSE,
pdMS_TO_TICKS(CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS));
if ((bits & DOMAIN_LOCAL_COMMAND_RUNNING_BIT) == 0U) {
(void)domain_runtime_local_command_stop(CONFIG_TQ_LOCAL_COMMAND_CONTROL_TIMEOUT_MS);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
static esp_err_t local_command_request_detection_state(bool suspended, int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
const uint32_t bounded_ms = local_command_default_timeout(timeout_ms);
if (suspended) {
local_command_set_pcm_admission(false);
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
if (!s_local_command.started || s_local_command.stop_requested ||
s_local_command.task == NULL) {
local_command_unlock();
return ESP_ERR_INVALID_STATE;
}
if (s_local_command.suspended == suspended &&
s_local_command.applied_generation == s_local_command.control_generation) {
local_command_unlock();
return ESP_OK;
}
const bool resume_from_clean_stop = !suspended &&
s_local_command.suspended &&
!s_local_command.reset_pending &&
!s_local_command.detection_running &&
s_local_command.applied_generation == s_local_command.control_generation;
s_local_command.suspended = suspended;
if (suspended || !resume_from_clean_stop) {
s_local_command.reset_pending = true;
}
const uint32_t generation = ++s_local_command.control_generation;
const EventBits_t expected = suspended ?
DOMAIN_LOCAL_COMMAND_SUSPENDED_BIT : DOMAIN_LOCAL_COMMAND_RUNNING_BIT;
xEventGroupClearBits(s_local_command.events, expected);
TaskHandle_t task = s_local_command.task;
local_command_unlock();
xTaskNotifyGive(task);
EventBits_t bits = xEventGroupWaitBits(
s_local_command.events,
expected | DOMAIN_LOCAL_COMMAND_STOPPED_BIT,
pdFALSE,
pdFALSE,
pdMS_TO_TICKS(bounded_ms));
if ((bits & expected) == 0U) {
return (bits & DOMAIN_LOCAL_COMMAND_STOPPED_BIT) != 0U ?
ESP_ERR_INVALID_STATE : ESP_ERR_TIMEOUT;
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
const bool applied = s_local_command.started &&
!s_local_command.stop_requested &&
s_local_command.suspended == suspended &&
s_local_command.applied_generation == generation;
local_command_unlock();
if (applied && !suspended) {
local_command_set_pcm_admission(true);
}
return applied ? ESP_OK : ESP_ERR_INVALID_STATE;
}
esp_err_t domain_runtime_local_command_suspend(int timeout_ms)
{
return local_command_request_detection_state(true, timeout_ms);
}
esp_err_t domain_runtime_local_command_resume(int timeout_ms)
{
return local_command_request_detection_state(false, timeout_ms);
}
esp_err_t domain_runtime_local_command_stop(int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
const uint32_t bounded_ms = local_command_default_timeout(timeout_ms);
const TickType_t started = xTaskGetTickCount();
if (s_local_command.lock == NULL) {
return ESP_OK;
}
local_command_set_pcm_admission(false);
esp_err_t result = ESP_OK;
esp_err_t observer_err = platform_multimodal_audio_unregister_pcm_observer(
local_command_pcm_observer,
NULL,
(int)local_command_remaining_ms(started, bounded_ms));
if (observer_err != ESP_OK && observer_err != ESP_ERR_INVALID_STATE) {
result = observer_err;
}
if (!local_command_lock()) {
return result != ESP_OK ? result : ESP_ERR_TIMEOUT;
}
if (!s_local_command.started) {
local_command_unlock();
return result;
}
s_local_command.stop_requested = true;
s_local_command.detection_running = false;
TaskHandle_t task = s_local_command.task;
local_command_unlock();
xEventGroupSetBits(s_local_command.events, DOMAIN_LOCAL_COMMAND_STOP_BIT);
if (task != NULL) {
xTaskNotifyGive(task);
}
const uint32_t remaining_ms = local_command_remaining_ms(started, bounded_ms);
EventBits_t bits = xEventGroupWaitBits(
s_local_command.events,
DOMAIN_LOCAL_COMMAND_STOPPED_BIT,
pdFALSE,
pdTRUE,
pdMS_TO_TICKS(remaining_ms));
if ((bits & DOMAIN_LOCAL_COMMAND_STOPPED_BIT) == 0U && result == ESP_OK) {
result = ESP_ERR_TIMEOUT;
}
TaskHandle_t stopped_task = task;
if (local_command_lock()) {
if (s_local_command.task == stopped_task) {
s_local_command.task = NULL;
}
local_command_unlock();
}
if (stopped_task != NULL) {
vTaskDelete(stopped_task);
}
if ((bits & DOMAIN_LOCAL_COMMAND_STOPPED_BIT) == 0U) {
ESP_LOGE(TAG, "Local command worker forced down after stop timeout");
}
local_command_cleanup_after_stop();
return result;
}
esp_err_t domain_runtime_local_command_get_snapshot(
domain_local_command_snapshot_t *out_snapshot)
{
if (out_snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_local_command.lock == NULL) {
memset(out_snapshot, 0, sizeof(*out_snapshot));
return ESP_OK;
}
if (!local_command_lock()) {
return ESP_ERR_TIMEOUT;
}
*out_snapshot = (domain_local_command_snapshot_t) {
.started = s_local_command.started,
.detection_running = s_local_command.detection_running,
.detection_generation = s_local_command.detection_generation,
.detection_count = s_local_command.detection_count,
.dropped_pcm_frames = local_command_dropped_pcm_frames(),
.task_stack_high_water_bytes = s_local_command.task_stack_high_water_bytes,
};
local_command_unlock();
return ESP_OK;
}