Add a `test` flag to lf.t55.writebl that sets the firmware test-mode bit, sending the datasheet's opcode-01 reconfiguration write (§5.10.3) instead of the standard opcode-10 write. This is the path that can rewrite a tag a normal write can't (corrupted/locked config), when the master key still permits it. Matches the C client's `lf t55 write -t`. Default off; routine writes are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
5.2 KiB
Python
124 lines
5.2 KiB
Python
import struct
|
|
import asyncio
|
|
from unittest.mock import AsyncMock
|
|
from pm3py.core.protocol import Cmd
|
|
from pm3py.core.transport import PM3Response
|
|
from pm3py.core.lf import LFCommands
|
|
|
|
def make_response(cmd, status, data):
|
|
return PM3Response(cmd=cmd, status=status, reason=0, ng=True, data=data)
|
|
|
|
def test_lf_tune():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
# First call: init, second call: measure returns uint32 voltage
|
|
t.send_ng.return_value = make_response(Cmd.MEASURE_ANTENNA_TUNING_LF, 0,
|
|
struct.pack("<I", 42000))
|
|
result = asyncio.get_event_loop().run_until_complete(lf.tune(divisor=95))
|
|
assert result["voltage_mV"] == 42000
|
|
# Firmware requires exactly 2-byte payloads ({1,div} init, {2,div} measure)
|
|
calls = t.send_ng.call_args_list
|
|
assert calls[0].args[1] == struct.pack("<BB", 1, 95)
|
|
assert calls[1].args[1] == struct.pack("<BB", 2, 95)
|
|
|
|
def test_lf_config_get():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
payload = struct.pack("<bbbhhib", 1, 8, 1, 95, 128, 0, 0)
|
|
t.send_ng.return_value = make_response(Cmd.LF_SAMPLING_GET_CONFIG, 0, payload)
|
|
result = asyncio.get_event_loop().run_until_complete(lf.config())
|
|
assert "decimation" in result
|
|
assert "bits_per_sample" in result
|
|
|
|
def test_lf_config_set_fire_and_forget():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
payload = struct.pack("<bbbhhib", 1, 8, 1, 95, 128, 0, 0)
|
|
t.send_ng.return_value = make_response(Cmd.LF_SAMPLING_GET_CONFIG, 0, payload)
|
|
asyncio.get_event_loop().run_until_complete(lf.config(divisor=88))
|
|
# Firmware never replies to SET — must be fire-and-forget
|
|
t.send_ng_no_response.assert_awaited_once()
|
|
assert t.send_ng_no_response.call_args.args[0] == Cmd.LF_SAMPLING_SET_CONFIG
|
|
|
|
def test_lf_t55_writebl():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
t.send_ng.return_value = make_response(Cmd.LF_T55XX_WRITEBL, 0, b"")
|
|
asyncio.get_event_loop().run_until_complete(
|
|
lf.t55.writebl(block=4, data=0x12345678, page=1,
|
|
password=0xAABBCCDD, downlink_mode=2))
|
|
payload = t.send_ng.call_args.args[1]
|
|
# data(4)+pwd(4)+blockno(1)+flags(1) = 10 bytes
|
|
assert len(payload) == 10
|
|
data, pwd, blockno, flags = struct.unpack("<IIBB", payload)
|
|
assert data == 0x12345678
|
|
assert pwd == 0xAABBCCDD
|
|
assert blockno == 4
|
|
# PwdMode(0x01) | Page(0x02) | downlink 2<<3 (0x10) = 0x13
|
|
assert flags == 0x13
|
|
|
|
def test_lf_t55_writebl_testmode():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
t.send_ng.return_value = make_response(Cmd.LF_T55XX_WRITEBL, 0, b"")
|
|
asyncio.get_event_loop().run_until_complete(
|
|
lf.t55.writebl(block=0, data=0x00148040, test=True))
|
|
_, _, blockno, flags = struct.unpack("<IIBB", t.send_ng.call_args.args[1])
|
|
assert blockno == 0
|
|
assert flags & 0x04 # test-mode bit set (downlink opcode 01)
|
|
assert not (flags & 0x01) # no password mode by default
|
|
|
|
def test_lf_t55_wakeup():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
t.send_ng.return_value = make_response(Cmd.LF_T55XX_WAKEUP, 0, b"")
|
|
asyncio.get_event_loop().run_until_complete(
|
|
lf.t55.wakeup(password=0x11223344, downlink_mode=3))
|
|
payload = t.send_ng.call_args.args[1]
|
|
assert len(payload) == 5
|
|
pwd, flags = struct.unpack("<IB", payload)
|
|
assert pwd == 0x11223344
|
|
assert flags == (3 << 3)
|
|
|
|
def test_lf_t55_reset():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
t.send_ng.return_value = make_response(Cmd.LF_T55XX_RESET_READ, 0, b"")
|
|
asyncio.get_event_loop().run_until_complete(lf.t55.reset(downlink_mode=2))
|
|
cmd, payload = t.send_ng.call_args.args[0], t.send_ng.call_args.args[1]
|
|
assert cmd == Cmd.LF_T55XX_RESET_READ
|
|
# single flags byte carrying downlink_mode<<3
|
|
assert payload == struct.pack("<B", 2 << 3)
|
|
|
|
def test_lf_t55_config_client_side():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
result = asyncio.get_event_loop().run_until_complete(lf.t55.config())
|
|
assert len(result["modes"]) == 4
|
|
# No firmware round-trip: SET_CONFIG is write-only and never replies
|
|
t.send_ng.assert_not_called()
|
|
|
|
|
|
def test_lf_read_uses_reader_field_acq():
|
|
# read() must turn the reader field ON (LF_ACQ_RAW_ADC) — SNIFF is field-off and reads nothing
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
t.send_ng.return_value = make_response(Cmd.LF_ACQ_RAW_ADC, 0, struct.pack("<I", 98304))
|
|
result = asyncio.get_event_loop().run_until_complete(lf.read(samples=12288))
|
|
assert result["samples"] == 98304
|
|
args, kwargs = t.send_ng.call_args
|
|
assert args[0] == Cmd.LF_ACQ_RAW_ADC
|
|
payload = args[1] if len(args) > 1 else kwargs["payload"]
|
|
assert len(payload) == 5 # lf_sample_payload_t: 30+1+1 bits + cotag byte
|
|
word = struct.unpack_from("<I", payload, 0)[0]
|
|
assert (word & 0x3FFFFFFF) == 12288 # samples field
|
|
assert (word >> 30) & 1 == 0 # realtime = 0
|
|
|
|
|
|
def test_lf_sniff_uses_field_off():
|
|
t = AsyncMock()
|
|
lf = LFCommands(t)
|
|
t.send_ng.return_value = make_response(Cmd.LF_SNIFF_RAW_ADC, 0, struct.pack("<I", 100))
|
|
asyncio.get_event_loop().run_until_complete(lf.sniff(samples=4096))
|
|
assert t.send_ng.call_args[0][0] == Cmd.LF_SNIFF_RAW_ADC
|