0

I have a device that can only accept 20 bytes at a time on the virtual serial port (using BLE). If I'm not mistaken, createNewLongWriteBuilder seems to be the perfect method for this.

Here is my attempt:

String newNameMsg = "SOME STRING THAT IS LONGER THAN 20 CHARACTERS";

byte[] byteMsg = newNameMsg.getBytes(Charset.forName("UTF-8"));
byte[] endLine = hexStringToByteArray("0D"); // signifies end of line for my device
byte[] newName = new byte[byteMsg.length + endLine.length];
System.arraycopy(byteMsg, 0, newName, 0, byteMsg.length);
System.arraycopy(endLine, 0, newName, byteMsg.length, endLine.length);


connectionObservable
    .flatMap(rxBleConnection -> rxBleConnection.createNewLongWriteBuilder()
    .setCharacteristicUuid(characteristicUuid)
    .setBytes(newName)
    .setMaxBatchSize(20) // my device only accepts 20 characters at a time.
    .setWriteOperationAckStrategy(new RxBleConnection.WriteOperationAckStrategy() {
            @Override
            public Observable<Boolean> call(Observable<Boolean> booleanObservable) {
                return Observable.just(true); // this is supposed to tell the LongWriteBuilder that we should continue sending data, correct?
            }
        })
    .build()
    )
    .subscribe(
        byteArray -> {
        // Written data.
        Log.i("BLE Controller","Data has been written!");
        },
        throwable -> {
        // Handle an error here.
        }
    );

Actual Results: The device does not receive any data, but the logs show:

D/RxBle#Radio: QUEUED RxBleRadioOperationCharacteristicLongWrite(107353908) D/RxBle#Radio: STARTED RxBleRadioOperationCharacteristicLongWrite(107353908) I/BLE Controller: Data has been written!

D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicLongWrite(107353908)

UPDATE 03/30/2017:

My original understanding was incorrect and had many issues with it. I was sending a subscription instead of an Observable.

s_noopy pointed out that:

The WriteOperationAckStrategy is effectively an equavilent of ? Observable.repeatWhen() operator. The filtering should happen inside the WOAS to trigger the repeat when ready.`

Current Situation:

I need to wait for my device to clear the TX flag before I can send the next batch. To do this, I need to implement setWriteOperationAckStrategy but I need to read the TX flag to see if it is clear before sending the next batch.

My attempt:

@Override
public Observable<Boolean> call(Observable<Boolean> objectObservable) {
    return connectionObservable
                .flatMap(rxBleConnection -> rxBleConnection.readCharacteristic(serialTX.getUuid()))
                .observeOn(AndroidSchedulers.mainThread());
}

UPDATE 07/30/2017:

Modified s_noopy's code so that I now have:

final ByteArrayBatchObservable byteArrayBatchObservable = new ByteArrayBatchObservable(newName, 19);
Log.i("BLE Controller","sending updated name"); 
byteArrayBatchObservable.flatMap(bytesBatch -> // using batches of data to write...
    connectionObservable.flatMap(rxBleConnection -> rxBleConnection.writeCharacteristic(vspT, bytesBatch) 
        .flatMap(writtenBytes -> { // ...and when each batch will be written...
            final Func1<byte[], Boolean> filterFunction = txBytes -> checkIfZero(txBytes);
            return rxBleConnection
                    .readCharacteristic(vspT.getUuid()) 
                    .repeat() // ...and repeat it...
                    .takeUntil(filterFunction)
                    .filter(filterFunction) // ...but don't emit anything until then...
                    .map(readBytes -> writtenBytes); // ...and emit the writtenBytesBatch...
        }
    ), 1)

)
.subscribe(
    byteArray -> {
        // Written data.
        Log.i("BLE Controller","BT has been renamed! :" + byteArray.toString());
    },
    throwable -> {
        // Handle an error here.
        Log.i("BLE Controller","BT rename ERROR");

    }
);

public boolean checkIfZero(byte[] txBytes){
    Log.i("BLE Controller", "checking if tx is cleared: " +txBytes.toString());
    for (byte b : txBytes) {
        if (b != 0) {
            return false;
        }
    }
    return true;
}

Current Situation:

I have tried to convert from using rxBleConnection to using the connectionObservable. It appears as though the first batch is written successfully, but even though the subscribe function returns a successfully written bytearray both times, the bluetooth device only sees the first batch

Logs

07-31 13:26:54.434 25060-25060/com.packet.sniffer I/BLE Controller: sending updated name
07-31 13:26:54.434 264-467/? I/ThermalEngine: Sensor:pa_therm1:33000 mC
07-31 13:26:54.440 25060-25060/com.packet.sniffer D/RxBle#Radio:   QUEUED RxBleRadioOperationCharacteristicWrite(99278425)
07-31 13:26:54.443 25060-25187/com.packet.sniffer D/RxBle#Radio:  STARTED RxBleRadioOperationCharacteristicWrite(99278425)
07-31 13:26:54.458 25060-25060/com.packet.sniffer D/RxBle#Radio:   QUEUED RxBleRadioOperationCharacteristicWrite(56755989)
07-31 13:26:54.611 25060-25072/com.packet.sniffer D/RxBle#BluetoothGatt: onCharacteristicWrite characteristic=569a2000-b87f-490c-92cb-11ba5ea5167c status=0
07-31 13:26:54.614 25060-25243/com.packet.sniffer D/RxBle#Radio:   QUEUED RxBleRadioOperationCharacteristicRead(32706505)
07-31 13:26:54.614 25060-25187/com.packet.sniffer D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicWrite(99278425)
07-31 13:26:54.615 25060-25187/com.packet.sniffer D/RxBle#Radio:  STARTED RxBleRadioOperationCharacteristicWrite(56755989)
07-31 13:26:54.714 25060-25071/com.packet.sniffer D/RxBle#BluetoothGatt: onCharacteristicWrite characteristic=569a2000-b87f-490c-92cb-11ba5ea5167c status=0
07-31 13:26:54.721 25060-25243/com.packet.sniffer D/RxBle#Radio:   QUEUED RxBleRadioOperationCharacteristicRead(39309874)
07-31 13:26:54.723 25060-25187/com.packet.sniffer D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicWrite(56755989)
07-31 13:26:54.724 25060-25187/com.packet.sniffer D/RxBle#Radio:  STARTED RxBleRadioOperationCharacteristicRead(32706505)
07-31 13:26:54.809 1980-2244/com.android.bluetooth I/bt_btif_gatt: set_read_value unformat.len = 20 
07-31 13:26:54.811 25060-25072/com.packet.sniffer D/RxBle#BluetoothGatt: onCharacteristicRead characteristic=569a2000-b87f-490c-92cb-11ba5ea5167c status=0
07-31 13:26:54.813 25060-25243/com.packet.sniffer I/BLE Controller: checking if tx is cleared: [B@c805018
07-31 13:26:54.813 25060-25243/com.packet.sniffer I/BLE Controller: BT has been renamed! :[B@5999a71
07-31 13:26:54.813 25060-25243/com.packet.sniffer I/BLE Controller: checking if tx is cleared: [B@c805018
07-31 13:26:54.816 25060-25187/com.packet.sniffer D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicRead(32706505)
07-31 13:26:54.817 25060-25187/com.packet.sniffer D/RxBle#Radio:  STARTED RxBleRadioOperationCharacteristicRead(39309874)
07-31 13:26:54.908 1980-2244/com.android.bluetooth I/bt_btif_gatt: set_read_value unformat.len = 20 
07-31 13:26:54.910 25060-25071/com.packet.sniffer D/RxBle#BluetoothGatt: onCharacteristicRead characteristic=569a2000-b87f-490c-92cb-11ba5ea5167c status=0
07-31 13:26:54.911 25060-25243/com.packet.sniffer I/BLE Controller: checking if tx is cleared: [B@a1f9bcf
07-31 13:26:54.911 25060-25243/com.packet.sniffer I/BLE Controller: BT has been renamed! :[B@208995c
07-31 13:26:54.911 25060-25243/com.packet.sniffer I/BLE Controller: checking if tx is cleared: [B@a1f9bcf
07-31 13:26:54.914 25060-25187/com.packet.sniffer D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicRead(39309874)
07-31 13:26:56.467 1980-2388/com.android.bluetooth D/HeadsetStateMachine: Disconnected process message: 10, size: 0

UPDATE 07/31/2017:

Updated to make previous code synchronous

byteArrayBatchObservable.flatMap(bytesBatch -> // using batches of data to write...
byteArrayBatchObservable.flatMap(bytesBatch -> // using batches of data to write...
    connectionObservable.flatMap(rxBleConnection -> rxBleConnection.writeCharacteristic(vspT, bytesBatch)
                            .flatMap(writtenBytes -> { // ...and when each batch will be written...
                                    final Func1<byte[], Boolean> filterFunction = txBytes -> checkIfZero(txBytes);
                                    return rxBleConnection
                                            .readCharacteristic(vspT.getUuid())
                                            .repeat() // ...and repeat it...
                                            .takeUntil(filterFunction)
                                            .filter(filterFunction) // ...but don't emit anything until then...
                                            .map(readBytes -> writtenBytes); // ...and emit the writtenBytesBatch...
                                }
                            ))
, 1)

Unfortunately still not working, here are the relevant logs:

4:55:35.108 25084-25084/com.packet.sniffer I/BLE Controller: sending updated name
07-31 14:55:35.110 25084-25084/com.packet.sniffer D/RxBle#Radio:   QUEUED RxBleRadioOperationCharacteristicWrite(135224277)
07-31 14:55:35.113 25084-25209/com.packet.sniffer D/RxBle#Radio:  STARTED RxBleRadioOperationCharacteristicWrite(135224277)
07-31 14:55:35.113 25084-25084/com.packet.sniffer I/BluetoothLEController: verify connectivity
07-31 14:55:35.219 25084-25096/com.packet.sniffer D/RxBle#BluetoothGatt: onCharacteristicWrite characteristic=569a2000-b87f-490c-92cb-11ba5ea5167c status=0
07-31 14:55:35.225 25084-25243/com.packet.sniffer D/RxBle#Radio:   QUEUED RxBleRadioOperationCharacteristicRead(155469961)
07-31 14:55:35.228 25084-25209/com.packet.sniffer D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicWrite(135224277)
07-31 14:55:35.229 25084-25209/com.packet.sniffer D/RxBle#Radio:  STARTED RxBleRadioOperationCharacteristicRead(155469961)
07-31 14:55:35.316 25084-25097/com.packet.sniffer D/RxBle#BluetoothGatt: onCharacteristicRead characteristic=569a2000-b87f-490c-92cb-11ba5ea5167c status=0
07-31 14:55:35.317 25084-25243/com.packet.sniffer I/BLE Controller: checking if tx is cleared: [B@f0396a7
07-31 14:55:35.317 25084-25243/com.packet.sniffer I/BLE Controller: BT has been renamed! :[B@8983754
07-31 14:55:35.317 25084-25243/com.packet.sniffer I/BLE Controller: checking if tx is cleared: [B@f0396a7
07-31 14:55:35.320 25084-25209/com.packet.sniffer D/RxBle#Radio: FINISHED RxBleRadioOperationCharacteristicRead(155469961)

1 Answers1

0

From the documentation of setWriteOperationAckStrategy():

If you want to delay the next batch use provided observable and add some custom behavior (delay, waiting for a message from the device, etc.)

Judging from your snippet here:

.setWriteOperationAckStrategy(new RxBleConnection.WriteOperationAckStrategy() {
    @Override
    public Observable<Boolean> call(Observable<Boolean> booleanObservable) {
        return Observable.just(true); // this is supposed to tell the LongWriteBuilder that we should continue sending data, correct?
    }
})

You are not interested in delay of the next batch. In this situation you can just leave out setting the WriteOperationAckStrategy as the documentation states: If this is not specified - the next batch of bytes is written right after the previous one has finished. Which is an equavilent to:

.setWriteOperationAckStrategy(new RxBleConnection.WriteOperationAckStrategy() {
    @Override
    public Observable<Boolean> call(Observable<Boolean> objectObservable) {
        return objectObservable;
    }
})

Edit: To delay the next batch write there is a need to delay the ACK signal. As the Long Write operation is making sure no other operation will happen in between writes - the only possible option is to relay on characteristic notifications / indications or other side channel events.

Edit 1: Alternative approach without using the Long Write could look like this. Consider a class that would make batches of a long byte[] that is needed to be written:

public class ByteArrayBatchObservable extends Observable<byte[]> {

    public ByteArrayBatchObservable(@NonNull final byte[] bytes, final int maxBatchSize) {
        super(SyncOnSubscribe.createSingleState(
                new Func0<ByteBuffer>() {
                    @Override
                    public ByteBuffer call() {
                        return ByteBuffer.wrap(bytes);
                    }
                },
                new Action2<ByteBuffer, Observer<? super byte[]>>() {
                    @Override
                    public void call(ByteBuffer byteBuffer, Observer<? super byte[]> observer) {
                        int nextBatchSize = Math.min(byteBuffer.remaining(), maxBatchSize);
                        if (nextBatchSize == 0) {
                            observer.onCompleted();
                            return;
                        }
                        final byte[] nextBatch = new byte[nextBatchSize];
                        byteBuffer.get(nextBatch);
                        observer.onNext(nextBatch);
                    }
                }
        ));
    }
}

Then in your code you could use a similar code to:

final ByteArrayBatchObservable byteArrayBatchObservable = new ByteArrayBatchObservable(longByteArrayToWrite, maxBatchSize); // create an observable that will make chunks of data small enough to write at once
return byteArrayBatchObservable.flatMap(bytesBatch -> // using batches of data to write...
                rxBleConnection.writeCharacteristic(characteristicUuid, bytesBatch) // ...write them on characteristic...
                        .flatMap(writtenBytes -> { // ...and when each batch will be written...
                                    final Func1<byte[], Boolean> filterFunction = txBytes -> txBytes.length == 1 && txBytes[0] == 0;
                                    return rxBleConnection
                                            .readCharacteristic(txCharacteristicUuid) // ...start reading the TX characteristic...
                                            .repeat() // ...and repeat it...
                                            .takeUntil(filterFunction) // ...until the read value will indicate that the device is ready for the next batch...
                                            .filter(filterFunction) // ...but don't emit anything until then...
                                            .map(readBytes -> writtenBytes); // ...and emit the writtenBytesBatch...
                                }
                        ),
        1 // ...to be sure that only one .writeCharacteristic() will be subscribed at any given time
);

Edit 2: Per what you wanted to achieve when writing a long name:

  1. Write a batch of data
  2. Perform a read of the device readiness flag
  3. If device is not ready go back to 2.
  4. If there is still more data to write go back to 1.

What you can see in logs:

  1. Batch of data is written
  2. Another batch of data is written
  3. Read is performed
  4. Another read is performed

And that is exactly what you have in your code as you are not synchronizing individual batches. It is probably a copy-paste error because of the place where you have put the .flatMap(Observable, 1):

final ByteArrayBatchObservable byteArrayBatchObservable = new ByteArrayBatchObservable(newName, 19);
Log.i("BLE Controller","sending updated name"); 
byteArrayBatchObservable.flatMap(bytesBatch -> // using batches of data to write...
    connectionObservable.flatMap(rxBleConnection -> rxBleConnection.writeCharacteristic(vspT, bytesBatch) 
        .flatMap(writtenBytes -> { // ...and when each batch will be written...
            final Func1<byte[], Boolean> filterFunction = txBytes -> checkIfZero(txBytes);
            return rxBleConnection
                    .readCharacteristic(vspT.getUuid()) 
                    .repeat() // ...and repeat it...
                    .takeUntil(filterFunction)
                    .filter(filterFunction) // ...but don't emit anything until then...
                    .map(readBytes -> writtenBytes); // ...and emit the writtenBytesBatch...
        }
    ), 1)

)

Where this code should be only a bit different to achieve synchronous behaviour:

final ByteArrayBatchObservable byteArrayBatchObservable = new ByteArrayBatchObservable(newName, 19);
Log.i("BLE Controller","sending updated name"); 
byteArrayBatchObservable.flatMap(bytesBatch -> // using batches of data to write...
    connectionObservable.flatMap(rxBleConnection -> rxBleConnection.writeCharacteristic(vspT, bytesBatch) 
        .flatMap(writtenBytes -> { // ...and when each batch will be written...
            final Func1<byte[], Boolean> filterFunction = txBytes -> checkIfZero(txBytes);
            return rxBleConnection
                    .readCharacteristic(vspT.getUuid()) 
                    .repeat() // ...and repeat it...
                    .takeUntil(filterFunction)
                    .filter(filterFunction) // ...but don't emit anything until then...
                    .map(readBytes -> writtenBytes); // ...and emit the writtenBytesBatch...
        }
    ))
    .take(1)
, 1)
Dariusz Seweryn
  • 3,212
  • 2
  • 14
  • 21
  • Hmm, what you're saying makes sense. In my case, I actually do want to delay the next batch. According to the documentation of my device, I need to wait for the TX characteristic of the Virtual Serial to be cleared (set to 0x00) before I can send the next batch. Would the correct solution be to implement the following: https://pastebin.com/WDuKVwYz – packet_sniffer Mar 30 '17 at 20:54
  • No - the pasted code would not work. First of all - the `WriteOperationAckStrategy` must return an `Observable` not a `Subscription`. Secondly in the pasted code there is no filtering for value `0x00` so the next batch would be sent even if the read value would be different. Does the `TX characteristic` support notifications? – Dariusz Seweryn Mar 30 '17 at 21:07
  • Ahh yes, please excuse my beginner mistakes... I am attempting to learn RxJava while implementing this library. I have done a quick read through of RxJava concepts, and I'm trying to apply them as I code. Unfortunately the TX characteristic does not support notifications. That being said, I'm a little stumped. If I return an observable for the TX characteristic, where would I filter the data? https://pastebin.com/zfPUUisR – packet_sniffer Mar 30 '17 at 21:16
  • The `WriteOperationAckStrategy` is effectively an equavilent of `Observable.repeatWhen()` operator. The filtering should happen inside the `WOAS` to trigger the repeat when ready. Please update the original post with the new information. I have an idea how to approach your scenario but I do not have a setup at the moment and I am going to sleep. I will update the answer tomorrow's morning. – Dariusz Seweryn Mar 30 '17 at 21:31
  • `connectionObservable.readCharacteristic(txCharacteristicUuid)` did not work for me, so instead I tried to register a flatmap to connectionObservable, which sounds correct to me, but I was unable to accomplish that as well – packet_sniffer Apr 01 '17 at 07:28
  • Yeah, I made a mistake in the answer and have just corrected it. Try it out and if it will not work - try to add information why (logs for instance could he helpful) – Dariusz Seweryn Apr 01 '17 at 09:12
  • I receive a compiler error `Error:(789, 49) error: incompatible types: cannot infer type-variable(s) E (argument mismatch; Observable is not a functional interface) where E,T are type-variables: E extends Object declared in method takeUntil(Observable extends E>) T extends Object declared in class Observable` Background information. I am running on API Level 21 and using retrolambda – packet_sniffer Apr 03 '17 at 19:38
  • Ah... yes - I forgot that the ACK contract needs a boolean at the end. Again edited my response. – Dariusz Seweryn Apr 03 '17 at 20:02
  • To test the code, I have inserted the longWrite code into the sample code CharacteristicOperationExampleActivity. Unfortunately, the code you have provided does not work, and I'm unsure if it is a bigger problem. Here are the relevant logs: https://pastebin.com/PvPV8iDA – packet_sniffer Apr 03 '17 at 20:30
  • That is true. Long write operation is making sure that no other operation will be executed in the meantime meaning that only notifications / indications or other side-channel events may be used to delay the ACK. That is what is happening when you try to solve problems late at night. `TX characteristic` needs to support notifications / indications to be used with Long Write. – Dariusz Seweryn Apr 03 '17 at 20:41
  • You can still create your own flow of `Observable`s that will expose a desired behaviour without usage of Long Write which blocks the radio. Alternatively you could use a Custom Operation but it is an option that demands a significant knowledge of how the Android API works. I will try to propose a workaround using a standard set of operations. It will not be an atomic operation like Long Write but it should work. – Dariusz Seweryn Apr 03 '17 at 20:51
  • Added an alternative answer. – Dariusz Seweryn Apr 04 '17 at 09:07
  • I have not had a chance to test it, but will let you know soon! Hoping to get it tested tomorrow. Thanks for your help – packet_sniffer Apr 06 '17 at 00:21
  • This completely slipped my mind, and I was unable to fix this problem and now I am revisiting this. In your latest update, I have tried to convert from using rxBleConnection to using the connectionObservable. It appears as though the first batch is written successfully, but even though the subscribe function returns a successfully written bytearray both times, the bluetooth device only sees the first batch. See update on original post for code – packet_sniffer Jul 30 '17 at 21:47
  • What version of the library do you use? Current one is `1.3.3` you can start with updating it. Prior to version `1.2.3` most of the `Observables` created by `RxBleConnection` were caching the results so they were effectively for one time usage only. – Dariusz Seweryn Jul 31 '17 at 09:25
  • I'm currently using 1.3.3. I try to keep it updated as much as possible :) – packet_sniffer Jul 31 '17 at 17:07
  • Updated my original post with new logs. Seems that the second write is not returning as I only see "BT has been renamed only once" – packet_sniffer Jul 31 '17 at 22:12
  • I have edited the last code sample but try to find out what is happening and why. – Dariusz Seweryn Aug 01 '17 at 06:03