TscPrinter.java 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package com.mes.print.tsc;
  2. import com.mes.print.config.LabelConfigLoader;
  3. import com.mes.print.config.PrinterConfig;
  4. import com.mes.util.AppLog;
  5. import java.text.SimpleDateFormat;
  6. import java.util.Date;
  7. import java.util.HashMap;
  8. import java.util.List;
  9. import java.util.Map;
  10. /**
  11. * TSC 标签机 - 通过 TSCLIB.dll 直接发指令打印
  12. *
  13. * 不依赖 BarTender,不读 .btw 文件。布局来自 config/label-config.json
  14. * (按 T09.btw / T68.btw 复刻,可用 label-editor.html 调整):
  15. * 程序目录下有就用外部的,没有就用 jar 内打包的那份。
  16. *
  17. * 标签上的可变内容全部来自扫到的客户码:
  18. * sn = 码本身(二维码 + 底部批次号)
  19. * productModel = 前缀固定映射:+KB60 → 2510094CDE2200(T09),+KB87 → 2510094ANB0900(T68)
  20. * serialNo = 码里的 4 位日流水补零到 7 位:+KB60IS3031T929000401 → 0000004
  21. * date = 打印日期(补打时由调用方传首次打印日期)
  22. *
  23. * label-config.json 里非 static 元素的 text 只是编辑器预览用的示例值,
  24. * 打印时取不到业务数据直接报错,绝不回退到示例值。
  25. */
  26. public class TscPrinter {
  27. /** TTP-244 Pro 为 203 dpi,1mm ≈ 8 点 */
  28. private static final double DOTS_PER_MM = 8.0;
  29. private final TSCLIB tsc = TSCLIB.INSTANCE;
  30. private final LabelConfigLoader configLoader;
  31. /** 默认布局:外部 config/label-config.json 优先,否则 jar 内置 */
  32. public TscPrinter() {
  33. try {
  34. this.configLoader = LabelConfigLoader.load();
  35. } catch (Exception e) {
  36. throw new RuntimeException("加载标签配置失败: " + e.getMessage(), e);
  37. }
  38. }
  39. /** 指定布局文件(测试用) */
  40. public TscPrinter(String configPath) {
  41. try {
  42. this.configLoader = new LabelConfigLoader(configPath);
  43. } catch (Exception e) {
  44. throw new RuntimeException("加载标签配置失败(" + configPath + "): " + e.getMessage(), e);
  45. }
  46. }
  47. /** 打印标签,日期取当天 */
  48. public void printLabel(String sn) {
  49. printLabel(sn, new Date());
  50. }
  51. /**
  52. * 打印标签
  53. * @param sn 扫到的客户码
  54. * @param printDate 标签上的日期
  55. */
  56. public void printLabel(String sn, Date printDate) {
  57. if (sn == null || sn.trim().isEmpty()) {
  58. throw new RuntimeException("客户码为空");
  59. }
  60. // 先组装数据,数据有问题直接报错,不去占打印机端口
  61. Map<String, String> dataMap = prepareDataMap(sn.trim(), printDate == null ? new Date() : printDate);
  62. // 整体偏移(print.properties 的 label.offset.x/y,毫米):换纸/校准后位置有偏差时现场微调
  63. int offX = mmToDots(PrinterConfig.getDouble("label.offset.x", 0));
  64. int offY = mmToDots(PrinterConfig.getDouble("label.offset.y", 0));
  65. String printerName = openPort();
  66. try {
  67. String setup = setupLabel();
  68. tsc.clearbuffer();
  69. drawLabelFromConfig(dataMap, offX, offY);
  70. tsc.printlabel("1", "1"); // 打印1份,每份1张
  71. String info = "[TSC] 打印 " + sn + " NO:" + dataMap.get("serialNo") + " " + setup
  72. + " 偏移=(" + offX + "," + offY + ")点 打印机=" + printerName;
  73. System.out.println(info);
  74. AppLog.info(info);
  75. } catch (Exception e) {
  76. throw new RuntimeException("打印失败: " + e.getMessage(), e);
  77. } finally {
  78. // 异常时也要释放端口,否则下一张 openport 会失败
  79. tsc.closeport();
  80. }
  81. }
  82. /**
  83. * 换纸校准:按当前标签尺寸设置后发 GAPDETECT,打印机走纸自动测量标签长度和间隙。
  84. * 会走出几张空白标签,校准结果保存在打印机里。
  85. */
  86. public void calibrate() {
  87. String printerName = openPort();
  88. try {
  89. String setup = setupLabel();
  90. tsc.sendcommand("GAPDETECT");
  91. String info = "[TSC] 换纸校准 GAPDETECT " + setup + " 打印机=" + printerName;
  92. System.out.println(info);
  93. AppLog.info(info);
  94. } catch (Exception e) {
  95. throw new RuntimeException("校准失败: " + e.getMessage(), e);
  96. } finally {
  97. tsc.closeport();
  98. }
  99. }
  100. /** 打开标签机端口,返回打印机名 */
  101. private String openPort() {
  102. String printerName = PrinterConfig.getLabelPrinterName();
  103. if (tsc.openport(printerName) != 1) {
  104. throw new RuntimeException("打开标签机失败: " + printerName + "(检查 TSC 驱动是否安装、打印机名是否一致)");
  105. }
  106. return printerName;
  107. }
  108. /**
  109. * 下发标签尺寸/速度/浓度/间隙(来自 label-config.json 的 labelSize),返回参数描述(写日志用)
  110. *
  111. * 高度必须是标签本身高度(height=38.1),不能含间隙:
  112. * 以前传 totalHeight(41.1=标签+间隙),打印长度正好等于一个标签节距,
  113. * 校准准确后每打一张都会越过下一张的起点,打印机就跳过一张,出现打一张空一张。
  114. */
  115. private String setupLabel() {
  116. LabelConfigLoader.LabelSize labelSize = configLoader.getLabelSize();
  117. String width = String.valueOf(labelSize.width);
  118. String height = String.valueOf(labelSize.height);
  119. String gap = String.valueOf(labelSize.gap);
  120. tsc.setup(
  121. width,
  122. height,
  123. "4", // 速度
  124. "10", // 浓度
  125. "0", // 传感器类型:0=间隙
  126. gap,
  127. "0" // 间隙偏移
  128. );
  129. return "SIZE=" + width + "x" + height + "mm GAP=" + gap + "mm";
  130. }
  131. private static int mmToDots(double mm) {
  132. return (int) Math.round(mm * DOTS_PER_MM);
  133. }
  134. /** 按配置逐个绘制元素,offX/offY 为整体偏移(点) */
  135. private void drawLabelFromConfig(Map<String, String> dataMap, int offX, int offY) {
  136. List<LabelConfigLoader.LabelElement> elements = configLoader.getElements();
  137. for (LabelConfigLoader.LabelElement element : elements) {
  138. int x = Math.max(0, element.x + offX);
  139. int y = Math.max(0, element.y + offY);
  140. if ("text".equals(element.type)) {
  141. tsc.windowsfont(
  142. x,
  143. y,
  144. element.fontSize,
  145. 0, // rotation
  146. element.fontStyle,
  147. 0, // underline
  148. element.fontFamily,
  149. resolveData(element, dataMap)
  150. );
  151. } else if ("qrcode".equals(element.type)) {
  152. // qrcode() 函数不工作,用原始 TSPL:QRCODE x,y,eccLevel,cellWidth,mode,rotation,"data"
  153. String qrCommand = String.format(
  154. "QRCODE %d,%d,%s,%s,%s,%s,\"%s\"",
  155. x,
  156. y,
  157. element.eccLevel,
  158. element.cellWidth,
  159. "A", // mode
  160. "0", // rotation
  161. resolveData(element, dataMap)
  162. );
  163. System.out.println("[TSC] " + element.id + " -> " + qrCommand);
  164. tsc.sendcommand(qrCommand);
  165. }
  166. }
  167. }
  168. private Map<String, String> prepareDataMap(String sn, Date printDate) {
  169. String productModel = productModelOf(sn);
  170. if (productModel == null) throw new RuntimeException("无法识别产品型号: " + sn);
  171. String serialNo = serialNoOf(sn);
  172. if (serialNo == null) throw new RuntimeException("无法从客户码解析流水号: " + sn);
  173. String date = new SimpleDateFormat("yyyy/MM/dd").format(printDate);
  174. Map<String, String> dataMap = new HashMap<>();
  175. dataMap.put("sn", sn);
  176. dataMap.put("productModel", productModel);
  177. dataMap.put("date", date);
  178. dataMap.put("serialNo", serialNo);
  179. dataMap.put("dateSerial", date + " NO:" + serialNo);
  180. return dataMap;
  181. }
  182. /** static 用配置里的固定文本;其他必须能从业务数据取到,否则报错 */
  183. private String resolveData(LabelConfigLoader.LabelElement element, Map<String, String> dataMap) {
  184. if ("static".equals(element.dataSource)) {
  185. return element.text == null ? "" : element.text;
  186. }
  187. String v = dataMap.get(element.dataSource);
  188. if (v == null || v.isEmpty()) {
  189. throw new RuntimeException("标签元素 [" + element.id + "] 数据源 [" + element.dataSource + "] 无数据,已中止打印");
  190. }
  191. return v;
  192. }
  193. /** 前缀 → 产品型号(固定值),识别不了返回 null */
  194. public static String productModelOf(String sn) {
  195. if (sn == null) return null;
  196. if (sn.startsWith("+KB60")) return "2510094CDE2200";
  197. if (sn.startsWith("+KB87")) return "2510094ANB0900";
  198. return null;
  199. }
  200. /** 前缀 → 产品(T09 / T68),识别不了返回 null */
  201. public static String productOf(String sn) {
  202. if (sn == null) return null;
  203. if (sn.startsWith("+KB60")) return "T09";
  204. if (sn.startsWith("+KB87")) return "T68";
  205. return null;
  206. }
  207. /**
  208. * 客户码 → 标签 NO(7 位)
  209. * 码结构:...T + 月(1) + 日(2) + 日流水(4) + 01,取倒数第 6~3 位的日流水
  210. * 解析不了返回 null
  211. */
  212. public static String serialNoOf(String sn) {
  213. if (sn == null || sn.length() < 6) return null;
  214. String seq = sn.substring(sn.length() - 6, sn.length() - 2);
  215. return seq.matches("\\d{4}") ? "000" + seq : null;
  216. }
  217. }