闭门造车姚师傅

低功耗蓝牙 Overview

字数统计: 2.6k阅读时长: 12 min
2019/01/13 Share

最近来 Keep 了,做智能设备相关的工作,所以认真看了看 BLE (低功耗蓝牙)的文档。这篇文章的内容几乎全部来源于 https://developer.android.com/guide/topics/connectivity/bluetooth-le,现在相当于直接 fork 一份,以后可能会慢慢加些其他的内容。这篇文档的示例代码比较学院派,或者叫比较 Sofeware Engineering 可能要用稍微长一些的时间看。

从 Android 4.3 开始,API Level 18 引入对 BLE 的支持。

BLE 的主要应用范围:

  • 小通信量数据的近距离传输
  • 基于距离传感的拓展应用(eg. Google Beacons)

顾名思义,和传统蓝牙相比,BLE 在功耗上有很大的优势。TI 的有些 BLE 模块的电流仅仅不到 1 微安( 数据来源 ),主流的 BLE 功耗应该都是这个数量级。

几个术语

  • GATT, Generic Attribute Profile

    GATT Profile 是通过 BLE 链路传输小数据包的一个通用规范,这些小数据包被称为 attribute。目前所有的低功耗应用技术规范(application profile)都是基于 GATT 的。

    The Bluetooth SIG defines many profiles for Low Energy devices. A profile is a specification for how a device works in a particular application. Note that a device can implement more than one profile. For example, a device could contain a heart rate monitor and a battery level detector.

  • ATT, Attribute Protocol

GATT 是基于 ATT 实现的,像 TCP/IP 一样,这套标准通常也被叫做 GATT/ATT。ATT 专门为运行在 BLE 设备上进行了优化。

  • Characteristic

    A characteristic contains a single value and 0-n descriptors that describe the characteristic’s value. A characteristic can be thought of as a type, analogous to a class.

  • Descriptor

    Descriptors are defined attributes that describe a characteristic value. For example, a descriptor might specify a human-readable description, an acceptable range for a characteristic’s value, or a unit of measure that is specific to a characteristic’s value.

  • Service

    A service is a collection of characteristics. 比如有个 Service 叫”心率传感器” 包含 “心率测量” 这个 characteristic

    You can find a list of existing GATT-based profiles and services on bluetooth.org.

角色,职责

接下来,看一下当 Android设备 和 BLE设备 交互的时候,他们之间可能的角色和对应的职能。

  • Central(中心) VS. Peripheral(外围),这个关系对应于 BLE 连接本身。“中心”设备扫描监听广播,“外围”设备发出广播。
  • GATT Server VS. GATT Client,这个关系对应当连接建立后两个设备怎么通信。

为了理解这里的区别,想象一下你有一个 Android 手机 和 一个 BLE 设备 —— 运动传感器。手机可以作为中心设备,运动传感器作为外围设备。(只有一个中心设备一个外围设备才可以,两个中心设备或者两个外围设备都是连不上的)。

一旦手机和运动传感器建立连接后,二者之间开始传输 GATT 元数据。取决于传输数据类型的情况,手机也运动传感器都可能扮演 Server 的角色。比如,如果运动传感器想向手机发送传感器数据,一般就是运动传感器作为 Server;再比如,如果运动传感器想从手机接受任何的数据更新(更新校准传感器甚至是 OTA 升级),这种情况下,一般是手机作为 Server。

BLE 权限

任何跟蓝牙相关的功能都需要声明 BLUETOOTH 权限,如果要开始扫描其他或者修改蓝牙设置,还要声明 BLUETOOTH_ADMIN 权限。

1
2
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

如果需要限制 app 只能在支持 BLE 的设备上运行,应该在 manifest 额外加上这一行:

1
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

不过不是强制要求 BLE 支持的话,可以设置 requiredfalse 或者直接不加上面那行 uses-feature ,然后在运行的时候使用 PackageManager.hasSystemFeature() 来判断当前设备是否支持 BLE:

1
2
3
4
5
6
// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
finish();
}

LE 信标通常跟地理位置联系在一起,为了使用 BluetoothLeScanner ,应该声明 ACCESS_COARSE_LOCATION 或者 ACCESS_FINE_LOCATION。不然的话,扫描不会返回结果。ops

配置 BLE

  1. 获取 BluetoothAdapter

    1
    2
    3
    4
    private val mBluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothManager.adapter
    }
  2. 开启蓝牙

    1
    2
    3
    4
    5
    6
    // Ensures Bluetooth is available on the device and it is enabled. If not,
    // displays a dialog requesting user permission to enable Bluetooth.
    if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

查找 BLE 设备

扫描操作比较耗电,有几点要注意的地方:

  • 找到想要的设备后,应该马上停止扫描
  • 不要一直一直扫描,最好设置最大扫描时间(永远不要过分肯定一个设备是可以连上的,如果一个设备跑到了最远连接距离之外或者关机了之类的,这时候持续的扫描会大量消耗电量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private const val SCAN_PERIOD: Long = 10000

/**
* Activity for scanning and displaying available BLE devices.
*/
class DeviceScanActivity(
private val mBluetoothAdapter: BluetoothAdapter,
private val mHandler: Handler
) : ListActivity() {

private var mScanning: Boolean = false

private fun scanLeDevice(enable: Boolean) {
when (enable) {
true -> {
// Stops scanning after a pre-defined scan period.
mHandler.postDelayed({
mScanning = false
mBluetoothAdapter.stopLeScan(mLeScanCallback)
}, SCAN_PERIOD)
mScanning = true
mBluetoothAdapter.startLeScan(mLeScanCallback)
}
else -> {
mScanning = false
mBluetoothAdapter.stopLeScan(mLeScanCallback)
}
}
}
}

如果只想扫描特定类型的外设,可以使用 startLeScan(UUID[], BluetoothAdapter.LeScanCallback),其中 UUID[] 代表想要扫描的 GATT 设备。

下面是一个 LeScanCallback 实现——将扫描到的蓝牙设备放到一个列表里显示:

1
2
3
4
5
6
7
8
val mLeDeviceListAdapter: LeDeviceListAdapter = ...

private val mLeScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
runOnUiThread {
mLeDeviceListAdapter.addDevice(device)
mLeDeviceListAdapter.notifyDataSetChanged()
}
}

不能在扫描 BLE 设备的时候同时扫描传统的蓝牙设备,反之亦然。

连接到 GATT Server

连接 BLE 设备上的 GATT Server,返回一个 BluetoothGatt,客户端通过这个 BluetoothGatt 与 BLE 设备交互。

1
2
3
var mBluetoothGatt: BluetoothGatt? = null
...
mBluetoothGatt = device.connectGatt(this, false, mGattCallback)

下面这个例子,app 这边有个 Activity 来连接、显示数据、显示 BLE 设备支持的 GATT 服务(Service) 和 特性(Characteristics)。这个 Activity 通过 BluetoothLeService 这个 Service 使用 BLE API 来跟 BLE 设备交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 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 mBluetoothManager;
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
private int mConnectionState = 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 mGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
mConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
Log.i(TAG, "Attempting to start service discovery:" +
mBluetoothGatt.discoverServices());

} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
mConnectionState = 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);
}
}
...
};
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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);
}

然后回到 ActivityBroadcastReceiver 来处理这些事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a
// result of read or notification operations.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
mConnected = true;
updateConnectionState(R.string.connected);
invalidateOptionsMenu();
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
mConnected = 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(mBluetoothLeService.getSupportedGattServices());
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
}
}
};

读取 BLE 属性

一旦 Android App 连接到 GATT Server 和 发现的服务后,就可以读写属性了。下面的这段代码遍历 Server 的服务和属性并且显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class DeviceControlActivity : 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 fun displayGattServices(gattServices: List<BluetoothGattService>?) {
if (gattServices == null) return
var uuid: String?
val unknownServiceString: String = resources.getString(R.string.unknown_service)
val unknownCharaString: String = resources.getString(R.string.unknown_characteristic)
val gattServiceData: MutableList<HashMap<String, String>> = mutableListOf()
val gattCharacteristicData: MutableList<ArrayList<HashMap<String, String>>> =
mutableListOf()
mGattCharacteristics = mutableListOf()

// Loops through available GATT Services.
gattServices.forEach { gattService ->
val currentServiceData = HashMap<String, String>()
uuid = gattService.uuid.toString()
currentServiceData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownServiceString)
currentServiceData[LIST_UUID] = uuid
gattServiceData += currentServiceData

val gattCharacteristicGroupData: ArrayList<HashMap<String, String>> = arrayListOf()
val gattCharacteristics = gattService.characteristics
val charas: MutableList<BluetoothGattCharacteristic> = mutableListOf()

// Loops through available Characteristics.
gattCharacteristics.forEach { gattCharacteristic ->
charas += gattCharacteristic
val currentCharaData: HashMap<String, String> = hashMapOf()
uuid = gattCharacteristic.uuid.toString()
currentCharaData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownCharaString)
currentCharaData[LIST_UUID] = uuid
gattCharacteristicGroupData += currentCharaData
}
mGattCharacteristics += charas
gattCharacteristicData += gattCharacteristicGroupData
}
}
}

注册 GATT 通知

除了了主动去拉取数据之外,注册回调以在 BLE 设备的特定属性发生变化时得到通知可能是更常见的需求

下面的代码片段展示了如何通过 setCharacteristicNotification() 来为特定属性注册回调通知:

1
2
3
4
5
6
7
8
9
10
lateinit var mBluetoothGatt: BluetoothGatt
lateinit var characteristic: BluetoothGattCharacteristic
var enabled: Boolean = true
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled)
val uuid: UUID = UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)
val descriptor = characteristic.getDescriptor(uuid).apply {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
}
mBluetoothGatt.writeDescriptor(descriptor)

当这个属性发生变化时,会触发BluetoothGattCallback#onCharacteristicChanged(android.bluetooth.BluetoothGatt, BluetoothGattCharacteristic),可以从这里拿到变化过的 Characteristic

关闭 Client APP

用完了 BLE 设备之后,应该显式调用 #close 以恰当地释放资源。

1
2
3
4
fun close() {
mBluetoothGatt?.close()
mBluetoothGatt = null
}

根据 BLE 设备信号强度推测相对位置的公式是:d = 10^((abs(RSSI) - A) / (10 * n))


为什么通常是 BLE 设备做 Server?

  • 通常情况下 BLE 设备拥有数据
  • BLE 设备做的事情更专一化,更适合 Server 这个角色
  • BLE 设备功耗低
  • 满足多客户端同时取数据的要求
CATALOG
  1. 1. 几个术语
  2. 2. 角色,职责
  3. 3. BLE 权限
  4. 4. 配置 BLE
  5. 5. 查找 BLE 设备
  6. 6. 连接到 GATT Server
  7. 7. 读取 BLE 属性
  8. 8. 注册 GATT 通知
  9. 9. 关闭 Client APP