news 2026/9/27 1:56:41

Zebra ZD888无驱动IP打印实战:ZPL指令与中文打印方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zebra ZD888无驱动IP打印实战:ZPL指令与中文打印方案

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 一下打印机 IP:

ping 192.168.1.201 -t

能正常返回时间说明打印机在网络里在线。但 ping 通只代表 IP 层通,ZPL 打印走的是 TCP 9100 端口,所以还得验证这个端口是否开放。Windows 下用 PowerShell 执行:

Test-NetConnection 192.168.1.201 -Port 9100

如果输出里TcpTestSucceeded : True,说明端口通了。Linux 下可以用 nc:

nc -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,data

ZD888 是 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 生成中文字图并转换成 ZPL:

from PIL import Image, ImageDraw, ImageFont def text_to_bitmap(text, font_path, font_size=48): # 先量出文字实际占用的矩形区域 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, font=font) 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, font=font, fill=0) 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,黑色为1;255是白色,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:i+8], 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.ttc,Linux 下可以用系统中的中文字体,比如/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 端口发送 ZPL

Python 是最快的落地方式。核心就是创建一个 socket 连接打印机的 9100 端口,发送 ZPL 字节流。下面这段是我在产线上稳定跑过的函数:

import socket def send_zpl(ip, zpl, port=9100): data = zpl.encode("ascii") with socket.create_connection((ip, port), timeout=5) as s: s.sendall(data) print(f"sent {len(data)} bytes to {ip}:{port}")

发送时可以按作业为单位建立连接。一次连接只发送一个^XA...^XZ作业,发完就关闭。很多人担心这样效率低,实际测试下来,一个标签数据通常只有几 KB,TCP 连接建立的开销几乎可以忽略。更重要的是,按作业断开连接能避免多个任务的数据在打印机缓冲区里交错,尤其是图像数据特别长时,一旦并发写入,标签容易互相污染。

如果 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 驱动折磨过,不妨试一下这条最简单的路径。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 1:56:08

XL4015可调电源实战评测:24V转5V满载5A散热方案全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:55:58

数学建模多元回归完整流程:从变量筛选到稳健性检验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:55:36

VBA接入豆包API,实现Word/WPS一键AI润色与翻译

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:55:36

焊接件表面缺陷检测:数据集解压到YOLOv8训练全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:53:40

Linux下FUSB302 PD快充驱动调试实战:从I2C到TCPM全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:53:18

基于STM32H750与AD7606的16通道同步采样数据采集系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华