import 'dart:async'; import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import '../../image/card_image.dart'; import 'pavo_ffi.dart'; /// 携带 HiTi 状态码的打印异常(错误码进任务 status,文案进 statusText)。 class HitiException implements Exception { HitiException(this.statusCode, this.message); final int statusCode; final String message; @override String toString() => message; } /// HiTi CS-2xx 打印机高层操作:枚举、状态、设备信息、单卡直打。 /// /// PavoApi 直打链路(对齐 SDK DemoCSharp buttonDirectPrint 与 C# Hiti/PavoPrinter.cs): /// PAVO_PrintOneCard(打印机名, 分层 BITMAP, 默认加热能量, 默认设置) → 轮询 CheckPrinterStatus 直到非忙碌。 /// 全部为同步 ffi 调用,必须在独立 isolate 中运行(见 PavoPrintBackend)。 class PavoPrinter { /// 枚举 USB 连接的证卡打印机名。SDK 缓冲可能是 UTF-8 或 UTF-16,两种都尝试解析。 static List enumUsbPrinters(PavoApi api) { final sizePtr = calloc(); try { var ret = api.enumUsbCardPrinters(nullptr, sizePtr); if (ret != 0 || sizePtr.value == 0) { return const []; } final buf = calloc(sizePtr.value); try { final size = sizePtr.value; sizePtr.value = size; ret = api.enumUsbCardPrinters(buf, sizePtr); if (ret != 0) { return const []; } return decodeAuto(buf.asTypedList(size)) .split(RegExp('[\x00\n\r,]')) .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); } finally { calloc.free(buf); } } finally { calloc.free(sizePtr); } } /// 按可打印字符占比自动判断 UTF-8 / UTF-16 编码。 static String decodeAuto(Uint8List buf) { final utf8Text = utf8.decode(buf, allowMalformed: true); if (printableRatio(utf8Text) >= 0.9) { return utf8Text; } final codes = []; for (var i = 0; i + 1 < buf.length; i += 2) { codes.add(buf[i] | (buf[i + 1] << 8)); } final utf16Text = String.fromCharCodes(codes); return printableRatio(utf16Text) >= 0.9 ? utf16Text : utf8Text; } static double printableRatio(String s) { if (s.isEmpty) return 0; var ok = 0; for (final c in s.codeUnits) { if ((c >= 0x20 && c <= 0x7e) || (c >= 0x4e00 && c <= 0x9fff) || c == 0) ok++; } return ok / s.length; } /// 查询设备状态码,0 = 就绪;API 调用失败时返回的就是错误码本身。 static int getStatus(PavoApi api, Pointer printer) { final statusPtr = calloc(); try { final ret = api.checkPrinterStatus(printer, statusPtr); return ret != 0 ? ret : statusPtr.value; } finally { calloc.free(statusPtr); } } /// 读字符串型设备信息(序列号/型号/固件,UTF-16LE)。 static String getDeviceInfoText(PavoApi api, Pointer printer, int infoType) { final buf = calloc(512); final lenPtr = calloc()..value = 512; try { final ret = api.getDeviceInfo(printer, infoType, buf, lenPtr); if (ret != 0 || lenPtr.value == 0) { return ''; } final n = lenPtr.value; final codes = []; for (var i = 0; i + 1 < n; i += 2) { final c = buf[i] | (buf[i + 1] << 8); if (c != 0) codes.add(c); } return String.fromCharCodes(codes); } finally { calloc.free(buf); calloc.free(lenPtr); } } /// 色带信息:(类型码, 剩余张数)。 static (int, int) getRibbonInfo(PavoApi api, Pointer printer) { final buf = calloc(64); final lenPtr = calloc()..value = 64; try { final ret = api.getDeviceInfo(printer, PavoApi.devinfoRibbonInfo, buf, lenPtr); if (ret != 0 || lenPtr.value < 8) { return (PavoApi.ribbonMissing, 0); } final v = buf.asTypedList(8); final type = v[0] | (v[1] << 8) | (v[2] << 16) | (v[3] << 24); final count = v[4] | (v[5] << 8) | (v[6] << 16) | (v[7] << 24); return (type, count); } finally { calloc.free(buf); calloc.free(lenPtr); } } static String ribbonTypeName(int type) => switch (type) { PavoApi.ribbonTypeYmcko => 'YMCKO', PavoApi.ribbonTypeK => 'Resin K', PavoApi.ribbonTypeKo => 'KO', PavoApi.ribbonTypeYmckok => 'YMCKOK', PavoApi.ribbonTypeHalfYmcko => '1/2 YMCKO', PavoApi.ribbonTypeYmckfo => 'YMCKFO', PavoApi.ribbonMissing => '未安装', _ => '未知($type)', }; /// 常见状态码中文文案,与前端翻译表一致。 static String statusTextOf(int status) => switch (status) { 0 => '就绪', PavoApi.dsPrinting => '打印机正在打印', PavoApi.dsProcessingData => '正在处理打印数据', PavoApi.dsSendingData => '正在向打印机发送数据', PavoApi.dsWriteFail => '向打印机发送命令失败', PavoApi.dsReadFail => '打印机响应失败', PavoApi.dsOffline => '打印机已断开连接或已关机', PavoApi.dsCardJam1 => '卡片通道卡纸,请开盖取出卡片', PavoApi.dsCardJam2 => '翻卡器处卡纸,请开盖取出卡片', PavoApi.dsCardJam3 => '出卡槽卡纸,请取出卡片', PavoApi.dsCmdSeqError => '命令序列错误,请重启打印机', PavoApi.dsCoverOpen => '打印机盖未关闭', PavoApi.dsCardOut => '缺卡或进卡失败,请补充卡片', PavoApi.dsCardLow => '卡量不足,请及时补充卡片', PavoApi.dsCardThickWrong => '卡片厚度选择器未正确安装', PavoApi.dsBusy => '设备忙碌,请稍候', PavoApi.dsRibbonMissing => '色带未安装', PavoApi.dsOutOfRibbon => '色带已用尽,请更换色带', PavoApi.dsUnsupportRibbon => '不支持的色带类型', PavoApi.dsUnknownRibbon => '无法识别色带', PavoApi.dsNoFlipperModule => '翻卡模块未安装', PavoApi.locked => '打印机已被锁定,请先解锁', _ => '设备状态异常 0x${status.toRadixString(16).padLeft(8, '0').toUpperCase()}', }; /// 打印一张卡。frontBgr 为彩色层(BGR),frontK 可选(树脂黑文字层)。 /// isLandscape 由正面图纵横比得出:横向 2 / 纵向 1(SDK byOrientation)。 /// copies 大于 1 时循环提交(直打接口无份数参数)。 static Future printOneCard( PavoApi api, Pointer printer, { required Uint8List frontBgr, Uint8List? frontK, Uint8List? backBgr, Uint8List? backK, required bool isLandscape, required int copies, }) async { final width = isLandscape ? CardImage.landscapeWidth : CardImage.landscapeHeight; final height = isLandscape ? CardImage.landscapeHeight : CardImage.landscapeWidth; final n = copies < 1 ? 1 : copies; for (var i = 0; i < n; i++) { final holder = LayerHolder(); final para = calloc(); final heat = calloc(); final settings = calloc(); try { para.ref ..dwSize = sizeOf() ..byOrientation = isLandscape ? 2 : 1 ..byCardThickness = 1 // 0.5mm 标准 PVC 卡 ..lpFrontBGR = holder.pin(frontBgr, 24, width, height) ..lpFrontK = frontK == null ? nullptr : holder.pin(frontK, 8, width, height) ..lpBackBGR = backBgr == null ? nullptr : holder.pin(backBgr, 24, width, height) ..lpBackK = backK == null ? nullptr : holder.pin(backK, 8, width, height); // heat/settings 由 calloc 零初始化,等价 C# 全 0(驱动默认能量/设置) final ret = api.printOneCard(printer, para, heat, settings); if (ret != 0) { throw HitiException(ret, 'PAVO_PrintOneCard 返回错误 0x${_hex8(ret)}'); } // 对齐 C# Demo(using 块内轮询):等固件处理数据后轮询,直到打印机脱离忙碌/打印态; // 像素内存在轮询完成后才释放(finally) await Future.delayed(const Duration(seconds: 3)); final status = await waitIdle(api, printer); if (status != 0) { throw HitiException(status, '打印中断,设备状态 0x${_hex8(status)}'); } } finally { holder.dispose(); calloc.free(para); calloc.free(heat); calloc.free(settings); } } } /// 轮询直到非 BUSY/PRINTING/PROCESSING/SENDING;超时 3 分钟抛错。 static Future waitIdle(PavoApi api, Pointer printer) async { final deadline = DateTime.now().add(const Duration(minutes: 3)); while (DateTime.now().isBefore(deadline)) { final status = getStatus(api, printer); if (status != PavoApi.dsBusy && status != PavoApi.dsPrinting && status != PavoApi.dsProcessingData && status != PavoApi.dsSendingData) { return status; } await Future.delayed(const Duration(seconds: 1)); } throw TimeoutException('等待打印机完成超时(3 分钟)'); } static String _hex8(int v) => v.toRadixString(16).padLeft(8, '0').toUpperCase(); } /// 打印期间持有非托管像素内存,dispose 统一释放(对齐 C# LayerHolder)。 class LayerHolder { final _allocations = []; /// 把像素缓冲包成 GDI BITMAP 结构并拷贝像素到非托管内存,返回 BITMAP 指针。 Pointer pin(Uint8List pixels, int bitsPerPixel, int width, int height) { final widthBytes = width * (bitsPerPixel ~/ 8); final bits = malloc(pixels.length); bits.asTypedList(pixels.length).setAll(0, pixels); final bmp = malloc(); bmp.ref ..bmType = 0 ..bmWidth = width ..bmHeight = height ..bmWidthBytes = widthBytes ..bmPlanes = 1 ..bmBitsPixel = bitsPerPixel ..bmBits = bits; _allocations ..add(bits) ..add(bmp); return bmp.cast(); } void dispose() { for (final p in _allocations) { malloc.free(p); } _allocations.clear(); } }