21、USB打印机开发:USB打印机类协议(IPP、PCL、PostScript)、Android端USB打印服务(PrintManager)、打印机发现与状态监控、实战:通过USB直接打印照片/文档
说到USB打印机开发,我脑子里第一个蹦出来的画面,是几年前在深圳一个工厂里调试热敏打印机驱动。那台打印机死活不响应我的指令,折腾了整整两天,最后发现是USB接口的端点地址搞错了。嗯,这种坑踩过一次,你就再也不会忘了。
这一章,我们聊聊Android设备怎么跟USB打印机打交道。说白了,就是让你的手机或平板能直接连打印机,把照片、文档打出来。别觉得简单,这里面的门道不少。
21.1 打印机类协议:IPP、PCL、PostScript
打印机不像U盘,插上就能读写。它有自己的语言。你想想看,你跟打印机说话,总得用它能听懂的方式吧?
21.1.1 IPP(Internet Printing Protocol)
IPP是互联网打印协议,现在很多网络打印机都支持。它基于HTTP,说白了就是把打印任务封装成HTTP请求发过去。Android系统原生就支持IPP,所以如果你用的是支持IPP的打印机,事情会简单很多。
但注意,IPP通常走网络,不是USB。USB打印机更多用的是下面这两种协议。
21.1.2 PCL(Printer Control Language)
PCL是惠普发明的打印机控制语言。它是一套指令集,用来描述页面布局、字体、图形等。PCL的特点是高效,适合打印文档。我在项目中遇到过一台老旧的激光打印机,只支持PCL 5e,折腾了好久才把中文打印出来——因为PCL 5e对中文字体的支持很有限。
PCL的指令格式大致是这样的:
// 初始化打印机
ESC E
// 设置页面大小
ESC &l26A
// 设置字体
ESC (s1p12v0s0b0T
// 打印文本
Hello, USB Printer!
// 换页
FF
每个ESC代表一个转义字符(0x1B),后面跟着参数。看着挺原始,但确实管用。
21.1.3 PostScript
PostScript是Adobe发明的页面描述语言。它比PCL更强大,能描述复杂的图形和排版。很多高端打印机都支持PostScript。但它的缺点是解析起来比较慢,文件也大。
PostScript的代码看起来更像一门编程语言:
%!
/Courier findfont 12 scalefont setfont
100 200 moveto
(Hello, USB Printer!) show
showpage
我个人习惯,如果打印机支持PostScript,我会优先用它。因为它的渲染效果确实好,尤其是打印图片的时候。
核心要点:
- IPP:网络打印,Android原生支持
- PCL:高效,适合文档,指令式
- PostScript:强大,适合图形,编程式
21.2 Android端USB打印服务(PrintManager)
Android从4.4(KitKat)开始引入了打印框架。核心就是PrintManager和PrintService。你不需要自己从头写USB驱动,Android已经帮你封装好了。
整个流程是这样的:
- 你的App通过
PrintManager创建一个打印任务 - 系统把任务交给对应的
PrintService(比如USB打印服务) - PrintService把数据转换成打印机能理解的格式(PCL/PostScript)
- 通过USB发送给打印机
下面是一个简单的代码示例,展示如何通过PrintManager发起打印:
// 获取PrintManager
PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
// 创建打印任务
PrintJob printJob = printManager.print("My Document",
new PrintDocumentAdapter() {
@Override
public void onWrite(PageRange[] pages, ParcelFileDescriptor destination,
CancellationSignal cancellationSignal, WriteResultCallback callback) {
// 在这里把文档内容写入destination
// 系统会把这个数据传给PrintService
callback.onWriteFinished(new PageRange[]{PageRange.ALL_PAGES});
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
CancellationSignal cancellationSignal, LayoutResultCallback callback,
Bundle metadata) {
// 在这里计算页面布局
callback.onLayoutFinished(new PrintDocumentInfo.Builder("my_doc.pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.setPageCount(1)
.build(), true);
}
}, null);
这段代码看着简单,但实际开发中坑不少。我曾经遇到一个问题:onWrite回调里写入的数据格式不对,打印机打出来全是乱码。后来发现,我需要把数据转换成PDF格式,因为Android的打印框架默认使用PDF作为中间格式。
小提示:
如果你要打印图片,建议先把Bitmap转换成PDF,再通过PrintManager打印。直接传JPEG数据,很多USB打印机不认。
21.3 打印机发现与状态监控
打印机连上USB了,你怎么知道它是什么型号?支持什么协议?状态是否正常?这就需要用到USB的UsbManager了。
21.3.1 发现打印机
Android通过USB设备描述符来识别打印机。打印机的deviceClass通常是7(打印机类)。下面是我常用的发现代码:
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
for (UsbDevice device : deviceList.values()) {
// 检查设备类是否为打印机
if (device.getDeviceClass() == 7) {
// 找到了打印机
Log.d("USB_PRINT", "发现打印机: " + device.getProductName());
// 获取接口信息
UsbInterface usbInterface = device.getInterface(0);
// 通常打印机有两个端点:一个批量输出,一个批量输入
for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
UsbEndpoint endpoint = usbInterface.getEndpoint(i);
Log.d("USB_PRINT", "端点: " + endpoint.getEndpointNumber() +
" 方向: " + (endpoint.getDirection() == UsbConstants.USB_DIR_OUT ? "OUT" : "IN"));
}
}
}
这里要注意,有些打印机可能被识别为deviceClass == 0(在接口级别定义类)。所以更稳妥的做法是检查接口的getInterfaceClass()是否为7。
21.3.2 状态监控
打印机状态通常通过USB的批量输入端点读取。不同厂商的协议不一样,但一般都会返回一个状态字节。比如:
- 0x00:空闲
- 0x01:打印中
- 0x02:缺纸
- 0x04:卡纸
- 0x08:错误
我曾经踩过一个坑:读取状态时没有做超时处理,结果打印机没响应,线程一直卡在bulkTransfer上。后来我加了个超时参数:
UsbDeviceConnection connection = usbManager.openDevice(device);
UsbEndpoint inEndpoint = ...; // 获取输入端点
byte[] buffer = new byte[8];
// 超时设为1000毫秒
int result = connection.bulkTransfer(inEndpoint, buffer, buffer.length, 1000);
if (result >= 0) {
// 解析状态
int status = buffer[0] & 0xFF;
switch (status) {
case 0x00:
Log.d("USB_PRINT", "打印机空闲");
break;
case 0x02:
Log.d("USB_PRINT", "缺纸!");
break;
case 0x04:
Log.d("USB_PRINT", "卡纸!");
break;
}
} else {
Log.e("USB_PRINT", "读取状态失败,错误码: " + result);
}
警告:
USB操作必须在子线程中执行,不能在主线程里做bulkTransfer。否则会ANR(应用无响应)。我见过有人把USB读写放在UI线程,结果一打印就卡死。
21.4 实战:通过USB直接打印照片/文档
好了,理论说完了,我们来点实际的。下面是一个完整的USB打印流程,从连接打印机到发送数据。
21.4.1 打印照片
打印照片的核心是把Bitmap转换成打印机支持的格式。对于PCL打印机,我通常把图片转换成单色位图,然后用PCL的光栅图形指令发送。
public void printBitmap(UsbDeviceConnection connection, Bitmap bitmap) {
// 1. 初始化打印机
byte[] initCmd = new byte[]{0x1B, 0x45}; // ESC E
connection.bulkTransfer(outEndpoint, initCmd, initCmd.length, 1000);
// 2. 设置页面
byte[] pageCmd = new byte[]{0x1B, 0x26, 0x6C, 0x26, 0x41}; // ESC &l26A (A4)
connection.bulkTransfer(outEndpoint, pageCmd, pageCmd.length, 1000);
// 3. 转换图片为单色位图
Bitmap monoBitmap = toMonochrome(bitmap);
int width = monoBitmap.getWidth();
int height = monoBitmap.getHeight();
// 4. 发送光栅图形数据
// PCL的栅格图形指令:ESC *bW
byte[] rasterStart = new byte[]{0x1B, 0x2A, 0x72, 0x31, 0x41}; // ESC *r1A (开始栅格)
connection.bulkTransfer(outEndpoint, rasterStart, rasterStart.length, 1000);
// 逐行发送
for (int y = 0; y < height; y++) {
byte[] rowData = getRowData(monoBitmap, y);
byte[] rowCmd = new byte[]{0x1B, 0x2A, 0x62, (byte) rowData.length, 0x57}; // ESC *bW
connection.bulkTransfer(outEndpoint, rowCmd, rowCmd.length, 1000);
connection.bulkTransfer(outEndpoint, rowData, rowData.length, 1000);
}
// 5. 结束栅格并换页
byte[] rasterEnd = new byte[]{0x1B, 0x2A, 0x72, 0x42}; // ESC *rB
connection.bulkTransfer(outEndpoint, rasterEnd, rasterEnd.length, 1000);
byte[] formFeed = new byte[]{0x0C}; // FF
connection.bulkTransfer(outEndpoint, formFeed, formFeed.length, 1000);
}
这段代码我简化了很多。实际项目中,你还要处理图片缩放、旋转、色彩深度转换等问题。我记得有一次打印出来的照片是倒着的,因为忘了处理Bitmap的坐标方向。
21.4.2 打印文档
打印文档更简单一些。如果你用的是Android的打印框架,直接把文档转成PDF,然后交给PrintManager就行。但如果你要直接通过USB发送,那就得把文档内容转换成PCL或PostScript。
对于纯文本,PCL有直接的文本打印指令:
public void printText(UsbDeviceConnection connection, String text) {
// 初始化
byte[] init = new byte[]{0x1B, 0x45};
connection.bulkTransfer(outEndpoint, init, init.length, 1000);
// 设置字体:Courier, 12pt
byte[] font = new byte[]{0x1B, 0x28, 0x73, 0x31, 0x70, 0x31, 0x32, 0x76, 0x30, 0x73, 0x30, 0x62, 0x30, 0x54};
connection.bulkTransfer(outEndpoint, font, font.length, 1000);
// 发送文本
byte[] textBytes = text.getBytes("ISO-8859-1");
connection.bulkTransfer(outEndpoint, textBytes, textBytes.length, 1000);
// 换页
byte[] ff = new byte[]{0x0C};
connection.bulkTransfer(outEndpoint, ff, ff.length, 1000);
}
注意编码问题。中文文本不能用ISO-8859-1,得用GBK或者UTF-8,但PCL对中文的支持取决于打印机有没有中文字库。没有的话,你只能把中文转成图片再打印。
实战总结:
- 照片打印:Bitmap → 单色位图 → PCL栅格指令
- 文档打印:文本 → PCL文本指令(或PDF → PrintManager)
- 状态监控:定时读取USB输入端点,解析状态字节
- 异常处理:超时、断线、缺纸都要考虑
21.5 知识体系图
下面这张图展示了USB打印机开发的核心知识结构,我把它画成了流程图,方便你理解整体脉络。
这张图从协议层到底层通信,再到实战应用,把整个知识脉络串起来了。你写代码的时候,可以对照这张图,看看自己卡在哪一层。
好了,USB打印机开发的核心内容就这些。从协议选择到Android框架,从设备发现到状态监控,再到最后的实战代码,每一步都有坑,但也都有解法。我个人觉得,做USB打印开发最重要的就是耐心——打印机不会告诉你它哪里不满意,你只能靠协议分析和日志排查。
如果你在实际开发中遇到问题,多看看打印机的技术手册,多抓USB日志分析。嗯,这些经验都是用加班换来的,希望你能少走弯路。