简介:这份面向TSC打印机二次开发入门者的压缩包,聚焦标签打印与二维码打印等常见任务中的库文件部署和第三方接口调用,适合刚接触设备编程、希望减少环境配置弯路的新手。包内共8个文件,以2个lib静态库、2个h头文件、2个dll动态库为主,另附1个驱动安装程序和1份中文TSPL文档,整体约18.89MB;win32与x64两套库及头文件搭配齐全,可直接复制使用,省去手动编写函数声明的麻烦。目前已有507人学习下载,说明其基础文件组合对入门者具有一定参考价值。对需要快速验证打印效果、理解库文件组织方式的读者来说,这组资料能提供完整的基础文件集合,配合中文文档可辅助认识指令与接口,降低环境搭建和调用第三方库时的试错成本。
1. 仓库里那台打不出二维码的标签机
前一阵去一个朋友的仓库帮忙,他们的出库流程卡在一个很小的环节:贴在外箱上的标签,二维码扫码枪十次里有三次读不出来,剩下七次得对着标签换好几个角度。打印软件用的是设备自带的编辑工具,模板改一处要手点十几下,一天几百箱货,人工核对的时间比打包还长。问题的根子不在打印机硬件,标签内容是操作员手工拼出来的,二维码尺寸、纠错等级、留白全靠肉眼估,打印机只是照着瞎打。
这份名为“TSC 打印机开发所需”的资源包,针对的正是这一段:标签打印、条码打印、二维码打印这条链路上最容易翻车的地方。它把 TSPL/TSPL2 指令手册、SDK 动态库、驱动、多语言示例代码和调机工具打包在一起。适合两类人:一是要给 WMS、MES、收银、溯源这类业务系统加上标签打印能力的后端开发;二是刚从“用自带编辑软件画标签”过渡到“用代码控制标签”的工程师。下面按我拆包、跑通、踩坑、改造的顺序讲。
2. 资源包拆开看:目录结构、SDK 与两种开发模式
2.1 解压之后先认清这几层目录
解压出来东西不少,但真正每天会用到的就三块:指令手册、SDK 目录、工具目录。驱动基本只装一次,示例代码看一遍就丢开。目录结构整体是这样的:
TSC打印机开发所需/ ├── SDK/ │ ├── Windows/ │ │ ├── DLL/ # 32/64 位动态库,C#/C++/VB 都调它 │ │ ├── C#/ # 示例工程,.sln 直接开 │ │ ├── C++/ │ │ └── Java/ # JNA 封装,适合服务端 │ ├── Linux/ │ └── Android/ ├── 指令手册/ │ ├── TSPL2指令手册.pdf # 核心,通读一遍胜过搜十篇帖子 │ └── 指令速查表.xlsx ├── 驱动/ │ └── Windows/ # 安装包 + inf,设备管理器手动指 ├── 工具/ │ ├── PrinterTool/ # 状态监控、固件升级、参数回读 │ └── DiagTool/ └── 模板示例/ └── *.prn # 存好的指令文本,记事本能直接打开各目录的实际用途,我按开发阶段列一下:
| 目录 | 里面是什么 | 什么时候会翻它 |
|---|---|---|
指令手册/ | TSPL2 全部指令 + 参数说明 | 写每一行指令、查参数范围 |
SDK/Windows/DLL/ | 动态库 + 接口头文件 | 用驱动模式或需要调打印机高级功能 |
SDK/*/示例 | 可编译的最小工程 | 第一次跑通、验证端口与库是否匹配 |
驱动/ | Windows 打印驱动 | 走 USB 虚拟打印队列时 |
工具/ | 状态监控、参数回读 | 标签偏位、报错灯闪、固件异常 |
模板示例/*.prn | 纯文本指令 | 学指令写法最快的素材 |
模板示例这个目录容易被忽略,其实价值很高。.prn文件就是一段纯文本指令,用记事本打开能看到别人是怎么组织 SIZE、GAP、BARCODE 顺序的。我一般先挑一个和业务最接近的.prn,把里面的文字和条码内容替换掉,直接发到打印机上验证,比从零开始写指令快得多。
提示:手册的版本要和机器固件对上。固件偏老的机器,某些新指令会被静默忽略,指令发过去了,标签上就是少一块,不报错。
2.2 指令流模式和驱动模式,选哪个
资源包里其实藏了两条完全不同的开发路径,很多人一开始没分清,结果代码写了一半才发现路走歪了。
第一条是指令流模式:你自己拼 TSPL2 文本指令,通过 TCP 9100、USB、串口直接写给打印机。打印机收到的是“画一行字在 (20,20)”“画一个 128 码在 (20,60)”,它自己负责渲染。这条路不依赖操作系统驱动,Windows、Linux、安卓、树莓派都能跑,一台打印机换个 IP 就能用,是服务端最常选的方式。
第二条是驱动模式:先在系统里装好打印驱动,打印机变成一个标准打印队列,然后走 SDK 里的动态库(常见是openport、sendcommand、printerfont、windowsfont、barcode、qrcode这类函数)或者直接 GDI 画图。好处是排版自由度高,能贴任意图片、任意字体;坏处是绑死操作系统,Linux 上会很难受,一台服务器带多台不同型号的打印机时驱动版本还容易打架。
我自己的判断标准很朴素:标签内容是结构化的(订单号、料号、条码、二维码),就走指令流;标签上要贴复杂图形、可变长富文本、公司 logo 带灰阶,就走驱动模式。大部分仓储、产线、溯源的场景其实属于前者。
| 对比项 | 指令流模式 | 驱动模式 |
|---|---|---|
| 跨平台 | 好,Linux/安卓都能发 | 差,基本绑 Windows |
| 依赖 | 只要网络通 | 要装驱动 + 动态库 |
| 排版能力 | 受指令集限制 | 接近普通文档打印 |
| 多打印机 | 换 IP 即可 | 每台建一个队列 |
| 调试难度 | 指令可见,好排查 | 出错信息藏在打印队列里 |
| 性能 | 高,几乎无中间层 | 一般,要过 spooler |
2.3 先把第一张标签打出来
别急着写业务代码,先确认“能打”这件事。网络打印机通常监听 9100 端口走 RAW 协议,用一段二十行的脚本就能验证整条链路通不通:
import socket PRINTER_IP = "192.168.1.50" # 打印机实际 IP,自检页上能看到 PRINTER_PORT = 9100 # RAW 打印端口,绝大多数网络机型默认 # 最小可用指令集:尺寸 -> 间隙 -> 方向 -> 清缓存 -> 画字 -> 出纸 cmds = ( "SIZE 60 mm,40 mm\r\n" # 标签纸宽 60mm、高 40mm "GAP 2 mm,0 mm\r\n" # 间隙纸,间隙 2mm,偏移 0 "DIRECTION 1\r\n" # 出纸方向,0 和 1 差 180 度 "CLS\r\n" # 清空图像缓冲区,防止上一张残留 'TEXT 20,20,"3",0,1,1,"HELLO"\r\n' # x=20,y=20,字体3,不旋转,1倍 "PRINT 1,1\r\n" # 打印 1 份,每份 1 张 ) with socket.create_connection((PRINTER_IP, PRINTER_PORT), timeout=5) as s: s.sendall(cmds.encode("ascii")) print("指令已发送")这段代码里每一个参数都不是随便填的。SIZE决定打印机的成像宽度,写小了右边内容会被裁掉,写大了会浪费碳带;GAP写错是最常见的“打三张偏一张”;DIRECTION决定你看到的标签是正的还是倒的;CLS一定要加,不加的话上一张没清掉的图会叠到下一张上,尤其是循环打标签时特别明显。指令之间用\r\n分隔,用\n在某些固件上会解析失败。
如果这张标签没出来,按这个顺序查:打印机自检页打一张,确认 IP 和型号;telnet IP 9100或nc -vz IP 9100看端口通不通;打印机的介质设置从“标签”改成“连续纸”再改回来,强制它重新测纸;最后才是怀疑指令。我见过太多次是 GAP 校准没做,机器一直以为自己没纸。
3. TSPL2 指令拆解:一条指令对应标签上的什么
3.1 坐标系用的是 dot,不是毫米
这是新手第一个大坑。手册上写SIZE 60 mm,40 mm可以带单位,但TEXT、BARCODE、QRCODE里的 x、y 坐标全是dot(点),不是毫米。203dpi 的机器一毫米约 8 个点,300dpi 约 11.8 个点。你在 203dpi 机器上按毫米填坐标,内容会全部挤到左上角。
我习惯在代码里统一用毫米思考,进函数前换算成 dot:
def mm2dot(mm: float, dpi: int = 203) -> int: """毫米转打印机点,dpi 决定换算比例:203dpi≈8dot/mm,300dpi≈11.8dot/mm""" return int(round(mm * dpi / 25.4)) print(mm2dot(10)) # 203dpi 下约 80 dot print(mm2dot(10, 300)) # 300dpi 下约 118 dot换机器的时候把dpi改掉,所有坐标自动跟着走。这个函数看着简单,但少了它,203 和 300 两种机器混用时,标签内容会整体缩到左边三分之一,调起来非常折磨。买机器的批次不统一,是项目里很常见的情况。
3.2 文字、一维码、二维码的指令写法
三条核心指令,参数顺序手册上写得清楚,但有几个位置容易填错。TEXT的格式是TEXT x,y,"字体",旋转角度,x方向倍数,y方向倍数,"内容";BARCODE是BARCODE x,y,"码制",高度,是否显示可读字符,旋转,窄条宽,宽条比,"内容";QRCODE是QRCODE x,y,纠错等级,模块宽度,模式,旋转,"内容"。
下面是我常用的一段完整标签指令,把文字、128 码和二维码放一起:
label = ( "SIZE 60 mm,40 mm\r\n" "GAP 2 mm,0 mm\r\n" "DIRECTION 1\r\n" "CLS\r\n" # 标题文字:字体 3 是内置矢量字,缩放 1 倍,左上角起 'TEXT 16,16,"3",0,1,1,"MATERIAL TAG"\r\n' # Code128 一维码:高度 60dot,可读字符开,窄条宽 2dot 'BARCODE 16,60,"128",60,1,0,2,2,"SN20240513001"\r\n' # 二维码:纠错等级 M,模块宽 3dot,自动模式 'QRCODE 16,150,M,3,A,0,"SN20240513001|LOT-A7"\r\n' "PRINT 1,1\r\n" )逐项说下容易踩的地方。一维码的“窄条宽”给 1 或 2 时看着很省地方,但仓库里拿手持枪隔半米扫,2 以下的识别率会明显掉,我一般给 2 到 3,机器是 300dpi 的话给 3 到 4。高度 60 dot 大约是 7.5 毫米,够扫码枪读,但如果是传送带上高速动态扫描,得给到 100 dot 以上。“是否显示可读字符”这个开关,产线上建议开,工人肉眼能核对一部分,出问题时也好定位。
二维码这边,纠错等级M是工业标签上比较稳的选择,L省空间但脏一点就读不出,Q、H更抗污但同样内容码会变大,小标签上可能放不下。模块宽度给 2 到 3 dot,低于 2 打印头的点阵就拼不出清晰的黑白边界了,扫不出来十有八九是这个值给太小。内容里带竖线|做分隔是常见的做法,扫码端自己拆。
注意:指令手册里码制名称是带引号的字符串,比如
"128"、"EAN13"、"CODE39"。写成数字不加引号,部分固件会静默忽略整条 BARCODE,标签上那一片就是空白,也不报错。
3.3 位图和图片:先把图转成单色位图
标签上要打公司 logo 或产品图,就要走BITMAP指令。它接收的是单色位图数据流,不是 PNG/JPG 文件本身,而且每行宽度要按字节对齐。很多人在这一步卡住,是因为直接把图片文件的二进制塞给打印机了。
资源包里的示例代码通常会用 SDK 的图片函数处理,但指令流模式下,我一般自己在服务端把图处理好再发:
from PIL import Image def img_to_bitmap(path: str) -> tuple[int, int, bytes]: """把图片转成 BITMAP 指令需要的单色数据,返回 (字节宽度, 高度, 数据)""" img = Image.open(path).convert("1") # 二值化,非黑即白 w, h = img.size width_bytes = (w + 7) // 8 # 每行按字节对齐,向上取整 buf = bytearray() for y in range(h): row = bytearray() for x in range(w): # convert("1") 后 1 为白 0 为黑,BITMAP 指令里 1 表示打点 row.append(0 if img.getpixel((x, y)) else 1) # 补齐到字节边界,不足位补 0 while len(row) % 8: row.append(0) for i in range(0, len(row), 8): byte = 0 for b in row[i:i + 8]: byte = (byte << 1) | b buf.append(byte) return width_bytes, h, bytes(buf)convert("1")这一步做了抖动处理,效果取决于原图对比度,纯色 logo 一般没问题,带渐变的图会变成一团麻点,最好先在设计软件里处理成黑白再放进来。width_bytes和h要原样填进BITMAP x,y,width_bytes,height,mode,data,其中mode填 0 表示覆盖,填 1 表示与已有内容做逻辑运算,贴图一般用 0。数据部分直接拼在指令后面,所以整条指令不能按字符串处理,得用 bytes 拼包。
3.4 把发送逻辑封装成一个可靠函数
直接把sendall散在业务代码里,出问题最难查。我一般封一个发送函数,把超时、分块、编码三件事收在一处:
import socket def send_tspl(ip: str, payload: str, port: int = 9100, timeout: float = 5.0): """发送 TSPL2 指令,返回是否成功送达""" data = payload.encode("gb18030", errors="replace") # 中文按 GB 编码 try: with socket.create_connection((ip, port), timeout=timeout) as s: # 大位图可能几 MB,按 8KB 分块发,避免打印机缓冲区溢出 for i in range(0, len(data), 8192): s.sendall(data[i:i + 8192]) return True except (socket.timeout, ConnectionError) as e: print(f"打印发送失败 {ip}: {e}") return False编码这块有讲究:中文内容如果打印机没装 Unicode 字体,用 UTF-8 发过去就是乱码,得用gb18030。这个参数我在不同固件上试过好几次,保险的做法是机器上先用工具把字体下装好,代码里固定gb18030。分块发送是因为一张带位图的标签指令可能到几 MB,一次性sendall在部分老机型上会撑爆接收缓冲,表现为“打印一半卡住”。
函数返回布尔值,业务层就能据此决定要不要重试、要不要把任务挂起。打印这件事不能假设一定成功,网线被拔、打印机断电、纸用完了,都会让标签悄悄消失。
4. 上层封装:模板化、队列和状态回读
4.1 模板不要写死在代码里
业务一变就改代码发版,是最难受的维护方式。我的做法是把标签模板存成带占位符的字符串,放数据库或者配置文件里,打印时做一次字段替换:
TEMPLATE = ( "SIZE 60 mm,40 mm\r\nGAP 2 mm,0 mm\r\nDIRECTION 1\r\nCLS\r\n" 'TEXT 16,16,"3",0,1,1,"{title}"\r\n' 'BARCODE 16,60,"128",60,1,0,2,2,"{sn}"\r\n' 'QRCODE 16,150,M,3,A,0,"{sn}|{lot}"\r\n' "PRINT {copies},1\r\n" ) def render(tpl: str, data: dict) -> str: """安全替换占位符,缺字段直接抛错,避免打出半张空标签""" for key in ("title", "sn", "lot", "copies"): if key not in data: raise ValueError(f"模板字段缺失: {key}") return tpl.format(**data)PRINT m,n里 m 是份数、n 是每份张数,做成占位符之后,补打几张不用改代码。字段缺失时宁可抛错也不要打半张空标签,这是血泪经验:空标签贴到货上比不贴更麻烦,仓库得回头一张张翻。
模板存数据库还有个附加好处,不同客户、不同物料可以用不同的标签版式,运营在后台配一下就行,不用开发介入。模板字段我一般加个version,改版之后保留旧版本,出问题能快速回滚。
4.2 多台打印机怎么排队
一个仓库往往好几台打印机,分布在不同的打包台。我的做法是按打印机建若干个队列,每个队列一个消费线程,业务侧只负责“把任务投到某台机器的队列里”,不关心它什么时候打:
import queue, threading class PrinterWorker(threading.Thread): def __init__(self, ip: str): super().__init__(daemon=True) self.ip = ip self.q = queue.Queue(maxsize=500) # 限长,防止打印机离线时内存涨爆 def submit(self, payload: str): self.q.put(payload) # 队列满会抛 Full,业务层捕获后转人工 def run(self): while True: payload = self.q.get() for attempt in range(3): # 失败重试 3 次,间隔递增 if send_tspl(self.ip, payload, timeout=5): break threading.Event().wait(0.5 * (attempt + 1)) self.q.task_done()队列要限长,这一点很多人不做。打印机断电的时候,任务还在不停往里塞,内存一路涨上去,最后整个服务被拖垮。限长之后满了就抛异常,业务层提示“打印机异常,请转人工”,比默默堆积好得多。重试次数也不要给太多,标签是有时效的,重试五次打出来的标签可能已经过期了。
4.3 状态回读:别假设打印机一定打出来了
指令发出去只是“送达”,不等于“打出来了”。要确认结果,得读打印机状态。常见做法是发~HS指令,打印机返回一串以逗号分隔的状态字段,涵盖纸张、碳带、暂停、错误标志这些信息;~!?可以查型号,~!I查固件信息。字段顺序各型号略有差异,以手册为准。
需要注意,~HS的返回是从打印机的双向通道回来的,走 RAW 9100 时部分机型不回数据,这时候就得靠驱动模式或者换用带双向能力的端口。我在项目里一般做两件事:一是每次批量打印前先发一次状态查询,机器处于错误态就提前告警;二是打完最后一张后等一秒再查一次,确认没有卡纸。这两步加起来不到二十行代码,但能把“标签没出来却没人知道”这类问题挡在仓库外面。
5. 避坑与排查:五个让我返工的细节
5.1 中文全变成问号或方块
现象:英文数字正常,中文位置全是?或者一片空白。原因:编码不对,或者打印机里没有对应的中文字体。用 UTF-8 发指令,固件按 GB 解码,就是乱码;字号用的是内置点阵字,本身就不含中文点阵。解决:发送端统一encode("gb18030");中文改用 TrueType 字体,先通过工具把字体下装到打印机,指令里引用字体名。下装一次长期有效,但换机器要重下,部署脚本里记得带上这一步。
5.2 二维码肉眼很清楚,扫码枪就是读不出
现象:标签上二维码看着方正清晰,手持枪贴近了也识别不了。原因:模块宽度太小,热敏打印头的点扩散把相邻模块糊成一片;或者二维码周围留白不够,被边框、文字挤掉了静区。解决:模块宽度提到 3 dot 起步,203dpi 的机器上尤其明显;二维码四周至少留 4 个模块的空白,坐标算的时候把这块位置预留出来;纠错等级从 L 提到 M。改完先拿实际要用的那把扫码枪试,不同枪的识读能力差别很大。
5.3 打三五张就偏一点,越打越歪
现象:前几张位置正常,打十几张之后内容整体往上或往下跑。原因:介质类型设置和实际纸张不匹配,机器按连续纸在走,没有按间隙定位;或者 GAP 校准值是旧的,换了纸卷没重新测。解决:先做一次测纸(多数机型是关机按住走纸键开机,等指示灯闪几次松手),让机器重新学习间隙位置;指令里GAP 2 mm,0 mm的数值要和实际纸张对齐;换纸卷批次之后重新测一次。这个坑我吃过两次,第二次是因为换了供应商,纸的间隙厚度变了。
5.4 服务里打印时好时坏,日志看不出来
现象:同样的代码,有时候一次成功,有时候超时,重试又能成功。原因:网络打印机响应慢,5 秒超时不够;或者多线程共用一个 socket 连接,指令互相穿插,打印机收到半条指令。解决:超时给到 8 到 10 秒;每次发送独立建连,不要在多个线程间共享 socket;发送前加锁或者干脆用队列串行化。指令流是纯文本协议,两条指令交错在一起,打印机会整段丢弃,表现为“什么都没打”。
5.5 批量打印时丢标签
现象:一次投 200 个任务,实际只出来 190 张,日志里没有报错。原因:连续快速发送时打印机缓冲区被填满,TCP 层面送达了,打印机内部丢包;或者队列没做背压,业务侧投得太快。解决:两个 PRINT 批次之间加几十毫秒间隔;队列消费端控制速率,一批打完再收下一批;关键业务在打印后回读状态确认。这个过程没法做到百分百可靠,所以我在业务上留了“补打”入口,扫码发现缺号能立刻重打一张。
6. 二次开发改造:让打印服务长在业务系统里
真正落地的时候,我会把打印从“一个工具函数”提升为“一个有状态的服务”。核心改动是三张表:模板表存指令模板和版本,打印机表存 IP、型号、dpi、所属工位,任务表存每一次打印的 payload、目标打印机、状态和重试次数。任务落库之后再进内存队列,服务重启时能从库里把未完成的任务捞回来重新投递,避免重启丢单。
这套结构带来的最大好处是可追溯。出了问题不用猜,直接查任务表:这条标签什么时候投的、发给了哪台机器、重试了几次、状态回读是什么。我在一个产线项目里就是靠这张表定位到,某台机器的 dpi 配置被人改成了 300,而实际是 203 的机器,所有坐标都偏了三分之一。
验证改造效果,我一般跑三个用例:单张正常打印、批量 200 张不丢号、拔掉网线后任务能挂起并在恢复后补打。第三个用例最能暴露设计缺陷,很多实现压根没考虑过打印机离线的情况。
| 改造点 | 改之前 | 改之后 |
|---|---|---|
| 模板 | 写死在代码里 | 存库 + 版本号 |
| 打印触发 | 同步调用,阻塞业务 | 落库 + 异步队列 |
| 失败处理 | 打日志,没人看 | 状态回读 + 补打入口 |
| 多打印机 | 配置项硬编码 IP | 打印机表按工位路由 |
| 排查手段 | 翻应用日志 | 查任务表 + 重放 payload |
从那以后我每次接标签打印的需求,都强制先跑一遍“拔网线”的测试用例,再谈功能开发。这台机器不联网也能把任务存住,是底线,不是加分项。希望帮到你。
本文还有配套的精品资源,点击获取