feat(registers): NTAG5 STATUS_REG / CONFIG_REG self-describing views

Add Ntag5StatusReg and Ntag5ConfigReg (Register subclasses) and bind
them as tag.status_reg / tag.config_reg on the NTAG 5 platform. Purely
additive: every existing per-field accessor (disable_nfc, arbiter_mode,
sram_enable, ...) stays, and the config_reg setter re-syncs the
_arbiter_mode / _sram_enabled sibling caches the per-field setters keep,
so the sim internals are untouched.

- Ntag5StatusReg (0xA0): field/VCC/boot/lock/EEPROM/SRAM status bits
- Ntag5ConfigReg (0xA1): disable_nfc, arbiter_mode (normal/mirror/
  pass-through/PHDC), sram_enable, config_pt_transfer_dir

Bit layout taken straight from the existing hand-rolled accessors, so
the register view decodes the very same bytes (cross-checked in tests).
5 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 08:54:24 -07:00
parent 0f597bc933
commit f8d01a9120
4 changed files with 131 additions and 4 deletions

View File

@@ -59,9 +59,10 @@ pm3py/
transponders/ # tag/transponder models (extracted from sim/) transponders/ # tag/transponder models (extracted from sim/)
bitfield.py # BitField descriptor + Register base — self-describing config/ bitfield.py # BitField descriptor + Register base — self-describing config/
# frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC # frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC
# access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C). Fields are # access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C, NTAG5
# documented attrs -> shell completion + repr decode table. # STATUS_REG/CONFIG_REG). Fields are documented attrs -> shell
# Layouts cross-checked vs the firmware submodule + NXP datasheets. # completion + repr decode table. Layouts cross-checked vs the
# firmware submodule + NXP datasheets.
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4 hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
# nxp/type2,ntag21x,ultralight,ntag_i2c # nxp/type2,ntag21x,ultralight,ntag_i2c
# (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus) # (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus)

View File

@@ -48,7 +48,9 @@ from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag from pm3py.transponders.hf.iso15693.nxp.icode_dna import IcodeDnaTag
from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import (
Ntag5PlatformTag, Ntag5StatusReg, Ntag5ConfigReg,
)
from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag
from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag
@@ -111,6 +113,7 @@ __all__ = [
"NxpIcodeTag", "IcodeSlixTag", "IcodeSlix2Tag", "NxpIcodeTag", "IcodeSlixTag", "IcodeSlix2Tag",
"Icode3Tag", "IcodeDnaTag", "Icode3Tag", "IcodeDnaTag",
"Ntag5PlatformTag", "Ntag5SwitchTag", "Ntag5LinkTag", "Ntag5BoostTag", "Ntag5PlatformTag", "Ntag5SwitchTag", "Ntag5LinkTag", "Ntag5BoostTag",
"Ntag5StatusReg", "Ntag5ConfigReg",
"DesfireTag", "DesfireReader", "DesfireTag", "DesfireReader",
"xEM", "xNT", "xM1", "FlexDF", "NExT", "MagicMifareClassicTag", "xEM", "xNT", "xM1", "FlexDF", "NExT", "MagicMifareClassicTag",
"MutationFuzzer", "GrammarFuzzer", "MutationFuzzer", "GrammarFuzzer",

View File

@@ -22,11 +22,52 @@ from .auth_aes import NxpAesAuth
from pm3py.sim.frame import RFFrame from pm3py.sim.frame import RFFrame
from .icode_slix2 import IcodeSlix2Tag from .icode_slix2 import IcodeSlix2Tag
from .nxp_icode import NXP_MFG from .nxp_icode import NXP_MFG
from pm3py.transponders.bitfield import BitField, Register
# NTAG 5 AREA_1 password identifiers (NTP5210/NTA5332 datasheets) # NTAG 5 AREA_1 password identifiers (NTP5210/NTA5332 datasheets)
PWD_AREA1_READ = 0x40 PWD_AREA1_READ = 0x40
PWD_AREA1_WRITE = 0x80 PWD_AREA1_WRITE = 0x80
_PT_DIR = {0: "I2C->NFC", 1: "NFC->I2C"}
class Ntag5StatusReg(Register):
"""NTAG 5 STATUS_REG (session register 0xA0) — the live interface/EEPROM/SRAM status. Read
via READ CONFIG; most bits are hardware status (read-only). Bytes 0-3 map MSB-first, so
STATUS_REG byte 0 is bits 31:24 and byte 1 is bits 23:16."""
_width_bits = 32
_label = "NTAG5 STATUS_REG (0xA0)"
# byte 0
eeprom_wr_busy = BitField(31, doc="status: EEPROM write in progress")
eeprom_wr_error = BitField(30, doc="status: EEPROM write/erase error")
sram_data_ready = BitField(29, doc="status: SRAM data ready to be read")
synch_block_write = BitField(28, doc="status: synchronised block write in progress")
synch_block_read = BitField(27, doc="status: synchronised block read in progress")
pt_transfer_dir = BitField(26, values=_PT_DIR, doc="live pass-through data-flow direction")
vcc_supply_ok = BitField(25, doc="status: VCC (I2C-side) supply present")
nfc_field_ok = BitField(24, doc="status: NFC field present and sufficient")
# byte 1
vcc_boot_ok = BitField(23, doc="status: booted from VCC supply")
nfc_boot_ok = BitField(22, doc="status: booted from the NFC field")
i2c_if_locked = BitField(17, doc="status: memory arbitration locked to I2C")
nfc_if_locked = BitField(16, doc="status: memory arbitration locked to NFC")
class Ntag5ConfigReg(Register):
"""NTAG 5 CONFIG_REG (session register 0xA1) — the writable NFC/I2C behaviour config. Byte 0
is bits 31:24."""
_width_bits = 32
_label = "NTAG5 CONFIG_REG (0xA1)"
disable_nfc = BitField(29, doc="silence the NFC interface (everything except INVENTORY)")
arbiter_mode = BitField(27, 26, doc="NFC/I2C memory arbitration",
values={0: "normal", 1: "mirror", 2: "pass-through", 3: "PHDC"})
sram_enable = BitField(25, doc="gate all SRAM access")
config_pt_transfer_dir = BitField(24, values=_PT_DIR, doc="pass-through data-flow direction")
class Ntag5PlatformTag(NxpAesAuth, IcodeSlix2Tag): class Ntag5PlatformTag(NxpAesAuth, IcodeSlix2Tag):
"""NTAG 5 platform base — shared by Switch, Link, and Boost. """NTAG 5 platform base — shared by Switch, Link, and Boost.
@@ -230,6 +271,32 @@ class Ntag5PlatformTag(NxpAesAuth, IcodeSlix2Tag):
def config_pt_transfer_dir(self, value: bool): def config_pt_transfer_dir(self, value: bool):
self._set_config_bit(0, 0, value) self._set_config_bit(0, 0, value)
# ----- self-describing register views (STATUS_REG / CONFIG_REG) -----
@property
def status_reg(self) -> Ntag5StatusReg:
"""STATUS_REG (0xA0) decoded as a self-describing :class:`Ntag5StatusReg`."""
return Ntag5StatusReg(int.from_bytes(self._status_reg, "big"))
@status_reg.setter
def status_reg(self, value: "Ntag5StatusReg | int") -> None:
self._status_reg[:] = int(value).to_bytes(4, "big")
@property
def config_reg(self) -> Ntag5ConfigReg:
"""CONFIG_REG (0xA1) decoded as a self-describing :class:`Ntag5ConfigReg`. Assign one
back (``tag.config_reg = cfg``) to program it."""
return Ntag5ConfigReg(int.from_bytes(self._config_reg, "big"))
@config_reg.setter
def config_reg(self, value: "Ntag5ConfigReg | int") -> None:
self._config_reg[:] = int(value).to_bytes(4, "big")
# keep the sibling caches the per-field setters maintain in sync
if hasattr(self, "_arbiter_mode"):
self._arbiter_mode = (self._config_reg[0] >> 2) & 0x03
if hasattr(self, "_sram_enabled"):
self._sram_enabled = bool(self._config_reg[0] & 0x02)
# ----- Session register control methods ----- # ----- Session register control methods -----
def set_nfc_field(self, active: bool): def set_nfc_field(self, active: bool):

View File

@@ -8,6 +8,7 @@ from pm3py.sim import (
MifareAccessConditions, MifareClassicTag, MifareAccessConditions, MifareClassicTag,
EM4100Code, EM4100Tag, EM4100Code, EM4100Tag,
NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211,
Ntag5StatusReg, Ntag5ConfigReg, Ntag5LinkTag,
) )
@@ -375,3 +376,58 @@ class TestNtagI2CRegisters:
assert "NTAG I2C config registers" in text assert "NTAG I2C config registers" in text
assert "transfer_dir" in text and "NFC->I2C" in text assert "transfer_dir" in text and "NFC->I2C" in text
assert "sram_mirror_block" in text assert "sram_mirror_block" in text
class TestNtag5Registers:
def test_config_reg_field_positions(self):
cfg = Ntag5ConfigReg(0)
cfg.arbiter_mode = "pass-through" # 0b10 at byte0 bits 3:2
cfg.sram_enable = True # byte0 bit1
cfg.disable_nfc = True # byte0 bit5
cfg.config_pt_transfer_dir = "NFC->I2C" # byte0 bit0
# byte0 = 0b0010_1011 = 0x2B (disable_nfc bit5 + arb=10 at bit3 + sram bit1 + ptdir bit0)
assert bytes(cfg)[0] == 0x2B
assert cfg.arbiter_mode == "pass-through"
assert cfg.disable_nfc is True
def test_status_reg_decode(self):
# byte0: nfc_field_ok(0)+vcc_supply_ok(1)+eeprom_wr_busy(7); byte1: nfc_if_locked(0)
st = Ntag5StatusReg(int.from_bytes(bytes([0b1000_0011, 0x01, 0, 0]), "big"))
assert st.nfc_field_ok is True
assert st.vcc_supply_ok is True
assert st.eeprom_wr_busy is True
assert st.sram_data_ready is False
assert st.nfc_if_locked is True
def test_view_agrees_with_existing_accessors(self):
tag = Ntag5LinkTag()
tag.arbiter_mode = 2 # existing per-field setter
tag.disable_nfc = True
tag.sram_enable = True
# the register view must decode the very same bytes
assert tag.config_reg.arbiter_mode == "pass-through"
assert tag.config_reg.disable_nfc is True
assert tag.config_reg.sram_enable is True
def test_assign_config_reg_updates_accessors_and_siblings(self):
tag = Ntag5LinkTag()
cfg = Ntag5ConfigReg(0)
cfg.arbiter_mode = "mirror" # 1
cfg.sram_enable = True
tag.config_reg = cfg
# existing property accessors (read straight off the bytes) must reflect it
assert tag.arbiter_mode == 1
assert tag.sram_enable is True
# and the sibling caches the individual setters maintain stay in sync
if hasattr(tag, "_arbiter_mode"):
assert tag._arbiter_mode == 1
if hasattr(tag, "_sram_enabled"):
assert tag._sram_enabled is True
def test_status_reg_binding_roundtrip(self):
tag = Ntag5LinkTag()
st = tag.status_reg
st.i2c_if_locked = True
tag.status_reg = st
assert tag.i2c_if_locked is True
assert tag.status_reg.i2c_if_locked is True