Android設備的藍牙通信

設置藍牙

我們都知道,在手機的設置-藍牙中,可以進行藍牙設置的相關操作。

其實可以不離開自己的APP,直接完成藍牙設置的主要操作,可以結合自己的業務需求,相應地提示用戶開啓相關設置,提升用戶體驗。

首先要知道,藍牙連接需要知道待連接設備的MAC地址。

已配對設備的MAC地址是已知的,只要對方開啓了藍牙並在連接範圍內,就能連接成功。

未配對設備則需要通過搜索才能知道MAC地址,知道MAC地址後如果直接請求和對方建立連接 ,則系統會提示先進行配對,不需要我們再對此做處理。

下面開始操作藍牙,先添加藍牙權限:

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  ...
</manifest>

如果希望應用啓動設備發現或操作藍牙設置,則還必須聲明 BLUETOOTH_ADMIN 權限。

再獲取藍牙適配器:

mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

如果設備沒有藍牙,mBluetoothAdapter將爲null

開啓藍牙

if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

這將顯示對話框,請求用戶允許啓用藍牙。

onActivityResult() 回調中將收到請求結果

注意:啓用可檢測性將會自動啓用藍牙。

獲取已配對設備

mBluetoothAdapter.getBondedDevices();

獲取設備後就可以連接設備了。

搜索藍牙

搜索設備對於藍牙適配器而言是一個非常繁重的操作過程,並且會消耗大量資源。

因此要及時使用 cancelDiscovery() 停止發現。

if (mAdapter.isDiscovering()) {
            mAdapter.cancelDiscovery();
        }
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        if (mReceiver == null) {
            mReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    String action = intent.getAction();
                    if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                        //根據需求進行相應操作
                    } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                        //根據需求進行相應操作
                    }
                }
            };
        }
        mActivity.registerReceiver(mReceiver, filter);
        mAdapter.startDiscovery();

這裏app將會在發現設備和搜索結束時收到廣播,可以實現自己的業務需求。

開啓可檢測性

如果尚未在設備上啓用藍牙,則啓用設備可檢測性將會自動啓用藍牙。

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

以上代碼將顯示對話框,請求用戶允許將設備設爲可檢測到,時間持續 300 秒。

通過EXTRA_DISCOVERABLE_DURATION來定義不同的持續時間。 應用可以設置的最大持續時間爲 3600 秒,值爲 0 則表示設備始終可檢測到。 任何小於 0 或大於 3600 的值都會自動設爲 120 秒。

藍牙通信

原理

服務端

一臺設備保持開放的 BluetoothServerSocket 來充當服務器,偵聽傳入的連接請求,並在接受一個請求後提供已連接的 BluetoothSocket(與 TCP Socket 相似)。 從 BluetoothServerSocket 獲取 BluetoothSocket 後,可以(並且應該)捨棄 BluetoothServerSocket,除非需要接受更多連接。

BluetoothServerSocket mServerSocket= mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);

這將獲取一個BluetoothServerSocket

BluetoothSocket socket = mServerSocket.accept();

調用 accept() 開始偵聽連接請求,成功後返回一個BluetoothSocket

這是一個阻塞調用。它將在連接被接受或發生異常時返回。

客戶端

另一臺設備作爲客戶端,獲取遠程設備的 BluetoothDevice 對象後,與服務端建立連接:

BluetoothSocket mSocket = device.createRfcommSocketToServiceRecord(MY_UUID);

這將獲取BluetoothSocket這裏的UUID必須和遠端設備一樣,後續的連接才能成功。

通用唯一標識符 (UUID) 是用於唯一標識信息的字符串 ID 的 128 位標準化格式。

可以使用網絡上的衆多隨機 UUID 生成器之一,然後使用 fromString(String) 初始化一個 UUID。

接着調用

mSocket.connect();

該方法將會阻塞,直至成功或拋出異常。

如果成功,則兩臺設備的藍牙藍牙連接已經建立,每臺設備都可以從BluetoothSocket獲取輸入輸出流,進行數據傳輸。

藍牙聊天app淺析

這個app是Google的一個官方sample,地址點這裏

在兩臺設備上都裝了這個app後,就可以通過藍牙進行聊天了。

因爲是一個app,所以這個app既包含服務端代碼,也包含客戶端代碼。

連接狀態
// Constants that indicate the current connection state
    public static final int STATE_NONE = 0;       // we're doing nothing
    public static final int STATE_LISTEN = 1;     // now listening for incoming connections
    public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
    public static final int STATE_CONNECTED = 3;  // now connected to a remote device

這是sample中的4個連接狀態,分別表示空狀態、監聽狀態、連接ing狀態、已連接狀態。

剛進入app爲空狀態,如果藍牙開啓則開啓服務端並進入監聽狀態,客戶端進行連接時進入連接ing狀態,連接成功則進入已連接狀態。

退出app時設爲空狀態,關閉socket,清空所有工作線程(下面會講到的3個線程),此時會造成讀取異常,但不應再連接藍牙設備。

偵聽線程

線程名AcceptThread,進入app後就開啓該線程,用來偵聽傳入的連接請求,線程阻塞直至異常或接入成功。

偵聽異常則重啓服務端。

接入成功則開啓“管理連接線程”,同時設置連接狀態爲已連接,此時可以(並且應該)捨棄 BluetoothServerSocket,除非需要接受更多連接。

連接線程

線程名ConnectThread,客戶端開啓該線程來連接服務端。

連接失敗則重啓服務端。

連接成功也是開啓“管理連接線程”,同時設置狀態爲已連接。

管理連接線程

線程名ConnectedThread,這是所有流式傳輸讀取和寫入操作的專門線程。

因爲 read(byte[]) 和 write(byte[]) 方法都是阻塞調用。

read(byte[]) 將會阻塞,直至從流式傳輸中讀取內容。

write(byte[]) 通常不會阻塞,但如果遠程設備沒有足夠快地調用 read(byte[]),並且中間緩衝區已滿,則其可能會保持阻塞狀態以實現流量控制。

因此,線程中的主無限循環專門用於讀取 InputStream,如果讀取異常,說明藍牙中斷,此時可以提示用戶,並進行重連(app退出應關閉socket,也會造成讀取異常,此時不應重連)。

可使用線程中單獨的公共方法來發起對 OutputStream 的寫入操作。

序列化和反序列化

藍牙傳輸中,輸入輸出流傳輸的是字節,字節和String的轉換容易,但有時候不能很方便地獲取數據細節。

如果發送方把對象轉換爲字節傳送,接收方再把字節轉換爲對象,即實現序列化和反序列化,就很方便了。

下面介紹如何實現這樣的需求:

ObjectOutputStream

首先,收發雙方定義實體類A,實現Serializable接口。

注意:實體類A在收發雙方的路徑必須相同,否則ObjectOutputStream會轉換 失敗!

接着雙方就可以在輸出輸入流進行轉換,示例代碼如下:

A a = new A();
發送方
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
     ObjectOutputStream oos = new ObjectOutputStream(baos);
     oos.writeObject(a);
} catch (IOException e) {
     e.printStackTrace();
}
mOutStream.write(baos.toByteArray());
接收方
 mInStream.read(buffer);
 ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
 A a = null;
 try {
      ObjectInputStream ois = new ObjectInputStream(bais);
      try {
           a = (BluetoothPos) ois.readObject();
      } catch (ClassNotFoundException e) {
           e.printStackTrace();
      }
 } catch (IOException e) {
     e.printStackTrace();
 }

Json

類似於後臺與客戶端的網絡通信,來實現序列化和反序列化。

優點:沒有“實體類A在收發雙方的路徑必須相同”這一限制,更加靈活方便!

發送方

通過Gson將實體對象轉換爲json字符串,再以UTF-8編碼得到字節矩陣,最後流式輸出。

String json = new Gson().toJson(a);
try {
    mOutStream.write(json.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}
接收方
 mInStream.read(buffer);
 String json = "";
 try {
      json = new String(buffer, "UTF-8").trim();
 } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
 }
 JsonReader reader = new JsonReader(new StringReader(json));
 reader.setLenient(true);
 A a= new Gson().fromJson(reader, A.class);

注意:這裏的json字符串必須調用trim()方法去除空格並調用JsonReader處理,不然反序列化時會報異常MalformedJsonException

參考資料:https://developer.android.com/guide/topics/connectivity/bluetooth.html#ConnectingDevices

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章