美文网首页移动互联网android杂记android
Android 蓝牙连接 ESC/POS 热敏打印机打印(蓝牙连

Android 蓝牙连接 ESC/POS 热敏打印机打印(蓝牙连

作者: VitaminChen | 来源:发表于2016-05-11 15:40 被阅读10400次

    公司的一个手机端的 CRM 项目最近要增加小票打印的功能,就是我们点外卖的时候经常会见到的那种小票。这里主要涉及到两大块的知识:

    • 蓝牙连接及数据传输
    • ESC/POS 打印指令

    蓝牙连接不用说了,太常见了,这篇主要介绍这部分的内容。但ESC/POS 打印指令是个什么鬼?简单说,我们常见的热敏小票打印机都支持这样一种指令,只要按照指令的格式向打印机发送指令,哪怕是不同型号品牌的打印机也会执行相同的动作。比如打印一行文本,换行,加粗等都有对应的指令,这部分内容放在下一篇介绍。

    本篇主要基于官方文档,相比官方文档,省去了大段的说明,更加便于快速上手。
    demo及打印指令讲解请看下篇

    1. 蓝牙权限

    想要使用蓝牙功能,首先要在 AndroidManifest 配置文件中声明蓝牙权限:

    <manifest> 
      <uses-permission android:name="android.permission.BLUETOOTH" />
      <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
      ...
    </manifest>
    

    BLUETOOTH 权限只允许建立蓝牙连接以及传输数据,但是如果要进行蓝牙设备发现等操作的话,还需要申请 BLUETOOTH_ADMIN 权限。

    2. 初始配置

    这里主要用到一个类 BluetoothAdapter。用法很简单,直接看代码:

    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
        // Device does not support Bluetooth
    }
    

    单例模式,全局只有一个实例,只要为 null,就代表设备不支持蓝牙,那么需要有相应的处理。
    如果设备支持蓝牙,那么接着检查蓝牙是否打开:

    if (!mBluetoothAdapter.isEnabled()) {
        Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(intent, REQUEST_ENABLE_BT);
    }
    

    如果蓝牙未打开,那么执行 startActivityForResult() 后,会弹出一个对话框询问是否要打开蓝牙,点击`是`之后就会自动打开蓝牙。成功打开蓝牙后就会回调到 onActivityResult()

    除了主动的打开蓝牙,还可以监听 BluetoothAdapter.ACTION_STATE_CHANGED
    广播,包含EXTRA_STATEEXTRA_PREVIOUS_STATE两个 extra 字段,可能的取值包括 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, and STATE_OFF。含义很清楚了,不解释。

    3. 发现设备

    初始化完成之后,蓝牙打开了,接下来就是扫描附近的设备,只需要一句话:

    mBluetoothAdapter.startDiscovery();
    

    不过这样只是开始执行设备发现,这肯定是一个异步的过程,我们需要注册一个广播,监听发现设备的广播,直接上代码:

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            
            // 当有设备被发现的时候会收到 action == BluetoothDevice.ACTION_FOUND 的广播
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
    
                //广播的 intent 里包含了一个 BluetoothDevice 对象
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    
                //假设我们用一个 ListView 展示发现的设备,那么每收到一个广播,就添加一个设备到 adapter 里
                mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
            }
        }
    };
    // 注册广播监听
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
    

    注释已经写的很清楚了,除了 BluetoothDevice.EXTRA_DEVICE 之外,还有一个 extra 字段 BluetoothDevice.EXTRA_CLASS, 可以得到一个 BluetoothClass 对象,主要用来保存设备的一些额外的描述信息,比如可以知道这是否是一个音频设备。

    关于设备发现,有两点需要注意:

    • startDiscovery() 只能扫描到那些状态被设为 可发现 的设备。安卓设备默认是不可发现的,要改变设备为可发现的状态,需要如下操作:
    Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
    //设置可被发现的时间,300s
    intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
    startActivity(intent);
    

    执行之后会弹出对话窗询问是否允许设备被设为可发现的状态,点击`是`之后设备即被设为可发现的状态。

    • startDiscovery()是一个十分耗费资源的操作,所以需要及时的调用cancelDiscovery()来释放资源。比如在进行设备连接之前,一定要先调用cancelDiscovery()

    4. 设备配对与连接

    4.1 配对

    当与一个设备第一次进行连接操作的时候,屏幕会弹出提示框询问是否允许配对,只有配对成功之后,才能建立连接。
    系统会保存所有的曾经成功配对过的设备信息。所以在执行startDiscovery()之前,可以先尝试查找已配对设备,因为这是一个本地信息读取的过程,所以比startDiscovery()要快得多,也避免占用过多资源。如果设备在蓝牙信号的覆盖范围内,就可以直接发起连接了。

    查找配对设备的代码如下:

    Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
    if (pairedDevices.size() > 0) {
        for (BluetoothDevice device : pairedDevices) {
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
    

    代码很简单,不解释了,就是调用BluetoothAdapter.getBondedDevices()得到一个 Set<BluetoothDevice> 并遍历取得已配对的设备信息。

    4.2 连接

    蓝牙设备的连接和网络连接的模型十分相似,都是Client-Server 模式,都通过一个 socket 来进行数据传输。那么作为一个 Android 设备,就存在三种情况:

    • 只作为 Client 端发起连接
    • 只作为 Server 端等待别人发起建立连接的请求
    • 同时作为 Client 和 Server

    因为是为了下一篇介绍连接热敏打印机打印做铺垫,所以这里先讲 Android 设备作为 Client 建立连接的情况。因为打印机是不可能主动跟 Android 设备建立连接的,所以打印机必然是作为 Server 被连接。

    4.2.1 作为 Client 连接
    1. 首先需要获取一个 BluetoothDevice 对象。获取的方法前面其实已经介绍过了,可以通过调用 startDiscovery()并监听广播获得,也可以通过查询已配对设备获得。
    2. 通过 BluetoothDevice.createRfcommSocketToServiceRecord(UUID) 得到 BluetoothSocket 对象
    3. 通过BluetoothSocket.connect()建立连接
    4. 异常处理以及连接关闭

    废话不多说,上代码:

    private class ConnectThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final BluetoothDevice mmDevice;
     
        public ConnectThread(BluetoothDevice device) {
    
            BluetoothSocket tmp = null;
            mmDevice = device;
            try {
                // 通过 BluetoothDevice 获得 BluetoothSocket 对象
                tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
            } catch (IOException e) { }
            mmSocket = tmp;
        }
         
        @Override
        public void run() {
            // 建立连接前记得取消设备发现
            mBluetoothAdapter.cancelDiscovery();
            try {
                // 耗时操作,所以必须在主线程之外进行
                mmSocket.connect();
            } catch (IOException connectException) {
                //处理连接建立失败的异常
                try {
                    mmSocket.close();
                } catch (IOException closeException) { }
                return;
            }
            doSomething(mmSocket);
        }
     
        //关闭一个正在进行的连接
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) { }
        }
    }
    

    device.createRfcommSocketToServiceRecord(MY_UUID) 这里需要传入一个 UUID,这个UUID 需要格外注意一下。简单的理解,它是一串约定格式的字符串,用来唯一的标识一种蓝牙服务。

    Client 发起连接时传入的 UUID 必须要和 Server 端设置的一样!否则就会报错!

    如果是连接热敏打印机这种情况,不知道 Server 端设置的 UUID 是什么怎么办?
    不用担心,因为一些常见的蓝牙服务协议已经有约定的 UUID。比如我们连接热敏打印机是基于 SPP 串口通信协议,其对应的 UUID 是 "00001101-0000-1000-8000-00805F9B34FB",所以实际的调用是这样:

    device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
    

    其他常见的蓝牙服务的UUID大家可以自行搜索。如果只是用于自己的应用之间的通信的话,那么理论上可以随便定义一个 UUID,只要 server 和 client 两边使用的 UUID 一致即可。更多关于 UUID 的介绍可以参考这里

    4.2.2 作为 Server 连接
    1. 通过BluetoothAdapter.listenUsingRfcommWithServiceRecord(String, UUID)获取一个 BluetoothServerSocket 对象。这里传入的第一个参数用来设置服务的名称,当其他设备扫描的时候就会显示这个名称。UUID 前面已经介绍过了。
    2. 调用BluetoothServerSocket.accept()开始监听连接请求。这是一个阻塞操作,所以当然也要放在主线程之外进行。当该操作成功执行,即有连接建立的时候,会返回一个BluetoothSocket 对象。
    3. 调用 BluetoothServerSocket.close() 会关闭监听连接的服务,但是当前已经建立的链接并不会受影响。

    还是看代码吧:

    private class AcceptThread extends Thread {
    
        private final BluetoothServerSocket mmServerSocket;
     
        public AcceptThread() {
    
            BluetoothServerSocket tmp = null;
            try {
                // client 必须使用一样的 UUID !!!
                tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
            } catch (IOException e) { }
            mmServerSocket = tmp;
        }
    
        @Override
        public void run() {
            BluetoothSocket socket = null;
            //阻塞操作
            while (true) {
                try {
                    socket = mmServerSocket.accept();
                } catch (IOException e) {
                    break;
                }
                //直到有有连接建立,才跳出死循环
                if (socket != null) {
                    //要在新开的线程执行,因为连接建立后,当前线程可能会关闭
                    doSomething(socket);
                    mmServerSocket.close();
                    break;
                }
            }
        }
     
        public void cancel() {
            try {
                mmServerSocket.close();
            } catch (IOException e) { }
        }
    }
    

    5. 数据传输

    终于经过了前面的4步,万事俱备只欠东风。而最后这一部分其实是最简单的,因为就只是简单的利用 InputStreamOutputStream进行数据的收发。
    示例代码:

    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
     
        public ConnectedThread(BluetoothSocket socket) {
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;
            //通过 socket 得到 InputStream 和 OutputStream
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) { }
     
            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }
     
        public void run() {
            byte[] buffer = new byte[1024];  // buffer store for the stream
            int bytes; // bytes returned from read()
     
            //不断的从 InputStream 取数据
            while (true) {
                try {
                    bytes = mmInStream.read(buffer);
                    mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                            .sendToTarget();
                } catch (IOException e) {
                    break;
                }
            }
        }
     
        //向 Server 写入数据
        public void write(byte[] bytes) {
            try {
                mmOutStream.write(bytes);
            } catch (IOException e) { }
        }
     
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) { }
        }
    }
    

    下一篇介绍通过手机操作热敏打印机打印的时候,还会用到这部分内容,所以这里就先不多讲了。

    敬请期待下篇

    相关文章

      网友评论

      • 房房1524:谢谢了 大兄弟 给你个start
      • JackDaddy:请问可以看一下源码吗
      • 南宫轩涵:楼主有qq吗?打印需要请教下
      • b6b8a8c72db7:楼主:有没同时作为Client 和Server 连接打印机的方式?因为小票机不会主动来连啊,作为Client 只能实现单连接,目前我想实现平板多连接怎么办?
      • 9a8ca4246bd4:请问楼主,如何获取打印机的连接状态
        tmyzh:我用GPrint那个SDK来实现功能的,不知道楼主了不了解,连接蓝牙的时候连接失败无具体返回值,这个让我无法知道连接失败原因
        VitaminChen:@carson123 这种蓝牙打印机的连接状态是不明确的,比如你连接上获取了socket,再把打印机关掉,此时获取socket的状态还是连接
      • 拾取灬回忆:写的非常好,赞一个
      • 虚通磨忍:获取打印机状态比如缺纸这个有么
      • a06f009b4607:可不可以加个Q,有问题请教,有关蓝牙连接设备进行通讯的问题,谢谢!
      • 9dc1b9f2bbe1:请教一个问题?
        我需要在传送完打印数据以后就关闭蓝牙连接。
        所以我这样调用
        mmOutStream.write(bytes);
        mmSocket.close();
        但是这个好像是异步的,一旦这样调用,打印数据传送就会立马中断,有没有什么办法解决?
        9dc1b9f2bbe1:@VitaminChen 嗯,我就是这么做的,write完flush一次,但如果不中断继续调用close打印就不完整
        VitaminChen:在 mmOutStream.write(bytes); 之后加一句 mmOutStream.flush() 试试看
      • 369caab2dfb2:我之前也做过蓝牙打印的demo,但感觉楼主分析的挺透彻的,可以分享一下源码吗?非常感谢
      • 76fdbfe1f352:LZ,请问一下怎么查询打印机状态,可以监听发送的字节是否打印完了不?谢谢
      • superoidlau:楼主,不知你在调用“mBluetoothSocket.connect();”的时候有没有报异常,见过这个异常吗:java.io.IOException: read failed, socket might closed or timeout, read ret: -1
        superoidlau:@蛋蛋大魔王 解决了,看一下谷歌写的蓝牙连接例子,
        蛋蛋大魔王: @ForeCheng 解决了吗?
      • superoidlau:蓝牙连接结果监听是否有回调呢??
        VitaminChen:@ForeCheng 没有,BluetoothSocket.connect()是个同步操作,函数正常返回就代表连接成功,如果抛异常就代表连接失败
      • George吴逸云:楼主,正在做一个app,蓝牙连接pos机,有些问题想咨询下你,可以加下你的QQ吗
        VitaminChen:@吴逸云 简信你了
      • 低吟浅唱1990:楼主有没有完整一点的代码? 还有有的文章是用BluetoothGatt 来行进连接的。 这两个有什么区别,什么情况下用哪个比较好。谢谢
      • next_discover:请问楼主,那个打印的模板是文本吗?并且那个排版是自己排版的吗?
        黄宏发:打印的是文本,需要自己排版的

      本文标题:Android 蓝牙连接 ESC/POS 热敏打印机打印(蓝牙连

      本文链接:https://www.haomeiwen.com/subject/pbzyrttx.html