hf.mfc renamed to hf.mf to match the C client's 'hf mf'. New hf.mfu (HFMFUCommands) provides rdbl/wrbl/dump over the dedicated CMD_HF_MIFAREU_READBL/WRITEBL using the firmware mful_readblock_t / mful_writeblock_t structs; a READ returns 4 pages (16 bytes) and the firmware replies them directly. keytype selects auth (0=none, 1=UL-C, 2=EV1/NTAG pwd, 3=UL-AES). Wire formats verified against firmware structs; needs a bench pass for the auth/write-length paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""High frequency commands: hf.*"""
|
|
import struct
|
|
import asyncio
|
|
from .protocol import Cmd
|
|
from .transport import PM3Transport, PM3Error, ProgressCallback
|
|
|
|
|
|
class HFCommands:
|
|
"""High frequency commands."""
|
|
|
|
def __init__(self, transport: PM3Transport):
|
|
self._t = transport
|
|
# Sub-modules attached by client.py
|
|
self.iso14a = None # type: ignore
|
|
self.iso15 = None # type: ignore
|
|
self.mf = None # MIFARE Classic # type: ignore
|
|
self.mfu = None # MIFARE Ultralight / NTAG # type: ignore
|
|
|
|
async def tune(self, on_progress: ProgressCallback = None) -> dict:
|
|
"""Measure HF antenna voltage.
|
|
|
|
CMD_MEASURE_ANTENNA_TUNING_HF is a START/MEASURE/STOP state machine that
|
|
requires a 1-byte mode arg (firmware rejects length!=1 with EINVARG):
|
|
mode 1 = START (spins up the HF FPGA), mode 2 = MEASURE (returns uint16 mV),
|
|
mode 3 = STOP. Voltage is a uint16_t, not uint32_t.
|
|
"""
|
|
await self._t.send_ng(Cmd.MEASURE_ANTENNA_TUNING_HF, bytes([1]), timeout=10.0) # START
|
|
resp = await self._t.send_ng(Cmd.MEASURE_ANTENNA_TUNING_HF, bytes([2]), timeout=10.0) # MEASURE
|
|
voltage = struct.unpack_from("<H", resp.data, 0)[0] if len(resp.data) >= 2 else 0
|
|
await self._t.send_ng(Cmd.MEASURE_ANTENNA_TUNING_HF, bytes([3]), timeout=10.0) # STOP
|
|
result = {
|
|
"voltage_mV": voltage,
|
|
"voltage_V": round(voltage / 1000.0, 3),
|
|
}
|
|
if on_progress:
|
|
ret = on_progress({"type": "hf_tune", "result": result})
|
|
if asyncio.iscoroutine(ret):
|
|
await ret
|
|
return result
|
|
|
|
async def search(self, on_progress: ProgressCallback = None) -> dict:
|
|
"""Search for HF tags. Tries 14a, then 15."""
|
|
results = {}
|
|
|
|
if on_progress:
|
|
ret = on_progress({"type": "hf_search", "phase": "14a"})
|
|
if asyncio.iscoroutine(ret):
|
|
await ret
|
|
|
|
if self.iso14a:
|
|
try:
|
|
card = await self.iso14a.scan()
|
|
if card.get("uid"):
|
|
results["iso14443a"] = card
|
|
except PM3Error:
|
|
pass
|
|
|
|
if on_progress:
|
|
ret = on_progress({"type": "hf_search", "phase": "15693"})
|
|
if asyncio.iscoroutine(ret):
|
|
await ret
|
|
|
|
if self.iso15:
|
|
try:
|
|
tag = await self.iso15.scan()
|
|
if tag.get("uid"):
|
|
results["iso15693"] = tag
|
|
except PM3Error:
|
|
pass
|
|
|
|
results["found"] = len(results) > 0
|
|
return results
|
|
|
|
async def sniff(self, samples_to_skip: int = 0, triggers_to_skip: int = 0,
|
|
skip_mode: int = 0, skip_ratio: int = 0,
|
|
on_progress: ProgressCallback = None) -> dict:
|
|
"""Sniff HF traffic.
|
|
|
|
Firmware reads a 10-byte PACKED struct
|
|
``{uint32 samplesToSkip; uint32 triggersToSkip; uint8 skipMode;
|
|
uint8 skipRatio}`` and, when the capture ends, replies with a
|
|
``{uint16 len}`` sample count. The struct must always be sent whole
|
|
(matching the C client's ``sizeof(params)``); under-sending leaves
|
|
the firmware reading uninitialised BigBuf bytes.
|
|
"""
|
|
payload = struct.pack("<IIBB", samples_to_skip, triggers_to_skip,
|
|
skip_mode, skip_ratio)
|
|
resp = await self._t.send_ng(Cmd.HF_SNIFF, payload, timeout=30.0)
|
|
samples = struct.unpack_from("<H", resp.data, 0)[0] if len(resp.data) >= 2 else 0
|
|
return {"status": resp.status, "samples": samples}
|
|
|
|
async def dropfield(self) -> dict:
|
|
"""Turn off HF field.
|
|
|
|
The firmware handler is a bare ``hf_field_off(); break;`` with no
|
|
``reply_ng``, so waiting for a response just blocks until timeout.
|
|
Fire-and-forget instead.
|
|
"""
|
|
await self._t.send_ng_no_response(Cmd.HF_DROPFIELD)
|
|
return {"status": 0}
|