mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Set up PN5180 NFC reader
This commit is contained in:
+143
-18
@@ -1,21 +1,146 @@
|
||||
### PN5180 NFC Reader (SPI)
|
||||
# SpoolBuddy Hardware Setup
|
||||
|
||||
| PN5180 Pin | Raspberry Pi GPIO | Wire Color |
|
||||
|------------|-------------------|------------|
|
||||
| VCC | Pin17 or 1 | Red |
|
||||
| GND | Pin 20 | Black |
|
||||
| SCK | Pin23 (GPIO11) | Yellow |
|
||||
| MISO | Pin21 (GPIO9) | Blue |
|
||||
| MOSI | Pin19 (GPIO10) | Green |
|
||||
| NSS (CS) | Pin24 (GPIO8) | Orange |
|
||||
| BUSY | Pin22 (GPIO25) | White |
|
||||
| RST | Pin18 (GPIO24) | Brown |
|
||||
## PN5180 NFC Reader (SPI)
|
||||
|
||||
### NAU7802 Scale (I2C)
|
||||
### Wiring
|
||||
|
||||
| NAU7802 Pin | Raspberry Pi GPIO | Wire Color |
|
||||
|-------------|-------------------|------------|
|
||||
| VCC | Pin17 or 1 | Red |
|
||||
| SDA | Pin 27 (GPIO 0) | Yellow |
|
||||
| SCL | Pin 28 (GPIO 1) | White |
|
||||
| GND | Pin 30 or Pin 25 | Black |
|
||||
| PN5180 Pin | Raspberry Pi Pin | GPIO | Wire Color |
|
||||
|------------|------------------|------|------------|
|
||||
| 3V3 | Pin 1 | — | Red |
|
||||
| 5V | Pin 2 | — | Red |
|
||||
| GND | Pin 20 | — | Black |
|
||||
| SCK | Pin 23 | GPIO11 | Yellow |
|
||||
| MISO | Pin 21 | GPIO9 | Blue |
|
||||
| MOSI | Pin 19 | GPIO10 | Green |
|
||||
| NSS (CS) | Pin 16 | GPIO23 | Orange |
|
||||
| BUSY | Pin 22 | GPIO25 | White |
|
||||
| RST | Pin 18 | GPIO24 | Brown |
|
||||
|
||||
> **Power:** The PN5180 board has two power pins. 3V3 powers the IC itself,
|
||||
> 5V powers the antenna booster and extends read range. Both should be connected.
|
||||
> Do NOT connect 5V to the 3V3 pin — it will destroy the reader.
|
||||
|
||||
> **NSS:** We use GPIO23 for manual chip-select instead of the default SPI CE0
|
||||
> (GPIO8) because the kernel SPI driver's automatic CS timing does not meet the
|
||||
> PN5180's requirements (5µs setup, 100µs hold). Manual CS via GPIO23 with
|
||||
> `spidev.no_cs = True` resolves this.
|
||||
|
||||
### Setup Steps
|
||||
|
||||
#### 1. Enable SPI
|
||||
|
||||
After a fresh Raspberry Pi OS install, SPI is disabled by default.
|
||||
|
||||
```bash
|
||||
sudo raspi-config
|
||||
# Navigate to: Interface Options -> SPI -> Enable
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
Verify after reboot:
|
||||
|
||||
```bash
|
||||
ls /dev/spidev0.*
|
||||
# Should show: /dev/spidev0.0 /dev/spidev0.1
|
||||
```
|
||||
|
||||
#### 2. Disable automatic CS (kernel SPI chip-select)
|
||||
|
||||
Since we use manual CS on GPIO23, we need to tell the SPI driver not to
|
||||
drive any hardware CS pins. Add this to `/boot/firmware/config.txt`:
|
||||
|
||||
```
|
||||
dtoverlay=spi0-0cs
|
||||
```
|
||||
|
||||
Then reboot:
|
||||
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
#### 3. Install system packages
|
||||
|
||||
```bash
|
||||
sudo apt install python3-spidev python3-libgpiod gpiod libgpiod3
|
||||
```
|
||||
|
||||
- `python3-spidev` / `libgpiod3` — system libraries for SPI and GPIO access
|
||||
- `gpiod` — command-line GPIO tools (useful for debugging)
|
||||
|
||||
#### 4. Install Python dependencies (in venv)
|
||||
|
||||
```bash
|
||||
pip install spidev gpiod
|
||||
```
|
||||
|
||||
- `spidev` — Python SPI bindings
|
||||
- `gpiod` — Python GPIO bindings via libgpiod (works on both RPi 4 and RPi 5)
|
||||
|
||||
#### 5. Solder all connections
|
||||
|
||||
Wago connectors or breadboard jumpers are unreliable for SPI — the PN5180
|
||||
is very sensitive to signal integrity issues (loose connections cause RF
|
||||
field flickering, phantom errors, and intermittent communication failures).
|
||||
**Solder all wires directly** for reliable operation.
|
||||
|
||||
#### 6. Verify hardware communication
|
||||
|
||||
Run the diagnostic script to confirm the PN5180 is responding:
|
||||
|
||||
```bash
|
||||
sudo python3 spoolbuddy/pn5180_diag.py
|
||||
```
|
||||
|
||||
Expected output includes product version (e.g. `v4.0`), firmware version,
|
||||
register dump, and "Diagnostics complete" at the end.
|
||||
|
||||
#### 7. Test tag reading
|
||||
|
||||
```bash
|
||||
sudo python3 spoolbuddy/read_tag.py
|
||||
```
|
||||
|
||||
Place a tag on the reader. Supported tag types:
|
||||
|
||||
| Tag Type | SAK | Use Case |
|
||||
|---------------------|--------|------------------------------|
|
||||
| MIFARE Classic 1K | `0x08` | Bambu Lab filament tags |
|
||||
| MIFARE Classic 4K | `0x18` | Bambu Lab filament tags |
|
||||
| NTAG (213/215/216) | `0x00` | SpoolEase / OpenPrintTag |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| All zeros from SPI reads | SPI not enabled | Run `raspi-config` and enable SPI, then reboot |
|
||||
| `GENERAL_ERROR` on SEND_DATA | Automatic CS timing too fast | Use manual CS on GPIO23 with `spi0-0cs` overlay |
|
||||
| `BUSY timeout` | Wiring issue or RST not connected | Check RST and BUSY pin connections |
|
||||
| RF field flickering on/off | Loose power wires | Solder all connections |
|
||||
| `No tag found` but tag is present | Wrong protocol or missing `setTransceiveMode()` | Ensure ISO 14443A config (`0x00, 0x80`) and `setTransceiveMode()` before every `SEND_DATA` |
|
||||
| Auth failed for block N | Wrong key derivation | Verify HKDF uses context `"RFID-A\0"` (7 bytes including null terminator) |
|
||||
| `EBUSY` when requesting GPIO8 | Kernel SPI driver owns CE0 | Use GPIO23 for NSS instead |
|
||||
|
||||
### Technical Notes
|
||||
|
||||
- SPI speed: **500 kHz** (higher speeds cause communication errors)
|
||||
- SPI mode: **0** (CPOL=0, CPHA=0)
|
||||
- CS timing: **5µs** setup after CS LOW, **100µs** hold after CS HIGH
|
||||
- BUSY handshake: wait for BUSY **HIGH** (processing started) then **LOW** (done) — waiting only for LOW is incorrect
|
||||
- `setTransceiveMode()`: must write `0x03` to SYSTEM_CONFIG bits 0-2 before every `SEND_DATA`, or the PN5180 buffers data but never transmits on RF
|
||||
- Bambu tags use **MIFARE Classic** with per-sector keys derived via **HKDF-SHA256** from a master key + tag UID
|
||||
- NTAG reads require **CRC disabled** (unlike MIFARE Classic which needs CRC enabled)
|
||||
- The PN5180 handles Crypto1 encryption/decryption internally via the `MFC_AUTHENTICATE` (0x0C) host command
|
||||
|
||||
---
|
||||
|
||||
## NAU7802 Scale (I2C)
|
||||
|
||||
### Wiring
|
||||
|
||||
| NAU7802 Pin | Raspberry Pi Pin | GPIO | Wire Color |
|
||||
|-------------|------------------|--------|------------|
|
||||
| VCC | Pin 1 | — | Red |
|
||||
| SDA | Pin 27 | GPIO 0 | Yellow |
|
||||
| SCL | Pin 28 | GPIO 1 | White |
|
||||
| GND | Pin 30 | — | Black |
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PN5180 NFC reader diagnostic script.
|
||||
|
||||
Connects to a PN5180 over SPI on a Raspberry Pi and reads
|
||||
hardware status, version info, and register state.
|
||||
|
||||
Wiring (from spoolbuddy/README.md):
|
||||
PN5180 VCC -> Pi Pin 1 (3.3V)
|
||||
PN5180 GND -> Pi Pin 20 (GND)
|
||||
PN5180 SCK -> Pi Pin 23 (GPIO11)
|
||||
PN5180 MISO -> Pi Pin 21 (GPIO9)
|
||||
PN5180 MOSI -> Pi Pin 19 (GPIO10)
|
||||
PN5180 NSS -> Pi Pin 24 (GPIO8 / CE0)
|
||||
PN5180 BUSY -> Pi Pin 22 (GPIO25)
|
||||
PN5180 RST -> Pi Pin 18 (GPIO24)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gpiod
|
||||
import spidev
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pin assignments (BCM numbering)
|
||||
# ---------------------------------------------------------------------------
|
||||
BUSY_PIN = 25 # Pin 22
|
||||
RST_PIN = 24 # Pin 18
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPI command instruction codes (NXP PN5180 datasheet Table 5)
|
||||
# ---------------------------------------------------------------------------
|
||||
CMD_WRITE_REGISTER = 0x00
|
||||
CMD_WRITE_REGISTER_OR_MASK = 0x01
|
||||
CMD_WRITE_REGISTER_AND_MASK = 0x02
|
||||
CMD_READ_REGISTER = 0x04
|
||||
CMD_READ_REGISTER_MULTIPLE = 0x05
|
||||
CMD_WRITE_EEPROM = 0x06
|
||||
CMD_READ_EEPROM = 0x07
|
||||
CMD_SEND_DATA = 0x09
|
||||
CMD_READ_DATA = 0x0A
|
||||
CMD_LOAD_RF_CONFIG = 0x11
|
||||
CMD_RF_ON = 0x16
|
||||
CMD_RF_OFF = 0x17
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Register addresses (32-bit each)
|
||||
# ---------------------------------------------------------------------------
|
||||
REG_SYSTEM_CONFIG = 0x00
|
||||
REG_IRQ_ENABLE = 0x01
|
||||
REG_IRQ_STATUS = 0x02
|
||||
REG_IRQ_CLEAR = 0x03
|
||||
REG_TRANSCEIVE_CONTROL = 0x04
|
||||
REG_TIMER1_RELOAD = 0x0C
|
||||
REG_TIMER1_CONFIG = 0x0F
|
||||
REG_RX_WAIT_CONFIG = 0x11
|
||||
REG_CRC_RX_CONFIG = 0x12
|
||||
REG_RX_STATUS = 0x13
|
||||
REG_CRC_TX_CONFIG = 0x19
|
||||
REG_RF_STATUS = 0x1D
|
||||
REG_SYSTEM_STATUS = 0x24
|
||||
REG_TEMP_CONTROL = 0x25
|
||||
|
||||
REGISTER_NAMES = {
|
||||
REG_SYSTEM_CONFIG: "SYSTEM_CONFIG",
|
||||
REG_IRQ_ENABLE: "IRQ_ENABLE",
|
||||
REG_IRQ_STATUS: "IRQ_STATUS",
|
||||
REG_IRQ_CLEAR: "IRQ_CLEAR",
|
||||
REG_TRANSCEIVE_CONTROL: "TRANSCEIVE_CONTROL",
|
||||
REG_TIMER1_RELOAD: "TIMER1_RELOAD",
|
||||
REG_TIMER1_CONFIG: "TIMER1_CONFIG",
|
||||
REG_RX_WAIT_CONFIG: "RX_WAIT_CONFIG",
|
||||
REG_CRC_RX_CONFIG: "CRC_RX_CONFIG",
|
||||
REG_RX_STATUS: "RX_STATUS",
|
||||
REG_CRC_TX_CONFIG: "CRC_TX_CONFIG",
|
||||
REG_RF_STATUS: "RF_STATUS",
|
||||
REG_SYSTEM_STATUS: "SYSTEM_STATUS",
|
||||
REG_TEMP_CONTROL: "TEMP_CONTROL",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EEPROM addresses
|
||||
# ---------------------------------------------------------------------------
|
||||
EEPROM_DIE_IDENTIFIER = 0x00 # 16 bytes
|
||||
EEPROM_PRODUCT_VERSION = 0x10 # 2 bytes
|
||||
EEPROM_FIRMWARE_VERSION = 0x12 # 2 bytes
|
||||
EEPROM_EEPROM_VERSION = 0x14 # 2 bytes
|
||||
EEPROM_IRQ_PIN_CONFIG = 0x1A # 1 byte
|
||||
|
||||
|
||||
def _find_gpio_chip():
|
||||
"""Find the right gpiochip for Raspberry Pi GPIO pins.
|
||||
|
||||
RPi 5 uses gpiochip4, RPi 4 uses gpiochip0.
|
||||
"""
|
||||
for path in ["/dev/gpiochip4", "/dev/gpiochip0"]:
|
||||
try:
|
||||
chip = gpiod.Chip(path)
|
||||
info = chip.get_info()
|
||||
# RPi 4: pinctrl-bcm2711, RPi 5: pinctrl-rp1
|
||||
if "pinctrl" in info.label:
|
||||
return chip
|
||||
chip.close()
|
||||
except (FileNotFoundError, PermissionError, OSError):
|
||||
continue
|
||||
raise RuntimeError("Could not find Raspberry Pi GPIO chip")
|
||||
|
||||
|
||||
class PN5180:
|
||||
"""Low-level driver for the PN5180 NFC frontend over SPI."""
|
||||
|
||||
def __init__(self, spi_bus=0, spi_device=0, spi_speed_hz=1_000_000, busy_pin=BUSY_PIN, rst_pin=RST_PIN):
|
||||
# GPIO setup via libgpiod
|
||||
self._chip = _find_gpio_chip()
|
||||
|
||||
self._busy_line = self._chip.request_lines(
|
||||
consumer="pn5180-diag",
|
||||
config={busy_pin: gpiod.LineSettings(direction=gpiod.line.Direction.INPUT)},
|
||||
)
|
||||
self._rst_line = self._chip.request_lines(
|
||||
consumer="pn5180-diag",
|
||||
config={
|
||||
rst_pin: gpiod.LineSettings(
|
||||
direction=gpiod.line.Direction.OUTPUT,
|
||||
output_value=gpiod.line.Value.ACTIVE,
|
||||
)
|
||||
},
|
||||
)
|
||||
self._busy_pin = busy_pin
|
||||
self._rst_pin = rst_pin
|
||||
|
||||
# SPI setup – mode 0 (CPOL=0, CPHA=0), MSB first
|
||||
self._spi = spidev.SpiDev()
|
||||
self._spi.open(spi_bus, spi_device)
|
||||
self._spi.max_speed_hz = spi_speed_hz
|
||||
self._spi.mode = 0b00
|
||||
self._spi.bits_per_word = 8
|
||||
|
||||
def close(self):
|
||||
self._spi.close()
|
||||
self._busy_line.release()
|
||||
self._rst_line.release()
|
||||
self._chip.close()
|
||||
|
||||
# -- low-level helpers --------------------------------------------------
|
||||
|
||||
def _busy_is_high(self):
|
||||
return self._busy_line.get_value(self._busy_pin) == gpiod.line.Value.ACTIVE
|
||||
|
||||
def _wait_busy(self, timeout_s=1.0):
|
||||
"""Block until BUSY goes LOW (PN5180 ready)."""
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while self._busy_is_high():
|
||||
if time.monotonic() > deadline:
|
||||
raise TimeoutError("PN5180 BUSY line did not go low")
|
||||
time.sleep(0.001)
|
||||
|
||||
def _send_command(self, tx_data, rx_len=0):
|
||||
"""Send an SPI command frame and optionally read a response frame.
|
||||
|
||||
The PN5180 SPI protocol is half-duplex:
|
||||
1. Send command frame (NSS held low for entire frame).
|
||||
2. Wait for BUSY high then low (command processed).
|
||||
3. If a response is expected, clock out rx_len bytes in a second frame.
|
||||
"""
|
||||
self._wait_busy()
|
||||
|
||||
# Transmit command
|
||||
self._spi.xfer2(list(tx_data))
|
||||
|
||||
if rx_len == 0:
|
||||
# Write-only command – wait for processing
|
||||
time.sleep(0.001)
|
||||
self._wait_busy()
|
||||
return None
|
||||
|
||||
# Wait for PN5180 to process command (BUSY goes high then low)
|
||||
time.sleep(0.001)
|
||||
self._wait_busy()
|
||||
|
||||
# Read response
|
||||
rx = self._spi.xfer2([0xFF] * rx_len)
|
||||
time.sleep(0.001)
|
||||
self._wait_busy()
|
||||
return bytes(rx)
|
||||
|
||||
# -- register operations ------------------------------------------------
|
||||
|
||||
def read_register(self, addr):
|
||||
"""Read a 32-bit register. Returns int."""
|
||||
resp = self._send_command([CMD_READ_REGISTER, addr], rx_len=4)
|
||||
return int.from_bytes(resp, "little")
|
||||
|
||||
def write_register(self, addr, value):
|
||||
"""Write a 32-bit value to a register."""
|
||||
self._send_command(
|
||||
[
|
||||
CMD_WRITE_REGISTER,
|
||||
addr,
|
||||
value & 0xFF,
|
||||
(value >> 8) & 0xFF,
|
||||
(value >> 16) & 0xFF,
|
||||
(value >> 24) & 0xFF,
|
||||
]
|
||||
)
|
||||
|
||||
def write_register_or_mask(self, addr, mask):
|
||||
self._send_command(
|
||||
[
|
||||
CMD_WRITE_REGISTER_OR_MASK,
|
||||
addr,
|
||||
mask & 0xFF,
|
||||
(mask >> 8) & 0xFF,
|
||||
(mask >> 16) & 0xFF,
|
||||
(mask >> 24) & 0xFF,
|
||||
]
|
||||
)
|
||||
|
||||
def write_register_and_mask(self, addr, mask):
|
||||
self._send_command(
|
||||
[
|
||||
CMD_WRITE_REGISTER_AND_MASK,
|
||||
addr,
|
||||
mask & 0xFF,
|
||||
(mask >> 8) & 0xFF,
|
||||
(mask >> 16) & 0xFF,
|
||||
(mask >> 24) & 0xFF,
|
||||
]
|
||||
)
|
||||
|
||||
# -- EEPROM operations --------------------------------------------------
|
||||
|
||||
def read_eeprom(self, addr, length):
|
||||
"""Read `length` bytes from EEPROM starting at `addr`."""
|
||||
return self._send_command([CMD_READ_EEPROM, addr, length], rx_len=length)
|
||||
|
||||
# -- reset --------------------------------------------------------------
|
||||
|
||||
def reset(self):
|
||||
"""Hardware-reset the PN5180 via the RST pin."""
|
||||
self._rst_line.set_value(self._rst_pin, gpiod.line.Value.INACTIVE)
|
||||
time.sleep(0.01)
|
||||
self._rst_line.set_value(self._rst_pin, gpiod.line.Value.ACTIVE)
|
||||
time.sleep(0.05)
|
||||
self._wait_busy(timeout_s=2.0)
|
||||
# Clear all IRQ flags
|
||||
self.write_register(REG_IRQ_CLEAR, 0xFFFFFFFF)
|
||||
|
||||
# -- version / identity -------------------------------------------------
|
||||
|
||||
def get_product_version(self):
|
||||
data = self.read_eeprom(EEPROM_PRODUCT_VERSION, 2)
|
||||
return f"{data[1]}.{data[0]}"
|
||||
|
||||
def get_firmware_version(self):
|
||||
data = self.read_eeprom(EEPROM_FIRMWARE_VERSION, 2)
|
||||
return f"{data[1]}.{data[0]}"
|
||||
|
||||
def get_eeprom_version(self):
|
||||
data = self.read_eeprom(EEPROM_EEPROM_VERSION, 2)
|
||||
return f"{data[1]}.{data[0]}"
|
||||
|
||||
def get_die_identifier(self):
|
||||
data = self.read_eeprom(EEPROM_DIE_IDENTIFIER, 16)
|
||||
return data.hex()
|
||||
|
||||
|
||||
def run_diagnostics():
|
||||
print("=" * 60)
|
||||
print("PN5180 NFC Reader Diagnostics")
|
||||
print("=" * 60)
|
||||
|
||||
nfc = PN5180()
|
||||
try:
|
||||
# Reset
|
||||
print("\n[1] Hardware reset...")
|
||||
nfc.reset()
|
||||
print(" Reset OK")
|
||||
|
||||
# Version info
|
||||
print("\n[2] Version info (EEPROM)")
|
||||
print(f" Product version : {nfc.get_product_version()}")
|
||||
print(f" Firmware version : {nfc.get_firmware_version()}")
|
||||
print(f" EEPROM version : {nfc.get_eeprom_version()}")
|
||||
print(f" Die identifier : {nfc.get_die_identifier()}")
|
||||
|
||||
# Register dump
|
||||
print("\n[3] Register dump")
|
||||
for addr, name in sorted(REGISTER_NAMES.items()):
|
||||
val = nfc.read_register(addr)
|
||||
print(f" 0x{addr:02X} {name:<24s} = 0x{val:08X}")
|
||||
|
||||
# IRQ status breakdown
|
||||
irq = nfc.read_register(REG_IRQ_STATUS)
|
||||
print(f"\n[4] IRQ status flags (0x{irq:08X})")
|
||||
irq_flags = [
|
||||
(0, "RX_IRQ"),
|
||||
(1, "TX_IRQ"),
|
||||
(2, "IDLE_IRQ"),
|
||||
(3, "MODE_DETECTED_IRQ"),
|
||||
(4, "CARD_ACTIVATED_IRQ"),
|
||||
(5, "STATE_CHANGE_IRQ"),
|
||||
(6, "RFOFF_DET_IRQ"),
|
||||
(7, "RFON_DET_IRQ"),
|
||||
(8, "TX_RFOFF_IRQ"),
|
||||
(9, "TX_RFON_IRQ"),
|
||||
(10, "RF_ACTIVE_ERROR_IRQ"),
|
||||
(14, "LPCD_IRQ"),
|
||||
]
|
||||
for bit, name in irq_flags:
|
||||
state = "SET" if irq & (1 << bit) else "---"
|
||||
print(f" bit {bit:2d}: {name:<28s} [{state}]")
|
||||
|
||||
# RF status
|
||||
rf = nfc.read_register(REG_RF_STATUS)
|
||||
print(f"\n[5] RF status (0x{rf:08X})")
|
||||
tx_rf_on = bool(rf & (1 << 0))
|
||||
rx_en = bool(rf & (1 << 1))
|
||||
print(f" TX RF active : {tx_rf_on}")
|
||||
print(f" RX enabled : {rx_en}")
|
||||
|
||||
# System status
|
||||
sys_stat = nfc.read_register(REG_SYSTEM_STATUS)
|
||||
print(f"\n[6] System status (0x{sys_stat:08X})")
|
||||
|
||||
# Temperature
|
||||
temp_ctrl = nfc.read_register(REG_TEMP_CONTROL)
|
||||
print(f"\n[7] Temp control register (0x{temp_ctrl:08X})")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Diagnostics complete - PN5180 is responding over SPI.")
|
||||
print("=" * 60)
|
||||
|
||||
except TimeoutError as e:
|
||||
print(f"\nERROR: {e}")
|
||||
print("Check wiring and ensure SPI is enabled (dtparam=spi=on in /boot/firmware/config.txt)")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
nfc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_diagnostics()
|
||||
@@ -0,0 +1,570 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PN5180 NFC tag reader — ported from working Pico firmware (pico-nfc-bridge.ino).
|
||||
|
||||
Key learnings from pico-nfc-bridge.ino:
|
||||
- Must call setTransceiveMode() before every SEND_DATA
|
||||
- waitBusy() must wait for HIGH then LOW (not just LOW)
|
||||
- Bambu tags are MIFARE Classic 1K (ISO 14443A), not ISO 15693
|
||||
- SPI at 500kHz, 5µs CS setup, 100µs post-CS delay
|
||||
- MFC_AUTHENTICATE (0x0C) is a PN5180 host command — Crypto1 handled in hardware
|
||||
- HKDF-SHA256 derives per-sector keys from master key + UID
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gpiod
|
||||
import spidev
|
||||
|
||||
BUSY_PIN = 25
|
||||
RST_PIN = 24
|
||||
NSS_PIN = 23 # Manual CS (moved from GPIO8)
|
||||
|
||||
# Bambu Lab MIFARE Classic key derivation constants (from pico-nfc-bridge.ino)
|
||||
BAMBU_MASTER_KEY = bytes(
|
||||
[
|
||||
0x9A,
|
||||
0x75,
|
||||
0x9C,
|
||||
0xF2,
|
||||
0xC4,
|
||||
0xF7,
|
||||
0xCA,
|
||||
0xFF,
|
||||
0x22,
|
||||
0x2C,
|
||||
0xB9,
|
||||
0x76,
|
||||
0x9B,
|
||||
0x41,
|
||||
0xBC,
|
||||
0x96,
|
||||
]
|
||||
)
|
||||
BAMBU_CONTEXT = b"RFID-A\x00" # 7 bytes including null terminator
|
||||
|
||||
# Blocks to read for Bambu tag data
|
||||
BAMBU_BLOCKS = [1, 2, 4, 5]
|
||||
|
||||
|
||||
def hkdf_derive_keys(uid: bytes) -> bytes:
|
||||
"""Derive 96 bytes of MIFARE key material (16 sectors * 6 bytes each).
|
||||
|
||||
Uses HKDF-SHA256 with the Bambu master key as salt and the tag UID as IKM.
|
||||
"""
|
||||
# HKDF-Extract: PRK = HMAC-SHA256(salt=master_key, IKM=uid)
|
||||
prk = hmac.new(BAMBU_MASTER_KEY, uid, hashlib.sha256).digest()
|
||||
|
||||
# HKDF-Expand: generate 96 bytes using context "RFID-A\0"
|
||||
okm = b""
|
||||
t = b""
|
||||
counter = 1
|
||||
while len(okm) < 96:
|
||||
t = hmac.new(prk, t + BAMBU_CONTEXT + bytes([counter]), hashlib.sha256).digest()
|
||||
okm += t
|
||||
counter += 1
|
||||
return okm[:96]
|
||||
|
||||
|
||||
def get_sector_key(keys: bytes, block: int) -> bytes:
|
||||
"""Get the 6-byte key for the sector containing the given block."""
|
||||
sector = block // 4
|
||||
return keys[sector * 6 : sector * 6 + 6]
|
||||
|
||||
|
||||
def _find_gpio_chip():
|
||||
for path in ["/dev/gpiochip4", "/dev/gpiochip0"]:
|
||||
try:
|
||||
chip = gpiod.Chip(path)
|
||||
if "pinctrl" in chip.get_info().label:
|
||||
return chip
|
||||
chip.close()
|
||||
except (FileNotFoundError, PermissionError, OSError):
|
||||
continue
|
||||
raise RuntimeError("No GPIO chip")
|
||||
|
||||
|
||||
class PN5180:
|
||||
def __init__(self):
|
||||
self._chip = _find_gpio_chip()
|
||||
self._lines = self._chip.request_lines(
|
||||
consumer="pn5180",
|
||||
config={
|
||||
BUSY_PIN: gpiod.LineSettings(direction=gpiod.line.Direction.INPUT),
|
||||
RST_PIN: gpiod.LineSettings(
|
||||
direction=gpiod.line.Direction.OUTPUT, output_value=gpiod.line.Value.ACTIVE
|
||||
),
|
||||
NSS_PIN: gpiod.LineSettings(
|
||||
direction=gpiod.line.Direction.OUTPUT, output_value=gpiod.line.Value.ACTIVE
|
||||
),
|
||||
},
|
||||
)
|
||||
self._spi = spidev.SpiDev()
|
||||
self._spi.open(0, 0)
|
||||
self._spi.max_speed_hz = 500_000 # 500kHz like Pico firmware
|
||||
self._spi.mode = 0b00
|
||||
self._spi.no_cs = True
|
||||
|
||||
def close(self):
|
||||
self._spi.close()
|
||||
self._lines.release()
|
||||
self._chip.close()
|
||||
|
||||
def _cs_low(self):
|
||||
self._lines.set_value(NSS_PIN, gpiod.line.Value.INACTIVE)
|
||||
time.sleep(0.000005) # 5µs setup
|
||||
|
||||
def _cs_high(self):
|
||||
self._lines.set_value(NSS_PIN, gpiod.line.Value.ACTIVE)
|
||||
time.sleep(0.000100) # 100µs post-CS delay
|
||||
|
||||
def _wait_busy(self, timeout_s=1.0):
|
||||
"""Wait for BUSY to go HIGH (processing) then LOW (done) — matches Pico firmware."""
|
||||
deadline = time.monotonic() + min(timeout_s, 0.010)
|
||||
# Wait for BUSY HIGH (PN5180 started processing)
|
||||
while self._lines.get_value(BUSY_PIN) != gpiod.line.Value.ACTIVE:
|
||||
if time.monotonic() > deadline:
|
||||
break # Timeout waiting for HIGH — command may have processed already
|
||||
time.sleep(0.00001)
|
||||
# Wait for BUSY LOW (PN5180 done)
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while self._lines.get_value(BUSY_PIN) == gpiod.line.Value.ACTIVE:
|
||||
if time.monotonic() > deadline:
|
||||
raise TimeoutError("BUSY timeout")
|
||||
time.sleep(0.0001)
|
||||
|
||||
def _cmd(self, data):
|
||||
self._cs_low()
|
||||
self._spi.xfer2(list(data))
|
||||
self._cs_high()
|
||||
self._wait_busy()
|
||||
|
||||
def _read_response(self, n):
|
||||
self._cs_low()
|
||||
result = self._spi.xfer2([0xFF] * n)
|
||||
self._cs_high()
|
||||
return result
|
||||
|
||||
# -- Register ops --
|
||||
|
||||
def write_reg(self, reg, val):
|
||||
self._cmd([0x00, reg, val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF])
|
||||
|
||||
def write_reg_or(self, reg, mask):
|
||||
self._cmd([0x01, reg, mask & 0xFF, (mask >> 8) & 0xFF, (mask >> 16) & 0xFF, (mask >> 24) & 0xFF])
|
||||
|
||||
def write_reg_and(self, reg, mask):
|
||||
self._cmd([0x02, reg, mask & 0xFF, (mask >> 8) & 0xFF, (mask >> 16) & 0xFF, (mask >> 24) & 0xFF])
|
||||
|
||||
def read_reg(self, reg):
|
||||
self._cmd([0x04, reg])
|
||||
time.sleep(0.000100) # Extra 100µs before read
|
||||
return int.from_bytes(self._read_response(4), "little")
|
||||
|
||||
def read_eeprom(self, addr, length):
|
||||
self._cmd([0x07, addr, length])
|
||||
time.sleep(0.000100)
|
||||
return bytes(self._read_response(length))
|
||||
|
||||
# -- Commands --
|
||||
|
||||
def reset(self):
|
||||
self._lines.set_value(RST_PIN, gpiod.line.Value.INACTIVE)
|
||||
time.sleep(0.050)
|
||||
self._lines.set_value(RST_PIN, gpiod.line.Value.ACTIVE)
|
||||
time.sleep(0.100)
|
||||
self._wait_busy(2.0)
|
||||
time.sleep(0.050)
|
||||
|
||||
def load_rf_config(self, tx, rx):
|
||||
self.write_reg(0x03, 0xFFFFFFFF) # Clear IRQs first
|
||||
time.sleep(0.000100)
|
||||
self._cmd([0x11, tx, rx])
|
||||
time.sleep(0.010)
|
||||
|
||||
def rf_on(self):
|
||||
self._cmd([0x16, 0x00])
|
||||
time.sleep(0.010)
|
||||
|
||||
def rf_off(self):
|
||||
self._cmd([0x17, 0x00])
|
||||
time.sleep(0.005)
|
||||
|
||||
def set_transceive_mode(self):
|
||||
"""Set SYSTEM_CONFIG command bits to TRANSCEIVE (0x03) — CRITICAL!"""
|
||||
sys_cfg = self.read_reg(0x00)
|
||||
sys_cfg = (sys_cfg & 0xFFFFFFF8) | 0x03
|
||||
self.write_reg(0x00, sys_cfg)
|
||||
|
||||
def send_data(self, data, valid_bits=0x00):
|
||||
self._cs_low()
|
||||
self._spi.xfer2([0x09, valid_bits] + list(data))
|
||||
self._cs_high()
|
||||
time.sleep(0.000100)
|
||||
self._wait_busy()
|
||||
|
||||
def read_data(self, length):
|
||||
self._cmd([0x0A, 0x00])
|
||||
return bytes(self._read_response(length))
|
||||
|
||||
# -- ISO 14443A --
|
||||
|
||||
def activate_type_a(self):
|
||||
"""Full Type A activation: WUPA -> Anticollision -> SELECT. Returns (uid, sak) or None."""
|
||||
# Crypto off, CRC off
|
||||
self.write_reg_and(0x00, 0xFFFFFFBF)
|
||||
self.write_reg_and(0x12, 0xFFFFFFFE)
|
||||
self.write_reg_and(0x19, 0xFFFFFFFE)
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
|
||||
# Reset to IDLE then TRANSCEIVE
|
||||
sys_cfg = self.read_reg(0x00)
|
||||
self.write_reg(0x00, sys_cfg & 0xFFFFFFF8) # IDLE
|
||||
time.sleep(0.001)
|
||||
self.write_reg(0x00, (sys_cfg & 0xFFFFFFF8) | 0x03) # TRANSCEIVE
|
||||
time.sleep(0.002)
|
||||
|
||||
# WUPA (7-bit)
|
||||
self.send_data([0x52], valid_bits=0x07)
|
||||
time.sleep(0.005)
|
||||
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 2 or rx_len == 511:
|
||||
# Try REQA
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
time.sleep(0.002)
|
||||
self.set_transceive_mode()
|
||||
time.sleep(0.002)
|
||||
self.send_data([0x26], valid_bits=0x07)
|
||||
time.sleep(0.005)
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 2 or rx_len == 511:
|
||||
return None
|
||||
|
||||
atqa = self.read_data(2)
|
||||
if atqa[0] == 0xFF or atqa[0] == 0x00:
|
||||
return None
|
||||
|
||||
# Anti-collision Level 1
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
self.set_transceive_mode()
|
||||
time.sleep(0.002)
|
||||
|
||||
self.send_data([0x93, 0x20])
|
||||
time.sleep(0.010)
|
||||
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 5 or rx_len > 64:
|
||||
return None
|
||||
|
||||
uid_buf = self.read_data(5)
|
||||
uid = uid_buf[:4]
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
if bcc != uid_buf[4]:
|
||||
return None
|
||||
|
||||
# SELECT
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
self.set_transceive_mode()
|
||||
time.sleep(0.002)
|
||||
|
||||
# Enable CRC for SELECT
|
||||
self.write_reg_or(0x19, 0x01)
|
||||
self.write_reg_or(0x12, 0x01)
|
||||
|
||||
self.send_data([0x93, 0x70, uid[0], uid[1], uid[2], uid[3], bcc])
|
||||
time.sleep(0.010)
|
||||
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 1:
|
||||
return None
|
||||
|
||||
sak_buf = self.read_data(min(rx_len, 3))
|
||||
sak = sak_buf[0]
|
||||
|
||||
return bytes(uid), sak
|
||||
|
||||
# -- MIFARE Classic --
|
||||
|
||||
def mfc_authenticate(self, block: int, key: bytes, uid: bytes) -> bool:
|
||||
"""MIFARE Classic authentication via PN5180 MFC_AUTHENTICATE (0x0C).
|
||||
|
||||
The PN5180 handles Crypto1 internally. After success, bit 6 of
|
||||
SYSTEM_CONFIG is set (MFC_CRYPTO1_ON) and all subsequent RF
|
||||
communication is encrypted/decrypted by the hardware.
|
||||
|
||||
Args:
|
||||
block: Block number to authenticate
|
||||
key: 6-byte MIFARE Key A
|
||||
uid: 4-byte tag UID
|
||||
Returns:
|
||||
True if authentication succeeded
|
||||
"""
|
||||
# Wait for BUSY LOW before starting
|
||||
deadline = time.monotonic() + 0.100
|
||||
while self._lines.get_value(BUSY_PIN) == gpiod.line.Value.ACTIVE:
|
||||
if time.monotonic() > deadline:
|
||||
return False
|
||||
time.sleep(0.001)
|
||||
|
||||
# MFC_AUTHENTICATE: [0x0C][key 6B][keyType][blockNo][uid 4B] = 13 bytes
|
||||
cmd = [0x0C] + list(key) + [0x60, block] + list(uid[:4])
|
||||
self._cs_low()
|
||||
self._spi.xfer2(cmd)
|
||||
self._cs_high()
|
||||
|
||||
# Wait for BUSY HIGH then LOW (auth can take up to 1s)
|
||||
self._wait_busy(timeout_s=1.0)
|
||||
|
||||
# Read 1-byte response: 0x00 = success
|
||||
self._cs_low()
|
||||
response = self._spi.xfer2([0xFF])
|
||||
self._cs_high()
|
||||
|
||||
return response[0] == 0x00
|
||||
|
||||
def mfc_read_block(self, block: int) -> bytes | None:
|
||||
"""Read a 16-byte MIFARE Classic block (must be authenticated first).
|
||||
|
||||
Returns 16 bytes of block data, or None on failure.
|
||||
"""
|
||||
# Clear IRQs
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
|
||||
# Set transceive mode (Crypto1 stays active from MFC_AUTHENTICATE)
|
||||
self.set_transceive_mode()
|
||||
time.sleep(0.001)
|
||||
|
||||
# Enable TX and RX CRC for encrypted read
|
||||
self.write_reg_or(0x19, 0x01)
|
||||
self.write_reg_or(0x12, 0x01)
|
||||
|
||||
# Send MIFARE READ command: 0x30 + block number
|
||||
self.send_data([0x30, block])
|
||||
time.sleep(0.010)
|
||||
|
||||
# Check RX status
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len != 16:
|
||||
return None
|
||||
|
||||
return self.read_data(16)
|
||||
|
||||
def ntag_read_pages(self, start_page: int, num_pages: int) -> bytes | None:
|
||||
"""Read NTAG pages (4 bytes each). No authentication required.
|
||||
|
||||
Uses NTAG READ command (0x30) which returns 4 pages (16 bytes) at a time.
|
||||
CRC must be disabled for NTAG reads.
|
||||
"""
|
||||
# Disable CRC for NTAG
|
||||
self.write_reg_and(0x19, 0xFFFFFFFE) # TX CRC off
|
||||
self.write_reg_and(0x12, 0xFFFFFFFE) # RX CRC off
|
||||
|
||||
result = bytearray()
|
||||
pages_read = 0
|
||||
while pages_read < num_pages:
|
||||
self.write_reg(0x03, 0xFFFFFFFF) # Clear IRQs
|
||||
self.set_transceive_mode()
|
||||
time.sleep(0.001)
|
||||
|
||||
# READ command: 0x30 + page number → returns 16 bytes (4 pages)
|
||||
self.send_data([0x30, start_page + pages_read])
|
||||
time.sleep(0.005)
|
||||
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 16:
|
||||
return None
|
||||
|
||||
data = self.read_data(16)
|
||||
# Copy only the pages we need
|
||||
pages_to_copy = min(4, num_pages - pages_read)
|
||||
result.extend(data[: pages_to_copy * 4])
|
||||
pages_read += 4 # Always advances by 4 (READ returns 4 pages)
|
||||
|
||||
return bytes(result)
|
||||
|
||||
def reactivate_card(self) -> tuple[bytes, int] | None:
|
||||
"""RF cycle and full re-select of the card. Returns (uid, sak) or None."""
|
||||
self.rf_off()
|
||||
time.sleep(0.010)
|
||||
|
||||
self.write_reg(0x03, 0xFFFFFFFF) # Clear IRQs
|
||||
self.load_rf_config(0x00, 0x80) # ISO 14443A
|
||||
time.sleep(0.005)
|
||||
|
||||
self.rf_on()
|
||||
time.sleep(0.020)
|
||||
|
||||
return self.activate_type_a()
|
||||
|
||||
def read_bambu_tag(self, uid: bytes) -> dict[int, bytes] | None:
|
||||
"""Read Bambu tag data blocks using HKDF-derived keys.
|
||||
|
||||
Args:
|
||||
uid: 4-byte tag UID (from activate_type_a)
|
||||
Returns:
|
||||
Dict mapping block number -> 16 bytes of data, or None on failure
|
||||
"""
|
||||
# Derive per-sector keys from UID
|
||||
keys = hkdf_derive_keys(uid)
|
||||
|
||||
# Clear Crypto1 state and IRQs
|
||||
self.write_reg_and(0x00, 0xFFFFFFBF) # Clear MFC_CRYPTO1_ON (bit 6)
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
|
||||
# Reactivate card (may have timed out)
|
||||
result = self.reactivate_card()
|
||||
if result is None:
|
||||
print(" Failed to reactivate card")
|
||||
return None
|
||||
|
||||
uid_check, _ = result
|
||||
if uid_check != uid:
|
||||
print(f" UID mismatch after reactivation: {uid_check.hex()} != {uid.hex()}")
|
||||
return None
|
||||
|
||||
# Read blocks with per-sector authentication
|
||||
blocks = {}
|
||||
current_sector = -1
|
||||
|
||||
for block in BAMBU_BLOCKS:
|
||||
sector = block // 4
|
||||
|
||||
# Authenticate when entering a new sector
|
||||
if sector != current_sector:
|
||||
key = get_sector_key(keys, block)
|
||||
if not self.mfc_authenticate(block, key, uid):
|
||||
print(f" Auth failed for block {block} (sector {sector})")
|
||||
return None
|
||||
current_sector = sector
|
||||
|
||||
# Read the block
|
||||
data = self.mfc_read_block(block)
|
||||
if data is None:
|
||||
print(f" Read failed for block {block}")
|
||||
return None
|
||||
blocks[block] = data
|
||||
|
||||
return blocks
|
||||
|
||||
def read_ntag(self, uid: bytes) -> bytes | None:
|
||||
"""Read NTAG pages 4-20 (NDEF data area, 68 bytes). No auth needed.
|
||||
|
||||
Used for SpoolEase / OpenPrintTag community tags.
|
||||
"""
|
||||
# Reactivate card
|
||||
result = self.reactivate_card()
|
||||
if result is None:
|
||||
print(" Failed to reactivate card")
|
||||
return None
|
||||
|
||||
return self.ntag_read_pages(start_page=4, num_pages=17)
|
||||
|
||||
|
||||
def _print_hex_dump(data: bytes, label: str, bytes_per_line: int = 16):
|
||||
"""Print a hex dump with ASCII sidebar."""
|
||||
for i in range(0, len(data), bytes_per_line):
|
||||
chunk = data[i : i + bytes_per_line]
|
||||
hex_str = " ".join(f"{b:02X}" for b in chunk)
|
||||
ascii_str = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||
print(f" {label}{i:3d}: {hex_str:<{bytes_per_line * 3}}|{ascii_str}|")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("PN5180 NFC Tag Reader")
|
||||
print(" Supports: Bambu (MIFARE Classic) + NTAG (SpoolEase/OpenPrintTag)")
|
||||
print("=" * 60)
|
||||
|
||||
nfc = PN5180()
|
||||
try:
|
||||
nfc.reset()
|
||||
ver = nfc.read_eeprom(0x10, 2)
|
||||
print(f"[1] Reset OK — product v{ver[1]}.{ver[0]}")
|
||||
|
||||
nfc.load_rf_config(0x00, 0x80) # ISO 14443A
|
||||
time.sleep(0.010)
|
||||
nfc.rf_on()
|
||||
time.sleep(0.030)
|
||||
nfc.set_transceive_mode()
|
||||
|
||||
rf = nfc.read_reg(0x1D)
|
||||
print(f"[2] RF ON (RF_STATUS=0x{rf:08X}, TX_RF={'ON' if rf & 1 else 'OFF'})")
|
||||
|
||||
print("[3] Scanning for tag...")
|
||||
result = nfc.activate_type_a()
|
||||
|
||||
if result is None:
|
||||
print(" No tag found.")
|
||||
sys.exit(1)
|
||||
|
||||
uid, sak = result
|
||||
tag_types = {0x00: "NTAG", 0x08: "MIFARE Classic 1K", 0x18: "MIFARE Classic 4K"}
|
||||
print(f" UID : {uid.hex().upper()}")
|
||||
print(f" SAK : 0x{sak:02X} ({tag_types.get(sak, 'Unknown')})")
|
||||
|
||||
if sak in (0x08, 0x18):
|
||||
# MIFARE Classic 1K or 4K — Bambu Lab tag
|
||||
print("[4] Reading Bambu tag data (MIFARE Classic)...")
|
||||
blocks = nfc.read_bambu_tag(uid)
|
||||
|
||||
if blocks is None:
|
||||
print(" Failed to read tag data.")
|
||||
nfc.rf_off()
|
||||
sys.exit(1)
|
||||
|
||||
print("[5] Tag data:")
|
||||
for block_num in BAMBU_BLOCKS:
|
||||
data = blocks[block_num]
|
||||
hex_str = " ".join(f"{b:02X}" for b in data)
|
||||
ascii_str = "".join(chr(b) if 32 <= b < 127 else "." for b in data)
|
||||
print(f" Block {block_num:2d}: {hex_str} |{ascii_str}|")
|
||||
|
||||
raw = b""
|
||||
for block_num in BAMBU_BLOCKS:
|
||||
raw += blocks[block_num]
|
||||
print(f"\n Raw payload ({len(raw)} bytes): {raw.hex().upper()}")
|
||||
|
||||
elif sak == 0x00:
|
||||
# NTAG — SpoolEase / OpenPrintTag
|
||||
print("[4] Reading NTAG data (pages 4-20)...")
|
||||
ntag_data = nfc.read_ntag(uid)
|
||||
|
||||
if ntag_data is None:
|
||||
print(" Failed to read NTAG data.")
|
||||
nfc.rf_off()
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[5] NTAG data ({len(ntag_data)} bytes):")
|
||||
_print_hex_dump(ntag_data, "page ")
|
||||
|
||||
else:
|
||||
print(f" Unsupported tag type (SAK=0x{sak:02X})")
|
||||
nfc.rf_off()
|
||||
sys.exit(1)
|
||||
|
||||
nfc.rf_off()
|
||||
print("\n" + "=" * 60)
|
||||
print("Tag read complete!")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
nfc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user