RxAndroidBle waiting for response from peripheral when write on it (not long write) in Kotlin - kotlin

I am trying to write to a peripheral in Android Kotlin using RxAndroidBle. The application writes to the peripheral and then the peripheral responds if this write request is successful, i.e.
According to the evaluation made of the information sent to the peripheral, the peripheral sends a response to the app if it is the expected information, if not the expected information, then the peripheral responds with a different response; In summary, it is a scenario very similar to an HTTP request via POST, information is sent and the server responds with a status if the information meets the requirements. I already managed to connect perfectly and read information from the peripheral in the following way:
override fun connectDeviceToGetInfoHardwareByBle(mac: String): Observable<Resource<HardwareInfoResponse>> {
val device: RxBleDevice = bleClient.getBleDevice(mac)
return Observable.defer {
device.bluetoothDevice.createBond()// it is a blocking function
device.establishConnection(false) // return Observable<RxBleConnection>
}
.delay(5, TimeUnit.SECONDS)
.flatMapSingle { connection ->
connection.requestMtu(515)
.flatMap {
Single.just(connection)
}
}
.flatMapSingle {
it.readCharacteristic(UUID.fromString(GET_HARDWARE_INFORMATION_CHARACTERISTIC))
.map { byteArray ->
evaluateHardwareInfoResponse(byteArray = byteArray)
}
}
.map {
Resource.success(data = it)
}
.take(1)
.onErrorReturn {
Timber.i("Rointe Ble* Error getting ble information. {$it}")
Resource.error(data = null, message = it.message.toString())
}
.doOnError {
Timber.i("Rointe Ble*","Error getting ble information."+it)
}
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
}
As you can see, the MTU is needed by the peripheral, and it answers what I need. After that response, I close that BLE connection and the app does another independent job on the network (HTTP). Then it is required to connect again but this time it is necessary to write JSON information to the peripheral and the device analyzes that JSON and gives some answers that I need as a return; How do I implement a write waiting for a response from the peripheral? Is it necessary to do a long-write for a JSON since I'm assigning MTU on the connection? I'm developing this in Kotlin under the Repository pattern.
The JSON sent is this:
{
"data": {
"id_hardware": "[ID_HARDWARE]",
"product_brand": <value>,
"product_type": <value>,
"product_model": <value>,
"nominal_power": <value>,
"industrialization_process_date": <value>,
"platform_api_path": "[Host_API_REST]",
"platform_streaming_path": "[Host_STREAMING]",
"updates_main_path": "[Host_UPDATES]",
"updates_alternative_path": "[Host_ALTERNATIVE_UPDATES]",
"check_updates_time": <value>,
"check_updates_day": <value>,
"auth_main_path": "[Host_AUTHORIZATION]",
"auth_alternative_path": "[Host_BACKUP_AUTHORIZATION]",
"analytics_path": "[Host_ANALYTICS]",
"idToken": "[ID_TOKEN]",
"refreshToken": "[REFRESH_TOKEN]",
"expiresIn": "3600",
"apiKey": "[API_KEY]",
"factory_wifi_ssid": "[FACTORY_WIFI_SSID]",
"factory_wifi_security_type": "[FACTORY_WIFI_TYPE]",
"factory_wifi_passphrase": "[FACTORY_WIFI_PASS]",
"factory_wifi_dhcp": 1,
"factory_wifi_device_ip": "[IPv4]",
"factory_wifi_subnet_mask": "[SubNetMask_IPv4]",
"factory_wifi_gateway": "[IPv4]"
},
"factory_version": 1,
"crc": ""
}
The peripheral analyzes that JSON and gives me some answers according to the JSON sent.
Now, the way I try to do the write expecting a response is this:
private fun setupNotifications(connection: RxBleConnection): Observable<Observable<ByteArray>> =
connection.setupNotification(UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC))
private fun performWrite(connection: RxBleConnection, notifications: Observable<ByteArray>, data: ByteArray): Observable<ByteArray> {
return connection.writeCharacteristic(UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC), data).toObservable()
}
override fun connectDeviceToWriteFactorySettingsByBle(mac: String, data: ByteArray): Observable<Resource<HardwareInfoResponse>> {
val device: RxBleDevice = bleClient.getBleDevice(mac)
return Observable.defer {
//device.bluetoothDevice.createBond()// it is a blocking function
device.establishConnection(false) // return Observable<RxBleConnection>
}
.delay(5, TimeUnit.SECONDS)
.flatMapSingle { connection ->
connection.requestMtu(515)
.flatMap {
Single.just(connection)
}
}
.flatMap(
{ connection -> setupNotifications(connection).delay(5, TimeUnit.SECONDS) },
{ connection, deviceCallbacks -> performWrite(connection, deviceCallbacks, data) }
)
.flatMap {
it
}
//.take(1) // after the successful write we are no longer interested in the connection so it will be released
.map {
Timber.i("Rointe Ble: Result write: ok ->{${it.toHex()}}")
Resource.success(data = evaluateHardwareInfoResponse(it))
}
//.take(1)
.onErrorReturn {
Timber.i("Rointe Ble: Result write: failed ->{${it.message.toString()}}")
Resource.error(data = HardwareInfoResponse.NULL_HARDWARE_INFO_RESPONSE, message = "Error write on device.")
}
.doOnError {
Timber.i("Rointe Ble*","Error getting ble information."+it)
}
//.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
}
As can be seen, the MTU is negotiated to the maximum and a single packet is sent (json file shown).
When I run my code it connects but shows this error:
com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException:
Cannot find client characteristic config descriptor (code 2) with
characteristic UUID 4f4a4554-4520-4341-4c4f-520001000002
Any help on Kotlin?
Thanks a lot!!

When I run my code it connects but shows this error:
com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException:
Cannot find client characteristic config descriptor (code 2) with
characteristic UUID 4f4a4554-4520-4341-4c4f-520001000002
You can fix this in two ways:
Change your peripheral code to include a Client Characteristic Config Descriptor on the characteristic that you want to use notifications on – this is the preferred way as it would make the peripheral conform to Bluetooth Specification
Use COMPAT mode when setting up notification which does not set CCCD value at all
How to clean UUID's characteristics cache? what happens is the library remember in cache maybe the last UUID registered. How I clean this cache?
It is possible to clear the cache by using BluetoothGatt#refresh and subsequently getting the new set of services which will allow bypassing the library UUID helper — you need to use functions that accept BluetoothGattCharacteristic instead of UUID.
Code that refreshes BluetoothGatt:
RxBleCustomOperation<Void> bluetoothGattRefreshCustomOp = (bluetoothGatt, rxBleGattCallback, scheduler) -> {
try {
Method bluetoothGattRefreshFunction = bluetoothGatt.getClass().getMethod("refresh");
boolean success = (Boolean) bluetoothGattRefreshFunction.invoke(bluetoothGatt);
if (!success) return Observable.error(new RuntimeException("BluetoothGatt.refresh() returned false"));
return Observable.<Void>empty().delay(500, TimeUnit.MILLISECONDS);
} catch (NoSuchMethodException e) {
return Observable.error(e);
} catch (IllegalAccessException e) {
return Observable.error(e);
} catch (InvocationTargetException e) {
return Observable.error(e);
}
};
Code that discovers services bypassing the library caches:
RxBleCustomOperation<List<BluetoothGattService>> discoverServicesCustomOp = (bluetoothGatt, rxBleGattCallback, scheduler) -> {
boolean success = bluetoothGatt.discoverServices();
if (!success) return Observable.error(new RuntimeException("BluetoothGatt.discoverServices() returned false"));
return rxBleGattCallback.getOnServicesDiscovered()
.take(1) // so this RxBleCustomOperation will complete after the first result from BluetoothGattCallback.onServicesDiscovered()
.map(RxBleDeviceServices::getBluetoothGattServices);
};

Related

how to gomobile stream communication with channels or callbacks

I am trying to implement a chat client with a function Listen() which is forever listening to incoming messages. In my exposed function I convert a channel by iterating over everything it sends and forwarding it with a callback by the receiver (eg Kotlin).
func (c *Chat) Listen(receiver MessageReceiver) error {
return c.auth.WithToken(func(token string) error {
ch, err := c.repo.Listen(token)
if err != nil {
return err
}
for msg := range ch {
receiver.Add(&Message{
From: msg.From,
Content: msg.Content,
})
}
return nil
})
}
When I call this in Kotlin I would like to transform it to a flow - but I have some problems with the load on the main thread - and is constantly getting messages about missing frames on the main thread.
fun listen() = channelFlow {
chat.listen {
trySendBlocking(Message(it.content, it.from, MessageType.IN))
}
}
Does anyone know how to deal with this type of usecase of gomobile (bind) ?

Java Selector for socket client not waked up after changing of interested ops from different thread

I use Java Selector for both server and client. For Server side it works perfect. It stops the thread when i call select() and wakes up when i change interest ops and it is ready for this operation..
But unfortunatelt it does not work for the same way for socket client. It stops the thread and does not wake up for reading or writing when i change interestedOps.
Creation of client connection:
selector = Selector.open()
SocketChannel.open().apply {
configureBlocking(false)
connect(address)
val key = socket.register(selector, SelectionKey.OP_READ or SelectionKey.OP_CONNECT)
val connection = ClientConnection(key) // Some stuff to hold the key for events
key.attach(connection)
}
Handle selection inside while loop:
val readyChannels = selector.select()
if (readyChannels == 0) continue
val keyIterator = selector.selectedKeys().iterator()
while (keyIterator.hasNext()) {
val key = keyIterator.next()
when (key.readyOps()) {
SelectionKey.OP_CONNECT -> {
val socket = (key.channel() as SocketChannel)
socket.finishConnect()
key.interestOps(key.interestOps() and SelectionKey.OP_CONNECT.inv())
// WORKS FINE!!!!!
key.interestOps(key.interestOps() and SelectionKey.OP_WRITE)
// Does not work at all. Selector will not wake up!
Thread(){
key.interestOps(key.interestOps() and SelectionKey.OP_WRITE)
}.start()
}
SelectionKey.OP_READ -> readPackets(key)
SelectionKey.OP_WRITE -> writePackets(key)
SelectionKey.OP_READ or SelectionKey.OP_WRITE -> {
writePackets(key)
readPackets(key)
}
}
keyIterator.remove()
}
So. The changing of interestOps from different thread does not work for socket clients. But it works fine for Server sockets..
Found solutions:
selector.select(300) -> use some timeout to wake up selector
selector.selectNow() -> use non blocking method and check the count of evetns
selector.wakeUp() -> save instance and wakeup it manually..
The question is Why it does not work ? Did I do some mistake? Something Missed?
UPD: Server side socket and selector
Creation of server socket:
selector = Selector.open()
serverSocket = ServerSocketChannel.open().apply {
socket().bind(address)
configureBlocking(false)
register(selector, SelectionKey.OP_ACCEPT)
}
Iteration of the selector inside Loop:
val readyChannels = selector.select()
if (readyChannels == 0) continue
val keyIterator = selector.selectedKeys().iterator()
while (keyIterator.hasNext()) {
val key = keyIterator.next()
when (key.readyOps()) {
SelectionKey.OP_ACCEPT -> {
val socket = serverSocket.accept().apply {
configureBlocking(false)
}
val client = clientFactory.createClient(selector,socket)
// Coroutines with Another thread context.
// There interestOps will be changed to send first data
_selectionAcceptFlow.tryEmit(client)
}
SelectionKey.OP_READ -> readPackets(key)
SelectionKey.OP_WRITE -> writePackets(key)
SelectionKey.OP_READ or SelectionKey.OP_WRITE -> {
writePackets(key)
readPackets(key)
}
}
keyIterator.remove()
}
If you call key.setInterestOps from a separate thread, you are creating a race condition between that call and the call to selector.select() in the client loop.
Your initial call to register does not contain SelectorKey.OP_WRITE. The first event triggered will be SelectorKey.OP_CONNECT. When handling that event, you indicate that in the future you are also interested in processing OP_WRITE.
If you do that in the same thread, then you are guaranteed that the interestOps are set the way you want them before the client loop reaches the call to selector.select(). If there is an OP_WRITE event available, you will process it immediatelly, otherwise the call blocks until it is available.
If you do that in a separate thread, then, depending on timing, you may run into a case where the client loop reaches the call to selector.select() and blocks even though there is an OP_WRITE event available. Since the separate thread did not yet change the interestOps, the OP_WRITE event is ignored.
I've included a self-contained example (client sending a message to server). To test different cases, you can comment/uncomment sections around line 90.
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.SelectionKey
import java.nio.channels.Selector
import java.nio.channels.ServerSocketChannel
import java.nio.channels.SocketChannel
import java.util.concurrent.CountDownLatch
val address = InetSocketAddress("localhost", 5454)
fun main() {
val serverSocketSignal = CountDownLatch(1)
Thread {
startServer(serverSocketSignal)
}.start()
Thread {
startClient(serverSocketSignal)
}.start()
}
fun startServer(serverSocketSignal: CountDownLatch) {
//prepare server socket
val selector = Selector.open()
val serverSocket = ServerSocketChannel.open().apply {
socket().bind(address)
configureBlocking(false)
register(selector, SelectionKey.OP_ACCEPT)
}
serverSocketSignal.countDown();
//run server loop
while (true) {
println("Server loop")
val readyChannels = selector.select()
if (readyChannels == 0) continue
val keyIterator = selector.selectedKeys().iterator()
while (keyIterator.hasNext()) {
val key = keyIterator.next()
when (key.readyOps()) {
SelectionKey.OP_ACCEPT -> {
println("Server ACCEPT")
val socket = serverSocket.accept().apply {
configureBlocking(false)
}
socket.register(selector, SelectionKey.OP_READ)
}
SelectionKey.OP_READ -> {
val buffer = ByteBuffer.allocate(1024)
val count = (key.channel() as SocketChannel).read(buffer)
val message = String(buffer.array(), 0, count)
println("Server READ - " + message)
}
}
keyIterator.remove()
}
}
}
fun startClient(serverSocketSignal: CountDownLatch) {
serverSocketSignal.await();
//prepare client socket
val selector = Selector.open()
SocketChannel.open().apply {
configureBlocking(false)
connect(address)
register(selector, SelectionKey.OP_CONNECT or SelectionKey.OP_READ)
}
//run client loop
while (true) {
println("Client loop")
val readyChannels = selector.select()
if (readyChannels == 0) continue
val keyIterator = selector.selectedKeys().iterator()
while (keyIterator.hasNext()) {
val key = keyIterator.next()
when (key.readyOps()) {
SelectionKey.OP_CONNECT -> {
println("Client CONNECT")
val socket = (key.channel() as SocketChannel)
socket.finishConnect()
key.interestOpsAnd(SelectionKey.OP_CONNECT.inv())
/*
This works
*/
key.interestOps(SelectionKey.OP_WRITE)
/*
This doesn't work because we're And-ing the interestOps an the OP_WRITE op was not specified when calling register()
*/
// key.interestOpsAnd(SelectionKey.OP_WRITE)
/*
This may or may not work, depending on which thread gets executed first
- it will work if the setting interestOps=OP_WRITE in the new thread gets executed before the selector.select() in the client loop
- it will not work if selector.select() in the client loop gets executed before setting interestOps=OP_WRITE in the new thread,
since there won't be anything to process and the selector.select() gets blocked
On my machine, pausing the client loop even for a small duration was enough to change the result (e.g. the Thread.sleep(1) below).
* */
// Thread {
// println("Client setting interestedOps to OP_WRITE from new thread")
// key.interestOps(SelectionKey.OP_WRITE)
// }.start()
// //Thread.sleep(1)
}
SelectionKey.OP_WRITE -> {
println("Client WRITE")
val buffer = ByteBuffer.wrap("test message from client".toByteArray());
(key.channel() as SocketChannel).write(buffer)
key.interestOps(0)
}
}
keyIterator.remove()
}
}
}
As for why it works for you on the server side - you would have to share the full code for the server and client (might be a timing issue or your selector might be woken up by some events you did not intend to listen for). The snippets provided in the question do not contain enough infomation.

How to return a Flux in async/reactive webclient request with subscribe method

I am using spring hexagonal architecture (port and adapter) as my application need to read the stream of data from the source topic, process/transforms the data, and send it to destination topic.
My application need to do the following actions.
Read the data (which will have the call back url)
Make an http call with the url in the incoming data (using webclient)
Get the a actual data and it needs to be transformed into another format.
Send the transformed data to the outgoing topic.
Here is my code,
public Flux<TargeData> getData(Flux<Message<EventInput>> message)
{
return message
.flatMap(it -> {
Event event = objectMapper.convertValue(it.getPayload(), Event.class);
String eventType = event.getHeader().getEventType();
String callBackURL = "";
if (DISTRIBUTOR.equals(eventType)) {
callBackURL = event.getHeader().getCallbackEnpoint();
WebClient client = WebClient.create();
Flux<NodeInput> nodeInputFlux = client.get()
.uri(callBackURL)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptTypes = new ArrayList<>();
acceptTypes.add(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(acceptTypes);
})
.exchangeToFlux(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(NodeInput.class);
}
return Flux.empty();
});
nodeInputFlux.subscribe( nodeInput -> {
SourceData source = objectMapper.convertValue(nodeInput, SourceData.class);
// return Flux.fromIterable(this.TransformImpl.transform(source));
});
}
return Flux.empty();
});
}
The commented line in the above code is giving the compilation as subscribe method does not allow return types.
I need a solution "without using block" here.
Please help me here, Thanks in advance.
I think i understood the logic. What do you may want is this:
public Flux<TargeData> getData(Flux<Message<EventInput>> message) {
return message
.flatMap(it -> {
// 1. marshall and unmarshall operations are CPU expensive and could harm event loop
return Mono.fromCallable(() -> objectMapper.convertValue(it.getPayload(), Event.class))
.subscribeOn(Schedulers.parallel());
})
.filter(event -> {
// 2. Moving the if-statement yours to a filter - same behavior
String eventType = event.getHeader().getEventType();
return DISTRIBUTOR.equals(eventType);
})
// Here is the trick 1 - your request below return Flux of SourceData the we will flatten
// into a single Flux<SourceData> instead of Flux<List<SourceData>> with flatMapMany
.flatMap(event -> {
// This WebClient should not be created here. Should be a singleton injected on your class
WebClient client = WebClient.create();
return client.get()
.uri(event.getHeader().getCallbackEnpoint())
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(SourceData.class);
}
return Flux.empty();
});
})
// Here is the trick 2 - supposing that transform return a Iterable of TargetData, then you should do this and will have Flux<TargetData>
// and flatten instead of Flux<List<TargetData>>
.flatMapIterable(source -> this.TransformImpl.transform(source));
}

Why doOnError logs different when subscribing with .subscribe in webflux

i need help to understand why the error message is not logged when subscribing to the stream with .subscribe(). However, the error is logged when subscribing with .block(). I understand that it might be to the block() call subscribing on the main thread. I was wondering which changes should i do in order to see the error logged when subscribing with .subscribe(). Also not sure whether it is the best way to deal listen for sqs messages on webflux.
Thanks,
#SqsListener(value = "${sqs.queues.email-notifications}", deletionPolicy = NEVER)
public void listenNotifications(MessageDto message, Acknowledgment acknowledgment) {
Mono.just(message)
.log("NotificationsSQSConsumer.listenNotifications")
.map(this::toSendEmailCommand)
.flatMap(sendEmailUseCase::handle)
.then(sendAck(acknowledgment))
.log("NotificationsSQSConsumer.sendAck")
//.block(); //.subscribe() nop**
}
private Mono<?> sendAck(Acknowledgment acknowledgment) {
return Mono.fromCallable(() -> acknowledgment.acknowledge().get())
.doOnError(throwable -> log.error("ERROR!:" + throwable.getMessage()))
.subscribeOn(Schedulers.boundedElastic());
}
class SendEmailUseCase {
public Mono<Void> handle(SendEMailCommand sendEMailCommand) {
return Mono.just(sendEMailCommand.getTemplateId())
.flatMap(templateLoader::getTemplate) //do a Mono.fromCallable on a bounded elastic
.map(s -> Tuples.of(new StringReader(s), sendEMailCommand.getTemplateId()))
.map(this::newMustache)
.map(c -> Tuples.of(c, sendEMailCommand.getTemplateData()))
.map(this::build)
.map(mailText -> Tuples.of(sendEMailCommand.getReceiver(), sendEMailCommand.getSubject(), mailText))
.map(this::newEmailMessage)
.flatMap(emailSender::sendMail); //do a Mono.fromCallable on bounded elastic
}
}
//Json
{
"dd.span_id":"0",
"dd.trace_id":"0",
"timestamp":"2021-03-04T15:53:00.588 03:00",
"event_type":"logging",
"level":"ERROR",
"thread_name":"boundedElastic-1",
"logger_name":"c.n.c.m.i.m.NotificationsSQSConsumer",
"message":"ERROR!:com.amazonaws.services.sqs.model.AmazonSQSException: Value AQEB/60/Nzg2oC9tdLlZT4BiztR8UAgd503EZ/bHQ7bE6WuKRfF59Y5l/2+gmqMZtSJs8sug6qUNuUt7pmXVM8G2S3TVt9yVc05t
}
I realized that i was missing the error callback on .subscribe()
#Async
#SqsListener(value = "${sqs.queues.email-notifications}", deletionPolicy = NEVER)
public void handle(MessageDto message, Acknowledgment acknowledgment) {
Mono.just(message)
.log("NotificationsSQSConsumer.listenNotifications")
.map(this::toSendEmailCommand)
.flatMap(sendEmailUseCase::handle)
.then(sendAck(acknowledgment))
.log("NotificationsSQSConsumer.sendAck")
.subscribe(o -> {
}, t -> log.error("ERROR!:" + t.getMessage()));
}
private Mono<?> sendAck(Acknowledgment acknowledgment) {
return Mono.fromCallable(() -> acknowledgment.acknowledge().get())
.subscribeOn(Schedulers.boundedElastic());
}

Run multiple tasks sequentially after emitter response

I am trying to create a communication controller for a hardware device that always responds with some delay. If I would only request one value, I could create a Single<ByteArray> and do the final conversion in .subscribe{ ...}.
But when I request more than one value I need to make sure that the second request happens after the first request has been fully closed.
Is that something that I can do with RxJava, e.g. defer? Or should I create a queue on my own and handle the sequence of events manually with my queue?
We're using RxJava anyway (and I'm obviously new to it) and of course it would be nice to use it for this purpose as well. But is that a good use-case?
Edit:
Code that I could use, but that wouldn't be generic enough:
hardware.write(byteArray)
.subscribe(
{
hardware.receiveResult().take(1)
.doFinally { /* dispose code */ }
.subscribe(
{ /* onSuccess */ }
{ /* onError */ }
.let { disposable = it }
},
{ /* onError */ }
)
All code for the next request in the queue could be put in the inner onSuccess and then the next one in that onSuccess. That would be executed sequentially but that wouldn't be generic enough. Any other class that makes a request would end up spoiling my sequence.
I am searching for a solution that builds up the queue automatic in the hardware communication controller class.
Long time passed, the project developed and we got a solution long time ago. Now I wanted to share it here:
fun writeSequential(data1: ByteArray, data2: ByteArray) {
disposable = hardwareWrite(data1)
.zipWith(hardwareWrite(data2))
.subscribe(
{
/* handle results.
it.first will be the first response,
it.second the second. */
},
{ /* handle error */ }
)
compositeDisposable.add(disposable)
}
fun hardwareWrite(data: ByteArray): Disposable {
var emitter: SingleEmitter<ByteArray>? = null
var single = Single.create<ByteArray> { emitter = it }
return hardware.write(data)
.subscribe(
{ hardwareRead(emitter) },
{ /* onError */ }
))
}
fun hardwareRead(emitter: SingleEmitter<ByteArray>): Disposable {
return hardware.receiveResult()
.take(1)
.timeout( /* your timeout */ )
.single( /* default value */ )
.doFinally( /* cleanup queue */ )
.subscribe(
{ emitter.onSuccess(it) }
{ emitter.onError(it) }
)
}
The solution is not perfect and now I see that the middle part doesn't do anything with the disposable result.
Also in out example it's a bit more complicated as hardwareWrite doesn't fire immediatelly but gets queued. This way we assure that the hardware is accessed sequentially and the result don't get mixed up.
Still I hope this might help someone, who is looking for a solution, and is maybe new to kotlin and/or RxJava stuff (like I was in the beginning of the project).