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:
1898
tools/Info.plist.ccid-backup
Normal file
1898
tools/Info.plist.ccid-backup
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user