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:
@@ -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)
|
## 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
|
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
|
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
|
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.
|
needs a finer probe (IC-reference / GET_SYSTEM_INFO) before its catalog can be routed.
|
||||||
|
|||||||
@@ -230,47 +230,81 @@ MFC = _catalog(
|
|||||||
# the request-FLAGS bitfield, NOT the command — so 15693 TagCommands carry an explicit opcode=
|
# 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
|
# (the command byte) for completion, and iso15_frame() assembles the wire frame (CRC added by
|
||||||
# hf.iso15.raw()).
|
# 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_INVENTORY = 0x04
|
||||||
F15_PROTO_EXT = 0x08
|
F15_PROTO_EXT = 0x08
|
||||||
F15_SELECT = 0x10
|
F15_SELECT = 0x10 # non-inventory: Select flag
|
||||||
F15_ADDRESSED = 0x20
|
F15_AFI = 0x10 # inventory: AFI flag (same bit, inventory mode)
|
||||||
F15_1SLOT = 0x20
|
F15_ADDRESSED = 0x20 # non-inventory: Address flag
|
||||||
|
F15_1SLOT = 0x20 # inventory: Nb_slots = 1 (else 16)
|
||||||
F15_OPTION = 0x40
|
F15_OPTION = 0x40
|
||||||
NXP_MFG = 0x04
|
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, inventory: bool = False,
|
high_rate: bool = True, option: bool = False, inventory: bool = False,
|
||||||
mfg: int | None = None) -> bytes:
|
select: bool = False, proto_ext: bool = False, two_subcarrier: bool = False,
|
||||||
"""Assemble an ISO 15693 request frame (pre-CRC): flags | command | [mfg] | [UID] | params.
|
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
|
Exposes the full request-flags byte (byte 0). Bits 5-6 differ by mode (ISO 15693-3 §7.3.1):
|
||||||
INVENTORY READ family, which are inventory commands rather than addressed reads."""
|
non-inventory -> Select(0x10)/Address(0x20); inventory -> AFI(0x10)/Nb_slots(0x20). Set
|
||||||
flags = F15_HIGH_RATE if high_rate else 0
|
``inventory=True`` for the INVENTORY family — 1 slot unless ``slots16``, and the AFI byte + flag
|
||||||
if inventory:
|
when ``afi`` is given; otherwise ``select``/``addressed`` apply. ``two_subcarrier`` (0x01),
|
||||||
flags |= F15_INVENTORY | F15_1SLOT # single-slot inventory (one tag in field)
|
``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:
|
if option:
|
||||||
flags |= F15_OPTION
|
flags |= F15_OPTION
|
||||||
if addressed and uid:
|
if inventory:
|
||||||
flags |= F15_ADDRESSED
|
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])
|
out = bytearray([flags, command])
|
||||||
if mfg is not None:
|
if mfg is not None:
|
||||||
out.append(mfg)
|
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)
|
u = bytes.fromhex(uid) if isinstance(uid, str) else bytes(uid)
|
||||||
out += u[::-1] # UID is transmitted LSByte-first
|
out += u[::-1] # UID is transmitted LSByte-first
|
||||||
return bytes(out) + bytes(params)
|
return bytes(out) + bytes(params)
|
||||||
|
|
||||||
|
|
||||||
def iso15_flags_decode(flags: int) -> str:
|
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:
|
if flags & F15_INVENTORY:
|
||||||
parts = ["inventory", "high rate" if flags & F15_HIGH_RATE else "low rate",
|
parts = ["inventory", "high rate" if flags & F15_HIGH_RATE else "low rate"]
|
||||||
"1 slot" if flags & F15_1SLOT else "16 slots"]
|
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:
|
else:
|
||||||
parts = ["high rate" if flags & F15_HIGH_RATE else "low rate",
|
parts = ["high rate" if flags & F15_HIGH_RATE else "low rate"]
|
||||||
"addressed" if flags & F15_ADDRESSED else "non-addressed"]
|
if flags & F15_SUBCARRIER:
|
||||||
|
parts.append("two subcarrier")
|
||||||
|
parts.append("addressed" if flags & F15_ADDRESSED else "non-addressed")
|
||||||
if flags & F15_SELECT:
|
if flags & F15_SELECT:
|
||||||
parts.append("select")
|
parts.append("select")
|
||||||
if flags & F15_PROTO_EXT:
|
if flags & F15_PROTO_EXT:
|
||||||
@@ -280,8 +314,8 @@ def iso15_flags_decode(flags: int) -> str:
|
|||||||
return " · ".join(parts)
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
# common flags bytes offered at frame byte 0
|
# common flags bytes offered at frame byte 0 (the decode hint covers any value you type)
|
||||||
ISO15_FLAG_LANDMARKS = [0x02, 0x22, 0x42, 0x62, 0x0A, 0x26, 0x06, 0x00]
|
ISO15_FLAG_LANDMARKS = [0x02, 0x22, 0x12, 0x42, 0x62, 0x0A, 0x26, 0x06, 0x36, 0x00]
|
||||||
|
|
||||||
|
|
||||||
# ---- ISO 15693 (generic) ----
|
# ---- ISO 15693 (generic) ----
|
||||||
|
|||||||
@@ -438,12 +438,27 @@ class TestCatalog:
|
|||||||
f = iso15_frame(0x20, bytes([4]), uid="e004010203040506", addressed=True)
|
f = iso15_frame(0x20, bytes([4]), uid="e004010203040506", addressed=True)
|
||||||
assert f == bytes.fromhex("2220") + bytes.fromhex("e004010203040506")[::-1] + bytes([4])
|
assert f == bytes.fromhex("2220") + bytes.fromhex("e004010203040506")[::-1] + bytes([4])
|
||||||
assert iso15_frame(0x20, bytes([4]), option=True) == b"\x42\x20\x04"
|
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):
|
def test_iso15_flags_decode(self):
|
||||||
assert iso15_flags_decode(0x02) == "high rate · non-addressed"
|
assert iso15_flags_decode(0x02) == "high rate · non-addressed"
|
||||||
assert iso15_flags_decode(0x22) == "high rate · addressed"
|
assert iso15_flags_decode(0x22) == "high rate · addressed"
|
||||||
assert "option" in iso15_flags_decode(0x42)
|
assert "option" in iso15_flags_decode(0x42)
|
||||||
assert "inventory" in iso15_flags_decode(0x26)
|
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):
|
def test_slix_catalog(self):
|
||||||
# ICODE SLIX datasheet command set: standard + NXP custom, frames via the header builder
|
# ICODE SLIX datasheet command set: standard + NXP custom, frames via the header builder
|
||||||
|
|||||||
Reference in New Issue
Block a user