Files
pm3py/pm3py/sim/memory.py
michael 668170457e feat: migrate sim framework from worktree to master
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>
2026-03-18 20:04:55 -07:00

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