From baad9dcfa0a298df4bf1b46c9f0cb5f9e36fbc2b Mon Sep 17 00:00:00 2001 From: chenjintao Date: Fri, 21 Aug 2026 13:55:32 +0800 Subject: [PATCH] feat(control_plane): apply light standby hardware policy --- .../control_plane/include/control_plane.h | 11 +- .../src/runtime/control_plane_light_standby.c | 287 ++++++++++++++++-- components/domain/include/domain.h | 6 + components/domain/internal/domain_internal.h | 1 + .../src/direction/domain_direction_service.c | 5 + .../src/runtime/domain_runtime_adapters.c | 15 + components/platform/include/platform.h | 14 + .../internal/multimodal/audio_pipeline.h | 1 + .../platform/internal/platform_internal.h | 1 + .../audio_pipeline_runtime_impl.c | 31 ++ .../platform/src/platform_stepper_driver.c | 19 +- .../src/runtime/platform_core_runtime.c | 12 + .../src/runtime/platform_multimodal_runtime.c | 5 + main/Kconfig.projbuild | 32 +- sdkconfig.defaults | 6 +- 15 files changed, 405 insertions(+), 41 deletions(-) diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index d93d873..ce0af58 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -142,9 +142,9 @@ esp_err_t control_plane_prepare_runtime_storage(void); /** * @brief Start the control-plane owned light-standby coordinator. * - * The coordinator tracks one inactivity deadline for the full runtime. Phase - * one records state transitions only; hardware power policies are attached in - * later phases. Repeated starts are idempotent. + * The coordinator tracks one inactivity deadline for the full runtime and + * applies configured local hardware power policies. Repeated starts are + * idempotent. */ esp_err_t control_plane_light_standby_start(void); @@ -159,8 +159,9 @@ esp_err_t control_plane_light_standby_stop(int timeout_ms); /** * @brief Record user activity and return the coordinator to ACTIVE state. * - * This call is non-blocking and safe from control-plane worker tasks. It must - * not be called directly from an ISR. + * If hardware is in light standby, this call restores configured hardware + * before returning. Transition serialization is bounded; the API must not be + * called directly from an ISR. */ esp_err_t control_plane_light_standby_record_activity( control_plane_light_standby_activity_t activity); diff --git a/components/control_plane/src/runtime/control_plane_light_standby.c b/components/control_plane/src/runtime/control_plane_light_standby.c index b44d0e4..faa025d 100644 --- a/components/control_plane/src/runtime/control_plane_light_standby.c +++ b/components/control_plane/src/runtime/control_plane_light_standby.c @@ -13,6 +13,7 @@ #include "esp_log.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include "sdkconfig.h" @@ -25,19 +26,41 @@ #endif #ifndef CONFIG_TQ_LIGHT_STANDBY_TASK_STACK_BYTES -#define CONFIG_TQ_LIGHT_STANDBY_TASK_STACK_BYTES 3072 +#define CONFIG_TQ_LIGHT_STANDBY_TASK_STACK_BYTES 4096 #endif #ifndef CONFIG_TQ_LIGHT_STANDBY_TASK_PRIORITY #define CONFIG_TQ_LIGHT_STANDBY_TASK_PRIORITY 3 #endif +#ifndef CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT +#define CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT 0 +#endif + +#ifndef CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT +#define CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT 10 +#endif + +#ifndef CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT +#define CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT 0 +#endif + +#ifndef CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD +#define CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD 0 +#endif + +#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT +#define CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT 100 +#endif + #ifndef CONFIG_TQ_LOCAL_TASK_CORE #define CONFIG_TQ_LOCAL_TASK_CORE 1 #endif #define CONTROL_PLANE_LIGHT_STANDBY_ELIGIBILITY_RECHECK_MS 1000U +#define CONTROL_PLANE_LIGHT_STANDBY_MOTOR_RELEASE_WAIT_MS 200U #define CONTROL_PLANE_LIGHT_STANDBY_STOP_POLL_MS 20U +#define CONTROL_PLANE_LIGHT_STANDBY_TRANSITION_WAIT_MS 1000U #if CONFIG_TQ_LOCAL_TASK_CORE < 0 #define CONTROL_PLANE_LIGHT_STANDBY_TASK_CORE tskNO_AFFINITY @@ -59,6 +82,8 @@ typedef struct { static const char *TAG = "cp_light_standby"; static control_plane_light_standby_runtime_t s_standby; static portMUX_TYPE s_standby_lock = portMUX_INITIALIZER_UNLOCKED; +static StaticSemaphore_t s_transition_lock_storage; +static SemaphoreHandle_t s_transition_lock; static uint64_t control_plane_light_standby_now_ms(void) { @@ -139,10 +164,103 @@ static bool control_plane_light_standby_runtime_is_eligible(void) !state.local_responding_started; } +static bool control_plane_light_standby_lock_transition(void) +{ + return s_transition_lock != NULL && + xSemaphoreTake( + s_transition_lock, + pdMS_TO_TICKS(CONTROL_PLANE_LIGHT_STANDBY_TRANSITION_WAIT_MS)) == pdTRUE; +} + +static void control_plane_light_standby_unlock_transition(void) +{ + if (s_transition_lock != NULL) { + (void)xSemaphoreGive(s_transition_lock); + } +} + +static esp_err_t control_plane_light_standby_apply_wake_policy(void) +{ + esp_err_t first_error = ESP_OK; + +#if CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT + esp_err_t backlight_err = domain_runtime_set_lcd_backlight_brightness( + CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT); + if (backlight_err != ESP_OK) { + ESP_LOGE(TAG, + "Restore backlight failed: %s", + esp_err_to_name(backlight_err)); + first_error = backlight_err; + } +#endif + +#if CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT + esp_err_t speaker_err = + domain_runtime_multimodal_audio_set_speaker_output_enabled(true); + if (speaker_err != ESP_OK) { + ESP_LOGE(TAG, + "Restore speaker output failed: %s", + esp_err_to_name(speaker_err)); + if (first_error == ESP_OK) { + first_error = speaker_err; + } + } +#endif + + return first_error; +} + +static esp_err_t control_plane_light_standby_apply_entry_policy(void) +{ +#if CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD + esp_err_t motor_err = domain_direction_release_motor_hold( + CONTROL_PLANE_LIGHT_STANDBY_MOTOR_RELEASE_WAIT_MS); + if (motor_err != ESP_OK) { + ESP_LOGE(TAG, + "Release motor hold failed: %s", + esp_err_to_name(motor_err)); + return motor_err; + } +#endif + +#if CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT + esp_err_t speaker_err = + domain_runtime_multimodal_audio_set_speaker_output_enabled(false); + if (speaker_err != ESP_OK) { + ESP_LOGE(TAG, + "Disable speaker output failed: %s", + esp_err_to_name(speaker_err)); + return speaker_err; + } +#endif + +#if CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT + esp_err_t backlight_err = domain_runtime_set_lcd_backlight_brightness( + CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT); + if (backlight_err != ESP_OK) { + ESP_LOGE(TAG, + "Dim backlight failed: %s", + esp_err_to_name(backlight_err)); +#if CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT + (void)domain_runtime_multimodal_audio_set_speaker_output_enabled(true); +#endif + return backlight_err; + } +#endif + + return ESP_OK; +} + static void control_plane_light_standby_enter(uint64_t now_ms) { + if (!control_plane_light_standby_lock_transition()) { + ESP_LOGW(TAG, "Enter transition lock timed out"); + return; + } + control_plane_light_standby_state_t from = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; bool entering = false; + uint64_t entry_activity_ms = 0U; portENTER_CRITICAL(&s_standby_lock); if (s_standby.running && !s_standby.stop_requested && @@ -150,6 +268,7 @@ static void control_plane_light_standby_enter(uint64_t now_ms) (now_ms - s_standby.last_activity_ms) >= (uint64_t)CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS) { from = s_standby.state; + entry_activity_ms = s_standby.last_activity_ms; s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING; s_standby.transition_count++; entering = true; @@ -157,20 +276,37 @@ static void control_plane_light_standby_enter(uint64_t now_ms) portEXIT_CRITICAL(&s_standby_lock); if (!entering) { + control_plane_light_standby_unlock_transition(); return; } control_plane_light_standby_log_transition(from, CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING, "idle_timeout"); + esp_err_t policy_err = control_plane_light_standby_apply_entry_policy(); bool entered = false; + bool restore_required = false; + const char *completion_reason = "hardware_policy"; portENTER_CRITICAL(&s_standby_lock); if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING && - !s_standby.stop_requested) { + !s_standby.stop_requested && + s_standby.last_activity_ms == entry_activity_ms && + policy_err == ESP_OK) { s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY; s_standby.standby_since_ms = now_ms; s_standby.transition_count++; entered = true; + } else if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) { + s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING; + s_standby.transition_count++; + restore_required = true; + if (policy_err != ESP_OK) { + completion_reason = "hardware_policy_failed"; + } else if (s_standby.stop_requested) { + completion_reason = "stop_requested"; + } else { + completion_reason = "activity_during_entry"; + } } portEXIT_CRITICAL(&s_standby_lock); @@ -178,8 +314,38 @@ static void control_plane_light_standby_enter(uint64_t now_ms) control_plane_light_standby_log_transition( CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING, CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY, - "phase1_state_only"); + completion_reason); + control_plane_light_standby_unlock_transition(); + return; } + + control_plane_light_standby_log_transition( + CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING, + CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, + completion_reason); + if (restore_required) { + (void)control_plane_light_standby_apply_wake_policy(); + } + + bool activated = false; + portENTER_CRITICAL(&s_standby_lock); + if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING) { + s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; + s_standby.standby_since_ms = 0U; + if (policy_err != ESP_OK) { + s_standby.last_activity_ms = now_ms; + } + s_standby.transition_count++; + activated = true; + } + portEXIT_CRITICAL(&s_standby_lock); + if (activated) { + control_plane_light_standby_log_transition( + CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, + CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE, + completion_reason); + } + control_plane_light_standby_unlock_transition(); } static void control_plane_light_standby_task(void *arg) @@ -232,6 +398,38 @@ static void control_plane_light_standby_task(void *arg) (void)ulTaskNotifyTake(pdTRUE, wait_ticks); } + if (control_plane_light_standby_lock_transition()) { + control_plane_light_standby_state_t from = + CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; + bool restore_required = false; + portENTER_CRITICAL(&s_standby_lock); + if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY || + s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING || + s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING) { + from = s_standby.state; + s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING; + s_standby.transition_count++; + restore_required = true; + } + portEXIT_CRITICAL(&s_standby_lock); + + if (restore_required) { + control_plane_light_standby_log_transition( + from, + CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, + "coordinator_stop"); + esp_err_t wake_err = control_plane_light_standby_apply_wake_policy(); + if (wake_err != ESP_OK) { + ESP_LOGW(TAG, + "Hardware restore during stop failed: %s", + esp_err_to_name(wake_err)); + } + } + control_plane_light_standby_unlock_transition(); + } else { + ESP_LOGW(TAG, "Stop transition lock timed out"); + } + portENTER_CRITICAL(&s_standby_lock); s_standby.task = NULL; s_standby.running = false; @@ -247,6 +445,13 @@ esp_err_t control_plane_light_standby_start(void) #if !CONFIG_TQ_LIGHT_STANDBY_ENABLE return ESP_ERR_NOT_SUPPORTED; #else + if (s_transition_lock == NULL) { + s_transition_lock = xSemaphoreCreateMutexStatic(&s_transition_lock_storage); + if (s_transition_lock == NULL) { + return ESP_ERR_NO_MEM; + } + } + uint64_t now_ms = control_plane_light_standby_now_ms(); portENTER_CRITICAL(&s_standby_lock); if (s_standby.running) { @@ -348,9 +553,8 @@ esp_err_t control_plane_light_standby_record_activity( #if !CONFIG_TQ_LIGHT_STANDBY_ENABLE return ESP_ERR_NOT_SUPPORTED; #else - control_plane_light_standby_state_t from = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; TaskHandle_t task = NULL; - bool waking = false; + bool wake_required = false; uint64_t now_ms = control_plane_light_standby_now_ms(); portENTER_CRITICAL(&s_standby_lock); @@ -363,6 +567,27 @@ esp_err_t control_plane_light_standby_record_activity( task = s_standby.task; if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY || s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) { + wake_required = true; + } + portEXIT_CRITICAL(&s_standby_lock); + + if (task != NULL) { + xTaskNotifyGive(task); + } + if (!wake_required) { + return ESP_OK; + } + if (!control_plane_light_standby_lock_transition()) { + ESP_LOGE(TAG, "Wake transition lock timed out"); + return ESP_ERR_TIMEOUT; + } + + control_plane_light_standby_state_t from = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; + bool waking = false; + portENTER_CRITICAL(&s_standby_lock); + if (s_standby.running && !s_standby.stop_requested && + (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY || + s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING)) { from = s_standby.state; s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING; s_standby.transition_count++; @@ -370,32 +595,36 @@ esp_err_t control_plane_light_standby_record_activity( } portEXIT_CRITICAL(&s_standby_lock); - if (waking) { - const char *reason = control_plane_light_standby_activity_name(activity); - control_plane_light_standby_log_transition( - from, - CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, - reason); - - bool activated = false; - portENTER_CRITICAL(&s_standby_lock); - if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING) { - s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; - s_standby.standby_since_ms = 0U; - s_standby.transition_count++; - activated = true; - } - portEXIT_CRITICAL(&s_standby_lock); - if (activated) { - control_plane_light_standby_log_transition( - CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, - CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE, - reason); - } + if (!waking) { + control_plane_light_standby_unlock_transition(); + return ESP_OK; } - if (task != NULL) { - xTaskNotifyGive(task); + const char *reason = control_plane_light_standby_activity_name(activity); + control_plane_light_standby_log_transition( + from, + CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, + reason); + esp_err_t wake_err = control_plane_light_standby_apply_wake_policy(); + + bool activated = false; + portENTER_CRITICAL(&s_standby_lock); + if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING) { + s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; + s_standby.standby_since_ms = 0U; + s_standby.transition_count++; + activated = true; + } + portEXIT_CRITICAL(&s_standby_lock); + if (activated) { + control_plane_light_standby_log_transition( + CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING, + CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE, + wake_err == ESP_OK ? reason : "hardware_restore_failed"); + } + control_plane_light_standby_unlock_transition(); + if (wake_err != ESP_OK) { + return wake_err; } return ESP_OK; #endif diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index fe61fc9..f741855 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -227,6 +227,8 @@ int domain_get_rotate_speed_us(void); */ esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms); esp_err_t domain_apply_rotation_delta(float delta); +/** Release stepper holding current; the next movement re-energizes automatically. */ +esp_err_t domain_direction_release_motor_hold(int timeout_ms); void domain_set_calibration(const domain_direction_calibration_t *config); bool domain_get_calibration(domain_direction_calibration_t *out_config); /** @@ -270,6 +272,8 @@ esp_err_t domain_runtime_multimodal_audio_start_uplink( esp_err_t domain_runtime_multimodal_audio_stop_uplink(int timeout_ms); bool domain_runtime_multimodal_audio_is_uplink_running(void); bool domain_runtime_multimodal_audio_is_initialized(void); +/** Gate speaker output while preserving the microphone capture path. */ +esp_err_t domain_runtime_multimodal_audio_set_speaker_output_enabled(bool enabled); /** * @brief Start local MultiNet detection for the Chinese phrase "喵掌柜". * @@ -608,6 +612,8 @@ esp_err_t domain_runtime_mini_program_provisioning_notify( esp_err_t domain_runtime_mini_program_provisioning_stop(int timeout_ms); esp_err_t domain_runtime_init_lcd_device(void); +/** Set LCD backlight PWM brightness from 0 to 100 percent. */ +esp_err_t domain_runtime_set_lcd_backlight_brightness(uint8_t brightness_percent); void domain_runtime_start_lcd_render_loop(void); /** * @brief Stop LCD render loop with bounded teardown. diff --git a/components/domain/internal/domain_internal.h b/components/domain/internal/domain_internal.h index 0427dc3..f272889 100644 --- a/components/domain/internal/domain_internal.h +++ b/components/domain/internal/domain_internal.h @@ -42,6 +42,7 @@ esp_err_t domain_runtime_platform_init(void); esp_err_t domain_runtime_stepper_rotate_angle_with_accel_timeout(float angle, int target_delay_us, int timeout_ms); +esp_err_t domain_runtime_stepper_release_hold(int timeout_ms); void domain_runtime_stepper_power_off(void); esp_err_t domain_runtime_digital_input_configure(const domain_digital_input_config_t *config); bool domain_runtime_digital_input_is_active(const domain_digital_input_config_t *config); diff --git a/components/domain/src/direction/domain_direction_service.c b/components/domain/src/direction/domain_direction_service.c index d476741..c116122 100644 --- a/components/domain/src/direction/domain_direction_service.c +++ b/components/domain/src/direction/domain_direction_service.c @@ -382,6 +382,11 @@ esp_err_t domain_apply_rotation_delta(float delta) return ESP_OK; } +esp_err_t domain_direction_release_motor_hold(int timeout_ms) +{ + return domain_runtime_stepper_release_hold(timeout_ms); +} + void domain_set_calibration(const domain_direction_calibration_t *config) { domain_calibration_store_set(config); diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index 728b311..b2d3081 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -714,6 +714,11 @@ bool domain_runtime_multimodal_audio_is_initialized(void) return platform_multimodal_audio_is_initialized(); } +esp_err_t domain_runtime_multimodal_audio_set_speaker_output_enabled(bool enabled) +{ + return platform_multimodal_audio_set_speaker_output_enabled(enabled); +} + esp_err_t domain_runtime_multimodal_audio_play_downstream_opus(const uint8_t *data, size_t len) { return platform_multimodal_audio_play_downstream_opus(data, len); @@ -1190,6 +1195,11 @@ void domain_runtime_stepper_power_off(void) platform_stepper_power_off(); } +esp_err_t domain_runtime_stepper_release_hold(int timeout_ms) +{ + return platform_stepper_release_hold(timeout_ms); +} + esp_err_t domain_runtime_digital_input_configure(const domain_digital_input_config_t *config) { if (config == NULL) { @@ -1329,6 +1339,11 @@ esp_err_t domain_runtime_init_lcd_device(void) return platform_init_lcd_device(); } +esp_err_t domain_runtime_set_lcd_backlight_brightness(uint8_t brightness_percent) +{ + return platform_lcd_set_backlight_brightness(brightness_percent); +} + void domain_runtime_start_lcd_render_loop(void) { platform_start_lcd_render_loop(); diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 0681fdf..a037d74 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -301,6 +301,13 @@ esp_err_t platform_read_firmware_version(char *out, size_t out_len); esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle, int target_delay_us, int timeout_ms); +/** + * @brief Release stepper holding current with bounded lock acquisition. + * + * A negative timeout is invalid, zero is non-blocking, and a positive value + * bounds the wait for an in-progress movement to finish. + */ +esp_err_t platform_stepper_release_hold(int timeout_ms); void platform_stepper_power_off(void); typedef enum { @@ -425,6 +432,13 @@ esp_err_t platform_multimodal_audio_start_uplink( esp_err_t platform_multimodal_audio_stop_uplink(int timeout_ms); bool platform_multimodal_audio_is_uplink_running(void); bool platform_multimodal_audio_is_initialized(void); +/** + * @brief Gate the speaker power amplifier without stopping microphone capture. + * + * Disabling first mutes the codec output and then powers down the speaker PA. + * Enabling restores the PA while leaving output muted until playback resumes. + */ +esp_err_t platform_multimodal_audio_set_speaker_output_enabled(bool enabled); /** * @brief Register the single mono PCM observer for the active capture path. * diff --git a/components/platform/internal/multimodal/audio_pipeline.h b/components/platform/internal/multimodal/audio_pipeline.h index 0f4bd59..229fe54 100644 --- a/components/platform/internal/multimodal/audio_pipeline.h +++ b/components/platform/internal/multimodal/audio_pipeline.h @@ -39,6 +39,7 @@ esp_err_t multimodal_audio_start_uplink(multimodal_audio_uplink_cb_t cb, void *c esp_err_t multimodal_audio_stop_uplink(int timeout_ms); bool multimodal_audio_is_uplink_running(void); bool multimodal_audio_is_initialized(void); +esp_err_t multimodal_audio_set_speaker_output_enabled(bool enabled); esp_err_t multimodal_audio_register_pcm_observer( multimodal_audio_pcm_observer_t observer, void *observer_ctx); diff --git a/components/platform/internal/platform_internal.h b/components/platform/internal/platform_internal.h index f00637f..927af3f 100644 --- a/components/platform/internal/platform_internal.h +++ b/components/platform/internal/platform_internal.h @@ -14,6 +14,7 @@ esp_err_t platform_stepper_driver_init(void); esp_err_t platform_stepper_driver_rotate_angle_with_accel(float angle, int target_delay_us, int timeout_ms); +esp_err_t platform_stepper_driver_release_hold(int timeout_ms); void platform_stepper_driver_power_off(void); void platform_power_hold_init(void); esp_err_t platform_io_effects_init(void); diff --git a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c index 20b69fc..1225732 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c @@ -1369,6 +1369,37 @@ bool multimodal_audio_is_initialized(void) return s_audio.initialized; } +esp_err_t multimodal_audio_set_speaker_output_enabled(bool enabled) +{ + multimodal_audio_ctx_t *ctx = &s_audio; + if (!ctx->initialized || ctx->codec_dev == NULL || ctx->gpio_if == NULL || + ctx->gpio_if->set == NULL) { + return ESP_ERR_INVALID_STATE; + } + + if (!enabled) { + esp_err_t mute_err = set_downstream_output_mute_if_needed( + ctx, + true, + "Light standby"); + if (mute_err != ESP_OK) { + return mute_err; + } + } + + int ret = ctx->gpio_if->set(CONFIG_MULTIMODAL_AUDIO_PA_GPIO, enabled); + if (ret != ESP_CODEC_DEV_OK) { + ESP_LOGW(TAG, + "Speaker PA power %s failed: 0x%x", + enabled ? "on" : "off", + ret); + return (esp_err_t)ret; + } + + ESP_LOGI(TAG, "Speaker PA power %s", enabled ? "on" : "off"); + return ESP_OK; +} + esp_err_t multimodal_audio_register_downstream_playback_callback( multimodal_audio_downstream_playback_callback_t cb, void *cb_ctx) diff --git a/components/platform/src/platform_stepper_driver.c b/components/platform/src/platform_stepper_driver.c index 524285e..07c57e2 100644 --- a/components/platform/src/platform_stepper_driver.c +++ b/components/platform/src/platform_stepper_driver.c @@ -404,18 +404,21 @@ esp_err_t platform_stepper_driver_rotate_angle_with_accel(float angle, return err; } -void platform_stepper_driver_power_off(void) +esp_err_t platform_stepper_driver_release_hold(int timeout_ms) { + if (timeout_ms < 0) { + return ESP_ERR_INVALID_ARG; + } if (!ensure_stepper_motion_mutex()) { - return; + return ESP_ERR_INVALID_STATE; } - TickType_t lock_ticks = pdMS_TO_TICKS(PLATFORM_STEPPER_POWER_OFF_WAIT_MS); - if (lock_ticks == 0) { + TickType_t lock_ticks = pdMS_TO_TICKS(timeout_ms); + if (timeout_ms > 0 && lock_ticks == 0) { lock_ticks = 1; } if (xSemaphoreTake(s_stepper_motion_mutex, lock_ticks) != pdTRUE) { - return; + return ESP_ERR_TIMEOUT; } portENTER_CRITICAL(&s_stepper_mux); @@ -423,4 +426,10 @@ void platform_stepper_driver_power_off(void) portEXIT_CRITICAL(&s_stepper_mux); (void)xSemaphoreGive(s_stepper_motion_mutex); + return ESP_OK; +} + +void platform_stepper_driver_power_off(void) +{ + (void)platform_stepper_driver_release_hold(PLATFORM_STEPPER_POWER_OFF_WAIT_MS); } diff --git a/components/platform/src/runtime/platform_core_runtime.c b/components/platform/src/runtime/platform_core_runtime.c index ad612a0..c02f451 100644 --- a/components/platform/src/runtime/platform_core_runtime.c +++ b/components/platform/src/runtime/platform_core_runtime.c @@ -300,6 +300,18 @@ void platform_stepper_power_off(void) platform_stepper_driver_power_off(); } +esp_err_t platform_stepper_release_hold(int timeout_ms) +{ + if (timeout_ms < 0) { + return ESP_ERR_INVALID_ARG; + } +#if !CONFIG_TQ_STEPPER_ENABLE + return ESP_OK; +#else + return platform_stepper_driver_release_hold(timeout_ms); +#endif +} + esp_err_t platform_configure_digital_input(const platform_digital_input_config_t *config) { return platform_digital_input_driver_configure(config); diff --git a/components/platform/src/runtime/platform_multimodal_runtime.c b/components/platform/src/runtime/platform_multimodal_runtime.c index 7445fe4..b8e16a9 100644 --- a/components/platform/src/runtime/platform_multimodal_runtime.c +++ b/components/platform/src/runtime/platform_multimodal_runtime.c @@ -210,6 +210,11 @@ bool platform_multimodal_audio_is_initialized(void) return multimodal_audio_is_initialized(); } +esp_err_t platform_multimodal_audio_set_speaker_output_enabled(bool enabled) +{ + return multimodal_audio_set_speaker_output_enabled(enabled); +} + esp_err_t platform_multimodal_audio_register_pcm_observer( platform_multimodal_audio_pcm_observer_t observer, void *user_data) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 0452d40..ad0a4e1 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -342,9 +342,39 @@ menu "Example Configuration" Time without key, local-wake, or voice interaction before entering light standby. The production default is 5 minutes. + config TQ_LIGHT_STANDBY_DIM_BACKLIGHT + bool "Dim LCD backlight in light standby" + default y + depends on TQ_LIGHT_STANDBY_ENABLE && TQ_SCREEN_ENABLE + + config TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT + int "Light-standby backlight brightness (percent)" + default 10 + range 1 100 + depends on TQ_LIGHT_STANDBY_DIM_BACKLIGHT + help + Backlight brightness while the LCD render loop continues. + The minimum remains visible so the base expression persists. + + config TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT + bool "Disable speaker output in light standby" + default y + depends on TQ_LIGHT_STANDBY_ENABLE + help + Mute codec output and disable the speaker power amplifier + while preserving microphone capture for local wake words. + + config TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD + bool "Release stepper holding current in light standby" + default y + depends on TQ_LIGHT_STANDBY_ENABLE + help + De-energize the stepper coils. A later movement automatically + restores motor drive before stepping. + config TQ_LIGHT_STANDBY_TASK_STACK_BYTES int "Light-standby task stack size (bytes)" - default 3072 + default 4096 range 2048 8192 depends on TQ_LIGHT_STANDBY_ENABLE diff --git a/sdkconfig.defaults b/sdkconfig.defaults index a59a0c8..ea259ff 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -52,7 +52,11 @@ CONFIG_TQ_SYSTEM_EVENT_QUEUE_LEN=32 CONFIG_TQ_SYSTEM_EVENT_TASK_STACK=4096 CONFIG_TQ_LIGHT_STANDBY_ENABLE=y CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000 -CONFIG_TQ_LIGHT_STANDBY_TASK_STACK_BYTES=3072 +CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y +CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10 +CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y +CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y +CONFIG_TQ_LIGHT_STANDBY_TASK_STACK_BYTES=4096 CONFIG_TQ_LIGHT_STANDBY_TASK_PRIORITY=3 CONFIG_TQ_REMOTE_LOG_ENABLE=y CONFIG_TQ_REMOTE_LOG_ENDPOINT="http://47.98.175.183:8090/log/batch"