fix(rawcli): re-select per command + auto-CRC (as needed)

Live-hardware fixes:
- Commands after identify returned nothing because the tag selection went
  stale (and a prior no-CRC frame could drop it). Re-select the tag before
  each command (silent — not traced) via _ensure_selected(). Note: this
  resets auth, so multi-step PWD_AUTH needs a later keep-session mode.
- Auto-append the ISO14443-A CRC on 14a frames so a bare `60` works —
  except the frames that carry no CRC (REQA/WUPA short frames, 0x9x 0x20
  anticollision), via _needs_crc(). Applies to catalog commands and manual
  raw alike.
- Drop the now-redundant GET_VERSION padding trim (raw() trims properly).
- help text: function-style commands are here, not "next phase".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 13:22:40 -07:00
parent 7b5af3f9d2
commit c04e50efd0
2 changed files with 34 additions and 5 deletions

View File

@@ -24,10 +24,10 @@ rawcli — raw command interface
transponder show the identified transponder transponder show the identified transponder
close drop the tag connection close drop the tag connection
quit | exit | q leave rawcli quit | exit | q leave rawcli
<hex ...> send raw bytes to the tag; the annotated exchange prints above <hex/bin ...> send raw bytes to the tag (CRC auto-appended on 14a); trace prints above
Type hex as "30 04" or "3004"; Ctrl-/ toggles hex/binary entry (0x/0b). Function-style Type hex as "30 04" or "3004"; Ctrl-/ toggles hex/binary entry (0x/0b). Function-style
transponder commands (READ(4), GET_DATA) land in the next phase.""" commands (READ(4), GET_VERSION()) come from the identified tag — listed below."""
def _connect(port): def _connect(port):
@@ -43,6 +43,33 @@ def _connect(port):
ISO14A_APPEND_CRC = 0x20 ISO14A_APPEND_CRC = 0x20
def _needs_crc(payload: bytes) -> bool:
"""Whether a 14a frame should get a CRC appended — everything except the frames ISO14443-A
sends without one: the 7-bit short frames REQA (0x26) / WUPA (0x52) and the ANTICOLLISION
frames (0x9x 0x20, which carry a BCC, not a CRC)."""
if not payload:
return False
b0 = payload[0]
if len(payload) == 1 and b0 in (0x26, 0x52):
return False
if len(payload) >= 2 and b0 in (0x93, 0x95, 0x97) and payload[1] == 0x20:
return False
return True
def _ensure_selected(device, protocol: str) -> None:
"""Re-select the 14a tag before a command so a stale/dropped selection (or a prior bad frame)
can't leave the tag unresponsive. Silent — the re-select isn't traced. Note: this resets any
prior auth, so a PWD_AUTH must be issued together with the command it protects (a later
keep-session mode will address multi-step auth)."""
if device is None or protocol != "hf14a":
return
try:
device.hf.iso14a.scan()
except Exception:
pass
def _raw_exchange(device, payload: bytes, protocol: str = "hf14a", crc: bool = False) -> bytes | None: def _raw_exchange(device, payload: bytes, protocol: str = "hf14a", crc: bool = False) -> bytes | None:
"""Issue one raw exchange over the identified protocol, returning the response bytes. ``crc`` """Issue one raw exchange over the identified protocol, returning the response bytes. ``crc``
appends the ISO14443-A CRC (catalog commands need it; manual raw hex is sent verbatim).""" appends the ISO14443-A CRC (catalog commands need it; manual raw hex is sent verbatim)."""
@@ -100,7 +127,9 @@ def dispatch(state: RawSession, line: str, out=print) -> bool:
if not cmd.payload: if not cmd.payload:
return True return True
response = _raw_exchange(state.device, cmd.payload, protocol=state.protocol) _ensure_selected(state.device, state.protocol)
response = _raw_exchange(state.device, cmd.payload, protocol=state.protocol,
crc=state.protocol == "hf14a") # 14a frames need CRC; bare `60` works
out(render_exchange(cmd.payload, response, protocol=state.protocol, is_tty=True)) out(render_exchange(cmd.payload, response, protocol=state.protocol, is_tty=True))
return True return True
@@ -121,6 +150,7 @@ def _handle_call(state: RawSession, cmd, out=print) -> None:
return return
frames = built if isinstance(built, list) else [built] # multi-frame (e.g. two-phase write) frames = built if isinstance(built, list) else [built] # multi-frame (e.g. two-phase write)
crc = catalog.protocol == "hf14a" # 14a commands need CRC to be accepted crc = catalog.protocol == "hf14a" # 14a commands need CRC to be accepted
_ensure_selected(state.device, catalog.protocol) # re-select once for the command
for frame in frames: for frame in frames:
response = _raw_exchange(state.device, frame, protocol=catalog.protocol, crc=crc) response = _raw_exchange(state.device, frame, protocol=catalog.protocol, crc=crc)
out(render_exchange(frame, response, protocol=catalog.protocol, is_tty=True)) out(render_exchange(frame, response, protocol=catalog.protocol, is_tty=True))

View File

@@ -186,8 +186,7 @@ def _probe_version(dev, fmt, emit) -> tuple[str | None, bytes | None]:
if not raw: if not raw:
return None, None return None, None
version = bytes(raw[:8]) version = bytes(raw[:8])
display = bytes(raw).rstrip(b"\x00") or version # drop the firmware buffer padding emit(fmt.format(1, bytes(raw)).strip("\n")) # raw() already trims to the received length
emit(fmt.format(1, display).strip("\n"))
model = decode_version(version) model = decode_version(version)
if model: if model:
emit(f"{model}") emit(f"{model}")