Android藍牙相關—藍牙打印

一、概述

最近公司剛好遇到個藍牙打印的功能,以前實習時看到過類似功能,剛好這次自己實現,順便記錄一下。

二、基本環境

權限:

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

初始化藍牙適配器:

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);
}

該intent爲強制自動打開藍牙,看機型會有不同的情況,通常機型會彈出對話框詢問是否打開藍牙;也可以處理成跳轉到系統藍牙界面:

Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivityForResult(intent, REQUEST_SETTING);

打開藍牙後,最好實現onActivityResult方法,收到迴應後設置一些初始化工作:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case REQUEST_ENABLE_BT:
                initBluetooth();
                break;
            case REQUEST_SETTING:
                initBluetooth();
                break;
            default:
                break;
        }
    }

三、掃描配對連接

1.掃描:發現設備
打開藍牙之後,接下來就是掃描附近的藍牙設備:

mBluetoothAdapter.startDiscovery();

這是個異步過程,通常需要十多秒的時間,掃描的開始、發現、結束均有廣播,所以,我們需要註冊廣播,並在onStop或者onDestory時解除註冊:

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // 發現設備
            // 得到BluetoothDevice對象的意圖
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            BluetoothClass bluetoothClass = intent.getParcelableExtra(BluetoothDevice.EXTRA_CLASS);
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                // 掃描結束
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)){
          // 掃描開始      
            }
        }
    };

廣播中的intent包含一個BluetoothDevice對象,通過

intent.getParcelableExtra(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()

2.配對連接
2.1 配對:
當與一個設備第一次進行連接操作的時需先配對,屏幕會彈出提示框詢問是否允許配對,只有配對成功之後,才能建立連接。

系統會保存所有的曾經成功配對過的設備信息。所以在執行startDiscovery()之前,可以先嚐試查找已配對設備,因爲這是一個本地信息讀取的過程,所以比startDiscovery()要快得多,也避免佔用過多資源。如果設備在藍牙信號的覆蓋範圍內,就可以直接發起連接了。

查找已配對的設備代碼如下:

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

2.2 連接:
藍牙連接也是Client-Server模式,通過一個socket來進行數據傳輸,作爲一個android設備,會存在三種情況:

  1. 只作爲 Client 端發起連接
  2. 只作爲 Server 端等待別人發起建立連接的請求
  3. 同時作爲 Client 和 Server

    但這篇重點是藍牙打印,需要連接打印機,所以只能作爲client端,畢竟打印機不可能主動跟其他設備發起連接;另外兩種情況應該是ble通信會經常遇到的,後續有機會也會學習記錄。

連接步驟:
1.獲取一個BluetoothDevice對象,可通過掃描並監聽廣播獲取,也可以通過查詢已配對設備獲得,還可以通過mac地址獲取:

BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(bd.getAddress());

2.獲得BluetoothSocket對象:

BluetoothSocket mmSocket = device.createRfcommSocketToServiceRecord(UUID);

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 端設置的一樣!否則就會報錯!一些常見的藍牙服務協議已經有約定的 UUID。比如我們連接熱敏打印機是基於 SPP 串口通信協議,其對應的 UUID 是 “00001101-0000-1000-8000-00805F9B34FB”。其他常見的藍牙服務的UUID大家可以自行搜索。如果只是用於自己的應用之間的通信的話,那麼理論上可以隨便定義一個 UUID,只要 server 和 client 兩邊使用的 UUID 一致即可。

四、藍牙數據傳輸

連接成功之後,我們可以利用InputStream 和OutputStream進行數據的收發。

代碼如下:

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) { }
    }
}

五、藍牙打印

其實藍牙打印就是從BluetoothSocket 得到了一個OutputStream,然後不停的往裏面write數據。

手機通過藍牙向打印機發送的都是純字節流,打印機如何知道該打印什麼呢?這裏就要 ESC/POS 打印控制命令,詳情可以百度,通常用到的命令如下:

public static final byte[][] byteCommands = { { 0x1b, 0x40 },// 復位打印機 0
            { 0x1b, 0x4d, 0x00 },// 標準ASCII字體1
            { 0x1b, 0x4d, 0x01 },// 壓縮ASCII字體2
            { 0x1d, 0x21, 0x00 },// 字體不放大3
            { 0x1d, 0x21, 0x02 },// 寬高加倍4
            { 0x1d, 0x21, 0x11 },// 寬高加倍5
            { 0x1b, 0x45, 0x00 },// 取消加粗模式6
            { 0x1b, 0x45, 0x01 },// 選擇加粗模式7
            { 0x1b, 0x7b, 0x00 },// 取消倒置打印8
            { 0x1b, 0x7b, 0x01 },// 選擇倒置打印9
            { 0x1d, 0x42, 0x00 },// 取消黑白反顯10
            { 0x1d, 0x42, 0x01 },// 選擇黑白反顯11
            { 0x1b, 0x56, 0x00 },// 取消順時針旋轉90°12
            { 0x1b, 0x56, 0x01 },// 選擇順時針旋轉90°13
            { 0x1b, 0x61, 0x30 },// 左對齊14
            { 0x1b, 0x61, 0x01 },// 居中對齊15
            { 0x1b, 0x61, 0x32 },// 右對齊16
            { 0x1C, 0x21, 0x0C },// 設置倍寬倍高17
            { 0x1B, 0x61, 0x00 },// 取消居中18
            { 0x1C, 0x21, 0x00 },// 取消倍寬19
            { 0x0a },// 換行20
            { 0x1d, 0x56, 0x42, 0x01 },// 切紙21
            { 0x1C, 0x21, 0x08 },// 稍微小一點的倍寬22
            { 0x0D, 0x1B, 0x40 },// 23
            { 0x1A },// 24
            // { 0x1b, 0x69 },// 切紙
    };

注:

每次打印開始之前,都要對打印機進行初始化,發送指令如下:

mService.write(byteCommands[0]);

注:

通常打印機最大寬度爲256個像素點,可以根據這個值來設置格式,不推薦使用空格等來達到打印效果,另,需要打印字符串的話需要進行轉碼,如下:

mService.write(("測試藍牙打印").getBytes("GB2312"));

// 或者封裝下OutputStream
OutputStreamWriter writer = new OutputStreamWriter(outputStream, "GB2312");

六、總結

關於藍牙連接部分的線程管理代碼是網上很多博客採用的,之前項目中領導應該也是這樣借鑑的;通過線程來管理不同的連接狀態情況,建議實現藍牙打印功能時:藍牙連接線程管理封裝成一個工具類,同時保存當前連接狀態以便隨時打印;具體打印內容格式則封裝成另一個類,方便擴展。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章