關於藍牙通信的開發實踐

關於藍牙通信的開發

  1. 權限問題:

    1. 需要權限如下:

      <uses-permission android:name="android.permission.BLUETOOTH"/>
      <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
      <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
      
    2. 並且需要在代碼中動態進行申請(萬惡的權限限制a)

    3. if (Build.VERSION.SDK_INT >= 23) {    
          if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) 
              != PackageManager.PERMISSION_GRANTED){       
              ActivityCompat.requestPermissions(MainActivity.this,              
                           new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 10);    
          }
      }
      
  2. 打開藍牙

    1. 打開藍牙主要使用Intent進行申請

      Toast.makeText(context, "藍牙未打開", Toast.LENGTH_SHORT).show();
      Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);//請求打開藍牙
      context.startActivity(intent);
      

      當調用上面申請後,系統會彈出是否打開藍牙。選擇是就可以了。

  3. 查找附近已匹配或可用設備

    這一步驟分爲兩類:一是已匹配的設備,而是可用設備。

    1. 已匹配設備:以前跟當前藍牙設備已經做過了匹配操作,當前藍牙設備記住了其藍牙信息,包括地址MAC、Name等等

      /**
       * 獲取已經配對的藍牙信息
       * adapter是藍牙的適配器BluetoothAdapter, 代表本地藍牙適配器(藍牙無線電)。BluetoothAdapter是所有藍牙交互的入口。使用這個你可以發現其他藍牙設備,查詢已配對的設備列表,使用一個已知的MAC地址來實例化一個BluetoothDevice,以及創建一個BluetoothServerSocket來爲監聽與其他設備的通信。
       * 獲取方式爲 BluetoothAdapter.getDefaultAdapter()
       */
      public ArrayList<String> getAdaptedBlueTooth() {
          Toast.makeText(context, "開始獲取", Toast.LENGTH_SHORT).show();
          bluetoothList = new ArrayList<>();
          if (adapter != null) {//是否有藍牙設備
              if (adapter.isEnabled()) {//藍牙設備是否可用
                  Set<BluetoothDevice> devices = adapter.getBondedDevices();//獲取到已經匹配的藍牙對象
                  if (devices.size() > 0) {
                      for (Iterator iterator = devices.iterator(); iterator.hasNext(); ) {
                          BluetoothDevice device = (BluetoothDevice) iterator.next();
                          bluetoothList.add(device.getAddress() + "設備名:" + device.getName());
                      }
                      return bluetoothList;
                  } else {
                      Toast.makeText(context, "沒有綁定的藍牙設備", Toast.LENGTH_SHORT).show();
                      searchForAvailableBluetooth();
                  }
              } else {
                  Toast.makeText(context, "藍牙未打開", Toast.LENGTH_SHORT).show();
                  Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);//請求打開藍牙
                  context.startActivity(intent);
              }
          } else {
              Toast.makeText(context, "沒有發現藍牙", Toast.LENGTH_SHORT).show();
          }
          return null;
      }
      
    2. 附近可用藍牙設備:發現附近可用藍牙設備,在Android中發現設備是一個過程,通過註冊receiver對響應的信息進行獲取。只有在附近藍牙設備是可被檢測的狀態下才會迴應發現請求,分享出自身的一些屬性值,如:設備名稱,類,MAC地址等

      //首先第一步是創建一個接收回應信息的廣播,並且對信息進行相應的處理 
      //第二步,註冊廣播,並且開啓掃描,開啓掃描只需要調用startDiscovery(),成功返回true,失敗返回false。
      //第三步,記得關閉掃描後,也要把廣播註冊掉
      /** 
       * 創建廣播,接收掃描到的藍牙信息
       */
      private class BluetoothReceiver extends BroadcastReceiver {
      
          /**
          * 這個receive每一次掃描到都會被調用
          * @param context
          * @param intent
          */
          @Override
          public void onReceive(Context context, Intent intent) {
              BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
              bluetoothList.add(device.getAddress() + "設備名:" + device.getName());//接收穫取到的藍牙地址信息
              //這裏可以使用handler進行數據的傳遞,更新UI界面
          }
      }
      /**
      * 註冊一個廣播,並且掃描課配對的藍牙信息
      */
      public void searchForAvailableBluetooth(){
          //1.註冊一個廣播,用於接收“發現設備”的廣播
      	intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
          receiver = new BluetoothReceiver();
          if(context != null){
              context.registerReceiver(receiver, intentFilter);
              isRegisted = true;
          }
          //2.創建藍牙適配器,並開始掃描
          //注意這裏需要對權限進行動態申請!!!!!!!!!!!!!!!!!!!!!!!
          BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
          if (Build.VERSION.SDK_INT >= 23) {
              if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
              {
                  ActivityCompat.requestPermissions((Activity)context,
                                                    new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 10);
              }
          }
          if(adapter.startDiscovery()){
              Toast.makeText(context, "掃描開始",Toast.LENGTH_SHORT).show();
          }else{
              Toast.makeText(context, "掃描未開始",Toast.LENGTH_SHORT).show();
          }
      }
      /**
      * 關閉掃描過程
      */
      public void tryToCancel(){
          //結束掃描過程,並且關閉廣播
          if (adapter.isDiscovering()) {
              adapter.cancelDiscovery();
          }
          if(isRegisted){
              context.unregisterReceiver(receiver);
          }
      }
      
  4. 連接設備

    1. 概念理解:配對和連接之間有一點是不同的。配對以爲着兩臺設備是知道彼此的存在的,它們有一個共享的鏈接密匙,可以用於授權以及建立一個加密的連接。而連接意味着設備目前共享一個RFCOMM通道,並且可以彼此傳輸數據。目前的安卓藍牙接口要求設備在建立一個RFCOMM連接之前先進行配對。(當你使用藍牙接口初始化一個加密的連接時,配對是自動執行的)。

    2. 那麼我們分爲兩步,一是配對,二是連接

      1. 配對:使用createBond

        Method createBondMethod = null;
        createBondMethod = BluetoothDevice.class.getMethod("createBond");
        if((boolean)createBondMethod.invoke(device)){
            Toast.makeText(context, "device 配對成功", Toast.LENGTH_SHORT).show();
        }
        
      2. 連接:連接需要分爲發起方client以及接受方server,通過上面的發現設備等,能夠獲取到要連接的藍牙設備的相應的信息BluetoothDevice,通過在相同的RFCOMM通道上有一個連接的BluetoothSocket時,就是相互連接上了。

        1. 服務端:服務端需要持有一個打開的BluetoothServerSocket來作爲服務器端。該socket的目的是爲了監聽外來的連接請求,當一個請求被接受之後,提供一個連接的BluetoothSocket.

          //注意:此處例子使用的Android開發中文api提供的例子,我自己寫的也是基於該例子進行一些相應的邏輯變化。
          /**
          * 主要步驟如下:
          * 1. 通過adapter調用listenUsingRfcommWithServiceRecord(String, UUID)得到一個BluetoothServerSocket。其中,這個String是你的服務的標誌名稱,系統將會把它寫入設備中的一個新的服務發現協議(SDP)數據庫條目中(名字是任意的,並且可以只是你應用的名字)。UUID同樣被包含在SDP條目中,並且將會成爲和客戶端設備連接協議的基礎。也就是說,當客戶端嘗試連接這個設備時,它將會攜帶一個UUID用於唯一指定它想要連接的服務器。這些UUIDs必須匹配以便該連接可以被接受(在下一步中)。 通過調用accept()開始監聽連接請求。
          * 2. 通過調用accept()開始監聽請求。這是一個阻塞調用,只有當一個遠程設備使用一個UUID發送了一個連接請求,並且該UUID和正在監聽的服務器socket註冊的UUID相匹配時,一個連接纔會被接受。成功後,accept() 將會返回一個已連接的 BluetoothSocket。(所以這意味着客戶端需要知道服務端的UUID!!!!!!!)(因爲是阻塞的,所以需要卡一個線程進行等待)
          * 3. close()
          */
          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); //這裏就可以對該socket進行操作了,可以獲取到他的相應的一些信息,例如:inputstream、outputstream
                          //例如我的操作,直接監聽客戶端發送的信息(這裏已經涉及到了藍牙數據交換了0-0)
                          inputStream = socket.getInputStream();
                          byte[] buffer = new byte[1024];
                          int bytes;
          
                          while(true){
                              outputStream= socket.getOutputStream();//判斷客戶端是否斷開
                              bytes = inputStream.read(buffer);
                              if(bytes == 0 || bytes == -1){
                                  ToastString("數據返回爲" + bytes);
                                  Message  message = handler.obtainMessage();
                                  message.what = MainActivity.CONNECTBREAK;
                                  handler.sendMessage(message);
                                  break;
                              }//判斷客戶端是否斷開
                              Message  message = handler.obtainMessage();
                              message.what = MainActivity.GETMESSAGE;
                              Bundle bundle = new Bundle();
                              bundle.putString(MainActivity.MESSAGEKEY, new String(buffer,0,bytes));
                              message.setData(bundle);
                              handler.sendMessage(message);
                          }
                          break;
                      }
                  }
              }
          
              /** Will cancel the listening socket, and cause the thread to finish */
              public void cancel() {
                  try {
                      mmServerSocket.close();
                  } catch (IOException e) { }
              }
          }
          
        2. 客戶端:客戶端首先是需要一個BluetoothDevice(上面的步驟已經獲取到了),然後使用它獲取到BluetoothSocket,然後調用connect()即可初始化一個連接。

          /**
          步驟
          1. 使用BluetoothDevice,通過調用createRfcommSocketToServiceRecord(UUID)來得到一個BluetoothSocket.
          此處的UUID必須跟服務器使用UUID是相匹配的。
          2. 調用connect()初始化一個連接。
          執行這個調用時,系統將會在遠程設備上執行一個SDP查找工作,來匹配UUID。如果查找成功,並且遠程設備接受了連接,它將會在連接過程中分享RFCOMM通道,而 connect() 將會返回。這個方法是阻塞的。如果,處於任何原因,該連接失敗了或者connect()超時了(大約12秒以後),那麼它將會拋出一個異常。
          因爲connect()是一個阻塞調用,這個連接過程應該總是在一個單獨的線程中執行。
          需要注意的是:你應該總是確保在你調用connect()時設備沒有執行設備查找工作。如果正在查找設備,那麼連接嘗試將會很大程度的減緩,並且很有可能會失敗。
          */
          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) { }
              }
          }
          
  5. 設備間數據交換

    1. 數據交換就很簡單了,分爲兩部分:1.寫入;2.讀取

    2. 寫入:寫入操作只需要對上述拿到的socketOutputStream進行操作,通過write(byte[])方法進行數據的寫入即可:

      public void writeMessage(byte[] bytes){
          try {
              output.write(bytes);
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
      
    3. 讀取:讀取操作只需要對上述拿到的socketInputStream進行操作,通過read(byte[])方法進行讀取即可。需要注意的是,read方法是一個阻塞的方法,所以我們需要在新的線程的循環中進行讀取數據。

      //此處只展示讀取代碼,上面寫服務器端的時候有一個比較完整的實例
      inputStream = socket.getInputStream();
      byte[] buffer = new byte[1024];
      int bytes;
      
      while(true){
          outputStream= socket.getOutputStream();//判斷客戶端是否斷開
          bytes = inputStream.read(buffer);
          if(bytes == 0 || bytes == -1){
              ToastString("數據返回爲" + bytes);
              Message  message = handler.obtainMessage();
              message.what = MainActivity.CONNECTBREAK;
              handler.sendMessage(message);
              break;
          }//判斷客戶端是否斷開
          Message  message = handler.obtainMessage();
          message.what = MainActivity.GETMESSAGE;
          Bundle bundle = new Bundle();
          bundle.putString(MainActivity.MESSAGEKEY, new String(buffer,0,bytes));
          message.setData(bundle);
          handler.sendMessage(message);
      }
      

完結!!!

寫藍牙這個小demo,我查了很多文章,但是都感覺暈頭轉向,很多方面都不是很懂,最後還是去看了Android的文檔比較實在!

在這裏附送 Android藍牙文檔,寫的很仔細,我上面的可能是以一個demo的形式進行展現。

查看完整藍牙通信demo的代碼,可以去我GitHub上面看一下:

GitHub的地址:https://github.com/shuhaoLIN/-Bluetooth-socket-

暫時寫的比較粗糙,後面再看看怎麼改善一下!

有什麼不對的地方,歡迎多多指正!!!!!

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