diff --git a/pn532.py b/pn532.py index a43ebf7..3454c31 100644 --- a/pn532.py +++ b/pn532.py @@ -156,4 +156,258 @@ class PN532_I2C: self._read_response(timeout=200) return True except Exception: - return False \ No newline at end of file + 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 \ No newline at end of file