diff --git a/components/control_plane/src/control_plane_lifecycle.c b/components/control_plane/src/control_plane_lifecycle.c index 351e5ce..5b80276 100644 --- a/components/control_plane/src/control_plane_lifecycle.c +++ b/components/control_plane/src/control_plane_lifecycle.c @@ -151,6 +151,7 @@ static void lifecycle_power_key_task_main(void *arg) { } ESP_LOGW(TAG, "POWER_KEY long press detected: stopping services and powering off"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_SHUTDOWN, NULL); esp_err_t stop_rc = ESP_ERR_INVALID_STATE; for (uint16_t attempt = 0; attempt < POWER_KEY_STOP_RETRY_COUNT; ++attempt) { @@ -282,6 +283,7 @@ esp_err_t control_plane_lifecycle_start(void) { goto start_failed; } printer_started = true; + (void)screen_preview_start_runtime_feedback(); failed_stage = "voice_interaction"; rc = lifecycle_run_step_with_retry("voice_interaction", lifecycle_start_voice); @@ -310,6 +312,7 @@ esp_err_t control_plane_lifecycle_start(void) { xSemaphoreGive(s_lock); } + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_RUNTIME_READY, NULL); ESP_LOGI(TAG, "control-plane lifecycle started"); return ESP_OK; @@ -321,6 +324,7 @@ start_failed: (void)lifecycle_stop_voice(); } if (printer_started) { + screen_preview_stop_runtime_feedback(); (void)lifecycle_stop_printer_protocol(); } if (system_started) { @@ -337,6 +341,10 @@ start_failed: "control-plane lifecycle start failed at stage=%s, rc=0x%x", failed_stage, (unsigned)rc); + screen_preview_context_t ctx = { + .detail = failed_stage, + }; + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_RUNTIME_ERROR, &ctx); return rc; } @@ -360,6 +368,7 @@ esp_err_t control_plane_lifecycle_stop(void) { xSemaphoreGive(s_lock); lifecycle_power_key_worker_unbind(); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_SHUTDOWN, NULL); esp_err_t first_err = ESP_OK; @@ -373,6 +382,7 @@ esp_err_t control_plane_lifecycle_stop(void) { first_err = rc; } + screen_preview_stop_runtime_feedback(); rc = lifecycle_stop_printer_protocol(); if (rc != ESP_OK && first_err == ESP_OK) { first_err = rc; diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index bed1628..d15e13f 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -112,6 +112,37 @@ esp_err_t image_generation_generate_png(const image_generation_request_t *req, size_t err_len); // ---------- screen_preview ---------- +typedef enum { + SCREEN_PREVIEW_EVENT_BOOTING = 0, + SCREEN_PREVIEW_EVENT_RUNTIME_READY, + SCREEN_PREVIEW_EVENT_RUNTIME_ERROR, + SCREEN_PREVIEW_EVENT_WIFI_PROVISIONING, + SCREEN_PREVIEW_EVENT_WIFI_CONNECTING, + SCREEN_PREVIEW_EVENT_WIFI_CONNECTED, + SCREEN_PREVIEW_EVENT_WIFI_FAILED, + SCREEN_PREVIEW_EVENT_WIFI_DISCONNECTED, + SCREEN_PREVIEW_EVENT_DEVICE_BUSY, + SCREEN_PREVIEW_EVENT_VOICE_CONNECTING, + SCREEN_PREVIEW_EVENT_VOICE_LISTENING, + SCREEN_PREVIEW_EVENT_VOICE_PROCESSING, + SCREEN_PREVIEW_EVENT_VOICE_RESPONDING, + SCREEN_PREVIEW_EVENT_VOICE_ERROR, + SCREEN_PREVIEW_EVENT_IMAGE_GENERATING, + SCREEN_PREVIEW_EVENT_IMAGE_READY, + SCREEN_PREVIEW_EVENT_IMAGE_FAILED, + SCREEN_PREVIEW_EVENT_PRINT_QUEUED, + SCREEN_PREVIEW_EVENT_PRINT_RUNNING, + SCREEN_PREVIEW_EVENT_PRINT_SUCCESS, + SCREEN_PREVIEW_EVENT_PRINT_FAILED, + SCREEN_PREVIEW_EVENT_PRINT_CANCELED, + SCREEN_PREVIEW_EVENT_SHUTDOWN, +} screen_preview_event_t; + +typedef struct { + const char *detail; + uint8_t progress; +} screen_preview_context_t; + esp_err_t screen_preview_show_raster(const uint8_t *raster, size_t raster_len, uint16_t width, @@ -128,6 +159,10 @@ esp_err_t screen_preview_show_text(const char *text, uint32_t timeout_ms, char *err, size_t err_len); +esp_err_t screen_preview_show_event(screen_preview_event_t event, + const screen_preview_context_t *ctx); +esp_err_t screen_preview_start_runtime_feedback(void); +void screen_preview_stop_runtime_feedback(void); // ---------- printer_protocol ---------- typedef enum { diff --git a/components/domain/src/product_test_service.c b/components/domain/src/product_test_service.c index 9e4d1d0..87ff0e5 100644 --- a/components/domain/src/product_test_service.c +++ b/components/domain/src/product_test_service.c @@ -59,6 +59,7 @@ #define PRODUCT_TEST_IMAGE_PRINT_MAX_HEIGHT 256 #define PRODUCT_TEST_IMAGE_DEFAULT_TIMEOUT_MS 90000 #define PRODUCT_TEST_IMAGE_DOWNLOAD_TIMEOUT_MS 30000 +#define PRODUCT_TEST_SCREEN_FLOW_STEP_MS 500 static const char *TAG = "product_test"; @@ -506,6 +507,68 @@ static void handle_screen(uint32_t seq, const char *name, cJSON *payload) { err[0] != '\0' ? err : NULL); } +static void handle_screen_flow(uint32_t seq, const char *name, cJSON *payload) { + uint32_t step_ms = json_get_u32(payload, "step_ms", PRODUCT_TEST_SCREEN_FLOW_STEP_MS); + step_ms = clamp_u32(step_ms, 100, 3000); + + const screen_preview_event_t events[] = { + SCREEN_PREVIEW_EVENT_WIFI_PROVISIONING, + SCREEN_PREVIEW_EVENT_RUNTIME_READY, + SCREEN_PREVIEW_EVENT_VOICE_LISTENING, + SCREEN_PREVIEW_EVENT_VOICE_PROCESSING, + SCREEN_PREVIEW_EVENT_IMAGE_GENERATING, + SCREEN_PREVIEW_EVENT_PRINT_RUNNING, + SCREEN_PREVIEW_EVENT_PRINT_SUCCESS, + SCREEN_PREVIEW_EVENT_PRINT_FAILED, + }; + + int64_t start = now_ms(); + uint32_t shown = 0; + esp_err_t rc = ESP_OK; + for (size_t i = 0; i < sizeof(events) / sizeof(events[0]); ++i) { + cJSON *progress = cJSON_CreateObject(); + if (progress != NULL) { + cJSON_AddNumberToObject(progress, "index", (double)(i + 1)); + cJSON_AddNumberToObject(progress, "total", (double)(sizeof(events) / sizeof(events[0]))); + } + emit_progress(seq, name, "screen_flow", progress); + + char detail[96] = {0}; + if (events[i] == SCREEN_PREVIEW_EVENT_WIFI_PROVISIONING) { + strlcpy(detail, "连接热点后配网", sizeof(detail)); + } else if (events[i] == SCREEN_PREVIEW_EVENT_PRINT_RUNNING) { + strlcpy(detail, "请勿拉纸", sizeof(detail)); + } else if (events[i] == SCREEN_PREVIEW_EVENT_PRINT_FAILED) { + strlcpy(detail, "printer out of paper", sizeof(detail)); + } else { + detail[0] = '\0'; + } + screen_preview_context_t ctx = { + .detail = detail, + .progress = (events[i] == SCREEN_PREVIEW_EVENT_PRINT_RUNNING) ? 45 : 0, + }; + rc = screen_preview_show_event(events[i], &ctx); + if (rc != ESP_OK && rc != ESP_ERR_NOT_SUPPORTED) { + break; + } + ++shown; + vTaskDelay(pdMS_TO_TICKS(step_ms)); + } + + cJSON *data = cJSON_CreateObject(); + if (data != NULL) { + cJSON_AddNumberToObject(data, "shown", (double)shown); + cJSON_AddNumberToObject(data, "total", (double)(sizeof(events) / sizeof(events[0]))); + } + emit_result(seq, + name, + (rc == ESP_OK || rc == ESP_ERR_NOT_SUPPORTED) ? "ok" : "fail", + (rc == ESP_ERR_NOT_SUPPORTED) ? ESP_OK : rc, + now_ms() - start, + data, + NULL); +} + static void handle_printer_status(uint32_t seq, const char *name) { int64_t start = now_ms(); printer_runtime_status_t printer = {0}; @@ -619,6 +682,7 @@ static void handle_image_generate(uint32_t seq, const char *name, cJSON *payload int64_t start = now_ms(); emit_progress(seq, name, "image_generate", NULL); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_GENERATING, NULL); image_generation_request_t req = { .prompt = prompt, @@ -633,6 +697,10 @@ static void handle_image_generate(uint32_t seq, const char *name, cJSON *payload char err[160] = {0}; esp_err_t rc = image_generation_generate_png(&req, &result, err, sizeof(err)); if (rc != ESP_OK) { + screen_preview_context_t preview_ctx = { + .detail = err, + }; + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_FAILED, &preview_ctx); emit_result(seq, name, "fail", @@ -654,11 +722,13 @@ static void handle_image_generate(uint32_t seq, const char *name, cJSON *payload if (!should_print) { image_generation_result_free(&result); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_READY, NULL); emit_result(seq, name, "ok", ESP_OK, now_ms() - start, image_data, NULL); return; } emit_progress(seq, name, "image_preview", NULL); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_READY, NULL); char preview_err[96] = {0}; esp_err_t preview_rc = screen_preview_show_png(result.png, result.png_len, @@ -1116,6 +1186,8 @@ static void dispatch_command(const command_context_t *cmd) { handle_wait_wifi(cmd->seq, cmd->name, cmd->payload); } else if (strcmp(cmd->name, "SCREEN") == 0) { handle_screen(cmd->seq, cmd->name, cmd->payload); + } else if (strcmp(cmd->name, "SCREEN_FLOW") == 0) { + handle_screen_flow(cmd->seq, cmd->name, cmd->payload); } else if (strcmp(cmd->name, "PRINTER_STATUS") == 0) { handle_printer_status(cmd->seq, cmd->name); } else if (strcmp(cmd->name, "GAP") == 0) { diff --git a/components/domain/src/screen_preview.c b/components/domain/src/screen_preview.c index 6740bef..d525b0b 100644 --- a/components/domain/src/screen_preview.c +++ b/components/domain/src/screen_preview.c @@ -1,12 +1,39 @@ #include "domain.h" #include "platform.h" +#include +#include #include #include #define SCREEN_PREVIEW_TEXT_SCALE 2 #define SCREEN_PREVIEW_TEXT_LINE_SPACING 1 #define SCREEN_PREVIEW_TEXT_MAX_HEIGHT 3000 +#define SCREEN_PREVIEW_STATUS_TIMEOUT_MS 3000 + +#define SCREEN_PREVIEW_LAYOUT_WIDTH 384 +#define SCREEN_PREVIEW_LAYOUT_HEIGHT 288 +#define SCREEN_PREVIEW_LAYOUT_BYTES_PER_ROW (SCREEN_PREVIEW_LAYOUT_WIDTH / 8) +#define SCREEN_PREVIEW_LAYOUT_LEN (SCREEN_PREVIEW_LAYOUT_BYTES_PER_ROW * SCREEN_PREVIEW_LAYOUT_HEIGHT) +#define SCREEN_PREVIEW_TITLE_SCALE 3 +#define SCREEN_PREVIEW_BODY_SCALE 2 +#define SCREEN_PREVIEW_BODY_LINE_SPACING 2 +#define SCREEN_PREVIEW_TITLE_Y 52 +#define SCREEN_PREVIEW_TITLE_ONLY_Y 96 +#define SCREEN_PREVIEW_PROGRESS_TITLE_Y 36 +#define SCREEN_PREVIEW_PROGRESS_DETAIL_Y 102 +#define SCREEN_PREVIEW_PROGRESS_BAR_X 72 +#define SCREEN_PREVIEW_PROGRESS_BAR_Y 176 +#define SCREEN_PREVIEW_PROGRESS_BAR_W 240 +#define SCREEN_PREVIEW_PROGRESS_BAR_H 16 +#define SCREEN_PREVIEW_PROGRESS_TEXT_Y 204 +#define SCREEN_PREVIEW_DETAIL_MAX_WIDTH 336 +#define SCREEN_PREVIEW_DETAIL_MAX_LINES 3 +#define SCREEN_PREVIEW_DETAIL_PROGRESS_MAX_LINES 2 +#define SCREEN_PREVIEW_LINE_MAX_BYTES 96 +#define SCREEN_PREVIEW_PRINT_PROGRESS_STEP 20 + +static const char *screen_preview_default_detail(screen_preview_event_t event); esp_err_t screen_preview_show_raster(const uint8_t *raster, size_t raster_len, @@ -77,3 +104,626 @@ esp_err_t screen_preview_show_text(const char *text, } return preview_rc; } + +static size_t screen_preview_utf8_char_width(const char *s, size_t remain, size_t *out_bytes) +{ + if (s == NULL || remain == 0 || out_bytes == NULL) { + return 0; + } + + unsigned char c = (unsigned char)s[0]; + if (c < 0x80u) { + *out_bytes = 1; + return 1; + } + if ((c & 0xE0u) == 0xC0u && remain >= 2) { + *out_bytes = 2; + return 2; + } + if ((c & 0xF0u) == 0xE0u && remain >= 3) { + *out_bytes = 3; + return 2; + } + if ((c & 0xF8u) == 0xF0u && remain >= 4) { + *out_bytes = 4; + return 2; + } + + *out_bytes = 1; + return 1; +} + +static uint16_t screen_preview_text_pixel_width(const char *text, uint8_t scale) +{ + if (text == NULL || text[0] == '\0') { + return 0; + } + + size_t total_cols = 0; + const char *p = text; + size_t remain = strlen(text); + while (remain > 0 && *p != '\0') { + size_t bytes = 0; + size_t cols = screen_preview_utf8_char_width(p, remain, &bytes); + if (bytes == 0) { + break; + } + if (*p == '\n' || *p == '\r') { + break; + } + total_cols += cols; + p += bytes; + remain -= bytes; + } + + return (uint16_t)(total_cols * 8u * scale); +} + +static void screen_preview_bitmap_set_black(uint8_t *buf, uint16_t x, uint16_t y) +{ + if (buf == NULL || x >= SCREEN_PREVIEW_LAYOUT_WIDTH || y >= SCREEN_PREVIEW_LAYOUT_HEIGHT) { + return; + } + size_t idx = (size_t)y * SCREEN_PREVIEW_LAYOUT_BYTES_PER_ROW + (x / 8u); + buf[idx] |= (uint8_t)(1u << (7u - (x % 8u))); +} + +static void screen_preview_draw_hline(uint8_t *buf, uint16_t x, uint16_t y, uint16_t w) +{ + for (uint16_t dx = 0; dx < w; ++dx) { + screen_preview_bitmap_set_black(buf, (uint16_t)(x + dx), y); + } +} + +static void screen_preview_draw_vline(uint8_t *buf, uint16_t x, uint16_t y, uint16_t h) +{ + for (uint16_t dy = 0; dy < h; ++dy) { + screen_preview_bitmap_set_black(buf, x, (uint16_t)(y + dy)); + } +} + +static void screen_preview_fill_rect(uint8_t *buf, + uint16_t x, + uint16_t y, + uint16_t w, + uint16_t h) +{ + for (uint16_t dy = 0; dy < h; ++dy) { + for (uint16_t dx = 0; dx < w; ++dx) { + screen_preview_bitmap_set_black(buf, (uint16_t)(x + dx), (uint16_t)(y + dy)); + } + } +} + +static void screen_preview_draw_progress_bar(uint8_t *buf, uint8_t progress) +{ + uint16_t x = SCREEN_PREVIEW_PROGRESS_BAR_X; + uint16_t y = SCREEN_PREVIEW_PROGRESS_BAR_Y; + uint16_t w = SCREEN_PREVIEW_PROGRESS_BAR_W; + uint16_t h = SCREEN_PREVIEW_PROGRESS_BAR_H; + + screen_preview_draw_hline(buf, x, y, w); + screen_preview_draw_hline(buf, x, (uint16_t)(y + h - 1u), w); + screen_preview_draw_vline(buf, x, y, h); + screen_preview_draw_vline(buf, (uint16_t)(x + w - 1u), y, h); + + if (progress > 100) { + progress = 100; + } + uint16_t fill_w = (uint16_t)(((uint32_t)(w - 4u) * progress) / 100u); + if (fill_w > 0) { + screen_preview_fill_rect(buf, (uint16_t)(x + 2u), (uint16_t)(y + 2u), fill_w, (uint16_t)(h - 4u)); + } +} + +static bool screen_preview_append_line(char lines[][SCREEN_PREVIEW_LINE_MAX_BYTES], + size_t max_lines, + size_t *line_count, + const char *start, + size_t len) +{ + if (lines == NULL || line_count == NULL || *line_count >= max_lines || start == NULL) { + return false; + } + + while (len > 0 && isspace((unsigned char)start[0])) { + ++start; + --len; + } + while (len > 0 && isspace((unsigned char)start[len - 1u])) { + --len; + } + if (len == 0) { + return true; + } + + if (len >= SCREEN_PREVIEW_LINE_MAX_BYTES) { + len = SCREEN_PREVIEW_LINE_MAX_BYTES - 1u; + } + memcpy(lines[*line_count], start, len); + lines[*line_count][len] = '\0'; + ++(*line_count); + return true; +} + +static void screen_preview_wrap_text(const char *text, + size_t max_cols, + char lines[][SCREEN_PREVIEW_LINE_MAX_BYTES], + size_t max_lines, + size_t *out_line_count) +{ + if (out_line_count != NULL) { + *out_line_count = 0; + } + if (text == NULL || text[0] == '\0' || lines == NULL || out_line_count == NULL || + max_lines == 0 || max_cols == 0) { + return; + } + + const char *line_start = text; + const char *p = text; + size_t remain = strlen(text); + size_t cols = 0; + + while (remain > 0 && *out_line_count < max_lines) { + if (*p == '\r') { + ++p; + --remain; + continue; + } + if (*p == '\n') { + (void)screen_preview_append_line(lines, + max_lines, + out_line_count, + line_start, + (size_t)(p - line_start)); + ++p; + --remain; + line_start = p; + cols = 0; + continue; + } + + size_t bytes = 0; + size_t char_cols = screen_preview_utf8_char_width(p, remain, &bytes); + if (bytes == 0) { + break; + } + + if (cols > 0 && cols + char_cols > max_cols) { + (void)screen_preview_append_line(lines, + max_lines, + out_line_count, + line_start, + (size_t)(p - line_start)); + line_start = p; + cols = 0; + if (*out_line_count >= max_lines) { + break; + } + } + + cols += char_cols; + p += bytes; + remain -= bytes; + } + + if (*out_line_count < max_lines && p > line_start) { + (void)screen_preview_append_line(lines, + max_lines, + out_line_count, + line_start, + (size_t)(p - line_start)); + } +} + +static esp_err_t screen_preview_render_text_line(uint8_t *dst, + const char *text, + uint8_t scale, + uint16_t y, + char *err, + size_t err_len) +{ + if (dst == NULL || text == NULL || text[0] == '\0') { + return ESP_OK; + } + + uint16_t text_w = screen_preview_text_pixel_width(text, scale); + uint16_t x = 0; + if (text_w < SCREEN_PREVIEW_LAYOUT_WIDTH) { + x = (uint16_t)((SCREEN_PREVIEW_LAYOUT_WIDTH - text_w) / 2u); + } + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *line = NULL; + size_t line_len = 0; + char render_err[96] = {0}; + esp_err_t rc = raster_tools_render_text_384(text, + scale, + 0, + (uint16_t)(32u * scale), + &width, + &height, + &line, + &line_len, + render_err, + sizeof(render_err)); + if (rc != ESP_OK) { + if (err != NULL && err_len > 0 && render_err[0] != '\0') { + strlcpy(err, render_err, err_len); + } + return rc; + } + + if (width > SCREEN_PREVIEW_LAYOUT_WIDTH) { + width = SCREEN_PREVIEW_LAYOUT_WIDTH; + } + if (height > SCREEN_PREVIEW_LAYOUT_HEIGHT - y) { + height = (uint16_t)(SCREEN_PREVIEW_LAYOUT_HEIGHT - y); + } + + for (uint16_t row = 0; row < height; ++row) { + const uint8_t *src_row = line + (size_t)row * SCREEN_PREVIEW_LAYOUT_BYTES_PER_ROW; + for (uint16_t col = 0; col < width; ++col) { + size_t byte_idx = col / 8u; + uint8_t bit = (uint8_t)(7u - (col % 8u)); + if ((src_row[byte_idx] & (uint8_t)(1u << bit)) != 0) { + screen_preview_bitmap_set_black(dst, (uint16_t)(x + col), (uint16_t)(y + row)); + } + } + } + + free(line); + return ESP_OK; +} + +static const char *screen_preview_default_detail(screen_preview_event_t event) +{ + switch (event) { + case SCREEN_PREVIEW_EVENT_BOOTING: + return ""; + case SCREEN_PREVIEW_EVENT_RUNTIME_READY: + return "按住说话"; + case SCREEN_PREVIEW_EVENT_RUNTIME_ERROR: + return "请重启"; + case SCREEN_PREVIEW_EVENT_WIFI_PROVISIONING: + return "连接热点后配网"; + case SCREEN_PREVIEW_EVENT_WIFI_CONNECTING: + return ""; + case SCREEN_PREVIEW_EVENT_WIFI_CONNECTED: + return ""; + case SCREEN_PREVIEW_EVENT_WIFI_FAILED: + return "检查密码或信号"; + case SCREEN_PREVIEW_EVENT_WIFI_DISCONNECTED: + return ""; + case SCREEN_PREVIEW_EVENT_DEVICE_BUSY: + return "稍后再试"; + case SCREEN_PREVIEW_EVENT_VOICE_CONNECTING: + return "请稍候"; + case SCREEN_PREVIEW_EVENT_VOICE_LISTENING: + return ""; + case SCREEN_PREVIEW_EVENT_VOICE_PROCESSING: + return ""; + case SCREEN_PREVIEW_EVENT_VOICE_RESPONDING: + return ""; + case SCREEN_PREVIEW_EVENT_VOICE_ERROR: + return "请稍后重试"; + case SCREEN_PREVIEW_EVENT_IMAGE_GENERATING: + return ""; + case SCREEN_PREVIEW_EVENT_IMAGE_READY: + return ""; + case SCREEN_PREVIEW_EVENT_IMAGE_FAILED: + return "请换个说法再试"; + case SCREEN_PREVIEW_EVENT_PRINT_QUEUED: + return ""; + case SCREEN_PREVIEW_EVENT_PRINT_RUNNING: + return "请勿拉纸"; + case SCREEN_PREVIEW_EVENT_PRINT_SUCCESS: + return "可以取走"; + case SCREEN_PREVIEW_EVENT_PRINT_FAILED: + return "检查纸张/电量"; + case SCREEN_PREVIEW_EVENT_PRINT_CANCELED: + return "已取消"; + case SCREEN_PREVIEW_EVENT_SHUTDOWN: + return ""; + default: + return ""; + } +} + +static bool screen_preview_event_is_user_visible(screen_preview_event_t event) +{ + switch (event) { + case SCREEN_PREVIEW_EVENT_WIFI_CONNECTING: + case SCREEN_PREVIEW_EVENT_WIFI_CONNECTED: + case SCREEN_PREVIEW_EVENT_WIFI_DISCONNECTED: + case SCREEN_PREVIEW_EVENT_VOICE_RESPONDING: + case SCREEN_PREVIEW_EVENT_IMAGE_READY: + case SCREEN_PREVIEW_EVENT_PRINT_QUEUED: + return false; + default: + return true; + } +} + +static void screen_preview_user_detail(screen_preview_event_t event, + const char *raw_detail, + char *out_detail, + size_t out_len) +{ + if (out_detail == NULL || out_len == 0) { + return; + } + out_detail[0] = '\0'; + + if (raw_detail == NULL || raw_detail[0] == '\0') { + strlcpy(out_detail, screen_preview_default_detail(event), out_len); + return; + } + + if (event == SCREEN_PREVIEW_EVENT_RUNTIME_ERROR || + event == SCREEN_PREVIEW_EVENT_WIFI_CONNECTED || + event == SCREEN_PREVIEW_EVENT_VOICE_ERROR) { + strlcpy(out_detail, screen_preview_default_detail(event), out_len); + return; + } + + if (event == SCREEN_PREVIEW_EVENT_PRINT_FAILED) { + if (strstr(raw_detail, "out of paper") != NULL) { + strlcpy(out_detail, "缺纸,请装纸", out_len); + return; + } + if (strstr(raw_detail, "battery too low") != NULL) { + strlcpy(out_detail, "电量低,请充电", out_len); + return; + } + if (strstr(raw_detail, "temperature out of range") != NULL) { + strlcpy(out_detail, "温度异常,稍后再试", out_len); + return; + } + if (strstr(raw_detail, "canceled") != NULL || strstr(raw_detail, "Cancel") != NULL) { + strlcpy(out_detail, "已取消", out_len); + return; + } + strlcpy(out_detail, screen_preview_default_detail(event), out_len); + return; + } + + if (event == SCREEN_PREVIEW_EVENT_IMAGE_FAILED) { + strlcpy(out_detail, screen_preview_default_detail(event), out_len); + return; + } + + strlcpy(out_detail, raw_detail, out_len); +} + +static const char *screen_preview_event_title(screen_preview_event_t event) +{ + switch (event) { + case SCREEN_PREVIEW_EVENT_BOOTING: + return "小Q画画机"; + case SCREEN_PREVIEW_EVENT_RUNTIME_READY: + return "准备好了"; + case SCREEN_PREVIEW_EVENT_RUNTIME_ERROR: + return "启动失败"; + case SCREEN_PREVIEW_EVENT_WIFI_PROVISIONING: + return "配网"; + case SCREEN_PREVIEW_EVENT_WIFI_CONNECTING: + return "联网中"; + case SCREEN_PREVIEW_EVENT_WIFI_CONNECTED: + return "已联网"; + case SCREEN_PREVIEW_EVENT_WIFI_FAILED: + return "联网失败"; + case SCREEN_PREVIEW_EVENT_WIFI_DISCONNECTED: + return "网络断开"; + case SCREEN_PREVIEW_EVENT_DEVICE_BUSY: + return "请稍候"; + case SCREEN_PREVIEW_EVENT_VOICE_CONNECTING: + return "连接中"; + case SCREEN_PREVIEW_EVENT_VOICE_LISTENING: + return "请说话"; + case SCREEN_PREVIEW_EVENT_VOICE_PROCESSING: + return "处理中"; + case SCREEN_PREVIEW_EVENT_VOICE_RESPONDING: + return "播放中"; + case SCREEN_PREVIEW_EVENT_VOICE_ERROR: + return "语音失败"; + case SCREEN_PREVIEW_EVENT_IMAGE_GENERATING: + return "生成中"; + case SCREEN_PREVIEW_EVENT_IMAGE_READY: + return "已生成"; + case SCREEN_PREVIEW_EVENT_IMAGE_FAILED: + return "生成失败"; + case SCREEN_PREVIEW_EVENT_PRINT_QUEUED: + return "准备打印"; + case SCREEN_PREVIEW_EVENT_PRINT_RUNNING: + return "正在打印"; + case SCREEN_PREVIEW_EVENT_PRINT_SUCCESS: + return "打印完成"; + case SCREEN_PREVIEW_EVENT_PRINT_FAILED: + return "打印失败"; + case SCREEN_PREVIEW_EVENT_PRINT_CANCELED: + return "已取消"; + case SCREEN_PREVIEW_EVENT_SHUTDOWN: + return "关机中"; + default: + return "状态更新"; + } +} + +esp_err_t screen_preview_show_event(screen_preview_event_t event, + const screen_preview_context_t *ctx) +{ + if (!screen_preview_event_is_user_visible(event)) { + return ESP_OK; + } + + const char *title = screen_preview_event_title(event); + const char *raw_detail = (ctx != NULL && ctx->detail != NULL && ctx->detail[0] != '\0') ? + ctx->detail : + screen_preview_default_detail(event); + uint8_t progress = ctx != NULL ? ctx->progress : 0; + bool show_progress = progress > 0 && progress < 100; + + char detail[160] = {0}; + screen_preview_user_detail(event, raw_detail, detail, sizeof(detail)); + + uint8_t *raster = (uint8_t *)calloc(1, SCREEN_PREVIEW_LAYOUT_LEN); + if (raster == NULL) { + return ESP_ERR_NO_MEM; + } + + char err[96] = {0}; + bool has_detail = detail[0] != '\0'; + uint16_t title_y = show_progress ? + SCREEN_PREVIEW_PROGRESS_TITLE_Y : + (has_detail ? SCREEN_PREVIEW_TITLE_Y : SCREEN_PREVIEW_TITLE_ONLY_Y); + esp_err_t rc = screen_preview_render_text_line(raster, + title, + SCREEN_PREVIEW_TITLE_SCALE, + title_y, + err, + sizeof(err)); + if (rc != ESP_OK) { + free(raster); + return rc; + } + + char lines[SCREEN_PREVIEW_DETAIL_MAX_LINES][SCREEN_PREVIEW_LINE_MAX_BYTES] = {0}; + size_t max_lines = show_progress ? SCREEN_PREVIEW_DETAIL_PROGRESS_MAX_LINES : SCREEN_PREVIEW_DETAIL_MAX_LINES; + size_t line_count = 0; + size_t max_cols = SCREEN_PREVIEW_DETAIL_MAX_WIDTH / (8u * SCREEN_PREVIEW_BODY_SCALE); + screen_preview_wrap_text(detail, max_cols, lines, max_lines, &line_count); + + uint16_t body_line_h = (uint16_t)(16u * SCREEN_PREVIEW_BODY_SCALE + + SCREEN_PREVIEW_BODY_LINE_SPACING * SCREEN_PREVIEW_BODY_SCALE); + uint16_t detail_block_h = (uint16_t)(line_count > 0 ? ((line_count - 1u) * body_line_h + 16u * SCREEN_PREVIEW_BODY_SCALE) : 0); + uint16_t detail_y = 0; + if (show_progress) { + detail_y = SCREEN_PREVIEW_PROGRESS_DETAIL_Y; + } else if (detail_block_h < SCREEN_PREVIEW_LAYOUT_HEIGHT) { + detail_y = (uint16_t)((SCREEN_PREVIEW_LAYOUT_HEIGHT - detail_block_h) / 2u + 34u); + if (detail_y > 190u) { + detail_y = 190u; + } + } + + for (size_t i = 0; i < line_count; ++i) { + rc = screen_preview_render_text_line(raster, + lines[i], + SCREEN_PREVIEW_BODY_SCALE, + (uint16_t)(detail_y + i * body_line_h), + err, + sizeof(err)); + if (rc != ESP_OK) { + free(raster); + return rc; + } + } + + if (show_progress) { + screen_preview_draw_progress_bar(raster, progress); + char progress_text[16] = {0}; + snprintf(progress_text, sizeof(progress_text), "%u%%", (unsigned)progress); + rc = screen_preview_render_text_line(raster, + progress_text, + SCREEN_PREVIEW_BODY_SCALE, + SCREEN_PREVIEW_PROGRESS_TEXT_Y, + err, + sizeof(err)); + if (rc != ESP_OK) { + free(raster); + return rc; + } + } + + rc = screen_preview_show_raster(raster, + SCREEN_PREVIEW_LAYOUT_LEN, + SCREEN_PREVIEW_LAYOUT_WIDTH, + SCREEN_PREVIEW_LAYOUT_HEIGHT, + SCREEN_PREVIEW_STATUS_TIMEOUT_MS, + err, + sizeof(err)); + free(raster); + return rc; +} + +static void screen_preview_printer_event_cb(const printer_job_event_t *event, void *user_data) +{ + (void)user_data; + if (event == NULL) { + return; + } + + static uint32_t s_print_job_id; + static uint8_t s_last_progress_bucket; + static bool s_running_shown; + screen_preview_event_t preview_event = SCREEN_PREVIEW_EVENT_PRINT_QUEUED; + switch (event->type) { + case PRINTER_JOB_EVENT_QUEUED: + return; + case PRINTER_JOB_EVENT_RUNNING: + case PRINTER_JOB_EVENT_PROGRESS: + preview_event = SCREEN_PREVIEW_EVENT_PRINT_RUNNING; + if (event->job.id != s_print_job_id) { + s_print_job_id = event->job.id; + s_last_progress_bucket = 0; + s_running_shown = false; + } + uint8_t progress = event->job.progress > 100 ? 100 : event->job.progress; + uint8_t bucket = (uint8_t)(progress / SCREEN_PREVIEW_PRINT_PROGRESS_STEP); + if (s_running_shown && bucket <= s_last_progress_bucket) { + return; + } + s_running_shown = true; + s_last_progress_bucket = bucket; + break; + case PRINTER_JOB_EVENT_FINISHED: + s_print_job_id = 0; + s_last_progress_bucket = 0; + s_running_shown = false; + if (event->job.state == PRINT_JOB_STATE_SUCCESS) { + preview_event = SCREEN_PREVIEW_EVENT_PRINT_SUCCESS; + } else if (event->job.state == PRINT_JOB_STATE_CANCELED) { + preview_event = SCREEN_PREVIEW_EVENT_PRINT_CANCELED; + } else { + preview_event = SCREEN_PREVIEW_EVENT_PRINT_FAILED; + } + break; + default: + break; + } + + char detail[128] = {0}; + if (preview_event == SCREEN_PREVIEW_EVENT_PRINT_FAILED && + event->job.error[0] != '\0') { + strlcpy(detail, event->job.error, sizeof(detail)); + } else if (preview_event == SCREEN_PREVIEW_EVENT_PRINT_QUEUED) { + strlcpy(detail, "准备打印", sizeof(detail)); + } else if (preview_event == SCREEN_PREVIEW_EVENT_PRINT_RUNNING) { + strlcpy(detail, "请勿拉纸", sizeof(detail)); + } else if (preview_event == SCREEN_PREVIEW_EVENT_PRINT_SUCCESS) { + strlcpy(detail, "可以取走", sizeof(detail)); + } else if (preview_event == SCREEN_PREVIEW_EVENT_PRINT_CANCELED) { + strlcpy(detail, "已取消", sizeof(detail)); + } else { + snprintf(detail, sizeof(detail), "任务#%u", (unsigned)event->job.id); + } + + screen_preview_context_t ctx = { + .detail = detail, + .progress = event->job.progress, + }; + (void)screen_preview_show_event(preview_event, &ctx); +} + +esp_err_t screen_preview_start_runtime_feedback(void) +{ + return printer_protocol_set_event_callback(screen_preview_printer_event_cb, NULL); +} + +void screen_preview_stop_runtime_feedback(void) +{ + (void)printer_protocol_set_event_callback(NULL, NULL); +} diff --git a/components/domain/src/system_runtime.c b/components/domain/src/system_runtime.c index 8c048f9..14ec2af 100644 --- a/components/domain/src/system_runtime.c +++ b/components/domain/src/system_runtime.c @@ -95,7 +95,13 @@ static void system_runtime_show_wifi_hint(void) { "未检测到Wi-Fi配置\n连接热点: %s\n打开: http://192.168.4.1", ap_ssid); - system_runtime_show_status_text(hint, SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS); + screen_preview_context_t ctx = { + .detail = hint, + }; + esp_err_t rc = screen_preview_show_event(SCREEN_PREVIEW_EVENT_WIFI_PROVISIONING, &ctx); + if (rc != ESP_OK && rc != ESP_ERR_NOT_SUPPORTED) { + system_runtime_show_status_text(hint, SYSTEM_RUNTIME_WIFI_HINT_TIMEOUT_MS); + } } static void system_runtime_try_show_wifi_hint_for_unprovisioned(void) { @@ -111,7 +117,7 @@ static void system_runtime_wifi_event_handler(wifi_manager_event_t event, void * 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); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_WIFI_CONNECTING, NULL); break; case WIFI_MANAGER_EVENT_STA_CONNECTED: { char ip[20] = {0}; @@ -119,16 +125,19 @@ static void system_runtime_wifi_event_handler(wifi_manager_event_t event, void * char text[96] = {0}; snprintf(text, sizeof(text), - "Wi-Fi已连接\nIP: %s", + "IP: %s", ip[0] != '\0' ? ip : "0.0.0.0"); - system_runtime_show_status_text(text, SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + screen_preview_context_t ctx = { + .detail = text, + }; + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_WIFI_CONNECTED, &ctx); break; } case WIFI_MANAGER_EVENT_STA_CONNECT_FAILED: - system_runtime_show_status_text("Wi-Fi连接失败\n请检查密码/信号后重试", SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_WIFI_FAILED, NULL); break; case WIFI_MANAGER_EVENT_STA_DISCONNECTED: - system_runtime_show_status_text("Wi-Fi已断开\n正在重连...", SYSTEM_RUNTIME_WIFI_STATUS_TIMEOUT_MS); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_WIFI_DISCONNECTED, NULL); break; default: break; diff --git a/components/domain/src/voice_interaction_marker.c b/components/domain/src/voice_interaction_marker.c index 43dd2e9..a539518 100644 --- a/components/domain/src/voice_interaction_marker.c +++ b/components/domain/src/voice_interaction_marker.c @@ -339,6 +339,7 @@ static void voice_image_generation_task(void *arg) { esp_err_t rc = image_generation_generate_png(&req, &result, gen_err, sizeof(gen_err)); printer_protocol_status_poll_pause_release(&status_poll_pause_token); if (rc == ESP_OK) { + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_READY, NULL); char submit_err[160] = {0}; esp_err_t submit_rc = voice_submit_marker_generated_image(task_arg, @@ -355,6 +356,10 @@ static void voice_image_generation_task(void *arg) { task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", (unsigned)submit_rc, submit_err[0] != '\0' ? submit_err : "marker image print failed"); + screen_preview_context_t ctx = { + .detail = submit_err, + }; + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_PRINT_FAILED, &ctx); } } else { ESP_LOGW(TAG, @@ -362,6 +367,10 @@ static void voice_image_generation_task(void *arg) { task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", (unsigned)rc, gen_err[0] != '\0' ? gen_err : "image generation failed"); + screen_preview_context_t ctx = { + .detail = gen_err, + }; + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_FAILED, &ctx); } image_generation_result_free(&result); @@ -391,6 +400,8 @@ static void voice_schedule_marker_image_generation(const char *prompt, const cha return; } + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_GENERATING, NULL); + voice_image_generation_task_arg_t *task_arg = (voice_image_generation_task_arg_t *)calloc(1, sizeof(*task_arg)); if (task_arg == NULL) { @@ -414,6 +425,7 @@ static void voice_schedule_marker_image_generation(const char *prompt, const cha ESP_LOGW(TAG, "marker_image_gen_skip: create task failed"); voice_marker_image_generation_release(); free(task_arg); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_IMAGE_FAILED, NULL); return; } } diff --git a/components/domain/src/voice_interaction_mic_key.c b/components/domain/src/voice_interaction_mic_key.c index 3d4f7ca..4d403cd 100644 --- a/components/domain/src/voice_interaction_mic_key.c +++ b/components/domain/src/voice_interaction_mic_key.c @@ -77,7 +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("语音已在录制中"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_LISTENING, NULL); return; } @@ -90,12 +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请等待当前任务完成"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_DEVICE_BUSY, NULL); return; } if (!voice_mic_key_session_ready(&voice)) { - voice_mic_key_show_status("语音连接中...\n请稍候"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_CONNECTING, NULL); 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) { @@ -103,7 +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请稍后重试"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_ERROR, NULL); return; } } @@ -115,7 +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请稍后重试"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_ERROR, NULL); return; } @@ -130,7 +130,7 @@ static void voice_mic_key_handle_press_down(void) { return; } - voice_mic_key_show_status("请开始说话"); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_LISTENING, NULL); ESP_LOGI(TAG, "MIC_KEY tap started"); } @@ -152,7 +152,7 @@ static void voice_mic_key_handle_press_up(void) { return; } - voice_mic_key_show_status("已结束录音\n正在处理..."); + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_PROCESSING, NULL); 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 4165a20..c99d271 100644 --- a/components/domain/src/voice_interaction_ws_protocol.c +++ b/components/domain/src/voice_interaction_ws_protocol.c @@ -1,6 +1,5 @@ #include "voice_interaction_internal.h" -#include #include #include @@ -16,99 +15,10 @@ 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 3000 static portMUX_TYPE s_response_finalize_lock = portMUX_INITIALIZER_UNLOCKED; static bool s_response_finalize_running; -static bool voice_is_space_char(char c) { - return isspace((unsigned char)c) != 0; -} - -static size_t voice_find_first_sentence_end(const char *text, size_t len) { - if (text == NULL || len == 0) { - return 0; - } - - for (size_t i = 0; i < len; ++i) { - unsigned char c = (unsigned char)text[i]; - if (c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || - c == '\n' || c == '\r') { - return i + 1; - } - - if (i + 2 < len) { - unsigned char b1 = (unsigned char)text[i]; - unsigned char b2 = (unsigned char)text[i + 1]; - unsigned char b3 = (unsigned char)text[i + 2]; - - // UTF-8: 。 (E3 80 82) - if (b1 == 0xE3 && b2 == 0x80 && b3 == 0x82) { - return i + 3; - } - // UTF-8: !(EF BC 81) ?(EF BC 9F) ;(EF BC 9B) :(EF BC 9A) - if (b1 == 0xEF && b2 == 0xBC && - (b3 == 0x81 || b3 == 0x9F || b3 == 0x9B || b3 == 0x9A)) { - return i + 3; - } - } - } - - return len; -} - -static const char *voice_get_text_without_first_sentence(const char *text) { - if (text == NULL || text[0] == '\0') { - return NULL; - } - - size_t len = strlen(text); - size_t begin = 0; - while (begin < len && voice_is_space_char(text[begin])) { - begin++; - } - if (begin >= len) { - return NULL; - } - - size_t first_end = begin + voice_find_first_sentence_end(text + begin, len - begin); - while (first_end < len && voice_is_space_char(text[first_end])) { - first_end++; - } - - if (first_end < len) { - return text + first_end; - } - - // Fallback: keep answer body after marker prefix when punctuation split - // does not leave a second sentence. - const char *marker_begin = strstr(text + begin, "/*"); - if (marker_begin == NULL) { - return NULL; - } - - const char *marker_end = strstr(marker_begin + 2, "*/"); - if (marker_end == NULL) { - return NULL; - } - - const char *cursor = marker_end + 2; - while (*cursor != '\0') { - unsigned char ch = (unsigned char)*cursor; - if (!voice_is_space_char((char)ch) && - ch != '.' && ch != '!' && ch != '?' && - ch != ':' && ch != ';' && ch != ',') { - break; - } - cursor++; - } - - if (*cursor == '\0') { - return NULL; - } - return cursor; -} - static esp_err_t voice_send_directive_with_retry(const char *action, const char *directive, bool with_dialog_id) { @@ -227,24 +137,6 @@ static bool voice_get_json_bool(cJSON *obj, const char *name, bool *out_value) { return true; } -static void voice_try_preview_final_text(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_SCREEN_PREVIEW_TIMEOUT_MS, - preview_err, - sizeof(preview_err)); - if (preview_rc != ESP_OK && preview_rc != ESP_ERR_NOT_SUPPORTED) { - ESP_LOGW(TAG, - "dialog_text_preview_failed: rc=0x%x msg=%s", - (unsigned)preview_rc, - preview_err[0] != '\0' ? preview_err : "show text failed"); - } -} - static void voice_handle_output_event(cJSON *output) { const char *event_name = voice_get_json_str(output, "event"); if (event_name == NULL) { @@ -259,13 +151,12 @@ static void voice_handle_output_event(cJSON *output) { bool finished = false; bool has_finished = voice_get_json_bool(output, "finished", &finished); - const char *speech_final_text = NULL; const char *responding_final_text = NULL; bool should_check_marker_in_first_sentence = false; bool should_try_send_speech = false; if (has_finished && finished) { if (strcmp(event_name, "SpeechContent") == 0) { - speech_final_text = voice_get_json_str(output, "text"); + /* Transcripts are intentionally not mirrored to the display. */ } else if (strcmp(event_name, "RespondingContent") == 0) { const char *final_text = voice_get_json_str(output, "text"); if (final_text == NULL || final_text[0] == '\0') { @@ -284,6 +175,8 @@ static void voice_handle_output_event(cJSON *output) { } bool should_close_audio = false; + bool should_show_status_event = false; + screen_preview_event_t status_event = SCREEN_PREVIEW_EVENT_RUNTIME_READY; if (voice_lock(VOICE_STATUS_LOCK_TIMEOUT_MS)) { if (dialog_id != NULL) { strlcpy(s_voice.dialog_id, dialog_id, sizeof(s_voice.dialog_id)); @@ -303,22 +196,32 @@ static void voice_handle_output_event(cJSON *output) { s_voice.response_first_sentence_checked = false; } should_try_send_speech = s_voice.tap_active && !s_voice.speech_active; + if (s_voice.tap_active) { + should_show_status_event = true; + status_event = SCREEN_PREVIEW_EVENT_VOICE_LISTENING; + } } else if (strcmp(state, "Thinking") == 0) { s_voice.dialog_state = VOICE_DIALOG_STATE_THINKING; s_voice.speech_active = false; if (prev_state != VOICE_DIALOG_STATE_THINKING) { s_voice.response_first_sentence_checked = false; } + should_show_status_event = true; + status_event = SCREEN_PREVIEW_EVENT_VOICE_PROCESSING; } else if (strcmp(state, "Responding") == 0) { s_voice.dialog_state = VOICE_DIALOG_STATE_RESPONDING; s_voice.speech_active = false; if (prev_state != VOICE_DIALOG_STATE_RESPONDING) { s_voice.response_first_sentence_checked = false; } + should_show_status_event = true; + status_event = SCREEN_PREVIEW_EVENT_VOICE_RESPONDING; } } } else if (strcmp(event_name, "SpeechEnded") == 0) { s_voice.speech_active = false; + should_show_status_event = true; + status_event = SCREEN_PREVIEW_EVENT_VOICE_PROCESSING; } else if (strcmp(event_name, "Stopped") == 0) { s_voice.started = false; s_voice.tap_active = false; @@ -337,6 +240,8 @@ static void voice_handle_output_event(cJSON *output) { s_voice.speech_active = false; s_voice.response_first_sentence_checked = false; should_close_audio = true; + should_show_status_event = true; + status_event = SCREEN_PREVIEW_EVENT_VOICE_ERROR; } voice_unlock(); } @@ -345,15 +250,8 @@ static void voice_handle_output_event(cJSON *output) { voice_close_audio_with_drain(VOICE_AUDIO_DRAIN_WAIT_MS, "server_event_close"); } - if (speech_final_text != NULL && speech_final_text[0] != '\0') { - voice_try_preview_final_text(speech_final_text); - } - - if (responding_final_text != NULL && responding_final_text[0] != '\0') { - const char *preview_text = voice_get_text_without_first_sentence(responding_final_text); - if (preview_text != NULL && preview_text[0] != '\0') { - voice_try_preview_final_text(preview_text); - } + if (should_show_status_event) { + (void)screen_preview_show_event(status_event, NULL); } if (should_check_marker_in_first_sentence) { @@ -432,6 +330,7 @@ void voice_ws_protocol_handle_text_message(const char *text, size_t len) { s_voice.dialog_state = VOICE_DIALOG_STATE_IDLE; voice_unlock(); } + (void)screen_preview_show_event(SCREEN_PREVIEW_EVENT_VOICE_ERROR, NULL); } cJSON_Delete(root); diff --git a/tools/product_test_uart.py b/tools/product_test_uart.py index 9bcbdcc..b27c51d 100755 --- a/tools/product_test_uart.py +++ b/tools/product_test_uart.py @@ -45,12 +45,14 @@ class ProductTestClient: baud: int, timeout: float, echo_logs: bool = False, + verbose: bool = False, jsonl_path: Optional[Path] = None, ) -> None: self.port = port self.baud = baud self.timeout = timeout self.echo_logs = echo_logs + self.verbose = verbose self.jsonl_path = jsonl_path self.seq = 1 self.serial = serial.Serial( @@ -204,8 +206,9 @@ class ProductTestClient: raise ProtocolError(f"{command} 执行期间设备重启,等待结果失败 seq={seq}") continue if frame.token == TOKEN_PROGRESS: - stage = frame.payload.get("stage", "-") - print(f" - {command}: {stage}") + if self.verbose: + stage = frame.payload.get("stage", "-") + print(f" - {command}: {stage}") continue if frame.token == TOKEN_RESULT: return frame.payload @@ -230,7 +233,8 @@ def default_steps(args: argparse.Namespace) -> List[Dict[str, Any]]: {"cmd": "PING", "timeout": 10}, {"cmd": "STATUS", "timeout": 10}, {"cmd": "WAIT_WIFI", "payload": {"timeout_ms": args.wifi_timeout_ms}, "timeout": args.wifi_timeout_ms / 1000 + 5}, - {"cmd": "SCREEN", "payload": {"text": "TQ Printer\nProduct Test\nScreen OK"}, "timeout": 15}, + {"cmd": "SCREEN", "payload": {"text": "小Q打印机\n屏幕测试\n显示正常"}, "timeout": 15}, + {"cmd": "SCREEN_FLOW", "payload": {"step_ms": 400}, "timeout": 20}, {"cmd": "PRINTER_STATUS", "timeout": 10}, {"cmd": "AUDIO_PROBE", "payload": {"samples": 320, "timeout_ms": 3000}, "timeout": 15}, {"cmd": "VOICE_SESSION", "payload": {"timeout_ms": args.voice_timeout_ms}, "timeout": args.voice_timeout_ms / 1000 + 10}, @@ -321,30 +325,25 @@ def _printer_status_summary(result: Dict[str, Any]) -> str: temp_max = printer.get("temperature_max_c") battery = printer.get("battery_percent") has_paper = printer.get("has_paper") - ntc_raw = printer.get("ntc_adc_raw") parts: List[str] = [] if has_paper is not None: - parts.append(f"paper={'ok' if has_paper else 'missing'}") + parts.append("paper=ok" if has_paper else "paper=missing") if battery is not None: parts.append(f"batt={battery}%") if temp is not None: - if temp_min is not None and temp_max is not None: - parts.append(f"temp={float(temp):.1f}C[{temp_min}-{temp_max}]") - else: - parts.append(f"temp={float(temp):.1f}C") - if ntc_raw is not None: - parts.append(f"ntc_raw={ntc_raw}") + parts.append(f"temp={float(temp):.1f}C") return " printer={" + " ".join(parts) + "}" if parts else "" def summarize_result(result: Dict[str, Any]) -> str: status = result.get("status", "-") rc = result.get("rc", "-") - duration = result.get("duration_ms", "-") message = result.get("message", "") suffix = f" msg={message}" if message else "" - return f"status={status} rc={rc} duration_ms={duration}{suffix}{_printer_status_summary(result)}" + if status == "ok" and rc == 0 and not suffix: + return f"ok{_printer_status_summary(result)}" + return f"status={status} rc={rc}{suffix}{_printer_status_summary(result)}" def run_steps( @@ -357,19 +356,18 @@ def run_steps( cmd = str(step["cmd"]) payload = step.get("payload") or {} timeout = float(step.get("timeout", client.timeout)) - print(f"[{index}] {cmd}") try: result = client.command(cmd, payload=payload, timeout_s=timeout) except ProtocolError as exc: failures += 1 - print(f" FAIL {exc}") + print(f"[{index}] {cmd}: FAIL {exc}") if not continue_on_fail: break continue status = result.get("status") ok = status == "ok" - print(f" {'OK' if ok else 'FAIL'} {summarize_result(result)}") + print(f"[{index}] {cmd}: {'OK' if ok else 'FAIL'} {summarize_result(result)}") if not ok: failures += 1 if not continue_on_fail: @@ -388,6 +386,7 @@ def parse_args(argv: List[str]) -> argparse.Namespace: parser.add_argument("--reset-delay", type=float, default=2.0, help="硬复位后等待设备启动的秒数") parser.add_argument("--jsonl", type=Path, help="保存收发帧 JSONL") parser.add_argument("--echo-logs", action="store_true", help="打印非协议串口日志") + parser.add_argument("--verbose", action="store_true", help="显示每条命令的中间阶段") parser.add_argument("--continue-on-fail", action="store_true", help="失败后继续执行后续步骤") parser.add_argument("--plan", type=Path, help="从 JSON 测试计划读取步骤") @@ -424,17 +423,18 @@ def main(argv: List[str]) -> int: baud=args.baud, timeout=args.timeout, echo_logs=args.echo_logs, + verbose=args.verbose, jsonl_path=args.jsonl, ) as client: if args.reset: - print(f"复位设备: {args.port}") + if args.verbose: + print(f"复位设备: {args.port}") client.hard_reset(args.reset_delay) - print(f"等待设备协议就绪: {args.port} @ {args.baud}") ready = client.wait_ready(args.ready_timeout) - print( - "READY " - + json.dumps(ready, ensure_ascii=False, separators=(",", ":")) - ) + if args.verbose: + print("READY " + json.dumps(ready, ensure_ascii=False, separators=(",", ":"))) + else: + print("设备就绪") failures = run_steps(client, steps, args.continue_on_fail) if failures: