简介:面向使用 Java 接入 Sewoo LK-B425 系列打印机的开发者,这份 SDK 提供可运行的打印 Demo 与 JNI 对接示例,帮助解决条形码、二维码、文本和图像打印以及设备状态监控等集成问题,适合具备一定 Java 基础、需要快速验证打印指令与接口调用的技术人员。压缩包共 14 个文件,约 248KB,包含 4 个 java 源码、4 个 class 编译文件、3 个 bmp 测试位图,以及 jar、dll 和 pdf 手册;其中 Java 源码便于阅读改造,JNI 动态库承接底层硬件交互,位图与 PDF 可用于对照打印效果和查阅接口说明。目前已有 196 人学习下载,可作为打印机对接的实用入门样例与排错参考。读者可从中了解 Java 调用本地打印接口的基本流程、常量配置与消息处理方式,并借助 TestPrint 等示例理解打印任务构造、按键交互和异常处理思路,进而迁移到自有项目中。
1. 从一张 100×80 的出库标签说起:LK-Bxx_JAVA_SDK.zip 里到底装了什么
某公司去年把出库单从 A4 纸改成 100mm×80mm 的铜版纸标签,采购了几台 LK-Bxx 系列的桌面标签机,随机光盘里翻出来的就是这份 LK-Bxx_JAVA_SDK.zip。仓储系统是 Java 写的,面单、库位码、批次条码要在业务侧直接拼出来,绕不开这份 Java SDK。它的定位很清楚:把 TSPL 这类纯文本指令封装成对象和方法,让业务代码不必自己拼字符串、算坐标。以 LK-B425 这一档机型为例,203dpi、支持间隙纸和黑标纸,是产线与仓库里最常见的一档。适合谁:做 WMS、MES、工单打印的 Java 后端,以及需要离线出单的桌面客户端开发者。
2. 拆包与链路选型:先弄明白指令是怎么送到打印机的
拿到压缩包不要急着 import。这类厂商 SDK 十有八九是「一个 jar 加一份指令手册」的薄封装,真正的能力在打印机固件里,SDK 只负责把方法翻译成 TSPL 字符串,再通过串口、USB 或 socket 写进去。搞清楚这条链路,后面调参数、排故障才有抓手。这一章把拆包、连接、协议判定三件事按顺序走完,每一步都能验证。
2.1 解压后先做三件事:看目录、看 jar、看 demo
我习惯先看清单再看内容,避免一上来就被一堆同名类绕晕。
# 只看结构,不解压 unzip -l LK-Bxx_JAVA_SDK.zip | head -60 # 解压到独立目录,避免污染现有工程 unzip -q LK-Bxx_JAVA_SDK.zip -d ./lkbxx-sdk # 看主 jar 里的包路径和类名,这一步决定你 import 什么 jar tf ./lkbxx-sdk/lib/*.jar | grep -v '\$' | head -80第一条命令输出的是文件树,重点看 lib、demo、doc、driver 四类;第二条把包摊开;第三条最关键,jar tf能直接告诉你 SDK 的包名结构,比如通信层通常会单独分包(port、comm、serial之类),指令拼装在一个command或tspl包里,业务层再包一个Printer或PrinterClient。如果 Eclipse 里 import 之后一片红,八成是漏了 lib 目录下的本地库 jar。
| 典型路径 | 内容 | 处理方式 |
|---|---|---|
lib/*.jar | SDK 主包 + 通信依赖(串口或 USB) | 全部加入 classpath,别只加主包 |
demo/ | 官方示例,通常一两个Main.java | 第一件事就是把它跑通 |
doc/ | 指令手册 PDF、API 说明 | 留着,排错时要查指令原文 |
driver/ | Windows 驱动、INF 文件 | 先在系统里装完,再插设备 |
*.dll/*.so | JNI 本地库 | 放到java.library.path下 |
提示:如果 lib 目录里同时出现
rxtxSerial、jSerialComm、usb4java这类名字,说明 SDK 对不同链路分别做了适配,你只需要引一条,不要把三条全部拖进来,本地库冲突是后面最难查的一类问题。
2.2 串口、USB、网口:三条链路的参数怎么写
链路选择本质上取决于部署形态。产线工位机离打印机一两米,串口最稳;办公室共享一台,走网口;桌面客户端插 USB,即插即用。三条路的参数不一样,但初始化套路是一致的:先建连接对象,再 open,最后才谈画布。
串口的关键是四个参数,波特率必须和打印机自检页上打印出来的一致,常见 9600、19200、38400、115200 四档;数据位 8、停止位 1、校验位无,这三项几乎不用改。改错波特率的现象很典型:端口能打开,写数据不报错,但打印机毫无反应,或者吐出一堆乱码字符。
// 类名与方法名以 demo 里的实际实现为准,这里用短名示意 PrinterClient serial = PrinterClient.builder() .portType(PortType.SERIAL) .portName("COM3") // Linux 下是 /dev/ttyUSB0,注意权限 .baudRate(115200) // 与自检页一致,不一致就是乱码 .dataBits(8) .stopBits(1) .parity(0) // 0 = none .build(); serial.open();网口就简单得多,绝大多数标签机走 9100 端口的裸数据通道,不需要驱动,不需要厂商工具,直接 socket 写字节流。这条路的坑不在连接,在超时和粘包:打印任务提交后要等回读状态,别写完就 close 连接。
PrinterClient net = PrinterClient.builder() .portType(PortType.TCP) .host("192.168.1.50") // 建议在路由器上做静态绑定 .port(9100) // 裸打印通道,非 515/631 .timeout(3000) // 单次写超时,单位毫秒 .build();USB 这条链路最麻烦。Windows 下多数标签机会同时暴露一个虚拟串口和一个 USB 打印接口,SDK 的 USB 模式走的是后者,需要 VID/PID 匹配。设备管理器里能看到设备不代表 Java 能拿到句柄,因为系统打印后台可能已经抢占。我的做法是:能用虚拟串口就不用 USB 原生模式,少一层 JNI 就少一个黑匣子。
2.3 协议判定:TSPL 还是 CPCL,决定了后面所有 API 长什么样
同样是「标签打印机」,指令集可能完全不同。TSPL 是坐标式的,有SIZE、GAP、CLS、TEXT、BARCODE、PRINT这套;CPCL 用! 0 200 200开头;EPL 更老,N、A、B、P一路到底;ESC/POS 是给小票机的,压根没有画布坐标概念。协议搞错,SDK 里所有绘制方法的参数含义都会错位。
判定方法有三个,按成本从低到高:一是关机按住走纸键开机,打一张自检页,上面会印固件版本和指令集;二是翻 doc 目录里的手册,标题里通常直接写 TSPL2;三是看 SDK 常量类,如果有一堆SIZE、GAP、DENSITY字样,基本就是 TSPL 系。
| 协议 | 典型起始指令 | 坐标单位 | 常见机型形态 |
|---|---|---|---|
| TSPL / TSPL2 | SIZE 100 mm,80 mm | dot | 桌面标签机主流 |
| CPCL | ! 0 200 200 800 1 | dot | 便携机、移动打印 |
| EPL | N/q 800 | dot | 老款机型 |
| ESC/POS | 0x1B 0x40 | 行 | 小票机,不适用标签画布 |
注意:不要凭包装盒上的参数猜协议。同一系列不同型号,固件可能一个走 TSPL 一个走 CPCL,判定必须以自检页或手册为准。
3. 最小可跑通:40 行 Java 打出第一张标签
跑通第一张标签的意义不在于这张标签好不好看,而在于把「连接—画布—元素—提交」这条链路一次性验证完。这条链路上任何一环出错,现象都长得差不多:打印机不动。所以第一版代码要尽量短、尽量直白,不要一上来就封装工具类。等这张标签出来了,再去谈坐标、字体和批量。
3.1 初始化顺序:连接在前,画布在后
顺序不能反。画布对象是依附在连接上的,很多 SDK 的newCanvas()内部会去读一次打印机状态(分辨率、纸张宽度),连接没建立就调用,要么抛空指针,要么拿到一份默认值,后面所有坐标全错。我见过最典型的翻车是:本地开发用网口,画布默认按 300dpi 算,现场换成 203dpi 的机器,标签整体缩小三分之一,字全挤在左上角。
正确的顺序是四步:建连接、open、建画布并显式声明尺寸与 dpi、提交。dpi 一定要显式传,不要依赖 SDK 探测,探测失败时它通常会静默回退到 203,而你手上是 300dpi 的机器。
public class LabelDemo { public static void main(String[] args) throws Exception { // 第 1 步:连接。这里的类名按 demo 实际实现替换 PrinterClient client = PrinterClient.builder() .portType(PortType.TCP) .host("192.168.1.50") .port(9100) .timeout(3000) .build(); client.open(); // 第 2 步:画布。尺寸单位 mm,dpi 必须与机器铭牌一致 LabelCanvas canvas = client.newCanvas() .widthMm(100) .heightMm(80) .gapMm(2) // 间隙纸填 2~3,黑标纸走另一套参数 .dpi(203) .build(); // 第 3 步:绘制。坐标单位是 dot,原点在左上角 canvas.text(16, 16, "出库单", 24, true); canvas.text(16, 64, "SKU: A-1024-01", 18, false); canvas.barcode(16, 120, BarcodeType.CODE128, 80, "A-1024-01", 2); canvas.qrcode(560, 120, 4, "SN:20240513001"); // 第 4 步:提交并等结果 PrintResult result = canvas.print(1); System.out.println(result.isOk() + " | " + result.getMessage()); client.close(); } }逻辑上有两点值得说。print(1)里的 1 是份数,不是重试次数,别混;返回的PrintResult如果 SDK 支持状态回读,getMessage()里会带缺纸、开盖、过热这类状态码,第一版就把这个字段打出来,后面排障能省很多事。
3.2 坐标系换算:mm 到 dot,别拿 300dpi 的公式套 203dpi
标签机的所有绘制 API 吃的是 dot,而设计师给你的是 mm。换算只有一个公式:dot = mm × dpi ÷ 25.4。203dpi 下 1mm ≈ 8 dot,300dpi 下 1mm ≈ 11.81 dot。100mm 宽的标签,203dpi 就是 800 dot,300dpi 就是 1181 dot。
实际写代码时不要每次现算,做一个常量表最省心:
| 分辨率 | 每毫米 dot | 100mm 宽 | 80mm 高 | 常用字号换算 |
|---|---|---|---|---|
| 203dpi | 7.99 | 800 | 640 | 24dot ≈ 3mm 高 |
| 300dpi | 11.81 | 1181 | 945 | 36dot ≈ 3mm 高 |
提示:坐标越界不会报错,打印机通常直接裁掉。元素「凭空消失」时先打印一下 x、y 和画布宽高,八成是超了边界,而不是 SDK 有 bug。
3.3 文本、条码、二维码的三种写法
文本最容易被低估。TSPL 的TEXT x,y,"font",rotation,x-mult,y-mult,"content"里,x-mult和y-mult是横向纵向放大倍数,很多 SDK 把它包装成「字号」,实际底层是倍数,所以你会发现字号只能取几个离散值。要打出非整数倍的字,得靠换内置字体或者走图片渲染。
条码的参数比文本多,关键在窄条宽度(narrow bar)。Code128 常见取值 1 到 3 dot,取 1 时标签小、密度高,但老机器的打印头和碳带稍有磨损就扫不出来;取 2 或 3 更稳妥,代价是条码变长。高度一般给 60 到 100 dot,太矮的条码扫码枪读起来很吃力。
二维码走QRCODE指令,参数是纠错等级(L/M/Q/H)、单元宽度、模式。纠错等级越高,同样的内容码就越大。印在铜版纸上、几小时内就出库的标签,M 级够用;要贴在产品上放几个月的,用 Q 或 H,脏一点、磨损一点还能读出来。
// 条码:窄条 2 dot,高度 80 dot,人眼可读字符打开 canvas.barcode(16, 120, BarcodeType.CODE128, 80, "A-1024-01", 2); // 二维码:纠错 M,单元 4 dot,内容建议控制在 60 字节内 canvas.qrcode(560, 120, QrEcc.M, 4, "SN:20240513001");4. 参数调优:浓度、速度、偏移与纸张校准
能打出来只是及格线,打得清楚、位置准、连打两百张不跑偏才是交付标准。这一章的四组参数,前两组决定清晰度,后两组决定位置稳定性。它们的共同点是:都在打印机侧生效,改代码之前先去打印机上改一遍,确认问题出在哪一侧,能省掉一半的调试时间。
4.1 浓度和速度:先定速度,再调浓度
浓度(density)和速度(speed)是一对跷跷板。速度越快,打印头在每一点上停留的时间越短,需要更高浓度补偿;浓度太高又会糊字、扩散,细笔画粘连。我的顺序是先定速度再调浓度,因为速度受产线节拍约束,浓度是自由变量。
典型取值:铜版纸 + 蜡基碳带,速度 3 到 4 inch/s,浓度 8 到 10(0 到 15 刻度);合成纸 + 混合基,速度 2 到 3,浓度 10 到 12。热敏纸不用碳带,速度可以拉到 5 到 6,浓度 7 到 9。这些只是起点,最终以实际扫读效果为准。
// 打印参数一般在画布或连接上设置,具体方法名看 SDK canvas.settings() .density(9) // 0~15,先给中间值再微调 .speed(4) // inch/s,受产线节拍约束优先定 .build();调参的验证方法别靠眼睛。同一批标签打五张,浓度从 7 开始每次加 1,用扫码枪逐张读,记录首次读取成功率。眼睛觉得都清楚的几张,扫码枪能读出差别,这是血泪经验。
4.2 偏移与间隙:打歪一毫米,可能是校准不是代码
标签打歪是最高频的投诉,但十次里有七次不是代码问题。三个来源要分清楚:一是纸张间隙没有校准,打印机不知道一张标签有多长,每张都往前飘一点点,打完十张就累积成明显的偏移;二是GAP参数填错,2mm 的间隙填成 3mm,绘制区整体下移;三是首张定位偏移(offset),打印机机械结构决定的固定差值,需要单独补偿。
校准优先用打印机自带的走纸校准:关机,按住走纸键开机,等指示灯闪两下松手,机器会自动走几张纸并记住间隙。这一步做完再跑代码,能排掉大部分问题。剩下的固定偏移,用 SDK 的偏移参数补:
// 显式声明间隙,单位 mm;黑标纸改用 BLINE 指令 client.sendRaw("GAP 2 mm,0 mm\r\n"); // 纵向偏移补偿,正值向下,单位 dot canvas.offsetY(8);提示:改偏移之前先打一张校准页(打一个方框和一条中线),把实际印出来的位置量出来,再决定补偿多少 dot。凭感觉填数字,只会越调越偏。
4.3 中文乱码的三个来源
中文打不出来或者打成方块,几乎全落在三处。第一处是字符编码:TSPL 的TEXT指令默认按单字节处理,中文必须走 GB18030 或 GBK,SDK 一般有个setEncoding或者单独的textCn方法,用错就是乱码。第二处是字体:内置字库里没有中文字模,只带 ASCII,这时无论编码怎么设都打不出字,必须换带中文字库的固件,或者把中文渲染成图片再打(BITMAP指令)。第三处是长度:中文一个字符占两字节,SDK 里如果按字符数算缓冲区长度,超长会被截断。
// 方案一:设置编码,SDK 负责转成打印机认的字节 canvas.setEncoding("GB18030"); canvas.text(16, 16, "出库单", 24, true); // 方案二:字体里没有中文字模时,自己渲染成位图再发 BufferedImage img = renderTextToImage("出库单", 24); canvas.bitmap(16, 16, img);两种方案的取舍很实际:方案一速度快、体积小,依赖固件字库;方案二什么机器都能打,缺点是每一帧都要生成位图,批量打印时 CPU 占用会上去。我一般先用方案一验证,确认固件支持中文就一路用下去;碰上老固件没字库的,再退到方案二。
5. 排查与避坑:五条踩过的血泪记录
这一章的五条都是现场真实发生过的,顺序按出现频率排。每一条我都尽量给出可复现的现象、能验证的原因、以及一次就能解决的方案。碰上问题建议先从这五条里对号入座,大部分情况不用翻手册。
5.1 中文打成方块或者一串问号
现象:英文数字正常,中文位置全是方块、问号,或者干脆一片空白。
原因:字符编码没设成 GB18030/GBK,SDK 按单字节把 UTF-8 的中文拆开发给打印机;或者固件字库里根本没有中文字模,编码设对了也没用,打印机会把不认识的字节丢掉。
解决:先设编码再打,设完还是方块,就换成位图方案。判定方法很简单:打一个「中」字,如果有轮廓但缺笔画,是字库问题;如果连轮廓都没有,是编码问题。
5.2 每张标签后面跟一张空白
现象:连打十张,实际出来二十张,一有一空交替出现。
原因:SIZE里声明的标签高度大于实际纸张高度,打印机按声明长度走纸,多出来的部分就成了一张空白;或者GAP的间隙值填大了,机器把一张纸当成两张。
解决:拿卡尺量实际标签高度和间隙,按实测值改SIZE和GAP,然后做一次走纸校准。别信包装盒上的标称值,纸张批次不同能差 0.5mm。
5.3 端口能打开,但写数据进去没反应
现象:open()返回成功,print()也不抛异常,打印机一动不动,状态灯常亮。
原因:串口波特率与打印机不一致;网口连到了 515 或 631(LPD/IPP 端口)而不是 9100;USB 模式下设备被系统打印后台占用。
解决:串口以自检页波特率为准;网口先用telnet host 9100确认端口通;USB 先在系统里停掉「使用后台打印」那一项,或者干脆改用虚拟串口。写完之后读一次状态,能回读就说明链路真通了。
5.4 批量打到第 N 张卡死
现象:单张正常,循环打印几十张后连接假死,后面的任务全部超时。
原因:打印机的接收缓冲区写满,上一次任务还没消费完就继续灌数据;或者连接对象没有复用,每张新建一个 socket,句柄耗尽。
解决:一个连接复用到结束,每提交一张读一次状态再提交下一张,中间加 30 到 50ms 间隔。真的要做高吞吐,就在应用侧排一个串行队列,永远保持「一进一出」,不要让多个线程同时往一个端口写。
5.5 条码肉眼清楚,扫码枪读不出来
现象:条码看得很清楚,边缘也不糊,但扫码枪反复扫不出来,手机摄像头反而能读。
原因:窄条宽度太小(1 dot)导致边缘对比度不足;条码两侧静区(quiet zone)被其他元素侵占;或者条码高度不够,扫码枪的扫描线要跨过足够多的条。
解决:窄条改到 2 到 3 dot,高度给到 80 dot 以上,左右各留出至少 10 倍窄条宽度的空白区。验证别用眼睛,直接用扫码枪,打五张读五张,成功率达到 100% 再上线。
6. 进阶:把 SDK 塞进 Spring Boot 并做一次可复现的验收
单机跑通和上生产是两件事。业务侧往往是一个 HTTP 接口进来就要出单,并发一上来,多个线程抢同一个端口,前面第 5 章第 4 条那个卡死就会准时出现。我的做法是把打印能力收成一个单例服务:连接复用、任务串行、出错即弃、下次重建。这样既避开了并发写端口的坑,也能在连接异常时自愈,不需要重启应用。
6.1 单例 + 串行队列:并发打印的唯一解
@Service public class LabelPrintService { // 单线程执行器,保证同一时刻只有一个任务在写端口 private final ExecutorService queue = Executors.newSingleThreadExecutor(); private volatile PrinterClient client; public CompletableFuture<Boolean> submit(LabelTask task) { return CompletableFuture.supplyAsync(() -> { try { PrinterClient c = client(); // 懒加载,首次调用时建立 c.print(task.toCanvas(c)); // 提交并阻塞等结果 return true; } catch (Exception e) { closeQuietly(); // 出异常就丢弃连接 return false; } }, queue); } private PrinterClient client() throws Exception { if (client == null || !client.isOpen()) { synchronized (this) { if (client == null || !client.isOpen()) { client = PrinterClient.builder() .portType(PortType.TCP) .host("192.168.1.50").port(9100) .timeout(5000).build(); client.open(); } } } return client; } }关键点是那个newSingleThreadExecutor()。它把并发问题在应用层就消化掉了,代价是吞吐受限于单台打印机的物理速度(一张 100×80 的标签大致 1 到 2 秒),这本来就是硬约束,多线程也快不了。closeQuietly()那一行是后悔药:出现超时、缺纸、开盖之后连接状态往往已经不可信,丢掉重建比修修补补靠谱。
6.2 一份能照着走的验收清单
上线前我会固定跑一遍下面这张表,每项都留一条实测记录。这张表的价值在于把「看起来能用」变成「有证据能用」,后面出问题也能快速定位是哪一层退化了。
| 验收项 | 操作 | 合格标准 |
|---|---|---|
| 协议与 dpi | 打自检页,比对代码里的dpi | 完全一致,无缩放 |
| 纸张定位 | 连打 20 张,量首张与末张的纵向位置差 | 累积偏移 ≤ 0.5mm |
| 中文与字体 | 打含中英文混排的样张 | 无方块、无截断 |
| 条码可读性 | 每张用扫码枪读,读 10 张 | 首次读取成功率 100% |
| 异常恢复 | 打印中开盖,再合上继续 | 服务自愈,不需重启 |
| 并发压测 | 20 个请求同时进接口 | 无端口占用异常,全部有序出单 |
注意:压测要在真实纸张上跑,不要用「打印到文件」的模拟模式,缓冲区满和走纸异常只有真机才暴露得出来。
从那以后我每次接这类标签打印项目,都会先打一张自检页确认协议和 dpi,再让业务代码往上堆,最后按上面这张表走一遍才敢交付。前面省掉的每一步,后面都会在产线上加倍还回来。希望帮到你。
本文还有配套的精品资源,点击获取