Bluetooth Low Energy(蓝牙低功耗开发)
关键术语和概念
以下是蓝牙低功耗关键术语与概念的一个总结:
- Generic Attribute Profile (GATT)— GATT profile 是一个通过 BLE 连接来发送和接收名为 "attributes"的小块数据的通用 profile. 当前所有低功耗应用的 profiles 都是基于GATT.
- Bluetooth SIG 为蓝牙低功耗设备定义了许多 profiles . 一个 profile 是用来表示某一设备如何工作于特定程序上的一个规格.需要注意的是一个设备可以实现不止一个 profile.例如, 一个设备可以同时包含心率监测器和电量级别检测器.
- Attribute Protocol (ATT)— GATT 建立在 Attribute Protocol (ATT)之上. 它也可以指代 GATT/ATT. ATT 是为运行在 BLE 设备上而优化过的. 它使用尽可能少的字节.每一个 attribute 都是通过Universally Unique Identifier (UUID)定义的独一无二的,作为一个标准的 128-bit 格式字符串 ID 来识别不同的信息. ATT 通过将attributes格式化为characteristics 和 services 来传输.
- Characteristic— 一个 characteristic 包含了一个单独的值和 0-n 个descriptors 来描述 characteristic 的值. 一个 characteristic 可以被认为是一种类型, 类似于一个类.
- Descriptor— Descriptors 是被定义的描述 characteristic 值的 attributes . 例如, 一个 descriptor 可能指定了一个人类可读的 描述, 一个 characteristic 值的可接受范围, 或者一个被指定来作为 characteristic 值的测量单位.
- Service— 一个 service是一系列characteristics 的集合. 例如, 你可以拥有一个service 叫做"Heart Rate Monitor" 包含了如"heart rate measurement."之类的characteristics. 你可以在bluetooth.org上发现一系列已存的 基于GATT的 profileis 和 services .
角色与职责
以下是当一个 Android 设备和一个 BLE 设备交互时的角色与职责:
- 中央 vs. 外围. 此条应用于 BLE 连接本身. 中央设备的角色负责扫描, 查找 advertisement, 外围角色的设备作为 advertisement.
- GATT 服务器 vs. GATT 客户端. 此条决定了一旦两个设备建立了连接,它们如何沟通.
要理解这个区别, 想象你拥有一个Android 手机和一个作为BLE 设备的活动追踪器. 手机作为中央角色; 活动追踪器作为外围角色 (为了建立一个BLE 连接你需要它们—--两样都只支持作为外围角色的设备,或者都只支持作为中央角色的设备是无法彼此通讯的).
一旦这个手机和活动追踪器建立了连接, 他们开始互相传输 GATT 元数据 . 基于他们传输数据的类型,当中的一部将扮演服务器角色. 例如, 如果活动追踪器想给手机提供传感器数据, 某种意义上活动追踪器将扮演服务器角色. 如果活动追踪器想要从手机接收更新, 那么手机将作为某种意义上的服务器.
作为文档中使用的例子, Android 程序 (运行在一部Android设备上) 是一个 GATT 客户端. 程序接收来自 GATT 服务器的数据, 这个服务器是个提供了HeartRate Profile的BLE 心率监听器 . 当然你可以选择性的让你的程序作为 GATT 服务器. 详细参考BluetoothGattServer
以获取更多信息.
BLE 权限
为了在你的程序里使用蓝牙组件, 你必须声明蓝牙权限 BLUETOOTH
.你需要这个权限来执行任何蓝牙通讯,比如请求连接, 接受连接, 以及数据传输.
如果你想要你的app能够发起蓝牙搜索或者操作蓝牙设置,你必须声明这个权限 BLUETOOTH_ADMIN
.注意: 如果你使用了BLUETOOTH_ADMIN
这个权限, 那你必须同时声明BLUETOOTH
权限.
在你程序的 manifest 里面声明权限. 例如:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
如果你想要声明你的程序只能在支持BLE特性的设备上运行,请在你程序的 manifest 里面加入以下内容:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
当然,如果你想要使你的程序在不支持BLE的设备上也能运行,你同样要在 manifest 里加入上述声明, 但是需要修改设置required="false"
. 然后在程序运行的时候你可以通过使用PackageManager.hasSystemFeature()
来判定BLE是否被支持:
// 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();
}
设置 BLE
在你的程序通过 BLE 运行之前, 你需要验证当前设备是支持 BLE的 , 之后, 再确定它是激活的.注意只有当<uses-feature.../>
设置为false的时候这才是必要的.
如果不支持BLE, 那你可以优雅的关掉BLE特性. 如果支持 BLE , 但是被禁用了, 那么你可以在程序内请求用户开启蓝牙. 这个设置需要两步来完成, 使用BluetoothAdapter
.
- 获取
BluetoothAdapter
所有的蓝牙活动都需要
BluetoothAdapter
.BluetoothAdapter
代表着设备本身的蓝牙适配器 (蓝牙通讯). 整个系统拥有一个蓝牙适配器, 而你的程序可以通过它来交互. 下面的片段展示了如何获取这个适配器. 主意这个方法通过getSystemService()
来获取一个BluetoothManager
的实例, 而BluetoothManager
又是获取适配器所需要的. Android 4.3 (API Level 18) 引入了BluetoothManager
:// Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); mBluetoothAdapter = bluetoothManager.getAdapter();
- 开启 Bluetooth
接下来, 你需要确保蓝牙是开启的. 使用
isEnabled()
去检查蓝牙当前是否开启. 如果这个方法返回了 false, 那么蓝牙被禁用了.如下片段展示了检查蓝牙是否开启. 如果没开启, 它会弹出一个错误提示来让用户到设置里开启蓝牙:private BluetoothAdapter mBluetoothAdapter; ... // 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 设备
想要发现BLE 设备, 你可以使用 startLeScan()
方法.这个方法带了一个参数BluetoothAdapter.LeScanCallback
. 你必须实现这个回调函数, 因为它定义了扫描结果如何返回. 因为扫描是相当耗电的, 你必须遵循以下规则:
- 一旦发现期待的设备,停止扫描.
- 不用循环扫描, 设置一个扫描时间限制. 一个之前可用的设备可能会移出范围, 继续扫描会耗干电量.
如下片段展示了如何开始和结束搜索:
/**
* Activity for scanning and displaying available BLE devices.
*/
public class DeviceScanActivity extends ListActivity {
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;
private Handler mHandler;
// 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.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}, SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback);
} else {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
...
}
...
}
如果你想扫描指定类型的周边设备, 你可以转而使用startLeScan(UUID[], BluetoothAdapter.LeScanCallback)
,提供一个指定了你的程序支持的GATTservices的UUID
类型的数组 .
以下是BluetoothAdapter.LeScanCallback
的一个实现,它是一个用来传递扫描结果的接口:
private LeDeviceListAdapter mLeDeviceListAdapter;
...
// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi,
byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLeDeviceListAdapter.addDevice(device);
mLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
注意: 你只能扫描蓝牙低功耗设备或者传统蓝牙设备, 在Bluetooth里描述的那些. 你不能同时扫描蓝牙低功耗设备和传统蓝牙设备.
连接到一个 GATT 服务器
和一个 BLE 设备交互的第一步是连接它—更确切的说, 连接到此设备的 GATT 服务器. 要连接到一个BLE的 GATT 服务器, 你可以使用connectGatt()
方法.这个方法带三个参数: 一个Context
对象,autoConnect
(boolean 值用来决定是否一旦一个 BLE 设备可用就自动连接到它), 以及一个BluetoothGattCallback
的引用:
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
这个与 GATT 服务器的连接由 BLE 设备管理, 并且返回了一个 BluetoothGatt
实例, 而通过这个实例你可以进行 GATT 客户端操作. 调用者 (即此 Android 程序) 即为 GATT 客户端. BluetoothGattCallback
用来为客户端传递结果, 例如连接状态, 以及一些更深入的 GATT 客户端操作.
在这个例子当中, BLE 程序提供了一个页面(DeviceControlActivity
)去连接,展示数据, 并且展示了该设备所支持的 GATT services 和 characteristics. 基于用户输入,该页面和一个名为BluetoothLeService
的Service
通讯,而它通过Android BLE API和 BLE 设备 进行交互:
// 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);
}
}
...
};
...
}
当一个特定的回调出发的时候, 它会调用合适的 broadcastUpdate()
帮助方法 并且传递给它一个 action. 注意数据解析在这一部分是和蓝牙心率测量profile specifications一致执行的:
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);
}
返回 DeviceControlActivity
,这些时间会被一个 BroadcastReceiver
处理:
// 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 属性
一旦你的程序连接上一个 GATT 服务器 并且发现了 services,就可以读、写它支持的属性. 例如, 下面片段迭代了服务器支持的所有 services 和 characteristics 并且将他们展示在了UI界面上:
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);
}
...
}
...
}
接收 GATT 通知
当设备的某个特定 characteristic 发生改变时 BLE 程序通常会要求得到通知. 如下片段展示了如何为某一 characteristic 设置通知, 通过使用setCharacteristicNotification()
方法:
private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
一旦开启了某个 characteristic 的通知,那么当这个 characteristic 在远程设备上发生改变时会触发一个回调方法onCharacteristicChanged()
:
@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
关闭客户端程序
一旦你的程序使用BLE完毕, 它应该调用close()
使系统适当的释放资源:
public void close() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
}