feat: v0.3.5 — PN532 driver, NFC operational, pinout from config
This commit is contained in:
123
boot_manager.py
123
boot_manager.py
@@ -14,37 +14,15 @@ import machine
|
||||
import ubinascii
|
||||
from machine import Pin, I2C
|
||||
|
||||
# ---- Optional OLED ----
|
||||
try:
|
||||
from ssd1306 import SSD1306_I2C
|
||||
_i2c = I2C(0, sda=Pin(4), scl=Pin(5))
|
||||
_oled = SSD1306_I2C(128, 64, _i2c)
|
||||
HAS_OLED = True
|
||||
except Exception:
|
||||
_oled = None
|
||||
HAS_OLED = False
|
||||
|
||||
# ---- Optional NFC (PN532 I2C) ----
|
||||
try:
|
||||
from pn532 import PN532_I2C
|
||||
HAS_NFC = True
|
||||
except Exception:
|
||||
HAS_NFC = False
|
||||
|
||||
# ---- Optional sensor (DHT22 on GP15) ----
|
||||
try:
|
||||
import dht
|
||||
_dht = dht.DHT22(Pin(15))
|
||||
HAS_SENSOR = True
|
||||
except Exception:
|
||||
HAS_SENSOR = False
|
||||
|
||||
# ---- Optional relay (GP16) ----
|
||||
try:
|
||||
_relay = Pin(16, Pin.OUT)
|
||||
HAS_RELAY = True
|
||||
except Exception:
|
||||
HAS_RELAY = False
|
||||
# Hardware instances — initialised in detect_capabilities() after config load
|
||||
_oled = None
|
||||
_nfc = None
|
||||
_dht = None
|
||||
_relay = None
|
||||
HAS_OLED = False
|
||||
HAS_NFC = False
|
||||
HAS_SENSOR = False
|
||||
HAS_RELAY = False
|
||||
|
||||
CONFIG_PATH = 'config.json'
|
||||
|
||||
@@ -82,7 +60,56 @@ def _show_state(state, detail=''):
|
||||
# Capability detection
|
||||
# =============================================================================
|
||||
|
||||
def detect_capabilities():
|
||||
def detect_capabilities(cfg=None):
|
||||
global _oled, _nfc, _dht, _relay
|
||||
global HAS_OLED, HAS_NFC, HAS_SENSOR, HAS_RELAY
|
||||
|
||||
pinout = cfg.get('pinout', {}) if cfg else {}
|
||||
i2c0_sda = pinout.get('i2c0_sda', 4)
|
||||
i2c0_scl = pinout.get('i2c0_scl', 5)
|
||||
i2c1_sda = pinout.get('i2c1_sda', 6)
|
||||
i2c1_scl = pinout.get('i2c1_scl', 7)
|
||||
dht_pin = pinout.get('dht_pin', 15)
|
||||
relay_pin = pinout.get('relay_pin', 16)
|
||||
|
||||
# OLED
|
||||
try:
|
||||
from ssd1306 import SSD1306_I2C
|
||||
_i2c0 = I2C(0, sda=Pin(i2c0_sda), scl=Pin(i2c0_scl))
|
||||
_oled = SSD1306_I2C(128, 64, _i2c0)
|
||||
HAS_OLED = True
|
||||
except Exception:
|
||||
_oled = None
|
||||
HAS_OLED = False
|
||||
|
||||
# NFC
|
||||
try:
|
||||
from pn532 import PN532_I2C
|
||||
_i2c1 = I2C(1, sda=Pin(i2c1_sda), scl=Pin(i2c1_scl))
|
||||
_nfc = PN532_I2C(_i2c1)
|
||||
HAS_NFC = True
|
||||
except Exception as e:
|
||||
print('[nfc] error:', e)
|
||||
_nfc = None
|
||||
HAS_NFC = False
|
||||
|
||||
# Sensor
|
||||
try:
|
||||
import dht
|
||||
_dht = dht.DHT22(Pin(dht_pin))
|
||||
HAS_SENSOR = True
|
||||
except Exception:
|
||||
_dht = None
|
||||
HAS_SENSOR = False
|
||||
|
||||
# Relay
|
||||
try:
|
||||
_relay = Pin(relay_pin, Pin.OUT)
|
||||
HAS_RELAY = True
|
||||
except Exception:
|
||||
_relay = None
|
||||
HAS_RELAY = False
|
||||
|
||||
caps = {
|
||||
'has_oled': HAS_OLED,
|
||||
'has_nfc': HAS_NFC,
|
||||
@@ -271,21 +298,6 @@ def run():
|
||||
if state == 'BOOT':
|
||||
_show(['Identity IoT', 'WIDE / AEL', '', 'Booting...', ''])
|
||||
utime.sleep(1)
|
||||
state = 'CAPABILITY_DETECT'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
elif state == 'CAPABILITY_DETECT':
|
||||
caps = detect_capabilities()
|
||||
_show([
|
||||
'Identity IoT',
|
||||
'NFC:' + ('Y' if caps['has_nfc'] else 'N') +
|
||||
' SNS:' + ('Y' if caps['has_sensor'] else 'N') +
|
||||
' RLY:' + ('Y' if caps['has_relay'] else 'N'),
|
||||
'',
|
||||
'Checking config...',
|
||||
'',
|
||||
])
|
||||
utime.sleep(1)
|
||||
state = 'CONFIG_CHECK'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -293,11 +305,26 @@ def run():
|
||||
cfg = load_config()
|
||||
if cfg and validate_config(cfg):
|
||||
print('[config] found and valid')
|
||||
state = 'WIFI_CONNECT'
|
||||
state = 'CAPABILITY_DETECT'
|
||||
else:
|
||||
print('[config] missing or invalid — provisioning mode')
|
||||
state = 'PROVISIONING'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
elif state == 'CAPABILITY_DETECT':
|
||||
caps = detect_capabilities(cfg)
|
||||
_show([
|
||||
'Identity IoT',
|
||||
'NFC:' + ('Y' if caps['has_nfc'] else 'N') +
|
||||
' SNS:' + ('Y' if caps['has_sensor'] else 'N') +
|
||||
' RLY:' + ('Y' if caps['has_relay'] else 'N'),
|
||||
'',
|
||||
'READY',
|
||||
'',
|
||||
])
|
||||
utime.sleep(1)
|
||||
state = 'WIFI_CONNECT'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
elif state == 'PROVISIONING':
|
||||
if caps.get('has_nfc'):
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
{
|
||||
"wifi_ssid": "YOUR_SSID",
|
||||
"wifi_pass": "YOUR_PASSWORD",
|
||||
"wifi_static": false,
|
||||
"wifi_ip": "192.168.1.50",
|
||||
"wifi_mask": "255.255.255.0",
|
||||
"wifi_gw": "192.168.1.1",
|
||||
"wifi_dns": "8.8.8.8",
|
||||
"comm_key": "YOUR_32_CHAR_KEY________________",
|
||||
"comm_iv": "YOUR_16_CHAR_IV_",
|
||||
"tenant_id": "1",
|
||||
"node_id": "0",
|
||||
"node_type": "access_control",
|
||||
"node_label": "terminal-01",
|
||||
"identity_iot_url": "https://api.parta.app/API/V1/identity_layer/identity_iot/",
|
||||
"identity_tag_url": "https://api.parta.app/API/V1/identity_layer/identity_tag/",
|
||||
"wifi_ssid": "YOUR_SSID",
|
||||
"wifi_pass": "YOUR_PASSWORD",
|
||||
"wifi_static": false,
|
||||
"wifi_ip": "192.168.1.50",
|
||||
"wifi_mask": "255.255.255.0",
|
||||
"wifi_gw": "192.168.1.1",
|
||||
"wifi_dns": "8.8.8.8",
|
||||
"comm_key": "YOUR_32_CHAR_KEY________________",
|
||||
"comm_iv": "YOUR_16_CHAR_IV_",
|
||||
"tenant_id": "1",
|
||||
"node_id": "0",
|
||||
"node_type": "access_control",
|
||||
"node_label": "terminal-01",
|
||||
"identity_iot_url": "https://api.parta.app/API/V1/identity_layer/identity_iot/",
|
||||
"identity_tag_url": "https://api.parta.app/API/V1/identity_layer/identity_tag/",
|
||||
"identity_node_url": "https://api.parta.app/API/V1/identity_tag/",
|
||||
"data_destination_url": ""
|
||||
"data_destination_url": "",
|
||||
"pinout": {
|
||||
"i2c0_sda": 4,
|
||||
"i2c0_scl": 5,
|
||||
"i2c1_sda": 6,
|
||||
"i2c1_scl": 7,
|
||||
"dht_pin": 15,
|
||||
"relay_pin": 16,
|
||||
"rain_pin": 26,
|
||||
"soil_pin": 27,
|
||||
"pot_pin": 28,
|
||||
"switch_pins": [
|
||||
17,
|
||||
18
|
||||
],
|
||||
"rotary_a": 19,
|
||||
"rotary_b": 20,
|
||||
"rotary_sw": 21,
|
||||
"gps_uart": 1,
|
||||
"gps_tx": 8,
|
||||
"gps_rx": 9
|
||||
}
|
||||
}
|
||||
155
pn532.py
Normal file
155
pn532.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# ============================================================+
|
||||
# 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
|
||||
Reference in New Issue
Block a user