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)开始引入了打印框架。核心就是PrintManagerPrintService。你不需要自己从头写USB驱动,Android已经帮你封装好了。

整个流程是这样的:

  1. 你的App通过PrintManager创建一个打印任务
  2. 系统把任务交给对应的PrintService(比如USB打印服务)
  3. PrintService把数据转换成打印机能理解的格式(PCL/PostScript)
  4. 通过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通信层 IPP PCL PostScript PrintManager UsbManager 协议解析与转换 数据格式适配 打印任务管理 PrintJob生命周期 设备发现与枚举 USB设备过滤 状态监控 端点轮询与解析 实战:USB直接打印照片/文档 Bitmap转换 → PCL指令封装 → USB批量传输 → 状态反馈

这张图从协议层到底层通信,再到实战应用,把整个知识脉络串起来了。你写代码的时候,可以对照这张图,看看自己卡在哪一层。


好了,USB打印机开发的核心内容就这些。从协议选择到Android框架,从设备发现到状态监控,再到最后的实战代码,每一步都有坑,但也都有解法。我个人觉得,做USB打印开发最重要的就是耐心——打印机不会告诉你它哪里不满意,你只能靠协议分析和日志排查。

如果你在实际开发中遇到问题,多看看打印机的技术手册,多抓USB日志分析。嗯,这些经验都是用加班换来的,希望你能少走弯路。