155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
# ============================================================+
|
|
# Identity IoT — PN532 NFC Driver for MicroPython
|
|
# (c) Copyright : ae Aeonian Engineering Limited - Hong Kong
|
|
# (c) Copyright : WIDE di D. Papa - Naples - Italy
|
|
# ============================================================+
|
|
|
|
import utime
|
|
from machine import Pin, I2C
|
|
|
|
PN532_I2C_ADDRESS = 0x24
|
|
PN532_CMD_GETFIRMWAREVERSION = 0x02
|
|
PN532_CMD_SAMCONFIGURATION = 0x14
|
|
PN532_CMD_INLISTPASSIVETARGET= 0x4A
|
|
PN532_CMD_INDATAEXCHANGE = 0x40
|
|
PN532_CMD_INRELEASE = 0x52
|
|
PN532_HOSTTOPN532 = 0xD4
|
|
PN532_PN532TOHOST = 0xD5
|
|
|
|
|
|
class PN532Error(Exception):
|
|
pass
|
|
|
|
|
|
class PN532_I2C:
|
|
|
|
def __init__(self, i2c: I2C, address: int = PN532_I2C_ADDRESS):
|
|
self._i2c = i2c
|
|
self._addr = address
|
|
|
|
# Wake
|
|
try:
|
|
self._i2c.writeto(self._addr, bytes([0x00]))
|
|
except Exception:
|
|
pass
|
|
utime.sleep_ms(500)
|
|
|
|
fw = self.get_firmware_version()
|
|
if not fw:
|
|
raise PN532Error('PN532 not found or not responding')
|
|
print('[pn532] firmware: IC=%02x Ver=%d Rev=%d' % (fw['ic'], fw['ver'], fw['rev']))
|
|
self._sam_config()
|
|
|
|
def _build_frame(self, cmd: list) -> bytes:
|
|
body = [PN532_HOSTTOPN532] + cmd
|
|
length = len(body)
|
|
lcs = (~length + 1) & 0xFF
|
|
dcs = (~(sum(body) & 0xFF) + 1) & 0xFF
|
|
return bytes([0x00, 0x00, 0xFF, length, lcs] + body + [dcs, 0x00])
|
|
|
|
def _send_command(self, cmd: list):
|
|
frame = self._build_frame(cmd)
|
|
self._i2c.writeto(self._addr, frame)
|
|
|
|
def _wait_ready(self, timeout=1000) -> bool:
|
|
t = timeout
|
|
while t > 0:
|
|
try:
|
|
b = self._i2c.readfrom(self._addr, 1)
|
|
if b[0] == 0x01:
|
|
return True
|
|
except Exception:
|
|
pass
|
|
utime.sleep_ms(10)
|
|
t -= 10
|
|
return False
|
|
|
|
def _read_response(self, length=32, timeout=1000):
|
|
"""Read after command — PN532 sends ACK then response."""
|
|
# Read ACK (6 bytes + status)
|
|
if not self._wait_ready(timeout):
|
|
return None
|
|
self._i2c.readfrom(self._addr, 7) # status + ACK frame
|
|
utime.sleep_ms(10)
|
|
|
|
# Read response
|
|
if not self._wait_ready(timeout):
|
|
return None
|
|
buf = self._i2c.readfrom(self._addr, length + 1)
|
|
|
|
# Parse: skip status byte, find data after header
|
|
# Frame: [status][0x00][0x00][0xFF][len][lcs][TFI][cmd+1][data...][dcs][0x00]
|
|
if len(buf) < 8:
|
|
return None
|
|
data_len = buf[4] # length field
|
|
if data_len < 2:
|
|
return bytes()
|
|
# data starts at index 7 (after status+preamble+start+len+lcs+TFI)
|
|
data = buf[7:7 + data_len - 1]
|
|
return bytes(data)
|
|
|
|
def _sam_config(self):
|
|
self._send_command([PN532_CMD_SAMCONFIGURATION, 0x01, 0x14, 0x01])
|
|
self._read_response(timeout=200)
|
|
|
|
def get_firmware_version(self) -> dict:
|
|
try:
|
|
self._send_command([PN532_CMD_GETFIRMWAREVERSION])
|
|
# First read: ACK frame
|
|
if not self._wait_ready(500):
|
|
return None
|
|
self._i2c.readfrom(self._addr, 7) # status + ACK
|
|
utime.sleep_ms(10)
|
|
# Second read: response frame
|
|
if not self._wait_ready(500):
|
|
return None
|
|
buf = self._i2c.readfrom(self._addr, 14)
|
|
# buf[0]=status, buf[1..3]=preamble, buf[4]=len, buf[5]=lcs
|
|
# buf[6]=TFI(0xD5), buf[7]=cmd(0x03), buf[8..11]=IC,Ver,Rev,Support
|
|
if len(buf) < 12:
|
|
return None
|
|
return {
|
|
'ic': buf[8],
|
|
'ver': buf[9],
|
|
'rev': buf[10],
|
|
'support': buf[11],
|
|
}
|
|
except Exception as e:
|
|
print('[pn532] fw error:', e)
|
|
return None
|
|
|
|
def read_passive_target(self, timeout=1000) -> bytes:
|
|
"""Wait for ISO14443A card. Returns UID bytes or None."""
|
|
self._send_command([PN532_CMD_INLISTPASSIVETARGET, 0x01, 0x00])
|
|
resp = self._read_response(length=32, timeout=timeout)
|
|
if not resp or len(resp) < 6:
|
|
return None
|
|
if resp[0] != 0x01:
|
|
return None
|
|
uid_len = resp[4]
|
|
return bytes(resp[5:5 + uid_len])
|
|
|
|
def read_uid(self, timeout=500) -> bytes:
|
|
return self.read_passive_target(timeout=timeout)
|
|
|
|
def read_page(self, page: int) -> bytes:
|
|
self._send_command([PN532_CMD_INDATAEXCHANGE, 0x01, 0x30, page & 0xFF])
|
|
resp = self._read_response(length=16, timeout=500)
|
|
if not resp or len(resp) < 4:
|
|
return None
|
|
return bytes(resp[:4])
|
|
|
|
def write_page(self, page: int, data: bytes) -> bool:
|
|
if len(data) != 4:
|
|
raise PN532Error('write_page: data must be 4 bytes')
|
|
self._send_command([PN532_CMD_INDATAEXCHANGE, 0x01, 0xA2, page & 0xFF] + list(data))
|
|
resp = self._read_response(timeout=500)
|
|
return resp is not None
|
|
|
|
def release_target(self) -> bool:
|
|
try:
|
|
self._send_command([PN532_CMD_INRELEASE, 0x01])
|
|
self._read_response(timeout=200)
|
|
return True
|
|
except Exception:
|
|
return False |