Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests) from .worktrees/sim-framework/ into pm3py/sim/. Import fixes: - 4 files: ..protocol/..transport → ..core.protocol/..core.transport - trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef - test_sim_pm3medium.py: flat imports → core.* - test_sim_trace_fmt.py: flat imports → core.* - Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*) 751 tests passing (686 sim + 65 core). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71 lines
2.1 KiB
Python
71 lines
2.1 KiB
Python
"""Memory primitives: DirtyByteArray, MemoryRegion, BlockAccess."""
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass, field
|
|
from typing import Callable
|
|
|
|
|
|
class DirtyByteArray(bytearray):
|
|
"""Bytearray that tracks whether it's been modified since last clear."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._dirty = False
|
|
self.on_dirty: Callable[[], None] | None = None
|
|
|
|
@property
|
|
def dirty(self) -> bool:
|
|
return self._dirty
|
|
|
|
def clear_dirty(self) -> None:
|
|
self._dirty = False
|
|
|
|
def _mark_dirty(self) -> None:
|
|
self._dirty = True
|
|
if self.on_dirty:
|
|
self.on_dirty()
|
|
|
|
def __setitem__(self, key, value):
|
|
super().__setitem__(key, value)
|
|
self._mark_dirty()
|
|
|
|
def extend(self, other):
|
|
super().extend(other)
|
|
self._mark_dirty()
|
|
|
|
def append(self, item):
|
|
super().append(item)
|
|
self._mark_dirty()
|
|
|
|
|
|
@dataclass
|
|
class BlockAccess:
|
|
"""Per-block access control hints for table compiler."""
|
|
read: str = "open" # "open", "password", "key", "aes", "deny"
|
|
write: str = "open"
|
|
write_mode: str = "normal" # "normal" = overwrite, "counter" = increment LE16
|
|
read_key: int | None = None
|
|
write_key: int | None = None
|
|
|
|
|
|
@dataclass
|
|
class MemoryRegion:
|
|
"""A named memory area within a transponder."""
|
|
name: str
|
|
data: DirtyByteArray
|
|
block_size: int = 0 # 0 = not block-addressed
|
|
eml_offset: int = -1 # firmware EML offset, -1 = table/relay only
|
|
rf_readable: bool = True # if True, table compiler generates READ entries
|
|
default_access: BlockAccess = field(default_factory=BlockAccess)
|
|
access_map: list[BlockAccess] | None = None
|
|
|
|
@property
|
|
def num_blocks(self) -> int:
|
|
if self.block_size <= 0:
|
|
return 0
|
|
return len(self.data) // self.block_size
|
|
|
|
def access_for_block(self, block: int) -> BlockAccess:
|
|
if self.access_map and block < len(self.access_map):
|
|
return self.access_map[block]
|
|
return self.default_access
|