feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -28,6 +28,8 @@
#include "av_pipeline_pool.h"
#include "esp_gmf_fifo.h"
#include "esp_codec_dev.h"
#include <stdatomic.h>
#include "freertos/semphr.h"
#include "esp_gmf_new_databus.h"
#include "esp_vad.h"
@@ -104,6 +106,18 @@ typedef struct {
static audio_manager_t audio_manager;
static audio_recorder_t audio_recorder;
static atomic_bool microphone_blocked, recorder_closing, recorder_suspended;
static StaticSemaphore_t recorder_mutex_storage;
static SemaphoreHandle_t recorder_mutex;
static portMUX_TYPE recorder_init_lock=portMUX_INITIALIZER_UNLOCKED;
static SemaphoreHandle_t recorder_control_lock(void) {
portENTER_CRITICAL(&recorder_init_lock);
if(!recorder_mutex)recorder_mutex=xSemaphoreCreateMutexStatic(&recorder_mutex_storage);
portEXIT_CRITICAL(&recorder_init_lock);
return recorder_mutex;
}
static atomic_uint microphone_inflight, microphone_reads;
static atomic_uint recorder_readers;
static audio_feeder_t audio_feeder;
static audio_playback_t audio_playback;
static audio_prompt_t audio_prompt;
@@ -163,8 +177,10 @@ static inline esp_opus_enc_frame_duration_t convert_opus_enc_frame_duration(int
static int audio_render_write_hdlr(uint8_t *pcm_data, uint32_t len, void *ctx)
{
ESP_LOGD(TAG, "Audio render write handler, data size: %ld", len);
esp_codec_dev_write(audio_manager.config.play_dev, pcm_data, len);
return ESP_OK;
int result=esp_codec_dev_write(audio_manager.config.play_dev, pcm_data, len);
extern void platform_conversation_observe(const char *type);
if(result==ESP_OK&&len)platform_conversation_observe("FIRST_PLAYBACK_STARTED");
return result;
}
static int playback_out_data_callback(uint8_t *data, int data_size, void *ctx)
@@ -240,7 +256,18 @@ static int recorder_outport_release_write(void *handle, esp_gmf_data_bus_block_t
static int recorder_inport_acquire_read(void *handle, esp_gmf_data_bus_block_t *blk, int wanted_size, int block_ticks)
{
// Increment before checking the gate, so standby can await every read that
// crossed the gate. While suspended no microphone samples enter the AFE.
for (;;) {
atomic_fetch_add(&microphone_inflight,1);
if (!atomic_load(&microphone_blocked)) break;
atomic_fetch_sub(&microphone_inflight,1);
if(atomic_load(&recorder_closing)){blk->valid_size=0;return ESP_GMF_IO_FAIL;}
vTaskDelay(pdMS_TO_TICKS(10));
}
int read_ret = esp_codec_dev_read(audio_manager.config.rec_dev, blk->buf, wanted_size);
atomic_fetch_add(&microphone_reads,1);
atomic_fetch_sub(&microphone_inflight,1);
if (read_ret != ESP_CODEC_DEV_OK) {
ESP_LOGE(TAG, "Microphone read failed: %d", read_ret);
blk->valid_size = 0;
@@ -531,6 +558,10 @@ static void recorder_cleanup_on_error(void)
esp_gmf_afe_manager_destroy(audio_recorder.afe_manager);
audio_recorder.afe_manager = NULL;
}
if (audio_recorder.pool) {
av_audio_recorder_pool_deinit(audio_recorder.pool);
audio_recorder.pool = NULL;
}
}
static esp_err_t recorder_configure_all_encoders(void)
@@ -662,7 +693,9 @@ static int feeder_outport_release_write(void *handle, esp_gmf_data_bus_block_t *
if (audio_manager.enable_mixer) {
esp_audio_render_stream_write(audio_manager.stream[AUDIO_RENDER_STREAM_FEEDER], blk->buf, blk->valid_size);
} else {
esp_codec_dev_write(audio_manager.config.play_dev, blk->buf, blk->valid_size);
int result=esp_codec_dev_write(audio_manager.config.play_dev, blk->buf, blk->valid_size);
extern void platform_conversation_observe(const char *type);
if(result==ESP_OK&&blk->valid_size)platform_conversation_observe("FIRST_PLAYBACK_STARTED");
}
return blk->valid_size;
}
@@ -1122,7 +1155,7 @@ esp_err_t audio_prompt_play(const char *url)
return ESP_OK;
}
esp_err_t audio_recorder_open(audio_recorder_config_t *config)
static esp_err_t recorder_open_internal(audio_recorder_config_t *config)
{
ESP_LOGI(TAG, "Opening audio recorder...");
@@ -1170,10 +1203,24 @@ esp_err_t audio_recorder_open(audio_recorder_config_t *config)
cleanup_and_exit:
ESP_LOGE(TAG, "Failed to open audio recorder, cleaning up...");
atomic_store(&recorder_closing,true);
recorder_cleanup_on_error();
atomic_store(&recorder_closing,false);
return ESP_FAIL;
}
esp_err_t audio_recorder_open(audio_recorder_config_t *config)
{
SemaphoreHandle_t lock=recorder_control_lock();xSemaphoreTake(lock,portMAX_DELAY);
esp_err_t e=recorder_open_internal(config);
atomic_store(&recorder_suspended,false);
if(e==ESP_OK && atomic_load(&microphone_blocked)) {
e=esp_gmf_afe_manager_suspend(audio_recorder.afe_manager,true)==ESP_GMF_ERR_OK?ESP_OK:ESP_FAIL;
atomic_store(&recorder_suspended,e==ESP_OK);
}
xSemaphoreGive(lock);return e;
}
esp_err_t audio_recorder_pause(void)
{
if (audio_recorder.state == AUDIO_RUN_STATE_CLOSED) {
@@ -1227,7 +1274,7 @@ esp_err_t audio_recorder_get_afe_manager_handle(esp_gmf_afe_manager_handle_t *af
}
esp_err_t audio_recorder_close(void)
static esp_err_t recorder_close_internal(void)
{
if (audio_recorder.state == AUDIO_RUN_STATE_CLOSED) {
ESP_LOGW(TAG, "Audio recorder is already closed");
@@ -1241,8 +1288,10 @@ esp_err_t audio_recorder_close(void)
if (audio_recorder.pipe) {
esp_gmf_fifo_abort(audio_recorder.fifo);
esp_gmf_pipeline_stop(audio_recorder.pipe);
audio_recorder.pipe = NULL;
}
// Abort wakes blocked readers. Keep the FIFO and pipeline alive until each
// reader has released its block; retain the pipeline handle for destroy.
while (atomic_load(&recorder_readers)) vTaskDelay(1);
if (audio_recorder.task) {
esp_gmf_task_deinit(audio_recorder.task);
audio_recorder.task = NULL;
@@ -1277,20 +1326,31 @@ esp_err_t audio_recorder_close(void)
return ESP_OK;
}
esp_err_t audio_recorder_close(void)
{
SemaphoreHandle_t lock=recorder_control_lock();xSemaphoreTake(lock,portMAX_DELAY);
atomic_store(&recorder_closing,true);
esp_err_t e=recorder_close_internal();
atomic_store(&recorder_closing,false);
xSemaphoreGive(lock);return e;
}
esp_err_t audio_recorder_read_data(uint8_t *data, int data_size)
{
if (!data || data_size <= 0) {
ESP_LOGE(TAG, "Invalid parameters for recorder read");
return ESP_FAIL;
}
if (audio_recorder.state == AUDIO_RUN_STATE_CLOSED || !audio_recorder.fifo) {
ESP_LOGE(TAG, "Recorder not available for reading");
atomic_fetch_add(&recorder_readers, 1);
if (atomic_load(&recorder_closing) || audio_recorder.state == AUDIO_RUN_STATE_CLOSED || !audio_recorder.fifo) {
atomic_fetch_sub(&recorder_readers, 1);
return ESP_FAIL;
}
esp_gmf_data_bus_block_t blk = {0};
int ret = esp_gmf_fifo_acquire_read(audio_recorder.fifo, &blk, data_size, portMAX_DELAY);
if (ret < 0) {
// ESP_LOGE(TAG, "Failed to acquire read from recorder FIFO (%d)", ret);
atomic_fetch_sub(&recorder_readers, 1);
return ret;
}
int bytes_to_copy = blk.valid_size;
@@ -1299,15 +1359,37 @@ esp_err_t audio_recorder_read_data(uint8_t *data, int data_size)
bytes_to_copy = data_size;
}
memcpy(data, blk.buf, bytes_to_copy);
esp_gmf_fifo_release_read(audio_recorder.fifo, &blk, portMAX_DELAY);
#if CONFIG_MEDIA_DUMP_ENABLE && CONFIG_MEDIA_DUMP_AUDIO_AFTER_AEC
media_dump_feed(NULL, (uint8_t *)blk.buf, blk.valid_size);
#endif /* CONFIG_MEDIA_DUMP_ENABLE && CONFIG_MEDIA_DUMP_AUDIO_AFTER_AEC */
esp_gmf_fifo_release_read(audio_recorder.fifo, &blk, portMAX_DELAY);
atomic_fetch_sub(&recorder_readers, 1);
return bytes_to_copy;
}
esp_err_t audio_feeder_feed_data(uint8_t *data, int data_size)
{
return audio_feeder_feed_data_timeout(data, data_size, UINT32_MAX);
}
esp_err_t audio_recorder_set_standby(bool standby)
{
SemaphoreHandle_t lock=recorder_control_lock();xSemaphoreTake(lock,portMAX_DELAY);
atomic_store(&microphone_blocked,standby);
esp_err_t e=ESP_OK;
if(audio_recorder.afe_manager && esp_gmf_afe_manager_suspend(audio_recorder.afe_manager,standby)!=ESP_GMF_ERR_OK)e=ESP_FAIL;
atomic_store(&recorder_suspended,standby && e==ESP_OK);
if(standby && e==ESP_OK) {
for(int i=0;i<100 && atomic_load(&microphone_inflight);i++)vTaskDelay(pdMS_TO_TICKS(10));
if(atomic_load(&microphone_inflight))e=ESP_ERR_TIMEOUT;
}
xSemaphoreGive(lock);return e;
}
bool audio_recorder_is_standby(void) { return atomic_load(&microphone_blocked) && atomic_load(&recorder_suspended) && !atomic_load(&microphone_inflight); }
uint32_t audio_recorder_microphone_reads(void) { return atomic_load(&microphone_reads); }
esp_err_t audio_feeder_feed_data_timeout(uint8_t *data, int data_size, uint32_t timeout_ms)
{
TickType_t wait_ticks = timeout_ms == UINT32_MAX ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
if (!data || data_size <= 0) {
ESP_LOGE(TAG, "Invalid parameters for playback feed");
return ESP_FAIL;
@@ -1319,17 +1401,17 @@ esp_err_t audio_feeder_feed_data(uint8_t *data, int data_size)
esp_gmf_data_bus_block_t blk = {0};
if (audio_feeder.config.decoder_cfg.format == AV_PROCESSOR_FORMAT_ID_PCM) {
int ret = esp_gmf_db_acquire_write(audio_feeder.fifo, &blk, data_size, portMAX_DELAY);
int ret = esp_gmf_db_acquire_write(audio_feeder.fifo, &blk, data_size, wait_ticks);
if (ret < 0) {
ESP_LOGE(TAG, "Failed to acquire write to playback FIFO (0x%x)", ret);
return ESP_FAIL;
}
blk.buf = (uint8_t *)data;
blk.valid_size = data_size;
ret = esp_gmf_db_release_write(audio_feeder.fifo, &blk, portMAX_DELAY);
ret = esp_gmf_db_release_write(audio_feeder.fifo, &blk, wait_ticks);
ESP_GMF_RET_ON_NOT_OK(TAG, ret, {return ESP_FAIL;}, "Failed to release write to playback FIFO");
} else {
int ret = esp_gmf_db_acquire_write(audio_feeder.fifo, &blk, data_size, portMAX_DELAY);
int ret = esp_gmf_db_acquire_write(audio_feeder.fifo, &blk, data_size, wait_ticks);
if (ret < 0) {
ESP_LOGE(TAG, "Failed to acquire write to playback FIFO (0x%x)", ret);
return ESP_FAIL;
@@ -1338,7 +1420,7 @@ esp_err_t audio_feeder_feed_data(uint8_t *data, int data_size)
memcpy(blk.buf, data, bytes_to_copy);
blk.valid_size = bytes_to_copy;
ret = esp_gmf_db_release_write(audio_feeder.fifo, &blk, portMAX_DELAY);
ret = esp_gmf_db_release_write(audio_feeder.fifo, &blk, wait_ticks);
ESP_GMF_RET_ON_NOT_OK(TAG, ret, {return ESP_FAIL;}, "Failed to release write to playback FIFO");
}
return ESP_OK;
@@ -251,6 +251,10 @@ esp_err_t audio_recorder_pause(void);
* - Other Appropriate esp_err_t error code on failure
*/
esp_err_t audio_recorder_resume(void);
// Pause microphone reads and AFE wake detection without destroying audio tasks.
esp_err_t audio_recorder_set_standby(bool standby);
bool audio_recorder_is_standby(void);
uint32_t audio_recorder_microphone_reads(void);
/**
* @brief Get the AFE manager handle of the audio recorder
@@ -333,6 +337,8 @@ esp_err_t audio_feeder_stop(void);
* - Other Appropriate esp_err_t error code on failure
*/
esp_err_t audio_feeder_feed_data(uint8_t *data, int data_size);
/* Bounded queue waits for clients with a local audio authorization deadline. */
esp_err_t audio_feeder_feed_data_timeout(uint8_t *data, int data_size, uint32_t timeout_ms);
/**
* @brief Open the audio playback module