androidkotlinnfcndef

Reading NDEF Tag via APDU


For context I am creating an Android Application that needs to read a NDEF text message from an NFC Type A card. I want to read this through my Android based Point Of Sale systems which require going through PICC and APDU commands (so I can't use the default Android NDEF library).

To write to the NFC tag, I am just using NFC Tools on the Google Play Store. You'll be able to see I've written "123456789" to the tag, but my code is actually reading "1234567123456789123456789". I hope this makes sense, and someone can see what is going wrong. Thanks!

The code:

  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()
    }

The logs:

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

The NFC Tag NFC Tools Screenshot NFC Tools Screenshot 2


Solution

  • From the NTAG215 datasheet section 10.2 command 0x30 is "READ" which returns 4 blocks (16 bytes) from the start address specified.

    So as you are only reading 5 blocks in total your loop only actually needs to be run twice to read starting at block 4 (which will return blocks 4,5,6,7) and the read again starting at block 8 (which will return blocks 8,9,10,11)

    Or better you could on a NTAG215 use the "FAST_READ" command 0x3A and specify the start and end blocks to read, which can be easier and faster than multiple "READ" command as long you are reading less than the max transceive length.

    e.g something like

    val start = 4
    val end = 8
    val rawResponse = nfcA.transceive(byteArrayOf(0x3A.toByte(), start.toByte(),end.toByte()))
    

    No loop needed.