Android 蓝牙BLE开发详解
由于年初接手了个有关蓝牙BLE的项目,开始了对蓝牙ble的学习,经过长时间的慢慢学习(学得太慢,太拖了),终于了解了该怎么写蓝牙BLE,现在就给大家分享一下。
一、了解蓝牙BLE
1、什么是BLE
首先,我们来了解一下,什么是蓝牙BLE。
BLE的全名是 Bluetooth Low Energy 就是低功耗蓝牙的意思,支持 API18(Android 4.3)及以上的设备。
它的特点有低成本、短距离、可互操作。
通过GATT协议来进行BLE设备之间的通信。
2、BLE的优势
相比于传统蓝牙的高耗能,这个BLE可以说是低耗能至极,一颗纽扣电池都够用一年。所以,现在穿戴设备的流行,离不开BLE的发展。
蓝牙BLE适合传输数据小但实时性要求比较高的设备。比如手环。
3、BLE设备有什么东西
然后,我们来看一看,一个蓝牙设备里面,有哪些东西。
一个BLE终端可以包含多个Service(服务)
一个Service可以包含多个Characteristic(特征)
一个Characteristic包含一个value和多个Descriptor(描述符),一个Descriptor包含一个Value。
其中,我们要注意的是,每一个Service、Characteristic都会有一个uuid,这是一个唯一值,我们接下来的传输数据,将用到这个。每一个Characteristic都有一个Value,我们就是通过改变这个值,来对设备进行交互的。
小总结
这里引用其他文章的总结,来给大家一个更清晰的理解。
Generic Attribute Profile (GATT)
通过BLE连接,读写属性类小数据的Profile通用规范。现在所有的BLE应用Profile都是基于GATT的。
Attribute Protocol (ATT)
GATT是基于ATT Protocol的。ATT针对BLE设备做了专门的优化,具体就是在传输过程中使用尽量少的数据。每个属性都有一个唯一的UUID,属性将以characteristics and services的形式传输。
Characteristic
Characteristic可以理解为一个数据类型,它包括一个value和0至多个对次value的描述(Descriptor)。
Descriptor 对Characteristic的描述,例如范围、计量单位等。
Service
Characteristic的集合。例如一个service叫做“Heart Rate Monitor”,它可能包含多个Characteristics,其中可能包含一个叫做“heart rate measurement”的Characteristic。
二、开始写代码
1、开启权限
和传统蓝牙一样,BLE我们也需要开启权限
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
在6.0以上的系统,我们要加入获取位置权限,不然,搜索不到ble设备的
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
除了蓝牙权限外,如果需要BLE feature则还需要声明uses-feature:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
按时required为true时,则应用只能在支持BLE的Android设备上安装运行;required为false时,Android设备均可正常安装运行,需要在代码运行时判断设备是否支持BLE feature:
// 检查手机是否支持BLE,不支持则退出
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "您的设备不支持蓝牙BLE,将关闭", Toast.LENGTH_SHORT).show();
finish();
}
2、获得BluetoothAdapter
初始化 Bluetooth adapter, 通过蓝牙管理器得到一个参考蓝牙适配器
final BluetoothManager bluetoothManager =(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
3、判断是否支持蓝牙并提示用户打开蓝牙
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
4、提示用户允许获得位置权限
在我们搜索之前,还应当有这么一步,去动态申请位置权限得到位置信息,这样才能搜索到设备。
@RequiresApi(api = Build.VERSION_CODES.M)
private void initPermission() {
if (ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
//判断是否需要向用户解释为何要此权限
if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.READ_CONTACTS)) {
showMessageOKCancel("你必须允许这个权限,否则无法搜索到BLE设备", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
}
});
return;
}
//请求权限
requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS},
MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
}
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", okListener)
.create()
.show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//用户允许改权限,0表示允许,-1表示拒绝 PERMISSION_GRANTED = 0, PERMISSION_DENIED = -1
//这里进行授权被允许的处理
//可以弹个Toast,感谢用户爸爸允许了。
Toast.makeText(MainActivity.this, "谢谢爸爸", Toast.LENGTH_SHORT).show();
} else {
//这里进行权限被拒绝的处理,就跳转到本应用的程序管理器
Toast.makeText(MainActivity.this, "请开启位置权限", Toast.LENGTH_SHORT).show();
Intent i = new Intent("android.settings.APPLICATION_DETAILS_SETTINGS");
String pkg = "com.android.settings";
String cls = "com.android.settings.applications.InstalledAppDetails";
i.setComponent(new ComponentName(pkg, cls));
i.setData(Uri.parse("package:" + getPackageName()));
startActivity(i);
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
我们最终的目标就是让用户同意获取位置权限,当然,还有很多方法可以实现,甚至更简单。
5、搜索设备
好了,经历了漫长的过程,我们来到了搜索设备的时候了。
调用BluetoothAdapter的startLeScan()方法来实现开始搜索。此方法时需要传入 BluetoothAdapter.LeScanCallback参数。搜素到的蓝牙设备都会通过这个回调返回。
注意设定一个搜索时间,超过这个时间后则停止搜索。
private BluetoothAdapter mBluetoothAdapter;
private boolean mScanning;//是否正在搜索
private Handler mHandler;
//15秒搜索时间
private static final long SCAN_PERIOD = 15000;
private void scanLeDevice(final boolean enable) {
if (enable) {//true
//10秒后停止搜索
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}, SCAN_PERIOD);
mScanning = true;
mBluetoothAdapter.startLeScan(mLeScanCallback); //开始搜索
} else {//false
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);//停止搜索
}
}
相应的BluetoothAdapter.LeScanCallback如下
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() {
//在这里可以把搜索到的设备保存起来
//device.getName();获取蓝牙设备名字
//device.getAddress();获取蓝牙设备mac地址
//这里的rssi即信号强度,即手机与设备之间的信号强度。
}
});
}
};
6、注册广播
在连接前,首先要注册广播。因为在连接后,我们通过广播传递连接状态。
public final static String ACTION_GATT_CONNECTED = "com.charon.www.BleCar.ACTION_GATT_CONNECTED";
public final static String ACTION_GATT_DISCONNECTED = "com.charon.www.BleCar.ACTION_GATT_DISCONNECTED";
public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.charon.www.BleCar.ACTION_GATT_SERVICES_DISCOVERED";
public final static String ACTION_DATA_AVAILABLE = "com.charon.www.BleCar.ACTION_DATA_AVAILABLE";
public final static String READ_RSSI = "com.charon.www.BleCar.READ_RSSI";
private static IntentFilter makeGattUpdateIntentFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);
intentFilter.addAction(BluetoothLeService.READ_RSSI);
return intentFilter;
}
在onResume()方法中注册广播,mGattUpdateReceiver为用来处理接收到的广播,就是连接的状态。
registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
7、了解BluetoothGatt
在真正的连接前,我们有必要来了解一下BluetoothGatt这个类。
BluetoothGatt这个类是用得最多,也是最重要的一个类了。
该类最主要的有以下几个方法
connect() :连接远程设备。
discoverServices() : 搜索连接设备所支持的service。
disconnect():断开与远程设备的GATT连接。
close():关闭GATT Client端。
readCharacteristic(characteristic) :读取指定的characteristic。
setCharacteristicNotification(characteristic, enabled) :设置当指定characteristic值变化时,发出通知。
getServices() :获取远程设备所支持的services。
8、连接
连接的时候,我们可以通过搜索到的mac地址来进行连接。
public boolean connect(final String address) {//4
Log.d(TAG, "连接" + mBluetoothDeviceAddress);
if (mBluetoothAdapter == null || address == null) {
Log.d(TAG,"BluetoothAdapter不能初始化 or 未知 address.");
return false;
}
// 以前连接过的设备,重新连接
if (mBluetoothDeviceAddress != null
&& address.equals(mBluetoothDeviceAddress)
&& mBluetoothGatt != null) {
Log.d(TAG,"尝试使用现在的 mBluetoothGatt连接.");
if (mBluetoothGatt.connect()) {
mConnectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
final BluetoothDevice device = mBluetoothAdapter
.getRemoteDevice(address);
if (device == null) {
Log.d(TAG, "设备没找到,不能连接");
return false;
}
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);//真正的连接
//这个方法需要三个参数:一个Context对象,自动连接(boolean值,表示只要BLE设备可用是否自动连接到它),和BluetoothGattCallback调用。
Log.d(TAG, "尝试新的连接.");
mBluetoothDeviceAddress = address;
mConnectionState = STATE_CONNECTING;
return true;
}
BluetoothGattCallback用于传递一些连接状态及结果,在这处理各种连接状态
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
String intentAction;
Log.d(TAG, "status" + status);
if (newState == BluetoothProfile.STATE_CONNECTED) {//当连接状态发生改变
intentAction = ACTION_GATT_CONNECTED;
mConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);//发送广播
Log.d(TAG, "连接GATT server");
// 连接成功后尝试发现服务
//通过mBluetoothGatt.discoverServices(),我们就可以获取到ble设备的所有Services。
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {//当设备无法连接
intentAction = ACTION_GATT_DISCONNECTED;
mConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "断开连接");
broadcastUpdate(intentAction); //发送广播
}
}
@Override
// 发现新服务,即调用了mBluetoothGatt.discoverServices()后,返回的数据
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
//得到所有Service
List<BluetoothGattService> supportedGattServices = gatt.getServices();
for (BluetoothGattService gattService : supportedGattServices) {
//得到每个Service的Characteristics
List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
int charaProp = gattCharacteristic.getProperties();
//所有Characteristics按属性分类
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
Log.d(TAG, "gattCharacteristic的UUID为:" + gattCharacteristic.getUuid());
Log.d(TAG, "gattCharacteristic的属性为: 可读");
readUuid.add(gattCharacteristic.getUuid());
}
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
Log.d(TAG, "gattCharacteristic的UUID为:" + gattCharacteristic.getUuid());
Log.d(TAG, "gattCharacteristic的属性为: 可写");
writeUuid.add(gattCharacteristic.getUuid());
}
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
Log.d(TAG, "gattCharacteristic的UUID为:" + gattCharacteristic.getUuid() + gattCharacteristic);
Log.d(TAG, "gattCharacteristic的属性为: 具备通知属性");
notifyUuid.add(gattCharacteristic.getUuid());
}
}
}
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
// 读写特性
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt,
BluetoothGattDescriptor descriptor, int status) {
}
//如果对一个特性启用通知,当远程蓝牙设备特性发送变化,回调函数onCharacteristicChanged( ))被触发。
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//mBluetoothGatt.readRemoteRssi()调用得到,rssi即信号强度,做防丢器时可以不断使用此方法得到最新的信号强度,从而得到距离。
broadcastUpdate(READ_RSSI);
}
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
System.out.println("--------write success----- status:" + status);
};
};
9、状态处理
我们可以在这里,进行各种更新界面,数据操作 。
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();//接收广播
Log.d(TAG, "action:" + action);
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
//做连接后的变化
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED
.equals(action)) {
//未连接
} else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED
.equals(action)) {
Toast.makeText(ControlActivity.this, "发现新services", Toast.LENGTH_SHORT).show();
} else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
} else if (BluetoothLeService.READ_RSSI.equals(action)) {
}
}
};
10、操控ble设备
操控前,我们先看一下我们获得了哪些东西?
通过遍历,我们可以得到Service和Characteristic,每个Characteristic都有唯一的uuid和相应属性,如可读、可写、可提醒。
手机来操控BLE设备,就是通过可写的Characteristic,去改变Characteristic下的值,发送Characteristic给设备,设备接收到信号后,就可以进行相应的操作。
BluetoothGatt
我们可以把它看成Android手机与BLE终端设备建立通信的一个管道,只有有了这个管道,我们才有了通信的前提。
BluetoothGattService
蓝牙设备的服务,在这里我们把BluetoothGattService比喻成班级。而Bluetoothdevice我们把它比喻成学校,一个学校里面可以有很多班级,也就是说我们每台BLE终端设备拥有多个服务,班级(各个服务)之间通过UUID(唯一标识符)区别。
BluetoothGattCharacteristic
蓝牙设备所拥有的特征,它是手机与BLE终端设备交换数据的关键,我们做的所有事情,目的就是为了得到它。在这里我们把它比喻成学生,一个班级里面有很多个学生,也就是说我们每个服务下拥有多个特征,学生(各个特征)之间通过UUID(唯一标识符)区别。
现在,我们已经得到了一个可写的Characteristic,我们知道了其uuid。
在相应控制界面,我们可以通过这个uuid,去得到Characteristic,调用 mBluetoothGatt.writeCharacteristic(characteristic)来发送数据。
private ArrayList<ArrayList<BluetoothGattCharacteristic>> mGattCharacteristics = new ArrayList<>();//需要先把可写的Characteristic添加进去
for (int i = 0; i < mGattCharacteristics.size(); i++) {
for (int j = 0; j < mGattCharacteristics.get(i).size(); j++) {
if (mGattCharacteristics.get(i).get(j).getUuid().toString().equals("0000fff6-0000-1000-8000-00805f9b34fb")) {//对应的uuid
characteristic = mGattCharacteristics.get(i).get(j);
write(characteristic,changeDate("123"));//写入的数据
mBluetoothLeService.writeCharacteristic(characteristic);
Log.d(TAG, "发送数据成功");
}
}
}
write()方法,其实就是设置characteristic的值
private void write(BluetoothGattCharacteristic characteristic, byte byteArray[]) {
characteristic.setValue(byteArray);
}
private void write(BluetoothGattCharacteristic characteristic, String string) {
characteristic.setValue(string);
}
三、总结
ok,经过漫长的学习,我们终于可以连接BLE设备并且可以给它发信号了,让我们再来总结一下。
首先,我们要判断手机是否支持BLE,并且获得各种权限,才能让我们之后的程序能正常运行。
然后,我们去搜索BLE设备,得到它的MAC地址。
其次,我们通过这个MAC地址去连接,连接成功后,去遍历得到Characteristic的uuid。
在我们需要发送数据的时候,通过这个uuid找到Characteristic,去设置其值,最后通过writeCharacteristic(characteristic)方法发送数据。
如果我们想知道手机与BLE设备的距离,则可以通过readRemoteRssi()去得到rssi值,通过这个信号强度,就可以换算得到距离。
只要我们连接上,我们就可以用BluetoothGatt的各种方法进行数据的读取等操作。
四、GitHub
这是我写的最近一个用到BLE的项目,传输数据已经写好了。
https://github.com/Charon1997/BleCar
当然了,GitHub上面也有很多很好的库了,大家可以看一看
https://github.com/Jasonchenlijian/FastBle
https://github.com/dingjikerbo/BluetoothKit
五、结语
PS.由于这是我第一次写博客,可能写得并不是很好,也有部分可能没理解透彻,希望大家一起来讨论。
还有连接多个设备的情况还没写,以后再说吧。
原文地址:AndroidBle开发