diff --git a/components/control_plane/src/control_plane_lifecycle.c b/components/control_plane/src/control_plane_lifecycle.c index 10a9dbe..04fc93b 100644 --- a/components/control_plane/src/control_plane_lifecycle.c +++ b/components/control_plane/src/control_plane_lifecycle.c @@ -278,6 +278,12 @@ esp_err_t control_plane_lifecycle_start(void) { voice_started = true; image_generation_schedule_prewarm(); + esp_err_t voice_prewarm_rc = voice_interaction_schedule_session_prewarm(); + if (voice_prewarm_rc != ESP_OK && voice_prewarm_rc != ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, + "voice session prewarm schedule failed: rc=0x%x", + (unsigned)voice_prewarm_rc); + } if (lifecycle_lock_take(1000)) { lifecycle_set_state_locked(CONTROL_PLANE_LIFECYCLE_STATE_RUNNING, ESP_OK, "running"); diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index 3ca6425..91f22bb 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -153,6 +153,20 @@ typedef struct { int64_t finished_ms; } print_job_info_t; +typedef enum { + PRINTER_JOB_EVENT_QUEUED = 0, + PRINTER_JOB_EVENT_RUNNING, + PRINTER_JOB_EVENT_PROGRESS, + PRINTER_JOB_EVENT_FINISHED, +} printer_job_event_type_t; + +typedef struct { + printer_job_event_type_t type; + print_job_info_t job; +} printer_job_event_t; + +typedef void (*printer_job_event_cb_t)(const printer_job_event_t *event, void *user_data); + typedef struct { // Printer-only option: bypass paper/temp/battery precheck for this print job. bool ignore_precheck; @@ -200,6 +214,7 @@ bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info); esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count); esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len); size_t printer_protocol_cleanup_jobs(bool include_success, bool include_failed, bool include_canceled); +esp_err_t printer_protocol_set_event_callback(printer_job_event_cb_t cb, void *user_data); esp_err_t printer_protocol_gap_move(uint32_t timeout_ms, char *err, size_t err_len); @@ -237,6 +252,7 @@ esp_err_t voice_interaction_stop(char *err, size_t err_len); esp_err_t voice_interaction_tap_start(char *err, size_t err_len); esp_err_t voice_interaction_tap_cancel(char *err, size_t err_len); +esp_err_t voice_interaction_schedule_session_prewarm(void); void voice_interaction_get_status(voice_interaction_status_t *out_status); const char *voice_interaction_dialog_state_str(voice_dialog_state_t state); diff --git a/components/domain/internal/printer_protocol_internal.h b/components/domain/internal/printer_protocol_internal.h index 1aba50e..984b775 100644 --- a/components/domain/internal/printer_protocol_internal.h +++ b/components/domain/internal/printer_protocol_internal.h @@ -67,6 +67,7 @@ extern bool s_protocol_stopping; int printer_protocol_find_job_idx_locked(uint32_t id); int printer_protocol_alloc_job_slot_locked(void); bool printer_protocol_is_terminal_state(print_job_state_t state); +void printer_protocol_emit_job_event(printer_job_event_type_t event_type, const job_slot_t *job); void printer_protocol_worker_task(void *arg); bool printer_protocol_is_stopping(void); diff --git a/components/domain/src/printer_protocol.c b/components/domain/src/printer_protocol.c index e52bbfc..c38a139 100644 --- a/components/domain/src/printer_protocol.c +++ b/components/domain/src/printer_protocol.c @@ -35,6 +35,9 @@ bool s_protocol_initialized; bool s_protocol_stopping; static const char *TAG = "printer_protocol"; +static printer_job_event_cb_t s_job_event_cb; +static void *s_job_event_user_data; +static portMUX_TYPE s_job_event_lock = portMUX_INITIALIZER_UNLOCKED; #if CONFIG_FREERTOS_UNICORE #define PRINT_WORKER_CORE_ID 0 @@ -46,6 +49,47 @@ static esp_err_t ensure_printer_driver_ready(void) { return platform_printer_init(); } +static void printer_protocol_fill_job_info_from_slot(const job_slot_t *slot, print_job_info_t *out_info) { + if (slot == NULL || out_info == NULL) { + return; + } + + memset(out_info, 0, sizeof(*out_info)); + out_info->id = slot->id; + out_info->state = slot->state; + out_info->progress = slot->progress; + out_info->width = slot->width; + out_info->height = slot->height; + out_info->data_len = slot->data_len; + out_info->created_ms = slot->created_ms; + out_info->started_ms = slot->started_ms; + out_info->finished_ms = slot->finished_ms; + strlcpy(out_info->density, slot->density, sizeof(out_info->density)); + strlcpy(out_info->error, slot->error, sizeof(out_info->error)); +} + +void printer_protocol_emit_job_event(printer_job_event_type_t event_type, const job_slot_t *job) { + if (job == NULL) { + return; + } + + printer_job_event_cb_t cb = NULL; + void *user_data = NULL; + portENTER_CRITICAL(&s_job_event_lock); + cb = s_job_event_cb; + user_data = s_job_event_user_data; + portEXIT_CRITICAL(&s_job_event_lock); + if (cb == NULL) { + return; + } + + printer_job_event_t event = { + .type = event_type, + }; + printer_protocol_fill_job_info_from_slot(job, &event.job); + cb(&event, user_data); +} + static void refresh_printer_status_locked(void) { platform_printer_sensors_t sensors = {0}; if (platform_printer_get_sensors(&sensors) == ESP_OK) { @@ -429,3 +473,11 @@ void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { xSemaphoreGive(s_mutex); } } + +esp_err_t printer_protocol_set_event_callback(printer_job_event_cb_t cb, void *user_data) { + portENTER_CRITICAL(&s_job_event_lock); + s_job_event_cb = cb; + s_job_event_user_data = user_data; + portEXIT_CRITICAL(&s_job_event_lock); + return ESP_OK; +} diff --git a/components/domain/src/printer_protocol_jobs.c b/components/domain/src/printer_protocol_jobs.c index 0bd76e8..ae1f33f 100644 --- a/components/domain/src/printer_protocol_jobs.c +++ b/components/domain/src/printer_protocol_jobs.c @@ -140,6 +140,7 @@ esp_err_t printer_protocol_submit_raster_job_ex(const uint8_t *raster, s_jobs[idx].data = copy; strlcpy(s_jobs[idx].density, density != NULL ? density : "中等", sizeof(s_jobs[idx].density)); bool ignore_precheck = s_jobs[idx].ignore_precheck; + job_slot_t queued_snapshot = s_jobs[idx]; xSemaphoreGive(s_mutex); @@ -177,6 +178,7 @@ esp_err_t printer_protocol_submit_raster_job_ex(const uint8_t *raster, density != NULL ? density : "中等", ignore_precheck, (unsigned)queue_depth); + printer_protocol_emit_job_event(PRINTER_JOB_EVENT_QUEUED, &queued_snapshot); return ESP_OK; } @@ -271,6 +273,7 @@ esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len) { bool canceled_finalized = false; + job_slot_t canceled_snapshot = {0}; if (job_id == 0) { if (err != NULL && err_len > 0) { @@ -308,16 +311,18 @@ esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; s_jobs[idx].progress = 0; - strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); + strlcpy(s_jobs[idx].error, "Job canceled.", sizeof(s_jobs[idx].error)); free(s_jobs[idx].data); s_jobs[idx].data = NULL; canceled_finalized = true; + canceled_snapshot = s_jobs[idx]; } else if (s_jobs[idx].state == PRINT_JOB_STATE_RUNNING) { - strlcpy(s_jobs[idx].error, "cancel requested", sizeof(s_jobs[idx].error)); + strlcpy(s_jobs[idx].error, "Cancel requested.", sizeof(s_jobs[idx].error)); } xSemaphoreGive(s_mutex); if (canceled_finalized) { + printer_protocol_emit_job_event(PRINTER_JOB_EVENT_FINISHED, &canceled_snapshot); } return ESP_OK; } diff --git a/components/domain/src/printer_protocol_worker.c b/components/domain/src/printer_protocol_worker.c index 12843c2..8d99906 100644 --- a/components/domain/src/printer_protocol_worker.c +++ b/components/domain/src/printer_protocol_worker.c @@ -12,6 +12,38 @@ static const char *TAG = "printer_protocol"; +static const char *printer_user_error_message(const char *raw_err, + esp_err_t rc, + bool canceled) { + if (canceled) { + return "Job canceled."; + } + + if (raw_err != NULL && raw_err[0] != '\0') { + if (strstr(raw_err, "out of paper") != NULL) { + return "Out of paper. Load paper and retry."; + } + if (strstr(raw_err, "battery too low") != NULL) { + return "Battery too low. Charge and retry."; + } + if (strstr(raw_err, "temperature out of range") != NULL) { + return "Printer temperature abnormal. Wait and retry."; + } + if (strstr(raw_err, "sensor read failed") != NULL) { + return "Sensor read failed. Retry."; + } + if (strstr(raw_err, "print timeout") != NULL) { + return "Printing timed out. Retry."; + } + return raw_err; + } + + if (rc == ESP_ERR_TIMEOUT) { + return "Printing timed out. Retry."; + } + return "Printing failed. Retry."; +} + static uint16_t density_to_hot_time(const char *density) { if (density == NULL) { return 2000; @@ -66,7 +98,9 @@ static bool precheck_printer_ready(bool ignore_precheck, char *err, size_t err_l static bool run_print_job_printer_driver(job_slot_t *job) { char check_err[96]; if (!precheck_printer_ready(job->ignore_precheck, check_err, sizeof(check_err))) { - snprintf(job->error, sizeof(job->error), "%s", check_err); + strlcpy(job->error, + printer_user_error_message(check_err, ESP_FAIL, false), + sizeof(job->error)); return false; } @@ -84,30 +118,38 @@ static bool run_print_job_printer_driver(job_slot_t *job) { .cancel_flag = &job->cancel_requested, }; + bool emit_progress = false; + job_slot_t progress_snapshot = {0}; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { job->progress = 10; + progress_snapshot = *job; + emit_progress = true; xSemaphoreGive(s_mutex); } + if (emit_progress) { + printer_protocol_emit_job_event(PRINTER_JOB_EVENT_PROGRESS, &progress_snapshot); + } char print_err[96] = {0}; esp_err_t rc = platform_printer_print(&req, print_err, sizeof(print_err)); if (rc != ESP_OK) { - if (print_err[0] != 0) { - snprintf(job->error, sizeof(job->error), "%s", print_err); - } else if (rc == ESP_ERR_TIMEOUT) { - snprintf(job->error, sizeof(job->error), "print timeout"); - } else if (rc == ESP_ERR_INVALID_STATE && job->cancel_requested) { - snprintf(job->error, sizeof(job->error), "job canceled"); - } else { - snprintf(job->error, sizeof(job->error), "printer print failed"); - } + strlcpy(job->error, + printer_user_error_message(print_err, rc, (rc == ESP_ERR_INVALID_STATE && job->cancel_requested)), + sizeof(job->error)); return false; } + emit_progress = false; + memset(&progress_snapshot, 0, sizeof(progress_snapshot)); if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { job->progress = 95; + progress_snapshot = *job; + emit_progress = true; xSemaphoreGive(s_mutex); } + if (emit_progress) { + printer_protocol_emit_job_event(PRINTER_JOB_EVENT_PROGRESS, &progress_snapshot); + } return true; } @@ -122,7 +164,7 @@ static bool run_print_job(job_slot_t *job) { job->ignore_precheck); if (printer_protocol_is_stopping()) { - snprintf(job->error, sizeof(job->error), "protocol stopping"); + strlcpy(job->error, "Service is stopping. Job canceled.", sizeof(job->error)); return false; } @@ -180,6 +222,7 @@ void printer_protocol_worker_task(void *arg) { job->started_ms = esp_timer_get_time() / 1000; job->progress = 1; s_busy_refcnt = 1; + job_slot_t running_snapshot = *job; int64_t queue_wait_ms = 0; if (job->started_ms >= job->created_ms) { queue_wait_ms = job->started_ms - job->created_ms; @@ -193,6 +236,7 @@ void printer_protocol_worker_task(void *arg) { (unsigned)job->height, job->density); xSemaphoreGive(s_mutex); + printer_protocol_emit_job_event(PRINTER_JOB_EVENT_RUNNING, &running_snapshot); bool ok = run_print_job(job); @@ -214,10 +258,13 @@ void printer_protocol_worker_task(void *arg) { } else if (s_jobs[idx].cancel_requested || printer_protocol_is_stopping()) { s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; if (s_jobs[idx].error[0] == 0) { - strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); + strlcpy(s_jobs[idx].error, "Job canceled.", sizeof(s_jobs[idx].error)); } } else { s_jobs[idx].state = PRINT_JOB_STATE_FAILED; + strlcpy(s_jobs[idx].error, + printer_user_error_message(s_jobs[idx].error, ESP_FAIL, false), + sizeof(s_jobs[idx].error)); } int64_t duration_ms = 0; @@ -239,9 +286,11 @@ void printer_protocol_worker_task(void *arg) { s_jobs[idx].error[0] != 0 ? s_jobs[idx].error : "-"); } + job_slot_t finished_snapshot = s_jobs[idx]; free(s_jobs[idx].data); s_jobs[idx].data = NULL; xSemaphoreGive(s_mutex); + printer_protocol_emit_job_event(PRINTER_JOB_EVENT_FINISHED, &finished_snapshot); } if (s_evt != NULL) { diff --git a/components/domain/src/system_runtime.c b/components/domain/src/system_runtime.c index 8061d7d..8c048f9 100644 --- a/components/domain/src/system_runtime.c +++ b/components/domain/src/system_runtime.c @@ -8,7 +8,8 @@ static const char *TAG = "system_runtime"; -#define SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS 220 +#define SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS 3000 +#define SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS 3000 static bool s_wifi_hint_shown; static bool s_power_key_ready; @@ -59,6 +60,24 @@ static esp_err_t system_runtime_power_key_init(void) { return ESP_OK; } +static void system_runtime_show_status_text(const char *text, uint32_t timeout_ms) { + if (text == NULL || text[0] == '\0') { + return; + } + + char preview_err[96] = {0}; + esp_err_t preview_rc = screen_preview_show_text(text, + timeout_ms, + preview_err, + sizeof(preview_err)); + if (preview_rc != ESP_OK && preview_rc != ESP_ERR_NOT_SUPPORTED) { + ESP_LOGW(TAG, + "status_preview_failed: rc=0x%x msg=%s", + (unsigned)preview_rc, + preview_err[0] != '\0' ? preview_err : "show text failed"); + } +} + static void system_runtime_show_wifi_hint(void) { if (s_wifi_hint_shown) { return; @@ -76,17 +95,7 @@ static void system_runtime_show_wifi_hint(void) { "未检测到Wi-Fi配置\n连接热点: %s\n打开: http://192.168.4.1", ap_ssid); - char preview_err[96] = {0}; - esp_err_t preview_rc = screen_preview_show_text(hint, - SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS, - preview_err, - sizeof(preview_err)); - if (preview_rc != ESP_OK && preview_rc != ESP_ERR_NOT_SUPPORTED) { - ESP_LOGW(TAG, - "wifi_hint_preview_failed: rc=0x%x msg=%s", - (unsigned)preview_rc, - preview_err[0] != '\0' ? preview_err : "show text failed"); - } + system_runtime_show_status_text(hint, SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS); } static void system_runtime_try_show_wifi_hint_for_unprovisioned(void) { @@ -97,8 +106,32 @@ static void system_runtime_try_show_wifi_hint_for_unprovisioned(void) { static void system_runtime_wifi_event_handler(wifi_manager_event_t event, void *user_data) { (void)user_data; - if (event == WIFI_MANAGER_EVENT_PROVISIONING_STARTED) { - system_runtime_show_wifi_hint(); + switch (event) { + case WIFI_MANAGER_EVENT_PROVISIONING_STARTED: + system_runtime_show_wifi_hint(); + break; + case WIFI_MANAGER_EVENT_STA_CONNECTING: + system_runtime_show_status_text("正在连接Wi-Fi...\n请稍候", SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + break; + case WIFI_MANAGER_EVENT_STA_CONNECTED: { + char ip[20] = {0}; + wifi_manager_get_ip(ip, sizeof(ip)); + char text[96] = {0}; + snprintf(text, + sizeof(text), + "Wi-Fi已连接\nIP: %s", + ip[0] != '\0' ? ip : "0.0.0.0"); + system_runtime_show_status_text(text, SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + break; + } + case WIFI_MANAGER_EVENT_STA_CONNECT_FAILED: + system_runtime_show_status_text("Wi-Fi连接失败\n请检查密码/信号后重试", SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + break; + case WIFI_MANAGER_EVENT_STA_DISCONNECTED: + system_runtime_show_status_text("Wi-Fi已断开\n正在重连...", SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + break; + default: + break; } } diff --git a/components/domain/src/voice_interaction.c b/components/domain/src/voice_interaction.c index b2a6abb..fb7af02 100644 --- a/components/domain/src/voice_interaction.c +++ b/components/domain/src/voice_interaction.c @@ -8,6 +8,15 @@ #include "esp_log.h" static const char *TAG = "voice_interaction"; +static TaskHandle_t s_voice_prewarm_task; +static bool s_voice_prewarm_task_with_caps; +static volatile bool s_voice_prewarm_stop_requested; + +#define VOICE_PREWARM_TASK_STACK 6144 +#define VOICE_PREWARM_CONNECT_TIMEOUT_MS 3000 +#define VOICE_PREWARM_WIFI_POLL_MS 300 +#define VOICE_PREWARM_RETRY_DELAY_MS 1500 +#define VOICE_PREWARM_MAX_ATTEMPTS 3 static uint32_t clamp_timeout_ms(uint32_t timeout_ms, uint32_t default_ms, uint32_t min_ms, uint32_t max_ms) { uint32_t value = timeout_ms == 0 ? default_ms : timeout_ms; @@ -74,6 +83,87 @@ static bool voice_wait_worker_tasks_exit(uint32_t timeout_ms) { return false; } +static void voice_session_prewarm_task(void *arg) { + (void)arg; + + uint32_t attempts = 0; + while (!s_voice_prewarm_stop_requested) { + voice_interaction_status_t status = {0}; + voice_interaction_get_status(&status); + if (status.session_active && status.ws_connected && status.started) { + break; + } + + if (!system_runtime_wifi_ready()) { + vTaskDelay(pdMS_TO_TICKS(VOICE_PREWARM_WIFI_POLL_MS)); + continue; + } + + char prewarm_err[128] = {0}; + esp_err_t prewarm_rc = voice_interaction_start_with_timeout(VOICE_PREWARM_CONNECT_TIMEOUT_MS, + prewarm_err, + sizeof(prewarm_err)); + if (prewarm_rc == ESP_OK) { + ESP_LOGI(TAG, "voice session prewarm ready"); + break; + } + + if (prewarm_rc == ESP_ERR_INVALID_STATE && + strstr(prewarm_err, "config missing") != NULL) { + ESP_LOGW(TAG, "voice session prewarm skipped: config missing"); + break; + } + + ++attempts; + if (attempts >= VOICE_PREWARM_MAX_ATTEMPTS) { + ESP_LOGW(TAG, + "voice session prewarm failed after retries: rc=0x%x err=%s", + (unsigned)prewarm_rc, + prewarm_err[0] != '\0' ? prewarm_err : "-"); + break; + } + vTaskDelay(pdMS_TO_TICKS(VOICE_PREWARM_RETRY_DELAY_MS)); + } + + bool delete_with_caps = false; + if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + delete_with_caps = s_voice_prewarm_task_with_caps; + s_voice_prewarm_task = NULL; + s_voice_prewarm_task_with_caps = false; + voice_unlock(); + } + voice_delete_self_task(delete_with_caps); +} + +static void voice_interaction_stop_prewarm_task(void) { + s_voice_prewarm_stop_requested = true; + + TaskHandle_t prewarm_task = NULL; + if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + prewarm_task = s_voice_prewarm_task; + voice_unlock(); + } + if (prewarm_task == NULL) { + return; + } + + (void)xTaskAbortDelay(prewarm_task); + + int64_t deadline_ms = voice_now_ms() + 500; + while (voice_now_ms() < deadline_ms) { + bool done = true; + if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + done = (s_voice_prewarm_task == NULL); + voice_unlock(); + } + if (done) { + return; + } + vTaskDelay(pdMS_TO_TICKS(20)); + } + ESP_LOGW(TAG, "voice prewarm task still running during stop, continue shutdown"); +} + const char *voice_interaction_dialog_state_str(voice_dialog_state_t state) { switch (state) { case VOICE_DIALOG_STATE_IDLE: @@ -103,6 +193,9 @@ esp_err_t voice_interaction_init(void) { s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE; s_voice.last_event_ms = voice_now_ms(); + s_voice_prewarm_task = NULL; + s_voice_prewarm_task_with_caps = false; + s_voice_prewarm_stop_requested = false; esp_err_t mic_key_err = voice_interaction_mic_key_init(); if (mic_key_err != ESP_OK) { @@ -115,6 +208,43 @@ esp_err_t voice_interaction_init(void) { return ESP_OK; } +esp_err_t voice_interaction_schedule_session_prewarm(void) { + if (!s_voice.ready) { + return ESP_ERR_INVALID_STATE; + } + + if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + return ESP_ERR_TIMEOUT; + } + bool already_running = (s_voice_prewarm_task != NULL); + voice_unlock(); + if (already_running) { + return ESP_OK; + } + + TaskHandle_t task = NULL; + bool with_caps = false; + s_voice_prewarm_stop_requested = false; + BaseType_t ok = voice_create_task_prefer_psram(voice_session_prewarm_task, + "voice_prewarm", + VOICE_PREWARM_TASK_STACK, + 3, + &task, + &with_caps); + if (ok != pdPASS || task == NULL) { + return ESP_ERR_NO_MEM; + } + + if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { + voice_delete_task(task, with_caps); + return ESP_ERR_TIMEOUT; + } + s_voice_prewarm_task = task; + s_voice_prewarm_task_with_caps = with_caps; + voice_unlock(); + return ESP_OK; +} + esp_err_t voice_interaction_start_with_timeout(uint32_t timeout_ms, char *err, size_t err_len) { uint32_t connect_timeout_ms = clamp_timeout_ms(timeout_ms, VOICE_START_CONNECT_TIMEOUT_MS, 1000, 30000); @@ -366,6 +496,8 @@ esp_err_t voice_interaction_stop_with_timeout(uint32_t timeout_ms, char *err, si return ESP_ERR_INVALID_STATE; } + voice_interaction_stop_prewarm_task(); + if (!voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { voice_fill_err(err, err_len, "voice lock timeout"); ESP_LOGW(TAG, "session_stop_abort: lock timeout"); diff --git a/components/domain/src/voice_interaction_marker.c b/components/domain/src/voice_interaction_marker.c index 6c33885..0a62186 100644 --- a/components/domain/src/voice_interaction_marker.c +++ b/components/domain/src/voice_interaction_marker.c @@ -17,7 +17,7 @@ static const char *TAG = "voice_interaction"; #define VOICE_IMAGE_MARKER_REPEAT_SUPPRESS_MS 45000 #define VOICE_IMAGE_PRINT_THRESHOLD 160 #define VOICE_IMAGE_PRINT_MAX_HEIGHT 2200 -#define VOICE_IMAGE_PREVIEW_TIMEOUT_MS 200 +#define VOICE_IMAGE_PREVIEW_TIMEOUT_MS 3000 #define VOICE_IMAGE_MARKER_TEMPLATE \ "%s,线稿风格,白色背景,边缘清晰,细节简洁,高对比,避免大面积阴影和灰度渐变,适配热敏打印机打印。" #define VOICE_IMAGE_MARKER_TEMPLATE_FALLBACK_SUBJECT "简洁卡通图案" diff --git a/components/domain/src/voice_interaction_mic_key.c b/components/domain/src/voice_interaction_mic_key.c index 812121e..3d4f7ca 100644 --- a/components/domain/src/voice_interaction_mic_key.c +++ b/components/domain/src/voice_interaction_mic_key.c @@ -9,6 +9,7 @@ #define VOICE_MIC_KEY_QUEUE_LEN 8 #define VOICE_MIC_KEY_SESSION_READY_WAIT_MS 2000 #define VOICE_MIC_KEY_SESSION_READY_POLL_MS 30 +#define VOICE_MIC_KEY_STATUS_PREVIEW_TIMEOUT_MS 3000 typedef enum { VOICE_MIC_KEY_CMD_PRESS_DOWN = 0, @@ -22,6 +23,26 @@ static TaskHandle_t s_mic_key_task; static bool s_mic_key_task_with_caps; static bool s_mic_key_ready; +static void voice_mic_key_show_status(const char *text) { + if (text == NULL || text[0] == '\0') { + return; + } + + char preview_err[96] = {0}; + esp_err_t preview_rc = screen_preview_show_text(text, + VOICE_MIC_KEY_STATUS_PREVIEW_TIMEOUT_MS, + preview_err, + sizeof(preview_err)); + if (preview_rc == ESP_OK || preview_rc == ESP_ERR_NOT_SUPPORTED) { + return; + } + + ESP_LOGW(TAG, + "mic_key_status_preview_failed: rc=0x%x msg=%s", + (unsigned)preview_rc, + preview_err[0] != '\0' ? preview_err : "show text failed"); +} + static bool voice_mic_key_round_busy(const voice_interaction_status_t *voice, const printer_runtime_status_t *printer) { if (voice == NULL || printer == NULL) { @@ -56,6 +77,7 @@ static void voice_mic_key_handle_press_down(void) { if (voice.tap_active) { ESP_LOGI(TAG, "MIC_KEY press ignored: tap already active"); + voice_mic_key_show_status("语音已在录制中"); return; } @@ -68,10 +90,12 @@ static void voice_mic_key_handle_press_down(void) { voice.marker_image_gen_running, printer.busy, printer.queue_depth); + voice_mic_key_show_status("设备忙碌中\n请等待当前任务完成"); return; } if (!voice_mic_key_session_ready(&voice)) { + voice_mic_key_show_status("语音连接中...\n请稍候"); char start_err[128] = {0}; esp_err_t start_rc = voice_interaction_start_with_timeout(0, start_err, sizeof(start_err)); if (start_rc != ESP_OK) { @@ -79,6 +103,7 @@ static void voice_mic_key_handle_press_down(void) { "MIC_KEY auto session start failed: %s (%s)", esp_err_to_name(start_rc), start_err[0] != '\0' ? start_err : "-"); + voice_mic_key_show_status("语音连接失败\n请稍后重试"); return; } } @@ -90,6 +115,7 @@ static void voice_mic_key_handle_press_down(void) { voice.session_active, voice.ws_connected, voice.started); + voice_mic_key_show_status("语音服务暂不可用\n请稍后重试"); return; } @@ -100,9 +126,11 @@ static void voice_mic_key_handle_press_down(void) { "MIC_KEY tap start failed: %s (%s)", esp_err_to_name(tap_rc), tap_err[0] != '\0' ? tap_err : "-"); + voice_mic_key_show_status("开始录音失败\n请重试"); return; } + voice_mic_key_show_status("请开始说话"); ESP_LOGI(TAG, "MIC_KEY tap started"); } @@ -120,9 +148,11 @@ static void voice_mic_key_handle_press_up(void) { "MIC_KEY tap stop failed: %s (%s)", esp_err_to_name(tap_rc), tap_err[0] != '\0' ? tap_err : "-"); + voice_mic_key_show_status("结束录音失败\n请重试"); return; } + voice_mic_key_show_status("已结束录音\n正在处理..."); ESP_LOGI(TAG, "MIC_KEY tap stopped"); } diff --git a/components/domain/src/voice_interaction_ws_protocol.c b/components/domain/src/voice_interaction_ws_protocol.c index 3d0f72a..4165a20 100644 --- a/components/domain/src/voice_interaction_ws_protocol.c +++ b/components/domain/src/voice_interaction_ws_protocol.c @@ -16,7 +16,7 @@ static const char *TAG = "voice_interaction"; #define VOICE_RESPONSE_FINALIZE_TASK_STACK 6144 #define VOICE_RESPONSE_FINALIZE_TASK_RETRY_COUNT 3 #define VOICE_RESPONSE_FINALIZE_TASK_RETRY_DELAY_MS 120 -#define VOICE_SCREEN_PREVIEW_TIMEOUT_MS 180 +#define VOICE_SCREEN_PREVIEW_TIMEOUT_MS 3000 static portMUX_TYPE s_response_finalize_lock = portMUX_INITIALIZER_UNLOCKED; static bool s_response_finalize_running; diff --git a/components/platform/assets/wifi_configuration.html b/components/platform/assets/wifi_configuration.html index 00ae253..c71ddee 100644 --- a/components/platform/assets/wifi_configuration.html +++ b/components/platform/assets/wifi_configuration.html @@ -337,6 +337,21 @@ status_connect_ok: '连接成功,正在关闭配网页面...', status_connect_failed: '连接失败:{error}。请重试。', status_request_failed: '请求失败,请重试。', + error_invalid_payload: '提交内容无效', + error_read_failed: '设备读取请求失败', + error_ssid_required: 'SSID 不能为空', + error_password_too_short: '密码至少需要 8 位', + error_wrong_password: '密码错误', + error_no_ap_found: '未找到该 Wi-Fi', + error_signal_too_weak: '信号太弱,请靠近路由器', + error_ap_not_responding: '路由器无响应', + error_association_failed: '连接被路由器拒绝', + error_ap_security_unsupported: '路由器加密方式暂不支持', + error_connect_timeout: '连接超时', + error_wifi_unavailable: '设备 Wi-Fi 暂不可用', + error_save_failed: '连接成功但保存失败,请重试', + error_connect_failed: '无法连接到该 Wi-Fi', + error_unknown: '未知错误', signal_unit: 'dBm' }, 'en-US': { @@ -359,6 +374,21 @@ status_connect_ok: 'Connected successfully. Closing this provisioning page...', status_connect_failed: 'Connection failed: {error}. Please try again.', status_request_failed: 'Request failed. Please try again.', + error_invalid_payload: 'Invalid request payload', + error_read_failed: 'Device failed to read request', + error_ssid_required: 'SSID is required', + error_password_too_short: 'Password must be at least 8 characters', + error_wrong_password: 'Wrong password', + error_no_ap_found: 'Wi-Fi network not found', + error_signal_too_weak: 'Signal is too weak', + error_ap_not_responding: 'Router is not responding', + error_association_failed: 'Router rejected the connection', + error_ap_security_unsupported: 'Wi-Fi security mode is unsupported', + error_connect_timeout: 'Connection timed out', + error_wifi_unavailable: 'Device Wi-Fi is unavailable', + error_save_failed: 'Connected, but failed to save credentials', + error_connect_failed: 'Unable to connect to this Wi-Fi', + error_unknown: 'Unknown error', signal_unit: 'dBm' } }; @@ -402,6 +432,18 @@ setStatusText(trWithParams(key, params), type); } + function mapProvisionError(code) { + if (!code) { + return tr('error_unknown'); + } + const key = 'error_' + String(code); + const mapped = tr(key); + if (mapped === key) { + return tr('error_connect_failed'); + } + return mapped; + } + function decodeSsid(value) { try { return decodeURIComponent(value); @@ -562,7 +604,9 @@ return; } - setStatusKey('status_connect_failed', 'err', { error: data.error || 'unknown' }); + setStatusKey('status_connect_failed', 'err', { + error: mapProvisionError(data.error) + }); } catch (_err) { setStatusKey('status_request_failed', 'err'); } diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 26b15f8..fb3eb6c 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -16,6 +16,10 @@ esp_err_t platform_bootstrap_init(void); // ---------- wifi_manager ---------- typedef enum { WIFI_MANAGER_EVENT_PROVISIONING_STARTED = 0, + WIFI_MANAGER_EVENT_STA_CONNECTING, + WIFI_MANAGER_EVENT_STA_CONNECTED, + WIFI_MANAGER_EVENT_STA_CONNECT_FAILED, + WIFI_MANAGER_EVENT_STA_DISCONNECTED, } wifi_manager_event_t; typedef void (*wifi_manager_event_cb_t)(wifi_manager_event_t event, void *user_data); diff --git a/components/platform/src/display_lcd_module.c b/components/platform/src/display_lcd_module.c index d044688..a9cc044 100644 --- a/components/platform/src/display_lcd_module.c +++ b/components/platform/src/display_lcd_module.c @@ -18,7 +18,7 @@ static const char *TAG = "lcd_module"; #define ST7789_SPI_HOST SPI2_HOST -#define LCD_PREVIEW_TIMEOUT_MS_DEFAULT 200 +#define LCD_PREVIEW_TIMEOUT_MS_DEFAULT 3000 #define LCD_LOCK_TIMEOUT_MS 1000 #define LCD_DEFAULT_DRAW_LINES 20 #define LCD_PREVIEW_FADE_DURATION_MS 150 diff --git a/components/platform/src/wifi_manager.c b/components/platform/src/wifi_manager.c index 7780e57..202c77a 100644 --- a/components/platform/src/wifi_manager.c +++ b/components/platform/src/wifi_manager.c @@ -66,6 +66,8 @@ static esp_netif_t *s_ap_netif; static httpd_handle_t s_prov_httpd; static esp_event_handler_instance_t s_wifi_event_inst; static esp_event_handler_instance_t s_ip_event_inst; +static uint8_t s_last_sta_disconnect_reason; +static bool s_last_sta_disconnect_reason_valid; static void wifi_manager_notify_event(wifi_manager_event_t event) { wifi_manager_event_cb_t cb = s_event_cb; @@ -75,6 +77,87 @@ static void wifi_manager_notify_event(wifi_manager_event_t event) { cb(event, s_event_cb_user_data); } +static const char *wifi_manager_error_from_disconnect_reason(uint8_t reason) { +#ifdef WIFI_REASON_AUTH_EXPIRE + if (reason == WIFI_REASON_AUTH_EXPIRE) { + return "wrong_password"; + } +#endif +#ifdef WIFI_REASON_AUTH_FAIL + if (reason == WIFI_REASON_AUTH_FAIL) { + return "wrong_password"; + } +#endif +#ifdef WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT + if (reason == WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT) { + return "wrong_password"; + } +#endif +#ifdef WIFI_REASON_HANDSHAKE_TIMEOUT + if (reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { + return "wrong_password"; + } +#endif +#ifdef WIFI_REASON_ASSOC_FAIL + if (reason == WIFI_REASON_ASSOC_FAIL) { + return "association_failed"; + } +#endif +#ifdef WIFI_REASON_CONNECTION_FAIL + if (reason == WIFI_REASON_CONNECTION_FAIL) { + return "association_failed"; + } +#endif +#ifdef WIFI_REASON_NO_AP_FOUND + if (reason == WIFI_REASON_NO_AP_FOUND) { + return "no_ap_found"; + } +#endif +#ifdef WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD + if (reason == WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD) { + return "ap_security_unsupported"; + } +#endif +#ifdef WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD + if (reason == WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD) { + return "signal_too_weak"; + } +#endif +#ifdef WIFI_REASON_BEACON_TIMEOUT + if (reason == WIFI_REASON_BEACON_TIMEOUT) { + return "ap_not_responding"; + } +#endif +#ifdef WIFI_REASON_ASSOC_EXPIRE + if (reason == WIFI_REASON_ASSOC_EXPIRE) { + return "ap_not_responding"; + } +#endif +#ifdef WIFI_REASON_ASSOC_LEAVE + if (reason == WIFI_REASON_ASSOC_LEAVE) { + return "ap_not_responding"; + } +#endif + + return "connect_failed"; +} + +static const char *wifi_manager_resolve_connect_error_code(esp_err_t connect_rc) { + if (connect_rc == ESP_ERR_TIMEOUT) { + return "connect_timeout"; + } + if (connect_rc == ESP_ERR_INVALID_STATE) { + return "wifi_unavailable"; + } + if (connect_rc != ESP_FAIL) { + return "connect_failed"; + } + if (s_last_sta_disconnect_reason_valid) { + return wifi_manager_error_from_disconnect_reason(s_last_sta_disconnect_reason); + } + return "connect_failed"; +} + static void wifi_manager_apply_sta_throughput_profile(void) { const uint8_t protocol = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N; esp_err_t proto_rc = esp_wifi_set_protocol(WIFI_IF_STA, protocol); @@ -175,11 +258,15 @@ static void wifi_event_handler(void *arg, if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { s_ready = false; if (!s_started || !s_sta_reconnect_enabled) { + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_DISCONNECTED); return; } wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data; uint8_t reason = disc != NULL ? disc->reason : 0; + s_last_sta_disconnect_reason = reason; + s_last_sta_disconnect_reason_valid = (reason != 0); + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_DISCONNECTED); if (s_retry_num < CONFIG_TQ_WIFI_MAXIMUM_RETRY) { (void)esp_wifi_connect(); @@ -208,6 +295,7 @@ static void wifi_event_handler(void *arg, if (s_wifi_event_group != NULL) { xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECTED); } } @@ -476,6 +564,8 @@ static esp_err_t wifi_manager_connect_sta(const wifi_sta_credentials_t *creds, b xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT); s_retry_num = 0; s_ready = false; + s_last_sta_disconnect_reason = 0; + s_last_sta_disconnect_reason_valid = false; s_sta_reconnect_enabled = false; esp_err_t err = esp_wifi_disconnect(); @@ -486,23 +576,28 @@ static esp_err_t wifi_manager_connect_sta(const wifi_sta_credentials_t *creds, b err = esp_wifi_set_mode(keep_softap ? WIFI_MODE_APSTA : WIFI_MODE_STA); if (err != ESP_OK) { + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECT_FAILED); return err; } err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config); if (err != ESP_OK) { + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECT_FAILED); return err; } err = esp_wifi_start(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECT_FAILED); return err; } err = esp_wifi_connect(); if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECT_FAILED); return err; } + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECTING); ESP_LOGI(TAG, "trying to connect to SSID:%s", creds->ssid); EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, @@ -526,10 +621,12 @@ static esp_err_t wifi_manager_connect_sta(const wifi_sta_credentials_t *creds, b if (bits & WIFI_FAIL_BIT) { ESP_LOGW(TAG, "failed to connect to SSID:%s", creds->ssid); + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECT_FAILED); return ESP_FAIL; } ESP_LOGW(TAG, "Wi-Fi connect timeout, SSID:%s", creds->ssid); + wifi_manager_notify_event(WIFI_MANAGER_EVENT_STA_CONNECT_FAILED); return ESP_ERR_TIMEOUT; } @@ -583,7 +680,9 @@ static esp_err_t wifi_manager_prov_submit_post_handler(httpd_req_t *req) { esp_err_t err = wifi_manager_connect_sta(&creds, true); if (err != ESP_OK) { - return wifi_manager_send_prov_json(req, false, "connect_failed"); + return wifi_manager_send_prov_json(req, + false, + wifi_manager_resolve_connect_error_code(err)); } err = wifi_manager_save_credentials(&creds); @@ -789,6 +888,8 @@ esp_err_t wifi_manager_start(void) { s_ready = false; s_sta_reconnect_enabled = true; s_provisioning_active = false; + s_last_sta_disconnect_reason = 0; + s_last_sta_disconnect_reason_valid = false; if (s_wifi_event_group == NULL) { s_wifi_event_group = xEventGroupCreate(); @@ -888,6 +989,8 @@ esp_err_t wifi_manager_stop(void) { s_ready = false; s_retry_num = 0; s_sta_reconnect_enabled = false; + s_last_sta_disconnect_reason = 0; + s_last_sta_disconnect_reason_valid = false; wifi_manager_stop_softap_provisioning();