docs: TableCompiler update implementation plan
4 tasks: update TableEntry to 120-byte format, compile_nxp_icode, compile_slix2, wire auto-compile into SimSession.start_15693. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
716
docs/plans/2026-03-17-table-compiler-update-plan.md
Normal file
716
docs/plans/2026-03-17-table-compiler-update-plan.md
Normal file
@@ -0,0 +1,716 @@
|
||||
# TableCompiler Update Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Update TableEntry to the new 120-byte firmware format with EML actions and groups, add NXP ICODE and SLIX2 compiler methods, and wire automatic table compilation into SimSession.
|
||||
|
||||
**Architecture:** Replace old 132-byte TableEntry (with match_mask) with new 120-byte format (with eml_action, groups, set_auth). Add `compile_nxp_icode()` and `compile_slix2()` to TableCompiler. SimSession auto-compiles and uploads table after sim starts.
|
||||
|
||||
**Tech Stack:** Python, pytest, existing sim-framework worktree
|
||||
|
||||
**Working directory:** `/home/work/pm3py/.worktrees/sim-framework/`
|
||||
|
||||
**Test command:** `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update TableEntry to new 120-byte format
|
||||
|
||||
**Files:**
|
||||
- Modify: `pm3py/sim/table_compiler.py`
|
||||
- Modify: `tests/test_sim_table_compiler.py`
|
||||
|
||||
**Step 1: Write failing tests for new format**
|
||||
|
||||
Replace the `TestTableEntry` class in `tests/test_sim_table_compiler.py`:
|
||||
|
||||
```python
|
||||
class TestTableEntry:
|
||||
def test_serialize_size(self):
|
||||
entry = TableEntry(match=b"\x26", response=b"\x04\x00")
|
||||
raw = entry.serialize()
|
||||
assert len(raw) == 120
|
||||
|
||||
def test_serialize_roundtrip(self):
|
||||
entry = TableEntry(
|
||||
match=b"\x26",
|
||||
match_mode=0,
|
||||
response=b"\x04\x00",
|
||||
response_flags=0x01,
|
||||
)
|
||||
raw = entry.serialize()
|
||||
parsed = TableEntry.deserialize(raw)
|
||||
assert parsed.match == b"\x26"
|
||||
assert parsed.response == b"\x04\x00"
|
||||
assert parsed.match_mode == 0
|
||||
assert parsed.response_flags == 0x01
|
||||
|
||||
def test_serialize_eml_fields(self):
|
||||
entry = TableEntry(
|
||||
match=b"\x02\xA1\x04",
|
||||
match_mode=1,
|
||||
response=bytes([0x00]),
|
||||
eml_action=1,
|
||||
eml_offset=2040,
|
||||
eml_len=1,
|
||||
eml_resp_insert=1,
|
||||
)
|
||||
raw = entry.serialize()
|
||||
parsed = TableEntry.deserialize(raw)
|
||||
assert parsed.eml_action == 1
|
||||
assert parsed.eml_offset == 2040
|
||||
assert parsed.eml_len == 1
|
||||
assert parsed.eml_resp_insert == 1
|
||||
|
||||
def test_serialize_group_fields(self):
|
||||
entry = TableEntry(
|
||||
match=b"\x02\xB3\x04\x04",
|
||||
response=bytes([0x00]),
|
||||
group=1,
|
||||
activate_groups=0x06,
|
||||
set_auth=0x04,
|
||||
)
|
||||
raw = entry.serialize()
|
||||
parsed = TableEntry.deserialize(raw)
|
||||
assert parsed.group == 1
|
||||
assert parsed.activate_groups == 0x06
|
||||
assert parsed.set_auth == 0x04
|
||||
|
||||
def test_serialize_consume_flag(self):
|
||||
entry = TableEntry(match=b"\x26", response=b"\x04\x00", flags=0x02)
|
||||
raw = entry.serialize()
|
||||
parsed = TableEntry.deserialize(raw)
|
||||
assert parsed.flags & 0x02
|
||||
|
||||
def test_exact_match(self):
|
||||
entry = TableEntry(match=b"\x26", response=b"\x04\x00")
|
||||
assert entry.matches(b"\x26")
|
||||
assert not entry.matches(b"\x52")
|
||||
|
||||
def test_prefix_match(self):
|
||||
entry = TableEntry(match=b"\x93\x20", match_mode=1, response=b"\x01\x02\x03\x04\x04")
|
||||
assert entry.matches(b"\x93\x20")
|
||||
assert entry.matches(b"\x93\x20\xFF\xFF")
|
||||
assert not entry.matches(b"\x95\x20")
|
||||
```
|
||||
|
||||
**Step 2: Run tests to see them fail**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestTableEntry -v`
|
||||
Expected: FAIL — old constructor expects positional args
|
||||
|
||||
**Step 3: Update TableEntry in table_compiler.py**
|
||||
|
||||
Replace the `TableEntry` dataclass, `ENTRY_SIZE`, remove `MATCH_MASKED`:
|
||||
|
||||
```python
|
||||
# Match modes
|
||||
MATCH_EXACT = 0
|
||||
MATCH_PREFIX = 1
|
||||
|
||||
# Entry size in firmware format (must match sim_table_entry_t)
|
||||
ENTRY_SIZE = 120
|
||||
SIM_TABLE_MAX_MATCH = 32
|
||||
SIM_TABLE_MAX_RESPONSE = 64
|
||||
|
||||
# Response flags
|
||||
RESP_FLAG_CRC = 0x01
|
||||
|
||||
# Entry flags
|
||||
ENTRY_FLAG_CONSUME = 0x02
|
||||
|
||||
# EML actions
|
||||
EML_NONE = 0
|
||||
EML_READ = 1
|
||||
EML_WRITE = 2
|
||||
EML_AES_CMAC = 3
|
||||
|
||||
# Config register offset in iso15_tag_t.data[]
|
||||
CONFIG_REG_OFFSET = 2040
|
||||
|
||||
|
||||
@dataclass
|
||||
class TableEntry:
|
||||
"""One entry in the firmware response table (120 bytes packed)."""
|
||||
match: bytes
|
||||
match_mode: int = MATCH_EXACT
|
||||
response: bytes = b""
|
||||
response_flags: int = 0
|
||||
eml_action: int = 0
|
||||
eml_offset: int = 0
|
||||
eml_len: int = 0
|
||||
eml_resp_insert: int = 0
|
||||
cmd_data_offset: int = 0
|
||||
cmd_data_len: int = 0
|
||||
group: int = 0
|
||||
activate_groups: int = 0
|
||||
deactivate_groups: int = 0
|
||||
set_auth: int = 0
|
||||
clear_auth: int = 0
|
||||
flags: int = 0
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
"""Pack into 120-byte firmware format."""
|
||||
buf = bytearray(ENTRY_SIZE)
|
||||
off = 0
|
||||
|
||||
# match[32] + match_len[1] + match_mode[1]
|
||||
m = self.match[:SIM_TABLE_MAX_MATCH]
|
||||
buf[off:off + len(m)] = m
|
||||
off += SIM_TABLE_MAX_MATCH
|
||||
buf[off] = len(m)
|
||||
buf[off + 1] = self.match_mode
|
||||
off += 2
|
||||
|
||||
# response[64] + response_len[1] + response_flags[1]
|
||||
r = self.response[:SIM_TABLE_MAX_RESPONSE]
|
||||
buf[off:off + len(r)] = r
|
||||
off += SIM_TABLE_MAX_RESPONSE
|
||||
buf[off] = len(r)
|
||||
buf[off + 1] = self.response_flags
|
||||
off += 2
|
||||
|
||||
# eml_action[1] + eml_offset[2 LE] + eml_len[1] + eml_resp_insert[1]
|
||||
# + cmd_data_offset[1] + cmd_data_len[1]
|
||||
buf[off] = self.eml_action
|
||||
struct.pack_into("<H", buf, off + 1, self.eml_offset)
|
||||
buf[off + 3] = self.eml_len
|
||||
buf[off + 4] = self.eml_resp_insert
|
||||
buf[off + 5] = self.cmd_data_offset
|
||||
buf[off + 6] = self.cmd_data_len
|
||||
off += 7
|
||||
|
||||
# group[1] + activate_groups[4 LE] + deactivate_groups[4 LE]
|
||||
# + set_auth[1] + clear_auth[1]
|
||||
buf[off] = self.group
|
||||
struct.pack_into("<I", buf, off + 1, self.activate_groups)
|
||||
struct.pack_into("<I", buf, off + 5, self.deactivate_groups)
|
||||
buf[off + 9] = self.set_auth
|
||||
buf[off + 10] = self.clear_auth
|
||||
off += 11
|
||||
|
||||
# flags[1] + _pad[1]
|
||||
buf[off] = self.flags
|
||||
off += 2
|
||||
|
||||
assert off == ENTRY_SIZE
|
||||
return bytes(buf)
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data: bytes) -> TableEntry:
|
||||
"""Unpack from 120-byte firmware format."""
|
||||
off = 0
|
||||
match_len = data[SIM_TABLE_MAX_MATCH]
|
||||
match = bytes(data[0:match_len])
|
||||
match_mode = data[SIM_TABLE_MAX_MATCH + 1]
|
||||
off = SIM_TABLE_MAX_MATCH + 2
|
||||
|
||||
resp_len = data[off + SIM_TABLE_MAX_RESPONSE]
|
||||
response = bytes(data[off:off + resp_len])
|
||||
response_flags = data[off + SIM_TABLE_MAX_RESPONSE + 1]
|
||||
off += SIM_TABLE_MAX_RESPONSE + 2
|
||||
|
||||
eml_action = data[off]
|
||||
eml_offset = struct.unpack_from("<H", data, off + 1)[0]
|
||||
eml_len = data[off + 3]
|
||||
eml_resp_insert = data[off + 4]
|
||||
cmd_data_offset = data[off + 5]
|
||||
cmd_data_len = data[off + 6]
|
||||
off += 7
|
||||
|
||||
group = data[off]
|
||||
activate_groups = struct.unpack_from("<I", data, off + 1)[0]
|
||||
deactivate_groups = struct.unpack_from("<I", data, off + 5)[0]
|
||||
set_auth = data[off + 9]
|
||||
clear_auth = data[off + 10]
|
||||
off += 11
|
||||
|
||||
flags = data[off]
|
||||
|
||||
return cls(
|
||||
match=match, match_mode=match_mode,
|
||||
response=response, response_flags=response_flags,
|
||||
eml_action=eml_action, eml_offset=eml_offset,
|
||||
eml_len=eml_len, eml_resp_insert=eml_resp_insert,
|
||||
cmd_data_offset=cmd_data_offset, cmd_data_len=cmd_data_len,
|
||||
group=group, activate_groups=activate_groups,
|
||||
deactivate_groups=deactivate_groups,
|
||||
set_auth=set_auth, clear_auth=clear_auth,
|
||||
flags=flags,
|
||||
)
|
||||
|
||||
def matches(self, cmd: bytes) -> bool:
|
||||
"""Check if a command matches this entry."""
|
||||
if self.match_mode == MATCH_EXACT:
|
||||
return cmd == self.match
|
||||
elif self.match_mode == MATCH_PREFIX:
|
||||
return len(cmd) >= len(self.match) and cmd[:len(self.match)] == self.match
|
||||
return False
|
||||
```
|
||||
|
||||
**Step 4: Update all existing callers and tests**
|
||||
|
||||
Replace old positional constructor calls throughout `table_compiler.py` and `tests/test_sim_table_compiler.py`:
|
||||
|
||||
Old: `TableEntry(b"\x26", 0, b"", b"\x04\x00", 0)`
|
||||
New: `TableEntry(match=b"\x26", response=b"\x04\x00")`
|
||||
|
||||
Old: `flags=0x01` (CRC)
|
||||
New: `response_flags=0x01`
|
||||
|
||||
Old: `flags=0x02` (consume)
|
||||
New: `flags=0x02` (unchanged — same field name, same bit)
|
||||
|
||||
Old: `match_mask=b""`
|
||||
New: removed
|
||||
|
||||
Update `ResponseTable.serialize` test: `assert len(raw) == 240` (2 * 120, not 264)
|
||||
|
||||
Update `SimSession.upload_table` test: `assert len(call_args[0][1]) == 120` (not 132)
|
||||
|
||||
Update `from_trace`: remove `match_mask=b""` parameter
|
||||
|
||||
**Step 5: Run all tests**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v`
|
||||
Expected: All pass
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/work/pm3py/.worktrees/sim-framework
|
||||
git add pm3py/sim/table_compiler.py tests/test_sim_table_compiler.py
|
||||
git commit --no-gpg-sign -m "feat: update TableEntry to 120-byte format with EML actions and groups"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add compile_nxp_icode method
|
||||
|
||||
**Files:**
|
||||
- Modify: `pm3py/sim/table_compiler.py`
|
||||
- Modify: `tests/test_sim_table_compiler.py`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
Append to `tests/test_sim_table_compiler.py`:
|
||||
|
||||
```python
|
||||
from pm3py.sim.nxp_icode import NxpIcodeTag
|
||||
from pm3py.sim.table_compiler import CONFIG_REG_OFFSET, EML_READ, EML_WRITE
|
||||
|
||||
|
||||
class TestTableCompilerNxpIcode:
|
||||
def test_compile_has_read_config_entries(self):
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
table = TableCompiler.compile_nxp_icode(tag)
|
||||
|
||||
# READ_CONFIG reg 0 (unaddressed normalized)
|
||||
entry = table.lookup(bytes([0x02, 0xA1, 0x04, 0x00]))
|
||||
assert entry is not None
|
||||
assert entry.eml_action == EML_READ
|
||||
assert entry.eml_offset == CONFIG_REG_OFFSET
|
||||
assert entry.eml_len == 1
|
||||
assert entry.eml_resp_insert == 1 # after flags byte
|
||||
|
||||
def test_compile_has_write_config_entries(self):
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
table = TableCompiler.compile_nxp_icode(tag)
|
||||
|
||||
entry = table.lookup(bytes([0x02, 0xA2, 0x04, 0x00]))
|
||||
assert entry is not None
|
||||
assert entry.eml_action == EML_WRITE
|
||||
assert entry.eml_offset == CONFIG_REG_OFFSET
|
||||
|
||||
def test_compile_has_get_random(self):
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
table = TableCompiler.compile_nxp_icode(tag)
|
||||
|
||||
entry = table.lookup(bytes([0x02, 0xB2, 0x04]))
|
||||
assert entry is not None
|
||||
assert len(entry.response) == 3 # flags + 2 random bytes
|
||||
|
||||
def test_compile_all_8_config_regs(self):
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
table = TableCompiler.compile_nxp_icode(tag)
|
||||
|
||||
for reg in range(8):
|
||||
entry = table.lookup(bytes([0x02, 0xA1, 0x04, reg]))
|
||||
assert entry is not None, f"Missing READ_CONFIG reg {reg}"
|
||||
assert entry.eml_offset == CONFIG_REG_OFFSET + reg
|
||||
|
||||
def test_config_regs_in_eml_range(self):
|
||||
"""Config reg EML offsets must be within tag->data[2048]."""
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
table = TableCompiler.compile_nxp_icode(tag)
|
||||
|
||||
for entry in table.entries:
|
||||
if entry.eml_action in (EML_READ, EML_WRITE):
|
||||
assert entry.eml_offset + entry.eml_len <= 2048
|
||||
```
|
||||
|
||||
**Step 2: Run to verify failure**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestTableCompilerNxpIcode -v`
|
||||
Expected: FAIL — `AttributeError: type object 'TableCompiler' has no attribute 'compile_nxp_icode'`
|
||||
|
||||
**Step 3: Implement compile_nxp_icode**
|
||||
|
||||
Add to `TableCompiler` class in `table_compiler.py`:
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def compile_nxp_icode(tag) -> ResponseTable:
|
||||
"""Compile NXP ICODE/NTAG5 custom command entries.
|
||||
|
||||
Generates table entries for READ/WRITE_CONFIG and GET_RANDOM.
|
||||
Config registers stored in EML at data[2040:2048].
|
||||
All entries use unaddressed format (firmware normalizes addressed cmds).
|
||||
"""
|
||||
entries = []
|
||||
num_regs = tag._config_regs if hasattr(tag, '_config_regs') else bytearray(8)
|
||||
num_config = len(num_regs)
|
||||
|
||||
for reg in range(num_config):
|
||||
# READ_CONFIG reg N: read 1 byte from EML, prefix with 0x00
|
||||
entries.append(TableEntry(
|
||||
match=bytes([0x02, 0xA1, 0x04, reg]),
|
||||
match_mode=MATCH_EXACT,
|
||||
response=bytes([0x00]),
|
||||
eml_action=EML_READ,
|
||||
eml_offset=CONFIG_REG_OFFSET + reg,
|
||||
eml_len=1,
|
||||
eml_resp_insert=1,
|
||||
))
|
||||
|
||||
# WRITE_CONFIG reg N: write 1 byte from cmd to EML
|
||||
entries.append(TableEntry(
|
||||
match=bytes([0x02, 0xA2, 0x04, reg]),
|
||||
match_mode=MATCH_EXACT,
|
||||
response=bytes([0x00]),
|
||||
eml_action=EML_WRITE,
|
||||
eml_offset=CONFIG_REG_OFFSET + reg,
|
||||
eml_len=1,
|
||||
cmd_data_offset=4, # value byte at position 4 in normalized cmd
|
||||
))
|
||||
|
||||
# GET_RANDOM: return pre-picked random
|
||||
import os
|
||||
random_bytes = os.urandom(2)
|
||||
entries.append(TableEntry(
|
||||
match=bytes([0x02, 0xB2, 0x04]),
|
||||
match_mode=MATCH_PREFIX,
|
||||
response=bytes([0x00]) + random_bytes,
|
||||
))
|
||||
|
||||
return ResponseTable(entries=entries)
|
||||
```
|
||||
|
||||
**Step 4: Run tests**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py -v`
|
||||
Expected: All pass
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/work/pm3py/.worktrees/sim-framework
|
||||
git add pm3py/sim/table_compiler.py tests/test_sim_table_compiler.py
|
||||
git commit --no-gpg-sign -m "feat: add TableCompiler.compile_nxp_icode for config registers + GET_RANDOM"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add compile_slix2 method
|
||||
|
||||
**Files:**
|
||||
- Modify: `pm3py/sim/table_compiler.py`
|
||||
- Modify: `tests/test_sim_table_compiler.py`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
Append to `tests/test_sim_table_compiler.py`:
|
||||
|
||||
```python
|
||||
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
|
||||
|
||||
|
||||
class TestTableCompilerSlix2:
|
||||
def test_compile_has_set_password(self):
|
||||
tag = IcodeSlix2Tag(
|
||||
uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
|
||||
read_password=0x12345678,
|
||||
)
|
||||
table = TableCompiler.compile_slix2(tag)
|
||||
|
||||
# SET_PASSWORD prefix match (error for wrong password)
|
||||
fallback = [e for e in table.entries if
|
||||
len(e.match) >= 3 and e.match[1] == 0xB3 and
|
||||
e.match_mode == MATCH_PREFIX]
|
||||
assert len(fallback) > 0
|
||||
assert fallback[-1].response == bytes([0x01, 0x0F]) # error
|
||||
|
||||
def test_compile_set_password_sets_auth(self):
|
||||
tag = IcodeSlix2Tag(
|
||||
uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
|
||||
read_password=0x12345678,
|
||||
)
|
||||
table = TableCompiler.compile_slix2(tag)
|
||||
|
||||
# Correct password entry should set auth bit
|
||||
auth_entries = [e for e in table.entries if e.set_auth != 0]
|
||||
assert len(auth_entries) > 0
|
||||
|
||||
def test_compile_includes_nxp_entries(self):
|
||||
"""SLIX2 table should include NXP base entries."""
|
||||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
table = TableCompiler.compile_slix2(tag)
|
||||
|
||||
# Should have READ_CONFIG from nxp_icode compiler
|
||||
entry = table.lookup(bytes([0x02, 0xA1, 0x04, 0x00]))
|
||||
assert entry is not None
|
||||
|
||||
def test_compile_privacy_mode_groups(self):
|
||||
"""Privacy mode should use groups to disable inventory."""
|
||||
tag = IcodeSlix2Tag(
|
||||
uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
|
||||
privacy_password=0xAABBCCDD,
|
||||
)
|
||||
table = TableCompiler.compile_slix2(tag)
|
||||
|
||||
# Should have a SET_PASSWORD entry that activates a group
|
||||
privacy_entries = [e for e in table.entries
|
||||
if e.activate_groups != 0]
|
||||
assert len(privacy_entries) > 0
|
||||
```
|
||||
|
||||
**Step 2: Run to verify failure**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestTableCompilerSlix2 -v`
|
||||
Expected: FAIL — `AttributeError: 'TableCompiler' has no attribute 'compile_slix2'`
|
||||
|
||||
**Step 3: Implement compile_slix2**
|
||||
|
||||
Add to `TableCompiler` class:
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def compile_slix2(tag) -> ResponseTable:
|
||||
"""Compile ICODE SLIX2 entries: passwords + privacy + NXP base.
|
||||
|
||||
Groups:
|
||||
0 = always active
|
||||
1 = normal operation (active unless privacy mode)
|
||||
"""
|
||||
# Start with NXP base entries (config regs, GET_RANDOM)
|
||||
table = TableCompiler.compile_nxp_icode(tag)
|
||||
|
||||
# Password auth bits: read=0x01, write=0x02, privacy=0x04, destroy=0x08
|
||||
PWD_AUTH_BITS = {0x01: 0x01, 0x02: 0x02, 0x04: 0x04, 0x08: 0x08}
|
||||
|
||||
# GET_RANDOM is already in the table — get the random bytes for XOR
|
||||
get_rnd = [e for e in table.entries if len(e.match) >= 2 and e.match[1] == 0xB2]
|
||||
random_bytes = get_rnd[0].response[1:3] if get_rnd else b"\x00\x00"
|
||||
|
||||
# SET_PASSWORD entries for each configured password
|
||||
for pwd_id, password in tag._passwords.items():
|
||||
if password is None:
|
||||
continue
|
||||
|
||||
auth_bit = PWD_AUTH_BITS.get(pwd_id, 0)
|
||||
|
||||
# Compute XORed password bytes (as reader would send)
|
||||
import struct as st
|
||||
pwd_bytes = st.pack("<I", password)
|
||||
xored = bytes([
|
||||
pwd_bytes[0] ^ random_bytes[0],
|
||||
pwd_bytes[1] ^ random_bytes[1],
|
||||
pwd_bytes[2] ^ random_bytes[0],
|
||||
pwd_bytes[3] ^ random_bytes[1],
|
||||
])
|
||||
|
||||
# Correct password entry (exact match with XOR'd bytes)
|
||||
entry_kwargs = dict(
|
||||
match=bytes([0x02, 0xB3, 0x04, pwd_id]) + xored,
|
||||
match_mode=MATCH_EXACT,
|
||||
response=bytes([0x00]),
|
||||
set_auth=auth_bit,
|
||||
)
|
||||
|
||||
# Privacy password also activates group 1
|
||||
if pwd_id == 0x04: # PWD_PRIVACY
|
||||
entry_kwargs['activate_groups'] = (1 << 1)
|
||||
|
||||
table.entries.append(TableEntry(**entry_kwargs))
|
||||
|
||||
# SET_PASSWORD fallback (wrong password → error)
|
||||
table.entries.append(TableEntry(
|
||||
match=bytes([0x02, 0xB3, 0x04]),
|
||||
match_mode=MATCH_PREFIX,
|
||||
response=bytes([0x01, 0x0F]), # error
|
||||
))
|
||||
|
||||
return table
|
||||
```
|
||||
|
||||
**Step 4: Run tests**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py -v`
|
||||
Expected: All pass
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/work/pm3py/.worktrees/sim-framework
|
||||
git add pm3py/sim/table_compiler.py tests/test_sim_table_compiler.py
|
||||
git commit --no-gpg-sign -m "feat: add TableCompiler.compile_slix2 with passwords and privacy groups"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Wire automatic table compilation into SimSession
|
||||
|
||||
**Files:**
|
||||
- Modify: `pm3py/sim/sim_session.py`
|
||||
- Modify: `tests/test_sim_table_compiler.py`
|
||||
|
||||
**Step 1: Write failing test**
|
||||
|
||||
Append to `tests/test_sim_table_compiler.py`:
|
||||
|
||||
```python
|
||||
class TestSimSessionTableCompile:
|
||||
def test_start_15693_compiles_nxp_table(self):
|
||||
"""start_15693 with compile=True should upload NXP table entries."""
|
||||
from pm3py.sim.sim_session import SimSession, CMD_SIM_TABLE_UPLOAD
|
||||
from unittest.mock import MagicMock
|
||||
import time
|
||||
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
|
||||
# Mock serial port
|
||||
mock_ser = MagicMock()
|
||||
mock_ser.in_waiting = 0
|
||||
mock_ser.read.return_value = b""
|
||||
|
||||
session = SimSession(port=mock_ser)
|
||||
session.start_15693(tag, trace=False, compile=True)
|
||||
|
||||
# Should have written: SIM command + table upload frames
|
||||
calls = mock_ser.write.call_args_list
|
||||
assert len(calls) >= 2 # at least sim start + table upload
|
||||
|
||||
def test_start_15693_no_compile_by_default(self):
|
||||
"""start_15693 without compile should not upload table."""
|
||||
from pm3py.sim.sim_session import SimSession
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
|
||||
|
||||
mock_ser = MagicMock()
|
||||
mock_ser.in_waiting = 0
|
||||
mock_ser.read.return_value = b""
|
||||
|
||||
session = SimSession(port=mock_ser)
|
||||
session.start_15693(tag, trace=False)
|
||||
|
||||
# Should have written only the SIM command (+ maybe EML sync)
|
||||
# No table upload
|
||||
calls = mock_ser.write.call_args_list
|
||||
# Check no frame contains CMD_SIM_TABLE_UPLOAD
|
||||
from pm3py.sim.sim_session import CMD_SIM_TABLE_UPLOAD
|
||||
import struct
|
||||
for call in calls:
|
||||
frame = call[0][0]
|
||||
if len(frame) >= 10:
|
||||
cmd_in_frame = struct.unpack_from("<H", frame, 6)[0]
|
||||
assert cmd_in_frame != CMD_SIM_TABLE_UPLOAD
|
||||
```
|
||||
|
||||
**Step 2: Run to verify failure**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestSimSessionTableCompile -v`
|
||||
Expected: FAIL — `start_15693` doesn't accept `compile` parameter
|
||||
|
||||
**Step 3: Add compile support to start_15693**
|
||||
|
||||
In `sim_session.py`, modify `start_15693()`:
|
||||
|
||||
```python
|
||||
def start_15693(self, tag, compile: bool = False, trace: bool = False) -> None:
|
||||
```
|
||||
|
||||
After `self._active = True` and before the trace setup, add:
|
||||
|
||||
```python
|
||||
if compile:
|
||||
self._compile_and_upload_table(tag, ser)
|
||||
```
|
||||
|
||||
Add the helper method:
|
||||
|
||||
```python
|
||||
def _compile_and_upload_table(self, tag, ser) -> None:
|
||||
"""Compile response table from tag model and upload to firmware."""
|
||||
from .table_compiler import TableCompiler, ResponseTable
|
||||
|
||||
table = ResponseTable(entries=[])
|
||||
|
||||
# NXP ICODE base (config regs, GET_RANDOM)
|
||||
if hasattr(tag, '_config_regs'):
|
||||
nxp_table = TableCompiler.compile_nxp_icode(tag)
|
||||
table.entries.extend(nxp_table.entries)
|
||||
|
||||
# SLIX2 (passwords, privacy)
|
||||
if hasattr(tag, '_passwords'):
|
||||
slix2_table = TableCompiler.compile_slix2(tag)
|
||||
# compile_slix2 already includes nxp_icode, so use it instead
|
||||
table = slix2_table
|
||||
|
||||
if not table.entries:
|
||||
return
|
||||
|
||||
# Upload: first chunk has 4-byte initial_groups header
|
||||
import struct
|
||||
initial_groups = 0xFFFFFFFF
|
||||
data = table.serialize()
|
||||
max_payload = 512
|
||||
|
||||
# First frame: header + entries
|
||||
header = struct.pack("<I", initial_groups)
|
||||
entries_per_chunk = (max_payload - len(header)) // 120
|
||||
first_data = data[:entries_per_chunk * 120]
|
||||
payload = header + first_data
|
||||
frame = encode_ng_frame(CMD_SIM_TABLE_UPLOAD, payload)
|
||||
ser.write(frame)
|
||||
import time
|
||||
time.sleep(0.05)
|
||||
if ser.in_waiting:
|
||||
ser.read(ser.in_waiting)
|
||||
|
||||
# Remaining chunks
|
||||
remaining = data[entries_per_chunk * 120:]
|
||||
chunk_size = (max_payload // 120) * 120
|
||||
for i in range(0, len(remaining), chunk_size):
|
||||
chunk = remaining[i:i + chunk_size]
|
||||
frame = encode_ng_frame(CMD_SIM_TABLE_UPDATE, chunk)
|
||||
ser.write(frame)
|
||||
time.sleep(0.05)
|
||||
if ser.in_waiting:
|
||||
ser.read(ser.in_waiting)
|
||||
|
||||
print(f"[SimSession] Uploaded {len(table.entries)} table entries")
|
||||
```
|
||||
|
||||
**Step 4: Run tests**
|
||||
|
||||
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v`
|
||||
Expected: All pass
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd /home/work/pm3py/.worktrees/sim-framework
|
||||
git add pm3py/sim/sim_session.py tests/test_sim_table_compiler.py
|
||||
git commit --no-gpg-sign -m "feat: auto-compile and upload response table in start_15693"
|
||||
```
|
||||
Reference in New Issue
Block a user