fix(screen): streamline runtime display states
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user