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
**Status**: Tool written, reader connection blocked
**Status**: Partial — tag detected, config read requires ACR1552
## 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
`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:
- ISO15693 INVENTORY to discover all tags by UID
- Addressed mode for all config reads (supports multiple tags in field)
- Reads CONFIG block (0x37) and EH/ED CONFIG block (0x3D)
- NXP system info for interface type verification
- Checks against xblink expected config values
**Reader support:**
- **ACR1552**: Full NXP config verification via transparent ISO15693 mode (NXP READ_CONFIG 0xC0)
- **uFR Zero (CCID)**: Basic tag info only — UID, EEPROM blocks. Cannot read NXP config registers.
## Expected Config
@@ -29,45 +26,56 @@ Features:
| EH enable | true | EH_CONFIG byte 0, bit 0 = 1 |
| 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**:
- 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
### What works (PCSC/CCID)
**Possible causes**:
1. Device may not be a uFR Zero — the CP210x bridge has generic descriptors
2. Reader firmware may need updating
3. Reader may be in a non-standard mode
4. Different hardware variant requiring different protocol
| Command | APDU | Result |
|---------|------|--------|
| GET_DATA (UID) | `FF CA 00 00 00` | UID: `E0:04:01:58:7F:8F:11:00` (NXP ISO15693) |
| READ_BINARY | `FF B0 00 xx 00` | Reads ISO15693 user EEPROM blocks |
| NDEF CC (block 0) | `FF B0 00 00 00` | `E1 40 80 09` — NDEF v4.0, 1024 bytes |
**Next steps**:
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)
### What doesn't work
## 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
# From ntag5sensor — already has full config read/write support
# ntag5sensor has proven ACR1552 + NXP custom command support:
chip = NTAG5Link(reader)
config = chip.get_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)
Use ntag5sensor's write functions (via ACR1552 or adapted for uFR Zero):
Use ntag5sensor's write functions via ACR1552:
```python
# 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,148 +1,60 @@
#!/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
to send NXP custom commands for reading config registers.
Supports two reader types:
- 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
reads each tag using addressed mode.
Usage: python3 ntag5_config_check.py [/dev/ttyUSBx]
Usage: python3 ntag5_config_check.py
"""
import ctypes
import sys
import os
# ---------------------------------------------------------------------------
# Load uFCoder library
# ---------------------------------------------------------------------------
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
from smartcard.System import readers
from smartcard.CardRequest import CardRequest
from smartcard.CardType import AnyCardType
from smartcard.CardConnection import CardConnection
# ---------------------------------------------------------------------------
# NXP NTAG5Link config constants (from ntag5sensor)
# ---------------------------------------------------------------------------
# Config addresses
NXP_CONFIG_ADDR_CONFIG = 0x37
NXP_CONFIG_ADDR_CONFIG = 0x37
NXP_CONFIG_ADDR_EH_CONFIG = 0x3D
# 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_LOW_FIELD_STRENGTH = (2 << 2)
NXP_CONFIG_0_EH_MODE_HIGH_FIELD_STRENGTH = (3 << 2)
NXP_CONFIG_0_SRAM_COPY_EN = (1 << 7)
NXP_CONFIG_0_EH_MODE_MASK = (3 << 2)
# Config byte 1 flags
NXP_CONFIG_1_PT_TRANSFER_DIR = (1 << 0)
NXP_CONFIG_1_SRAM_ENABLE = (1 << 1)
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_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)
NXP_CONFIG_1_SRAM_ENABLE = (1 << 1)
NXP_CONFIG_1_ARBITER_MASK = (3 << 2)
NXP_CONFIG_1_USE_CASE_MASK = (3 << 4)
# EH config flags
NXP_EH_CONFIG_EH_ENABLE = (1 << 0)
NXP_EH_CONFIG_VOUT_V_MASK = (3 << 1)
NXP_EH_CONFIG_DISABLE_POWER_CHECK = (1 << 3)
NXP_EH_CONFIG_VOUT_I_MASK = (7 << 4)
NXP_EH_CONFIG_EH_ENABLE = (1 << 0)
NXP_EH_CONFIG_VOUT_V_MASK = (3 << 1)
# 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_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"}
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",
3: "i2c_to_nfc_passthrough", 4: "nfc_to_i2c_passthrough",
5: "arbiter_lock", 6: "ndef_msg_tlv_length", 7: "standby_mode",
8: "write_cmd_indication", 9: "read_cmd_indication",
10: "start_of_command_indication", 11: "read_from_synch_block",
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"}
# ---------------------------------------------------------------------------
# xblink expected configuration
# ---------------------------------------------------------------------------
EXPECTED = {
"eh_mode": "low_field_strength",
"use_case": "i2c_slave",
@@ -153,206 +65,223 @@ EXPECTED = {
}
# ---------------------------------------------------------------------------
# Low-level transceive
# PCSC helpers
# ---------------------------------------------------------------------------
def iso15693_transceive(cmd_bytes, expected_response_len=32):
"""Send raw ISO15693 command and return response bytes (flags stripped)."""
cmd = (ctypes.c_uint8 * len(cmd_bytes))(*cmd_bytes)
rcv = (ctypes.c_uint8 * 256)()
rcv_len = ctypes.c_uint32(0)
def hex_str(data):
return ' '.join(f'{b:02X}' for b in data)
status = ufr.uart_transceive(cmd, len(cmd_bytes), rcv, expected_response_len, ctypes.byref(rcv_len))
if status != UFR_OK:
if rcv_len.value > 0:
data = bytes(rcv[:rcv_len.value])
if data[0] == 0x00: # Response flags OK
return data[1:]
else:
print(f" WARNING: Response flags = 0x{data[0]:02X} (error)")
return data[1:]
print(f" ERROR: uart_transceive failed, status=0x{status:04X}, rcv_len={rcv_len.value}")
return None
def transmit(conn, apdu):
"""Send APDU, return (data, sw1, sw2). Raises on transmit error."""
res, sw1, sw2 = conn.transmit(list(apdu))
return bytes(res), sw1, sw2
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
def pcsc_read_binary(conn, block, le=0x00):
"""Read a single ISO15693 block via PCSC READ_BINARY."""
try:
data, sw1, sw2 = transmit(conn, [0xFF, 0xB0, 0x00, block, le])
if sw1 == 0x90 and sw2 == 0x00:
return data
except Exception:
pass
return None
# ---------------------------------------------------------------------------
# ISO15693 inventory
# ACR1552 transparent ISO15693 support
# ---------------------------------------------------------------------------
def inventory():
"""Run ISO15693 INVENTORY to discover all tags in the field.
def acr1552_available():
"""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
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_INVENTORY | ISO_FLAG_NB_SLOTS_1
cmd = [flags, ISO_CMD_INVENTORY, 0x00] # mask_length=0 (no mask)
# Response: flags(1) + DSFID(1) + UID(8) = 10 bytes
data = iso15693_transceive(cmd, 10)
if data is None:
return []
CLA_PSEUDO = 0xFF
INS_TRANS = 0xC2
TLV_TAG_ERROR = 0xC0
TLV_TAG_CMD_DATA = 0x95
TLV_TAG_CMD_TIMEOUT = 0x5F46
TLV_TAG_CMD_FWTI = 0xFF6E
TLV_TAG_RESP_STATUS = 0x96
TLV_TAG_RESP_FRAMING = 0x92
TLV_TAG_RESP_DATA = 0x97
uids = []
if len(data) >= 9:
dsfid = data[0]
uid = data[1:9] # 8 bytes, LSB first
uids.append(uid)
def __init__(self, conn):
from ber_tlv.tlv import Tlv
self.Tlv = Tlv
self.conn = conn
# 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.
# Single-slot works when only one tag is present; with multiple tags,
# 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 disconnect(self):
self._transmit_pseudo(0x00, bytes([0x82, 0x00]))
# ---------------------------------------------------------------------------
# Addressed config reads
# ---------------------------------------------------------------------------
def _transmit_pseudo(self, function, data):
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):
"""Read NTAG5Link config block via NXP READ_CONFIG in addressed mode.
Args:
uid: 8-byte UID (LSB first, as returned by inventory)
address: config block address (e.g. 0x37)
num_blocks: number of 4-byte blocks to read
"""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS
cmd = [flags, NXP_CMD_READ_CONFIG, NXP_CMD_MANUF_CODE_NXP]
cmd.extend(uid) # 8-byte UID
cmd.extend([address, num_blocks - 1])
# Response: 1 byte flags + num_blocks * 4 bytes data
return iso15693_transceive(cmd, 1 + num_blocks * 4)
def transmit_iso15693(self, cmd_data):
"""Send raw ISO15693 command, return response data (flags stripped)."""
tlv_data = self.Tlv.build([
(self.TLV_TAG_CMD_TIMEOUT, (1000000).to_bytes(4, "big")),
(self.TLV_TAG_CMD_FWTI, bytes([0x03, 0x01, 15])),
(self.TLV_TAG_CMD_DATA, bytes(cmd_data)),
])
tlv = self._transmit_pseudo(0x01, tlv_data)
data = tlv[self.TLV_TAG_RESP_DATA]
if data[0] & 0x01: # Error flag
raise Exception(f"ISO15693 error: {data[1]:02X}")
return data[1:] # Strip flags byte
def read_config_block_nonaddressed(address, num_blocks=1):
"""Read NTAG5Link config block in non-addressed mode (single tag only)."""
cmd = [ISO_FLAG_DATA_RATE, NXP_CMD_READ_CONFIG, NXP_CMD_MANUF_CODE_NXP,
address, num_blocks - 1]
return iso15693_transceive(cmd, 1 + num_blocks * 4)
def acr1552_read_config(reader, address, num_blocks=1):
"""Read NXP config block via ACR1552 transparent mode."""
cmd = bytes([0x02, 0xC0, 0x04, address, num_blocks - 1])
return reader.transmit_iso15693(cmd)
def get_system_info_addressed(uid):
"""Get ISO15693 system info in addressed mode."""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS
cmd = [flags, ISO_CMD_SYSTEM_INFO]
cmd.extend(uid)
# Response varies; request generous buffer
return iso15693_transceive(cmd, 32)
def acr1552_get_system_info(reader):
"""Get ISO15693 system info via ACR1552."""
cmd = bytes([0x02, 0x2B])
return reader.transmit_iso15693(cmd)
def get_nxp_system_info_addressed(uid):
"""Get NXP-specific system info in addressed mode."""
flags = ISO_FLAG_DATA_RATE | ISO_FLAG_ADDRESS
cmd = [flags, NXP_CMD_SYSTEM_INFO, NXP_CMD_MANUF_CODE_NXP]
cmd.extend(uid)
return iso15693_transceive(cmd, 32)
def acr1552_get_nxp_info(reader):
"""Get NXP system info via ACR1552."""
cmd = bytes([0x02, 0xAB, 0x04])
return reader.transmit_iso15693(cmd)
# ---------------------------------------------------------------------------
# Decoders
# ---------------------------------------------------------------------------
def decode_uid(uid_bytes):
"""Format UID for display (MSB first, colon-separated)."""
return ":".join(f"{b:02X}" for b in reversed(uid_bytes))
def decode_uid_from_pcsc(uid_bytes):
"""Format UID from PCSC GET_DATA response (already in NFC byte order)."""
uid_reversed = bytes(reversed(uid_bytes))
return ":".join(f"{b:02X}" for b in uid_reversed)
def decode_system_info(data):
"""Decode ISO15693 GET_SYSTEM_INFO response."""
if data is None or len(data) < 9:
return None
info_flags = data[0]
uid = data[1:9][::-1] # reverse to MSB first
res = {"uid": uid, "uid_str": ":".join(f"{b:02X}" for b in uid)}
uid = data[1:9][::-1]
res = {"uid_str": ":".join(f"{b:02X}" for b in uid)}
idx = 9
if info_flags & 0x01 and idx < len(data): # DSFID
res["dsfid"] = data[idx]
idx += 1
if info_flags & 0x02 and idx < len(data): # AFI
res["afi"] = data[idx]
idx += 1
if info_flags & 0x04 and idx + 1 < len(data): # Memory size
if info_flags & 0x01 and idx < len(data):
res["dsfid"] = data[idx]; idx += 1
if info_flags & 0x02 and idx < len(data):
res["afi"] = data[idx]; idx += 1
if info_flags & 0x04 and idx + 1 < len(data):
res["num_blocks"] = data[idx] + 1
res["block_size"] = (data[idx + 1] & 0x1F) + 1
res["memory_bytes"] = res["num_blocks"] * res["block_size"]
idx += 2
if info_flags & 0x08 and idx < len(data): # IC reference
res["ic_ref"] = data[idx]
idx += 1
if info_flags & 0x08 and idx < len(data):
res["ic_ref"] = data[idx]; idx += 1
return res
def decode_nxp_system_info(data):
def decode_nxp_info(data):
"""Decode NXP GET_NXP_SYSTEM_INFO response."""
if data is None or len(data) < 7:
return None
res = {
b3 = data[6]
return {
"pp_pointer": data[0],
"lock_bits_raw": data[2],
"interface": INTERFACE_MAP.get((b3 >> 5) & 0x03, "unknown"),
"num_keys": b3 & 0x0F,
}
if len(data) >= 7:
b3 = data[6]
res["interface"] = INTERFACE_MAP.get((b3 >> 5) & 0x03, "unknown")
res["num_keys"] = b3 & 0x0F
return res
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:
return None
b0, b1, b2 = data[0], data[1], data[2]
return {
"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),
"lock_session_reg": bool(b0 & NXP_CONFIG_0_LOCK_SESSION_REG),
"auto_standby": bool(b0 & 0x01),
"lock_session_reg": bool(b0 & 0x02),
"eh_mode": EH_MODE_MAP.get((b0 >> 2) & 0x03, "unknown"),
"sram_copy_en": bool(b0 & NXP_CONFIG_0_SRAM_COPY_EN),
"pt_transfer_dir": "reader_to_tag" if (b1 & NXP_CONFIG_1_PT_TRANSFER_DIR) else "tag_to_reader",
"sram_enabled": bool(b1 & NXP_CONFIG_1_SRAM_ENABLE),
"sram_copy_en": bool(b0 & 0x80),
"pt_transfer_dir": "reader_to_tag" if (b1 & 0x01) else "tag_to_reader",
"sram_enabled": bool(b1 & 0x02),
"arbiter_mode": ARBITER_MAP.get((b1 >> 2) & 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),
"gpio0_slew_fast": bool(b2 & NXP_CONFIG_2_GPIO0_SLEW_RATE),
"gpio1_slew_fast": bool(b2 & NXP_CONFIG_2_GPIO1_SLEW_RATE),
"lock_block_supported": bool(b2 & NXP_CONFIG_2_LOCK_BLOCK_COMMAND_SUPPORTED),
"ext_commands_supported": bool(b2 & NXP_CONFIG_2_EXTENDED_COMMANDS_SUPPORTED),
"eh_arbiter_mode_en": bool(b1 & 0x80),
"gpio0_slew_fast": bool(b2 & 0x01),
"gpio1_slew_fast": bool(b2 & 0x02),
"lock_block_supported": bool(b2 & 0x04),
"ext_commands_supported": bool(b2 & 0x08),
"gpio0_pad_in": GPIO_PAD_MAP.get((b2 >> 4) & 0x03, "unknown"),
"gpio1_pad_in": GPIO_PAD_MAP.get((b2 >> 6) & 0x03, "unknown"),
}
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:
return None
eh = data[0]
ed = data[2] if len(data) > 2 else 0
return {
"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"),
"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"),
"ed_config": ED_CONFIG_MAP.get(ed & 0x0F, "unknown"),
}
# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------
def print_config(config):
"""Display CONFIG block contents."""
print(f" Raw bytes: {config['raw']}")
print(f" EH mode: {config['eh_mode']}")
print(f" Use case: {config['use_case']}")
print(f" SRAM enabled: {config['sram_enabled']}")
print(f" Arbiter mode: {config['arbiter_mode']}")
print(f" Auto standby: {config['auto_standby']}")
print(f" SRAM copy: {config['sram_copy_en']}")
print(f" PT transfer dir: {config['pt_transfer_dir']}")
print(f" EH arbiter mode: {config['eh_arbiter_mode_en']}")
print(f" GPIO0 pad: {config['gpio0_pad_in']}")
print(f" GPIO1 pad: {config['gpio1_pad_in']}")
print(f" Lock block: {config['lock_block_supported']}")
print(f" Ext commands: {config['ext_commands_supported']}")
def print_eh_config(eh_config):
"""Display EH/ED CONFIG block contents."""
print(f" Raw bytes: {eh_config['raw']}")
print(f" EH enable: {eh_config['eh_enable']}")
print(f" EH voltage: {eh_config['eh_voltage']}")
print(f" EH current limit: {eh_config['eh_current']}")
print(f" Power check: {'disabled' if eh_config['disable_power_check'] else 'enabled'}")
print(f" ED config: {eh_config['ed_config']}")
def check_against_expected(config, eh_config):
"""Compare config against xblink expected values."""
@@ -365,7 +294,6 @@ def check_against_expected(config, eh_config):
("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
@@ -377,153 +305,185 @@ def check_against_expected(config, eh_config):
if all_ok:
print("\n All config values match xblink requirements!")
else:
print("\n Some values need updating. Use ntag5sensor tooling to reconfigure.")
print("\n Some values need updating. Use ntag5sensor + ACR1552 to reconfigure.")
return all_ok
# ---------------------------------------------------------------------------
# Reader-specific flows
# ---------------------------------------------------------------------------
def read_and_check_tag(uid):
"""Read all config from one tag and verify against xblink requirements.
def run_with_acr1552(conn):
"""Full config verification via ACR1552 transparent ISO15693 mode."""
print("Initializing ACR1552 transparent ISO15693 mode...")
reader = ACR1552ISO15693(conn)
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)
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', '?')} blocks x {sys_info.get('block_size', '?')} 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}")
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)
# 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.get('interface', '?')}")
print(f" Num keys: {nxp_info.get('num_keys', '?')}")
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:
check_against_expected(config, eh_config)
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(" 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)
print(f" Type: ISO15693 (manufacturer code 0x{uid_rev[1]:02X})")
else:
config_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_CONFIG)
config = decode_config(config_data)
print(f" Failed to read UID: SW={sw1:02X}{sw2:02X}")
if config:
print(f" Raw bytes: {config['raw']}")
print(f" EH mode: {config['eh_mode']}")
print(f" Use case: {config['use_case']}")
print(f" SRAM enabled: {config['sram_enabled']}")
print(f" Arbiter mode: {config['arbiter_mode']}")
print(f" Auto standby: {config['auto_standby']}")
print(f" SRAM copy: {config['sram_copy_en']}")
print(f" PT transfer dir: {config['pt_transfer_dir']}")
print(f" EH arbiter mode: {config['eh_arbiter_mode_en']}")
print(f" GPIO0 pad: {config['gpio0_pad_in']}")
print(f" GPIO1 pad: {config['gpio1_pad_in']}")
print(f" Lock block: {config['lock_block_supported']}")
print(f" Ext commands: {config['ext_commands_supported']}")
# 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 config block.")
print(" Failed to read block 0")
# --- 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)
# 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")
if eh_config:
print(f" Raw bytes: {eh_config['raw']}")
print(f" EH enable: {eh_config['eh_enable']}")
print(f" EH voltage: {eh_config['eh_voltage']}")
print(f" EH current limit: {eh_config['eh_current']}")
print(f" Power check: {'disabled' if eh_config['disable_power_check'] else 'enabled'}")
print(f" ED config: {eh_config['ed_config']}")
else:
print(" Failed to read EH config block.")
# --- Check against xblink expected values ---
if config and eh_config:
return check_against_expected(config, eh_config)
return False
# 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
# ---------------------------------------------------------------------------
def main():
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
print(f"Connecting to uFR Zero on {port}...")
# 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}")
reader_list = readers()
if not reader_list:
print("No PCSC readers found. Is pcscd running?")
sys.exit(1)
import time
time.sleep(0.5)
print("Reader connected.")
print("Available readers:")
for i, r in enumerate(reader_list):
print(f" [{i}] {r}")
# Enter ISO15693 transparent mode (tx_crc=1, rx_crc=1, rf_timeout=10000us, uart_timeout=500ms)
status = ufr.card_transceive_mode_start(1, 1, 10000, 500)
if status != UFR_OK:
print(f"ERROR: Failed to enter transceive mode, status=0x{status:04X}")
ufr.ReaderClose()
sys.exit(1)
# Detect reader type
acr1552_reader = None
any_reader = None
for r in reader_list:
name = str(r)
if name.startswith("ACS ACR1552"):
acr1552_reader = r
break
if any_reader is None:
any_reader = r
print("ISO15693 transparent mode active.")
target_reader = acr1552_reader or any_reader
reader_name = str(target_reader)
is_acr1552 = acr1552_reader is not None
# --- Step 1: Inventory ---
print("\n=== ISO15693 Inventory ===")
uids = inventory()
if not uids:
print(" No tags found. Trying non-addressed mode as fallback...")
print("\n=== Tag (non-addressed) ===")
read_and_check_tag(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(f" Found {len(uids)} tag(s):")
for i, uid in enumerate(uids):
print(f" [{i}] UID: {decode_uid(uid)}")
print("Mode: Standard PCSC (basic tag info only)")
# --- Step 2: Read each tag ---
all_ok = True
for i, uid in enumerate(uids):
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
# 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)
if len(uids) > 1:
print(f"\n{'='*50}")
if all_ok:
print("All tags pass xblink config check.")
else:
print("Some tags have config mismatches.")
card.connection.connect()
atr = bytes(card.connection.getATR())
print(f"Connected! ATR: {hex_str(atr)}")
# Cleanup
ufr.card_transceive_mode_stop()
ufr.ReaderClose()
print("\nReader closed.")
# Verify ISO15693 tag (check ATR for 03 06 0B = ISO 15693 Part 3)
if b'\x03\x06\x0B' in atr:
print("Tag type: ISO 15693")
elif b'\x03\x06\x03' in atr:
print("Tag type: ISO 14443-3A")
else:
print("Tag type: unknown (continuing anyway)")
# Run appropriate flow
if is_acr1552:
run_with_acr1552(card.connection)
else:
run_with_generic_pcsc(card.connection, reader_name)
card.connection.disconnect()
print("\nDone.")
if __name__ == "__main__":