diff --git a/docs/rawcli-todo.md b/docs/rawcli-todo.md index c37ae21..411e0a7 100644 --- a/docs/rawcli-todo.md +++ b/docs/rawcli-todo.md @@ -44,7 +44,9 @@ rendered dropdown on a real terminal is the only thing left unautomated. ## After MFC — 15693 vicinity chips: ICODE SLIX ✅, ICODE SLIX2 ✅, NTAG5 (next) 1. **15693 header builder** ✅ — `iso15_frame()` (flags + command + NXP mfg 0x04), shared by all - 15693 catalogs. + 15693 catalogs. The **full request-flags byte** is settable: sub-carrier (0x01), data-rate, + inventory, protocol-ext (0x08), and the mode-dependent bits 5-6 (Select/Address vs AFI byte + + Nb_slots 1/16), plus option. `iso15_flags_decode` decodes every bit for the byte-0 hint. 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. diff --git a/pm3py/cli/rawcli/catalog.py b/pm3py/cli/rawcli/catalog.py index 8626387..d2f99b4 100644 --- a/pm3py/cli/rawcli/catalog.py +++ b/pm3py/cli/rawcli/catalog.py @@ -230,47 +230,81 @@ MFC = _catalog( # the request-FLAGS bitfield, NOT the command — so 15693 TagCommands carry an explicit opcode= # (the command byte) for completion, and iso15_frame() assembles the wire frame (CRC added by # hf.iso15.raw()). -F15_HIGH_RATE = 0x02 +# Request-flags byte (byte 0). Bits 5-6 (0x10/0x20) have two meanings, chosen by the Inventory +# flag (0x04): non-inventory -> Select/Address; inventory -> AFI/Nb_slots (ISO 15693-3 §7.3.1). +F15_SUBCARRIER = 0x01 # two subcarriers (else one) +F15_HIGH_RATE = 0x02 # data_rate: high (else low) F15_INVENTORY = 0x04 F15_PROTO_EXT = 0x08 -F15_SELECT = 0x10 -F15_ADDRESSED = 0x20 -F15_1SLOT = 0x20 +F15_SELECT = 0x10 # non-inventory: Select flag +F15_AFI = 0x10 # inventory: AFI flag (same bit, inventory mode) +F15_ADDRESSED = 0x20 # non-inventory: Address flag +F15_1SLOT = 0x20 # inventory: Nb_slots = 1 (else 16) F15_OPTION = 0x40 NXP_MFG = 0x04 def iso15_frame(command: int, params: bytes = b"", *, uid=None, addressed: bool = False, high_rate: bool = True, option: bool = False, inventory: bool = False, - mfg: int | None = None) -> bytes: - """Assemble an ISO 15693 request frame (pre-CRC): flags | command | [mfg] | [UID] | params. + select: bool = False, proto_ext: bool = False, two_subcarrier: bool = False, + slots16: bool = False, afi: int | None = None, mfg: int | None = None) -> bytes: + """Assemble an ISO 15693 request frame (pre-CRC): flags | command | [mfg] | [UID|AFI] | params. - ``inventory=True`` sets the inventory + single-slot flags (0x26 with high rate) for the ICODE - INVENTORY READ family, which are inventory commands rather than addressed reads.""" - flags = F15_HIGH_RATE if high_rate else 0 - if inventory: - flags |= F15_INVENTORY | F15_1SLOT # single-slot inventory (one tag in field) + Exposes the full request-flags byte (byte 0). Bits 5-6 differ by mode (ISO 15693-3 §7.3.1): + non-inventory -> Select(0x10)/Address(0x20); inventory -> AFI(0x10)/Nb_slots(0x20). Set + ``inventory=True`` for the INVENTORY family — 1 slot unless ``slots16``, and the AFI byte + flag + when ``afi`` is given; otherwise ``select``/``addressed`` apply. ``two_subcarrier`` (0x01), + ``proto_ext`` (0x08) and ``option`` (0x40) apply in both modes. An addressed UID is transmitted + LSByte-first; the inventory AFI byte precedes ``params`` (i.e. the mask length/value).""" + flags = 0 + if two_subcarrier: + flags |= F15_SUBCARRIER + if high_rate: + flags |= F15_HIGH_RATE + if proto_ext: + flags |= F15_PROTO_EXT if option: flags |= F15_OPTION - if addressed and uid: - flags |= F15_ADDRESSED + if inventory: + flags |= F15_INVENTORY + if afi is not None: + flags |= F15_AFI # inventory: AFI flag (bit 5) + if not slots16: + flags |= F15_1SLOT # inventory: Nb_slots = 1 (bit 6) + else: + if select: + flags |= F15_SELECT # non-inventory: Select (bit 5) + if addressed and uid: + flags |= F15_ADDRESSED # non-inventory: Address (bit 6) out = bytearray([flags, command]) if mfg is not None: out.append(mfg) - if addressed and uid: + if inventory: + if afi is not None: + out.append(afi) + elif addressed and uid: u = bytes.fromhex(uid) if isinstance(uid, str) else bytes(uid) out += u[::-1] # UID is transmitted LSByte-first return bytes(out) + bytes(params) def iso15_flags_decode(flags: int) -> str: - """Human decode of a 15693 request-flags byte (byte 0), for the raw-entry hint.""" + """Human decode of a 15693 request-flags byte (byte 0), for the raw-entry hint. Covers every + bit; bits 5-6 are mode-dependent (Select/Address vs AFI/Nb_slots, see iso15_frame).""" if flags & F15_INVENTORY: - parts = ["inventory", "high rate" if flags & F15_HIGH_RATE else "low rate", - "1 slot" if flags & F15_1SLOT else "16 slots"] + parts = ["inventory", "high rate" if flags & F15_HIGH_RATE else "low rate"] + if flags & F15_SUBCARRIER: + parts.append("two subcarrier") + parts.append("1 slot" if flags & F15_1SLOT else "16 slots") + if flags & F15_AFI: + parts.append("AFI") + if flags & F15_PROTO_EXT: + parts.append("protocol-ext") else: - parts = ["high rate" if flags & F15_HIGH_RATE else "low rate", - "addressed" if flags & F15_ADDRESSED else "non-addressed"] + parts = ["high rate" if flags & F15_HIGH_RATE else "low rate"] + if flags & F15_SUBCARRIER: + parts.append("two subcarrier") + parts.append("addressed" if flags & F15_ADDRESSED else "non-addressed") if flags & F15_SELECT: parts.append("select") if flags & F15_PROTO_EXT: @@ -280,8 +314,8 @@ def iso15_flags_decode(flags: int) -> str: return " · ".join(parts) -# common flags bytes offered at frame byte 0 -ISO15_FLAG_LANDMARKS = [0x02, 0x22, 0x42, 0x62, 0x0A, 0x26, 0x06, 0x00] +# common flags bytes offered at frame byte 0 (the decode hint covers any value you type) +ISO15_FLAG_LANDMARKS = [0x02, 0x22, 0x12, 0x42, 0x62, 0x0A, 0x26, 0x06, 0x36, 0x00] # ---- ISO 15693 (generic) ---- diff --git a/tests/test_rawcli.py b/tests/test_rawcli.py index 63775be..b899315 100644 --- a/tests/test_rawcli.py +++ b/tests/test_rawcli.py @@ -438,12 +438,27 @@ class TestCatalog: f = iso15_frame(0x20, bytes([4]), uid="e004010203040506", addressed=True) assert f == bytes.fromhex("2220") + bytes.fromhex("e004010203040506")[::-1] + bytes([4]) assert iso15_frame(0x20, bytes([4]), option=True) == b"\x42\x20\x04" + # full flags coverage: every request-flag bit is settable + assert iso15_frame(0x20, bytes([4]), select=True) == b"\x12\x20\x04" # 0x10 select + assert iso15_frame(0x20, bytes([4]), proto_ext=True) == b"\x0A\x20\x04" # 0x08 proto-ext + assert iso15_frame(0x20, bytes([4]), two_subcarrier=True) == b"\x03\x20\x04" # 0x01 + assert iso15_frame(0x20, bytes([4]), high_rate=False) == b"\x00\x20\x04" # low rate + assert iso15_frame(0x01, inventory=True, slots16=True) == b"\x06\x01" # 16-slot inventory + # inventory AFI: flag 0x10 set + AFI byte inserted before the params (mask/blocks) + assert iso15_frame(0x01, bytes([0]), inventory=True, afi=0x9A) == bytes.fromhex("36019a00") def test_iso15_flags_decode(self): assert iso15_flags_decode(0x02) == "high rate · non-addressed" assert iso15_flags_decode(0x22) == "high rate · addressed" assert "option" in iso15_flags_decode(0x42) assert "inventory" in iso15_flags_decode(0x26) + # every bit decodes, in both modes + assert "select" in iso15_flags_decode(0x12) + assert "protocol-ext" in iso15_flags_decode(0x0A) + assert "two subcarrier" in iso15_flags_decode(0x03) + assert "16 slots" in iso15_flags_decode(0x06) + assert "AFI" in iso15_flags_decode(0x36) + assert iso15_flags_decode(0x00).startswith("low rate") def test_slix_catalog(self): # ICODE SLIX datasheet command set: standard + NXP custom, frames via the header builder