Android蓝牙通信机制详解

10,878 阅读23分钟

1. 传统蓝牙和低功耗蓝牙的区别

简单分类:

这篇文章将涉及两种蓝牙的底层协议,两种蓝牙的对设备角色的分配,两种蓝牙的开启、扫描、建立连接、通信,代码详尽,逻辑清晰。

蓝牙模块有经典蓝牙模块,低功耗蓝牙模块(v4.0/4.1/4.2),以及蓝牙双模模块(支持蓝牙所有版本,兼容低功耗蓝牙及经典蓝牙)。

蓝牙4.0是一个综合协议规范,它除了提出了新的 LE 规范,还囊括了 BR / EDR 规范,并在实际使用中分为了单模(Single mode)和双模(Dual mode)版本,前者仅支持 LE 规范且不能和蓝牙4.0之前的版本通信,后者同时支持 LE 和 BR / EDR 规范,并且兼容旧版蓝牙。

image.png 从左到右分别是:经典蓝牙、蓝牙双模、蓝牙单模。

用途区别:

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,两者互不兼容。

image.png

理解GAP

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

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

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

传统蓝牙的协议栈

image.png

L2CAP协议(Logical Link Control and Adaptation Protocol)

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

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

image.png

逻辑信道可以工作在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 有线通信,以无线的方式链接多个设备,克服同步问题。

LE协议:

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:

image.png

每个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… 。 点开这个网址,能找到一些16bit的assigned-numbers,例如下面的姓氏、性别、体重、身高等。
image.png 可将assigned-numbers转化为UUID:

public static final long assigned_number = 0x2A93L;
public static final long leastSigBits = 0x800000805f9b34fbL;
...
UUID bl_128_bit_value = toUuid(assigned_number);
Log.d(TAG, bl_128_bit_value.toString() + "\n");
...
public static UUID toUuid(long assignedNumber) {
    return new UUID((assignedNumber << 32) | 0x1000, leastSigBits);
}

输出结果: image.png

image.png

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

传统蓝牙建立连接过程

一个设备会发起一个连接另外设备的请求。
另一个设备等待另外一个设备发起连接请求。 image.png

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。这里使用的ServiceRecord是
  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低功耗蓝牙开发

BLE的角色分配

中央(Central)和外围(Peripheral)

很多文章把 中央 和 外围 类比为 服务端 和 客户端 来理解,最好不要这样理解。理解为中央和地方更简单。中央控制地方,中央收税,地方缴税,提供服务(service);地方可以有多个,中央只能有一个;中央跟中央不能建立连接,地方和地方也不能建立连接。理论上有的设备支持在中央和地方两种角色之间切换,实际用到不多。 image.png

按一下智能手表的蓝牙配对键,配对指示灯开始闪烁,外围设备进行广播( Advertising),暴露自己的蓝牙SSID、MAC地址和提供的服务(Service)。用手机扫描(Scan)到设备后,点击设备项,建立连接。

Service — Service是一系列Characteristic。例如,您可能拥有名为“心率监测器”的服务,其中包括“心率”等Characteristic,每个等Characteristic可以包含一个至多个 descriptor,代表Characteristic的单位和值

image.png

连接建立后,中央和外围设备可以相互发送数据,这时两者转换为我们熟悉的客户端服务端角色,发送数据的一方为服务端,接收数据的一方为客户端。

image.png

1. 外围设备广播

设定广播:

private AdvertiseSettings buildAdvertiseSettings() {
    AdvertiseSettings.Builder builder = new AdvertiseSettings.Builder()
            //设置广播模式:低功耗、平衡、低延时,广播间隔时间依次越来越短
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
            //设置是否可以连接,一般不可连接广播应用在iBeacon设备上
            .setConnectable(true)
            //设置广播的最长时间,最大时长为LIMITED_ADVERTISING_MAX_MILLIS(180秒)
            .setTimeout(10*1000)
            //设置广播的信号强度 ADVERTISE_TX_POWER_ULTRA_LOW, ADVERTISE_TX_POWER_LOW,
            //ADVERTISE_TX_POWER_MEDIUM, ADVERTISE_TX_POWER_HIGH 信号强度依次增强
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH);

    return builder.build();
}
  • 对外广播的数据(数据限制 31 Bytes)
private AdvertiseData buildAdvertiseData() {
    AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
            //添加厂家信息,第一个参数为厂家ID(不足两个字节会自动补0,例如这里为0x34,实际数据则为34,00)
            //一般情况下无需设置,否则会出现无法被其他设备扫描到的情况
            .addManufacturerData(0x34, new byte[]{0x56})
            //添加服务进行广播,即对外广播本设备拥有的服务
            .addServiceData(...);
            //是否广播信号强度
            .setIncludeTxPowerLevel(true)
            //是否广播设备名称
            .setIncludeDeviceName(true);

    return dataBuilder.build();
}
  • 对外广播(即允许被扫描到)
private void advertise() {
 BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
    BluetoothAdapter mAdapter = bluetoothManager.getAdapter(); 
    AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            //广播成功,建议在这里开启服务
        }
        @Override
        public void onStartFailure(int errorCode) {
             //广播失败
        }
    };
    BluetoothLeAdvertiser mAdvertiser = mAdapter.getBluetoothLeAdvertiser();
    mAdvertiser.startAdvertising(buildAdvertiseSettings(), buildAdvertiseData(), mAdvertiseCallback);
}
  • 开启服务(Service)
//声明需要广播的服务的UUID和特征的UUID,注意不要占用蓝牙设备默认的UUID
UUID UUID_SERVICE = UUID.fromString("00001354-0000-1000-8000-00805f9b34fb");
UUID UUID_CHARACTERISTIC = UUID.fromString("00001355-0000-1000-8000-00805f9b34fb");
UUID UUID_DESCRIPTOR = UUID.fromString("00001356-0000-1000-8000-00805f9b34fb");

//外围设备状态、数据回调,详情见后面
BluetoothGattServerCallback serverCallback = new BluetoothGattServerCallback() {...};
//GATT协议服务
BluetoothGattServer server = bluetoothManager.openGattServer(this, serverCallback);

//创建一个服务
BluetoothGattService service = new BluetoothGattService(UUID_SERVICE,
        BluetoothGattService.SERVICE_TYPE_PRIMARY);

//创建一个特征,首先此characteristic属性满足BluetoothGattCharacteristic.PROPERTY_WRITY或BluetoothGattCharacteristic.PROPERTY_WRITY_NO_RESPONSE,
//如果其property都不包含这两个,写特征值writeCharacteristic()函数直接返回false,什么都不做处理。其次此characteristic权限应满足
//BluetoothGattCharacteristic.PERMISSION_WRITE,否则onCharacteristicWrite()回调收到GATT_WRITE_NOT_PERMITTED回应
//如果需要既能读,又能写,则可以参考如下写法
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(Constants.UUID_CHARACTERISTIC,
        BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE,
        BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ);

//创建一个描述符
BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID_DESCRIPTOR,
        BluetoothGattDescriptor.PERMISSION_READ);

//将描述符添加到特征中,一个特征可以包含0至多个描述符
characteristic.addDescriptor(descriptor);
//将特征添加到服务中,一个服务可以包含1到多个特征
service.addCharacteristic(characteristic);
server.addService(service);
  • GATT服务端的回调,继承BluetoothGattServerCallback
public abstract class BluetoothGattServerCallback {
    public BluetoothGattServerCallback() {
    }

    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
        //连接状态被改变
    }

    public void onServiceAdded(int status, BluetoothGattService service) {
        //添加服务
    }

    public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
        //被读取特征
    }

    public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        //被写入数据,其中device是写入的设备,value是写入的值,responseNeeded指是否需要恢复,如果需要恢复则调用gattServer.sendResponse()方法回复
    }

    public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
        //被读取描述符
    }

    public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        //被写入描述符
    }

    public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
    }

    @Override
    public void onNotificationSent(BluetoothDevice device, int status) {
    }

    @Override
    public void onMtuChanged(BluetoothDevice device, int mtu) {
    }

    @Override
    public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
    }

    @Override
    public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
    }
}

2.中央设备搜索

获取 BluetoothAdapter

先加权限:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
//获取扫描结果权限,Android6.0以上需要动态权限(这两个权限是在经典蓝牙的扫描中需要声明的)
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
//或者
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 //当android:required为true的时候,app只能强制运行在支持BLE功能的设备商,为false的时候,可以运行在所有设备上,但某些方法需要手动检测,否则可能存在隐性BUG 
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
//小于等于API17时直接使用BluetoothAdapter.getDefaultAdapter()来获取Adapter
private BluetoothAdapter getAdapter(){
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
            mBluetoothAdapter = mBluetoothManager.getAdapter();
        } else {
            mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        }
    return mBluetoothAdapter;
}
if(mBluetoothAdapter == null){
    //蓝牙不可用
}

//Ble不可用
private boolean checkIfSupportBle(){
    return getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
开启蓝牙
//开启蓝牙功能需要一小段时间,所以不能执行开启蓝牙立即执行其他操作,这时蓝牙实际还没有开启,回出现异常,所以后续操作应该在蓝牙状态广播中处理
private void enableBluetooth(){
    if (mBluetoothAdapter == null && !mBluetoothAdapter.isEnabled()) {
        //使用系统弹框来启动蓝牙,REQUEST_ENABLE_BT为自定义的开启蓝牙请求码
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

        //或者静默打开
        bluetoothAdapter.enable();
    }
}

//静默关闭
bluetoothAdapter.disable();
扫描外围设备

查找 BLE 设备,使用 startLeScan() 方法。此方法将 BluetoothAdapter.LeScanCallback 作为参数。您必须实现此回调,因为这是返回扫描结果的方式。扫描非常耗电,因此应遵循以下准则:

  • 找到所需设备后,立即停止扫描。
  • 绝对不进行循环扫描,并设置扫描时间限制。
public class DeviceScanActivity extends ListActivity {

    private BluetoothAdapter bluetoothAdapter;
    private boolean mScanning;
    private Handler handler;

    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ...
    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    bluetoothAdapter.stopLeScan(leScanCallback);
                }
            }, SCAN_PERIOD);

            mScanning = true;
            bluetoothAdapter.startLeScan(leScanCallback);
        } else {
            mScanning = false;
            bluetoothAdapter.stopLeScan(leScanCallback);
        }
        ...
    }
...
}

如果想扫描特定类型的外围设备,则可调用 startLeScan(UUID[], BluetoothAdapter.LeScanCallback),它会提供一组 UUID 对象,用于指定您的应用支持的 GATT 服务。

private LeDeviceListAdapter leDeviceListAdapter;
...
// Device scan callback.
private BluetoothAdapter.LeScanCallback leScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        runOnUiThread(new Runnable() {
           @Override
           public void run() {
               leDeviceListAdapter.addDevice(device);
               leDeviceListAdapter.notifyDataSetChanged();
           }
       });
   }
};
连接到 GATT 服务器

要连接到 BLE 设备上的 GATT 服务器,请使用 connectGatt() 方法。此方法采用三个参数:一个 Context 对象、autoConnect(布尔值,指示是否在可用时自动连接到 BLE 设备),以及对 BluetoothGattCallback 的引用:

bluetoothGatt = device.connectGatt(this, false, gattCallback);
// A service that interacts with the BLE device via the Android BLE API.
public class BluetoothLeService extends Service {
    private final static String TAG = BluetoothLeService.class.getSimpleName();

    private BluetoothManager bluetoothManager;
    private BluetoothAdapter bluetoothAdapter;
    private String bluetoothDeviceAddress;
    private BluetoothGatt bluetoothGatt;
    private int connectionState = STATE_DISCONNECTED;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

    // Various callback methods defined by the BLE API.
    private final BluetoothGattCallback gattCallback =
            new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                connectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                Log.i(TAG, "Attempting to start service discovery:" +
                        bluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                connectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }

        @Override
        // New services discovered
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        // Result of a characteristic read operation
        public void onCharacteristicRead(BluetoothGatt gatt,
                BluetoothGattCharacteristic characteristic,
                int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }
     ...
    };
...
}
接收到蓝牙数据
private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
                             final BluetoothGattCharacteristic characteristic) {
    final Intent intent = new Intent(action);

    // This is special handling for the Heart Rate Measurement profile. Data
    // parsing is carried out as per profile specifications.
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
        int flag = characteristic.getProperties();
        int format = -1;
        if ((flag & 0x01) != 0) {
            format = BluetoothGattCharacteristic.FORMAT_UINT16;
            Log.d(TAG, "Heart rate format UINT16.");
        } else {
            format = BluetoothGattCharacteristic.FORMAT_UINT8;
            Log.d(TAG, "Heart rate format UINT8.");
        }
        final int heartRate = characteristic.getIntValue(format, 1);
        Log.d(TAG, String.format("Received heart rate: %d", heartRate));
        intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
    } else {
        // For all other profiles, writes the data formatted in HEX.
        final byte[] data = characteristic.getValue();
        if (data != null && data.length > 0) {
            final StringBuilder stringBuilder = new StringBuilder(data.length);
            for(byte byteChar : data)
                stringBuilder.append(String.format("%02X ", byteChar));
            intent.putExtra(EXTRA_DATA, new String(data) + "\n" +
                    stringBuilder.toString());
        }
    }
    sendBroadcast(intent);
}
private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
            connected = true;
            updateConnectionState(R.string.connected);
            invalidateOptionsMenu();
        } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
            connected = false;
            updateConnectionState(R.string.disconnected);
            invalidateOptionsMenu();
            clearUI();
        } else if (BluetoothLeService.
                ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
            // Show all the supported services and characteristics on the
            // user interface.
            displayGattServices(bluetoothLeService.getSupportedGattServices());
        } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
            displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
        }
    }
};
读取 BLE 属性
public class DeviceControlActivity extends Activity {
    ...
    // Demonstrates how to iterate through the supported GATT
    // Services/Characteristics.
    // In this sample, we populate the data structure that is bound to the
    // ExpandableListView on the UI.
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null) return;
        String uuid = null;
        String unknownServiceString = getResources().
                getString(R.string.unknown_service);
        String unknownCharaString = getResources().
                getString(R.string.unknown_characteristic);
        ArrayList<HashMap<String, String>> gattServiceData =
                new ArrayList<HashMap<String, String>>();
        ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                = new ArrayList<ArrayList<HashMap<String, String>>>();
        mGattCharacteristics =
                new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

        // Loops through available GATT Services.
        for (BluetoothGattService gattService : gattServices) {
            HashMap<String, String> currentServiceData =
                    new HashMap<String, String>();
            uuid = gattService.getUuid().toString();
            currentServiceData.put(
                    LIST_NAME, SampleGattAttributes.
                            lookup(uuid, unknownServiceString));
            currentServiceData.put(LIST_UUID, uuid);
            gattServiceData.add(currentServiceData);

            ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                    new ArrayList<HashMap<String, String>>();
            List<BluetoothGattCharacteristic> gattCharacteristics =
                    gattService.getCharacteristics();
            ArrayList<BluetoothGattCharacteristic> charas =
                    new ArrayList<BluetoothGattCharacteristic>();
           // Loops through available Characteristics.
            for (BluetoothGattCharacteristic gattCharacteristic :
                    gattCharacteristics) {
                charas.add(gattCharacteristic);
                HashMap<String, String> currentCharaData =
                        new HashMap<String, String>();
                uuid = gattCharacteristic.getUuid().toString();
                currentCharaData.put(
                        LIST_NAME, SampleGattAttributes.lookup(uuid,
                                unknownCharaString));
                currentCharaData.put(LIST_UUID, uuid);
                gattCharacteristicGroupData.add(currentCharaData);
            }
            mGattCharacteristics.add(charas);
            gattCharacteristicData.add(gattCharacteristicGroupData);
         }
    ...
    }
...
}
接收通知

BLE 应用通常会要求在设备上的特定特征发生变化时收到通知。以下代码段展示如何使用 setCharacteristicNotification() 方法设置特征的通知:

bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);

为某个特征启用通知后,如果远程设备上的特征发生更改,则会触发 onCharacteristicChanged() 回调:

@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
关闭客户端应用
public void close() {
    if (bluetoothGatt == null) {
        return;
    }
    bluetoothGatt.close();
    bluetoothGatt = null;
}