Android BLE开发入门(2) —— 连接

在上一篇文章中,我详细的介绍了BLE的扫描。 在这片文章中, 我们将探索 BLE的连接(connecting ), 断开连接(disconnecting ) 和 发现服务(discovering services)。

原文:medium.com/@martijn.va...

连接设备

当你通过扫描发现设备后, 你可以通过调用 connectGatt() 来连接设备。 它会返回一个 BluetoothGatt 对象, 你可以通过这个对象来进行 GATT 的相关操作, 例如对特征值(characteristics)的读写操作(即与BLE设备的双向数据传递)。然而, 有两个版本的 connectGatt 方法。最新的 Android 版本增加了更多的变化,但是因为我们想要和 Android 6兼容,我们只看这两个:

java 复制代码
BluetoothGatt connectGatt(Context context, boolean autoConnect,
        BluetoothGattCallback callback)
        
BluetoothGatt connectGatt(Context context, boolean autoConnect,
        BluetoothGattCallback callback, int transport)

第一个方法的内部实现在源码中是调用第二个方法,transport 的参数的值为 TRANSPORT_AUTO。如果你想连接 BLE 设备,那么这不是一个正确的值。

vbnet 复制代码
    /**
     * 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).
     * @throws IllegalArgumentException if callback is null
     */
    public BluetoothGatt connectGatt(Context context, boolean autoConnect,
            BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
    }

TRANSPORT_AUTO 同时支持BLE和经典蓝牙设备。这意味着你对所建立的连接类型没有"偏好",并且希望 Android设备 来选择。目前还不清楚 Android 是如何选择的,所以这可能会导致相当不可预料的结果,许多人都报告了这个问题。所以建议你使用第二个版本的方法, 将transport参数值设置为 TRANSPORT_LE

ini 复制代码
BluetoothGatt gatt = device.connectGatt(context, false, bluetoothGattCallback, TRANSPORT_LE);

第一个参数是 context。

第二个参数是 autoconnect,代表的意思是, 是否要立即连接。使用 false 意味着 立即连接, Android会尝试连接30s,然后超时。当连接超时, 你会收到一个133的错误码。 这 并不是连接超时的错误码。 它在谷歌的源码中被定义为 GATT_ERROR,不幸的是,在其他场合也会出现这种错误。还要记住,一次只能使用 false 发出一个连接,因为 Android 会取消任何其他值为 false 的连接,如果有的话。

第三个参数是 BluetoothGattCallback,用于此设备的回调。 这个回调和扫描的回调不一样。 该回调用于所有特定于设备的操作,如读和写。我们将在下一篇文章中聊聊它。

Autoconnect = true

如果你将 autoconnect 参数设置为 true, Android 只要看到设备就会连接, 并且没有超时时间的限制。所以蓝牙栈底层会不断扫描, 发现设备时就会发起连接。如果您希望在某个已知设备可用时重新连接到该设备,那么这很方便。事实上,这是重新连接的首选方式。 你可以简单的创建一个 BluetoothDevice 对象, 调用 connectGatt ,将 autoconnect参数设置为 true

ini 复制代码
BluetoothDevice device = bluetoothAdapter.getRemoteDevice("12:34:56:AA:BB:CC");

BluetoothGatt gatt = device.connectGatt(context, true, bluetoothGattCallback, TRANSPORT_LE);

请记住,这只有在设备位于蓝牙缓存中或者之前已经绑定的情况下才能工作! 然而, 重启你的设备或者打开关闭蓝牙,将会清除蓝牙缓存。 所以你每次需要在自动连接之前, 进行必要的检查。

为了确认一个设备是否已经被缓存,你可以通过这种方式判断。创建 BluetoothDevice 之后,您应该执行 getType () ,如果它返回 TYPE_UNKNOWN,那么设备显然没有被缓存。如果是这种情况,您必须首先用这个 mac 地址扫描设备(使用非主动扫描模式) ,然后您可以再次使用自动连接。

现在自动连接很好用,所以你应该试一试。然而,自动连接的一大缺点是,与先积极扫描,然后将自动连接设置为 false 相比,连接需要更长的时间。这是因为 Android 内部使用低功耗设置扫描。更糟糕的是,每个手机制造商的连接时间也各不相同!要快速连接,首先积极扫描,然后连接与自动连接设置为 flase。自动连接的另一个优点是您可以发出许多自动连接。但是使用非自动连接时,一次只能尝试一次连接!

连接状态变更

调用 connectGatt 后,底层蓝牙栈 会将结果 通过 onConnectionStateChange 回调通知你。 每一次连接状态的变化, 都会在此回调中通知。

处理此回调并非易事!不要被你在互联网上找到的过于简单化的例子所愚弄。大多数简单的例子看起来像这样:

arduino 复制代码
public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        gatt.discoverServices();
    } else {
        gatt.close();
    }
}

如您所见,代码只查看 newState 参数,完全忽略 status 参数。但是,在许多情况下,这段代码可能会正常工作很久,而且并不完全是错误的!当您连接上后,接下来要做的事情实际上是调用 discoverServices。如果断开连接,确实需要调用 close()来释放 Android 堆栈中的资源。实际上,这对于在 Android 上使 BLE 工作非常重要,所以让我们立即讨论它!

当调用 connectGatt 时,堆栈内部会注册一个新的"客户端接口"(clientIf)。您可能已经注意到 logcat 中有这样一行:

yaml 复制代码
D/BluetoothGatt: connect() - device: B0:49:5F:01:20:XX, auto: false
D/BluetoothGatt: registerApp()
D/BluetoothGatt: registerApp() --- UUID=0e47c0cf-ef13--4afb-9f54--8cf3e9e808d5
D/BluetoothGatt: onClientRegistered() --- status=0 clientIf=6

它显示 client "6"是在我调用 connectGatt 之后注册的。Android 有一个30个客户端的限制(由栈的源代码中的 GATT_MAX_APPS 常量定义) ,如果你到达它将不再连接到设备,你会得到一个错误!奇怪的是,在启动你的手机之后,你的手机就已经在5或6上运行了,所以我猜 Android 自己就使用了第一个。因此,如果您从未调用 close () ,那么每次调用 connectGatt 时都会看到这个数字上升。当您调用 close ()时,堆栈将取消注册您的回调,在内部释放客户端,您将看到数字再次下降。

css 复制代码
D/BluetoothGatt: close()
D/BluetoothGatt: unregisterApp() --- mClientIf=6

因此请记住,无论您做什么,您总是在断开连接后调用 close ()

连接状态

newState 变量包含新的连接状态,可以有4个潜在值:

  • STATE_CONNECTED
  • STATE_DISCONNECTED
  • STATE_CONNECTING
  • STATE_DISCONNECTING

我想这些状态已经说明了一切。虽然根据文档,STATE_CONNECTINGSTATE_DISCONNECTING 是可能的,但我从未在实践中看到过它们。所以,如果你不处理他们,你可能没事。但是为了确保我建议您明确地处理它们,并且只有在真正断开连接时才调用 close ()。在我的应用程序中,我通常明确地处理状态,但我不做任何事情。

状态属性

在我给出的例子中,status 字段被完全忽略,但是它实际上非常重要。该字段本质上是一个错误代码。如果您接收到 GATT_SUCCESS,这意味着是个成功操作的状态。

如果您收到的值不是 GATT_SUCCESS, 有些地方出错了,status 会告诉你原因。不幸的是,BluetoothGatt 对象公开的错误代码非常少。可以参考

在实践中遇到的最常见的是状态代码133,即 GATT_ERROR。那只是意味着"出了问题"... ... 没什么帮助。更多关于 GATT_ERROR稍后介绍。

现在我们知道了 newStatestatus 字段是什么,我们可以完善下 onConnectionStateChange 回调。

arduino 复制代码
public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
if(status == GATT_SUCCESS) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        // We successfully connected, proceed with service discovery
        gatt.discoverServices();
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        // We successfully disconnected on our own request
        gatt.close();
    } else {
        // We're CONNECTING or DISCONNECTING, ignore for now    
    }
} else {
   // An error happened...figure out what happened!
   ...
   gatt.close();
}

这不是我们的最终实现,我们将在本文中进一步扩展它。但至少我们现在已经从成功案例中挑出了错误案例

绑定状态 bond state

还有最后一个参数需要考虑进 onConnectionStateChange回调, 绑定状态 bond state, 它不是作为参数传递的,所以我们需要这样获取它:

ini 复制代码
int bondstate = device.getBondState();

bond state 的值 可以为,BOND_NONEBOND_BONDINGBOND_BONDED,这些状态将影响你如何处理连接成功后的操作。

  • BOND_NONE: 你可以调用 discoverServices()
  • BOND_BONDING:绑定正在进行中, 因为蓝牙栈正忙, 不能调用 discoverServices(), 调用的话可能会引起 连接断开或者 discoverServices() 失败。 等绑定完成后,你才可以调用 discoverServices()
  • BOND_BONDED:如果您使用的是 Android 8或更高版本,您可以立即调用 discoveryServices () ,但如果不是,您可能需要添加一个延迟。在 Android 7或更低版本上,如果您的设备具有 服务更改特征 ,Android 堆栈仍然忙于处理它,如果没有延迟地调用 discoveryServices() 将使其失败。所以你必须增加1000-1500毫秒的延迟。所需的确切延迟时间取决于设备的特性数量。因为在这一点上你还不知道设备是否有服务改变的特点,它是建议简单地总是延迟。

因此,除了连接状态和状态属性外,还必须考虑到绑定状态。我是这么做的:

ini 复制代码
if (status == GATT_SUCCESS) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        int bondstate = device.getBondState();
        // Take action depending on the bond state
        if(bondstate == BOND_NONE || bondstate == BOND_BONDED) {

            // Connected to device, now proceed to discover it's services but delay a bit if needed
            int delayWhenBonded = 0;
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
                delayWhenBonded = 1000;
            }
            final int delay = bondstate == BOND_BONDED ? delayWhenBonded : 0;
            discoverServicesRunnable = new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, String.format(Locale.ENGLISH, "discovering services of '%s' with delay of %d ms", getName(), delay));
                    boolean result = gatt.discoverServices();
                    if (!result) {
                        Log.e(TAG, "discoverServices failed to start");
                    }
                    discoverServicesRunnable = null;
                }
            };
            bleHandler.postDelayed(discoverServicesRunnable, delay);
        } else if (bondstate == BOND_BONDING) {
            // Bonding process in progress, let it complete
            Log.i(TAG, "waiting for bonding to complete");
        }
....

处理异常

现在我们处理了成功的操作,我们需要查看错误。有许多情况实际上是非常正常的,它们会表现为"错误":

  • BLE设备故意断开连接 status 19 (GATT_CONN_TERMINATE_PEER_USER).
  • 连接超时,设备自己断开连接 status 8 (GATT_CONN_TIMEOUT)
  • 通信中出现低级错误,导致连接丢失。 通常您会收到一个状态133(GATT_ERROR)或一个更具体的错误代码
  • 堆栈一开始就没有成功连接 在这种情况下,您还将收到一个状态133(GATT_ERROR)
  • 在服务发现或连接期间,连接丢失 在这种情况下,您需要调查为什么会发生这种情况,并且可能需要重试连接。

前两种情况完全正常,除了调用 close ()以及可能进行一些内部清理(如处理 BluetoothGatt 对象)之外,没有其他事情可做。

在其他情况下,您可能需要通知应用程序的其他部分或在 UI 中显示某些内容、

连接的过程中出现 status 133

在Android 尝试连接设备时,常常会看到状态133。133状态可能有很多原因,其中一些是你可以控制的:

  • 确保在断开连接时始终调用 close()。如果你不这样做,下次你肯定会得到133 错误码。
  • 请确保在调用 connectGatt() 时始终使用 TRANSPORT_LE
  • 重新启动您的手机,如果你在开发的过程中遇到这个问题。您可能已经通过调试损坏了堆栈,并且堆栈处于不再正常运行的状态。重新启动你的手机可能会解决问题。
  • 确保BLE设备是否处于活跃状态。autoconnect 设置为 false 的 connectGatt 在30秒后超时,您将收到一个133。
  • 更换BLE设备的电池。设备通常在电池电量很低时开始运行不正常。

如果您已经尝试了以上所有方法,但仍然看到 status 133,您只需要 重试连接 !这是我一直没能理解或找到解决办法的 Android 错误之一。由于某些原因,有时在连接到设备时会得到133,但是如果您调用 close ()并重试它,就会没有问题!我怀疑是 Android 缓存出了问题导致了这一切,close ()调用使它回到了正确的状态。但我真的只是猜测... 如果有人知道如何解决这个问题,请告诉我!

断开连接

如果你想断开连接,你需要

  • disconnect()
  • 等待 onConnectionStateChange 回调
  • close()
  • 释放掉 gatt 对象

disconnect() 命令将实际执行断开连接,并且还将更新 蓝牙 堆栈的内部连接状态。然后它将触发对 onConnectionStateChange 的回调,通知您新的状态现在"断开连接"。

close() 调用将注销 BluetoothGattCallback 并释放我们已经讨论过的"客户端接口"。

最后,处理 BluetoothGatt 对象将释放与连接相关的其他资源。

错误的断开方式

如果你看看在互联网上可以找到的例子,你会发现有些人断开连接的方式有点不同。

有时你看到这个:

  • disconnect()
  • 紧接着调用 close()

这将"或多或少"奏效。设备将断开连接,但您可能永远不会收到"断开连接"状态的回调。这是因为 disconnect ()异步 的,close() 立即注销回调!所以当 Android 准备触发回调时,它不能调用回调,因为你已经注销了它!

有时候开发者不调用 disconnect() ,而只调用 close()。这将最终断开设备的连接,但这不是正确的方式,因为 disconnect()是 Android 堆栈实际上在内部更新状态的地方。它不仅断开活动连接,还将取消挂起的自动连接。因此,如果您只调用 close() ,任何挂起的自动连接仍然可能导致一个新的连接!

取消连接尝试

如果调用了 connectGatt() 并想取消连接尝试,则需要调用 disconnect()。因为您还没有连接,所以在 onConnectionStateChange 上不会回调!因此,等待几毫秒的 disconnect() 完成,然后调用 close () 释放资源。

当您成功地取消一个连接时,您将在日志中看到以下内容:

ruby 复制代码
D/BluetoothGatt: cancelOpen() --- device: CF:A9:BA:D9:62:9E

如果自动连接设置为 false,则可能永远不会取消连接。但是,对于将 autoconnect 设置为 true 的连接,这样做是非常常见的。例如,当您的应用程序在前台时,您通常希望连接到设备,但是当您的应用程序到后台时,您可能希望停止连接,从而取消发起的连接。

发现服务

连接到设备之后,必须通过调用 discoveryServices() 来发现它的服务。 这将导致蓝牙栈发出一系列底层命令来检索 services(服务 ), characteristics(特征)descriptors(描述符) 。这可能需要一点时间,通常是一整秒左右,这取决于您的设备具有多少服务、特征和描述符。当 Android 完成后,它将调用 onServicesDiscovered 回调。

发现服务后,您必须检查的第一件事是查看是否存在错误:

scss 复制代码
// Check if the service discovery succeeded. If not disconnect
if (status == GATT_INTERNAL_ERROR) {
    Log.e(TAG, "Service discovery failed");
    disconnect();
    return;
}

如果出现错误(通常是 GATT_INTERNAL_ERROR,值为129) ,您必须断开连接,因为您将无法执行有意义的操作。不能打开通知或读/写。所以只要断开连接并重试连接即可。

如果一切顺利,您可以继续获取服务列表,并执行自己的操作:

less 复制代码
final List<BluetoothGattService> services = gatt.getServices();
Log.i(TAG, String.format(Locale.ENGLISH,"discovered %d services for '%s'", services.size(), getName()));
// Do additional processing of the services
...

缓存服务

Android 栈会缓存它找到的服务、特性和描述符。因此,第一个连接将触发一个真正的服务发现和随后的连接,将返回一个缓存版本。这符合蓝牙标准。这通常是完全没问题的,并加快了从设备接收数据所需的时间。

但是,在某些情况下,您可能希望清除服务缓存,以便再次发现服务。这种情况的典型用例是固件更新, 它将更改设备所具有的服务或特性的场景。有一个隐藏的方法是通过反射来清楚服务缓存。

ini 复制代码
private boolean clearServicesCache()
{
    boolean result = false;
    try {
        Method refreshMethod = bluetoothGatt.getClass().getMethod("refresh");
        if(refreshMethod != null) {
            result = (boolean) refreshMethod.invoke(bluetoothGatt);
        }
    } catch (Exception e) {
        HBLogger.e(TAG, "ERROR: Could not invoke refresh method");
    }
    return result;
}

这个方法是异步的,所以给它一些时间来执行。

需要注意的点

虽然连接和断开连接听起来很简单,但是有一些奇怪的地方你应该注意。

  • 在尝试连接时,您可能会偶尔看到错误133。本文在前面已经解释过这一点, 在开发的过程中要尽量避免它。
  • 有时连接调用只是挂起,没有发生超时onConnectionStateChange 回调也从未调用过。这种情况并不经常发生,但我见过它发生时,外围设备的电池快要没电了,或者你超出了设备的操作范围(该范围没有了信号)。我的猜测是,有些通信发生了,但是随后中止,堆栈挂起。我的解决办法是启动您自己的连接计时器,并在超时后断开/关闭。当堆栈挂起时,它基本上是一个保护措施。
  • 有些Android设备似乎在扫描时连接有问题。据报道,华为 P8 Lite 手机就是出现这种问题的手机之一。所以请确保在连接之前停止扫描。
  • 所有与连接和断开连接相关的调用都是 异步 的。蓝牙栈执行它们需要一些时间。要避免在极短的时间做出大量BLE的相关操作。
相关推荐
小何开发6 分钟前
Android Studio 安装教程
android·ide·android studio
开发者阿伟23 分钟前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_4381509935 分钟前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu1 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd
木头没有瓜2 小时前
ruoyi 请求参数类型不匹配,参数[giftId]要求类型为:‘java.lang.Long‘,但输入值为:‘orderGiftUnionList
android·java·okhttp
键盘侠0072 小时前
springboot 上传图片 转存成webp
android·spring boot·okhttp
江上清风山间明月2 小时前
flutter bottomSheet 控件详解
android·flutter·底部导航·bottomsheet
Crossoads4 小时前
【汇编语言】外中断(一)—— 外中断的魔法:PC机键盘如何触发计算机响应
android·开发语言·数据库·深度学习·机器学习·计算机外设·汇编语言
sunphp开发者5 小时前
黑客攻击网站,篡改首页问题排查修复
android·js
我又来搬代码了5 小时前
【Android Studio】创建新项目遇到的一些问题
android·ide·android studio