目录
二、E1BleTemperatureNodeActivity实现的功能
5. BluetoothGattCallback回调对象及其重写的函数
一、BLE开发流程
BLE的开发流程基本上如下图所示,先判断是否支持,在判断是否打开,而后根据address建立Gatt连接实例,重写回调对象的方法负责监听蓝牙的状态变化以实现不同的功能。
使用到的API:
BluetoothManager
官方文档:BluetoothManager | Android Developers
蓝牙管理器,主要用于获取蓝牙适配器和管理所有和蓝牙相关的东西。
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter
官方文档:BluetoothAdapter | Android Developers
本地蓝牙适配器,用于一些蓝牙的基本操作,主要完成三方面的功能:a.开关蓝牙设备 b.扫描蓝牙设备 c.获取蓝牙设备的状态信息(name或address)。// BluetoothManager.getAdapter()方法获取此设备的默认BLUETOOTH适配器 this.mBluetoothAdapter = bluetoothManager.getAdapter();
BluetoothDevice
官方文档:BluetoothDevice | Android Developers
蓝牙设备对象,代表一个具体的蓝牙外设,包含一些蓝牙设备的属性,比如设备名称、mac地址等。由BluetoothAdapter对象传入蓝牙外设的address才能实例化对应address的BluetoothDevice对象。// 一个BluetoothGatt对应一个BluetoothGattCallback对象,而BluetoothDevice对象的获取只能靠BluetoothAdapter.getRemoteDevice(address)来获取 // 这的remoteDevice就代表一个蓝牙外设 BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(address);
BluetoothGatt
官网文档:BluetoothGatt | Android Developers
蓝牙通用属性协议,定义了BLE通讯的基本规则,是BluetoothProfile的实现类,Gatt是Generic Attribute Profile的缩写,用于连接设备、搜索设备可提供的Service等操作,连接是要传入BluetoothGattCallback对象,并重写一些方法。// 获取到BluetoothDevice之后就通过BluetoothDevice的connectGatt方法获取类似于socket的BluetoothGatt对象 mRemoteBluetoothGatt = remoteDevice.connectGatt(this, false, bluetoothGattCallback);
BluetoothGattCallback
官方文档:BluetoothGattCallback | Android Developers (google.cn)
重写一些方法以完成我们需要的一些自定义功能。蓝牙设备连接成功后,用于回调一些操作的结果,必须连接成功后才会回调。他是所有蓝牙数据回调的处理者,也是整个蓝牙操作当中最为核心的一部分。它里面有很多方法,但并非所有都需要在开发当中用到,这里列出来只是作为部分解析,需要哪个方法,就重写哪个方法,不需要的,直接去掉。
方法的功能和调用顺序可见:BluetoothGattCallback | Android Developers (google.cn)
private BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { //侦测蓝牙连接状态的函数,在这些回调函数中应该是第一个被调用的 } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { //当发现设备服务时,会回调到此处,下面是遍历所有发现的服务 } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //读取特征后回调到此处。 } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //写入特征后回调到此处,status == BluetoothGatt.GATT_SUCCESS代表写入成功 } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { //当特征(值)发生变化时回调到此处。 } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { //读取描述符后回调到此处。 } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { //写入描述符后回调到此处 } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { //暂时没有用过。 } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { //Rssi表示设备与中心的信号强度,发生变化时回调到此处。 } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { //暂时没有用过。 } };
BluetoothProfile
官方文档:BluetoothProfile - Android中文版 - API参考文档 (apiref.com)
一个通用的蓝牙规范,设备之间按照这个规范来收发数据,在我们的比赛中,会用到他里面的一些状态常量判断蓝牙的连接状态。//BluetoothProfile的STATE_CONNECTED常量表示该配置文件处于连接状态,也就是蓝牙连接成功. //参考https://www.apiref.com/android-zh/android/bluetooth/BluetoothProfile.html /** * 连接状态: * * The profile is in disconnected state *public static final int STATE_DISCONNECTED = 0; * * The profile is in connecting state *public static final int STATE_CONNECTING = 1; * * The profile is in connected state *public static final int STATE_CONNECTED = 2; * * The profile is in disconnecting state *public static final int STATE_DISCONNECTING = 3; * */
BluetoothGattService
蓝牙设备提供的服务,是蓝牙设备特征Characteristic的集合。BluetoothGattCharacteristic
感觉名字有点不太适合,他是承载GATT数据传输服务的基本数据单元,通信时候的数据就打包在里面。BluetoothGattDescriptor
官方文档:BluetoothGattDescriptor - Android中文版 - API参考文档 (apiref.com)
蓝牙设备特征描述符,是对特征Characteristic的额外描述。
二、E1BleTemperatureNodeActivity实现的功能
①、描绘、更新显示温湿度的页面B。
②、负责蓝牙BLE的连接、数据解析与命令发送。
三、函数结构。
一共包含:11个方法,还包括一个BluetoothGattCallback回调对象,其中重写了四个方法。除此之外还有一个Thread对象,也重写了一个线程方法,总的来说是11+4+1=16个方法。
函数的大致作用以及调用关系如下图所示。
四、 函数具体介绍
1.getExtractData()
用于联系BleDeviceActivity页面和E1BleTemperatureNodeActivity页面。完成两个页面之间的数据(蓝牙name和address)传输。
private void getExtraData() {
// getIntent()方法:获得启动当前Activity时的Intent内容。
// 其实不难理解:在这个蓝牙温湿度的程序中,其实有两个页面:
// 一个是搜索、展示蓝牙的页面(BLeDeviceListActivity)。在这个页面会以一个自定义的ForBleAdapter展示ListView蓝牙设备信息列表。
// 用户在诸多蓝牙设备中选择自己相连接的设备,然后通过下面的函数(见BleDeviceListActivity的85-93行)将选定的蓝牙信息打包成intent发送至第二个页面
// 第二个页面也就是选定蓝牙后与其建立通信的页面(E1BleTemperatureNodeActivity.java),下面的函数就是监听用户的点击,然后携带对应的蓝牙设备name和address跳转到第二个页面
// this.mDeviceListView.setOnItemClickListener(new OnItemClickListener() { //监听事件,监听用户点击的蓝牙ListView的哪个Item,然后将对应的蓝牙设备信息打包发送至下一个即将启动的页面
// @Override
// public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Intent intent = new Intent(BleDeviceListActivity.this, E1BleTemperatureNodeActivity.class);
// intent.putExtra("name", mBleDevices.get(position).getName());
// intent.putExtra("address", mBleDevices.get(position).getAddress());
// startActivity(intent); //由new Intent(BleDeviceListActivity.this, E1BleTemperatureNodeActivity.class)可知会启动E1BleTemperatureNodeActivity页面
// }
// }
// 第二个页面启动后会通过getIntent方法获取上一个页面(BleDeviceListActivity)传入的Intent对象
// 而这个给Intent对象中包含着需要连接的蓝牙的name与Address
this.mRemoteDevName = getIntent().getStringExtra("name");
this.mRemoteDevAddress = getIntent().getStringExtra("address");
}
2.initViews()
①标题栏TitleBar控件初始化。
②监听事件的设置。
③温湿度节点展示框的绑定。
/**
* 控件初始化
*/
private void initViews() {
//①标题栏TitleBar控件初始化。
initTitleBar();
//③温湿度节点展示框的绑定。
this.mRealTemperatureTv = (TextView) findViewById(R.id.realTemperature);
this.mRealHumidityTv = (TextView) findViewById(R.id.realHumidity);
}
/**
* 初始化标题栏.TitleBar从左到右依次是 ImageView 、 TextView 、 ToggleButton
*/
private void initTitleBar() {
mImageView = (ImageView) findViewById(R.id.backImageView);
mImageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mConnectionState) {
showDisconnectBleDialog();
} else {
// 用于结束一个Activity的声明周期,举一个例子
// Activity1-》Activity2-》Activity3
// 当第二步跳转时加finish()时候,在Activity3点击返回直接就到Activity1,不经过activity2,因为Activity2已经被销毁
// 若第二部跳转不加finish()时候,在Activity3点击返回就返回到Activity2
finish();
// 关于android中finish与OnDestory()的区别:
// finish会调用onDestory方法。调用Activity.finish()的时候,系统只是将最上面的Activity出栈。因此你点击back键,再也找不到这个Activity
// 而调用onDestory的时候,会将当前的占用的资源全部删除。当再次重新进入这个页面的时候,必须重新创建,执行onCreate()方法
}
}
});
// TextView类型的mTitleTextView对象用于展示蓝牙设备的deviceName
this.mTitleTextView = (TextView) findViewById(R.id.deviceName);
this.mTitleTextView.setText(this.mRemoteDevName);
// ToggleButton类型的mConnectRemoteDeviceBtn为连接/断开的按钮。用来连接蓝牙设备
this.mConnectRemoteDeviceBtn = (ToggleButton) findViewById(R.id.connectBleBtn);
this.mConnectRemoteDeviceBtn.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// TODO Auto-generated method stub
if (isChecked) { //要是点击的话isChecked就是true
connectBleDevice(true, mRemoteDevAddress);
} else {
connectBleDevice(false, "");
}
}
});
}
/*
第二步initTitleBar()调用的子函数:显示断开连接的那个弹出框。
* */
private void showDisconnectBleDialog() {
AlertDialog.Builder dialogBuild = new AlertDialog.Builder(this);
dialogBuild.setMessage("蓝牙已连接,确定断开?");
// 1.设置正向按钮
dialogBuild.setPositiveButton("断开连接", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
connectBleDevice(false, "");
finish();
}
});
// 2.设置负向按钮
dialogBuild.setNegativeButton("再想想", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
// 3.create()一个对象并且.show()调用
AlertDialog alertDialog = dialogBuild.create();
alertDialog.show();
}
3. initBLuetooth()
完成两个功能:
①、判断设备是否支持蓝牙功能
②、向用户申请开启蓝牙权限。
①判断是否支持蓝牙
/**
* 初始化蓝牙适配器,获取设备是否支持蓝牙,
* 通过Boolean mSupportedBLE的参数值来判断
*/
private void initBluetoothAdapter() {
// 创建第一个对象BluetoothManager,这个对象由调用系统服务getsystemService(Context.BLUETOOTH_SERVICE)来产生
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
// 拿到之后就是BluetoothAdapter,这个对象通过BluetoothManager.getAdapter()方法获得
this.mBluetoothAdapter = bluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Toast.makeText(this, "设备不支持蓝牙功能", Toast.LENGTH_LONG).show();
} else {
mSupportedBLE = true;
}
}
②判断系统蓝牙是否启动
/**
* 判断系统蓝牙是否启用
*/
private void enableBluetooth() {
if (mSupportedBLE) {
// 如果支持蓝牙,则跳转到打开蓝牙的提示框
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(intent);
}
}
4.connectBleDevice()
那我们如何建立GATT连接以及如何读取Characteristic下的数据呢?
* 1.通过BluetoothAdapter获取制定蓝牙设备的BluetoothDevice实例。(在本程序中指的是mRemoteDevice)
* 2.建立GATT连接。使用步骤1中获取的设备实例,调用其connectGatt函数建立连接,再这一步需要传入重写的BluetoothGattCallback实例
* 3.重写GattCallback回调。第二步建立GATT连接时,需要传入BluetoothGattCallback实例,因此我们得先实例一个BluetoothGattCallback类,并且重写其回调函数
* 4.若GATT连接成功,onConnectionStateChange()函数回调。GATT成功或失败,会回调GattCallback下的onConnectionStateChange()函数,接下来会调用mBluetoothGatt.discoverServices()函数。功能是查询已连接的GATT下的service。
* 5.onServicesDiscovered()方法回调,获取制定的Service和Characteristic,上一步调用BluetoothGatt.discoverServices()后,系统会回调GattCallback下的onServicesDiscovered()函数,这表明我们已经可以通过指定的UUID来获取指定的Service实例了
* 在onServicesDiscovered()函数回调后,通过UUID先获取service,然后再使用获取到的service和UUID获取Characteristic,最后mBluetoothGatt.readCharacteristic(mVIDPIDCharacteristic);读取这个Characteristic
* 6.onCharacteristicRead()函数回调,读取Characteristic的value值,上一步调用readCharacteristic()后,系统会回调gattCallback下的onCharacteristicRead(),此时我们使用回参characteristic直接getValue()即可读取到数值
/**
* 实际建立与BLE设备通信联系的客户端的就是这个函数。
* 其实也不仅仅是这个函数,他传入的bluetoothGattCallback回调对象会重写并使用四个功能函数,
*
* @param address 设备地址:String类型
* 传入true:进行连接
* 传入false:进行断开
*/
private void connectBleDevice(boolean connect, String address) {
if (mBluetoothAdapter == null) {
return;
}
if (connect) { //如果传入true,则为要求连接
// 一个BluetoothGatt对应一个BluetoothGattCallback对象,而BluetoothDevice对象的获取只能靠BluetoothAdapter.getRemoteDevice(address)来获取
BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(address);
// 获取到BluetoothDevice之后就通过BluetoothDevice的connectGatt方法获取类似于socket的BluetoothGatt对象
mRemoteBluetoothGatt = remoteDevice.connectGatt(this, false, bluetoothGattCallback);
} else { //如果传入false,则为要求断开
if (mRemoteBluetoothGatt != null) {
// disconnect()断开已建立的连接,或取消当前正在进行的连接尝试,
mRemoteBluetoothGatt.disconnect();
}
}
}
5. BluetoothGattCallback回调对象及其重写的函数
数据交互全靠该回调对象重写的方法。但是重写什么方法,各个方法都是干什么用的。参考官方文档:BluetoothGattCallback | Android Developers (google.cn)
以及:(23条消息) Android 低功耗蓝牙开发(数据交互)_初学者-Study的博客-CSDN博客_android低功耗蓝牙开发
/*
下面的BluetoothGattCallback对象是供mRemoteBluetoothGatt = remoteDevice.connectGatt(this, false, bluetoothGattCallback)回调用的
里面主要就是一个onConnectionStateChange函数,根据连接成功与否有相应的操作
若成功: 调用BluetoothGatt的discoverServices方法。查询已经连接的Gatt支持的Service。
而后会调用onServicesDiscovered()方法,在那个方法里面表面我们已经可以通过指定的UUID来获取指定的Service实例了,然后获取Characteristic和Descriptor。
* */
private BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
// 不止这一个函数,下面四个函数都是包含在bluetoothGattCallback对象里面的,BluetoothGattCallback对象需要重写很多方法,
// 本来的都没加@Override,其实加上更好,更容易让人看出来这是个重写的方法,并且编辑器也可以为我们检查一下是否重写正确,但是不加@Override也可以正常识别,因为JM会自动识别重写的函数,但是因为有的没重写对会导致识别不到重写的函数,因此还是加上@Override比较好
// 而且不需要super调用父类函数(只有需要调用父类函数的时候再写super)
/*
在这四个函数中,这个函数是首先被调用的函数,类似于启动的初始化函数。
很明显这个是对蓝牙连接状态的回调查看蓝牙是否连接成功。
* */
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
// 这个newState参考的是BluetoothProfile的Constant
switch (newState) {
//BluetoothProfile的STATE_CONNECTED常量表示该配置文件处于连接状态,也就是蓝牙连接成功.
//参考https://www.apiref.com/android-zh/android/bluetooth/BluetoothProfile.html
/**
* 连接状态:
* * The profile is in disconnected state *public static final int STATE_DISCONNECTED = 0;
* * The profile is in connecting state *public static final int STATE_CONNECTING = 1;
* * The profile is in connected state *public static final int STATE_CONNECTED = 2;
* * The profile is in disconnecting state *public static final int STATE_DISCONNECTING = 3;
*
*/
case BluetoothProfile.STATE_CONNECTED:
Log.e(TAG, "设备连接成功,开始发现设备服务。");
// 下面的函数很有必要,他的目的是让我们第二个重写的方法->OnServicesDiscovered显示所有的Services
gatt.discoverServices();
mConnectionState = true;
break;
case BluetoothProfile.STATE_DISCONNECTED: {//STATE_DISCONNECTED常量表示配置文件处于断开状态
Log.e(TAG, "设备连接已断开");
mConnectionState = false;
}
default:
break;
}
}
/*
* 通过Gatt.discoverServices()设置触发。
* 功能是:通过UUID获取制定的服务service,只获取大赛的实验箱的节点蓝牙服务,并且过滤掉无用的其他杂七杂八的蓝牙设备信号。
* 这是发现服务的,不是发现蓝牙的,这时候蓝牙已经连接成功了。
* 在这个回调中做的就是打开通知开关
* */
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
// Step1:获取远程节点提供的服务,你可以将他理解为虚拟化的远程节点。
BluetoothGattService remoteService = gatt.getService(UUID.fromString(Global.UUID_DS_SERVICE));
if (remoteService != null) {
Log.e(TAG, "发现大赛指定蓝牙GATT服务");
// Step2:过滤只得到试验箱节点的Characteristic。
mRemoteCharacteristic = remoteService.getCharacteristic(UUID.fromString(Global.UUID_DS_CHARACTERISTIC));
if (mRemoteCharacteristic != null) {
Log.e(TAG, "发现大赛指定蓝牙GATT特征");
//设置通知。
BluetoothGattDescriptor descriptor = mRemoteCharacteristic.getDescriptor(UUID.fromString(Global.UUID_DS_DESCRIPTOR));
if (descriptor != null) {
//配置本地通知,为什么要配置本地通知?那什么又是本地通知呢?
//我们这个实验要求实时更新BLE的特征变化,那我们就得在特征变化时收到通知才行,我们用Gatt的setCharacteristicNotification()为我们选中的特征启动通知
//一旦该特征Characteristic在节点上发生变化(也就是温湿度发生变化),就会触发onCharacteristicisChanged()函数回调
//而就我们这个实验来讲,我们把解析温湿度数据以及实时更新UI的函数写在onCharacteristicisChanged函数中了,
// 通过这样就实现了我们这个实验的目的:实时检测温湿度变化并更新UI显示
gatt.setCharacteristicNotification(mRemoteCharacteristic, true);
// 配置远程。
// descriptor.setValue()用来将本地的descriptor存储的值更新
// ENABLE_NOTIFICATION_VALUE:用于启动客户端配置描述符
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
//将给定的描述符写入关联的远程设备
//为onDescriptorWrite()设置触发回调,需要传入BluetoothGattDescriptor对象
gatt.writeDescriptor(descriptor);
} else {
Log.e(TAG, "没有发现大赛指定GATT描述");
}
} else {
Log.e(TAG, "没有发现大赛指定蓝牙GATT特征");
}
} else {
Log.e(TAG, "没有发现大赛指定的蓝牙GATT服务");
}
}
// 描述符写入回调,由gatt.writeDescriptor(descriptor)设置回调
// 这部分函数作用是:启动一个线程,线程循环向温湿度节点发送读取命令,间隔两秒。
// 注意一个误区:我们发送的读取命令是打包在Characteristic中的发送的,而非放在Descriptor里面的(mRemoteCharacteristic.setValue(Read_CMD_Bytes);)
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
Log.e(TAG, "Gatt通知已设置:" + descriptor.getUuid().toString());
// 启动数据发送线程
if (!isReadRunning) {
isReadRunning = true;
// mReadTheThread线程重写的run()方法中包含发送数据的操作
mReadTheThread.start();
}
}
/*
下面这个函数是用来解析节点返回的数据,当特征发生变化时候自动触发调用
见https://www.apiref.com/android-zh/android/bluetooth/BluetoothGattCharacteristic.html官方API
当Cahracteristic发生变化的时候调用。
* */
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
Log.e(TAG, "onCharacteristicChanged : " + ByteUtils.bytes2HexString(characteristic.getValue()));
//01 03 04
//01 FF 湿度计算:(01 * 256) + 0xFF = 511, 则湿度的实际值为51.1RH%
//00 FF 温度计算:(00 * 256) + 0xFF = 255,则温度的实际值为25.5℃
//8b bf
if (characteristic.getValue().length == 9) {
final float mRealHumidityFloat = (float) ((characteristic.getValue()[3] & 0xff) * 256 + (characteristic.getValue()[4] & 0xff)) / 10;
final float mRealTempturatureFloat = (float) ((characteristic.getValue()[5] & 0xff) * 256 + (characteristic.getValue()[6] & 0xff)) / 10;
// runOnUiThread是android自带的方法:用于更新应用UI,不能什么都放到主线程里面,主线程很忙的,既要处理绘制UI,响应用户的交互等
// 将UI的更新任务放到工作线程之外的线程,有助于获得更流畅的用户体验,避免无响应(也就是卡死情况)的发生。
// 还涉及到handle和looper的知识,这里不赘述了。
runOnUiThread(new Runnable() {
@Override
public void run() {
mRealHumidityTv.setText(String.valueOf(mRealHumidityFloat));
mRealTemperatureTv.setText(String.valueOf(mRealTempturatureFloat));
}
});
}
}
};
/**
* 写命令到远程BLE设备
* 不属于BluetoothCallback中要重写的函数,但是Callback却用到了下面这个函数来发送命令。
*
* @param bytes 数据
*/
private void writeGattCharacteristic(byte[] bytes) {
if (mRemoteCharacteristic != null) {
Log.e(TAG, "发送数据 " + ByteUtils.bytes2HexString(bytes));
mRemoteCharacteristic.setValue(bytes);
mRemoteBluetoothGatt.writeCharacteristic(mRemoteCharacteristic);
}
}
//这个线程用来发送命令:每2s发送一次命令:
private Thread mReadTheThread = new Thread() {
public void run() {
while (mConnectionState) {
writeGattCharacteristic(READ_BLE_CMD);
try {
sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
学习参考:
官方文档:https://developer.android.google.cn/reference/android/
Android bluetooth创建GATT连接并读取设备信息 - 简书 (jianshu.com)
低功耗蓝牙Ble的详细使用流程 - 简书 (jianshu.com)
(23条消息) Android 低功耗蓝牙开发(数据交互)_初学者-Study的博客-CSDN博客_android低功耗蓝牙开发