Files
dzc 689a07ca44 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 已注册
2026-09-21 12:03:27 +08:00

94 lines
2.9 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}