fix(screen): streamline runtime display states

This commit is contained in:
admin
2026-04-30 10:07:25 +08:00
parent 7e77c6809a
commit d83180975d
9 changed files with 841 additions and 154 deletions

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -1,12 +1,39 @@
#include "domain.h"
#include "platform.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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);
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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");
}

View File

@@ -1,6 +1,5 @@
#include "voice_interaction_internal.h"
#include <ctype.h>
#include <stdio.h>
#include <string.h>
@@ -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);

View File

@@ -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: