1. 先说结论ZPL无驱动打印解决了什么问题先交代一下背景。我接触 Zebra ZD888 是因为仓库里需要大量打印物料标签走的是热敏纸方案。第一次部署时最痛苦的不是标签内容怎么做而是每换一台电脑就得装一遍驱动。Windows 更新偶尔会把驱动状态搞坏仓库里那台老旧的 Windows 工控机还经常弹“驱动程序无法启动”。后来我索性不走驱动直接通过网线把打印机接到交换机上用 ZPL 指令往打印机的 9100 端口塞数据标签照样一张张打出来。从那时起这套“无驱动 IP 打印”方案就成了我处理 Zebra 打印的首选。无驱动 IP 打印说白了就是打印机是一台带网口的设备自己会解析 ZPL 语言不需要电脑端安装打印驱动。你只要知道打印机的 IP就能像发 HTTP 请求一样把一段纯文本指令发到特定端口打印机收到后立刻执行打印。这对跨平台场景尤其友好Windows、Linux、安卓 PDA、嵌入式终端都能用同一套代码。非常适合产线、仓库、医院标签、商超价格签这类要维护很多台终端的环境。那 ZPL 又是什么它是 Zebra 打印机自己的一套打印编程语言打开一个空白标签、定位、打印文字、画条码、关闭标签全靠几个字母组合完成。下面这段是最小的 ZPL 指令^XA ^FO50,50^A0N,32,32^FDHello ZD888^FS ^XZ^XA表示一个打印作业开始^FO50,50把打印起点定位在距离标签左上角 (50,50) 的位置^A0N,32,32设置字体^FD后面是要打印的内容^FS表示该字段结束最后的^XZ告诉打印机“这个作业结束开打”。这套语法看起来简单但组合起来能实现绝大多数标签需求这也是我为什么极力推荐用 ZPL 而不是依赖驱动的原因。2. 动手前必须做好的准备IP 与端口2.1 给 ZD888 设置固定 IP 的几种方式想要实现无驱动 IP 打印第一步是把打印机接到局域网并给打印机设置一个固定的 IP。ZD888 不带触摸屏但获取和修改 IP 并不麻烦。最快的办法是打印一张打印机配置标签。不同批次的 ZD888 按键布局略不一样但大部分支持这个操作先关闭打印机电源按住 Feed 键再开机等指示灯开始闪烁后松开打印机会自动打印出“Configuration Report”。这张标签上有一个 Network 区域里面写着当前 IP 地址、子网掩码、网关等信息。如果打印机已经联网就从这个标签上拿到当前 IP。设置静态 IP 我一般用两种方式。第一种是 Zebra Setup Utilities这是 Zebra 官方提供的一体化配置工具。用 USB 线把打印机接到电脑打开软件选中对应的打印机进入“网络配置”选项卡把“IP 协议”改成静态填入你规划的 IP、子网掩码、网关写好后点应用打印机重启就生效。第二种是浏览器方式前提是打印机已经联网直接用浏览器访问打印机的当前 IP会进入打印机的内置管理页面在 Network 菜单里同样可以设置静态 IP。不过 ZD888 的网口分版本出厂没有网口的型号需要另配网络打印服务器。确认硬件支持网口后再继续往下做。提示静态 IP 一定要避开路由器 DHCP 地址池否则后面会有 IP 冲突的隐患。比如路由器自动分配范围是 192.168.1.100-200那打印机就设置在 192.168.1.201 或更靠后的地址。2.2 用 ping 和 9100 端口确认打印机在线IP 设置好后先别急着发指令确认网络通了再说。最简单的验证是在电脑上 ping 一下打印机 IPping 192.168.1.201 -t能正常返回时间说明打印机在网络里在线。但 ping 通只代表 IP 层通ZPL 打印走的是 TCP 9100 端口所以还得验证这个端口是否开放。Windows 下用 PowerShell 执行Test-NetConnection 192.168.1.201 -Port 9100如果输出里TcpTestSucceeded : True说明端口通了。Linux 下可以用 ncnc -vz 192.168.1.201 9100端口不通的时候首先查打印机自己是否开启了 Port 9100 服务。个别型号在打印机面板或配置页里有一个“端口”相关设置确认没被关闭。其次看 Windows 防火墙尤其是工控机装了第三方安全软件时经常会把本机发出的 TCP 连接拦掉。实际上Zebra 9100 端口默认是开启的我遇到端口不通大多是因为打印机的“睡眠模式”或者网络配置没保存成功重启打印机再试一般能恢复。3. ZPL 指令核心拆解条码、坐标与中文3.1 最小可用的 ZPL 模板与常用命令ZPL 命令看着很长但常用的就那几个。我做了个表方便你对照命令作用示例^XA开始一个打印作业^XA^XZ结束并打印^XZ^LH设置标签原点^LH0,0^FO字段左上角坐标^FO50,50^FD定义字段数据^FDABC-123^FS^FS结束当前字段^FS^A选择字体^A0N,32,32^BCCode128 条码^BCN,80,Y,N^BQ二维码^BQN,2,6^BY条码单元宽度与比例^BY2,3,100^GF图形数据^GFA,total,total,row,dataZD888 是 203dpi 打印机也就是每英寸 203 点换算成毫米大概是每毫米 8 点。设计坐标时如果习惯用毫米可以直接乘 8。例如想放到距离左边 20mm、上边 10mm 的位置对应的坐标就是^FO160,80。一条实际生产用的条码指令通常长这样^XA ^LH0,0 ^FO40,20^A0N,32,32^FD物料编号^FS ^FO40,80^BY2,3,80^BCN,80,Y,N,N^FD1234567890^FS ^XZ这里^BY2,3,80表示条码窄条宽度为 2 点宽窄比为 3:1条码高度 80 点。^BCN,80,Y,N,N的意思是从上到下依次是Code128 标准模式、高度、是否打印可读字符、是否打印校验位、是否把起始字符打印出来。刚开始不用硬记参数顺序只要知道哪个位置是高度、哪个位置是可读字符设计时多用实际打印验证就能试出来。3.2 让 ZD888 打出中文图片方式最稳“zpl 打印中文”是个热词也是很多人第一次用 ZPL 就会被卡住的地方。ZD888 出厂固件里通常没有完整的中文字库所以直接发这一段^XA^FO50,50^A0N,32,32^FD你好世界^FS^XZ大概率打出来是空白的或者一排在标签上显示为黑块。原因很简单打印机不认识这些字符对应的字形。网上有方案说用^CI28选择 UTF-8 编码就能打中文这个方法需要打印机固件内置对应字库我在 ZD888 上试过并不是所有批次都能成功。也有方案说下载一个中文字体到打印机内存然后通过^A调用字体这样做理论可行但字体文件很大ZD888 的内存小刷新固件或者恢复出厂设置后字体容易丢维护成本高。所以我自己最终采用的是把中文渲染成单色位图再用 ZPL 的^GF命令打印。原理是先通过程序把文字画成一张黑白图片黑点代表需要打印的墨点白点留空然后把图片的每一个像素转换成十六进制字符串塞进^GF指令。打印机收到的是纯 ASCII 字符不存在编码问题所以中文能稳定打印而且字体样式可控。这里给一段 Python 代码用 Pillow 生成中文字图并转换成 ZPLfrom PIL import Image, ImageDraw, ImageFont def text_to_bitmap(text, font_path, font_size48): # 先量出文字实际占用的矩形区域 font ImageFont.truetype(font_path, font_size) tmp Image.new(1, (10, 10), 255) tmp_draw ImageDraw.Draw(tmp) bbox tmp_draw.textbbox((0, 0), text, fontfont) width bbox[2] - bbox[0] 4 height bbox[3] - bbox[1] 4 # 重新创建与文字尺寸匹配的画布 img Image.new(1, (width, height), 255) draw ImageDraw.Draw(img) draw.text((2 - bbox[0], 2 - bbox[1]), text, fontfont, fill0) return img def bitmap_to_zpl(img, x, y): width, height img.size bytes_per_row (width 7) // 8 hex_data for py in range(height): row_bits for px in range(width): # 图像中白色为0黑色为1255是白色0是黑色 row_bits 0 if img.getpixel((px, py)) 255 else 1 # 凑整到8的倍数 row_bits 0 * ((8 - len(row_bits) % 8) % 8) for i in range(0, len(row_bits), 8): hex_data %02X % int(row_bits[i:i8], 2) total_bytes bytes_per_row * height return f^XA^FO{x},{y}^GFA,{total_bytes},{total_bytes},{bytes_per_row},{hex_data}^FS^XZ使用时要指定一个带中文字符的 ttf 字体文件Windows 下可以用C:/Windows/Fonts/msyh.ttcLinux 下可以用系统中的中文字体比如/usr/share/fonts/truetype/wqy/wqy-microhei.ttc。调用方式img text_to_bitmap(仓库A-01, C:/Windows/Fonts/msyh.ttc, 48) zpl bitmap_to_zpl(img, 80, 100) print(zpl)生成的 ZPL 会是一长串十六进制数据直接发到打印机就能打印出中文。^GF的前两个参数是图像数据总字节数第三个参数是每行字节数这几个参数必须和实际图像数据严格对应差一个字节打印机会直接报错或者打印出乱码。我见过很多人改了几个像素之后忘了重新计算 total图像就坏了所以我建议把生成和计算封装成同一个函数不要手动维护。3.3 一张综合二维码标签的完整 ZPL 示例把前面的内容串起来一个包含文字、Code128 条码、二维码的标签 ZPL 大概是这样的^XA ^LH0,0 ^FO40,30^A0N,36,36^FDASSET-001^FS ^FO40,90^BY2,3,80^BCN,80,Y,N,N^FD1234567890^FS ^FO40,200^BQN,2,6^FDhttps://example.com/asset/001^FS ^XZ其中^BQN,2,6是二维码指令N 表示普通模式2 是纠错级别对应约 25% 容错6 是二维码放大倍数。需要打中文内容时就把^FD那行替换成^GF生成的图片段。我会把标签里的固定部分做成模板动态内容用字符串替换模板放配置文件里程序启动时读一次后续打印效率很高。4. 无驱动 IP 打印的代码落地4.1 用 Python 通过 9100 端口发送 ZPLPython 是最快的落地方式。核心就是创建一个 socket 连接打印机的 9100 端口发送 ZPL 字节流。下面这段是我在产线上稳定跑过的函数import socket def send_zpl(ip, zpl, port9100): data zpl.encode(ascii) with socket.create_connection((ip, port), timeout5) as s: s.sendall(data) print(fsent {len(data)} bytes to {ip}:{port})发送时可以按作业为单位建立连接。一次连接只发送一个^XA...^XZ作业发完就关闭。很多人担心这样效率低实际测试下来一个标签数据通常只有几 KBTCP 连接建立的开销几乎可以忽略。更重要的是按作业断开连接能避免多个任务的数据在打印机缓冲区里交错尤其是图像数据特别长时一旦并发写入标签容易互相污染。如果 ZPL 里含中文文本并且你的打印机固件支持 UTF-8 中文要改用zpl.encode(utf-8)。但按前面说的ZD888 上我推荐图像方案所以最终发送的 ZPL 仍然是 ASCII 编码。4.2 用 C# 和 PowerShell 做同样的事C# 在 Windows 工控机、MES 客户端里用得很多。用TcpClient发送using System.Net.Sockets; using System.Text; string ip 192.168.1.201; string zpl ^XA^FO50,50^A0N,32,32^FDHello C#^FS^XZ; using var tcp new TcpClient(); await tcp.ConnectAsync(ip, 9100); byte[] data Encoding.ASCII.GetBytes(zpl); await tcp.GetStream().WriteAsync(data, 0, data.Length); await tcp.GetStream().FlushAsync();PowerShell 适合快速测试和运维脚本不需要编译$ip 192.168.1.201 $zpl ^XA^FO50,50^A0N,32,32^FDHello PowerShell^FS^XZ $tcp New-Object System.Net.Sockets.TcpClient $tcp.Connect($ip, 9100) $stream $tcp.GetStream() $bytes [System.Text.Encoding]::ASCII.GetBytes($zpl) $stream.Write($bytes, 0, $bytes.Length) $stream.Flush() $stream.Close() $tcp.Close()发送完最好调用Flush再关闭不然某些语言里缓冲区没写完就把连接关了标签内容会被截断。C# 的TcpClient在Dispose时一般会自动处理但显式 Flush 是更稳妥的习惯。4.3 如何在业务系统里集成这种打印方式这种打印方式最适合作为独立的打印服务使用。业务系统把标签数据拼装成 ZPL 字符串提交给一个内部打印服务打印服务负责连接打印机、处理重试、记录日志。前端浏览器完全不需要装任何驱动也不需要调用本地打印对话框打印动作全部发生在后端。我做过一个仓储项目Web 发货页面点一下“打印”后端 Java 服务直接构造 ZPL通过 socket 发送给仓库里的 ZD888。因为打印指令和业务数据是同一套系统换仓库、换打印机时只要改 IP 配置不用动前端代码。如果有多台打印机甚至可以做简单的负载均衡比如按货道、按区域把不同标签路由到不同打印机。需要注意并发问题。如果多个线程同时向同一台打印机的 9100 端口发数据打印机固件虽然会缓冲但大量数据同时进去依然可能互相穿插。我后来在打印服务层加了一个按打印机 IP 维度的锁同一台打印机同一时刻只允许一个发送任务。这样牺牲了一点吞吐量但标签正确率是 100%。5. 现场经常翻车的几个问题与排查记录5.1 中文打出来是方框或乱码中文打印异常我总结基本是三类原因。第一是打印机没有中文字库。直接发^FD中文^FS打印机可能只输出一个空区域或方框。前面已经讲了用^GF图片方式最省事。第二是发送端编码与打印机期望的编码不一致。如果在 Windows 下用 GBK 编码生成 ZPL而打印机固件按 UTF-8 去解码中文字节就会被拆成奇怪的字符。所以只要走文本方式必须保证两边编码完全一致。第三是文件保存时带了 BOM 头尤其用记事本保存的脚本第一行可能混入 BOM打印出来的内容最前面多一个黑点或者出现“锘挎”这种乱码。用 VS Code 或现代编辑器保存成 UTF-8 无 BOM 格式。5.2 数据通了却不打印或卡纸如果你确认 TCP 端口已经通也成功发送了数据打印机就是不动先检查打印机是不是处于暂停状态。ZD888 机身上的按键可以控制暂停官方工具里也能看状态。打印机暂停时数据发过去会被缓冲但不是不接收只是不执行打印。按一下暂停键一般就恢复了。还有一个常见问题是标签间隙没被识别。打印机一直认为纸没到正确位置所以标签一进一出不出内容。这时候需要重新校准介质在软件的“介质校准”里执行或者按照说明书的按键组合操作。换了一卷不同规格的标签纸后我强烈建议重新校准一次。如果发送的图像数据不完整也可能出现打印头动了但标签上只有一半内容。原因是^GF的 total_bytes、bytesPerRow 和实际数据长度对不上。可以在代码里加一个断言确保十六进制字符串长度等于 total_bytes 乘以 2。5.3 IP 地址变了导致所有客户端集体失效这是无驱动打印最容易被坑的地方。打印机默认是 DHCP 获取 IP路由器一旦重启分配的地址可能变化所有终端里写死的旧 IP 就全部失效。那天仓库里十台电脑打印全部失败最后发现是打印机从 .201 变成了 .188。解决办法很简单就是在路由器里给打印机做 DHCP 地址保留。你先把打印机的 MAC 地址记下来然后在路由器后台 DHCP 设置中把这个 MAC 绑定到固定 IP。这样打印机即便断电重连拿到的还是同一个地址。另一种办法是彻底把打印机的网络模式改成 Static手动填 IP。两种办法我都用过更推荐在路由器里做保留因为万一以后要改打印机地址不需要一台一台去登录打印机设置直接改路由器就行。6. 最后分享一个我常用的调试技巧调试 ZPL 时我一般是先用 Labelary 这个在线工具做预览。它的地址很好记打开后把 ZPL 粘贴进去右侧立即生成标签预览图能直接看到坐标是否越界、条码是否生成、二维码内容是否正确。在真机上反复试错太废标签纸先用在线预览把指令调通确认效果后再发到 ZD888效率能提升一大截。还有一个经验是正式环境里尽量把标签模板做成静态文件而不是在代码里拼字符串。比如把没数据的 ZPL 模板单独存成.zpl文件动态内容用__TOKEN__这种占位符标记程序运行时只替换占位符。这样后续要调整标签格式直接改模板文件不用重新编译。而且模板文件可以用 Git 管理谁改了什么一目了然。我个人在实际操作中最大的体会是无驱动 IP 打印不是高深技术但它在部署和维护上的收益非常明显。只要把 IP 规划好、中文字体问题提前用图片方案规避后面基本不会再有乱七八糟的问题。如果你也被 Zebra 驱动折磨过不妨试一下这条最简单的路径。
