Rewrite config check for PCSC, document uFR Zero CCID limitations

The uFR Zero presents as CCID (not serial), so the uFCoder library
cannot communicate with it. Rewrote ntag5_config_check.py to use
pyscard with auto-detection: ACR1552 for full NXP config verification,
generic PCSC for basic tag info. Removed uFCoder library (unusable).

Backed up CCID Info.plist before adding uFR Zero VID:PID entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-03 15:27:04 -08:00
parent db492050e3
commit 90eab7990c
4 changed files with 2251 additions and 385 deletions

View File

@@ -1,22 +1,19 @@
# NTAG5Link Config Verification via uFR Zero # NTAG5Link Config Verification via NFC Reader
**Date**: 2026-03-03 **Date**: 2026-03-03
**Status**: Tool written, reader connection blocked **Status**: Partial — tag detected, config read requires ACR1552
## Goal ## Goal
Read-only verification that the NTAG5Link Click board's configuration matches xblink requirements, using the Digital Logic uFR Zero Multi-ISO reader. Read-only verification that the NTAG5Link Click board's configuration matches xblink requirements.
## Tool ## Tool
`tools/ntag5_config_check.py` — Python script using uFCoder library (ctypes) in ISO15693 transparent mode. `tools/ntag5_config_check.py` — Python script using pyscard (PCSC) with auto-detection of reader capabilities.
Features: **Reader support:**
- ISO15693 INVENTORY to discover all tags by UID - **ACR1552**: Full NXP config verification via transparent ISO15693 mode (NXP READ_CONFIG 0xC0)
- Addressed mode for all config reads (supports multiple tags in field) - **uFR Zero (CCID)**: Basic tag info only — UID, EEPROM blocks. Cannot read NXP config registers.
- Reads CONFIG block (0x37) and EH/ED CONFIG block (0x3D)
- NXP system info for interface type verification
- Checks against xblink expected config values
## Expected Config ## Expected Config
@@ -29,45 +26,56 @@ Features:
| EH enable | true | EH_CONFIG byte 0, bit 0 = 1 | | EH enable | true | EH_CONFIG byte 0, bit 0 = 1 |
| EH voltage | 3.0V | EH_CONFIG byte 0, bits 2:1 = 10 | | EH voltage | 3.0V | EH_CONFIG byte 0, bits 2:1 = 10 |
## Reader Connection Issue ## uFR Zero Investigation Results
**Problem**: uFCoder library returns `UFR_TIMEOUT_ERR` (0x1002) for all `ReaderOpenEx` parameter combinations. The D-Logic uFR Zero Multi-ISO reader presents as a **CCID** (USB SmartCard class 0x0B) device, not serial.
**USB device**: `10c4:ea60` — Silicon Labs CP2102 USB to UART Bridge Controller, iSerial "0001". Generic descriptor, no D-Logic branding in USB strings. **USB**: VID:PID `3629:301b`, serial "UD101156"
**Raw serial investigation**: ### What works (PCSC/CCID)
- At 1 Mbps (expected uFR baud): responds with `00 80` (2 bytes) — not a valid uFR protocol frame (expected 6+ bytes with `0x55...0xAA` framing)
- At 115200 bps: returns ASCII shell commands mentioning "systemctl", "proxmark3" paths
- At 250 Kbps: returns structured but unrecognized data
**Possible causes**: | Command | APDU | Result |
1. Device may not be a uFR Zero — the CP210x bridge has generic descriptors |---------|------|--------|
2. Reader firmware may need updating | GET_DATA (UID) | `FF CA 00 00 00` | UID: `E0:04:01:58:7F:8F:11:00` (NXP ISO15693) |
3. Reader may be in a non-standard mode | READ_BINARY | `FF B0 00 xx 00` | Reads ISO15693 user EEPROM blocks |
4. Different hardware variant requiring different protocol | NDEF CC (block 0) | `FF B0 00 00 00` | `E1 40 80 09` — NDEF v4.0, 1024 bytes |
**Next steps**: ### What doesn't work
1. Physically verify the device is indeed the uFR Zero (check labels, LEDs)
2. Try the D-Logic `ufr_online` Windows utility to confirm reader identity
3. If confirmed as uFR Zero, check firmware version and update if needed
4. Alternative: use the ACR1552 PCSC reader with ntag5sensor directly (requires porting the reader to share the field with NTAG5 Click)
## Fallback Plan: ntag5sensor + ACR1552 | Approach | Result | Why |
|----------|--------|-----|
| uFCoder `ReaderOpen` | 0x1002 (timeout) | Library scans serial/tty, doesn't support CCID USB |
| uFCoder `ReaderOpenHnd_ZeroUSB` | 0x0054 (opening error) | Even with pcscd stopped |
| PCSC transparent (FF C2) | SW=6A81 | Not supported by uFR Zero CCID firmware |
| CCID escape (IOCTL 0x42000001) | 6A81 for all commands | Advertised but non-functional |
| Raw ISO15693 via SCardControl | 6A81 | No passthrough mechanism |
If the uFR Zero connection cannot be resolved, use the existing ntag5sensor Python tooling with the ACR1552 PCSC reader: ### Key finding: CP210x is NOT the uFR Zero
`/dev/ttyUSB0` (`10c4:ea60`, Silicon Labs CP210x) is a **separate device** (Proxmark3). The uFR Zero is a pure CCID device with no serial interface.
### CCID setup required
The uFR Zero must be added to the CCID driver's device list:
1. Add VID `0x3629` / PID `0x301B` to `/usr/lib/pcsc/drivers/ifd-ccid.bundle/Contents/Info.plist`
2. Set `ifdDriverOptions` to `0x0001` (enables CCID exchange — though escape commands still return 6A81)
3. Restart pcscd: `systemctl restart pcscd`
## Path Forward: ACR1552
The ACR1552 reader supports transparent ISO15693 mode (FF C2 pseudo-APDUs with TLV encoding), enabling NXP custom commands. The `ntag5_config_check.py` tool auto-detects the ACR1552 and switches to full config verification mode.
```python ```python
# From ntag5sensor — already has full config read/write support # ntag5sensor has proven ACR1552 + NXP custom command support:
chip = NTAG5Link(reader) chip = NTAG5Link(reader)
config = chip.get_config_info() config = chip.get_config_info()
eh_config = chip.get_eh_ed_config_info() eh_config = chip.get_eh_ed_config_info()
``` ```
The ACR1552 is already proven with ntag5sensor. Only change needed: physically position the ACR1552 over the NTAG5 Click board instead of the uFR Zero.
## Config Change Plan (if mismatches found) ## Config Change Plan (if mismatches found)
Use ntag5sensor's write functions (via ACR1552 or adapted for uFR Zero): Use ntag5sensor's write functions via ACR1552:
```python ```python
# Set I2C slave mode + SRAM passthrough + SRAM enable # Set I2C slave mode + SRAM passthrough + SRAM enable

1898
tools/Info.plist.ccid-backup Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,86 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Read NTAG5Link configuration via uFR Zero reader (read-only). """NTAG5Link configuration verification via PCSC reader.
Uses the Digital Logic uFCoder library in ISO15693 transparent mode Supports two reader types:
to send NXP custom commands for reading config registers. - ACR1552: Full NXP config verification via transparent ISO15693 mode
- uFR Zero (CCID): Basic tag info only (UID, EEPROM). NXP config registers
require custom commands not available through CCID. Use ACR1552 or
ntag5sensor directly for full config verification.
Supports multiple tags in the field: runs INVENTORY first, then Usage: python3 ntag5_config_check.py
reads each tag using addressed mode.
Usage: python3 ntag5_config_check.py [/dev/ttyUSBx]
""" """
import ctypes
import sys import sys
import os import os
# --------------------------------------------------------------------------- from smartcard.System import readers
# Load uFCoder library from smartcard.CardRequest import CardRequest
# --------------------------------------------------------------------------- from smartcard.CardType import AnyCardType
from smartcard.CardConnection import CardConnection
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LIB_PATH = os.path.join(SCRIPT_DIR, "libuFCoder-x86_64.so")
if not os.path.exists(LIB_PATH):
print(f"ERROR: uFCoder library not found at {LIB_PATH}")
sys.exit(1)
ufr = ctypes.cdll.LoadLibrary(LIB_PATH)
# ---------------------------------------------------------------------------
# uFCoder function signatures
# ---------------------------------------------------------------------------
ufr.ReaderOpenEx.argtypes = [ctypes.c_uint32, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_char_p]
ufr.ReaderOpenEx.restype = ctypes.c_uint32
ufr.ReaderClose.argtypes = []
ufr.ReaderClose.restype = ctypes.c_uint32
ufr.card_transceive_mode_start.argtypes = [ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32]
ufr.card_transceive_mode_start.restype = ctypes.c_uint32
ufr.card_transceive_mode_stop.argtypes = []
ufr.card_transceive_mode_stop.restype = ctypes.c_uint32
ufr.uart_transceive.argtypes = [
ctypes.POINTER(ctypes.c_uint8), # send_data
ctypes.c_uint8, # send_len
ctypes.POINTER(ctypes.c_uint8), # rcv_data
ctypes.c_uint32, # bytes_to_receive
ctypes.POINTER(ctypes.c_uint32), # rcv_len
]
ufr.uart_transceive.restype = ctypes.c_uint32
# UFR status codes
UFR_OK = 0
# ---------------------------------------------------------------------------
# ISO15693 flags and commands
# ---------------------------------------------------------------------------
# Request flags
ISO_FLAG_SUB_CARRIER = (1 << 0)
ISO_FLAG_DATA_RATE = (1 << 1)
ISO_FLAG_INVENTORY = (1 << 2)
ISO_FLAG_PROTOCOL_EXT = (1 << 3)
# When INVENTORY flag is NOT set:
ISO_FLAG_SELECT = (1 << 4)
ISO_FLAG_ADDRESS = (1 << 5)
ISO_FLAG_OPTION = (1 << 6)
# When INVENTORY flag IS set:
ISO_FLAG_AFI = (1 << 4)
ISO_FLAG_NB_SLOTS_1 = (1 << 5) # single slot
# Standard ISO15693 commands
ISO_CMD_INVENTORY = 0x01
ISO_CMD_STAY_QUIET = 0x02
ISO_CMD_SYSTEM_INFO = 0x2B
# NXP custom commands
NXP_CMD_SYSTEM_INFO = 0xAB
NXP_CMD_READ_CONFIG = 0xC0
NXP_CMD_MANUF_CODE_NXP = 0x04
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# NXP NTAG5Link config constants (from ntag5sensor) # NXP NTAG5Link config constants (from ntag5sensor)
@@ -91,58 +27,34 @@ NXP_CONFIG_ADDR_CONFIG = 0x37
NXP_CONFIG_ADDR_EH_CONFIG = 0x3D NXP_CONFIG_ADDR_EH_CONFIG = 0x3D
# Config byte 0 flags # Config byte 0 flags
NXP_CONFIG_0_AUTO_STANDBY_MODE_EN = (1 << 0)
NXP_CONFIG_0_LOCK_SESSION_REG = (1 << 1)
NXP_CONFIG_0_EH_MODE_MASK = (3 << 2) NXP_CONFIG_0_EH_MODE_MASK = (3 << 2)
NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH = (2 << 2)
NXP_CONFIG_0_EH_MODE_HIGH_FIELD_STRENGTH = (3 << 2)
NXP_CONFIG_0_SRAM_COPY_EN = (1 << 7)
# Config byte 1 flags # Config byte 1 flags
NXP_CONFIG_1_PT_TRANSFER_DIR = (1 << 0)
NXP_CONFIG_1_SRAM_ENABLE = (1 << 1) NXP_CONFIG_1_SRAM_ENABLE = (1 << 1)
NXP_CONFIG_1_ARBITER_MASK = (3 << 2) NXP_CONFIG_1_ARBITER_MASK = (3 << 2)
NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH = (2 << 2)
NXP_CONFIG_1_USE_CASE_MASK = (3 << 4) NXP_CONFIG_1_USE_CASE_MASK = (3 << 4)
NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE = (0 << 4)
NXP_CONFIG_1_EH_ARBITER_MODE_EN = (1 << 7)
# Config byte 2 flags
NXP_CONFIG_2_GPIO0_SLEW_RATE = (1 << 0)
NXP_CONFIG_2_GPIO1_SLEW_RATE = (1 << 1)
NXP_CONFIG_2_LOCK_BLOCK_COMMAND_SUPPORTED = (1 << 2)
NXP_CONFIG_2_EXTENDED_COMMANDS_SUPPORTED = (1 << 3)
NXP_CONFIG_2_GPIO0_PAD_MASK = (3 << 4)
NXP_CONFIG_2_GPIO1_PAD_MASK = (3 << 6)
# EH config flags # EH config flags
NXP_EH_CONFIG_EH_ENABLE = (1 << 0) NXP_EH_CONFIG_EH_ENABLE = (1 << 0)
NXP_EH_CONFIG_VOUT_V_MASK = (3 << 1) NXP_EH_CONFIG_VOUT_V_MASK = (3 << 1)
NXP_EH_CONFIG_DISABLE_POWER_CHECK = (1 << 3)
NXP_EH_CONFIG_VOUT_I_MASK = (7 << 4)
# Lookup tables # Lookup tables
EH_MODE_MAP = {0: "rfu0", 1: "rfu1", 2: "low_field_strength", 3: "high_field_strength"}
ARBITER_MAP = {0: "normal", 1: "sram_mirror", 2: "sram_passthrough", 3: "sram_phdc"}
USE_CASE_MAP = {0: "i2c_slave", 1: "i2c_master", 2: "gpio_pwm", 3: "tristate"}
EH_V_SEL = {0: "1.8V", 1: "2.4V", 2: "3.0V", 3: "RFU"} EH_V_SEL = {0: "1.8V", 1: "2.4V", 2: "3.0V", 3: "RFU"}
EH_I_SEL = {0: "0.4mA", 1: "0.6mA", 2: "1.4mA", 3: "2.7mA", EH_I_SEL = {0: "0.4mA", 1: "0.6mA", 2: "1.4mA", 3: "2.7mA",
4: "4.0mA", 5: "6.5mA", 6: "9.0mA", 7: "12.5mA"} 4: "4.0mA", 5: "6.5mA", 6: "9.0mA", 7: "12.5mA"}
ARBITER_MAP = {0: "normal", 1: "sram_mirror", 2: "sram_passthrough", 3: "sram_phdc"}
USE_CASE_MAP = {0: "i2c_slave", 1: "i2c_master", 2: "gpio_pwm", 3: "tristate"}
GPIO_PAD_MAP = {0: "disabled", 1: "plain_pullup", 2: "plain", 3: "plain_pulldown"}
EH_MODE_MAP = {0: "rfu0", 1: "rfu1", 2: "low_field_strength", 3: "high_field_strength"}
ED_CONFIG_MAP = {0: "disable", 1: "nfc_field_detect", 2: "pwm", ED_CONFIG_MAP = {0: "disable", 1: "nfc_field_detect", 2: "pwm",
3: "i2c_to_nfc_passthrough", 4: "nfc_to_i2c_passthrough", 3: "i2c_to_nfc_passthrough", 4: "nfc_to_i2c_passthrough",
5: "arbiter_lock", 6: "ndef_msg_tlv_length", 7: "standby_mode", 5: "arbiter_lock", 6: "ndef_msg_tlv_length", 7: "standby_mode",
8: "write_cmd_indication", 9: "read_cmd_indication", 8: "write_cmd_indication", 9: "read_cmd_indication",
10: "start_of_command_indication", 11: "read_from_synch_block", 10: "start_of_command_indication", 11: "read_from_synch_block",
12: "write_to_synch_block", 13: "software_interrupt"} 12: "write_to_synch_block", 13: "software_interrupt"}
GPIO_PAD_MAP = {0: "disabled", 1: "plain_pullup", 2: "plain", 3: "plain_pulldown"}
INTERFACE_MAP = {0: "nfc_only", 1: "gpio", 2: "rfu", 3: "gpio_i2c"} INTERFACE_MAP = {0: "nfc_only", 1: "gpio", 2: "rfu", 3: "gpio_i2c"}
# ---------------------------------------------------------------------------
# xblink expected configuration # xblink expected configuration
# ---------------------------------------------------------------------------
EXPECTED = { EXPECTED = {
"eh_mode": "low_field_strength", "eh_mode": "low_field_strength",
"use_case": "i2c_slave", "use_case": "i2c_slave",
@@ -153,277 +65,199 @@ EXPECTED = {
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Low-level transceive # PCSC helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def iso15693_transceive(cmd_bytes, expected_response_len=32): def hex_str(data):
"""Send raw ISO15693 command and return response bytes (flags stripped).""" return ' '.join(f'{b:02X}' for b in data)
cmd = (ctypes.c_uint8 * len(cmd_bytes))(*cmd_bytes)
rcv = (ctypes.c_uint8 * 256)()
rcv_len = ctypes.c_uint32(0)
status = ufr.uart_transceive(cmd, len(cmd_bytes), rcv, expected_response_len, ctypes.byref(rcv_len))
if status != UFR_OK: def transmit(conn, apdu):
if rcv_len.value > 0: """Send APDU, return (data, sw1, sw2). Raises on transmit error."""
data = bytes(rcv[:rcv_len.value]) res, sw1, sw2 = conn.transmit(list(apdu))
if data[0] == 0x00: # Response flags OK return bytes(res), sw1, sw2
return data[1:]
else:
print(f" WARNING: Response flags = 0x{data[0]:02X} (error)") def pcsc_read_binary(conn, block, le=0x00):
return data[1:] """Read a single ISO15693 block via PCSC READ_BINARY."""
print(f" ERROR: uart_transceive failed, status=0x{status:04X}, rcv_len={rcv_len.value}") try:
data, sw1, sw2 = transmit(conn, [0xFF, 0xB0, 0x00, block, le])
if sw1 == 0x90 and sw2 == 0x00:
return data
except Exception:
pass
return None return None
data = bytes(rcv[:rcv_len.value])
if len(data) > 0 and data[0] != 0x00:
print(f" WARNING: Response flags = 0x{data[0]:02X}")
return data[1:] if len(data) > 1 else data
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ISO15693 inventory # ACR1552 transparent ISO15693 support
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def inventory(): def acr1552_available():
"""Run ISO15693 INVENTORY to discover all tags in the field. """Check if ber_tlv is installed (required for ACR1552 transparent mode)."""
try:
from ber_tlv.tlv import Tlv
return True
except ImportError:
return False
Returns list of 8-byte UIDs (as bytes, LSB first as received on air).
class ACR1552ISO15693:
"""ACR1552 reader with transparent ISO15693 mode.
Adapted from ntag5sensor/reader/acr1552.py.
""" """
# Single-slot inventory: flags = data_rate | inventory | single_slot CLA_PSEUDO = 0xFF
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_INVENTORY | ISO_FLAG_NB_SLOTS_1 INS_TRANS = 0xC2
cmd = [flags, ISO_CMD_INVENTORY, 0x00] # mask_length=0 (no mask) TLV_TAG_ERROR = 0xC0
# Response: flags(1) + DSFID(1) + UID(8) = 10 bytes TLV_TAG_CMD_DATA = 0x95
data = iso15693_transceive(cmd, 10) TLV_TAG_CMD_TIMEOUT = 0x5F46
if data is None: TLV_TAG_CMD_FWTI = 0xFF6E
return [] TLV_TAG_RESP_STATUS = 0x96
TLV_TAG_RESP_FRAMING = 0x92
TLV_TAG_RESP_DATA = 0x97
uids = [] def __init__(self, conn):
if len(data) >= 9: from ber_tlv.tlv import Tlv
dsfid = data[0] self.Tlv = Tlv
uid = data[1:9] # 8 bytes, LSB first self.conn = conn
uids.append(uid) # Begin transparent session
self._transmit_pseudo(0x00, bytes([0x81, 0x00]))
# Switch to ISO15693 L3
self._transmit_pseudo(0x02, bytes([0x8F, 0x02, 0x02, 0x03]))
# TODO: for multi-slot (16 slots), would iterate and collect multiple UIDs. def disconnect(self):
# Single-slot works when only one tag is present; with multiple tags, self._transmit_pseudo(0x00, bytes([0x82, 0x00]))
# collisions occur and we'd need anti-collision. For now this covers
# the primary use case (one NTAG5 Click on the reader).
return uids
# --------------------------------------------------------------------------- def _transmit_pseudo(self, function, data):
# Addressed config reads apdu = bytes([self.CLA_PSEUDO, self.INS_TRANS, 0x00, function,
# --------------------------------------------------------------------------- len(data)]) + data + bytes([0x00])
res, sw1, sw2 = self.conn.transmit(list(apdu))
if sw1 != 0x90 or sw2 != 0x00:
raise Exception(f"PCSC error: {sw1:02X}{sw2:02X}")
tlv = dict(self.Tlv.parse(bytes(res)))
error = tlv[self.TLV_TAG_ERROR]
if error[0] != 0x00 or error[1] != 0x90 or error[2] != 0x00:
raise Exception(f"Transparent error: {hex_str(error)}")
return tlv
def read_config_block_addressed(uid, address, num_blocks=1): def transmit_iso15693(self, cmd_data):
"""Read NTAG5Link config block via NXP READ_CONFIG in addressed mode. """Send raw ISO15693 command, return response data (flags stripped)."""
tlv_data = self.Tlv.build([
Args: (self.TLV_TAG_CMD_TIMEOUT, (1000000).to_bytes(4, "big")),
uid: 8-byte UID (LSB first, as returned by inventory) (self.TLV_TAG_CMD_FWTI, bytes([0x03, 0x01, 15])),
address: config block address (e.g. 0x37) (self.TLV_TAG_CMD_DATA, bytes(cmd_data)),
num_blocks: number of 4-byte blocks to read ])
""" tlv = self._transmit_pseudo(0x01, tlv_data)
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS data = tlv[self.TLV_TAG_RESP_DATA]
cmd = [flags, NXP_CMD_READ_CONFIG, NXP_CMD_MANUF_CODE_NXP] if data[0] & 0x01: # Error flag
cmd.extend(uid) # 8-byte UID raise Exception(f"ISO15693 error: {data[1]:02X}")
cmd.extend([address, num_blocks - 1]) return data[1:] # Strip flags byte
# Response: 1 byte flags + num_blocks * 4 bytes data
return iso15693_transceive(cmd, 1 + num_blocks * 4)
def read_config_block_nonaddressed(address, num_blocks=1): def acr1552_read_config(reader, address, num_blocks=1):
"""Read NTAG5Link config block in non-addressed mode (single tag only).""" """Read NXP config block via ACR1552 transparent mode."""
cmd = [ISO_FLAG_DATA_RATE, NXP_CMD_READ_CONFIG, NXP_CMD_MANUF_CODE_NXP, cmd = bytes([0x02, 0xC0, 0x04, address, num_blocks - 1])
address, num_blocks - 1] return reader.transmit_iso15693(cmd)
return iso15693_transceive(cmd, 1 + num_blocks * 4)
def get_system_info_addressed(uid): def acr1552_get_system_info(reader):
"""Get ISO15693 system info in addressed mode.""" """Get ISO15693 system info via ACR1552."""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS cmd = bytes([0x02, 0x2B])
cmd = [flags, ISO_CMD_SYSTEM_INFO] return reader.transmit_iso15693(cmd)
cmd.extend(uid)
# Response varies; request generous buffer
return iso15693_transceive(cmd, 32)
def get_nxp_system_info_addressed(uid): def acr1552_get_nxp_info(reader):
"""Get NXP-specific system info in addressed mode.""" """Get NXP system info via ACR1552."""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS cmd = bytes([0x02, 0xAB, 0x04])
cmd = [flags, NXP_CMD_SYSTEM_INFO, NXP_CMD_MANUF_CODE_NXP] return reader.transmit_iso15693(cmd)
cmd.extend(uid)
return iso15693_transceive(cmd, 32)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Decoders # Decoders
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def decode_uid(uid_bytes): def decode_uid_from_pcsc(uid_bytes):
"""Format UID for display (MSB first, colon-separated).""" """Format UID from PCSC GET_DATA response (already in NFC byte order)."""
return ":".join(f"{b:02X}" for b in reversed(uid_bytes)) uid_reversed = bytes(reversed(uid_bytes))
return ":".join(f"{b:02X}" for b in uid_reversed)
def decode_system_info(data): def decode_system_info(data):
"""Decode ISO15693 GET_SYSTEM_INFO response.""" """Decode ISO15693 GET_SYSTEM_INFO response."""
if data is None or len(data) < 9: if data is None or len(data) < 9:
return None return None
info_flags = data[0] info_flags = data[0]
uid = data[1:9][::-1] # reverse to MSB first uid = data[1:9][::-1]
res = {"uid": uid, "uid_str": ":".join(f"{b:02X}" for b in uid)} res = {"uid_str": ":".join(f"{b:02X}" for b in uid)}
idx = 9 idx = 9
if info_flags & 0x01 and idx < len(data): # DSFID if info_flags & 0x01 and idx < len(data):
res["dsfid"] = data[idx] res["dsfid"] = data[idx]; idx += 1
idx += 1 if info_flags & 0x02 and idx < len(data):
if info_flags & 0x02 and idx < len(data): # AFI res["afi"] = data[idx]; idx += 1
res["afi"] = data[idx] if info_flags & 0x04 and idx + 1 < len(data):
idx += 1
if info_flags & 0x04 and idx + 1 < len(data): # Memory size
res["num_blocks"] = data[idx] + 1 res["num_blocks"] = data[idx] + 1
res["block_size"] = (data[idx + 1] & 0x1F) + 1 res["block_size"] = (data[idx + 1] & 0x1F) + 1
res["memory_bytes"] = res["num_blocks"] * res["block_size"] res["memory_bytes"] = res["num_blocks"] * res["block_size"]
idx += 2 idx += 2
if info_flags & 0x08 and idx < len(data): # IC reference if info_flags & 0x08 and idx < len(data):
res["ic_ref"] = data[idx] res["ic_ref"] = data[idx]; idx += 1
idx += 1
return res return res
def decode_nxp_system_info(data): def decode_nxp_info(data):
"""Decode NXP GET_NXP_SYSTEM_INFO response.""" """Decode NXP GET_NXP_SYSTEM_INFO response."""
if data is None or len(data) < 7: if data is None or len(data) < 7:
return None return None
res = {
"pp_pointer": data[0],
"lock_bits_raw": data[2],
}
if len(data) >= 7:
b3 = data[6] b3 = data[6]
res["interface"] = INTERFACE_MAP.get((b3 >> 5) & 0x03, "unknown") return {
res["num_keys"] = b3 & 0x0F "pp_pointer": data[0],
"interface": INTERFACE_MAP.get((b3 >> 5) & 0x03, "unknown"),
return res "num_keys": b3 & 0x0F,
}
def decode_config(data): def decode_config(data):
"""Decode CONFIG block (address 0x37) — 4 bytes.""" """Decode CONFIG block (NXP config address 0x37) — 4 bytes."""
if data is None or len(data) < 3: if data is None or len(data) < 3:
return None return None
b0, b1, b2 = data[0], data[1], data[2] b0, b1, b2 = data[0], data[1], data[2]
return { return {
"raw": [f"0x{b:02X}" for b in data[:4]] if len(data) >= 4 else [f"0x{b:02X}" for b in data], "raw": [f"0x{b:02X}" for b in data[:4]] if len(data) >= 4 else [f"0x{b:02X}" for b in data],
"auto_standby": bool(b0 & NXP_CONFIG_0_AUTO_STANDBY_MODE_EN), "auto_standby": bool(b0 & 0x01),
"lock_session_reg": bool(b0 & NXP_CONFIG_0_LOCK_SESSION_REG), "lock_session_reg": bool(b0 & 0x02),
"eh_mode": EH_MODE_MAP.get((b0 >> 2) & 0x03, "unknown"), "eh_mode": EH_MODE_MAP.get((b0 >> 2) & 0x03, "unknown"),
"sram_copy_en": bool(b0 & NXP_CONFIG_0_SRAM_COPY_EN), "sram_copy_en": bool(b0 & 0x80),
"pt_transfer_dir": "reader_to_tag" if (b1 & NXP_CONFIG_1_PT_TRANSFER_DIR) else "tag_to_reader", "pt_transfer_dir": "reader_to_tag" if (b1 & 0x01) else "tag_to_reader",
"sram_enabled": bool(b1 & NXP_CONFIG_1_SRAM_ENABLE), "sram_enabled": bool(b1 & 0x02),
"arbiter_mode": ARBITER_MAP.get((b1 >> 2) & 0x03, "unknown"), "arbiter_mode": ARBITER_MAP.get((b1 >> 2) & 0x03, "unknown"),
"use_case": USE_CASE_MAP.get((b1 >> 4) & 0x03, "unknown"), "use_case": USE_CASE_MAP.get((b1 >> 4) & 0x03, "unknown"),
"eh_arbiter_mode_en": bool(b1 & NXP_CONFIG_1_EH_ARBITER_MODE_EN), "eh_arbiter_mode_en": bool(b1 & 0x80),
"gpio0_slew_fast": bool(b2 & NXP_CONFIG_2_GPIO0_SLEW_RATE), "gpio0_slew_fast": bool(b2 & 0x01),
"gpio1_slew_fast": bool(b2 & NXP_CONFIG_2_GPIO1_SLEW_RATE), "gpio1_slew_fast": bool(b2 & 0x02),
"lock_block_supported": bool(b2 & NXP_CONFIG_2_LOCK_BLOCK_COMMAND_SUPPORTED), "lock_block_supported": bool(b2 & 0x04),
"ext_commands_supported": bool(b2 & NXP_CONFIG_2_EXTENDED_COMMANDS_SUPPORTED), "ext_commands_supported": bool(b2 & 0x08),
"gpio0_pad_in": GPIO_PAD_MAP.get((b2 >> 4) & 0x03, "unknown"), "gpio0_pad_in": GPIO_PAD_MAP.get((b2 >> 4) & 0x03, "unknown"),
"gpio1_pad_in": GPIO_PAD_MAP.get((b2 >> 6) & 0x03, "unknown"), "gpio1_pad_in": GPIO_PAD_MAP.get((b2 >> 6) & 0x03, "unknown"),
} }
def decode_eh_config(data): def decode_eh_config(data):
"""Decode EH/ED CONFIG block (address 0x3D) — 4 bytes.""" """Decode EH/ED CONFIG block (NXP config address 0x3D) — 4 bytes."""
if data is None or len(data) < 1: if data is None or len(data) < 1:
return None return None
eh = data[0] eh = data[0]
ed = data[2] if len(data) > 2 else 0 ed = data[2] if len(data) > 2 else 0
return { return {
"raw": [f"0x{b:02X}" for b in data[:4]] if len(data) >= 4 else [f"0x{b:02X}" for b in data], "raw": [f"0x{b:02X}" for b in data[:4]] if len(data) >= 4 else [f"0x{b:02X}" for b in data],
"eh_enable": bool(eh & NXP_EH_CONFIG_EH_ENABLE), "eh_enable": bool(eh & 0x01),
"eh_voltage": EH_V_SEL.get((eh >> 1) & 0x03, "unknown"), "eh_voltage": EH_V_SEL.get((eh >> 1) & 0x03, "unknown"),
"disable_power_check": bool(eh & NXP_EH_CONFIG_DISABLE_POWER_CHECK), "disable_power_check": bool(eh & 0x08),
"eh_current": EH_I_SEL.get((eh >> 4) & 0x07, "unknown"), "eh_current": EH_I_SEL.get((eh >> 4) & 0x07, "unknown"),
"ed_config": ED_CONFIG_MAP.get(ed & 0x0F, "unknown"), "ed_config": ED_CONFIG_MAP.get(ed & 0x0F, "unknown"),
} }
# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------
def check_against_expected(config, eh_config): def print_config(config):
"""Compare config against xblink expected values.""" """Display CONFIG block contents."""
print("\n--- xblink Config Check ---")
checks = [
("EH mode", config.get("eh_mode"), EXPECTED["eh_mode"]),
("Use case", config.get("use_case"), EXPECTED["use_case"]),
("SRAM enabled", config.get("sram_enabled"), EXPECTED["sram_enabled"]),
("Arbiter mode", config.get("arbiter_mode"), EXPECTED["arbiter_mode"]),
("EH enable", eh_config.get("eh_enable"), EXPECTED["eh_enable"]),
("EH voltage", eh_config.get("eh_voltage"), EXPECTED["eh_voltage"]),
]
all_ok = True
for name, actual, expected in checks:
match = actual == expected
symbol = "OK" if match else "MISMATCH"
if not match:
all_ok = False
print(f" [{symbol:8s}] {name:20s}: {actual} (expected: {expected})")
if all_ok:
print("\n All config values match xblink requirements!")
else:
print("\n Some values need updating. Use ntag5sensor tooling to reconfigure.")
return all_ok
def read_and_check_tag(uid):
"""Read all config from one tag and verify against xblink requirements.
Args:
uid: 8-byte UID (LSB first) or None for non-addressed mode
"""
use_addressed = uid is not None
uid_str = decode_uid(uid) if uid else "(non-addressed)"
# --- ISO15693 system info ---
if use_addressed:
print(f"\nISO15693 System Info...")
sys_data = get_system_info_addressed(uid)
sys_info = decode_system_info(sys_data)
if sys_info:
print(f" Memory: {sys_info.get('memory_bytes', '?')} bytes "
f"({sys_info.get('num_blocks', '?')} blocks x {sys_info.get('block_size', '?')} bytes)")
if "ic_ref" in sys_info:
print(f" IC ref: 0x{sys_info['ic_ref']:02X}")
else:
print(" Failed to read system info.")
# --- NXP system info ---
if use_addressed:
print(f"\nNXP System Info...")
nxp_data = get_nxp_system_info_addressed(uid)
nxp_info = decode_nxp_system_info(nxp_data)
if nxp_info:
print(f" Interface: {nxp_info.get('interface', '?')}")
print(f" Num keys: {nxp_info.get('num_keys', '?')}")
else:
print(" Failed to read NXP system info (may not be an NXP tag).")
# --- CONFIG block (0x37) ---
print(f"\nCONFIG block (0x37)...")
if use_addressed:
config_data = read_config_block_addressed(uid, NXP_CONFIG_ADDR_CONFIG)
else:
config_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_CONFIG)
config = decode_config(config_data)
if config:
print(f" Raw bytes: {config['raw']}") print(f" Raw bytes: {config['raw']}")
print(f" EH mode: {config['eh_mode']}") print(f" EH mode: {config['eh_mode']}")
print(f" Use case: {config['use_case']}") print(f" Use case: {config['use_case']}")
@@ -437,93 +271,219 @@ def read_and_check_tag(uid):
print(f" GPIO1 pad: {config['gpio1_pad_in']}") print(f" GPIO1 pad: {config['gpio1_pad_in']}")
print(f" Lock block: {config['lock_block_supported']}") print(f" Lock block: {config['lock_block_supported']}")
print(f" Ext commands: {config['ext_commands_supported']}") print(f" Ext commands: {config['ext_commands_supported']}")
else:
print(" Failed to read config block.")
# --- EH/ED CONFIG block (0x3D) ---
print(f"\nEH/ED CONFIG block (0x3D)...")
if use_addressed:
eh_data = read_config_block_addressed(uid, NXP_CONFIG_ADDR_EH_CONFIG)
else:
eh_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_EH_CONFIG)
eh_config = decode_eh_config(eh_data)
if eh_config: def print_eh_config(eh_config):
"""Display EH/ED CONFIG block contents."""
print(f" Raw bytes: {eh_config['raw']}") print(f" Raw bytes: {eh_config['raw']}")
print(f" EH enable: {eh_config['eh_enable']}") print(f" EH enable: {eh_config['eh_enable']}")
print(f" EH voltage: {eh_config['eh_voltage']}") print(f" EH voltage: {eh_config['eh_voltage']}")
print(f" EH current limit: {eh_config['eh_current']}") print(f" EH current limit: {eh_config['eh_current']}")
print(f" Power check: {'disabled' if eh_config['disable_power_check'] else 'enabled'}") print(f" Power check: {'disabled' if eh_config['disable_power_check'] else 'enabled'}")
print(f" ED config: {eh_config['ed_config']}") print(f" ED config: {eh_config['ed_config']}")
else:
print(" Failed to read EH config block.")
# --- Check against xblink expected values ---
def check_against_expected(config, eh_config):
"""Compare config against xblink expected values."""
print("\n--- xblink Config Check ---")
checks = [
("EH mode", config.get("eh_mode"), EXPECTED["eh_mode"]),
("Use case", config.get("use_case"), EXPECTED["use_case"]),
("SRAM enabled", config.get("sram_enabled"), EXPECTED["sram_enabled"]),
("Arbiter mode", config.get("arbiter_mode"), EXPECTED["arbiter_mode"]),
("EH enable", eh_config.get("eh_enable"), EXPECTED["eh_enable"]),
("EH voltage", eh_config.get("eh_voltage"), EXPECTED["eh_voltage"]),
]
all_ok = True
for name, actual, expected in checks:
match = actual == expected
symbol = "OK" if match else "MISMATCH"
if not match:
all_ok = False
print(f" [{symbol:8s}] {name:20s}: {actual} (expected: {expected})")
if all_ok:
print("\n All config values match xblink requirements!")
else:
print("\n Some values need updating. Use ntag5sensor + ACR1552 to reconfigure.")
return all_ok
# ---------------------------------------------------------------------------
# Reader-specific flows
# ---------------------------------------------------------------------------
def run_with_acr1552(conn):
"""Full config verification via ACR1552 transparent ISO15693 mode."""
print("Initializing ACR1552 transparent ISO15693 mode...")
reader = ACR1552ISO15693(conn)
try:
# System info
print("\nISO15693 System Info...")
sys_data = acr1552_get_system_info(reader)
sys_info = decode_system_info(sys_data)
if sys_info:
print(f" UID: {sys_info['uid_str']}")
print(f" Memory: {sys_info.get('memory_bytes', '?')} bytes "
f"({sys_info.get('num_blocks', '?')} x {sys_info.get('block_size', '?')})")
if "ic_ref" in sys_info:
print(f" IC ref: 0x{sys_info['ic_ref']:02X}")
# NXP system info
print("\nNXP System Info...")
nxp_data = acr1552_get_nxp_info(reader)
nxp_info = decode_nxp_info(nxp_data)
if nxp_info:
print(f" Interface: {nxp_info['interface']}")
print(f" Num keys: {nxp_info['num_keys']}")
# CONFIG block
print("\nCONFIG block (0x37)...")
config_data = acr1552_read_config(reader, NXP_CONFIG_ADDR_CONFIG)
config = decode_config(config_data)
if config:
print_config(config)
# EH/ED CONFIG block
print("\nEH/ED CONFIG block (0x3D)...")
eh_data = acr1552_read_config(reader, NXP_CONFIG_ADDR_EH_CONFIG)
eh_config = decode_eh_config(eh_data)
if eh_config:
print_eh_config(eh_config)
# Verify
if config and eh_config: if config and eh_config:
return check_against_expected(config, eh_config) check_against_expected(config, eh_config)
return False finally:
reader.disconnect()
def run_with_generic_pcsc(conn, reader_name):
"""Basic tag info via standard PCSC commands (uFR Zero, etc).
READ_BINARY maps to ISO15693 READ_SINGLE_BLOCK — can only read user
EEPROM, not NXP config registers (which require custom command 0xC0).
"""
# Get UID
print("\nTag UID...")
try:
data, sw1, sw2 = transmit(conn, [0xFF, 0xCA, 0x00, 0x00, 0x00])
except Exception as e:
print(f" Failed: {e}")
return
if sw1 == 0x90:
print(f" UID: {decode_uid_from_pcsc(data)}")
print(f" Raw: {hex_str(data)}")
# Check if NXP ISO15693 tag (E0 04 prefix in reversed UID)
uid_rev = bytes(reversed(data))
if len(uid_rev) >= 2 and uid_rev[0] == 0xE0 and uid_rev[1] == 0x04:
print(f" Type: NXP ISO15693 (manufacturer code 0x04)")
else:
print(f" Type: ISO15693 (manufacturer code 0x{uid_rev[1]:02X})")
else:
print(f" Failed to read UID: SW={sw1:02X}{sw2:02X}")
# Read EEPROM block 0 (Capability Container)
print("\nEEPROM Block 0 (Capability Container)...")
data = pcsc_read_binary(conn, 0x00)
if data:
print(f" Data: {hex_str(data)}")
if len(data) >= 4 and data[0] == 0xE1:
print(f" NDEF formatted: version {data[1] >> 4}.{data[1] & 0x0F}, "
f"max size {data[2] * 8} bytes")
else:
print(" Failed to read block 0")
# Read a few EEPROM blocks to verify access
print("\nEEPROM Blocks 1-4 (user data)...")
for block in range(1, 5):
data = pcsc_read_binary(conn, block)
if data:
print(f" Block 0x{block:02X}: {hex_str(data)}")
else:
print(f" Block 0x{block:02X}: read failed")
# Report limitation
print("\n" + "=" * 60)
print("LIMITATION: NXP config registers cannot be read")
print("=" * 60)
print(f"\nReader '{reader_name}' only supports standard PCSC commands,")
print("which map to ISO15693 READ_SINGLE_BLOCK for user EEPROM.")
print("NXP config registers (CONFIG 0x37, EH_CONFIG 0x3D) require")
print("the NXP custom READ_CONFIG command (0xC0), which needs a")
print("reader with transparent ISO15693 passthrough support.")
print("\nTo verify NTAG5Link config for xblink:")
print(" 1. Use ACR1552 reader with this tool (auto-detected)")
print(" 2. Use ntag5sensor directly: python3 -m ntag5sensor info")
print(" 3. Use NXP TagInfo app on Android")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Main # Main
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def main(): def main():
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0" reader_list = readers()
print(f"Connecting to uFR Zero on {port}...") if not reader_list:
print("No PCSC readers found. Is pcscd running?")
# Open reader: reader_type=1 (uFR @ 1Mbps), port_interface=1 (serial/CP210x)
status = ufr.ReaderOpenEx(1, port.encode(), 1, None)
if status != UFR_OK:
print(f"ERROR: Failed to open reader, status=0x{status:04X}")
sys.exit(1) sys.exit(1)
import time print("Available readers:")
time.sleep(0.5) for i, r in enumerate(reader_list):
print("Reader connected.") print(f" [{i}] {r}")
# Enter ISO15693 transparent mode (tx_crc=1, rx_crc=1, rf_timeout=10000us, uart_timeout=500ms) # Detect reader type
status = ufr.card_transceive_mode_start(1, 1, 10000, 500) acr1552_reader = None
if status != UFR_OK: any_reader = None
print(f"ERROR: Failed to enter transceive mode, status=0x{status:04X}") for r in reader_list:
ufr.ReaderClose() name = str(r)
if name.startswith("ACS ACR1552"):
acr1552_reader = r
break
if any_reader is None:
any_reader = r
target_reader = acr1552_reader or any_reader
reader_name = str(target_reader)
is_acr1552 = acr1552_reader is not None
print(f"\nUsing: {reader_name}")
if is_acr1552:
if not acr1552_available():
print("ERROR: ber_tlv package required for ACR1552. Install: pip install ber-tlv")
sys.exit(1)
print("Mode: ACR1552 transparent ISO15693 (full NXP config access)")
else:
print("Mode: Standard PCSC (basic tag info only)")
# Connect to tag
print("\nWaiting for tag...")
request = CardRequest(timeout=30, readers=[target_reader], cardType=AnyCardType())
try:
card = request.waitforcard()
except Exception as e:
print(f"No tag found: {e}")
sys.exit(1) sys.exit(1)
print("ISO15693 transparent mode active.") card.connection.connect()
atr = bytes(card.connection.getATR())
print(f"Connected! ATR: {hex_str(atr)}")
# --- Step 1: Inventory --- # Verify ISO15693 tag (check ATR for 03 06 0B = ISO 15693 Part 3)
print("\n=== ISO15693 Inventory ===") if b'\x03\x06\x0B' in atr:
uids = inventory() print("Tag type: ISO 15693")
elif b'\x03\x06\x03' in atr:
if not uids: print("Tag type: ISO 14443-3A")
print(" No tags found. Trying non-addressed mode as fallback...")
print("\n=== Tag (non-addressed) ===")
read_and_check_tag(None)
else: else:
print(f" Found {len(uids)} tag(s):") print("Tag type: unknown (continuing anyway)")
for i, uid in enumerate(uids):
print(f" [{i}] UID: {decode_uid(uid)}")
# --- Step 2: Read each tag --- # Run appropriate flow
all_ok = True if is_acr1552:
for i, uid in enumerate(uids): run_with_acr1552(card.connection)
print(f"\n{'='*50}")
print(f"=== Tag {i}: {decode_uid(uid)} ===")
print(f"{'='*50}")
ok = read_and_check_tag(uid)
if not ok:
all_ok = False
if len(uids) > 1:
print(f"\n{'='*50}")
if all_ok:
print("All tags pass xblink config check.")
else: else:
print("Some tags have config mismatches.") run_with_generic_pcsc(card.connection, reader_name)
# Cleanup card.connection.disconnect()
ufr.card_transceive_mode_stop() print("\nDone.")
ufr.ReaderClose()
print("\nReader closed.")
if __name__ == "__main__": if __name__ == "__main__":