You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android通过APDU读取NFC Type A卡NDEF文本数据异常排查

Android NFC Type A卡NDEF文本读取异常问题

我正在开发一款Android应用,需通过基于Android的POS系统读取NFC Type A卡中的NDEF文本消息,该系统要求使用PICC和APDU命令,因此无法使用Android默认NDEF库。我用Google Play商店的NFC Tools向标签写入了“123456789”,但自行编写的代码读出的却是“1234567123456789123456789”。希望有人能指出问题所在,感谢!

代码

private fun extractNdef(tag: Tag): String? {
    val nfcA = NfcA.get(tag) ?: return null

    nfcA.use {
        nfcA.connect()

        // Read blocks 4-8
        val allData = StringBuilder()
        for (block in 4..8) {
            val response = try {
                val rawResponse = nfcA.transceive(byteArrayOf(0x30.toByte(), block.toByte()))
                rawResponse.joinToString("", { "%02x".format(it) })
            } catch (e: IOException) {
                Log.e("NFC", "Failed to read block $block", e)
                return null
            }
            allData.append(response)
            Log.d("NFC", "Block $block Data: $response")
        }

        val ndefData = allData.toString()
        Log.d("NFC", "Combined NDEF Data: $ndefData")

        return parseNdefData(ndefData)
    }
}

fun parseNdefData(hexData: String): String {
    // Convert hex data to bytes
    val bytes = hexData.chunked(2).map { it.toInt(16).toByte() }.toByteArray()

    // Search for the text record identifier and language code prefix
    val textRecordType = 0x54.toByte() // "T" for text records in NDEF
    val langCodeLength = 3 // Length of 'T' + length of "en" (2 bytes)

    // Locate and decode the text content
    val contentBuilder = StringBuilder()
    for (i in bytes.indices) {
        if (bytes[i] == textRecordType && i + langCodeLength < bytes.size) {
            // Start extracting bytes after 'T' and "en" prefix (language code)
            val textContentBytes = bytes.drop(i + langCodeLength + 1)
                .takeWhile { it != 0xFE.toByte() && it != 0x00.toByte() } // Stop at `FE` or padding `00`

            // Convert to string and filter only ASCII digits
            contentBuilder.append(
                textContentBytes
                    .toByteArray()
                    .toString(Charsets.UTF_8)
                    .filter { it.isDigit() }
            )
        }
    }
    return contentBuilder.toString()
}

日志

2024-10-29 12:27:25.226 27321-27339 NFC                     com.passentry.nfc_reader_libraries   D  Block 4 Data: 0310d1010c5402656e31323334353637
2024-10-29 12:27:25.238 27321-27339 NFC                     com.passentry.nfc_reader_libraries   D  Block 5 Data: 0c5402656e313233343536373839fe00
2024-10-29 12:27:25.253 27321-27339 NFC                     com.passentry.nfc_reader_libraries   D  Block 6 Data: 6e313233343536373839fe0000000000
2024-10-29 12:27:25.266 27321-27339 NFC                     com.passentry.nfc_reader_libraries   D  Block 7 Data: 343536373839fe000000000000000000
2024-10-29 12:27:25.284 27321-27339 NFC                     com.passentry.nfc_reader_libraries   D  Block 8 Data: 3839fe00000000000000000000000000
2024-10-29 12:27:25.285 27321-27339 NFC                     com.passentry.nfc_reader_libraries   D  Combined NDEF Data: 0310d1010c5402656e313233343536370c5402656e313233343536373839fe006e313233343536373839fe0000000000343536373839fe0000000000000000003839fe00000000000000000000000000
2024-10-29 12:27:25.349 27321-27339 MainActivity            com.passentry.nfc_reader_libraries   D  Pass ID: 1234567123456789123456789

NFC标签截图

  • NFC Tools截图1
  • NFC Tools截图2

问题分析与修复方案

核心问题

  1. 读取了超出NDEF消息范围的块:MIFARE Classic的NDEF数据遵循TLV结构,块4的前两个字节0310表示NDEF消息标签(03)和长度(10即16字节)。你的代码读取了块4到8,包含了大量标签内的残留/填充数据,这些数据里有重复的文本记录片段。
  2. 解析逻辑错误:
    • 硬编码langCodeLength=3完全错误,语言码长度是NDEF文本记录有效载荷的第一个字节(比如日志里的02,对应2字节的en),不是固定值。
    • 循环遍历所有0x54('T')的位置,导致把残留数据里的重复片段多次提取拼接,最终得到重复的文本内容。

修复代码

1. 修正NDEF数据读取逻辑(只读取必要的块)
private fun extractNdef(tag: Tag): String? {
    val nfcA = NfcA.get(tag) ?: return null

    nfcA.use {
        nfcA.connect()

        // 先读取块4获取NDEF消息长度
        val block4 = try {
            nfcA.transceive(byteArrayOf(0x30.toByte(), 4.toByte()))
        } catch (e: IOException) {
            Log.e("NFC", "Failed to read block 4", e)
            return null
        }

        // 解析TLV结构:Tag(0x03) + Length
        if (block4[0] != 0x03.toByte()) {
            Log.e("NFC", "Not an NDEF tag")
            return null
        }
        val ndefLength = block4[1].toInt() and 0xFF
        Log.d("NFC", "NDEF Message Length: $ndefLength")

        // 计算需要读取的块数:块4开始,每个块16字节,前2字节是TLV头部
        val totalBytesNeeded = 2 + ndefLength // TLV头部2字节 + NDEF消息长度
        val blocksToRead = (totalBytesNeeded + 15) / 16 // 向上取整
        val allData = StringBuilder()
        allData.append(block4.joinToString("", { "%02x".format(it) }))

        // 读取剩余的块(如果需要)
        for (block in 5 until 4 + blocksToRead) {
            val response = try {
                val rawResponse = nfcA.transceive(byteArrayOf(0x30.toByte(), block.toByte()))
                rawResponse.joinToString("", { "%02x".format(it) })
            } catch (e: IOException) {
                Log.e("NFC", "Failed to read block $block", e)
                return null
            }
            allData.append(response)
            Log.d("NFC", "Block $block Data: $response")
        }

        // 截取有效NDEF数据(去掉TLV头部后的ndefLength字节)
        val ndefHex = allData.substring(4, 4 + ndefLength * 2) // 前4个字符是0310(2字节)
        Log.d("NFC", "Filtered NDEF Data: $ndefHex")

        return parseNdefData(ndefHex)
    }
}
2. 修正NDEF文本解析逻辑
fun parseNdefData(hexData: String): String {
    val bytes = hexData.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
    if (bytes.isEmpty()) return ""

    // 解析第一个NDEF记录(单记录场景)
    val recordHeader = bytes[0]
    val isShortRecord = (recordHeader and 0x10.toByte()) != 0.toByte()
    val typeLength = bytes[1].toInt() and 0xFF
    val payloadLength = if (isShortRecord) {
        bytes[2].toInt() and 0xFF
    } else {
        // 长记录场景,这里简化处理单记录短场景
        (bytes[2].toInt() and 0xFF shl 24) or
                (bytes[3].toInt() and 0xFF shl 16) or
                (bytes[4].toInt() and 0xFF shl 8) or
                (bytes[5].toInt() and 0xFF)
    }

    // 找到文本记录类型('T')
    val typeStart = if (isShortRecord) 3 else 6
    if (typeLength != 1 || bytes[typeStart] != 0x54.toByte()) {
        Log.e("NFC", "Not an NDEF text record")
        return ""
    }

    // 解析文本有效载荷
    val payloadStart = typeStart + typeLength
    val langCodeLength = bytes[payloadStart].toInt() and 0xFF
    val textStart = payloadStart + 1 + langCodeLength
    val textBytes = bytes.copyOfRange(textStart, payloadStart + payloadLength)

    // 转换为字符串并过滤数字
    return textBytes.toString(Charsets.UTF_8).filter { it.isDigit() }
}

修复说明

  • 读取逻辑:先解析块4的TLV头部获取NDEF消息长度,只读取必要的块,避免引入残留数据。
  • 解析逻辑:严格按照NDEF记录结构解析,只处理第一个(也是唯一的)文本记录,正确读取语言码长度,避免重复提取。

内容的提问来源于stack exchange,提问作者Tom Rowbotham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 17:17:03