@OptIn(DelicateCoroutinesApi::class, ObsoleteCoroutinesApi::class) data class MessageProcessor(val ble: DkBleManager, val job: Job) { val TAG = "MessageProcessor" fun sendCommand(command: Command) { val ex = CancellationException("Could not send command to reader") requests.trySend(command).onFailure { Timber.e(ex) Timber.e(it) } } val cardChannel = Channel(10, BufferOverflow.DROP_OLDEST) private val incomingData = Channel(100, BufferOverflow.DROP_OLDEST, onUndeliveredElement = { Log.e(TAG, "Failed to process incoming data $it") }) private val requests = Channel(100, BufferOverflow.DROP_OLDEST, onUndeliveredElement = { it.callback.cancel( CancellationException("Too many requests have been stacked.") )}) private var currentRequest: Command? = null val scope = CoroutineScope(Dispatchers.IO + job) init { ble.writeReaderDataTo(incomingData) scope.launch { incomingData.consumeAsFlow().runningFold(ParsingState.NoData as ParsingState) { state, bytes -> parse(state, bytes) }.map { when(it) { ParsingState.NoData -> Log.d(TAG, "No Data") is ParsingState.InvalidFrameTag -> Log.e(TAG, it.toString()) is ParsingState.MessageTooShort -> Log.e(TAG, it.toString()) is ParsingState.NoPriorStartMessage -> Log.e(TAG, it.toString()) is ParsingState.WrongSerialNumber -> Log.e(TAG, it.toString()) is ParsingState.ParsingInProgress -> Log.d(TAG, "Waiting for more frames") is ParsingState.ParsingSuccessful -> { val responseToCommand = (it.cmd - 1).toByte() val req = currentRequest if (req != null && responseToCommand == req.commandByte) { currentRequest = null try { req.channel.send(it) req.channel.close() } catch (e: ClosedSendChannelException) { Log.w(TAG, "Channel closed, probably because the data is on a page boundary.\n${e.stackTraceToString()}") } } else { if (autoCardScanEnabled && responseToCommand == BtConstants.ACTIVATE_PICC_COM) { Log.d(TAG, "Found a card on auto card search.") onMessage(it) } else { Log.w(TAG, "Got a message not awaited: $it") } } } } }.collect() } val requestTrigger = Channel(1).apply { trySend(Unit) } // Used to signal that the next request can begin. scope.launch { requests.consumeEach { btCommand -> requestTrigger.receive() currentRequest = btCommand val bytes = btCommand.toBytes(ble.comByteManager) ble.writeToReader(bytes) val retryTicker = ticker(2000, 10000) btCommand.callback.invokeOnCancellation { scope.launch { if (btCommand.channel.isClosedForSend || requestTrigger.isClosedForSend) { Log.w(TAG, "Command channel closed, but already cancelled. Channel: ${btCommand.channel}, request trigger: ${requestTrigger}") } else { Log.i(TAG, "Callback of BT command got canceled, advancing...") val result = requestTrigger.trySend(Unit) Log.d(TAG, "Request trigger result when cancelling: $result") btCommand.channel.close() retryTicker.cancel() } } } var retry = 0 @Suppress("ControlFlowWithEmptyBody") while(select { btCommand.channel.onReceiveCatching { retryTicker.cancel() val res = requestTrigger.trySend(Unit) val result = it.getOrNull() try { if (result != null) { btCommand.callback.resume(result) } false } catch (e: IllegalStateException) { Log.e(TAG, e.stackTraceToString()) false } } retryTicker.onReceiveCatching { Log.d(TAG, "Ticker for $btCommand") if (!it.isClosed) { Log.w(TAG, "Didn't get a response for $btCommand, sending request again.") ble.writeToReader(btCommand.toBytes(ble.comByteManager)) if (retry > 5) { btCommand.callback.resumeWithException(CommandFailedException("Retried 5 times, no dice.")) retryTicker.cancel() requestTrigger.send(Unit) false } else { retry++ true } } else { Log.d(TAG, "Retry ticker got closed, probably success.") false } } }) { } } } } private var autoCardScanEnabled = false private val autoCardScanLock = Mutex() suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) { Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled") autoCardScanLock.withLock { if (autoCardScanEnabled != enabled) { val response = suspendCancellableCoroutine { continuation -> sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType)) } if (!response.isSuccessful() && enabled) { autoCardScanEnabled = false throw Exception("Auto card scan request failed: ${response.error.dump()}") } else { // somehow disabling it always returns a fail autoCardScanEnabled = enabled } delay(100.milliseconds) } else { Timber.d { "Card Scan already in requested state" } } } } suspend fun sendCloseRf() { requestAutoCardScan(0, 0, false) } suspend fun onMessage(message: ParsingState.ParsingSuccessful) { val command = message.cmd val rcvBytes = message.data when ((command - 1).toByte()) { ComByteManager.ACTIVATE_PICC_COM -> { var uidBytes: ByteArray? = null var atrBytes: ByteArray? = null if (message.isSuccessful()) { when (rcvBytes[0]) { // (CARD_TYPE_ISO4443_A) -> { // uidBytes = ByteArray(4) // System.arraycopy(rcvBytes, 1, uidBytes, 0, 4) // atrBytes = ByteArray(rcvBytes.size - 5) // System.arraycopy(rcvBytes, 5, atrBytes, 0, rcvBytes.size - 5) // knownCard = true // cpuCard = CpuCard(this@DeviceManager, uidBytes, atrBytes) // } // (CARD_TYPE_MIFARE) -> { // uidBytes = ByteArray(4) // System.arraycopy(rcvBytes, 1, uidBytes, 0, 4) // atrBytes = ByteArray(rcvBytes.size - 5) // System.arraycopy(rcvBytes, 5, atrBytes, 0, rcvBytes.size - 5) // knownCard = true // mifare = Mifare(this@DeviceManager, uidBytes, atrBytes) // } // (CARD_TYPE_ISO15693) -> { // uidBytes = ByteArray(8) // atrBytes = ByteArray(1) // System.arraycopy(rcvBytes, 1, uidBytes, 0, 8) // knownCard = true // iso15693Card = Iso15693Card(this@DeviceManager, uidBytes, atrBytes) // } (BtConstants.CARD_TYPE_ULTRALIGHT) -> { uidBytes = ByteArray(7) System.arraycopy(rcvBytes, 1, uidBytes, 0, 7) atrBytes = ByteArray(rcvBytes.size - 8) System.arraycopy(rcvBytes, 8, atrBytes, 0, rcvBytes.size - 8) val ultralightCommands = object : UltralightCommands { override suspend fun ultralightCmd(data: ByteArray): ByteArray { val result = suspendCancellableCoroutine { continuation -> sendCommand(Command.UltralightCmd(continuation, data)) } if (result.isSuccessful()) { return result.data } else { if (result.error.contentEquals(byteArrayOf(0x6E, 0x81.toByte()))) { throw CardNoResponseException("Card most likely got removed.") } else { Log.e(TAG, "Got error: ${result.error.dump()}") throw CardNoResponseException("Reader sent error code: ${result.error.dump()}") } } } override suspend fun ultralightLongRead( startAddress: Byte, length: Byte ): ByteArray { val result = suspendCancellableCoroutine { continuation -> sendCommand(Command.UltralightLongRead(continuation, startAddress, length)) } if (result.isSuccessful()) { return result.data } else { if (result.error.contentEquals(byteArrayOf(0x6E, 0x81.toByte()))) { throw CardNoResponseException("Card most likely got removed.") } else { Log.e(TAG, "Got error: ${result.error.dump()}") throw CardNoResponseException("Reader sent error code: ${result.error.dump()}") } } } override suspend fun ultralightLongWrite( startAddress: Byte, data: ByteArray ) { val result = suspendCancellableCoroutine { continuation -> sendCommand(Command.UltralightLongWrite(continuation, startAddress, data)) } if (!result.isSuccessful()) { if (result.error.contentEquals(byteArrayOf(0x6E, 0x81.toByte()))) { throw CardNoResponseException("Card most likely got removed.") } else { Log.e(TAG, "Got error: ${result.error.dump()}") throw CardNoResponseException("Reader sent error code: ${result.error.dump()}") } } } } Log.d(TAG, "Found a card, sending with id ${uidBytes.dump()}") cardChannel.send( Ntag21x( ultralightCommands, uidBytes, atrBytes ) ) } // (CARD_TYPE_DESFire) -> { // uidBytes = ByteArray(7) // System.arraycopy(rcvBytes, 1, uidBytes, 0, 7) // atrBytes = ByteArray(rcvBytes.size - 8) // System.arraycopy(rcvBytes, 8, atrBytes, 0, rcvBytes.size - 8) // knownCard = true // desFire = DESFire(this@DeviceManager, uidBytes, atrBytes) // } // (CARD_TYPE_ISO4443_B) -> { // uidBytes = ByteArray(4) // atrBytes = ByteArray(rcvBytes.size - 1) // System.arraycopy(rcvBytes, 1, atrBytes, 0, rcvBytes.size - 1) // knownCard = true // iso14443bCard = Iso14443bCard(this@DeviceManager, uidBytes, atrBytes) // } // (CARD_TYPE_FELICA) -> { // uidBytes = ByteArray(4) // atrBytes = ByteArray(rcvBytes.size - 1) // System.arraycopy(rcvBytes, 1, atrBytes, 0, rcvBytes.size - 1) // knownCard = true // feliCa = FeliCa(this@DeviceManager, uidBytes, atrBytes) // } else -> { Timber.tag(TAG).w { "Unknown card type: ${rcvBytes[0].toHexString()}" } } } } else { cardChannel.send(null) } } } } suspend fun pingCard() { sendCloseRf() val response = retry(CommandFailedException::class, 3) { val response = suspendCancellableCoroutine { continuation -> sendCommand(Command.PingCard(continuation, BtConstants.CARD_TYPE_ULTRALIGHT)) } if (response.error.contentEquals(byteArrayOf(0x6E, 0x81.toByte()))) { throw CommandFailedException("Got an error from the reader. ${response.data.dump()}") } response } scope.launch { onMessage(response) } } suspend fun requestVoltage(): Double { val response = suspendCancellableCoroutine { continuation -> sendCommand(Command.BatteryVoltage(continuation)) } if (response.data.size == 2) { return (response.data[0].toInt() and 0x00ff shl 8 or (response.data[1] .toInt() and 0x00ff)) / 100.0 } else { Timber.w { "Response got length: ${response.data.size} with data: ${response.data.dump()}" } throw CardNoResponseException() } } }