feat(printer): add direct thermal backend and debug controls

This commit is contained in:
admin
2026-02-26 17:48:28 +08:00
parent e7aa0e75bf
commit 2b6bcc83dc
17 changed files with 1866 additions and 86 deletions

View File

@@ -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);

View File

@@ -53,12 +53,31 @@ static const char *s_web_index =
"\n"
" <label>Printer Name</label>\n"
" <input id='printerName' type='text' value='TQPrinter'>\n"
" <label>Printer Backend</label>\n"
" <select id='printerBackend' style='max-width:220px'>\n"
" <option value='direct' selected>Direct (GPIO)</option>\n"
" <option value='ble'>BLE</option>\n"
" </select>\n"
" <div class='row'>\n"
" <button id='btnConnect'>Connect Printer</button>\n"
" <button id='btnAutoConnect' class='alt'>Auto Connect</button>\n"
" <button id='btnDisconnect' class='warn'>Disconnect</button>\n"
" </div>\n"
"\n"
" <label>Paper Detect</label>\n"
" <div class='row'>\n"
" <button id='btnPaperCheck' class='alt'>Check Paper</button>\n"
" </div>\n"
" <div id='paperState' class='small'>纸张状态:未知</div>\n"
" <div id='paperMeta' class='small'>Backend: -, Connected: -, Transport: -, GPIO: -, Expect: -</div>\n"
" <div class='row'>\n"
" <label style='margin:0;display:flex;align-items:center;gap:6px;'>\n"
" <input id='ignorePrecheck' type='checkbox' style='width:auto;'>\n"
" Ignore direct precheck (paper/temp/battery)\n"
" </label>\n"
" </div>\n"
" <div class='small'>Only effective on Direct backend. Use for sensor bring-up/debug.</div>\n"
"\n"
" <label>Text to Print</label>\n"
" <textarea id='printText'>Hello from ESP32-S3!\nOrder: A1024\nThank you</textarea>\n"
" <div class='row'>\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"
" 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"
" });\n"
" syncPaperUi(data);\n"
" return data;\n"
" });\n"
"\n"
" document.getElementById('btnAutoConnect').onclick = () => run(() => callApi('/v1/printer/connect', 'POST', {\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"
" });\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"

View File

@@ -2,11 +2,42 @@
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#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);

View File

@@ -18,24 +18,30 @@ 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,
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));
@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,10 +517,20 @@ static void status_poll_task(void *arg) {
break;
}
if (ble_printer_client_is_connected() && can_poll) {
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();
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;
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;

View File

@@ -4,8 +4,22 @@
#include <stdio.h>
#include <string.h>
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");

View File

@@ -48,11 +48,12 @@ 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,
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) {
@@ -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;

View File

@@ -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,7 +76,8 @@ static uint16_t density_to_hot_time(const char *density) {
return 2000;
}
static bool precheck_printer_ready(char *err, size_t err_len) {
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;
@@ -101,35 +106,52 @@ static bool precheck_printer_ready(char *err, size_t err_len) {
snprintf(err, err_len, "temperature too high");
return false;
}
return true;
}
static bool run_print_job(job_slot_t *job) {
if (direct_ignore_precheck) {
return true;
}
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;
}
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;
}
return true;
}
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;

View File

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

View File

@@ -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);

View File

@@ -0,0 +1,769 @@
#include "platform.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#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
}

View File

@@ -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);
}

View File

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