前言
蓝牙4.0是蓝牙技术联盟于2010年7月7日推出的新标准,包含高速蓝牙、经典蓝牙和低功耗蓝牙(Bluetooth low energy,简称BLE)三种模式,分别应对数据交换与传输、信息沟通与设备连接、低带宽设备连接为主的不同应用需求。
随着蓝牙技术由手机、游戏、耳机、便携电脑和汽车等传统应用领域向物联网、医疗等新领域扩展,蓝牙设备对低功耗的要求会越来越高。从以下表中传统蓝牙和低功耗蓝牙的技术对比可以看到,低功耗蓝牙相比传统蓝牙功耗大幅降低,极大地适应了物联网发展的需求。
技术规范 | 传统蓝牙 | 低功耗蓝牙 |
---|---|---|
无线电频率 | 2.4GHz | 2.4GHz |
理论通信距离 | 100m | >100m |
空中数据线 | 1~3Mbps | 1Mbps |
支持活跃从设备数 | 7 | 未定义(理论最大值 2 32 2^{32} 232) |
延迟 | 100ms | 6ms |
安全性 | 64/128bit | 128bit AES |
语音能力 | 有 | 无 |
耗电量 | 1W(参考值) | 0.01~0.5W(依赖使用请款) |
峰值电流消耗 | <30mA | <15mA |
自从API 5中首次引入了蓝牙之后,Android支持蓝牙已经很长时间了。这种蓝牙被称为传统的蓝牙。从API 18开始,开发者可以使用低功耗蓝牙(Bluetooth low energy,简称BLE),或者Bluetooth Smart。BLE提供了一个通用协议版本,它使用一些增强手段来允许使用更低的功耗,接收端和发送端都可以省电。它也带来了使用新协议的能力,比如Eddystone,当有一个设备在附近的时候,允许使用“beacons”来检测与它进行交互而不用配对。
为了更进一步使用蓝牙,在API 21+中加入了Generic Access Profile蓝牙栈。
蓝牙通信的步骤
- Discovery
- Exploration
- Interaction
在Discovery阶段,两个设备把它们的当前可用的信息广播给对方。当它们找到彼此之后,智能设备会进入配对或者信息交换模式,然后开始把唯一的地址广播出去,以便其他在蓝牙射频距离之内的蓝牙设备可以接收。
一旦两个设备发现彼此之后,他们会进入Exploration阶段。在这个阶段,一个设备会发出请求与另一个设备进行配对。根据设备和当前支持的蓝牙,配对有可能不需要交换数据,因为数据可以通过加密密钥来传递。
接下来进入Interaction阶段。虽然这并不是作为一个安全措施而严格要求的,在设备进入一个完全交互性模式之前,它可能要求输入一个密码或者交换密码来确认设备是在连接到期待的设备。请注意,从BLE开始,这不是一个标准的配对模式,因为连接是偶然的。
不管是工作于传统的蓝牙或者是BLE,都可以在android.bluetooth
包里找到想要的API。而且,访问蓝牙射频需要请求用户权限,因此,你要把下列代码加到manifest XML中:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
第一个权限是允许访问蓝牙硬件,然而第二个权限允许开启蓝牙射频以及把它用于设备的Discovery。
如果工作于BLE设备,而想通过过滤APP使只有支持BLE的设备才能从Google Play Store下载你的应用,可以使用下面的<uses-feature>
元素和以下的权限:
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
UUID
UUID指的是统一标识码,主要用于标识Service和characteristic(特征,主要指的是一些BLE蓝牙的小功能,比如电量、心率、步数、卡路里等)。一个Service可以包含多个Characteristic,比如健康的Service可以包含了心率、步数、卡路里等Characteristic。
UUID的格式一般是“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”可到“http://www.uuidgenerator.com”申请。
更多可以参见:服务化框架-分布式Unique ID的生成方法一览
##开启蓝牙
当工作于传统的蓝牙时,你需要使用BluetoothAdapter类和getDefaultAdapter()函数来查看设备上的蓝牙是否可用。如果当前由一个适配器可用但没有开启,你可以发送一个Intent来开启蓝牙。下面的代码演示了这是如何实现的。
BluetoothAdapter myBluetooth = BluetoothAdapter.getDefaultAdapter();
if(!myBluetooth.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
开启BLE适配器的流程类似,但是有一个明显的差别就是使用BluetoothManager
来获取适配器,而不是使用BluetoothAdapter
类来获取。创建适配器之后,你可以检查这个适配器是否存在以及是否开启。代码如下:
private BluetoothAdapter myBluetoothAdapter;
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
myBluetoothAdapter = bluetoothManager.getAdapter();Bluetooth
// 如果适配器不存在或者适配器没有打开
if (myBluetoothAdapter == null || !myBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
// 启动BLE蓝牙
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
现在蓝牙已经开启并且可以使用了,是时候去查找附近的设备了。
使用蓝牙发现设备
如果以前没有和设备配对过,并且正在使用传统的蓝牙,你将需要扫描你能连接的可用设备。这可以通过调用startDiscovery()
实现,它会开始一个短扫描,查看附近的设备哪些可以连接上。下面的代码展示了如何使用一个BroadcastReceiver来发送Intent给扫描中找到的蓝牙设备:
// 扫描传统的蓝牙设备
private final BroadcastReceiver myReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// 如果找到一个蓝牙设备,从Intent创建一个对象。
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 显示找到的设备的名字和地址。
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
};
// 注册BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(myReceiver, filter);
完成扫描之后,应该使用cancelDiscovery()方法。这可以使资源和处理器密集型的活动停止,从而提高性能。你也应该记得应用生命周期的onDestory()函数里注销myReceiver。
如果已经和一个设备配对,则可以通过获取以前配对过的设备列表来节约一些设备资源,然后扫描查看它们是否还可用。下面的代码演示了如何获取设备列表:
Set<BluetoothDevice> pairedDevices = myBluetoothAdapter.getBondedDevices();
if(pairedDevices.size() > 0) {
for(BluetoothDevice device : pariedDevices) {
// add found devices to a view
myArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
因为BLE设备行为不一样,所以扫描它们时要使用不一样的方法。startLeScan()
函数扫描设备,然后使用一个回调来显示扫描结果。下面的代码演示了如何扫描和使用回调函数显示结果:
private BluetoothAdapter myBluetoothAdapter;
private boolean myScanning;
private Handler myHandler;
// 20s后停止扫描
private static final long SCAN_PERIOD = 20000;
private void scanLeDevice(final boolean enable) {
if (enable) {
// 在一个预定义的时间之后停止扫描
myHandler.postDelayed(new Runnable() {
@Override
public void run() {
myScanning = false;
myBluetoothAdapter.stopLeScan(myLeScanCallback);
}
}, SCAN_PERIOD);
myScanning = true;
myBluetoothAdapter.startLeScan(myLeScanCallback);
} else {
myScanning = false;
myBluetoothAdapter.stopLeScan(myLeScanCallback);
}
}
private LeDeviceListAdapter myLeDeviceListAdapter;
// BLE扫描回调
private BluetoothAdapter.LeScanCallback myLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi,
byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
myLeDeviceListAdapter.addDevice(device);
myLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
使用传统的蓝牙连接
使用传统的蓝牙通信的时候,一个设备必须是服务器。请注意,一个服务器可以有多个客户端,而它是扮演着任意其他连接设备的中间人的角色。客户端彼此之间不能直接通信,因此,服务器必须要转发和管理多个客户端之间的共享数据。
为了建立通信,我们打开了一个Socket,然后把数据传过去。为了确保数据被传到正确的客户端,创建Socket时必须要传递全局唯一标识符(简称UUID)。Socket创建之后,使用accept()来监听,然后当它完成后,使用close()关闭Socket。使用**accept()**的时候要特别小心,因为它是阻塞的,因此,不能在主线程中运行。下面代码演示了如何创建socket
和作为服务器接收通信:
private class AcceptThread extends Thread {
private final BluetoothServerSocket myServerSocket;
public AcceptThread() {
// 创建一个临时对象和myServerSocket一起使用,因为myServerSocket是final。
BluetoothServerSocket tmp = null;
try {
// MY_UUID 是应用 UUID 字符串。
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
myServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// 确保 myServerSocket 不为空
if (myServerSocket != null) {
// 使用循环来保持Socket开放而不管是错误还是返回数据。
while (true) {
try {
socket = myServerSocket.accept();
} catch (IOException e) {
break;
}
if (socket != null) {
// 在一个不同的线程中使用一个方法来处理返回数据。
manageConnectedSocket(socket);
myServerSocket.close();
break;
}
}
}
}
// 这个方法会关闭Socket和线程
public void cancel() {
try {
myServerSocket.close();
} catch (IOException e) { }
}
}
如果要以客户端的方式连接,需要创建一个包含服务器的BluetoothDevice
对象。然后,需要传一个匹配的UUID,它会被用来确保你和正确的设备进行通信。就像以服务器的方式进行通信那样,connect()
函数用来建立连接和获取数据或者错误。下面的代码演示了如何以客户端的方式连接:
private class ConnectThread extends Thread {
private final BluetoothSocket mySocket;
private final BluetoothDevice myDevice;
public ConnectThread(BluetoothDevice device) {
// 为mySocket创建一个临时对象,因为mySocket是final。
BluetoothSocket tmp = null;
myDevice = device;
// 获取一个 BluetoothSocket 来和 BluetoothDevice 连接
try {
// MY_UUID 是应用的 UUID 字符串。
tmp = device.createRfcomySocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mySocket = tmp;
}
public void run() {
// 取消 discovery 是因为它的连接会变慢。
mBluetoothAdapter.cancelDiscovery();
try {
// 使用Socket来连接或者抛出一个异常
// 这个方法是堵塞的。
mySocket.connect();
} catch (IOException connectException) {
// 不能连接,关闭Socket
try {
mySocket.close();
} catch (IOException closeException) { }
return;
}
// 在一个不同的线程里使用一个方法处理返回数据
manageConnectedSocket(mySocket);
}
// 这个方法会关闭Socket和线程
public void cancel() {
try {
mySocket.close();
} catch (IOException e) { }
}
}
使用BLE通信
前面提过,BLE在Exploration和Interaction阶段有一些小的改变。设备不用配对或者提供一个密码,而是可以直接检测设备,从而无交互地执行秘钥交换。这些秘钥提供了一种加密方法,它可以用来在设备之间加密和解密数据,而不需要在它们之间由一个成功的配对。
不用有一个确定的服务器和客户端关系,你只需要连到设备的**Generic Attribute Profile (GATT)**服务器。这可以通过connectGatt()
方法实现,该函数利用context
,Boolean
来决定autoConnect
和对回调函数的引用。代码如下:
myBluetoothGatt = device.connectGatt(this, false, myGattCallback);
说明:
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) {
return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
}
autoConnect参数 | 说明 |
---|---|
true | 自动连接。 |
false | 直接连接。 |
区分 | 自动连接(autoConnect为true)是一个连接尝试,它有30秒的超时时间。当直接连接在进行时,它将暂停所有当前的自动连接。如果已经有一个直接连接挂起,那么最后一个直接连接将不会立即执行,而是在前面已经完成的时候开始排队并开始执行。 有了自动连接,你可以同时拥有多个挂起的连接,而且它们永远不会超时(直到显式地中止或直到蓝牙关闭)。 如果连接是通过自动连接建立的,Android将会自动尝试在断开连接时重新连接到远程设备,直到您手动调用 disconnect() 或close() 。一旦连接通过直接连接断开连接,就不会尝试重新连接到远程设备。 直接连接(autoConnect为false)有不同的扫描间隔和扫描窗口,比自动连接更高,这意味着它将花费更多的无线电时间来监听远程设备的连接广播,也就是说,连接将会更快地建立起来。 |
这个回调函数可以在service
或者其他形式的逻辑里调用。下面是一个在service
里使用它的例子:
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;
myConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
Log.i(TAG, "Attempting to start service discovery:" +
gatt.discoverServices());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
myConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "Disconnected from GATT server.");
broadcastUpdate(intentAction);
}
}
@Override
// 发现新的服务
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
// 结果字符读取操作
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 调用一个更新函数来传输数据
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
};
在这个例子中,GATT服务器连接的敌方或者数据传输的时候,一个名叫broadcastUpdate()
方法会被调用,该函数处理你定制的逻辑。下面的例子演示了如何使用StringBuilder
处理传输的数据:
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);
// 为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);
}
如果处理通过Intent
发送的数据,需要创建一个BroadcastReceiver
。这个receiver
会收到不止一个设备的数据;它也监听GATT服务器的状态。通过监听事件,你可以处理断开连接、连接、数据传输和service
。下面是处理这些事件的一个例子:
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)) {
myConnected = true;
updateConnectionState(R.string.connected);
invalidateOptionsMenu();
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
myConnected = false;
updateConnectionState(R.string.disconnected);
invalidateOptionsMenu();
clearUI();
} else if (BluetoothLeService.
ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
// 为支持的服务和特性更新UI
displayGattServices(mBluetoothLeService.getSupportedGattServices());
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
}
}
};
连上GATT服务器后,就可以通过BluetoothGattService
找到可用的服务,并从那里读/写数据。你也可以使用myBluetoothGatt
对象来创建一个GATT通知的监听器,并使用setCharacteristicNotification()
方法通知本地系统一个特征值已经改变。为了通知远程系统,需要得到特征BluetoothGattDescriptor
,而且使用setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
给它设置值。然后,你可以用gatt.writeDescriptor
把值发送给远程系统。当BluetoothGattCallback
里的onDescriptorWrite
执行的时候,你就可以准备好接收更新了。完成设置后,当有GATT通知的时候,可以重写onCharacteristicChanged()
函数去广播一个更新。
当完成与BLE设备的通信的时候,请使用close()
方法关闭连接。线面是一个使用close()
的例子:
public void close() {
if (myBluetoothGatt == null) {
return;
}
myBluetoothGatt.close();
myBluetoothGatt = null;
}
附录
- 代码的示例请见:googlesamples/android-BluetoothLeGatt
- 这是Google官方给的代码示例,本文采用的就是该官方示例。
- Android Development Patterns-Best Practices for Professional Developers
- [美]Phil Dutson
- 《物联网导论——刘云浩》
- Which correct flag of autoConnect in connectGatt of BLE?
- 实现蓝牙的自动配对功能
- 这个Demo主要是通过蓝牙设备的名称查找,如果找到就用默认的PIN码(0000或者1234)进行连接。
- 一个成熟的蓝牙框架——FastBLE
其他
- 普通蓝牙EDR连接,BLE蓝牙连接,示例,操作蓝牙打印机Demo
- Android-BLE
- AndroidBLE蓝牙框架,包括扫描、连接、设置通知、发送数据、读取、接收数据和OTA升级以及各种直观的回调,近乎一行代码植入项目,可扩展配置蓝牙相关操作。