一、低功耗蓝牙发展史
传统蓝牙虽然历史悠久,但是它的缺陷也很明显,包括但不限于下列几点:
(1)需要两部设备配对之后才能继续连接,而且连接速度也慢。
(2)连接之后就一直保持传输链路,很消耗电能。
(3)数据传输的有效距离不到10米,导致使用场景受限。
为解决传统蓝牙的上述痛点,蓝牙技术联盟制定了低功耗蓝牙技术,并于2012年纳入蓝牙4.0规范。低功耗蓝牙又称蓝牙低能耗(Bluetooth Low Energy,BLE),与之相对应的,蓝牙4.0之前的蓝牙技术被称作经典蓝牙,也称传统蓝牙。因为BLE采取非常快速的连接方式,所以平时处于“非连接”状态,此时链路两端仅是知晓对方,只有在必要时才开启链路,完成传输后会尽快关闭链路。BLE技术与之前版本的蓝牙标准相比,主要有三个方面的改进:更省电、连接速度更快、传输距离更远。更详尽的蓝牙技术发展历程如图【蓝牙技术发展历程】所示
![](https://img.haomeiwen.com/i2639112/09a2cf8e80a5a6bb.png)
低功耗蓝牙不同于传统蓝牙,它规定所有BLE设备遵循统一的通用属性规范(Generic Attribute Profile,GATT),基于该规范制定了BLE通信的基本守则。为了理清BLE设备之间的交互过程,有必要解释一下相关术语:
-
BLE从机,又称服务端,它接受GATT指令,并根据指令调整自身行为,例如蓝牙灯泡、蓝牙锁、蓝牙小车等。
-
BLE主机,又称客户端,它向服务端发送GATT指令,令其遵照指令行事,例如操控蓝牙小车的手机等。
-
特征值(characteristic),BLE通过参数来传输数据,服务端定好一个参数,然后客户端对该参数进行读、写、通知等操作,这种参数被称作特征值。
-
服务(service),一个特征值往往不够用,比如这个特征值专用于灯光亮度,那个特征值专用于灯光颜色,存在多个特征值的话可能还会对它们分类,分好的种类配一个UUID(Universally Unique Identifier,通用唯一识别码)就被称作服务。一个设备可拥有多个服务,每个服务也可包含多个特征值,每个特征值又存在多种属性(properties),例如长度(size)、权限(permission)、值(value)、描述符(descriptor)等。把上述术语关联起来,形成GATT规范的内容框架,如图【GATT规范的内容框架】所示:
GATT规范的内容框架.png
了解了BLE技术的基本概念,再来动手尝试App的BLE编程。由于BLE集成于蓝牙4.0,因此AndroidManifest.xml同样要声明蓝牙的相关权限,此外还要通过uses-feature声明仅在支持BLE的设备上运行,权限声明的配置如下所示:
<!-- 蓝牙 --> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" tools:ignore="ProtectedPermissions" /> <!-- 仅在支持BLE(即蓝牙4.0)的设备上运行 --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <!-- 如果Android6.0 蓝牙搜索不到设备,需要补充下面两个权限 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
现在绝大多数智能手机都支持BLE,这些手机既能充当BLE从机(服务端),又可以充当BLE主机(客户端)。把手机当作客户端使用的话,就得主动扫描周围有哪些服务端设备。当然,在扫描开始前要先初始化蓝牙适配器,初始化代码示例如下BleScanActivity完整代码
private BluetoothAdapter mBluetoothAdapter; // 声明一个蓝牙适配器对象
private BluetoothDevice mRemoteDevice; // 声明一个蓝牙设备对象
private BluetoothGatt mBluetoothGatt; // 声明一个蓝牙GATT客户端对象
// 初始化蓝牙适配器
private void initBluetooth() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "当前设备不支持低功耗蓝牙", Toast.LENGTH_SHORT).show();
finish(); // 关闭当前页面
}
// 获取蓝牙管理器,并从中得到蓝牙适配器
BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bm.getAdapter(); // 获取蓝牙适配器
}
接着调用蓝牙适配器的getBluetoothLeScanner方法,获得BluetoothLeScanner扫描器对象。这个扫描器主要有startScan和stopScan两个方法,其中startScan方法表示开始扫描BLE设备,调用它的示例代码如下:
// 创建一个开启BLE扫描的任务
@SuppressLint("MissingPermission")
private Runnable mScanStart = new Runnable() {
@Override
public void run() {
if (!isScaning && BluetoothUtil.getBlueToothStatus()) {
isScaning = true;
// 获取BLE设备扫描器
BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
scanner.startScan(mScanCallback); // 开始扫描BLE设备
tv_discovery.setText("正在搜索低功耗蓝牙设备");
} else {
mHandler.postDelayed(this, 2000);
}
}
};
stopScan方法表示停止扫描BLE设备,调用它的示例代码如下:
// 创建一个停止BLE扫描的任务
@SuppressLint("MissingPermission")
private Runnable mScanStop = () -> {
isScaning = false;
// 获取BLE设备扫描器
BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
scanner.stopScan(mScanCallback); // 停止扫描BLE设备
tv_discovery.setText("停止搜索低功耗蓝牙设备");
};
无论startScan方法还是stopScan方法,它们都需要传入回调对象的参数,如同监听器那样,一旦收到扫描结果,就触发回调对象的指定方法。下面是扫描回调对象的示例代码:
// 创建一个扫描回调对象
@SuppressLint("MissingPermission")
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
if (TextUtils.isEmpty(result.getDevice().getName())) {
return;
}
Log.d(TAG, "callbackType="+callbackType+", result="+result.toString());
// 下面把找到的蓝牙设备添加到设备映射和设备列表
BlueDevice device = new BlueDevice(result.getDevice().getName(), result.getDevice().getAddress(), 0);
mDeviceMap.put(device.address, device);
mDeviceList.clear();
mDeviceList.addAll(mDeviceMap.values());
runOnUiThread(() -> mListAdapter.notifyDataSetChanged());
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
以上的扫描回调代码表明,收到扫描结果会触发onScanResult方法,其参数为ScanResult类型,调用它的getDevice方法即可获得BLE服务端的设备对象。也可调用蓝牙适配器的getRemoteDevice方法,根据对方的MAC地址得到它的设备对象。之后再调用设备对象的connectGatt方法,连接GATT服务器并获得客户端的GATT对象,示例代码如下:
// 根据设备地址获得远端的蓝牙设备对象
mRemoteDevice = mBluetoothAdapter.getRemoteDevice(item.address);
Log.d(TAG, "onItemClick address="+mRemoteDevice.getAddress()+", name="+mRemoteDevice.getName());
// 连接GATT服务器
mBluetoothGatt = mRemoteDevice.connectGatt(this, false, mGattCallback);
注意,connectGatt方法的第三个输入参数为BluetoothGattCallback类型,表示这里要传入事先定义的GATT回调对象。下面是定义一个GATT回调对象的示例代码:
// 发现BLE服务端的服务列表及其特征值时回调
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
Log.d(TAG, "onServicesDiscovered status"+status);
if (status == BluetoothGatt.GATT_SUCCESS) {
// 获取GATT服务器提供的服务列表
List<BluetoothGattService> gattServiceList= mBluetoothGatt.getServices();
for (BluetoothGattService gattService : gattServiceList) {
List<BluetoothGattCharacteristic> charaList = gattService.getCharacteristics();
for (BluetoothGattCharacteristic chara : charaList) {
int charaProp = chara.getProperties(); // 获取该特征的属性
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
read_UUID_chara = chara.getUuid();
read_UUID_service = gattService.getUuid();
Log.d(TAG, "read_chara=" + read_UUID_chara + ", read_service=" + read_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
write_UUID_chara = chara.getUuid();
write_UUID_service = gattService.getUuid();
Log.d(TAG, "write_chara=" + write_UUID_chara + ", write_service=" + write_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {
write_UUID_chara = chara.getUuid();
write_UUID_service = gattService.getUuid();
Log.d(TAG, "no_response write_chara=" + write_UUID_chara + ", write_service=" + write_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
notify_UUID_chara = chara.getUuid();
notify_UUID_service = gattService.getUuid();
Log.d(TAG, "notify_chara=" + notify_UUID_chara + ", notify_service=" + notify_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
indicate_UUID_chara = chara.getUuid();
indicate_UUID_service = gattService.getUuid();
Log.d(TAG, "indicate_chara=" + indicate_UUID_chara + ", indicate_service=" + indicate_UUID_service);
}
}
}
} else {
Log.d(TAG, "onServicesDiscovered fail-->" + status);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic chara, int status) {
super.onCharacteristicRead(gatt, chara, status);
Log.d(TAG, "onCharacteristicRead");
if (status == BluetoothGatt.GATT_SUCCESS) {}
}
// 收到BLE服务端的数据变更时回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic chara) {
super.onCharacteristicChanged(gatt, chara);
String message = new String(chara.getValue()); // 把服务端返回的数据转成字符串
Log.d(TAG, "onCharacteristicChanged "+message);
}
// 收到BLE服务端的数据写入时回调
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic chara, int status) {
super.onCharacteristicWrite(gatt, chara, status);
Log.d(TAG, "onCharacteristicWrite");
if (status == BluetoothGatt.GATT_SUCCESS) {}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
Log.d(TAG, "onDescriptorWrite");
if (status == BluetoothGatt.GATT_SUCCESS) {}
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
super.onReadRemoteRssi(gatt, rssi, status);
Log.d(TAG, "onReadRemoteRssi");
if (status == BluetoothGatt.GATT_SUCCESS) {}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorRead(gatt, descriptor, status);
Log.d(TAG, "onDescriptorRead");
if ((status == BluetoothGatt.GATT_SUCCESS)) {}
}
};
虽然BluetoothGattCallback接口定义了许多方法,但是简单应用只需下列4个方法:
● onConnectionStateChange:BLE连接的状态发生变化时回调。如果连接成功,就调用GATT对象的discoverServices方法查找BLE服务;如果连接失败,就调用GATT对象的close方法关闭连接。
● onServicesDiscovered:发现BLE服务端的服务列表及其特征值时回调。如果执行成功,就调用GATT对象的getServices方法,获取GATT规范的服务列表,并遍历每个服务的特征值,分析服务端都提供了哪些服务与客户端交互。注意,必须事先在onConnectionStateChange中调用GATT对象的discoverServices方法,接着才会触发这里的onServicesDiscovered方法。如下【特征值类型的取值说明表】
![](https://img.haomeiwen.com/i2639112/1fdb18a750dfbd18.png)
● onCharacteristicChanged:收到BLE服务端的数据变更时回调。该方法会收到服务端送来的消息。
● onCharacteristicWrite:收到BLE服务端的数据写入时回调。如果执行成功,就表示服务端已经收到客户端发给它的消息。
若想在界面上显示扫描发现的设备,则可重写onConnectionStateChange方法,在连接成功的过程中将新设备添加至列表。补充界面列表的刷新代码之后,先打开手机的蓝牙功能,再运行并测试该App,可观察到扫描周围的BLE设备得到的结果,如下图所示
![](https://img.haomeiwen.com/i2639112/06a970ec8afd805d.png)
二、发送BLE广播
上面手机可以扫描发现周围的BLE设备,其实手机自身也能变成BLE设备让别人发现。不过仅仅开启蓝牙功能尚不足以使其变身,还得让手机向外发送BLE广播。在发送BLE广播之前要初始化蓝牙适配器,示例代码如下:
private BluetoothAdapter mBluetoothAdapter; // 声明一个蓝牙适配器对象
private BluetoothDevice mRemoteDevice; // 声明一个蓝牙设备对象
private BluetoothGatt mBluetoothGatt; // 声明一个蓝牙GATT客户端对象
// 初始化蓝牙适配器
private void initBluetooth() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "当前设备不支持低功耗蓝牙", Toast.LENGTH_SHORT).show();
finish(); // 关闭当前页面
}
// 获取蓝牙管理器,并从中得到蓝牙适配器
BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bm.getAdapter(); // 获取蓝牙适配器
}
接着调用蓝牙适配器的getBluetoothLeAdvertiser方法,获得BluetoothLeAdvertiser广播器对象。这个广播器主要有startAdvertising和stopAdvertising两个方法。其中,startAdvertising方法表示开始发送BLE广播,调用它的示例代码如下:BleAdvertiseActivity完整代码
// 开始低功耗蓝牙广播
@SuppressLint("MissingPermission")
private void startAdvertise(String ble_name) {
// 设置广播参数
AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setConnectable(true) // 是否允许连接
.setTimeout(0) // 设置超时时间
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.build();
// 设置广播内容
AdvertiseData advertiseData = new AdvertiseData.Builder()
.setIncludeDeviceName(true) // 是否把设备名称也广播出去
.setIncludeTxPowerLevel(true) // 是否把功率电平也广播出去
.build();
mBluetoothAdapter.setName(ble_name); // 设置BLE服务端的名称
// 获取BLE广播器
BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
// BLE服务端开始广播,好让别人发现自己
advertiser.startAdvertising(settings, advertiseData, mAdvertiseCallback);
}
stopAdvertising方法表示停止发送BLE广播,调用它的示例代码如下:BleAdvertiseActivity完整代码
// 停止低功耗蓝牙广播
@SuppressLint("MissingPermission")
private void stopAdvertise() {
if (mBluetoothAdapter != null) {
// 获取BLE广播器
BluetoothLeAdvertiser advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
if (advertiser != null) {
advertiser.stopAdvertising(mAdvertiseCallback); // 停止低功耗蓝牙广播
}
}
}
无论是startAdvertising方法还是stopAdvertising方法,它们都需要传入回调对象的参数,如同监听器一样,一旦成功发送广播,就触发回调对象的指定方法。下面是定义一个广播回调对象的示例代码:BleAdvertiseActivity完整代码
// 创建一个低功耗蓝牙广播回调对象
private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settings) {
Log.d(TAG, "低功耗蓝牙广播成功:"+settings.toString());
addService(); // 添加读写服务UUID,特征值等
String desc = String.format("BLE服务端“%s”正在对外广播", et_name.getText().toString());
tv_hint.setText(desc);
}
@Override
public void onStartFailure(int errorCode) {
Log.d(TAG, "低功耗蓝牙广播失败,错误代码为"+errorCode);
tv_hint.setText("低功耗蓝牙广播失败,错误代码为"+errorCode);
}
};
以上的广播回调代码表明,成功发送广播会触发onStartSuccess方法,在该方法中要给BLE服务端添加服务及其特征值,并开启GATT服务器等待客户端连接。添加服务的示例代码如下:BleAdvertiseActivity完整代码
// 添加读写服务UUID,特征值等
@SuppressLint("MissingPermission")
private void addService() {
BluetoothGattService gattService = new BluetoothGattService(
BleConstant.UUID_SERVER, BluetoothGattService.SERVICE_TYPE_PRIMARY);
// 只读的特征值
BluetoothGattCharacteristic charaRead = new BluetoothGattCharacteristic(BleConstant.UUID_CHAR_READ,
BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
// 只写的特征值
BluetoothGattCharacteristic charaWrite = new BluetoothGattCharacteristic(BleConstant.UUID_CHAR_WRITE,
BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_WRITE);
gattService.addCharacteristic(charaRead); // 将特征值添加到服务里面
gattService.addCharacteristic(charaWrite); // 将特征值添加到服务里面
// 开启GATT服务器等待客户端连接
mGattServer = mBluetoothManager.openGattServer(this, mGattCallback);
mGattServer.addService(gattService); // 向GATT服务器添加指定服务
}
注意,openGattServer方法的第二个输入参数为BluetoothGattServerCallback类型,表示这里要传入事先定义的GATT服务器回调对象。下面是一个GATT服务器回调对象的示例代码:
// 创建一个GATT服务器回调对象
@SuppressLint("MissingPermission")
private BluetoothGattServerCallback mGattCallback = new BluetoothGattServerCallback() {
// BLE连接的状态发生变化时回调
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
super.onConnectionStateChange(device, status, newState);
Log.d(TAG, "onConnectionStateChange device=" + device.toString() + " status=" + status + " newState=" + newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
runOnUiThread(() -> {
String desc = String.format("%s\n已连接BLE客户端,对方名称为%s,MAC地址为%s",
tv_hint.getText().toString(), device.getName(), device.getAddress());
tv_hint.setText(desc);
});
}
}
// 收到BLE客户端写入请求时回调
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic chara, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
super.onCharacteristicWriteRequest(device, requestId, chara, preparedWrite, responseNeeded, offset, value);
String message = new String(value); // 把客户端发来的数据转成字符串
Log.d(TAG, "收到了客户端发过来的数据 " + message);
}
};
BluetoothGattServerCallback接口定义了许多方法,不过简单应用只需下列两个方法:
● onConnectionStateChange:BLE连接的状态发生变化时回调。此时如果已经连接,就从输入参数获取客户端的设备对象,并处理后续的连接逻辑。
● onCharacteristicWriteRequest:收到BLE客户端写入请求时回调。该方法会收到客户端发来的消息。
分别在两部手机上安装测试App,一部手机充当BLE服务端,另一部手机充当BLE客户端。在服务端手机上点击开始广播按钮,此时正在广播的服务端界面如下图【服务端手机正在对外广播】所示;接着让客户端手机进入扫描页面,可以找到正在广播的BLE服务端,如图【客户端扫描发现BLE服务端】所示。
![](https://img.haomeiwen.com/i2639112/1bd284acf15c317f.png)
![](https://img.haomeiwen.com/i2639112/21ca1997e6c1b8f5.png)
三、通过主从BLE实现聊天应用
上面讲到手机通过向外发送BLE广播成为BLE服务端,进而被另一部手机在扫描BLE设备时发现,那么两部手机之间又该如何通信呢?这里牵涉到GATT服务端与GATT客户端的交互,首当其冲的是BluetoothGattServer和BluetoothGatt两个类,其中前者充当GATT服务端的角色,后者充当GATT客户端的角色。为了夯实BLE技术基础,有必要熟悉一下这两个GATT工具的用法。
首先看BluetoothGattServer,它的对象是怎么得到的呢?原来调用蓝牙管理器对象的openGattServer方法会开启GATT服务器并返回BluetoothGattServer类型的服务端对象,之后可通过服务端对象进行GATT相关操作。BluetoothGattServer的常用方法如下:
● addService:向GATT服务器添加指定服务。
● sendResponse:向GATT客户端发送应答,告诉它成功收到了要写入的数据。
● notifyCharacteristicChanged:向GATT客户端发送本地特征值已更新的通知。
● close:关闭GATT服务器。其次看BluetoothGatt,之前在上面提到,调用蓝牙设备对象的connectGatt方法会连接GATT服务器并返回BluetoothGatt类型的客户端对象,之后可通过客户端对象与服务端进行通信。BluetoothGatt的常用方法如下:
● discoverServices:开始查找GATT服务器提供的服务,查找成功会触发onServicesDiscovered方法。
● getServices:获取GATT服务器提供的服务列表。
● writeCharacteristic:往GATT服务器写入特征值。
● setCharacteristicNotification:开启或关闭特征值的通知(第二个输入参数为true表示开启)。开启之后才能收到服务器的特征值更新通知。
● disconnect:断开GATT连接。
● close:关闭GATT客户端。
GATT服务端与客户端的通信流程,主要包括三个方面:建立GATT连接、客户端向服务端发消息、服务端向客户端发消息。
1、建立GATT连接
首先需要打开服务器,然后客户端才能连上服务器。在GATT规范中,所有特征值都被封装在服务中,有了服务之后才允许读写特征值,故而GATT服务端的addService方法在先,GATT客户端的discoverServices方法在后。GATT服务端与客户端的连接过程如图【ATT服务端与客户端的连接过程】所示。
![](https://img.haomeiwen.com/i2639112/99ef671ba277b3f1.png)
2、客户端向服务端发消息
GATT客户端调用writeCharacteristic方法,会往GATT服务器写入特征值。服务器收到请求后,需调用GATT服务端的sendResponse方法向GATT客户端发送应答,告诉它成功收到了要写入的数据。此时客户端触发onCharacteristicWrite方法,表示收到了服务端的写入应答。客户端写入与服务端应答的交互过程如图【客户端向服务端发消息的过程】所示。
![](https://img.haomeiwen.com/i2639112/26d9c35e36a8eda8.png)
3、服务端向客户端发消息
服务端向客户端发消息,客户端理应无条件收到才是,但是GATT规范偏偏规定了通知开关,只有客户端开启了通知才会收到服务端的消息;一旦客户端关闭了通知,那么服务端干什么它都漠不关心了。GATT客户端开启了通知后,GATT服务端调用notifyCharacteristicChanged方法向客户端发送特征值变更通知,然后触发客户端的onCharacteristicChanged方法,客户端再在该方法中处理收到的消息。服务端写特征值并通知客户端的交互过程如图【服务端向客户端发消息的过程】所示。
![](https://img.haomeiwen.com/i2639112/42cb31d296bfd3a2.png)
搞清楚GATT服务端与客户端的通信流程,接着书写BLE通信代码便好办多了,无非是按部就班照流程套模板而已。然而BLE的读写操作可不简单,因为GATT规范划分了好几种特征值,有只读特征值、可写特征值、通知特征值、指示特征值等,其中只读特征值只允许客户端读、不允许客户端写,可写特征值既允许客户端读也允许客户端写,不同种类的特征值分别适用不同的业务场景。
另外,GATT规范要求每次传输的消息大小不能超过20字节,若待传输的消息长度超过了20字节,就得切片后分次传输。比如GATT服务端向客户端发消息,需要先按照20字节将消息切片再将切片后的消息列表依次发出去。GATT服务端发送消息的示例代码如下:BleServerActivity完整代码
// 发送聊天消息
@SuppressLint("MissingPermission")
private void sendMessage() {
String message = et_input.getText().toString();
if (TextUtils.isEmpty(message)) {
Toast.makeText(this, "请先输入聊天消息", Toast.LENGTH_SHORT).show();
return;
}
et_input.setText("");
ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
List<String> msgList = ChatUtil.splitString(message, 20); // 按照20字节切片
for (String msg : msgList) {
mReadChara.setValue(msg); // 设置读特征值
// 发送本地特征值已更新的通知
mGattServer.notifyCharacteristicChanged(mRemoteDevice, mReadChara, false);
}
appendChatMsg(message, true); // 往聊天窗口添加聊天消息
}
至于接收消息,不存在消息过长的问题,无论收到什么消息都原样接收处理。下面是GATT服务端接收客户端消息的示例代码:BleServerActivity完整代码
// 收到BLE客户端写入请求时回调
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic chara, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
super.onCharacteristicWriteRequest(device, requestId, chara, preparedWrite, responseNeeded, offset, value);
String message = new String(value); // 把客户端发来的数据转成字符串
Log.d(TAG, "收到了客户端发过来的数据 " + message);
// 向GATT客户端发送应答,告诉它成功收到了要写入的数据
mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, chara.getValue());
runOnUiThread(() -> appendChatMsg(message, false)); // 往聊天窗口添加聊天消息
}
GATT客户端不仅要将消息切片,还不能一股脑地连续发送切片消息。因为服务端接收消息需要时间,如果一下子涌来多条消息,那么前面的消息会被后面的消息覆盖,导致服务端只能处理最后一条消息。正确的做法是,每次发送切片消息之前先检查上次的消息是否被服务端成功接收。只有服务端已经收到上一条消息,客户端才能发送下一条消息,否则就得继续等待直至服务端确认收到上次消息。按照以上逻辑编写GATT客户端的消息发送代码,示例如下:BleClientActivity完整代码
private boolean isLastSuccess = true; // 上一条消息是否发送成功
// 发送聊天消息
private void sendMessage() {
String message = et_input.getText().toString();
if (TextUtils.isEmpty(message)) {
Toast.makeText(this, "请先输入聊天消息", Toast.LENGTH_SHORT).show();
return;
}
et_input.setText("");
ViewUtil.hideOneInputMethod(this, et_input); // 隐藏软键盘
new MessageThread(message).start(); // 启动消息发送线程
appendChatMsg(message, true); // 往聊天窗口添加聊天消息
}
// 定义一个消息发送线程
@SuppressLint("MissingPermission")
private class MessageThread extends Thread {
private List<String> msgList; // 消息列表
public MessageThread(String message) {
msgList = ChatUtil.splitString(message, 20);
}
@Override
public void run() {
// 拿到写的特征值
BluetoothGattCharacteristic chara = mBluetoothGatt.getService(BleConstant.UUID_SERVER)
.getCharacteristic(BleConstant.UUID_CHAR_WRITE);
for (int i=0; i<msgList.size(); i++) {
if (isLastSuccess) { // 需要等到上一条回调成功之后,才能发送下一条消息
isLastSuccess = false;
Log.d(TAG, "writeCharacteristic "+msgList.get(i));
chara.setValue(msgList.get(i)); // 设置写特征值
mBluetoothGatt.writeCharacteristic(chara); // 往GATT服务器写入特征值
} else {
i--;
}
try {
sleep(300); // 休眠300毫秒,等待上一条的回调通知
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
客户端上次发送的消息成功接收与否要在onCharacteristicWrite方法中校验,该方法判断如果是成功状态,则表示服务端的确收到了客户端消息,此时再更改成功与否的标志位。GATT客户端的写入回调方法的示例代码如下:BleClientActivity完整代码
// 收到BLE服务端的数据变更时回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic chara) {
super.onCharacteristicChanged(gatt, chara);
String message = new String(chara.getValue()); // 把服务端返回的数据转成字符串
Log.d(TAG, "onCharacteristicChanged "+message);
runOnUiThread(() ->appendChatMsg(message, false)); // 往聊天窗口添加聊天消息
}
接收消息不存在前述的繁文缛节,直接在onCharacteristicChanged方法中处理收到的消息即可。下面是GAT客户端接收服务端消息的示例代码:
// 收到BLE服务端的数据写入时回调
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic chara, int status) {
super.onCharacteristicWrite(gatt, chara, status);
Log.d(TAG, "onCharacteristicWrite status="+status);
if (status == BluetoothGatt.GATT_SUCCESS) {
isLastSuccess = true;
} else {
Log.d(TAG, "write fail->" + status);
}
}
网友评论