feat(rawcli): full INVENTORY_READ request body (mask + AFI) — 15693 complete
_inv_read_frame builds the complete ICODE inventory-read request body: [AFI] | mask_len(bits) | mask value | first_block | count-1. INVENTORY_READ and FAST_INVENTORY_READ gain optional mask_len/mask (selective anticollision, ceil(bits/8) value bytes) and afi (filter byte + the 0x10 inventory flag, flags -> 0x36). Mask-less stays the default, so existing frames are unchanged. With this the ISO 15693 support is datasheet-complete end to end: the full request-flags byte, both SLIX and SLIX2 command sets, and the inventory-read body. Verified live (mask-less / 16-bit mask / AFI forms build + decode + round-trip) and unit-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,10 +57,11 @@ 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
|
||||
is fixed).
|
||||
|
||||
`INVENTORY_READ` (0xA0) / `FAST_INVENTORY_READ` (0xA1) ✅ — added to `_SLIX` (and inherited by
|
||||
`_SLIX2`) with the inventory flags (0x26) + mask-less body (`mask_len 0 | first_block | count-1`)
|
||||
via a new `inventory=` option on `iso15_frame`. Mask-based inventory (AFI/mask value) is a further
|
||||
refinement if ever needed.
|
||||
`INVENTORY_READ` (0xA0) / `FAST_INVENTORY_READ` (0xA1) ✅ — in `_SLIX` (inherited by `_SLIX2`),
|
||||
**full request body** via `_inv_read_frame`: `[AFI] | mask_len(bits) | mask value | first_block |
|
||||
count-1`, with the inventory flags set by `iso15_frame(inventory=…, afi=…)`. Mask-less (default),
|
||||
mask-based selective anticollision, and AFI filter all supported. **15693 is now datasheet-complete
|
||||
end to end** (flags byte + both SLIX/SLIX2 command sets + inventory-read body).
|
||||
|
||||
**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.
|
||||
|
||||
@@ -318,6 +318,22 @@ def iso15_flags_decode(flags: int) -> str:
|
||||
ISO15_FLAG_LANDMARKS = [0x02, 0x22, 0x12, 0x42, 0x62, 0x0A, 0x26, 0x06, 0x36, 0x00]
|
||||
|
||||
|
||||
def _inv_read_frame(cmd: int, first_block, count, mask_len="0", mask="", afi="") -> bytes:
|
||||
"""INVENTORY READ / FAST INVENTORY READ request body:
|
||||
``[AFI] | mask_len(bits) | mask_value | first_block | (count-1)``.
|
||||
|
||||
``mask_len`` 0 (default) reads the one tag in the field. A non-zero ``mask_len`` (bits) plus a
|
||||
``mask`` value (hex, LSByte-first on the wire, padded/truncated to ceil(bits/8) bytes) does
|
||||
selective anticollision; ``afi`` (when given) adds the AFI filter byte + flag. This is the full
|
||||
ICODE inventory-read request per the datasheet."""
|
||||
ml = _int(mask_len)
|
||||
nbytes = (ml + 7) // 8
|
||||
mask_bytes = ((_hex(mask) if mask else b"") + b"\x00" * nbytes)[:nbytes]
|
||||
body = bytes([ml]) + mask_bytes + bytes([_int(first_block), _int(count) - 1])
|
||||
return iso15_frame(cmd, body, inventory=True,
|
||||
afi=(_int(afi) if str(afi) != "" else None), mfg=NXP_MFG)
|
||||
|
||||
|
||||
# ---- ISO 15693 (generic) ----
|
||||
ISO15 = _catalog(
|
||||
"ISO 15693", "hf15",
|
||||
@@ -389,15 +405,16 @@ _SLIX = _catalog(
|
||||
"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),
|
||||
"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 family: inventory + block read in one exchange. Full body via _inv_read_frame
|
||||
# ([AFI] | mask_len | mask | first_block | count-1); mask-less reads the one tag in the field.
|
||||
_c("INVENTORY_READ", ["first_block", "count", "mask_len", "mask", "afi"],
|
||||
lambda first_block, count, mask_len="0", mask="", afi="":
|
||||
_inv_read_frame(0xA0, first_block, count, mask_len, mask, afi),
|
||||
"inventory + read <count> blocks from <first_block>; optional <mask_len> bits + <mask> value "
|
||||
"(selective anticollision) and <afi> filter (0xA0)", opcode=0xA0),
|
||||
_c("FAST_INVENTORY_READ", ["first_block", "count", "mask_len", "mask", "afi"],
|
||||
lambda first_block, count, mask_len="0", mask="", afi="":
|
||||
_inv_read_frame(0xA1, first_block, count, mask_len, mask, afi),
|
||||
"INVENTORY_READ with the response at the higher data rate (0xA1)", opcode=0xA1),
|
||||
)
|
||||
|
||||
|
||||
@@ -474,6 +474,9 @@ class TestCatalog:
|
||||
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"
|
||||
# full body: 16-bit mask (selective anticollision), and AFI filter (flag 0x10 -> flags 0x36)
|
||||
assert _SLIX.get("INVENTORY_READ").build("0", "4", "16", "abcd") == bytes.fromhex("26a00410abcd0003")
|
||||
assert _SLIX.get("INVENTORY_READ").build("0", "4", afi="0x9a") == bytes.fromhex("36a0049a000003")
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user