package com.mes.print.tsc; import com.mes.print.config.LabelConfigLoader; import com.mes.print.config.PrinterConfig; import com.mes.util.AppLog; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * TSC 标签机 - 通过 TSCLIB.dll 直接发指令打印 * * 不依赖 BarTender,不读 .btw 文件。布局来自 config/label-config.json * (按 T09.btw / T68.btw 复刻,可用 label-editor.html 调整): * 程序目录下有就用外部的,没有就用 jar 内打包的那份。 * * 标签上的可变内容全部来自扫到的客户码: * sn = 码本身(二维码 + 底部批次号) * productModel = 前缀固定映射:+KB60 → 2510094CDE2200(T09),+KB87 → 2510094ANB0900(T68) * serialNo = 码里的 4 位日流水补零到 7 位:+KB60IS3031T929000401 → 0000004 * date = 打印日期(补打时由调用方传首次打印日期) * * label-config.json 里非 static 元素的 text 只是编辑器预览用的示例值, * 打印时取不到业务数据直接报错,绝不回退到示例值。 */ public class TscPrinter { /** TTP-244 Pro 为 203 dpi,1mm ≈ 8 点 */ private static final double DOTS_PER_MM = 8.0; private final TSCLIB tsc = TSCLIB.INSTANCE; private final LabelConfigLoader configLoader; /** 默认布局:外部 config/label-config.json 优先,否则 jar 内置 */ public TscPrinter() { try { this.configLoader = LabelConfigLoader.load(); } catch (Exception e) { throw new RuntimeException("加载标签配置失败: " + e.getMessage(), e); } } /** 指定布局文件(测试用) */ public TscPrinter(String configPath) { try { this.configLoader = new LabelConfigLoader(configPath); } catch (Exception e) { throw new RuntimeException("加载标签配置失败(" + configPath + "): " + e.getMessage(), e); } } /** 打印标签,日期取当天 */ public void printLabel(String sn) { printLabel(sn, new Date()); } /** * 打印标签 * @param sn 扫到的客户码 * @param printDate 标签上的日期 */ public void printLabel(String sn, Date printDate) { if (sn == null || sn.trim().isEmpty()) { throw new RuntimeException("客户码为空"); } // 先组装数据,数据有问题直接报错,不去占打印机端口 Map dataMap = prepareDataMap(sn.trim(), printDate == null ? new Date() : printDate); // 整体偏移(print.properties 的 label.offset.x/y,毫米):换纸/校准后位置有偏差时现场微调 int offX = mmToDots(PrinterConfig.getDouble("label.offset.x", 0)); int offY = mmToDots(PrinterConfig.getDouble("label.offset.y", 0)); String printerName = openPort(); try { String setup = setupLabel(); tsc.clearbuffer(); drawLabelFromConfig(dataMap, offX, offY); tsc.printlabel("1", "1"); // 打印1份,每份1张 String info = "[TSC] 打印 " + sn + " NO:" + dataMap.get("serialNo") + " " + setup + " 偏移=(" + offX + "," + offY + ")点 打印机=" + printerName; System.out.println(info); AppLog.info(info); } catch (Exception e) { throw new RuntimeException("打印失败: " + e.getMessage(), e); } finally { // 异常时也要释放端口,否则下一张 openport 会失败 tsc.closeport(); } } /** * 换纸校准:按当前标签尺寸设置后发 GAPDETECT,打印机走纸自动测量标签长度和间隙。 * 会走出几张空白标签,校准结果保存在打印机里。 */ public void calibrate() { String printerName = openPort(); try { String setup = setupLabel(); tsc.sendcommand("GAPDETECT"); String info = "[TSC] 换纸校准 GAPDETECT " + setup + " 打印机=" + printerName; System.out.println(info); AppLog.info(info); } catch (Exception e) { throw new RuntimeException("校准失败: " + e.getMessage(), e); } finally { tsc.closeport(); } } /** 打开标签机端口,返回打印机名 */ private String openPort() { String printerName = PrinterConfig.getLabelPrinterName(); if (tsc.openport(printerName) != 1) { throw new RuntimeException("打开标签机失败: " + printerName + "(检查 TSC 驱动是否安装、打印机名是否一致)"); } return printerName; } /** * 下发标签尺寸/速度/浓度/间隙(来自 label-config.json 的 labelSize),返回参数描述(写日志用) * * 高度必须是标签本身高度(height=38.1),不能含间隙: * 以前传 totalHeight(41.1=标签+间隙),打印长度正好等于一个标签节距, * 校准准确后每打一张都会越过下一张的起点,打印机就跳过一张,出现打一张空一张。 */ private String setupLabel() { LabelConfigLoader.LabelSize labelSize = configLoader.getLabelSize(); String width = String.valueOf(labelSize.width); String height = String.valueOf(labelSize.height); String gap = String.valueOf(labelSize.gap); tsc.setup( width, height, "4", // 速度 "10", // 浓度 "0", // 传感器类型:0=间隙 gap, "0" // 间隙偏移 ); return "SIZE=" + width + "x" + height + "mm GAP=" + gap + "mm"; } private static int mmToDots(double mm) { return (int) Math.round(mm * DOTS_PER_MM); } /** 按配置逐个绘制元素,offX/offY 为整体偏移(点) */ private void drawLabelFromConfig(Map dataMap, int offX, int offY) { List elements = configLoader.getElements(); for (LabelConfigLoader.LabelElement element : elements) { int x = Math.max(0, element.x + offX); int y = Math.max(0, element.y + offY); if ("text".equals(element.type)) { tsc.windowsfont( x, y, element.fontSize, 0, // rotation element.fontStyle, 0, // underline element.fontFamily, resolveData(element, dataMap) ); } else if ("qrcode".equals(element.type)) { // qrcode() 函数不工作,用原始 TSPL:QRCODE x,y,eccLevel,cellWidth,mode,rotation,"data" String qrCommand = String.format( "QRCODE %d,%d,%s,%s,%s,%s,\"%s\"", x, y, element.eccLevel, element.cellWidth, "A", // mode "0", // rotation resolveData(element, dataMap) ); System.out.println("[TSC] " + element.id + " -> " + qrCommand); tsc.sendcommand(qrCommand); } } } private Map prepareDataMap(String sn, Date printDate) { String productModel = productModelOf(sn); if (productModel == null) throw new RuntimeException("无法识别产品型号: " + sn); String serialNo = serialNoOf(sn); if (serialNo == null) throw new RuntimeException("无法从客户码解析流水号: " + sn); String date = new SimpleDateFormat("yyyy/MM/dd").format(printDate); Map dataMap = new HashMap<>(); dataMap.put("sn", sn); dataMap.put("productModel", productModel); dataMap.put("date", date); dataMap.put("serialNo", serialNo); dataMap.put("dateSerial", date + " NO:" + serialNo); return dataMap; } /** static 用配置里的固定文本;其他必须能从业务数据取到,否则报错 */ private String resolveData(LabelConfigLoader.LabelElement element, Map dataMap) { if ("static".equals(element.dataSource)) { return element.text == null ? "" : element.text; } String v = dataMap.get(element.dataSource); if (v == null || v.isEmpty()) { throw new RuntimeException("标签元素 [" + element.id + "] 数据源 [" + element.dataSource + "] 无数据,已中止打印"); } return v; } /** 前缀 → 产品型号(固定值),识别不了返回 null */ public static String productModelOf(String sn) { if (sn == null) return null; if (sn.startsWith("+KB60")) return "2510094CDE2200"; if (sn.startsWith("+KB87")) return "2510094ANB0900"; return null; } /** 前缀 → 产品(T09 / T68),识别不了返回 null */ public static String productOf(String sn) { if (sn == null) return null; if (sn.startsWith("+KB60")) return "T09"; if (sn.startsWith("+KB87")) return "T68"; return null; } /** * 客户码 → 标签 NO(7 位) * 码结构:...T + 月(1) + 日(2) + 日流水(4) + 01,取倒数第 6~3 位的日流水 * 解析不了返回 null */ public static String serialNoOf(String sn) { if (sn == null || sn.length() < 6) return null; String seq = sn.substring(sn.length() - 6, sn.length() - 2); return seq.matches("\\d{4}") ? "000" + seq : null; } }