近期的項(xiàng)目涉及到藍(lán)牙通訊,于是就整理了一下藍(lán)牙的通訊機(jī)制的知識點(diǎn)学歧。
藍(lán)牙通訊主要是配對和連接兩個過程罩引。
配對和連接是兩個不同的概念,請不要混為一談枝笨,配對上的設(shè)備不代表已經(jīng)連接袁铐。
首先我們需要權(quán)限
<manifest ... >
<uses-permission android:name="android.permission.BLUETOOTH" /> ...
</manifest>
BluetoothAdapter
代表本地藍(lán)牙適配器(藍(lán)牙無線電)。BluetoothAdapter
是所有藍(lán)牙交互的入口伺帘。使用這個你可以發(fā)現(xiàn)其他藍(lán)牙設(shè)備昭躺,查詢已配對的設(shè)備列表,使用一個已知的MAC地址來實(shí)例化一個BluetoothDevice
伪嫁。
//通常我們使用該方法獲得藍(lán)牙的本地屬性领炫。
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// 代表設(shè)備不支持藍(lán)牙
}
BluetoothDevice
代表一個遠(yuǎn)程藍(lán)牙設(shè)備,使用這個來請求一個與遠(yuǎn)程設(shè)備的BluetoothSocket
連接张咳,或者查詢關(guān)于設(shè)備名稱帝洪、地址似舵、類和連接狀態(tài)等設(shè)備信息。
//通過mac地址來獲得遠(yuǎn)程藍(lán)牙設(shè)備葱峡,通常我們也使用查找設(shè)備的廣播來獲得遠(yuǎn)程藍(lán)牙設(shè)備砚哗,稍后會介紹
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddress);
//該類的方法與adapter類似
開啟藍(lán)牙
(以下的mBluetoothAdapter
表示本地藍(lán)牙設(shè)備,mBluetoothDevice
表示遠(yuǎn)程藍(lán)牙設(shè)備)
方法一:
//利用系統(tǒng)設(shè)置開啟藍(lán)牙
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600);
startActivity(discoverableIntent);
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
方法二
mBluetoothAdapter.enable()
//需要權(quán)限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
藍(lán)牙搜尋廣播
Action 靜態(tài)注冊
<intent-filter>
<action android:name="android.bluetooth.device.action.FOUND" />
</intent-filter>
動態(tài)注冊
// 定義一個廣播 for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// 當(dāng)搜尋到設(shè)備
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
//獲得遠(yuǎn)程設(shè)備信息
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 保存設(shè)備的mac地址與藍(lán)牙名稱
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
};
// 動態(tài)注冊代碼
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);
//不要忘了在onDestroy注銷廣播喲
掃描代碼
mBluetoothAdapter.startDiscovery();//開始掃描
//藍(lán)牙掃描是非常耗費(fèi)資源與時間的砰奕,當(dāng)我們掃描到需要操作的設(shè)備的時候蛛芥,我們需要停止掃描來獲得更好的連接效率。
mBluetoothAdapter.cancelDiscovery();//取消掃描
設(shè)置藍(lán)牙可被搜索
Intent discoverableIntent = newIntent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);//后面的參數(shù)是可被發(fā)現(xiàn)的時間军援,最大支持3600秒
startActivity(discoverableIntent);
藍(lán)牙配對
如果只是單純的配對仅淑,我們可以利用反射:
(這里將會介紹兩張配對方式,反射是第一種胸哥,第二種是通過連接來配對涯竟,這里的配對都不需要輸入pin
碼的)
try {
//檢查是否處于未配對狀態(tài)
if (mBluetoothDevice.getBondState() == BluetoothDevice.BOND_NONE) {
Method creMethod = BluetoothDevice.class.getMethod("createBond");
Log.e("TAG", "開始配對");
creMethod.invoke(mBluetoothDevice);
}
} catch (Exception e) {
// TODO: handle exception
//DisplayMessage("無法配對!");
e.printStackTrace();
}
取消配對
static public boolean removeBond(Class btClass, BluetoothDevice btDevice)
throws Exception
{
Method removeBondMethod = btClass.getMethod("removeBond");
Boolean returnValue = (Boolean) removeBondMethod.invoke(btDevice);
return returnValue.booleanValue();
}
連接與配對
是的藍(lán)牙的連接類似
TCP的socket
連接空厌,但是不同的是藍(lán)牙的BluetoothSock
不是new出來的庐船,而是通過一個靜態(tài)方法根據(jù)UUID
生成的,而不是端口號嘲更。同一時間一個通道只允許一個socket
連接通訊(多線程或許能解決這個問題)筐钟,但是大多藍(lán)牙情景都是一對一的連接。
為了在兩臺設(shè)備上創(chuàng)建一個連接赋朦,你必須實(shí)現(xiàn)服務(wù)器端和客戶端兩頭的機(jī)制盗棵,因?yàn)橐粋€設(shè)備必須打開一個服務(wù)器socket
,而另一個設(shè)備初始化創(chuàng)建(使用服務(wù)器設(shè)備的MAC地址來初始化一個連接)北发。當(dāng)他們在相同的RFCOMM
通道上有一個已連接的BluetoothSocket
時纹因,服務(wù)器和客戶被認(rèn)為是互相連接了。
**注意:如果兩臺設(shè)備之前沒有配對過琳拨,那么Android框架將會自動顯示一個請求配對的通知或?qū)υ捒虿t恰。因此,?dāng)嘗試連接設(shè)備時狱庇,你的應(yīng)用不需要考慮設(shè)備是否配對過惊畏。你的RFCOMM
連接嘗試將會阻塞,知道用戶成功配對密任,或者用戶拒絕失敗時颜启,或者配對失敗,或者超時
**
BluetoothSocket
代表一個藍(lán)牙socket
的接口(和TCP Socket
類似)浪讳。這是一個連接點(diǎn)缰盏,它允許一個應(yīng)用與其他藍(lán)牙設(shè)備通過InputStream
和OutputStream
交換數(shù)據(jù)。
BluetoothServerSocket secure = null;
if(sdk>=10){
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}else{
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}
//UUID可以參考網(wǎng)上內(nèi)容自己生成
UUID
一個全局唯一的標(biāo)識符(
UUID
)是一個標(biāo)準(zhǔn)的128-bit格式的string ID
,它被用于唯一標(biāo)識信息口猜。一個UUID
的關(guān)鍵點(diǎn)是它非常大以至于你可以隨機(jī)選擇而不會發(fā)生崩潰负溪。在這種情況下,它被用于唯一地指定你的應(yīng)用中的藍(lán)牙服務(wù)济炎。為了得到一個UUID
以在你的應(yīng)用中使用川抡,你可以使用網(wǎng)絡(luò)上的任何一種隨機(jī)UUID產(chǎn)生器,然后使用fromString(String)
初始化一個UUID
须尚。
BluetoothServerSocket
代表一個開放的服務(wù)器socket
崖堤,它監(jiān)聽接受的請求(與TCP ServerSocket
類似)。為了連接兩臺Android設(shè)備耐床,一個設(shè)備必須使用這個類開啟一個服務(wù)器socket
倘感。當(dāng)一個遠(yuǎn)程藍(lán)牙設(shè)備開始一個和該設(shè)備的連接請求,BluetoothServerSocket
將會返回一個已連接的BluetoothSocket
咙咽,接受該連接。
BluetoothSocket socket;
if (sdk < 10) {
socket = device.createRfcommSocketToServiceRecord(BlueToothControl.SECURE_UUID);
} else {//sdk >= 10
socket = device.createInsecureRfcommSocketToServiceRecord(BlueToothControl.INSECURE_UUID);
}
//UUID可以參考網(wǎng)上內(nèi)容自己生成
下面我會舉一個栗子淤年,并且說明是如何進(jìn)行通訊的
服務(wù)端
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
// 使用一個臨時對象來標(biāo)志mmServerSocket,
// 因?yàn)閙mServerSocket是final類型
BluetoothServerSocket tmp = null;
try {
// MY_UUID 是應(yīng)用的 UUID string, 同樣也在客戶端使用
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// 保持監(jiān)聽直到建立連接或者發(fā)生異常
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
break;
}
// 如果接收到連接
if (socket != null) {
// 用一個線程去做一些連接的管理工作
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
//該方法用于中斷監(jiān)聽并且斷開連接
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) { }
}
}
- 用
listenUsingRfcommWithServiceRecord(String, UUID)
得到一個BluetoothServerSocket
钧敞。這個String是你的服務(wù)的標(biāo)志名稱,系統(tǒng)將會把它寫入設(shè)備中的一個新的服務(wù)發(fā)現(xiàn)協(xié)議(SDP
)數(shù)據(jù)庫條目中(名字是任意的麸粮,并且可以只是你應(yīng)用的名字)溉苛。UUID
同樣被包含在SDP
條目中,并且將會成為和客戶端設(shè)備連接協(xié)議的基礎(chǔ)弄诲。也就是說愚战,當(dāng)客戶端嘗試連接這個設(shè)備時,它將會攜帶一個UUID
用于唯一指定它想要連接的服務(wù)器齐遵。這些UUIDs
必須匹配以便該連接可以被接受(在下一步中)寂玲。 通過調(diào)用accept()
開始監(jiān)聽連接請求。
- 通過調(diào)用
accept()
開始監(jiān)聽連接請求梗摇。這一個阻塞調(diào)用拓哟。在一個連接被接受或一個異常出現(xiàn)時,它將會返回伶授。只有當(dāng)一個遠(yuǎn)程設(shè)備使用一個UUID
發(fā)送了一個連接請求断序,并且該UUID和正在監(jiān)聽的服務(wù)器socket
注冊的UUID
相匹配時,一個連接才會被接受糜烹。成功后违诗,accept()
將會返回一個已連接的BluetoothSocket
。
- 調(diào)用
close()
疮蹦,除非你想要接受更多的連接诸迟。這將釋放服務(wù)器socket
和它所有的資源,但是不會關(guān)閉accept()
返回的已連接的BluetoothSocket
。不同于TCP/IP
亮蒋,RFCOMM
僅僅允許每一個通道上在某一時刻只有一個已連接的客戶端扣典,因此在大多數(shù)情況下在接受一個已連接的socket
后,在BluetoothServerSocket
上調(diào)用close()
是非常必要的慎玖。
-
accept()
不應(yīng)該再主活動UI
線程上執(zhí)行贮尖,因?yàn)樗且粋€阻塞調(diào)用,并且將會阻止任何與應(yīng)用的交互行為趁怔。它通常在你的應(yīng)用管理的一個新的線程中使用一個BluetoothServerSocket
或BluetoothSocket
來完成所有工作湿硝。為了中止一個阻塞調(diào)用,例如accept()
润努,從你的其他線程里在BluetoothServerSocket
(或BluetoothSocket
) 上調(diào)用close()
关斜,然后阻塞調(diào)用就會立即返回。注意在BluetoothServerSocket
或BluetoothSocket
上所有的方法都是線程安全的铺浇。
客戶端
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
//使用臨時變量來標(biāo)志mmSocket,
// 因?yàn)?mmSocket 是 final
BluetoothSocket tmp = null;
mmDevice = device;
// 通過得到的BluetoothDevice 獲取 BluetoothSocket來連接
try {
// MY_UUID 是應(yīng)用的UUID string, 同樣也用于服務(wù)端
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mmSocket = tmp;
}
public void run() {
// 你應(yīng)該在連接前總是這樣做痢畜,而不需要考慮是否真的有在執(zhí)行查詢?nèi)蝿?wù)(但是如果你想要檢查,調(diào)用 isDiscovering())
//檢查會很大程度影響效率
mBluetoothAdapter.cancelDiscovery();
try {
// 通過socket.connect()來連接. 同時也會阻塞線程
// 直到連接成功或者拋出異常
mmSocket.connect();
} catch (IOException connectException) {
// 無法連接鳍侣,關(guān)閉socket并退出
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
//做一些管理socket的工作
manageConnectedSocket(mmSocket);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
為了和一個遠(yuǎn)程設(shè)備(一個持有服務(wù)器
socket
的設(shè)備)初始化一個連接丁稀,你必須首先得到一個BluetoothDevice
對象來表示這個遠(yuǎn)程設(shè)備。(上面的課程Finding Devices
講述了如何得到一個BluetoothDevice
)倚聚。然后你必須使用BluetoothDevice
來得到一個BluetoothSocket
线衫,然后初始化該連接。
下面是基本的過程:
- 使用
BluetoothDevice
惑折,通過調(diào)用createRfcommSocketToServiceRecord(UUID)
來得到一個BluetoothSocket
授账。T這將初始化一個BluetoothSocket
,它連接到該BluetoothDevice
惨驶。這里傳遞的UUID必須和服務(wù)器設(shè)備開啟它的BluetoothServerSocket
時使用的UUID
相匹配白热。
- 通過調(diào)用
connect()
初始化一個連接。執(zhí)行這個調(diào)用時粗卜,系統(tǒng)將會在遠(yuǎn)程設(shè)備上執(zhí)行一個SDP
查找工作棘捣,來匹配UUID
。如果查找成功休建,并且遠(yuǎn)程設(shè)備接受了連接乍恐,它將會在連接過程中分享RFCOMM
通道,而connect()
將會返回测砂。這個方法是阻塞的茵烈。如果,處于任何原因砌些,該連接失敗了或者connect()
超時了(大約12秒以后)呜投,那么它將會拋出一個異常加匈。
因?yàn)?code>connect()是一個阻塞調(diào)用,這個連接過程應(yīng)該總是在一個單獨(dú)的線程中執(zhí)行仑荐。
注意:你應(yīng)該總是確保在你調(diào)用connect()
時設(shè)備沒有執(zhí)行設(shè)備查找工作雕拼。如果正在查找設(shè)備,那么連接嘗試將會很大程度的減緩粘招,并且很有可能會失敗啥寇。
**當(dāng)你使用完你的 BluetoothSocket
后,總是調(diào)用close()
來清除資源洒扎。這樣做將會立即關(guān)閉已連接的socket
辑甜,然后清除所有的內(nèi)部資源
**
讀寫流
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的流信息
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; //緩沖字符數(shù)組
// 保持通訊
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// 發(fā)送包含的信息給UI
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}
/* 調(diào)用該方法去發(fā)送信息 */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
/* 調(diào)用該方法關(guān)閉連接*/
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
**當(dāng)然,實(shí)現(xiàn)的細(xì)節(jié)需要考慮袍冷。首先并且最重要的是磷醋,你應(yīng)該為所有輸入和輸出的數(shù)據(jù)流使用一個專屬的線程。這是十分重要的胡诗,因?yàn)?code>read(byte[]) 和 write(byte[])
方法都是阻塞調(diào)用邓线。 read(byte[])
將會發(fā)生阻塞知道送數(shù)據(jù)流中讀取到了一些東西。write(byte[])
不經(jīng)常發(fā)生阻塞煌恢,但是當(dāng)遠(yuǎn)程設(shè)備沒有足夠迅速地調(diào)用read(byte[])
而中間緩沖區(qū)已經(jīng)負(fù)載時可以阻塞骇陈。因此,你的線程中的主要循環(huán)應(yīng)該是專門從InputStream
中讀取數(shù)據(jù)的症虑。一個單獨(dú)的公共方法可以被用于初始化向OutputStream
中寫入數(shù)據(jù)。
**
如文中有錯誤归薛,歡迎指出谍憔。