Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.codebutler.farebot.card.RawCard
import com.codebutler.farebot.card.nfc.pn533.PN533
import com.codebutler.farebot.card.nfc.pn533.PN533Device
import com.codebutler.farebot.shared.nfc.CardScanner
import com.codebutler.farebot.shared.nfc.ReadingProgress
import com.codebutler.farebot.shared.nfc.ScannedTag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -62,6 +63,9 @@ class DesktopCardScanner : CardScanner {
private val _isScanning = MutableStateFlow(false)
override val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()

private val _readingProgress = MutableStateFlow<ReadingProgress?>(null)
override val readingProgress: StateFlow<ReadingProgress?> = _readingProgress.asStateFlow()

private var scanJob: Job? = null
private val scope = CoroutineScope(Dispatchers.IO)

Expand All @@ -83,11 +87,16 @@ class DesktopCardScanner : CardScanner {
_scannedTags.tryEmit(tag)
},
onCardRead = { rawCard ->
_readingProgress.value = null
_scannedCards.tryEmit(rawCard)
},
onError = { error ->
_readingProgress.value = null
_scanErrors.tryEmit(error)
},
onProgress = { current, total ->
_readingProgress.value = ReadingProgress(current, total)
},
)
} catch (e: Exception) {
if (isActive) {
Expand Down Expand Up @@ -119,7 +128,13 @@ class DesktopCardScanner : CardScanner {

private suspend fun discoverBackends(): List<NfcReaderBackend> {
val backends = mutableListOf<NfcReaderBackend>(PcscReaderBackend())
val transports = PN533Device.openAll()
val transports =
try {
PN533Device.openAll()
} catch (e: UnsatisfiedLinkError) {
println("[DesktopCardScanner] libusb not available: ${e.message}")
emptyList()
}
if (transports.isEmpty()) {
backends.add(PN533ReaderBackend())
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ interface NfcReaderBackend {
onCardDetected: (ScannedTag) -> Unit,
onCardRead: (RawCard<*>) -> Unit,
onError: (Throwable) -> Unit,
onProgress: (suspend (current: Int, total: Int) -> Unit)? = null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ abstract class PN53xReaderBackend(
onCardDetected: (ScannedTag) -> Unit,
onCardRead: (RawCard<*>) -> Unit,
onError: (Throwable) -> Unit,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
) {
val transport =
preOpenedTransport
Expand All @@ -72,7 +73,7 @@ abstract class PN53xReaderBackend(
val pn533 = PN533(transport)
try {
initDevice(pn533)
pollLoop(pn533, onCardDetected, onCardRead, onError)
pollLoop(pn533, onCardDetected, onCardRead, onError, onProgress)
} finally {
pn533.close()
}
Expand All @@ -83,6 +84,7 @@ abstract class PN53xReaderBackend(
onCardDetected: (ScannedTag) -> Unit,
onCardRead: (RawCard<*>) -> Unit,
onError: (Throwable) -> Unit,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
) {
while (true) {
println("[$name] Polling for cards...")
Expand Down Expand Up @@ -118,7 +120,7 @@ abstract class PN53xReaderBackend(
onCardDetected(ScannedTag(id = tagId, techList = listOf(cardTypeName)))

try {
val rawCard = readTarget(pn533, target)
val rawCard = readTarget(pn533, target, onProgress)
onCardRead(rawCard)
println("[$name] Card read successfully")
} catch (e: Exception) {
Expand All @@ -141,15 +143,17 @@ abstract class PN53xReaderBackend(
private suspend fun readTarget(
pn533: PN533,
target: PN533.TargetInfo,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
): RawCard<*> =
when (target) {
is PN533.TargetInfo.TypeA -> readTypeACard(pn533, target)
is PN533.TargetInfo.FeliCa -> readFeliCaCard(pn533, target)
is PN533.TargetInfo.TypeA -> readTypeACard(pn533, target, onProgress)
is PN533.TargetInfo.FeliCa -> readFeliCaCard(pn533, target, onProgress)
}

private suspend fun readTypeACard(
pn533: PN533,
target: PN533.TargetInfo.TypeA,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
): RawCard<*> {
val info = PN533CardInfo.fromTypeA(target)
val tagId = target.uid
Expand All @@ -158,39 +162,40 @@ abstract class PN53xReaderBackend(
return when (info.cardType) {
CardType.MifareDesfire, CardType.ISO7816 -> {
val transceiver = createTransceiver(pn533, target.tg)
ISO7816Dispatcher.readCard(tagId, transceiver)
ISO7816Dispatcher.readCard(tagId, transceiver, onProgress)
}

CardType.MifareClassic -> {
val tech = PN533ClassicTechnology(pn533, target.tg, tagId, info)
ClassicCardReader.readCard(tagId, tech, null)
ClassicCardReader.readCard(tagId, tech, null, onProgress = onProgress)
}

CardType.MifareUltralight -> {
val tech = PN533UltralightTechnology(pn533, target.tg, info)
UltralightCardReader.readCard(tagId, tech)
UltralightCardReader.readCard(tagId, tech, onProgress)
}

CardType.CEPAS -> {
val transceiver = createTransceiver(pn533, target.tg)
CEPASCardReader.readCard(tagId, transceiver)
CEPASCardReader.readCard(tagId, transceiver, onProgress)
}

else -> {
val transceiver = createTransceiver(pn533, target.tg)
ISO7816Dispatcher.readCard(tagId, transceiver)
ISO7816Dispatcher.readCard(tagId, transceiver, onProgress)
}
}
}

private suspend fun readFeliCaCard(
pn533: PN533,
target: PN533.TargetInfo.FeliCa,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
): RawCard<*> {
val tagId = target.idm
println("[$name] FeliCa card: IDm=${tagId.hex()}")
val adapter = PN533FeliCaTagAdapter(pn533, target.idm)
return FeliCaReader.readTag(tagId, adapter)
return FeliCaReader.readTag(tagId, adapter, onProgress = onProgress)
}

private suspend fun waitForRemoval(pn533: PN533) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class PcscReaderBackend : NfcReaderBackend {
onCardDetected: (ScannedTag) -> Unit,
onCardRead: (RawCard<*>) -> Unit,
onError: (Throwable) -> Unit,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
) {
val factory = TerminalFactory.getDefault()
val terminals =
Expand Down Expand Up @@ -95,7 +96,7 @@ class PcscReaderBackend : NfcReaderBackend {
println("[PC/SC] Tag ID: ${tagId.hex()}")

onCardDetected(ScannedTag(id = tagId, techList = listOf(info.cardType.name)))
val rawCard = readCard(info, channel, tagId)
val rawCard = readCard(info, channel, tagId, onProgress)
onCardRead(rawCard)
println("[PC/SC] Card read successfully")
} finally {
Expand All @@ -118,31 +119,32 @@ class PcscReaderBackend : NfcReaderBackend {
info: PCSCCardInfo,
channel: javax.smartcardio.CardChannel,
tagId: ByteArray,
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
): RawCard<*> =
when (info.cardType) {
CardType.MifareDesfire, CardType.ISO7816 -> {
val transceiver = PCSCCardTransceiver(channel)
ISO7816Dispatcher.readCard(tagId, transceiver)
ISO7816Dispatcher.readCard(tagId, transceiver, onProgress)
}

CardType.MifareClassic -> {
val tech = PCSCClassicTechnology(channel, info)
ClassicCardReader.readCard(tagId, tech, null)
ClassicCardReader.readCard(tagId, tech, null, onProgress = onProgress)
}

CardType.MifareUltralight -> {
val tech = PCSCUltralightTechnology(channel, info)
UltralightCardReader.readCard(tagId, tech)
UltralightCardReader.readCard(tagId, tech, onProgress)
}

CardType.FeliCa -> {
val adapter = PCSCFeliCaTagAdapter(channel)
FeliCaReader.readTag(tagId, adapter)
FeliCaReader.readTag(tagId, adapter, onProgress = onProgress)
}

CardType.CEPAS -> {
val transceiver = PCSCCardTransceiver(channel)
CEPASCardReader.readCard(tagId, transceiver)
CEPASCardReader.readCard(tagId, transceiver, onProgress)
}

CardType.Vicinity -> {
Expand All @@ -152,7 +154,7 @@ class PcscReaderBackend : NfcReaderBackend {

else -> {
val transceiver = PCSCCardTransceiver(channel)
ISO7816Dispatcher.readCard(tagId, transceiver)
ISO7816Dispatcher.readCard(tagId, transceiver, onProgress)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ class ISO7816TagReader(
tag: Tag,
tech: CardTransceiver,
cardKeys: CardKeys?,
): RawCard<*> = ISO7816Dispatcher.readCard(tagId, tech)
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
): RawCard<*> = ISO7816Dispatcher.readCard(tagId, tech, onProgress)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.codebutler.farebot.key.CardKeys
import com.codebutler.farebot.persist.CardKeysPersister
import com.codebutler.farebot.shared.nfc.CardScanner
import com.codebutler.farebot.shared.nfc.CardUnauthorizedException
import com.codebutler.farebot.shared.nfc.ReadingProgress
import com.codebutler.farebot.shared.nfc.ScannedTag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -51,6 +52,9 @@ class AndroidCardScanner(
private val _isScanning = MutableStateFlow(false)
override val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()

private val _readingProgress = MutableStateFlow<ReadingProgress?>(null)
override val readingProgress: StateFlow<ReadingProgress?> = _readingProgress.asStateFlow()

private var isObserving = false

fun startObservingTags() {
Expand All @@ -65,12 +69,17 @@ class AndroidCardScanner(
_isScanning.value = true
try {
val cardKeys = getCardKeys(ByteUtils.getHexString(tag.id))
val rawCard = tagReaderFactory.getTagReader(tag.id, tag, cardKeys).readTag()
val rawCard =
tagReaderFactory.getTagReader(tag.id, tag, cardKeys).readTag { current, total ->
_readingProgress.value = ReadingProgress(current, total)
}
_readingProgress.value = null
if (rawCard.isUnauthorized()) {
throw CardUnauthorizedException(rawCard.tagId(), rawCard.cardType())
}
_scannedCards.emit(rawCard)
} catch (error: Throwable) {
_readingProgress.value = null
_scanErrors.emit(error)
} finally {
_isScanning.value = false
Expand Down
1 change: 1 addition & 0 deletions app/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<string name="tab_explore">Explore</string>
<string name="scan">Scan</string>
<string name="reading_card">Reading\u2026</string>
<string name="hold_card_near_reader">Hold your card near the reader</string>
<string name="search_supported_cards">Search</string>
<string name="nfc_disabled_tap_to_enable">Enable NFC to scan cards</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ fun FareBotApp(
navController.navigate(Screen.AddKey.createRoute(tagId, cardType))
},
onScanCard = { homeViewModel.startActiveScan() },
onCancelScan = { homeViewModel.stopActiveScan() },
historyUiState = historyUiState,
onNavigateToCard = { itemId ->
val cardKey = historyViewModel.getCardNavKey(itemId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ package com.codebutler.farebot.shared.nfc

import com.codebutler.farebot.card.RawCard
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

data class ReadingProgress(
val current: Int,
val total: Int,
)

data class ScannedTag(
val id: ByteArray,
val techList: List<String>,
Expand Down Expand Up @@ -66,6 +72,10 @@ interface CardScanner {
/** Whether scanning is currently in progress. */
val isScanning: StateFlow<Boolean>

/** Reading progress — non-null when a card read is in progress with known total. */
val readingProgress: StateFlow<ReadingProgress?>
get() = MutableStateFlow(null)

/**
* Start an active scan session (e.g., iOS NFC dialog).
* Results are emitted to [scannedCards].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,25 @@ object ISO7816Dispatcher {
suspend fun readCard(
tagId: ByteArray,
transceiver: CardTransceiver,
onProgress: (suspend (current: Int, total: Int) -> Unit)? = null,
): RawCard<*> {
val iso7816Card = tryISO7816(tagId, transceiver)
val iso7816Card = tryISO7816(tagId, transceiver, onProgress)
if (iso7816Card != null) {
return iso7816Card
}
return DesfireCardReader.readCard(tagId, transceiver)
return DesfireCardReader.readCard(tagId, transceiver, onProgress)
}

private suspend fun tryISO7816(
tagId: ByteArray,
transceiver: CardTransceiver,
onProgress: (suspend (current: Int, total: Int) -> Unit)? = null,
): RawCard<*>? {
val appConfigs = buildAppConfigs()
if (appConfigs.isEmpty()) return null

return try {
ISO7816CardReader.readCard(tagId, transceiver, appConfigs)
ISO7816CardReader.readCard(tagId, transceiver, appConfigs, onProgress)
} catch (e: Exception) {
println("[ISO7816Dispatcher] ISO7816 read attempt failed: $e")
null
Expand Down
Loading