feat(rawcli): command catalogs + hints for identified LF chips

catalog_for() returned None for LF, so after identifying a T5577/EM4100 the completer and
input_hint had nothing — no command suggestions, no hex/binary opcode hints. Now LF chips
get catalogs like the HF ones:

- T5577 catalog: READ(block) / WRITE(block, data) / WAKE(password) / DETECT(), with the
  downlink opcode exposed via build() for hex+binary completion, block-role hints (0=config,
  7=password, 1-6=data), and execution via a new run() path (LF isn't a raw-byte exchange —
  it drives lf.t55 / the demod). READ(0)/DETECT use the reliable rotation-fixed config read;
  data-block reads are best-effort and labelled as such. capture.read_t55xx_block added.
- LF read-only credentials (native EM4100/HID/AWID/FDX-B) get a minimal catalog (INFO -> re-read).
- catalog_for dispatches protocol "lf": "T5577" in the label -> T5577, else read-only.
- TagCommand gains an optional run(device, *args); completer/_opcode tolerate build=None.

Hardware-verified: identify -> catalog T5577, help lists the commands, DETECT()/READ(0) return
config 00148040 (EM4100). Tests: LF resolver, T5577 opcodes, T5577 block-role hints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-15 13:53:15 -07:00
parent 6c5409c111
commit a0b70a1984
6 changed files with 152 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ from __future__ import annotations
import inspect
from . import ask
from . import protocols
# how many envelope samples to pull from BigBuf after a capture (a T55xx/EM frame is a few
@@ -114,6 +115,21 @@ def read_config(device) -> dict | None:
return protocols.decode_t55xx_config(data) if data else None
def read_t55xx_block(device, block: int) -> int | None:
"""Best-effort read of a T55xx 32-bit data block: send the block read, demod the first
repeating 32-bit word. A bare block has no header/CRC, so the bit alignment (rotation) can't
be verified — this is inherently best-effort. Block 0 (config) is the reliable one; read it
via read_config, which resolves the rotation against known presets."""
data = _capture(device, lambda: _try(lambda: device.lf.t55.readbl(int(block)), None))
if not data:
return None
for bits in ask.manchester_bits(data):
clean = [b for b in bits if b is not None]
for word in protocols._repeating_words(clean, 32):
return word
return None
def identify_lf(device, emit=None) -> dict | None:
"""Full LF identify against a live device. Returns a result dict (``found``/``field``/
``label``/``chip``/``config``/``emulating``/``emitted``) or ``None`` if the device is