From 588f1a28be80a25fbedacf5f4c0c9a4b7176484e Mon Sep 17 00:00:00 2001 From: Kumi Date: Sat, 2 Mar 2024 18:10:17 +0100 Subject: [PATCH] Current status --- .gitignore | 1 + adafruit_pn532.py | 468 ++++++++++++++++++++++++++++++++++++++++++++++ boot.py | 0 digitalio.py | 206 ++++++++++++++++++++ install.sh | 5 + main.py | 117 ++++++++++++ pn532_i2c.py | 143 ++++++++++++++ 7 files changed, 940 insertions(+) create mode 100644 .gitignore create mode 100644 adafruit_pn532.py create mode 100644 boot.py create mode 100644 digitalio.py create mode 100644 install.sh create mode 100644 main.py create mode 100644 pn532_i2c.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c11b8b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/pymakr.conf \ No newline at end of file diff --git a/adafruit_pn532.py b/adafruit_pn532.py new file mode 100644 index 0000000..851f36a --- /dev/null +++ b/adafruit_pn532.py @@ -0,0 +1,468 @@ +# Adafruit PN532 NFC/RFID control library. +# Author: Tony DiCola +# +# The MIT License (MIT) +# +# Copyright (c) 2015-2018 Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +""" +``adafruit_pn532`` +==================================================== + +This module will let you communicate with a PN532 RFID/NFC shield or breakout +using I2C, SPI or UART. + +* Author(s): Original Raspberry Pi code by Tony DiCola, CircuitPython by ladyada + +Implementation Notes +-------------------- + +**Hardware:** + +* Adafruit `PN532 Breakout `_ +* Adafruit `PN532 Shield `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" + +import time +from digitalio import Direction + +from micropython import const + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PN532.git" + +# pylint: disable=bad-whitespace +_PREAMBLE = const(0x00) +_STARTCODE1 = const(0x00) +_STARTCODE2 = const(0xFF) +_POSTAMBLE = const(0x00) + +_HOSTTOPN532 = const(0xD4) +_PN532TOHOST = const(0xD5) + +# PN532 Commands +_COMMAND_DIAGNOSE = const(0x00) +_COMMAND_GETFIRMWAREVERSION = const(0x02) +_COMMAND_GETGENERALSTATUS = const(0x04) +_COMMAND_READREGISTER = const(0x06) +_COMMAND_WRITEREGISTER = const(0x08) +_COMMAND_READGPIO = const(0x0C) +_COMMAND_WRITEGPIO = const(0x0E) +_COMMAND_SETSERIALBAUDRATE = const(0x10) +_COMMAND_SETPARAMETERS = const(0x12) +_COMMAND_SAMCONFIGURATION = const(0x14) +_COMMAND_POWERDOWN = const(0x16) +_COMMAND_RFCONFIGURATION = const(0x32) +_COMMAND_RFREGULATIONTEST = const(0x58) +_COMMAND_INJUMPFORDEP = const(0x56) +_COMMAND_INJUMPFORPSL = const(0x46) +_COMMAND_INLISTPASSIVETARGET = const(0x4A) +_COMMAND_INATR = const(0x50) +_COMMAND_INPSL = const(0x4E) +_COMMAND_INDATAEXCHANGE = const(0x40) +_COMMAND_INCOMMUNICATETHRU = const(0x42) +_COMMAND_INDESELECT = const(0x44) +_COMMAND_INRELEASE = const(0x52) +_COMMAND_INSELECT = const(0x54) +_COMMAND_INAUTOPOLL = const(0x60) +_COMMAND_TGINITASTARGET = const(0x8C) +_COMMAND_TGSETGENERALBYTES = const(0x92) +_COMMAND_TGGETDATA = const(0x86) +_COMMAND_TGSETDATA = const(0x8E) +_COMMAND_TGSETMETADATA = const(0x94) +_COMMAND_TGGETINITIATORCOMMAND = const(0x88) +_COMMAND_TGRESPONSETOINITIATOR = const(0x90) +_COMMAND_TGGETTARGETSTATUS = const(0x8A) + +_RESPONSE_INDATAEXCHANGE = const(0x41) +_RESPONSE_INLISTPASSIVETARGET = const(0x4B) + +_WAKEUP = const(0x55) + +_MIFARE_ISO14443A = const(0x00) + +# Mifare Commands +MIFARE_CMD_AUTH_A = const(0x60) +MIFARE_CMD_AUTH_B = const(0x61) +MIFARE_CMD_READ = const(0x30) +MIFARE_CMD_WRITE = const(0xA0) +MIFARE_CMD_TRANSFER = const(0xB0) +MIFARE_CMD_DECREMENT = const(0xC0) +MIFARE_CMD_INCREMENT = const(0xC1) +MIFARE_CMD_STORE = const(0xC2) +MIFARE_ULTRALIGHT_CMD_WRITE = const(0xA2) + +# Prefixes for NDEF Records (to identify record type) +NDEF_URIPREFIX_NONE = const(0x00) +NDEF_URIPREFIX_HTTP_WWWDOT = const(0x01) +NDEF_URIPREFIX_HTTPS_WWWDOT = const(0x02) +NDEF_URIPREFIX_HTTP = const(0x03) +NDEF_URIPREFIX_HTTPS = const(0x04) +NDEF_URIPREFIX_TEL = const(0x05) +NDEF_URIPREFIX_MAILTO = const(0x06) +NDEF_URIPREFIX_FTP_ANONAT = const(0x07) +NDEF_URIPREFIX_FTP_FTPDOT = const(0x08) +NDEF_URIPREFIX_FTPS = const(0x09) +NDEF_URIPREFIX_SFTP = const(0x0A) +NDEF_URIPREFIX_SMB = const(0x0B) +NDEF_URIPREFIX_NFS = const(0x0C) +NDEF_URIPREFIX_FTP = const(0x0D) +NDEF_URIPREFIX_DAV = const(0x0E) +NDEF_URIPREFIX_NEWS = const(0x0F) +NDEF_URIPREFIX_TELNET = const(0x10) +NDEF_URIPREFIX_IMAP = const(0x11) +NDEF_URIPREFIX_RTSP = const(0x12) +NDEF_URIPREFIX_URN = const(0x13) +NDEF_URIPREFIX_POP = const(0x14) +NDEF_URIPREFIX_SIP = const(0x15) +NDEF_URIPREFIX_SIPS = const(0x16) +NDEF_URIPREFIX_TFTP = const(0x17) +NDEF_URIPREFIX_BTSPP = const(0x18) +NDEF_URIPREFIX_BTL2CAP = const(0x19) +NDEF_URIPREFIX_BTGOEP = const(0x1A) +NDEF_URIPREFIX_TCPOBEX = const(0x1B) +NDEF_URIPREFIX_IRDAOBEX = const(0x1C) +NDEF_URIPREFIX_FILE = const(0x1D) +NDEF_URIPREFIX_URN_EPC_ID = const(0x1E) +NDEF_URIPREFIX_URN_EPC_TAG = const(0x1F) +NDEF_URIPREFIX_URN_EPC_PAT = const(0x20) +NDEF_URIPREFIX_URN_EPC_RAW = const(0x21) +NDEF_URIPREFIX_URN_EPC = const(0x22) +NDEF_URIPREFIX_URN_NFC = const(0x23) + +_GPIO_VALIDATIONBIT = const(0x80) +_GPIO_P30 = const(0) +_GPIO_P31 = const(1) +_GPIO_P32 = const(2) +_GPIO_P33 = const(3) +_GPIO_P34 = const(4) +_GPIO_P35 = const(5) + +_ACK = b'\x00\x00\xFF\x00\xFF\x00' +_FRAME_START = b'\x00\x00\xFF' +# pylint: enable=bad-whitespace + + +def _reset(pin): + """Perform a hardware reset toggle""" + pin.direction = Direction.OUTPUT + pin.value = True + time.sleep(0.1) + pin.value = False + time.sleep(0.5) + pin.value = True + time.sleep(0.1) + + +class BusyError(Exception): + """Base class for exceptions in this module.""" + pass + + +class PN532: + """PN532 driver base, must be extended for I2C/SPI/UART interfacing""" + + def __init__(self, *, debug=False, reset=None): + """Create an instance of the PN532 class + """ + self.debug = debug + if reset: + if debug: + print("Resetting") + _reset(reset) + + try: + self._wakeup() + self.get_firmware_version() # first time often fails, try 2ce + return + except (BusyError, RuntimeError): + pass + self.get_firmware_version() + + def _read_data(self, count): + # Read raw data from device, not including status bytes: + # Subclasses MUST implement this! + raise NotImplementedError + + def _write_data(self, framebytes): + # Write raw bytestring data to device, not including status bytes: + # Subclasses MUST implement this! + raise NotImplementedError + + def _wait_ready(self, timeout): + # Check if busy up to max length of 'timeout' seconds + # Subclasses MUST implement this! + raise NotImplementedError + + def _wakeup(self): + # Send special command to wake up + raise NotImplementedError + + def _write_frame(self, data): + """Write a frame to the PN532 with the specified data bytearray.""" + assert data is not None and 1 < len( + data) < 255, 'Data must be array of 1 to 255 bytes.' + # Build frame to send as: + # - Preamble (0x00) + # - Start code (0x00, 0xFF) + # - Command length (1 byte) + # - Command length checksum + # - Command bytes + # - Checksum + # - Postamble (0x00) + length = len(data) + frame = bytearray(length+8) + frame[0] = _PREAMBLE + frame[1] = _STARTCODE1 + frame[2] = _STARTCODE2 + checksum = sum(frame[0:3]) + frame[3] = length & 0xFF + frame[4] = (~length + 1) & 0xFF + frame[5:-2] = data + checksum += sum(data) + frame[-2] = ~checksum & 0xFF + frame[-1] = _POSTAMBLE + # Send frame. + if self.debug: + print('Write frame: ', [hex(i) for i in frame]) + self._write_data(bytes(frame)) + + def _read_frame(self, length): + """Read a response frame from the PN532 of at most length bytes in size. + Returns the data inside the frame if found, otherwise raises an exception + if there is an error parsing the frame. Note that less than length bytes + might be returned! + """ + # Read frame with expected length of data. + response = self._read_data(length+8) + if self.debug: + print('Read frame:', [hex(i) for i in response]) + + # Swallow all the 0x00 values that preceed 0xFF. + offset = 0 + while response[offset] == 0x00: + offset += 1 + if offset >= len(response): + raise RuntimeError( + 'Response frame preamble does not contain 0x00FF!') + if response[offset] != 0xFF: + raise RuntimeError( + 'Response frame preamble does not contain 0x00FF!') + offset += 1 + if offset >= len(response): + raise RuntimeError('Response contains no data!') + # Check length & length checksum match. + frame_len = response[offset] + if (frame_len + response[offset+1]) & 0xFF != 0: + raise RuntimeError( + 'Response length checksum did not match length!') + # Check frame checksum value matches bytes. + checksum = sum(response[offset+2:offset+2+frame_len+1]) & 0xFF + if checksum != 0: + raise RuntimeError( + 'Response checksum did not match expected value: ', checksum) + # Return frame data. + return response[offset+2:offset+2+frame_len] + + def call_function(self, command, response_length=0, params=[], timeout=1): # pylint: disable=dangerous-default-value + """Send specified command to the PN532 and expect up to response_length + bytes back in a response. Note that less than the expected bytes might + be returned! Params can optionally specify an array of bytes to send as + parameters to the function call. Will wait up to timeout seconds + for a response and return a bytearray of response bytes, or None if no + response is available within the timeout. + """ + # Build frame data with command and parameters. + data = bytearray(2+len(params)) + data[0] = _HOSTTOPN532 + data[1] = command & 0xFF + for i, val in enumerate(params): + data[2+i] = val + # Send frame and wait for response. + try: + self._write_frame(data) + except OSError: + self._wakeup() + if self.debug: + print("call_function OSError") + return None + if not self._wait_ready(timeout): + if self.debug: + print("call_function timeout 1") + return None + # Verify ACK response and wait to be ready for function response. + if not _ACK == self._read_data(len(_ACK)): + raise RuntimeError('Did not receive expected ACK from PN532!') + if self.debug: + print("call_function no ack") + if not self._wait_ready(timeout): + if self.debug: + print("call_function timeout 2") + return None + # Read response bytes. + response = self._read_frame(response_length+2) + # Check that response is for the called function. + if not (response[0] == _PN532TOHOST and response[1] == (command+1)): + raise RuntimeError('Received unexpected command response!') + # Return response data. + return response[2:] + + def get_firmware_version(self): + """Call PN532 GetFirmwareVersion function and return a tuple with the IC, + Ver, Rev, and Support values. + """ + if self.debug: + print("Get firmware version") + response = self.call_function( + _COMMAND_GETFIRMWAREVERSION, 4, timeout=0.5) + if response is None: + raise RuntimeError('Failed to detect the PN532') + if self.debug: + print("Get firmware version response:", tuple(response)) + return tuple(response) + + def SAM_configuration(self): # pylint: disable=invalid-name + """Configure the PN532 to read MiFare cards.""" + # Send SAM configuration command with configuration for: + # - 0x01, normal mode + # - 0x14, timeout 50ms * 20 = 1 second + # - 0x01, use IRQ pin + # Note that no other verification is necessary as call_function will + # check the command was executed as expected. + self.call_function(_COMMAND_SAMCONFIGURATION, + params=[0x01, 0x14, 0x01]) + + def read_passive_target(self, card_baud=_MIFARE_ISO14443A, timeout=1): + """Wait for a MiFare card to be available and return its UID when found. + Will wait up to timeout seconds and return None if no card is found, + otherwise a bytearray with the UID of the found card is returned. + """ + # Send passive read command for 1 card. Expect at most a 7 byte UUID. + try: + response = self.call_function(_COMMAND_INLISTPASSIVETARGET, + params=[0x01, card_baud], + response_length=19, + timeout=timeout) + except BusyError: + return None # no card found! + # If no response is available return None to indicate no card is present. + if response is None: + return None + # Check only 1 card with up to a 7 byte UID is present. + if response[0] != 0x01: + raise RuntimeError('More than one card detected!') + if response[5] > 7: + raise RuntimeError('Found card with unexpectedly long UID!') + # Return UID of card. + return response[6:6+response[5]] + + def mifare_classic_authenticate_block(self, uid, block_number, key_number, key): # pylint: disable=invalid-name + """Authenticate specified block number for a MiFare classic card. Uid + should be a byte array with the UID of the card, block number should be + the block to authenticate, key number should be the key type (like + MIFARE_CMD_AUTH_A or MIFARE_CMD_AUTH_B), and key should be a byte array + with the key data. Returns True if the block was authenticated, or False + if not authenticated. + """ + # Build parameters for InDataExchange command to authenticate MiFare card. + uidlen = len(uid) + keylen = len(key) + params = bytearray(3+uidlen+keylen) + params[0] = 0x01 # Max card numbers + params[1] = key_number & 0xFF + params[2] = block_number & 0xFF + params[3:3+keylen] = key + params[3+keylen:] = uid + # Send InDataExchange request and verify response is 0x00. + response = self.call_function(_COMMAND_INDATAEXCHANGE, + params=params, + response_length=1) + return response[0] == 0x00 + + def mifare_classic_read_block(self, block_number): + """Read a block of data from the card. Block number should be the block + to read. If the block is successfully read a bytearray of length 16 with + data starting at the specified block will be returned. If the block is + not read then None will be returned. + """ + # Send InDataExchange request to read block of MiFare data. + response = self.call_function(_COMMAND_INDATAEXCHANGE, + params=[0x01, MIFARE_CMD_READ, + block_number & 0xFF], + response_length=17) + # Check first response is 0x00 to show success. + if response[0] != 0x00: + return None + # Return first 4 bytes since 16 bytes are always returned. + return response[1:] + + def mifare_classic_write_block(self, block_number, data): + """Write a block of data to the card. Block number should be the block + to write and data should be a byte array of length 16 with the data to + write. If the data is successfully written then True is returned, + otherwise False is returned. + """ + assert data is not None and len( + data) == 16, 'Data must be an array of 16 bytes!' + # Build parameters for InDataExchange command to do MiFare classic write. + params = bytearray(19) + params[0] = 0x01 # Max card numbers + params[1] = MIFARE_CMD_WRITE + params[2] = block_number & 0xFF + params[3:] = data + # Send InDataExchange request. + response = self.call_function(_COMMAND_INDATAEXCHANGE, + params=params, + response_length=1) + return response[0] == 0x0 + + def ntag2xx_write_block(self, block_number, data): + """Write a block of data to the card. Block number should be the block + to write and data should be a byte array of length 4 with the data to + write. If the data is successfully written then True is returned, + otherwise False is returned. + """ + assert data is not None and len( + data) == 4, 'Data must be an array of 4 bytes!' + # Build parameters for InDataExchange command to do NTAG203 classic write. + params = bytearray(3+len(data)) + params[0] = 0x01 # Max card numbers + params[1] = MIFARE_ULTRALIGHT_CMD_WRITE + params[2] = block_number & 0xFF + params[3:] = data + # Send InDataExchange request. + response = self.call_function(_COMMAND_INDATAEXCHANGE, + params=params, + response_length=1) + return response[0] == 0x00 + + def ntag2xx_read_block(self, block_number): + """Read a block of data from the card. Block number should be the block + to read. If the block is successfully read a bytearray of length 16 with + data starting at the specified block will be returned. If the block is + not read then None will be returned. + """ + return self.mifare_classic_read_block(block_number)[0:4] # only 4 bytes per page diff --git a/boot.py b/boot.py new file mode 100644 index 0000000..e69de29 diff --git a/digitalio.py b/digitalio.py new file mode 100644 index 0000000..9f2427b --- /dev/null +++ b/digitalio.py @@ -0,0 +1,206 @@ +# SPDX-FileCopyrightText: Copyright (c) 2019 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`digitalio` +============================== +DigitalIO for ESP32 over SPI. + +* Author(s): Brent Rubell, based on Adafruit_Blinka digitalio implementation + and bcm283x Pin implementation. + +https://github.com/adafruit/Adafruit_Blinka/blob/master/src/adafruit_blinka/microcontroller/bcm283x/pin.py +https://github.com/adafruit/Adafruit_Blinka/blob/master/src/digitalio.py +""" +from micropython import const + + +class Pin: + """ + Implementation of CircuitPython API Pin Handling + for ESP32SPI. + + :param int esp_pin: Valid ESP32 GPIO Pin, predefined in ESP32_GPIO_PINS. + :param ESP_SPIcontrol esp: The ESP object we are using. + + NOTE: This class does not currently implement reading digital pins + or the use of internal pull-up resistors. + """ + + # pylint: disable=invalid-name + IN = const(0x00) + OUT = const(0x01) + LOW = const(0x00) + HIGH = const(0x01) + _value = LOW + _mode = IN + pin_id = None + + ESP32_GPIO_PINS = set( + [0, 1, 2, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33] + ) + + def __init__(self, esp_pin, esp): + if esp_pin in self.ESP32_GPIO_PINS: + self.pin_id = esp_pin + else: + raise AttributeError("Pin %d is not a valid ESP32 GPIO Pin." % esp_pin) + self._esp = esp + + def init(self, mode=IN): + """Initalizes a pre-defined pin. + :param mode: Pin mode (IN, OUT, LOW, HIGH). Defaults to IN. + """ + if mode is not None: + if mode == self.IN: + self._mode = self.IN + self._esp.set_pin_mode(self.pin_id, 0) + elif mode == self.OUT: + self._mode = self.OUT + self._esp.set_pin_mode(self.pin_id, 1) + else: + raise RuntimeError("Invalid mode defined") + + def value(self, val=None): + """Sets ESP32 Pin GPIO output mode. + :param val: Pin output level (LOW, HIGH) + """ + if val is not None: + if val == self.LOW: + self._value = val + self._esp.set_digital_write(self.pin_id, 0) + elif val == self.HIGH: + self._value = val + self._esp.set_digital_write(self.pin_id, 1) + else: + raise RuntimeError("Invalid value for pin") + else: + raise NotImplementedError( + "digitalRead not currently implemented in esp32spi" + ) + + def __repr__(self): + return str(self.pin_id) + + +# pylint: disable = too-few-public-methods +class DriveMode: + """DriveMode Enum.""" + + PUSH_PULL = None + OPEN_DRAIN = None + + +DriveMode.PUSH_PULL = DriveMode() +DriveMode.OPEN_DRAIN = DriveMode() + + +class Direction: + """DriveMode Enum.""" + + INPUT = None + OUTPUT = None + + +Direction.INPUT = Direction() +Direction.OUTPUT = Direction() + + +class DigitalInOut: + """Implementation of DigitalIO module for ESP32SPI. + + :param ESP_SPIcontrol esp: The ESP object we are using. + :param int pin: Valid ESP32 GPIO Pin, predefined in ESP32_GPIO_PINS. + """ + + _pin = None + # pylint: disable = attribute-defined-outside-init + def __init__(self, esp, pin): + self._esp = esp + self._pin = Pin(pin, self._esp) + self.direction = Direction.INPUT + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.deinit() + + def deinit(self): + """De-initializes the pin object.""" + self._pin = None + + def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL): + """Set the drive mode and value and then switch to writing out digital values. + :param bool value: Default mode to set upon switching. + :param DriveMode drive_mode: Drive mode for the output. + """ + self._direction = Direction.OUTPUT + self._drive_mode = drive_mode + self.value = value + + def switch_to_input(self, pull=None): + """Sets the pull and then switch to read in digital values. + :param Pull pull: Pull configuration for the input. + """ + raise NotImplementedError( + "Digital reads are not currently supported in ESP32SPI." + ) + + @property + def direction(self): + """Returns the pin's direction.""" + return self.__direction + + @direction.setter + def direction(self, pin_dir): + """Sets the direction of the pin. + :param Direction dir: Pin direction (Direction.OUTPUT or Direction.INPUT) + """ + self.__direction = pin_dir + if pin_dir is Direction.OUTPUT: + self._pin.init(mode=Pin.OUT) + self.value = False + self.drive_mode = DriveMode.PUSH_PULL + elif pin_dir is Direction.INPUT: + self._pin.init(mode=Pin.IN) + else: + raise AttributeError("Not a Direction") + + @property + def value(self): + """Returns the digital logic level value of the pin.""" + return self._pin.value() == 1 + + @value.setter + def value(self, val): + """Sets the digital logic level of the pin. + :param type value: Pin logic level. + :param int value: Pin logic level. 1 is logic high, 0 is logic low. + :param bool value: Pin logic level. True is logic high, False is logic low. + """ + if self.direction is Direction.OUTPUT: + self._pin.value(1 if val else 0) + else: + raise AttributeError("Not an output") + + @property + def drive_mode(self): + """Returns pin drive mode.""" + if self.direction is Direction.OUTPUT: + return self._drive_mode + raise AttributeError("Not an output") + + @drive_mode.setter + def drive_mode(self, mode): + """Sets the pin drive mode. + :param DriveMode mode: Defines the drive mode when outputting digital values. + Either PUSH_PULL or OPEN_DRAIN + """ + if mode is DriveMode.OPEN_DRAIN: + raise NotImplementedError( + "Drive mode %s not implemented in ESP32SPI." % mode + ) + if mode is DriveMode.PUSH_PULL: + self._pin.init(mode=Pin.OUT) \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..b63560a --- /dev/null +++ b/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for i in *.py; do + pyboard.py -f cp $i :; +done \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..6ff2c23 --- /dev/null +++ b/main.py @@ -0,0 +1,117 @@ +from micropython import const +from os import uname +from machine import SoftSPI, Pin + +import uasyncio as asyncio + +import aioble +import bluetooth +import random +import struct +import machine +import time +import gc +import sys + +import pn532_i2c + +# Based on example from: https://github.com/micropython/micropython-lib/blob/master/micropython/bluetooth/aioble/examples/temp_sensor.py +# Unfortunately, there is no license information for aioble anywhere to be found... + +# Also based on https://github.com/somervda/nfc-tester which doesn't come with a license file either... + +RFID_BASE = lambda x: "1408%s-1337-BABE-B00B-C0FFEEC0FFEE" % (((4 - len(str(x))) * "0") + str(x)) +RFID_SERVICE_UUID = bluetooth.UUID(RFID_BASE(1)) +RFID_CHAR_UUID = bluetooth.UUID(RFID_BASE(2)) + +_ADV_INTERVAL_MS = 250_000 + +rfid_service = aioble.Service(RFID_SERVICE_UUID) +rfid_characteristic = aioble.Characteristic( + rfid_service, RFID_CHAR_UUID, read=True, notify=True +) +aioble.register_services(rfid_service) + +async def peripheral_task(): + while True: + try: + async with await aioble.advertise( + _ADV_INTERVAL_MS, + name="RFIDTest", + services=[RFID_SERVICE_UUID], + ) as connection: + print("Connection from", connection.device) + await connection.disconnected() + except BaseException as e: + print("Exception in peripheral_task:", e) + +async def read_task(rdr): + # Start listening for a card + time.sleep_ms(1000) + print("Waiting for RFID/NFC card...") + + while True: + try: + do_read(rdr) + except Exception as e: + print("Exception in read_task:", e) + await asyncio.sleep_ms(100) + +def do_read(rdr): + gc.collect() + last_uid = None + while True: + time.sleep_ms(100) + try: + uid = rdr.read_passive_target(timeout=0.2) + if last_uid == uid: + continue + if uid is None: + print("No card found") + last_uid = None + continue + + struid = "".join(['{:0>{w}}'.format(hex(i)[2:], w=2) for i in uid]) + print("Found card with UID:", struid) + rfid_characteristic.write(uid) + last_uid = uid + except Exception as e: + print("Something failed:", e) + pass + +async def main(): + try: + i2c = machine.I2C(0, scl=machine.Pin(1), sda=machine.Pin(0)) + devices = i2c.scan() + if len(devices) == 0: + print("No i2c device !") + return 1 + + else: + print('i2c devices found:', len(devices)) + for device in devices: + print("Decimal address: ", device, " | Hex address: ", hex(device)) + + pn532 = pn532_i2c.PN532_I2C(i2c, debug=False) + ic, ver, rev, support = pn532.get_firmware_version() + print("Found PN532 with firmware version: {0}.{1}".format(ver, rev)) + pn532.SAM_configuration() + + time.sleep_ms(1000) + + gc.collect() + + await asyncio.gather( + peripheral_task(), + read_task(pn532) + ) + + except KeyboardInterrupt: + print("Interrupted") + + except Exception as e: + print("Exception in main:", e) + return 1 + + +asyncio.run(main()) \ No newline at end of file diff --git a/pn532_i2c.py b/pn532_i2c.py new file mode 100644 index 0000000..7b919d9 --- /dev/null +++ b/pn532_i2c.py @@ -0,0 +1,143 @@ +# Adafruit PN532 NFC/RFID control library. +# Author: Tony DiCola +# +# The MIT License (MIT) +# +# Copyright (c) 2015-2018 Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +``adafruit_pn532.i2c`` +==================================================== + +This module will let you communicate with a PN532 RFID/NFC shield or breakout +using I2C. + +* Author(s): Original Raspberry Pi code by Tony DiCola, CircuitPython by ladyada, + refactor by Carter Nelson + +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PN532.git" + +import time +from machine import I2C, Pin +from digitalio import Direction +from micropython import const +from adafruit_pn532 import PN532, BusyError, _reset + +# pylint: disable=bad-whitespace +_I2C_ADDRESS = const(0x24) + +_NOT_BUSY = const(0x01) + + +class PN532_I2C(PN532): + """Driver for the PN532 connected over I2C.""" + + def __init__(self, i2c, *, irq=None, reset=None, req=None, debug=False): + """Create an instance of the PN532 class using I2C. Note that PN532 + uses clock stretching. Optional IRQ pin (not used), + reset pin and debugging output. + """ + self.debug = debug + self._irq = irq + self._req = req + if reset: + # Changed this logic so it is not circuit python dependent (No use of direction) + # Do the reset at the I2c level + if self.debug: + print("reset") + reset_pin = Pin(reset, Pin.OUT) + reset_pin.value(1) + time.sleep(0.1) + reset_pin.value(0) + time.sleep(0.5) + reset_pin.value(1) + time.sleep(0.1) + # _reset(reset) + # self._i2c = I2C.SoftI2C(i2c, _I2C_ADDRESS) + self._i2c = i2c + # call super.__init__ without reset pin (To get around circuitpython pin specifics) + super().__init__(debug=debug) + + def _wakeup(self): # pylint: disable=no-self-use + """Send any special commands/data to wake up PN532""" + if self._req: + self._req.direction = Direction.OUTPUT + self._req.value = True + time.sleep(0.1) + self._req.value = False + time.sleep(0.1) + self._req.value = True + time.sleep(0.5) + + def _wait_ready(self, timeout=1): + """Poll PN532 if status byte is ready, up to `timeout` seconds""" + # Updated to use time_ns vs time.monotonic of circuitpython + status = bytearray(1) + timestamp = time.time_ns()/1000000000 + if self.debug: + print("wait ready timestamp:", timestamp) + while ((time.time_ns()/1000000000) - timestamp) < timeout: + try: + # with self._i2c: + self._i2c.readfrom_into(_I2C_ADDRESS, status) + except OSError: + self._wakeup() + continue + if status == b'\x01': + return True # No longer busy + else: + time.sleep(0.05) # lets ask again soon! + # Timed out! + if self.debug: + print("wait_ready time.time_ns():", time.time_ns()/1000000000) + return False + + def _read_data(self, count): + """Read a specified count of bytes from the PN532.""" + if self.debug: + print("_read_data") + # Build a read request frame. + frame = bytearray(count+1) + status_byte = bytearray(1) + # Updated to use readfrom_into (Circutpython readfrom not supported) + self._i2c.readfrom_into(_I2C_ADDRESS, status_byte) + if self.debug: + print("_read_data status_byte: ", status_byte) + if status_byte[0] != _NOT_BUSY: # not ready + if self.debug: + print("_read_data busy_error ") + raise BusyError + if self.debug: + print("_read_data readfrom_into") + # Updated to use readfrom_into (Circutpython readfrom not supported) + self._i2c.readfrom_into(_I2C_ADDRESS, frame) + if self.debug: + print("_read_data frame: ", frame) + return frame[1:] # don't return the status byte + + def _write_data(self, framebytes): + """Write a specified count of bytes to the PN532""" + # Updated to using writeto - circuitpython write not supported + if self.debug: + print('_write data: ', [hex(i) for i in framebytes]) + self._i2c.writeto(_I2C_ADDRESS, framebytes)