diff --git a/docs/rawcli-todo.md b/docs/rawcli-todo.md index f0d33ec..33c290e 100644 --- a/docs/rawcli-todo.md +++ b/docs/rawcli-todo.md @@ -41,16 +41,26 @@ rendered dropdown on a real terminal is the only thing left unautomated. authenticate step is the reader's job before these. - Autocomplete must work in hex + binary (see the bug above). -## After MFC — 15693 vicinity chips: ICODE SLIX, ICODE SLIX2, NTAG5 +## After MFC — 15693 vicinity chips: ICODE SLIX ✅, ICODE SLIX2 ✅, NTAG5 (next) -1. **15693 header builder** (shared foundation). Replace the ad-hoc `bytes([0x02, cmd, …])` with a - real request-header builder: flags (data rate, addressed-vs-nonaddressed + 8-byte UID, option, - protocol-extension) + command + NXP **manufacturer code 0x04** for ICODE/NTAG5 custom commands. -2. **Per-chip identify.** Today identify stamps `"ISO15693"` generically; resolve the actual IC - (UID mfg byte `E0 04…` + GET_SYSTEM_INFO / IC-reference) so catalog + map key off SLIX2 / NTAG5. -3. **Catalogs + memory maps** per chip (blocks, config, counters, EAS/AFI, password/AES) — sourced - from the models in `transponders/hf/iso15693/nxp/` (`icode_slix`, `icode_slix2`, `ntag5_*`). -4. Same autocomplete UX (hex + binary). +1. **15693 header builder** ✅ — `iso15_frame()` (flags + command + NXP mfg 0x04), shared by all + 15693 catalogs. +2. **Per-chip identify** — `identify.name_iso15` distinguishes SLIX vs SLIX2 (READ_SIGNATURE 0xBD + probe). **NTAG5 and ICODE DNA still fall through to "ICODE SLIX"** (they share `E0 04`); NTAG5 + needs a finer probe (IC-reference / GET_SYSTEM_INFO) before its catalog can be routed. +3. **Catalogs** — `_SLIX` (plain) and `_SLIX2` (`_SLIX` + READ_SIGNATURE 0xBD / + PASSWORD_PROTECTION_64BIT 0xBB / STAY_QUIET_PERSISTENT 0xBC, 80 blocks) done, dispatched by + `catalog_for`, verified live. **NTAG5 catalog** (READ/WRITE_CONFIG, the NTAG5 config registers, + password/AES) still to build — source from `transponders/hf/iso15693/nxp/ntag5_*`. +4. Autocomplete UX ✅ (hex + binary, command names + opcode-by-value; the byte-line completion bug + is fixed). + +**Completeness gap (both SLIX catalogs):** `INVENTORY_READ` (0xA0) / `FAST_INVENTORY_READ` (0xA1) +are in the datasheet + the models but not yet in the catalogs — they carry an inventory request +body (AFI/mask/block range), so they need a small dedicated builder, not a bare opcode. + +**Hardware verify pending:** SLIX2 catalog was verified only against unit tests + a live-session +drive (no SLIX2 tag on the reader). Confirm on a real SLIX2 when one is available. ## Also queued diff --git a/pm3py/cli/rawcli/catalog.py b/pm3py/cli/rawcli/catalog.py index 2daf153..e33aabf 100644 --- a/pm3py/cli/rawcli/catalog.py +++ b/pm3py/cli/rawcli/catalog.py @@ -352,6 +352,19 @@ _SLIX = _catalog( ) +# ICODE SLIX2 (SL2S2602): everything the SLIX has, plus three SLIX2-only commands and a larger +# 80-block memory. READ_SIGNATURE (0xBD) is the one identify probes to tell SLIX2 from SLIX. +_SLIX2 = _catalog( + "ICODE SLIX2", "hf15", *_SLIX.commands.values(), + _c("READ_SIGNATURE", [], lambda: iso15_frame(0xBD, mfg=NXP_MFG), + "read the 32-byte ECC originality signature — SLIX2 only (0xBD)", opcode=0xBD), + _c("PASSWORD_PROTECTION_64BIT", [], lambda: iso15_frame(0xBB, mfg=NXP_MFG), + "combine the read+write passwords into 64-bit protection — SLIX2 only (0xBB)", opcode=0xBB), + _c("STAY_QUIET_PERSISTENT", [], lambda: iso15_frame(0xBC, mfg=NXP_MFG), + "persistent quiet that survives power cycles — SLIX2 only (0xBC)", opcode=0xBC), +) + + # ---- LF T5577 / ATA5577 (block read/write over the T55xx downlink; executes via lf.t55) ---- # build() exists only to expose the downlink opcode for hex/binary completion; execution goes # through run() (the device method), since LF isn't a raw-byte exchange like 14a. @@ -420,6 +433,8 @@ def catalog_for(session) -> Catalog | None: """Resolve the command catalog for the session's identified transponder, or None.""" name = (session.transponder or "").upper() if session.protocol == "hf15": + if "SLIX2" in name: + return _SLIX2 return _SLIX if "SLIX" in name or "ICODE" in name else ISO15 if session.protocol == "hf14a": if not session.transponder: diff --git a/tests/test_rawcli.py b/tests/test_rawcli.py index d0945c5..8c89e8e 100644 --- a/tests/test_rawcli.py +++ b/tests/test_rawcli.py @@ -11,7 +11,7 @@ from pm3py.cli.rawcli.trace_view import render_exchange from pm3py.cli.rawcli.parser import parse_token, parse_bytes, parse_line from pm3py.cli.rawcli.entry import type_digit, is_byte_line, reconcile_bases from pm3py.cli.rawcli.identify import identify, clear, name_14a, format_summary -from pm3py.cli.rawcli.catalog import (TYPE2, ISO15, T5577, LF_READONLY, MFC, _SLIX, +from pm3py.cli.rawcli.catalog import (TYPE2, ISO15, T5577, LF_READONLY, MFC, _SLIX, _SLIX2, iso15_frame, iso15_flags_decode, catalog_for, catalog_for as _cf) from pm3py.cli.rawcli.tlv import parse_tlv, format_tlv from pm3py.cli.rawcli.completer import RawCompleter @@ -458,6 +458,20 @@ class TestCatalog: s = RawSession(); s.protocol, s.transponder = "hf15", "ICODE SLIX" assert catalog_for(s) is _SLIX + def test_slix2_catalog(self): + # SLIX2 = the full SLIX datasheet set plus three SLIX2-only commands (0xBD/0xBB/0xBC) + assert set(_SLIX.commands) <= set(_SLIX2.commands) # superset of SLIX + for name, op in [("READ_SIGNATURE", 0xBD), ("PASSWORD_PROTECTION_64BIT", 0xBB), + ("STAY_QUIET_PERSISTENT", 0xBC)]: + assert name not in _SLIX.commands # SLIX2-only + assert _SLIX2.get(name).opcode() == op + assert _SLIX2.by_opcode(op).name == name # raw opcode maps back + assert _SLIX2.get("READ_SIGNATURE").build() == b"\x02\xBD\x04" # NXP mfg inserted + # dispatch: SLIX2 -> _SLIX2, plain SLIX still -> _SLIX (SLIX2 test comes first) + s = RawSession(); s.protocol = "hf15" + s.transponder = "ICODE SLIX2"; assert catalog_for(s) is _SLIX2 + s.transponder = "ICODE SLIX"; assert catalog_for(s) is _SLIX + def test_resolver(self): s = RawSession() assert catalog_for(s) is None # nothing identified