尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

安卓蓝牙打印XE10实战:抛弃SDK,用SPP与ESC/POS完成热敏打印

安卓蓝牙打印XE10实战:抛弃SDK,用SPP与ESC/POS完成热敏打印 简介这是一份面向 Delphi XE10 开发者的安卓蓝牙打印实现资源适合需要在 Android 端快速集成蓝牙打印功能的中高级移动开发人员。资源围绕蓝牙通信完整流程展开涵盖开启蓝牙、搜索与配对设备、建立 SPP 连接、发送打印数据及断开连接等关键环节并配有设备选择界面、配对逻辑、连接管理、数据转换与打印命令构造等源代码模块便于对照实际项目移植与调试。压缩包共 226 个文件包含 dcu、o、a、bpi、bpl、lib 等编译产物与库文件以及 dpk/dproj 工程文件、xml 配置和 pdf 说明文档包体约 9.44MB整体结构清晰。目前已有 782 人学习下载适合需要解决手机蓝牙打印兼容性问题的开发者参考。 做安卓开发这几年跟蓝牙打印机的爱恨情仇能写一部连续剧。上个月帮客户做连锁仓库的标签打印系统又被一台老掉牙的“安卓蓝牙打印 XE10”折腾了一整晚。熟悉这型号的朋友应该懂——80mm热敏便携机里的常青树市面上还有大量存量在跑偏偏厂商给的SDK裹脚布一样又臭又长各种Activity跳来跳去文档还停留在安卓6时代。最后实在受不了干脆把SDK扔了自己用BluetoothSocket走SPP协议直接拼ESC/POS指令问题反而全解决了。这篇文章就把这套完整方案写出来从蓝牙连接、权限适配、指令封装到图片和二维码打印以及我实测下来碰到过的各种坑。不管你是要给老式便携打印机做兼容还是开发自己的小票/标签打印App这套思路都能直接用而且不依赖任何厂商SDK。1. 项目整体设计与方案选型1.1 XE10到底是什么来头先说清楚这个型号很多人一听“安卓蓝牙打印 XE10”以为是某个App其实它是指一种非常常见的蓝牙热敏便携打印机。10的意思是80mm纸宽打印分辨率203dpi打印速度一般100mm/s左右支持蓝牙、USB甚至部分网口。这参数放在今天不算惊艳但正好卡在物流面单、商超小票、餐饮后厨、移动执法这类“需要边走边打”的场景市场上存量非常大。但这类打印机有个致命问题厂商SDK质量参差不齐往往提供一个巨难用的安卓库里面塞满了各种自定义Activity你在自己的App里跳来跳去完全没法做UI定制。而且SDK版本老旧targetSdk调高一点就各种崩溃。我这次索性不走SDK直接在蓝牙协议层和打印机对话。打印机厂商统一遵循ESC/POS指令集只要能建立SPP连接、把字节流发过去剩下的事就简单了。1.2 为什么采用经典蓝牙SPP而不是BLE选通信方案时很多人会被绕晕尤其是查资料时看到“蓝牙br ble区别”这种话题。简单说我们平时说的经典蓝牙BR/EDR和低功耗蓝牙BLE是两个时代的产品。经典蓝牙优势是传输速度快、数据量大适合持续流式通信BLE优势是功耗极低、连接快但payload很小一般用于手环、传感器这类轻量设备。打印机需要一次性发送几十KB甚至上百KB的点阵数据还要保证连续输出这活儿只有经典蓝牙SPP能干。SPP全称Serial Port Profile把蓝牙通道模拟成串口Android底层直接提供了BluetoothSocket连接方式不需要额外的通信协议。BLE虽然也有个“蓝牙打印”的变种但很多老机器根本不支持传输大图时能把人急死。所以方案选型上想都没想直接定死SPP。这里顺便提一句实际测试发现XE10走SPP时波特率并不重要不像传统串口那样需要约定9600还是115200因为RFCOMM本身就是流式传输不需要配置波特率。1.3 打印协议选型坚持ESC/POSESC/POS是爱普生推的一种打印控制语言用简单的转义指令控制打印机的所有行为初始化、字体、对齐、走纸、切纸、图片、条码、二维码。几乎所有热敏打印机都兼容这个指令集XE10也不例外。选择ESC/POS而不是厂商私有协议最大的好处是兼容性——以后换打印机品牌只要对方支持ESC/POS业务代码一行都不用改。2. 安卓蓝牙通道搭建与权限适配2.1 安卓权限适配从6.0到14.0的区别这块是“安卓蓝牙打印”的入场券也是新手最容易翻车的地方。Android 6到11时代只需要申请BLUETOOTH和BLUETOOTH_ADMIN两个普通权限在Manifest里声明就行。但从Android 12开始谷歌把蓝牙权限直接拆成了三个运行时权限BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE必须在运行时向用户申请而且跳转到系统设置打开蓝牙的方式也变了。千万注意BLUETOOTH_SCAN只是扫描周边设备真正连接设备还需要BLUETOOTH_CONNECT两个权限缺一不可。另外有个特别坑的细节在部分国产ROM上扫描蓝牙设备还必须同时申请定位权限否则返回值一直是空——这个不是安卓官方要求是厂商加的逻辑但做兼容适配时必须带上。我的做法是直接在Manifest里声明旧权限让老系统兼容然后在运行时把新权限和定位权限一次性申请完避免用户授权弹窗弹个没完。2.2 搜索配对与建立RFCOMM连接连接经典蓝牙打印机的路径是蓝牙适配器 → 发现设备 → 配对 → 建立RFCOMM Socket。有几个卡死人小细节这里逐个说。搜索设备时如果直接用startDiscovery()很多设备扫描结果是偶发性的你需要注册BroadcastReceiver监听ACTION_FOUND。但注意已经配对过的设备不会再次出现在ACTION_FOUND里所以一般先调getBondedDevices()拿已经配对过的列表再启动搜索补漏。配对本身用createBond()这个方法是异步的监听ACTION_BOND_STATE_CHANGED判断结果。配对成功后连接代码要这样写// 需要放在工作线程里执行不能在主线程 BluetoothDevice device bluetoothAdapter.getRemoteDevice(address); UUID sppUuid UUID.fromString(00001101-0000-1000-8000-00805F9B34FB); BluetoothSocket socket device.createRfcommSocketToServiceRecord(sppUuid); socket.connect();这个UUID就是SPP服务的固定标识市面上所有蓝牙打印机都认它。但这里我要特别提醒一个深坑在某些定制系统或部分打印机固件上createRfcommSocketToServiceRecord会连接失败或者连接时间巨长。这时候别死磕用反射大法直接指定通道号Method m device.getClass().getMethod(createRfcommSocket, new Class[] {int.class}); BluetoothSocket socket (BluetoothSocket) m.invoke(device, 1); socket.connect();通道号取1成功率最高但也见过个别机器要在1和2之间切换才能连上。实测经验是优先标准方法失败再反射连接两种方法都放在线程里做超时控制。这也是为什么老开发者都说“蓝牙打印的尽头是玄学”。2.3 数据通道的线程管理与分包发送连接建立后拿到socket.getOutputStream()和getInputStream()打印就是往输出流里写数据。但这里有个很多人会忽略的点一定不要把Socket放在主线程里用因为阻塞式读写会直接卡爆UI。而且代码里要设计一个独立的写线程配合一个任务队列防止多界面同时触发的打印请求相互干扰。还有一个重要的分包处理。热敏打印机的输入缓冲区不大一次性写入几万字节的位图数据很容易导致数据丢失表现就是打印到一半卡住或者输出乱码。我一般会封装一个write命令超过2KB的数据按2KB一片切片发送每个slice之间sleep 10ms给打印机时间消化数据public void sendData(byte[] data) throws IOException { int chunkSize 2048; int len data.length; int offset 0; while (offset len) { int size Math.min(chunkSize, len - offset); OutputStream out socket.getOutputStream(); out.write(data, offset, size); out.flush(); offset size; try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }这个分包逻辑看起来简单却是解决“打印到一半断掉”的有效手段。不要问我怎么知道的问就是当年一台新机器第一次打印就卡半天。3. ESC/POS指令封装与打印核心实现3.1 文本打印初始化、对齐、字体、加粗指令封装的境界是所有打印机的行为最终都变成一段byte数组。先记几个最常用的初始化打印机0x1B 0x40 —— 每次打印前必发把打印机的字体、行距、对齐全部复位。对齐方式0x1B 0x61 nn0左对齐1居中2右对齐。字符大小0x1D 0x21 nn的bit0~2控制宽度倍数bit4~6控制高度倍数。加粗0x1B 0x45 nn1开启n0关闭。走纸换行0x0A发一个换行符就打印一行并走纸。这里有个中文编码的大坑安卓字符串默认是UTF-8但绝大多热敏打印机内置的字库是GBK/GB2312直接发送UTF-8编码的中文会变成一串乱码或者干脆不打印。解决办法是转换编码String content 这是一行测试; byte[] data content.getBytes(GBK);但如果打印机说明书里明确写支持UTF-8那就不用转。判断方法很简单连上后先打一行中文乱码就换编码格式。XE10实测用GBK没问题。3.2 图片打印点阵位图的生成与发送图片打印是ESC/POS里最讲究的一块。打印机的图片逻辑是把一张图片转成黑白点阵每8个像素打包成一个字节按行按列逐字节发送给打印机。指令是0x1D 0x76 0x30 m xL xH yL yH data后面跟着的就是成串的点阵数据。发数据前要算好图片宽高打印机固定203dpi假设要打一张80mm宽的图片像素宽大约是80/25.4*203约等于640像素。但注意每行像素数必须是8的倍数否则最后会错位。处理方式是创建一个新的Bitmap宽度对齐到8的倍数不足的部分填白色。我写过一个通用的转换方法直接用Bitmap.getPixels拿到像素数组然后把亮度低于阈值的点当成黑点否则白点。这里阈值选1280x80对比度比较合适。代码大概长这样private byte[] bitmapToEscPosData(Bitmap bitmap) { int width bitmap.getWidth(); int height bitmap.getHeight(); int bytesPerLine (width 7) / 8; byte[] data new byte[bytesPerLine * height]; int[] pixels new int[width * height]; bitmap.getPixels(pixels, 0, width, 0, 0, width, height); int pos 0; for (int y 0; y height; y) { for (int x 0; x bytesPerLine; x) { int byten 0; for (int bit 0; bit 8; bit) { int px x * 8 bit; if (px width) { int color pixels[y * width px]; int r (color 16) 0xFF; int g (color 8) 0xFF; int b color 0xFF; int gray (r * 299 g * 587 b * 114 500) / 1000; if (gray 128) { byten | (0x80 bit); } } } data[pos] (byte) byten; } } return data; }转换完成后按照指令格式组装m0表示正常模式xL/xH是宽度低字节和高字节yL/yH是高度然后直接sendData。一个经常被忽略的问题是大Bitmap在手机内存里可能几百KB但在打印机侧它并不需要那么多冗余信息。打印前先把Bitmap缩放到一个合理的宽度比如512像素高度按比例缩放这样打印速度会快很多效果也不差。3.3 二维码打印用ZXing生成再转点阵现在的标签几乎都要带二维码。有两种方案一是用ESC/POS内置的二维码指令让打印机自己生成二是用ZXing在app端生成Bitmap再转成点阵打印。第一种快、清晰但指令比较复杂而且不同固件对二维码模型参数支持不一致第二种兼容性好但打印时间略长。我倾向于用ZXing生成后走图片打印因为这样二维码样式和排版完全自己控制。ZXing生成二维码核心就一句话QRCodeWriter writer new QRCodeWriter(); BitMatrix matrix writer.encode(content, BarcodeFormat.QR_CODE, size, size); Bitmap bitmap bitMatrixToBitmap(matrix);生成之后别忘了加边框——二维码四周建议留白至少2个像素模块不然扫码枪识别率会降低。实测这个细节对打印质量影响挺大因为热敏纸在打印时边缘容易模糊。3.4 走纸与切纸一个容易被忽略的细节打印结束要正确走纸。发0x0A换行几次是不够的因为标签纸有固定的标签高度走少了下一张标签定位不对走多了浪费纸。XE10这类支持标签定位的打印机通常要发0x1D 0x56 0x42 0x00执行切纸和自动定位。但在小票模式下只需要发几个换行再加上一条黑标检测指令即可。这里提醒一句测试的时候不要总拿一张纸来回打因为我遇到过纸张定位器在高温下失灵的情况打印内容总偏移半行这种低级硬件问题排查起来真的让人崩溃。4. 实测问题与排查速查项目做完收尾时我把遇到的典型问题整理了张表全是实打实踩过的坑希望你能少走些弯路。问题现场可能原因解决思路搜不到打印机的蓝牙信号打印机处于休眠状态或已连接他机关机重启打印机连续按几次按键唤醒先解绑再重新搜索连接需要很久而且经常失败createRfcommSocketToServiceRecord兼容性问题改用反射法createRfcommSocket(1)或换成通道2试一次中文全部变成乱码编码格式不匹配将字符串编码改为GBK确认打印机字库格式打印的图片变成一条黑带或者整体错位图片宽度不是8的倍数或点阵转换算法有误对齐宽度检查位运算顺序务必测试单色位图打印到一半停下来后面内容丢失一次写入数据过多打印机缓冲溢出分包发送切片2KB后加延时某些手机连接成功后打印机没有反应部分定制ROM的蓝牙协议栈处理方式不同尝试连接前先关闭蓝牙再开启或者配对后等待500ms再建立Socket打印速度异常慢图片像素过大点阵数据冗余把Bitmap宽度压到512左右再发送4.1 关于“安卓蓝牙打印 XE10”的具体兼容性备注XE10这个型号的固件版本差异比较大早期批次和后期批次的指令支持细节有些出入。比如有些固件对0x1D 0x76 0x30的图片指令支持不完整打印出来全黑或者花屏。遇到这种情况不要急可以换用另一种位图指令0x1D 0x2A 0x00GS *位图模式很多老款打印机对这个指令支持更稳定。代码层面我一般把图片指令抽成一个策略接口打不了就自动降级换另一种保证用户的标签纸不浪费。4.2 测试时我常用的“打印九九乘法表”式验证脚本这里分享一个小技巧拿到一台没接触过的打印机先别急着调业务界面我先发一段包含各种格式的测试命令确认基础字体、对齐、加粗、换行是否正常然后再打印一张64x64的小二维码验证点阵和图片逻辑。这个习惯帮我避免了很多理想化代码直接上线后被现实打脸的尴尬。可以把这种测试脚本理解成打印界的“print hello world”。5. 最后再分享三个实操心得第一蓝牙打印的调试期一定要在代码里加日志记录每次发送的字节数、耗时和异常栈。我就靠日志排查过一次“打印一张标签要30秒”的问题最后定位到是某条指令被重复发送了上百次纯属自己写代码时手滑。第二如果项目预算允许采购时选支持网口或USB口的机型做备选方案。蓝牙方案在信号干扰大的环境比如仓库里全是金属货架会非常不稳定而USB或者网口几乎没有这种烦恼。我这次项目交付时就在每台打印机旁边留了USB线关键时刻能救命。第三打印机固件能升级就升。XE10的固件其实更新过好几版修复了不少蓝牙连接和指令兼容的bug。拿到机器先去官网查固件版本如果设备用得时间太长很可能旧固件会带来各种莫名奇妙的问题。这看起来是废话但很多团队真的会忽略。蓝牙打印这条技术路线本质上就是串口通信套了一层蓝牙壳。把协议层吃透不依赖厂商SDK你就能在形形色色的打印机之间游刃有余。希望这篇“安卓蓝牙打印 XE10”的实操记录能帮你少踩几个坑也让那些还在工厂、仓库、街边小店服役的老机器继续发挥余热。本文还有配套的精品资源点击获取
返回列表