Android BLE开发入门(4) —— 绑定

在之前的文章中,我讨论了BLE数据的读写操作 以及如何实现一个用于执行这些命令的队列。在本文中,我们将讨论的主题是 绑定

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

绑定

有些设备需要进行"绑定"才能正常工作。从技术上讲,绑定意味着生成、交换和存储加密密钥,以使通信更加安全。当进行绑定过程时,Android可能会要求用户 同意输入PIN码密码短语。在后续的连接中,Android知道设备已经绑定,只会进行一些"底层"存储密钥的交换。绑定的重要优势在于连接是加密的,因此更加安全。

绑定(bonding)的内容在Google的文档中几乎没有详细记录,因此不清楚应用程序应该如何处理。其中一个你可能注意到的是 createBond 方法。在iOS中根本没有这样的方法,操作系统会自动处理所有绑定过程!那么为什么需要调用 createBond() 呢?我觉得这很奇怪,因为这意味着你事先就知道哪些设备需要进行绑定,哪些不需要。蓝牙并不是设计成这样的,外围设备通常会明确说明它们需要进行绑定。因此,我开始深入研究并进行了一些实验。花了一些时间,但最终发现其实很简单。

以下是处理绑定的原则:

  • 让 Android 处理绑定 当设备指示需要绑定或尝试读取或写入加密的特征时,Android会自动为您执行绑定操作。在大多数情况下,您无需自己调用createBond()方法。

  • 在绑定过程进行中,请不要进行任何操作 如果在绑定过程进行中执行发现服务、读写等操作,可能会导致错误并且连接可能会中断。请让Android自行处理。

  • 在绑定完成后,请继续执行您之前的操作队列 一旦绑定完成,您需要继续进行之前的操作。

  • 如果您知道自己在做什么,并且确实有必要 ,您可以调用 createBond() 来自行启动与设备的绑定过程。但这应该是个例外情况。

是什么触发了绑定过程?

有三种常见的触发绑定过程的方式:

  1. 在与外设 建立连接 时,外设可以发出信号,表示在进行任何其他操作之前需要进行绑定。
  2. 一个特征值可以被加密以进行读取或写入操作 。当你尝试读取或写入时,它会触发绑定操作。如果绑定成功,读取或写入操作也会成功。如果绑定失败,读取或写入操作也会失败,并且会出现 INSUFFICIENT_AUTHENTICATION 错误。这与iOS上的错误相同。
  3. 你可以通过调用 createBond() 方法来 触发绑定过程。如果你的设备需要这个过程,那么它可能不兼容iOS,因为iOS没有相应的调用方法。但在蓝牙标准中是可能的!

让我们详细讨论这几种情况:

连接时进行绑定

如果外围设备在连接建立后需要直接进行配对,当调用 onConnectionStateChange 时,配对状态将为 BOND_BONDING 。这意味着配对正在进行中,你不应该做任何操作,比如调用 discoverServices() ,直到配对完成!如果你这样做,可能会出现意外断开连接或服务发现失败的情况。因此,你需要在 onConnectionStateChanged 中专门处理这种情况,像这样:

scss 复制代码
// Take action depending on the bond state
if(bondstate == BOND_NONE || bondstate == BOND_BONDED) {
    // Connected to device, now proceed to discover it's services
    ... 
} else if (bondstate == BOND_BONDING) {
    // Bonding process has already started let it complete
    Log.i(TAG, "waiting for bonding to complete");
}

为了跟踪配对过程的进展,你需要在调用 connectGatt 之前为 ACTION_BOND_STATE_CHANGED 注册一个 BroadcastReceiver ,并在配对过程中检查进度。这个回调将在配对状态发生变化时被多次调用。

java 复制代码
context.registerReceiver(bondStateReceiver, new IntentFilter(ACTION_BOND_STATE_CHANGED));

private final BroadcastReceiver bondStateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

        // Ignore updates for other devices
        if (bluetoothGatt == null || !device.getAddress().equals(bluetoothGatt.getDevice().getAddress()))
            return;

        // Check if action is valid
        if(action == null) return;

        // Take action depending on new bond state
        if (action.equals(ACTION_BOND_STATE_CHANGED)) {
            final int bondState = intent.getIntExtra(EXTRA_BOND_STATE, ERROR);
            final int previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);

            switch (bondState) {
                case BOND_BONDING:
                    // Bonding started
                    ...
                    break;
                case BOND_BONDED:
                    // Bonding succeeded
                    ...
                    break;
                case BOND_NONE:
                    // Oh oh
                    ...
                    break;
            }
        }
    }
};

一旦配对成功,你可以开始发现服务的操作,因为这之前还没有发生。你可以通过检查是否已经存在服务来进行双重确认:

typescript 复制代码
case BOND_BONDED:
    // Bonding succeeded
    Log.d(TAG, "bonded");

    // Check if there are services
    if(bluetoothGatt.getServices().isEmpty()) {
        // No services discovered yet
        bleHandler.post(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, String.format("discovering services of '%s'", getName()));
                boolean result = bluetoothGatt.discoverServices();
                if (!result) {
                    Log.e(TAG, "discoverServices failed to start");
                }
            }
        });
    }

当读写加密特征值时进行绑定

当读写操作触发绑定时,操作将首先失败,并且设备将返回 GATT_INSUFFICIENT_AUTHENTICATION 的错误。在 Android 6 和 7 上,你实际上会在 onCharacteristicReadonCharacteristicWrite 中收到此错误,但是绑定过程已经在 Android 内部启动。从 Android 8 开始,你将不会看到此错误,并且 Android 将在绑定完成后自动重试操作。然而,在 Android 6 和 7 上,你需要自己捕获错误并在配对完成后重试操作。因此,你需要在绑定完成后捕获错误并重试操作。

所以当你收到错误时,你不需要做任何操作,也不要立即完成指令。

arduino 复制代码
public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, int status) {
    // Perform some checks on the status field
    if (status != GATT_SUCCESS) {
        if (status == GATT_INSUFFICIENT_AUTHENTICATION ) {
            // Characteristic encrypted and needs bonding,
            // So retry operation after bonding completes
            // This only happens on Android 5/6/7
            Log.w(TAG, "read needs bonding, bonding in progress");
            return;
        } else {
            Log.e(TAG, String.format(Locale.ENGLISH,"ERROR: Read failed for characteristic: %s, status %d", characteristic.getUuid(), status));
            completedCommand();
            return;
        }
    }
...

一旦绑定完成,你需要检查队列是否仍然处于"繁忙"状态,这意味着某个命令尚未完成。然后你可以重试该操作。

scss 复制代码
case BOND_BONDED:
    // Bonding succeeded
    Log.d(TAG, "bonded");

    // Check if there are services
    ...
    // If bonding was triggered by a read/write, we must retry it
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
        if (commandQueueBusy && !manuallyBonding) {
            bleHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "retrying command after bonding");
                    retryCommand();
                }
            }, 50);
        }
    }

主动调用 createBond

虽然我之前告诉过你不要自己调用 createBond 方法,但你有可能会调用。然而,请确认是否真的需要这样做。在iOS上没有 createBond 的等效方法,所以如果调用 createBond 是唯一的配对方式,那么你的设备可能不兼容iOS。iOS的硬件设计指南明确表示不支持此功能。我尝试过几十个蓝牙低功耗设备,只有在特殊情况下才需要自己调用 createBond 方法。

如果你主动调用了 createBond 方法,仍然不能同时进行其他操作,并且需要注册广播来监听配对过程。在这个意义上,它与其他情况下的使用方式相同。请注意,如果你在已经配对的设备上调用 createBond 方法,会收到错误提示,因此在调用之前请进行检查。

还有一个原因你可能仍然想自己调用 createBond 方法,那就是为了更方便地进行重新连接。如果你想通过MAC地址获取一个BluetoothDevice对象,只有当你的设备被缓存或配对时才能成功!因此,你可能希望利用这个事实进行配对,即使它并不是必需的。这样一来,你就不需要再次扫描设备...可能会派上用场!

移除绑定

作为用户,你可以在蓝牙设置菜单中看到已配对设备的列表。在那里,你可以移除设备,从而解除配对关系。

完全处理解除配对需要一些时间。

奇怪的是,没有官方提供的API来解除绑定。虽然有一个隐藏的方法 removeBond,但你必须使用反射来调用它。以下是如何实现的:

java 复制代码
try {
    Method method = device.getClass().getMethod("removeBond", (Class[]) null);
    result = (boolean) method.invoke(device, (Object[]) null);
    if (result) {
        Log.i(TAG, "Successfully removed bond");
    }
    return result;
} catch (Exception e) {
    Log.e(TAG, "ERROR: could not remove bond");
    e.printStackTrace();
    return false;
}

失去绑定

除了移除绑定之外,你还可能会失去绑定!大多数设备只支持与一个手机进行配对。正常的使用情况如下:

  • 手机A与设备X进行配对
  • 手机B与设备X进行配对
  • 手机A重新连接到设备X,此时配对关系丢失了

因此,当手机A再次连接时,初始时会显示配对状态为 BOND_BONDED ,但在广播接收器中会立即接收到新的状态 BOND_NONE。通过比较先前的配对状态和新的状态,您可以确定是否刚刚失去了一个配对关系。

java 复制代码
case BOND_NONE:
    if(previousBondState == BOND_BONDING) {
       // Bonding failed
       ...
    } else {
       // Bond lost
       ...
    }
    disconnect();
    break;

当您失去一个配对关系时,必须断开连接,否则会发生奇怪的事情,通信将无法正常工作。当您重新连接时,Android会尝试重新建立配对关系。同样适用于配对失败的情况。

有一个小问题/错误您需要知道。如果您失去一个绑定关系,似乎需要 1秒钟 才能完全更新蓝牙堆栈的内部管理。因此,如果您失去一个绑定关系,立即断开连接并重新连接,Android会告诉您设备仍然处于配对状态,但它不会完全工作...所以在断开连接和重新连接之前请等待1秒钟。

总结

所以,这就结束了关于 Android BLE开发 的系列文章!我希望这些信息对你有用,能让你更愉快地使用BLE。我发现,更好地理解Android上的BLE之后,你可以使其工作得相当好。加油!

相关推荐
雨白7 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
星源~7 小时前
tree 命令集成到 Git Bash:可视化目录结构的指南
git·单片机·物联网·嵌入式·项目开发
漫步企鹅7 小时前
【蓝牙】Linux Qt4查看已经配对的蓝牙信息
linux·qt·蓝牙·配对
kk爱闹8 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空10 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭10 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日11 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安11 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑11 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟16 小时前
CTF Web的数组巧用
android