使用低功耗蓝牙在iOS和Android之间进行通信

47
我有一个使用CoreBluetooth在iPad(中心)和iPhone(周边设备)之间通信的应用程序。我有一个服务具有两个特征。我有一台运行最新Android 4.3并支持BTLE的Nexus 7。Android有点晚加入BTLE行列, 但是他们似乎采取了与iOS类似的方法,最初只支持作为中心,周边模式则在以后的版本中实现。我可以加载示例Android BTLE应用程序并浏览附近的周边设备。当我的iPhone作为外围设备广告时,在Android侧的附近周边设备列表中可以看到CBAdvertisementDataLocalNameKey的值。我可以连接到iPhone,当连接成功时,蓝牙标志从浅灰色变成黑色。连接总是持续确切的10秒钟,然后断开连接。在Android侧,连接后应立即看到可用服务和特征列表。我已经证明了Android代码设置正确,因为我可以将其连接到我拥有的TI CC2541DK-SENSOR硬件上,并在连接时列出所有服务和特征。
我花了几天时间进行故障排除,但没有成功。问题是我无法确定哪个设备遇到错误,因此导致断开连接。在连接阶段或服务发现阶段,CBPeripheralManagerDelegate没有回调,因此我不知道出现错误的具体时间(如果错误出现在iOS侧)。在Android侧,调用一个方法来启动服务发现,但是他们的回调“onServicesDiscovered”没有被调用,这很困惑。是否有任何方法可以深入探究iOS侧的BTLE通信细节,以查看正在发生什么并确定发生了哪个错误?

你使用的是最新的iOS系统,对吧? - www.jensolsson.se
1
你应该购买一个BLE USB适配器,并使用TI的数据包嗅探解决方案:http://www.ti.com/tool/packet-sniffer。你提到的内容听起来很奇怪。另外,试试这个应用程序:https://itunes.apple.com/tr/app/ble-utility/id606210918?mt=8它可以轻松地在i*设备上模拟服务。如果成功了,那么问题很可能在你的代码中。如果失败了,那么需要进行更深入的检查。 - allprog
1
@allprog 我下载了BLE Utility应用程序和LightBlue,这两个应用程序与我的自定义应用程序遇到了相同的结果。我发现另一个线程链接在这里:https://code.google.com/p/android/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars&groupby=&sort=&id=58725 看起来这可能是Android端的低级实现错误,但尚未得到确认。 - afrederick
很有可能。他们刚刚推出并从零开始。肯定会有问题。可悲的是,谷歌似乎更或多或少地忽略了BLE。他们甚至没有花太多时间将API Java化。请保持此帖子更新。我认为你很快就会提交答案。 :) - allprog
2
我遇到了相同的问题,Nexus 4在4.3上使用LightBlue连接作为外设的iPhone 5。可以连接,但设备发现从未完成并且会断开连接。将Nexus 4更新到4.4(构建号KRT16S),连接和发现服务正常工作。还能够读写特性。 - vee
显示剩余3条评论
6个回答

30

我已经历了至少一周的相同问题。我已经在这里提出了问题,并且已经自己回答了。主要问题是安卓的一个BUG问题。它在固定的L2CAP通道上发送了一个不允许的命令。

但是当安卓与普通外围BLE设备通信时,它运行得非常好。实际上,BLE示例非常有效。问题出现在与iOS设备通信时:连接建立后,它们开始协商连接参数(这个阶段不会发生在普通的BLE外围设备中),这就是问题出现的时候。安卓向iOS发送错误的命令,iOS断开连接。基本上就是这样。

一些问题已经报告给谷歌,其中一个已经被接受,我希望他们很快开始解决这个问题。

不幸的是,你能做的就是等待下一个安卓版本发布。无论如何,我强烈建议您查看我的问题报告及所有测试文档,如果您想对这个问题有所了解。

这是链接:https://code.google.com/p/android/issues/detail?id=58725


1
感谢解释。你的问题也被原帖作者发现了。希望这个问题能在下一个版本中得到解决。 - allprog
4
大家好,这个问题有任何进展吗?安卓团队在处理这个问题吗?我在他们的问题跟踪中没有看到任何更新... - andresmafra
1
它在2014年12月7日被Google标记为过时。然而,人们报告称它仍然是一个问题。 - ThomasW

20

我已经写了一个相对简单的工作示例,并在Github上以开源形式包含:https://github.com/GitGarage。目前,这个示例只经过了Android Nexus 9和iPhone 5s的测试,但我认为它也适用于Nexus 6和各种型号的iPhone。目前,它被明确设置为在一个Android和一个iPhone之间通信,但我认为它可以进行更多的调整。

以下是关键方法...

安卓端 - 发送到iOS:

private void sendMessage() {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            if (mBTAdapter == null) {
                return;
            }
            if (mBTAdvertiser == null) {
                mBTAdvertiser = mBTAdapter.getBluetoothLeAdvertiser();
            }
               // get the full message from the UI
            String textMessage = mEditText.getText().toString(); 
            if (textMessage.length() > 0)
            {
                   // add 'Android' as the user name
                String message = "Android: " + textMessage; 

                while (message.length() > 0) {
                    String subMessage;
                    if(message.length() > 8)
                    {    // add dash to unfinished messages
                        subMessage = message.substring(0,8) + "-"; 
                        message = message.substring(8);
                        for (int i = 0; i < 20; i++) // twenty times (better safe than sorry) send this part of the message. duplicate parts will be ignored
                        {
                            AdvertiseData ad = BleUtil.makeAdvertiseData(subMessage);
                            mBTAdvertiser.startAdvertising(BleUtil.createAdvSettings(true, 100), ad, mAdvCallback);
                            mBTAdvertiser.stopAdvertising(mAdvCallback);
                        }
                    }
                    else
                    {  // otherwise, send the last part
                        subMessage = message;
                        message = "";
                        for (int i = 0; i < 5; i++)
                        {
                            AdvertiseData ad = BleUtil.makeAdvertiseData(subMessage);
                            mBTAdvertiser.startAdvertising(
                                    BleUtil.createAdvSettings(true, 40), ad,
                                    mAdvCallback);
                            mBTAdvertiser.stopAdvertising(mAdvCallback);
                        }
                    }
                }
                threadHandler.post(updateRunnable);
            }
        }
    });
    thread.start();
}

DROID SIDE - 从iOS接收:

@Override
public void onLeScan(final BluetoothDevice newDevice, final int newRssi,
                     final byte[] newScanRecord) {

    int startByte = 0;
    String hex = asHex(newScanRecord).substring(0,29);
       // check five times, startByte was used for something else before
    while (startByte <= 5) {
       // check if this is a repeat message
        if (!Arrays.asList(used).contains(hex)) {
            used[ui] = hex;

            String message = new String(newScanRecord);
            String firstChar = message.substring(5, 6);
            Pattern pattern = Pattern.compile("[ a-zA-Z0-9~!@#$%^&*()_+{}|:\"<>?`\\-=;',\\./\\[\\]\\\\]", Pattern.DOTALL);
               // if the message is comprised of standard characters...
            Matcher matcher = pattern.matcher(firstChar);
            if (firstChar.equals("L"))
            {
                firstChar = message.substring(6, 7);
                pattern = Pattern.compile("[ a-zA-Z0-9~!@#$%^&*()_+{}|:\"<>?`\\-=;',\\./\\[\\]\\\\]", Pattern.DOTALL);
                matcher = pattern.matcher(firstChar);
            }

            if(matcher.matches())
            {
                TextView textViewToChange = (TextView) findViewById(R.id.textView);
                String oldText = textViewToChange.getText().toString();
                int len = 0;
                String subMessage = "";
                   // add this portion to our final message
                while (matcher.matches())  
                {
                    subMessage = message.substring(5, 6+len);
                    matcher = pattern.matcher(message.substring(5+len, 6+len));
                    len++;
                }
                subMessage = subMessage.substring(0,subMessage.length()-1);

                Log.e("Address",newDevice.getAddress());
                Log.e("Data",asHex(newScanRecord));
                boolean enter = subMessage.length() == 16;
                enter = enter && !subMessage.substring(15).equals("-");
                enter = enter || subMessage.length() < 16;
                textViewToChange.setText(oldText + subMessage.substring(0, subMessage.length() - 1) + (enter ? "\n" : ""));
                ui = ui == 2 ? -1 : ui;
                ui++;

                Log.e("String", subMessage);
            }
            break;
        }
        startByte++;
    }
}

iOS侧 - 发送到Android:

func startAdvertisingToPeripheral() {
    var allTime:UInt64 = 0;
    if (dataToSend != nil)
    {
        datastring = NSString(data:dataToSend, encoding:NSUTF8StringEncoding) as String
        datastring = "iPhone: " + datastring
        if (datastring.length > 15)
        {
            for (var i:Double = 0; i < Double(datastring.length)/15.000; i++)
            {
                let delay = i/10.000 * Double(NSEC_PER_SEC)
                let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
                allTime = time
                dispatch_after(time, dispatch_get_main_queue(), { () -> Void in self.sendPart() });
            }
        }
        else
        {
            var messageUUID = StringToUUID(datastring)
            if !peripheralManager.isAdvertising {
                peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [CBUUID(string: messageUUID)]])
            }
        }
    }
}

iOS端-从Android接收:

func centralManager(central: CBCentralManager!, didDiscoverPeripheral peripheral: CBPeripheral!, advertisementData: [NSObject : AnyObject]!, RSSI: NSNumber!) {

    delegate?.didDiscoverPeripheral(peripheral)
    var splitUp = split("\(advertisementData)") {$0 == "\n"}
    if (splitUp.count > 1)
    {
        var chop = splitUp[1]
        chop = chop[0...chop.length-2]
        var chopSplit = split("\(chop)") {$0 == "\""}

        if !(chopSplit.count > 1 && chopSplit[1] == "Device Information")
        {
            var hexString = chop[4...7] + chop[12...19] + chop[21...26]
            var datas = hexString.dataFromHexadecimalString()
            var string = NSString(data: datas!, encoding: NSUTF8StringEncoding) as String
            if (!contains(usedList,string))
            {
                usedList.append(string)
                if (string.length == 9 && string[string.length-1...string.length-1] == "-")
                {
                    finalString = finalString + string[0...string.length-2]
                }
                else
                {
                    lastString = finalString + string + "\n"
                    println(lastString)
                    finalString = ""
                    usedList = newList
                    usedList.append(string)
                }
            }
        }
    }
}

刚试着编译了一下,但是BLEMingle.swift报了17个错误...你能检查一下你的git状态吗?非常感谢你的努力。即使它不能正常工作,也是巨大的帮助。 - domenukk
你尝试过清理并重新构建所有内容吗?或者在不同的账户上检查一下?也许它甚至没有重新编译这个文件,因为你什么都没改? - domenukk
1
好的,太棒了。我认为难点已经解决了,对于发送消息到Android,我考虑发送相同的消息部分多次,然后在接收端它只需保留最近的二十个左右的消息部分,并取消任何重复的消息。进展! - omikes
我尝试了这段代码,但它并不起作用,因为从内部来说,存在一个 Android 的 bug。关于编译,使用 XCode 7 无法工作和编译,但在 Android 上可以编译。我已经用 Objective-C 编写了 iOS 代码以使其可编译,但它仍然不起作用。 - AlfuryDB
3
我已经将其重新运作,并升级到最新版本的iOS和Android。试试看吧。 - omikes
显示剩余6条评论

6
作为我们在跨平台BLE主题上的研究和开发的一部分,我想添加一些信息到这个帖子中。
使用小米Mi A1(操作系统版本Oreo, Android 8.0),外围模式可以正常工作。
以下是我们在iPhone 8和小米Mi A1上进行研究时发现的吞吐量观察结果,但仍需要与最新的三星S8使用的其他自定义Android操作系统进行成熟度测试。下面的数据基于write_with_response。
1. iPhone 8(BLE 5.0)作为中央设备,Linux桌面(Ubuntu 16.04 with BLE dongle 4.0):MTU=2048,吞吐量-每秒2.5千字节。 2. iPhone 8(BLE 5.0)作为中央设备,并搭配具有BLE 4.2版本的安卓操作系统作为外围设备(小米Mi A1):MTU=180,吞吐量-每秒2.5千字节。 3. iPhone 8(BLE 5.0)作为中央设备,并搭配具有BLE 4.2版本的iPhone 7 plus作为外围设备 :MTU=512,吞吐量-每秒7.1千字节。 4. iPhone 8(BLE 5.0)作为中央设备,并搭配具有BLE 5.0版本的三星S8作为外围设备:三星S8无法作为外围设备工作。 5. iPhone 8(BLE 5.0)作为中央设备,并搭配具有BLE 5.0版本的iPhone 8 plus作为外围设备:MTU=512,吞吐量-每秒15.5千字节。

这是非常有用的信息。你有任何想法为什么它可能会在S8上失败吗? - cutsoy
我认为这可能是由于三星安卓定制操作系统版本的原因。但我找不到任何官方文档来证实。 - Sudhin Philip

1
我正在使用一个Android中心和一个iOS外设进行类似的操作。我发现,如果没有任何东西订阅外设的服务,它们会断开连接。
不要忘记在订阅时更新描述符,否则它实际上不会执行任何操作(即不会在iOS端调用委托方法)。
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
    if (mBluetoothAdapter == null || mBluetoothGatt == null) {
        Log.v(TAG, "BluetoothAdapter not initialized");
        return;
    }

    UUID uuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");    // UUID for client config desc
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(uuid);
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt.writeDescriptor(descriptor);

    mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
}

值得注意的是,我甚至无法在Android设备上看到iOS设备进行普通BLE扫描(startLeScan),但通过使用广播接收器启动BT Classic扫描解决了这个问题(startDiscovery)。

-1

我想分享一下我的经验,因为我曾经处理过这个问题,但由于谷歌没有提供支持,所以我放弃了。上述代码,我非常感谢,但它不起作用。你可以在合理的时间内编写一个iOS到iOS或Android到Android的蓝牙低功耗应用程序,但当你尝试在iOS和Android之间通信时,问题就出现了。有一个被充分记录的谷歌问题(https://code.google.com/p/android/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars&groupby=&sort=&id=58725),我参与了其中,但谷歌根本没有回应,而且似乎他们关闭了这个问题,Android M中也没有任何改变,因为我一直在查看代码,没有看到进一步的差异。问题出现在Android尝试连接时,特别是在“if else”语句中;这段代码基本上拒绝了传输并切断了通信,所以它不起作用。目前还没有解决方案。你可以使用WiFi直连解决方案,但这是一个限制,并且在执行此操作时会出现更多问题。如果你想使用外部硬件(树莓派、传感器等)实现BLE,那么这个问题就不存在了,但在iOS和Android之间不起作用。两个平台的技术基本相同,但在Android中没有很好地实现,或者是谷歌故意设置的陷阱,以防止打开在两个平台之间通信的大门。


-2

我们一直在尝试跨平台BLE连接(iOS<-> Android),并发现仍然存在许多不兼容和连接问题。

如果您的用例是以功能为驱动的,并且只需要基本的数据交换,我建议您查看可以为您实现跨平台通信的框架和库,而无需从头开始构建。

例如http://p2pkit.io或Google Nearby

免责声明:我在Uepaa工作,开发Android和iOS的p2pkit.io。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接