/* * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #pragma once #include #include #include #include "esp_err.h" #ifdef __cplusplus extern "C" { #endif typedef struct { float initial_angle; int rotate_speed_us; } domain_direction_axis_config_t; typedef struct { bool enabled; float calibration_angle; float step_angle_deg; float max_sweep_deg; bool search_clockwise; int settle_delay_ms; } domain_direction_calibration_t; typedef enum { DOMAIN_WIFI_PROMPT_WIFI_SUCCESS = 0, DOMAIN_WIFI_PROMPT_AFTER_WIFI_SUCCESS, DOMAIN_WIFI_PROMPT_WIFI_CONNECTING, DOMAIN_WIFI_PROMPT_WIFI_FAILED, DOMAIN_WIFI_PROMPT_WIFI_CONFIG, } domain_wifi_prompt_id_t; typedef struct { void (*set_keep_awake)(bool keep_awake); esp_err_t (*play_prompt)(domain_wifi_prompt_id_t prompt); esp_err_t (*play_prompt_file)(const char *path); } domain_wifi_behavior_hooks_t; typedef enum { DOMAIN_MULTIMODAL_TRANSPORT_EVENT_CONNECTED = 0, DOMAIN_MULTIMODAL_TRANSPORT_EVENT_DISCONNECTED, DOMAIN_MULTIMODAL_TRANSPORT_EVENT_ERROR, DOMAIN_MULTIMODAL_TRANSPORT_EVENT_TEXT_MESSAGE, DOMAIN_MULTIMODAL_TRANSPORT_EVENT_BINARY_MESSAGE, } domain_multimodal_transport_event_type_t; typedef struct { domain_multimodal_transport_event_type_t type; const uint8_t *data; size_t data_len; } domain_multimodal_transport_event_t; typedef void (*domain_multimodal_transport_event_callback_t)( const domain_multimodal_transport_event_t *event, void *user_data); typedef esp_err_t (*domain_multimodal_audio_uplink_cb_t)( const uint8_t *data, size_t len, void *user_data); typedef enum { DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_STARTED = 0, DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_COMPLETED, DOMAIN_MULTIMODAL_DOWNSTREAM_PLAYBACK_STOPPED, } domain_multimodal_downstream_playback_event_type_t; typedef struct { domain_multimodal_downstream_playback_event_type_t type; } domain_multimodal_downstream_playback_event_t; typedef void (*domain_multimodal_downstream_playback_callback_t)( const domain_multimodal_downstream_playback_event_t *event, void *user_data); typedef enum { DOMAIN_EXPRESSION_IDLE = 0, DOMAIN_EXPRESSION_SERVER_ERROR, DOMAIN_EXPRESSION_SLOT_REELS, DOMAIN_EXPRESSION_WIFI_DISCONNECTED, DOMAIN_EXPRESSION_WEALTH_SPIN, DOMAIN_EXPRESSION_WEALTH_DIRECTION, DOMAIN_EXPRESSION_MUYU, DOMAIN_EXPRESSION_CHOUQIAN, DOMAIN_EXPRESSION_WUXING, } domain_expression_id_t; typedef enum { DOMAIN_EXPRESSION_TRIGGER_UNSPECIFIED = 0, DOMAIN_EXPRESSION_TRIGGER_SERVER_TASK_FAILED, DOMAIN_EXPRESSION_TRIGGER_SERVER_OUTPUT_ERROR, DOMAIN_EXPRESSION_TRIGGER_SLOT_SWITCH, DOMAIN_EXPRESSION_TRIGGER_WIFI_DISCONNECTED, DOMAIN_EXPRESSION_TRIGGER_WIFI_RECONNECTED, DOMAIN_EXPRESSION_TRIGGER_MODEL_DIRECTION, DOMAIN_EXPRESSION_TRIGGER_DAILY_FORTUNE, DOMAIN_EXPRESSION_TRIGGER_PTT_DOUBLE_CLICK, DOMAIN_EXPRESSION_TRIGGER_SW2_SINGLE_CLICK, DOMAIN_EXPRESSION_TRIGGER_SW2_DOUBLE_CLICK, } domain_expression_trigger_t; typedef struct { domain_expression_id_t id; domain_expression_trigger_t trigger; size_t direction_index; /* Only used by DOMAIN_EXPRESSION_WEALTH_DIRECTION */ } domain_expression_request_t; esp_err_t domain_init(const domain_direction_axis_config_t *config, const domain_direction_calibration_t *calibration); const domain_direction_axis_config_t *domain_direction_default_axis_config(void); const domain_direction_calibration_t *domain_direction_default_calibration(void); bool domain_is_initialized(void); size_t domain_get_direction_count(void); bool domain_get_direction_view(size_t index, const char **out_name_cn, const char **out_name_en, float *out_angle); bool domain_find_direction_by_name(const char *input, size_t *out_index); bool domain_get_current_direction_index(size_t *out_index); float domain_get_current_angle(void); void domain_set_current_angle(float angle); int domain_get_rotate_speed_us(void); /** * @brief Rotate to an absolute direction angle. * * `timeout_ms <= 0` uses the default domain timeout. The timeout is the total * caller-visible budget, including state-lock acquisition and motor movement. */ esp_err_t domain_rotate_to_angle_with_timeout(float target_angle, int timeout_ms); esp_err_t domain_apply_rotation_delta(float delta); void domain_set_calibration(const domain_direction_calibration_t *config); bool domain_get_calibration(domain_direction_calibration_t *out_config); /** * @brief Run direction calibration. * * `timeout_ms <= 0` uses the default calibration timeout. The timeout is the * total caller-visible budget, including state-lock acquisition, movement * steps, switch settle delays, and release sweep. */ esp_err_t domain_calibrate_with_timeout(const domain_direction_calibration_t *config, int timeout_ms); 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); /** * @brief Stop the multimodal transport with bounded teardown. * * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. */ esp_err_t domain_runtime_multimodal_transport_stop(int timeout_ms); bool domain_runtime_multimodal_transport_is_connected(void); esp_err_t domain_runtime_multimodal_transport_send_text(const char *label, const char *text); esp_err_t domain_runtime_multimodal_transport_send_text_realtime(const char *label, const char *text); esp_err_t domain_runtime_multimodal_transport_send_binary_realtime(const uint8_t *data, size_t len); esp_err_t domain_runtime_multimodal_audio_init(void); void domain_runtime_multimodal_audio_deinit(void); esp_err_t domain_runtime_multimodal_audio_start_uplink( domain_multimodal_audio_uplink_cb_t cb, void *user_data); /** * @brief Stop multimodal uplink capture/stream with bounded teardown. * * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. */ 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); esp_err_t domain_runtime_multimodal_audio_play_downstream_opus(const uint8_t *data, size_t len); esp_err_t domain_runtime_multimodal_audio_begin_downstream_response(void); esp_err_t domain_runtime_multimodal_audio_finish_downstream_response(void); esp_err_t domain_runtime_multimodal_audio_stop_downstream_playback(void); esp_err_t domain_runtime_multimodal_audio_register_downstream_playback_callback( domain_multimodal_downstream_playback_callback_t cb, void *user_data); esp_err_t domain_runtime_multimodal_ptt_button_init(void); bool domain_runtime_multimodal_ptt_button_is_pressed(void); esp_err_t domain_runtime_slot_feature_switch_init(void); bool domain_runtime_slot_feature_switch_is_active(void); esp_err_t domain_runtime_prepare_storage(void); esp_err_t domain_runtime_mount_littlefs(void); esp_err_t domain_runtime_mount_logfs(void); void domain_runtime_read_device_identity(char *device_id, size_t device_id_len, char *serial_number, size_t serial_number_len); esp_err_t domain_runtime_write_device_identity(const char *device_id, const char *serial_number); typedef enum { DOMAIN_RUNTIME_AUTH_RECORD_KEY_MATERIAL = 0, DOMAIN_RUNTIME_AUTH_RECORD_STATE, } domain_runtime_auth_record_t; /** * @brief Domain-owned device-auth persistence port, backed by platform blobs. * * The domain facade owns only the auth record slots, storage key mapping, and * legacy migration boundary. Callers own the serialized record schema and pass * opaque bytes. `load_record` returns `ESP_ERR_NOT_FOUND` when no record exists * yet; `save_record` commits atomically. */ 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); esp_err_t domain_runtime_auth_store_save_record(domain_runtime_auth_record_t record, const void *data, size_t len); /* ------------------------------------------------------------------------- */ /* Daily fortune: user birth profile + server-computed daily result. */ /* Owned by the domain layer; see docs/adr/0002-daily-fortune-contracts.md. */ /* ------------------------------------------------------------------------- */ #define DOMAIN_BIRTH_PLACE_MAX_LEN 64 #define DOMAIN_BIRTH_RESIDENCE_MAX_LEN 64 #define DOMAIN_BIRTH_MBTI_MAX_LEN 8 #define DOMAIN_BIRTH_TZ_MAX_LEN 32 typedef struct { int year; int month; int day; int hour; int minute; char birth_place[DOMAIN_BIRTH_PLACE_MAX_LEN]; char current_residence[DOMAIN_BIRTH_RESIDENCE_MAX_LEN]; char mbti[DOMAIN_BIRTH_MBTI_MAX_LEN]; char timezone[DOMAIN_BIRTH_TZ_MAX_LEN]; bool valid; } domain_birth_profile_t; #define DOMAIN_FORTUNE_BAZI_MAX_LEN 64 #define DOMAIN_FORTUNE_ELEMENT_MAX_LEN 8 #define DOMAIN_FORTUNE_DIRECTION_MAX_LEN 16 #define DOMAIN_FORTUNE_DATE_MAX_LEN 12 typedef struct { char bazi[DOMAIN_FORTUNE_BAZI_MAX_LEN]; char element[DOMAIN_FORTUNE_ELEMENT_MAX_LEN]; char lucky_color[DOMAIN_FORTUNE_ELEMENT_MAX_LEN]; char wealth_direction[DOMAIN_FORTUNE_DIRECTION_MAX_LEN]; char target_date[DOMAIN_FORTUNE_DATE_MAX_LEN]; size_t wealth_direction_index; bool valid; } domain_fortune_result_t; typedef void (*domain_birth_profile_observer_t)(const domain_birth_profile_t *profile, void *user_data); /** * @brief Persist / query the user birth profile (NVS-backed via the platform store). * * `set` requires a valid profile, persists it, then notifies the registered * observer with a snapshot (invoked outside any lock). `get` returns false when * no valid profile has been stored. Storage is the single source of truth; no * higher-layer mirror should be kept. */ esp_err_t domain_birth_profile_store_set(const domain_birth_profile_t *profile); bool domain_birth_profile_store_get(domain_birth_profile_t *out_profile); /** * @brief Persist / query the most recent daily fortune result. */ esp_err_t domain_fortune_result_store_set(const domain_fortune_result_t *result); bool domain_fortune_result_store_get(domain_fortune_result_t *out_result); esp_err_t domain_fortune_result_store_clear(void); /** * @brief Register the single observer notified when a new birth profile is * stored (e.g. submitted over the LAN configuration page). Pass NULL to clear it. The * callback must be lightweight (it runs in the caller's context). */ void domain_runtime_register_birth_profile_observer(domain_birth_profile_observer_t cb, void *user_data); /** * @brief Wall-clock time synchronization, bridged to the platform SNTP * primitive. `start` is idempotent and sets the process time zone * (`posix_tz`, e.g. "CST-8"); `is_synced` reports whether the clock has * been set. Call `start` after the network is up. */ esp_err_t domain_runtime_time_sync_start(const char *sntp_server, const char *posix_tz); bool domain_runtime_time_is_synced(void); #define DOMAIN_USER_CONFIG_HOSTNAME_MAX_LEN 32 typedef struct { uint16_t port; char hostname[DOMAIN_USER_CONFIG_HOSTNAME_MAX_LEN]; } domain_user_config_server_config_t; typedef struct { bool running; uint16_t port; char hostname[DOMAIN_USER_CONFIG_HOSTNAME_MAX_LEN]; } domain_user_config_server_state_t; /** * @brief Start the domain-owned user profile and fortune web application. * * The platform supplies only the bounded raw HTTP transport. This domain * service owns the routes, JSON semantics, validation, and embedded page. * Start is idempotent while the service is starting or running. */ esp_err_t domain_user_config_server_start( const domain_user_config_server_config_t *config); /** * @brief Stop the user configuration server with bounded teardown. * * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before the platform applies deterministic * fallback. */ esp_err_t domain_user_config_server_stop(int timeout_ms); esp_err_t domain_user_config_server_get_state( domain_user_config_server_state_t *out_state); /** * @brief POST domain JSON data through the lower-layer HTTPS transport adapter. * * Optional `device_id` is sent as X-Device-Id and optional `bearer_token` is * sent as Authorization: Bearer . The response body is written to the * caller-owned buffer and NUL-terminated on success. A completed HTTP exchange * returns ESP_OK regardless of status code; callers inspect `out_status`. * * `timeout_ms < 0` is invalid. `timeout_ms == 0` uses the platform HTTPS * default bounded timeout. On `ESP_ERR_TIMEOUT`, the lower-layer exchange is * aborted and cleaned up before this function returns; no retry is performed. */ 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); /** * @brief Console (UART) line-transport primitives, bridged to the platform. * * `read_bytes` returns bytes read (0 on timeout, <0 on error). A zero timeout * polls once; negative timeouts are invalid and return <0. Used by the factory * provisioning CLI. */ esp_err_t domain_runtime_console_install(void); int domain_runtime_console_read_bytes(uint8_t *buf, size_t len, int timeout_ms); void domain_runtime_console_write_line(const char *line); void domain_runtime_behavior_set_keep_awake(bool keep_awake); esp_err_t domain_runtime_behavior_play_prompt(domain_wifi_prompt_id_t prompt); esp_err_t domain_runtime_behavior_play_prompt_file(const char *path); void domain_runtime_wifi_portal_set_behavior_hooks(const domain_wifi_behavior_hooks_t *hooks); esp_err_t domain_runtime_wifi_portal_init_defaults(const char *seed_ssid, const char *seed_password); bool domain_runtime_wifi_portal_has_saved_credentials(void); bool domain_runtime_wifi_portal_sta_is_connected(void); esp_err_t domain_runtime_wifi_portal_start_config_ap(const char *ssid_prefix); bool domain_runtime_wifi_connect_error_keeps_credentials(esp_err_t err); /** * @brief Connect STA using stored credentials with explicit timeout. * * Semantics: * - `timeout_ms <= 0`: use default timeout. * - Returns `ESP_OK` when STA is already connected (idempotent). * - Returns explicit failure on timeout/connection error. */ esp_err_t domain_runtime_wifi_portal_connect_sta(int timeout_ms); esp_err_t domain_runtime_wifi_portal_save_credentials(const char *ssid, const char *password); esp_err_t domain_runtime_wifi_portal_clear_saved_credentials(void); esp_err_t domain_runtime_init_lcd_device(void); void domain_runtime_start_lcd_render_loop(void); /** * @brief Stop LCD render loop with bounded teardown. * * `timeout_ms < 0` is invalid, zero requests asynchronous stop, and positive * values wait up to the bound before lower layers apply deterministic fallback. */ esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms); esp_err_t domain_runtime_request_expression(const domain_expression_request_t *request); /** * @brief Preload assets for an expression without changing the active display. * * Intended for asset-backed expressions that are likely to play soon. The * request is copied and the LCD render task performs the preload * asynchronously. Unsupported or non-asset expressions return an explicit * error. */ esp_err_t domain_runtime_preheat_expression(const domain_expression_request_t *request); /** * @brief Request an expression and block until its finite asset sequence * completes, or @p timeout_ms elapses. * * Intended for finite presentations such as the wealth spin intro. Returns the * underlying request error if queuing fails, ESP_ERR_TIMEOUT if playback does * not finish within the timeout (the caller may proceed anyway), or * ESP_ERR_INVALID_STATE if the LCD is not running. A looping expression * (repeat_count == 0) never completes and will time out. */ esp_err_t domain_runtime_request_expression_blocking(const domain_expression_request_t *request, uint32_t timeout_ms); #ifdef __cplusplus } #endif