云打印机 MQTT 直连实战:完整字段表、JSON / XML 排版与防重打设计(附 Python 示例)
大多数商用云打印机只有「设备 → 厂商云 → 你的系统」一条路,数据必须经过厂商。本文给出一条可私有化的路径:设备直连你自有的 MQTT Broker,打印内容全程不落厂商云。含官方通信协议(v2025-11-10)的完整下行字段表、XML 与 JSON 两套排版示例、14 项上报状态码、
id防重打设计,以及 Broker 选型与接入要点。
一、为什么需要「直连」
市面上商用云打印机的默认架构是云中转:
打印机 ←→ 厂商云 ←→ 你的 SaaS / ERP这条链路的问题不在功能,在合规与自主权:
- 金融 / 医疗 / 政务客户的打印内容(单据、处方、凭条)不得落到第三方服务器;
- 你的业务系统里,设备是厂商的资产—— 换供应商等于换掉全部设备;
- 厂商云故障 = 你的全部门店停摆。
MQTT 直连把中间那一跳去掉:
打印机 ←→ 你自有的 MQTT Broker设备只认 Broker 地址与凭证,打印内容不出你的服务器。协议规范(MQTT 直连 / HTTP / 自定义指令)计划于 2026-10 以 Apache 2.0 开放,届时你可以自行实现 Broker 端,也可以在多家供应商之间迁移而不必换设备。
云中转与直连不是二选一 —— 成熟的方案两套都支持,按客户合规等级切换。该直连方案目前已有国内与海外用户商用运行。
二、协议全貌:三条约定 + 一种配置方式
官方《云打印机对接第三方 MQTT 服务器通信协议》只约束三件事,其余交给部署方:
| 项 | 约定 |
|---|---|
| 可接入的服务器类型 | 阿里云 MQTT、腾讯云 MQTT、自建 MQTT、电信云等(有定制需求可另行对接) |
| QoS | 订阅主题与发布主题一律 QoS = 1 |
| Clean Session | 设备默认清除会话;若希望设备接收离线期间的消息,需在 PC 配置工具写入参数时选择「不清除 Session」 |
| 参数写入方式 | 设备开机后通过USB 数据线连接 PC,用《云打印设备配置工具》写入 Broker 地址与凭证 |
设备侧覆盖热敏打印机、标签打印机、打印云盒三类形态。
这里有一个容易误解的点:协议不定义「设备如何知道自己该连哪个 Broker」—— 因为它不是协议行为,而是出厂 / 现场配置行为:参数经 USB 写入设备。所以部署流程里必然有一步「批量写入参数」,这步用官方 PC 工具即可完成,不需要自己改固件。
三、Topic 主题:规范约定「方向」,不固化「字符串」
协议只约定主题的角色,不约定具体字符串:
- 订阅主题:接收打印任务、语音播报消息、命令消息 —— QoS = 1
- 发布主题:上报设备状态与打印结果 —— QoS = 1
也就是说sw/xxx/task/down这类字符串不属于协议定义范围,而由部署方自定义。下面给一套推荐命名方案(工程建议,非协议强制),好处是便于通配符订阅与分环境隔离:
| 推荐 Topic | 方向 | QoS | 用途 |
|---|---|---|---|
{product}/{ver}/{sn}/task | 下行 | 1 | 打印任务 |
{product}/{ver}/{sn}/voice | 下行 | 1 | 语音播报 |
{product}/{ver}/{sn}/cmd | 下行 | 1 | 配置管理命令 |
{product}/{ver}/{sn}/status | 上行 | 1 | 设备状态与打印结果 |
平台侧用+通配符订阅,避免为每台设备单独建订阅:
{product}/{ver}/+/status落地时请把主题字符串写进你的部署文档 —— 因为协议不强制,跨供应商迁移时这部分要重新约定,这是直连方案需要提前规划的一点。
四、下行:打印与语音消息字段表
服务器推送到设备的消息为 UTF-8 JSON。完整字段如下(官方 v2025-11-10 规范):
| 关键字 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | uint32 | 是 | 打印任务 ID;服务端确保不重复,用于防重复打印。设备上报结果以此 ID 标识任务。取值范围0 – 4294967295 |
type | int | 是 | 打印数据排版类型(打印语言),见下表 |
contents | String / Array / Object | 是 | 打印内容,数据格式与type保持一致 |
pWidth | int | 否 | 打印纸张宽度 mm:58 / 80 / 110,默认 58 |
pCopy | int | 否 | 打印份数,默认 1 |
pType | int | 否 | 纸张类型,默认 1;纸张为标签时必传 |
vType | int | 否 | 播报类型 |
vMessage | string | 否 | 语音播报文本(TTS 合成)或音频下载 URL |
vMode | int | 否 | 播报排队策略,默认 0 |
message | string | 否 | 预留 HTTP 下载类型的打印内容;与contents二选一,优先取contents |
gateway | string | 否 | 仅打印云盒:"usb"发往 USB 打印机;"192.168.2.11"发往指定局域网 IP 打印机 |
type取值
| 值 | 含义 | contents形态 |
|---|---|---|
| 1 | ESC(BASE64 编码后数据) | 字符串 |
| 2 | CPCL | 字符串 |
| 3 | TSPL | 字符串 |
| 4 | XML(UTF-8) | 字符串 |
| 5 | JSON(UTF-8) | Array(小票)/ Object(标签) |
| 6 | PNG(URL 链接或 base64 内容) | 字符串 |
| 99 | 打印设备配置管理 | Object |
其中type=4的 XML 又分两种设计:基础 XML兼容市面各运打印平台;矢量打印 XML是专为全球语言打印设计的排版框架。XML 与 JSON 的完整排版定义见官网文档中心:https://www.sw-aiot.com/doc.html。
pType取值:1 连续纸(默认)|2 标签纸|3 黑标纸|4 穿孔纸|8 连续纸(用标签指令打印小票)
vType取值:-1 无播报|0 定制语音(配合vMessage)|1 播报「你有新的订单」|10000 指定语音链接下载播报
vMode取值:0 按队列顺序播报,任务先插入队列(默认)|1 立即播报|2 立即播报并清空待播报队列
五、下行示例(均取自官方规范)
5.1 XML 排版(type=4)—— 58mm 小票:
{"id":12345678,"type":4,"pType":1,"pWidth":58,"pCopy":1,"vType":0,"vMessage":"测试语音","contents":"<CB>收款小票</CB><BR>商户存根 请妥善保管<BR>--------------------------------<BR>商户名称 小熊每日鲜水果店<BR>商户编号 1005800029<BR>交易时间 2020-12-11 10:23:30<BR>交易订单号 1005800012112007217<BR>支付单号 202012112200140385141256912<BR>优惠金额 5.00<BR>备注 测试小票<BR>交易状态 收款成功<BR>支付方式 支付宝-公众号支付<BR>交易金额 126.50 元<BR><BR><QR>10058001200047217</QR><BR>--------------------------------<BR>打印时间 2020-11-12 11:02:36 签名<BR>"}常用 XML 标签:<BR>换行|<CB>居中放大|<B>放大一倍|<C>居中|<L>字体变高一倍|<W>字体变宽一倍|<BOLD>加粗|<RIGHT>右对齐|<CUT>切纸|<PLUGIN>钱箱或外置音响|<IMG>PNG 图片 URL|<QR>二维码(单个订单最多打印一个)|<BC128_A>/<BC128>/<BC128_C>条码(A 为数字 + 大写字母混合,58mm 最多 14 位;C 为纯数字)
5.2 JSON 排版 · 小票(type=5,contents为 Array):
{"id":12345678,"vType":0,"vMessage":"这是测试语音","pType":1,"pWidth":58,"pCopy":1,"type":5,"contents":[{"cont":"云打印 JSON 排版","type":"title"},{"thead":{"名称":"40%","单价":"25%","数量":"15%","金额":"20%"},"tbody":[["云打印机(4G/WiFi)",100,1,100],["4G 音箱(摄像头)",999.44,1,999.44]]},{"cont":"合计:2009.1 元","size":"11","align":"left"},{"both_sides":["收款金额","258.00"]},{"both_sides":["支付方式","微信支付"]},{"cont":"1234567890","type":"qrcode","align":"center","size":"60"},{"cont":"1","type":"cut"}]}小票元素type:title(居中放大标题)|text(文本行,cont可为字符串或数组)|png(图片最大 60K)|bmp(图片最大 6K)|qrcode(最大 512 字节)|bc128(默认 CODE128A)/bc128a/bc128c|div_line(-分割线,支持居中插字)|div_star(*分割线)|cut(cont为"0"全切 /"1"半切)|plugin(开钱箱)
通用属性:align(left 默认 / center / right)|bold(true / false)|size(两位数字"AB",A 为倍宽、B 为倍高,各取 0–7)|both_sides(第一个字符串左对齐、第二个右对齐)|thead+tbody(多列表格,thead可传对象映射列宽占比,也可传数组只给占比)
5.3 JSON 排版 · 标签(type=5,contents为 Object):
{"id":12345678,"pType":2,"type":5,"contents":{"SIZE":[60,40],"DIRECTION":0,"DENSITY":7,"SOUND":3,"label":[{"type":"TEXT","x":10,"y":20,"font":"2","w":2,"h":2,"r":0,"cont":"放大 1 倍"},{"type":"QRCODE","x":10,"y":70,"e":"M","w":5,"cont":"12345678"},{"type":"BC128","x":240,"y":70,"h":30,"show":0,"r":0,"narrow":1,"wide":1,"cont":"1234567889"},{"type":"BAR","x":10,"y":300,"w":420,"h":4},{"type":"BITMAP","x":10,"y":200,"cont":"data:image/bmp;base64,..."}]}}标签顶层参数:SIZE([宽, 高] mm)|DIRECTION|DENSITY|SOUND|label(元素数组)。
标签元素类型:TEXT(x/y/font/w/h/r/cont)|QRCODE(e纠错等级、w尺寸)|BC128(h高度、show是否显示可读字符、narrow/wide条宽比)|BAR(w/h画线)|BITMAP(base64 BMP)
同一份 JSON 载荷在HTTP 推送与MQTT 直连两条通路上完全通用 —— 你可以先用 HTTP 跑通原型,量上来后切 MQTT,业务侧序列化代码一行不用改。
六、下行:远程配置管理(type=99)
设备支持云端远程设置音量、重启、切换默认指令模式:
{"id":12345678,"type":99,"contents":{"cmd":"volume","value":1}}| 命令 | 载荷 | 说明 |
|---|---|---|
volume | {"cmd":"volume","value":1} | 音量:0 静音 / 1 低音量 / 2 中音量 / 3 大音量 / 4 最大 |
reboot | {"cmd":"reboot"} | 系统重启 |
label_cmd | {"cmd":"label_cmd","print":"tspl"} | 默认标签指令,可选tspl/cpcl/zpl,默认 tspl |
print_cmd | {"cmd":"print_cmd","cmd":"tspl"} | 设置打印机默认语言,可选tspl/cpcl/esc |
七、上行:状态与打印结果上报
设备通过发布主题上报,UTF-8 JSON:
| 关键字 | 类型 | 说明 |
|---|---|---|
devicename | string | 云打印设备 SN(机器码),与三元组的 DeviceName 或 ClientID 一致 |
id | uint32 | 打印任务 ID,与服务器下发的任务 ID 一致;上报设备状态时不带此字段,注意区分 |
code | int | 结果码 |
code状态码
| code | 含义 |
|---|---|
| 0 | 打印成功 / 打印机正常 |
| 100 | 未知错误,通讯异常 |
| 101 | 打印机缺纸 |
| 102 | 打印机开盖 |
| 103 | 打印机过热 |
| 201 | 没有id |
| 202 | 没有type |
| 203 | 无效type参数(未在定义范围) |
| 204 | 没有 msg |
| 205 | 无效 msg(如链接非标准 HTTP) |
| 206 | 下载错误(重复下载 6 次) |
| 207 | 任务长度超出设备缓冲区 |
| 208 | 内容格式错误 |
| 209 | 重复任务,不执行打印(根据 ID 判断) |
三类上报样例
// 4G 机型开机连上 MQTT 服务后上报一次(imei 为 15 位数字,iccid 为 20 位卡号){"devicename":"dev1","imei":"123456789012345","iccid":"12345678901234567890"}// 设备状态上报:缺纸(不带 id){"devicename":"dev1","code":101}// 打印结果上报:任务 123456789 打印成功{"devicename":"dev1","id":123456789,"code":0}带扫码功能的产品另有一路上报:
{"devicename":"dev1","scan_content":"abcd1234"}。
八、防重打:设备是最后一道闸
自建链路最容易踩的坑是同一张单子打两遍。这套协议把防线放在两层:
- 服务端:
id由服务端保证不重复,下发前登记,落库唯一索引。 - 设备端:内置防重打 ——同一
id在不断电情况下只打印一次,重复推送时直接回报code = 209(重复任务,不执行打印)。
# 服务端:下发前登记,落库唯一索引(列名对应协议里的 id)# CREATE UNIQUE INDEX uk_mqtt_id ON print_tasks(mqtt_id);# 设备端:命中已执行表 → 不打印,回报 {"devicename":"...","id":<id>,"code":209}配套工程约定:
- 设备只在打印完成后上报结果,回报体里的
id与下发一致; - 服务端收到该
id且code = 0才把任务置为「已完成」; - 未收到回报的任务超时后重新入队并复用同一个
id(不要新建 ID)—— 即使重复到达,设备侧防重打会自动跳过。
⚠️
id是uint32,上限 4294967295。若沿用雪花 ID 之类的 64 位方案,务必先截断或做映射,否则溢出后防重打逻辑会失效。
九、鉴权与部署要点
| 等级 | 方案 | 适用 |
|---|---|---|
| L1 基础 | Username = 设备 Token,Password 可空 | 内网 / 测试 |
| L2 增强 | Password =HMAC-SHA256(device_secret, "{ts}\n{nonce}\n{device_sn}") | 公网生产 |
| L3 强安全 | mTLS 客户端证书,CN={device_sn}, O={customer_id} | 金融 / 医疗 / 政企 |
生产环境必须 TLS(8883),这条没有妥协余地。其余建议值:
- 端口:1883(TCP,仅限内网)/ 8883(TLS,生产必须);
- Keep Alive 60s;
- ClientID 建议使用设备 SN —— 规范要求
devicename与三元组的 DeviceName 或 ClientID 一致,用随机 ClientID 会与设备侧会话互相踢下线; - 单条载荷做上限控制:JSON 票据里
png≤ 60K、bmp≤ 6K、qrcode≤ 512 字节,超限会命中code = 207(超缓冲区)或208(格式错误)。大图请走type=6传 URL。
最小可运行示例(Python Paho)
importjsonimportpaho.mqtt.clientasmqtt BROKER,PORT="broker.example.com",8883DEVICE_SN="SW-03DW-ABC123"TOPIC_TASK=f"swprint/v1/{DEVICE_SN}/task"# 下行:设备订阅TOPIC_STATUS=f"swprint/v1/{DEVICE_SN}/status"# 上行:设备发布client=mqtt.Client(client_id=DEVICE_SN)client.username_pw_set("sw_dev_abc123def456","")client.tls_set(ca_certs="/path/to/ca.crt")defon_connect(c,userdata,flags,rc):ifrc!=0:returnc.subscribe(TOPIC_TASK,qos=1)# 接收打印任务 / 语音 / 命令task={"id":100001,"type":5,"pType":1,"pWidth":58,"pCopy":1,"contents":[{"cont":"深圳商为","type":"title"},{"cont":"Hello PB-03DW","type":"text","bold":True},{"cont":"https://sw-aiot.com","type":"qrcode","align":"center","size":"04"},],}c.publish(TOPIC_TASK,json.dumps(task,ensure_ascii=False),qos=1)defon_message(c,userdata,msg):# 设备侧上报:{"devicename":"...","id":100001,"code":0}print(msg.topic,msg.payload.decode("utf-8"))client.on_connect=on_connect client.on_message=on_message client.connect(BROKER,PORT,keepalive=60)client.loop_forever()Node.js 侧同理(mqtt包,mqtts://协议 +ca证书)。Go 用 Eclipse Paho,注意SetClientID(device_sn)与SetUsername(token)。
十、Broker 选型:官方支持三类服务器
协议对 Broker 无绑定,官方明确支持接入阿里云 MQTT、腾讯云 MQTT、自建 MQTT、电信云。自建一条命令即可起一个 EMQX:
# docker-compose.ymlservices:emqx:image:emqx/emqx:5.6ports:-"1883:1883"# MQTT-"8883:8883"# MQTT over TLS-"18083:18083"# Dashboardvolumes:-./data:/opt/emqx/datadocker compose up -d即可。Mosquitto / HiveMQ / AWS IoT Core 同样适用。
十一、几个常见坑
- ClientID 用随机值→ 与设备侧会话互相踢下线。规范要求
devicename与 DeviceName / ClientID 一致,ClientID 请用设备 SN。 id用 64 位自增→ 超出 uint32 上限(4294967295)后溢出,防重打逻辑失效。- 把 Topic 字符串当成协议标准→ 换 Broker 或换供应商时发现对不上。协议不固化主题,落地时务必写进部署文档。
- 图片 base64 塞进 JSON 票据→ 超过 60K 会报
207/208。大图用type=6的 URL 模式。 - Clean Session 设成 false 却没做会话恢复→ 重连后收到一堆历史消息。初期保持默认(清除会话)。
- 生产用 1883 明文→ 数据裸奔。
相关能力:本文协议内容以商为《云打印机对接第三方 MQTT 通信协议》v2025-11-10 为准(HTTP 侧 v1.0)。该 MQTT 直连方案已有国内与海外用户商用运行;协议规范计划 2026-10 以 Apache 2.0 开放。XML / JSON 完整排版定义见官网文档中心:https://www.sw-aiot.com/doc.html