Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Help]: Multiple MSD in advertisement is wrong on Android #1047

Open
1 task done
P0labrD opened this issue Nov 7, 2024 · 12 comments
Open
1 task done

[Help]: Multiple MSD in advertisement is wrong on Android #1047

P0labrD opened this issue Nov 7, 2024 · 12 comments
Labels
help Questions, help, observations, or possible bugs

Comments

@P0labrD
Copy link

P0labrD commented Nov 7, 2024

Requirements

  • I've looked at the README 'Common Problems' section

Have you checked this problem on the example app?

No

FlutterBluePlus Version

1.33.2

Flutter Version

3.24.3

What OS?

Android, iOS

OS Version

iOS 15.8.3, Android 14

Bluetooth Module

ESP32-WROOM-32E

What is your problem?

I'm looking at the advertisementData of my bluetooth device on both Android and iOS, and I can't figure why the format of
manufacturerData and msd properties are different: the manufacturerId key is accessed differently between both os

{
 "android": {
   "advName": "advName",
   "txPowerLevel": null,
   "appearance": null,
   "connectable": true,
   "manufacturerData": {
     "0": [5, 54 /* ... */],
     "12345": []
   },
   "msd": [
     [0, 0, 5, 54 /* ... */],
     [48, 57]
   ],
   "serviceData": {},
   "serviceUuids": []
 },

 "ios": {
   "advName": "advName",
   "txPowerLevel": null,
   "appearance": null,
   "connectable": true,
   "manufacturerData": {
     "12345": [0, 0, 5, 54 /* ... */]
   },
   "msd": [[48, 57, 0, 0, 5, 54 /* ... */]],
   "serviceData": {},
   "serviceUuids": []
 }
}

I can't find any documentation on this difference, so before introducing platform specific code in production, I would like to be sure it is developed as intented.

Thank you very much!

Logs

{
  "android": {
    "advName": "advName",
    "txPowerLevel": null,
    "appearance": null,
    "connectable": true,
    "manufacturerData": {
      "0": [5, 54 /* ... */],
      "12345": []
    },
    "msd": [
      [0, 0, 5, 54 /* ... */],
      [48, 57]
    ],
    "serviceData": {},
    "serviceUuids": []
  },

  "ios": {
    "advName": "advName",
    "txPowerLevel": null,
    "appearance": null,
    "connectable": true,
    "manufacturerData": {
      "12345": [0, 0, 5, 54 /* ... */]
    },
    "msd": [[48, 57, 0, 0, 5, 54 /* ... */]],
    "serviceData": {},
    "serviceUuids": []
  }
}
@P0labrD P0labrD added the help Questions, help, observations, or possible bugs label Nov 7, 2024
@chipweinberger
Copy link
Owner

chipweinberger commented Nov 7, 2024

they should not be different. please open a PR if there is a bug.

msd = list of full raw data, split by manufacturer
manufacturerData = map of <manufactureId, data without manufactureId>

@chipweinberger
Copy link
Owner

chipweinberger commented Nov 8, 2024

also, where is this json coming from?

please print out the advertisement data directly and report back

print(advertisementData)

  @override
  String toString() {
    return 'AdvertisementData{'
        'advName: $advName, '
        'txPowerLevel: $txPowerLevel, '
        'appearance: $appearance, '
        'connectable: $connectable, '
        'manufacturerData: $manufacturerData, '
        'serviceData: $serviceData, '
        'serviceUuids: $serviceUuids'
        '}';
  }

@P0labrD
Copy link
Author

P0labrD commented Nov 8, 2024

I wrote the json with what I saw in my debugger, anonymizing the data
Here's the real (still anonymized) print ouput for iOS:

flutter: AdvertisementData{advName: product-name, txPowerLevel: null, appearance: null, connectable: true, manufacturerData: {12345: [0, 0, 5, 54, 48, 48, 85, 48, 54, 57, 48, 57, 50, 49]}, serviceData: {}, serviceUuids: []}

And for Android:

I/flutter (19040): AdvertisementData{advName: product-name, txPowerLevel: null, appearance: null, connectable: true, manufacturerData: {0: [5, 54, 48, 48, 85, 48, 54, 57, 48, 57, 50, 49], 12345: []}, serviceData: {}, serviceUuids: []}

So if I understand correctly, my device is seen with one manufacturerId 12345 for iOS, but with two for android, 0 and 12345, but with the rawData being on the wrong side

I'm not a hardware developer myself, but I could ask the one who did develop on the chip what he implemented in order to give good reproduction info.

Here's the plugin method I call to start scanning:

FlutterBluePlus.startScan(
  timeout: timeout,
  withMsd: [MsdFilter(12345)],
  removeIfGone: const Duration(seconds: 3),
  continuousUpdates: true,
);

@chipweinberger
Copy link
Owner

chipweinberger commented Nov 8, 2024

the android code is here. you should debug it further :)

particularly, you should print the result of : byte[] bytes = adv.getBytes();

it probably has a bug. iOS is probably correct.

    Map<Integer, byte[]> getManufacturerSpecificData(ScanRecord adv) {
        byte[] bytes = adv.getBytes();
        Map<Integer, byte[]> manufacturerDataMap = new HashMap<>();
        int n = 0;
        while (n < bytes.length) {

            // layout:
            // n[0] = fieldlen
            // n[1] = datatype (MSD)
            // n[2] = manufacturerId (low)
            // n[3] = manufacturerId (high)
            // n[4] = data...
            int fieldLen = bytes[n] & 0xFF;

            // no more or malformed data
            if (fieldLen <= 0) {
                break;
            }

            // Ensuring we don't go past the bytes array
            if (n + fieldLen >= bytes.length) {
                break;
            }

            int dataType = bytes[n + 1] & 0xFF;

            // Manufacturer Specific Data magic number
            // At least 3 bytes: 2 for manufacturer ID & 1 for dataType
            if (dataType == 0xFF && fieldLen >= 3) {

                // Manufacturer Id
                int high = (bytes[n + 3] & 0xFF) << 8;
                int low = (bytes[n + 2] & 0xFF);
                int manufacturerId = high | low;

                // the length of the msd data,
                // excluding manufacturerId & dataType
                int msdLen = fieldLen - 3;

                // ptr to msd data
                // excluding manufacturerId & dataType
                int msdPtr = n + 4;

                // add to map
                if (manufacturerDataMap.containsKey(manufacturerId)) {
                    // If the manufacturer ID already exists, append the new data to the existing list
                    byte[] existingData = manufacturerDataMap.get(manufacturerId);
                    byte[] mergedData = new byte[existingData.length + msdLen];
                    // Merge arrays
                    System.arraycopy(existingData, 0, mergedData, 0, existingData.length);
                    System.arraycopy(bytes, msdPtr, mergedData, existingData.length, msdLen);
                    manufacturerDataMap.put(manufacturerId, mergedData);
                } else {
                    // Otherwise, put the new manufacturer ID and its data into the map
                    byte[] data = new byte[msdLen];
                    // Starting from n+4 because manufacturerId occupies n+2 and n+3
                    System.arraycopy(bytes, msdPtr, data, 0, data.length);
                    manufacturerDataMap.put(manufacturerId, data);
                }
            }

            n += fieldLen + 1;
        }

        return manufacturerDataMap;
    }
    ```

@chipweinberger
Copy link
Owner

@MrCsabaToth

@MrCsabaToth
Copy link
Contributor

@MrCsabaToth

On Sunday I might have a little time to inspect the related code. I don't have an iOS device though, so it'll be Android and code inspection. As far as I know last time I modified it because of the FedEx tag, it meant to be the same. It had an off by one byte bug you corrected later.

@chipweinberger
Copy link
Owner

i inspected the code, looked fine to me.

@MrCsabaToth
Copy link
Contributor

MrCsabaToth commented Nov 10, 2024

Hey @P0labrD, could you screenshot the raw advertisement data displayed in nRF Connect on Android (https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp) vs iOS (https://apps.apple.com/us/app/nrf-connect-for-mobile/id1054362403) so we can have even more munition? The raw data looks like this #785 (comment) and this #785 (comment)

@P0labrD
Copy link
Author

P0labrD commented Nov 12, 2024

Hello @MrCsabaToth, thank you for your time.

Here are the two raw advertisement data displayed:

Android iOS

@P0labrD
Copy link
Author

P0labrD commented Nov 13, 2024

Update: my hardware developer apparently did not fully understand the Core Blutooth Specification Part A 1.4. Manufacturer Specific Data: as seen on the android screen above, he decided to put the manufacturerId on another "lane" as the specific data, whereas it should be but in the 2 first bytes of the 16 bytes of the manufacturerData.

What I don't understand at the moment is how iOS CoreBluetooth works, and how it "magically" did the matching itself.

@chipweinberger
Copy link
Owner

yes we should still try to fix them so they match

@chipweinberger
Copy link
Owner

chipweinberger commented Nov 22, 2024

So I think I have the answer.

According to ChatGPT, in BLE you should only have one MSD in an advertisement. Having multiple breaks spec.

So that's why iOS just concatenates all the data together. This is clear in the code. manufData just an array, not a map.

I don't know how we missed this. iOS code:

- (NSDictionary *)bmScanAdvertisement:(NSString*)remoteId
             advertisementData:(NSDictionary<NSString *, id> *)advertisementData
                          RSSI:(NSNumber *)RSSI
{
    NSString     *advName        = advertisementData[CBAdvertisementDataLocalNameKey];
    NSNumber     *connectable    = advertisementData[CBAdvertisementDataIsConnectable];
    NSNumber     *txPower        = advertisementData[CBAdvertisementDataTxPowerLevelKey];
    NSData       *manufData      = advertisementData[CBAdvertisementDataManufacturerDataKey];
    NSArray      *serviceUuids   = advertisementData[CBAdvertisementDataServiceUUIDsKey];
    NSDictionary *serviceData    = advertisementData[CBAdvertisementDataServiceDataKey];

    // Manufacturer Data
    NSDictionary* manufDataB = nil;
    if (manufData != nil && manufData.length >= 2) {
        
        // first 2 bytes are manufacturerId (little endian)
        uint8_t bytes[2];
        [manufData getBytes:bytes length:2];
        unsigned short manufId = (unsigned short) (bytes[0] | bytes[1] << 8);

        // trim off first 2 bytes
        NSData* trimmed = [manufData subdataWithRange:NSMakeRange(2, manufData.length - 2)];
        NSString* hex = [self convertDataToHex:trimmed];
        
        manufDataB = @{
            @(manufId): hex,
        };
    }

So we should fix this on Android, and remove all the "map" stuff. And fix/break the API.

@MrCsabaToth , can you do this?

The public facing API should be.

adv.msd.raw
adv.msd.payload
adv.msd.id

Also lets simplify the BmScanAdvertisement to just pass the raw concatenated msd data. We'll do the 2 byte parsing in Dart.

@chipweinberger chipweinberger changed the title [Help]: Why is the format of manufacturerData different between iOS and Android? [Help]: Multiple MSD in advertisement differs between Android vs iOS Nov 22, 2024
@chipweinberger chipweinberger changed the title [Help]: Multiple MSD in advertisement differs between Android vs iOS [Help]: Multiple MSD in advertisement wrong on Android Nov 22, 2024
@chipweinberger chipweinberger changed the title [Help]: Multiple MSD in advertisement wrong on Android [Help]: Multiple MSD in advertisement is wrong on Android Dec 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help Questions, help, observations, or possible bugs
Projects
None yet
Development

No branches or pull requests

3 participants