Android 藍牙連接 ESC/POS 熱敏打印機打印(藍牙連接篇)

公司的一個手機端的 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) { }
    }
}

下一篇介紹通過手機操作熱敏打印機打印的時候,還會用到這部分內容,所以這里就先不多講了。

敬請期待下篇

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容