From 453d1cca68d6985881572303d31b9abc1874b923 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 17 Mar 2026 18:57:23 -0700 Subject: [PATCH] docs: ISO 15693 relay timing findings Tested retry-based relay and spin-wait approaches. Relay works mechanically via retry but is unreliable (reader-dependent). Spin-wait fails due to USB stack latency. Table is the only viable mechanism for 15693 custom commands. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/RELAY_TIMING_FINDINGS.md | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/RELAY_TIMING_FINDINGS.md diff --git a/docs/RELAY_TIMING_FINDINGS.md b/docs/RELAY_TIMING_FINDINGS.md new file mode 100644 index 0000000..a694a39 --- /dev/null +++ b/docs/RELAY_TIMING_FINDINGS.md @@ -0,0 +1,80 @@ +# 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.**