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:
@@ -1,6 +1,6 @@
|
||||
"""ISO 15693 commands: hf.iso15.*"""
|
||||
import struct
|
||||
from .protocol import Cmd
|
||||
from .protocol import Cmd, crc15_bytes
|
||||
from .transport import PM3Transport, PM3Error
|
||||
|
||||
# Re-export trace symbols for backward compat with existing test imports
|
||||
@@ -11,12 +11,28 @@ from ..trace import (
|
||||
format_sniff_line,
|
||||
)
|
||||
|
||||
# ISO15 PM3 flags
|
||||
# ISO15 PM3 transport flags — values MUST match firmware include/iso15.h
|
||||
ISO15_CONNECT = 0x01
|
||||
ISO15_NO_DISCONNECT = 0x02
|
||||
ISO15_RAW = 0x04
|
||||
ISO15_APPEND_CRC = 0x08
|
||||
ISO15_READ_RESPONSE = 0x10
|
||||
ISO15_APPEND_CRC = 0x08 # legacy: ARM firmware ignores it — CRC is added host-side
|
||||
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:
|
||||
@@ -33,8 +49,7 @@ class HF15Commands:
|
||||
# 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.
|
||||
iso_cmd = bytes([0x26, 0x01, 0x00]) # flags, inventory cmd, mask_len=0
|
||||
flags = ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_APPEND_CRC
|
||||
payload = struct.pack("<BH", flags, len(iso_cmd)) + iso_cmd
|
||||
payload = _iso15_payload(iso_cmd)
|
||||
resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0)
|
||||
|
||||
if resp.status != 0 or len(resp.data) < 10:
|
||||
@@ -51,6 +66,17 @@ class HF15Commands:
|
||||
"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:
|
||||
"""Read a single block."""
|
||||
if uid:
|
||||
@@ -59,8 +85,7 @@ class HF15Commands:
|
||||
else:
|
||||
iso_cmd = bytes([0x02, 0x20, block]) # addressed without UID
|
||||
|
||||
flags = ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_APPEND_CRC
|
||||
payload = struct.pack("<BH", flags, len(iso_cmd)) + iso_cmd
|
||||
payload = _iso15_payload(iso_cmd)
|
||||
resp = await self._t.send_ng(Cmd.HF_ISO15693_COMMAND, payload, timeout=5.0)
|
||||
|
||||
if resp.status != 0 or len(resp.data) < 2:
|
||||
@@ -92,8 +117,7 @@ class HF15Commands:
|
||||
else:
|
||||
iso_cmd = bytes([0x02, 0x21, block]) + data
|
||||
|
||||
flags = ISO15_CONNECT | ISO15_READ_RESPONSE | ISO15_APPEND_CRC
|
||||
payload = struct.pack("<BH", flags, len(iso_cmd)) + iso_cmd
|
||||
payload = _iso15_payload(iso_cmd)
|
||||
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
|
||||
|
||||
@@ -68,6 +68,23 @@ def crc16_a(data: bytes) -> int:
|
||||
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 ---
|
||||
class PM3Status(IntEnum):
|
||||
SUCCESS = 0
|
||||
|
||||
@@ -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():
|
||||
assert crc16_a(b"") == 0x6363 # init 0xC6C6 reflected = 0x6363
|
||||
@@ -15,3 +15,20 @@ def test_crc16_a_ping_preamble():
|
||||
crc = crc16_a(preamble)
|
||||
assert isinstance(crc, int)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user