• 注册
  • Android博客 Android博客 关注:0 内容:1125

    Android蓝牙通信机制详解

  • 查看作者
  • 打赏作者
  • 当前位置: 职业司 > Android开发 > Android博客 > 正文
    • Android博客
    • 1. 传统蓝牙和低功耗蓝牙的区别

      简单分类:

      蓝牙可分为经典蓝牙模块(v1.1/1.2/2.0/2.1/3.0),低功耗蓝牙模块(v4.0/4.1/4.2),以及蓝牙双模模块(支持蓝牙所有版本,兼容低功耗蓝牙及经典蓝牙)。
      蓝牙4.0是一个综合协议规范,它除了提出了新的 LE 规范,还囊括了 BR / EDR 规范,并在实际使用中分为了单模(Single mode)和双模(Dual mode)版本,前者仅支持 LE 规范且不能和蓝牙4.0之前的版本通信,后者同时支持 LE 和 BR / EDR 规范,并且兼容旧版蓝牙。

      用途区别:

      1、低功耗蓝牙的发送和接受任务会以最快的速度完成,完成之后蓝牙BLE会暂停发射无线(但是还是会接受),等待下一次连接再激活;传统蓝牙是持续保持连接。

      2、低功耗蓝牙的广播信道(为保证网络不互相干扰而划分)仅有3个;传统蓝牙是32个。

      3、低功耗蓝牙“完成”一次连接(即扫描其它设备、建立链路、发送数据、认证和适当地结束)只需3ms;传统蓝牙完成相同的连接周期需要数百毫秒。

      4、低功耗蓝牙使用非常短的数据包,多应用于实时性要求比较高,但是数据速率比较低的产品,遥控类的如键盘,遥控鼠标,传感设备的数据发送,如心跳带,血压计,温度传感器等;传统蓝牙使用的数据包长度较长,可用于数据量比较大的传输,如语音,音乐,较高数据量传输等。

      5、低功耗蓝牙无功率级别,一般发送功率在+4dBm,一般在空旷距离,达到70m的传输距离;传统蓝牙有3个功率级别,Class1,Class2,Class3,分别支持100m,10m,1m的传输距离。

      协议栈区别:

      Profiles 定义了一个实际的应用场景。具体来说,Profiles 定义了蓝牙系统中从PHY(物理层)到 L2CAP 的每层以及核心规范之外的任何其他协议所需的功能和特性。传统蓝牙使用的Profile是SPP和REFCOMM,LE蓝牙使用的Profiles为GATT/ATT,两者互不兼容。

      Android蓝牙通信机制详解

      理解GAP

      GAP,即通用访问配置文件,所有蓝牙设备都必须实现的基本配置文件。  它定义了蓝牙设备的基本要求,例如,对于 BR / EDR,它定义了蓝牙设备以包括无线电,基带,链路管理器,L2CAP 和服务发现协议功能; 对于 LE,它定义了物理层,链路层,L2CAP,安全管理器,GATT / ATT。这将所有各层连接在一起,形成蓝牙设备的基本要求。它还描述了设备搜寻,连接建立,安全性,身份验证,关联模型和服务搜寻的行为方法。

        • 在BR / EDR中,GAP定义每个设备为单一角色,其可能具备的功能包括设备如何相互发现,建立连接以及描述用于身份验证的安全关联模型。设备可能只具备其中一种或多种功能,比如只具有启动或接受连接功能。
        • 在 LE 中,GAP 定义了四个特定角色:BroadcasterObserverPeripheral 和 Central。如果底层 Controller 支持这些角色或角色组合,则设备也可同时支持多个角色。但是,在某一时刻只能支持其中一个角色。每个角色都指定了底层Controller的要求。这允许控制器针对特定用例进行优化。

      传统蓝牙协议把每个蓝牙设备都看作对等的角色,而LE蓝牙支持我们根据实际用途,给不同的设备分配不同的角色,这样能进一步根据角色特点来优化传输速度和耗电量。

      传统蓝牙的协议栈

      Android蓝牙通信机制详解

      L2CAP协议(Logical Link Control and Adaptation Protocol)

      L2CAP协议处于 Adaption Layer层,顾名思义是为是为高层协议如SDP、REFCOMM提供接口,并且与更底层的HCI协议通信的协议。HCI协议提供了访问蓝牙芯片的统一接口并被烧录到蓝牙芯片中。Android开发不必考虑低于Adaption Layer的协议层。

      L2CAP传输是基于信道的概念,类似于fifo的通信通道,他是一个点对点的通道,每个通道都有一个独立的信道标识符(channel identifier,CID)。CID标识了信道的每一端,有些固定的CID用作特殊用途,如信令信道,固定的CID=0x0001(LE固定为0x0005)。该信道用于创建和建立面向连接的数据信道,并可对这些信道的特性变化进行协商。当两端协商好配置信息之后,会动态分配另一个信道,0x0040之后都是为动态分配的信道,这些信道用于上层的数据传输。下图说明了不同设备之间的L2CAP实体间通信的使用方式:

      Android蓝牙通信机制详解

      逻辑信道可以工作在5种不同的模式下(可以理解为5种不同的使用场景),最后一种是LE设备特有的:

      1. Basic L2CAP Mode(equivalent to L2CAP specification in Bluetooth v1.1) 默认模式,在未选择其他模式的情况下,用此模式。
      2. Flow Control Mode 此模式下不会进行重传,但是丢失的数据能够被检测到,并报告丢失。
      3. Retransmission Mode 此模式确保数据包都能成功的传输给对端设备。
      4. Enhanced Retransmission Mode 此模式和重传模式类似,加入了Poll-bit(一种轮询机制)等提高恢复效率。
      5. Streaming Mode 此模式是为了真实的实时传输,数据包被编号但是不需要ACK确认。设定一个超时定时器,一旦定时器超时就将超时数据冲掉。
      6. LE Credit Based Flow Control Mode 被用于LE设备通讯。

      REFCOMM协议

      RFCOMM处于传输层,RFCOMM提供了基于L2CAP协议的串行(9针RS-232)模拟,支持在两个蓝牙设备间高达60路的通信连接。RFCOMM 的目的,是针对如何在两个不同设备(通信的两端)上的应用之间保证一条完整的通信路径,并在它们之间保持一通信段。

      SPP协议

      Serial Port Profile,即串口配置文件,定义了使用蓝牙进行 RS232(或类似)串口仿真的协议和过程。这也是蓝牙诞生之初的主要功能:替代 RS232 有线通信,以无线的方式链接多个设备,克服同步问题。

      ATT协议(Attribute protocol)

      简单来说,ATT层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。BLE协议栈中,开发者接触最多的就是ATT。BLE引入了attribute概念,用来描述一条一条的数据。Attribute除了定义数据,同时定义该数据可以使用的ATT命令,因此这一层被称为ATT层。

      GATT协议(Generic attribute profile )

      GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。没有GATT,BLE协议栈也能跑,但互联互通就会出问题,也正是因为有了GATT和各种各样的应用profile,BLE摆脱了ZigBee等无线协议的兼容性困境,成了出货量最大的2.4G无线通信产品。

      SDP协议(Service Discovery Protocol, 服务发现协议)

      SDP协议让客户机的应用程序发现存在的服务器应用程序提供的服务以及这些服务的属性。SDP只提供发现服务的机制,不提供使用这些服务的方法。每个蓝牙设备都需要一个SDP Service,只做Client的蓝牙设备除外。
      发现的一个servcie的所有信息的集合就是一个service record:

      Android蓝牙通信机制详解

      每个service record都是由多个service attribute组成,service attribute由 attribute ID和attribute Value组成的。Attribute ID是由Assigned Value定义好的,例如Record Handle Attribute的ID为0x0000。

      其中UUID是一个128bit全局惟一的标识符,预设的UUID可以在蓝牙官网中查询:www.bluetooth.com/specificati… 。

      *使用预设UUID的好处是客户端可以快速准确的识别服务端是什么设备,可以提供什么服务。手机系统很多都把使用预设UUID的服务和相关代码添加进去了,这样可以在不安装另外App的情况下,正确的解析出从服务端(从设备,如血压计、心率计、温度计等)上传的信息。
      *

      传统蓝牙建立连接过程

      一个设备会发起一个连接另外设备的请求。
      另一个设备等待另外一个设备发起连接请求。
      Android蓝牙通信机制详解

      Android传统蓝牙开发

      准备

      1. 声明蓝牙权限
      ```
      <manifest ... >
        <uses-permission android:name="android.permission.BLUETOOTH" />
        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
        <!-- If your app targets Android 9 or lower, you can declare
             ACCESS_COARSE_LOCATION instead. -->
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        ...
      </manifest>
      复制代码
      2. 获取 BluetoothAdapter
      BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
      if (bluetoothAdapter == null) {
          // Device doesn't support Bluetooth
      }
      复制代码
      3.启用蓝牙
      ```
      if (!bluetoothAdapter.isEnabled()) {
          Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
          startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
      }
      复制代码

      调用 isEnabled(),以检查当前是否已启用蓝牙。如果此方法返回 false,则表示蓝牙处于停用状态。调用
      startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
      系统将显示对话框,请求用户允许启用蓝牙。

      搜索设备

      新设备 ————> 配对设备 ————> 连接设备

      一、 扫描设备是一个重量级操作
      二、 没有配对过的设备,如果没有启用“可检测性”,就无法被扫描获取到设备信息
      三、 **配对**是指两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。
      四、 **连接**是指设备当前共享一个 RFCOMM 通道,并且能够向彼此传输数据。当前的 Android Bluetooth API 要求规定,只有先对设备进行配对,然后才能建立 RFCOMM 连接。在使用 Bluetooth API 发起加密连接时,系统会自动执行配对。
      复制代码
      1. 查询已配对设备

      执行设备发现之前,可以查询已配对的设备集,以了解所需的设备是否处于已检测到状态。

      Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
      if (pairedDevices.size() > 0) {
          // There are paired devices. Get the name and address of each paired device.
          for (BluetoothDevice device : pairedDevices) {
              String deviceName = device.getName();
              String deviceHardwareAddress = device.getAddress(); // MAC address
          }
      }
      复制代码
      2. 发现设备

      如要开始发现设备,只需调用 startDiscovery()。该进程为异步操作,并且会返回一个布尔值,指示发现进程是否已成功启动。发现进程通常包含约 12 秒钟的查询扫描,随后会对发现的每台设备进行页面扫描,以检索其蓝牙名称。
      应该针对 ACTION_FOUND Intent 注册一个 BroadcastReceiver,以便接收每台发现的设备的相关信息。系统会为每台设备广播此 Intent。Intent 包含额外字段 EXTRA_DEVICE 和 EXTRA_CLASS,二者又分别包含 BluetoothDevice (其中包含设备名称和MAC地址等关键信息)和 BluetoothClass(其中包含设备类型如电脑、手机、耳机以及音频、电话等用途信息)。

      @Override
      protected void onCreate(Bundle savedInstanceState) {
          ...
          // Register for broadcasts when a device is discovered.
          IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
          registerReceiver(receiver, filter);
      }
      // Create a BroadcastReceiver for ACTION_FOUND.
      private final BroadcastReceiver receiver = new BroadcastReceiver() {
          public void onReceive(Context context, Intent intent) {
              String action = intent.getAction();
              if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                  // Discovery has found a device. Get the BluetoothDevice
                  // object and its info from the Intent.
                  BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                  String deviceName = device.getName();
                  String deviceHardwareAddress = device.getAddress(); // MAC address
              }
          }
      };
      @Override
      protected void onDestroy() {
          super.onDestroy();
          ...
          // Don't forget to unregister the ACTION_FOUND receiver.
          unregisterReceiver(receiver);
      }
      复制代码
      3.启用可检测性(设备可见)

      默认情况下,设备处于可检测到模式的时间为 120 秒(2 分钟)。通过添加 EXTRA_DISCOVERABLE_DURATION Extra 属性定义不同的持续时间,最高可达 3600 秒。、

      Intent discoverableIntent =
              new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
      discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
      startActivity(discoverableIntent);
      复制代码

      在此过程中手机会弹窗询问用户是否打开可见性。Activity 将会收到对 onActivityResult() 回调的调用,其结果代码等于设备可检测到的持续时间。如果用户选择否或出现错误,则结果代码为 RESULT_CANCELED
      监听可见性改变的广播:
      为 ACTION_SCAN_MODE_CHANGED Intent 注册 BroadcastReceiver。此 Intent 将包含额外字段 EXTRA_SCAN_MODE 和 EXTRA_PREVIOUS_SCAN_MODE,二者分别提供新的和旧的扫描模式。每个 Extra 属性可能拥有以下值:

      • SCAN_MODE_CONNECTABLE_DISCOVERABLE
        设备处于可检测到模式。
      • SCAN_MODE_CONNECTABLE
        设备未处于可检测到模式,但仍能收到连接。
      • SCAN_MODE_NONE
        设备未处于可检测到模式,且无法收到连接。

      连接设备

      传统蓝牙是通过建立REFCCOM sockect来进行通信的,类似于socket通信,一台设备需要开放服务器套接字并处于listen状态,而另一台设备使用服务器的MAC地址发起连接。连接建立后,服务器和客户端就都通过对BluetoothSocket进行读写操作来进行通信。

      1.服务器端监听连接

      当您需要连接两台设备时,其中一台设备必须保持开放的 BluetoothServerSocket,从而充当服务器。 在接受连接后,创建BluetoothSocket,然后应该回收BluetoothServerSocket,除非还有更多设备需要连接。

      1. 通过调用 listenUsingRfcommWithServiceRecord() 获取 BluetoothServerSocket
      2. 通过调用 accept() 开始侦听连接请求。
      3. 如果您无需接受更多连接,请调用 close()
      //由于 `accept()` 是阻塞调用,因此您不应在主 Activity 界面线程中执行该调用
      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 {
                //UUID 是一种标准化的 128 位格式,可供字符串 ID 用来对信息进行
      //唯一标识。UUID 的特点是其足够庞大,因此您可以选择任意随机 ID,
      //而不会与其他任何 ID 发生冲突。
                  tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
              } catch (IOException e) {
                  Log.e(TAG, "Socket's listen() method failed", e);
              }
              mmServerSocket = tmp;
          }
          public void run() {
              BluetoothSocket socket = null;
              // Keep listening until exception occurs or a socket is returned.
              while (true) {
                  try {
      /**
      *这是一个阻塞调用。当服务器接受连接或异常发生时,该调用便会返
      *回。只有当远程设备发送包含 UUID 的连接请求,并且该 UUID 与使
      *用此侦听服务器套接字注册的 UUID 相匹配时,服务器才会接受连接。
      *连接成功后,`accept()` 将返回已连接的 `BluetoothSocket`
      **/
                      socket = mmServerSocket.accept();
                  } catch (IOException e) {
                      Log.e(TAG, "Socket's accept() method failed", e);
                      break;
                  }
                  if (socket != null) {
                      // A connection was accepted. Perform work associated with
                      // the connection in a separate thread.
                      manageMyConnectedSocket(socket);
                      mmServerSocket.close();
                      break;
                  }
              }
          }
          // Closes the connect socket and causes the thread to finish.
          public void cancel() {
              try {
      /**
      *此方法调用会释放服务器套接字及其所有资源,但不会关闭 `accept()` 所
      *返回的已连接的 `BluetoothSocket`。与 TCP/IP 不同,RFCOMM 一次只
      *允许每个通道有一个已连接的客户端,因此大多数情况下,在接受已连接的
      *套接字后,您可以立即在 `BluetoothServerSocket` 上调
      *用 `close()`。
      **/
                  mmServerSocket.close();
              } catch (IOException e) {
                  Log.e(TAG, "Could not close the connect socket", e);
              }
          }
      }
      复制代码
      2.客户端发起连接
      1. 首先获取表示该远程设备的 `BluetoothDevice` 对象
      2. 调用BluetoothDevice的`createRfcommSocketToServiceRecord(UUID)` 获取 `BluetoothSocket`
      3. 通过调用 `connect()` 发起连接。请注意,此方法为阻塞调用
      复制代码
      //由于 `connect()` 是阻塞调用,因此您应始终在主 Activity(界面)线程以外的
      //线程中执行此连接步骤。
      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;
              try {
                  // Get a BluetoothSocket to connect with the given BluetoothDevice.
                  /**
      *此方法会初始化 `BluetoothSocket` 对象,以便客户端连接
      *至 `BluetoothDevice`。此处传递的 UUID 必须与服务器设备在调
      *用 `listenUsingRfcommWithServiceRecord(String, UUID)` 开
      *放其 `BluetoothServerSocket` 时所用的 UUID 相匹配。如要使用
      *匹配的 UUID,请通过硬编码方式将 UUID 字符串写入您的应用,然后
      *通过服务器和客户端代码引用该字符串。
      **/
                  tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
              } catch (IOException e) {
                  Log.e(TAG, "Socket's create() method failed", e);
              }
              mmSocket = tmp;
          }
          public void run() {
              // Cancel discovery because it otherwise slows down the connection.
              bluetoothAdapter.cancelDiscovery();
              try {
                  // Connect to the remote device through the socket. This call blocks
                  /**
      *当客户端调用此方法后,系统会执行 SDP 查找,以找到带有所匹配
      *UUID 的远程设备。如果查找成功并且远程设备接受连接,则其会共享
      *RFCOMM 通道以便在连接期间使用,并且 `connect()` 方法将会返
      *回。如果连接失败,或者 `connect()` 方法超时(约 12 秒后),则
      *此方法将引发 `IOException`。
      **/
                  mmSocket.connect();
              } catch (IOException connectException) {
                  // Unable to connect; close the socket and return.
                  try {
                      mmSocket.close();
                  } catch (IOException closeException) {
                      Log.e(TAG, "Could not close the client socket", closeException);
                  }
                  return;
              }
              // The connection attempt succeeded. Perform work associated with
              // the connection in a separate thread.
              manageMyConnectedSocket(mmSocket);
          }
          // Closes the client socket and causes the thread to finish.
          public void cancel() {
              try {
                  mmSocket.close();
              } catch (IOException e) {
                  Log.e(TAG, "Could not close the client socket", e);
              }
          }
      }
      复制代码
      3.管理REFCOMM连接
      1. 使用 getInputStream() 和 getOutputStream(),分别获取通过套接字处理数据传输的 InputStream 和 OutputStream
      2. 使用 read(byte[]) 和 write(byte[]) 读取数据以及将其写入数据流。
      public class MyBluetoothService {
          private static final String TAG = "MY_APP_DEBUG_TAG";
          private Handler handler; // handler that gets info from Bluetooth service
          // Defines several constants used when transmitting messages between the
          // service and the UI.
          private interface MessageConstants {
              public static final int MESSAGE_READ = 0;
              public static final int MESSAGE_WRITE = 1;
              public static final int MESSAGE_TOAST = 2;
              // ... (Add other message types here as needed.)
          }
          private class ConnectedThread extends Thread {
              private final BluetoothSocket mmSocket;
              private final InputStream mmInStream;
              private final OutputStream mmOutStream;
              private byte[] mmBuffer; // mmBuffer store for the stream
              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();
                  } catch (IOException e) {
                      Log.e(TAG, "Error occurred when creating input stream", e);
                  }
                  try {
                      tmpOut = socket.getOutputStream();
                  } catch (IOException e) {
                      Log.e(TAG, "Error occurred when creating output stream", e);
                  }
                  mmInStream = tmpIn;
                  mmOutStream = tmpOut;
              }
              public void run() {
                  mmBuffer = new byte[1024];
                  int numBytes; // bytes returned from read()
                  // Keep listening to the InputStream until an exception occurs.
                  while (true) {
                      try {
                          // Read from the InputStream.
                          numBytes = mmInStream.read(mmBuffer);
                          // Send the obtained bytes to the UI activity.
                          Message readMsg = handler.obtainMessage(
                                  MessageConstants.MESSAGE_READ, numBytes, -1,
                                  mmBuffer);
                          readMsg.sendToTarget();
                      } catch (IOException e) {
                          Log.d(TAG, "Input stream was disconnected", e);
                          break;
                      }
                  }
              }
              // Call this from the main activity to send data to the remote device.
              public void write(byte[] bytes) {
                  try {
                      mmOutStream.write(bytes);
                      // Share the sent message with the UI activity.
                      Message writtenMsg = handler.obtainMessage(
                              MessageConstants.MESSAGE_WRITE, -1, -1, mmBuffer);
                      writtenMsg.sendToTarget();
                  } catch (IOException e) {
                      Log.e(TAG, "Error occurred when sending data", e);
                      // Send a failure message back to the activity.
                      Message writeErrorMsg =
                              handler.obtainMessage(MessageConstants.MESSAGE_TOAST);
                      Bundle bundle = new Bundle();
                      bundle.putString("toast",
                              "Couldn't send data to the other device");
                      writeErrorMsg.setData(bundle);
                      handler.sendMessage(writeErrorMsg);
                  }
              }
              // Call this method from the main activity to shut down the connection.
              public void cancel() {
                  try {
                      mmSocket.close();
                  } catch (IOException e) {
                      Log.e(TAG, "Could not close the connect socket", e);
                  }
              }
          }
      }
      复制代码

      Android低功耗蓝牙开发

      请登录之后再进行评论

      登录

      手机阅读天地(APP)

      • 微信公众号
      • 微信小程序
      • 安卓APP
      手机浏览,惊喜多多
      匿名树洞,说我想说!
      问答悬赏,VIP可见!
      密码可见,回复可见!
      即时聊天、群聊互动!
      宠物孵化,赠送礼物!
      动态像框,专属头衔!
      挑战/抽奖,金币送不停!
      赶紧体会下,不会让你失望!
    • 实时动态
    • 签到
    • 做任务
    • 发表内容
    • 偏好设置
    • 到底部
    • 帖子间隔 侧栏位置:
    • 还没有账号?点这里立即注册