添加热敏打印机调试功能

This commit is contained in:
admin
2026-02-27 16:39:54 +08:00
parent 2b6bcc83dc
commit 46a800c499
10 changed files with 951 additions and 35 deletions

View File

@@ -78,6 +78,57 @@ static const char *s_web_index =
" </div>\n"
" <div class='small'>Only effective on Direct backend. Use for sensor bring-up/debug.</div>\n"
"\n"
" <label>Direct Debug</label>\n"
" <div class='small'>参数标签中的字段名与 <code>/v1/printer/direct/debug</code> 的 JSON 键保持一致。</div>\n"
" <div class='row'>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>shift_clock_high_us (CLK高电平时长, us)</label>\n"
" <input id='dbgShiftHighUs' type='number' min='1' max='50' value='5' placeholder='1..50' style='max-width:100%'>\n"
" </div>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>shift_clock_low_us (CLK低电平时长, us)</label>\n"
" <input id='dbgShiftLowUs' type='number' min='1' max='50' value='5' placeholder='1..50' style='max-width:100%'>\n"
" </div>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>latch_pulse_us (LAT脉宽, us)</label>\n"
" <input id='dbgLatchUs' type='number' min='1' max='50' value='1' placeholder='1..50' style='max-width:100%'>\n"
" </div>\n"
" </div>\n"
" <div class='row'>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>override_strobe_on_us (加热点火时长覆盖, us)</label>\n"
" <input id='dbgHotUs' type='number' min='0' max='10000' value='0' placeholder='0 或 100..10000' style='max-width:100%'>\n"
" </div>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>override_strobe_interval_us (加热间隔覆盖, us)</label>\n"
" <input id='dbgStrobeGapUs' type='number' min='0' max='10000' value='0' placeholder='0..10000' style='max-width:100%'>\n"
" </div>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>override_motor_step_us (电机步进延时覆盖, us)</label>\n"
" <input id='dbgMotorUs' type='number' min='0' max='20000' value='0' placeholder='0 或 100..20000' style='max-width:100%'>\n"
" </div>\n"
" <div style='min-width:220px;max-width:260px;flex:1;'>\n"
" <label style='margin:0 0 4px 0;'>override_steps_per_line (每行步进数覆盖)</label>\n"
" <input id='dbgStepsPerLine' type='number' min='0' max='8' value='0' placeholder='0..8' style='max-width:100%'>\n"
" </div>\n"
" </div>\n"
" <div class='row'>\n"
" <label style='margin:0;display:flex;align-items:center;gap:6px;'>\n"
" <input id='dbgStbActiveHigh' type='checkbox' checked style='width:auto;'>\n"
" strobe_active_high (STB有效电平为高)\n"
" </label>\n"
" <label style='margin:0;display:flex;align-items:center;gap:6px;'>\n"
" <input id='dbgBoostActiveHigh' type='checkbox' checked style='width:auto;'>\n"
" boost_active_high (BOOST有效电平为高)\n"
" </label>\n"
" <button id='btnDebugLoad' class='alt'>Load Debug</button>\n"
" <button id='btnDebugApply' class='alt'>Apply Debug</button>\n"
" <button id='btnDebugReset' class='warn'>Reset Debug</button>\n"
" <button id='btnDebugAutoScan' class='alt'>Auto Diagnose (Direct)</button>\n"
" </div>\n"
" <div class='small'>override_* 参数填 0 表示不覆盖,恢复为运行时默认策略。</div>\n"
" <div class='small'>Auto Diagnose 使用低功耗测试图案若点击后仍重启Brownout请先改善供电或降低热头功率。</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"
@@ -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,

View File

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