序言:
Android平台支援了藍芽連線stack,允許裝置無線地和其他藍芽裝置交換資料。
app的框架透過Android Bluetooth APIs提供藍芽存取的機能,
這些這些API讓app能使用藍芽裝置來開啟點對點或多點間的無線功能。
使用藍芽的API,app可以做到下面幾件事:
a. 藍芽裝置掃描
b. 跟本地藍芽對接器要求尋找藍芽配對
c. 建立RFCOMM的頻道
d. 透過服務探索(service discovery)來連接其他裝置
e. 上/下傳輸資料至其他裝置
f. 管理多重的連線
註: RFCOMM通訊協定是基於L2CAP通訊協定上提供序列埠(RS-232)的模擬功能。
L2CAP -> Logical Link Control and Adaptation Protocol
The Basics:
這篇會描述Android Bluetooth APIs提供四個主要的必要工作,用來溝通藍芽連線:
a. 設定藍芽
b. 尋找當地的可用或已配對的裝置
c. 連結裝置
d. 裝置之前的資料傳送/接收
所有的藍芽API都在android.bluetooth的package裡提供。
底下是對於做藍芽連線所需的一些物件(classes)和介面(interfaces)。
BluetoothAdapter
代表本地的藍芽配對器,可以當作給藍芽交流的進入點。
透過它可以探索其他的藍芽裝置並尋求配對、
和已知MAC位址的裝置連線(實例化BluetoothDevice),
或開BluetoothServerSocket來監聽和其他裝置的溝通。
BluetoothDevice
代表遠端的藍芽裝置,用它來取得和遠端裝置的連線(透過BluetoothSocket),
或者要求取得裝置的資訊(名稱、位址、class、和連結狀態)
BluetoothSocket
代表類似TCP Socket的一個藍芽socket介面。這是連線的接點,
允許app和別的藍芽裝置交換資料(透過 InputStream 和 OutputStream)
BluetoothServerSocket
代表類似TCP的ServerSocket,在Android devices的連接中,
其中一個必須要當server(並在自己的class中開server socket)。
遠端的藍芽裝置發出連線要求時,如果連線被接受(accepted),
BluetoothServerSocket會回傳一個連線成功的BluetoothSocket。
BluetoothClass
描述了一個藍芽裝置整體的特性和機能。
這是一組定義裝置的主要和非主要的classes和services屬性(properties),
這並不是完全可靠地描述全部的藍芽裝置所能支援的服務,
但可以當作裝置種類的有用提示。
BluetoothProfile
代表藍芽資料(profile)的介面,可以定義裝置間藍芽連線的規範設定。
BluetoothHeadset
對手機的耳機支援。
BluetoothA2dp
高音質的藍芽連線,A2dp表"Advanced Audio Distribution Profile"
(這四個就別管它了......)
BluetoothHealth
代表一個操縱藍芽服務的Health Device(例如血壓計嗎? 我不知道= =)Profile的proxy。
BluetoothHealthCallback
抽象的class,用來實作前面的BluetoothHealth的callback。
BluetoothHealthAppConfiguration
對前面相應的app設定,第三方支援等。
BluetoothProfile.ServiceListener
和IPC clients相應的連/斷線相關
Bluetooth Permissions:
要使用藍芽功能,必須要最少開放這兩個權限:BLUETOOTH和BLUETOOTH_ADMIN。
BLUETOOTH權限用來執行藍芽溝通(要求/接受連線,以及傳輸資料)。
BLUETOOTH_ADMIN權限用來執行裝置探索或操作藍芽設定。因為一般來說,
藍芽裝置都需要去搜尋到當地(local)藍芽的裝置(阿不然怎麼用= =)
除了power manager當使用者請求時,會修改到藍芽需求。
定義藍芽的許可需求要在app中的manifest增加<uses-permission>。
例:
<manifest ... > <uses-permission android:name="android.permission.BLUETOOTH" /> ...
</manifest>
Setting Up Bluetooth:
開始之前要確認藍芽在裝置上是否支援且打開。
不支援的話當然你可以優雅地(XDDDD)關掉所有藍芽功能囉!但如果支援卻是關閉的話,
app會在要用到藍芽功能時對使用者進行request(不會離開程式)。
設定上是使用BluetoothAdapter。
1. 取得匹配器
對每個藍芽的activity來說,BluetoothAdapter都是必要的。
使用static的getDefaultAdapter()來拿到裝置自己的匹配器,
接下來就將它當成一個物件來互動就可以了。
如果回傳是null表示這台裝置不支援藍芽,那你的故事就到此為止Game Over了喵!
舉例來說:
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null) { // Device does not support Bluetooth }
2. 打開藍芽
接著我們要確保藍芽是打開的,透過isEnabled()來確認現況,若是false的話我們就得打開它。
使用startActivityForResult()並將ACTION_REQUEST_ENABLE的action Intent丟進去。
在app的表現就是會出現一個request要求使用者在app中授權打開藍芽(而不會跳出程式)
舉例來說:
if (!mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
對話框會像上面那張圖一樣出現要求授權。
REQUEST_ENABLE_BT常數是本地定義的大於0的integer,也就是,
系統在你onActivityResult()的實作中回傳的requestCode參數。
成功的話我們會得到一個RESULT_OK的result code,否則會得到RESULT_CANCELED。
接下來這邊是可做可不做的,你的app也可以監聽ACTION_STATE_CHANGED的廣播Intent,
因為系統在藍芽狀態改變時會發出廣播。當中會包含extra fields EXTRA_STATE和
EXTRA_PREVIOUS_STATE,分別包含新舊兩個藍芽的狀態。
可能的值有:
STATE_TURNING_ON、STATE_ON、STATE_TURNING_OFF、STATE_OFF。
當app在執行時監聽這個廣播有助於偵測藍芽狀態的變動。
Querying paired devices:
在裝置探索前先去query那些配對的(paired)裝置會是一個挺值得的選擇。
我們可以使用getBondedDevices()來達成。
這函式會回傳一組代表配對的BluetoothDevices,
舉例來說,我們可以用ArrayAdapter來顯示出每個裝置的名稱:
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); // If there are paired devices if (pairedDevices.size() > 0) { // Loop through paired devices for (BluetoothDevice device : pairedDevices) { // Add the name and address to an array adapter to show in a ListView mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } }
所有從BluetoothDevice的Object所需用來開始一個連線的就只有MAC address而已。
上面範例中這項資訊和裝置名稱被一起存到mArrayAdapter。
之後MAC address可以被分離出來用作開始連線。
Discovering devices:
開始探索 -> startDiscovery()
它會回傳一個boolean來表探索是否成功開始,探索一般約12秒,
並回傳一頁找到的裝置及取得的藍芽名稱。
你的app必須註冊一個BroadcastReceiver給ACTION_FOUND Intent,
來接收每個被發現的裝置的資訊。
對每個(被發現的)裝置來說系統會廣播ACTION_FOUND的Intent,
Intent中帶了extra fields: EXTRA_DEVICE和EXTRA_CLASS,
分別包含了一個BluetoothDevice和一個BluetoothClass。
舉例來說:
// Create a BroadcastReceiver for ACTION_FOUND private final BroadcastReceiver mReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // When discovery finds a device if (BluetoothDevice.ACTION_FOUND.equals(action)) { // Get the BluetoothDevice object from the Intent BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // Add the name and address to an array adapter to show in a ListView mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); } } }; // Register the BroadcastReceiver IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
同樣的,所有從BluetoothDevice的Object所需用來開始一個連線的就只有MAC address而已。
上面範例中這項資訊和裝置名稱被一起存到mArrayAdapter。
之後MAC address可以被分離出來用作開始連線。
(這點完全跟上一個小標題重複)
值得注意的是裝置探索對藍芽匹配器是很吃重的工作,且會吃掉很大量的資源。
如果要關掉可以使用cancelDiscovery()。
注意在連線的時候因為探索會大量降低頻寬,所以這時別開著探索。
Eabling discoverability:
如果想讓本地裝置能被探索到->startActivityForResult(Intent, int),用ACTION_REQUEST_DISCOVERABLE的action Intent。
這也會像前面的模式一樣,要求開啟權限來進行接下來的能被探索的功能。
預設為120秒,可以用extra加進EXTRA_DISCOVERABLE_DURATION。
最常是3600秒,0的話表設定成一直可被探索不關掉。
下面的範例設定為300秒:
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); startActivity(discoverableIntent);
同樣如果使用者不同意的話onActivityResult()會回傳RESULT_CANCELLED。
我們也可以監聽ACTION_SCAN_MODE_CHANGED的Intent來偵測目前的狀況。
得到的狀態會有三種,分別表可見可連結、可連結、不可見,
就不再贅述。
Connecting Devices:
為了在你的app中連結兩個裝置,server和client都得要實作,
因為必然要是一邊server一邊client。client是拿server的MAC位址來開始連線。
當兩個間有一個BluetoothSocket在同一個RFCOMM頻道上時,
我們將兩者視為是彼此連結的。
當兩邊都擁有server機制的時候,兩邊都可以開始連線並且作為client(另一邊則作為server)。
Connecting as a server:
Server必須要拿著一個開放的BluetoothServerSocket,
監聽進來的連線請求。當其中有被接受的時候,提供一個連線的BluetoothSocket。
(用來跟Client溝通的管道)
接著Server就可以丟掉了,除非你要接受更多的連線。
對藍芽來說,是使用listenUsingRfcommWithServiceRecord(String, UUID)來取得BluetoothServerSocket的。(UUID是一個128-bit format的string ID)
大致上:
拿到ServerSocket->開始嘗試accept->接到socket後就透過其開始溝通client。
除了拿到ServerSocket方式不一樣以外,
其他基本上和Java TCP的連線作法如出一轍。
範例:
private class AcceptThread extends Thread { private final BluetoothServerSocket mmServerSocket; public AcceptThread() { // Use a temporary object that is later assigned to mmServerSocket, // because mmServerSocket is final BluetoothServerSocket tmp = null; try { // MY_UUID is the app's UUID string, also used by the client code tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID); } catch (IOException e) { } mmServerSocket = tmp; } public void run() { BluetoothSocket socket = null; // Keep listening until exception occurs or a socket is returned while (true) { try { socket = mmServerSocket.accept(); } catch (IOException e) { break; } // If a connection was accepted if (socket != null) { // Do work to manage the connection (in a separate thread) manageConnectedSocket(socket); mmServerSocket.close(); break; } } } /** Will cancel the listening socket, and cause the thread to finish */ public void cancel() { try { mmServerSocket.close(); } catch (IOException e) { } } }
值得注意的是manageConnectedSocket()是一個fictional method,
它會開一個thread來做資料傳輸。
然後最後當然不要忘記關掉連線。
Connecting as a client:
client取得socket的方式是使用BluetoothDevice的createRfcommSocketToServiceRecord(UUID),
來得到一個回傳的BluetoothSocket。
然後接著要藉由connet()來連線,一般而言如果過12秒的話就會視為time out並丟出例外。
範例如下:
private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { // Use a temporary object that is later assigned to mmSocket, // because mmSocket is final BluetoothSocket tmp = null; mmDevice = device; // Get a BluetoothSocket to connect with the given BluetoothDevice try { // MY_UUID is the app's UUID string, also used by the server code tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { } mmSocket = tmp; } public void run() { // Cancel discovery because it will slow down the connection mBluetoothAdapter.cancelDiscovery(); try { // Connect the device through the socket. This will block // until it succeeds or throws an exception mmSocket.connect(); } catch (IOException connectException) { // Unable to connect; close the socket and get out try { mmSocket.close(); } catch (IOException closeException) { } return; } // Do work to manage the connection (in a separate thread) manageConnectedSocket(mmSocket); } /** Will cancel an in-progress connection, and close the socket */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } }
注意在cancelDiscovery()是在連線前叫的,就如上面講過的相關問題,
在確保有連線要開始的時候就該先關掉探索。
而且這個函式不論原本有沒有關探索都可以安全的呼叫。
manageConnectedSocket()和close()和在server裡的用意是一樣的。
Managing a Connection:
這裡同樣跟Java TCP非常相似,我們透過InputStream和OutputStream來進行資料傳輸,
然後一般是使用read(byte[])和write(byte[])來取得串流。
(如果真的需要的話也可以再外加上BufferedReader和BufferedWriter)
範例:
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; // Get the input and output streams, using temp objects because // member streams are final 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() // Keep listening to the InputStream until an exception occurs while (true) { try { // Read from the InputStream bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI activity mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { break; } } } /* Call this from the main activity to send data to the remote device */ public void write(byte[] bytes) { try { mmOutStream.write(bytes); } catch (IOException e) { } } /* Call this from the main activity to shutdown the connection */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } }
建構子(constructor)會去要求必要的串流,且一旦執行,
thread就會開始等資料從InputStream進來。(並且read)
往外送的資料則是write()的method(並且write)。
同樣不要忘記要在cancel()的地方將socket做關閉。
Working with Profiles:
從Android 3.0版本開始Bluetooth API包含了支援使用Bluetooth profiles的做法。
Bluetooth profile就是一個無線的介面用來定位裝置間基於藍芽的溝通。
比如免持的(Hands-Free)profile。對於手機連線無線耳機來說,
兩個裝置都必須要支援Hands-Free profile。
如同最上面介紹的,Android Bluetooth API提供了實作介面給下面的Bluetooth profiles:
Headset(耳機、免持)、A2DP(無線高音質的藍芽串流)、
Heath Device(API leve 14,即4.0以上支援,可以連接心跳、血液、熱感等等的偵測器)
使用profile的基礎的步驟:
1. 拿預設的匹配器
2. 用getProfileProxy()來建立連線並關聯profile(下面的例子是跟Headset)
3. 設置好一個BluetoothProfile.ServiceListener。當連結或斷線時,
這監聽器會發出通知給BluetoothProfile IPC clients。
4. 在onServiceConnected()裡,處理profile proxy object。
5. 一旦拿到了profile proxy object,你可以用它來監控連線的狀況,
並且執行其他的相關於該profile的操作。
下面範例介紹連線到一個Bluetooth proxy object以控制Headset profile的方法:
BluetoothHeadset mBluetoothHeadset; // Get the default adapter BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // Establish connection to the proxy. mBluetoothAdapter.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET); private BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() { public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile == BluetoothProfile.HEADSET) { mBluetoothHeadset = (BluetoothHeadset) proxy; } } public void onServiceDisconnected(int profile) { if (profile == BluetoothProfile.HEADSET) { mBluetoothHeadset = null; } } }; // ... call functions on mBluetoothHeadset // Close proxy connection after use. mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);
Vendor-specific AT commands:
從Android 3.0版本開始,app可以註冊接收先由耳機傳送,
廠商定義的AT commands的系統廣播。
接收ACTION_VENDOR_SPECIFIC_HEADSET_EVENT intent的廣播可以處理之。
Health Device Profile:
Android 4.0版本引入了對於藍芽健康Profile(HDP)的支援。
這可以讓你做出可以連線溝通Health Device並支援藍芽的app。
對於其API的概念,可以參照如下表格:
Concept | Description |
---|---|
Source | A role defined in HDP. A source is a health device that transmits medical data (weight scale, glucose meter, thermometer, etc.) to a smart device such as an Android phone or tablet. |
Sink | A role defined in HDP. In HDP, a sink is the smart device that receives the medical data. In an Android HDP application, the sink is represented by a BluetoothHealthAppConfiguration object. |
Registration | Refers to registering a sink for a particular health device. |
Connection | Refers to opening a channel between a health device and a smart device such as an Android phone or tablet. |
如果要做一個HDP的app:
1. 取得BluetoothHealth proxy object的參照(reference)
2. 新開一個BluetoothHealthCallback,
並註冊app configuration(BluetoothHealthAppConfiguration)作為health sink。
3. 和HD做連線,一些裝置會初始連線,對那些裝置而言這步就不需要執行了。
4. 當成功連結了HD,使用file descriptor來讀寫資料。
取得的資料必須要使用以IEEE 11073-xxxxx規範來實作的health manager來轉譯。
5. 當結束時,關掉health channel並反註冊app。當沒額外活動時,channel也會被關掉。
沒有留言:
張貼留言