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 ""