feat: 饭卡打印助手 Flutter 版(Phase 1/2:协议服务器 + macOS CUPS 真机打印)

- 协议与 C# 版 card-printer-agent 逐字段对齐(38200-38202 顺延、CORS *、串行任务队列、30 分钟保留)
- macOS CUPS 后端:lpoptions 队列发现 + lp 参数列表提交(LC_ALL=C 防本地化粘连坑)
- --mock 模拟模式 + bin/agent.dart headless 入口 + 测试卡生成工具
- 真机验收:HiTi CS-200e status 就绪 + 真实出卡;macOS 通用二进制构建通过
- App Sandbox 已关闭(服务需绑回环端口并调用 lp 子进程);zaxprint URL scheme 已注册
This commit is contained in:
dzc committed 2026-09-21 12:03:27 +08:00
commit 689a07ca44
46 files changed
+2986

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import 'dart:async';
import '../print/backend.dart';
import '../print/cups_backend.dart' show noopLog;
import '../utils/command_runner.dart';
import 'protocol.dart';
/// 打印队列中的单个任务运行时状态。
class PrintQueueTask {
PrintQueueTask({required this.taskId, required this.request});
final String taskId;
final PrintRequest request;
/// printing / done / error
String state = 'printing';
int status = 0;
String error = '';
DateTime? finishedAt;
}
/// 串行打印队列:打印机物理上一次只能打一张卡,任务逐个执行。
///
/// 任务状态存内存字典供前端轮询;完成后保留 30 分钟(对齐 C# 版 PrintQueue)。
class PrintQueue {
PrintQueue(this._backend, {void Function(String line)? onLog}) : _log = onLog ?? noopLog {
_cleanupTimer = Timer.periodic(const Duration(minutes: 5), (_) => _cleanupOld());
}
final PrintBackend _backend;
final void Function(String line) _log;
final _tasks = <String, PrintQueueTask>{};
Future<void> _tail = Future.value();
Timer? _cleanupTimer;
int _seq = 0;
/// 提交打印任务,立即返回 taskId。
String submit(PrintRequest request) {
if (request.front == null || request.front!.color.isEmpty) {
throw ArgumentError('缺少正面卡面图(front.color)');
}
final task = PrintQueueTask(taskId: 't${++_seq}', request: request);
_tasks[task.taskId] = task;
// 串行链:每个任务等前一个完成后执行,异常在 _run 内消化,链条不中断
_tail = _tail.then((_) => _run(task));
return task.taskId;
}
PrintTaskDto? get(String taskId) {
final task = _tasks[taskId];
if (task == null) return null;
return PrintTaskDto(taskId: task.taskId, state: task.state, status: task.status, statusText: task.error);
}
Future<void> _run(PrintQueueTask task) async {
_log('[${task.taskId}] 开始打印(份数 ${task.request.copies})');
try {
await _backend.printOneCard(task.request);
task.state = 'done';
_log('[${task.taskId}] 打印完成');
} catch (e) {
task.state = 'error';
task.error = errorMessageOf(e);
_log('[${task.taskId}] 打印失败:${task.error}');
} finally {
task.finishedAt = DateTime.now();
}
}
/// 清掉已完成/失败超 30 分钟的任务,避免内存常驻增长。
void _cleanupOld() {
final now = DateTime.now();
_tasks.removeWhere((_, task) =>
task.finishedAt != null && now.difference(task.finishedAt!).inMinutes > 30);
}
void dispose() {
_cleanupTimer?.cancel();
_cleanupTimer = null;
}
/// 从任意异常里提取可读文案(前端 statusText 直接展示)。
static String errorMessageOf(Object e) {
if (e is CommandException) return e.message;
try {
final dynamic err = e;
final msg = err.message;
if (msg is String && msg.isNotEmpty) return msg;
} catch (_) {}
return e.toString();
}
}