https://kotlinlang.org logo
#coroutines
Title
# coroutines
r

reactormonk

07/04/2022, 12:43 PM
I'm building a little helper on top of a callback routine via
suspendCoroutine
- but the code has some rather strict timing constraints, aka one code block should fully exit before another of the same kind should enter - how would I enforce that? Code looks like this:
Copy code
suspend fun BluetoothReader.transmitEscapeCommandS(input: ByteArray): CommandAPDU {
    return suspendCoroutine { block ->
        if (this.transmitEscapeCommand(input)) {
            this.setOnEscapeResponseAvailableListener { _, response, errorCode ->
                if (errorCode == 0) {
                    block.resume(CommandAPDU(response))
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnEscapeResponseAvailableListener(null)
            }
        } else {
            block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
        }
    }
}
The listener can only really be set once, so there's a bit of a race condition there.
j

Joffrey

07/04/2022, 12:53 PM
I'm not sure I understand how this is supposed to work. What triggers the thing that will make the callback fire? Is it
transmitEscapeCommand()
? If yes, then why do you call this before setting the listener? It could theoretically fire the listener before you even registered it (even without any other concurrent calls to your function)
Also, can
transmitEscapeCommand
return false without firing the listener?
r

reactormonk

07/04/2022, 12:56 PM
Ah, good point. Thanks. Yeah,
transmitEscapeCommand()
sends data to the hardware. Which then responds back via the set listener. A
false
from
transmitEscapeCommand()
means it couldn't be sent.
Yeah, that could totally happen in terms of race condition, but it's really unlikely. Probably should account for it 🤔
j

Joffrey

07/04/2022, 12:57 PM
You could simply register the listener first (in any case), then call
transmitEscapeCommand
, and then if it returns
false
set the listener to
null
before resuming with exception
r

reactormonk

07/04/2022, 12:58 PM
I like that idea, thanks!
j

Joffrey

07/04/2022, 12:58 PM
As for the initial question, you could use a
Mutex
to prevent concurrent calls to your function from stepping on each other's toes
r

reactormonk

07/04/2022, 12:59 PM
Copy code
val transmitEscapeCommandLock = Mutex()

suspend fun BluetoothReader.transmitEscapeCommandS(input: ByteArray): CommandAPDU {
    return transmitEscapeCommandLock.withLock {
        suspendCoroutine { block ->
        this.setOnEscapeResponseAvailableListener { _, response, errorCode ->
            if (errorCode == 0) {
                block.resume(CommandAPDU(response))
            } else {
                block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
            }
            this.setOnEscapeResponseAvailableListener(null)
        }
        if (this.transmitEscapeCommand(input)) {
        } else {
            block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            this.setOnEscapeResponseAvailableListener(null)
        }
    }
}
^ final code
j

Joffrey

07/04/2022, 1:01 PM
Yep, I would just rewrite the last
if
this way:
Copy code
val escapeSent = transmitEscapeCommand(input)
if (!escapeSent) {
    setOnEscapeResponseAvailableListener(null)
    block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
}
(set the listener to null first, by the way)
r

reactormonk

07/04/2022, 1:01 PM
Yeah, IDE reminded me of that ^^
Good point, might get wonky otherwise
Copy code
suspend fun BluetoothReader.transmitEscapeCommandS(input: ByteArray): CommandAPDU {
    return transmitEscapeCommandLock.withLock {
        suspendCoroutine { block ->
            this.setOnEscapeResponseAvailableListener { _, response, errorCode ->
                if (errorCode == 0) {
                    block.resume(CommandAPDU(response))
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnEscapeResponseAvailableListener(null)
            }
            if (this.transmitEscapeCommand(input)) {
                Log.d("BluetoothSupport", "Sending command $input")
            } else {
                this.setOnEscapeResponseAvailableListener(null)
                block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            }
        }
    }
}
👍 1
j

Joffrey

07/04/2022, 1:05 PM
That works too! I just like the explanatory variable to highlight the meaning of the return value of the
transmit
function. But that's again just my taste/style. Also in Kotlin it's not necessary to use explicit
this
, and not very common to do so.
Note that another option to avoid concurrent calls would be to use
withContext
with a single-threaded dispatcher (or a dispatcher with
limitedParallelism
) but that usually implies more context-switching and I would say it's not very desirable to force the dispatcher at such low level
r

reactormonk

07/04/2022, 1:05 PM
I think I'm using
this
here because it's an extension function, feels kinda weird otherwise.
Nah, you can switch dispatchers around IIRC, so it could be messed up otherwise. So a
Mutex
is the correct solution here IMO.
j

Joffrey

07/04/2022, 1:08 PM
If your function is defined like
suspend fun BluetoothReader.transmitEscapeCommandS(...) = withContext(yourDispatcher) { .. }
, the callers won't be able to override the dispatcher. But that's not very good practice especially because of that - it makes it hard to test for instance. But yeah, in any case, I agree that
Mutex
is better, which is my initial point 🙂
r

reactormonk

07/04/2022, 1:11 PM
Copy code
val enableNotificationSLock = Mutex()

suspend fun BluetoothReader.enableNotificationS(input: Boolean) {
    return enableNotificationSLock.withLock {
        suspendCoroutine { block ->
            this.setOnEnableNotificationCompleteListener { _, errorCode ->
                if (errorCode == 0) {
                    block.resume(Unit)
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnEnableNotificationCompleteListener(null)
            }
            val sent = this.enableNotification(input)
            if (sent) {
                Log.d("BluetoothSupport", "Sent notification request $input")
            } else {
                this.setOnEnableNotificationCompleteListener(null)
                block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            }
        }
    }
}

val transmitApduSLock = Mutex()

suspend fun BluetoothReader.transmitApduS(input: ByteArray): CommandAPDU {
    return transmitApduSLock.withLock {
        suspendCoroutine { block ->
            this.setOnResponseApduAvailableListener { _, response, errorCode ->
                if (errorCode == 0) {
                    block.resume(CommandAPDU(response))
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnResponseApduAvailableListener(null)
            }
            val sent = this.transmitApdu(input)
            if (sent) {
                Log.d("BluetoothSupport", "Sent apdu $input")
            } else {
                this.setOnResponseApduAvailableListener(null)
                block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            }
        }
    }
}

val transmitEscapeCommandLock = Mutex()

suspend fun BluetoothReader.transmitEscapeCommandS(input: ByteArray): CommandAPDU {
    return transmitEscapeCommandLock.withLock {
        suspendCoroutine { block ->
            this.setOnEscapeResponseAvailableListener { _, response, errorCode ->
                if (errorCode == 0) {
                    block.resume(CommandAPDU(response))
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnEscapeResponseAvailableListener(null)
            }
            val sent = this.transmitEscapeCommand(input)
            if (sent) {
                Log.d("BluetoothSupport", "Sent escape command $input")
            } else {
                this.setOnEscapeResponseAvailableListener(null)
                block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            }
        }
    }
}

val getDeviceInfoLock = Mutex()

// TODO figure out the exact type of Any
suspend fun BluetoothReader.getDeviceInfoS(input: Int): Pair<Int, Any> {
    return getDeviceInfoLock.withLock {
        suspendCoroutine { block ->
            this.setOnDeviceInfoAvailableListener { _, int, response, errorCode ->
                if (errorCode == 0) {
                    block.resume(Pair(int, response))
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnDeviceInfoAvailableListener(null)
            }
            val sent = this.getDeviceInfo(input)
            if (sent) {
                Log.d("BluetoothSupport", "Sent device info request $input")
            } else {
                this.setOnDeviceInfoAvailableListener(null)
                block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            }
        }
    }
}

//interface OnCardStatusChangeListener {
//    fun onCardStatusChange(var1: BluetoothReader?, var2: Int)
//}
//
//interface OnCardStatusAvailableListener {
//    fun onCardStatusAvailable(var1: BluetoothReader?, var2: Int, var3: Int)
//}
//
//interface OnCardPowerOffCompleteListener {
//    fun onCardPowerOffComplete(var1: BluetoothReader?, var2: Int)
//}

val authenticateSLock = Mutex()

suspend fun BluetoothReader.authenticateS(input: ByteArray): Unit {
    return authenticateSLock.withLock {
        suspendCoroutine { block ->
            this.setOnAuthenticationCompleteListener { _, errorCode ->
                if (errorCode == 0) {
                    block.resume(Unit)
                } else {
                    block.resumeWithException(ReaderCommandException("Error code from reader: $errorCode"))
                }
                this.setOnAuthenticationCompleteListener(null)
            }
            val sent = this.authenticate(input)
            if (sent) {
                Log.d("BluetoothSupport", "Sent auth request")
            } else {
                this.setOnAuthenticationCompleteListener(null)
                block.resumeWithException(ReaderCommandException("Couldn't send command. Bonded?"))
            }
        }
    }
}

//interface OnAtrAvailableListener {
//    fun onAtrAvailable(var1: BluetoothReader?, var2: ByteArray?, var3: Int)
//}
^ this is the full trainwreck... I'm wondering if there's any reasonable way to condense some of the pasta.
I tried a bit with abstracting over the different Listeners, but the compiler couldn't deal with me munging the functions around.
j

Joffrey

07/04/2022, 1:17 PM
It seems to me that the design of the underlying API was not really meant for one-shot callbacks after all, since basically there is a single listener of each type that you can register, and it's not a callback passed to the method that sends the message. It's a bit funky. At the same time, maybe I'm missing some information, but there doesn't seem to be a way to track which message was the cause of which callback call. I mean, if multiple calls to
authenticate()
happen for instance, how is the
OnAuthenticationCompleteListener
(in the original API) supposed to know which call the result is from? Is there something in the first argument to allow that?
r

reactormonk

07/04/2022, 1:18 PM
The hardware specification explicitly mentions there shall be no interleaving of requests
... so I cross my fingers and pray that's true.
It's a bit funky.
Very diplomatic of you.
😆 1
there doesn't seem to be a way to track which message was the cause of which callback call.
It's basically "the previous one on the timeline". According to the specs. Pretty sure that's gonna blow up somewhere
j

Joffrey

07/04/2022, 1:21 PM
I have never worked with Bluetooth directly, but every single time I saw something related to the topic, it was always pretty "funky" (not to say crappy) APIs 😆
r

reactormonk

07/04/2022, 1:22 PM
You pay for the hardware, not the software.
Found this gem in the example project:
Copy code
} catch (Exception e) {
            if (!e.getMessage().contains("null")) {
                return;
            }
🤦 1
If you agree it's probably a reasonable way to deal with the "funkiness", I'll leave it at that for now
j

Joffrey

07/04/2022, 1:24 PM
I guess sometimes you just have to, but this looks extreme. I mean why not a more specific exception type? Please don't tell me the underlying API throws instances of
Exception
directly...
r

reactormonk

07/04/2022, 1:28 PM
I don't think there's even exceptions, just `Boolean`s if it worked or not
j

Joffrey

07/04/2022, 1:28 PM
Oh btw, in order to avoid having to rename all the functions with
-S
suffixes, and more importantly to avoid mistakes of calling the original API directly, you might want to wrap the
BluetoothReader
into your own class. This also opens the door for things like registering a single listener of each type, and make them send to channels that the suspending wrappers will receive from. This way, you don't even have to use
suspendCoroutine
, nor to keep registering/unregistering new callback instances all the time.
r

reactormonk

07/04/2022, 1:30 PM
My intuition tells me that doesn't have the same restorative power as the current registering / unregistering in terms of weird state - if one command somehow fails, with the current version, you won't have off-by-one channel responses which will throw you off.
I like the wrapping idea, time for some copypasta
j

Joffrey

07/04/2022, 1:31 PM
That's fair, I was maybe putting too much trust in the boolean result of the calls? It feels like you lost all trust in this API - probably understandably.
r

reactormonk

07/04/2022, 1:32 PM
Not just the API, might also be that the device simply ran out of battery before it could transmit the command back 😄
👌 1
I'll have to double-check the bluetooth spec, maybe there's a sequence number somewhere, but I doubt it
3 Views