| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- 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<String, String> 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<String, String> dataMap, int offX, int offY) {
- List<LabelConfigLoader.LabelElement> 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<String, String> 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<String, String> 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<String, String> 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;
- }
- }
|