don / cordova-plugin-ble-central

Bluetooth Low Energy (BLE) Central plugin for Apache Cordova (aka PhoneGap)
Apache License 2.0
942 stars 603 forks source link

iOS crashes when advertising data contains 'kCBAdvDataLeBluetoothDeviceAddress' (was: Terminating app due to uncaught exception 'NSInvalidArgumentException') #685

Closed shooijen closed 4 years ago

shooijen commented 5 years ago

I have been working with this library for some time now and had not experienced any problems so far. Until last week, I received an update from iOS (12.2 to 12.3). After this update Im getting the following error message on all variants of my App made with Ionic in combination with this BLE library:

2019-05-20 13:46:32.105034+0200 MyApp[615:55841] Discovered { advertising = { kCBAdvDataIsConnectable = 1; }; id = "F9D8D19F-AB0C-2BB0-86DB-8B02F33FF7BD"; name = "Sjors\U2019s iMac Pro (4)"; rssi = "-53"; } 2019-05-20 13:46:32.161832+0200 MyApp[615:55841] Discovered { advertising = { kCBAdvDataIsConnectable = 1; kCBAdvDataLeBluetoothDeviceAddress = <b2a4763f 76ff01>; kCBAdvDataLocalName = XXXXX; kCBAdvDataServiceUUIDs = ( "C8DFEE02-1BB6-44D6-9842-2FE4A8874768" ); }; id = "FD7E7083-F1B7-0FAC-0CB3-50876382BAE9"; name = XXXXX; rssi = "-59"; } 2019-05-20 13:46:32.162820+0200 MyApp[615:55841] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (_NSInlineData)' *** First throw call stack: (0x1c26ce3a8 0x1c18d3d00 0x1c25d3be4 0x1c31ed920 0x1c31f08a8 0x1c31f0a44 0x1c25b2a14 0x1c31efe54 0x1c31f08a8 0x1c31f0a44 0x1c25b2a14 0x1c31efe54 0x1c31f0af4 0x1c25b1d24 0x1c31f0294 0x1c30a3280 0x1c30a2f90 0x10256bee0 0x1025767d4 0x10256b930 0x102548d44 0x1c822ed04 0x1c822fe3c 0x1c823e42c 0x10265b1f8 0x10265c778 0x102664a34 0x102665778 0x102669fb8 0x1c265e024 0x1c2658cd4 0x1c2658254 0x1c4897d8c 0x1ef9a04c0 0x1025446bc 0x1c2114fd8) libc++abi.dylib: terminating with uncaught exception of type NSException

package.json (example project with only BLE module) { "name": "myApp", "version": "0.0.1", "author": "Ionic Framework", "homepage": "https://ionicframework.com/", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, "dependencies": { "@angular/common": "^7.2.2", "@angular/core": "^7.2.2", "@angular/forms": "^7.2.2", "@angular/http": "^7.2.2", "@angular/platform-browser": "^7.2.2", "@angular/platform-browser-dynamic": "^7.2.2", "@angular/router": "^7.2.2", "@ionic-native/ble": "^5.6.0", "@ionic-native/core": "^5.0.0", "@ionic-native/splash-screen": "^5.0.0", "@ionic-native/status-bar": "^5.0.0", "@ionic/angular": "^4.1.0", "@ionic/pro": "2.0.4", "cordova-android": "7.1.4", "cordova-ios": "4.5.5", "cordova-plugin-ble-central": "1.2.2", "cordova-plugin-compat": "^1.2.0", "cordova-plugin-device": "^2.0.2", "cordova-plugin-ionic-keyboard": "^2.1.3", "cordova-plugin-ionic-webview": "^4.0.1", "cordova-plugin-splashscreen": "^5.0.2", "cordova-plugin-statusbar": "^2.4.2", "cordova-plugin-whitelist": "^1.3.3", "core-js": "^2.5.4", "rxjs": "~6.5.1", "tslib": "^1.9.0", "zone.js": "~0.8.29" }, "devDependencies": { "@angular-devkit/architect": "~0.13.8", "@angular-devkit/build-angular": "~0.13.8", "@angular-devkit/core": "~7.3.8", "@angular-devkit/schematics": "~7.3.8", "@angular/cli": "~7.3.8", "@angular/compiler": "~7.2.2", "@angular/compiler-cli": "~7.2.2", "@angular/language-service": "~7.2.2", "@ionic/angular-toolkit": "~1.5.1", "@types/node": "~12.0.0", "@types/jasmine": "~2.8.8", "@types/jasminewd2": "~2.0.3", "codelyzer": "~4.5.0", "jasmine-core": "~2.99.1", "jasmine-spec-reporter": "~4.2.1", "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", "karma-jasmine": "~1.1.2", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.4.0", "ts-node": "~8.1.0", "tslint": "~5.16.0", "typescript": "~3.1.6" }, "description": "An Ionic project", "cordova": { "plugins": { "cordova-plugin-ble-central": {}, "cordova-plugin-whitelist": {}, "cordova-plugin-statusbar": {}, "cordova-plugin-device": {}, "cordova-plugin-splashscreen": {}, "cordova-plugin-ionic-webview": {}, "cordova-plugin-ionic-keyboard": {} }, "platforms": [ "ios" ] } }

If I look at the error message from Xcode, it looks like there is error in JSON format. 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (_NSInlineData)'

shooijen commented 5 years ago

For now I removed the [dictionary setObject: [self advertising] forKey: @"advertising"]; from CBPeripheral+Extensions.m

-(NSDictionary *)asDictionary {
    NSString *uuidString = NULL;
    if (self.identifier.UUIDString) {
        uuidString = self.identifier.UUIDString;
    } else {
        uuidString = @"";
    }

    NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
    [dictionary setObject: uuidString forKey: @"id"];

    if ([self name]) {
        [dictionary setObject: [self name] forKey: @"name"];
    }

    if ([self savedRSSI]) {
        [dictionary setObject: [self savedRSSI] forKey: @"rssi"];
    }

//    if ([self advertising]) {
//        [dictionary setObject: [self advertising] forKey: @"advertising"];
//    }

    if([[self services] count] > 0) {
        [self serviceAndCharacteristicInfo: dictionary];
    }

    return dictionary;
}

Advertising object that cause the problem:

    advertising =     {
        kCBAdvDataIsConnectable = 1;
        kCBAdvDataLeBluetoothDeviceAddress = <0f625713 dbf301>;
        kCBAdvDataLocalName = XXXXX;
        kCBAdvDataServiceUUIDs =         (
            "C8DFEE02-1BB6-44D6-9842-2FE4A8874768"
        );
    };

Advertising objects with no error: Example 1:

    advertising =     {
        kCBAdvDataIsConnectable = 0;
        kCBAdvDataManufacturerData =         {
            CDVType = ArrayBuffer;
            data = "BgABCSACMohNpkNcsHLd36eohNh3+iV2hkISouU=";
        };
    };

Example 1:

    advertising =     {
        kCBAdvDataIsConnectable = 1;
    };

Are there any BLE standards for advertising objects?

droghio commented 5 years ago

If you are up for a little debugging, what happens if you remove the kCBAdvDataLeBluetoothDeviceAddress key from the advertisement data? My guess is a new key was added to the attributes list in iOS 12.3 which is not directly JSON serialize.

ghost commented 5 years ago

I have the same problem. Log shows the following advertising details:

    advertising =     {
        kCBAdvDataIsConnectable = 1;
        kCBAdvDataLocalName = MARTIN;
        kCBAdvDataManufacturerData =         {
            CDVType = ArrayBuffer;
            data = "FLGIoMj9GZqSBA==";
        };
        kCBAdvDataServiceUUIDs =         (
            FFE0,
            FEE0
        );
    };
    id = "A539504A-EF21-688D-DAD9-FF214C5768DD";
    name = MARTIN;
    rssi = "-54";

    advertising =     {
        kCBAdvDataIsConnectable = 1;
    };
    id = "68B10163-B862-82D0-0240-068039DC7BD8";
    rssi = "-94";

    advertising =     {
        kCBAdvDataIsConnectable = 1;
    };
    id = "A0E71A93-F3C4-3000-42DF-9C3E67349557";
    name = Raticate;
    rssi = "-43";

    advertising =     {
        kCBAdvDataIsConnectable = 1;
        kCBAdvDataLeBluetoothDeviceAddress = <0089198f aa6fa0>;
        kCBAdvDataLocalName = "[LG] webOS TV UJ750V";
        kCBAdvDataManufacturerData =         {
            CDVType = ArrayBuffer;
            data = "xAALMwITFYA=";
        };
        kCBAdvDataServiceUUIDs =         (
            FEB9
        );
    };
    id = "64D3EA2D-F247-A40A-E6C5-65E422E6C856";
    name = "[LG] webOS TV UJ750V";
    rssi = "-86";

and then the app crashes with:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (_NSInlineData)'
*** First throw call stack:
(0x18e4d127c 0x18d6ab9f8 0x18e3db4b0 0x18efd4c48 0x18efd7b50 0x18e3bb000 0x18efd7120 0x18efd7b50 0x18e3bb000 0x18efd7120 0x18efd7d84 0x18e3ba36c 0x18efd753c 0x18ee93bdc 0x18ee93904 0x100259064 0x10024d3c8 0x100248144 0x10023f7cc 0x193f6210c 0x193f631e8 0x193f71430 0x1006336f0 0x100634c74 0x10063cbf4 0x10063d8ec 0x10064256c 0x18e462c1c 0x18e45db54 0x18e45d0b0 0x19065d79c 0x1bac93978 0x1002393a8 0x18df228e0)
libc++abi.dylib: terminating with uncaught exception of type NSException
ghost commented 5 years ago

Wondering if the square brackets in name = "[LG] webOS TV UJ750V" are the problem?

Confirmed: This is my TV advertising. If I turn the TV off the problem goes away. Turn it back on and... BOOM! :-)

ghost commented 5 years ago

I changed setAdvertisementData in CBPeripheral+Extensions.m to overwrite the CBAdvertisementDataLocalNameKey with a new hard coded value. I'm pretty certain it's the advertised device name that's causing this crash but my app still crashes, even with this hard coded name change. There's another attribute in the dictionary just called 'name' though and this also contains the Complete Local Name from the advertising packet. I'm guessing this value is breaking the JSON processing.

Discovered {
    advertising =     {
        kCBAdvDataIsConnectable = 1;
        kCBAdvDataLeBluetoothDeviceAddress = <0089198f aa6fa0>;
        kCBAdvDataLocalName = TESTTEST;
        kCBAdvDataManufacturerData =         {
            CDVType = ArrayBuffer;
            data = "xAAVMwITFYA=";
        };
        kCBAdvDataServiceUUIDs =         (
            FEB9
        );
    };
    id = "C8D0B43C-82B7-AA41-CB90-8FAB9916F58A";
    name = "[LG] webOS TV UJ750V";
    rssi = "-63";
}

I have barely any experience of Objective C btw....

Does anyone have any ideas for fixing or working around this problem? Yes, I know I could just turn my TV off :-)

@don do you have any ideas?

ghost commented 5 years ago

It's not the square brackets. I found I could change the advertised name in my TV settings and its advertising still crashes the app:

2019-06-01 20:54:14.347482+0100 bitty controller[315:10790] Discovered {
    advertising =     {
        kCBAdvDataIsConnectable = 1;
        kCBAdvDataLeBluetoothDeviceAddress = <0089198f aa6fa0>;
        kCBAdvDataLocalName = telly;
        kCBAdvDataManufacturerData =         {
            CDVType = ArrayBuffer;
            data = "xAAtMwITFYA=";
        };
        kCBAdvDataServiceUUIDs =         (
            FEB9
        );
    };
    id = "BE9B8BAC-70C5-E0BE-803D-51E1AD905042";
    name = telly;
    rssi = "-73";
}

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (_NSInlineData)'
ghost commented 5 years ago

Removing kCBAdvDataLeBluetoothDeviceAddress stops the crash from happening so I guess there's an issue parsing that field:

This is the problem field value:

kCBAdvDataLeBluetoothDeviceAddress = <0089198f aa6fa0>;

In setAdvertisementData in CBPeripheral+Extensions.m I added this hack to strip it out and the problem stopped happening:

    // Remove the device address data
    NSData *btaddr = [dict objectForKey:@"kCBAdvDataLeBluetoothDeviceAddress"];
    if (btaddr) {
        [dict removeObjectForKey:@"kCBAdvDataLeBluetoothDeviceAddress"];
    }
ferrisimo commented 4 years ago

I have exactly the same issue. The fix was to add the device remove for the address, but in serializableAdvertisementData() not setAdvertisementData().

Many thanks for the fix.

gersonfa commented 4 years ago

@bluetooth-mdw Hey, did you just add that code to the end of the method? Or is it necessary to do something else? Thank you

ghost commented 4 years ago

I just placed it at the end of the method. It's a nasty hack, of that I have no doubt. But it unblocked my progress and is good enough until someone comes up with something better and gets it merged into this repo.

jospete commented 4 years ago

I made a fork here that changes the logic in serializableAdvertisementData() to dump known properties into a fresh dictionary rather than returning a mutated raw advertising data dictionary.

Unfortunately I don't have a peripheral handy that causes this issue to occur (but getting complaints about one of my company's apps that uses this plugin).

Anyone with a peripheral exhibiting this behavior willing to test this fork out?

JacquesBoileau commented 4 years ago

I have said device at home (an LG tv). I could reproduce the problem with the original plugin using an ionic app. I have tested your fix. It works in my environment and fixes the crash. It does not seem to have any side effect. Great!

Will this be ported to the original plugin?

Thanks for your help!

jospete commented 4 years ago

@JacquesBoileau Thanks for the help with testing!

I'll make a PR to have this merged into the original plugin.

dylanrynhart commented 4 years ago

@jospete Thanks so much for this patch. I've been struggling with this issue for a few weeks also and I didn't know what the problem was. Thanks to this post I was able to build an ESP32 board with the following Raw advertising packet: 0x09, 0xFF, 0xAA, 0xBB, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0x08, 0x1B, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD

And your patch prevented the issue for me. I wouldn't know where to start with objective-c, so I really appreciate your time.

Just in case anyone is reading this who hasn't had to learn about Raw advert packets, it's roughly like this, as I understand it...

There is a sequence of Bytes for each GAP (generic access profile) value. the first is the number of Bytes in this packet including the GAP identifier (which is next) The second Byte is the GAP identifier itself, here's a link to them all: https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/

And following that, is the Bytes that make up the data.

My Packet above is as follows:

  1. There are 0x09 Bytes in the first packet

  2. The GAP is 0xFF (Manufacturer Specific Data)

  3. 0xAA, 0xBB, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB is the data in the packet.

  4. There are 0x08 Bytes in the second packet

  5. The GAP is 0x1B (LE Bluetooth Device Address) >>This is the offending element <<

  6. 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD is the data in the packet.

An important note... !!! If I make the 0x1B packet 7 bytes long, ( e.g. 6 bytes of data and the GAP reference ), then it doesn't cause this bug. It seems to be only when there are 7 bytes of data that the issue arises ? as the post above from @bluetooth-mdw helpfully illustrates.

I suspect the answer might be in the core spec, which I don't have ?

don commented 4 years ago

@bluetooth-mdw good tip on the LG TV. I used mine to troubleshoot this problem.

It looks like the offending key is kCBAdvDataLeBluetoothDeviceAddress is 0x1B <<LE Bluetooth Device Address>> according to the GAP advertising data type.

1.16 LE BLUETOOTH DEVICE ADDRESS 1.16.1 Description The LE Bluetooth Device Address data type defines the device address of the local device and the address type on the LE transport. This data type shall exist only once. It may be sent by an out of band method to a peer device.

1.16.2 goes on to describes the format, which basically says it's a MAC address and a bit denoting if it's public or private address. This could be really useful information in apps. Unfortunately the key isn't public in Advertisement Data Retrieval Keys so accessing it probably constitutes using a private API.

@jospete thanks for the patch. I'll get a version of it merged in soon

ghost commented 4 years ago

@don excellent, thanks for taking care of this and for sharing your analysis!