Code Monkey home page Code Monkey logo

Comments (5)

dariuszseweryn avatar dariuszseweryn commented on July 30, 2024

No detailed tests were performed.
Usually BLE is not about sending a bulk of data quickly but adding additional layers of abstraction and using RxJava obviously adds some computation work for the processor and memory allocation.
If I would have enough time I would perform some comparison test between specialised implementation of Android BLE API vs RxAndroidBle in firmware update scenario.

from rxandroidble.

maoueh avatar maoueh commented on July 30, 2024

We are currently using RxAndroidBle to flash the firmware of a device over the air. The firmware is about 300kb. Nice thing is that I also have a proof of concept app for the same flashing process using pure Android API.

These are not real experiments but simple rough estimate. Using RxAndroidBle, it takes up to 3 minutes to transfer everything as for native API, it's about 40s.

The big bottleneck in the library right now for heavy data transfer is that all write operations (in fact simply all operations) are done on the UI thread. This make the transfer process really longer than expected as it's fighting for the UI thread resources.

We noted a minimal 2x increase when just shutting down the screen of the device for example.

I read in the code that you're doing this because Samsung 4.3 fails when connectGatt operation is not performed on the UI thread. Do you think it would be possible to make a conditional to only run the operations on the UI thread for a subset of all devices?

from rxandroidble.

uKL avatar uKL commented on July 30, 2024

#165

from rxandroidble.

uKL avatar uKL commented on July 30, 2024

As a result of this issue, there will be a report of write operation benchmark (16kB) with nRF based mock device:

  1. Write with native Android APIs (write with BluetoothGatt and direct use of BluetoothGattCallback)
  2. Write with LongWriteOperationBuilder
  3. Looped writeCharacteristic operation of this library
  4. Custom operation with write with BluetoothGatt and response with RxBleGattCallback.
  5. Custom operation with write with BluetoothGatt and direct use of BluetoothGattCallback.

from rxandroidble.

dariuszseweryn avatar dariuszseweryn commented on July 30, 2024

Now I finally made some performance tests for sending out as much data as possible using various approaches.

Specification

Peripheral: nRF51822 with SoftDevice S110 8.0.0
Connection Interval: 11.25 ms
MTU: 23 (default—nRF51 does not support different MTUs)
Library version: 1.4.1

Test Algorithm

  1. The central (phone) is sending 19 packets with 20 bytes each to the peripheral. The first byte contains the index of the packet.
  2. After every 19 packets the peripheral sends a notification to the central with indexes of the packets it received. The first byte contains the index of the response. (i.e. the first response packet would contain [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], second [1, 19, 20, 21, ...])
  3. The central receives the notification and repeats from the first step.

Speed calculation

Every response notification (2. step) is timestamped. All responses are buffered for 60 seconds. We take the last notification timestamp and the first notification timestamp and calculate the time difference. The resulting speed is ((number of received notifications - 1) * number of packets for each notification (19) * number of bytes in each packet (20)) / time difference

Code:

Observable.concat(
        perform(device, this::unoptimizedSendUuid).map(aFloat -> "Unoptimized UUID: " + String.valueOf(aFloat) + " Bps"),
        Observable.<String>empty().delay(1, TimeUnit.SECONDS),
        perform(device, this::unoptimizedSendCharacteristic).map(aFloat -> "Unoptimized Char: " + String.valueOf(aFloat) + " Bps"),
        Observable.<String>empty().delay(1, TimeUnit.SECONDS),
        perform(device, this::longWriteSend).map(aFloat -> "Long Write: " + String.valueOf(aFloat) + " Bps"),
        Observable.<String>empty().delay(1, TimeUnit.SECONDS),
        perform(device, this::optimizedSend).map(aFloat -> "Optimized: " + String.valueOf(aFloat) + " Bps")
)
        .observeOn(AndroidSchedulers.mainThread())
        .doOnUnsubscribe(this::clearSubscription)
        .subscribe(
                result -> Log.e("RESULT", result),
                e -> Log.e("ERROR", "Whoops!", e)
        );

private Observable<Float> perform(RxBleDevice device, TestSetup testSetup) {
    return device.establishConnection(false)
            .flatMap(RxBleConnection::discoverServices,
                    (connection, services) -> services.getCharacteristic(genericCommunicationCharacteristicUuid)
                            .flatMap(
                                    connection::setupNotification,
                                    (characteristic, responseObs) -> {
                                        characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                                        final Completable readyForTransmission =
                                                responseObs.filter(bytes -> bytes[0] == 0x01).first().toCompletable();
                                        final Observable<byte[]> speedTest = testSetup
                                                .create(new TestConnection(connection, responseObs, characteristic, 19));
                                        return readyForTransmission.andThen(speedTest);
                                    }
                            )
                            .flatMap(observable -> observable)
            )
            .flatMap(observable -> observable)
            .timestamp()
            .buffer(1, TimeUnit.MINUTES)
            .take(1)
            .map(timestampeds -> {
                final int bytesSent = (timestampeds.size() - 1) * 20 * 19;
                final float timeSeconds = (timestampeds.get(timestampeds.size() - 1).getTimestampMillis() - timestampeds.get(0).getTimestampMillis()) * 0.001f;
                return bytesSent / timeSeconds;
            });
}

interface TestSetup {

    Observable<byte[]> create(TestConnection testConnection);
}

static class TestConnection {

    final RxBleConnection connection;

    final Observable<byte[]> responseObs;

    final BluetoothGattCharacteristic characteristic;

    final int batchCount;

    public TestConnection(RxBleConnection connection, Observable<byte[]> responseObs,
                          BluetoothGattCharacteristic characteristic, int batchCount) {
        this.connection = connection;
        this.responseObs = responseObs;
        this.characteristic = characteristic;
        this.batchCount = batchCount;
    }
}

I have compared three approaches to sending data (all of them with setting BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE):

  1. Using `RxBleConnection.writeCharacteristic(UUID, byte[])
private Observable<byte[]> unoptimizedSendUuid(TestConnection testConnection) {
    final RxBleConnection connection = testConnection.connection;
    final BluetoothGattCharacteristic characteristic = testConnection.characteristic;
    final UUID uuid = characteristic.getUuid();
    return Observable.range(0, testConnection.batchCount)
            .concatMap(frameIndex -> connection.writeCharacteristic(
                    uuid,
                    new byte[]{
                            frameIndex.byteValue(), 0, 0, 0, 0, 0, 0, 0, 0,
                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
                    }
            ))
            .ignoreElements()
            .repeatWhen(observable -> Observable.zip(observable, testConnection.responseObs, (o, bytes) -> o))
            .mergeWith(testConnection.responseObs);
}
  1. Using `RxBleConnection.writeCharacteristic(BluetoothGattCharacteristic, byte[])
private Observable<byte[]> unoptimizedSendCharacteristic(TestConnection testConnection) {
    final RxBleConnection connection = testConnection.connection;
    final BluetoothGattCharacteristic characteristic = testConnection.characteristic;
    return Observable.range(0, testConnection.batchCount)
            .concatMap(frameIndex -> connection.writeCharacteristic(
                    characteristic,
                    new byte[]{
                            frameIndex.byteValue(), 0, 0, 0, 0, 0, 0, 0, 0,
                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
                    }
            ))
            .ignoreElements()
            .repeatWhen(observable -> Observable.zip(observable, testConnection.responseObs, (o, bytes) -> o))
            .mergeWith(testConnection.responseObs);
}
  1. Using RxBleConnection.createNewLongWriteBuilder()
private Observable<byte[]> longWriteSend(TestConnection testConnection) {
    final byte[] bytesToSend = new byte[20 * 19];
    for (int i = 0; i < bytesToSend.length; i = i + 20) {
        bytesToSend[i] = (byte) (i / 20);
    }
    return testConnection.connection
            .createNewLongWriteBuilder()
            .setBytes(bytesToSend)
            .setCharacteristic(testConnection.characteristic)
            .build()
            .ignoreElements()
            .repeatWhen(observable -> Observable.zip(observable, testConnection.responseObs, (o, bytes) -> o))
            .mergeWith(testConnection.responseObs);
}
  1. Using RxBleConnection.queue(RxBleCustomOperation)
    Please note that this implementation is not a example to follow—it does not check for the disconnection of the peripheral or cancellation of the operation.
private Observable<byte[]> optimizedSend(TestConnection testConnection) {
    return testConnection.connection.queue((bluetoothGatt, rxBleGattCallback, scheduler) -> Observable.create(
            emitter -> {
                Log.i("START", String.valueOf(testConnection.batchCount));
                final byte[] data = new byte[20];
                testConnection.characteristic.setValue(data);
                final AtomicBoolean writeCompleted = new AtomicBoolean(false);
                final AtomicBoolean ackCompleted = new AtomicBoolean(false);
                final AtomicInteger batchesSent = new AtomicInteger(0);
                final Runnable writeNextBatch = () -> {
                    data[0]++;
                    if (!bluetoothGatt.writeCharacteristic(testConnection.characteristic)) {
                        emitter.onError(new BleGattCannotStartException(bluetoothGatt, BleGattOperationType.CHARACTERISTIC_WRITE));
                    } else {
                        Log.i("SEND", String.valueOf(data[0]));
                        batchesSent.incrementAndGet();
                    }
                };
                final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
                    @Override
                    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                        if (status != BluetoothGatt.GATT_SUCCESS) {
                            emitter.onError(new BleGattException(gatt, status, BleGattOperationType.CHARACTERISTIC_WRITE));
                        } else if (batchesSent.get() == testConnection.batchCount) {
                            if (ackCompleted.get()) {
                                batchesSent.set(0);
                                ackCompleted.set(false);
                                emitter.onNext(null);
                                writeNextBatch.run();
                            } else {
                                writeCompleted.set(true);
                            }
                        } else {
                            characteristic.setValue(data);
                            writeNextBatch.run();
                        }
                    }

                    @Override
                    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                        final byte[] bytes = characteristic.getValue();
                        Log.i("ACK", Arrays.toString(bytes) + "/" + bytes.length + "/" + System.identityHashCode(bytes));
                        characteristic.setValue(data);
                        if (writeCompleted.get()) {
                            batchesSent.set(0);
                            writeCompleted.set(false);
                            emitter.onNext(null);
                            writeNextBatch.run();
                        } else {
                            ackCompleted.set(true);
                        }
                    }
                };

                rxBleGattCallback.setNativeCallback(bluetoothGattCallback);

                Log.i("SEND", String.valueOf(data[0]));
                if (!bluetoothGatt.writeCharacteristic(testConnection.characteristic)) {
                    emitter.onError(new BleGattCannotStartException(bluetoothGatt, BleGattOperationType.CHARACTERISTIC_WRITE));
                } else {
                    batchesSent.incrementAndGet();
                }
            },
            Emitter.BackpressureMode.NONE
    ));
}

Results (in Bytes per second)

central \ implementation unoptimizedSendUuid unoptimizedSendCharacteristic longWriteSend optimizedSend
Micromax Canvas A107 (5.0) 2463.4 2544.2 3479.5 4604.4
Nexus 5 (6.0.1) 2555.1 2593.2 2657.8 2759.1
Samsung Galaxy S6 SM-G920F (7.0) 1631.9 1789.4 2290.3 3867.7
Motorola Droid XT1030 (4.4.4) 1737.2 1960.8 2707.6 3010.1
Asus Zenfone 5 T00J (4.4.2) 1589.2 1758.0 2450.2 4010.2
Google Pixel (8.0.0) 2513.5 2646.9 2835.7 4602.5

Results for Connection Interval = 100 ms

central \ implementation unoptimizedSendUuid unoptimizedSendCharacteristic longWriteSend optimizedSend
Micromax Canvas A107 (5.0) 576.0 571.9 567.8 570.8
Nexus 5 (6.0.1) 605.0 612.0 623.9 624.5
Samsung Galaxy S6 SM-G920F (7.0) 629.0 619.9 623.2 632.3
Motorola Droid XT1030 (4.4.4) 342.3 342.9 343.0 344.8
Asus Zenfone 5 T00J (4.4.2) 546.7 542.9 560.7 545.7
Samsung Galaxy S3 GT-I9300 (4.3) 579.2 585.6 577.3 568.7
Google Pixel (8.0.0) 757.3 757.3 758.6 758.7

Conclusion

  • When interacting with a low Connection Interval peripheral it may be worth to use native implementation or custom operation to mitigate the downturn that RxJava causes.
  • When the Connection Interval grows the benefit from optimisations are less visible. It could need a detailed research but it seems that for Connection Interval > 50 ms every implementation should perform similarly.
  • The potential throughput of different Android handsets varies a lot (even in the same environment) because of the raw speed of the device (the route time of going through all the system layers from BluetoothGatt.writeCharacteristic() to BluetoothGattCallback.onCharacteristicWrite()) and number of buffers that it's Bluetooth Chip has.

I hope this quick research helps you.

Best Regards

from rxandroidble.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.