# ISO 15693 Relay Timing Findings **Date:** 2026-03-17 **Hardware:** PM3 Easy, firmware branch `sim-relay` **Test script:** `test_relay_timing.py` in sim-framework worktree ## Summary The 15693 retry-based relay **works mechanically** but is **not reliable enough for production use**. The response table (`sim_table`) is the correct approach for custom/vendor commands. ## How the relay works 1. Firmware receives unknown command from reader 2. Firmware does NOT respond (reader times out at ~311µs FDT) 3. Firmware relays command to Python over USB 4. Python computes response, sends back over USB 5. Firmware caches the response 6. If the reader retries the same command, firmware serves the cached response ## Timing measurements | Metric | Value | |--------|-------| | Python roundtrip (receive relay → send response) | 0.11 - 0.25 ms | | ISO 15693 FDT (frame delay time) | 0.311 ms (311 µs) | | Python roundtrip vs FDT | Under budget on Python side | | First-attempt response | **NO** — firmware cannot respond in time | | Retry-based response | **YES** — works when reader retries | ## Why it doesn't work in practice The Python-side timing (0.11-0.25ms) is under the 311µs FDT budget, but this only measures the USB round-trip. The firmware's total path (receive RF frame → decode → relay over USB → receive response → encode RF → transmit) exceeds FDT on the first attempt. The firmware stays silent, and the reader must retry. **The problem is retry behavior:** - Retry is not guaranteed by ISO 15693 — it's reader-dependent - Some readers retry, some don't - Some NFC stacks (Android) retry certain commands but not others - The number of retries varies (1-3 typically, but not specified) - A reader that doesn't retry will see "no response" and may drop the tag **Observed behavior with Android phone:** - Standard commands (inventory, read block): handled by firmware natively, no relay needed - Custom command `0xFE` (fabricated): reader retried, cached response served successfully - NXP READ_CONFIG `0xA1`: reader did NOT retry (may have been dropped by silence check in earlier firmware — inconclusive) ## What works instead **Stateful response table (`sim_table.c`):** - Pre-computed responses uploaded to firmware BigBuf before sim starts - Firmware does linear scan lookup (~2µs on 48MHz ARM7) - Response served within FDT on first attempt — no retry needed - Supports: static responses, EML read/write, AES-CMAC computation, group-based state machine - All NXP custom commands (READ/WRITE_CONFIG, GET_RANDOM, SET_PASSWORD) handled via table - TAM/MAM authentication handled via table's AES-CMAC action (~100-150µs) **The relay remains as a fallback** for truly unknown commands that aren't in the table, but it should not be relied upon for correct operation. ## Recommendations for future work 1. **Do not design features that depend on relay timing.** Always use the response table for commands that must work reliably. 2. **The relay is useful for debugging/development** — it lets you see what commands a reader sends without pre-configuring table entries. 3. **If relay reliability is ever needed**, potential improvements: - DMA-based USB transfer (current `reply_ng` is polled) - Dedicated USB endpoint for relay traffic (avoid contention with debug/trace) - Firmware-side response prediction (start encoding a likely response while waiting for USB) - None of these are guaranteed to meet 311µs FDT consistently 4. **Spin-wait approach tested and failed.** We modified the firmware to spin-wait on `data_available()` for ~283µs after relaying, hoping to catch the Python response before FDT deadline. Results: - Python reports sending response in 0.14-0.38ms - But USB stack latency means `data_available()` doesn't see it in time - The spin-wait also broke the retry path (cached responses stopped being served) - **Reverted.** The USB round-trip to/from the ARM7 is fundamentally too slow for inline relay. 5. **For 14443-A Layer 4 (ISO-DEP)**, the relay IS viable via S(WTX) — the card requests a waiting time extension and the reader must wait. This gives seconds, not microseconds. The 15693 timing constraint does not apply there. ## Final verdict **Table for everything. Relay is a debug/fallback tool, not a production mechanism.**