feat(rawcli): INVENTORY_READ / FAST_INVENTORY_READ for the SLIX catalogs
Completes the ICODE SLIX/SLIX2 datasheet command set. These fuse an inventory round with a block read; unlike every other catalog command they need the inventory bit set in the request flags. Added an `inventory=` option to iso15_frame (sets inventory + single-slot, 0x26 with high rate) and both commands to _SLIX (inherited by _SLIX2), with the mask-less body mask_len 0 | first_block | count-1 (matching the tag model's _handle_inventory_read: first_block at data[4], num_blocks at data[5]+1). Verified live (completer names both, opcode 0xA0/0xA1 map back, entry round-trips) and unit-tested. Mask-based inventory (AFI/mask value) left as a future refinement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,10 @@ rendered dropdown on a real terminal is the only thing left unautomated.
|
|||||||
4. Autocomplete UX ✅ (hex + binary, command names + opcode-by-value; the byte-line completion bug
|
4. Autocomplete UX ✅ (hex + binary, command names + opcode-by-value; the byte-line completion bug
|
||||||
is fixed).
|
is fixed).
|
||||||
|
|
||||||
**Completeness gap (both SLIX catalogs):** `INVENTORY_READ` (0xA0) / `FAST_INVENTORY_READ` (0xA1)
|
`INVENTORY_READ` (0xA0) / `FAST_INVENTORY_READ` (0xA1) ✅ — added to `_SLIX` (and inherited by
|
||||||
are in the datasheet + the models but not yet in the catalogs — they carry an inventory request
|
`_SLIX2`) with the inventory flags (0x26) + mask-less body (`mask_len 0 | first_block | count-1`)
|
||||||
body (AFI/mask/block range), so they need a small dedicated builder, not a bare opcode.
|
via a new `inventory=` option on `iso15_frame`. Mask-based inventory (AFI/mask value) is a further
|
||||||
|
refinement if ever needed.
|
||||||
|
|
||||||
**Hardware verify pending:** SLIX2 catalog was verified only against unit tests + a live-session
|
**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.
|
drive (no SLIX2 tag on the reader). Confirm on a real SLIX2 when one is available.
|
||||||
|
|||||||
@@ -241,9 +241,15 @@ NXP_MFG = 0x04
|
|||||||
|
|
||||||
|
|
||||||
def iso15_frame(command: int, params: bytes = b"", *, uid=None, addressed: bool = False,
|
def iso15_frame(command: int, params: bytes = b"", *, uid=None, addressed: bool = False,
|
||||||
high_rate: bool = True, option: bool = False, mfg: int | None = None) -> bytes:
|
high_rate: bool = True, option: bool = False, inventory: bool = False,
|
||||||
"""Assemble an ISO 15693 request frame (pre-CRC): flags | command | [mfg] | [UID] | params."""
|
mfg: int | None = None) -> bytes:
|
||||||
|
"""Assemble an ISO 15693 request frame (pre-CRC): flags | command | [mfg] | [UID] | 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
|
flags = F15_HIGH_RATE if high_rate else 0
|
||||||
|
if inventory:
|
||||||
|
flags |= F15_INVENTORY | F15_1SLOT # single-slot inventory (one tag in field)
|
||||||
if option:
|
if option:
|
||||||
flags |= F15_OPTION
|
flags |= F15_OPTION
|
||||||
if addressed and uid:
|
if addressed and uid:
|
||||||
@@ -349,6 +355,16 @@ _SLIX = _catalog(
|
|||||||
"enter privacy mode with the privacy password (0xBA)", opcode=0xBA),
|
"enter privacy mode with the privacy password (0xBA)", opcode=0xBA),
|
||||||
_c("DESTROY", ["xor_pwd"], lambda xor_pwd: iso15_frame(0xB9, _hex(xor_pwd), mfg=NXP_MFG),
|
_c("DESTROY", ["xor_pwd"], lambda xor_pwd: iso15_frame(0xB9, _hex(xor_pwd), mfg=NXP_MFG),
|
||||||
"permanently destroy the tag — IRREVERSIBLE (0xB9)", opcode=0xB9),
|
"permanently destroy the tag — IRREVERSIBLE (0xB9)", opcode=0xB9),
|
||||||
|
# INVENTORY READ family: inventory + block read in one exchange. Body = mask_len | first_block
|
||||||
|
# | (count-1); mask-less (mask_len 0) reads the one tag in the field. Inventory flags (0x26).
|
||||||
|
_c("INVENTORY_READ", ["first_block", "count"],
|
||||||
|
lambda first_block, count: iso15_frame(
|
||||||
|
0xA0, bytes([0x00, _int(first_block), _int(count) - 1]), inventory=True, mfg=NXP_MFG),
|
||||||
|
"inventory + read <count> blocks from <first_block>, mask-less (0xA0)", opcode=0xA0),
|
||||||
|
_c("FAST_INVENTORY_READ", ["first_block", "count"],
|
||||||
|
lambda first_block, count: iso15_frame(
|
||||||
|
0xA1, bytes([0x00, _int(first_block), _int(count) - 1]), inventory=True, mfg=NXP_MFG),
|
||||||
|
"INVENTORY_READ with the response at the higher data rate (0xA1)", opcode=0xA1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,11 @@ class TestCatalog:
|
|||||||
# opcode = the command byte (frame byte 1), so by_opcode maps correctly
|
# opcode = the command byte (frame byte 1), so by_opcode maps correctly
|
||||||
assert _SLIX.get("GET_RANDOM").opcode() == 0xB2
|
assert _SLIX.get("GET_RANDOM").opcode() == 0xB2
|
||||||
assert _SLIX.by_opcode(0xB2).name == "GET_RANDOM" and _SLIX.by_opcode(0x20).name == "READ_BLOCK"
|
assert _SLIX.by_opcode(0xB2).name == "GET_RANDOM" and _SLIX.by_opcode(0x20).name == "READ_BLOCK"
|
||||||
|
# INVENTORY READ family: inventory flags (0x26) | 0xA0/0xA1 | mfg | mask_len 0 | first | count-1
|
||||||
|
assert _SLIX.get("INVENTORY_READ").build("0", "4") == bytes.fromhex("26a004000003")
|
||||||
|
assert _SLIX.get("FAST_INVENTORY_READ").build("2", "1") == bytes.fromhex("26a104000200")
|
||||||
|
assert _SLIX.by_opcode(0xA0).name == "INVENTORY_READ"
|
||||||
|
assert _SLIX.by_opcode(0xA1).name == "FAST_INVENTORY_READ"
|
||||||
# dispatch: an identified SLIX (or generic ICODE) resolves to this catalog
|
# dispatch: an identified SLIX (or generic ICODE) resolves to this catalog
|
||||||
s = RawSession(); s.protocol, s.transponder = "hf15", "ICODE SLIX"
|
s = RawSession(); s.protocol, s.transponder = "hf15", "ICODE SLIX"
|
||||||
assert catalog_for(s) is _SLIX
|
assert catalog_for(s) is _SLIX
|
||||||
@@ -467,6 +472,7 @@ class TestCatalog:
|
|||||||
assert _SLIX2.get(name).opcode() == op
|
assert _SLIX2.get(name).opcode() == op
|
||||||
assert _SLIX2.by_opcode(op).name == name # raw opcode maps back
|
assert _SLIX2.by_opcode(op).name == name # raw opcode maps back
|
||||||
assert _SLIX2.get("READ_SIGNATURE").build() == b"\x02\xBD\x04" # NXP mfg inserted
|
assert _SLIX2.get("READ_SIGNATURE").build() == b"\x02\xBD\x04" # NXP mfg inserted
|
||||||
|
assert _SLIX2.by_opcode(0xA0).name == "INVENTORY_READ" # inherited from SLIX
|
||||||
# dispatch: SLIX2 -> _SLIX2, plain SLIX still -> _SLIX (SLIX2 test comes first)
|
# dispatch: SLIX2 -> _SLIX2, plain SLIX still -> _SLIX (SLIX2 test comes first)
|
||||||
s = RawSession(); s.protocol = "hf15"
|
s = RawSession(); s.protocol = "hf15"
|
||||||
s.transponder = "ICODE SLIX2"; assert catalog_for(s) is _SLIX2
|
s.transponder = "ICODE SLIX2"; assert catalog_for(s) is _SLIX2
|
||||||
|
|||||||
Reference in New Issue
Block a user