feat(lf): test-mode T55xx write (opcode 01) for recovery

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>
This commit is contained in:
michael
2026-07-16 20:01:24 -07:00
parent a990d725ef
commit 907522f0ae
2 changed files with 22 additions and 2 deletions

View File

@@ -25,8 +25,15 @@ class T55xxCommands:
}
async def writebl(self, block: int, data: int, page: int = 0,
password: int | None = None, downlink_mode: int = 0) -> dict:
"""Write a T55xx block."""
password: int | None = None, downlink_mode: int = 0,
test: bool = False) -> dict:
"""Write a T55xx block.
``test`` uses the test-mode write (downlink opcode 01) instead of the standard opcode 10.
Test mode is the datasheet's reconfiguration path (§5.10.3) — it can rewrite a tag that a
normal write can't (a corrupted/locked config), provided the master key still permits it
(allowed for key 9, denied once key 6 is set). Use it for recovery, not routine writes.
"""
pwd = password if password is not None else 0
# Firmware t55xx_write_block_t: data(4)+pwd(4)+blockno(1)+flags(1).
# flags: 0x01 PwdMode, 0x02 Page, 0x04 test, 0x18 downlink_mode<<3.
@@ -35,6 +42,8 @@ class T55xxCommands:
flags |= 0x01
if page:
flags |= 0x02
if test:
flags |= 0x04
flags |= (downlink_mode & 0x03) << 3
payload = struct.pack("<IIBB", data, pwd, block, flags)
resp = await self._t.send_ng(Cmd.LF_T55XX_WRITEBL, payload, timeout=5.0)

View File

@@ -57,6 +57,17 @@ def test_lf_t55_writebl():
# 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)