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
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,39 @@ Do NOT claim work is complete without verification.

When continuing from a previous session, check for implementation plans and session transcripts in `~/.claude/` to recover context rather than starting from scratch.

### 10. Use Kermit for all logging

Use `co.touchlab.kermit.Logger` for all logging. Never use `println()`, `e.printStackTrace()`, `android.util.Log`, `NSLog`, or `console.log` in Kotlin code.

```kotlin
import co.touchlab.kermit.Logger

// Create a tagged logger (use class/module name as tag)
private val log = Logger.withTag("MyClass")

// Log levels (least to most severe): v, d, i, w, e, a
log.d { "Debug message with $variable" } // Use lambda syntax for lazy eval
log.w(exception) { "Warning with context" } // Attach throwable
log.e(exception) { "Error description" } // Errors — replaces printStackTrace()

// In catch blocks — NEVER swallow exceptions silently:
catch (e: Exception) {
log.w(e) { "Descriptive message about what failed" }
// ... handle gracefully
}

// For expected/benign exceptions, still log at debug level:
catch (e: SpecificException) {
log.d { "Expected: description" }
}
```

Do NOT:
- Use `println()` for logging (except in CLI tools under `tools/`)
- Call `e.printStackTrace()` — use `log.e(e) { "msg" }` instead
- Catch exceptions without logging them (no silent swallowing)
- Use `catch (_: Exception)` without at least a `log.d` call

## Build Commands

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package com.codebutler.farebot.desktop

import co.touchlab.kermit.Logger
import com.codebutler.farebot.card.RawCard
import com.codebutler.farebot.card.nfc.pn533.PN533
import com.codebutler.farebot.card.nfc.pn533.PN533Device
Expand All @@ -48,6 +49,8 @@ import kotlinx.coroutines.launch
* the error is logged and the other backends continue scanning.
* Results from any backend are emitted to the shared [scannedCards] flow.
*/
private val log = Logger.withTag("DesktopCardScanner")

class DesktopCardScanner : CardScanner {
override val requiresActiveScan: Boolean = true

Expand Down Expand Up @@ -80,7 +83,7 @@ class DesktopCardScanner : CardScanner {
val backendJobs =
backends.map { backend ->
launch {
println("[DesktopCardScanner] Starting ${backend.name} backend")
log.i { "Starting ${backend.name} backend" }
try {
backend.scanLoop(
onCardDetected = { tag ->
Expand All @@ -100,11 +103,11 @@ class DesktopCardScanner : CardScanner {
)
} catch (e: Exception) {
if (isActive) {
println("[DesktopCardScanner] ${backend.name} backend failed: ${e.message}")
log.w(e) { "${backend.name} backend failed" }
}
} catch (e: Error) {
// Catch LinkageError / UnsatisfiedLinkError from native libs
println("[DesktopCardScanner] ${backend.name} backend unavailable: ${e.message}")
log.w(e) { "${backend.name} backend unavailable" }
}
}
}
Expand Down Expand Up @@ -132,7 +135,7 @@ class DesktopCardScanner : CardScanner {
try {
PN533Device.openAll()
} catch (e: UnsatisfiedLinkError) {
println("[DesktopCardScanner] libusb not available: ${e.message}")
log.w(e) { "libusb not available" }
emptyList()
}
if (transports.isEmpty()) {
Expand All @@ -144,7 +147,7 @@ class DesktopCardScanner : CardScanner {
val probe = PN533(transport)
val fw = probe.getFirmwareVersion()
val label = "PN53x #${index + 1}"
println("[DesktopCardScanner] $label firmware: $fw")
log.i { "$label firmware: $fw" }
if (fw.version >= 2) {
backends.add(PN533ReaderBackend(transport))
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.codebutler.farebot.card.CardType
import com.codebutler.farebot.shared.FareBotApp
import com.codebutler.farebot.shared.di.LocalAppGraph
Expand All @@ -26,6 +29,24 @@ import javax.imageio.ImageIO
private const val ICON_PATH = "composeResources/farebot.app.generated.resources/drawable/ic_launcher.png"

fun main() {
Logger.setLogWriters(
object : LogWriter() {
override fun log(
severity: Severity,
message: String,
tag: String,
throwable: Throwable?,
) {
val ts = java.time.LocalTime.now()
val prefix = "$ts ${severity.name[0]}/$tag: "
println("$prefix$message")
throwable?.stackTraceToString()?.lineSequence()?.forEach { line ->
println("$prefix$line")
}
}
},
)

System.setProperty("apple.awt.application.appearance", "system")

val desktop = Desktop.getDesktop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PN533ReaderBackend(

override suspend fun initDevice(pn533: PN533) {
val fw = pn533.getFirmwareVersion()
println("[$name] Firmware: $fw")
log.i { "Firmware: $fw" }
pn533.samConfiguration()
pn533.setMaxRetries(passiveActivation = 0x02)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package com.codebutler.farebot.desktop

import co.touchlab.kermit.Logger
import com.codebutler.farebot.card.CardType
import com.codebutler.farebot.card.RawCard
import com.codebutler.farebot.card.cepas.CEPASCardReader
Expand Down Expand Up @@ -51,6 +52,8 @@ import kotlinx.coroutines.delay
abstract class PN53xReaderBackend(
private val preOpenedTransport: Usb4JavaPN533Transport? = null,
) : NfcReaderBackend {
protected val log by lazy { Logger.withTag(name) }

protected abstract suspend fun initDevice(pn533: PN533)

protected open fun createTransceiver(
Expand Down Expand Up @@ -87,7 +90,7 @@ abstract class PN53xReaderBackend(
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
) {
while (true) {
println("[$name] Polling for cards...")
log.i { "Polling for cards..." }

// Try ISO 14443-A (106 kbps) first — covers Classic, Ultralight, DESFire
var target = pn533.inListPassiveTarget(baudRate = PN533.BAUD_RATE_106_ISO14443A)
Expand Down Expand Up @@ -122,20 +125,21 @@ abstract class PN53xReaderBackend(
try {
val rawCard = readTarget(pn533, target, onProgress)
onCardRead(rawCard)
println("[$name] Card read successfully")
log.i { "Card read successfully" }
} catch (e: Exception) {
println("[$name] Read error: ${e.message}")
log.e(e) { "Read error" }
onError(e)
}

// Release target
try {
pn533.inRelease(target.tg)
} catch (_: PN533Exception) {
} catch (e: PN533Exception) {
log.d(e) { "inRelease failed (expected)" }
}

// Wait for card removal by polling until no target detected
println("[$name] Waiting for card removal...")
log.i { "Waiting for card removal..." }
waitForRemoval(pn533)
}
}
Expand All @@ -157,7 +161,7 @@ abstract class PN53xReaderBackend(
): RawCard<*> {
val info = PN533CardInfo.fromTypeA(target)
val tagId = target.uid
println("[$name] Type A card: type=${info.cardType}, SAK=0x%02X, UID=${tagId.hex()}".format(target.sak))
log.i { "Type A card: type=${info.cardType}, SAK=0x%02X, UID=${tagId.hex()}".format(target.sak) }

return when (info.cardType) {
CardType.MifareDesfire, CardType.ISO7816 -> {
Expand Down Expand Up @@ -193,7 +197,7 @@ abstract class PN53xReaderBackend(
onProgress: (suspend (current: Int, total: Int) -> Unit)?,
): RawCard<*> {
val tagId = target.idm
println("[$name] FeliCa card: IDm=${tagId.hex()}")
log.i { "FeliCa card: IDm=${tagId.hex()}" }
val adapter = PN533FeliCaTagAdapter(pn533, target.idm)
return FeliCaReader.readTag(tagId, adapter, onProgress = onProgress)
}
Expand All @@ -208,7 +212,8 @@ abstract class PN53xReaderBackend(
baudRate = PN533.BAUD_RATE_212_FELICA,
initiatorData = SENSF_REQ,
)
} catch (_: PN533Exception) {
} catch (e: PN533Exception) {
log.d(e) { "Poll during removal check failed" }
null
}
if (target == null) {
Expand All @@ -217,7 +222,8 @@ abstract class PN53xReaderBackend(
// Card still present, release and keep waiting
try {
pn533.inRelease(target.tg)
} catch (_: PN533Exception) {
} catch (e: PN533Exception) {
log.d(e) { "inRelease during removal wait failed" }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package com.codebutler.farebot.desktop

import co.touchlab.kermit.Logger
import com.codebutler.farebot.card.CardType
import com.codebutler.farebot.card.RawCard
import com.codebutler.farebot.card.cepas.CEPASCardReader
Expand All @@ -41,6 +42,8 @@ import javax.smartcardio.CardException
import javax.smartcardio.CommandAPDU
import javax.smartcardio.TerminalFactory

private val log = Logger.withTag("PcscReaderBackend")

/**
* PC/SC reader backend using javax.smartcardio.
*
Expand Down Expand Up @@ -69,18 +72,18 @@ class PcscReaderBackend : NfcReaderBackend {
}

val terminal = terminals.first()
println("[PC/SC] Using reader: ${terminal.name}")
log.i { "Using reader: ${terminal.name}" }

while (true) {
println("[PC/SC] Waiting for card...")
log.i { "Waiting for card..." }
terminal.waitForCardPresent(0)

try {
val card = terminal.connect("*")
try {
val atr = card.atr.bytes
val info = PCSCCardInfo.fromATR(atr)
println("[PC/SC] Card detected: type=${info.cardType}, ATR=${atr.hex()}")
log.i { "Card detected: type=${info.cardType}, ATR=${atr.hex()}" }

val channel = card.basicChannel

Expand All @@ -93,24 +96,25 @@ class PcscReaderBackend : NfcReaderBackend {
} else {
byteArrayOf()
}
println("[PC/SC] Tag ID: ${tagId.hex()}")
log.i { "Tag ID: ${tagId.hex()}" }

onCardDetected(ScannedTag(id = tagId, techList = listOf(info.cardType.name)))
val rawCard = readCard(info, channel, tagId, onProgress)
onCardRead(rawCard)
println("[PC/SC] Card read successfully")
log.i { "Card read successfully" }
} finally {
try {
card.disconnect(false)
} catch (_: Exception) {
} catch (e: Exception) {
log.d(e) { "Card disconnect failed" }
}
}
} catch (e: Exception) {
println("[PC/SC] Read error: ${e.message}")
log.e(e) { "Read error" }
onError(e)
}

println("[PC/SC] Waiting for card removal...")
log.i { "Waiting for card removal..." }
terminal.waitForCardAbsent(0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class RCS956ReaderBackend(
pn533.resetMode()

val fw = pn533.getFirmwareVersion()
println("[$name] Firmware: $fw (RC-S956)")
log.i { "Firmware: $fw (RC-S956)" }

// mute() = resetMode + super().mute()
pn533.resetMode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package com.codebutler.farebot.shared.nfc

import co.touchlab.kermit.Logger
import com.codebutler.farebot.card.RawCard
import com.codebutler.farebot.card.china.ChinaRegistry
import com.codebutler.farebot.card.desfire.DesfireCardReader
Expand All @@ -36,6 +37,8 @@ import com.codebutler.farebot.card.nfc.CardTransceiver
* then falls back to the DESFire protocol if no known application is found.
*/
object ISO7816Dispatcher {
private val log = Logger.withTag("ISO7816Dispatcher")

suspend fun readCard(
tagId: ByteArray,
transceiver: CardTransceiver,
Expand All @@ -59,7 +62,7 @@ object ISO7816Dispatcher {
return try {
ISO7816CardReader.readCard(tagId, transceiver, appConfigs, onProgress)
} catch (e: Exception) {
println("[ISO7816Dispatcher] ISO7816 read attempt failed: $e")
log.w(e) { "ISO7816 read attempt failed" }
null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

package com.codebutler.farebot.shared.serialize

import co.touchlab.kermit.Logger
import com.codebutler.farebot.base.util.ByteUtils
import com.codebutler.farebot.card.RawCard
import com.codebutler.farebot.card.classic.raw.RawClassicBlock
Expand Down Expand Up @@ -60,6 +61,8 @@ import kotlin.time.Instant
* Metrodroid JSON tree, similar to how FlipperNfcParser handles Flipper NFC dumps.
*/
object MetrodroidJsonParser {
private val log = Logger.withTag("MetrodroidJsonParser")

fun parse(obj: JsonObject): RawCard<*>? {
val tagId = parseTagId(obj)
val scannedAt = parseScannedAt(obj)
Expand Down Expand Up @@ -398,7 +401,7 @@ object MetrodroidJsonParser {
return try {
ByteUtils.hexStringToByteArray(hex)
} catch (e: Exception) {
println("[MetrodroidJsonParser] Failed to parse hex string: $e")
log.w(e) { "Failed to parse hex string" }
ByteArray(0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.codebutler.farebot.shared.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.codebutler.farebot.base.ui.HeaderListItem
import com.codebutler.farebot.base.util.DateFormatStyle
import com.codebutler.farebot.base.util.formatDate
Expand Down Expand Up @@ -39,6 +40,8 @@ class CardViewModel(
private val cardSerializer: CardSerializer,
private val cardPersister: CardPersister,
) : ViewModel() {
private val log = Logger.withTag("CardViewModel")

private val _uiState = MutableStateFlow(CardUiState())
val uiState: StateFlow<CardUiState> = _uiState.asStateFlow()

Expand Down Expand Up @@ -154,8 +157,7 @@ class CardViewModel(
)
}
} catch (ex: Exception) {
println("[FareBot] Card load error: ${ex::class.simpleName}: ${ex.message}")
ex.printStackTrace()
log.e(ex) { "Card load error: ${ex::class.simpleName}: ${ex.message}" }
_uiState.value =
CardUiState(
isLoading = false,
Expand Down
Loading