主要记录操作蓝牙遇到的问题
使用框架:
github:https://github.com/dingjikerbo/Android-BluetoothKit
蓝牙基本操作什么的都不多说了,该框架优点很多很多人做过功课笔记怎么使用,
动态代理来实现
代码解读参见 BluetoothKit源码解读
这里主要记录一些坑
前提确认过给过所有权限,比如定位wifi,Android 6.0及以上 蓝牙搜索需要定位权限。
注意:查证发现,直接down该项目主页library作为module引用和使用gradle引用的某些类的代码不同步。
0x00 扫不到经典蓝牙
解决:
构建SearchRequest 要把查找经典蓝牙条件加进去。
image.png
SearchRequest request = new SearchRequest.Builder()
.searchBluetoothLeDevice(3000, 3) // 先扫BLE设备3次,每次3s
.searchBluetoothClassicDevice(5000) // 再扫经典蓝牙5s
.searchBluetoothLeDevice(2000) // 再扫BLE设备2s
.build();
0x01 添加经典蓝牙系统的所有配对过的设备显示出来
解决办法:自己做过滤处理。
直接返回的手机配对过的经典蓝牙设备的信号量都为0.
private List<SearchResult> mDevices = new ArrayList<>(); //搜索到的device list
private final SearchResponse mSearchResponse = new SearchResponse() {
@Override
public void onSearchStarted() {
LogUtil.e("onSearchStarted");
mDevices.clear();
}
@Override
public void onDeviceFounded(SearchResult device) {
if (device.rssi != 0 && device.getName() != null && !device.getName().toUpperCase().equals("NULL") && !mDevices.contains(device)) {
mDevices.add(device);
}
}
@Override
public void onSearchStopped() {
LogUtil.e("onSearchStopped");
}
@Override
public void onSearchCanceled() {
LogUtil.e("onSearchCanceled");
}
};
过滤条件如代码,其中 device.getName().toUpperCase().equals("NULL")
是因为 后来查看 源码中是强制给name 为null 情况下 赋值NULL。
参见 {@link com.inuker.bluetooth.library.search.SearchResult#getName() }
public String getName() {
String name = device.getName();
return TextUtils.isEmpty(name) ? "NULL" : name;
}
0x02 BluetoothKit 连接不上经典蓝牙或者一直没反应
这个问题困扰很久,此框架作者强调了主要给BLE设备用的,多次查找debug最终定位到位置,参见
* {@link com.inuker.bluetooth.library.connect.BleConnectWorker#openGatt() }
@Override
public boolean openGatt() {
checkRuntime();
BluetoothLog.v(String.format("openGatt for %s", getAddress()));
if (mBluetoothGatt != null) {
BluetoothLog.e(String.format("Previous gatt not closed"));
return true;
}
Context context = BluetoothUtils.getContext();
BluetoothGattCallback callback = new BluetoothGattResponse(mBluetoothGattResponse);
if (Version.isMarshmallow()) {
mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, callback);
}
if (mBluetoothGatt == null) {
BluetoothLog.e(String.format("openGatt failed: connectGatt return null!"));
return false;
}
return true;
}
其中 Version.isMarshmallow()方法是做真值判断:
public static boolean isMarshmallow() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
现在的Android 设备6.0 Android M之前的 几乎很少不存在了,所以,问题出在
mBluetoothGatt = mBluetoothDevice.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE);
打开源码(Android 9.0)看,
/**
* Connect to GATT Server hosted by this device. Caller acts as GATT client.
* The callback is used to deliver results to Caller, such as connection status as well
* as any further GATT client operations.
* The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
* GATT client operations.
*
* @param callback GATT callback handler that will receive asynchronous callbacks.
* @param autoConnect Whether to directly connect to the remote device (false) or to
* automatically connect as soon as the remote device becomes available (true).
* @param transport preferred transport for GATT connections to remote dual-mode devices {@link
* BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
* BluetoothDevice#TRANSPORT_LE}
* @throws IllegalArgumentException if callback is null
*/
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback, int transport) {
return (connectGatt(context, autoConnect, callback, transport, PHY_LE_1M_MASK));
}
transport 的参数可以为:
/**
* No preference of physical transport for GATT connections to remote dual-mode devices
*/
public static final int TRANSPORT_AUTO = 0;
/**
* Prefer BR/EDR transport for GATT connections to remote dual-mode devices
*/
public static final int TRANSPORT_BREDR = 1;
/**
* Prefer LE transport for GATT connections to remote dual-mode devices
*/
public static final int TRANSPORT_LE = 2;
可以看出来transport作为连接偏好,框架在系统OS版本是6.0及以后为采用TRANSPORT_LE,所以修改源码中连接方式,全部采用BluetoothDevice.TRANSPORT_AUTO 可以连接经典蓝牙 ,
缺点/不足:实测 连接经典蓝牙相对耗时或者说慢了一点。
0x03 搜索蓝牙无任何数据,立即结束。
仔细查看会有错误输出:
BtGatt.GattService: App 'xxx' is scanning too frequently
排除其他的可能权限设备等,问题原因 搜索太频繁。
Android 7.0 及以上 底层对搜索做了限制,30秒内最多搜索5次,频繁搜索底层不响应并报Error Log。
解决办法,控制搜索五次的总时长 大于30秒就好,
0x04 关于service和character的操作建议
当连接成功后会拿到所有的service,然后根据厂商给定的可操作service和character去进行读写操作。
这里建议service不要缓存,UUID不变,但是这些service都会和gatt关联的,如果gatt变了,那service就没有用,直接影响service和character做任何读写操作都会出错。
所以建议每次连接上时都去discover service,然后重新去比对校验自己需要操作的service和character,
这里实践出连接同一个设备在不同页面都需要进行操作的思路:
每次连接都重新保存自己需要操作的service和character,然后根据单例对象需要操作的时候去操作,
/**
* 自定义管理对象 继承 BluetoothClient
*/
public class NewBluetoothClient extends BluetoothClient {
private BleGattProfile bleGattProfile;//已连接的BleGattProfile 信息
public BleGattProfile getBleGattProfile() {
return bleGattProfile;
}
public void setBleGattProfile(BleGattProfile bleGattProfile) {
this.bleGattProfile = bleGattProfile;
}
public NewBluetoothClient(Context context) {
super(context);
}
然后实现一个 单例
public class ClientManager {
private static NewBluetoothClient mClient;
public static NewBluetoothClient getClient() {
if (mClient == null) {
synchronized (ClientManager.class) {
if (mClient == null) {
mClient = new NewBluetoothClient(WexInit.getContext());
}
}
}
return mClient;
}
}
在每次连接成功的时候保存所有的service
BleConnectOptions options = new BleConnectOptions.Builder()
.setConnectRetry(0)
.setConnectTimeout(6000)
.setServiceDiscoverRetry(2)
.setServiceDiscoverTimeout(6000)
.build();
ClientManager.getClient().connect(mac, options, new BleConnectResponse() {
@Override
public void onResponse(int code, BleGattProfile profile) {
showDialog(false);
if (code == REQUEST_SUCCESS) {
//需要保存当前BleGattProfile 信息 获取可操作的 特征
ClientManager.getClient().setBleGattProfile(profile);
ToastUtil.showMessage("蓝牙连接成功");
} else {
ToastUtil.showMessage("蓝牙连接失败");
}
}
});
在需要操作的时候,先判断连接状态,然后拿到保存的所有service ,遍历找到需要操作的,
if (ClientManager.getClient().getBleGattProfile() == null || ClientManager.getClient().getConnectStatus(macAddreee) != BluetoothProfile.STATE_CONNECTED) {
//do something
return;
}
checkPermission();
//必須每次查找 可能出現更换设备现象
for (BleGattService service : ClientManager.getClient().getBleGattProfile().getServices()) {
if (!service.getUUID().toString().toLowerCase().contains( SERVICE_CHARACTERISTIC))
continue;
for (BleGattCharacter character : service.getCharacters()) {
if (character.getUuid().toString().toLowerCase().contains( CHARACTER_CHARACTERISTIC)) {
mService = service.getUUID();
mCharacter = character.getUuid();
break;
}
}
}
说明:
SERVICE_CHARACTERISTIC,
CHARACTER_CHARACTERISTIC为厂商给定的特征值。
mService mCharacter 为需要操作的uuid。
0x05 关于日志太多
使用gradle 引用无解,使用module 源码引用,自己修改下日志控制类就好了
com.inuker.bluetooth.library.utils.BluetoothLog 中增加一个开关。
有需要的拿走不谢。
public class BluetoothLog {
private static boolean allow = true;
private static final String LOG_TAG = "miio-bluetooth";
public static void enableLog(boolean enable) {
allow = enable;
}
public static void i(String msg) {
if (allow) {
Log.i(LOG_TAG, msg);
}
}
public static void e(String msg) {
if (allow) {
Log.e(LOG_TAG, msg);
}
}
public static void v(String msg) {
if (allow) {
Log.v(LOG_TAG, msg);
}
}
public static void d(String msg) {
if (allow) {
Log.d(LOG_TAG, msg);
}
}
public static void w(String msg) {
if (allow) {
Log.w(LOG_TAG, msg);
}
}
public static void e(Throwable e) {
if (allow) {
e(getThrowableString(e));
}
}
public static void w(Throwable e) {
if (allow) {
w(getThrowableString(e));
}
}
private static String getThrowableString(Throwable e) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
while (e != null) {
e.printStackTrace(printWriter);
e = e.getCause();
}
String text = writer.toString();
printWriter.close();
return text;
}
}
网友评论