Communicating between iOS and Android with Bluetooth LE

42,506

Solution 1

I've already gone through this for at least one week having this same issue. I've already asked a question here and I've already answered on my own. The main problem is an Android BUG issue. It's sending a non permitted command on a fixed L2CAP channnel.

But when Android is communicating with normal peripheral BLE devices, it works pretty well. In fact, the BLE sample works like a charm. The problem is when is comunicating with an iOS device for example: Just after the connection is made, they start negotiating their connection parameters (this phase doesn't happen with normal BLE peripheral), and this is when the problem comes up. Android sends a bad command to iOS, iOS drops the connection. That's basically how it works

Some issues have been already reported to Google, and one of them have been already accepted and I hope they will start working on it soon.

Unfortunately, what you can do, is to wait until next Android release. Anyway, I highly suggest you to have a look at my issue report with all my test documents if you want to make some light on this problem.

Here's the link: https://code.google.com/p/android/issues/detail?id=58725

Solution 2

I've written a simple working example, well relatively simple, and included it open-source on Github: https://github.com/GitGarage. So far it has only been tested with an Android Nexus 9 and an iPhone 5s, but I presume it would also work with a Nexus 6 and various iPhone types. So far it is set up explicitly to communicate between one Android and one iPhone, but I presume it is tweakable to do much more.

Here are the key methods...

DROID SIDE - Sending to 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 - Receiving from 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 SIDE - Sending to 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 SIDE - Receiving from 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)
                }
            }
        }
    }
}

Solution 3

I would like to add few information to this thread as a part of our RnD on BLE topic between cross platform.

Peripheral mode is working without any issues with Xiomi Mi A1 (OS version Oreo, Android 8.0).

Here are few observation on throughput that we found during our RnD on iPhone 8 and Xiomi Mi A1 but it still has to get matured with other custom Android OS used in latest Samsung S8. The below data is based on write_with_response.

  1. iPhone 8 (BLE 5.0) as Central and Linux desktop (Ubuntu 16.04 with BLE dongle 4.0): MTU = 2048 : Throughput - 2.5 KiloBytes per sec.

  2. iPhone 8 (BLE 5.0) as Central and Android OS with BLE version 4.2 as Peripheral(Xiomi Mi A1): MTU = 180 : Throughput - 2.5 KiloBytes per sec.

  3. iPhone 8 (BLE 5.0) as Central and iPhone 7 plus (BLE 4.2) as Peripheral : MTU = 512 : Throughput - 7.1 KiloBytes per sec.

  4. iPhone 8 (BLE 5.0) as Central and Samsung S8 (BLE 5.0) as Peripheral : Samsung S8 failed to work as peripheral

  5. iPhone 8 (BLE 5.0) as Central and iPhone 8 plus (BLE 5.0) as Peripheral : MTU = 512 : Throughput - 15.5 KiloBytes per sec.

Solution 4

I am doing something similar with an Android central and an iOS peripheral. I found that they would disconnect if nothing subscribed to any of the peripheral's services.

Don't forget to update the descriptor when subscribing else it doesn't actually do anything (i.e. call the delegate method on the iOS side).

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);
}

It might also be of note that I couldn't even see the iOS device doing an normal BLE scan on the Android device (startLeScan), but starting a BT Classic scan with a broadcast receiver solved the problem (startDiscovery).

Share:
42,506

Related videos on Youtube

afrederick
Author by

afrederick

Updated on January 14, 2021

Comments

  • afrederick
    afrederick over 3 years

    I've got a working app using CoreBluetooth to communicate between an iPad (central) and iPhone (peripheral). I have one service that has two characteristics. I have a Nexus 7 running the latest Android 4.3 with BTLE support. Android is a bit late to jump on the BTLE bandwagon but it appears they are approaching it similarly to how iOS did, where initially they only support acting as a central with the peripheral mode coming in a later version. I can load the sample Android BTLE app and browse for nearby peripherals. With my iPhone advertising as a peripheral I can see the value from CBAdvertisementDataLocalNameKey in the list of nearby peripherals on the Android side. I can connect to the iPhone and the Bluetooth symbol turns from light gray to black when the connection is made. The connection always lasts exactly 10 seconds and then disconnects. On the Android side I'm supposed to see a list of available services and characteristics appear immediately upon connection. I've proved the Android code is setup correctly because I can connnect it to the TI CC2541DK-SENSOR hardware that I have and all services and characteristics are listed upon connecting to it.

    I've spent the last few days troubleshooting the issue with no success. The problem is I can't determine which device is experiencing an error and thus causing the disconnection. There are no callbacks from CBPeripheralManagerDelegate during the connection phase or service discovery phase so I have no idea at what point an error occurs (if the error is on the iOS side). On the Android side a method is called to initiate service discovery however their callback "onServicesDiscovered" is never called which is perplexing. Is there any way I can dig into the guts of the BTLE communication on the iOS side to see what's going on and determine what error is taking place?

    • www.jensolsson.se
      www.jensolsson.se almost 11 years
      You use the latest iOS, right?
    • afrederick
      afrederick almost 11 years
      @www.jensolsson.se Yes
    • allprog
      allprog almost 11 years
      You should buy a BLE dongle and use TI's packet sniffer solution: ti.com/tool/packet-sniffer What you mention sounds weird. Also, try this app: itunes.apple.com/tr/app/ble-utility/id606210918?mt=8 It makes it easy to simulate services on the i* device. If that succeeds, then the issue is most probably in your code. If that fails, then deeper inspection is required.
    • afrederick
      afrederick almost 11 years
      @allprog I downloaded the BLE Utility app as well as LightBlue, both apps experienced the same results as my custom app. I found another thread that links here: code.google.com/p/android/issues/… It appears as if this could be an low level implementation bug on the Android side, but it has yet to be confirmed.
    • allprog
      allprog almost 11 years
      That's very probable. They rolled it out just now and started from nothing. There surely are issues. Sadly Google seems to more or less ignore BLE. They didn't even spend much time Java-izing the API. Keep this post updated. I think you'll submit the answer soon. :)
    • afrederick
      afrederick almost 11 years
      @allprog Will do! It will help if people go and "star" that issue on the code.google.com site. Hopefully this gets resolved in the next revision of Android 4.3, I sure would love to get iOS and Android talking back and forth!
    • edoardotognoni
      edoardotognoni almost 11 years
      Hi everyone. I'm the developer which reported the isse that @afrederick linked. I think that allprog said the truth. Anyway I give you guys an update. There is another issue report at AOSP page which has been owned by an Android guy. The issue is the same of mine one. So let's hope they will get their hands-on! This is the issue link:code.google.com/p/android/issues/detail?id=58896 Keep an eye on it!
    • vee
      vee over 10 years
      I encountered the same issue, Nexus 4 on 4.3 connecting to iPhone 5 acting as peripheral using LightBlue. Could connect but device discovery never finished and would disconnect. Updated the Nexus 4 to 4.4 (Build number KRT16S) and connection and discovering services works. Was able to read and write characteristics as well.
  • allprog
    allprog almost 11 years
    Thanks for the explanation. Your issue was found by the OP too. Let's hope this gets solved in the next release.
  • andresmafra
    andresmafra about 10 years
    Hi guys, any update on this? Android team is working on this issue? I see no updates on their issue tracking...
  • ThomasW
    ThomasW about 9 years
    It was marked obsolete by Google on Dec 7, 2014. However, people are reporting it is still an issue.
  • domenukk
    domenukk about 9 years
    Thank you for sharing. It's good to know it works in theory and good to have some sample code. However when I tried to checkout your git example BLEMingle.swift and BLECentralDelegate.swift were missing.
  • Inder Kumar Rathore
    Inder Kumar Rathore about 9 years
    Tried to compile your your ios code using xcode 6.3.2 it doesn't compile, after solving compiler errors. I run the code but was not able to send the text. Will try to solve the problem.
  • domenukk
    domenukk almost 9 years
    Just tried to compile it, but BLEMingle.swift throws 17 errors... Could you check your git status? Thanks a lot for your effort. Even non-functioning it's a huge help.
  • idiogo
    idiogo almost 9 years
    Xcode 6.3, 17 errors on BLEMingle.swift. Are you sure that you pushed to github the latest version on the master branch?
  • domenukk
    domenukk almost 9 years
    Did you try to clean and rebuild everything? Or check it out on a different account? Maybe it didn't even bother to recompile this file since you didn't change anything?
  • domenukk
    domenukk almost 9 years
    While compiling or running? I am currently not at home but will try later this week and report back. ;)
  • domenukk
    domenukk almost 9 years
    I checked it out on another computer and it compiles just fine. I just removed the references to the jar that were still in the build. Sending files to iOS works flawlessly! Great work! However to the Nexus 6 it sometimes only receives parts of the messages. I'm debugging a bit now.
  • omikes
    omikes almost 9 years
    OK, awesome. I think the hard part is done, for sending messages to Android I was thinking of sending the same message part multiple times, then on the receiving end it would just keep a log of twenty or so recent message parts and cancel out any duplicates that come across. Progress!
  • domenukk
    domenukk almost 9 years
    Anyway we'll be running into massive problems with this solution as soon as there are more than 2 devices involved...
  • AlfuryDB
    AlfuryDB over 8 years
    Ive tried this code and it doesn´t work because basically there is an android bug, internally speaking. About compiling, using XCode 7 doesnt work and doesnt compile, it compiles in Android though. Ive written the iOS code in objective-c to make it compilable but it doesnt work either.
  • omikes
    omikes over 7 years
    I've got it working again, updated to the latest versions of iOS and Android. Give it a try.
  • cutsoy
    cutsoy about 6 years
    This is really useful information. Do you have any idea why it might fail with the S8?
  • Sudhin Philip
    Sudhin Philip about 6 years
    I believe it might be due to Samsung Android customized OS version. But I cannot find any official documentation on this.