refactor(control_plane,domain): split oversized print and protocol modules
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
582
main/control_plane/src/rest_server_print_image.c
Normal file
582
main/control_plane/src/rest_server_print_image.c
Normal file
@@ -0,0 +1,582 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
524
main/control_plane/src/rest_server_print_render.c
Normal file
524
main/control_plane/src/rest_server_print_render.c
Normal file
@@ -0,0 +1,524 @@
|
||||
#include "rest_server_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -4,8 +4,20 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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);
|
||||
|
||||
@@ -1,115 +1,37 @@
|
||||
#include "printer_protocol.h"
|
||||
#include "printer_protocol_internal.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
323
main/domain/src/printer_protocol_jobs.c
Normal file
323
main/domain/src/printer_protocol_jobs.c
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "printer_protocol.h"
|
||||
#include "printer_protocol_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
322
main/domain/src/printer_protocol_worker.c
Normal file
322
main/domain/src/printer_protocol_worker.c
Normal file
@@ -0,0 +1,322 @@
|
||||
#include "printer_protocol.h"
|
||||
#include "printer_protocol_internal.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user