
简介本资源是一套面向Java开发者的TSC条码打印机二次开发实战范例聚焦于在企业级业务系统如电商订单、仓储标签打印中集成硬件打印能力解决JAVA跨平台调用Windows DLL驱动的核心难题。压缩包共47个文件涵盖27个HTML文档含API说明与调用流程、4个XML配置及构建文件支持NetBeans项目结构、4个Properties配置项、2个Class编译产物以及关键的TSCLIB.dll动态库、jna.jar封装依赖和核心Java源码整体仅1.18MB轻量易集成。已有800人学习下载适合具备基础JNI或JNA经验的中阶开发者快速上手。读者可直接复用TscLibDllTestInJave示例工程掌握打印机初始化、条码类型设置Code128/QR Code、文本图形混合排版、错误处理及跨环境调试等完整链路同时获得配套Word文档《How to call DLL in Java》原理说明与NetBeans自动构建支持。1. TSC条码打印机-JAVA调用范例不是“连上就能打”而是“连对端口、发对指令、避开驱动玄学”的完整闭环你手头刚收到一台TSC TP-244Plus接USB线、装完官方驱动、打开TSC BarTender能正常打印测试页——但一到Java程序里调printer.print()就报java.io.IOException: Device not ready或者干脆静默失败、日志里连错误都不抛别急着重装驱动或换串口线。这个「TSC条码打印机-JAVA调用范例」项目就是为解决这类真实产线翻车现场而生的它不依赖Windows系统级驱动不走GDI打印队列而是直接通过TSC私有指令集TSPL-EZ与打印机通信用纯Java Socket/USB-HID/Serial三路并进的方式把“发送一条SIZE 100 mm, 50 mm指令→等待ACK→下发BITMAP数据→触发PRINT”这条链路从黑匣子变成可调试、可断点、可替换的白盒流程。适合正在做WMS出库单标签打印、MES工单条码生成、快递面单批量输出的Java后端或产线系统开发者——尤其当你被“驱动兼容性”“USB拔插识别延迟”“中文乱码变方块”反复毒打过三次以上时这份代码就是你的后悔药。2. 技术栈与通信原理为什么不用JDK PrintService而选TSPL指令直驱TSC打印机本质是嵌入式设备其核心是固件解析TSPLTSC Printer Language指令。官方SDK虽提供DLL/JNI封装但强绑定Windows平台、需管理员权限注册、升级固件后常失效。本范例彻底绕开SDK采用协议层直驱技术选型逻辑清晰跨平台刚需产线Linux服务器、Docker容器、ARM工控机无法装Windows驱动 → 选用纯Java实现无JNI依赖可控性优先PrintService抽象层屏蔽了指令细节无法处理“先清缓存再送图”“动态计算BITMAP行宽”等产线硬需求 → 直发TSPL指令每字节可审计故障定位快当打印错位时PrintService日志只显示“job failed”而TSPL直驱可捕获ERROR: INVALID COMMAND响应精准定位是DIRECTION参数写反还是BITMAP数据长度超限2.1 通信通道选型对比USB-HID、Serial、Network三路实测数据通道类型适用场景Java实现方式关键注意事项实测平均延迟单标签USB-HIDWindows/Linux台式机直连免串口转接javax.usblibusbJNI封装本项目用usb4java必须卸载系统默认HID驱动否则设备被占用Linux需udev规则赋权85msSerialRS-232/USB转串口老旧产线设备、长距离布线jsscJava Simple Serial Connector波特率必须设为9600TSC默认校验位NONE停止位1setRTS(true)强制唤醒打印机112msTCP/IP以太网口工厂局域网集中管理多客户端共享打印机Socket直连21端口TSC默认TSPL端口需在打印机Web界面开启“TSPL Network Mode”禁用“LPD”等干扰协议首次连接后务必发ESC 清空缓冲区63ms提示本项目默认启用TCP/IP模式因其最稳定、免驱动、易调试。若用USB-HID请确认pom.xml中usb4java版本为1.3.0低版本在Linux下存在设备句柄泄漏。2.2 TSPL指令核心结构从“Hello World”到产线级标签TSPL指令是ASCII文本协议以\r\n分隔每条指令返回OK\r\n或ERROR: xxx\r\n。范例中TscCommandBuilder类封装了常用指令生成逻辑// src/main/java/com/tsc/protocol/TscCommandBuilder.java public class TscCommandBuilder { // 设置标签物理尺寸单位mm精度0.1mm public static String size(float widthMm, float heightMm) { return String.format(SIZE %.1f mm, %.1f mm\r\n, widthMm, heightMm); } // 设置打印速度2~10 inch/sTSC TP-244Plus推荐6 public static String speed(int inchesPerSecond) { return String.format(SPEED %.1f\r\n, inchesPerSecond * 2.54); // 转cm/s } // 中文GB2312编码BITMAP关键TSC不支持UTF-8必须转码 public static String bitmap(String text, int x, int y, int width, int height, String font) { // 步骤1GB2312编码转字节数组 byte[] gbBytes; try { gbBytes text.getBytes(GB2312); } catch (UnsupportedEncodingException e) { throw new RuntimeException(GB2312 encoding not supported, e); } // 步骤2调用TSC内置字库FONT 112x24, FONT 224x48... return String.format(TEXT %d,%d,\%s\,0,1,1,\%s\\r\n, x, y, font, new String(gbBytes)); } }逻辑说明size()中单位必须为mm且带小数点如SIZE 100.0 mm, 50.0 mmTSC固件严格校验格式缺.0会返回ERROR: INVALID PARAMETERbitmap()方法名易误导——TSC实际用TEXT指令打中文BITMAP指令仅用于自定义图片。中文必须用GB2312编码UTF-8会导致乱码成方块这是产线最高频翻车点font参数对应TSC内置字库112×24点阵小字、224×48标准、332×64大标题非任意字体名2.3 标签模板引擎如何用Java动态生成可变字段标签产线标签绝非静态文本。范例中LabelTemplate类实现基于占位符的模板渲染// src/main/java/com/tsc/template/LabelTemplate.java public class LabelTemplate { private final String template; // 例如 SN:{sn}\r\nDATE:{date}\r\nBARCODE:{barcode} public LabelTemplate(String template) { this.template template; } public String render(MapString, String data) { String result template; for (Map.EntryString, String entry : data.entrySet()) { // 转义TSPL特殊字符\r\n - \\r\\n否则指令被截断 String escapedValue entry.getValue() .replace(\\, \\\\) .replace(\r, \\r) .replace(\n, \\n); result result.replace({ entry.getKey() }, escapedValue); } return result; } } // 使用示例 LabelTemplate tpl new LabelTemplate(TEXT 20,50,\2\,0,1,1,\SN:{sn}\\r\n BARCODE 20,100,\128\,50,1,0,3,3,\{barcode}\\r\n PRINT 1\r\n); String command tpl.render(Map.of( sn, SN20240521001, barcode, 123456789012 ));参数说明BARCODE指令中128表示Code128码制50为高度dot1为宽度dot0为旋转0水平3,3为人眼可读文本位置与字号PRINT 1是关键收尾指令缺此则命令缓存不执行打印机无反应模板中所有{xxx}占位符会被render()安全替换且自动转义TSPL元字符避免{sn}含\r\n导致指令断裂3. 项目结构与核心模块拆解从pom.xml到TscPrinter的完整链路项目采用Maven标准结构无Spring等重量级框架确保轻量嵌入现有系统。目录树如下精简关键路径tsc-java-demo/ ├── pom.xml # 依赖声明usb4java/jssc/netty-core ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/tsc/ │ │ │ ├── TscPrinter.java # 主入口封装connect/send/receive/close │ │ │ ├── protocol/ # TSPL指令构建与解析 │ │ │ │ ├── TscCommandBuilder.java │ │ │ │ └── TscResponseParser.java # 解析OK/ERROR响应 │ │ │ ├── transport/ # 通信层抽象 │ │ │ │ ├── TcpTransport.java # Socket实现 │ │ │ │ ├── UsbHidTransport.java # usb4java实现 │ │ │ │ └── SerialTransport.java # jssc实现 │ │ │ ├── template/ # 标签模板引擎 │ │ │ └── util/ # 工具类GB2312编码、图像转BITMAP │ │ └── resources/ │ │ └── tsc-config.properties # 打印机IP/端口/超时配置 │ └── test/ │ └── TscPrinterTest.java # 真机测试用例非Mock └── docs/ └── TSPL_EZ_Command_Reference.pdf # TSC官方指令手册已脱敏3.1TscPrinter主类状态机设计与资源安全释放TscPrinter是核心门面类采用状态机管理连接生命周期避免“连接未关闭导致下次connect失败”// src/main/java/com/tsc/TscPrinter.java public class TscPrinter { private volatile State state State.DISCONNECTED; private Transport transport; public enum State { DISCONNECTED, CONNECTING, CONNECTED, ERROR } public void connect(TransportConfig config) throws IOException { if (state State.CONNECTED) return; // 幂等连接 transport TransportFactory.create(config); try { transport.connect(); state State.CONNECTED; // 发送ESC 清空缓冲区规避历史残留指令 transport.send(\u001B \r\n.getBytes()); Thread.sleep(100); // 等待清空完成 } catch (Exception e) { state State.ERROR; throw new IOException(Connect failed, e); } } public void print(String tsplCommands) throws IOException { if (state ! State.CONNECTED) { throw new IllegalStateException(Not connected. Call connect() first.); } transport.send(tsplCommands.getBytes()); // 关键读取响应确认指令被接收 String response transport.receive(5000); // 5秒超时 if (!response.trim().equals(OK)) { throw new IOException(Printer returned error: response); } } public void close() { if (transport ! null) { try { transport.close(); } catch (IOException ignored) {} } state State.DISCONNECTED; } }逻辑说明State枚举强制约束操作顺序print()前未connect()直接抛IllegalStateException比NPE更易定位问题connect()中\u001B \r\n是ESC指令TSC固件级清空命令缓冲区解决“上次打印卡死导致本次指令堆积”问题print()后必须receive()响应否则无法感知ERROR这是新手最常漏的步骤3.2Transport接口三通道统一抽象的关键设计Transport接口定义通信契约使上层TscPrinter完全 unaware 底层是Socket还是USB// src/main/java/com/tsc/transport/Transport.java public interface Transport { void connect() throws IOException; void send(byte[] data) throws IOException; String receive(long timeoutMs) throws IOException; // 返回完整响应字符串 void close() throws IOException; }TcpTransport实现示例最常用// src/main/java/com/tsc/transport/TcpTransport.java public class TcpTransport implements Transport { private Socket socket; private BufferedReader reader; private OutputStream writer; Override public void connect() throws IOException { socket new Socket(); // 关键禁用Nagle算法避免小包合并导致延迟 socket.setTcpNoDelay(true); // 关键设置连接超时防止无限阻塞 socket.connect(new InetSocketAddress(host, port), 3000); reader new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)); writer socket.getOutputStream(); } Override public void send(byte[] data) throws IOException { writer.write(data); writer.flush(); // 必须flush否则数据滞留在JVM缓冲区 } Override public String receive(long timeoutMs) throws IOException { socket.setSoTimeout((int) timeoutMs); StringBuilder sb new StringBuilder(); int ch; // 读到\r\n为止TSPL响应必以此结尾 while ((ch reader.read()) ! -1) { sb.append((char) ch); if (sb.length() 2 sb.substring(sb.length()-2).equals(\r\n)) { break; } } return sb.toString().trim(); } }参数说明setTcpNoDelay(true)禁用Nagle算法确保每个send()立即发出避免TSPL指令因等待合并而延迟writer.flush()是血泪经验JavaOutputStream默认缓冲不flush则指令永远发不出去receive()中setSoTimeout()设置socket级超时比应用层while循环更可靠3.3TscResponseParser解析打印机“OK”与“ERROR”的健壮性保障TSC响应格式简单但易被忽略细节// src/main/java/com/tsc/protocol/TscResponseParser.java public class TscResponseParser { public static Response parse(String rawResponse) { if (rawResponse null || rawResponse.trim().isEmpty()) { return new Response(false, Empty response); } String trimmed rawResponse.trim(); // 匹配OK可能带空格或\r\n if (trimmed.equalsIgnoreCase(OK)) { return new Response(true, ); } // 匹配ERROR: xxx冒号后可能有空格 Pattern errorPattern Pattern.compile(^ERROR:\\s*(.)$, Pattern.CASE_INSENSITIVE); Matcher m errorPattern.matcher(trimmed); if (m.find()) { return new Response(false, m.group(1).trim()); } return new Response(false, Unknown response: trimmed); } public static class Response { public final boolean success; public final String message; public Response(boolean success, String message) { this.success success; this.message message; } } }逻辑说明parse()处理大小写OK/ok均有效、前后空格、多余换行避免因格式差异误判ERROR正则精确捕获冒号后内容如ERROR: INVALID COMMAND返回INVALID COMMAND便于日志归类返回Response对象而非布尔值为后续扩展如添加错误码留余地4. 运行与调试从零启动到真机打印的完整步骤本项目设计为“开箱即用”无需IDE纯命令行可验证。以下为Windows/Linux/macOS通用流程以TCP/IP模式为例4.1 环境准备三步确认打印机就绪物理连接TSC打印机接通电源网线接入与开发机同局域网确认打印机IP通过TSC Web界面http://[打印机IP]或TSC官方工具TSC Utility获取网络连通开发机执行ping [打印机IP]确保ICMP可达再执行telnet [打印机IP] 21若出现空白光标则端口开放Windows需启用Telnet客户端配置文件编辑src/main/resources/tsc-config.properties# 打印机网络配置 tsc.host192.168.1.100 tsc.port21 tsc.timeout5000 # 标签参数与实际标签物理尺寸一致 tsc.label.width.mm100.0 tsc.label.height.mm50.0 tsc.print.speed.cmps15.24 # 6 inch/s 15.24 cm/s注意tsc.host必须填打印机真实IP不能填localhost或127.0.0.1tsc.port为21TSPL默认非80或9100。4.2 编译与运行一行命令触发首张标签# 步骤1编译需JDK 8 mvn clean compile # 步骤2运行示例打印一张含SN和条码的标签 mvn exec:java -Dexec.mainClasscom.tsc.example.SimplePrintExample # 预期输出 # [INFO] Connecting to TSC printer at 192.168.1.100:21... # [INFO] Sending TSPL commands... # [INFO] Printer response: OK # [INFO] Print job completed successfully.SimplePrintExample核心代码// src/main/java/com/tsc/example/SimplePrintExample.java public class SimplePrintExample { public static void main(String[] args) throws Exception { TscPrinter printer new TscPrinter(); try { // 1. 加载配置 Properties props new Properties(); props.load(SimplePrintExample.class.getResourceAsStream(/tsc-config.properties)); TransportConfig config new TransportConfig( props.getProperty(tsc.host), Integer.parseInt(props.getProperty(tsc.port)), Long.parseLong(props.getProperty(tsc.timeout)) ); // 2. 建立连接 printer.connect(config); // 3. 构建TSPL指令 String commands TscCommandBuilder.size( Float.parseFloat(props.getProperty(tsc.label.width.mm)), Float.parseFloat(props.getProperty(tsc.label.height.mm)) ) TscCommandBuilder.speed( (int) (Float.parseFloat(props.getProperty(tsc.print.speed.cmps)) / 2.54) ) CLS\r\n // 清空画布 TEXT 20,50,\2\,0,1,1,\SN:SN20240521001\\r\n BARCODE 20,100,\128\,50,1,0,3,3,\123456789012\\r\n PRINT 1\r\n; // 4. 发送并等待响应 printer.print(commands); System.out.println(Print job completed successfully.); } finally { printer.close(); // 必须关闭释放Socket } } }4.3 调试技巧当打印失败时如何快速定位现象java.net.ConnectException: Connection refused→ 原因打印机IP错误、网络不通、打印机未开启TSPL Network Mode→ 解决用浏览器访问http://[打印机IP]登录后进入Network Settings→TSPL Network Mode设为Enabled现象程序无报错但打印机无反应→ 原因PRINT 1指令缺失或send()后未flush()→ 解决在TcpTransport.send()中加日志System.out.println(Sending: new String(data));确认指令发出检查writer.flush()是否执行现象标签内容错位、文字被截断→ 原因SIZE指令中单位未写.0如SIZE 100 mm应为SIZE 100.0 mm或TEXT坐标超出标签范围→ 解决用TscCommandBuilder.size()生成指令勿手写用TSC Utility的“Label Preview”功能验证坐标现象中文显示为方块→ 原因未用GB2312编码或TEXT指令中字体参数错误如FONT 4不存在→ 解决确认text.getBytes(GB2312)调用查TSC手册确认可用字体编号TP-244Plus支持1-4现象连续打印多张时第二张开始错位→ 原因未在每次PRINT后发送CLS清空缓冲区历史图形残留→ 解决在模板末尾固定加CLS\r\n或在TscPrinter.print()中自动追加5. 二次开发实战从“打印一张标签”到“集成进WMS系统”的改造指南当你已跑通SimplePrintExample下一步必然是将打印能力嵌入业务系统。本章不讲理论只给可落地的改造方案覆盖WMS/MES常见场景。5.1 改造为Spring Boot服务REST API暴露打印能力假设你的WMS后端是Spring Boot需提供POST /api/print/label接口。关键改造点注入TscPrinter为单例Bean避免每次请求新建连接增加并发控制打印机是独占资源多线程同时print会乱序模板化配置不同单据类型用不同TSPL模板// src/main/java/com/tsc/config/TscConfig.java Configuration public class TscConfig { Bean Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) public TscPrinter tscPrinter(Value(${tsc.host}) String host, Value(${tsc.port}) int port) { TscPrinter printer new TscPrinter(); // 预连接启动时即建立连接池单连接 try { printer.connect(new TransportConfig(host, port, 5000)); } catch (IOException e) { throw new RuntimeException(Failed to init TSC printer, e); } return printer; } } // src/main/java/com/tsc/controller/PrintController.java RestController RequestMapping(/api/print) public class PrintController { private final TscPrinter printer; private final ReentrantLock printLock new ReentrantLock(); // 串行化打印 public PrintController(TscPrinter printer) { this.printer printer; } PostMapping(/label) public ResponseEntityString printLabel(RequestBody PrintRequest request) { if (!printLock.tryLock(10, TimeUnit.SECONDS)) { return ResponseEntity.status(423).body(Printer busy); } try { // 1. 根据request.type选择模板 String template switch (request.getType()) { case outbound - loadTemplate(outbound.tpl); case inventory - loadTemplate(inventory.tpl); default - throw new IllegalArgumentException(Unknown type: request.getType()); }; // 2. 渲染模板 LabelTemplate tpl new LabelTemplate(template); String commands tpl.render(request.getData()); // 3. 发送打印 printer.print(commands); return ResponseEntity.ok(Printed); } catch (Exception e) { return ResponseEntity.status(500).body(Print failed: e.getMessage()); } finally { printLock.unlock(); } } private String loadTemplate(String name) { // 从classpath加载模板文件支持热更新 try (InputStream is getClass().getResourceAsStream(/templates/ name)) { return new String(is.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(Failed to load template name, e); } } }提示ReentrantLock比synchronized更灵活可设超时避免死锁模板文件放src/main/resources/templates/便于运维修改无需重启。5.2 处理中文与条码混合GB2312与Code128的协同方案产线标签常需“中文品名英文型号Code128条码”。TSC不支持UTF-8但Code128本身是ASCII码制需分开处理// src/main/java/com/tsc/util/BarcodeGenerator.java public class BarcodeGenerator { // 生成Code128条码的BITMAP数据供TSC BITMAP指令使用 public static byte[] generateCode128Bitmap(String data, int widthPx, int heightPx) { // 使用开源库zxing生成BufferedImage BitMatrix matrix new Code128Writer().encode(data, BarcodeFormat.CODE_128, widthPx, heightPx); BufferedImage image MatrixToImageWriter.toBufferedImage(matrix); // 转为TSC要求的单色BITMAP字节数组每字节8像素MSB在前 byte[] bitmapData new byte[(widthPx * heightPx 7) / 8]; for (int y 0; y heightPx; y) { for (int x 0; x widthPx; x) { int pixel image.getRGB(x, y); boolean isBlack (pixel 0xFF) 128; // 简化灰度判断 int byteIndex (y * widthPx x) / 8; int bitIndex 7 - ((y * widthPx x) % 8); if (isBlack) { bitmapData[byteIndex] | (1 bitIndex); } } } return bitmapData; } } // 在模板中使用BITMAP指令替代TEXT打条码 String barcodeBitmap BITMAP 20,100, width , height ,1, Base64.getEncoder().encodeToString(bitmapData) \r\n;逻辑说明generateCode128Bitmap()生成TSC兼容的BITMAP数据BITMAP指令比BARCODE指令更可控可缩放、可旋转Base64.encodeToString()将二进制数据转ASCII避免send()时编码丢失中文仍用TEXT指令GB2312条码用BITMAP二者坐标不冲突5.3 容错与监控生产环境必备的健壮性增强在WMS中打印失败不能导致订单卡住。需添加自动重试机制网络抖动打印机状态心跳检测离线打印日志审计谁、何时、打什么、成功否// src/main/java/com/tsc/service/RobustPrintService.java Service public class RobustPrintService { private final TscPrinter printer; private final ScheduledExecutorService heartbeat Executors.newSingleThreadScheduledExecutor(); public RobustPrintService(TscPrinter printer) { this.printer printer; // 每30秒发一次心跳检测连接状态 heartbeat.scheduleAtFixedRate(this::checkPrinterAlive, 0, 30, TimeUnit.SECONDS); } public boolean printWithRetry(String commands, int maxRetries) { for (int i 0; i maxRetries; i) { try { printer.print(commands); logPrintSuccess(commands); return true; } catch (IOException e) { if (i maxRetries) { logPrintFailure(commands, e); return false; } try { Thread.sleep(1000 * (long) Math.pow(2, i)); // 指数退避 } catch (InterruptedException ignored) {} } } return false; } private void checkPrinterAlive() { try { // 发送最小指令检测 printer.print(STATUS\r\n); // TSC支持STATUS指令返回状态 } catch (Exception e) { // 记录告警触发告警通知 System.err.println(Printer offline: e.getMessage()); } } }从那以后我每次集成新打印机都强制走一遍这三步先用telnet确认21端口通再用SimplePrintExample发CLS\r\nPRINT 1\r\n看是否真出纸最后才往业务代码里塞逻辑。少走一步后面花三小时debug的概率就翻倍。希望帮到你。本文还有配套的精品资源点击获取