diff --git a/components/control_plane/internal/rest_server_internal.h b/components/control_plane/internal/rest_server_internal.h index a09c191..330d2b2 100644 --- a/components/control_plane/internal/rest_server_internal.h +++ b/components/control_plane/internal/rest_server_internal.h @@ -22,6 +22,7 @@ esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req, uint16_t width, uint16_t height, const char *density, + bool direct_ignore_precheck, const char *warning, const char *trace_id); diff --git a/components/control_plane/src/rest_server.c b/components/control_plane/src/rest_server.c index ad43aa2..95e78ae 100644 --- a/components/control_plane/src/rest_server.c +++ b/components/control_plane/src/rest_server.c @@ -53,12 +53,31 @@ static const char *s_web_index = "\n" " \n" " \n" + " \n" + " \n" "
\n" " \n" " \n" " \n" "
\n" "\n" + " \n" + "
\n" + " \n" + "
\n" + "
纸张状态:未知
\n" + "
Backend: -, Connected: -, Transport: -, GPIO: -, Expect: -
\n" + "
\n" + " \n" + "
\n" + "
Only effective on Direct backend. Use for sensor bring-up/debug.
\n" + "\n" " \n" " \n" "
\n" @@ -121,6 +140,10 @@ static const char *s_web_index = " const out = document.getElementById('out');\n" " const apiKeyEl = document.getElementById('apiKey');\n" " const printerNameEl = document.getElementById('printerName');\n" + " const printerBackendEl = document.getElementById('printerBackend');\n" + " const paperStateEl = document.getElementById('paperState');\n" + " const paperMetaEl = document.getElementById('paperMeta');\n" + " const ignorePrecheckEl = document.getElementById('ignorePrecheck');\n" " const printTextEl = document.getElementById('printText');\n" " const imagePromptEl = document.getElementById('imagePrompt');\n" " const imgSizeEl = document.getElementById('imgSize');\n" @@ -168,6 +191,46 @@ static const char *s_web_index = " return new Promise((resolve) => setTimeout(resolve, ms));\n" " }\n" "\n" + " function selectedPrinterBackend() {\n" + " const v = (printerBackendEl && printerBackendEl.value) ? String(printerBackendEl.value).toLowerCase() : 'ble';\n" + " return v === 'direct' ? 'direct' : 'ble';\n" + " }\n" + "\n" + " function selectedIgnorePrecheck() {\n" + " return !!(ignorePrecheckEl && ignorePrecheckEl.checked);\n" + " }\n" + "\n" + " function syncPaperUi(st) {\n" + " if (!paperStateEl || !paperMetaEl) {\n" + " return;\n" + " }\n" + " if (!st || typeof st !== 'object') {\n" + " paperStateEl.textContent = '纸张状态:未知';\n" + " paperMetaEl.textContent = 'Backend: -, Connected: -, Transport: -, GPIO: -, Expect: -';\n" + " return;\n" + " }\n" + "\n" + " const hasPaper = !!st.has_paper;\n" + " paperStateEl.textContent = hasPaper ? '纸张状态:有纸' : '纸张状态:缺纸';\n" + "\n" + " const backend = st.printer_backend ? String(st.printer_backend) : '-';\n" + " const connected = !!st.printer_connected;\n" + " const transportReady = !!st.printer_transport_ready;\n" + " const paperLevel = (st.paper_gpio_level === undefined || st.paper_gpio_level === null) ? '-' : String(st.paper_gpio_level);\n" + " const paperExpect = (st.paper_present_level === undefined || st.paper_present_level === null) ? '-' : String(st.paper_present_level);\n" + " paperMetaEl.textContent = 'Backend: ' + backend + ', Connected: ' + connected + ', Transport: ' + transportReady + ', GPIO: ' + paperLevel + ', Expect: ' + paperExpect;\n" + "\n" + " if (printerBackendEl && (backend === 'ble' || backend === 'direct')) {\n" + " printerBackendEl.value = backend;\n" + " }\n" + " }\n" + "\n" + " async function fetchPrinterStatus() {\n" + " const st = await callApi('/v1/printer/status', 'GET');\n" + " syncPaperUi(st);\n" + " return st;\n" + " }\n" + "\n" " function voiceRoundBusy(st) {\n" " if (!st) return false;\n" " const state = (st.voice_dialog_state || '').toLowerCase();\n" @@ -363,6 +426,7 @@ static const char *s_web_index = " threshold: toInt(imgThresholdEl.value, 160, 0, 255),\n" " max_height: toInt(imgMaxHeightEl.value, 2200, 64, 3000),\n" " density: imgDensityEl.value || '中等',\n" + " ignore_precheck: selectedIgnorePrecheck(),\n" " scale_to_width: imgScaleToWidthEl.checked,\n" " invert: imgInvertEl.checked,\n" " timeout_ms: toInt(imgTimeoutEl.value, 45000, 5000, 180000),\n" @@ -393,24 +457,41 @@ static const char *s_web_index = " }\n" "\n" " document.getElementById('btnHealth').onclick = () => run(() => callApi('/v1/health', 'GET'));\n" - " document.getElementById('btnStatus').onclick = () => run(() => callApi('/v1/printer/status', 'GET'));\n" + " document.getElementById('btnStatus').onclick = () => run(() => fetchPrinterStatus());\n" " document.getElementById('btnJobs').onclick = () => run(() => callApi('/v1/jobs', 'GET'));\n" "\n" - " document.getElementById('btnConnect').onclick = () => run(() => callApi('/v1/printer/connect', 'POST', {\n" - " name: printerNameEl.value.trim() || 'TQPrinter',\n" - " timeout_ms: 15000\n" - " }));\n" + " document.getElementById('btnConnect').onclick = () => run(async () => {\n" + " const data = await callApi('/v1/printer/connect', 'POST', {\n" + " backend: selectedPrinterBackend(),\n" + " name: printerNameEl.value.trim() || 'TQPrinter',\n" + " timeout_ms: 15000\n" + " });\n" + " syncPaperUi(data);\n" + " return data;\n" + " });\n" "\n" - " document.getElementById('btnAutoConnect').onclick = () => run(() => callApi('/v1/printer/connect', 'POST', {\n" - " name: '*',\n" - " timeout_ms: 20000\n" - " }));\n" + " document.getElementById('btnAutoConnect').onclick = () => run(async () => {\n" + " const data = await callApi('/v1/printer/connect', 'POST', {\n" + " backend: selectedPrinterBackend(),\n" + " name: '*',\n" + " timeout_ms: 20000\n" + " });\n" + " syncPaperUi(data);\n" + " return data;\n" + " });\n" "\n" - " document.getElementById('btnDisconnect').onclick = () => run(() => callApi('/v1/printer/disconnect', 'POST', {}));\n" + " document.getElementById('btnDisconnect').onclick = () => run(async () => {\n" + " const data = await callApi('/v1/printer/disconnect', 'POST', {});\n" + " try { await fetchPrinterStatus(); } catch (_) {}\n" + " return data;\n" + " });\n" + "\n" + " document.getElementById('btnPaperCheck').onclick = () => run(() => fetchPrinterStatus());\n" "\n" " document.getElementById('btnPrintText').onclick = () => run(() => callApi('/v1/print/text', 'POST', {\n" " text: printTextEl.value,\n" " density: '中等',\n" + " ignore_precheck: selectedIgnorePrecheck(),\n" " scale: 2,\n" " line_spacing: 2,\n" " max_height: 1800\n" @@ -447,6 +528,9 @@ static const char *s_web_index = " setVoiceTalkButtonState(false);\n" " (async () => {\n" " try {\n" + " await fetchPrinterStatus();\n" + " } catch (_) {}\n" + " try {\n" " await refreshVoiceStatus();\n" " } catch (_) {}\n" " })();\n" diff --git a/components/control_plane/src/rest_server_ops.c b/components/control_plane/src/rest_server_ops.c index 747185a..c7c4d00 100644 --- a/components/control_plane/src/rest_server_ops.c +++ b/components/control_plane/src/rest_server_ops.c @@ -2,11 +2,42 @@ #include #include +#include #include #include "control_plane.h" #include "domain.h" +static const char *printer_backend_str(printer_backend_t backend) { + switch (backend) { + case PRINTER_BACKEND_BLE: + return "ble"; + case PRINTER_BACKEND_DIRECT: + return "direct"; + default: + return "unknown"; + } +} + +static bool parse_printer_backend(const cJSON *json, printer_backend_t *out_backend) { + if (json == NULL || out_backend == NULL) { + return false; + } + cJSON *jbackend = cJSON_GetObjectItemCaseSensitive((cJSON *)json, "backend"); + if (!cJSON_IsString(jbackend) || jbackend->valuestring == NULL) { + return false; + } + if (strcasecmp(jbackend->valuestring, "ble") == 0) { + *out_backend = PRINTER_BACKEND_BLE; + return true; + } + if (strcasecmp(jbackend->valuestring, "direct") == 0) { + *out_backend = PRINTER_BACKEND_DIRECT; + return true; + } + return false; +} + static void fill_runtime_diag_json(cJSON *root) { runtime_diag_snapshot_t snapshot = {0}; runtime_diag_get_snapshot(&snapshot); @@ -54,12 +85,20 @@ static void fill_runtime_json(cJSON *root) { cJSON_AddStringToObject(root, "wifi_mode", "sta"); cJSON_AddStringToObject(root, "ip", ip); - cJSON_AddBoolToObject(root, "ble_connected", st.connected); - cJSON_AddBoolToObject(root, "ble_notify_ready", st.notify_ready); + cJSON_AddStringToObject(root, "printer_backend", printer_backend_str(st.backend)); + cJSON_AddBoolToObject(root, "printer_connected", st.connected); + cJSON_AddBoolToObject(root, "printer_transport_ready", st.transport_ready); + cJSON_AddBoolToObject(root, "ble_connected", st.backend == PRINTER_BACKEND_BLE && st.connected); + cJSON_AddBoolToObject(root, "ble_notify_ready", st.backend == PRINTER_BACKEND_BLE && st.notify_ready); cJSON_AddBoolToObject(root, "printer_busy", st.busy); cJSON_AddBoolToObject(root, "has_paper", st.has_paper); + cJSON_AddNumberToObject(root, "paper_gpio_level", st.paper_gpio_level); + cJSON_AddNumberToObject(root, "paper_present_level", st.paper_present_level); cJSON_AddNumberToObject(root, "battery_percent", st.battery_percent); cJSON_AddNumberToObject(root, "temperature", st.temperature); + cJSON_AddBoolToObject(root, "supports_gap_move", st.supports_gap_move); + cJSON_AddBoolToObject(root, "supports_label_offset", st.supports_label_offset); + cJSON_AddBoolToObject(root, "supports_ota", st.supports_ota); cJSON_AddNumberToObject(root, "queue_depth", st.queue_depth); cJSON_AddNumberToObject(root, "mtu", st.mtu); cJSON_AddNumberToObject(root, "last_status_ms", (double)st.last_status_ms); @@ -117,6 +156,8 @@ esp_err_t rest_server_connect_post(httpd_req_t *req) { char *body = NULL; char name[32] = "TQPrinter"; uint32_t timeout_ms = runtime_policy_rest_printer_connect_timeout_ms(); + printer_backend_t backend = printer_protocol_get_backend(); + bool backend_specified = false; if (req->content_len > 0) { esp_err_t body_err = rest_server_read_body(req, &body); @@ -140,21 +181,53 @@ esp_err_t rest_server_connect_post(httpd_req_t *req) { timeout_ms = (uint32_t)jtimeout->valuedouble; } + cJSON *jbackend = cJSON_GetObjectItemCaseSensitive(json, "backend"); + if (jbackend != NULL) { + backend_specified = true; + if (!parse_printer_backend(json, &backend)) { + cJSON_Delete(json); + free(body); + return rest_server_send_error(req, "400 Bad Request", "backend must be ble or direct"); + } + } + cJSON_Delete(json); free(body); } - esp_err_t err = printer_protocol_connect(name, timeout_ms); + printer_connect_options_t opt = { + .backend = backend, + .name = backend == PRINTER_BACKEND_BLE ? name : NULL, + .timeout_ms = timeout_ms, + }; + char connect_err[96] = {0}; + esp_err_t err = printer_protocol_connect_ex(&opt, connect_err, sizeof(connect_err)); if (err != ESP_OK) { if (err == ESP_ERR_TIMEOUT) { - return rest_server_send_error(req, "504 Gateway Timeout", "connect timeout"); + return rest_server_send_error(req, + "504 Gateway Timeout", + connect_err[0] != '\0' ? connect_err : "connect timeout"); } - return rest_server_send_error(req, "500 Internal Server Error", "connect failed"); + if (err == ESP_ERR_NOT_SUPPORTED || err == ESP_ERR_INVALID_STATE) { + return rest_server_send_error(req, + "409 Conflict", + connect_err[0] != '\0' ? connect_err : "connect failed"); + } + if (err == ESP_ERR_INVALID_ARG) { + return rest_server_send_error(req, + "400 Bad Request", + connect_err[0] != '\0' ? connect_err : "invalid connect params"); + } + return rest_server_send_error(req, + "500 Internal Server Error", + connect_err[0] != '\0' ? connect_err : "connect failed"); } cJSON *root = cJSON_CreateObject(); cJSON_AddBoolToObject(root, "ok", true); cJSON_AddStringToObject(root, "message", "connected"); + cJSON_AddStringToObject(root, "backend", printer_backend_str(backend)); + cJSON_AddBoolToObject(root, "backend_specified", backend_specified); fill_runtime_json(root); err = rest_server_send_json(req, "200 OK", root); diff --git a/components/control_plane/src/rest_server_print.c b/components/control_plane/src/rest_server_print.c index 54e075b..b664e86 100644 --- a/components/control_plane/src/rest_server_print.c +++ b/components/control_plane/src/rest_server_print.c @@ -18,27 +18,33 @@ esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req, uint16_t width, uint16_t height, const char *density, + bool direct_ignore_precheck, 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", + "print submit start, trace=%s, raster_bytes=%u, size=%ux%u, density=%s, ignore_precheck=%d", trace, (unsigned)raw_len, (unsigned)width, (unsigned)height, - density != NULL ? density : "中等"); + density != NULL ? density : "中等", + direct_ignore_precheck); char submit_err[128] = {0}; uint32_t job_id = 0; - esp_err_t submit_rc = printer_protocol_submit_raster_job(raw, - raw_len, - width, - height, - density, - &job_id, - submit_err, - sizeof(submit_err)); + printer_print_options_t options = { + .direct_ignore_precheck = direct_ignore_precheck, + }; + esp_err_t submit_rc = printer_protocol_submit_raster_job_ex(raw, + raw_len, + width, + height, + density, + &options, + &job_id, + submit_err, + sizeof(submit_err)); if (submit_rc != ESP_OK) { ESP_LOGW(TAG, "print submit failed, trace=%s, rc=0x%x, msg=%s", @@ -56,6 +62,7 @@ esp_err_t rest_server_print_submit_raster_job_and_reply(httpd_req_t *req, cJSON_AddBoolToObject(root, "ok", true); cJSON_AddNumberToObject(root, "job_id", job_id); cJSON_AddStringToObject(root, "state", "queued"); + cJSON_AddBoolToObject(root, "ignore_precheck", direct_ignore_precheck); if (warning != NULL && warning[0] != '\0') { cJSON_AddStringToObject(root, "warning", warning); } @@ -87,6 +94,7 @@ esp_err_t rest_server_print_raster_post(httpd_req_t *req) { cJSON *jencoding = cJSON_GetObjectItemCaseSensitive(json, "encoding"); cJSON *jdata = cJSON_GetObjectItemCaseSensitive(json, "data"); cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck"); if (!cJSON_IsNumber(jwidth) || !cJSON_IsNumber(jheight) || !cJSON_IsString(jencoding) || !cJSON_IsString(jdata)) { @@ -102,6 +110,7 @@ esp_err_t rest_server_print_raster_post(httpd_req_t *req) { const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) ? jdensity->valuestring : "中等"; + bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false); uint8_t *raw = NULL; size_t raw_len = 0; @@ -117,6 +126,7 @@ esp_err_t rest_server_print_raster_post(httpd_req_t *req) { (uint16_t)jwidth->valuedouble, (uint16_t)jheight->valuedouble, density, + direct_ignore_precheck, NULL, NULL); free(raw); diff --git a/components/control_plane/src/rest_server_print_image.c b/components/control_plane/src/rest_server_print_image.c index e9d124f..938981c 100644 --- a/components/control_plane/src/rest_server_print_image.c +++ b/components/control_plane/src/rest_server_print_image.c @@ -16,6 +16,7 @@ typedef struct { bool invert; uint16_t max_height; const char *density; + bool direct_ignore_precheck; } image_print_options_t; typedef struct { @@ -68,7 +69,8 @@ static void parse_image_upload_options(httpd_req_t *req, uint8_t *out_threshold, bool *out_invert, uint16_t *out_max_height, - const char **out_density) { + const char **out_density, + bool *out_direct_ignore_precheck) { if (out_scale_to_width != NULL) { *out_scale_to_width = true; } @@ -84,6 +86,9 @@ static void parse_image_upload_options(httpd_req_t *req, if (out_density != NULL) { *out_density = "中等"; } + if (out_direct_ignore_precheck != NULL) { + *out_direct_ignore_precheck = false; + } if (req == NULL) { return; @@ -138,6 +143,14 @@ static void parse_image_upload_options(httpd_req_t *req, *out_density = map_density_text(value); } + if (httpd_query_key_value(query, "ignore_precheck", value, sizeof(value)) == ESP_OK && + out_direct_ignore_precheck != NULL) { + bool parsed = *out_direct_ignore_precheck; + if (parse_bool_text(value, &parsed)) { + *out_direct_ignore_precheck = parsed; + } + } + free(query); } @@ -150,6 +163,7 @@ static void image_print_options_set_defaults(image_print_options_t *out) { out->invert = false; out->max_height = 2200; out->density = "中等"; + out->direct_ignore_precheck = false; } static void parse_image_print_options_from_json(cJSON *json, image_print_options_t *out) { @@ -162,6 +176,7 @@ static void parse_image_print_options_from_json(cJSON *json, image_print_options cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert"); cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); + cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck"); 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); @@ -174,6 +189,7 @@ static void parse_image_print_options_from_json(cJSON *json, image_print_options if (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) { out->density = map_density_text(jdensity->valuestring); } + out->direct_ignore_precheck = rest_server_json_bool_with_default(jignore, out->direct_ignore_precheck); } static bool parse_image_size_text(const char *text, uint32_t *out_w, uint32_t *out_h) { @@ -345,21 +361,24 @@ static esp_err_t rest_server_print_image_binary(httpd_req_t *req) { bool invert = false; uint16_t max_height = 2200; const char *density = "中等"; + bool direct_ignore_precheck = false; parse_image_upload_options(req, &scale_to_width, &threshold, &invert, &max_height, - &density); + &density, + &direct_ignore_precheck); ESP_LOGI(TAG, - "image process start (upload), png_bytes=%u, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s", + "image process start (upload), png_bytes=%u, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s, ignore_precheck=%d", (unsigned)req->content_len, scale_to_width, (unsigned)threshold, invert, (unsigned)max_height, - density != NULL ? density : "中等"); + density != NULL ? density : "中等", + direct_ignore_precheck); char *body = NULL; esp_err_t body_err = rest_server_read_body(req, &body); @@ -407,6 +426,7 @@ static esp_err_t rest_server_print_image_binary(httpd_req_t *req) { width, height, density, + direct_ignore_precheck, NULL, NULL); free(raster); @@ -458,12 +478,13 @@ static esp_err_t rest_server_print_image_generate(httpd_req_t *req) { (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", + "image process config, scale_to_width=%d, threshold=%u, invert=%d, max_height=%u, density=%s, ignore_precheck=%d", print_opt.scale_to_width, (unsigned)print_opt.threshold, print_opt.invert, (unsigned)print_opt.max_height, - print_opt.density != NULL ? print_opt.density : "中等"); + print_opt.density != NULL ? print_opt.density : "中等", + print_opt.direct_ignore_precheck); image_generation_request_t gen_req = { .prompt = gen_opt.prompt, @@ -553,6 +574,7 @@ static esp_err_t rest_server_print_image_generate(httpd_req_t *req) { width, height, print_opt.density, + print_opt.direct_ignore_precheck, warning, request_id); diff --git a/components/control_plane/src/rest_server_print_render.c b/components/control_plane/src/rest_server_print_render.c index c6342fa..45312b3 100644 --- a/components/control_plane/src/rest_server_print_render.c +++ b/components/control_plane/src/rest_server_print_render.c @@ -159,6 +159,7 @@ esp_err_t rest_server_print_qr_post(httpd_req_t *req) { cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "module_scale"); cJSON *jmargin = cJSON_GetObjectItemCaseSensitive(json, "margin_modules"); cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck"); if (!cJSON_IsString(jtext) || jtext->valuestring == NULL || jtext->valuestring[0] == '\0') { cJSON_Delete(json); @@ -168,6 +169,7 @@ esp_err_t rest_server_print_qr_post(httpd_req_t *req) { const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) ? jdensity->valuestring : "中等"; + bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false); uint8_t ecc_level = 3; /* default H */ if (cJSON_IsString(jecc) && jecc->valuestring != NULL) { @@ -224,6 +226,7 @@ esp_err_t rest_server_print_qr_post(httpd_req_t *req) { width, height, density, + direct_ignore_precheck, NULL, NULL); free(raster); @@ -250,6 +253,7 @@ esp_err_t rest_server_print_label_post(httpd_req_t *req) { cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); cJSON *jgap = cJSON_GetObjectItemCaseSensitive(json, "gap_move_before"); cJSON *joffset = cJSON_GetObjectItemCaseSensitive(json, "offset_tenths_mm"); + cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck"); bool gap_move_before = rest_server_json_bool_with_default(jgap, true); bool has_offset = cJSON_IsNumber(joffset); @@ -287,6 +291,7 @@ esp_err_t rest_server_print_label_post(httpd_req_t *req) { const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) ? jdensity->valuestring : "中等"; + bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false); uint16_t width = 0; uint16_t height = 0; @@ -313,6 +318,7 @@ esp_err_t rest_server_print_label_post(httpd_req_t *req) { width, height, density, + direct_ignore_precheck, NULL, NULL); free(raster); @@ -341,6 +347,7 @@ esp_err_t rest_server_print_text_post(httpd_req_t *req) { cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); cJSON *jline = cJSON_GetObjectItemCaseSensitive(json, "line_spacing"); cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height"); + cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck"); if (!cJSON_IsString(jtext) || jtext->valuestring == NULL) { cJSON_Delete(json); @@ -350,6 +357,7 @@ esp_err_t rest_server_print_text_post(httpd_req_t *req) { const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) ? jdensity->valuestring : "中等"; + bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false); uint8_t scale = 2; uint8_t line_spacing = 2; uint16_t max_height = 2000; @@ -394,6 +402,7 @@ esp_err_t rest_server_print_text_post(httpd_req_t *req) { width, height, density, + direct_ignore_precheck, render_msg, NULL); free(raster); @@ -422,6 +431,7 @@ esp_err_t rest_server_print_receipt_post(httpd_req_t *req) { cJSON *jfooter = cJSON_GetObjectItemCaseSensitive(json, "footer"); cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density"); cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale"); + cJSON *jignore = cJSON_GetObjectItemCaseSensitive(json, "ignore_precheck"); const char *title = (cJSON_IsString(jtitle) && jtitle->valuestring != NULL) ? jtitle->valuestring @@ -432,6 +442,7 @@ esp_err_t rest_server_print_receipt_post(httpd_req_t *req) { const char *density = (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) ? jdensity->valuestring : "中等"; + bool direct_ignore_precheck = rest_server_json_bool_with_default(jignore, false); uint8_t scale = 2; if (cJSON_IsNumber(jscale) && jscale->valuedouble >= 1 && jscale->valuedouble <= 4) { scale = (uint8_t)jscale->valuedouble; @@ -516,6 +527,7 @@ esp_err_t rest_server_print_receipt_post(httpd_req_t *req) { width, height, density, + direct_ignore_precheck, render_msg, NULL); free(raster); diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index 0b6bafa..fa32c95 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -130,14 +130,44 @@ typedef struct { uint8_t patch; } printer_ota_version_t; +typedef enum { + PRINTER_BACKEND_BLE = 0, + PRINTER_BACKEND_DIRECT = 1, +} printer_backend_t; + typedef struct { + printer_backend_t backend; + const char *name; + uint32_t timeout_ms; +} printer_connect_options_t; + +typedef struct { + // Direct backend only: bypass paper/temp/battery precheck for this print job. + bool direct_ignore_precheck; +} printer_print_options_t; + +typedef struct { + bool supports_connect; + bool supports_gap_move; + bool supports_label_offset; + bool supports_ota; +} printer_capabilities_t; + +typedef struct { + printer_backend_t backend; bool connected; + bool transport_ready; bool notify_ready; bool busy; bool has_paper; + int8_t paper_gpio_level; + uint8_t paper_present_level; uint8_t battery_percent; float temperature; uint16_t mtu; + bool supports_gap_move; + bool supports_label_offset; + bool supports_ota; uint32_t queue_depth; int64_t last_status_ms; } printer_runtime_status_t; @@ -147,8 +177,13 @@ typedef uint32_t printer_status_poll_pause_token_t; esp_err_t printer_protocol_init(void); esp_err_t printer_protocol_stop(uint32_t timeout_ms); +esp_err_t printer_protocol_set_backend(printer_backend_t backend, char *err, size_t err_len); +printer_backend_t printer_protocol_get_backend(void); + esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms); +esp_err_t printer_protocol_connect_ex(const printer_connect_options_t *opt, char *err, size_t err_len); void printer_protocol_disconnect(void); +void printer_protocol_get_capabilities(printer_capabilities_t *out_capabilities); void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status); printer_status_poll_pause_token_t printer_protocol_status_poll_pause_acquire(void); @@ -162,6 +197,15 @@ esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, uint32_t *out_job_id, char *err, size_t err_len); +esp_err_t printer_protocol_submit_raster_job_ex(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + const char *density, + const printer_print_options_t *options, + uint32_t *out_job_id, + char *err, + size_t err_len); bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info); esp_err_t printer_protocol_list_jobs(print_job_info_t *out_jobs, size_t max_jobs, size_t *out_count); diff --git a/components/domain/internal/printer_protocol_internal.h b/components/domain/internal/printer_protocol_internal.h index 4c43032..bb26c7d 100644 --- a/components/domain/internal/printer_protocol_internal.h +++ b/components/domain/internal/printer_protocol_internal.h @@ -44,6 +44,7 @@ typedef struct { bool used; bool cancel_requested; + bool direct_ignore_precheck; uint32_t id; print_job_state_t state; uint8_t progress; @@ -60,6 +61,8 @@ typedef struct { typedef struct { bool has_paper; + int8_t paper_gpio_level; + uint8_t paper_present_level; uint8_t battery; float temperature; int64_t updated_ms; diff --git a/components/domain/src/printer_protocol.c b/components/domain/src/printer_protocol.c index 3c3b627..adb3038 100644 --- a/components/domain/src/printer_protocol.c +++ b/components/domain/src/printer_protocol.c @@ -21,6 +21,8 @@ uint32_t s_status_poll_pause_tokens[STATUS_POLL_PAUSE_SLOT_MAX]; parsed_status_t s_status = { .has_paper = true, + .paper_gpio_level = -1, + .paper_present_level = 0, .battery = 100, .temperature = 25.0f, .updated_ms = 0, @@ -36,6 +38,8 @@ TaskHandle_t s_worker_task; TaskHandle_t s_status_poll_task_handle; bool s_protocol_initialized; bool s_protocol_stopping; +printer_backend_t s_backend = PRINTER_BACKEND_BLE; +bool s_ble_client_initialized; static const char *TAG = "printer_protocol"; @@ -45,6 +49,61 @@ static const char *TAG = "printer_protocol"; #define PRINT_WORKER_CORE_ID 1 #endif +static void on_rx_frame(const uint8_t *data, size_t len); + +static void backend_capabilities_for(printer_backend_t backend, printer_capabilities_t *out_caps) { + if (out_caps == NULL) { + return; + } + memset(out_caps, 0, sizeof(*out_caps)); + out_caps->supports_connect = true; + out_caps->supports_gap_move = true; + if (backend == PRINTER_BACKEND_BLE) { + out_caps->supports_label_offset = true; + out_caps->supports_ota = true; + } +} + +static esp_err_t ensure_ble_client_ready(void) { + if (s_ble_client_initialized) { + return ESP_OK; + } + esp_err_t err = ble_printer_client_init(on_rx_frame); + if (err == ESP_OK) { + s_ble_client_initialized = true; + } + return err; +} + +static esp_err_t ensure_backend_ready(printer_backend_t backend) { + if (backend == PRINTER_BACKEND_BLE) { + return ensure_ble_client_ready(); + } + if (backend == PRINTER_BACKEND_DIRECT) { + if (!runtime_policy_direct_printer_enabled()) { + return ESP_ERR_NOT_SUPPORTED; + } + return platform_direct_printer_init(); + } + return ESP_ERR_NOT_SUPPORTED; +} + +static void refresh_direct_status_locked(void) { + if (s_backend != PRINTER_BACKEND_DIRECT) { + return; + } + + platform_printer_sensors_t sensors = {0}; + if (platform_direct_printer_get_sensors(&sensors) == ESP_OK) { + s_status.has_paper = sensors.has_paper; + s_status.paper_gpio_level = sensors.paper_gpio_level; + s_status.paper_present_level = sensors.paper_present_level; + s_status.battery = sensors.battery_percent; + s_status.temperature = sensors.temperature_c; + s_status.updated_ms = sensors.updated_ms; + } +} + static size_t status_poll_pause_depth_locked(void) { size_t depth = 0; for (size_t i = 0; i < STATUS_POLL_PAUSE_SLOT_MAX; ++i) { @@ -77,6 +136,8 @@ static void reset_runtime_state_locked(void) { s_status_poll_pause_next_token = 1; s_status.has_paper = true; + s_status.paper_gpio_level = -1; + s_status.paper_present_level = 0; s_status.battery = 100; s_status.temperature = 25.0f; s_status.updated_ms = 0; @@ -260,6 +321,9 @@ esp_err_t printer_protocol_send_frame(uint8_t cmd, if (!s_protocol_initialized || printer_protocol_is_stopping()) { return ESP_ERR_INVALID_STATE; } + if (s_backend != PRINTER_BACKEND_BLE) { + return ESP_ERR_NOT_SUPPORTED; + } uint8_t frame[260]; size_t total = 4 + payload_len + (with_checksum ? 1 : 0); @@ -440,10 +504,12 @@ static void status_poll_task(void *arg) { while (true) { bool should_stop = false; bool can_poll = false; + printer_backend_t backend = PRINTER_BACKEND_BLE; if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { should_stop = s_protocol_stopping; can_poll = (s_busy_refcnt == 0) && (status_poll_pause_depth_locked() == 0); + backend = s_backend; xSemaphoreGive(s_mutex); } @@ -451,9 +517,19 @@ static void status_poll_task(void *arg) { break; } - if (ble_printer_client_is_connected() && can_poll) { - uint8_t dummy = 0x00; - (void)printer_protocol_send_frame(CMD_GET_STATUS, &dummy, 0, true); + if (can_poll) { + if (backend == PRINTER_BACKEND_BLE) { + if (ble_printer_client_is_connected()) { + uint8_t dummy = 0x00; + (void)printer_protocol_send_frame(CMD_GET_STATUS, &dummy, 0, true); + } + } else if (backend == PRINTER_BACKEND_DIRECT) { + if (platform_direct_printer_is_connected() && + xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + refresh_direct_status_locked(); + xSemaphoreGive(s_mutex); + } + } } vTaskDelay(pdMS_TO_TICKS(runtime_policy_printer_status_poll_interval_ms())); } @@ -492,6 +568,8 @@ static void on_rx_frame(const uint8_t *data, size_t len) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { s_status.has_paper = has_paper; + s_status.paper_gpio_level = -1; + s_status.paper_present_level = 0; s_status.battery = battery; s_status.temperature = temperature; s_status.updated_ms = esp_timer_get_time() / 1000; @@ -564,8 +642,12 @@ esp_err_t printer_protocol_init(void) { runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, 0); s_protocol_initialized = true; s_protocol_stopping = false; + s_ble_client_initialized = false; + s_backend = (runtime_policy_printer_default_backend() == 1 && runtime_policy_direct_printer_enabled()) + ? PRINTER_BACKEND_DIRECT + : PRINTER_BACKEND_BLE; - esp_err_t err = ble_printer_client_init(on_rx_frame); + esp_err_t err = ensure_backend_ready(s_backend); if (err != ESP_OK) { s_protocol_initialized = false; destroy_runtime_objects(); @@ -630,6 +712,7 @@ esp_err_t printer_protocol_stop(uint32_t timeout_ms) { } ble_printer_client_disconnect(); + platform_direct_printer_disconnect(); bool worker_done = (s_worker_task == NULL); bool poll_done = (s_status_poll_task_handle == NULL); @@ -673,23 +756,166 @@ esp_err_t printer_protocol_stop(uint32_t timeout_ms) { s_status_poll_task_handle = NULL; s_protocol_initialized = false; s_protocol_stopping = false; + s_ble_client_initialized = false; + s_backend = PRINTER_BACKEND_BLE; runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, 0); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, 0); + platform_direct_printer_deinit(); + ESP_LOGI(TAG, "printer protocol stopped"); return (worker_done && poll_done) ? ESP_OK : ESP_ERR_TIMEOUT; } -esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms) { - if (!s_protocol_initialized || s_protocol_stopping) { +esp_err_t printer_protocol_set_backend(printer_backend_t backend, char *err, size_t err_len) { + if (backend != PRINTER_BACKEND_BLE && backend != PRINTER_BACKEND_DIRECT) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "invalid backend"); + } + return ESP_ERR_INVALID_ARG; + } + if (backend == PRINTER_BACKEND_DIRECT && !runtime_policy_direct_printer_enabled()) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "direct backend disabled"); + } + return ESP_ERR_NOT_SUPPORTED; + } + if (!s_protocol_initialized || s_protocol_stopping || s_mutex == NULL) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer protocol unavailable"); + } return ESP_ERR_INVALID_STATE; } - return ble_printer_client_connect(target_name, timeout_ms); + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(runtime_policy_printer_control_lock_timeout_ms())) != pdTRUE) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "lock timeout"); + } + return ESP_ERR_TIMEOUT; + } + + printer_backend_t old_backend = s_backend; + if (old_backend == backend) { + xSemaphoreGive(s_mutex); + return ESP_OK; + } + + if (s_busy_refcnt != 0 || (s_job_queue != NULL && uxQueueMessagesWaiting(s_job_queue) > 0)) { + xSemaphoreGive(s_mutex); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer busy"); + } + return ESP_ERR_INVALID_STATE; + } + + s_backend = backend; + xSemaphoreGive(s_mutex); + + ble_printer_client_disconnect(); + platform_direct_printer_disconnect(); + + esp_err_t rc = ensure_backend_ready(backend); + if (rc != ESP_OK) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + s_backend = old_backend; + xSemaphoreGive(s_mutex); + } + (void)ensure_backend_ready(old_backend); + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "backend init failed"); + } + return rc; + } + + if (backend == PRINTER_BACKEND_DIRECT && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + refresh_direct_status_locked(); + xSemaphoreGive(s_mutex); + } + return ESP_OK; +} + +printer_backend_t printer_protocol_get_backend(void) { + if (!s_protocol_initialized || s_mutex == NULL) { + return s_backend; + } + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + return s_backend; + } + printer_backend_t backend = s_backend; + xSemaphoreGive(s_mutex); + return backend; +} + +esp_err_t printer_protocol_connect(const char *target_name, uint32_t timeout_ms) { + printer_connect_options_t opt = { + .backend = printer_protocol_get_backend(), + .name = target_name, + .timeout_ms = timeout_ms, + }; + return printer_protocol_connect_ex(&opt, NULL, 0); +} + +esp_err_t printer_protocol_connect_ex(const printer_connect_options_t *opt, char *err, size_t err_len) { + if (!s_protocol_initialized || s_protocol_stopping) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "printer protocol unavailable"); + } + return ESP_ERR_INVALID_STATE; + } + + printer_backend_t backend = printer_protocol_get_backend(); + const char *name = NULL; + uint32_t timeout_ms = 0; + if (opt != NULL) { + backend = opt->backend; + name = opt->name; + timeout_ms = opt->timeout_ms; + } + + esp_err_t rc = printer_protocol_set_backend(backend, err, err_len); + if (rc != ESP_OK) { + return rc; + } + + if (backend == PRINTER_BACKEND_BLE) { + rc = ensure_backend_ready(PRINTER_BACKEND_BLE); + if (rc != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "ble backend unavailable"); + } + return rc; + } + return ble_printer_client_connect(name, timeout_ms); + } + + if (backend == PRINTER_BACKEND_DIRECT) { + rc = ensure_backend_ready(PRINTER_BACKEND_DIRECT); + if (rc != ESP_OK) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "direct backend unavailable"); + } + return rc; + } + return platform_direct_printer_connect(timeout_ms); + } + + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "unsupported backend"); + } + return ESP_ERR_NOT_SUPPORTED; } void printer_protocol_disconnect(void) { ble_printer_client_disconnect(); + platform_direct_printer_disconnect(); +} + +void printer_protocol_get_capabilities(printer_capabilities_t *out_capabilities) { + if (out_capabilities == NULL) { + return; + } + backend_capabilities_for(printer_protocol_get_backend(), out_capabilities); } void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { @@ -698,17 +924,47 @@ void printer_protocol_get_runtime_status(printer_runtime_status_t *out_status) { } memset(out_status, 0, sizeof(*out_status)); + out_status->paper_gpio_level = -1; + out_status->backend = printer_protocol_get_backend(); - ble_link_state_t link = {0}; - ble_printer_client_get_link_state(&link); + printer_capabilities_t caps = {0}; + backend_capabilities_for(out_status->backend, &caps); + out_status->supports_gap_move = caps.supports_gap_move; + out_status->supports_label_offset = caps.supports_label_offset; + out_status->supports_ota = caps.supports_ota; - out_status->connected = link.connected; - out_status->notify_ready = link.notify_ready; - out_status->mtu = link.mtu; + if (out_status->backend == PRINTER_BACKEND_BLE) { + ble_link_state_t link = {0}; + ble_printer_client_get_link_state(&link); + out_status->connected = link.connected; + out_status->notify_ready = link.notify_ready; + out_status->transport_ready = link.connected && link.notify_ready; + out_status->mtu = link.mtu; + } else if (out_status->backend == PRINTER_BACKEND_DIRECT) { + out_status->connected = platform_direct_printer_is_connected(); + out_status->transport_ready = out_status->connected; + out_status->notify_ready = false; + out_status->mtu = 0; + + platform_printer_sensors_t sensors = {0}; + if (platform_direct_printer_get_sensors(&sensors) == ESP_OK && + s_mutex != NULL && + xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + s_status.has_paper = sensors.has_paper; + s_status.paper_gpio_level = sensors.paper_gpio_level; + s_status.paper_present_level = sensors.paper_present_level; + s_status.battery = sensors.battery_percent; + s_status.temperature = sensors.temperature_c; + s_status.updated_ms = sensors.updated_ms; + xSemaphoreGive(s_mutex); + } + } if (s_mutex != NULL && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { out_status->busy = s_busy_refcnt > 0; out_status->has_paper = s_status.has_paper; + out_status->paper_gpio_level = s_status.paper_gpio_level; + out_status->paper_present_level = s_status.paper_present_level; out_status->battery_percent = s_status.battery; out_status->temperature = s_status.temperature; out_status->last_status_ms = s_status.updated_ms; diff --git a/components/domain/src/printer_protocol_commands.c b/components/domain/src/printer_protocol_commands.c index 18a2984..970871f 100644 --- a/components/domain/src/printer_protocol_commands.c +++ b/components/domain/src/printer_protocol_commands.c @@ -4,8 +4,22 @@ #include #include +static bool is_direct_backend(void) { + return printer_protocol_get_backend() == PRINTER_BACKEND_DIRECT; +} + +static esp_err_t unsupported_for_direct(char *err, size_t err_len, const char *op) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "%s not supported on direct backend", op); + } + return ESP_ERR_NOT_SUPPORTED; +} esp_err_t printer_protocol_gap_move(uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return platform_direct_printer_gap_move(timeout_ms, err, err_len); + } + if (!ble_printer_client_is_connected()) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer not connected"); @@ -34,6 +48,10 @@ esp_err_t printer_protocol_gap_move(uint32_t timeout_ms, char *err, size_t err_l } esp_err_t printer_protocol_get_label_offset(uint8_t *out_offset, uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "label offset"); + } + if (out_offset == NULL) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "invalid args"); @@ -88,6 +106,10 @@ esp_err_t printer_protocol_get_label_offset(uint8_t *out_offset, uint32_t timeou } esp_err_t printer_protocol_set_label_offset(uint8_t offset, uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "label offset"); + } + if (!ble_printer_client_is_connected()) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer not connected"); @@ -115,6 +137,10 @@ esp_err_t printer_protocol_set_label_offset(uint8_t offset, uint32_t timeout_ms, } esp_err_t printer_protocol_ota_jump_boot(uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "ota jump boot"); + } + if (!ble_printer_client_is_connected()) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer not connected"); @@ -142,6 +168,10 @@ esp_err_t printer_protocol_ota_jump_boot(uint32_t timeout_ms, char *err, size_t } esp_err_t printer_protocol_ota_jump_app(uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "ota jump app"); + } + if (!ble_printer_client_is_connected()) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer not connected"); @@ -169,6 +199,10 @@ esp_err_t printer_protocol_ota_jump_app(uint32_t timeout_ms, char *err, size_t e } esp_err_t printer_protocol_ota_erase_page(uint16_t page_num, uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "ota erase"); + } + if (!ble_printer_client_is_connected()) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "printer not connected"); @@ -207,6 +241,10 @@ esp_err_t printer_protocol_ota_write_frame(uint16_t packet_num, uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "ota write"); + } + if ((data_len > 0 && data == NULL) || data_len > OTA_MAX_DATA_PER_FRAME) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "invalid frame length"); @@ -252,6 +290,10 @@ esp_err_t printer_protocol_ota_get_version(printer_ota_version_t *out_version, uint32_t timeout_ms, char *err, size_t err_len) { + if (is_direct_backend()) { + return unsupported_for_direct(err, err_len, "ota version"); + } + if (out_version == NULL) { if (err != NULL && err_len > 0) { snprintf(err, err_len, "invalid args"); diff --git a/components/domain/src/printer_protocol_jobs.c b/components/domain/src/printer_protocol_jobs.c index aab88ab..dcbff03 100644 --- a/components/domain/src/printer_protocol_jobs.c +++ b/components/domain/src/printer_protocol_jobs.c @@ -48,14 +48,15 @@ bool printer_protocol_is_terminal_state(print_job_state_t state) { 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) { +esp_err_t printer_protocol_submit_raster_job_ex(const uint8_t *raster, + size_t raster_len, + uint16_t width, + uint16_t height, + const char *density, + const printer_print_options_t *options, + 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"); @@ -134,9 +135,11 @@ esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, s_jobs[idx].width = width; s_jobs[idx].height = height; s_jobs[idx].data_len = raster_len; + s_jobs[idx].direct_ignore_precheck = (options != NULL && options->direct_ignore_precheck); 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)); + bool ignore_precheck = s_jobs[idx].direct_ignore_precheck; xSemaphoreGive(s_mutex); @@ -166,12 +169,13 @@ esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, 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", + "job queued, job_id=%u, raster_bytes=%u, size=%ux%u, density=%s, ignore_precheck=%d, queue_depth=%u", (unsigned)id, (unsigned)raster_len, (unsigned)width, (unsigned)height, density != NULL ? density : "中等", + ignore_precheck, (unsigned)queue_depth); runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_PRINTER_JOB_SUBMITTED, 1); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, (int32_t)queue_depth); @@ -179,6 +183,25 @@ esp_err_t printer_protocol_submit_raster_job(const uint8_t *raster, return ESP_OK; } +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) { + return printer_protocol_submit_raster_job_ex(raster, + raster_len, + width, + height, + density, + NULL, + out_job_id, + err, + err_len); +} + bool printer_protocol_get_job(uint32_t job_id, print_job_info_t *out_info) { if (out_info == NULL || job_id == 0) { return false; diff --git a/components/domain/src/printer_protocol_worker.c b/components/domain/src/printer_protocol_worker.c index f30ccf5..d703d41 100644 --- a/components/domain/src/printer_protocol_worker.c +++ b/components/domain/src/printer_protocol_worker.c @@ -15,6 +15,10 @@ static const char *TAG = "printer_protocol"; static bool request_status_sync(uint32_t timeout_ms) { int64_t old_ms; + if (printer_protocol_get_backend() != PRINTER_BACKEND_BLE) { + return false; + } + if (printer_protocol_is_stopping()) { return false; } @@ -72,64 +76,82 @@ static uint16_t density_to_hot_time(const char *density) { 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; +static bool precheck_printer_ready(bool direct_ignore_precheck, char *err, size_t err_len) { + if (printer_protocol_get_backend() == PRINTER_BACKEND_BLE) { + 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; } - parsed_status_t status; - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) != pdTRUE) { - snprintf(err, err_len, "status lock failed"); - return false; + if (direct_ignore_precheck) { + return true; } - status = s_status; - xSemaphoreGive(s_mutex); - - if (!status.has_paper) { + platform_printer_sensors_t sensors = {0}; + if (platform_direct_printer_get_sensors(&sensors) != ESP_OK) { + snprintf(err, err_len, "sensor read failed"); + return false; + } + if (!sensors.has_paper) { snprintf(err, err_len, "printer out of paper"); return false; } - if (status.battery <= 40) { + float temp_min = runtime_policy_direct_printer_temp_min_c(); + float temp_max = runtime_policy_direct_printer_temp_max_c(); + if (temp_max <= temp_min) { + temp_max = temp_min + 1.0f; + } + if (sensors.temperature_c < temp_min || sensors.temperature_c > temp_max) { + snprintf(err, err_len, "temperature out of range"); + return false; + } + + if (sensors.battery_percent < runtime_policy_direct_printer_battery_min_percent()) { 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) { +static bool run_print_job_ble(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 (printer_protocol_is_stopping()) { - snprintf(job->error, sizeof(job->error), "protocol stopping"); - return false; - } - 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))) { + if (!precheck_printer_ready(false, 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; @@ -232,6 +254,81 @@ static bool run_print_job(job_slot_t *job) { return true; } +static bool run_print_job_direct(job_slot_t *job) { + if (!platform_direct_printer_is_connected()) { + snprintf(job->error, sizeof(job->error), "printer not connected"); + return false; + } + + char check_err[96]; + if (!precheck_printer_ready(job->direct_ignore_precheck, check_err, sizeof(check_err))) { + snprintf(job->error, sizeof(job->error), "%s", check_err); + return false; + } + + platform_direct_print_request_t req = { + .width = job->width, + .height = job->height, + .raster = job->data, + .raster_len = job->data_len, + .strobe_on_time_us = density_to_hot_time(job->density), + .strobe_interval_us = runtime_policy_direct_printer_strobe_interval_us(), + .motor_step_delay_us = runtime_policy_direct_printer_motor_step_us(), + .motor_steps_per_line = runtime_policy_direct_printer_steps_per_line(), + .timeout_ms = runtime_policy_direct_printer_operation_timeout_ms(), + .ignore_precheck = job->direct_ignore_precheck, + .cancel_flag = &job->cancel_requested, + }; + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + job->progress = 10; + xSemaphoreGive(s_mutex); + } + + char direct_err[96] = {0}; + esp_err_t rc = platform_direct_printer_print(&req, direct_err, sizeof(direct_err)); + if (rc != ESP_OK) { + if (direct_err[0] != '\0') { + snprintf(job->error, sizeof(job->error), "%s", direct_err); + } else if (rc == ESP_ERR_TIMEOUT) { + snprintf(job->error, sizeof(job->error), "print timeout"); + } else if (rc == ESP_ERR_INVALID_STATE && job->cancel_requested) { + snprintf(job->error, sizeof(job->error), "job canceled"); + } else { + snprintf(job->error, sizeof(job->error), "direct print failed"); + } + return false; + } + + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(200)) == pdTRUE) { + job->progress = 95; + xSemaphoreGive(s_mutex); + } + return true; +} + +static bool run_print_job(job_slot_t *job) { + ESP_LOGI(TAG, + "job %u print start, backend=%s, raster_bytes=%u, size=%ux%u, density=%s, ignore_precheck=%d", + (unsigned)job->id, + printer_protocol_get_backend() == PRINTER_BACKEND_BLE ? "ble" : "direct", + (unsigned)job->data_len, + (unsigned)job->width, + (unsigned)job->height, + job->density, + job->direct_ignore_precheck); + + if (printer_protocol_is_stopping()) { + snprintf(job->error, sizeof(job->error), "protocol stopping"); + return false; + } + + if (printer_protocol_get_backend() == PRINTER_BACKEND_BLE) { + return run_print_job_ble(job); + } + return run_print_job_direct(job); +} + void printer_protocol_worker_task(void *arg) { (void)arg; diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index ba0704c..1efdb67 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -3,6 +3,7 @@ idf_component_register( "src/platform_bootstrap.c" "src/wifi_manager.c" "src/ble_printer_client.c" + "src/direct_thermal_printer.c" "src/display_st7789.c" "src/voice_audio.c" "src/runtime_policy.c" @@ -19,6 +20,7 @@ idf_component_register( esp_event nvs_flash mbedtls + esp_adc esp_lcd esp_codec_dev esp_audio_codec diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 2b0d049..6dee0b4 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -37,6 +37,41 @@ bool ble_printer_client_is_connected(void); void ble_printer_client_get_link_state(ble_link_state_t *out_state); esp_err_t ble_printer_client_write(const uint8_t *data, size_t len); +// ---------- direct_thermal_printer ---------- +typedef struct { + bool has_paper; + int8_t paper_gpio_level; + uint8_t paper_present_level; + uint8_t battery_percent; + float temperature_c; + int64_t updated_ms; +} platform_printer_sensors_t; + +typedef struct { + uint16_t width; + uint16_t height; + const uint8_t *raster; + size_t raster_len; + uint16_t strobe_on_time_us; + uint16_t strobe_interval_us; + uint16_t motor_step_delay_us; + uint8_t motor_steps_per_line; + uint32_t timeout_ms; + bool ignore_precheck; + const volatile bool *cancel_flag; +} platform_direct_print_request_t; + +esp_err_t platform_direct_printer_init(void); +void platform_direct_printer_deinit(void); +esp_err_t platform_direct_printer_connect(uint32_t timeout_ms); +void platform_direct_printer_disconnect(void); +bool platform_direct_printer_is_connected(void); +esp_err_t platform_direct_printer_get_sensors(platform_printer_sensors_t *out_sensors); +esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *request, + char *err, + size_t err_len); +esp_err_t platform_direct_printer_gap_move(uint32_t timeout_ms, char *err, size_t err_len); + // ---------- display_st7789 ---------- esp_err_t platform_display_init(void); esp_err_t platform_display_show_test_pattern(void); @@ -79,6 +114,26 @@ uint32_t runtime_policy_printer_worker_queue_wait_ms(void); uint32_t runtime_policy_printer_queue_retry_delay_ms(void); uint32_t runtime_policy_printer_status_poll_interval_ms(void); uint32_t runtime_policy_printer_stop_timeout_ms(void); +uint8_t runtime_policy_printer_default_backend(void); + +bool runtime_policy_direct_printer_enabled(void); +uint32_t runtime_policy_direct_printer_operation_timeout_ms(void); +uint16_t runtime_policy_direct_printer_strobe_on_us(void); +uint16_t runtime_policy_direct_printer_strobe_interval_us(void); +uint16_t runtime_policy_direct_printer_motor_step_us(void); +bool runtime_policy_direct_printer_motor_reverse(void); +uint8_t runtime_policy_direct_printer_steps_per_line(void); +uint16_t runtime_policy_direct_printer_gap_steps(void); +float runtime_policy_direct_printer_temp_min_c(void); +float runtime_policy_direct_printer_temp_max_c(void); +uint8_t runtime_policy_direct_printer_battery_min_percent(void); +uint8_t runtime_policy_direct_printer_paper_present_level(void); +uint16_t runtime_policy_direct_printer_battery_empty_mv(void); +uint16_t runtime_policy_direct_printer_battery_full_mv(void); +uint16_t runtime_policy_direct_printer_battery_divider_ratio_x1000(void); +uint32_t runtime_policy_direct_printer_ntc_pullup_ohms(void); +uint32_t runtime_policy_direct_printer_ntc_r25_ohms(void); +uint32_t runtime_policy_direct_printer_ntc_beta(void); uint32_t runtime_policy_rest_printer_connect_timeout_ms(void); uint32_t runtime_policy_rest_label_timeout_ms(void); diff --git a/components/platform/src/direct_thermal_printer.c b/components/platform/src/direct_thermal_printer.c new file mode 100644 index 0000000..8c446ef --- /dev/null +++ b/components/platform/src/direct_thermal_printer.c @@ -0,0 +1,769 @@ +#include "platform.h" + +#include +#include +#include + +#include "driver/gpio.h" +#include "esp_adc/adc_oneshot.h" +#include "esp_log.h" +#include "esp_rom_sys.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "sdkconfig.h" + +#ifndef CONFIG_TQ_DIRECT_PRINTER_ENABLE +#define CONFIG_TQ_DIRECT_PRINTER_ENABLE 1 +#endif + +#ifndef CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH +#define CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH 1 +#endif + +#if CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH +#define BOOST_ON_LEVEL 1 +#define BOOST_OFF_LEVEL 0 +#else +#define BOOST_ON_LEVEL 0 +#define BOOST_OFF_LEVEL 1 +#endif + +#define ADC_RAW_MAX 4095 + +static const char *TAG = "direct_printer"; +static const uint8_t k_print_yield_lines = 8; +static const uint16_t k_gap_yield_steps = 32; + +typedef struct { + bool ready; + adc_unit_t unit; + adc_channel_t channel; +} adc_pin_t; + +typedef struct { + bool initialized; + bool connected; + uint8_t motor_phase; + SemaphoreHandle_t lock; + adc_oneshot_unit_handle_t adc1_handle; + adc_oneshot_unit_handle_t adc2_handle; + adc_pin_t battery_adc; + adc_pin_t ntc_adc; +} direct_printer_state_t; + +static direct_printer_state_t s_state; + +static const uint8_t k_motor_step_table[4] = {0x05, 0x09, 0x0A, 0x06}; + +static bool is_gpio_valid(int gpio_num) { + return gpio_num >= 0; +} + +static void write_err(char *err, size_t err_len, const char *text) { + if (err != NULL && err_len > 0) { + strlcpy(err, text, err_len); + } +} + +static void set_gpio_level_if_valid(int gpio_num, int level) { + if (!is_gpio_valid(gpio_num)) { + return; + } + (void)gpio_set_level(gpio_num, level); +} + +static esp_err_t config_output_pin(int gpio_num, int level, const char *name) { + if (!is_gpio_valid(gpio_num)) { + return ESP_OK; + } + + gpio_config_t cfg = { + .pin_bit_mask = 1ULL << gpio_num, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t err = gpio_config(&cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "gpio_config output failed: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err)); + return err; + } + err = gpio_set_level(gpio_num, level); + if (err != ESP_OK) { + ESP_LOGE(TAG, "gpio_set_level failed: %s pin=%d level=%d err=%s", name, gpio_num, level, esp_err_to_name(err)); + } + return err; +} + +static esp_err_t config_input_pin(int gpio_num, bool pull_up, const char *name) { + if (!is_gpio_valid(gpio_num)) { + return ESP_OK; + } + + gpio_config_t cfg = { + .pin_bit_mask = 1ULL << gpio_num, + .mode = GPIO_MODE_INPUT, + .pull_up_en = pull_up ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t err = gpio_config(&cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "gpio_config input failed: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err)); + } + return err; +} + +static void set_stb_level_locked(int level) { + set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_12_PIN, level); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_34_PIN, level); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_56_PIN, level); +} + +static void motor_apply_pattern_locked(uint8_t pattern) { + set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTA_P_PIN, (pattern >> 0) & 0x01); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTA_N_PIN, (pattern >> 1) & 0x01); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTB_P_PIN, (pattern >> 2) & 0x01); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_OUTB_N_PIN, (pattern >> 3) & 0x01); +} + +static void motor_off_locked(void) { + motor_apply_pattern_locked(0x00); +} + +static void safe_drive_off_locked(bool keep_boost_on) { + set_stb_level_locked(0); + set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_MOSI_PIN, 0); + set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 1); + motor_off_locked(); + if (!keep_boost_on) { + set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, BOOST_OFF_LEVEL); + } +} + +static void pulse_latch_locked(void) { + set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 0); + esp_rom_delay_us(1); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 1); +} + +static void fire_strobe_locked(uint16_t on_us, uint16_t interval_us) { + set_stb_level_locked(1); + esp_rom_delay_us(on_us); + set_stb_level_locked(0); + if (interval_us > 0) { + esp_rom_delay_us(interval_us); + } +} + +static void motor_step_once_locked(uint16_t step_delay_us) { + uint8_t pattern = k_motor_step_table[s_state.motor_phase & 0x03]; + motor_apply_pattern_locked(pattern); + if (step_delay_us > 0) { + esp_rom_delay_us(step_delay_us); + } + if (runtime_policy_direct_printer_motor_reverse()) { + s_state.motor_phase = (uint8_t)((s_state.motor_phase + 3) & 0x03); + } else { + s_state.motor_phase = (uint8_t)((s_state.motor_phase + 1) & 0x03); + } +} + +static void write_line_bits_locked(const uint8_t *line, size_t line_bytes) { + for (size_t i = 0; i < line_bytes; ++i) { + uint8_t byte = line[i]; + for (int bit = 7; bit >= 0; --bit) { + int pixel = ((byte >> bit) & 0x01) ? 1 : 0; + set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_MOSI_PIN, pixel); + set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 1); + esp_rom_delay_us(1); + set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0); + esp_rom_delay_us(1); + } + } +} + +static esp_err_t ensure_adc_unit(adc_unit_t unit, adc_oneshot_unit_handle_t *out_handle) { + if (out_handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + + adc_oneshot_unit_handle_t *slot = NULL; + if (unit == ADC_UNIT_1) { + slot = &s_state.adc1_handle; + } else if (unit == ADC_UNIT_2) { + slot = &s_state.adc2_handle; + } else { + return ESP_ERR_NOT_SUPPORTED; + } + + if (*slot == NULL) { + adc_oneshot_unit_init_cfg_t cfg = { + .unit_id = unit, + .clk_src = ADC_RTC_CLK_SRC_DEFAULT, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + esp_err_t err = adc_oneshot_new_unit(&cfg, slot); + if (err != ESP_OK) { + ESP_LOGW(TAG, "adc unit init failed: unit=%d err=%s", (int)unit, esp_err_to_name(err)); + return err; + } + } + + *out_handle = *slot; + return ESP_OK; +} + +static void init_adc_pin_if_possible(int gpio_num, const char *name, adc_pin_t *out_pin) { + if (out_pin == NULL) { + return; + } + + memset(out_pin, 0, sizeof(*out_pin)); + if (!is_gpio_valid(gpio_num)) { + return; + } + + adc_unit_t unit = ADC_UNIT_1; + adc_channel_t channel = ADC_CHANNEL_0; + esp_err_t err = adc_oneshot_io_to_channel(gpio_num, &unit, &channel); + if (err != ESP_OK) { + ESP_LOGW(TAG, "adc gpio unsupported: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err)); + return; + } + + adc_oneshot_unit_handle_t handle = NULL; + err = ensure_adc_unit(unit, &handle); + if (err != ESP_OK) { + return; + } + + adc_oneshot_chan_cfg_t chan_cfg = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, + }; + err = adc_oneshot_config_channel(handle, channel, &chan_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "adc channel config failed: %s pin=%d err=%s", name, gpio_num, esp_err_to_name(err)); + return; + } + + out_pin->ready = true; + out_pin->unit = unit; + out_pin->channel = channel; +} + +static esp_err_t read_adc_raw_locked(const adc_pin_t *pin, int *out_raw) { + if (pin == NULL || out_raw == NULL || !pin->ready) { + return ESP_ERR_INVALID_STATE; + } + + adc_oneshot_unit_handle_t handle = NULL; + if (pin->unit == ADC_UNIT_1) { + handle = s_state.adc1_handle; + } else if (pin->unit == ADC_UNIT_2) { + handle = s_state.adc2_handle; + } + + if (handle == NULL) { + return ESP_ERR_INVALID_STATE; + } + + return adc_oneshot_read(handle, pin->channel, out_raw); +} + +static bool read_paper_present_locked(int *out_level) { + if (!is_gpio_valid(CONFIG_TQ_PRINT_PAPER_PIN)) { + if (out_level != NULL) { + *out_level = -1; + } + return true; + } + int level = gpio_get_level(CONFIG_TQ_PRINT_PAPER_PIN); + if (out_level != NULL) { + *out_level = level; + } + return level == (int)runtime_policy_direct_printer_paper_present_level(); +} + +static uint16_t battery_raw_to_mv(int raw) { + if (raw < 0) { + raw = 0; + } + if (raw > ADC_RAW_MAX) { + raw = ADC_RAW_MAX; + } + float adc_mv = ((float)raw * 3300.0f) / (float)ADC_RAW_MAX; + float ratio = (float)runtime_policy_direct_printer_battery_divider_ratio_x1000() / 1000.0f; + float batt_mv = adc_mv * ratio; + if (batt_mv < 0.0f) { + batt_mv = 0.0f; + } + if (batt_mv > 65535.0f) { + batt_mv = 65535.0f; + } + return (uint16_t)batt_mv; +} + +static uint8_t battery_mv_to_percent(uint16_t batt_mv) { + uint16_t empty_mv = runtime_policy_direct_printer_battery_empty_mv(); + uint16_t full_mv = runtime_policy_direct_printer_battery_full_mv(); + if (full_mv <= empty_mv) { + return 100; + } + if (batt_mv <= empty_mv) { + return 0; + } + if (batt_mv >= full_mv) { + return 100; + } + return (uint8_t)(((uint32_t)(batt_mv - empty_mv) * 100u) / (uint32_t)(full_mv - empty_mv)); +} + +static float ntc_raw_to_temp_c(int raw) { + if (raw <= 0 || raw >= ADC_RAW_MAX) { + return 25.0f; + } + + float pullup = (float)runtime_policy_direct_printer_ntc_pullup_ohms(); + float r25 = (float)runtime_policy_direct_printer_ntc_r25_ohms(); + float beta = (float)runtime_policy_direct_printer_ntc_beta(); + if (pullup <= 0.0f || r25 <= 0.0f || beta <= 0.0f) { + return 25.0f; + } + + float ratio = (float)raw / (float)(ADC_RAW_MAX - raw); + float r_ntc = pullup * ratio; + if (r_ntc <= 1.0f) { + return 25.0f; + } + + float inv_t = (1.0f / (273.15f + 25.0f)) + (logf(r_ntc / r25) / beta); + if (inv_t <= 0.0f) { + return 25.0f; + } + return (1.0f / inv_t) - 273.15f; +} + +static void sample_sensors_locked(platform_printer_sensors_t *out_sensors) { + if (out_sensors == NULL) { + return; + } + + memset(out_sensors, 0, sizeof(*out_sensors)); + int paper_level = -1; + out_sensors->has_paper = read_paper_present_locked(&paper_level); + out_sensors->paper_gpio_level = (int8_t)paper_level; + out_sensors->paper_present_level = runtime_policy_direct_printer_paper_present_level(); + out_sensors->battery_percent = 100; + out_sensors->temperature_c = 25.0f; + out_sensors->updated_ms = esp_timer_get_time() / 1000; + + int battery_raw = 0; + if (read_adc_raw_locked(&s_state.battery_adc, &battery_raw) == ESP_OK) { + uint16_t batt_mv = battery_raw_to_mv(battery_raw); + out_sensors->battery_percent = battery_mv_to_percent(batt_mv); + } + + int ntc_raw = 0; + if (read_adc_raw_locked(&s_state.ntc_adc, &ntc_raw) == ESP_OK) { + out_sensors->temperature_c = ntc_raw_to_temp_c(ntc_raw); + } +} + +static esp_err_t precheck_before_print_locked(char *err, size_t err_len) { + platform_printer_sensors_t sensors = {0}; + sample_sensors_locked(&sensors); + + if (!sensors.has_paper) { + write_err(err, err_len, "printer out of paper"); + return ESP_ERR_INVALID_STATE; + } + + float temp_min = runtime_policy_direct_printer_temp_min_c(); + float temp_max = runtime_policy_direct_printer_temp_max_c(); + if (temp_max <= temp_min) { + temp_max = temp_min + 1.0f; + } + if (sensors.temperature_c < temp_min || sensors.temperature_c > temp_max) { + write_err(err, err_len, "temperature out of range"); + return ESP_ERR_INVALID_STATE; + } + + uint8_t battery_min = runtime_policy_direct_printer_battery_min_percent(); + if (sensors.battery_percent < battery_min) { + write_err(err, err_len, "battery too low"); + return ESP_ERR_INVALID_STATE; + } + + return ESP_OK; +} + +static bool has_timed_out(int64_t deadline_ms) { + if (deadline_ms <= 0) { + return false; + } + return (esp_timer_get_time() / 1000) > deadline_ms; +} + +esp_err_t platform_direct_printer_init(void) { +#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + if (s_state.initialized) { + return ESP_OK; + } + + memset(&s_state, 0, sizeof(s_state)); + s_state.lock = xSemaphoreCreateMutex(); + if (s_state.lock == NULL) { + return ESP_ERR_NO_MEM; + } + + esp_err_t err = ESP_OK; + err = config_output_pin(CONFIG_TQ_PRINT_STB_12_PIN, 0, "print_stb12"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_PRINT_STB_34_PIN, 0, "print_stb34"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_PRINT_STB_56_PIN, 0, "print_stb56"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_PRINT_LAT_PIN, 1, "print_lat"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_SPI_SHARED_MOSI_PIN, 0, "print_mosi"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0, "print_clk"); + if (err != ESP_OK) { + return err; + } + + err = config_output_pin(CONFIG_TQ_PRINT_OUTA_P_PIN, 0, "print_outa_p"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_PRINT_OUTA_N_PIN, 0, "print_outa_n"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_PRINT_OUTB_P_PIN, 0, "print_outb_p"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_PRINT_OUTB_N_PIN, 0, "print_outb_n"); + if (err != ESP_OK) { + return err; + } + err = config_output_pin(CONFIG_TQ_KEY_PRINT_PIN, BOOST_OFF_LEVEL, "print_boost"); + if (err != ESP_OK) { + return err; + } + + err = config_input_pin(CONFIG_TQ_PRINT_PAPER_PIN, true, "print_paper"); + if (err != ESP_OK) { + return err; + } + + init_adc_pin_if_possible(CONFIG_TQ_BATTERY_ADC_PIN, "battery_adc", &s_state.battery_adc); + init_adc_pin_if_possible(CONFIG_TQ_NTC_ADC_PIN, "ntc_adc", &s_state.ntc_adc); + + s_state.motor_phase = 0; + s_state.connected = false; + s_state.initialized = true; + ESP_LOGI(TAG, "direct printer initialized"); + return ESP_OK; +#endif +} + +void platform_direct_printer_deinit(void) { + if (!s_state.initialized) { + return; + } + + if (s_state.lock != NULL && xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(100)) == pdTRUE) { + safe_drive_off_locked(false); + s_state.connected = false; + xSemaphoreGive(s_state.lock); + } + + if (s_state.adc1_handle != NULL) { + (void)adc_oneshot_del_unit(s_state.adc1_handle); + s_state.adc1_handle = NULL; + } + if (s_state.adc2_handle != NULL) { + (void)adc_oneshot_del_unit(s_state.adc2_handle); + s_state.adc2_handle = NULL; + } + if (s_state.lock != NULL) { + vSemaphoreDelete(s_state.lock); + } + memset(&s_state, 0, sizeof(s_state)); +} + +esp_err_t platform_direct_printer_connect(uint32_t timeout_ms) { + (void)timeout_ms; +#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + esp_err_t err = platform_direct_printer_init(); + if (err != ESP_OK) { + return err; + } + + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + if (!s_state.connected) { + set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, BOOST_ON_LEVEL); + vTaskDelay(pdMS_TO_TICKS(10)); + safe_drive_off_locked(true); + s_state.connected = true; + } + + xSemaphoreGive(s_state.lock); + return ESP_OK; +#endif +} + +void platform_direct_printer_disconnect(void) { + if (!s_state.initialized || s_state.lock == NULL) { + return; + } + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) { + return; + } + safe_drive_off_locked(false); + s_state.connected = false; + xSemaphoreGive(s_state.lock); +} + +bool platform_direct_printer_is_connected(void) { + if (!s_state.initialized || s_state.lock == NULL) { + return false; + } + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(100)) != pdTRUE) { + return false; + } + bool connected = s_state.connected; + xSemaphoreGive(s_state.lock); + return connected; +} + +esp_err_t platform_direct_printer_get_sensors(platform_printer_sensors_t *out_sensors) { + if (out_sensors == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (!s_state.initialized || s_state.lock == NULL) { + return ESP_ERR_INVALID_STATE; + } + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + sample_sensors_locked(out_sensors); + xSemaphoreGive(s_state.lock); + return ESP_OK; +} + +esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *request, + char *err, + size_t err_len) { +#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE + write_err(err, err_len, "direct backend disabled"); + return ESP_ERR_NOT_SUPPORTED; +#else + if (request == NULL || request->raster == NULL || request->width == 0 || request->height == 0) { + write_err(err, err_len, "invalid args"); + return ESP_ERR_INVALID_ARG; + } + if (request->width != 384) { + write_err(err, err_len, "width must be 384"); + return ESP_ERR_INVALID_ARG; + } + + const size_t line_bytes = (size_t)request->width / 8u; + const size_t expected_len = line_bytes * (size_t)request->height; + if (request->raster_len != expected_len) { + write_err(err, err_len, "raster size mismatch"); + return ESP_ERR_INVALID_SIZE; + } + + if (!s_state.initialized || s_state.lock == NULL) { + write_err(err, err_len, "direct backend not initialized"); + return ESP_ERR_INVALID_STATE; + } + + uint32_t timeout_ms = request->timeout_ms; + if (timeout_ms == 0) { + timeout_ms = runtime_policy_direct_printer_operation_timeout_ms(); + } + + const uint16_t strobe_on_us = (request->strobe_on_time_us > 0) + ? request->strobe_on_time_us + : runtime_policy_direct_printer_strobe_on_us(); + const uint16_t strobe_interval_us = (request->strobe_interval_us > 0) + ? request->strobe_interval_us + : runtime_policy_direct_printer_strobe_interval_us(); + const uint16_t motor_step_us = (request->motor_step_delay_us > 0) + ? request->motor_step_delay_us + : runtime_policy_direct_printer_motor_step_us(); + const uint8_t steps_per_line = (request->motor_steps_per_line > 0) + ? request->motor_steps_per_line + : runtime_policy_direct_printer_steps_per_line(); + + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) != pdTRUE) { + write_err(err, err_len, "direct printer lock timeout"); + return ESP_ERR_TIMEOUT; + } + + if (!s_state.connected) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "direct printer not connected"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t rc = ESP_OK; + if (!request->ignore_precheck) { + rc = precheck_before_print_locked(err, err_len); + if (rc != ESP_OK) { + safe_drive_off_locked(true); + xSemaphoreGive(s_state.lock); + return rc; + } + } + + int64_t deadline_ms = (int64_t)(esp_timer_get_time() / 1000) + (int64_t)timeout_ms; + + for (uint16_t line = 0; line < request->height; ++line) { + if (request->cancel_flag != NULL && *request->cancel_flag) { + write_err(err, err_len, "job canceled"); + rc = ESP_ERR_INVALID_STATE; + break; + } + if (has_timed_out(deadline_ms)) { + write_err(err, err_len, "print timeout"); + rc = ESP_ERR_TIMEOUT; + break; + } + + if (!request->ignore_precheck && (line & 0x0F) == 0) { + rc = precheck_before_print_locked(err, err_len); + if (rc != ESP_OK) { + break; + } + } + + const uint8_t *line_ptr = &request->raster[(size_t)line * line_bytes]; + write_line_bits_locked(line_ptr, line_bytes); + pulse_latch_locked(); + fire_strobe_locked(strobe_on_us, strobe_interval_us); + + for (uint8_t i = 0; i < steps_per_line; ++i) { + if (request->cancel_flag != NULL && *request->cancel_flag) { + write_err(err, err_len, "job canceled"); + rc = ESP_ERR_INVALID_STATE; + break; + } + if (has_timed_out(deadline_ms)) { + write_err(err, err_len, "print timeout"); + rc = ESP_ERR_TIMEOUT; + break; + } + motor_step_once_locked(motor_step_us); + } + if (rc != ESP_OK) { + break; + } + + // Yield periodically so IDLE task can run and service task WDT on the same CPU. + if (((line + 1u) % k_print_yield_lines) == 0u) { + vTaskDelay(1); + } + } + + safe_drive_off_locked(true); + xSemaphoreGive(s_state.lock); + return rc; +#endif +} + +esp_err_t platform_direct_printer_gap_move(uint32_t timeout_ms, char *err, size_t err_len) { +#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE + write_err(err, err_len, "direct backend disabled"); + return ESP_ERR_NOT_SUPPORTED; +#else + if (!s_state.initialized || s_state.lock == NULL) { + write_err(err, err_len, "direct backend not initialized"); + return ESP_ERR_INVALID_STATE; + } + if (timeout_ms == 0) { + timeout_ms = runtime_policy_direct_printer_operation_timeout_ms(); + } + + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) != pdTRUE) { + write_err(err, err_len, "direct printer lock timeout"); + return ESP_ERR_TIMEOUT; + } + if (!s_state.connected) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "direct printer not connected"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t rc = ESP_OK; + uint16_t steps = runtime_policy_direct_printer_gap_steps(); + uint16_t step_delay_us = runtime_policy_direct_printer_motor_step_us(); + platform_printer_sensors_t sensors = {0}; + sample_sensors_locked(&sensors); + + uint8_t battery_min = runtime_policy_direct_printer_battery_min_percent(); + if (sensors.battery_percent < battery_min) { + write_err(err, err_len, "battery too low"); + safe_drive_off_locked(true); + xSemaphoreGive(s_state.lock); + return ESP_ERR_INVALID_STATE; + } + + // Per JX-2R-01 guidance: out-of-paper feed should run at reduced speed (roughly <=600 PPS). + if (!sensors.has_paper && step_delay_us < 1667) { + step_delay_us = 1667; + } + if (!sensors.has_paper) { + ESP_LOGW(TAG, + "gap move while out-of-paper, gpio_level=%d expect=%u step_us=%u", + sensors.paper_gpio_level, + sensors.paper_present_level, + step_delay_us); + } + + int64_t deadline_ms = (int64_t)(esp_timer_get_time() / 1000) + (int64_t)timeout_ms; + + for (uint16_t i = 0; i < steps; ++i) { + if (has_timed_out(deadline_ms)) { + write_err(err, err_len, "gap move timeout"); + rc = ESP_ERR_TIMEOUT; + break; + } + motor_step_once_locked(step_delay_us); + if (((i + 1u) % k_gap_yield_steps) == 0u) { + vTaskDelay(1); + } + } + + safe_drive_off_locked(true); + xSemaphoreGive(s_state.lock); + return rc; +#endif +} diff --git a/components/platform/src/runtime_policy.c b/components/platform/src/runtime_policy.c index d7c4024..eeb6d30 100644 --- a/components/platform/src/runtime_policy.c +++ b/components/platform/src/runtime_policy.c @@ -34,6 +34,82 @@ #define CONFIG_TQ_PRINTER_STOP_TIMEOUT_MS 8000 #endif +#ifndef CONFIG_TQ_PRINTER_DEFAULT_BACKEND +#define CONFIG_TQ_PRINTER_DEFAULT_BACKEND 0 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_ENABLE +#define CONFIG_TQ_DIRECT_PRINTER_ENABLE 1 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_OPERATION_TIMEOUT_MS +#define CONFIG_TQ_DIRECT_PRINTER_OPERATION_TIMEOUT_MS 90000 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_STROBE_ON_US +#define CONFIG_TQ_DIRECT_PRINTER_STROBE_ON_US 1000 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_STROBE_INTERVAL_US +#define CONFIG_TQ_DIRECT_PRINTER_STROBE_INTERVAL_US 200 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_MOTOR_STEP_US +#define CONFIG_TQ_DIRECT_PRINTER_MOTOR_STEP_US 2000 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_MOTOR_REVERSE +#define CONFIG_TQ_DIRECT_PRINTER_MOTOR_REVERSE 0 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_STEPS_PER_LINE +#define CONFIG_TQ_DIRECT_PRINTER_STEPS_PER_LINE 2 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_GAP_STEPS +#define CONFIG_TQ_DIRECT_PRINTER_GAP_STEPS 96 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_TEMP_MIN_C +#define CONFIG_TQ_DIRECT_PRINTER_TEMP_MIN_C 15 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_TEMP_MAX_C +#define CONFIG_TQ_DIRECT_PRINTER_TEMP_MAX_C 55 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_BATTERY_MIN_PERCENT +#define CONFIG_TQ_DIRECT_PRINTER_BATTERY_MIN_PERCENT 5 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_PAPER_PRESENT_LEVEL +#define CONFIG_TQ_DIRECT_PRINTER_PAPER_PRESENT_LEVEL 0 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_BATTERY_EMPTY_MV +#define CONFIG_TQ_DIRECT_PRINTER_BATTERY_EMPTY_MV 3300 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_BATTERY_FULL_MV +#define CONFIG_TQ_DIRECT_PRINTER_BATTERY_FULL_MV 4200 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_BATTERY_DIVIDER_RATIO_X1000 +#define CONFIG_TQ_DIRECT_PRINTER_BATTERY_DIVIDER_RATIO_X1000 2000 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_NTC_PULLUP_OHMS +#define CONFIG_TQ_DIRECT_PRINTER_NTC_PULLUP_OHMS 10000 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_NTC_R25_OHMS +#define CONFIG_TQ_DIRECT_PRINTER_NTC_R25_OHMS 10000 +#endif + +#ifndef CONFIG_TQ_DIRECT_PRINTER_NTC_BETA +#define CONFIG_TQ_DIRECT_PRINTER_NTC_BETA 3950 +#endif + #ifndef CONFIG_TQ_REST_PRINTER_CONNECT_TIMEOUT_MS #define CONFIG_TQ_REST_PRINTER_CONNECT_TIMEOUT_MS 15000 #endif @@ -64,6 +140,26 @@ static uint32_t clamp_u32(uint32_t value, uint32_t min_value, uint32_t max_value return value; } +static uint16_t clamp_u16(uint16_t value, uint16_t min_value, uint16_t max_value) { + if (value < min_value) { + return min_value; + } + if (value > max_value) { + return max_value; + } + return value; +} + +static uint8_t clamp_u8(uint8_t value, uint8_t min_value, uint8_t max_value) { + if (value < min_value) { + return min_value; + } + if (value > max_value) { + return max_value; + } + return value; +} + uint32_t runtime_policy_wifi_connect_timeout_ms(void) { return clamp_u32(CONFIG_TQ_WIFI_CONNECT_TIMEOUT_MS, 3000, 60000); } @@ -105,6 +201,87 @@ uint32_t runtime_policy_printer_stop_timeout_ms(void) { return clamp_u32(CONFIG_TQ_PRINTER_STOP_TIMEOUT_MS, 1000, 30000); } +uint8_t runtime_policy_printer_default_backend(void) { + return (uint8_t)clamp_u32(CONFIG_TQ_PRINTER_DEFAULT_BACKEND, 0, 1); +} + +bool runtime_policy_direct_printer_enabled(void) { + return CONFIG_TQ_DIRECT_PRINTER_ENABLE != 0; +} + +uint32_t runtime_policy_direct_printer_operation_timeout_ms(void) { + return clamp_u32(CONFIG_TQ_DIRECT_PRINTER_OPERATION_TIMEOUT_MS, 1000, 300000); +} + +uint16_t runtime_policy_direct_printer_strobe_on_us(void) { + return clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_STROBE_ON_US, 100, 10000); +} + +uint16_t runtime_policy_direct_printer_strobe_interval_us(void) { + return clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_STROBE_INTERVAL_US, 0, 10000); +} + +uint16_t runtime_policy_direct_printer_motor_step_us(void) { + return clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_MOTOR_STEP_US, 100, 20000); +} + +bool runtime_policy_direct_printer_motor_reverse(void) { + return CONFIG_TQ_DIRECT_PRINTER_MOTOR_REVERSE != 0; +} + +uint8_t runtime_policy_direct_printer_steps_per_line(void) { + return clamp_u8((uint8_t)CONFIG_TQ_DIRECT_PRINTER_STEPS_PER_LINE, 1, 8); +} + +uint16_t runtime_policy_direct_printer_gap_steps(void) { + return clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_GAP_STEPS, 1, 2000); +} + +float runtime_policy_direct_printer_temp_min_c(void) { + return (float)clamp_u32((uint32_t)CONFIG_TQ_DIRECT_PRINTER_TEMP_MIN_C, 0, 120); +} + +float runtime_policy_direct_printer_temp_max_c(void) { + return (float)clamp_u32((uint32_t)CONFIG_TQ_DIRECT_PRINTER_TEMP_MAX_C, 1, 150); +} + +uint8_t runtime_policy_direct_printer_battery_min_percent(void) { + return clamp_u8((uint8_t)CONFIG_TQ_DIRECT_PRINTER_BATTERY_MIN_PERCENT, 0, 100); +} + +uint8_t runtime_policy_direct_printer_paper_present_level(void) { + return clamp_u8((uint8_t)CONFIG_TQ_DIRECT_PRINTER_PAPER_PRESENT_LEVEL, 0, 1); +} + +uint16_t runtime_policy_direct_printer_battery_empty_mv(void) { + return clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_BATTERY_EMPTY_MV, 2500, 5000); +} + +uint16_t runtime_policy_direct_printer_battery_full_mv(void) { + uint16_t empty_mv = runtime_policy_direct_printer_battery_empty_mv(); + uint16_t full_mv = clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_BATTERY_FULL_MV, 2600, 6000); + if (full_mv <= empty_mv) { + full_mv = (uint16_t)(empty_mv + 100); + } + return full_mv; +} + +uint16_t runtime_policy_direct_printer_battery_divider_ratio_x1000(void) { + return clamp_u16((uint16_t)CONFIG_TQ_DIRECT_PRINTER_BATTERY_DIVIDER_RATIO_X1000, 1000, 10000); +} + +uint32_t runtime_policy_direct_printer_ntc_pullup_ohms(void) { + return clamp_u32(CONFIG_TQ_DIRECT_PRINTER_NTC_PULLUP_OHMS, 1000, 200000); +} + +uint32_t runtime_policy_direct_printer_ntc_r25_ohms(void) { + return clamp_u32(CONFIG_TQ_DIRECT_PRINTER_NTC_R25_OHMS, 1000, 200000); +} + +uint32_t runtime_policy_direct_printer_ntc_beta(void) { + return clamp_u32(CONFIG_TQ_DIRECT_PRINTER_NTC_BETA, 1000, 10000); +} + uint32_t runtime_policy_rest_printer_connect_timeout_ms(void) { return clamp_u32(CONFIG_TQ_REST_PRINTER_CONNECT_TIMEOUT_MS, 1000, 60000); } diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 6bc4386..defa11b 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -71,6 +71,116 @@ config TQ_PRINTER_STOP_TIMEOUT_MS range 1000 30000 default 8000 +config TQ_PRINTER_DEFAULT_BACKEND + int "Printer default backend (0=BLE, 1=Direct)" + range 0 1 + default 0 + +config TQ_DIRECT_PRINTER_ENABLE + bool "Enable direct thermal printer backend" + default y + +config TQ_DIRECT_PRINTER_OPERATION_TIMEOUT_MS + int "Direct printer max operation timeout (ms)" + range 1000 300000 + default 90000 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_STROBE_ON_US + int "Direct printer strobe on-time (us)" + range 100 10000 + default 1000 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_STROBE_INTERVAL_US + int "Direct printer strobe interval (us)" + range 0 10000 + default 200 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_MOTOR_STEP_US + int "Direct printer motor step delay (us)" + range 100 20000 + default 2000 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_MOTOR_REVERSE + bool "Direct printer motor direction reversed" + default n + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_STEPS_PER_LINE + int "Direct printer motor steps per printed line" + range 1 8 + default 2 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_GAP_STEPS + int "Direct printer gap move steps" + range 1 2000 + default 96 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_TEMP_MIN_C + int "Direct printer allowed minimum temperature (C)" + range 0 120 + default 15 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_TEMP_MAX_C + int "Direct printer allowed maximum temperature (C)" + range 1 150 + default 55 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_BATTERY_MIN_PERCENT + int "Direct printer minimum battery percent" + range 0 100 + default 5 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_PAPER_PRESENT_LEVEL + int "Direct printer paper-present GPIO level (0/1)" + range 0 1 + default 0 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_BATTERY_EMPTY_MV + int "Direct printer battery empty voltage (mV)" + range 2500 5000 + default 3300 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_BATTERY_FULL_MV + int "Direct printer battery full voltage (mV)" + range 2600 6000 + default 4200 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_BATTERY_DIVIDER_RATIO_X1000 + int "Direct printer battery divider ratio x1000" + range 1000 10000 + default 2000 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_NTC_PULLUP_OHMS + int "Direct printer NTC pull-up resistor (ohms)" + range 1000 200000 + default 10000 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_NTC_R25_OHMS + int "Direct printer NTC R25 resistance (ohms)" + range 1000 200000 + default 10000 + depends on TQ_DIRECT_PRINTER_ENABLE + +config TQ_DIRECT_PRINTER_NTC_BETA + int "Direct printer NTC beta constant" + range 1000 10000 + default 3950 + depends on TQ_DIRECT_PRINTER_ENABLE + config TQ_API_KEY string "REST API Key (optional, empty means disabled)" default ""