diff --git a/lib/print/cups_backend.dart b/lib/print/cups_backend.dart index 63d4eeb..dbf83ac 100644 --- a/lib/print/cups_backend.dart +++ b/lib/print/cups_backend.dart @@ -114,23 +114,65 @@ class CupsPrintBackend implements PrintBackend { try { final back = request.back; if (back != null && back.color.isNotEmpty) { - backFile = await _writeTempPng(back.color); + backFile = await _writeTempPng(request.back!.color); } final copies = request.copies < 1 ? 1 : request.copies; for (var i = 0; i < copies; i++) { - await runCommand('lp', ['-d', queue.name, frontFile]); + await _submitAndTrack(queue.name, frontFile); if (backFile != null) { - await runCommand('lp', ['-d', queue.name, backFile]); + await _submitAndTrack(queue.name, backFile); } } - _log('已提交到 CUPS 队列 ${queue.name}(份数 $copies${backFile != null ? ',含背面' : ''})'); + _log('打印完成:CUPS 队列 ${queue.name}(份数 $copies${backFile != null ? ',含背面' : ''})'); } finally { await _deleteQuiet(frontFile); await _deleteQuiet(backFile); } } + /// CUPS 作业跟踪超时(秒):单卡物理打印约 20-30s,留足余量; + /// 超时视为卡纸/缺卡/暂停等异常,任务转失败而非假装完成。 + static const _jobTrackTimeoutSec = 170; + + /// lp 提交并跟踪到**物理打印完成**(作业离开 CUPS 活动队列)才返回。 + /// + /// lp 返回只代表"已入队"(约 0.1s),若不跟踪,任务瞬间变"完成", + /// 前端批量进度会整体提前跑满(第一张还没打页面就 100%)。 + /// 解析 lp 输出的作业号,轮询 `lpstat -o`(仅列未完成作业)直到作业号消失。 + Future _submitAndTrack(String queueName, String file) async { + final out = await runCommand('lp', ['-d', queueName, file]); + // lp 输出被系统本地化(中文"请求id是X(1个文件)",LC_ALL=C 压不住,同 lpstat 的坑), + // 作业号取到空白或中英文括号为止,避免把"(1个文件)"尾巴带进作业号 + final m = RegExp(r'request id is\s+([^\s((]+)|请求id是\s*([^\s((]+)').firstMatch(out); + final jobId = (m?.group(1) ?? m?.group(2) ?? '').trim(); + if (jobId.isEmpty) { + // 输出格式异常时退化为旧行为(提交即完成),不影响出卡 + _log('lp 已提交(未解析到作业号,跳过完成跟踪):${out.trim()}'); + return; + } + _log('CUPS 作业 $jobId 已入队,等待物理打印完成…'); + final deadline = DateTime.now().add(const Duration(seconds: _jobTrackTimeoutSec)); + while (DateTime.now().isBefore(deadline)) { + await Future.delayed(const Duration(seconds: 1)); + String active; + try { + active = await runCommand('lpstat', ['-o'], tolerateExit: true); + } on CommandException { + return; // lpstat 不可用,放弃跟踪(作业仍会打印) + } + if (!active.contains(jobId)) { + _log('CUPS 作业 $jobId 打印完成'); + return; + } + } + throw StateError('打印超时:作业 $jobId 长时间未完成,请检查打印机(卡纸/缺卡/被暂停)'); + } + /// data URL PNG 落临时文件供 lp 提交。 + /// + /// 2026-09-23 方向验证:打印机链路做上下翻转,此处曾加 flipVertical 抵消并打出 + /// 正向成品(白卡实证);当前按用户要求**临时去掉翻转做消融测试**——预测:上下颠倒。 + /// 验证后若需恢复,把 out 改回 img.encodePng(img.flipVertical(decoded))。 Future _writeTempPng(String dataUrl) async { final comma = dataUrl.indexOf(','); final base64Part = comma >= 0 ? dataUrl.substring(comma + 1) : dataUrl;