Android内置平台在Android4.3(API 18)开始引入了在 中央设备 对低功耗蓝牙(BLE)的支持,并且提供了一系列的API用于应用进行发现蓝牙设备、寻找服务以及传输信息等操作。
以下是BLE的常见的使用场景:
在近距离内两个BLE设备的少量数据的传递。
与Google Beacons等接近式传感器交互,根据用户当前的位置为用户提供定制的服务。
相对于经典蓝牙(Classic Bluetooth),低功耗蓝牙(BLE)设计的目的为了更显著地降低在通讯过程中的功耗。这就使得Android的应用与那些对功耗有严格要求的BLE设备之间通讯成为可能,例如接近式传感器、心率监视器和健身设备。
解读:这段的介绍中,首先是介绍了低功耗蓝牙(BLE)的最大的特点:低功耗。其次是表述的是从Android4.3(API 18)之后才开始支持BLE的,若想在一个Android设备中开发BLE相关的程序,那这个设备必须具备两个条件:Android4.3以上的操作系统和蓝牙4.0以上的模块。最后是介绍了BLE的一般使用场景和BLE相关的设备。
以下是关键BLE术语和概念的摘要:
解读:这一节介绍就是BLE的核心的概念,只有理解好这些概念,才能更好在BLE开发中解决问题。
相对于经典蓝牙的流方式进行数据交换,BLE采用的是GATT/ATT,然后通过一种特别的通道Characteristic来进行数据的接收和发送。
从我理解来看,在一个BLE设备中,数据的传输都是基于GATT/ATT的。那设备中数据的载体是什么?其实数据的载体就是属性(attribute),一个BLE设备当中可能有多个attribute,每个attribute通过ATT协议转化成了一个service,而service是一个集合,是characteristic的集合。一个service里面可能有多个characteristic,而数据就是存在characteristic中。我们可以在这些characteristic中进行读、写或者通知等等操作。
以下是一个Android设备和一个BLE设备交互时所对应的角色和职责:
- 中央和外围。这对应着BLE自身的连接。中央设备扫描、寻找广告,而外围设备则是发送广告。
为了明白这些区别,想象你同时拥有一台Android手机和一个活动追踪器(BLE设备)。手机作为中央设备,活动追踪器作为外围设备(你需要具备两种角色设备才能建立一个BLE连接,两者都担任外围设备角色不能互相通信,同样两者都担任中央设备角色也不能互相通信)。
一旦设备和活动追踪器建立起一个连接,它们就开始相互传递GATT的元数据。根据它们传递的数据类型,其中一方需要担任服务器的角色。例如说,如果活动追踪器想要发送传感器的数据到手机上,活动追踪器合理地就担任了服务端的角色。假如活动追踪器想接收手机上更新变化的数据,那么这时,手机就合理地担任服务端的角色。
本篇文档中讲到的案例中(后面),Android 应用(运行在Android设备中)是一个GATT客户端。应用从一个GATT服务端获取数据,而担任这个GATT服务端的是一个支持Heart Rate Profile 的BLE心率监测器设备。但是你也可以交替选择你的Android 应用担任GATT服务端角色。具体请参考BluetoothGattServer以获取更多的信息。
解读:这段主要描述Android设备和BLE设备在通讯的过程中扮演的角色。一般Android设备是中央设备,BLE设备为外围设备。外围设备是被扫描的,中央设备扫描发现外围设备,一个中央设备可以对应多个外围设备。 只有两种设备同时存在,才能建立一个BLE连接。
两种设备建立连接之后,根据数据的传递流向,分为服务端和客户端。服务端提供数据,客户端获取数据。在数据通讯的过程中,两种设备的角色可以根据数据的传递流向来互换。
为了在你的程序里能使用蓝牙功能,你需要声明该蓝牙权限BLUETOOTH。你执行任何的蓝牙的交互操作都需要这个权限,比如请求一个连接,接受一个连接以及传输数据。
如果你希望你的应用发起一个发现设备或者修改蓝牙设置的操作,你还必须声明BLUETOOTH_ADMIN权限。注意:如果你声明了BLUETOOTH_ADMIN权限,你还必须同时声明BLUETOOTH权限。
在应用程序的清单文件中声明蓝牙权限。例如:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
如果你希望你的应用只能在支持BLE的设备上是可用的,那么你需要在应用程序的清单文件上声明如下:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
当然,如果你想使你的应用在不支持BLE的设备也是可用的,你仍然要声明上述的内容,不同的是,设置required = "false"
。然后,在运行的时候你就可以决定BLE的可用性了通过PackageManager.hasSystemFeature()
的方式:
// 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();
}
解读:调用Android设备上的BLE模块,需要在程序这中声明对应的权限,一般都要同时声明
BLUETOOTH
以及BLUETOOTH_ADMIN
这两个权限才能满足BLE开发的大部分需求。关于第三个特性的声明,主要表达的是你可以选择让你程序只在支持BLE模块的Android设备正常运行,否则程序都无法打开。而另外一种就是打开程序,在程序代码里面进行判断设备是否支持BLE的判断,然后优雅的拒绝不支持BLE的设备(蜜汁微笑)。
为了从NETWORK_PROVIDER或者GPS_PROVIDER获取信息位置的变化,你必须通过在Android的清单文件中分别声明ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATIO}权限来请求用户的权限。没有这些权限,你的应用请求位置信息更新的的操作将会失败,并且出现权限错误的信息。
如果你同时使用NETWORK_PROVIDER和GPS_PROVOIDER,你只需要请求ACCESS_FINE_LOCATION权限,因为它包括了这两个提供者的权限。ACCESS_COARSE_LOCATION只允许访问NETWORK_PROVODER.
警告:如果你的应用定位只能在Android5.0(API level 21)或更高的系统中运行,你必须在清单文件中声明feature,
android.hardware.location.network
或者android.hardware.location.gps
,声明哪一个feature取决于你的应用是从NETWORK_PROVIDER还是从GPS_PROVIDER中获取的位置信息的更新。如果你的应用从它们中的其中一个位置提供者的资源中获取了位置的信息,那么你需要在你的应用的清单文件中声明应用需要使用这些硬件功能。若在Android5.0(API 21)之前版本上运行该应用,请求的ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限中,默认包含了使用位置硬件功能的声明。然而,在Android5.0(API level 21)和更高的版本中请求者两个权限不会请求位置相关的硬件功能,需要手动添加上关于使用位置相关的硬件功能的声明。
下面的代码示例演示了一个应用从设备的GPS中读取数据需要在在清单文件中如何声明所需权限和硬件功能。
... >
"android.permission.ACCESS_FINE_LOCATION" />
...
"android.hardware.location.gps" />
...
解读:我也不太明白这两个位置权限在BLE操作中扮演一个怎么样的角色,或许只有用到位置相关的操作时才会涉及到(吐槽:但是这个介绍的BLE的主题有毛线关系啊?!!!),还有需要注意的是,有的人在Android6.0的设备中,发现必须声明这些权限才能正常进行BLE的操作:6.0机型扫描不到蓝牙设备(完全不明白为什么要这么做,若有人知道,希望告知,不胜感激!)。既然在6.0需要声明其中的权限,那么就如上面所述,对应的硬件功能的声明也要加上,如果你应用定位到23,这两个权限就还同时涉及到6.0的权限运行时操作(真是蛋痛),不然无法进行BLE操作。反正我在Android5.0的设备中没有声明这些权限和功能,一样能正常运行。
你的应用程序在BLE上进行通讯之前,你需要检测当前的Android的设备上是否支持BLE,如果支持,还要确保设备上的蓝牙是可见的。需要注意的是,只有当
中android:required = false
时,这步检测才是需要的。
如果当前设备不支持BLE,你应该优雅禁用掉BLE功能相关的操作。如果是支持BLE的,但蓝牙却不可见,之后你就可以在不离开你的应用程序的界面上请求用户开启蓝牙。这些操作需要使用BluetoothAdapter来完成,分两个步骤:
private BluetoothAdapter mBluetoothAdapter;
...
// Initializes Bluetooth adapter.
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
false
,表示当前蓝牙是不可见的。下面的代码块检查蓝牙是否是可见的,如果是不可见的,该代码段会显示一条错误,提示用户转到“设置”以启用蓝牙:// 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);
}
注意:传递给startActivityForResult(android.content.Intent, int)的
REQUEST_ENABLE_BT
常量是一个本地定义的整数(该数必须大于0),它在系统中实现作为requestCode
的参数返回到你的onActivityResult(int, int, android.content,Intent)
方法中去。
解读:从这段开始,这篇文档通过讲解Google工程师写的一个BLE案例来展现BLE的知识和使用。附上这个案例在GitHub上的地址:BluetoothLeGatt。同时附上我个人对这个案例的浅陋分析:BLE开发浅谈
在程序中使用startLeScan()方法来扫描BLE设备。这个方法需要一个BluetoothAdapter.LeScanCallback
来作为参数。你必须实现这个回调,因为这个回调返回了扫描 BLE设备的结果。由于扫描是比较耗电的操作,你应该遵守以下的指导:
- 一旦你扫描到你想要的设备,立刻停止扫描操作。
- 千万不要在你扫描操作中做死循环扫描。然后你需要设置一个时长来限制扫描设备的时间。因为一个以前可被扫描的设备可能被移出了可扫描的距离范围,导致无法被扫描,若再继续进行扫描,电池将会被耗尽。
下面的代码块演示了如何开始和停止一个扫描操作:
/**
* 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);
}
...
}
...
}
如果你只想扫描指定类型的外围设备,你可用startLeScann(UUID[], BluetoothAdapter.LeScannCallback)来代替上面扫描的方法,并提供
一个你的应用所支持的GATT服务的UUID对象数组。
下面是BluetoothAdapter.LeScanCallback
回调的实现,它用于传递扫描BLE设备的结果:
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文档中描述的一样,你只能同时扫描其中的一种设备,要么是蓝牙低功耗设备,要么是经典蓝牙设备,不能同时扫描这两种设备。
解读:在设置完BLE之后(使其可用,且获取适配器对象),然后就是扫描周围是可用的BLE设备。需要注意的是在程序中做扫描操作的几条禁忌,就是在扫描设备的过程中,尽量减少对电池的消耗。
(应用程序)和BLE设备交互的第一步就是连接它——进一步来说,是连接上BLE设备中的GATT服务。你可以使用connectGatt方法来连接到在一个BLE设备上的GATT服务。这个方法需要三个参数:Context对象,autoConnect(boolean类型,表示BLE设备一旦变得可访问的,是否自动连接),以及一个BluetoothGattCallback回调的实例:
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
代码连接到的是BLE设备上管理的GATT服务,并且返回一个BluetoothGatt实例,你可以在后续中使用它来进行GATT客户端相关的操作,而GATT客户端就是Android应用(调用者)。BluetoothGattCallback回调是用于传递连接给客户端,这些结果包括连接状态,以及任何其他GATT客户端的操作。
在这个案例中,BLE的应用提供了一个Activity(DeviceControlActivity
)来作为连接、显示数据,以及显示BLE设备所支持的GATT的service和characteristic。基于用户的输入,这个Activity和一个叫做BluetoothLeService
的Service(四大组件之一)进行通信,这个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()
辅助方法,并且传递一个标识参数。需要注意的是,本节中的数据解析是根据蓝牙心率测量配置文件规范执行的:
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));
}
}
};
解读:Android BLE API最核心的操作当属连接到BLE设备上的GATT服务这些操作。既然在上节的描述中,扫描到指定的设备,然后这节通过扫描到的设备的实例对象进行GATT连接,连接的参数中有三个:Context,autoConnect以及最重要的GattCallback回调。该回调中基本包含了应用程序对BLE设备操作的全部回馈,包括连接状态改变、service和characteristic的发现、读写characteristic数据、notify指定characteristic的数据以及连接中读取RSSI值回调等。然后触发了那些回调,在这些回调中,调用发送广播的方法,将一些数据返回到Activity中去,处理或者显示。
一旦你的Android应用连接上BLE设备上GATT服务端,并且发现了service,那么应用就可以在里面那些支持读写的属性进行读和写。 例如下面的代码块遍历了BLE服务端所有的service和characteristic,并在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 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> gattServiceData =
new ArrayList>();
ArrayList>> gattCharacteristicData
= new ArrayList>>();
mGattCharacteristics =
new ArrayList>();
// Loops through available GATT Services.
for (BluetoothGattService gattService : gattServices) {
HashMap currentServiceData =
new HashMap();
uuid = gattService.getUuid().toString();
currentServiceData.put(
LIST_NAME, SampleGattAttributes.
lookup(uuid, unknownServiceString));
currentServiceData.put(LIST_UUID, uuid);
gattServiceData.add(currentServiceData);
ArrayList> gattCharacteristicGroupData =
new ArrayList>();
List gattCharacteristics =
gattService.getCharacteristics();
ArrayList charas =
new ArrayList();
// Loops through available Characteristics.
for (BluetoothGattCharacteristic gattCharacteristic :
gattCharacteristics) {
charas.add(gattCharacteristic);
HashMap currentCharaData =
new HashMap();
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服务端中,有多个的属性,它们通过ATT转换service和characteristic。这段代码就是遍历BLE服务端所有的service,然后service下的所有characteristic。
Android BEL应用要求在BLE设备的某个特定的characteristic中数据发生变化时接收到通知是很常见的。下面的代码块演示了如何设置给一个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);
}
解读:notify(通知)这个坑最好还是趟一下,你就明白其中的原理……算了,characteristic分为多类,如可读、可写、可通知等等(还有几种没列出),其中通知是最复杂的,对可通知的characteristic设置通知:
setCharacteristicNotification()
方法之后,还要设置该characteristic的Descriptor
(见本段第一处代码),如果你任性不设置的话,那你根本得不到通知!
一旦你的应用使用完BLE设备,程序中应该调用close方法,这样系统才能适当地释放资源:
public void close() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
}
解读:
close()
方法释放资源,并非如此简单,其实并没有完全断开与设备的联系,下一次进行扫描连接的话,BLE记忆的还是上一次的设备,若要在下一次连接不同的设备,这个问题着实有点令人头痛,而且真其中坑有点多,我也有点模糊,就不献丑。其实翻译完之后,我也是有点懵比的,好吧,若想理解官方给出的案例,请参考BluetoothGatt,若想看看其他人的BLE的数据框架我推荐这个FastBle。
原文:Bluetooth Low Energy
参考译文:Android蓝牙低功耗