feat(registers): NTAG I2C plus config/session/PT_I2C registers

Add self-describing registers for the NTAG I2C plus (NT3H2111/2211),
decoded bit-exact from the NT3H2111 datasheet (Tables 10-14):

- NtagI2CConfig   config registers E8h/E9h (NC_REG: pass-through, FD_ON/
                  FD_OFF, SRAM mirror, transfer dir; LAST_NDEF_BLOCK;
                  SRAM_MIRROR_BLOCK; watchdog; I2C_CLOCK_STR; REG_LOCK)
- NtagI2CSession  session registers ECh/EDh — same NC_REG copy plus the
                  NS_REG status byte and NEG_AUTH_REACHED
- NtagI2CPtI2C    PT_I2C (E7h): 2K_PROT / SRAM_PROT / I2C_PROT

Config and session share one _NtagI2CRegBlock base (byte 6 differs:
REG_LOCK vs NS_REG). Bound via tag.config / tag.session / tag.pt_i2c;
the fresh-tag decode matches the shipped defaults. 7 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 08:45:39 -07:00
parent 2063a33a2b
commit 0f597bc933
4 changed files with 199 additions and 5 deletions

View File

@@ -7,6 +7,7 @@ from pm3py.sim import (
Ntag21xConfig, NTAG213, MF0UL21, MifareUltralightC,
MifareAccessConditions, MifareClassicTag,
EM4100Code, EM4100Tag,
NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211,
)
@@ -299,3 +300,78 @@ class TestEM4100Code:
assert "EM4100 64-bit code" in text
assert "0102030405" in text
assert "valid" in text and "yes" in text
class TestNtagI2CRegisters:
def test_fresh_config_defaults(self):
# NT3H2111 ships E8=01 00 F8 48, E9=08 01 00 00 (datasheet defaults)
cfg = NT3H2111().config
assert cfg.transfer_dir == "NFC->I2C" # NC_REG bit0 = 1
assert cfg.pthru_on is False
assert cfg.sram_mirror_on is False
assert cfg.sram_mirror_block == 0xF8
assert cfg.watchdog == 0x0848
assert cfg.i2c_clock_str is True
assert cfg.reg_lock_i2c is False and cfg.reg_lock_nfc is False
def test_nc_reg_fields_roundtrip(self):
tag = NT3H2111()
cfg = tag.config
cfg.pthru_on = True
cfg.sram_mirror_on = True
cfg.fd_on = "tag selected" # 0b10
cfg.fd_off = "field off / last NDEF read" # 0b10
cfg.last_ndef_block = 0x2A
tag.config = cfg # program E8/E9
back = tag.config
assert back.pthru_on is True and back.sram_mirror_on is True
assert back.fd_on == "tag selected"
assert back.fd_off == "field off / last NDEF read"
assert back.last_ndef_block == 0x2A
def test_reg_lock(self):
cfg = NtagI2CConfig(0)
cfg.reg_lock_nfc = True
# REG_LOCK is byte 6 (bits 15:8); bit0 = NFC lock -> byte6 == 0x01
assert bytes(cfg)[6] == 0x01
assert cfg.reg_lock_nfc is True and cfg.reg_lock_i2c is False
def test_session_ns_reg_decode(self):
# NS_REG is byte 6 of the EC/ED block; set NDEF_DATA_READ + RF_FIELD_PRESENT + busy
sess = NtagI2CSession(0)
assert sess.rf_field_present is False
raw = int(NtagI2CSession(0))
# byte6 bit7 (ndef_data_read)=bit15, bit0 (rf_field_present)=bit8, bit1 (busy)=bit9
sess = NtagI2CSession((1 << 15) | (1 << 8) | (1 << 9))
assert sess.ndef_data_read is True
assert sess.rf_field_present is True
assert sess.eeprom_wr_busy is True
assert sess.rf_locked is False
def test_session_binding(self):
tag = NT3H2111()
sess = tag.session
# session copy shares NC_REG defaults (transfer_dir=1)
assert sess.transfer_dir == "NFC->I2C"
assert sess.rf_field_present is False # NS_REG starts clear
def test_pt_i2c(self):
pt = NtagI2CPtI2C(0)
pt.sram_prot = True
pt.i2c_prot = "protected area read-only" # 0b01, unambiguous
assert pt.sram_prot is True
assert pt.i2c_prot == "protected area read-only"
assert int(pt) == (1 << 2) | (1 << 0)
# datasheet 1Xb == "no access to protected area" for both codes 2 and 3
assert NtagI2CPtI2C(2).i2c_prot == "protected area no access"
assert NtagI2CPtI2C(3).i2c_prot == "protected area no access"
# tag binding
tag = NT3H2211()
tag.pt_i2c = pt
assert tag.pt_i2c.i2c_prot == "protected area read-only"
def test_repr_is_decode_table(self):
text = repr(NT3H2111().config)
assert "NTAG I2C config registers" in text
assert "transfer_dir" in text and "NFC->I2C" in text
assert "sram_mirror_block" in text