feat: v0.3.7 — NTAG424 DNA NDEF ISO7816 read working

This commit is contained in:
2026-06-07 17:46:21 +02:00
parent 6d1d550074
commit 1467fc4886

256
pn532.py
View File

@@ -156,4 +156,258 @@ class PN532_I2C:
self._read_response(timeout=200)
return True
except Exception:
return False
return False
def read_ndef(self, timeout=2000) -> str:
"""
Read NDEF TextRecord from NTAG.
Returns text content string or None.
NDEF TLV starts at page 4 (byte offset 16).
"""
# Read pages 4..19 (64 bytes) — enough for our payload
data = bytearray()
for page in range(4, 45):
chunk = self.read_page(page)
if chunk is None:
break
data += chunk
if not data:
return None
# Parse TLV — find NDEF TLV (type 0x03)
i = 0
while i < len(data):
t = data[i]; i += 1
if t == 0xFE: # Terminator TLV
break
if t == 0x00: # Null TLV
continue
# Length
if i >= len(data):
break
l = data[i]; i += 1
if l == 0xFF: # 3-byte length
if i + 2 >= len(data):
break
l = (data[i] << 8) | data[i+1]; i += 2
if t != 0x03: # Skip non-NDEF TLVs
i += l
continue
# NDEF message
ndef_msg = data[i:i+l]
break
else:
return None
if not ndef_msg:
return None
# Parse NDEF record
# Byte 0: flags (MB|ME|CF|SR|IL|TNF)
# Byte 1: type length
# Byte 2: payload length (SR=1) or 4 bytes (SR=0)
# Byte 3+: type, id?, payload
if len(ndef_msg) < 4:
return None
flags = ndef_msg[0]
type_len = ndef_msg[1]
sr = (flags >> 4) & 1 # Short Record
il = (flags >> 3) & 1 # ID Length present
idx = 2
if sr:
payload_len = ndef_msg[idx]; idx += 1
else:
if idx + 4 > len(ndef_msg): return None
payload_len = (ndef_msg[idx]<<24 | ndef_msg[idx+1]<<16 |
ndef_msg[idx+2]<<8 | ndef_msg[idx+3]); idx += 4
if il:
id_len = ndef_msg[idx]; idx += 1
idx += id_len
# Skip type bytes
idx += type_len
if idx + payload_len > len(ndef_msg):
return None
payload = ndef_msg[idx:idx+payload_len]
# TextRecord: first byte = status (lang length), then lang, then text
if len(payload) < 1:
return None
lang_len = payload[0] & 0x3F
text_start = 1 + lang_len
if text_start > len(payload):
return None
try:
return bytes(payload[text_start:]).decode('utf-8')
except Exception:
return None
# =========================================================================
# ISO 7816 / NTAG424 DNA NDEF read
# =========================================================================
def _apdu(self, apdu: list, timeout=500, resp_len=64):
"""Send ISO 7816 APDU via InDataExchange and return response bytes."""
self._send_command([PN532_CMD_INDATAEXCHANGE, 0x01] + apdu)
# Step 1: wait ready, read ACK
if not self._wait_ready(timeout):
return None
self._i2c.readfrom(self._addr, 7) # ACK frame
utime.sleep_ms(20)
# Step 2: wait ready, read response
if not self._wait_ready(timeout):
return None
buf = self._i2c.readfrom(self._addr, resp_len + 8)
# buf: [status][preamble x3][len][lcs][TFI=0xD5][cmd=0x41][err][data...][SW1][SW2][dcs][0x00]
# data after err byte: index 9, length = buf[4]-3 (TFI+cmd+err)
if len(buf) < 10:
return None
data_len = buf[4]
if data_len < 3:
return None
err = buf[8]
if err != 0x00:
print('[pn532] apdu err: %02x' % err)
return None
# data = everything from index 9 to 9+(data_len-3), then SW1 SW2
data = bytes(buf[9:9 + data_len - 2])
return data
def _sw(self, resp) -> tuple:
"""Extract SW1 SW2 — last 2 bytes of clean APDU response."""
if not resp or len(resp) < 2:
return (0x00, 0x00)
return (resp[-2], resp[-1])
def _sw2(self, resp) -> tuple:
"""Extract SW1 SW2 — first 2 bytes when response is SW only."""
if not resp or len(resp) < 2:
return (0x00, 0x00)
return (resp[0], resp[1])
def read_ndef_iso(self, timeout=2000) -> str:
"""
Read NDEF from NTAG424 DNA via ISO 7816 command set.
No authentication required for default NDEF file.
Returns text content or None.
Must be called while tag is in field — polls for tag first.
"""
# 0. Poll for tag — activate it for InDataExchange
uid = self.read_passive_target(timeout=timeout)
if uid is None:
return None
utime.sleep_ms(100)
# 1. Select NDEF Application (D2 76 00 00 85 01 01)
sel_app = [0x00, 0xA4, 0x04, 0x00, 0x07,
0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00]
resp = self._apdu(sel_app)
if not resp:
return None
sw1, sw2 = resp[0], resp[1]
if sw1 != 0x90:
print('[pn532] select app failed: %02x %02x' % (sw1, sw2))
return None
# 2. Select NDEF file (E1 04)
sel_file = [0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04]
resp = self._apdu(sel_file)
if not resp:
return None
sw1, sw2 = resp[0], resp[1]
if sw1 != 0x90:
print('[pn532] select file failed: %02x %02x' % (sw1, sw2))
return None
# 3. Read first 2 bytes to get NDEF length
read_len = [0x00, 0xB0, 0x00, 0x00, 0x02]
resp = self._apdu(read_len)
if not resp or len(resp) < 4:
return None
# resp = [len_hi][len_lo][SW1][SW2]
if len(resp) < 4:
return None
sw1, sw2 = resp[2], resp[3]
if sw1 != 0x90:
print('[pn532] read len failed: %02x %02x' % (sw1, sw2))
return None
ndef_len = (resp[0] << 8) | resp[1]
if ndef_len == 0:
return None
# 4. Read NDEF data (max 200 bytes per read)
ndef_data = bytearray()
offset = 2
remaining = ndef_len
while remaining > 0:
chunk = min(remaining, 50) # PN532 I2C frame limit
read_data = [0x00, 0xB0,
(offset >> 8) & 0xFF,
offset & 0xFF,
chunk & 0xFF]
resp = self._apdu(read_data, timeout=timeout, resp_len=chunk+3)
if not resp:
break
if len(resp) < 2:
break
sw1, sw2 = resp[-2], resp[-1]
if sw1 != 0x90:
print('[pn532] read data failed: %02x %02x' % (sw1, sw2))
break
ndef_data += resp[:-2] # exclude SW1 SW2
offset += chunk
remaining -= chunk
if not ndef_data:
return None
# 5. Parse NDEF message
# ndef_data = clean NDEF bytes, no response code prefix
if len(ndef_data) < 3:
return None
flags = ndef_data[0]
type_len = ndef_data[1]
sr = (flags >> 4) & 1
il = (flags >> 3) & 1
idx = 2
if sr:
if idx >= len(ndef_data): return None
payload_len = ndef_data[idx]; idx += 1
else:
if idx + 4 > len(ndef_data): return None
payload_len = (ndef_data[idx]<<24 | ndef_data[idx+1]<<16 |
ndef_data[idx+2]<<8 | ndef_data[idx+3]); idx += 4
if il:
id_len = ndef_data[idx]; idx += 1
idx += id_len
idx += type_len # skip type bytes
if idx + payload_len > len(ndef_data):
return None
payload = ndef_data[idx:idx+payload_len]
# TextRecord: byte 0 = status (lang_len), then lang, then text
if len(payload) < 1:
return None
lang_len = payload[0] & 0x3F
text_start = 1 + lang_len
if text_start > len(payload):
return None
try:
return bytes(payload[text_start:]).decode('utf-8')
except Exception:
return None