/* * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include "domain.h" #include #include #include #include "domain_internal.h" #include "platform.h" #include "esp_random.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" #include "sdkconfig.h" #ifndef CONFIG_MULTIMODAL_PTT_BUTTON_GPIO #error "CONFIG_MULTIMODAL_PTT_BUTTON_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_MULTIMODAL_PTT_BUTTON_ACTIVE_LEVEL #error "CONFIG_MULTIMODAL_PTT_BUTTON_ACTIVE_LEVEL must be defined in Kconfig" #endif #ifndef CONFIG_MULTIMODAL_PTT_BUTTON_PULLUP #define CONFIG_MULTIMODAL_PTT_BUTTON_PULLUP 0 #endif #ifndef CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN #define CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN 0 #endif #ifndef CONFIG_TQ_SLOT_SWITCH_GPIO #error "CONFIG_TQ_SLOT_SWITCH_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SLOT_SWITCH_ACTIVE_LEVEL #error "CONFIG_TQ_SLOT_SWITCH_ACTIVE_LEVEL must be defined in Kconfig" #endif #ifndef CONFIG_TQ_SLOT_SWITCH_PULLUP #define CONFIG_TQ_SLOT_SWITCH_PULLUP 0 #endif #ifndef CONFIG_TQ_SLOT_SWITCH_PULLDOWN #define CONFIG_TQ_SLOT_SWITCH_PULLDOWN 0 #endif #ifndef CONFIG_TQ_CALIBRATION_SWITCH_GPIO #error "CONFIG_TQ_CALIBRATION_SWITCH_GPIO must be defined in Kconfig" #endif #ifndef CONFIG_TQ_CALIBRATION_SWITCH_ACTIVE_LEVEL #error "CONFIG_TQ_CALIBRATION_SWITCH_ACTIVE_LEVEL must be defined in Kconfig" #endif #ifndef CONFIG_TQ_CALIBRATION_SWITCH_PULLUP #define CONFIG_TQ_CALIBRATION_SWITCH_PULLUP 0 #endif #ifndef CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN #define CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN 0 #endif static domain_multimodal_transport_event_callback_t s_domain_transport_event_cb; static void *s_domain_transport_event_user_data; static domain_multimodal_downstream_playback_callback_t s_domain_downstream_playback_cb; static void *s_domain_downstream_playback_user_data; static domain_wifi_behavior_hooks_t s_domain_wifi_behavior_hooks; static portMUX_TYPE s_domain_runtime_adapter_lock = portMUX_INITIALIZER_UNLOCKED; static const char *TAG = "domain_rt_adapters"; static const uint8_t s_server_error_frame_order[] = {0, 1, 2, 1, 0}; static const uint8_t s_wifi_disconnected_frame_order[] = {0, 1, 2, 3, 2, 1, 0}; static const uint8_t s_wifi_connecting_frame_order[] = {0}; static const uint8_t s_basic_face_frame_order[] = {0, 1, 2}; static const uint8_t s_muyu_frame_order[] = {0, 1, 2, 1}; static const uint8_t s_listening_frame_order[] = {0, 1, 2}; static const uint8_t s_ota_update_frame_order[] = {0, 1, 2}; static const char *const s_chouqian_result_asset_dirs[] = { "chouqian_buyun", "chouqian_heping", "chouqian_shangshang", "chouqian_xiaoji", "chouqian_zhongshang", }; typedef struct { const char *asset_dir; uint16_t icon_id; uint16_t icon_x; uint16_t icon_y; } domain_wuxing_result_t; /* Keep this table ordered by domain_wuxing_element_t. */ static const domain_wuxing_result_t s_wuxing_results[] = { {.asset_dir = "shui", .icon_id = 0, .icon_x = 124, .icon_y = 8}, {.asset_dir = "mu", .icon_id = 1, .icon_x = 217, .icon_y = 92}, {.asset_dir = "huo", .icon_id = 2, .icon_x = 198, .icon_y = 171}, {.asset_dir = "tu", .icon_id = 3, .icon_x = 46, .icon_y = 178}, {.asset_dir = "jin", .icon_id = 4, .icon_x = 18, .icon_y = 102}, }; _Static_assert(sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]) == DOMAIN_WUXING_ELEMENT_COUNT, "Wuxing result assets must match domain_wuxing_element_t"); /* * Wealth-direction reveal asset directories, indexed parallel to the direction * catalog order in components/domain/src/direction/domain_direction_catalog.c * (s_default_directions). Keep these in sync if the catalog order changes. */ static const char *const s_wealth_direction_asset_dirs[] = { "south", /* 0 南/S */ "southwest", /* 1 西南/SW */ "west", /* 2 西/W */ "northwest", /* 3 西北/NW */ "north", /* 4 北/N */ "northeast", /* 5 东北/NE */ "east", /* 6 东/E */ "southeast", /* 7 东南/SE */ }; #define DOMAIN_WEALTH_SPIN_FRAME_MS 50U #define DOMAIN_WEALTH_SPIN_REPEAT_COUNT 1U #define DOMAIN_WEALTH_DIRECTION_FRAME_MS 1200U #define DOMAIN_CHOUQIAN_FRAME_MS 500U #define DOMAIN_CHOUQIAN_FINAL_FRAME_MS 1500U #define DOMAIN_WUXING_QIANZHI_FRAME_MS 100U #define DOMAIN_WUXING_REVEAL_FRAME_MS 100U #define DOMAIN_WUXING_HOLD_FRAME_MS 500U #define DOMAIN_AUTH_KEY_STORE_NAMESPACE "auth_key" #define DOMAIN_AUTH_STATE_STORE_NAMESPACE "auth" #define DOMAIN_AUTH_KEY_STORE_BLOB_KEY "keymat" #define DOMAIN_AUTH_STATE_STORE_BLOB_KEY "state" #define DOMAIN_AUTH_LEGACY_KEY_PRIV_KEY "sk" #define DOMAIN_AUTH_LEGACY_KEY_PUB_KEY "pk" #define DOMAIN_AUTH_LEGACY_KEY_PUB_JKT "jkt" #define DOMAIN_AUTH_LEGACY_KEY_VERSION_KEY "kv" #define DOMAIN_AUTH_LEGACY_STATE_JWT_KEY "jwt" #define DOMAIN_AUTH_LEGACY_STATE_EXP_KEY "exp" #define DOMAIN_AUTH_LEGACY_STATE_SCOPE_KEY "scope" #define DOMAIN_AUTH_LEGACY_STATE_KEY_VERSION_KEY "kv" #define DOMAIN_AUTH_LEGACY_STATE_WAIT_FACTORY_KEY "waitf" #define DOMAIN_AUTH_LEGACY_STATE_FAIL_COUNT_KEY "failc" #define DOMAIN_AUTH_LEGACY_STATE_NEXT_RETRY_KEY "nextr" typedef struct { domain_expression_id_t id; platform_lcd_presentation_request_t platform_request; } domain_expression_catalog_entry_t; static const domain_expression_catalog_entry_t s_expression_catalog[] = { { .id = DOMAIN_EXPRESSION_IDLE, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 120, .interrupt_current = true, .return_to_idle = false, .asset_dir = "jichubiaoqing", .frame_order = s_basic_face_frame_order, .frame_order_count = sizeof(s_basic_face_frame_order) / sizeof(s_basic_face_frame_order[0]), .repeat_count = 0, .frame_ms = 100, }, }, { .id = DOMAIN_EXPRESSION_SERVER_ERROR, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 100, .interrupt_current = true, .return_to_idle = true, .asset_dir = "jichubiaoqing", .frame_order = s_server_error_frame_order, .frame_order_count = sizeof(s_server_error_frame_order) / sizeof(s_server_error_frame_order[0]), .repeat_count = 5, .frame_ms = 150, }, }, { .id = DOMAIN_EXPRESSION_SLOT_REELS, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_SLOT_REELS, .priority = 20, .interrupt_current = false, .return_to_idle = true, }, }, { .id = DOMAIN_EXPRESSION_WIFI_CONNECTING, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 120, .interrupt_current = true, .return_to_idle = false, .asset_dir = "wifi_connecting_qr", .frame_order = s_wifi_connecting_frame_order, .frame_order_count = sizeof(s_wifi_connecting_frame_order) / sizeof(s_wifi_connecting_frame_order[0]), .repeat_count = 0, .frame_ms = 1000, }, }, { .id = DOMAIN_EXPRESSION_WIFI_DISCONNECTED, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 120, .interrupt_current = false, .return_to_idle = false, .asset_dir = "wifi_disconnected", .frame_order = s_wifi_disconnected_frame_order, .frame_order_count = sizeof(s_wifi_disconnected_frame_order) / sizeof(s_wifi_disconnected_frame_order[0]), .repeat_count = 0, .frame_ms = 150, }, }, { .id = DOMAIN_EXPRESSION_WEALTH_SPIN, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 110, .interrupt_current = true, .return_to_idle = false, .asset_dir = "spin", .repeat_count = DOMAIN_WEALTH_SPIN_REPEAT_COUNT, .frame_ms = DOMAIN_WEALTH_SPIN_FRAME_MS, }, }, { .id = DOMAIN_EXPRESSION_MUYU, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 125, .interrupt_current = true, .return_to_idle = true, .asset_dir = "muyu", .frame_order = s_muyu_frame_order, .frame_order_count = sizeof(s_muyu_frame_order) / sizeof(s_muyu_frame_order[0]), .repeat_count = 1, .frame_ms = 50, .duration_ms = 0, }, }, { .id = DOMAIN_EXPRESSION_LISTENING, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 130, .interrupt_current = true, .return_to_idle = false, .asset_dir = "listening", .frame_order = s_listening_frame_order, .frame_order_count = sizeof(s_listening_frame_order) / sizeof(s_listening_frame_order[0]), .repeat_count = 0, .frame_ms = 500, }, }, { .id = DOMAIN_EXPRESSION_OTA_UPDATE, .platform_request = { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 200, .interrupt_current = true, .return_to_idle = false, .asset_dir = "ota", .frame_order = s_ota_update_frame_order, .frame_order_count = sizeof(s_ota_update_frame_order) / sizeof(s_ota_update_frame_order[0]), .repeat_count = 0, .frame_ms = 200, }, }, }; static void snapshot_domain_wifi_behavior_hooks(domain_wifi_behavior_hooks_t *out_hooks) { if (out_hooks == NULL) { return; } portENTER_CRITICAL(&s_domain_runtime_adapter_lock); *out_hooks = s_domain_wifi_behavior_hooks; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); } static void snapshot_domain_transport_callback(domain_multimodal_transport_event_callback_t *out_cb, void **out_user_data) { domain_multimodal_transport_event_callback_t cb = NULL; void *user_data = NULL; portENTER_CRITICAL(&s_domain_runtime_adapter_lock); cb = s_domain_transport_event_cb; user_data = s_domain_transport_event_user_data; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); if (out_cb != NULL) { *out_cb = cb; } if (out_user_data != NULL) { *out_user_data = user_data; } } static void snapshot_domain_downstream_playback_callback( domain_multimodal_downstream_playback_callback_t *out_cb, void **out_user_data) { domain_multimodal_downstream_playback_callback_t cb = NULL; void *user_data = NULL; portENTER_CRITICAL(&s_domain_runtime_adapter_lock); cb = s_domain_downstream_playback_cb; user_data = s_domain_downstream_playback_user_data; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); if (out_cb != NULL) { *out_cb = cb; } if (out_user_data != NULL) { *out_user_data = user_data; } } static esp_err_t storage_get_optional_u8(const char *ns, const char *key, uint8_t *out, bool *found_any) { esp_err_t err = platform_storage_get_u8(ns, key, out); if (err == ESP_OK) { if (found_any != NULL) { *found_any = true; } return ESP_OK; } return (err == ESP_ERR_NOT_FOUND) ? ESP_OK : err; } static esp_err_t storage_get_optional_u32(const char *ns, const char *key, uint32_t *out, bool *found_any) { esp_err_t err = platform_storage_get_u32(ns, key, out); if (err == ESP_OK) { if (found_any != NULL) { *found_any = true; } return ESP_OK; } return (err == ESP_ERR_NOT_FOUND) ? ESP_OK : err; } static esp_err_t storage_get_optional_u64(const char *ns, const char *key, uint64_t *out, bool *found_any) { esp_err_t err = platform_storage_get_u64(ns, key, out); if (err == ESP_OK) { if (found_any != NULL) { *found_any = true; } return ESP_OK; } return (err == ESP_ERR_NOT_FOUND) ? ESP_OK : err; } static void dispatch_domain_transport_event(domain_multimodal_transport_event_type_t type, const uint8_t *data, size_t data_len) { domain_multimodal_transport_event_callback_t cb = NULL; void *cb_user_data = NULL; snapshot_domain_transport_callback(&cb, &cb_user_data); if (cb == NULL) { return; } domain_multimodal_transport_event_t event = { .type = type, .data = data, .data_len = data_len, }; cb(&event, cb_user_data); } static void domain_transport_connected_adapter(void *user_data) { (void)user_data; dispatch_domain_transport_event(DOMAIN_MULTIMODAL_TRANSPORT_EVENT_CONNECTED, NULL, 0U); } static void domain_transport_disconnected_adapter(void *user_data) { (void)user_data; dispatch_domain_transport_event(DOMAIN_MULTIMODAL_TRANSPORT_EVENT_DISCONNECTED, NULL, 0U); } static void domain_transport_error_adapter(void *user_data) { (void)user_data; dispatch_domain_transport_event(DOMAIN_MULTIMODAL_TRANSPORT_EVENT_ERROR, NULL, 0U); } static void domain_transport_text_adapter(const uint8_t *data, size_t data_len, void *user_data) { (void)user_data; dispatch_domain_transport_event(DOMAIN_MULTIMODAL_TRANSPORT_EVENT_TEXT_MESSAGE, data, data_len); } static void domain_transport_binary_adapter(const uint8_t *data, size_t data_len, void *user_data) { (void)user_data; dispatch_domain_transport_event(DOMAIN_MULTIMODAL_TRANSPORT_EVENT_BINARY_MESSAGE, data, data_len); } static const char *domain_wifi_prompt_file_path(domain_wifi_prompt_id_t prompt) { switch (prompt) { case DOMAIN_WIFI_PROMPT_WIFI_SUCCESS: #ifdef CONFIG_VOICE_PROMPT_WIFI_SUCCESS_FILE return CONFIG_VOICE_PROMPT_WIFI_SUCCESS_FILE; #else return NULL; #endif case DOMAIN_WIFI_PROMPT_AFTER_WIFI_SUCCESS: #ifdef CONFIG_VOICE_PROMPT_AFTER_WIFI_SUCCESS_FILE return CONFIG_VOICE_PROMPT_AFTER_WIFI_SUCCESS_FILE; #else return NULL; #endif case DOMAIN_WIFI_PROMPT_WIFI_CONNECTING: #ifdef CONFIG_VOICE_PROMPT_WIFI_CONNECTING_FILE return CONFIG_VOICE_PROMPT_WIFI_CONNECTING_FILE; #else return NULL; #endif case DOMAIN_WIFI_PROMPT_WIFI_FAILED: #ifdef CONFIG_VOICE_PROMPT_WIFI_FAILED_FILE return CONFIG_VOICE_PROMPT_WIFI_FAILED_FILE; #else return NULL; #endif case DOMAIN_WIFI_PROMPT_WIFI_CONFIG: #ifdef CONFIG_VOICE_PROMPT_WIFI_CONFIG_FILE return CONFIG_VOICE_PROMPT_WIFI_CONFIG_FILE; #else return NULL; #endif default: return NULL; } } static void domain_downstream_playback_dispatch( domain_multimodal_downstream_playback_event_type_t type, uint32_t playback_generation) { domain_multimodal_downstream_playback_callback_t cb = NULL; void *cb_user_data = NULL; snapshot_domain_downstream_playback_callback(&cb, &cb_user_data); if (cb == NULL) { return; } const domain_multimodal_downstream_playback_event_t event = { .type = type, .playback_generation = playback_generation, }; cb(&event, cb_user_data); } static void domain_downstream_playback_started_adapter(uint32_t playback_generation, void *user_data) { (void)user_data; domain_downstream_playback_dispatch(DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_STARTED, playback_generation); } static void domain_downstream_playback_completed_adapter(uint32_t playback_generation, void *user_data) { (void)user_data; domain_downstream_playback_dispatch(DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_COMPLETED, playback_generation); } static void domain_downstream_playback_stopped_adapter(uint32_t playback_generation, void *user_data) { (void)user_data; domain_downstream_playback_dispatch(DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_STOPPED, playback_generation); } static void domain_downstream_playback_stalled_adapter(uint32_t playback_generation, void *user_data) { (void)user_data; domain_downstream_playback_dispatch(DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_STALLED, playback_generation); } static void domain_wifi_set_keep_awake_adapter(bool keep_awake, void *user_data) { (void)user_data; domain_wifi_behavior_hooks_t hooks = {0}; snapshot_domain_wifi_behavior_hooks(&hooks); if (hooks.set_keep_awake != NULL) { hooks.set_keep_awake(keep_awake); } } static esp_err_t domain_wifi_play_prompt_file_adapter(const char *path, void *user_data) { (void)user_data; domain_wifi_behavior_hooks_t hooks = {0}; snapshot_domain_wifi_behavior_hooks(&hooks); if (hooks.play_prompt_file == NULL) { return ESP_ERR_NOT_SUPPORTED; } return hooks.play_prompt_file(path); } esp_err_t domain_runtime_time_sync_start(const char *sntp_server, const char *posix_tz) { return platform_time_sync_start(sntp_server, posix_tz); } bool domain_runtime_time_is_synced(void) { return platform_time_is_synced(); } esp_err_t domain_runtime_post_json(const char *url, const char *payload, const char *device_id, const char *bearer_token, int timeout_ms, int tx_buffer_size, int rx_buffer_size, char *response_body, size_t response_body_capacity, size_t *out_response_body_len, int *out_status) { if (out_response_body_len != NULL) { *out_response_body_len = 0U; } if (out_status != NULL) { *out_status = -1; } if (url == NULL || url[0] == '\0' || payload == NULL || response_body == NULL || response_body_capacity < 2U) { return ESP_ERR_INVALID_ARG; } platform_https_header_t headers[2] = {0}; size_t header_count = 0U; if (device_id != NULL && device_id[0] != '\0') { headers[header_count++] = (platform_https_header_t) { .name = "X-Device-Id", .value = device_id, }; } char *auth_header = NULL; if (bearer_token != NULL && bearer_token[0] != '\0') { int needed = snprintf(NULL, 0, "Bearer %s", bearer_token); if (needed <= 0) { return ESP_FAIL; } auth_header = (char *)malloc((size_t)needed + 1U); if (auth_header == NULL) { return ESP_ERR_NO_MEM; } snprintf(auth_header, (size_t)needed + 1U, "Bearer %s", bearer_token); headers[header_count++] = (platform_https_header_t) { .name = "Authorization", .value = auth_header, }; } platform_https_post_json_request_t platform_request = { .url = url, .payload = payload, .headers = headers, .header_count = header_count, .timeout_ms = timeout_ms, .tx_buffer_size = tx_buffer_size, .rx_buffer_size = rx_buffer_size, }; platform_https_response_t platform_response = { .body = response_body, .body_capacity = response_body_capacity, .body_len = 0U, .status_code = -1, }; esp_err_t err = platform_https_post_json(&platform_request, &platform_response); free(auth_header); if (out_response_body_len != NULL) { *out_response_body_len = platform_response.body_len; } if (out_status != NULL) { *out_status = platform_response.status_code; } return err; } esp_err_t domain_runtime_multimodal_transport_start( const char *bearer_token, const char *workspace_id, domain_multimodal_transport_event_callback_t cb, void *user_data) { portENTER_CRITICAL(&s_domain_runtime_adapter_lock); s_domain_transport_event_cb = cb; s_domain_transport_event_user_data = user_data; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); const platform_multimodal_transport_callbacks_t callbacks = { .on_connected = domain_transport_connected_adapter, .on_disconnected = domain_transport_disconnected_adapter, .on_error = domain_transport_error_adapter, .on_text_payload = domain_transport_text_adapter, .on_binary_payload = domain_transport_binary_adapter, .user_data = NULL, }; esp_err_t err = platform_multimodal_transport_start( bearer_token, workspace_id, (cb != NULL) ? &callbacks : NULL); if (err != ESP_OK) { portENTER_CRITICAL(&s_domain_runtime_adapter_lock); s_domain_transport_event_cb = NULL; s_domain_transport_event_user_data = NULL; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); } return err; } esp_err_t domain_runtime_multimodal_transport_stop(int timeout_ms) { esp_err_t err = platform_multimodal_transport_stop(timeout_ms); portENTER_CRITICAL(&s_domain_runtime_adapter_lock); s_domain_transport_event_cb = NULL; s_domain_transport_event_user_data = NULL; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); return err; } bool domain_runtime_multimodal_transport_is_connected(void) { return platform_multimodal_transport_is_connected(); } esp_err_t domain_runtime_multimodal_transport_send_text(const char *label, const char *text) { return platform_multimodal_transport_send_text(label, text); } esp_err_t domain_runtime_multimodal_transport_send_text_realtime(const char *label, const char *text) { return platform_multimodal_transport_send_text_realtime(label, text); } esp_err_t domain_runtime_multimodal_transport_send_binary_realtime(const uint8_t *data, size_t len) { return platform_multimodal_transport_send_binary_realtime(data, len); } esp_err_t domain_runtime_multimodal_audio_init(void) { return platform_multimodal_audio_init(); } void domain_runtime_multimodal_audio_deinit(void) { platform_multimodal_audio_deinit(); } esp_err_t domain_runtime_multimodal_audio_start_uplink( domain_multimodal_audio_uplink_cb_t cb, void *user_data) { return platform_multimodal_audio_start_uplink(cb, user_data); } esp_err_t domain_runtime_multimodal_audio_stop_uplink(int timeout_ms) { return platform_multimodal_audio_stop_uplink(timeout_ms); } bool domain_runtime_multimodal_audio_is_uplink_running(void) { return platform_multimodal_audio_is_uplink_running(); } 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); } esp_err_t domain_runtime_multimodal_audio_begin_downstream_response( uint32_t *out_playback_generation) { return platform_multimodal_audio_begin_downstream_response(out_playback_generation); } esp_err_t domain_runtime_multimodal_audio_finish_downstream_response(void) { return platform_multimodal_audio_finish_downstream_response(); } esp_err_t domain_runtime_multimodal_audio_stop_downstream_playback(void) { return platform_multimodal_audio_stop_downstream_playback(); } esp_err_t domain_runtime_multimodal_audio_register_downstream_playback_callback( domain_multimodal_downstream_playback_callback_t cb, void *user_data) { const platform_multimodal_downstream_playback_callbacks_t callbacks = { .on_started = domain_downstream_playback_started_adapter, .on_completed = domain_downstream_playback_completed_adapter, .on_stopped = domain_downstream_playback_stopped_adapter, .on_stalled = domain_downstream_playback_stalled_adapter, .user_data = NULL, }; esp_err_t err = platform_multimodal_audio_register_downstream_playback_callbacks( cb != NULL ? &callbacks : NULL); if (err == ESP_OK) { portENTER_CRITICAL(&s_domain_runtime_adapter_lock); s_domain_downstream_playback_cb = cb; s_domain_downstream_playback_user_data = user_data; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); } return err; } static domain_digital_input_config_t domain_runtime_multimodal_ptt_button_config(void) { domain_digital_input_config_t config = { .gpio_num = CONFIG_MULTIMODAL_PTT_BUTTON_GPIO, .active_level = CONFIG_MULTIMODAL_PTT_BUTTON_ACTIVE_LEVEL, .pullup_enabled = CONFIG_MULTIMODAL_PTT_BUTTON_PULLUP, .pulldown_enabled = CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN, }; return config; } esp_err_t domain_runtime_multimodal_ptt_button_init(void) { domain_digital_input_config_t config = domain_runtime_multimodal_ptt_button_config(); ESP_LOGI(TAG, "PTT button config gpio=%d active_level=%d pullup=%d pulldown=%d", config.gpio_num, config.active_level, config.pullup_enabled, config.pulldown_enabled); return domain_runtime_digital_input_configure(&config); } bool domain_runtime_multimodal_ptt_button_is_pressed(void) { domain_digital_input_config_t config = domain_runtime_multimodal_ptt_button_config(); return domain_runtime_digital_input_is_active(&config); } static domain_digital_input_config_t domain_runtime_slot_feature_switch_config(void) { domain_digital_input_config_t config = { .gpio_num = CONFIG_TQ_SLOT_SWITCH_GPIO, .active_level = CONFIG_TQ_SLOT_SWITCH_ACTIVE_LEVEL, .pullup_enabled = CONFIG_TQ_SLOT_SWITCH_PULLUP, .pulldown_enabled = CONFIG_TQ_SLOT_SWITCH_PULLDOWN, }; return config; } esp_err_t domain_runtime_slot_feature_switch_init(void) { domain_digital_input_config_t config = domain_runtime_slot_feature_switch_config(); ESP_LOGI(TAG, "Slot feature switch config gpio=%d active_level=%d pullup=%d pulldown=%d", config.gpio_num, config.active_level, config.pullup_enabled, config.pulldown_enabled); return domain_runtime_digital_input_configure(&config); } bool domain_runtime_slot_feature_switch_is_active(void) { domain_digital_input_config_t config = domain_runtime_slot_feature_switch_config(); return domain_runtime_digital_input_is_active(&config); } static domain_digital_input_config_t domain_runtime_calibration_switch_config(void) { domain_digital_input_config_t config = { .gpio_num = CONFIG_TQ_CALIBRATION_SWITCH_GPIO, .active_level = CONFIG_TQ_CALIBRATION_SWITCH_ACTIVE_LEVEL, .pullup_enabled = CONFIG_TQ_CALIBRATION_SWITCH_PULLUP, .pulldown_enabled = CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN, }; return config; } esp_err_t domain_runtime_calibration_switch_init(void) { domain_digital_input_config_t config = domain_runtime_calibration_switch_config(); ESP_LOGI(TAG, "Calibration switch config gpio=%d active_level=%d pullup=%d pulldown=%d", config.gpio_num, config.active_level, config.pullup_enabled, config.pulldown_enabled); esp_err_t err = domain_runtime_digital_input_configure(&config); if (err == ESP_OK) { ESP_LOGI(TAG, "KEY1/SW1 ready gpio=%d initial_state=%s", config.gpio_num, domain_runtime_digital_input_is_active(&config) ? "pressed" : "released"); } return err; } bool domain_runtime_calibration_switch_is_active(void) { domain_digital_input_config_t config = domain_runtime_calibration_switch_config(); return domain_runtime_digital_input_is_active(&config); } esp_err_t domain_runtime_platform_init(void) { return platform_init(); } esp_err_t domain_runtime_prepare_storage(void) { return platform_prepare_storage(); } esp_err_t domain_runtime_mount_littlefs(void) { return platform_mount_littlefs(); } esp_err_t domain_runtime_mount_logfs(void) { return platform_mount_logfs(); } void domain_runtime_read_device_identity(char *device_id, size_t device_id_len, char *serial_number, size_t serial_number_len) { platform_read_device_identity(device_id, device_id_len, serial_number, serial_number_len); } esp_err_t domain_runtime_read_firmware_version(char *out, size_t out_len) { return platform_read_firmware_version(out, out_len); } esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number) { return platform_write_device_identity(device_id, serial_number); } typedef struct { uint8_t priv_key[32]; uint8_t pub_key[65]; uint8_t pub_key_jkt[32]; uint32_t key_version; bool valid; } domain_auth_key_material_legacy_blob_t; typedef struct { char jwt[1536]; uint64_t exp_epoch_s; uint32_t scope_mask; uint32_t key_version; bool jwt_valid; bool wait_factory_persisted; uint32_t fail_count; uint64_t next_retry_epoch_s; } domain_auth_state_legacy_blob_t; static bool auth_store_record_target(domain_runtime_auth_record_t record, const char **out_ns, const char **out_key) { if (out_ns == NULL || out_key == NULL) { return false; } switch (record) { case DOMAIN_RUNTIME_AUTH_RECORD_KEY_MATERIAL: *out_ns = DOMAIN_AUTH_KEY_STORE_NAMESPACE; *out_key = DOMAIN_AUTH_KEY_STORE_BLOB_KEY; return true; case DOMAIN_RUNTIME_AUTH_RECORD_STATE: *out_ns = DOMAIN_AUTH_STATE_STORE_NAMESPACE; *out_key = DOMAIN_AUTH_STATE_STORE_BLOB_KEY; return true; default: return false; } } static esp_err_t load_legacy_auth_key_material(void *out, size_t out_len, size_t *out_len_actual) { if (out_len_actual != NULL) { *out_len_actual = 0U; } if (out == NULL || out_len < sizeof(domain_auth_key_material_legacy_blob_t)) { return ESP_ERR_INVALID_SIZE; } domain_auth_key_material_legacy_blob_t key = {0}; size_t priv_len = 0U; size_t pub_len = 0U; size_t jkt_len = 0U; esp_err_t priv_err = platform_storage_get_blob(DOMAIN_AUTH_KEY_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_KEY_PRIV_KEY, key.priv_key, sizeof(key.priv_key), &priv_len); esp_err_t pub_err = platform_storage_get_blob(DOMAIN_AUTH_KEY_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_KEY_PUB_KEY, key.pub_key, sizeof(key.pub_key), &pub_len); esp_err_t jkt_err = platform_storage_get_blob(DOMAIN_AUTH_KEY_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_KEY_PUB_JKT, key.pub_key_jkt, sizeof(key.pub_key_jkt), &jkt_len); if (priv_err != ESP_OK || pub_err != ESP_OK || jkt_err != ESP_OK) { return ESP_ERR_NOT_FOUND; } if (priv_len != sizeof(key.priv_key) || pub_len != sizeof(key.pub_key) || jkt_len != sizeof(key.pub_key_jkt)) { return ESP_ERR_INVALID_SIZE; } uint32_t key_version = 0U; esp_err_t kv_err = platform_storage_get_u32(DOMAIN_AUTH_KEY_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_KEY_VERSION_KEY, &key_version); key.key_version = (kv_err == ESP_OK && key_version > 0U) ? key_version : 1U; key.valid = true; memcpy(out, &key, sizeof(key)); if (out_len_actual != NULL) { *out_len_actual = sizeof(key); } return ESP_OK; } static esp_err_t load_legacy_auth_state(void *out, size_t out_len, size_t *out_len_actual) { if (out_len_actual != NULL) { *out_len_actual = 0U; } if (out == NULL || out_len < sizeof(domain_auth_state_legacy_blob_t)) { return ESP_ERR_INVALID_SIZE; } domain_auth_state_legacy_blob_t *state = calloc(1, sizeof(*state)); if (state == NULL) { return ESP_ERR_NO_MEM; } bool found_any = false; size_t jwt_len = 0U; esp_err_t err = platform_storage_get_string(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_JWT_KEY, state->jwt, sizeof(state->jwt), &jwt_len); if (err == ESP_OK) { found_any = true; state->jwt_valid = jwt_len > 1U && state->jwt[0] != '\0'; } else if (err != ESP_ERR_NOT_FOUND) { free(state); return err; } err = storage_get_optional_u64(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_EXP_KEY, &state->exp_epoch_s, &found_any); if (err != ESP_OK) { free(state); return err; } err = storage_get_optional_u32(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_SCOPE_KEY, &state->scope_mask, &found_any); if (err != ESP_OK) { free(state); return err; } err = storage_get_optional_u32(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_KEY_VERSION_KEY, &state->key_version, &found_any); if (err != ESP_OK) { free(state); return err; } uint8_t wait_factory = 0U; err = storage_get_optional_u8(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_WAIT_FACTORY_KEY, &wait_factory, &found_any); if (err != ESP_OK) { free(state); return err; } state->wait_factory_persisted = wait_factory != 0U; err = storage_get_optional_u32(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_FAIL_COUNT_KEY, &state->fail_count, &found_any); if (err != ESP_OK) { free(state); return err; } err = storage_get_optional_u64(DOMAIN_AUTH_STATE_STORE_NAMESPACE, DOMAIN_AUTH_LEGACY_STATE_NEXT_RETRY_KEY, &state->next_retry_epoch_s, &found_any); if (err != ESP_OK) { free(state); return err; } if (!found_any) { free(state); return ESP_ERR_NOT_FOUND; } memcpy(out, state, sizeof(*state)); if (out_len_actual != NULL) { *out_len_actual = sizeof(*state); } free(state); return ESP_OK; } static esp_err_t load_legacy_auth_record(domain_runtime_auth_record_t record, void *out, size_t out_len, size_t *out_len_actual) { switch (record) { case DOMAIN_RUNTIME_AUTH_RECORD_KEY_MATERIAL: return load_legacy_auth_key_material(out, out_len, out_len_actual); case DOMAIN_RUNTIME_AUTH_RECORD_STATE: return load_legacy_auth_state(out, out_len, out_len_actual); default: return ESP_ERR_INVALID_ARG; } } esp_err_t domain_runtime_auth_store_load_record(domain_runtime_auth_record_t record, void *out, size_t out_len, size_t *out_len_actual) { if (out_len_actual != NULL) { *out_len_actual = 0U; } if (out == NULL || out_len == 0U) { return ESP_ERR_INVALID_ARG; } const char *ns = NULL; const char *key = NULL; if (!auth_store_record_target(record, &ns, &key)) { return ESP_ERR_INVALID_ARG; } size_t actual = 0U; esp_err_t err = platform_storage_get_blob(ns, key, out, out_len, &actual); if (err == ESP_OK) { if (out_len_actual != NULL) { *out_len_actual = actual; } return ESP_OK; } if (err != ESP_ERR_NOT_FOUND) { return err; } err = load_legacy_auth_record(record, out, out_len, &actual); if (err != ESP_OK) { return err; } esp_err_t save_err = platform_storage_set_blob(ns, key, out, actual); if (save_err != ESP_OK) { ESP_LOGW(TAG, "Persist migrated auth record %d failed: %s", (int)record, esp_err_to_name(save_err)); } else { ESP_LOGI(TAG, "Migrated legacy auth record %d", (int)record); } if (out_len_actual != NULL) { *out_len_actual = actual; } return ESP_OK; } esp_err_t domain_runtime_auth_store_save_record(domain_runtime_auth_record_t record, const void *data, size_t len) { if (data == NULL || len == 0U) { return ESP_ERR_INVALID_ARG; } const char *ns = NULL; const char *key = NULL; if (!auth_store_record_target(record, &ns, &key)) { return ESP_ERR_INVALID_ARG; } return platform_storage_set_blob(ns, key, data, len); } esp_err_t domain_runtime_console_install(void) { return platform_console_install(); } int domain_runtime_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms) { return platform_console_read_bytes(buf, len, timeout_ms); } void domain_runtime_console_write_line(const char *line) { platform_console_write_line(line); } esp_err_t domain_runtime_stepper_rotate_angle_with_accel_timeout(float angle, int target_delay_us, int timeout_ms) { return platform_stepper_rotate_angle_with_accel_timeout(angle, target_delay_us, timeout_ms); } 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) { return ESP_ERR_INVALID_ARG; } platform_digital_input_config_t platform_config = { .gpio_num = config->gpio_num, .active_level = config->active_level, .pullup_enabled = config->pullup_enabled, .pulldown_enabled = config->pulldown_enabled, }; return platform_configure_digital_input(&platform_config); } bool domain_runtime_digital_input_is_active(const domain_digital_input_config_t *config) { if (config == NULL) { return false; } platform_digital_input_config_t platform_config = { .gpio_num = config->gpio_num, .active_level = config->active_level, .pullup_enabled = config->pullup_enabled, .pulldown_enabled = config->pulldown_enabled, }; return platform_is_digital_input_active(&platform_config); } void domain_runtime_behavior_set_keep_awake(bool keep_awake) { platform_behavior_set_keep_awake(keep_awake); } esp_err_t domain_runtime_behavior_play_prompt(domain_wifi_prompt_id_t prompt) { const char *path = domain_wifi_prompt_file_path(prompt); if (path == NULL || path[0] == '\0') { return ESP_ERR_NOT_SUPPORTED; } return platform_behavior_play_prompt_file(path); } esp_err_t domain_runtime_behavior_play_prompt_file(const char *path) { return platform_behavior_play_prompt_file(path); } void domain_runtime_wifi_portal_set_behavior_hooks(const domain_wifi_behavior_hooks_t *hooks) { if (hooks == NULL) { portENTER_CRITICAL(&s_domain_runtime_adapter_lock); memset(&s_domain_wifi_behavior_hooks, 0, sizeof(s_domain_wifi_behavior_hooks)); portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); platform_wifi_portal_set_behavior_hooks(NULL); return; } portENTER_CRITICAL(&s_domain_runtime_adapter_lock); s_domain_wifi_behavior_hooks = *hooks; portEXIT_CRITICAL(&s_domain_runtime_adapter_lock); platform_wifi_behavior_hooks_t platform_hooks = { .set_keep_awake = domain_wifi_set_keep_awake_adapter, .play_prompt_file = domain_wifi_play_prompt_file_adapter, .user_data = NULL, }; platform_wifi_portal_set_behavior_hooks(&platform_hooks); } esp_err_t domain_runtime_wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password) { return platform_wifi_portal_init_defaults(seed_ssid, seed_password); } bool domain_runtime_wifi_portal_has_saved_credentials(void) { return platform_wifi_portal_has_saved_credentials(); } bool domain_runtime_wifi_portal_sta_is_connected(void) { return platform_wifi_portal_sta_is_connected(); } esp_err_t domain_runtime_wifi_portal_start_config_ap(const char *ssid_prefix) { return platform_wifi_portal_start_config_ap(ssid_prefix); } bool domain_runtime_wifi_connect_error_keeps_credentials(esp_err_t err) { return platform_wifi_connect_error_keeps_credentials(err); } esp_err_t domain_runtime_wifi_portal_connect_sta(int timeout_ms) { return platform_wifi_portal_connect_sta(timeout_ms); } esp_err_t domain_runtime_wifi_portal_connect_sta_with_credentials( const char *ssid, const char *password, int timeout_ms) { return platform_wifi_portal_connect_sta_with_credentials(ssid, password, timeout_ms); } esp_err_t domain_runtime_wifi_portal_reconnect_sta(int timeout_ms) { return platform_wifi_portal_reconnect_sta(timeout_ms); } esp_err_t domain_runtime_wifi_portal_stop_sta(void) { return platform_wifi_portal_stop_sta(); } esp_err_t domain_runtime_wifi_portal_set_light_standby(bool standby) { return platform_wifi_portal_set_light_standby(standby); } bool domain_runtime_is_network_connect_error(esp_err_t err) { return platform_https_is_connect_error(err); } esp_err_t domain_runtime_wifi_portal_save_credentials(const char *ssid, const char *password) { return platform_wifi_portal_save_credentials(ssid, password); } esp_err_t domain_runtime_wifi_portal_clear_saved_credentials(void) { return platform_wifi_portal_clear_saved_credentials(); } 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(); } esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms) { return platform_stop_lcd_render_loop(timeout_ms); } esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *status) { if (status == NULL) { return ESP_ERR_INVALID_ARG; } const platform_lcd_status_overlay_t overlay = { .low_battery_visible = status->valid && (status->level_state == DOMAIN_BATTERY_LEVEL_LOW || status->level_state == DOMAIN_BATTERY_LEVEL_CRITICAL), .charging_visible = status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING, }; return platform_lcd_set_status_overlay(&overlay); } static const domain_expression_catalog_entry_t *domain_find_expression_entry(domain_expression_id_t id) { for (size_t i = 0; i < sizeof(s_expression_catalog) / sizeof(s_expression_catalog[0]); ++i) { if (s_expression_catalog[i].id == id) { return &s_expression_catalog[i]; } } return NULL; } static esp_err_t domain_resolve_expression_platform_request( const domain_expression_request_t *request, platform_lcd_presentation_request_t *out_platform_request) { if (request == NULL || out_platform_request == NULL) { return ESP_ERR_INVALID_ARG; } if (request->id == DOMAIN_EXPRESSION_WEALTH_DIRECTION) { size_t direction_count = sizeof(s_wealth_direction_asset_dirs) / sizeof(s_wealth_direction_asset_dirs[0]); if (request->direction_index >= direction_count) { ESP_LOGW(TAG, "Wealth-direction reveal index out of range: %u (max %u)", (unsigned int)request->direction_index, (unsigned int)direction_count); return ESP_ERR_INVALID_ARG; } *out_platform_request = (platform_lcd_presentation_request_t) { .kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE, .priority = 110, .interrupt_current = true, /* equal priority + interrupt replaces the spin loop */ .return_to_idle = true, .asset_dir = s_wealth_direction_asset_dirs[request->direction_index], .repeat_count = 1, /* play the reveal once, then return to idle */ .frame_ms = DOMAIN_WEALTH_DIRECTION_FRAME_MS, }; return ESP_OK; } if (request->id == DOMAIN_EXPRESSION_CHOUQIAN) { static platform_lcd_presentation_step_t s_chouqian_steps[6]; const size_t result_count = sizeof(s_chouqian_result_asset_dirs) / sizeof(s_chouqian_result_asset_dirs[0]); size_t result_index = esp_random() % result_count; if (request->chouqian_result_index_valid) { if (request->chouqian_result_index >= result_count) { ESP_LOGW(TAG, "Chouqian result index out of range: %u (max %u)", (unsigned int)request->chouqian_result_index, (unsigned int)result_count); return ESP_ERR_INVALID_ARG; } result_index = request->chouqian_result_index; } const char *result_asset = s_chouqian_result_asset_dirs[result_index]; s_chouqian_steps[0] = (platform_lcd_presentation_step_t) { .asset_dir = "chouqian_qianzhi", .frame_index = 0, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_CHOUQIAN_FRAME_MS, }; s_chouqian_steps[1] = (platform_lcd_presentation_step_t) { .asset_dir = "chouqian_qianzhi", .frame_index = 1, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_CHOUQIAN_FRAME_MS, }; s_chouqian_steps[2] = (platform_lcd_presentation_step_t) { .asset_dir = "chouqian_qianzhi", .frame_index = 2, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_CHOUQIAN_FRAME_MS, }; s_chouqian_steps[3] = (platform_lcd_presentation_step_t) { .asset_dir = result_asset, .frame_index = 0, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_CHOUQIAN_FRAME_MS, }; s_chouqian_steps[4] = (platform_lcd_presentation_step_t) { .asset_dir = result_asset, .frame_index = 1, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_CHOUQIAN_FRAME_MS, }; s_chouqian_steps[5] = (platform_lcd_presentation_step_t) { .asset_dir = result_asset, .frame_index = 2, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_CHOUQIAN_FINAL_FRAME_MS, }; *out_platform_request = (platform_lcd_presentation_request_t) { .kind = PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE, .priority = 125, .interrupt_current = true, .return_to_idle = true, .steps = s_chouqian_steps, .step_count = sizeof(s_chouqian_steps) / sizeof(s_chouqian_steps[0]), }; ESP_LOGI(TAG, "Chouqian result asset=%s", result_asset); return ESP_OK; } if (request->id == DOMAIN_EXPRESSION_WUXING) { static platform_lcd_presentation_step_t s_wuxing_steps[3]; const size_t result_count = sizeof(s_wuxing_results) / sizeof(s_wuxing_results[0]); size_t result_index = esp_random() % result_count; if (request->wuxing_element_valid) { if ((size_t)request->wuxing_element >= result_count) { ESP_LOGW(TAG, "Wuxing element out of range: %u (max %u)", (unsigned int)request->wuxing_element, (unsigned int)result_count); return ESP_ERR_INVALID_ARG; } result_index = (size_t)request->wuxing_element; } const domain_wuxing_result_t *result = &s_wuxing_results[result_index]; s_wuxing_steps[0] = (platform_lcd_presentation_step_t) { .asset_dir = "qianzhiwuxing", .frame_index = 0, .icon_id = result->icon_id, .icon_x = result->icon_x, .icon_y = result->icon_y, .delay_ms = DOMAIN_WUXING_QIANZHI_FRAME_MS, }; s_wuxing_steps[1] = (platform_lcd_presentation_step_t) { .asset_dir = result->asset_dir, .frame_index = 0, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_WUXING_REVEAL_FRAME_MS, }; s_wuxing_steps[2] = (platform_lcd_presentation_step_t) { .asset_dir = result->asset_dir, .frame_index = 1, .icon_id = PLATFORM_LCD_PRESENTATION_NO_ICON, .delay_ms = DOMAIN_WUXING_HOLD_FRAME_MS, }; *out_platform_request = (platform_lcd_presentation_request_t) { .kind = PLATFORM_LCD_PRESENTATION_STEP_SEQUENCE, .priority = 125, .interrupt_current = true, .return_to_idle = true, .steps = s_wuxing_steps, .step_count = sizeof(s_wuxing_steps) / sizeof(s_wuxing_steps[0]), }; ESP_LOGI(TAG, "Wuxing result asset=%s", result->asset_dir); return ESP_OK; } const domain_expression_catalog_entry_t *entry = domain_find_expression_entry(request->id); if (entry == NULL) { ESP_LOGW(TAG, "Unknown expression request id=%d trigger=%d", (int)request->id, (int)request->trigger); return ESP_ERR_NOT_FOUND; } *out_platform_request = entry->platform_request; return ESP_OK; } esp_err_t domain_runtime_request_expression(const domain_expression_request_t *request) { platform_lcd_presentation_request_t platform_request = {0}; esp_err_t resolve_err = domain_resolve_expression_platform_request(request, &platform_request); if (resolve_err != ESP_OK) { return resolve_err; } ESP_LOGI(TAG, "Expression request id=%d trigger=%d", (int)request->id, (int)request->trigger); return platform_lcd_request_presentation(&platform_request); } esp_err_t domain_runtime_preheat_expression(const domain_expression_request_t *request) { platform_lcd_presentation_request_t platform_request = {0}; esp_err_t resolve_err = domain_resolve_expression_platform_request(request, &platform_request); if (resolve_err != ESP_OK) { return resolve_err; } if (platform_request.kind != PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE) { return ESP_ERR_INVALID_ARG; } ESP_LOGI(TAG, "Expression preheat id=%d trigger=%d", (int)request->id, (int)request->trigger); return platform_lcd_preheat_presentation(&platform_request); } esp_err_t domain_runtime_request_expression_blocking(const domain_expression_request_t *request, uint32_t timeout_ms) { /* Snapshot the playback completion counter before queuing so the wait stays * robust to the asynchronous request not having started yet. */ uint32_t since_count = platform_lcd_presentation_completion_count(); esp_err_t err = domain_runtime_request_expression(request); if (err != ESP_OK) { return err; } return platform_lcd_wait_presentation_completion(since_count, timeout_ms); }