diff --git a/.gitignore b/.gitignore index 776aeec..eef899c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__/ .pytest_cache/ managed_components +JX_2R_01测试程序_STM32F103RF \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 191e7bc..235fa80 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "idf.currentSetup": "/Users/moyyang/esp/v5.5.2/esp-idf", "idf.flashType": "UART", - "idf.port": "/dev/tty.wchusbserial5B0B0251771", + "idf.port": "/dev/tty.usbserial-110", "idf.openOcdConfigs": [ "board/esp32s3-builtin.cfg" ], diff --git a/components/control_plane/internal/rest_server_internal.h b/components/control_plane/internal/rest_server_internal.h index 330d2b2..be010f9 100644 --- a/components/control_plane/internal/rest_server_internal.h +++ b/components/control_plane/internal/rest_server_internal.h @@ -30,6 +30,8 @@ esp_err_t rest_server_health_get(httpd_req_t *req); esp_err_t rest_server_connect_post(httpd_req_t *req); esp_err_t rest_server_disconnect_post(httpd_req_t *req); esp_err_t rest_server_status_get(httpd_req_t *req); +esp_err_t rest_server_printer_direct_debug_get(httpd_req_t *req); +esp_err_t rest_server_printer_direct_debug_post(httpd_req_t *req); esp_err_t rest_server_print_raster_post(httpd_req_t *req); esp_err_t rest_server_print_image_post(httpd_req_t *req); diff --git a/components/control_plane/src/rest_server.c b/components/control_plane/src/rest_server.c index 95e78ae..85c9b84 100644 --- a/components/control_plane/src/rest_server.c +++ b/components/control_plane/src/rest_server.c @@ -78,6 +78,57 @@ static const char *s_web_index = " \n" "
Only effective on Direct backend. Use for sensor bring-up/debug.
\n" "\n" + " \n" + "
参数标签中的字段名与 /v1/printer/direct/debug 的 JSON 键保持一致。
\n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
\n" + "
override_* 参数填 0 表示不覆盖,恢复为运行时默认策略。
\n" + "
Auto Diagnose 使用低功耗测试图案;若点击后仍重启(Brownout),请先改善供电或降低热头功率。
\n" + "\n" " \n" " \n" "
\n" @@ -155,6 +206,16 @@ static const char *s_web_index = " const imgScaleToWidthEl = document.getElementById('imgScaleToWidth');\n" " const imgInvertEl = document.getElementById('imgInvert');\n" " const imgPromptExtendEl = document.getElementById('imgPromptExtend');\n" + " const dbgShiftHighUsEl = document.getElementById('dbgShiftHighUs');\n" + " const dbgShiftLowUsEl = document.getElementById('dbgShiftLowUs');\n" + " const dbgLatchUsEl = document.getElementById('dbgLatchUs');\n" + " const dbgHotUsEl = document.getElementById('dbgHotUs');\n" + " const dbgStrobeGapUsEl = document.getElementById('dbgStrobeGapUs');\n" + " const dbgMotorUsEl = document.getElementById('dbgMotorUs');\n" + " const dbgStepsPerLineEl = document.getElementById('dbgStepsPerLine');\n" + " const dbgStbActiveHighEl = document.getElementById('dbgStbActiveHigh');\n" + " const dbgBoostActiveHighEl = document.getElementById('dbgBoostActiveHigh');\n" + " const btnDebugAutoScanEl = document.getElementById('btnDebugAutoScan');\n" " const btnVoiceTalkEl = document.getElementById('btnVoiceTalk');\n" " const voiceStateEl = document.getElementById('voiceState');\n" " const voiceErrEl = document.getElementById('voiceErr');\n" @@ -162,6 +223,7 @@ static const char *s_web_index = " let voiceTalkHolding = false;\n" " let voiceTalkBusy = false;\n" " let voiceStatusSyncing = false;\n" + " let debugAutoScanBusy = false;\n" "\n" " function log(obj) {\n" " if (typeof obj === 'string') {\n" @@ -200,6 +262,330 @@ static const char *s_web_index = " return !!(ignorePrecheckEl && ignorePrecheckEl.checked);\n" " }\n" "\n" + " function fillDirectDebugForm(data) {\n" + " if (!data || typeof data !== 'object') {\n" + " return;\n" + " }\n" + " if (dbgShiftHighUsEl && data.shift_clock_high_us !== undefined) dbgShiftHighUsEl.value = String(data.shift_clock_high_us);\n" + " if (dbgShiftLowUsEl && data.shift_clock_low_us !== undefined) dbgShiftLowUsEl.value = String(data.shift_clock_low_us);\n" + " if (dbgLatchUsEl && data.latch_pulse_us !== undefined) dbgLatchUsEl.value = String(data.latch_pulse_us);\n" + " if (dbgHotUsEl && data.override_strobe_on_us !== undefined) dbgHotUsEl.value = String(data.override_strobe_on_us);\n" + " if (dbgStrobeGapUsEl && data.override_strobe_interval_us !== undefined) dbgStrobeGapUsEl.value = String(data.override_strobe_interval_us);\n" + " if (dbgMotorUsEl && data.override_motor_step_us !== undefined) dbgMotorUsEl.value = String(data.override_motor_step_us);\n" + " if (dbgStepsPerLineEl && data.override_steps_per_line !== undefined) dbgStepsPerLineEl.value = String(data.override_steps_per_line);\n" + " if (dbgStbActiveHighEl && data.strobe_active_high !== undefined) dbgStbActiveHighEl.checked = !!data.strobe_active_high;\n" + " if (dbgBoostActiveHighEl && data.boost_active_high !== undefined) dbgBoostActiveHighEl.checked = !!data.boost_active_high;\n" + " }\n" + "\n" + " function buildDirectDebugRequest() {\n" + " return {\n" + " shift_clock_high_us: toInt(dbgShiftHighUsEl ? dbgShiftHighUsEl.value : '5', 5, 1, 50),\n" + " shift_clock_low_us: toInt(dbgShiftLowUsEl ? dbgShiftLowUsEl.value : '5', 5, 1, 50),\n" + " latch_pulse_us: toInt(dbgLatchUsEl ? dbgLatchUsEl.value : '1', 1, 1, 50),\n" + " strobe_active_high: !!(dbgStbActiveHighEl && dbgStbActiveHighEl.checked),\n" + " boost_active_high: !!(dbgBoostActiveHighEl && dbgBoostActiveHighEl.checked),\n" + " override_strobe_on_us: toInt(dbgHotUsEl ? dbgHotUsEl.value : '0', 0, 0, 10000),\n" + " override_strobe_interval_us: toInt(dbgStrobeGapUsEl ? dbgStrobeGapUsEl.value : '0', 0, 0, 10000),\n" + " override_motor_step_us: toInt(dbgMotorUsEl ? dbgMotorUsEl.value : '0', 0, 0, 20000),\n" + " override_steps_per_line: toInt(dbgStepsPerLineEl ? dbgStepsPerLineEl.value : '0', 0, 0, 8)\n" + " };\n" + " }\n" + "\n" + " async function fetchDirectDebug() {\n" + " const data = await callApi('/v1/printer/direct/debug', 'GET');\n" + " fillDirectDebugForm(data);\n" + " return data;\n" + " }\n" + "\n" + " function sanitizeDirectDebugConfig(data) {\n" + " if (!data || typeof data !== 'object') {\n" + " return null;\n" + " }\n" + " return {\n" + " shift_clock_high_us: toInt(data.shift_clock_high_us, 5, 1, 50),\n" + " shift_clock_low_us: toInt(data.shift_clock_low_us, 5, 1, 50),\n" + " latch_pulse_us: toInt(data.latch_pulse_us, 1, 1, 50),\n" + " strobe_active_high: data.strobe_active_high === undefined ? true : !!data.strobe_active_high,\n" + " boost_active_high: data.boost_active_high === undefined ? true : !!data.boost_active_high,\n" + " override_strobe_on_us: toInt(data.override_strobe_on_us, 0, 0, 10000),\n" + " override_strobe_interval_us: toInt(data.override_strobe_interval_us, 0, 0, 10000),\n" + " override_motor_step_us: toInt(data.override_motor_step_us, 0, 0, 20000),\n" + " override_steps_per_line: toInt(data.override_steps_per_line, 0, 0, 8)\n" + " };\n" + " }\n" + "\n" + " function errorToText(err) {\n" + " if (err === null || err === undefined) {\n" + " return '';\n" + " }\n" + " if (typeof err === 'string') {\n" + " return err;\n" + " }\n" + " if (err.data && typeof err.data === 'object' && err.data.error) {\n" + " return String(err.data.error);\n" + " }\n" + " if (typeof err.message === 'string' && err.message) {\n" + " return err.message;\n" + " }\n" + " try {\n" + " return JSON.stringify(err);\n" + " } catch (_) {\n" + " return String(err);\n" + " }\n" + " }\n" + "\n" + " function buildDirectDiagRasterPayload() {\n" + " const width = 384;\n" + " const height = 72;\n" + " const lineBytes = width / 8;\n" + " const bytes = new Uint8Array(lineBytes * height);\n" + "\n" + " for (let y = 0; y < height; ++y) {\n" + " const offset = y * lineBytes;\n" + " for (let i = 0; i < lineBytes; ++i) {\n" + " bytes[offset + i] = 0x00;\n" + " }\n" + " bytes[offset] = 0xF0;\n" + " bytes[offset + 1] = 0x0F;\n" + " bytes[offset + lineBytes - 2] = 0x0F;\n" + " bytes[offset + lineBytes - 1] = 0xF0;\n" + " if ((y % 12) === 5) {\n" + " for (let i = 16; i < 32; ++i) {\n" + " bytes[offset + i] = 0xFF;\n" + " }\n" + " }\n" + " if ((y % 12) === 6) {\n" + " for (let i = 20; i < 28; ++i) {\n" + " bytes[offset + i] = 0xFF;\n" + " }\n" + " }\n" + " }\n" + "\n" + " let binary = '';\n" + " const chunk = 1024;\n" + " for (let i = 0; i < bytes.length; i += chunk) {\n" + " const part = bytes.subarray(i, i + chunk);\n" + " binary += String.fromCharCode(...part);\n" + " }\n" + "\n" + " return {\n" + " width,\n" + " height,\n" + " data: btoa(binary)\n" + " };\n" + " }\n" + "\n" + " async function waitJobTerminal(jobId, timeoutMs) {\n" + " const deadline = Date.now() + timeoutMs;\n" + " while (Date.now() < deadline) {\n" + " const info = await callApi('/v1/jobs/' + String(jobId), 'GET');\n" + " const state = info && info.state ? String(info.state).toLowerCase() : '';\n" + " if (state === 'success' || state === 'failed' || state === 'canceled') {\n" + " return info;\n" + " }\n" + " await sleep(500);\n" + " }\n" + " throw new Error('wait job timeout: ' + String(jobId));\n" + " }\n" + "\n" + " function inferDirectDiagConclusion(results) {\n" + " const conclusion = {\n" + " summary: '',\n" + " likely_causes: [],\n" + " next_actions: []\n" + " };\n" + "\n" + " if (!Array.isArray(results) || results.length === 0) {\n" + " conclusion.summary = '未执行任何测试。';\n" + " return conclusion;\n" + " }\n" + "\n" + " const visible = results.find((item) => item && item.user_visible === true);\n" + " if (visible) {\n" + " const cfg = visible.config || {};\n" + " conclusion.summary = '已找到可见输出参数:' + String(visible.name || visible.index || 'unknown');\n" + " if (cfg.boost_active_high === false) {\n" + " conclusion.likely_causes.push('BOOST 极性可能与默认配置相反。');\n" + " }\n" + " if (cfg.strobe_active_high === false) {\n" + " conclusion.likely_causes.push('STB 极性可能与默认配置相反。');\n" + " }\n" + " if (Number(cfg.override_strobe_on_us || 0) >= 2000) {\n" + " conclusion.likely_causes.push('默认加热点火时长偏短,热量不足。');\n" + " }\n" + " if (conclusion.likely_causes.length === 0) {\n" + " conclusion.likely_causes.push('直连默认时序与当前硬件存在偏差。');\n" + " }\n" + " conclusion.next_actions.push('将可见输出对应参数固定为直连默认配置,再复测文本和图片打印。');\n" + " return conclusion;\n" + " }\n" + "\n" + " const mergedError = results\n" + " .map((item) => String((item && (item.error || item.api_error)) || '').toLowerCase())\n" + " .join(' | ');\n" + " if (mergedError.includes('paper')) {\n" + " conclusion.likely_causes.push('纸张检测状态异常(缺纸或纸检测电平不匹配)。');\n" + " }\n" + " if (mergedError.includes('battery')) {\n" + " conclusion.likely_causes.push('电池电量过低,直连预检拦截或加热功率不足。');\n" + " }\n" + " if (mergedError.includes('temperature')) {\n" + " conclusion.likely_causes.push('温度超范围,直连预检拦截。');\n" + " }\n" + "\n" + " const successCount = results.filter((item) => String((item && item.state) || '').toLowerCase() === 'success').length;\n" + " if (successCount > 0) {\n" + " conclusion.likely_causes.push('任务执行成功但肉眼无输出,优先排查热头加热链路(BOOST、STB、热头供电与地)。');\n" + " conclusion.likely_causes.push('若极性扫描均无效,继续排查 MOSI/CLK/LAT 连线或排线接触问题。');\n" + " conclusion.next_actions.push('用万用表或示波器确认 KEY_PRINT(BOOST) 与 STB 在打印时是否出现有效电平翻转。');\n" + " } else {\n" + " conclusion.likely_causes.push('直连打印任务未成功完成,需先处理接口错误再判断打印质量。');\n" + " conclusion.next_actions.push('打开 Jobs 查看每条失败任务的 error 字段并逐项修复。');\n" + " }\n" + "\n" + " conclusion.summary = '未找到可见输出参数。';\n" + " return conclusion;\n" + " }\n" + "\n" + " async function runDirectAutoScan() {\n" + " if (debugAutoScanBusy) {\n" + " throw new Error('direct auto diagnosis is already running');\n" + " }\n" + "\n" + " debugAutoScanBusy = true;\n" + " if (btnDebugAutoScanEl) {\n" + " btnDebugAutoScanEl.disabled = true;\n" + " btnDebugAutoScanEl.textContent = 'Running Auto Diagnose...';\n" + " }\n" + "\n" + " const report = {\n" + " ok: true,\n" + " mode: 'direct_debug_auto_scan',\n" + " started_at: new Date().toISOString(),\n" + " tests: []\n" + " };\n" + "\n" + " const previousBackend = selectedPrinterBackend();\n" + " let originalDebug = null;\n" + "\n" + " try {\n" + " originalDebug = sanitizeDirectDebugConfig(await fetchDirectDebug());\n" + "\n" + " if (printerBackendEl) {\n" + " printerBackendEl.value = 'direct';\n" + " }\n" + "\n" + " const connectData = await callApi('/v1/printer/connect', 'POST', {\n" + " backend: 'direct',\n" + " name: printerNameEl.value.trim() || 'TQPrinter',\n" + " timeout_ms: 15000\n" + " });\n" + " syncPaperUi(connectData);\n" + " report.connect_backend = connectData && connectData.backend ? connectData.backend : 'direct';\n" + "\n" + " const rasterPayload = buildDirectDiagRasterPayload();\n" + " const makeConfig = function(strobeHigh, boostHigh, hotUs, gapUs, motorUs) {\n" + " return {\n" + " shift_clock_high_us: 5,\n" + " shift_clock_low_us: 5,\n" + " latch_pulse_us: 1,\n" + " strobe_active_high: strobeHigh,\n" + " boost_active_high: boostHigh,\n" + " override_strobe_on_us: hotUs,\n" + " override_strobe_interval_us: gapUs,\n" + " override_motor_step_us: motorUs,\n" + " override_steps_per_line: 2\n" + " };\n" + " };\n" + "\n" + " const combos = [\n" + " { name: '默认极性 + 点火1200us', config: makeConfig(true, true, 1200, 260, 2800) },\n" + " { name: '默认极性 + 点火1600us', config: makeConfig(true, true, 1600, 300, 3000) },\n" + " { name: '翻转 STB 极性', config: makeConfig(false, true, 1600, 300, 3000) },\n" + " { name: '翻转 BOOST 极性', config: makeConfig(true, false, 1600, 300, 3000) },\n" + " { name: '翻转 STB + BOOST 极性', config: makeConfig(false, false, 1600, 300, 3000) },\n" + " { name: '翻转 STB + BOOST + 点火2200us', config: makeConfig(false, false, 2200, 360, 3200) }\n" + " ];\n" + "\n" + " for (let i = 0; i < combos.length; ++i) {\n" + " const combo = combos[i];\n" + " const entry = {\n" + " index: i + 1,\n" + " name: combo.name,\n" + " config: combo.config\n" + " };\n" + "\n" + " try {\n" + " const applyData = await callApi('/v1/printer/direct/debug', 'POST', combo.config);\n" + " fillDirectDebugForm(applyData);\n" + "\n" + " const submit = await callApi('/v1/print/raster', 'POST', {\n" + " width: rasterPayload.width,\n" + " height: rasterPayload.height,\n" + " density: '中等',\n" + " encoding: 'base64_msb_1bpp',\n" + " data: rasterPayload.data,\n" + " ignore_precheck: true\n" + " });\n" + "\n" + " entry.job_id = submit.job_id;\n" + " const job = await waitJobTerminal(submit.job_id, 90000);\n" + " entry.state = job.state;\n" + " entry.error = job.error;\n" + "\n" + " if (String(job.state || '').toLowerCase() === 'success') {\n" + " const prompt = '测试 ' + String(i + 1) + '/' + String(combos.length) + ' 已完成(' + combo.name + ')。\\n是否看到了明显黑白条纹?\\n确定=看到了,取消=没看到。';\n" + " entry.user_visible = window.confirm(prompt);\n" + " report.tests.push(entry);\n" + " if (entry.user_visible) {\n" + " report.stopped_early = true;\n" + " report.first_visible_test = entry.index;\n" + " report.first_visible_name = combo.name;\n" + " break;\n" + " }\n" + " } else {\n" + " report.tests.push(entry);\n" + " }\n" + " } catch (err) {\n" + " entry.api_error = errorToText(err);\n" + " report.tests.push(entry);\n" + " }\n" + "\n" + " await sleep(300);\n" + " }\n" + "\n" + " report.conclusion = inferDirectDiagConclusion(report.tests);\n" + " report.finished_at = new Date().toISOString();\n" + " return report;\n" + " } catch (e) {\n" + " report.ok = false;\n" + " report.error = errorToText(e);\n" + " report.finished_at = new Date().toISOString();\n" + " return report;\n" + " } finally {\n" + " if (originalDebug != null) {\n" + " try {\n" + " const restored = await callApi('/v1/printer/direct/debug', 'POST', originalDebug);\n" + " fillDirectDebugForm(restored);\n" + " report.restored_original_debug = true;\n" + " } catch (_) {\n" + " report.restored_original_debug = false;\n" + " }\n" + " }\n" + "\n" + " if (printerBackendEl && previousBackend) {\n" + " printerBackendEl.value = previousBackend;\n" + " }\n" + " try { await fetchPrinterStatus(); } catch (_) {}\n" + "\n" + " debugAutoScanBusy = false;\n" + " if (btnDebugAutoScanEl) {\n" + " btnDebugAutoScanEl.disabled = false;\n" + " btnDebugAutoScanEl.textContent = 'Auto Diagnose (Direct)';\n" + " }\n" + " }\n" + " }\n" + "\n" " function syncPaperUi(st) {\n" " if (!paperStateEl || !paperMetaEl) {\n" " return;\n" @@ -487,6 +873,19 @@ static const char *s_web_index = " });\n" "\n" " document.getElementById('btnPaperCheck').onclick = () => run(() => fetchPrinterStatus());\n" + " document.getElementById('btnDebugLoad').onclick = () => run(() => fetchDirectDebug());\n" + " document.getElementById('btnDebugApply').onclick = () => run(async () => {\n" + " const payload = buildDirectDebugRequest();\n" + " const data = await callApi('/v1/printer/direct/debug', 'POST', payload);\n" + " fillDirectDebugForm(data);\n" + " return data;\n" + " });\n" + " document.getElementById('btnDebugReset').onclick = () => run(async () => {\n" + " const data = await callApi('/v1/printer/direct/debug', 'POST', { reset_defaults: true });\n" + " fillDirectDebugForm(data);\n" + " return data;\n" + " });\n" + " document.getElementById('btnDebugAutoScan').onclick = () => run(() => runDirectAutoScan());\n" "\n" " document.getElementById('btnPrintText').onclick = () => run(() => callApi('/v1/print/text', 'POST', {\n" " text: printTextEl.value,\n" @@ -531,6 +930,9 @@ static const char *s_web_index = " await fetchPrinterStatus();\n" " } catch (_) {}\n" " try {\n" + " await fetchDirectDebug();\n" + " } catch (_) {}\n" + " try {\n" " await refreshVoiceStatus();\n" " } catch (_) {}\n" " })();\n" @@ -564,7 +966,7 @@ esp_err_t rest_server_start(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = CONFIG_TQ_HTTP_PORT; config.uri_match_fn = httpd_uri_match_wildcard; - config.max_uri_handlers = 32; + config.max_uri_handlers = 36; config.stack_size = 10240; #if CONFIG_FREERTOS_UNICORE config.core_id = 0; @@ -620,6 +1022,20 @@ esp_err_t rest_server_start(void) { }; httpd_register_uri_handler(s_server, &status); + httpd_uri_t direct_debug_get_uri = { + .uri = "/v1/printer/direct/debug", + .method = HTTP_GET, + .handler = rest_server_printer_direct_debug_get, + }; + httpd_register_uri_handler(s_server, &direct_debug_get_uri); + + httpd_uri_t direct_debug_post_uri = { + .uri = "/v1/printer/direct/debug", + .method = HTTP_POST, + .handler = rest_server_printer_direct_debug_post, + }; + httpd_register_uri_handler(s_server, &direct_debug_post_uri); + httpd_uri_t voice_status = { .uri = "/v1/voice/status", .method = HTTP_GET, diff --git a/components/control_plane/src/rest_server_ops.c b/components/control_plane/src/rest_server_ops.c index c7c4d00..3993a36 100644 --- a/components/control_plane/src/rest_server_ops.c +++ b/components/control_plane/src/rest_server_ops.c @@ -265,6 +265,226 @@ esp_err_t rest_server_status_get(httpd_req_t *req) { return err; } +static void add_direct_debug_json_fields(cJSON *root, const printer_direct_debug_config_t *cfg) { + cJSON_AddNumberToObject(root, "shift_clock_high_us", cfg->shift_clock_high_us); + cJSON_AddNumberToObject(root, "shift_clock_low_us", cfg->shift_clock_low_us); + cJSON_AddNumberToObject(root, "latch_pulse_us", cfg->latch_pulse_us); + cJSON_AddBoolToObject(root, "strobe_active_high", cfg->strobe_active_high); + cJSON_AddBoolToObject(root, "boost_active_high", cfg->boost_active_high); + cJSON_AddNumberToObject(root, "override_strobe_on_us", cfg->override_strobe_on_us); + cJSON_AddNumberToObject(root, "override_strobe_interval_us", cfg->override_strobe_interval_us); + cJSON_AddNumberToObject(root, "override_motor_step_us", cfg->override_motor_step_us); + cJSON_AddNumberToObject(root, "override_steps_per_line", cfg->override_steps_per_line); +} + +static esp_err_t direct_debug_error_response(httpd_req_t *req, esp_err_t rc, const char *msg) { + if (rc == ESP_ERR_INVALID_ARG) { + return rest_server_send_error(req, "400 Bad Request", msg); + } + if (rc == ESP_ERR_NOT_SUPPORTED || rc == ESP_ERR_INVALID_STATE) { + return rest_server_send_error(req, "409 Conflict", msg); + } + if (rc == ESP_ERR_TIMEOUT) { + return rest_server_send_error(req, "504 Gateway Timeout", msg); + } + return rest_server_send_error(req, "500 Internal Server Error", msg); +} + +esp_err_t rest_server_printer_direct_debug_get(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + printer_direct_debug_config_t cfg = {0}; + char get_err[128] = {0}; + esp_err_t rc = printer_protocol_get_direct_debug_config(&cfg, get_err, sizeof(get_err)); + if (rc != ESP_OK) { + return direct_debug_error_response(req, + rc, + get_err[0] != '\0' ? get_err : "get direct debug failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddStringToObject(root, "backend", printer_backend_str(printer_protocol_get_backend())); + add_direct_debug_json_fields(root, &cfg); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + +esp_err_t rest_server_printer_direct_debug_post(httpd_req_t *req) { + if (!rest_server_auth_ok(req)) { + return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); + } + + printer_direct_debug_config_t cfg = {0}; + char get_err[128] = {0}; + esp_err_t rc = printer_protocol_get_direct_debug_config(&cfg, get_err, sizeof(get_err)); + if (rc != ESP_OK) { + return direct_debug_error_response(req, + rc, + get_err[0] != '\0' ? get_err : "get direct debug failed"); + } + + bool reset_defaults = false; + bool has_update = false; + if (req->content_len > 0) { + char *body = NULL; + esp_err_t body_err = rest_server_read_body(req, &body); + if (body_err != ESP_OK) { + return rest_server_send_error(req, "400 Bad Request", "invalid request body"); + } + + cJSON *json = cJSON_Parse(body); + free(body); + if (json == NULL) { + return rest_server_send_error(req, "400 Bad Request", "invalid json"); + } + + cJSON *jreset = cJSON_GetObjectItemCaseSensitive(json, "reset_defaults"); + if (cJSON_IsBool(jreset)) { + reset_defaults = cJSON_IsTrue(jreset); + } + + cJSON *jshift_hi = cJSON_GetObjectItemCaseSensitive(json, "shift_clock_high_us"); + if (jshift_hi != NULL) { + if (!cJSON_IsNumber(jshift_hi) || jshift_hi->valuedouble < 1 || jshift_hi->valuedouble > 50) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "shift_clock_high_us must be 1..50"); + } + cfg.shift_clock_high_us = (uint16_t)jshift_hi->valuedouble; + has_update = true; + } + + cJSON *jshift_lo = cJSON_GetObjectItemCaseSensitive(json, "shift_clock_low_us"); + if (jshift_lo != NULL) { + if (!cJSON_IsNumber(jshift_lo) || jshift_lo->valuedouble < 1 || jshift_lo->valuedouble > 50) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "shift_clock_low_us must be 1..50"); + } + cfg.shift_clock_low_us = (uint16_t)jshift_lo->valuedouble; + has_update = true; + } + + cJSON *jlatch = cJSON_GetObjectItemCaseSensitive(json, "latch_pulse_us"); + if (jlatch != NULL) { + if (!cJSON_IsNumber(jlatch) || jlatch->valuedouble < 1 || jlatch->valuedouble > 50) { + cJSON_Delete(json); + return rest_server_send_error(req, "400 Bad Request", "latch_pulse_us must be 1..50"); + } + cfg.latch_pulse_us = (uint16_t)jlatch->valuedouble; + has_update = true; + } + + cJSON *jstb = cJSON_GetObjectItemCaseSensitive(json, "strobe_active_high"); + if (cJSON_IsBool(jstb)) { + cfg.strobe_active_high = cJSON_IsTrue(jstb); + has_update = true; + } + + cJSON *jboost = cJSON_GetObjectItemCaseSensitive(json, "boost_active_high"); + if (cJSON_IsBool(jboost)) { + cfg.boost_active_high = cJSON_IsTrue(jboost); + has_update = true; + } + + cJSON *jstrobe_on = cJSON_GetObjectItemCaseSensitive(json, "override_strobe_on_us"); + if (jstrobe_on != NULL) { + if (!cJSON_IsNumber(jstrobe_on) || jstrobe_on->valuedouble < 0 || jstrobe_on->valuedouble > 10000) { + cJSON_Delete(json); + return rest_server_send_error(req, + "400 Bad Request", + "override_strobe_on_us must be 0..10000"); + } + if (jstrobe_on->valuedouble > 0 && jstrobe_on->valuedouble < 100) { + cJSON_Delete(json); + return rest_server_send_error(req, + "400 Bad Request", + "override_strobe_on_us must be 0 or 100..10000"); + } + cfg.override_strobe_on_us = (uint16_t)jstrobe_on->valuedouble; + has_update = true; + } + + cJSON *jstrobe_gap = cJSON_GetObjectItemCaseSensitive(json, "override_strobe_interval_us"); + if (jstrobe_gap != NULL) { + if (!cJSON_IsNumber(jstrobe_gap) || jstrobe_gap->valuedouble < 0 || jstrobe_gap->valuedouble > 10000) { + cJSON_Delete(json); + return rest_server_send_error(req, + "400 Bad Request", + "override_strobe_interval_us must be 0..10000"); + } + cfg.override_strobe_interval_us = (uint16_t)jstrobe_gap->valuedouble; + has_update = true; + } + + cJSON *jmotor = cJSON_GetObjectItemCaseSensitive(json, "override_motor_step_us"); + if (jmotor != NULL) { + if (!cJSON_IsNumber(jmotor) || jmotor->valuedouble < 0 || jmotor->valuedouble > 20000) { + cJSON_Delete(json); + return rest_server_send_error(req, + "400 Bad Request", + "override_motor_step_us must be 0..20000"); + } + if (jmotor->valuedouble > 0 && jmotor->valuedouble < 100) { + cJSON_Delete(json); + return rest_server_send_error(req, + "400 Bad Request", + "override_motor_step_us must be 0 or 100..20000"); + } + cfg.override_motor_step_us = (uint16_t)jmotor->valuedouble; + has_update = true; + } + + cJSON *jsteps = cJSON_GetObjectItemCaseSensitive(json, "override_steps_per_line"); + if (jsteps != NULL) { + if (!cJSON_IsNumber(jsteps) || jsteps->valuedouble < 0 || jsteps->valuedouble > 8) { + cJSON_Delete(json); + return rest_server_send_error(req, + "400 Bad Request", + "override_steps_per_line must be 0..8"); + } + cfg.override_steps_per_line = (uint8_t)jsteps->valuedouble; + has_update = true; + } + + cJSON_Delete(json); + } + + char set_err[128] = {0}; + rc = printer_protocol_set_direct_debug_config(has_update ? &cfg : NULL, + reset_defaults, + set_err, + sizeof(set_err)); + if (rc != ESP_OK) { + return direct_debug_error_response(req, + rc, + set_err[0] != '\0' ? set_err : "set direct debug failed"); + } + + memset(&cfg, 0, sizeof(cfg)); + memset(get_err, 0, sizeof(get_err)); + rc = printer_protocol_get_direct_debug_config(&cfg, get_err, sizeof(get_err)); + if (rc != ESP_OK) { + return direct_debug_error_response(req, + rc, + get_err[0] != '\0' ? get_err : "get direct debug failed"); + } + + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "ok", true); + cJSON_AddBoolToObject(root, "reset_defaults", reset_defaults); + cJSON_AddBoolToObject(root, "updated", has_update); + cJSON_AddStringToObject(root, "backend", printer_backend_str(printer_protocol_get_backend())); + add_direct_debug_json_fields(root, &cfg); + + esp_err_t err = rest_server_send_json(req, "200 OK", root); + cJSON_Delete(root); + return err; +} + esp_err_t rest_server_label_gap_move_post(httpd_req_t *req) { if (!rest_server_auth_ok(req)) { return rest_server_send_error(req, "401 Unauthorized", "unauthorized"); diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index fa32c95..ab9ae99 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -146,6 +146,18 @@ typedef struct { bool direct_ignore_precheck; } printer_print_options_t; +typedef struct { + uint16_t shift_clock_high_us; + uint16_t shift_clock_low_us; + uint16_t latch_pulse_us; + bool strobe_active_high; + bool boost_active_high; + uint16_t override_strobe_on_us; + uint16_t override_strobe_interval_us; + uint16_t override_motor_step_us; + uint8_t override_steps_per_line; +} printer_direct_debug_config_t; + typedef struct { bool supports_connect; bool supports_gap_move; @@ -184,6 +196,13 @@ 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); +esp_err_t printer_protocol_get_direct_debug_config(printer_direct_debug_config_t *out_config, + char *err, + size_t err_len); +esp_err_t printer_protocol_set_direct_debug_config(const printer_direct_debug_config_t *config, + bool reset_defaults, + char *err, + size_t err_len); 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); diff --git a/components/domain/src/printer_protocol.c b/components/domain/src/printer_protocol.c index adb3038..ca5befb 100644 --- a/components/domain/src/printer_protocol.c +++ b/components/domain/src/printer_protocol.c @@ -51,6 +51,14 @@ static const char *TAG = "printer_protocol"; static void on_rx_frame(const uint8_t *data, size_t len); +static printer_backend_t default_backend_on_startup(void) { + // This hardware has a first-class direct path; BLE remains opt-in via API. + if (runtime_policy_direct_printer_enabled()) { + return PRINTER_BACKEND_DIRECT; + } + return PRINTER_BACKEND_BLE; +} + static void backend_capabilities_for(printer_backend_t backend, printer_capabilities_t *out_caps) { if (out_caps == NULL) { return; @@ -643,9 +651,7 @@ esp_err_t printer_protocol_init(void) { 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; + s_backend = default_backend_on_startup(); esp_err_t err = ensure_backend_ready(s_backend); if (err != ESP_OK) { @@ -757,7 +763,7 @@ esp_err_t printer_protocol_stop(uint32_t timeout_ms) { s_protocol_initialized = false; s_protocol_stopping = false; s_ble_client_initialized = false; - s_backend = PRINTER_BACKEND_BLE; + s_backend = default_backend_on_startup(); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_STATUS_POLL_PAUSE_DEPTH, 0); runtime_diag_set_gauge(RUNTIME_DIAG_GAUGE_PRINTER_QUEUE_DEPTH, 0); diff --git a/components/domain/src/printer_protocol_commands.c b/components/domain/src/printer_protocol_commands.c index 970871f..54b50bc 100644 --- a/components/domain/src/printer_protocol_commands.c +++ b/components/domain/src/printer_protocol_commands.c @@ -15,6 +15,12 @@ static esp_err_t unsupported_for_direct(char *err, size_t err_len, const char *o return ESP_ERR_NOT_SUPPORTED; } +static void write_err(char *err, size_t err_len, const char *msg) { + if (err != NULL && err_len > 0) { + snprintf(err, err_len, "%s", msg); + } +} + 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); @@ -343,6 +349,68 @@ esp_err_t printer_protocol_ota_get_version(printer_ota_version_t *out_version, return ESP_OK; } +esp_err_t printer_protocol_get_direct_debug_config(printer_direct_debug_config_t *out_config, + char *err, + size_t err_len) { + if (out_config == NULL) { + write_err(err, err_len, "invalid args"); + return ESP_ERR_INVALID_ARG; + } + if (!runtime_policy_direct_printer_enabled()) { + write_err(err, err_len, "direct backend disabled"); + return ESP_ERR_NOT_SUPPORTED; + } + + platform_direct_debug_config_t cfg = {0}; + esp_err_t rc = platform_direct_printer_get_debug_config(&cfg); + if (rc != ESP_OK) { + write_err(err, err_len, "get direct debug config failed"); + return rc; + } + + out_config->shift_clock_high_us = cfg.shift_clock_high_us; + out_config->shift_clock_low_us = cfg.shift_clock_low_us; + out_config->latch_pulse_us = cfg.latch_pulse_us; + out_config->strobe_active_high = cfg.strobe_active_high; + out_config->boost_active_high = cfg.boost_active_high; + out_config->override_strobe_on_us = cfg.override_strobe_on_us; + out_config->override_strobe_interval_us = cfg.override_strobe_interval_us; + out_config->override_motor_step_us = cfg.override_motor_step_us; + out_config->override_steps_per_line = cfg.override_steps_per_line; + return ESP_OK; +} + +esp_err_t printer_protocol_set_direct_debug_config(const printer_direct_debug_config_t *config, + bool reset_defaults, + char *err, + size_t err_len) { + if (config == NULL && !reset_defaults) { + write_err(err, err_len, "invalid args"); + return ESP_ERR_INVALID_ARG; + } + if (!runtime_policy_direct_printer_enabled()) { + write_err(err, err_len, "direct backend disabled"); + return ESP_ERR_NOT_SUPPORTED; + } + + platform_direct_debug_config_t cfg = {0}; + platform_direct_debug_config_t *pcfg = NULL; + if (config != NULL) { + cfg.shift_clock_high_us = config->shift_clock_high_us; + cfg.shift_clock_low_us = config->shift_clock_low_us; + cfg.latch_pulse_us = config->latch_pulse_us; + cfg.strobe_active_high = config->strobe_active_high; + cfg.boost_active_high = config->boost_active_high; + cfg.override_strobe_on_us = config->override_strobe_on_us; + cfg.override_strobe_interval_us = config->override_strobe_interval_us; + cfg.override_motor_step_us = config->override_motor_step_us; + cfg.override_steps_per_line = config->override_steps_per_line; + pcfg = &cfg; + } + + return platform_direct_printer_set_debug_config(pcfg, reset_defaults, err, err_len); +} + const char *printer_protocol_job_state_str(print_job_state_t state) { switch (state) { case PRINT_JOB_STATE_NONE: diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 6dee0b4..65baaa8 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -61,12 +61,29 @@ typedef struct { const volatile bool *cancel_flag; } platform_direct_print_request_t; +typedef struct { + uint16_t shift_clock_high_us; + uint16_t shift_clock_low_us; + uint16_t latch_pulse_us; + bool strobe_active_high; + bool boost_active_high; + uint16_t override_strobe_on_us; + uint16_t override_strobe_interval_us; + uint16_t override_motor_step_us; + uint8_t override_steps_per_line; +} platform_direct_debug_config_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_get_debug_config(platform_direct_debug_config_t *out_config); +esp_err_t platform_direct_printer_set_debug_config(const platform_direct_debug_config_t *config, + bool reset_defaults, + char *err, + size_t err_len); esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *request, char *err, size_t err_len); diff --git a/components/platform/src/direct_thermal_printer.c b/components/platform/src/direct_thermal_printer.c index 8c446ef..06f161d 100644 --- a/components/platform/src/direct_thermal_printer.c +++ b/components/platform/src/direct_thermal_printer.c @@ -23,14 +23,21 @@ #endif #if CONFIG_TQ_KEY_PRINT_BOOST_ACTIVE_HIGH -#define BOOST_ON_LEVEL 1 -#define BOOST_OFF_LEVEL 0 +#define BOOST_DEFAULT_ACTIVE_HIGH 1 #else -#define BOOST_ON_LEVEL 0 -#define BOOST_OFF_LEVEL 1 +#define BOOST_DEFAULT_ACTIVE_HIGH 0 #endif #define ADC_RAW_MAX 4095 +#define DEBUG_SHIFT_CLOCK_US_MIN 1 +#define DEBUG_SHIFT_CLOCK_US_MAX 50 +#define DEBUG_LATCH_PULSE_US_MIN 1 +#define DEBUG_LATCH_PULSE_US_MAX 50 + +static const uint16_t k_default_shift_clock_high_us = 5; +static const uint16_t k_default_shift_clock_low_us = 5; +static const uint16_t k_default_latch_pulse_us = 1; +static const bool k_default_strobe_active_high = true; static const char *TAG = "direct_printer"; static const uint8_t k_print_yield_lines = 8; @@ -46,6 +53,7 @@ typedef struct { bool initialized; bool connected; uint8_t motor_phase; + platform_direct_debug_config_t debug; SemaphoreHandle_t lock; adc_oneshot_unit_handle_t adc1_handle; adc_oneshot_unit_handle_t adc2_handle; @@ -57,6 +65,31 @@ static direct_printer_state_t s_state; static const uint8_t k_motor_step_table[4] = {0x05, 0x09, 0x0A, 0x06}; +static int boost_on_level_locked(void) { + return s_state.debug.boost_active_high ? 1 : 0; +} + +static int boost_off_level_locked(void) { + return s_state.debug.boost_active_high ? 0 : 1; +} + +static int strobe_active_level_locked(void) { + return s_state.debug.strobe_active_high ? 1 : 0; +} + +static int strobe_idle_level_locked(void) { + return s_state.debug.strobe_active_high ? 0 : 1; +} + +static void set_debug_defaults_locked(void) { + memset(&s_state.debug, 0, sizeof(s_state.debug)); + s_state.debug.shift_clock_high_us = k_default_shift_clock_high_us; + s_state.debug.shift_clock_low_us = k_default_shift_clock_low_us; + s_state.debug.latch_pulse_us = k_default_latch_pulse_us; + s_state.debug.strobe_active_high = k_default_strobe_active_high; + s_state.debug.boost_active_high = BOOST_DEFAULT_ACTIVE_HIGH != 0; +} + static bool is_gpio_valid(int gpio_num) { return gpio_num >= 0; } @@ -118,9 +151,10 @@ static esp_err_t config_input_pin(int gpio_num, bool pull_up, const char *name) } 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); + int out_level = level ? strobe_active_level_locked() : strobe_idle_level_locked(); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_12_PIN, out_level); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_34_PIN, out_level); + set_gpio_level_if_valid(CONFIG_TQ_PRINT_STB_56_PIN, out_level); } static void motor_apply_pattern_locked(uint8_t pattern) { @@ -141,13 +175,19 @@ static void safe_drive_off_locked(bool keep_boost_on) { 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); + set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, boost_off_level_locked()); } } static void pulse_latch_locked(void) { + uint16_t latch_us = s_state.debug.latch_pulse_us; + if (latch_us < DEBUG_LATCH_PULSE_US_MIN) { + latch_us = DEBUG_LATCH_PULSE_US_MIN; + } else if (latch_us > DEBUG_LATCH_PULSE_US_MAX) { + latch_us = DEBUG_LATCH_PULSE_US_MAX; + } set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 0); - esp_rom_delay_us(1); + esp_rom_delay_us(latch_us); set_gpio_level_if_valid(CONFIG_TQ_PRINT_LAT_PIN, 1); } @@ -174,15 +214,28 @@ static void motor_step_once_locked(uint16_t step_delay_us) { } static void write_line_bits_locked(const uint8_t *line, size_t line_bytes) { + uint16_t high_us = s_state.debug.shift_clock_high_us; + uint16_t low_us = s_state.debug.shift_clock_low_us; + if (high_us < DEBUG_SHIFT_CLOCK_US_MIN) { + high_us = DEBUG_SHIFT_CLOCK_US_MIN; + } else if (high_us > DEBUG_SHIFT_CLOCK_US_MAX) { + high_us = DEBUG_SHIFT_CLOCK_US_MAX; + } + if (low_us < DEBUG_SHIFT_CLOCK_US_MIN) { + low_us = DEBUG_SHIFT_CLOCK_US_MIN; + } else if (low_us > DEBUG_SHIFT_CLOCK_US_MAX) { + low_us = DEBUG_SHIFT_CLOCK_US_MAX; + } + 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); + esp_rom_delay_us(high_us); set_gpio_level_if_valid(CONFIG_TQ_SPI_SHARED_CLK_PIN, 0); - esp_rom_delay_us(1); + esp_rom_delay_us(low_us); } } } @@ -423,17 +476,18 @@ esp_err_t platform_direct_printer_init(void) { if (s_state.lock == NULL) { return ESP_ERR_NO_MEM; } + set_debug_defaults_locked(); esp_err_t err = ESP_OK; - err = config_output_pin(CONFIG_TQ_PRINT_STB_12_PIN, 0, "print_stb12"); + err = config_output_pin(CONFIG_TQ_PRINT_STB_12_PIN, strobe_idle_level_locked(), "print_stb12"); if (err != ESP_OK) { return err; } - err = config_output_pin(CONFIG_TQ_PRINT_STB_34_PIN, 0, "print_stb34"); + err = config_output_pin(CONFIG_TQ_PRINT_STB_34_PIN, strobe_idle_level_locked(), "print_stb34"); if (err != ESP_OK) { return err; } - err = config_output_pin(CONFIG_TQ_PRINT_STB_56_PIN, 0, "print_stb56"); + err = config_output_pin(CONFIG_TQ_PRINT_STB_56_PIN, strobe_idle_level_locked(), "print_stb56"); if (err != ESP_OK) { return err; } @@ -466,7 +520,7 @@ esp_err_t platform_direct_printer_init(void) { if (err != ESP_OK) { return err; } - err = config_output_pin(CONFIG_TQ_KEY_PRINT_PIN, BOOST_OFF_LEVEL, "print_boost"); + err = config_output_pin(CONFIG_TQ_KEY_PRINT_PIN, boost_off_level_locked(), "print_boost"); if (err != ESP_OK) { return err; } @@ -527,7 +581,7 @@ esp_err_t platform_direct_printer_connect(uint32_t timeout_ms) { } if (!s_state.connected) { - set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, BOOST_ON_LEVEL); + set_gpio_level_if_valid(CONFIG_TQ_KEY_PRINT_PIN, boost_on_level_locked()); vTaskDelay(pdMS_TO_TICKS(10)); safe_drive_off_locked(true); s_state.connected = true; @@ -577,6 +631,103 @@ esp_err_t platform_direct_printer_get_sensors(platform_printer_sensors_t *out_se return ESP_OK; } +esp_err_t platform_direct_printer_get_debug_config(platform_direct_debug_config_t *out_config) { +#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE + (void)out_config; + return ESP_ERR_NOT_SUPPORTED; +#else + if (out_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t init_rc = platform_direct_printer_init(); + if (init_rc != ESP_OK) { + return init_rc; + } + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(500)) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + *out_config = s_state.debug; + xSemaphoreGive(s_state.lock); + return ESP_OK; +#endif +} + +esp_err_t platform_direct_printer_set_debug_config(const platform_direct_debug_config_t *config, + bool reset_defaults, + char *err, + size_t err_len) { +#if !CONFIG_TQ_DIRECT_PRINTER_ENABLE + (void)config; + (void)reset_defaults; + write_err(err, err_len, "direct backend disabled"); + return ESP_ERR_NOT_SUPPORTED; +#else + if (config == NULL && !reset_defaults) { + write_err(err, err_len, "invalid args"); + return ESP_ERR_INVALID_ARG; + } + + esp_err_t init_rc = platform_direct_printer_init(); + if (init_rc != ESP_OK) { + write_err(err, err_len, "direct backend not initialized"); + return init_rc; + } + if (xSemaphoreTake(s_state.lock, pdMS_TO_TICKS(1000)) != pdTRUE) { + write_err(err, err_len, "direct printer lock timeout"); + return ESP_ERR_TIMEOUT; + } + + if (reset_defaults) { + set_debug_defaults_locked(); + } + + if (config != NULL) { + if (config->shift_clock_high_us < DEBUG_SHIFT_CLOCK_US_MIN || + config->shift_clock_high_us > DEBUG_SHIFT_CLOCK_US_MAX || + config->shift_clock_low_us < DEBUG_SHIFT_CLOCK_US_MIN || + config->shift_clock_low_us > DEBUG_SHIFT_CLOCK_US_MAX) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "shift clock us out of range (1..50)"); + return ESP_ERR_INVALID_ARG; + } + if (config->latch_pulse_us < DEBUG_LATCH_PULSE_US_MIN || + config->latch_pulse_us > DEBUG_LATCH_PULSE_US_MAX) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "latch pulse us out of range (1..50)"); + return ESP_ERR_INVALID_ARG; + } + if (config->override_strobe_on_us != 0 && + (config->override_strobe_on_us < 100 || config->override_strobe_on_us > 10000)) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "strobe_on_us must be 0 or 100..10000"); + return ESP_ERR_INVALID_ARG; + } + if (config->override_strobe_interval_us > 10000) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "strobe_interval_us must be 0..10000"); + return ESP_ERR_INVALID_ARG; + } + if (config->override_motor_step_us != 0 && + (config->override_motor_step_us < 100 || config->override_motor_step_us > 20000)) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "motor_step_us must be 0 or 100..20000"); + return ESP_ERR_INVALID_ARG; + } + if (config->override_steps_per_line > 8) { + xSemaphoreGive(s_state.lock); + write_err(err, err_len, "steps_per_line must be 0..8"); + return ESP_ERR_INVALID_ARG; + } + + s_state.debug = *config; + } + + safe_drive_off_locked(s_state.connected); + xSemaphoreGive(s_state.lock); + return ESP_OK; +#endif +} + esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *request, char *err, size_t err_len) { @@ -610,19 +761,6 @@ esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *r 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; @@ -634,6 +772,32 @@ esp_err_t platform_direct_printer_print(const platform_direct_print_request_t *r return ESP_ERR_INVALID_STATE; } + uint16_t strobe_on_us = (request->strobe_on_time_us > 0) + ? request->strobe_on_time_us + : runtime_policy_direct_printer_strobe_on_us(); + uint16_t strobe_interval_us = (request->strobe_interval_us > 0) + ? request->strobe_interval_us + : runtime_policy_direct_printer_strobe_interval_us(); + uint16_t motor_step_us = (request->motor_step_delay_us > 0) + ? request->motor_step_delay_us + : runtime_policy_direct_printer_motor_step_us(); + uint8_t steps_per_line = (request->motor_steps_per_line > 0) + ? request->motor_steps_per_line + : runtime_policy_direct_printer_steps_per_line(); + + if (s_state.debug.override_strobe_on_us > 0) { + strobe_on_us = s_state.debug.override_strobe_on_us; + } + if (s_state.debug.override_strobe_interval_us > 0) { + strobe_interval_us = s_state.debug.override_strobe_interval_us; + } + if (s_state.debug.override_motor_step_us > 0) { + motor_step_us = s_state.debug.override_motor_step_us; + } + if (s_state.debug.override_steps_per_line > 0) { + steps_per_line = s_state.debug.override_steps_per_line; + } + esp_err_t rc = ESP_OK; if (!request->ignore_precheck) { rc = precheck_before_print_locked(err, err_len); @@ -725,6 +889,9 @@ esp_err_t platform_direct_printer_gap_move(uint32_t timeout_ms, char *err, size_ 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(); + if (s_state.debug.override_motor_step_us > 0) { + step_delay_us = s_state.debug.override_motor_step_us; + } platform_printer_sensors_t sensors = {0}; sample_sensors_locked(&sensors);