一、连上了但打不出,才是真正的折磨
开发物流/快递 App 时,蓝牙打印是最容易让人崩溃的模块。
你以为最难的是搜不到蓝牙设备?其实连上设备(连接 GATT)只是第一步。真正的噩梦在于:你以为发送成功了(onCharacteristicWrite返回了 status == GATT_SUCCESS),但打印机一动不动,或者打出来是一堆乱码。
蓝牙面单打印是一个软硬结合的深水区,涉及CPCL 指令语法、中文编码机制、BLE 分包策略三大核心技术点。
二、CPCL 指令层:打印机只认“方言”
便携式面单打印机通常支持 CPCL(Comtec Printer Control Language)或 TSPL。面单通常使用 CPCL,它的核心逻辑是“画布坐标系统”。
一份标准的 CPCL 面单指令如下:
java
public String buildCpcl(Waybill waybill) { StringBuilder sb = new StringBuilder(); // 1. 页面初始化:! 0 200 200 [高度] [打印份数] sb.append("! 0 200 200 400 1\r\n"); // 2. 打印条码:BARCODE [类型] [宽度] [比例] [高度] [X坐标] [Y坐标] [内容] sb.append("BARCODE 128 2 1 80 30 30 ") .append(waybill.getTrackingNo()).append("\r\n"); // 3. 打印文字:TEXT [字体] [大小] [X坐标] [Y坐标] [内容] sb.append("TEXT 8 0 30 140 收件人: ") .append(waybill.getReceiverName()).append("\r\n"); // 4. 结束并打印:FORM + PRINT sb.append("FORM\r\n"); sb.append("PRINT\r\n"); return sb.toString(); }2.1 致命坑位一:\r\n结尾
绝对不能用\n或\r替代\r\n!CPCL 语法严格要求指令以\r\n结尾。如果只写\n,很多打印机会直接吞掉指令,导致“写入成功但不走纸”。
2.2 致命坑位二:GBK 编码
CPCL 打印机自带的字库通常是 GBK 字库。如果你用 Java 默认的 UTF-8 发送包含“张三”的字符串,打出来就是乱码。必须在发送前强转 GBK 字节流:
java
// 错误写法:byte[] data = cpclString.getBytes(StandardCharsets.UTF_8); // 正确写法: byte[] data = cpclString.getBytes("GBK");三、BLE 分包写入:不能“一口气吃成胖子”
蓝牙低功耗(BLE)的单次最大传输单元(MTU)通常是 20 字节(部分机型协商后为 512 字节)。一张面单的 CPCL 指令可能长达 1000 字节以上。如果一次性写入,必定失败或连接断开。
3.1 Android 端的分包与节流
Android 支持主动协商 MTU,协商完成后,每次写包的大小不能超过MTU - 3。且发完一包必须等待onCharacteristicWrite回调,才能发下一包,否则会冲垮打印机脆弱的蓝牙接收缓冲区。
java
public void writeCpcl(BluetoothGatt gatt, BluetoothGattCharacteristic charac, byte[] data) { int mtu = 23; // 默认 23 字节 int chunkSize = mtu - 3; // 实际可用 20 字节 for (int i = 0; i < data.length; i += chunkSize) { int end = Math.min(data.length, i + chunkSize); byte[] chunk = Arrays.copyOfRange(data, i, end); charac.setValue(chunk); gatt.writeCharacteristic(charac); // 发送 // 核心:必须等待写入回调,否则可能丢包 waitForWriteCallback(); // 如果打印机极慢,还需要 Thread.sleep(15) 额外延时 } }3.2 iOS 端的串行化
iOS 无法主动修改 MTU,必须使用withResponse模式,通过 CoreBluetooth 的didWriteValueFor回调来驱动下一包的发送,天然串行化。
四、指令层的高级排坑
条码打印偏移:如果发现条码打印偏出纸面,先检查
BARCODE指令里的 X/Y 坐标,很多打印机物理原点(0,0)在左上角,但有些机型的原点是左下角。打印机状态回传:开了
NOTIFY后,打印机可能会回传状态字节(如缺纸、开盖、低电量)。App 应该解析这些字节并做弹窗提示,而不是等用户点了几次打印都没出纸才去检查打印机。大报文内存溢出:超过 4KB 的超长报文,不仅要分包,还要分批次
flush缓冲区,防止打印机的内存溢出死机。
五、总结
蓝牙面单打印,代码量不大,但全是细节。要跨过“连得上”到“打得出”的鸿沟,必须做到:指令层保证以\r\n结尾、字符编码降级为 GBK;传输层严格遵守 MTU 限制,依靠回调驱动分包写入,绝不暴力循环发送。掌握了这两点,才能在五花八门的便携打印机前游刃有余。
> 本文作者:深圳+Eyf•半字节科技▪技术产品团队
> 物流、快递、快运作业管理系统,让每一票,都清晰可控。从任务分配到签收回传,一站式管理每个作业环节。