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