# 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(" 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(" 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("= 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(" 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("