diff --git a/CLAUDE.md b/CLAUDE.md index 370572f..d6ebb48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,8 @@ The LP5562 driver does NOT enforce timing delays internally — the caller is re ## Key I2C Addresses +Both devices share the same I2C bus (A4/A5). Non-conflicting addresses — no bus arbitration issues. + | Device | Address | Notes | |--------|---------|-------| | LP5562 | 0x30 | ADDR_SEL pins both low (LP5562EVM default) | @@ -95,9 +97,11 @@ Key config constants (from `../ntag5sensor/vicinity/ntag5link.py`): |----------|------------|----------| | A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data | | A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock | -| D0/A0 | LP5562 EN | Hardware enable (GPIO, active high) | -| TBD | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) | -| TBD | Hall sensor | Recovery mode trigger (EIC wake) | +| D0/A0 | LP5562 EN line (open-drain) | Wired-AND with hall sensor, pull-up to VCC | +| TBD (EIC) | Hall sensor output | EIC wake + wired-AND to LP5562 EN line | +| TBD (EIC) | NTAG5 FD pin | Field detect / SRAM write indication (EIC wake) | + +**LP5562 EN wired-AND**: D0/A0 (open-drain) and hall sensor (open-drain, active-low) both connect to LP5562 EN with a 1M pull-up. Either can force EN low. Magnet kills LEDs at hardware level regardless of MCU state. ## Testing diff --git a/README.md b/README.md index 26877b8..02d37ef 100644 --- a/README.md +++ b/README.md @@ -10,26 +10,29 @@ A battery-free subdermal LED implant that harvests energy from an NFC field. An I2C (0x30) Phone/Reader ))) NFC ((( NTAG5Link ---------> LP5562 -----> RGBW LEDs | | | - | VOUT | FD pin | EN - | (EH) | | - v v | - +--SAMD21E--+ | - | D0/A0 ----------------+ (GPIO enable) - | A4/A5 (I2C bus) ------+ - | GPIO <--- Hall Sensor - +----------+ + | VOUT | FD pin | EN (wired-AND) + | (EH) | | | + v v | | + +--SAMD21E--+ | | + | D0/A0 (open-drain) ---+ | + | A4/A5 (I2C bus) ------+ | + | EIC <--- Hall Sensor ---+ (open-drain) + +----------+ | + Rpull-up (1M) + | + VCC ``` All power comes from NTAG5Link energy harvesting — no battery. ## Components -| Component | Part | Role | I2C Addr | Key Specs | -|-----------|------|------|----------|-----------| -| NFC + Power | NXP NTAG5Link (NTP53x2) | NFC interface, energy harvesting, pattern storage | 0x54 (slave) | 2048B EEPROM, 256B SRAM, ISO15693, 1.8/2.4/3.0V EH output | -| MCU | SAMD21E18A | Firmware (Rust), pattern loading, sleep/wake | — | 256KB flash, 32KB RAM, ARM Cortex-M0+, 32-pin QFN | -| LED Driver | TI LP5562 | RGBW LED control with autonomous patterns | 0x30 | 4ch RGBW, 3 execution engines, 16 cmds/engine, 0-25.5mA/ch | -| Recovery | Hall effect sensor | Safe mode / recovery trigger | — | GPIO input with EIC wake from sleep | +| Component | Part | Role | I2C Addr | Key Specs | +| --------------- | ------------------------------------ | --------------------------------------------------- | ------------ | ---------------------------------------------------------- | +| NFC + Power | NXP NTAG5Link (NTP53x2) | NFC interface, energy harvesting, pattern storage | 0x54 (slave) | 2048B EEPROM, 256B SRAM, ISO15693, 1.8/2.4/3.0V EH output | +| MCU | SAMD21E18A | Firmware (Rust), pattern loading, sleep/wake | — | 256KB flash, 32KB RAM, ARM Cortex-M0+, 32-pin QFN | +| LED Driver | TI LP5562 | RGBW LED control with autonomous patterns | 0x30 | 4ch RGBW, 3 execution engines, 16 cmds/engine, 0-25.5mA/ch | +| Recovery + Swap | Hall effect sensor (e.g. TI DRV5032) | Pattern cycling, recovery trigger, hardware EN kill | — | Open-drain, active-low, omnipolar, <2µA quiescent | **Dev board**: Seeed XIAO M0 (SAMD21G18A) + LP5562EVM + MikroElektronika NTAG5 Link Click @@ -38,13 +41,14 @@ All power comes from NTAG5Link energy harvesting — no battery. ### Boot Sequence 1. NFC field detected by NTAG5Link -2. VOUT powers SAMD21E and LP5562 (MCU drives EN pin high on D0/A0) -3. SAMD21E boots, checks hall sensor GPIO (asserted = recovery mode) -4. Normal boot: reads LED pattern from NTAG5Link EEPROM via I2C -5. Programs LP5562 execution engines with pattern data -6. Enters STANDBY sleep (~5uA) -7. LP5562 runs patterns autonomously using internal oscillator -8. NFC field drops → VOUT drops → clean power-off +2. VOUT powers SAMD21E; pull-up asserts LP5562 EN (unless hall sensor is active) +3. SAMD21E boots, reads hall sensor GPIO +4. If hall asserted → **boot-time recovery** (see Recovery Mode below) +5. Normal boot: reads LED pattern from NTAG5Link EEPROM via I2C +6. Programs LP5562 execution engines with pattern data +7. Enters STANDBY sleep (~5µA) +8. LP5562 runs patterns autonomously using internal oscillator +9. NFC field drops → VOUT drops → clean power-off ### Pattern Update Sequence @@ -57,25 +61,37 @@ All power comes from NTAG5Link energy harvesting — no battery. 7. Reprograms LP5562 engines with new pattern 8. Returns to STANDBY sleep -### Recovery Mode +### Hall Sensor Interaction -If the hall sensor is asserted at boot (magnet held near implant): +The hall sensor serves three purposes via a wired-AND circuit: the hall sensor (open-drain, active-low) and MCU GPIO (open-drain) share the LP5562 EN line with a pull-up resistor. When the magnet is present, EN is forced low at the hardware level — independent of MCU state. + +**Pattern Swap (tap <1s):** +1. Magnet near → hall pulls EN low → LEDs off instantly (hardware) +2. MCU wakes via EIC interrupt, starts timer +3. Magnet removed → MCU re-asserts EN (pin high-Z, pull-up takes over) +4. Loads next stored pattern → LEDs back on + +**Boot-time Recovery:** +If the hall sensor is asserted when the MCU powers up (magnet was present before NFC field arrived): +- LP5562 EN held low by hardware (LEDs never turn on) - Loads a hardcoded default pattern (solid white, low brightness) - Ignores EEPROM pattern data - Sets recovery flag in SRAM for phone app detection - Remains awake for firmware update if supported +**Hardware Safety:** Even if the MCU firmware crashes, holding a magnet near the implant forces the LEDs off at the circuit level. + ## Power Budget All power from NTAG5Link energy harvesting. Budget is constrained and TBD pending prototype measurements. -| Parameter | Value | Notes | -|-----------|-------|-------| -| EH max current | ~12.5mA | Highest threshold setting | -| EH voltage | 3.0V | Required for LP5562 (2.7-5.5V) and SAMD21E (1.62-3.63V) | -| LP5562 per channel | 0-25.5mA | Must limit to ~2-5mA/ch for EH budget | -| SAMD21E active | ~3-5mA | During boot and pattern programming | -| SAMD21E standby | ~5uA | After LP5562 engines are running | +| Parameter | Value | Notes | +| ------------------ | -------- | ------------------------------------------------------- | +| EH max current | ~12.5mA | Highest threshold setting | +| EH voltage | 3.0V | Required for LP5562 (2.7-5.5V) and SAMD21E (1.62-3.63V) | +| LP5562 per channel | 0-25.5mA | Must limit to ~2-5mA/ch for EH budget | +| SAMD21E active | ~3-5mA | During boot and pattern programming | +| SAMD21E standby | ~5uA | After LP5562 engines are running | ## Getting Started @@ -112,13 +128,16 @@ cargo hf2 --release Current prototype (XIAO M0 + LP5562EVM): -| XIAO Pin | LP5562EVM | Function | -|----------|-----------|----------| -| A4 (SDA) | SDA | I2C data | -| A5 (SCL) | SCL | I2C clock | -| D0/A0 | EN | Hardware enable (GPIO) | -| 3V3 | VCC | Power | -| GND | GND | Ground | +| XIAO Pin | Connection | Function | +| --------- | --------------------------- | ------------------------------------------- | +| A4 (SDA) | LP5562 SDA, NTAG5 SDA | Shared I2C data | +| A5 (SCL) | LP5562 SCL, NTAG5 SCL | Shared I2C clock | +| D0/A0 | LP5562 EN line (open-drain) | Hardware enable, wired-AND with hall sensor | +| TBD (EIC) | Hall sensor output | Interrupt/wake + wired-AND to EN line | +| 3V3 | VCC | Power | +| GND | GND | Ground | + +LP5562 EN line has a 1M pull-up resistor to VCC. Both D0/A0 (open-drain) and the hall sensor (open-drain, active-low) connect to this line — either can pull EN low. ## Project Status diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md index 8e76b1d..328213b 100644 --- a/docs/DEVELOPMENT_PLAN.md +++ b/docs/DEVELOPMENT_PLAN.md @@ -80,6 +80,12 @@ This gives us real pattern data to inform the EEPROM storage format later (M5), **Important**: The MCU accesses NTAG5Link as a standard I2C slave at address 0x54 — plain register read/write. This is NOT the NFC-side ISO15693 custom commands used by the Python ntag5sensor tooling. +**I2C bus management**: LP5562 (0x30) and NTAG5Link (0x54) share the same I2C bus (A4/A5). Both addresses are used simultaneously — the MCU talks to each device by its address. Key considerations: +- Both are I2C slave devices with non-conflicting addresses +- LP5562 engines run autonomously after programming — no ongoing I2C traffic needed +- Pause LP5562 engines (set exec to Hold) before lengthy NTAG5 I2C transactions to avoid any bus contention edge cases +- NTAG5Link's use_case must be set to `i2c_slave` (not `i2c_master`) so it doesn't drive the bus + **Tasks**: 1. Write `src/ntag5/mod.rs` — `Ntag5Link` struct, generic over `embedded_hal::i2c::I2c` 2. Write `src/ntag5/registers.rs` — register map constants from `ntag5link.py` @@ -182,24 +188,37 @@ Typical 3-engine pattern: 16 + 3×34 + 4 = **122 bytes** (fits easily in 2048B E | LP5562 quiescent | ~0.5mA | ~0.5mA | | LP5562 LEDs (4ch @ 3mA) | ~12mA | ~12mA | | NTAG5Link | ~100µA | ~1µA | +| Hall sensor (DRV5032) | ~1.6µA | ~1.6µA | +| EN pull-up (1M @ 3V) | ~0 | ~0 (3µA only when EN low) | | **Total** | **~17mA** | **~12.5mA** | Key insight: LED current dominates. MCU sleep saves ~4mA. Total must stay under EH max (~12.5mA), so LED current must be limited. **Done when**: MCU sleeps after boot, LP5562 runs patterns, MCU wakes on NFC to accept new pattern. -### M8: Recovery Mode +### M8: Recovery Mode + Pattern Swap -**Goal**: Hall sensor provides safe mode entry for post-implant recovery. +**Goal**: Hall sensor provides pattern cycling (tap) and safe mode entry (hold + confirm). + +**Circuit**: Wired-AND on LP5562 EN — hall sensor (open-drain) + MCU GPIO (open-drain) + pull-up resistor. Magnet presence forces EN low at hardware level, independent of MCU state. Hall output also connected to MCU EIC pin for interrupt/wake. See `docs/plans/2026-03-03-hall-en-design.md`. + +**Interaction model**: +- **Boot-time recovery**: Hall asserted at power-on → EN held low (hardware), MCU skips EEPROM, loads default pattern, stays awake +- **Tap (<1s)**: LEDs off instantly (hardware EN drop) → magnet removed → MCU loads next pattern → LEDs back +- **Hold (>3s + confirm)**: LEDs off (hardware) → magnet removed → MCU blinks warning (3x red) → 2s window for confirmation tap → if confirmed, enters recovery mode. No second tap → resumes normal operation +- **MCU crashed + magnet**: Hall still kills EN (hardware safety) **Tasks**: -1. Wire hall sensor to EIC-capable GPIO pin -2. Boot-time check: if hall sensor asserted → recovery mode -3. Recovery behavior: load hardcoded solid white (low brightness), skip EEPROM, stay awake -4. Set recovery flag in SRAM so phone app can detect it -5. Add hall sensor as runtime EIC wake source (e.g., cycle patterns) +1. Wire hall sensor: open-drain output to EN line (wired-AND) + separate wire to EIC pin +2. Add pull-up resistor (100k-1M) on EN line +3. Configure MCU GPIO (D0/A0) as open-drain for EN control +4. Implement EIC interrupt handler for hall sensor pin +5. Implement boot-time recovery: read hall GPIO at startup, if asserted → default pattern, skip EEPROM +6. Implement runtime state machine: tap detection, hold timing, warning blink, recovery confirmation +7. Recovery behavior: load hardcoded solid white (low brightness), skip EEPROM, stay awake +8. Set recovery flag in SRAM so phone app can detect it -**Done when**: Holding magnet near hall sensor at boot triggers recovery mode with default LED pattern. +**Done when**: Boot-time recovery works (magnet at power-on), tap cycles patterns at runtime, hold + confirm enters runtime recovery, hardware EN kill works even if MCU is unresponsive. ### M9: Power Characterization @@ -272,7 +291,7 @@ Deferred until Groups A-D are solid. | Risk | Severity | Mitigation | |------|----------|------------| | Power budget too tight for 4ch LED | High | Limit current to 2-3mA/ch, use pulsed patterns, run fewer channels | -| I2C bus contention (LP5562 + NTAG5) | Medium | Pause LP5562 engines before NTAG5 I2C access, resume after | +| I2C bus contention (LP5562 + NTAG5) | Medium | Non-conflicting addresses (0x30 vs 0x54). Pause LP5562 engines during NTAG5 I2C bursts. Verify NTAG5 is in i2c_slave mode (not i2c_master) | | EEPROM write endurance (~100K cycles) | Low | SRAM for transient comms, EEPROM only for pattern persistence | | Boot time too slow | Low | Estimated ~15-20ms — appears instant to user | | NFC coupling distance too short | Medium | Optimize antenna, low field strength EH mode, accept 1-3cm | diff --git a/docs/STATUS.md b/docs/STATUS.md index e6ff2f5..26c9cb7 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -131,6 +131,9 @@ | Firmware update strategy | Deferred | UF2 for dev, NFC OTA evaluated later | | NTAG5Link I2C slave address | Assumed 0x54 | Verify in M4 with Click board | | Pattern binary format | Deferred to M5 | Design after M2-M3 engine experience | +| Hall sensor + EN circuit | Designed | Wired-AND: hall + MCU open-drain on EN with 1M pull-up. See `docs/plans/2026-03-03-hall-en-design.md` | +| Hall sensor part selection | Decided | DRV5032FB (SOT-23, 8.4mT, prototype) → DRV5032FE (X2SON 1x1mm, final PCB) | +| EN pull-up value | Decided | 1M — zero steady-state draw, 3µA when EN low, ~10µs rise time | ## Hardware Inventory @@ -140,6 +143,6 @@ | LP5562EVM | Available | TI eval module, RGBW LEDs, I2C addr 0x30 | | Mini-USB cable | Not on hand | Needed to flash XIAO M0 | | NTAG5 Link Click | Available, partially wired | Missing I2C jumper to XIAO | -| Hall effect sensor | Not available | Need to source | +| Hall effect sensor | Not available | Need to source — TI DRV5032FB (SOT-23) for prototype | | ACR1552 PCSC reader | Available | For ntag5sensor Python tooling | | Multimeter | Available | For power budget measurements | diff --git a/docs/plans/2026-03-03-hall-en-design.md b/docs/plans/2026-03-03-hall-en-design.md new file mode 100644 index 0000000..fb5ff20 --- /dev/null +++ b/docs/plans/2026-03-03-hall-en-design.md @@ -0,0 +1,159 @@ +# Hall Sensor + LP5562 EN Wired-AND Circuit Design + +**Date**: 2026-03-03 +**Status**: Design in progress + +## Problem + +The implant needs a hall sensor for three functions: +1. **Pattern swap** — quick magnet tap cycles to the next stored pattern +2. **Runtime recovery** — long hold + confirmation tap enters safe mode +3. **Boot-time recovery** — magnet present at power-on skips EEPROM, loads default +4. **Hardware safety** — LEDs forced off by magnet even if MCU firmware has crashed + +These must work within extreme space and power constraints (energy-harvested implant). + +## Circuit: Wired-AND on LP5562 EN + +Zero extra active components. Hall sensor and MCU share the EN line via open-drain outputs. + +``` + VCC (3.0V from NTAG5Link EH) + | + 1M (pull-up resistor) + | +EN ------+------- MCU D0/A0 (open-drain) + | + +------- Hall sensor OUT (open-drain, active-low) + | + LP5562 EN pin + + +Hall sensor OUT ---- MCU EIC pin (separate connection for interrupt/wake) +``` + +### How it works + +| State | MCU pin | Hall sensor | EN line | LP5562 | +|-------|---------|-------------|---------|--------| +| Normal operation | High-Z | High-Z (no magnet) | High (pull-up) | On | +| Magnet present | High-Z | Driving low | Low | Off | +| MCU disabling LP5562 | Driving low | High-Z | Low | Off | +| Magnet + MCU low | Driving low | Driving low | Low | Off | + +### Pull-up resistor: 1M + +- Steady-state current: ~0 (both open-drains high-Z, no path to ground) +- Current when EN low: 3V / 1M = 3 µA (only during magnet or MCU-driven low) +- Rise time: ~10 µs (1M x ~10pF parasitic) — instant from human perspective +- LP5562 EN input is high-impedance CMOS — no DC loading + +## Hall Sensor: TI DRV5032 + +| Variant | Use | Iq | Threshold | Output | Package | +|---------|-----|-----|-----------|--------|---------| +| **DRV5032FB** | Prototype | 1.6 µA | 8.4 mT (omnipolar) | Open-drain | SOT-23 | +| **DRV5032FE** | Final PCB | 0.55 µA | 4.5 mT (omnipolar) | Open-drain | X2SON (1x1mm) | + +- **Omnipolar**: Triggers on either magnetic pole (user doesn't need to orient magnet) +- **8.4 mT threshold (FB)**: Higher threshold = harder to accidentally trigger. Requires deliberate, close magnet interaction. Good for "don't accidentally enter recovery" requirement. +- **Open-drain, active-low**: Required for wired-AND topology +- **SOT-23 for prototyping**: Easy to hand-solder; X2SON for final implant PCB + +## Interaction Model + +### Pattern Swap (tap <1s) + +1. Magnet near → hall pulls EN low → LEDs off instantly (hardware, zero MCU latency) +2. MCU wakes via EIC interrupt on hall sensor pin, starts timer +3. Magnet removed (hall goes high-Z, pull-up restores EN) +4. MCU detects short press (<1s) → loads next stored pattern → LEDs back on + +### Runtime Recovery (hold >3s + confirm) + +1. Magnet near → EN low (hardware), MCU wakes via EIC +2. MCU counts hold duration while hall stays asserted +3. Magnet removed after >3s → MCU notes "long hold detected" +4. MCU re-enables LP5562 → blinks warning pattern (3x red flash) +5. MCU waits 2s for confirmation tap +6. Confirmation tap received → enters recovery mode +7. No confirmation → resumes normal operation (reloads previous pattern) + +### Boot-time Recovery + +1. NFC field arrives, VOUT powers system +2. Hall sensor already active (magnet was present before power) → EN held low by hardware +3. MCU boots, reads hall GPIO → asserted → boot-time recovery +4. Loads hardcoded default pattern (solid white, low brightness) +5. Skips EEPROM pattern data +6. Sets recovery flag in SRAM for phone app detection +7. When magnet removed, EN goes high, default pattern runs + +### Hardware Safety (MCU crashed) + +- Holding magnet forces EN low at circuit level regardless of MCU state +- No firmware dependency — pure hardware path +- Magnet removal restores EN via pull-up (LP5562 resumes last-programmed pattern) + +## Firmware State Machine (M8) + +``` + ┌─────────────┐ + power-on -->│ BOOT_CHECK │ + └──────┬──────┘ + hall low? │ + ┌───yes─────┼────no───┐ + v │ v + ┌──────────┐ │ ┌─────────────┐ + │ RECOVERY │ │ │ RUNNING │<──────────────┐ + └──────────┘ │ └──────┬──────┘ │ + │ hall EIC │ │ + │ interrupt│ │ + │ v │ + │ ┌─────────────┐ │ + │ │ HALL_ACTIVE │ │ + │ └──────┬──────┘ │ + │ hall released │ + │ │ │ │ + │ <1s >3s │ + │ │ │ │ + │ v v │ + │ ┌──────┐ ┌──────────────┐ │ + │ │ SWAP │ │ WARN_BLINK │ │ + │ └──┬───┘ └──────┬───────┘ │ + │ │ hall tap │ no tap │ + │ │ in 2s? │ │ + │ │ ┌──yes ┌──no │ + │ │ v v │ + │ │ ┌────────┐ │ │ + │ │ │RECOVERY│ │ │ + │ │ └────────┘ │ │ + │ └──────────────┴──────────┘ +``` + +States: +- **BOOT_CHECK**: Read hall GPIO. If asserted → RECOVERY. Else → RUNNING. +- **RUNNING**: LP5562 engines active, MCU in STANDBY sleep. Wakes on hall EIC. +- **HALL_ACTIVE**: Magnet detected. Timer running. Waiting for release. +- **SWAP**: Load next pattern, re-enable LP5562 → RUNNING. +- **WARN_BLINK**: Blink 3x red. Wait 2s for confirmation tap. +- **RECOVERY**: Default pattern, skip EEPROM, stay awake, set SRAM flag. + +## Power Impact + +| Component | Added draw | When | +|-----------|-----------|------| +| Hall sensor (DRV5032FB) | 1.6 µA | Always (quiescent) | +| Pull-up resistor (1M) | 3 µA | Only when EN is low (magnet or MCU-driven) | +| Pull-up resistor (1M) | ~0 | Steady state (EN high, no current path) | + +Total added quiescent: **~1.6 µA** — negligible vs LP5562 LED current (~12 mA). + +## Open Questions + +| Question | Notes | +|----------|-------| +| Debounce needed? | Hall sensors are usually clean (no mechanical bounce), but verify with DRV5032 | +| EIC pin selection on SAMD21E | Must be EIC-capable for wake from STANDBY. Check SAMD21E pinout. | +| Magnet strength/distance | What N52 neodymium size triggers DRV5032FB at implant depth (~3-5mm subdermal)? | +| Multiple stored patterns | How many patterns to cycle through? Currently TBD (depends on EEPROM format, M5) | diff --git a/docs/plans/2026-03-03-ntag5-config-check.md b/docs/plans/2026-03-03-ntag5-config-check.md new file mode 100644 index 0000000..147ac69 --- /dev/null +++ b/docs/plans/2026-03-03-ntag5-config-check.md @@ -0,0 +1,90 @@ +# NTAG5Link Config Verification via uFR Zero + +**Date**: 2026-03-03 +**Status**: Tool written, reader connection blocked + +## Goal + +Read-only verification that the NTAG5Link Click board's configuration matches xblink requirements, using the Digital Logic uFR Zero Multi-ISO reader. + +## Tool + +`tools/ntag5_config_check.py` — Python script using uFCoder library (ctypes) in ISO15693 transparent mode. + +Features: +- ISO15693 INVENTORY to discover all tags by UID +- Addressed mode for all config reads (supports multiple tags in field) +- Reads CONFIG block (0x37) and EH/ED CONFIG block (0x3D) +- NXP system info for interface type verification +- Checks against xblink expected config values + +## Expected Config + +| Setting | Expected | Config Location | +|---------|----------|----------------| +| EH mode | low_field_strength | CONFIG byte 0, bits 3:2 = 10 | +| Use case | i2c_slave | CONFIG byte 1, bits 5:4 = 00 | +| SRAM enabled | true | CONFIG byte 1, bit 1 = 1 | +| Arbiter mode | sram_passthrough | CONFIG byte 1, bits 3:2 = 10 | +| EH enable | true | EH_CONFIG byte 0, bit 0 = 1 | +| EH voltage | 3.0V | EH_CONFIG byte 0, bits 2:1 = 10 | + +## Reader Connection Issue + +**Problem**: uFCoder library returns `UFR_TIMEOUT_ERR` (0x1002) for all `ReaderOpenEx` parameter combinations. + +**USB device**: `10c4:ea60` — Silicon Labs CP2102 USB to UART Bridge Controller, iSerial "0001". Generic descriptor, no D-Logic branding in USB strings. + +**Raw serial investigation**: +- At 1 Mbps (expected uFR baud): responds with `00 80` (2 bytes) — not a valid uFR protocol frame (expected 6+ bytes with `0x55...0xAA` framing) +- At 115200 bps: returns ASCII shell commands mentioning "systemctl", "proxmark3" paths +- At 250 Kbps: returns structured but unrecognized data + +**Possible causes**: +1. Device may not be a uFR Zero — the CP210x bridge has generic descriptors +2. Reader firmware may need updating +3. Reader may be in a non-standard mode +4. Different hardware variant requiring different protocol + +**Next steps**: +1. Physically verify the device is indeed the uFR Zero (check labels, LEDs) +2. Try the D-Logic `ufr_online` Windows utility to confirm reader identity +3. If confirmed as uFR Zero, check firmware version and update if needed +4. Alternative: use the ACR1552 PCSC reader with ntag5sensor directly (requires porting the reader to share the field with NTAG5 Click) + +## Fallback Plan: ntag5sensor + ACR1552 + +If the uFR Zero connection cannot be resolved, use the existing ntag5sensor Python tooling with the ACR1552 PCSC reader: + +```python +# From ntag5sensor — already has full config read/write support +chip = NTAG5Link(reader) +config = chip.get_config_info() +eh_config = chip.get_eh_ed_config_info() +``` + +The ACR1552 is already proven with ntag5sensor. Only change needed: physically position the ACR1552 over the NTAG5 Click board instead of the uFR Zero. + +## Config Change Plan (if mismatches found) + +Use ntag5sensor's write functions (via ACR1552 or adapted for uFR Zero): + +```python +# Set I2C slave mode + SRAM passthrough + SRAM enable +chip.write_config1( + sram_enable=True, + arbiter_mode=NXP_CONFIG_1_ARBITER_MODE_SRAM_PASSTHROUGH, + use_case=NXP_CONFIG_1_USE_CASE_CONF_I2C_SLAVE +) + +# Set EH: 3.0V, enabled, current limit TBD +chip.write_eh_ed_config( + enable=True, + voltage=NXP_EH_CONFIG_EH_VOUT_V_SEL_3_0, + current=NXP_EH_CONFIG_EH_VOUT_I_SEL_12_5, # max for testing + ed_config=NXP_ED_CONFIG_NFC_FIELD_DETECT +) + +# Set EH mode in CONFIG byte 0 +chip.write_config0(eh_mode=NXP_CONFIG_0_EH_MODE_LOW_FIELD_STRENGTH) +``` diff --git a/tools/libuFCoder-x86_64.so b/tools/libuFCoder-x86_64.so new file mode 100755 index 0000000..f1e47a5 Binary files /dev/null and b/tools/libuFCoder-x86_64.so differ diff --git a/tools/ntag5_config_check.py b/tools/ntag5_config_check.py new file mode 100644 index 0000000..fd86020 --- /dev/null +++ b/tools/ntag5_config_check.py @@ -0,0 +1,530 @@ +#!/usr/bin/env python3 +"""Read NTAG5Link configuration via uFR Zero reader (read-only). + +Uses the Digital Logic uFCoder library in ISO15693 transparent mode +to send NXP custom commands for reading config registers. + +Supports multiple tags in the field: runs INVENTORY first, then +reads each tag using addressed mode. + +Usage: python3 ntag5_config_check.py [/dev/ttyUSBx] +""" + +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 + +# --------------------------------------------------------------------------- +# NXP NTAG5Link config constants (from ntag5sensor) +# --------------------------------------------------------------------------- + +# Config addresses +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) + +# 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) + +# 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) + +# Lookup tables +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"} + +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", + "sram_enabled": True, + "arbiter_mode": "sram_passthrough", + "eh_enable": True, + "eh_voltage": "3.0V", +} + +# --------------------------------------------------------------------------- +# Low-level transceive +# --------------------------------------------------------------------------- + +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) + + 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 + + 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 +# --------------------------------------------------------------------------- + +def inventory(): + """Run ISO15693 INVENTORY to discover all tags in the field. + + Returns list of 8-byte UIDs (as bytes, LSB first as received on air). + """ + # 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 [] + + uids = [] + if len(data) >= 9: + dsfid = data[0] + uid = data[1:9] # 8 bytes, LSB first + uids.append(uid) + + # 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 + +# --------------------------------------------------------------------------- +# Addressed config reads +# --------------------------------------------------------------------------- + +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 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 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 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) + +# --------------------------------------------------------------------------- +# 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_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)} + + 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 + 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 + + return res + + +def decode_nxp_system_info(data): + """Decode NXP GET_NXP_SYSTEM_INFO response.""" + if data is None or len(data) < 7: + return None + + res = { + "pp_pointer": data[0], + "lock_bits_raw": data[2], + } + + 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.""" + 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), + "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), + "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), + "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.""" + 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_voltage": EH_V_SEL.get((eh >> 1) & 0x03, "unknown"), + "disable_power_check": bool(eh & NXP_EH_CONFIG_DISABLE_POWER_CHECK), + "eh_current": EH_I_SEL.get((eh >> 4) & 0x07, "unknown"), + "ed_config": ED_CONFIG_MAP.get(ed & 0x0F, "unknown"), + } + +# --------------------------------------------------------------------------- +# Verification +# --------------------------------------------------------------------------- + +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 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" 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']}") + 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: + 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 + +# --------------------------------------------------------------------------- +# 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}") + sys.exit(1) + + import time + time.sleep(0.5) + print("Reader connected.") + + # 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) + + print("ISO15693 transparent mode active.") + + # --- 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) + else: + print(f" Found {len(uids)} tag(s):") + for i, uid in enumerate(uids): + print(f" [{i}] UID: {decode_uid(uid)}") + + # --- 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 + + 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.") + + # Cleanup + ufr.card_transceive_mode_stop() + ufr.ReaderClose() + print("\nReader closed.") + + +if __name__ == "__main__": + main()