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,148 +1,60 @@
#!/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)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Config addresses # Config addresses
NXP_CONFIG_ADDR_CONFIG = 0x37 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_EH_MODE_MASK = (3 << 2)
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)
# 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_USE_CASE_MASK = (3 << 4)
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)
# 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,206 +65,223 @@ 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)")
return data[1:]
print(f" ERROR: uart_transceive failed, status=0x{status:04X}, rcv_len={rcv_len.value}")
return None
data = bytes(rcv[:rcv_len.value])
if len(data) > 0 and data[0] != 0x00: def pcsc_read_binary(conn, block, le=0x00):
print(f" WARNING: Response flags = 0x{data[0]:02X}") """Read a single ISO15693 block via PCSC READ_BINARY."""
return data[1:] if len(data) > 1 else data 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(): 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
b3 = data[6]
res = { return {
"pp_pointer": data[0], "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): 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 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): def check_against_expected(config, eh_config):
"""Compare config against xblink expected values.""" """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 enable", eh_config.get("eh_enable"), EXPECTED["eh_enable"]),
("EH voltage", eh_config.get("eh_voltage"), EXPECTED["eh_voltage"]), ("EH voltage", eh_config.get("eh_voltage"), EXPECTED["eh_voltage"]),
] ]
all_ok = True all_ok = True
for name, actual, expected in checks: for name, actual, expected in checks:
match = actual == expected match = actual == expected
@@ -377,153 +305,185 @@ def check_against_expected(config, eh_config):
if all_ok: if all_ok:
print("\n All config values match xblink requirements!") print("\n All config values match xblink requirements!")
else: 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 return all_ok
# ---------------------------------------------------------------------------
# Reader-specific flows
# ---------------------------------------------------------------------------
def read_and_check_tag(uid): def run_with_acr1552(conn):
"""Read all config from one tag and verify against xblink requirements. """Full config verification via ACR1552 transparent ISO15693 mode."""
print("Initializing ACR1552 transparent ISO15693 mode...")
reader = ACR1552ISO15693(conn)
Args: try:
uid: 8-byte UID (LSB first) or None for non-addressed mode # System info
""" print("\nISO15693 System Info...")
use_addressed = uid is not None sys_data = acr1552_get_system_info(reader)
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) sys_info = decode_system_info(sys_data)
if sys_info: if sys_info:
print(f" UID: {sys_info['uid_str']}")
print(f" Memory: {sys_info.get('memory_bytes', '?')} bytes " 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: if "ic_ref" in sys_info:
print(f" IC ref: 0x{sys_info['ic_ref']:02X}") print(f" IC ref: 0x{sys_info['ic_ref']:02X}")
else:
print(" Failed to read system info.")
# --- NXP system info --- # NXP system info
if use_addressed: print("\nNXP System Info...")
print(f"\nNXP System Info...") nxp_data = acr1552_get_nxp_info(reader)
nxp_data = get_nxp_system_info_addressed(uid) nxp_info = decode_nxp_info(nxp_data)
nxp_info = decode_nxp_system_info(nxp_data)
if nxp_info: if nxp_info:
print(f" Interface: {nxp_info.get('interface', '?')}") print(f" Interface: {nxp_info['interface']}")
print(f" Num keys: {nxp_info.get('num_keys', '?')}") 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: else:
print(" Failed to read NXP system info (may not be an NXP tag).") print(f" Type: ISO15693 (manufacturer code 0x{uid_rev[1]:02X})")
# --- CONFIG block (0x37) ---
print(f"\nCONFIG block (0x37)...")
if use_addressed:
config_data = read_config_block_addressed(uid, NXP_CONFIG_ADDR_CONFIG)
else: else:
config_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_CONFIG) print(f" Failed to read UID: SW={sw1:02X}{sw2:02X}")
config = decode_config(config_data)
if config: # Read EEPROM block 0 (Capability Container)
print(f" Raw bytes: {config['raw']}") print("\nEEPROM Block 0 (Capability Container)...")
print(f" EH mode: {config['eh_mode']}") data = pcsc_read_binary(conn, 0x00)
print(f" Use case: {config['use_case']}") if data:
print(f" SRAM enabled: {config['sram_enabled']}") print(f" Data: {hex_str(data)}")
print(f" Arbiter mode: {config['arbiter_mode']}") if len(data) >= 4 and data[0] == 0xE1:
print(f" Auto standby: {config['auto_standby']}") print(f" NDEF formatted: version {data[1] >> 4}.{data[1] & 0x0F}, "
print(f" SRAM copy: {config['sram_copy_en']}") f"max size {data[2] * 8} bytes")
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']}")
else: else:
print(" Failed to read config block.") print(" Failed to read block 0")
# --- EH/ED CONFIG block (0x3D) --- # Read a few EEPROM blocks to verify access
print(f"\nEH/ED CONFIG block (0x3D)...") print("\nEEPROM Blocks 1-4 (user data)...")
if use_addressed: for block in range(1, 5):
eh_data = read_config_block_addressed(uid, NXP_CONFIG_ADDR_EH_CONFIG) data = pcsc_read_binary(conn, block)
else: if data:
eh_data = read_config_block_nonaddressed(NXP_CONFIG_ADDR_EH_CONFIG) print(f" Block 0x{block:02X}: {hex_str(data)}")
eh_config = decode_eh_config(eh_data) else:
print(f" Block 0x{block:02X}: read failed")
if eh_config: # Report limitation
print(f" Raw bytes: {eh_config['raw']}") print("\n" + "=" * 60)
print(f" EH enable: {eh_config['eh_enable']}") print("LIMITATION: NXP config registers cannot be read")
print(f" EH voltage: {eh_config['eh_voltage']}") print("=" * 60)
print(f" EH current limit: {eh_config['eh_current']}") print(f"\nReader '{reader_name}' only supports standard PCSC commands,")
print(f" Power check: {'disabled' if eh_config['disable_power_check'] else 'enabled'}") print("which map to ISO15693 READ_SINGLE_BLOCK for user EEPROM.")
print(f" ED config: {eh_config['ed_config']}") print("NXP config registers (CONFIG 0x37, EH_CONFIG 0x3D) require")
else: print("the NXP custom READ_CONFIG command (0xC0), which needs a")
print(" Failed to read EH config block.") print("reader with transparent ISO15693 passthrough support.")
print("\nTo verify NTAG5Link config for xblink:")
# --- Check against xblink expected values --- print(" 1. Use ACR1552 reader with this tool (auto-detected)")
if config and eh_config: print(" 2. Use ntag5sensor directly: python3 -m ntag5sensor info")
return check_against_expected(config, eh_config) print(" 3. Use NXP TagInfo app on Android")
return False
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 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)
sys.exit(1) 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(f"\nUsing: {reader_name}")
print("\n=== ISO15693 Inventory ===") if is_acr1552:
uids = inventory() if not acr1552_available():
print("ERROR: ber_tlv package required for ACR1552. Install: pip install ber-tlv")
if not uids: sys.exit(1)
print(" No tags found. Trying non-addressed mode as fallback...") print("Mode: ACR1552 transparent ISO15693 (full NXP config access)")
print("\n=== Tag (non-addressed) ===")
read_and_check_tag(None)
else: else:
print(f" Found {len(uids)} tag(s):") print("Mode: Standard PCSC (basic tag info only)")
for i, uid in enumerate(uids):
print(f" [{i}] UID: {decode_uid(uid)}")
# --- Step 2: Read each tag --- # Connect to tag
all_ok = True print("\nWaiting for tag...")
for i, uid in enumerate(uids): request = CardRequest(timeout=30, readers=[target_reader], cardType=AnyCardType())
print(f"\n{'='*50}") try:
print(f"=== Tag {i}: {decode_uid(uid)} ===") card = request.waitforcard()
print(f"{'='*50}") except Exception as e:
ok = read_and_check_tag(uid) print(f"No tag found: {e}")
if not ok: sys.exit(1)
all_ok = False
if len(uids) > 1: card.connection.connect()
print(f"\n{'='*50}") atr = bytes(card.connection.getATR())
if all_ok: print(f"Connected! ATR: {hex_str(atr)}")
print("All tags pass xblink config check.")
else:
print("Some tags have config mismatches.")
# Cleanup # Verify ISO15693 tag (check ATR for 03 06 0B = ISO 15693 Part 3)
ufr.card_transceive_mode_stop() if b'\x03\x06\x0B' in atr:
ufr.ReaderClose() print("Tag type: ISO 15693")
print("\nReader closed.") 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__": if __name__ == "__main__":