本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
前言:
最近在工作中使用到蓝牙的功能,当然我们这个蓝牙时跟蓝牙芯片结合使用的,而不是手机跟手机连接通信的。其实本质时差不多的,只是设备不一样罢了。在这里我不会贴出蓝牙那些协议等等复杂的名词解释,因为这个不是一两句话能解释的清楚,在我们先不太了解蓝牙的这些专业名词之前,我们先掌握它的基本使用就可以了,后续如果想深入了解的话,我们再花时间去学习。本文介绍的低功耗的蓝牙,是Android 4.3才开始支持的,而要使用传统蓝牙和高版本的蓝牙请参照官方文档,有中文介绍哦。
官方蓝牙文档:https://developer.android.google.cn/guide/topics/connectivity/bluetooth.html
官方不同版本蓝牙示例(有3个):
https://github.com/googlesamples?utf8=%E2%9C%93&q=bluetooth&type=&language=
一、蓝牙的基本使用流程(草图)
从图上可以看出我这个例子只是单方面的通信,即手机只接收数据而不发送数据。
二、蓝牙关键类
BluetoothManager:蓝牙管理服务,如果对Android基本框架熟悉的话,你会发现蓝牙也属于最底层的驱动模块里,那么要使用蓝牙的东西就需要使用(BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)来获取了。不过看是把图贴一下。
BluetoothAdapter:表示本地设备蓝牙适配器。 BluetoothAdapter允许您执行基本的蓝牙任务,例如启动设备发现,查询已绑定(配对)设备的列表,使用已知MAC地址实例化BluetoothDevice,并创建一个BluetoothServerSocket以监听来自其他设备的连接请求,并启动扫描蓝牙LE设备。这个类时关键类,后面会大量使用到它。
BluetoothAdapter.LeScanCallback:用于提供LE扫描结果的回调界面。这个类就是扫描的回调接口,不过再Android 5.0以上使用抽象类ScanCallback。
BluetoothLeScanner:该类提供了对蓝牙LE设备执行扫描相关操作的方法。应用程序可以使用ScanFilter扫描特定类型的蓝牙LE设备。它还可以请求不同类型的回调来传递结果。不过这个类是在Android 5.0(API21)以上才出现的,也就是对于Android 4.3的以上我们只会使用BluetoothAdapter来进行扫描等操作,当然如果是5.0以上的可以使用这个类来代替的。
ScanCallback:蓝牙LE扫描回调,使用这些回调报告扫描结果。这个是抽象类与BluetoothLeScanner配套使用。
BluetoothDevice:表示远程蓝牙设备,BluetoothDevice允许您创建与相应设备的连接或关于它的查询信息,例如名称,地址,类和绑定状态。
BluetoothProfile:配置文件代理。每个公共配置文件实现这个接口。它有几个直接子类,每个子类再不同场景中使用,如BluetoothA2dp, BluetoothGatt, BluetoothGattServer, BluetoothHeadset, BluetoothHealth,另外还有个隐藏子类BluetoothInputDevice(这个可以用在HID的情况下)。在当前例子中使用到的是BluetoothGatt。
BluetoothGatt:该类提供蓝牙GATT功能,以实现与蓝牙智能或智能就绪设备的通信。后续使用该类做连接、断开、关闭等操作。
BluetoothGattCallback:文档没有直接的解释,只说了被用在连接设备时候的回调。虽然文档没有详细说明,但是这个回调会在后续的连接、断开、通信中起到关键作用。
好了,主要的几个类介绍的差不多了。接下来我们使用这几个类练习一下。
1、获取蓝牙管理服务和适配器
bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
获取了适配器后,我们可以通过适配器是否为null来判断是否支持蓝牙功能(想必现在的手机应该都支持吧)。另外适配器还提供了以下的方法:
/**
* 启用蓝牙
*/
public boolean enableBluetooth()
{
if (isSupportBluetooth())
{
return bluetoothAdapter.enable();
} else
{
return false;
}
}
/**
* 是否启用了
*
* @return
*/
public boolean isEnabled()
{
if (isSupportBluetooth())
{
return bluetoothAdapter.isEnabled();
}
return false;
}
/**
* 禁用蓝牙
*/
public boolean disableBluetooth()
{
if (isSupportBluetooth())
{
return bluetoothAdapter.disable();
} else
{
return false;
}
}
/**
* 扫描设备
*/
public void scanLeDevice()
{
if (isSupportBluetooth() && isEnabled())
{
setScanning(true);
getBluetoothAdapter().startLeScan(leScanCallback);
getHandler().postDelayed(new Runnable()
{
@Override
public void run()
{
stopLeScan();
}
}, getScanPeriod());
}
}
/**
* 停止扫描
*/
public void stopLeScan()
{
if (isSupportBluetooth() && isEnabled())
{
setScanning(false);
getBluetoothAdapter().stopLeScan(leScanCallback);
}
}
上面的就是启用、关闭、扫描、停止的几个方法。需要注意的是在启用蓝牙的时候在不同手机会弹出启用对话框,比如魅族。那么这个时候你需要通过isEnabled方法来判断是否启用了,如果没有启用则使用startActivity来启用,然后通过Activity或者Fragment的onActivityResult回调方法来做余下的操作了。
startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE);
好了,使用上面的扫描方法就可以扫描到蓝牙设备了,是不是很简单啊。再说明一下,上面的leScanCallback就是上面提到的BluetoothAdapter.LeScanCallback,你需要实现它就可以了。
2、权限配置
不过使用蓝牙也是要配置权限的,如果你没有在Manifest中配置,在Android Studio中使用上面的方法的时候它会报错要求你加入权限。我在这里列一下我使用到的权限:
<!-- 需要硬件支持低功耗蓝牙 -->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true"/>
<!-- 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- Android 5.0以上蓝牙还需要位置权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
是的,你会发现使用蓝牙还需要位置权限?我开始也觉得一个蓝牙也要位置权限,后来在翻看官方文档的时候才知道的,不然你在5.0以上的手机上扫不出蓝牙的!详情请翻阅官方文档:https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le.html#user-permission。
3、连接设备
有了蓝牙设备后接下来我们连接下,然后发发数据看看。连接设备其实也很简单,就是通过扫描到的蓝牙设备对象来连接即可。蓝牙设备就是上面提到的BluetoothDevice,这个类有一个方法:
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) {
return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
}
第一个Context就不说了;第二个参数autoConnect的解释是“是否直接连接到远程设备(false)或一旦远程设备可用即可自动连接(true)”;第三个参数也是上面提到的抽象类,这个类在连接过程中起到关键作用。最后方法会返回一个BluetoothGatt对象,后续我们通过这个对象可以重连、断开、关闭设备。贴下我例子中的连接方法:
/**
* 连接设备,如果服务未开启或者地址为空的话就返回false;如果地址存在是否连接成功取决与蓝牙底层
*
* @param address
* @return 是否连接到
*/
@Override
public boolean connectDevice(String address)
{
if (arshowBluetooth.getBluetoothAdapter() == null || address == null)
{
return false;
}
//如果之前有连接过就直接连接,重新连接
if (address.equals(mAddress) && mBluetoothGatt != null)
{
return mBluetoothGatt.connect();
}
BluetoothDevice device = arshowBluetooth.getBluetoothAdapter().getRemoteDevice(address);
if (device == null)
{
return false;
}
//false表示直接连接,true表示远程设备可用之后连接
mBluetoothGatt = device.connectGatt(context, false, mGattCallback);
mAddress = address;
return true;
}
抽象类
/**
* 连接、发现、通信回调
*/
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback()
{
/**
* 连接状态改变回调
* @param gatt
* @param status
* @param newState
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
{
if (newState == BluetoothProfile.STATE_CONNECTED)
{
//阅读连接的远程设备的RSSI。
gatt.readRemoteRssi();
// 发现远程设备提供的服务及其特性和描述符
gatt.discoverServices();
}
bluetoothCallback.connectionStateChange(gatt.getDevice(), newState);
}
/**
* 当远程设备的远程服务列表,特征和描述符已被更新,即已发现新服务时,调用回调。表示可以与之通信了。
* @param gatt
* @param status
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status)
{
if (status == BluetoothGatt.GATT_SUCCESS)
{
// 得到服务对象
BluetoothGattService service = gatt.getService(BluetoothConstant.UUID_SERVICE);
if (service == null)
{
return;
}
// 得到此服务结点下Characteristic对象
final BluetoothGattCharacteristic gattCharacteristic = service
.getCharacteristic(BluetoothConstant.UUID_CHARACTERISTIC);
if (gattCharacteristic == null)
{
return;
}
gatt.setCharacteristicNotification(gattCharacteristic, true);
BluetoothGattDescriptor descriptor = gattCharacteristic
.getDescriptor(BluetoothConstant.UUID_DESCRIPTOR);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
bluetoothCallback.serviceDiscoveryed(gatt.getDevice(), status);
} else
{
LogUtils.w(getClass().getSimpleName(), "onServicesDiscovered received: " + status);
}
}
/**
* 由于远程特征通知而触发回调。
* @param gatt
* @param characteristic
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic)
{
try
{
bluetoothCallback.valueChanged(new String(characteristic.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
}
/**
* 返回远程设备的信号强度,最大值理论值为0
* @param gatt
* @param rssi
* @param status
*/
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status)
{
super.onReadRemoteRssi(gatt, rssi, status);
}
};
这个类里面有很多方法,不过都被这个类实现了,我们根据自己的情况来重写。解释下上面列出的方法:
onConnectionStateChange:监测蓝牙设备的连接、断开、读取设备信号强度值、发现服务(翻译的好别扭)。连接和断开可以通过newState参数来判断,注意是和BluetoothProfile的常量值来对比的,这个类也是上面提到过的。其实这个类提供了4个状态值,只不过在使用过程中只有连接(状态值2)和断开(状态值0),其他的没用到。
/** 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;
至于后面的读取设备信号强度值、发现服务这两个分别用在获取信号强度和通信的。怎么解释?要知道蓝牙也是无线传输的,既然是无线就跟移动网、WIFI一样都存在信号强弱的情况,那么通过这个就可以获取了。信号强度值是负数,负数越大信号越好(最大理论值是0),单位是dBm。而发现服务,我们在成功连接后调用gatt.discoverServices();方法既可以获取与连接的设备进行通信了,最后回调onServicesDiscovered方法。
onServicesDiscovered:这个方法里面我们需要做的就是使用蓝牙的UUID获取服务、特征、描述等对象,然后就可以给手机发送数据了。这里需要注意的是UUID不能随便写,而是设备厂商或者通用的UUID,否则你是无法使用的。说到这个UUID,其实我也比较迷惑的,很多文章在介绍UUID的时候都是一笔带过或者直接拿来用根本不解释这是干嘛的,而且也没找到一个比较全面介绍蓝牙协议等专业文档(谁找到了,麻烦给我留一个地址)。其实说到这,里面提到的特征、描述等对象,我也没详细去说请原谅。
补充下UUID的说明,UUID就是上面说到的厂商设置通用的,比如00002a29-0000-1000-8000-00805f9b34fb这个就代表生产厂商的UUID,使用它可以获取厂商信息,另外我也收录了一些常见的UUID,可以参考。常见UUID
onCharacteristicChanged:远程特性通知,回调触发。就是上面执行完后,只要连接的设备触发某些动作(比如我的蓝牙芯片按钮按下、抬起),这个方法就会被调用了。然后我们在这个方法中就可以获取远程设备发过来的信息。
好了,其实写到我这个例子基本的使用已经差不多了,剩下的无法就是蓝牙的断开、连接、关闭(释放资源)
/**
* 断开设备
*/
@Override
public void disconnectDevice()
{
if (mBluetoothGatt != null)
{
mBluetoothGatt.disconnect();
}
}
@Override
public void closeDevice()
{
if (mBluetoothGatt != null)
{
mBluetoothGatt.close();
}
}
最后,贴下我的例子截图以及我使用的蓝牙芯片,是不是感觉高大上啊,哈哈~
好了,我这个蓝牙芯片在连接之前的LED指示灯是一闪一闪的,而连接成功后就会常亮的。上图的3个按钮也是对应芯片的按键的。不过在这里说明一下,由于为了保密,我这里不会放出与蓝牙芯片的例子,而是单独写了一个简单的例子,不过底层的东西是不变的。另外这个是单方面的通信交互,如何做到双向通信呢?由于本例子只需要单向通信,所以就没涉及到,不过也是上面的例子的范畴,大家可以自己去研究下。另外说明下官方提供的低功耗的例子在使用“发现服务”的那个回调方法存在问题(连接了接收不到数据),我这里跟官方的例子还是不一样的。
另外说明下,我的Android Studio版本是2.4 preview 3版本,对应的gradle版本是gradle-3.4.1-all,所有低版本的自行修改配置。
GitHub地址:https://github.com/Xanthuim/BluetoothSample
最后说明下Android系统的低功耗是在4.3版本才支持的,低版本只能使用传统蓝牙啊。麻烦你们还是先看下谷歌官方文档最好。谷歌蓝牙主要分3个版本4.3以前是传统蓝牙,从4.3开始支持低功耗蓝牙(BLE),从5.0(记得是)又有新的功能了,但是没去研究。