From 7965818d13e0eff560a76e8a25574f4d3d86a0dd Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 25 Feb 2026 13:46:44 +0800 Subject: [PATCH] refactor(control_plane,domain): split oversized print and protocol modules --- main/CMakeLists.txt | 4 + .../internal/rest_server_internal.h | 8 + main/control_plane/src/rest_server_print.c | 1116 +---------------- .../src/rest_server_print_image.c | 582 +++++++++ .../src/rest_server_print_render.c | 524 ++++++++ .../internal/printer_protocol_internal.h | 73 ++ main/domain/src/printer_protocol.c | 716 +---------- main/domain/src/printer_protocol_jobs.c | 323 +++++ main/domain/src/printer_protocol_worker.c | 322 +++++ 9 files changed, 1886 insertions(+), 1782 deletions(-) create mode 100644 main/control_plane/src/rest_server_print_image.c create mode 100644 main/control_plane/src/rest_server_print_render.c create mode 100644 main/domain/src/printer_protocol_jobs.c create mode 100644 main/domain/src/printer_protocol_worker.c diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 0817a3b..c534cba 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -6,9 +6,13 @@ idf_component_register( "control_plane/src/rest_server_common.c" "control_plane/src/rest_server_ops.c" "control_plane/src/rest_server_print.c" + "control_plane/src/rest_server_print_image.c" + "control_plane/src/rest_server_print_render.c" "control_plane/src/rest_server_jobs.c" "control_plane/src/rest_server_voice.c" "domain/src/printer_protocol.c" + "domain/src/printer_protocol_jobs.c" + "domain/src/printer_protocol_worker.c" "domain/src/printer_protocol_commands.c" "domain/src/raster_tools.c" "domain/src/raster_tools_image_qr.c" diff --git a/main/control_plane/internal/rest_server_internal.h b/main/control_plane/internal/rest_server_internal.h index 76c876e..a09c191 100644 --- a/main/control_plane/internal/rest_server_internal.h +++ b/main/control_plane/internal/rest_server_internal.h @@ -16,6 +16,14 @@ bool rest_server_parse_uri_u32_tail(const char *uri, uint32_t *out_value); esp_err_t rest_server_base64_decode_alloc(const char *b64, uint8_t **out_raw, size_t *out_len); void rest_server_appendf(char *dst, size_t cap, size_t *offset, const char *fmt, ...); bool rest_server_json_bool_with_default(cJSON *item, bool default_value); +esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req, + const uint8_t *raw, + size_t raw_len, + uint16_t width, + uint16_t height, + const char *density, + const char *warning, + const char *trace_id); esp_err_t rest_server_health_get(httpd_req_t *req); esp_err_t rest_server_connect_post(httpd_req_t *req); diff --git a/main/control_plane/src/rest_server_print.c b/main/control_plane/src/rest_server_print.c index dcba339..d241dbc 100644 --- a/main/control_plane/src/rest_server_print.c +++ b/main/control_plane/src/rest_server_print.c @@ -1,14 +1,10 @@ #include "rest_server_internal.h" -#include #include #include -#include #include "esp_log.h" -#include "image_generation.h" #include "printer_protocol.h" -#include "raster_tools.h" static const char *TAG = "rest_print"; @@ -16,14 +12,14 @@ static const char *trace_id_or_default(const char *trace_id) { return (trace_id != NULL && trace_id[0] != '\0') ? trace_id : "-"; } -static esp_err_t submit_raster_job_and_reply(httpd_req_t *req, - const uint8_t *raw, - size_t raw_len, - uint16_t width, - uint16_t height, - const char *density, - const char *warning, - const char *trace_id) { +esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req, + const uint8_t *raw, + size_t raw_len, + uint16_t width, + uint16_t height, + const char *density, + const char *warning, + const char *trace_id) { const char *trace = trace_id_or_default(trace_id); ESP_LOGI(TAG, "print submit start, trace=%s, raster_bytes=%u, size=%ux%u, density=%s", @@ -50,8 +46,8 @@ static esp_err_t submit_raster_job_and_reply(httpd_req_t *req, (unsigned)submit_rc, submit_err[0] != '\0' ? submit_err : "submit failed"); return rest_server_send_error(req, - "400 Bad Request", - submit_err[0] != '\0' ? submit_err : "submit failed"); + "400 Bad Request", + submit_err[0] != '\0' ? submit_err : "submit failed"); } ESP_LOGI(TAG, "print submit done, trace=%s, job_id=%u", trace, (unsigned)job_id); @@ -69,434 +65,6 @@ static esp_err_t submit_raster_job_and_reply(httpd_req_t *req, return err; } -static bool parse_bool_text(const char *text, bool *out_value) { - if (text == NULL || out_value == NULL) { - return false; - } - if (strcmp(text, "1") == 0 || strcasecmp(text, "true") == 0 || strcasecmp(text, "yes") == 0) { - *out_value = true; - return true; - } - if (strcmp(text, "0") == 0 || strcasecmp(text, "false") == 0 || strcasecmp(text, "no") == 0) { - *out_value = false; - return true; - } - return false; -} - -static const char *map_density_text(const char *text) { - if (text == NULL || text[0] == '\0') { - return "中等"; - } - if (strcasecmp(text, "light") == 0 || strcmp(text, "较淡") == 0) { - return "较淡"; - } - if (strcasecmp(text, "medium") == 0 || strcmp(text, "中等") == 0) { - return "中等"; - } - if (strcasecmp(text, "dark") == 0 || strcmp(text, "较浓") == 0) { - return "较浓"; - } - if (strcasecmp(text, "max") == 0 || strcmp(text, "最深") == 0) { - return "最深"; - } - return "中等"; -} - -static void parse_image_upload_options(httpd_req_t *req, - bool *out_scale_to_width, - uint8_t *out_threshold, - bool *out_invert, - uint16_t *out_max_height, - const char **out_density) { - if (out_scale_to_width != NULL) { - *out_scale_to_width = true; - } - if (out_threshold != NULL) { - *out_threshold = 160; - } - if (out_invert != NULL) { - *out_invert = false; - } - if (out_max_height != NULL) { - *out_max_height = 2200; - } - if (out_density != NULL) { - *out_density = "中等"; - } - - if (req == NULL) { - return; - } - - int qlen = httpd_req_get_url_query_len(req); - if (qlen <= 0 || qlen > 512) { - return; - } - - char *query = (char *)calloc(1, (size_t)qlen + 1); - if (query == NULL) { - return; - } - - if (httpd_req_get_url_query_str(req, query, (size_t)qlen + 1) != ESP_OK) { - free(query); - return; - } - - char value[48] = {0}; - if (httpd_query_key_value(query, "scale_to_width", value, sizeof(value)) == ESP_OK && out_scale_to_width != NULL) { - bool parsed = *out_scale_to_width; - if (parse_bool_text(value, &parsed)) { - *out_scale_to_width = parsed; - } - } - - if (httpd_query_key_value(query, "invert", value, sizeof(value)) == ESP_OK && out_invert != NULL) { - bool parsed = *out_invert; - if (parse_bool_text(value, &parsed)) { - *out_invert = parsed; - } - } - - if (httpd_query_key_value(query, "threshold", value, sizeof(value)) == ESP_OK && out_threshold != NULL) { - long v = strtol(value, NULL, 10); - if (v >= 0 && v <= 255) { - *out_threshold = (uint8_t)v; - } - } - - if (httpd_query_key_value(query, "max_height", value, sizeof(value)) == ESP_OK && out_max_height != NULL) { - long v = strtol(value, NULL, 10); - if (v >= 64 && v <= 3000) { - *out_max_height = (uint16_t)v; - } - } - - if (httpd_query_key_value(query, "density", value, sizeof(value)) == ESP_OK && out_density != NULL) { - *out_density = map_density_text(value); - } - - free(query); -} - -typedef struct { - bool scale_to_width; - uint8_t threshold; - bool invert; - uint16_t max_height; - const char *density; -} image_print_options_t; - -typedef struct { - char prompt[801]; - char size[24]; - bool has_size; - bool prompt_extend; - bool has_seed; - uint32_t seed; - uint32_t timeout_ms; - uint32_t fetch_timeout_ms; -} image_generate_options_t; - -static void image_print_options_set_defaults(image_print_options_t *out) { - if (out == NULL) { - return; - } - out->scale_to_width = true; - out->threshold = 160; - out->invert = false; - out->max_height = 2200; - out->density = "中等"; -} - -static void parse_image_print_options_from_json(cJSON *json, image_print_options_t *out) { - if (json == NULL || out == NULL) { - return; - } - - cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width"); - cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold"); - cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert"); - cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); - cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); - - out->scale_to_width = rest_server_json_bool_with_default(jscale, out->scale_to_width); - out->invert = rest_server_json_bool_with_default(jinvert, out->invert); - if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) { - out->threshold = (uint8_t)jthreshold->valuedouble; - } - if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { - out->max_height = (uint16_t)jmaxh->valuedouble; - } - if (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) { - out->density = map_density_text(jdensity->valuestring); - } -} - -static bool parse_image_size_text(const char *text, uint32_t *out_w, uint32_t *out_h) { - if (text == NULL || out_w == NULL || out_h == NULL) { - return false; - } - - const char *sep = strchr(text, '*'); - if (sep == NULL || sep == text || *(sep + 1) == '\0') { - return false; - } - - char wbuf[12] = {0}; - char hbuf[12] = {0}; - size_t wlen = (size_t)(sep - text); - size_t hlen = strlen(sep + 1); - if (wlen == 0 || hlen == 0 || wlen >= sizeof(wbuf) || hlen >= sizeof(hbuf)) { - return false; - } - - memcpy(wbuf, text, wlen); - memcpy(hbuf, sep + 1, hlen); - wbuf[wlen] = '\0'; - hbuf[hlen] = '\0'; - - char *wend = NULL; - char *hend = NULL; - long w = strtol(wbuf, &wend, 10); - long h = strtol(hbuf, &hend, 10); - if (wend == wbuf || hend == hbuf || *wend != '\0' || *hend != '\0') { - return false; - } - if (w <= 0 || h <= 0) { - return false; - } - - *out_w = (uint32_t)w; - *out_h = (uint32_t)h; - return true; -} - -static bool parse_image_generation_options(cJSON *json, - image_generate_options_t *out, - char *err, - size_t err_len) { - if (json == NULL || out == NULL) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid args"); - } - return false; - } - - memset(out, 0, sizeof(*out)); - out->timeout_ms = CONFIG_TQ_Z_IMAGE_TIMEOUT_MS; - out->fetch_timeout_ms = CONFIG_TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS; - - cJSON *jprompt = cJSON_GetObjectItemCaseSensitive(json, "prompt"); - if (!cJSON_IsString(jprompt) || jprompt->valuestring == NULL || jprompt->valuestring[0] == '\0') { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "prompt is required"); - } - return false; - } - - size_t prompt_len = strlen(jprompt->valuestring); - if (prompt_len > 800) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "prompt too long, max 800 chars"); - } - return false; - } - strlcpy(out->prompt, jprompt->valuestring, sizeof(out->prompt)); - - cJSON *jsize = cJSON_GetObjectItemCaseSensitive(json, "size"); - if (cJSON_IsString(jsize) && jsize->valuestring != NULL && jsize->valuestring[0] != '\0') { - uint32_t w = 0; - uint32_t h = 0; - if (!parse_image_size_text(jsize->valuestring, &w, &h)) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "size must be like 1120*1440"); - } - return false; - } - - uint64_t pixels = (uint64_t)w * (uint64_t)h; - if (pixels < (512ULL * 512ULL) || pixels > (2048ULL * 2048ULL)) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "size pixels must be in [512*512, 2048*2048]"); - } - return false; - } - - out->has_size = true; - strlcpy(out->size, jsize->valuestring, sizeof(out->size)); - } - - cJSON *jextend = cJSON_GetObjectItemCaseSensitive(json, "prompt_extend"); - out->prompt_extend = rest_server_json_bool_with_default(jextend, false); - - cJSON *jseed = cJSON_GetObjectItemCaseSensitive(json, "seed"); - if (cJSON_IsNumber(jseed)) { - if (jseed->valuedouble < 0 || jseed->valuedouble > 2147483647.0) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "seed must be in [0, 2147483647]"); - } - return false; - } - out->has_seed = true; - out->seed = (uint32_t)jseed->valuedouble; - } - - cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms"); - if (cJSON_IsNumber(jtimeout)) { - if (jtimeout->valuedouble < 5000 || jtimeout->valuedouble > 180000) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "timeout_ms must be 5000..180000"); - } - return false; - } - out->timeout_ms = (uint32_t)jtimeout->valuedouble; - } - - cJSON *jfetch_timeout = cJSON_GetObjectItemCaseSensitive(json, "fetch_timeout_ms"); - if (cJSON_IsNumber(jfetch_timeout)) { - if (jfetch_timeout->valuedouble < 2000 || jfetch_timeout->valuedouble > 120000) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "fetch_timeout_ms must be 2000..120000"); - } - return false; - } - out->fetch_timeout_ms = (uint32_t)jfetch_timeout->valuedouble; - } - - return true; -} - -static esp_err_t decode_image_json_to_raster(cJSON *json, - uint16_t *out_width, - uint16_t *out_height, - uint8_t **out_raster, - size_t *out_len, - char *err, - size_t err_len) { - if (json == NULL || out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid args"); - } - return ESP_ERR_INVALID_ARG; - } - - *out_width = 0; - *out_height = 0; - *out_raster = NULL; - *out_len = 0; - - cJSON *jwidth = cJSON_GetObjectItemCaseSensitive(json, "width"); - cJSON *jheight = cJSON_GetObjectItemCaseSensitive(json, "height"); - cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding"); - cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data"); - - if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) || - !cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "missing required image fields"); - } - return ESP_ERR_INVALID_ARG; - } - - uint16_t width = (uint16_t)jwidth->valuedouble; - uint16_t height = (uint16_t)jheight->valuedouble; - if (width == 0 || height == 0) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid image size"); - } - return ESP_ERR_INVALID_ARG; - } - - if (strcmp(jencoding->valuestring, "base64_msb_1bpp") == 0) { - uint8_t *raw = NULL; - size_t raw_len = 0; - esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &raw, &raw_len); - if (b64_rc != ESP_OK) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid base64"); - } - return ESP_ERR_INVALID_ARG; - } - *out_width = width; - *out_height = height; - *out_raster = raw; - *out_len = raw_len; - return ESP_OK; - } - - if (strcmp(jencoding->valuestring, "base64_gray8") == 0) { - uint8_t *gray = NULL; - size_t gray_len = 0; - esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &gray, &gray_len); - if (b64_rc != ESP_OK) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid base64"); - } - return ESP_ERR_INVALID_ARG; - } - - size_t expected = (size_t)width * height; - if (gray_len != expected) { - free(gray); - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "gray8 size mismatch"); - } - return ESP_ERR_INVALID_SIZE; - } - - cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width"); - cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold"); - cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert"); - cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); - - bool scale_to_width = rest_server_json_bool_with_default(jscale, true); - bool invert = rest_server_json_bool_with_default(jinvert, false); - uint8_t threshold = 160; - uint16_t max_height = 2200; - if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) { - threshold = (uint8_t)jthreshold->valuedouble; - } - if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { - max_height = (uint16_t)jmaxh->valuedouble; - } - - uint16_t out_w = 0; - uint16_t out_h = 0; - uint8_t *raster = NULL; - size_t raster_len = 0; - esp_err_t conv_rc = raster_tools_convert_gray8_to_raster_384(gray, - width, - height, - scale_to_width, - threshold, - invert, - max_height, - &out_w, - &out_h, - &raster, - &raster_len, - err, - err_len); - free(gray); - if (conv_rc != ESP_OK) { - return conv_rc; - } - - *out_width = out_w; - *out_height = out_h; - *out_raster = raster; - *out_len = raster_len; - return ESP_OK; - } - - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "unsupported encoding"); - } - return ESP_ERR_NOT_SUPPORTED; -} - esp_err_t rest_server_print_raster_post(httpd_req_t *req) { if (!rest_server_auth_ok(req)) { return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); @@ -543,663 +111,15 @@ esp_err_t rest_server_print_raster_post(httpd_req_t *req) { return rest_server_send_error(req, "400 Bad Request", "invalid base64"); } - esp_err_t submit_rc = submit_raster_job_and_reply(req, - raw, - raw_len, - (uint16_t)jwidth->valuedouble, - (uint16_t)jheight->valuedouble, - density, - NULL, - NULL); + esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, + raw, + raw_len, + (uint16_t)jwidth->valuedouble, + (uint16_t)jheight->valuedouble, + density, + NULL, + NULL); free(raw); cJSON_Delete(json); return submit_rc; } - -static esp_err_t rest_server_send_image_generation_error(httpd_req_t *req, - esp_err_t rc, - const char *msg) { - const char *message = (msg != NULL && msg[0] != '\0') ? msg : "image generation failed"; - if (rc == ESP_ERR_INVALID_ARG) { - return rest_server_send_error(req, "400 Bad Request", message); - } - if (rc == ESP_ERR_INVALID_STATE) { - return rest_server_send_error(req, "503 Service Unavailable", message); - } - if (rc == ESP_ERR_TIMEOUT) { - return rest_server_send_error(req, "504 Gateway Timeout", message); - } - if (rc == ESP_ERR_NO_MEM) { - return rest_server_send_error(req, "500 Internal Server Error", message); - } - return rest_server_send_error(req, "502 Bad Gateway", message); -} - -static esp_err_t rest_server_print_image_binary(httpd_req_t *req) { - bool scale_to_width = true; - uint8_t threshold = 160; - bool invert = false; - uint16_t max_height = 2200; - const char *density = "中等"; - parse_image_upload_options(req, - &scale_to_width, - &threshold, - &invert, - &max_height, - &density); - - ESP_LOGI(TAG, - "image process start (upload), png_bytes=%u, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s", - (unsigned)req->content_len, - scale_to_width, - (unsigned)threshold, - invert, - (unsigned)max_height, - density != NULL ? density : "中等"); - - char *body = NULL; - esp_err_t body_err = rest_server_read_body(req, &body); - if (body_err != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", "invalid request body"); - } - - uint16_t width = 0; - uint16_t height = 0; - uint8_t *raster = NULL; - size_t raster_len = 0; - char decode_err[128] = {0}; - esp_err_t decode_rc = raster_tools_convert_png_to_raster_384((const uint8_t *)body, - (size_t)req->content_len, - scale_to_width, - threshold, - invert, - max_height, - &width, - &height, - &raster, - &raster_len, - decode_err, - sizeof(decode_err)); - free(body); - if (decode_rc != ESP_OK) { - ESP_LOGW(TAG, - "image process failed (upload), rc=0x%x, msg=%s", - (unsigned)decode_rc, - decode_err[0] != '\0' ? decode_err : "decode png failed"); - return rest_server_send_error(req, - "400 Bad Request", - decode_err[0] != '\0' ? decode_err : "decode png failed"); - } - - ESP_LOGI(TAG, - "image process done (upload), raster_size=%ux%u, raster_bytes=%u", - (unsigned)width, - (unsigned)height, - (unsigned)raster_len); - - esp_err_t submit_rc = submit_raster_job_and_reply(req, - raster, - raster_len, - width, - height, - density, - NULL, - NULL); - free(raster); - return submit_rc; -} - -static esp_err_t rest_server_print_image_generate(httpd_req_t *req) { - char *body = NULL; - cJSON *json = NULL; - image_generation_result_t *gen_result = NULL; - uint8_t *raster = NULL; - char request_id[80] = {0}; - esp_err_t ret = ESP_FAIL; - - esp_err_t body_err = rest_server_read_body(req, &body); - if (body_err != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", "invalid request body"); - } - - json = cJSON_Parse(body); - free(body); - body = NULL; - if (json == NULL) { - return rest_server_send_error(req, "400 Bad Request", "invalid json"); - } - - image_generate_options_t gen_opt = {0}; - char parse_err[128] = {0}; - if (!parse_image_generation_options(json, &gen_opt, parse_err, sizeof(parse_err))) { - ret = rest_server_send_error(req, - "400 Bad Request", - parse_err[0] != '\0' ? parse_err : "invalid image options"); - goto cleanup; - } - - image_print_options_t print_opt = {0}; - image_print_options_set_defaults(&print_opt); - parse_image_print_options_from_json(json, &print_opt); - cJSON_Delete(json); - json = NULL; - - ESP_LOGI(TAG, - "image generate start, prompt_len=%u, size=%s, prompt_extend=%d, seed=%s, timeout_ms=%u, fetch_timeout_ms=%u", - (unsigned)strlen(gen_opt.prompt), - gen_opt.has_size ? gen_opt.size : "default", - gen_opt.prompt_extend, - gen_opt.has_seed ? "set" : "auto", - (unsigned)gen_opt.timeout_ms, - (unsigned)gen_opt.fetch_timeout_ms); - ESP_LOGI(TAG, - "image process config, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s", - print_opt.scale_to_width, - (unsigned)print_opt.threshold, - print_opt.invert, - (unsigned)print_opt.max_height, - print_opt.density != NULL ? print_opt.density : "中等"); - - image_generation_request_t gen_req = { - .prompt = gen_opt.prompt, - .size = gen_opt.has_size ? gen_opt.size : NULL, - .prompt_extend = gen_opt.prompt_extend, - .has_seed = gen_opt.has_seed, - .seed = gen_opt.seed, - .generation_timeout_ms = gen_opt.timeout_ms, - .download_timeout_ms = gen_opt.fetch_timeout_ms, - }; - - gen_result = (image_generation_result_t *)calloc(1, sizeof(*gen_result)); - if (gen_result == NULL) { - ret = rest_server_send_error(req, "500 Internal Server Error", "no memory"); - goto cleanup; - } - image_generation_result_reset(gen_result); - - char model_err[160] = {0}; - esp_err_t gen_rc = image_generation_generate_png(&gen_req, - gen_result, - model_err, - sizeof(model_err)); - if (gen_rc != ESP_OK) { - ESP_LOGW(TAG, - "image generate failed, rc=0x%x, msg=%s", - (unsigned)gen_rc, - model_err[0] != '\0' ? model_err : "image generation failed"); - ret = rest_server_send_image_generation_error(req, gen_rc, model_err); - goto cleanup; - } - - if (gen_result->request_id[0] != '\0') { - strlcpy(request_id, gen_result->request_id, sizeof(request_id)); - } - - ESP_LOGI(TAG, - "image generate done, request_id=%s, model_size=%ux%u, png_bytes=%u", - request_id[0] != '\0' ? request_id : "-", - (unsigned)gen_result->width, - (unsigned)gen_result->height, - (unsigned)gen_result->png_len); - - uint16_t width = 0; - uint16_t height = 0; - size_t raster_len = 0; - char decode_err[128] = {0}; - esp_err_t decode_rc = raster_tools_convert_png_to_raster_384(gen_result->png, - gen_result->png_len, - print_opt.scale_to_width, - print_opt.threshold, - print_opt.invert, - print_opt.max_height, - &width, - &height, - &raster, - &raster_len, - decode_err, - sizeof(decode_err)); - if (decode_rc != ESP_OK) { - ESP_LOGW(TAG, - "image process failed (generated), rc=0x%x, msg=%s", - (unsigned)decode_rc, - decode_err[0] != '\0' ? decode_err : "decode generated image failed"); - ret = rest_server_send_error(req, - "400 Bad Request", - decode_err[0] != '\0' ? decode_err : "decode generated image failed"); - goto cleanup; - } - - ESP_LOGI(TAG, - "image process done (generated), raster_size=%ux%u, raster_bytes=%u", - (unsigned)width, - (unsigned)height, - (unsigned)raster_len); - - char warning[160] = {0}; - if (request_id[0] != '\0') { - snprintf(warning, sizeof(warning), "z-image request_id=%s", request_id); - } - - ret = submit_raster_job_and_reply(req, - raster, - raster_len, - width, - height, - print_opt.density, - warning, - request_id); - -cleanup: - if (body != NULL) { - free(body); - } - if (json != NULL) { - cJSON_Delete(json); - } - if (raster != NULL) { - free(raster); - } - if (gen_result != NULL) { - image_generation_result_free(gen_result); - free(gen_result); - } - return ret; -} - -esp_err_t rest_server_print_image_post(httpd_req_t *req) { - if (!rest_server_auth_ok(req)) { - return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); - } - - char content_type[96] = {0}; - if (httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)) == ESP_OK) { - if (strstr(content_type, "application/json") != NULL) { - return rest_server_print_image_generate(req); - } - if (strstr(content_type, "image/png") == NULL) { - return rest_server_send_error(req, "415 Unsupported Media Type", "Content-Type must be application/json or image/png"); - } - } - - return rest_server_print_image_binary(req); -} - -esp_err_t rest_server_print_qr_post(httpd_req_t *req) { - if (!rest_server_auth_ok(req)) { - return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); - } - - char *body = NULL; - esp_err_t body_err = rest_server_read_body(req, &body); - if (body_err != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", "invalid request body"); - } - - cJSON *json = cJSON_Parse(body); - free(body); - if (json == NULL) { - return rest_server_send_error(req, "400 Bad Request", "invalid json"); - } - - cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text"); - cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); - cJSON *jecc = cJSON_GetObjectItemCaseSensitive(json, "ecc"); - cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "module_scale"); - cJSON *jmargin = cJSON_GetObjectItemCaseSensitive(json, "margin_modules"); - cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); - - if (!cJSON_IsString(jtext) || jtext->valuestring == NULL || jtext->valuestring[0] == '\0') { - cJSON_Delete(json); - return rest_server_send_error(req, "400 Bad Request", "text is required"); - } - - const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) - ? jdensity->valuestring - : "中等"; - - uint8_t ecc_level = 3; /* default H */ - if (cJSON_IsString(jecc) && jecc->valuestring != NULL) { - if (strcasecmp(jecc->valuestring, "L") == 0) { - ecc_level = 0; - } else if (strcasecmp(jecc->valuestring, "M") == 0) { - ecc_level = 1; - } else if (strcasecmp(jecc->valuestring, "Q") == 0) { - ecc_level = 2; - } else if (strcasecmp(jecc->valuestring, "H") == 0) { - ecc_level = 3; - } - } - - uint8_t module_scale = 0; - uint8_t margin_modules = 2; - uint16_t max_height = 2200; - if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 0 && jscale->valuedouble <= 16) { - module_scale = (uint8_t)jscale->valuedouble; - } - if (cJSON_IsNumber(jmargin) && jmargin->valuedouble >= 1 && jmargin->valuedouble <= 12) { - margin_modules = (uint8_t)jmargin->valuedouble; - } - if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { - max_height = (uint16_t)jmaxh->valuedouble; - } - - uint16_t width = 0; - uint16_t height = 0; - uint8_t *raster = NULL; - size_t raster_len = 0; - char render_msg[128] = {0}; - esp_err_t qr_rc = raster_tools_render_qr_384(jtext->valuestring, - module_scale, - margin_modules, - ecc_level, - max_height, - &width, - &height, - &raster, - &raster_len, - render_msg, - sizeof(render_msg)); - cJSON_Delete(json); - if (qr_rc != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", render_msg[0] != '\0' ? render_msg : "qr render failed"); - } - - esp_err_t submit_rc = submit_raster_job_and_reply(req, - raster, - raster_len, - width, - height, - density, - NULL, - NULL); - free(raster); - return submit_rc; -} - -esp_err_t rest_server_print_label_post(httpd_req_t *req) { - if (!rest_server_auth_ok(req)) { - return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); - } - - char *body = NULL; - esp_err_t body_err = rest_server_read_body(req, &body); - if (body_err != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", "invalid request body"); - } - - cJSON *json = cJSON_Parse(body); - free(body); - if (json == NULL) { - return rest_server_send_error(req, "400 Bad Request", "invalid json"); - } - - cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); - cJSON *jgap = cJSON_GetObjectItemCaseSensitive(json, "gap_move_before"); - cJSON *joffset = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm"); - - bool gap_move_before = rest_server_json_bool_with_default(jgap, true); - bool has_offset = cJSON_IsNumber(joffset); - uint8_t offset_value = 0; - if (has_offset) { - if (joffset->valuedouble < 0 || joffset->valuedouble > 254) { - cJSON_Delete(json); - return rest_server_send_error(req, "400 Bad Request", "offset_tenths_mm must be 0..254"); - } - offset_value = (uint8_t)joffset->valuedouble; - } - - if (has_offset) { - char cmd_err[128] = {0}; - esp_err_t offset_rc = printer_protocol_set_label_offset(offset_value, 3000, cmd_err, sizeof(cmd_err)); - if (offset_rc != ESP_OK) { - cJSON_Delete(json); - return rest_server_send_error(req, - "409 Conflict", - cmd_err[0] != '\0' ? cmd_err : "set label offset failed"); - } - } - - if (gap_move_before) { - char cmd_err[128] = {0}; - esp_err_t gap_rc = printer_protocol_gap_move(5000, cmd_err, sizeof(cmd_err)); - if (gap_rc != ESP_OK) { - cJSON_Delete(json); - return rest_server_send_error(req, - "409 Conflict", - cmd_err[0] != '\0' ? cmd_err : "gap move failed"); - } - } - - const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) - ? jdensity->valuestring - : "中等"; - - uint16_t width = 0; - uint16_t height = 0; - uint8_t *raster = NULL; - size_t raster_len = 0; - char decode_err[128] = {0}; - esp_err_t decode_rc = decode_image_json_to_raster(json, - &width, - &height, - &raster, - &raster_len, - decode_err, - sizeof(decode_err)); - cJSON_Delete(json); - if (decode_rc != ESP_OK) { - return rest_server_send_error(req, - "400 Bad Request", - decode_err[0] != '\0' ? decode_err : "decode image failed"); - } - - esp_err_t submit_rc = submit_raster_job_and_reply(req, - raster, - raster_len, - width, - height, - density, - NULL, - NULL); - free(raster); - return submit_rc; -} - -esp_err_t rest_server_print_text_post(httpd_req_t *req) { - if (!rest_server_auth_ok(req)) { - return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); - } - - char *body = NULL; - esp_err_t body_err = rest_server_read_body(req, &body); - if (body_err != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", "invalid request body"); - } - - cJSON *json = cJSON_Parse(body); - free(body); - if (json == NULL) { - return rest_server_send_error(req, "400 Bad Request", "invalid json"); - } - - cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text"); - cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); - cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); - cJSON *jline = cJSON_GetObjectItemCaseSensitive(json, "line_spacing"); - cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); - - if (!cJSON_IsString(jtext) || jtext->valuestring == NULL) { - cJSON_Delete(json); - return rest_server_send_error(req, "400 Bad Request", "text is required"); - } - - const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) - ? jdensity->valuestring - : "中等"; - uint8_t scale = 2; - uint8_t line_spacing = 2; - uint16_t max_height = 2000; - - if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 6) { - scale = (uint8_t)jscale->valuedouble; - } - if (cJSON_IsNumber(jline) && jline->valuedouble >= 0 && jline->valuedouble <= 12) { - line_spacing = (uint8_t)jline->valuedouble; - } - if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { - max_height = (uint16_t)jmaxh->valuedouble; - } - - uint16_t width = 0; - uint16_t height = 0; - uint8_t *raster = NULL; - size_t raster_len = 0; - char render_msg[128] = {0}; - - esp_err_t render_rc = raster_tools_render_text_384(jtext->valuestring, - scale, - line_spacing, - max_height, - &width, - &height, - &raster, - &raster_len, - render_msg, - sizeof(render_msg)); - cJSON_Delete(json); - - if (render_rc != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", render_msg[0] != '\0' ? render_msg : "text render failed"); - } - - esp_err_t submit_rc = submit_raster_job_and_reply(req, - raster, - raster_len, - width, - height, - density, - render_msg, - NULL); - free(raster); - return submit_rc; -} - -esp_err_t rest_server_print_receipt_post(httpd_req_t *req) { - if (!rest_server_auth_ok(req)) { - return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); - } - - char *body = NULL; - esp_err_t body_err = rest_server_read_body(req, &body); - if (body_err != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", "invalid request body"); - } - - cJSON *json = cJSON_Parse(body); - free(body); - if (json == NULL) { - return rest_server_send_error(req, "400 Bad Request", "invalid json"); - } - - cJSON *jtitle = cJSON_GetObjectItemCaseSensitive(json, "title"); - cJSON *jitems = cJSON_GetObjectItemCaseSensitive(json, "items"); - cJSON *jfooter = cJSON_GetObjectItemCaseSensitive(json, "footer"); - cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); - cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); - - const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL) - ? jtitle->valuestring - : "TQ RECEIPT"; - const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL) - ? jfooter->valuestring - : "Thank you"; - const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) - ? jdensity->valuestring - : "中等"; - uint8_t scale = 2; - if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 4) { - scale = (uint8_t)jscale->valuedouble; - } - - size_t text_cap = 8192; - char *text = (char *)calloc(1, text_cap); - if (text == NULL) { - cJSON_Delete(json); - return rest_server_send_error(req, "500 Internal Server Error", "no memory"); - } - - size_t off = 0; - rest_server_appendf(text, text_cap, &off, "%s\n", title); - rest_server_appendf(text, text_cap, &off, "--------------------------------\n"); - - double total = 0.0; - if (cJSON_IsArray(jitems)) { - int n = cJSON_GetArraySize(jitems); - for (int i = 0; i < n; ++i) { - cJSON *it = cJSON_GetArrayItem(jitems, i); - if (!cJSON_IsObject(it)) { - continue; - } - - cJSON *jname = cJSON_GetObjectItemCaseSensitive(it, "name"); - cJSON *jqty = cJSON_GetObjectItemCaseSensitive(it, "qty"); - cJSON *jprice = cJSON_GetObjectItemCaseSensitive(it, "price"); - - const char *name = (cJSON_IsString(jname) && jname->valuestring != NULL) - ? jname->valuestring - : "item"; - int qty = cJSON_IsNumber(jqty) ? (int)jqty->valuedouble : 1; - double price = cJSON_IsNumber(jprice) ? jprice->valuedouble : 0.0; - double line_total = qty * price; - total += line_total; - - rest_server_appendf(text, - text_cap, - &off, - "%-16.16s x%-3d %8.2f\n", - name, - qty, - line_total); - } - } else { - rest_server_appendf(text, text_cap, &off, "(no items)\n"); - } - - rest_server_appendf(text, text_cap, &off, "--------------------------------\n"); - rest_server_appendf(text, text_cap, &off, "TOTAL: %.2f\n", total); - rest_server_appendf(text, text_cap, &off, "%s\n", footer); - - uint16_t width = 0; - uint16_t height = 0; - uint8_t *raster = NULL; - size_t raster_len = 0; - char render_msg[128] = {0}; - - esp_err_t render_rc = raster_tools_render_text_384(text, - scale, - 2, - 2200, - &width, - &height, - &raster, - &raster_len, - render_msg, - sizeof(render_msg)); - free(text); - cJSON_Delete(json); - - if (render_rc != ESP_OK) { - return rest_server_send_error(req, "400 Bad Request", render_msg[0] != '\0' ? render_msg : "receipt render failed"); - } - - esp_err_t submit_rc = submit_raster_job_and_reply(req, - raster, - raster_len, - width, - height, - density, - render_msg, - NULL); - free(raster); - return submit_rc; -} diff --git a/main/control_plane/src/rest_server_print_image.c b/main/control_plane/src/rest_server_print_image.c new file mode 100644 index 0000000..e785d9d --- /dev/null +++ b/main/control_plane/src/rest_server_print_image.c @@ -0,0 +1,582 @@ +#include "rest_server_internal.h" + +#include +#include +#include +#include + +#include "esp_log.h" +#include "image_generation.h" +#include "raster_tools.h" + +static const char *TAG = "rest_print"; + +typedef struct { + bool scale_to_width; + uint8_t threshold; + bool invert; + uint16_t max_height; + const char *density; +} image_print_options_t; + +typedef struct { + char prompt[801]; + char size[24]; + bool has_size; + bool prompt_extend; + bool has_seed; + uint32_t seed; + uint32_t timeout_ms; + uint32_t fetch_timeout_ms; +} image_generate_options_t; + +static bool parse_bool_text(const char *text, bool *out_value) { + if (text == NULL || out_value == NULL) { + return false; + } + if (strcmp(text, "1") == 0 || strcasecmp(text, "true") == 0 || strcasecmp(text, "yes") == 0) { + *out_value = true; + return true; + } + if (strcmp(text, "0") == 0 || strcasecmp(text, "false") == 0 || strcasecmp(text, "no") == 0) { + *out_value = false; + return true; + } + return false; +} + +static const char *map_density_text(const char *text) { + if (text == NULL || text[0] == '\0') { + return "中等"; + } + if (strcasecmp(text, "light") == 0 || strcmp(text, "较淡") == 0) { + return "较淡"; + } + if (strcasecmp(text, "medium") == 0 || strcmp(text, "中等") == 0) { + return "中等"; + } + if (strcasecmp(text, "dark") == 0 || strcmp(text, "较浓") == 0) { + return "较浓"; + } + if (strcasecmp(text, "max") == 0 || strcmp(text, "最深") == 0) { + return "最深"; + } + return "中等"; +} + +static void parse_image_upload_options(httpd_req_t *req, + bool *out_scale_to_width, + uint8_t *out_threshold, + bool *out_invert, + uint16_t *out_max_height, + const char **out_density) { + if (out_scale_to_width != NULL) { + *out_scale_to_width = true; + } + if (out_threshold != NULL) { + *out_threshold = 160; + } + if (out_invert != NULL) { + *out_invert = false; + } + if (out_max_height != NULL) { + *out_max_height = 2200; + } + if (out_density != NULL) { + *out_density = "中等"; + } + + if (req == NULL) { + return; + } + + int qlen = httpd_req_get_url_query_len(req); + if (qlen <= 0 || qlen > 512) { + return; + } + + char *query = (char *)calloc(1, (size_t)qlen + 1); + if (query == NULL) { + return; + } + + if (httpd_req_get_url_query_str(req, query, (size_t)qlen + 1) != ESP_OK) { + free(query); + return; + } + + char value[48] = {0}; + if (httpd_query_key_value(query, "scale_to_width", value, sizeof(value)) == ESP_OK && + out_scale_to_width != NULL) { + bool parsed = *out_scale_to_width; + if (parse_bool_text(value, &parsed)) { + *out_scale_to_width = parsed; + } + } + + if (httpd_query_key_value(query, "invert", value, sizeof(value)) == ESP_OK && out_invert != NULL) { + bool parsed = *out_invert; + if (parse_bool_text(value, &parsed)) { + *out_invert = parsed; + } + } + + if (httpd_query_key_value(query, "threshold", value, sizeof(value)) == ESP_OK && out_threshold != NULL) { + long v = strtol(value, NULL, 10); + if (v >= 0 && v <= 255) { + *out_threshold = (uint8_t)v; + } + } + + if (httpd_query_key_value(query, "max_height", value, sizeof(value)) == ESP_OK && out_max_height != NULL) { + long v = strtol(value, NULL, 10); + if (v >= 64 && v <= 3000) { + *out_max_height = (uint16_t)v; + } + } + + if (httpd_query_key_value(query, "density", value, sizeof(value)) == ESP_OK && out_density != NULL) { + *out_density = map_density_text(value); + } + + free(query); +} + +static void image_print_options_set_defaults(image_print_options_t *out) { + if (out == NULL) { + return; + } + out->scale_to_width = true; + out->threshold = 160; + out->invert = false; + out->max_height = 2200; + out->density = "中等"; +} + +static void parse_image_print_options_from_json(cJSON *json, image_print_options_t *out) { + if (json == NULL || out == NULL) { + return; + } + + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width"); + cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold"); + cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + + out->scale_to_width = rest_server_json_bool_with_default(jscale, out->scale_to_width); + out->invert = rest_server_json_bool_with_default(jinvert, out->invert); + if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) { + out->threshold = (uint8_t)jthreshold->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + out->max_height = (uint16_t)jmaxh->valuedouble; + } + if (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) { + out->density = map_density_text(jdensity->valuestring); + } +} + +static bool parse_image_size_text(const char *text, uint32_t *out_w, uint32_t *out_h) { + if (text == NULL || out_w == NULL || out_h == NULL) { + return false; + } + + const char *sep = strchr(text, '*'); + if (sep == NULL || sep == text || *(sep + 1) == '\0') { + return false; + } + + char wbuf[12] = {0}; + char hbuf[12] = {0}; + size_t wlen = (size_t)(sep - text); + size_t hlen = strlen(sep + 1); + if (wlen == 0 || hlen == 0 || wlen >= sizeof(wbuf) || hlen >= sizeof(hbuf)) { + return false; + } + + memcpy(wbuf, text, wlen); + memcpy(hbuf, sep + 1, hlen); + wbuf[wlen] = '\0'; + hbuf[hlen] = '\0'; + + char *wend = NULL; + char *hend = NULL; + long w = strtol(wbuf, &wend, 10); + long h = strtol(hbuf, &hend, 10); + if (wend == wbuf || hend == hbuf || *wend != '\0' || *hend != '\0') { + return false; + } + if (w <= 0 || h <= 0) { + return false; + } + + *out_w = (uint32_t)w; + *out_h = (uint32_t)h; + return true; +} + +static bool parse_image_generation_options(cJSON *json, + image_generate_options_t *out, + char *err, + size_t err_len) { + if (json == NULL || out == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return false; + } + + memset(out, 0, sizeof(*out)); + out->timeout_ms = CONFIG_TQ_Z_IMAGE_TIMEOUT_MS; + out->fetch_timeout_ms = CONFIG_TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS; + + cJSON *jprompt = cJSON_GetObjectItemCaseSensitive(json, "prompt"); + if (!cJSON_IsString(jprompt) || jprompt->valuestring == NULL || jprompt->valuestring[0] == '\0') { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "prompt is required"); + } + return false; + } + + size_t prompt_len = strlen(jprompt->valuestring); + if (prompt_len > 800) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "prompt too long, max 800 chars"); + } + return false; + } + strlcpy(out->prompt, jprompt->valuestring, sizeof(out->prompt)); + + cJSON *jsize = cJSON_GetObjectItemCaseSensitive(json, "size"); + if (cJSON_IsString(jsize) && jsize->valuestring != NULL && jsize->valuestring[0] != '\0') { + uint32_t w = 0; + uint32_t h = 0; + if (!parse_image_size_text(jsize->valuestring, &w, &h)) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "size must be like 1120*1440"); + } + return false; + } + + uint64_t pixels = (uint64_t)w * (uint64_t)h; + if (pixels < (512ULL * 512ULL) || pixels > (2048ULL * 2048ULL)) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "size pixels must be in [512*512, 2048*2048]"); + } + return false; + } + + out->has_size = true; + strlcpy(out->size, jsize->valuestring, sizeof(out->size)); + } + + cJSON *jextend = cJSON_GetObjectItemCaseSensitive(json, "prompt_extend"); + out->prompt_extend = rest_server_json_bool_with_default(jextend, false); + + cJSON *jseed = cJSON_GetObjectItemCaseSensitive(json, "seed"); + if (cJSON_IsNumber(jseed)) { + if (jseed->valuedouble < 0 || jseed->valuedouble > 2147483647.0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "seed must be in [0, 2147483647]"); + } + return false; + } + out->has_seed = true; + out->seed = (uint32_t)jseed->valuedouble; + } + + cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms"); + if (cJSON_IsNumber(jtimeout)) { + if (jtimeout->valuedouble < 5000 || jtimeout->valuedouble > 180000) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "timeout_ms must be 5000..180000"); + } + return false; + } + out->timeout_ms = (uint32_t)jtimeout->valuedouble; + } + + cJSON *jfetch_timeout = cJSON_GetObjectItemCaseSensitive(json, "fetch_timeout_ms"); + if (cJSON_IsNumber(jfetch_timeout)) { + if (jfetch_timeout->valuedouble < 2000 || jfetch_timeout->valuedouble > 120000) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "fetch_timeout_ms must be 2000..120000"); + } + return false; + } + out->fetch_timeout_ms = (uint32_t)jfetch_timeout->valuedouble; + } + + return true; +} + +static esp_err_t rest_server_send_image_generation_error(httpd_req_t *req, + esp_err_t rc, + const char *msg) { + const char *message = (msg != NULL && msg[0] != '\0') ? msg : "image generation failed"; + if (rc == ESP_ERR_INVALID_ARG) { + return rest_server_send_error(req, "400 Bad Request", message); + } + if (rc == ESP_ERR_INVALID_STATE) { + return rest_server_send_error(req, "503 Service Unavailable", message); + } + if (rc == ESP_ERR_TIMEOUT) { + return rest_server_send_error(req, "504 Gateway Timeout", message); + } + if (rc == ESP_ERR_NO_MEM) { + return rest_server_send_error(req, "500 Internal Server Error", message); + } + return rest_server_send_error(req, "502 Bad Gateway", message); +} + +static esp_err_t rest_server_print_image_binary(httpd_req_t *req) { + bool scale_to_width = true; + uint8_t threshold = 160; + bool invert = false; + uint16_t max_height = 2200; + const char *density = "中等"; + parse_image_upload_options(req, + &scale_to_width, + &threshold, + &invert, + &max_height, + &density); + + ESP_LOGI(TAG, + "image process start (upload), png_bytes=%u, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s", + (unsigned)req->content_len, + scale_to_width, + (unsigned)threshold, + invert, + (unsigned)max_height, + density != NULL ? density : "中等"); + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char decode_err[128] = {0}; + esp_err_t decode_rc = raster_tools_convert_png_to_raster_384((const uint8_t *)body, + (size_t)req->content_len, + scale_to_width, + threshold, + invert, + max_height, + &width, + &height, + &raster, + &raster_len, + decode_err, + sizeof(decode_err)); + free(body); + if (decode_rc != ESP_OK) { + ESP_LOGW(TAG, + "image process failed (upload), rc=0x%x, msg=%s", + (unsigned)decode_rc, + decode_err[0] != '\0' ? decode_err : "decode png failed"); + return rest_server_send_error(req, + "400 Bad Request", + decode_err[0] != '\0' ? decode_err : "decode png failed"); + } + + ESP_LOGI(TAG, + "image process done (upload), raster_size=%ux%u, raster_bytes=%u", + (unsigned)width, + (unsigned)height, + (unsigned)raster_len); + + esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + NULL, + NULL); + free(raster); + return submit_rc; +} + +static esp_err_t rest_server_print_image_generate(httpd_req_t *req) { + char *body = NULL; + cJSON *json = NULL; + image_generation_result_t *gen_result = NULL; + uint8_t *raster = NULL; + char request_id[80] = {0}; + esp_err_t ret = ESP_FAIL; + + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + json = cJSON_Parse(body); + free(body); + body = NULL; + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + image_generate_options_t gen_opt = {0}; + char parse_err[128] = {0}; + if (!parse_image_generation_options(json, &gen_opt, parse_err, sizeof(parse_err))) { + ret = rest_server_send_error(req, + "400 Bad Request", + parse_err[0] != '\0' ? parse_err : "invalid image options"); + goto cleanup; + } + + image_print_options_t print_opt = {0}; + image_print_options_set_defaults(&print_opt); + parse_image_print_options_from_json(json, &print_opt); + cJSON_Delete(json); + json = NULL; + + ESP_LOGI(TAG, + "image generate start, prompt_len=%u, size=%s, prompt_extend=%d, seed=%s, timeout_ms=%u, fetch_timeout_ms=%u", + (unsigned)strlen(gen_opt.prompt), + gen_opt.has_size ? gen_opt.size : "default", + gen_opt.prompt_extend, + gen_opt.has_seed ? "set" : "auto", + (unsigned)gen_opt.timeout_ms, + (unsigned)gen_opt.fetch_timeout_ms); + ESP_LOGI(TAG, + "image process config, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s", + print_opt.scale_to_width, + (unsigned)print_opt.threshold, + print_opt.invert, + (unsigned)print_opt.max_height, + print_opt.density != NULL ? print_opt.density : "中等"); + + image_generation_request_t gen_req = { + .prompt = gen_opt.prompt, + .size = gen_opt.has_size ? gen_opt.size : NULL, + .prompt_extend = gen_opt.prompt_extend, + .has_seed = gen_opt.has_seed, + .seed = gen_opt.seed, + .generation_timeout_ms = gen_opt.timeout_ms, + .download_timeout_ms = gen_opt.fetch_timeout_ms, + }; + + gen_result = (image_generation_result_t *)calloc(1, sizeof(*gen_result)); + if (gen_result == NULL) { + ret = rest_server_send_error(req, "500 Internal Server Error", "no memory"); + goto cleanup; + } + image_generation_result_reset(gen_result); + + char model_err[160] = {0}; + esp_err_t gen_rc = image_generation_generate_png(&gen_req, + gen_result, + model_err, + sizeof(model_err)); + if (gen_rc != ESP_OK) { + ESP_LOGW(TAG, + "image generate failed, rc=0x%x, msg=%s", + (unsigned)gen_rc, + model_err[0] != '\0' ? model_err : "image generation failed"); + ret = rest_server_send_image_generation_error(req, gen_rc, model_err); + goto cleanup; + } + + if (gen_result->request_id[0] != '\0') { + strlcpy(request_id, gen_result->request_id, sizeof(request_id)); + } + + ESP_LOGI(TAG, + "image generate done, request_id=%s, model_size=%ux%u, png_bytes=%u", + request_id[0] != '\0' ? request_id : "-", + (unsigned)gen_result->width, + (unsigned)gen_result->height, + (unsigned)gen_result->png_len); + + uint16_t width = 0; + uint16_t height = 0; + size_t raster_len = 0; + char decode_err[128] = {0}; + esp_err_t decode_rc = raster_tools_convert_png_to_raster_384(gen_result->png, + gen_result->png_len, + print_opt.scale_to_width, + print_opt.threshold, + print_opt.invert, + print_opt.max_height, + &width, + &height, + &raster, + &raster_len, + decode_err, + sizeof(decode_err)); + if (decode_rc != ESP_OK) { + ESP_LOGW(TAG, + "image process failed (generated), rc=0x%x, msg=%s", + (unsigned)decode_rc, + decode_err[0] != '\0' ? decode_err : "decode generated image failed"); + ret = rest_server_send_error(req, + "400 Bad Request", + decode_err[0] != '\0' ? decode_err : "decode generated image failed"); + goto cleanup; + } + + ESP_LOGI(TAG, + "image process done (generated), raster_size=%ux%u, raster_bytes=%u", + (unsigned)width, + (unsigned)height, + (unsigned)raster_len); + + char warning[160] = {0}; + if (request_id[0] != '\0') { + snprintf(warning, sizeof(warning), "z-image request_id=%s", request_id); + } + + ret = rest_server_print_submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + print_opt.density, + warning, + request_id); + +cleanup: + if (body != NULL) { + free(body); + } + if (json != NULL) { + cJSON_Delete(json); + } + if (raster != NULL) { + free(raster); + } + if (gen_result != NULL) { + image_generation_result_free(gen_result); + free(gen_result); + } + return ret; +} + +esp_err_t rest_server_print_image_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char content_type[96] = {0}; + if (httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)) == ESP_OK) { + if (strstr(content_type, "application/json") != NULL) { + return rest_server_print_image_generate(req); + } + if (strstr(content_type, "image/png") == NULL) { + return rest_server_send_error(req, + "415 Unsupported Media Type", + "Content-Type must be application/json or image/png"); + } + } + + return rest_server_print_image_binary(req); +} diff --git a/main/control_plane/src/rest_server_print_render.c b/main/control_plane/src/rest_server_print_render.c new file mode 100644 index 0000000..43261a5 --- /dev/null +++ b/main/control_plane/src/rest_server_print_render.c @@ -0,0 +1,524 @@ +#include "rest_server_internal.h" + +#include +#include +#include +#include + +#include "printer_protocol.h" +#include "raster_tools.h" + +static esp_err_t decode_image_json_to_raster(cJSON *json, + uint16_t *out_width, + uint16_t *out_height, + uint8_t **out_raster, + size_t *out_len, + char *err, + size_t err_len) { + if (json == NULL || out_width == NULL || out_height == NULL || out_raster == NULL || out_len == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + *out_width = 0; + *out_height = 0; + *out_raster = NULL; + *out_len = 0; + + cJSON *jwidth = cJSON_GetObjectItemCaseSensitive(json, "width"); + cJSON *jheight = cJSON_GetObjectItemCaseSensitive(json, "height"); + cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding"); + cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data"); + + if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) || + !cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "missing required image fields"); + } + return ESP_ERR_INVALID_ARG; + } + + uint16_t width = (uint16_t)jwidth->valuedouble; + uint16_t height = (uint16_t)jheight->valuedouble; + if (width == 0 || height == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid image size"); + } + return ESP_ERR_INVALID_ARG; + } + + if (strcmp(jencoding->valuestring, "base64_msb_1bpp") == 0) { + uint8_t *raw = NULL; + size_t raw_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &raw, &raw_len); + if (b64_rc != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid base64"); + } + return ESP_ERR_INVALID_ARG; + } + *out_width = width; + *out_height = height; + *out_raster = raw; + *out_len = raw_len; + return ESP_OK; + } + + if (strcmp(jencoding->valuestring, "base64_gray8") == 0) { + uint8_t *gray = NULL; + size_t gray_len = 0; + esp_err_t b64_rc = rest_server_base64_decode_alloc(jdata->valuestring, &gray, &gray_len); + if (b64_rc != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid base64"); + } + return ESP_ERR_INVALID_ARG; + } + + size_t expected = (size_t)width * height; + if (gray_len != expected) { + free(gray); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "gray8 size mismatch"); + } + return ESP_ERR_INVALID_SIZE; + } + + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width"); + cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold"); + cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + + bool scale_to_width = rest_server_json_bool_with_default(jscale, true); + bool invert = rest_server_json_bool_with_default(jinvert, false); + uint8_t threshold = 160; + uint16_t max_height = 2200; + if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) { + threshold = (uint8_t)jthreshold->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + max_height = (uint16_t)jmaxh->valuedouble; + } + + uint16_t out_w = 0; + uint16_t out_h = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + esp_err_t conv_rc = raster_tools_convert_gray8_to_raster_384(gray, + width, + height, + scale_to_width, + threshold, + invert, + max_height, + &out_w, + &out_h, + &raster, + &raster_len, + err, + err_len); + free(gray); + if (conv_rc != ESP_OK) { + return conv_rc; + } + + *out_width = out_w; + *out_height = out_h; + *out_raster = raster; + *out_len = raster_len; + return ESP_OK; + } + + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "unsupported encoding"); + } + return ESP_ERR_NOT_SUPPORTED; +} + +esp_err_t rest_server_print_qr_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jecc = cJSON_GetObjectItemCaseSensitive(json, "ecc"); + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "module_scale"); + cJSON *jmargin = cJSON_GetObjectItemCaseSensitive(json, "margin_modules"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + + if (!cJSON_IsString(jtext) || jtext->valuestring == NULL || jtext->valuestring[0] == '\0') { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "text is required"); + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + + uint8_t ecc_level = 3; /* default H */ + if (cJSON_IsString(jecc) && jecc->valuestring != NULL) { + if (strcasecmp(jecc->valuestring, "L") == 0) { + ecc_level = 0; + } else if (strcasecmp(jecc->valuestring, "M") == 0) { + ecc_level = 1; + } else if (strcasecmp(jecc->valuestring, "Q") == 0) { + ecc_level = 2; + } else if (strcasecmp(jecc->valuestring, "H") == 0) { + ecc_level = 3; + } + } + + uint8_t module_scale = 0; + uint8_t margin_modules = 2; + uint16_t max_height = 2200; + if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 0 && jscale->valuedouble <= 16) { + module_scale = (uint8_t)jscale->valuedouble; + } + if (cJSON_IsNumber(jmargin) && jmargin->valuedouble >= 1 && jmargin->valuedouble <= 12) { + margin_modules = (uint8_t)jmargin->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + max_height = (uint16_t)jmaxh->valuedouble; + } + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char render_msg[128] = {0}; + esp_err_t qr_rc = raster_tools_render_qr_384(jtext->valuestring, + module_scale, + margin_modules, + ecc_level, + max_height, + &width, + &height, + &raster, + &raster_len, + render_msg, + sizeof(render_msg)); + cJSON_Delete(json); + if (qr_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + render_msg[0] != '\0' ? render_msg : "qr render failed"); + } + + esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + NULL, + NULL); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_label_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jgap = cJSON_GetObjectItemCaseSensitive(json, "gap_move_before"); + cJSON *joffset = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm"); + + bool gap_move_before = rest_server_json_bool_with_default(jgap, true); + bool has_offset = cJSON_IsNumber(joffset); + uint8_t offset_value = 0; + if (has_offset) { + if (joffset->valuedouble < 0 || joffset->valuedouble > 254) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "offset_tenths_mm must be 0..254"); + } + offset_value = (uint8_t)joffset->valuedouble; + } + + if (has_offset) { + char cmd_err[128] = {0}; + esp_err_t offset_rc = printer_protocol_set_label_offset(offset_value, 3000, cmd_err, sizeof(cmd_err)); + if (offset_rc != ESP_OK) { + cJSON_Delete(json); + return rest_server_send_error(req, + "409 Conflict", + cmd_err[0] != '\0' ? cmd_err : "set label offset failed"); + } + } + + if (gap_move_before) { + char cmd_err[128] = {0}; + esp_err_t gap_rc = printer_protocol_gap_move(5000, cmd_err, sizeof(cmd_err)); + if (gap_rc != ESP_OK) { + cJSON_Delete(json); + return rest_server_send_error(req, + "409 Conflict", + cmd_err[0] != '\0' ? cmd_err : "gap move failed"); + } + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char decode_err[128] = {0}; + esp_err_t decode_rc = decode_image_json_to_raster(json, + &width, + &height, + &raster, + &raster_len, + decode_err, + sizeof(decode_err)); + cJSON_Delete(json); + if (decode_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + decode_err[0] != '\0' ? decode_err : "decode image failed"); + } + + esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + NULL, + NULL); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_text_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jtext = cJSON_GetObjectItemCaseSensitive(json, "text"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); + cJSON *jline = cJSON_GetObjectItemCaseSensitive(json, "line_spacing"); + cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + + if (!cJSON_IsString(jtext) || jtext->valuestring == NULL) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "text is required"); + } + + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + uint8_t scale = 2; + uint8_t line_spacing = 2; + uint16_t max_height = 2000; + + if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 6) { + scale = (uint8_t)jscale->valuedouble; + } + if (cJSON_IsNumber(jline) && jline->valuedouble >= 0 && jline->valuedouble <= 12) { + line_spacing = (uint8_t)jline->valuedouble; + } + if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) { + max_height = (uint16_t)jmaxh->valuedouble; + } + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char render_msg[128] = {0}; + + esp_err_t render_rc = raster_tools_render_text_384(jtext->valuestring, + scale, + line_spacing, + max_height, + &width, + &height, + &raster, + &raster_len, + render_msg, + sizeof(render_msg)); + cJSON_Delete(json); + + if (render_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + render_msg[0] != '\0' ? render_msg : "text render failed"); + } + + esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + render_msg, + NULL); + free(raster); + return submit_rc; +} + +esp_err_t rest_server_print_receipt_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jtitle = cJSON_GetObjectItemCaseSensitive(json, "title"); + cJSON *jitems = cJSON_GetObjectItemCaseSensitive(json, "items"); + cJSON *jfooter = cJSON_GetObjectItemCaseSensitive(json, "footer"); + cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); + + const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL) + ? jtitle->valuestring + : "TQ RECEIPT"; + const char *footer = (cJSON_IsString(jfooter) && jfooter->valuestring != NULL) + ? jfooter->valuestring + : "Thank you"; + const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) + ? jdensity->valuestring + : "中等"; + uint8_t scale = 2; + if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 4) { + scale = (uint8_t)jscale->valuedouble; + } + + size_t text_cap = 8192; + char *text = (char *)calloc(1, text_cap); + if (text == NULL) { + cJSON_Delete(json); + return rest_server_send_error(req, "500 Internal Server Error", "no memory"); + } + + size_t off = 0; + rest_server_appendf(text, text_cap, &off, "%s\n", title); + rest_server_appendf(text, text_cap, &off, "--------------------------------\n"); + + double total = 0.0; + if (cJSON_IsArray(jitems)) { + int n = cJSON_GetArraySize(jitems); + for (int i = 0; i < n; ++i) { + cJSON *it = cJSON_GetArrayItem(jitems, i); + if (!cJSON_IsObject(it)) { + continue; + } + + cJSON *jname = cJSON_GetObjectItemCaseSensitive(it, "name"); + cJSON *jqty = cJSON_GetObjectItemCaseSensitive(it, "qty"); + cJSON *jprice = cJSON_GetObjectItemCaseSensitive(it, "price"); + + const char *name = (cJSON_IsString(jname) && jname->valuestring != NULL) + ? jname->valuestring + : "item"; + int qty = cJSON_IsNumber(jqty) ? (int)jqty->valuedouble : 1; + double price = cJSON_IsNumber(jprice) ? jprice->valuedouble : 0.0; + double line_total = qty * price; + total += line_total; + + rest_server_appendf(text, + text_cap, + &off, + "%-16.16s x%-3d %8.2f\n", + name, + qty, + line_total); + } + } else { + rest_server_appendf(text, text_cap, &off, "(no items)\n"); + } + + rest_server_appendf(text, text_cap, &off, "--------------------------------\n"); + rest_server_appendf(text, text_cap, &off, "TOTAL: %.2f\n", total); + rest_server_appendf(text, text_cap, &off, "%s\n", footer); + + uint16_t width = 0; + uint16_t height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char render_msg[128] = {0}; + + esp_err_t render_rc = raster_tools_render_text_384(text, + scale, + 2, + 2200, + &width, + &height, + &raster, + &raster_len, + render_msg, + sizeof(render_msg)); + free(text); + cJSON_Delete(json); + + if (render_rc != ESP_OK) { + return rest_server_send_error(req, + "400 Bad Request", + render_msg[0] != '\0' ? render_msg : "receipt render failed"); + } + + esp_err_t submit_rc = rest_server_print_submit_raster_job_and_reply(req, + raster, + raster_len, + width, + height, + density, + render_msg, + NULL); + free(raster); + return submit_rc; +} diff --git a/main/domain/internal/printer_protocol_internal.h b/main/domain/internal/printer_protocol_internal.h index 8bc78e0..ba7f41d 100644 --- a/main/domain/internal/printer_protocol_internal.h +++ b/main/domain/internal/printer_protocol_internal.h @@ -4,8 +4,20 @@ #include #include +#include "printer_protocol.h" #include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#define PROTO_ADDR 0x01 + +#define CMD_POWER 0x00 +#define CMD_GET_STATUS 0x01 +#define CMD_SET_DISTANCE 0x02 +#define CMD_SET_PARAM 0x03 +#define CMD_SEND_DATA 0x04 #define CMD_GAP_MOVE 0x05 #define CMD_GET_OFFSET 0x06 #define CMD_SET_OFFSET 0x07 @@ -15,8 +27,49 @@ #define CMD_BOOT_JUMP_APP 0xA3 #define CMD_BOOT_GET_VERSION 0xA4 +#define EVT_ACK BIT0 + +#define JOB_QUEUE_LEN 8 +#define JOB_SLOT_MAX 16 +#define MAX_RASTER_BYTES (384 * 3000 / 8) + #define OTA_MAX_DATA_PER_FRAME 236u +typedef struct { + bool used; + bool cancel_requested; + uint32_t id; + print_job_state_t state; + uint8_t progress; + uint16_t width; + uint16_t height; + size_t data_len; + char density[16]; + char error[96]; + int64_t created_ms; + int64_t started_ms; + int64_t finished_ms; + uint8_t *data; +} job_slot_t; + +typedef struct { + bool has_paper; + uint8_t battery; + float temperature; + int64_t updated_ms; +} parsed_status_t; + +extern SemaphoreHandle_t s_mutex; +extern QueueHandle_t s_job_queue; +extern EventGroupHandle_t s_evt; +extern uint32_t s_busy_refcnt; +extern parsed_status_t s_status; +extern uint8_t s_last_rsp_cmd; +extern uint16_t s_last_rsp_payload_len; +extern uint8_t s_last_rsp_payload[252]; +extern uint32_t s_next_job_id; +extern job_slot_t s_jobs[JOB_SLOT_MAX]; + bool printer_protocol_acquire_control_lane(uint32_t timeout_ms, char *err, size_t err_len); void printer_protocol_release_control_lane(void); esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, @@ -30,3 +83,23 @@ esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, uint16_t *out_payload_len, char *err, size_t err_len); +esp_err_t printer_protocol_send_frame(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum); +bool printer_protocol_wait_response(uint8_t cmd, + uint8_t *out_payload, + size_t out_payload_cap, + uint16_t *out_payload_len, + uint32_t timeout_ms); +bool printer_protocol_send_cmd_with_ack(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum, + uint32_t timeout_ms); + +int printer_protocol_find_job_idx_locked(uint32_t id); +int printer_protocol_alloc_job_slot_locked(void); +bool printer_protocol_is_terminal_state(print_job_state_t state); + +void printer_protocol_worker_task(void *arg); diff --git a/main/domain/src/printer_protocol.c b/main/domain/src/printer_protocol.c index 8622bf4..a920de0 100644 --- a/main/domain/src/printer_protocol.c +++ b/main/domain/src/printer_protocol.c @@ -1,115 +1,37 @@ #include "printer_protocol.h" #include "printer_protocol_internal.h" -#include #include -#include #include #include "ble_printer_client.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" -#include "freertos/event_groups.h" -#include "freertos/queue.h" -#include "freertos/semphr.h" #include "freertos/task.h" -#define PROTO_ADDR 0x01 +SemaphoreHandle_t s_mutex; +QueueHandle_t s_job_queue; +EventGroupHandle_t s_evt; -#define CMD_POWER 0x00 -#define CMD_GET_STATUS 0x01 -#define CMD_SET_DISTANCE 0x02 -#define CMD_SET_PARAM 0x03 -#define CMD_SEND_DATA 0x04 +uint32_t s_busy_refcnt; -#define EVT_ACK BIT0 - -#define JOB_QUEUE_LEN 8 -#define JOB_SLOT_MAX 16 -#define MAX_RASTER_BYTES (384 * 3000 / 8) - -typedef struct { - bool used; - bool cancel_requested; - uint32_t id; - print_job_state_t state; - uint8_t progress; - uint16_t width; - uint16_t height; - size_t data_len; - char density[16]; - char error[96]; - int64_t created_ms; - int64_t started_ms; - int64_t finished_ms; - uint8_t *data; -} job_slot_t; - -typedef struct { - bool has_paper; - uint8_t battery; - float temperature; - int64_t updated_ms; -} parsed_status_t; - -static SemaphoreHandle_t s_mutex; -static QueueHandle_t s_job_queue; -static EventGroupHandle_t s_evt; - -static uint32_t s_busy_refcnt; - -static parsed_status_t s_status = { +parsed_status_t s_status = { .has_paper = true, .battery = 100, .temperature = 25.0f, .updated_ms = 0, }; -static uint8_t s_last_rsp_cmd; -static uint16_t s_last_rsp_payload_len; -static uint8_t s_last_rsp_payload[252]; +uint8_t s_last_rsp_cmd; +uint16_t s_last_rsp_payload_len; +uint8_t s_last_rsp_payload[252]; + +uint32_t s_next_job_id = 1; +job_slot_t s_jobs[JOB_SLOT_MAX]; -static uint32_t s_next_job_id = 1; -static job_slot_t s_jobs[JOB_SLOT_MAX]; static const char *TAG = "printer_protocol"; -static int find_job_idx_locked(uint32_t id) { - for (int i = 0; i < JOB_SLOT_MAX; ++i) { - if (s_jobs[i].used && s_jobs[i].id == id) { - return i; - } - } - return -1; -} - -static int alloc_job_slot_locked(void) { - for (int i = 0; i < JOB_SLOT_MAX; ++i) { - if (!s_jobs[i].used) { - return i; - } - } - - /* Reuse completed slot when table is full. */ - for (int i = 0; i < JOB_SLOT_MAX; ++i) { - if (s_jobs[i].state == PRINT_JOB_STATE_SUCCESS || - s_jobs[i].state == PRINT_JOB_STATE_FAILED || - s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { - free(s_jobs[i].data); - memset(&s_jobs[i], 0, sizeof(s_jobs[i])); - return i; - } - } - - return -1; -} - -static bool is_terminal_state(print_job_state_t state) { - return state == PRINT_JOB_STATE_SUCCESS || - state == PRINT_JOB_STATE_FAILED || - state == PRINT_JOB_STATE_CANCELED; -} - bool printer_protocol_acquire_control_lane(uint32_t timeout_ms, char *err, size_t err_len) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) { if (err != NULL && err_len > 0) { @@ -150,7 +72,10 @@ static uint8_t checksum8(const uint8_t *data, size_t len) { return (uint8_t)(sum & 0xFF); } -static esp_err_t send_frame(uint8_t cmd, const uint8_t *payload, uint16_t payload_len, bool with_checksum) { +esp_err_t printer_protocol_send_frame(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum) { uint8_t frame[260]; size_t total = 4 + payload_len + (with_checksum ? 1 : 0); if (total > sizeof(frame)) { @@ -177,11 +102,11 @@ static void clear_ack_signal(void) { xEventGroupClearBits(s_evt, EVT_ACK); } -static bool wait_response(uint8_t cmd, - uint8_t *out_payload, - size_t out_payload_cap, - uint16_t *out_payload_len, - uint32_t timeout_ms) { +bool printer_protocol_wait_response(uint8_t cmd, + uint8_t *out_payload, + size_t out_payload_cap, + uint16_t *out_payload_len, + uint32_t timeout_ms) { int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; while (true) { @@ -234,19 +159,19 @@ static bool wait_response(uint8_t cmd, static bool wait_ack(uint8_t cmd, uint32_t timeout_ms) { uint8_t payload[252]; uint16_t payload_len = 0; - if (!wait_response(cmd, payload, sizeof(payload), &payload_len, timeout_ms)) { + if (!printer_protocol_wait_response(cmd, payload, sizeof(payload), &payload_len, timeout_ms)) { return false; } return payload_len >= 1 && payload[0] == 0x01; } -static bool send_cmd_with_ack(uint8_t cmd, - const uint8_t *payload, - uint16_t payload_len, - bool with_checksum, - uint32_t timeout_ms) { +bool printer_protocol_send_cmd_with_ack(uint8_t cmd, + const uint8_t *payload, + uint16_t payload_len, + bool with_checksum, + uint32_t timeout_ms) { clear_ack_signal(); - if (send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { + if (printer_protocol_send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { return false; } return wait_ack(cmd, timeout_ms); @@ -264,7 +189,7 @@ esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, char *err, size_t err_len) { clear_ack_signal(); - if (send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { + if (printer_protocol_send_frame(cmd, payload, payload_len, with_checksum) != ESP_OK) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "send frame failed"); } @@ -273,7 +198,7 @@ esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, uint8_t rsp[252]; uint16_t rsp_len = 0; - if (!wait_response(cmd, rsp, sizeof(rsp), &rsp_len, timeout_ms)) { + if (!printer_protocol_wait_response(cmd, rsp, sizeof(rsp), &rsp_len, timeout_ms)) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "response timeout"); } @@ -301,310 +226,6 @@ esp_err_t printer_protocol_send_cmd_wait_response(uint8_t cmd, return ESP_OK; } -static bool request_status_sync(uint32_t timeout_ms) { - int64_t old_ms; - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { - return false; - } - old_ms = s_status.updated_ms; - xSemaphoreGive(s_mutex); - - uint8_t payload = 0x00; - if (send_frame(CMD_GET_STATUS, &payload, 0, true) != ESP_OK) { - return false; - } - - int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; - while ((esp_timer_get_time() / 1000) < deadline) { - vTaskDelay(pdMS_TO_TICKS(20)); - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { - continue; - } - - bool updated = s_status.updated_ms > old_ms; - xSemaphoreGive(s_mutex); - - if (updated) { - return true; - } - } - - return false; -} - -static uint16_t density_to_hot_time(const char *density) { - if (density == NULL) { - return 2000; - } - if (strcmp(density, "较淡") == 0) { - return 1000; - } - if (strcmp(density, "中等") == 0) { - return 1500; - } - if (strcmp(density, "较浓") == 0) { - return 2000; - } - if (strcmp(density, "最深") == 0) { - return 3000; - } - return 2000; -} - -static bool precheck_printer_ready(char *err, size_t err_len) { - if (!request_status_sync(1200)) { - snprintf(err, err_len, "status timeout"); - return false; - } - - parsed_status_t status; - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { - snprintf(err, err_len, "status lock failed"); - return false; - } - - status = s_status; - xSemaphoreGive(s_mutex); - - if (!status.has_paper) { - snprintf(err, err_len, "printer out of paper"); - return false; - } - - if (status.battery <= 40) { - snprintf(err, err_len, "battery too low"); - return false; - } - - if (status.temperature >= 60.0f) { - snprintf(err, err_len, "temperature too high"); - return false; - } - - return true; -} - -static bool run_print_job(job_slot_t *job) { - char err[96]; - err[0] = '\0'; - uint8_t next_progress_log = 25; - - ESP_LOGI(TAG, - "job %u print start, raster_bytes=%u, size=%ux%u, density=%s", - (unsigned)job->id, - (unsigned)job->data_len, - (unsigned)job->width, - (unsigned)job->height, - job->density); - - if (!ble_printer_client_is_connected()) { - snprintf(job->error, sizeof(job->error), "printer not connected"); - ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); - return false; - } - - if (!precheck_printer_ready(err, sizeof(err))) { - snprintf(job->error, sizeof(job->error), "%s", err); - ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); - return false; - } - - if (job->cancel_requested) { - snprintf(job->error, sizeof(job->error), "job canceled"); - ESP_LOGW(TAG, "job %u print canceled before start", (unsigned)job->id); - return false; - } - - uint8_t power_on = 0x01; - if (!send_cmd_with_ack(CMD_POWER, &power_on, 1, true, 1500)) { - snprintf(job->error, sizeof(job->error), "power on failed"); - ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); - return false; - } - - uint16_t hot_time = density_to_hot_time(job->density); - uint8_t param[4] = { - 0x01, - 0x02, - (uint8_t)((hot_time >> 8) & 0xFF), - (uint8_t)(hot_time & 0xFF), - }; - - if (!send_cmd_with_ack(CMD_SET_PARAM, param, sizeof(param), true, 1500)) { - snprintf(job->error, sizeof(job->error), "set print param failed"); - ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); - return false; - } - - const size_t chunk_max = 240; - size_t total_chunks = (size_t)ceil((double)job->data_len / (double)chunk_max); - ESP_LOGI(TAG, - "job %u data transfer start, chunks=%u, chunk_bytes=%u, hot_time=%u", - (unsigned)job->id, - (unsigned)total_chunks, - (unsigned)chunk_max, - (unsigned)hot_time); - - for (size_t i = 0; i < total_chunks; ++i) { - if (job->cancel_requested) { - snprintf(job->error, sizeof(job->error), "job canceled"); - ESP_LOGW(TAG, "job %u print canceled at chunk %u/%u", (unsigned)job->id, (unsigned)(i + 1), (unsigned)total_chunks); - return false; - } - - size_t start = i * chunk_max; - size_t remain = job->data_len - start; - size_t chunk_len = remain > chunk_max ? chunk_max : remain; - - if (!send_cmd_with_ack(CMD_SEND_DATA, - &job->data[start], - (uint16_t)chunk_len, - false, - 2500)) { - snprintf(job->error, sizeof(job->error), "send chunk timeout at %u", (unsigned)i); - ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); - return false; - } - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { - job->progress = (uint8_t)(((i + 1) * 90) / total_chunks); - if (job->progress < 5) { - job->progress = 5; - } - xSemaphoreGive(s_mutex); - } - - while (job->progress >= next_progress_log && next_progress_log <= 90) { - ESP_LOGI(TAG, - "job %u print progress=%u%% (%u/%u chunks)", - (unsigned)job->id, - (unsigned)job->progress, - (unsigned)(i + 1), - (unsigned)total_chunks); - next_progress_log = (uint8_t)(next_progress_log + 25); - } - } - - ESP_LOGI(TAG, "job %u data transfer done", (unsigned)job->id); - - uint8_t power_off = 0x00; - (void)send_cmd_with_ack(CMD_POWER, &power_off, 1, true, 1200); - - uint8_t feed_payload[3] = {0x2B, 0x00, 0x0C}; - (void)send_cmd_with_ack(CMD_SET_DISTANCE, feed_payload, sizeof(feed_payload), true, 1200); - - ESP_LOGI(TAG, "job %u print command sequence done", (unsigned)job->id); - return true; -} - -static void worker_task(void *arg) { - (void)arg; - - while (true) { - uint32_t job_id; - if (xQueueReceive(s_job_queue, &job_id, portMAX_DELAY) != pdTRUE) { - continue; - } - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - continue; - } - - int idx = find_job_idx_locked(job_id); - if (idx < 0) { - xSemaphoreGive(s_mutex); - continue; - } - - if (s_jobs[idx].state == PRINT_JOB_STATE_CANCELED) { - free(s_jobs[idx].data); - s_jobs[idx].data = NULL; - if (s_jobs[idx].finished_ms == 0) { - s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; - } - xSemaphoreGive(s_mutex); - continue; - } - - if (s_busy_refcnt != 0) { - xSemaphoreGive(s_mutex); - xQueueSendToFront(s_job_queue, &job_id, 0); - vTaskDelay(pdMS_TO_TICKS(20)); - continue; - } - - job_slot_t *job = &s_jobs[idx]; - job->state = PRINT_JOB_STATE_RUNNING; - job->started_ms = esp_timer_get_time() / 1000; - job->progress = 1; - s_busy_refcnt = 1; - int64_t queue_wait_ms = 0; - if (job->started_ms >= job->created_ms) { - queue_wait_ms = job->started_ms - job->created_ms; - } - ESP_LOGI(TAG, - "job %u running, queue_wait_ms=%lld, raster_bytes=%u, size=%ux%u, density=%s", - (unsigned)job->id, - (long long)queue_wait_ms, - (unsigned)job->data_len, - (unsigned)job->width, - (unsigned)job->height, - job->density); - xSemaphoreGive(s_mutex); - - bool ok = run_print_job(job); - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - continue; - } - - if (s_busy_refcnt > 0) { - --s_busy_refcnt; - } - s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; - if (s_jobs[idx].state != PRINT_JOB_STATE_CANCELED) { - s_jobs[idx].progress = 100; - } - - if (ok) { - s_jobs[idx].state = PRINT_JOB_STATE_SUCCESS; - s_jobs[idx].error[0] = '\0'; - } else if (s_jobs[idx].cancel_requested) { - s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; - if (s_jobs[idx].error[0] == '\0') { - strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); - } - } else { - s_jobs[idx].state = PRINT_JOB_STATE_FAILED; - } - - int64_t duration_ms = 0; - if (s_jobs[idx].finished_ms >= s_jobs[idx].started_ms) { - duration_ms = s_jobs[idx].finished_ms - s_jobs[idx].started_ms; - } - if (s_jobs[idx].state == PRINT_JOB_STATE_SUCCESS) { - ESP_LOGI(TAG, - "job %u done, state=%s, duration_ms=%lld", - (unsigned)s_jobs[idx].id, - printer_protocol_job_state_str(s_jobs[idx].state), - (long long)duration_ms); - } else { - ESP_LOGW(TAG, - "job %u done, state=%s, duration_ms=%lld, error=%s", - (unsigned)s_jobs[idx].id, - printer_protocol_job_state_str(s_jobs[idx].state), - (long long)duration_ms, - s_jobs[idx].error[0] != '\0' ? s_jobs[idx].error : "-"); - } - - free(s_jobs[idx].data); - s_jobs[idx].data = NULL; - xSemaphoreGive(s_mutex); - } -} - static void status_poll_task(void *arg) { (void)arg; @@ -617,7 +238,7 @@ static void status_poll_task(void *arg) { if (ble_printer_client_is_connected() && can_poll) { uint8_t dummy = 0x00; - (void)send_frame(CMD_GET_STATUS, &dummy, 0, true); + (void)printer_protocol_send_frame(CMD_GET_STATUS, &dummy, 0, true); } vTaskDelay(pdMS_TO_TICKS(5000)); } @@ -693,7 +314,7 @@ esp_err_t printer_protocol_init(void) { return err; } - BaseType_t ok = xTaskCreate(worker_task, "print_worker", 6144, NULL, 6, NULL); + BaseType_t ok = xTaskCreate(printer_protocol_worker_task, "print_worker", 6144, NULL, 6, NULL); if (ok != pdPASS) { return ESP_ERR_NO_MEM; } @@ -703,6 +324,7 @@ esp_err_t printer_protocol_init(void) { return ESP_ERR_NO_MEM; } + ESP_LOGI(TAG, "printer protocol initialized"); return ESP_OK; } @@ -738,277 +360,3 @@ void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { xSemaphoreGive(s_mutex); } } - -esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, - size_t raster_len, - uint16_t width, - uint16_t height, - const char *density, - uint32_t *out_job_id, - char *err, - size_t err_len) { - if (raster == NULL || raster_len == 0 || width == 0 || height == 0) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid args"); - } - return ESP_ERR_INVALID_ARG; - } - - if (width != 384) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "width must be 384"); - } - return ESP_ERR_INVALID_ARG; - } - - if (raster_len > MAX_RASTER_BYTES) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "raster too large"); - } - return ESP_ERR_INVALID_SIZE; - } - - size_t expected_len = ((size_t)width / 8u) * (size_t)height; - if (expected_len != raster_len) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "size mismatch exp=%u got=%u", - (unsigned)expected_len, - (unsigned)raster_len); - } - return ESP_ERR_INVALID_SIZE; - } - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "lock timeout"); - } - return ESP_ERR_TIMEOUT; - } - - int idx = alloc_job_slot_locked(); - if (idx < 0) { - xSemaphoreGive(s_mutex); - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "job table full"); - } - return ESP_ERR_NO_MEM; - } - - uint8_t *copy = (uint8_t *)malloc(raster_len); - if (copy == NULL) { - xSemaphoreGive(s_mutex); - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "malloc failed"); - } - return ESP_ERR_NO_MEM; - } - - memcpy(copy, raster, raster_len); - - uint32_t id = s_next_job_id++; - if (s_next_job_id == 0) { - s_next_job_id = 1; - } - - memset(&s_jobs[idx], 0, sizeof(s_jobs[idx])); - s_jobs[idx].used = true; - s_jobs[idx].id = id; - s_jobs[idx].state = PRINT_JOB_STATE_QUEUED; - s_jobs[idx].progress = 0; - s_jobs[idx].width = width; - s_jobs[idx].height = height; - s_jobs[idx].data_len = raster_len; - s_jobs[idx].created_ms = esp_timer_get_time() / 1000; - s_jobs[idx].data = copy; - strlcpy(s_jobs[idx].density, density != NULL ? density : "中等", sizeof(s_jobs[idx].density)); - - xSemaphoreGive(s_mutex); - - if (xQueueSend(s_job_queue, &id, pdMS_TO_TICKS(500)) != pdTRUE) { - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) == pdTRUE) { - int rollback_idx = find_job_idx_locked(id); - if (rollback_idx >= 0) { - free(s_jobs[rollback_idx].data); - memset(&s_jobs[rollback_idx], 0, sizeof(s_jobs[rollback_idx])); - } - xSemaphoreGive(s_mutex); - } - - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "job queue full"); - } - ESP_LOGW(TAG, - "job enqueue failed, job_id=%u, reason=%s", - (unsigned)id, - (err != NULL && err[0] != '\0') ? err : "job queue full"); - return ESP_ERR_TIMEOUT; - } - - if (out_job_id != NULL) { - *out_job_id = id; - } - - UBaseType_t queue_depth = uxQueueMessagesWaiting(s_job_queue); - ESP_LOGI(TAG, - "job queued, job_id=%u, raster_bytes=%u, size=%ux%u, density=%s, queue_depth=%u", - (unsigned)id, - (unsigned)raster_len, - (unsigned)width, - (unsigned)height, - density != NULL ? density : "中等", - (unsigned)queue_depth); - - return ESP_OK; -} - -bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info) { - if (out_info == NULL || job_id == 0) { - return false; - } - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { - return false; - } - - int idx = find_job_idx_locked(job_id); - if (idx < 0) { - xSemaphoreGive(s_mutex); - return false; - } - - memset(out_info, 0, sizeof(*out_info)); - - out_info->id = s_jobs[idx].id; - out_info->state = s_jobs[idx].state; - out_info->progress = s_jobs[idx].progress; - out_info->width = s_jobs[idx].width; - out_info->height = s_jobs[idx].height; - out_info->data_len = s_jobs[idx].data_len; - out_info->created_ms = s_jobs[idx].created_ms; - out_info->started_ms = s_jobs[idx].started_ms; - out_info->finished_ms = s_jobs[idx].finished_ms; - strlcpy(out_info->density, s_jobs[idx].density, sizeof(out_info->density)); - strlcpy(out_info->error, s_jobs[idx].error, sizeof(out_info->error)); - - xSemaphoreGive(s_mutex); - return true; -} - -esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count) { - if (out_count == NULL) { - return ESP_ERR_INVALID_ARG; - } - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { - return ESP_ERR_TIMEOUT; - } - - size_t count = 0; - for (int i = 0; i < JOB_SLOT_MAX; ++i) { - if (!s_jobs[i].used || s_jobs[i].state == PRINT_JOB_STATE_NONE) { - continue; - } - if (out_jobs != NULL && count < max_jobs) { - memset(&out_jobs[count], 0, sizeof(out_jobs[count])); - out_jobs[count].id = s_jobs[i].id; - out_jobs[count].state = s_jobs[i].state; - out_jobs[count].progress = s_jobs[i].progress; - out_jobs[count].width = s_jobs[i].width; - out_jobs[count].height = s_jobs[i].height; - out_jobs[count].data_len = s_jobs[i].data_len; - out_jobs[count].created_ms = s_jobs[i].created_ms; - out_jobs[count].started_ms = s_jobs[i].started_ms; - out_jobs[count].finished_ms = s_jobs[i].finished_ms; - strlcpy(out_jobs[count].density, s_jobs[i].density, sizeof(out_jobs[count].density)); - strlcpy(out_jobs[count].error, s_jobs[i].error, sizeof(out_jobs[count].error)); - } - ++count; - } - - xSemaphoreGive(s_mutex); - *out_count = count; - return ESP_OK; -} - -esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len) { - if (job_id == 0) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "invalid job id"); - } - return ESP_ERR_INVALID_ARG; - } - - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "lock timeout"); - } - return ESP_ERR_TIMEOUT; - } - - int idx = find_job_idx_locked(job_id); - if (idx < 0) { - xSemaphoreGive(s_mutex); - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "job not found"); - } - return ESP_ERR_NOT_FOUND; - } - - if (is_terminal_state(s_jobs[idx].state)) { - xSemaphoreGive(s_mutex); - if (err != NULL && err_len > 0) { - snprintf(err, err_len, "job already finished"); - } - return ESP_ERR_INVALID_STATE; - } - - s_jobs[idx].cancel_requested = true; - if (s_jobs[idx].state == PRINT_JOB_STATE_QUEUED) { - s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; - s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; - s_jobs[idx].progress = 0; - strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); - free(s_jobs[idx].data); - s_jobs[idx].data = NULL; - } else if (s_jobs[idx].state == PRINT_JOB_STATE_RUNNING) { - strlcpy(s_jobs[idx].error, "cancel requested", sizeof(s_jobs[idx].error)); - } - - xSemaphoreGive(s_mutex); - return ESP_OK; -} - -size_t printer_protocol_cleanup_jobs(bool include_success, bool include_failed, bool include_canceled) { - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { - return 0; - } - - size_t removed = 0; - for (int i = 0; i < JOB_SLOT_MAX; ++i) { - if (!s_jobs[i].used) { - continue; - } - - bool match = false; - if (include_success && s_jobs[i].state == PRINT_JOB_STATE_SUCCESS) { - match = true; - } - if (include_failed && s_jobs[i].state == PRINT_JOB_STATE_FAILED) { - match = true; - } - if (include_canceled && s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { - match = true; - } - - if (!match) { - continue; - } - - free(s_jobs[i].data); - memset(&s_jobs[i], 0, sizeof(s_jobs[i])); - ++removed; - } - - xSemaphoreGive(s_mutex); - return removed; -} diff --git a/main/domain/src/printer_protocol_jobs.c b/main/domain/src/printer_protocol_jobs.c new file mode 100644 index 0000000..aefb280 --- /dev/null +++ b/main/domain/src/printer_protocol_jobs.c @@ -0,0 +1,323 @@ +#include "printer_protocol.h" +#include "printer_protocol_internal.h" + +#include +#include +#include + +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "printer_protocol"; + +int printer_protocol_find_job_idx_locked(uint32_t id) { + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (s_jobs[i].used && s_jobs[i].id == id) { + return i; + } + } + return -1; +} + +int printer_protocol_alloc_job_slot_locked(void) { + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (!s_jobs[i].used) { + return i; + } + } + + /* Reuse completed slot when table is full. */ + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (s_jobs[i].state == PRINT_JOB_STATE_SUCCESS || + s_jobs[i].state == PRINT_JOB_STATE_FAILED || + s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { + free(s_jobs[i].data); + memset(&s_jobs[i], 0, sizeof(s_jobs[i])); + return i; + } + } + + return -1; +} + +bool printer_protocol_is_terminal_state(print_job_state_t state) { + return state == PRINT_JOB_STATE_SUCCESS || + state == PRINT_JOB_STATE_FAILED || + state == PRINT_JOB_STATE_CANCELED; +} + +esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + const char *density, + uint32_t *out_job_id, + char *err, + size_t err_len) { + if (raster == NULL || raster_len == 0 || width == 0 || height == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid args"); + } + return ESP_ERR_INVALID_ARG; + } + + if (width != 384) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "width must be 384"); + } + return ESP_ERR_INVALID_ARG; + } + + if (raster_len > MAX_RASTER_BYTES) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "raster too large"); + } + return ESP_ERR_INVALID_SIZE; + } + + size_t expected_len = ((size_t)width / 8u) * (size_t)height; + if (expected_len != raster_len) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "size mismatch exp=%u got=%u", + (unsigned)expected_len, + (unsigned)raster_len); + } + return ESP_ERR_INVALID_SIZE; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "lock timeout"); + } + return ESP_ERR_TIMEOUT; + } + + int idx = printer_protocol_alloc_job_slot_locked(); + if (idx < 0) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job table full"); + } + return ESP_ERR_NO_MEM; + } + + uint8_t *copy = (uint8_t *)malloc(raster_len); + if (copy == NULL) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "malloc failed"); + } + return ESP_ERR_NO_MEM; + } + + memcpy(copy, raster, raster_len); + + uint32_t id = s_next_job_id++; + if (s_next_job_id == 0) { + s_next_job_id = 1; + } + + memset(&s_jobs[idx], 0, sizeof(s_jobs[idx])); + s_jobs[idx].used = true; + s_jobs[idx].id = id; + s_jobs[idx].state = PRINT_JOB_STATE_QUEUED; + s_jobs[idx].progress = 0; + s_jobs[idx].width = width; + s_jobs[idx].height = height; + s_jobs[idx].data_len = raster_len; + s_jobs[idx].created_ms = esp_timer_get_time() / 1000; + s_jobs[idx].data = copy; + strlcpy(s_jobs[idx].density, density != NULL ? density : "中等", sizeof(s_jobs[idx].density)); + + xSemaphoreGive(s_mutex); + + if (xQueueSend(s_job_queue, &id, pdMS_TO_TICKS(500)) != pdTRUE) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) == pdTRUE) { + int rollback_idx = printer_protocol_find_job_idx_locked(id); + if (rollback_idx >= 0) { + free(s_jobs[rollback_idx].data); + memset(&s_jobs[rollback_idx], 0, sizeof(s_jobs[rollback_idx])); + } + xSemaphoreGive(s_mutex); + } + + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job queue full"); + } + ESP_LOGW(TAG, + "job enqueue failed, job_id=%u, reason=%s", + (unsigned)id, + (err != NULL && err[0] != '\0') ? err : "job queue full"); + return ESP_ERR_TIMEOUT; + } + + if (out_job_id != NULL) { + *out_job_id = id; + } + + UBaseType_t queue_depth = uxQueueMessagesWaiting(s_job_queue); + ESP_LOGI(TAG, + "job queued, job_id=%u, raster_bytes=%u, size=%ux%u, density=%s, queue_depth=%u", + (unsigned)id, + (unsigned)raster_len, + (unsigned)width, + (unsigned)height, + density != NULL ? density : "中等", + (unsigned)queue_depth); + + return ESP_OK; +} + +bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info) { + if (out_info == NULL || job_id == 0) { + return false; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return false; + } + + int idx = printer_protocol_find_job_idx_locked(job_id); + if (idx < 0) { + xSemaphoreGive(s_mutex); + return false; + } + + memset(out_info, 0, sizeof(*out_info)); + + out_info->id = s_jobs[idx].id; + out_info->state = s_jobs[idx].state; + out_info->progress = s_jobs[idx].progress; + out_info->width = s_jobs[idx].width; + out_info->height = s_jobs[idx].height; + out_info->data_len = s_jobs[idx].data_len; + out_info->created_ms = s_jobs[idx].created_ms; + out_info->started_ms = s_jobs[idx].started_ms; + out_info->finished_ms = s_jobs[idx].finished_ms; + strlcpy(out_info->density, s_jobs[idx].density, sizeof(out_info->density)); + strlcpy(out_info->error, s_jobs[idx].error, sizeof(out_info->error)); + + xSemaphoreGive(s_mutex); + return true; +} + +esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count) { + if (out_count == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + size_t count = 0; + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (!s_jobs[i].used || s_jobs[i].state == PRINT_JOB_STATE_NONE) { + continue; + } + if (out_jobs != NULL && count < max_jobs) { + memset(&out_jobs[count], 0, sizeof(out_jobs[count])); + out_jobs[count].id = s_jobs[i].id; + out_jobs[count].state = s_jobs[i].state; + out_jobs[count].progress = s_jobs[i].progress; + out_jobs[count].width = s_jobs[i].width; + out_jobs[count].height = s_jobs[i].height; + out_jobs[count].data_len = s_jobs[i].data_len; + out_jobs[count].created_ms = s_jobs[i].created_ms; + out_jobs[count].started_ms = s_jobs[i].started_ms; + out_jobs[count].finished_ms = s_jobs[i].finished_ms; + strlcpy(out_jobs[count].density, s_jobs[i].density, sizeof(out_jobs[count].density)); + strlcpy(out_jobs[count].error, s_jobs[i].error, sizeof(out_jobs[count].error)); + } + ++count; + } + + xSemaphoreGive(s_mutex); + *out_count = count; + return ESP_OK; +} + +esp_err_t printer_protocol_cancel_job(uint32_t job_id, char *err, size_t err_len) { + if (job_id == 0) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid job id"); + } + return ESP_ERR_INVALID_ARG; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "lock timeout"); + } + return ESP_ERR_TIMEOUT; + } + + int idx = printer_protocol_find_job_idx_locked(job_id); + if (idx < 0) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job not found"); + } + return ESP_ERR_NOT_FOUND; + } + + if (printer_protocol_is_terminal_state(s_jobs[idx].state)) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "job already finished"); + } + return ESP_ERR_INVALID_STATE; + } + + s_jobs[idx].cancel_requested = true; + if (s_jobs[idx].state == PRINT_JOB_STATE_QUEUED) { + s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; + s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; + s_jobs[idx].progress = 0; + strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); + free(s_jobs[idx].data); + s_jobs[idx].data = NULL; + } else if (s_jobs[idx].state == PRINT_JOB_STATE_RUNNING) { + strlcpy(s_jobs[idx].error, "cancel requested", sizeof(s_jobs[idx].error)); + } + + xSemaphoreGive(s_mutex); + return ESP_OK; +} + +size_t printer_protocol_cleanup_jobs(bool include_success, bool include_failed, bool include_canceled) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) { + return 0; + } + + size_t removed = 0; + for (int i = 0; i < JOB_SLOT_MAX; ++i) { + if (!s_jobs[i].used) { + continue; + } + + bool match = false; + if (include_success && s_jobs[i].state == PRINT_JOB_STATE_SUCCESS) { + match = true; + } + if (include_failed && s_jobs[i].state == PRINT_JOB_STATE_FAILED) { + match = true; + } + if (include_canceled && s_jobs[i].state == PRINT_JOB_STATE_CANCELED) { + match = true; + } + + if (!match) { + continue; + } + + free(s_jobs[i].data); + memset(&s_jobs[i], 0, sizeof(s_jobs[i])); + ++removed; + } + + xSemaphoreGive(s_mutex); + return removed; +} diff --git a/main/domain/src/printer_protocol_worker.c b/main/domain/src/printer_protocol_worker.c new file mode 100644 index 0000000..a712eae --- /dev/null +++ b/main/domain/src/printer_protocol_worker.c @@ -0,0 +1,322 @@ +#include "printer_protocol.h" +#include "printer_protocol_internal.h" + +#include +#include +#include + +#include "ble_printer_client.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "printer_protocol"; + +static bool request_status_sync(uint32_t timeout_ms) { + int64_t old_ms; + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + return false; + } + old_ms = s_status.updated_ms; + xSemaphoreGive(s_mutex); + + uint8_t payload = 0x00; + if (printer_protocol_send_frame(CMD_GET_STATUS, &payload, 0, true) != ESP_OK) { + return false; + } + + int64_t deadline = esp_timer_get_time() / 1000 + timeout_ms; + while ((esp_timer_get_time() / 1000) < deadline) { + vTaskDelay(pdMS_TO_TICKS(20)); + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + continue; + } + + bool updated = s_status.updated_ms > old_ms; + xSemaphoreGive(s_mutex); + + if (updated) { + return true; + } + } + + return false; +} + +static uint16_t density_to_hot_time(const char *density) { + if (density == NULL) { + return 2000; + } + if (strcmp(density, "较淡") == 0) { + return 1000; + } + if (strcmp(density, "中等") == 0) { + return 1500; + } + if (strcmp(density, "较浓") == 0) { + return 2000; + } + if (strcmp(density, "最深") == 0) { + return 3000; + } + return 2000; +} + +static bool precheck_printer_ready(char *err, size_t err_len) { + if (!request_status_sync(1200)) { + snprintf(err, err_len, "status timeout"); + return false; + } + + parsed_status_t status; + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { + snprintf(err, err_len, "status lock failed"); + return false; + } + + status = s_status; + xSemaphoreGive(s_mutex); + + if (!status.has_paper) { + snprintf(err, err_len, "printer out of paper"); + return false; + } + + if (status.battery <= 40) { + snprintf(err, err_len, "battery too low"); + return false; + } + + if (status.temperature >= 60.0f) { + snprintf(err, err_len, "temperature too high"); + return false; + } + + return true; +} + +static bool run_print_job(job_slot_t *job) { + char err[96]; + err[0] = '\0'; + uint8_t next_progress_log = 25; + + ESP_LOGI(TAG, + "job %u print start, raster_bytes=%u, size=%ux%u, density=%s", + (unsigned)job->id, + (unsigned)job->data_len, + (unsigned)job->width, + (unsigned)job->height, + job->density); + + if (!ble_printer_client_is_connected()) { + snprintf(job->error, sizeof(job->error), "printer not connected"); + ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); + return false; + } + + if (!precheck_printer_ready(err, sizeof(err))) { + snprintf(job->error, sizeof(job->error), "%s", err); + ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); + return false; + } + + if (job->cancel_requested) { + snprintf(job->error, sizeof(job->error), "job canceled"); + ESP_LOGW(TAG, "job %u print canceled before start", (unsigned)job->id); + return false; + } + + uint8_t power_on = 0x01; + if (!printer_protocol_send_cmd_with_ack(CMD_POWER, &power_on, 1, true, 1500)) { + snprintf(job->error, sizeof(job->error), "power on failed"); + ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); + return false; + } + + uint16_t hot_time = density_to_hot_time(job->density); + uint8_t param[4] = { + 0x01, + 0x02, + (uint8_t)((hot_time >> 8) & 0xFF), + (uint8_t)(hot_time & 0xFF), + }; + + if (!printer_protocol_send_cmd_with_ack(CMD_SET_PARAM, param, sizeof(param), true, 1500)) { + snprintf(job->error, sizeof(job->error), "set print param failed"); + ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); + return false; + } + + const size_t chunk_max = 240; + size_t total_chunks = (size_t)ceil((double)job->data_len / (double)chunk_max); + ESP_LOGI(TAG, + "job %u data transfer start, chunks=%u, chunk_bytes=%u, hot_time=%u", + (unsigned)job->id, + (unsigned)total_chunks, + (unsigned)chunk_max, + (unsigned)hot_time); + + for (size_t i = 0; i < total_chunks; ++i) { + if (job->cancel_requested) { + snprintf(job->error, sizeof(job->error), "job canceled"); + ESP_LOGW(TAG, + "job %u print canceled at chunk %u/%u", + (unsigned)job->id, + (unsigned)(i + 1), + (unsigned)total_chunks); + return false; + } + + size_t start = i * chunk_max; + size_t remain = job->data_len - start; + size_t chunk_len = remain > chunk_max ? chunk_max : remain; + + if (!printer_protocol_send_cmd_with_ack(CMD_SEND_DATA, + &job->data[start], + (uint16_t)chunk_len, + false, + 2500)) { + snprintf(job->error, sizeof(job->error), "send chunk timeout at %u", (unsigned)i); + ESP_LOGW(TAG, "job %u print aborted: %s", (unsigned)job->id, job->error); + return false; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + job->progress = (uint8_t)(((i + 1) * 90) / total_chunks); + if (job->progress < 5) { + job->progress = 5; + } + xSemaphoreGive(s_mutex); + } + + while (job->progress >= next_progress_log && next_progress_log <= 90) { + ESP_LOGI(TAG, + "job %u print progress=%u%% (%u/%u chunks)", + (unsigned)job->id, + (unsigned)job->progress, + (unsigned)(i + 1), + (unsigned)total_chunks); + next_progress_log = (uint8_t)(next_progress_log + 25); + } + } + + ESP_LOGI(TAG, "job %u data transfer done", (unsigned)job->id); + + uint8_t power_off = 0x00; + (void)printer_protocol_send_cmd_with_ack(CMD_POWER, &power_off, 1, true, 1200); + + uint8_t feed_payload[3] = {0x2B, 0x00, 0x0C}; + (void)printer_protocol_send_cmd_with_ack(CMD_SET_DISTANCE, feed_payload, sizeof(feed_payload), true, 1200); + + ESP_LOGI(TAG, "job %u print command sequence done", (unsigned)job->id); + return true; +} + +void printer_protocol_worker_task(void *arg) { + (void)arg; + + while (true) { + uint32_t job_id; + if (xQueueReceive(s_job_queue, &job_id, portMAX_DELAY) != pdTRUE) { + continue; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + continue; + } + + int idx = printer_protocol_find_job_idx_locked(job_id); + if (idx < 0) { + xSemaphoreGive(s_mutex); + continue; + } + + if (s_jobs[idx].state == PRINT_JOB_STATE_CANCELED) { + free(s_jobs[idx].data); + s_jobs[idx].data = NULL; + if (s_jobs[idx].finished_ms == 0) { + s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; + } + xSemaphoreGive(s_mutex); + continue; + } + + if (s_busy_refcnt != 0) { + xSemaphoreGive(s_mutex); + xQueueSendToFront(s_job_queue, &job_id, 0); + vTaskDelay(pdMS_TO_TICKS(20)); + continue; + } + + job_slot_t *job = &s_jobs[idx]; + job->state = PRINT_JOB_STATE_RUNNING; + job->started_ms = esp_timer_get_time() / 1000; + job->progress = 1; + s_busy_refcnt = 1; + int64_t queue_wait_ms = 0; + if (job->started_ms >= job->created_ms) { + queue_wait_ms = job->started_ms - job->created_ms; + } + ESP_LOGI(TAG, + "job %u running, queue_wait_ms=%lld, raster_bytes=%u, size=%ux%u, density=%s", + (unsigned)job->id, + (long long)queue_wait_ms, + (unsigned)job->data_len, + (unsigned)job->width, + (unsigned)job->height, + job->density); + xSemaphoreGive(s_mutex); + + bool ok = run_print_job(job); + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + continue; + } + + if (s_busy_refcnt > 0) { + --s_busy_refcnt; + } + s_jobs[idx].finished_ms = esp_timer_get_time() / 1000; + if (s_jobs[idx].state != PRINT_JOB_STATE_CANCELED) { + s_jobs[idx].progress = 100; + } + + if (ok) { + s_jobs[idx].state = PRINT_JOB_STATE_SUCCESS; + s_jobs[idx].error[0] = '\0'; + } else if (s_jobs[idx].cancel_requested) { + s_jobs[idx].state = PRINT_JOB_STATE_CANCELED; + if (s_jobs[idx].error[0] == '\0') { + strlcpy(s_jobs[idx].error, "job canceled", sizeof(s_jobs[idx].error)); + } + } else { + s_jobs[idx].state = PRINT_JOB_STATE_FAILED; + } + + int64_t duration_ms = 0; + if (s_jobs[idx].finished_ms >= s_jobs[idx].started_ms) { + duration_ms = s_jobs[idx].finished_ms - s_jobs[idx].started_ms; + } + if (s_jobs[idx].state == PRINT_JOB_STATE_SUCCESS) { + ESP_LOGI(TAG, + "job %u done, state=%s, duration_ms=%lld", + (unsigned)s_jobs[idx].id, + printer_protocol_job_state_str(s_jobs[idx].state), + (long long)duration_ms); + } else { + ESP_LOGW(TAG, + "job %u done, state=%s, duration_ms=%lld, error=%s", + (unsigned)s_jobs[idx].id, + printer_protocol_job_state_str(s_jobs[idx].state), + (long long)duration_ms, + s_jobs[idx].error[0] != '\0' ? s_jobs[idx].error : "-"); + } + + free(s_jobs[idx].data); + s_jobs[idx].data = NULL; + xSemaphoreGive(s_mutex); + } +}