From f8d01a9120f6014f89e47680a30c4938a96ee00d Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 14 Jul 2026 08:54:24 -0700 Subject: [PATCH] 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 --- CLAUDE.md | 7 +- pm3py/sim/__init__.py | 5 +- .../hf/iso15693/nxp/ntag5_platform.py | 67 +++++++++++++++++++ tests/test_bitfield.py | 56 ++++++++++++++++ 4 files changed, 131 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f5b5a25..a61fd99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,9 +59,10 @@ pm3py/ transponders/ # tag/transponder models (extracted from sim/) bitfield.py # BitField descriptor + Register base — self-describing config/ # frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC - # access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C). Fields are - # documented attrs -> shell completion + repr decode table. - # Layouts cross-checked vs the firmware submodule + NXP datasheets. + # access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C, NTAG5 + # STATUS_REG/CONFIG_REG). Fields are documented attrs -> shell + # completion + repr decode table. Layouts cross-checked vs the + # firmware submodule + NXP datasheets. hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4 # nxp/type2,ntag21x,ultralight,ntag_i2c # (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus) diff --git a/pm3py/sim/__init__.py b/pm3py/sim/__init__.py index 48c917d..9f6ba07 100644 --- a/pm3py/sim/__init__.py +++ b/pm3py/sim/__init__.py @@ -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.icode3 import Icode3Tag 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_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag @@ -111,6 +113,7 @@ __all__ = [ "NxpIcodeTag", "IcodeSlixTag", "IcodeSlix2Tag", "Icode3Tag", "IcodeDnaTag", "Ntag5PlatformTag", "Ntag5SwitchTag", "Ntag5LinkTag", "Ntag5BoostTag", + "Ntag5StatusReg", "Ntag5ConfigReg", "DesfireTag", "DesfireReader", "xEM", "xNT", "xM1", "FlexDF", "NExT", "MagicMifareClassicTag", "MutationFuzzer", "GrammarFuzzer", diff --git a/pm3py/transponders/hf/iso15693/nxp/ntag5_platform.py b/pm3py/transponders/hf/iso15693/nxp/ntag5_platform.py index 8e2f6fe..362ee00 100644 --- a/pm3py/transponders/hf/iso15693/nxp/ntag5_platform.py +++ b/pm3py/transponders/hf/iso15693/nxp/ntag5_platform.py @@ -22,11 +22,52 @@ from .auth_aes import NxpAesAuth from pm3py.sim.frame import RFFrame from .icode_slix2 import IcodeSlix2Tag from .nxp_icode import NXP_MFG +from pm3py.transponders.bitfield import BitField, Register # NTAG 5 AREA_1 password identifiers (NTP5210/NTA5332 datasheets) PWD_AREA1_READ = 0x40 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): """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): 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 ----- def set_nfc_field(self, active: bool): diff --git a/tests/test_bitfield.py b/tests/test_bitfield.py index f4f1f9a..5310ace 100644 --- a/tests/test_bitfield.py +++ b/tests/test_bitfield.py @@ -8,6 +8,7 @@ from pm3py.sim import ( MifareAccessConditions, MifareClassicTag, EM4100Code, EM4100Tag, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211, + Ntag5StatusReg, Ntag5ConfigReg, Ntag5LinkTag, ) @@ -375,3 +376,58 @@ class TestNtagI2CRegisters: assert "NTAG I2C config registers" in text assert "transfer_dir" in text and "NFC->I2C" 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