151 lines
5.4 KiB
Dart
151 lines
5.4 KiB
Dart
import 'dart:isolate';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:ffi/ffi.dart';
|
||
|
||
import '../../image/card_image.dart';
|
||
import '../../server/protocol.dart';
|
||
import '../../utils/file_log.dart';
|
||
import '../backend.dart';
|
||
import '../cups_backend.dart' show noopLog;
|
||
import 'pavo_ffi.dart';
|
||
import 'pavo_printer.dart';
|
||
|
||
/// Windows 打印后端:HiTi PavoApi 直打(BGR/K 分层 BITMAP)。
|
||
///
|
||
/// 所有 ffi 调用(含像素转换的 CPU 密集计算)都放进 `Isolate.run`:
|
||
/// PavoApi 的同步调用最长可阻塞数秒(发送 2MB 像素)、打印轮询最长 3 分钟,
|
||
/// 放独立 isolate 保证主 isolate 的 HTTP 服务(任务状态轮询)不被卡住。
|
||
///
|
||
/// **重要**:传给 Isolate.run 的闭包/函数不得捕获任何实例状态(this 链会经
|
||
/// onLog→AgentLog→StreamController→UI 订阅链一路拽到 Timer 等 unsendable 对象,
|
||
/// isolate 消息序列化直接抛 "Illegal argument in isolate message")——
|
||
/// 因此 isolate 入口全部是顶层函数,只以纯数据(字符串)进出。
|
||
class PavoPrintBackend implements PrintBackend {
|
||
PavoPrintBackend({void Function(String line)? onLog}) : _log = onLog ?? noopLog;
|
||
|
||
final void Function(String line) _log;
|
||
|
||
@override
|
||
String get name => 'pavo';
|
||
|
||
@override
|
||
Future<PrinterStatus> getStatus() {
|
||
// 日志文件路径以纯数据传入 isolate(isolate 内 stdout 在 GUI 模式下丢失)
|
||
final logPath = FileLog.currentPath;
|
||
return Isolate.run(() => pavoStatusSync(logPath));
|
||
}
|
||
|
||
@override
|
||
Future<void> printOneCard(PrintRequest request) {
|
||
_log('Pavo 直打任务开始(份数 ${request.copies},后端在子 isolate 执行)');
|
||
final front = request.front!;
|
||
final back = request.back;
|
||
// 先解出纯数据再进闭包:闭包只捕获字符串/整数(最小捕获面,杜绝实例链)
|
||
final frontColor = front.color;
|
||
final frontK = front.k;
|
||
final backColor = back?.color;
|
||
final backK = back?.k;
|
||
final copies = request.copies;
|
||
final logPath = FileLog.currentPath;
|
||
return Isolate.run(() => pavoPrintSync(
|
||
frontColor: frontColor,
|
||
frontK: frontK,
|
||
backColor: backColor,
|
||
backK: backK,
|
||
copies: copies,
|
||
logPath: logPath,
|
||
));
|
||
}
|
||
}
|
||
|
||
/// isolate 入口:查询打印机状态(顶层函数,入参均为纯数据)。
|
||
PrinterStatus pavoStatusSync(String? logPath) {
|
||
try {
|
||
final api = PavoApi();
|
||
final printers = PavoPrinter.enumUsbPrinters(api, logPath: logPath);
|
||
if (printers.isEmpty) {
|
||
return PrinterStatus.offline('打印机未连接');
|
||
}
|
||
final printer = printers.first.toNativeUtf16();
|
||
try {
|
||
final status = PavoPrinter.getStatus(api, printer);
|
||
final (ribbonType, remainCount) = PavoPrinter.getRibbonInfo(api, printer);
|
||
final dto = PrinterStatus(
|
||
online: true,
|
||
model: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoModelName),
|
||
firmware: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoFirmwareVersion),
|
||
connection: 'USB',
|
||
ribbon: RibbonInfo(
|
||
type: PavoPrinter.ribbonTypeName(ribbonType),
|
||
// 设备返回剩余张数,按常见 100 张装 YMCKO 色带折算百分比
|
||
remain: ribbonType == PavoApi.ribbonMissing ? -1 : (remainCount > 100 ? 100 : remainCount),
|
||
remainCount: remainCount,
|
||
),
|
||
status: status,
|
||
statusText: PavoPrinter.statusTextOf(status),
|
||
);
|
||
if (status == PavoApi.dsCardLow || status == PavoApi.dsCardOut) {
|
||
dto.cardLow = true;
|
||
}
|
||
return dto;
|
||
} finally {
|
||
calloc.free(printer);
|
||
}
|
||
} catch (e) {
|
||
return PrinterStatus.offline('状态读取失败:$e');
|
||
}
|
||
}
|
||
|
||
/// isolate 入口:转换图层并直打(顶层函数,参数全部为可发送的纯数据)。
|
||
Future<void> pavoPrintSync({
|
||
required String frontColor,
|
||
String? frontK,
|
||
String? backColor,
|
||
String? backK,
|
||
required int copies,
|
||
String? logPath,
|
||
}) async {
|
||
final api = PavoApi();
|
||
final printers = PavoPrinter.enumUsbPrinters(api, logPath: logPath);
|
||
if (printers.isEmpty) {
|
||
throw StateError('未找到 USB 连接的 HiTi 证卡打印机');
|
||
}
|
||
final printer = printers.first.toNativeUtf16();
|
||
try {
|
||
// Windows Pavo 直打方向校准(2026-09-23 实测):纵向位图 + byOrientation=1 会被
|
||
// SDK 旋转 90° 且锚定错位;统一预旋转成 1014×642 横向位图后按 byOrientation=2 打印。
|
||
// 旋转方向常量见 card_image.dart 的 pavoPortraitRotateClockwise(测试卡校准)。
|
||
final frontImg = CardImage.fromDataUrl(frontColor).toLandscape();
|
||
final frontBgr = frontImg.toBgr24();
|
||
final Uint8List? frontKGray = frontK != null && frontK.isNotEmpty
|
||
? CardImage.fromDataUrl(frontK).toLandscape().toGray8()
|
||
: null;
|
||
final Uint8List? backBgr;
|
||
final Uint8List? backKGray;
|
||
if (backColor != null && backColor.isNotEmpty) {
|
||
final backImg = CardImage.fromDataUrl(backColor).toLandscape();
|
||
backBgr = backImg.toBgr24();
|
||
backKGray = backK != null && backK.isNotEmpty
|
||
? CardImage.fromDataUrl(backK).toLandscape().toGray8()
|
||
: null;
|
||
} else {
|
||
backBgr = null;
|
||
backKGray = null;
|
||
}
|
||
|
||
await PavoPrinter.printOneCard(
|
||
api,
|
||
printer,
|
||
frontBgr: frontBgr,
|
||
frontK: frontKGray,
|
||
backBgr: backBgr,
|
||
backK: backKGray,
|
||
isLandscape: true,
|
||
copies: copies,
|
||
);
|
||
} finally {
|
||
calloc.free(printer);
|
||
}
|
||
}
|