Android 藍牙連接 ESC/POS 熱敏打印機打印實例(藍牙連接篇)
公司的一個手機端的 CRM 項目最近要增加小票打印的功能,就是我們點外賣的時候經(jīng)常會見到的那種小票。這里主要涉及到兩大塊的知識:
- 藍牙連接及數(shù)據(jù)傳輸
- ESC/POS 打印指令
藍牙連接不用說了,太常見了,這篇主要介紹這部分的內(nèi)容。但ESC/POS 打印指令是個什么鬼?簡單說,我們常見的熱敏小票打印機都支持這樣一種指令,只要按照指令的格式向打印機發(fā)送指令,哪怕是不同型號品牌的打印機也會執(zhí)行相同的動作。比如打印一行文本,換行,加粗等都有對應的指令,這部分內(nèi)容放在下一篇介紹。
本篇主要基于官方文檔,相比官方文檔,省去了大段的說明,更加便于快速上手。
1. 藍牙權限
想要使用藍牙功能,首先要在 AndroidManifest 配置文件中聲明藍牙權限:
<manifest> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> ... </manifest>
BLUETOOTH
權限只允許建立藍牙連接以及傳輸數(shù)據(jù),但是如果要進行藍牙設備發(fā)現(xiàn)等操作的話,還需要申請 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); }
如果藍牙未打開,那么執(zhí)行 startActivityForResult()
后,會彈出一個對話框詢問是否要打開藍牙,點擊`是`之后就會自動打開藍牙。成功打開藍牙后就會回調(diào)到 onActivityResult()。
除了主動的打開藍牙,還可以監(jiān)聽 BluetoothAdapter.ACTION_STATE_CHANGED
廣播,包含EXTRA_STATE
和EXTRA_PREVIOUS_STATE
兩個 extra 字段,可能的取值包括 STATE_TURNING_ON
, STATE_ON, STATE_TURNING_OFF
, and STATE_OFF
。含義很清楚了,不解釋。
3. 發(fā)現(xiàn)設備
初始化完成之后,藍牙打開了,接下來就是掃描附近的設備,只需要一句話:
mBluetoothAdapter.startDiscovery();
不過這樣只是開始執(zhí)行設備發(fā)現(xiàn),這肯定是一個異步的過程,我們需要注冊一個廣播,監(jiān)聽發(fā)現(xiàn)設備的廣播,直接上代碼:
private final BroadcastReceiver mReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // 當有設備被發(fā)現(xiàn)的時候會收到 action == BluetoothDevice.ACTION_FOUND 的廣播 if (BluetoothDevice.ACTION_FOUND.equals(action)) { //廣播的 intent 里包含了一個 BluetoothDevice 對象 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //假設我們用一個 ListView 展示發(fā)現(xiàn)的設備,那么每收到一個廣播,就添加一個設備到 adapter 里 mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } } }; // 注冊廣播監(jiān)聽 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
注釋已經(jīng)寫的很清楚了,除了 BluetoothDevice.EXTRA_DEVICE
之外,還有一個 extra 字段 BluetoothDevice.EXTRA_CLASS
, 可以得到一個 BluetoothClass 對象,主要用來保存設備的一些額外的描述信息,比如可以知道這是否是一個音頻設備。
關于設備發(fā)現(xiàn),有兩點需要注意:
startDiscovery()
只能掃描到那些狀態(tài)被設為 可發(fā)現(xiàn) 的設備。安卓設備默認是不可發(fā)現(xiàn)的,要改變設備為可發(fā)現(xiàn)的狀態(tài),需要如下操作:
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); //設置可被發(fā)現(xiàn)的時間,00s intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); startActivity(intent);
執(zhí)行之后會彈出對話窗詢問是否允許設備被設為可發(fā)現(xiàn)的狀態(tài),點擊`是`之后設備即被設為可發(fā)現(xiàn)的狀態(tài)。
startDiscovery()
是一個十分耗費資源的操作,所以需要及時的調(diào)用cancelDiscovery()
來釋放資源。比如在進行設備連接之前,一定要先調(diào)用cancelDiscovery()
4. 設備配對與連接
4.1 配對
當與一個設備第一次進行連接操作的時候,屏幕會彈出提示框詢問是否允許配對,只有配對成功之后,才能建立連接。
系統(tǒng)會保存所有的曾經(jīng)成功配對過的設備信息。所以在執(zhí)行startDiscovery()
之前,可以先嘗試查找已配對設備,因為這是一個本地信息讀取的過程,所以比startDiscovery()
要快得多,也避免占用過多資源。如果設備在藍牙信號的覆蓋范圍內(nèi),就可以直接發(fā)起連接了。
查找配對設備的代碼如下:
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); if (pairedDevices.size() > 0) { for (BluetoothDevice device : pairedDevices) { mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } }
代碼很簡單,不解釋了,就是調(diào)用BluetoothAdapter.getBondedDevices()
得到一個 Set<BluetoothDevice>
并遍歷取得已配對的設備信息。
4.2 連接
藍牙設備的連接和網(wǎng)絡連接的模型十分相似,都是Client-Server 模式,都通過一個 socket 來進行數(shù)據(jù)傳輸。那么作為一個 Android 設備,就存在三種情況:
- 只作為 Client 端發(fā)起連接
- 只作為 Server 端等待別人發(fā)起建立連接的請求
- 同時作為 Client 和 Server
因為是為了下一篇介紹連接熱敏打印機打印做鋪墊,所以這里先講 Android 設備作為 Client 建立連接的情況。因為打印機是不可能主動跟 Android 設備建立連接的,所以打印機必然是作為 Server 被連接。
4.2.1 作為 Client 連接
- 首先需要獲取一個 BluetoothDevice 對象。獲取的方法前面其實已經(jīng)介紹過了,可以通過調(diào)用 startDiscovery()并監(jiān)聽廣播獲得,也可以通過查詢已配對設備獲得。
- 通過 BluetoothDevice.createRfcommSocketToServiceRecord(UUID) 得到BluetoothSocket 對象
- 通過BluetoothSocket.connect()建立連接
- 異常處理以及連接關閉
廢話不多說,上代碼:
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() { // 建立連接前記得取消設備發(fā)現(xiàn) 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 發(fā)起連接時傳入的 UUID 必須要和 Server 端設置的一樣!否則就會報錯!
如果是連接熱敏打印機這種情況,不知道 Server 端設置的 UUID 是什么怎么辦?
不用擔心,因為一些常見的藍牙服務協(xié)議已經(jīng)有約定的 UUID。比如我們連接熱敏打印機是基于 SPP 串口通信協(xié)議,其對應的 UUID 是 "00001101-0000-1000-8000-00805F9B34FB",所以實際的調(diào)用是這樣:
device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
其他常見的藍牙服務的UUID大家可以自行搜索。如果只是用于自己的應用之間的通信的話,那么理論上可以隨便定義一個 UUID,只要 server 和 client 兩邊使用的 UUID 一致即可。
4.2.2 作為 Server 連接
- 通過
BluetoothAdapter.listenUsingRfcommWithServiceRecord(String, UUID)
獲取一個 BluetoothServerSocket 對象。這里傳入的第一個參數(shù)用來設置服務的名稱,當其他設備掃描的時候就會顯示這個名稱。UUID 前面已經(jīng)介紹過了。 - 調(diào)用
BluetoothServerSocket.accept()
開始監(jiān)聽連接請求。這是一個阻塞操作,所以當然也要放在主線程之外進行。當該操作成功執(zhí)行,即有連接建立的時候,會返回一個BluetoothSocket 對象。 - 調(diào)用
BluetoothServerSocket.close()
會關閉監(jiān)聽連接的服務,但是當前已經(jīng)建立的鏈接并不會受影響。
還是看代碼吧:
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; } //直到有有連接建立,才跳出死循環(huán) if (socket != null) { //要在新開的線程執(zhí)行,因為連接建立后,當前線程可能會關閉 doSomething(socket); mmServerSocket.close(); break; } } } public void cancel() { try { mmServerSocket.close(); } catch (IOException e) { } } }
5. 數(shù)據(jù)傳輸
終于經(jīng)過了前面的4步,萬事俱備只欠東風。而最后這一部分其實是最簡單的,因為就只是簡單的利用 InputStream
和OutputStream
進行數(shù)據(jù)的收發(fā)。
示例代碼:
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 取數(shù)據(jù) while (true) { try { bytes = mmInStream.read(buffer); mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { break; } } } //向 Server 寫入數(shù)據(jù) public void write(byte[] bytes) { try { mmOutStream.write(bytes); } catch (IOException e) { } } public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } }
下一篇介紹通過手機操作熱敏打印機打印的時候,還會用到這部分內(nèi)容,所以這里就先不多講了。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Android實現(xiàn)PDF預覽打印功能
- Android gradle插件打印時間戳的方法詳解
- Android編程實現(xiàn)計算兩個日期之間天數(shù)并打印所有日期的方法
- Android中如何安全地打印日志詳解
- Mac 下 Android Studio 不打印日志的解決辦法
- Android jni調(diào)試打印char陣列的實例詳解
- Android下的POS打印機調(diào)用的簡單實現(xiàn)
- Android 藍牙連接 ESC/POS 熱敏打印機打印實例(ESC/POS指令篇)
- Android打印機--小票打印格式及模板設置實例代碼
- Android進階——安卓調(diào)用ESC/POS打印機打印實例
- Android手機通過藍牙連接佳博打印機的實例代碼
- Android實現(xiàn)系統(tǒng)打印功能
相關文章
Android ImageButton自定義按鈕的按下效果的代碼實現(xiàn)方法分享
這篇文章主要介紹了Android ImageButton自定義按鈕的按下效果的代碼實現(xiàn)方法,需要的朋友可以參考下2014-02-02Android中使用TextView實現(xiàn)高仿京東淘寶各種倒計時效果
今天給大家?guī)淼氖莾H僅使用一個TextView實現(xiàn)一個高仿京東、淘寶、唯品會等各種電商APP的活動倒計時。今天小編把實現(xiàn)代碼分享到腳本之家平臺,對android textclock 倒計時效果感興趣的朋友參考下吧2016-10-10flutter 自定義websocket路由的實現(xiàn)
這篇文章主要介紹了flutter 自定義websocket路由的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12Android Fragment的靜態(tài)注冊和動態(tài)注冊創(chuàng)建步驟
這篇文章主要介紹了Android Fragment的靜態(tài)注冊和動態(tài)注冊創(chuàng)建步驟,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02android實現(xiàn)在橫豎屏切換時頁面信息不被重置的示例分享
這篇文章主要介紹了android實現(xiàn)在橫豎屏切換時頁面信息不被重置的示例,需要的朋友可以參考下2014-02-02Android中創(chuàng)建對話框(確定取消對話框、單選對話框、多選對話框)實例代碼
這篇文章主要介紹了詳解Android中創(chuàng)建對話框(確定取消對話框、單選對話框、多選對話框)的相關資料,需要的朋友可以參考下2016-04-04