From 23bbbb4ed2de2f87fbe562f8499c69e7cbf868fb Mon Sep 17 00:00:00 2001 From: Niko Date: Sun, 7 Jun 2026 16:27:15 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20v0.3.5=20=E2=80=94=20PN532=20driver,=20?= =?UTF-8?q?NFC=20operational,=20pinout=20from=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- boot_manager.py | 123 +++++++++++++++++++++-------------- config_example.json | 53 ++++++++++----- pn532.py | 155 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 64 deletions(-) create mode 100644 pn532.py diff --git a/boot_manager.py b/boot_manager.py index 2ec4a7a..053cf15 100644 --- a/boot_manager.py +++ b/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'): diff --git a/config_example.json b/config_example.json index 3f7bf5f..778fc9a 100644 --- a/config_example.json +++ b/config_example.json @@ -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 + } } \ No newline at end of file diff --git a/pn532.py b/pn532.py new file mode 100644 index 0000000..bfa700c --- /dev/null +++ b/pn532.py @@ -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 \ No newline at end of file