feat(rawcli): ICODE SLIX datasheet catalog — 15693 header builder + position-aware completion
Adds ISO 15693 / ICODE support in the datasheet-catalog style: - iso15_frame() header builder assembles a request (flags | command | [NXP mfg 0x04] | [UID LSByte-first] | params); hf.iso15.raw() sends it (CRC host-side, auto long-wait for write/lock). - ICODE SLIX (SL2S2002) catalog: the datasheet command set — standard 15693 (READ/WRITE/LOCK block, READ_MULTIPLE, AFI/DSFID, GET_SYSTEM_INFO, security, select/reset/stay-quiet) and NXP customs (GET_NXP_SYSTEM_INFO, GET_RANDOM, SET/WRITE/LOCK_PASSWORD, PROTECT_PAGE, EAS set/reset/ lock/alarm/write-id, ENABLE_PRIVACY, DESTROY). catalog_for routes SLIX/ICODE names here. - TagCommand gains an explicit opcode= : in 15693 the frame's first byte is the request FLAGS, not the command, so completion keys on the command byte (byte 1). - Position-aware raw completion for hf15: byte 0 = the flags menu with a live decode of each value, byte 1 = the command, byte 2 = the 04 mfg code on custom commands (previewed with the user). Function-call form and command-name completion unchanged. - Per-chip identify: E0 04 -> NXP ICODE; a full 32-byte READ_SIGNATURE distinguishes SLIX2 from SLIX. Robust UID fetch (inventory retry + a validated direct GET_SYSTEM_INFO fallback), since vicinity anticollision misses often. Hardware-verified on an ICODE SLIX: identify -> "ICODE SLIX" (reliably), the frames build correctly (02 2B / 02 20 00 / 02 B2 04), and the flags/command/mfg menus + block map resolve. (Command *responses* intermittently desync on this specific flaky PM3 link — a known device issue across protocols, not the framing.) 1376 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,8 @@ 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, catalog_for, catalog_for as _cf
|
||||
from pm3py.cli.rawcli.catalog import (TYPE2, ISO15, T5577, LF_READONLY, MFC, _SLIX,
|
||||
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
|
||||
from pm3py.cli.rawcli.app import dispatch
|
||||
@@ -249,9 +250,16 @@ class TestIdentify:
|
||||
assert page_role(s.transponder, 4) == "user memory"
|
||||
|
||||
def test_15693_when_e0_uid(self):
|
||||
# E0 04 = NXP ICODE; no READ_SIGNATURE (mock) -> named ICODE SLIX, not generic ISO15693
|
||||
s = RawSession(device=_mock_device(scan15={"found": True, "uid": "e004010203040506"}))
|
||||
r = identify(s)
|
||||
assert r["found"] and s.field == "hf" and s.protocol == "hf15"
|
||||
assert s.transponder == "ICODE SLIX"
|
||||
|
||||
def test_15693_non_nxp_generic(self):
|
||||
# a non-NXP E0 UID stays generic ISO15693
|
||||
s = RawSession(device=_mock_device(scan15={"found": True, "uid": "e007010203040506"}))
|
||||
identify(s)
|
||||
assert s.transponder == "ISO15693"
|
||||
|
||||
def test_15693_rejects_bogus_uid(self):
|
||||
@@ -418,8 +426,37 @@ class TestCatalog:
|
||||
assert "not yet wired" in MFC.get("PERSONALIZE_UID").run(dev, "2") # EV1 opcode-only
|
||||
|
||||
def test_iso15_builds(self):
|
||||
# 15693 frames are flags | command | [mfg] | params, assembled by iso15_frame
|
||||
assert ISO15.get("READ_BLOCK").build("4") == b"\x02\x20\x04"
|
||||
assert ISO15.get("INVENTORY").build() == b"\x26\x01\x00"
|
||||
assert ISO15.get("GET_SYSTEM_INFO").build() == b"\x02\x2B"
|
||||
assert ISO15.get("READ_BLOCK").opcode() == 0x20 # opcode = the command byte, not flags
|
||||
|
||||
def test_iso15_frame_builder(self):
|
||||
assert iso15_frame(0x20, bytes([4])) == b"\x02\x20\x04" # non-addressed, high rate
|
||||
assert iso15_frame(0xB2, mfg=0x04) == b"\x02\xB2\x04" # NXP custom -> mfg byte
|
||||
# addressed: flags 0x22, UID appended LSByte-first
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
def test_slix_catalog(self):
|
||||
# ICODE SLIX datasheet command set: standard + NXP custom, frames via the header builder
|
||||
assert _SLIX.get("READ_BLOCK").build("4") == b"\x02\x20\x04"
|
||||
assert _SLIX.get("GET_RANDOM").build() == b"\x02\xB2\x04" # NXP mfg inserted
|
||||
assert _SLIX.get("SET_PASSWORD").build("4", "DEADBEEF") == bytes.fromhex("02b30404deadbeef")
|
||||
assert _SLIX.get("READ_MULTIPLE").build("0", "4") == b"\x02\x23\x00\x03" # count-1 on wire
|
||||
# opcode = the command byte (frame byte 1), so by_opcode maps correctly
|
||||
assert _SLIX.get("GET_RANDOM").opcode() == 0xB2
|
||||
assert _SLIX.by_opcode(0xB2).name == "GET_RANDOM" and _SLIX.by_opcode(0x20).name == "READ_BLOCK"
|
||||
# dispatch: an identified SLIX (or generic ICODE) resolves to this catalog
|
||||
s = RawSession(); s.protocol, s.transponder = "hf15", "ICODE SLIX"
|
||||
assert catalog_for(s) is _SLIX
|
||||
|
||||
def test_resolver(self):
|
||||
s = RawSession()
|
||||
@@ -615,6 +652,24 @@ class TestCompleter:
|
||||
gv = next(c for c in cs if "GET_VERSION" in self._disp(c))
|
||||
assert gv.text == "01100000" # raw binary byte, not the command name
|
||||
|
||||
def test_iso15_position_aware_completion(self):
|
||||
# 15693 raw frame: byte 0 = flags menu (decoded), byte 1 = command, byte 2 = NXP mfg
|
||||
s = RawSession()
|
||||
s.protocol, s.transponder = "hf15", "ICODE SLIX"
|
||||
flags = {c.text: c.display_meta_text for c in self._complete(s, "")}
|
||||
assert flags["02"] == "high rate · non-addressed"
|
||||
assert flags["22"] == "high rate · addressed"
|
||||
cmds = {c.text: self._disp(c) for c in self._complete(s, "02 ")} # after flags -> command
|
||||
assert "READ_BLOCK" in cmds["20"] and "GET_RANDOM" in cmds["B2"]
|
||||
assert "04" in [c.text for c in self._complete(s, "02 B2 ")] # custom -> mfg byte
|
||||
assert self._complete(s, "02 20 ") == [] # standard cmd -> no mfg
|
||||
# function-call form still gets the block map, command names still complete by letters
|
||||
assert any("user memory" in (c.display_meta_text or "") for c in self._complete(s, "READ_BLOCK("))
|
||||
assert any("GET_RANDOM" in self._disp(c) for c in self._complete(s, "get_r"))
|
||||
# binary flags render as 8 bits
|
||||
s.entry_mode = "bin"
|
||||
assert any(c.text == "00000010" for c in self._complete(s, ""))
|
||||
|
||||
def test_mfc_opcode_completion(self):
|
||||
# the new MIFARE Classic datasheet opcodes surface by hex AND binary value, inserting the byte
|
||||
s = RawSession()
|
||||
|
||||
Reference in New Issue
Block a user