feat(rawcli): complete the 15693 request-flags byte in iso15_frame

The header builder previously set only high-rate, inventory+1slot, addressed and
option — F15_PROTO_EXT and F15_SELECT were even defined but unused, so several valid
request-flag bits could not be expressed. Complete it: sub-carrier (0x01), protocol-
extension (0x08), Select (0x10), 16-slot inventory (Nb_slots=0), and the inventory AFI
flag (0x10) with its AFI byte inserted before the params. Bits 5-6 are now branched
correctly by mode (Select/Address for non-inventory, AFI/Nb_slots for inventory), per
ISO 15693-3 §7.3.1. iso15_flags_decode decodes every bit in both modes, and the byte-0
flag landmarks gain select (0x12) and inventory+AFI (0x36).

Existing frames are unchanged (verified by the preserved 0x02/0x22/0x42/0x26 tests);
adds coverage for every newly-settable bit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-16 13:01:54 -07:00
parent 7b028f4ddc
commit 251e276fbb
3 changed files with 73 additions and 22 deletions

View File

@@ -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.

View File

@@ -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 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
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) ----

View File

@@ -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