一、概述
最近公司剛好遇到個藍牙打印的功能,以前實習時看到過類似功能,剛好這次自己實現,順便記錄一下。
二、基本環境
權限:
<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設備,會存在三種情況:
- 只作爲 Client 端發起連接
- 只作爲 Server 端等待別人發起建立連接的請求
同時作爲 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");
六、總結
關於藍牙連接部分的線程管理代碼是網上很多博客採用的,之前項目中領導應該也是這樣借鑑的;通過線程來管理不同的連接狀態情況,建議實現藍牙打印功能時:藍牙連接線程管理封裝成一個工具類,同時保存當前連接狀態以便隨時打印;具體打印內容格式則封裝成另一個類,方便擴展。