Generated using AI. Be aware that everything might not be accurate.



Chapter 9: Practical Examples — Reading, Writing & Debugging

This chapter brings together the concepts from all previous chapters in the form of complete, working Python examples. Each example targets a specific task: scanning an unknown tag, reading MIFARE Classic blocks, reading and writing NDEF, and debugging common errors.

All examples assume a PN532-based reader connected via USB or I2C, or an ACR122U. Hardware setup is covered in Chapter 7; library installation in Chapter 8.

Longer scripts are in the code/ folder (referenced by filename). Short illustrative snippets are shown inline.


9.1 Example 1: Scan and Identify a Tag

Goal: Detect any tag, print its type, UID, ATQA, and SAK.

Full script: code/01_scan_tag.py

import nfc

def on_connect(tag):
    print(f"Tag type : {tag.type}")
    print(f"UID      : {tag.identifier.hex().upper()}")
    if hasattr(tag, 'atq'):
        print(f"ATQA     : {tag.atq.hex().upper()}")
    if hasattr(tag, 'sak'):
        print(f"SAK      : {tag.sak:02X}")
    return False  # release tag immediately

with nfc.ContactlessFrontend('usb') as clf:
    print("Hold a tag near the reader...")
    clf.connect(rdwr={'on-connect': on_connect})

Sample output for an NTAG213:

Tag type : Type2Tag
UID      : 04 6A B3 32 A5 63 80
ATQA     : 0044
SAK      : 00

Sample output for a MIFARE Classic 1K:

Tag type : MifareClassic
UID      : A3 1F 9C 44
ATQA     : 0004
SAK      : 08

9.2 Example 2: Read a MIFARE Classic Block

Goal: Authenticate to sector 0 with Key A and read block 1.

Full script: code/02_mifare_classic_read.py

import nfc

KEY_A = bytearray.fromhex('FFFFFFFFFFFF')  # factory default

def on_connect(tag):
    if tag.type != 'MifareClassic':
        print("Not a MIFARE Classic tag")
        return False
    sector = 0
    block  = 1
    try:
        tag.authenticate(block, KEY_A, b'\x60')   # 0x60 = key A
        data = tag.read(block)
        print(f"Block {block}: {data.hex()}")
    except nfc.tag.TagCommandError as e:
        print(f"Error: {e}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Notes:

  • tag.authenticate(block_number, key, key_type) — key_type b'\x60' = Key A, b'\x61' = Key B.
  • Authentication grants access to the entire sector containing the block.
  • If the key is wrong, a TagCommandError with status NAK is raised.
  • Manufacturer block (block 0) is readable with any valid key but not writable.

9.3 Example 3: Dump All Sectors of a MIFARE Classic 1K

Goal: Try a list of common keys on each sector and dump readable sectors.

Full script: code/03_mifare_classic_dump.py

import nfc

COMMON_KEYS = [
    bytes.fromhex('FFFFFFFFFFFF'),
    bytes.fromhex('A0A1A2A3A4A5'),
    bytes.fromhex('D3F7D3F7D3F7'),
    bytes.fromhex('000000000000'),
    bytes.fromhex('B0B1B2B3B4B5'),
]

def try_read_sector(tag, sector, keys):
    first_block = sector * 4
    for key in keys:
        for key_type, code in [(bytearray(key), b'\x60'),
                                (bytearray(key), b'\x61')]:
            try:
                tag.authenticate(first_block, key_type, code)
                blocks = [tag.read(first_block + i).hex()
                          for i in range(4)]
                return blocks, key.hex(), 'A' if code == b'\x60' else 'B'
            except Exception:
                continue
    return None, None, None

def on_connect(tag):
    if tag.type != 'MifareClassic':
        return False
    print(f"UID: {tag.identifier.hex()}")
    for sector in range(16):
        blocks, key, key_id = try_read_sector(tag, sector, COMMON_KEYS)
        if blocks:
            print(f"Sector {sector:2d} (Key {key_id}={key}):")
            for i, b in enumerate(blocks):
                print(f"  Block {sector*4+i}: {b}")
        else:
            print(f"Sector {sector:2d}: LOCKED (no matching key found)")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

9.4 Example 4: Read NDEF from an NTAG213

Goal: Read and print the NDEF message from an NTAG213 sticker.

import nfc

def on_connect(tag):
    if not tag.ndef:
        print("No NDEF data found (tag may not be NDEF-formatted)")
        return False
    print(f"NDEF version  : {tag.ndef.version}")
    print(f"NDEF capacity : {tag.ndef.capacity} bytes")
    print(f"NDEF length   : {tag.ndef.length} bytes")
    print(f"Writeable     : {tag.ndef.is_writeable}")
    print()
    for i, record in enumerate(tag.ndef.records):
        print(f"Record {i}: {record}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Sample output for an NTAG213 with a URL:

NDEF version  : 1.0
NDEF capacity : 137 bytes
NDEF length   : 15 bytes
Writeable     : True

Record 0: UriRecord('https://example.com')

9.5 Example 5: Write a URI NDEF Record to an NTAG213

Goal: Write https://example.com to a blank or already-formatted NTAG213.

import nfc, ndef

TARGET_URL = "https://example.com"

def on_connect(tag):
    if tag.ndef is None:
        print("Tag is not NDEF-formatted. Attempting format...")
        tag.format()
    records = [ndef.UriRecord(TARGET_URL)]
    tag.ndef.records = records
    print(f"Wrote NDEF URI: {TARGET_URL}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Notes:

  • tag.format() writes the Capability Container and initialises the TLV structure.
  • tag.ndef.records = [...] encodes the records and writes them in a single operation.
  • For read-only tags (locked CC byte), the write will raise an exception.

Full script with error handling: code/05_write_ndef_uri.py


9.6 Example 6: Write NDEF to MIFARE Classic (MAD)

Writing NDEF to MIFARE Classic requires MAD setup. This is more involved: you need to write the MAD sector (sector 0) with the correct AID, then write NDEF TLV content in the NDEF sectors.

nfcpy handles this through tag.format() which writes MAD1 or MAD2 as appropriate:

import nfc, ndef

def on_connect(tag):
    if tag.type != 'MifareClassic':
        return False
    mad_key = bytearray.fromhex('A0A1A2A3A4A5')
    ndef_key = bytearray.fromhex('D3F7D3F7D3F7')
    # Format creates MAD with default keys
    tag.format(wipe=0x00)
    records = [ndef.UriRecord("https://example.com")]
    tag.ndef.records = records
    print("NDEF written to MIFARE Classic via MAD")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Note: tag.format() on MIFARE Classic writes the NDEF key (D3F7D3F7D3F7) to Key A of all NDEF sectors. This is the standard NFC Forum key for MIFARE Classic NDEF. The MAD key (A0A1A2A3A4A5) is written to sector 0 Key A. After formatting, the card is only accessible with these standard keys.


9.7 Example 7: Read Raw Pages from NTAG213

Goal: Read all user pages directly (bypassing NDEF), useful for debugging.

Full script: code/07_ntag_dump.py

import nfc

def on_connect(tag):
    if tag.type != 'Type2Tag':
        return False
    print(f"UID: {tag.identifier.hex()}")
    # Type2Tag exposes read(page) returning 16 bytes (4 pages)
    for page in range(0, 45, 4):
        data = tag.read(page)
        for i in range(4):
            offset = i * 4
            row = page + i
            print(f"  Page {row:3d}: {data[offset:offset+4].hex()}")
    return False

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

Sample output (pages 0–7):

UID: 046ab332a56380
  Page   0: 04 6a b3 cd
  Page   1: 32 a5 63 80
  Page   2: a8 48 00 00
  Page   3: e1 10 12 00    ← Capability Container
  Page   4: 03 0f d1 01    ← NDEF TLV start
  Page   5: 0b 55 04 65
  Page   6: 78 61 6d 70
  Page   7: 6c 65 2e 63

9.8 Example 8: ACR122U with pyscard — Get UID

Goal: Use pyscard (PC/SC) to get the UID of any tag.

from smartcard.System import readers
from smartcard.util import toHexString, toBytes

r = readers()
if not r:
    print("No readers found")
    exit()

print(f"Using reader: {r[0]}")
conn = r[0].createConnection()
conn.connect()

# ACR122U GET UID pseudo-APDU
get_uid = [0xFF, 0xCA, 0x00, 0x00, 0x00]
data, sw1, sw2 = conn.transmit(get_uid)
if sw1 == 0x90:
    print(f"UID: {toHexString(data)}")
else:
    print(f"Error: SW={sw1:02X}{sw2:02X}")
conn.disconnect()

9.9 Debugging Common Errors

NAK (Negative Acknowledgement)

Symptoms: TagCommandError: [NAK], authentication failure, write failure.

Causes and fixes:

  • Wrong key → check key value and key type (A vs B)
  • Access bits deny the operation → decode access bytes for the sector
  • Tag has been locked → check lock bytes
  • Timing: tag left the field before command completed → reduce polling interval

NFCID / Anticollision errors

Symptoms: Tag detected but immediately lost; inconsistent reads.

Causes and fixes:

  • Multiple tags in field → remove all but one
  • Tag moving during read → hold steady
  • Metal close to antenna → increase distance from metal surfaces
  • Reader antenna detuned → check board mounting

“No such device” / reader not found

nfc.clf.device.Error: [Errno 13] Permission denied

Fix (Linux): Add udev rule or add your user to the plugdev group:

sudo usermod -aG plugdev $USER
# then log out and back in

pcscd / libnfc conflict

If both pcscd and a libnfc-based tool try to claim the same USB reader:

sudo service pcscd stop   # temporarily stop PC/SC daemon
# ... run libnfc tool ...
sudo service pcscd start  # restart

Or permanently blacklist the ACR122U from pcscd by adding it to /etc/modprobe.d/blacklist.conf:

blacklist pn533
blacklist nfc

NDEF not found on a formatted tag

Symptoms: tag.ndef is None despite the tag being “NFC-enabled”.

Causes:

  • CC magic byte is not 0xE1 → write correct CC manually
  • CC version not 0x10 → write E1 10 [size] 00
  • TLV not terminated → write 0xFE after NDEF TLV
  • Tag is Type 1 (Topaz) which nfcpy may not enumerate as NDEF automatically

Write fails silently

Check:

  • CC access byte (byte 3 of page 3): 0x00 = R/W; 0xFF = read-only
  • Dynamic lock bits at page 40: 0x00 0x00 0x00 = unlocked; 0xFF 0xFF 0xFF = fully locked
  • Sector access bits for MIFARE Classic (see Chapter 3)

9.10 Example 9: Encode and Parse NDEF Manually with ndeflib

Goal: Build an NDEF message by hand, encode to bytes, parse back.

import ndef

# Build a multi-record Smart Poster: URL + title
records = [
    ndef.SmartposterRecord(
        resource="https://example.com",
        title=[("en", "Example website"), ("fr", "Site exemple")],
        action="open",
    )
]
encoded = b"".join(ndef.message_encoder(records))
print(f"Encoded ({len(encoded)} bytes): {encoded.hex()}")

# Parse back
for record in ndef.message_decoder(encoded):
    print(f"Type: {type(record).__name__}")
    print(f"  URI: {record.resource}")
    for lang, text in record.title:
        print(f"  Title [{lang}]: {text}")

Summary

  • nfcpy’s ContactlessFrontend with a callback is the standard pattern for all Python NFC work.
  • tag.ndef.records provides direct NDEF read/write without manual TLV handling.
  • MIFARE Classic requires tag.authenticate(block, key, key_type) before tag.read / tag.write.
  • Raw page access on NTAG21x uses tag.read(page) which returns 16 bytes (4 pages at a time).
  • pyscard with FF CA 00 00 00 is the easiest way to get a UID from an ACR122U.
  • Most errors are caused by wrong keys, locked tags, wrong CC, or missing TLV terminator.

← Chapter 8: Software Libraries Table of Contents Chapter 10: Action Plan →


>> You can subscribe to my mailing list here for a monthly update. <<