diff --git a/README.md b/README.md index 89047b9..f22f36f 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ This script listens for NFC tags via a USB reader (like the **PN532**) and autom ## 🧰 Requirements - Linux device running **Plexamp headless** -- A **PN532 NFC reader** connected via USB or UART - (uses `/dev/ttyUSB*`) +- A **PN532 NFC reader** connected via USB or UART + (uses `/dev/ttyUSB*` or `/dev/ttyACM*`) - Python 3.8 or newer - Internet access for package installation - The service assumes you are using the default `pi` user from raspberry pi install. If you are using a different user update the `plexamp-nfc.service` prior to install. @@ -108,7 +108,7 @@ python main.py ``` | Issue | Possible Fix | | --------------------------- | ------------------------------------------------------------------------------------- | -| `Waiting for NFC reader...` | Check your PN532 connection. Run `ls /dev/ttyUSB*` to confirm the device appears. | +| `Waiting for NFC reader...` | Check your PN532 connection. Run `ls /dev/tty{USB,ACM}*` to confirm the device appears. | | `Failed to trigger Plexamp` | Make sure Plexamp headless is running on the same host and listening on port `32500`. | | Service won’t start | Check logs via `sudo journalctl -u plexamp-nfc.service -xe`. | | No tags detected | Try different baud rate in `main.py` (`baudrate=115200` by default). | diff --git a/main.py b/main.py index 99e27f8..191649b 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,99 @@ import time -import os -import glob import serial import requests from adafruit_pn532.uart import PN532_UART +from serial.tools import list_ports + + +PN532KILLER_VID_PID = (0x1A86, 0x55D3) + + +class PlexampPN532_UART(PN532_UART): + # Don't crash on no-card polls + def get_passive_target(self, timeout=1): + response = self.process_response( + 0x4A, # InListPassiveTarget + response_length=64, + timeout=timeout, + ) + + if not response or response[0] == 0: + return None + + if response[0] != 1: + raise RuntimeError(f"Unexpected target count: {response[0]}") + + uid_length = response[5] + if uid_length > 7: + raise RuntimeError("Found card with unexpectedly long UID!") + + return response[6 : 6 + uid_length] + + +class PN532Killer_UART(PlexampPN532_UART): + @staticmethod + def _crc16a(data): + crc = 0x6363 + for value in data: + value ^= crc & 0xFF + value = (value ^ (value << 4)) & 0xFF + crc = (crc >> 8) ^ (value << 8) ^ (value << 3) ^ (value >> 4) + return (crc & 0xFFFF).to_bytes(2, byteorder="little") + + def ntag2xx_read_block(self, block_number): + # PN532Killer acknowledges InDataExchange reads but doesn't return + # data. Its supported raw-card path is InCommunicateThru with + # the ISO14443A CRC appended to the MIFARE READ command. + command = bytes((0x30, block_number & 0xFF)) + response = self.call_function( + 0x42, # InCommunicateThru + params=command + self._crc16a(command), + response_length=19, # status + 16 data bytes + 2 CRC bytes + ) + + if not response or response[0] != 0: + return None + + # A MIFARE READ returns four pages. Keep the requested 4-byte page to + # match the Adafruit ntag2xx_read_block API. + return response[1:5] + + +# ---------------------------- +# Helper: identify pn532killer boards using usb metadata +# ---------------------------- +def is_pn532killer(port): + product = port.product or "" + description = port.description or "" + return ( + "PN532Killer" in product + or "PN532Killer" in description + or (port.vid, port.pid) == PN532KILLER_VID_PID + ) + # ---------------------------- # Helper: find PN532 serial device # ---------------------------- def find_uart_device(): - devices = glob.glob("/dev/ttyUSB*") - return devices[0] if devices else None + ports = list(list_ports.comports()) + + # Prefer readers that can be positively identified + for port in ports: + if is_pn532killer(port): + return port + + # Generic ttyACMs are only considered when no ttyUSBs exist + for device_prefix in ("/dev/ttyUSB", "/dev/ttyACM"): + candidates = sorted( + (port for port in ports if port.device.startswith(device_prefix)), + key=lambda port: port.device, + ) + if candidates: + return candidates[0] + + return None + # ---------------------------- # Helper: parse NDEF URI @@ -33,19 +116,35 @@ def parse_ndef_uri(tag_data): full_url = full_url.rstrip(" \n\r\x00\xfe\t") return full_url + # ---------------------------- # NFC reader setup # ---------------------------- def connect_reader(): while True: - device = find_uart_device() - if not device: + port = find_uart_device() + if not port: print("Waiting for NFC reader...") time.sleep(2) continue + + device = port.device try: - ser = serial.Serial(device, baudrate=115200, timeout=1) - pn532 = PN532_UART(ser, debug=False) + ser = serial.Serial(baudrate=115200, timeout=1) + + if is_pn532killer(port): + # pySerial asserts DTR and RTS by default. Incorrect DTR/RTS + # handling on a PN532Killer disrupts its configuration. + ser.dtr = False + ser.rts = False + + ser.port = device + ser.open() + + reader_class = ( + PN532Killer_UART if is_pn532killer(port) else PlexampPN532_UART + ) + pn532 = reader_class(ser, debug=False) pn532.SAM_configuration() print(f"NFC reader connected on {device}") return pn532 @@ -53,6 +152,7 @@ def connect_reader(): print(f"Failed to connect to {device}: {e}") time.sleep(2) + # ---------------------------- # Debounce & memory variables # ---------------------------- @@ -61,6 +161,7 @@ def connect_reader(): last_seen_time = 0 forget_after = 30 # seconds to "forget" a tag after removal + # ---------------------------- # Main # ---------------------------- @@ -155,6 +256,3 @@ def connect_reader(): except Exception as e: print(f"Reader error: {e}. Reconnecting...") pn532 = connect_reader() - - -