背景
接上文Android手机之间使用经典蓝牙进行通信,记一次技术预研_1 - 掘金 (juejin.cn)
iOS开发总算是搞明白经典蓝牙和低功耗蓝牙BLE的关系了,也给我带来了一个坏消息,iOS设备因为历史原因(要收硬件的苹果税)不支持经典蓝牙,只能使用BLE进行通信,直接把上篇文章经典蓝牙的路线给堵上了。
迫不得已再来看看BLE,却发现BLE其实也是可以用来给手机通信的,上次其实是被他的描述给坑了。经过一番折腾,我这边也算是又整了个demo完成了通信。GATT协议不需要预先配对,也算是意外之喜吧,至于半双工的特性对我的需求完全没有什么影响。
概述
名词解释
- 服务端:BluetoothGattServer,创建好蓝牙服务,等待客户端前来连接,并在收到消息后给出响应的一端,类似HTTP协议的服务端。
- 客户端:BluetoothGatt,主动连接蓝牙服务,发送消息并接收响应的一端,类似HTTP协议的客户端。
流程
截图
公共部分
一个工具类,提供了BLE硬件的检查、蓝牙开关的检查、定位开关和定位权限的检查。后续双端实现代码中出现的manager
和adapter
均为这个工具类get出来的。双方约定客户端以UUID_SERVER_CHAR_WRITE
对应的特征值(简称写特征)发送消息,服务端使用UID UUID_SERVER_CHAR_READ
对应的特征值(简称读特征)响应消息供客户端读取。因此需要给读特征添加一个描述,使用的UUID是UUID_SERVER_CHAR_READ_DESC
。
公共部分:硬件检查、蓝牙开关检查
客户端特需:定位开关和权限
Java
public class BleHelper {
public static final String TAG = "BleHelper";
public static final UUID UUID_SERVICE = UUID.fromString("");
public static final UUID UUID_SERVER_CHAR_READ = UUID.fromString("");
public static final UUID UUID_SERVER_CHAR_READ_DESC = UUID.fromString("");
public static final UUID UUID_SERVER_CHAR_WRITE = UUID.fromString("");
public static boolean checkHasBleHardware(Context context) {
if (context == null) {
return false;
}
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
public static boolean checkAll(Context context) {
if (context == null) {
return false;
}
if (!enable(context)) {
Log.e(TAG, "BleHelp初始化失败:" + "(用户操作)未打开手机蓝牙,蓝牙功能无法使用......");
Toast.makeText(context, "未打开手机蓝牙,蓝牙功能无法使用...", Toast.LENGTH_LONG).show();
return false;
}
return true;
}
/**
* 蓝牙客户端需要GPS开启、定位权限,否则搜索不到设备
* @param context
* @return
*/
public static boolean checkBleClient(Context context) {
if (context == null) {
return false;
}
if (!isOPenGps(context)) {
Log.e(TAG, "BleHelp初始化失败:" + "(用户操作)GPS未打开,蓝牙功能无法使用...");
Toast.makeText(context, "GPS未打开,蓝牙功能无法使用", Toast.LENGTH_LONG).show();
return false;
}
return checkAll(context);
}
/**
* 打开手机蓝牙
*
* @return true 表示打开成功
*/
public static boolean enable(Context context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
return false;
}
if (!getBluetoothAdapter().isEnabled()) {
//若未打开手机蓝牙,则会弹出一个系统的是否打开/关闭蓝牙的对话框,禁止或者未处理返回false,允许返回true
//若已打开手机蓝牙,直接返回true
if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
return false;
}
boolean enableState = getBluetoothAdapter().enable();
Log.d(TAG, "(用户操作)手机蓝牙是否打开成功:" + enableState);
return enableState;
} else {
return true;
}
}
/**
* 判断GPS是否开启,GPS或者AGPS开启一个就认为是开启的
*
* @return true 表示开启
*/
public static boolean isOPenGps(Context context) {
LocationManager locationManager
= (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
// 通过GPS卫星定位,定位级别可以精确到街(通过24颗卫星定位,在室外和空旷的地方定位准确、速度快)
boolean gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
// 通过WLAN或移动网络(3G/2G)确定的位置(也称作AGPS,辅助GPS定位。主要用于在室内或遮盖物(建筑群或茂密的深林等)密集的地方定位)
boolean network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
if (gps || network) {
Log.d(TAG, "GPS状态:打开");
return true;
}
Log.e(TAG, "GPS状态:关闭");
return false;
}
public static BluetoothManager getBluetoothManager(Context context) {
return (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
}
public static BluetoothAdapter getBluetoothAdapter() {
return BluetoothAdapter.getDefaultAdapter();
}
}
服务端实现
1,对外开启广播
Java
//广播设置(必须)
private final AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //广播模式: 低功耗,平衡,低延迟
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //发射功率级别: 极低,低,中,高
.setTimeout(0)
.setConnectable(true) //能否连接,广播分为可连接广播和不可连接广播
.build();
//广播数据(必须,广播启动就会发送)
private final AdvertiseData advertiseData = new AdvertiseData.Builder()
.setIncludeDeviceName(true) //包含蓝牙名称
.setIncludeTxPowerLevel(true) //包含发射功率级别
.addManufacturerData(1, new byte[]{23, 33}) //设备厂商数据,自定义
.build();
//扫描响应数据(可选,当客户端扫描时才发送)
private final AdvertiseData scanResponse = new AdvertiseData.Builder()
.addManufacturerData(2, new byte[]{66, 66}) //设备厂商数据,自定义
.addServiceUuid(new ParcelUuid(BleHelper.UUID_SERVICE)) //服务UUID
// .addServiceData(new ParcelUuid(UUID_SERVICE), new byte[]{2}) //服务数据,自定义
.build();
// BLE广播Callback
private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.d(BleHelper.TAG, "BLE广播开启成功");
}
@Override
public void onStartFailure(int errorCode) {
Log.e(BleHelper.TAG, "BLE广播开启失败,错误码:" + errorCode);
}
};
/**
* 1, 启动广播
*/
private void startBroadcast() {
// 注意:必须要开启可连接的BLE广播,其它设备才能发现并连接BLE服务端!
BluetoothLeAdvertiser bluetoothLeAdvertiser = adapter.getBluetoothLeAdvertiser();
bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseCallback);
}
2,初始化服务端
Java
private void initBleService() {
// =============启动BLE蓝牙服务端======================================
BluetoothGattService service = new BluetoothGattService(BleHelper.UUID_SERVICE,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
//添加可读+通知characteristic
characteristicRead = new BluetoothGattCharacteristic(BleHelper.UUID_SERVER_CHAR_READ,
BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
characteristicRead.addDescriptor(new BluetoothGattDescriptor(BleHelper.UUID_SERVER_CHAR_READ_DESC,
BluetoothGattCharacteristic.PERMISSION_WRITE));
service.addCharacteristic(characteristicRead);
//添加可写characteristic
BluetoothGattCharacteristic characteristicWrite = new BluetoothGattCharacteristic(BleHelper.UUID_SERVER_CHAR_WRITE,
BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE);
service.addCharacteristic(characteristicWrite);
if (manager != null) {
bluetoothGattServer = manager.openGattServer(this, mBluetoothGattServerCallback);
}
bluetoothGattServer.addService(service);
}
/**
* 服务事件的回调
*/
private final BluetoothGattServerCallback mBluetoothGattServerCallback= new BluetoothGattServerCallback() {
/**
* 1.连接状态发生变化时
*/
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
Log.d(BleHelper.TAG, String.format("1.onConnectionStateChange:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.d(BleHelper.TAG, String.format("1.onConnectionStateChange:status = %s, newState =%s ", status, newState));
if (newState == BluetoothAdapter.STATE_CONNECTED) {
Log.d(BleHelper.TAG, "1.onConnectionStateChange:设备连接成功");
changeStatus("连接成功");
} else if (newState == BluetoothAdapter.STATE_DISCONNECTED) {
Log.d(BleHelper.TAG, "1.onConnectionStateChange:设备断开连接");
changeStatus("连接断开");
}
}
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
Log.d(BleHelper.TAG, String.format("onServiceAdded:status = %s", status));
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
Log.d(BleHelper.TAG, String.format("onCharacteristicReadRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.d(BleHelper.TAG, String.format("onCharacteristicReadRequest:requestId = %s, offset = %s", requestId, offset));
bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
}
/**
* 3. onCharacteristicWriteRequest,接收具体的字节
*/
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] requestBytes) {
Log.d(BleHelper.TAG, String.format("3.onCharacteristicWriteRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.d(BleHelper.TAG, String.format("3.onCharacteristicWriteRequest:requestId = %s, preparedWrite=%s, responseNeeded=%s, offset=%s, value=%s", requestId, preparedWrite, responseNeeded, offset, requestBytes.toString()));
bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, requestBytes);
//4.处理响应内容
onResponseToClient(requestBytes, device, requestId, characteristic);
}
/**
* 2.描述被写入时,在这里执行 bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS... 收,触发 onCharacteristicWriteRequest
*/
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
Log.d(BleHelper.TAG, String.format("2.onDescriptorWriteRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.d(BleHelper.TAG, String.format("2.onDescriptorWriteRequest:requestId = %s, preparedWrite = %s, responseNeeded = %s, offset = %s, value = %s,", requestId, preparedWrite, responseNeeded, offset, value.toString()));
// now tell the connected device that this was all successfull
bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
}
/**
* 5.特征被读取。当回复响应成功后,客户端会读取然后触发本方法
*/
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
Log.d(BleHelper.TAG, String.format("5.onDescriptorReadRequest:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.d(BleHelper.TAG, String.format("5.onDescriptorReadRequest:requestId = %s", requestId));
bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
}
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
super.onNotificationSent(device, status);
Log.d(BleHelper.TAG, String.format("5.onNotificationSent:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.d(BleHelper.TAG, String.format("5.onNotificationSent:status = %s", status));
}
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
super.onMtuChanged(device, mtu);
Log.d(BleHelper.TAG, String.format("onMtuChanged:mtu = %s", mtu));
}
@Override
public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
super.onExecuteWrite(device, requestId, execute);
Log.d(BleHelper.TAG, String.format("onExecuteWrite:requestId = %s", requestId));
}
};
/**
* 4.处理响应内容
*
* @param reqeustBytes
* @param device
* @param requestId
* @param characteristic
*/
private void onResponseToClient(byte[] reqeustBytes, BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic) {
Log.e(BleHelper.TAG, String.format("4.onResponseToClient:device name = %s, address = %s", device.getName(), device.getAddress()));
Log.e(BleHelper.TAG, String.format("4.onResponseToClient:requestId = %s", requestId));
Log.e(BleHelper.TAG, "4.收到:");
String str = etContent.getText().toString();
characteristicRead.setValue(str.getBytes());
bluetoothGattServer.notifyCharacteristicChanged(device, characteristicRead, false);
Log.i(BleHelper.TAG, "4.响应:" + str);
Message msg = handler.obtainMessage(MSG_WHAT_RECEIVER);
msg.obj = new String(reqeustBytes);
handler.sendMessage(msg);
}
注释都写的比较清楚了:
- 当客户端连接或者断开连接服务时,回调
onConnectionStateChange
- 收到客户端发来的消息是,回调
onCharacteristicWriteRequest
,并调用onResponseToClient
方法响应客户端 - 客户端读取响应内容,回调
onDescriptorReadRequest
3,结束
记得该关的关一下,如果需要把广播也关闭的话,就再添加一个bluetoothLeAdvertiser.stopAdvertising
,传入和开启广播相同的AdvertiseCallback
即可。
Java
@Override
protected void onDestroy() {
super.onDestroy();
if (bluetoothGattServer != null) {
Log.d(BleHelper.TAG, "关闭蓝牙服务端");
bluetoothGattServer.close();
}
}
客户端实现
1,扫描蓝牙设备
这里的first
和上一篇文章一样,是个demo用的临时变量,避免重复扫设备重复连接用的。
Java
private void startScanDevice() {
changeStatus("正在搜索设备");
Log.i(BleHelper.TAG, "正在搜索设备");
adapter.getBluetoothLeScanner().startScan(mScanCallback);
}
// 扫描结果Callback
private final ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
// result.getScanRecord() 获取BLE广播数据
if (device == null || TextUtils.isEmpty(device.getName())) {
return;
}
Log.i(BleHelper.TAG, "发现设备" + device.getName() + " " + device.getAddress());
if (TextUtils.equals(deviceName, device.getName()) && first) {
first = false;
changeStatus("正在连接设备");
adapter.getBluetoothLeScanner().stopScan(mScanCallback);
mBluetoothGatt = device.connectGatt(BleSubActivity.this, false, gattCallback);
}
}
};
2,找到服务端后连接
其实上一步的最后一句device.connectGatt
就已经在连接了,下面代码是连接后的回调。
Java
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,int newState) {
//连接状态改变的Callback
if (newState == BluetoothGatt.STATE_CONNECTED) {
Log.d(BleHelper.TAG, "设备连接上 开始扫描服务");
// 连接成功后,开始扫描服务
changeStatus("连接成功,开始发现服务");
mBluetoothGatt.discoverServices();
}
if (newState == BluetoothGatt.STATE_DISCONNECTED) {
// 连接断开
/*连接断开后的相应处理*/
changeStatus("连接断开");
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//服务发现成功的Callback
//获取服务列表
servicesList = mBluetoothGatt.getServices();
if (servicesList == null || servicesList.size() == 0) {
Log.e(BleHelper.TAG, "未发现服务");
changeStatus("未发现服务");
return;
}
for (BluetoothGattService service : servicesList) {
Log.i(BleHelper.TAG, "找到服务 " + service.getUuid().toString());
}
changeStatus("连接中......");
BluetoothGattService service = mBluetoothGatt.getService(BleHelper.UUID_SERVICE);
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
Log.e(BleHelper.TAG, "找到特征 " + characteristic.getUuid().toString());
}
readCharacteristic = service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_READ);
writeCharacteristic = service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_WRITE);
mBluetoothGatt.setCharacteristicNotification(readCharacteristic, true);
BluetoothGattDescriptor descriptor = readCharacteristic.getDescriptor(BleHelper.UUID_SERVER_CHAR_READ_DESC);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
//若开启监听成功则会回调BluetoothGattCallback中的onDescriptorWrite()方法
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//写入Characteristic
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(BleHelper.TAG, "发送成功");
changeStatus("发送成功");
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//读取Characteristic
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
// 通知Characteristic
byte[] value = characteristic.getValue();
if (value != null) {
String str = new String(value);
Log.d(BleHelper.TAG, "收到消息:" + str);
setTvReceiver(str);
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//写入Descriptor
if (status == BluetoothGatt.GATT_SUCCESS) {
//开启监听成功,可以向设备写入命令了
Log.d(BleHelper.TAG, "开启监听成功");
changeStatus("准备就绪");
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//读取Descriptor
}
};
3,取出特征值和描述
因为是一个回调,也不方便拆开来发,代码已经在上面了,这里简单解释一下。
Java
BluetoothGattService service = mBluetoothGatt.getService(BleHelper.UUID_SERVICE);
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
Log.e(BleHelper.TAG, "找到特征 " + characteristic.getUuid().toString());
}
这里是GATT服务本身的UUID,意义不大。
Java
readCharacteristic = service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_READ);
writeCharacteristic = service.getCharacteristic(BleHelper.UUID_SERVER_CHAR_WRITE);
mBluetoothGatt.setCharacteristicNotification(readCharacteristic, true);
BluetoothGattDescriptor descriptor = readCharacteristic.getDescriptor(BleHelper.UUID_SERVER_CHAR_READ_DESC);
这是取出读写特征值、读特征的描述
Java
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
这里往读特征的描述设置一下有变化了通知,设置完成后回调onDescriptorWrite
方法就说明设置完成,可以发消息了。
4,发送接收消息
发消息,有资料的说法是单次传输不超过20个字节,但是我实际使用下来并不是硬性限制。传输的字节太多,协议会自动修改MTU值来适应,只要传输的频率不高完全不用自己手动控制。
Java
// 发消息
String content = etContent.getText().toString();
writeCharacteristic.setValue(content);
mBluetoothGatt.writeCharacteristic(writeCharacteristic);
接收响应是上面代码中的onCharacteristicChanged
回调
Java
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,BluetoothGattCharacteristic characteristic) {
// 通知Characteristic
byte[] value = characteristic.getValue();
if (value != null) {
String str = new String(value);
Log.d(BleHelper.TAG, "收到消息:" + str);
setTvReceiver(str);
}
}
5,结束
关闭扫描和关闭客户端。虽然在连接成功里面已经关闭过一次扫描了,但是这里最好还是再关一次,毕竟可能存在扫不到服务端设备的情况。
Java
@Override
protected void onDestroy() {
super.onDestroy();
adapter.getBluetoothLeScanner().stopScan(mScanCallback);
if (mBluetoothGatt != null) {
Log.d(BleHelper.TAG, "关闭蓝牙客户端");
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
}
}
已知问题
UUID.randomUUID()
生成的是128bit的UUID,iOS用在读写特征上没有问题,但是一旦用来生成描述就会闪退。目前有别的工作在处理,等过阵子继续研究一下。iOS开发找的代码是4位16进制数的UUID,咱也不知道和其他蓝牙程序的重复概率有多大,不敢用。