|
|
@@ -4,10 +4,12 @@ import com.mes.print.config.LabelConfigLoader;
|
|
|
import com.mes.print.config.PrinterConfig;
|
|
|
import com.mes.util.AppLog;
|
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
+import java.util.Locale;
|
|
|
import java.util.Map;
|
|
|
|
|
|
/**
|
|
|
@@ -23,6 +25,10 @@ import java.util.Map;
|
|
|
* serialNo = 码里的 4 位日流水补零到 7 位:+KB60IS3031T929000401 → 0000004
|
|
|
* date = 打印日期(补打时由调用方传首次打印日期)
|
|
|
*
|
|
|
+ * 二维码实际尺寸 = cellWidth(点) × 模块数,模块数由内容和纠错等级决定,
|
|
|
+ * 跟配置里的 width/height(只有编辑器在用)无关,所以打印前会按元素框夹一次 cellWidth,
|
|
|
+ * 免得二维码撑出预留区域、吃掉静区或压到旁边文字(扫不出来)。
|
|
|
+ *
|
|
|
* label-config.json 里非 static 元素的 text 只是编辑器预览用的示例值,
|
|
|
* 打印时取不到业务数据直接报错,绝不回退到示例值。
|
|
|
*/
|
|
|
@@ -31,6 +37,37 @@ public class TscPrinter {
|
|
|
/** TTP-244 Pro 为 203 dpi,1mm ≈ 8 点 */
|
|
|
private static final double DOTS_PER_MM = 8.0;
|
|
|
|
|
|
+ /** TSPL QRCODE 单元宽度允许范围(点) */
|
|
|
+ private static final int MIN_CELL_WIDTH = 1;
|
|
|
+ private static final int MAX_CELL_WIDTH = 10;
|
|
|
+ /**
|
|
|
+ * 想扫得稳,单元宽度至少要这么多点(0.625mm)。
|
|
|
+ * 实测(bin 里的 ZXing 渲染解码自检):4 点(0.5mm)时黑模块只要涨 1 个点就糊在一起读不出来,
|
|
|
+ * 5 点以上才有余量,所以配置低于这个值时告警提醒放大元素框。
|
|
|
+ */
|
|
|
+ private static final int ROBUST_CELL_WIDTH = 5;
|
|
|
+ /** 配置里没写单元宽度时的兜底值:0.5mm,模块不会糊在一起 */
|
|
|
+ private static final int DEFAULT_CELL_WIDTH = 6;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 字节模式容量(字符数):[纠错等级 0=L/1=M/2=Q/3=H][版本-1],版本 1~10。
|
|
|
+ * 只用于估算二维码模块数;按字节模式算比字母数字模式保守(模块数只会多算不会少算),
|
|
|
+ * 保证夹出来的单元宽度一定装得下。
|
|
|
+ */
|
|
|
+ private static final int[][] QR_BYTE_CAPACITY = {
|
|
|
+ // L M Q H
|
|
|
+ { 17, 14, 11, 7}, // v1 (21×21)
|
|
|
+ { 32, 26, 20, 14}, // v2 (25×25)
|
|
|
+ { 53, 42, 32, 24}, // v3 (29×29)
|
|
|
+ { 78, 62, 46, 34}, // v4 (33×33)
|
|
|
+ { 106, 84, 60, 44}, // v5 (37×37)
|
|
|
+ { 134, 106, 74, 58}, // v6 (41×41)
|
|
|
+ { 154, 122, 86, 64}, // v7 (45×45)
|
|
|
+ { 192, 152, 108, 84}, // v8 (49×49)
|
|
|
+ { 230, 180, 130, 98}, // v9 (53×53)
|
|
|
+ { 271, 213, 151, 119}, // v10 (57×57)
|
|
|
+ };
|
|
|
+
|
|
|
private final TSCLIB tsc = TSCLIB.INSTANCE;
|
|
|
private final LabelConfigLoader configLoader;
|
|
|
|
|
|
@@ -76,9 +113,10 @@ public class TscPrinter {
|
|
|
try {
|
|
|
String setup = setupLabel();
|
|
|
tsc.clearbuffer();
|
|
|
- drawLabelFromConfig(dataMap, offX, offY);
|
|
|
+ String qrInfo = drawLabelFromConfig(dataMap, offX, offY);
|
|
|
tsc.printlabel("1", "1"); // 打印1份,每份1张
|
|
|
String info = "[TSC] 打印 " + sn + " NO:" + dataMap.get("serialNo") + " " + setup
|
|
|
+ + (qrInfo.isEmpty() ? "" : " QR=" + qrInfo)
|
|
|
+ " 偏移=(" + offX + "," + offY + ")点 打印机=" + printerName;
|
|
|
System.out.println(info);
|
|
|
AppLog.info(info);
|
|
|
@@ -134,7 +172,7 @@ public class TscPrinter {
|
|
|
width,
|
|
|
height,
|
|
|
"4", // 速度
|
|
|
- "10", // 浓度
|
|
|
+ "10", // 浓度:现场验证过 10 的对比度最好;二维码模块别小于 0.625mm,靠模块尺寸抗点增益
|
|
|
"0", // 传感器类型:0=间隙
|
|
|
gap,
|
|
|
"0" // 间隙偏移
|
|
|
@@ -146,9 +184,13 @@ public class TscPrinter {
|
|
|
return (int) Math.round(mm * DOTS_PER_MM);
|
|
|
}
|
|
|
|
|
|
- /** 按配置逐个绘制元素,offX/offY 为整体偏移(点) */
|
|
|
- private void drawLabelFromConfig(Map<String, String> dataMap, int offX, int offY) {
|
|
|
+ /**
|
|
|
+ * 按配置逐个绘制元素,offX/offY 为整体偏移(点)
|
|
|
+ * @return 二维码实际尺寸描述(没画二维码返回空串)
|
|
|
+ */
|
|
|
+ private String drawLabelFromConfig(Map<String, String> dataMap, int offX, int offY) {
|
|
|
List<LabelConfigLoader.LabelElement> elements = configLoader.getElements();
|
|
|
+ String qrDesc = "";
|
|
|
for (LabelConfigLoader.LabelElement element : elements) {
|
|
|
int x = Math.max(0, element.x + offX);
|
|
|
int y = Math.max(0, element.y + offY);
|
|
|
@@ -164,21 +206,140 @@ public class TscPrinter {
|
|
|
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);
|
|
|
+ qrDesc = drawQr(element, x, y, dataMap);
|
|
|
}
|
|
|
}
|
|
|
+ return qrDesc;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 画二维码。只使用 ZXing 自绘,不再把内容交给打印机固件解析。
|
|
|
+ * @return 日志描述
|
|
|
+ */
|
|
|
+ private String drawQr(LabelConfigLoader.LabelElement element, int x, int y, Map<String, String> dataMap) {
|
|
|
+ String data = resolveData(element, dataMap);
|
|
|
+ String eccLevel = eccLevelOf(element.eccLevel);
|
|
|
+ String mode = PrinterConfig.get("label.qr.mode", "rect").trim().toLowerCase(Locale.ROOT);
|
|
|
+
|
|
|
+ if ("firmware".equals(mode)) {
|
|
|
+ throw new IllegalArgumentException("不允许使用二维码固件模式,请将 label.qr.mode 设置为 rect");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先用 ZXing 算出真实模块数,再按元素框夹紧单元宽度。
|
|
|
+ int modules = QrRenderer.build(data, eccLevel, 1).modules;
|
|
|
+ int cellWidth = cellWidthFor(element, modules);
|
|
|
+ QrRenderer qr = QrRenderer.build(data, eccLevel, cellWidth);
|
|
|
+ if ("bitmap".equals(mode)) {
|
|
|
+ throw new IllegalArgumentException("bitmap 模式需要确认 TSCLIB 二进制接口可用,请使用 rect 模式");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<QrRenderer.Block> blocks = qr.blocks();
|
|
|
+ for (QrRenderer.Block b : blocks) {
|
|
|
+ // 分块坐标含静区偏移,让二维码符号左上角落在 (x,y)。
|
|
|
+ tsc.sendcommand(String.format(Locale.ROOT, "BAR %d,%d,%d,%d",
|
|
|
+ x + (b.x - QrRenderer.QUIET_MODULES) * cellWidth,
|
|
|
+ y + (b.y - QrRenderer.QUIET_MODULES) * cellWidth,
|
|
|
+ b.w * cellWidth, b.h * cellWidth));
|
|
|
+ }
|
|
|
+ String how = "自绘" + blocks.size() + "块";
|
|
|
+ log("[TSC] 二维码内容=[" + data + "] 字节=" + data.getBytes(StandardCharsets.UTF_8).length
|
|
|
+ + " " + modules + "模块×" + cellWidth + "点 纠错=" + eccLevel
|
|
|
+ + " 静区=" + QrRenderer.QUIET_MODULES + "模块 " + how + " 位置=(" + x + "," + y + ")");
|
|
|
+ return modules + "模块×" + cellWidth + "点 " + how;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 点阵位序:print.properties 的 label.qr.bitorder,msb(默认) / lsb */
|
|
|
+ private static boolean isMsbFirst() {
|
|
|
+ return !"lsb".equalsIgnoreCase(PrinterConfig.get("label.qr.bitorder", "msb").trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void log(String message) {
|
|
|
+ System.out.println(message);
|
|
|
+ AppLog.info(message);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 二维码单元宽度(点):取配置值,但不允许超过元素框能装下的最大值。
|
|
|
+ * 配置里的 width/height 只是编辑器预览用的框,打印机只认 cellWidth × 模块数,
|
|
|
+ * 所以这里必须再夹一次:超框就压小并告警,防止二维码出框、静区不足导致偶尔扫不出来。
|
|
|
+ */
|
|
|
+ private static int cellWidthFor(LabelConfigLoader.LabelElement element, int modules) {
|
|
|
+ int configured = parseCellWidth(element.cellWidth);
|
|
|
+ int box = Math.min(element.width, element.height);
|
|
|
+ int effective = configured;
|
|
|
+ if (box > 0 && modules > 0) {
|
|
|
+ effective = Math.min(configured, Math.max(MIN_CELL_WIDTH, box / modules));
|
|
|
+ if (effective < configured) {
|
|
|
+ String warn = "[TSC] 二维码 " + modules + " 模块 × " + configured + " 点 = "
|
|
|
+ + (configured * modules) + " 点,超出元素框 " + box + " 点,已按 " + effective + " 点打印";
|
|
|
+ System.out.println(warn);
|
|
|
+ AppLog.info(warn);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (effective < ROBUST_CELL_WIDTH) {
|
|
|
+ String warn = "[TSC] 二维码单元宽度只有 " + effective + " 点("
|
|
|
+ + String.format(Locale.ROOT, "%.2f", effective / DOTS_PER_MM)
|
|
|
+ + "mm/模块),再小容易被热敏点增益糊掉、扫不出来;建议把元素框放大到至少 "
|
|
|
+ + (modules * ROBUST_CELL_WIDTH) + " 点";
|
|
|
+ System.out.println(warn);
|
|
|
+ AppLog.info(warn);
|
|
|
+ }
|
|
|
+ return Math.max(MIN_CELL_WIDTH, Math.min(MAX_CELL_WIDTH, effective));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 估算二维码边长(模块数):21 + 4×(版本-1)。
|
|
|
+ * 内容按字节模式(UTF-8 字节数)估,只多不少;纠错等级越高、内容越长,版本越大。
|
|
|
+ */
|
|
|
+ private static int qrModuleCount(String data, String eccLevel) {
|
|
|
+ int len = data == null ? 0 : data.getBytes(StandardCharsets.UTF_8).length;
|
|
|
+ int ecc = eccIndex(eccLevel);
|
|
|
+ int version = 40;
|
|
|
+ for (int v = 1; v <= QR_BYTE_CAPACITY.length; v++) {
|
|
|
+ if (len <= QR_BYTE_CAPACITY[v - 1][ecc]) {
|
|
|
+ version = v;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (version == 40) {
|
|
|
+ // 超出表范围(正常码长不会走到这里):保守外推,宁可算大不算小
|
|
|
+ int capacity = QR_BYTE_CAPACITY[QR_BYTE_CAPACITY.length - 1][ecc];
|
|
|
+ for (int v = QR_BYTE_CAPACITY.length + 1; v <= 40; v++) {
|
|
|
+ capacity += 34;
|
|
|
+ if (len <= capacity) {
|
|
|
+ version = v;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 21 + 4 * (version - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 配置里单元宽度可能是字符串 "8"/"8.0" 或数字,统一转成整数点(非法值回退默认,别把 "8.0" 直接发给打印机) */
|
|
|
+ private static int parseCellWidth(String raw) {
|
|
|
+ if (raw == null || raw.trim().isEmpty()) return DEFAULT_CELL_WIDTH;
|
|
|
+ try {
|
|
|
+ return (int) Math.round(Double.parseDouble(raw.trim()));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ System.err.println("[TSC] 二维码单元宽度非法: " + raw + ",按 " + DEFAULT_CELL_WIDTH + " 点打印");
|
|
|
+ return DEFAULT_CELL_WIDTH;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 纠错等级规范化:非法/缺失回退 M(L 只有 7%,打印头一脏就容易扫不出来) */
|
|
|
+ private static String eccLevelOf(String raw) {
|
|
|
+ if (raw == null) return "M";
|
|
|
+ String v = raw.trim().toUpperCase(Locale.ROOT);
|
|
|
+ return ("L".equals(v) || "M".equals(v) || "Q".equals(v) || "H".equals(v)) ? v : "M";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int eccIndex(String raw) {
|
|
|
+ switch (eccLevelOf(raw)) {
|
|
|
+ case "L": return 0;
|
|
|
+ case "Q": return 2;
|
|
|
+ case "H": return 3;
|
|
|
+ default: return 1;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private Map<String, String> prepareDataMap(String sn, Date printDate) {
|