fix(iso15): correct transport flags + append CRC host-side (scan/rdbl/wrbl)

ISO15_READ_RESPONSE was 0x10 but the firmware bit is 0x20 (0x10 is HIGH_SPEED, which pm3py lacked), so scan/rdbl/wrbl sent flags 0x19 with READ_RESPONSE never set -> firmware never listened and replied empty. The ARM firmware also ignores ISO15_APPEND_CRC (the C client adds the CRC host-side via AddCrc15), so pm3py's CRC-less frames were never answered by a real tag.

Fix flag values to match firmware include/iso15.h, add crc15 (CRC-16/X-25), and route all iso15 commands through _iso15_payload (appends CRC, sends CONNECT|HIGH_SPEED|READ_RESPONSE=0x31). Payload now byte-matches the C client getUID. Pre-existing bug, independent of the firmware rebase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-04 22:48:23 -07:00
parent 35f16b7664
commit f2b23f768b
3 changed files with 69 additions and 11 deletions

View File

@@ -1,6 +1,6 @@
"""ISO 15693 commands: hf.iso15.*""" """ISO 15693 commands: hf.iso15.*"""
import struct import struct
from .protocol import Cmd from .protocol import Cmd, crc15_bytes
from .transport import PM3Transport, PM3Error from .transport import PM3Transport, PM3Error
# Re-export trace symbols for backward compat with existing test imports # Re-export trace symbols for backward compat with existing test imports
@@ -11,12 +11,28 @@ from ..trace import (
format_sniff_line, format_sniff_line,
) )
# ISO15 PM3 flags # ISO15 PM3 transport flags — values MUST match firmware include/iso15.h
ISO15_CONNECT = 0x01 ISO15_CONNECT = 0x01
ISO15_NO_DISCONNECT = 0x02 ISO15_NO_DISCONNECT = 0x02
ISO15_RAW = 0x04 ISO15_RAW = 0x04
ISO15_APPEND_CRC = 0x08 ISO15_APPEND_CRC = 0x08 # legacy: ARM firmware ignores it — CRC is added host-side
ISO15_READ_RESPONSE = 0x10 ISO15_HIGH_SPEED = 0x10
ISO15_READ_RESPONSE = 0x20
ISO15_LONG_WAIT = 0x40
def _iso15_payload(iso_cmd: bytes) -> bytes:
"""Build a HF_ISO15693_COMMAND payload from a raw ISO15693 command.
Appends the ISO15693 CRC host-side (the ARM firmware ignores ISO15_APPEND_CRC —
the client must add it, like the C client's AddCrc15) and prefixes the PM3 transport
flags. HIGH_SPEED + READ_RESPONSE make the firmware receive at high data rate and
actually listen for the tag's answer — without READ_RESPONSE (0x20) the firmware
passes a NULL rx buffer and replies empty.
"""
raw = iso_cmd + crc15_bytes(iso_cmd)
flags = ISO15_CONNECT | ISO15_HIGH_SPEED | ISO15_READ_RESPONSE
return struct.pack("<BH", flags, len(raw)) + raw
class HF15Commands: class HF15Commands:
@@ -33,8 +49,7 @@ class HF15Commands:
# so this parser would read the struct header as a zeroed UID. 1-slot gives the # so this parser would read the struct header as a zeroed UID. 1-slot gives the
# simple single-tag response this method expects. Use hf 15 reader --all for many. # simple single-tag response this method expects. Use hf 15 reader --all for many.
iso_cmd = bytes([0x26, 0x01, 0x00]) # flags, inventory cmd, mask_len=0 iso_cmd = bytes([0x26, 0x01, 0x00]) # flags, inventory cmd, mask_len=0
flags = ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_APPEND_CRC payload = _iso15_payload(iso_cmd)
payload = struct.pack("<BH", flags, len(iso_cmd)) + iso_cmd
resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0) resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0)
if resp.status != 0 or len(resp.data) < 10: if resp.status != 0 or len(resp.data) < 10:
@@ -51,6 +66,17 @@ class HF15Commands:
"response_flags": resp_flags, "response_flags": resp_flags,
} }
async def raw_inventory(self, inv_flags: int = 0x26) -> dict:
"""Diagnostic: send a raw ISO15693 inventory with a caller-chosen request-flags
byte and return the raw device response (no parsing). Use to isolate 1-slot vs
16-slot behavior: inv_flags=0x26 → 1 slot (SLOT1 set), 0x06 → 16 slots.
"""
iso_cmd = bytes([inv_flags, 0x01, 0x00]) # inv flags, INVENTORY cmd, mask_len=0
payload = _iso15_payload(iso_cmd)
resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0)
return {"inv_flags": hex(inv_flags), "status": resp.status,
"len": len(resp.data), "data": resp.data.hex()}
async def rdbl(self, block: int, uid: str | None = None) -> dict: async def rdbl(self, block: int, uid: str | None = None) -> dict:
"""Read a single block.""" """Read a single block."""
if uid: if uid:
@@ -59,8 +85,7 @@ class HF15Commands:
else: else:
iso_cmd = bytes([0x02, 0x20, block]) # addressed without UID iso_cmd = bytes([0x02, 0x20, block]) # addressed without UID
flags = ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_APPEND_CRC payload = _iso15_payload(iso_cmd)
payload = struct.pack("<BH", flags, len(iso_cmd)) + iso_cmd
resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0) resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0)
if resp.status != 0 or len(resp.data) < 2: if resp.status != 0 or len(resp.data) < 2:
@@ -92,8 +117,7 @@ class HF15Commands:
else: else:
iso_cmd = bytes([0x02, 0x21, block]) + data iso_cmd = bytes([0x02, 0x21, block]) + data
flags = ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_APPEND_CRC payload = _iso15_payload(iso_cmd)
payload = struct.pack("<BH", flags, len(iso_cmd)) + iso_cmd
resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0) resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0)
success = resp.status == 0 and len(resp.data) > 0 and resp.data[0] == 0 success = resp.status == 0 and len(resp.data) > 0 and resp.data[0] == 0

View File

@@ -68,6 +68,23 @@ def crc16_a(data: bytes) -> int:
return crc return crc
def crc15(data: bytes) -> int:
"""ISO 15693 / NFC Type 5 CRC (CRC-16/X-25): poly=0x8408 (reflected), init=0xFFFF,
refin/refout=true, xorout=0xFFFF. Check value crc15(b"123456789") == 0x906E."""
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0x8408 if (crc & 1) else (crc >> 1)
return (~crc) & 0xFFFF
def crc15_bytes(data: bytes) -> bytes:
"""ISO 15693 CRC as appended on the wire (LSB byte first)."""
c = crc15(data)
return bytes([c & 0xFF, (c >> 8) & 0xFF])
# --- Error codes --- # --- Error codes ---
class PM3Status(IntEnum): class PM3Status(IntEnum):
SUCCESS = 0 SUCCESS = 0

View File

@@ -1,4 +1,4 @@
from pm3py.core.protocol import crc16_a from pm3py.core.protocol import crc16_a, crc15, crc15_bytes
def test_crc16_a_empty(): def test_crc16_a_empty():
assert crc16_a(b"") == 0x6363 # init 0xC6C6 reflected = 0x6363 assert crc16_a(b"") == 0x6363 # init 0xC6C6 reflected = 0x6363
@@ -15,3 +15,20 @@ def test_crc16_a_ping_preamble():
crc = crc16_a(preamble) crc = crc16_a(preamble)
assert isinstance(crc, int) assert isinstance(crc, int)
assert 0 <= crc <= 0xFFFF assert 0 <= crc <= 0xFFFF
def test_crc15_known_vector():
# ISO15693 / NFC Type 5 CRC == CRC-16/X-25; standard check string -> 0x906E
assert crc15(b"123456789") == 0x906E
def test_crc15_residue():
# Appending crc15 (LSB-first) then re-running the raw X-25 loop over the full
# frame yields the X-25 residue 0xF0B8 — the property firmware CheckCrc15 uses.
data = bytes([0x26, 0x01, 0x00])
frame = data + crc15_bytes(data)
crc = 0xFFFF
for byte in frame:
crc ^= byte
for _ in range(8):
crc = (crc >> 1) ^ 0x8408 if (crc & 1) else (crc >> 1)
assert crc == 0xF0B8