在上一篇文章中,我详细的介绍了BLE的扫描。 在这片文章中, 我们将探索 BLE的连接(connecting ), 断开连接(disconnecting ) 和 发现服务(discovering services)。
连接设备
当你通过扫描发现设备后, 你可以通过调用 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_CONNECTING 和 STATE_DISCONNECTING 是可能的,但我从未在实践中看到过它们。所以,如果你不处理他们,你可能没事。但是为了确保我建议您明确地处理它们,并且只有在真正断开连接时才调用 close ()
。在我的应用程序中,我通常明确地处理状态,但我不做任何事情。
状态属性
在我给出的例子中,status 字段被完全忽略,但是它实际上非常重要。该字段本质上是一个错误代码。如果您接收到 GATT_SUCCESS
,这意味着是个成功操作的状态。
如果您收到的值不是 GATT_SUCCESS
, 有些地方出错了,status 会告诉你原因。不幸的是,BluetoothGatt 对象公开的错误代码非常少。可以参考
在实践中遇到的最常见的是状态代码133,即 GATT_ERROR
。那只是意味着"出了问题"... ... 没什么帮助。更多关于 GATT_ERROR稍后介绍。
现在我们知道了 newState
和 status
字段是什么,我们可以完善下 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_NONE
, BOND_BONDING
, BOND_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的相关操作。