Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android BLE client only returns 600 byte of data in onCharacteristicRead

I've got a Bluetooth server that uses bleno and returns a list of available Wifi networks to the client. The code for readCharacteristic looks basically like this:

class ReadCharacteristic extends bleno.Characteristic {

constructor(uuid, name, action) {
    super({
        uuid: uuid,
        properties: ["read"],
        value: null,
        descriptors: [
            new bleno.Descriptor({
                uuid: "2901",
                value: name
              })
        ]
    });
    this.actionFunction = action;
}

onReadRequest(offset, callback) {
    console.log("Offset: " + offset);

if(offset === 0) {
        const result = this.actionFunction();
    result.then(value => {
        this.actionFunctionResult = value;
            const data = new Buffer.from(value).slice(0,bleno.mtu);
            console.log("onReadRequest: " + data.toString('utf-8'));

            callback(this.RESULT_SUCCESS, data);
        }, err => {
            console.log("onReadRequest error: " + err);
            callback(this.RESULT_UNLIKELY_ERROR);
        }).catch( err => {
            console.log("onReadRequest error: " + err);
            callback(this.RESULT_UNLIKELY_ERROR);
        });
}
else {
    let data = new Buffer.from(this.actionFunctionResult);
    if(offset > data.length) {
        callback(this.RESULT_INVALID_OFFSET, null);
    }
    data = data.slice(offset+1, offset+bleno.mtu);
    console.log(data.toString('utf-8'));
    callback(this.RESULT_SUCCESS, data);
}
}
}

(I've tried data = data.slice(offset+1, offset+bleno.mtu); and like this data = data.slice(offset+1);)

The client is an Android app that reads this Characteristic.

The Android part for reading looks like this:

            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                                int newState) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    gatt.requestMtu(256);

                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.i(TAG, "Disconnected from GATT server.");

                    mFancyShowCaseView.show();
                    gatt.close();
                    scanForBluetoothDevices();
                }
            }


            @Override
            public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
                if (status != BluetoothGatt.GATT_SUCCESS) {
                    Log.e(TAG, "Can't set mtu to: " + mtu);
                } else {
                    Log.i(TAG, "Connected to GATT server. MTU: " + mtu);
                    Log.i(TAG, "Attempting to start service discovery:" +
                            mWifiProvisioningService.discoverServices());
                }
            }


            @Override
            // New services discovered
            public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.d(TAG, "ACTION_GATT_SERVICES_DISCOVERED");

                    BluetoothGattService wifiProvisioningService = gatt.getService(WIFI_PROVISIONING_SERVICE_UUID);
                    BluetoothGattCharacteristic currentConnectedWifiCharacteristic = wifiProvisioningService.getCharacteristic(WIFI_ID_UUID);
                    BluetoothGattCharacteristic availableWifiCharacteristic = wifiProvisioningService.getCharacteristic(WIFI_SCAN_UUID);

                    // Only read the first characteristic and add the 2nd one to a list as we have to wait
                    // for the read return before we read the 2nd one.
                    if (!gatt.readCharacteristic(currentConnectedWifiCharacteristic)) {
                        Log.e(TAG, "Error while reading current connected wifi name.");
                    }
                    readCharacteristics.add(availableWifiCharacteristic);

                } else {
                    Log.w(TAG, "onServicesDiscovered received: " + status);
                }
            }


            @Override
            // Result of a characteristic read operation
            public void onCharacteristicRead(BluetoothGatt gatt,
                                             BluetoothGattCharacteristic characteristic,
                                             int status) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    UUID characteristicUUID = characteristic.getUuid();
                    if (WIFI_ID_UUID.equals(characteristicUUID)) {
                        Log.d(TAG, "HEUREKA we found the current wifi name: " + new String(characteristic.getValue()));
                        final String currentWifiName = new String(characteristic.getValue());
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                ((TextView) findViewById(R.id.currentWifiTxt)).setText(currentWifiName);
                                findViewById(R.id.currentWifiTxtProgress).setVisibility(View.GONE);
                            }
                        });

                    } else if (WIFI_SCAN_UUID.equals(characteristicUUID)) {
                        Log.d(TAG, "HEUREKA we found the wifi list: " + new String(characteristic.getValue()));
                        List<String> wifiListArrayList = new ArrayList<>();

                        try {
                            JSONObject wifiListRoot = new JSONObject(characteristic.getStringValue(0));
                            JSONArray wifiListJson = wifiListRoot.getJSONArray("list");

                            for (int i = 0; i < wifiListJson.length(); i++) {
                                wifiListArrayList.add(wifiListJson.get(i).toString());
                            }

                        } catch (JSONException e) {
                            Log.e(TAG, e.toString());
                            return;
                        }
                        final String[] wifiList = new String[wifiListArrayList.size()];
                        wifiListArrayList.toArray(wifiList);

                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                ((ListView) findViewById(R.id.availableWifiList)).setAdapter(new ArrayAdapter<String>(mContext, R.layout.wifi_name_list_item, wifiList));
                                findViewById(R.id.currentWifiTxtProgress).setVisibility(View.GONE);
                            }
                        });
                    } else {
                        Log.i(TAG, "Unexpected Gatt vale: " + new String(characteristic.getValue()));
                    }

                    if (readCharacteristics.size() > 0) {
                        BluetoothGattCharacteristic readCharacteristic = readCharacteristics.get(0);
                        if (!gatt.readCharacteristic(readCharacteristic)) {
                            Log.e(TAG, "Error while writing descriptor for connected wifi");
                        }
                        readCharacteristics.remove(readCharacteristic);
                    }
                }
            }

The MTU is adjusted to 256 bytes. Which I reflected on the server when reading the list. The call itself works fine and returns the list but if the list contains more then 600 bytes only 600 bytes are available on Android. I'm somehow certain that the JS server sends all the data but for some reason the Android client only receives or caches 600 bytes which does not seem correct.

I've found this post: Android BLE - Peripheral | onCharacteristicRead return wrong value or part of it (but repeated)

and this: Android BLE - How is large characteristic value read in chunks (using an offset)?

But both didn't solve my issue. I'm aware that I need to wait for one read to return before I start the next read and that I need to wait till MTU is written before I continue to read data. To the best of my knowledge this is reflected in the source you see above. I'm kind of lost here.

Any idea is highly apprechiated.

Thanks a lot

like image 709
Thor_Bux Avatar asked Feb 12 '18 07:02

Thor_Bux


1 Answers

For anyone who comes across this post also wondering why Android seems to return only 600 bytes for long GATT characteristics like this question is asking about, it all comes down to how Bluedroid (Android's Bluetooth stack) implements their GATT Client and how its out of spec. In my case, I was using an ESP32-based IoT device as my GATT Server and Android (SDK 24) for the GATT Client.

According the spec (Bluetooth Core 4.2; Vol 3, Part F: 3.2.9), the maximum size for a characteristic value (inherited from ATT's attribute value) is 512 bytes. However, for some reason, Bluedroid does not attempt to enforce this requirement, instead decided upon a maximum size of 600; which can be seen if you dive into the Bluedroid source and find the macro GATT_MAX_ATTR_LEN which is set to 600 (stack/include/gatt_api.h:125). Since in my case (and yours it seems) I was implementing the read request response code, I did not see to enforce the 512 byte limit on reads for characteristics either.

Now, its important to realize just how it seems Bluedroid reads characteristics and how that relates to both the MTU size, the maximum size of a read (should be 512, but is 600 for Bluedroid) and how to handle data longer than that maximum size. The MTU size is the largest packet size on the ATT level you can use. So, for each call to BluetoothGatt.readCharacteristic, you may be sending one or more read requests to the server depending on if Bluedroid thinks the characteristic size exceeds the MTU size. On a low level, Bluedroid will first send a ATT Read Request (0x0a) and if the packet is MTU bytes in length, it will follow up with a ATT Read Blob Request (0x0c) with the offset set to the MTU size. It will continue to send ATT Read Blob Requests until either the ATT Read Blob Response is less than MTU bytes in length or until the maximum characteristic size is reached (ie, 600 for Bluedroid). Its important to note that if the MTU size is not a perfect multiple of 600 for data longer than 600 bytes, the remaining bytes will be discarded (as Bluedroid never actually expects to read 600 bytes since it thinks the GATT Server will enforce the 512 byte limit on characteristic sizes). So, if your data exceeds the 600 byte limit (or 512 limit for safety), you should expect to call BluetoothGatt.readCharacteristic multiple times. Heres a simple example for reading lots of data on the Android side (sorry Im not using bleno so cannot give you the code to fix that side), it relies on first sending the length of the data as a unsigned 32-bit integer and then reading out the data with repeated calls to BluetoothGatt.readCharacteristic if the data is longer than 600 bytes:

private int readLength;
private StringBuilder packet; // In my case, Im building a string out of the data

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                    int newState) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        gatt.requestMtu(201); // NOTE: If you are going to read a long piece of data, its best to make this value a factor of 600 + 1, like 51, 61, 101, 151, etc due to the risk of data loss if the last packet contains more than 600 bytes of cumulative data
    }
}

@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    gatt.discoverServices();
}

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    // Kick off a read
    BluetoothGattCharacteristic characteristic = gatt.getService(UUID.fromString(SERVICE_UUID)).getCharacteristic(UUID.fromString(CHAR_UUID));
    readLength = 0;
    gatt.readCharacteristic(characteristic);
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    if (readLength == 0) {
        readLength = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
        packet = new StringBuilder();
        gatt.readCharacteristic(characteristic);
    } else {
        byte[] data = charactertic.getValue();
        packet.append(new String(data));
        readLength -= data.length;

        if (readLength == 0) {
            // Got all data this time; you can now process the data however you want
        } else {
            gatt.readCharacteristic(characteristic);
        }
    }
}
like image 150
Unn Avatar answered Oct 23 '22 12:10

Unn