mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
+130
-230
@@ -10,42 +10,28 @@ Wiring (from spoolbuddy/README.md):
|
||||
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 NSS -> Pi Pin 16 (GPIO23, manual CS)
|
||||
PN5180 BUSY -> Pi Pin 22 (GPIO25)
|
||||
PN5180 RST -> Pi Pin 18 (GPIO24)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gpiod
|
||||
import spidev
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pin assignments (BCM numbering)
|
||||
# ---------------------------------------------------------------------------
|
||||
BUSY_PIN = 25 # Pin 22
|
||||
RST_PIN = 24 # Pin 18
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "daemon")))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
# ---------------------------------------------------------------------------
|
||||
from pn5180 import ( # noqa: E402
|
||||
NSS_PIN as DRIVER_NSS_PIN,
|
||||
PN5180,
|
||||
RST_PIN as DRIVER_RST_PIN,
|
||||
SPI_BUS as DRIVER_SPI_BUS,
|
||||
SPI_DEVICE as DRIVER_SPI_DEVICE,
|
||||
)
|
||||
|
||||
REG_SYSTEM_CONFIG = 0x00
|
||||
REG_IRQ_ENABLE = 0x01
|
||||
REG_IRQ_STATUS = 0x02
|
||||
@@ -59,25 +45,9 @@ REG_RX_STATUS = 0x13
|
||||
REG_CRC_TX_CONFIG = 0x19
|
||||
REG_RF_STATUS = 0x1D
|
||||
REG_SYSTEM_STATUS = 0x24
|
||||
REG_SIGPRO_CONFIG = 0x1A # Signal Processing Configuration
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -88,181 +58,38 @@ 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.
|
||||
def _check_spi_device_access() -> str:
|
||||
"""Check that the configured spidev exists and can be opened."""
|
||||
spi_path = f"/dev/spidev{DRIVER_SPI_BUS}.{DRIVER_SPI_DEVICE}"
|
||||
if not os.path.exists(spi_path):
|
||||
raise FileNotFoundError(f"SPI device not found: {spi_path}")
|
||||
|
||||
RPi 5 uses gpiochip4, RPi 4 uses gpiochip0.
|
||||
fd = os.open(spi_path, os.O_RDWR)
|
||||
os.close(fd)
|
||||
return spi_path
|
||||
|
||||
|
||||
def _self_test_control_pins(nfc: PN5180):
|
||||
"""Toggle NSS and RST pins and print observed line state.
|
||||
Uses public set_pin/get_pin methods to avoid direct access to driver internals.
|
||||
"""
|
||||
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")
|
||||
for pin_name, pin_num in (("NSS", DRIVER_NSS_PIN), ("RST", DRIVER_RST_PIN)):
|
||||
nfc.set_pin(pin_num, True)
|
||||
time.sleep(0.005)
|
||||
active_state = nfc.get_pin(pin_num)
|
||||
|
||||
nfc.set_pin(pin_num, False)
|
||||
time.sleep(0.005)
|
||||
inactive_state = nfc.get_pin(pin_num)
|
||||
|
||||
class PN5180:
|
||||
"""Low-level driver for the PN5180 NFC frontend over SPI."""
|
||||
# Restore idle-high level used by this driver.
|
||||
nfc.set_pin(pin_num, True)
|
||||
|
||||
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)},
|
||||
print(
|
||||
f" {pin_name} pin {pin_num}: "
|
||||
f"ACTIVE->{'ACTIVE' if active_state else 'INACTIVE'}, "
|
||||
f"INACTIVE->{'ACTIVE' if inactive_state else 'INACTIVE'}"
|
||||
)
|
||||
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():
|
||||
@@ -270,29 +97,73 @@ def run_diagnostics():
|
||||
print("PN5180 NFC Reader Diagnostics")
|
||||
print("=" * 60)
|
||||
|
||||
nfc = PN5180()
|
||||
nfc = None
|
||||
try:
|
||||
print("\n[1] SPI device check...")
|
||||
spi_path = _check_spi_device_access()
|
||||
print(f" SPI device OK: {spi_path}")
|
||||
|
||||
nfc = PN5180()
|
||||
|
||||
print("\n[2] Control pin self-test (NSS/RST)...")
|
||||
_self_test_control_pins(nfc)
|
||||
|
||||
# Reset
|
||||
print("\n[1] Hardware reset...")
|
||||
print("\n[3] 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()}")
|
||||
print("\n[4] Version info (EEPROM)")
|
||||
product = nfc.read_eeprom(EEPROM_PRODUCT_VERSION, 2)
|
||||
firmware = nfc.read_eeprom(EEPROM_FIRMWARE_VERSION, 2)
|
||||
eeprom = nfc.read_eeprom(EEPROM_EEPROM_VERSION, 2)
|
||||
die_id = nfc.read_eeprom(EEPROM_DIE_IDENTIFIER, 16)
|
||||
|
||||
print(f" Product version : {product[1]}.{product[0]}")
|
||||
print(f" Firmware version : {firmware[1]}.{firmware[0]}")
|
||||
print(f" EEPROM version : {eeprom[1]}.{eeprom[0]}")
|
||||
print(f" Die identifier : {die_id.hex()}")
|
||||
|
||||
# Register dump
|
||||
print("\n[3] Register dump")
|
||||
for addr, name in sorted(REGISTER_NAMES.items()):
|
||||
val = nfc.read_register(addr)
|
||||
print("\n[5] Register dump")
|
||||
# Use register names from the script (not in pn5180.py)
|
||||
REGISTER_NAMES_DUMP = {
|
||||
0x00: "SYSTEM_CONFIG",
|
||||
0x01: "IRQ_ENABLE",
|
||||
0x02: "IRQ_STATUS",
|
||||
0x03: "IRQ_CLEAR",
|
||||
0x04: "TRANSCEIVE_CONTROL",
|
||||
0x0C: "TIMER1_RELOAD",
|
||||
0x0F: "TIMER1_CONFIG",
|
||||
0x11: "RX_WAIT_CONFIG",
|
||||
0x12: "CRC_RX_CONFIG",
|
||||
0x13: "RX_STATUS",
|
||||
0x19: "CRC_TX_CONFIG",
|
||||
0x1A: "SIGPRO_CONFIG",
|
||||
0x1D: "RF_STATUS",
|
||||
0x24: "SYSTEM_STATUS",
|
||||
0x25: "TEMP_CONTROL",
|
||||
}
|
||||
for addr, name in sorted(REGISTER_NAMES_DUMP.items()):
|
||||
val = nfc.read_reg(addr)
|
||||
print(f" 0x{addr:02X} {name:<24s} = 0x{val:08X}")
|
||||
|
||||
# SIGPRO_CONFIG ISO/IEC14443 mode check
|
||||
sigpro_val = nfc.read_reg(REG_SIGPRO_CONFIG)
|
||||
sigpro_mode = (sigpro_val >> 0) & 0b111
|
||||
baudrate_map = {
|
||||
0b100: "106 kBd (ISO/IEC14443 type A/B)",
|
||||
0b101: "212 kBd (FeliCa 212 kBd)",
|
||||
0b110: "424 kBd (FeliCa 424 kBd)",
|
||||
0b111: "848 kBd",
|
||||
}
|
||||
baudrate_str = baudrate_map.get(sigpro_mode, "Unknown or reserved")
|
||||
print(f"\n[5b] SIGPRO_CONFIG (0x1A) bits 2:0 = 0b{sigpro_mode:03b} ({baudrate_str})")
|
||||
|
||||
# IRQ status breakdown
|
||||
irq = nfc.read_register(REG_IRQ_STATUS)
|
||||
print(f"\n[4] IRQ status flags (0x{irq:08X})")
|
||||
irq = nfc.read_reg(REG_IRQ_STATUS)
|
||||
print(f"\n[6] IRQ status flags (0x{irq:08X})")
|
||||
irq_flags = [
|
||||
(0, "RX_IRQ"),
|
||||
(1, "TX_IRQ"),
|
||||
@@ -312,20 +183,48 @@ def run_diagnostics():
|
||||
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})")
|
||||
rf = nfc.read_reg(REG_RF_STATUS)
|
||||
print(f"\n[7] 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})")
|
||||
sys_stat = nfc.read_reg(REG_SYSTEM_STATUS)
|
||||
print(f"\n[8] System status (0x{sys_stat:08X})")
|
||||
|
||||
# System status bit breakdown
|
||||
sys_stat_bits = [
|
||||
(9, "LDO_TVDD_OK"),
|
||||
(8, "PARAMETER_ERROR"),
|
||||
(7, "SYNTAX_ERROR"),
|
||||
(6, "SEMANTIC_ERROR"),
|
||||
(5, "STBY_PREVENT_RFLD"),
|
||||
(4, "BOOT_TEMP"),
|
||||
(3, "BOOT_SOFT_RESET"),
|
||||
(2, "BOOT_WUC"),
|
||||
(1, "BOOT_RFLD"),
|
||||
(0, "BOOT_POR"),
|
||||
]
|
||||
for bit, symbol in sys_stat_bits:
|
||||
state = "SET" if sys_stat & (1 << bit) else "---"
|
||||
print(f" bit {bit:2d}: {symbol:<18s} [{state}]")
|
||||
|
||||
# Temperature
|
||||
temp_ctrl = nfc.read_register(REG_TEMP_CONTROL)
|
||||
print(f"\n[7] Temp control register (0x{temp_ctrl:08X})")
|
||||
temp_ctrl = nfc.read_reg(REG_TEMP_CONTROL)
|
||||
print(f"\n[9] Temp control register (0x{temp_ctrl:08X})")
|
||||
|
||||
# TEMP_DELTA bits 1:0
|
||||
temp_delta = (temp_ctrl >> 0) & 0b11
|
||||
temp_delta_map = {
|
||||
0b00: "85°C",
|
||||
0b01: "115°C",
|
||||
0b10: "125°C",
|
||||
0b11: "135°C",
|
||||
}
|
||||
temp_delta_str = temp_delta_map.get(temp_delta, "Unknown")
|
||||
print(f" bits 1:0 TEMP_DELTA = 0b{temp_delta:02b} ({temp_delta_str})")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Diagnostics complete - PN5180 is responding over SPI.")
|
||||
@@ -339,7 +238,8 @@ def run_diagnostics():
|
||||
print(f"\nERROR: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
nfc.close()
|
||||
if nfc is not None:
|
||||
nfc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+123
-49
@@ -12,15 +12,30 @@ Key learnings from pico-nfc-bridge.ino:
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import gpiod
|
||||
import spidev
|
||||
|
||||
BUSY_PIN = 25
|
||||
RST_PIN = 24
|
||||
NSS_PIN = 23 # Manual CS (moved from GPIO8)
|
||||
|
||||
def _env_int(name: str, default: int) -> int:
|
||||
value = os.environ.get(name)
|
||||
if value is None or value == "":
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
BUSY_PIN = _env_int("SPOOLBUDDY_NFC_BUSY_PIN", 25)
|
||||
RST_PIN = _env_int("SPOOLBUDDY_NFC_RST_PIN", 24)
|
||||
NSS_PIN = _env_int("SPOOLBUDDY_NFC_NSS_PIN", 23) # Manual CS by default
|
||||
SPI_BUS = _env_int("SPOOLBUDDY_NFC_SPI_BUS", 0)
|
||||
SPI_DEVICE = _env_int("SPOOLBUDDY_NFC_SPI_DEVICE", 0)
|
||||
SPI_SPEED_HZ = _env_int("SPOOLBUDDY_NFC_SPI_SPEED_HZ", 500_000)
|
||||
|
||||
# Bambu Lab MIFARE Classic key derivation constants (from pico-nfc-bridge.ino)
|
||||
BAMBU_MASTER_KEY = bytes(
|
||||
@@ -102,8 +117,8 @@ class PN5180:
|
||||
},
|
||||
)
|
||||
self._spi = spidev.SpiDev()
|
||||
self._spi.open(0, 0)
|
||||
self._spi.max_speed_hz = 500_000 # 500kHz like Pico firmware
|
||||
self._spi.open(SPI_BUS, SPI_DEVICE)
|
||||
self._spi.max_speed_hz = SPI_SPEED_HZ
|
||||
self._spi.mode = 0b00
|
||||
self._spi.no_cs = True
|
||||
|
||||
@@ -357,37 +372,68 @@ class PN5180:
|
||||
|
||||
return self.read_data(16)
|
||||
|
||||
def _ntag_reactivate(self) -> bool:
|
||||
"""Full hardware reset + RF activation for NTAG re-selection between reads.
|
||||
|
||||
The PN5180 enters an unrecoverable state after an NTAG READ command —
|
||||
simple RF off/on cycles cannot re-select the tag. A full GPIO hardware
|
||||
reset is required to clear the internal transceiver state.
|
||||
"""
|
||||
self.reset()
|
||||
self.load_rf_config(0x00, 0x80) # ISO 14443A
|
||||
time.sleep(0.010)
|
||||
self.rf_on()
|
||||
time.sleep(0.030)
|
||||
self.set_transceive_mode()
|
||||
|
||||
return self.activate_type_a() is not None
|
||||
|
||||
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.
|
||||
The PN5180 cannot issue consecutive NTAG READs in one session — the card
|
||||
stops responding after the first READ. We do a full RF cycle and re-select
|
||||
with extended timing between each 4-page batch.
|
||||
"""
|
||||
# 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)
|
||||
if pages_read > 0:
|
||||
if not self._ntag_reactivate():
|
||||
print(f" Failed to reactivate card before page {start_page + pages_read}")
|
||||
return None
|
||||
|
||||
# READ command: 0x30 + page number → returns 16 bytes (4 pages)
|
||||
# Setup: Crypto1 off, TX CRC on, RX CRC off, IDLE→TRANSCEIVE
|
||||
self.write_reg_and(0x00, 0xFFFFFFBF)
|
||||
self.write_reg_or(0x19, 0x01)
|
||||
self.write_reg_and(0x12, 0xFFFFFFFE)
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
|
||||
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)
|
||||
|
||||
# READ command: 0x30 + page → returns 16 bytes (4 pages)
|
||||
self.send_data([0x30, start_page + pages_read])
|
||||
time.sleep(0.005)
|
||||
time.sleep(0.010)
|
||||
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 16:
|
||||
# Tag may have fewer pages than requested (e.g. MIFARE Ultralight
|
||||
# has only 16 pages). Return what we have so far.
|
||||
if result:
|
||||
return bytes(result)
|
||||
print(f" NTAG read page {start_page + pages_read}: rx_len={rx_len} (expected >=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)
|
||||
pages_read += 4
|
||||
|
||||
return bytes(result)
|
||||
|
||||
@@ -459,38 +505,40 @@ class PN5180:
|
||||
"""Write 4 bytes to a single NTAG page.
|
||||
|
||||
NTAG WRITE command: 0xA2 + page_number + 4 bytes data.
|
||||
CRC disabled (same as reads). Returns True on ACK (0x0A).
|
||||
TX CRC on (tag requires it). Always returns True — the 4-bit ACK
|
||||
cannot be captured by the PN5180, so verification is deferred to
|
||||
ntag_write_pages() which reads back all written data.
|
||||
"""
|
||||
if len(data) != 4:
|
||||
return False
|
||||
|
||||
# Disable CRC
|
||||
self.write_reg_and(0x19, 0xFFFFFFFE) # TX CRC off
|
||||
# Crypto1 off, TX CRC on (tag expects CRC), RX CRC off (ACK is 4-bit, no CRC)
|
||||
self.write_reg_and(0x00, 0xFFFFFFBF) # Crypto1 off
|
||||
self.write_reg_or(0x19, 0x01) # TX CRC on
|
||||
self.write_reg_and(0x12, 0xFFFFFFFE) # RX CRC off
|
||||
self.write_reg(0x03, 0xFFFFFFFF) # Clear IRQs
|
||||
|
||||
# Clear IRQs and set transceive mode
|
||||
self.write_reg(0x03, 0xFFFFFFFF)
|
||||
self.set_transceive_mode()
|
||||
# Reset state machine: 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)
|
||||
|
||||
# WRITE command: 0xA2 + page + 4 bytes
|
||||
self.send_data([0xA2, page] + list(data))
|
||||
time.sleep(0.005)
|
||||
|
||||
# Check for ACK: NTAG ACK is 4-bit 0x0A
|
||||
rx_status = self.read_reg(0x13)
|
||||
rx_len = rx_status & 0x1FF
|
||||
if rx_len < 1:
|
||||
return False
|
||||
|
||||
ack = self.read_data(1)
|
||||
return ack[0] == 0x0A
|
||||
# PN5180 cannot reliably capture the 4-bit ACK, so always return True
|
||||
return True
|
||||
|
||||
def ntag_write_pages(self, start_page: int, data: bytes) -> bool:
|
||||
"""Write data to consecutive NTAG pages starting at start_page.
|
||||
|
||||
Pads last chunk to 4 bytes. Verifies by reading back.
|
||||
Returns True if write + verify succeeded.
|
||||
Pads last chunk to 4 bytes. Verification is skipped — the PN5180
|
||||
cannot reliably read back NTAG pages after a batch write (the
|
||||
second READ command gets no response). The write itself is reliable:
|
||||
the tag ACKs each page (RX SOF detected on every response).
|
||||
"""
|
||||
# Pad to 4-byte boundary
|
||||
padded = bytearray(data)
|
||||
@@ -498,25 +546,17 @@ class PN5180:
|
||||
padded.append(0x00)
|
||||
|
||||
# Write page by page
|
||||
num_pages = len(padded) // 4
|
||||
for i in range(0, len(padded), 4):
|
||||
page = start_page + (i // 4)
|
||||
chunk = bytes(padded[i : i + 4])
|
||||
if not self.ntag_write_page(page, chunk):
|
||||
print(f" NTAG write failed at page {page} (of {num_pages} pages)")
|
||||
return False
|
||||
time.sleep(0.002)
|
||||
|
||||
# Reactivate card for verification read
|
||||
result = self.reactivate_card()
|
||||
if result is None:
|
||||
return False
|
||||
|
||||
# Read back and verify
|
||||
num_pages = len(padded) // 4
|
||||
readback = self.ntag_read_pages(start_page, num_pages)
|
||||
if readback is None:
|
||||
return False
|
||||
|
||||
return readback[: len(data)] == data
|
||||
print(f" NTAG write complete ({num_pages} pages)")
|
||||
return True
|
||||
|
||||
def read_ntag(self, uid: bytes) -> bytes | None:
|
||||
"""Read NTAG pages 4-20 (NDEF data area, 68 bytes). No auth needed.
|
||||
@@ -547,7 +587,36 @@ def main():
|
||||
print(" Supports: Bambu (MIFARE Classic) + NTAG (SpoolEase/OpenPrintTag)")
|
||||
print("=" * 60)
|
||||
|
||||
nfc = PN5180()
|
||||
try:
|
||||
nfc = PN5180()
|
||||
except (OSError, RuntimeError, PermissionError) as e:
|
||||
print(f"\nERROR: Failed to initialize NFC reader: {e}")
|
||||
|
||||
# Check if it's a resource conflict
|
||||
error_str = str(e).lower()
|
||||
is_resource_conflict = any(x in error_str for x in ["busy", "resource", "already in use", "permission denied"])
|
||||
|
||||
if is_resource_conflict:
|
||||
print("\nGPIO/SPI RESOURCE IN USE: Another process is using the NFC reader.")
|
||||
print("This typically means the SpoolBuddy daemon is already reading tags.")
|
||||
print("\nTo run this diagnostic, stop the daemon first:")
|
||||
print(" sudo systemctl stop bambuddy")
|
||||
print(" # Run diagnostic")
|
||||
print(" .../read_tag.py")
|
||||
print(" # Restart daemon when done:")
|
||||
print(" sudo systemctl start bambuddy")
|
||||
else:
|
||||
print("\nCheck:")
|
||||
print(" - Correct GPIO chip is available (/dev/gpiochip0 or /dev/gpiochip4)")
|
||||
print(f" - SPI device is available (SPI_BUS={SPI_BUS}, SPI_DEVICE={SPI_DEVICE})")
|
||||
print(" - GPIO and SPI permissions are correct")
|
||||
# Only print full traceback for unexpected errors
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
nfc.reset()
|
||||
ver = nfc.read_eeprom(0x10, 2)
|
||||
@@ -570,7 +639,12 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
uid, sak = result
|
||||
tag_types = {0x00: "NTAG", 0x08: "MIFARE Classic 1K", 0x18: "MIFARE Classic 4K"}
|
||||
tag_types = {
|
||||
0x00: "NTAG",
|
||||
0x04: "NTAG (MIFARE Ultralight)",
|
||||
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')})")
|
||||
|
||||
@@ -596,8 +670,8 @@ def main():
|
||||
raw += blocks[block_num]
|
||||
print(f"\n Raw payload ({len(raw)} bytes): {raw.hex().upper()}")
|
||||
|
||||
elif sak == 0x00:
|
||||
# NTAG — SpoolEase / OpenPrintTag
|
||||
elif sak in (0x00, 0x04):
|
||||
# NTAG / MIFARE Ultralight family — SpoolEase / OpenPrintTag
|
||||
print("[4] Reading NTAG data (pages 4-20)...")
|
||||
ntag_data = nfc.read_ntag(uid)
|
||||
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NAU7802 Scale Diagnostic — ported from SpoolBuddy Rust firmware.
|
||||
"""NAU7802 Scale Diagnostic.
|
||||
|
||||
I2C address: 0x2A
|
||||
Bus: /dev/i2c-0 (GPIO0/GPIO1 on RPi)
|
||||
Bus: /dev/i2c-1 (GPIO2/GPIO3 on RPi)
|
||||
"""
|
||||
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import smbus2
|
||||
|
||||
I2C_BUS = 0
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "daemon")))
|
||||
|
||||
from nau7802 import NAU7802
|
||||
|
||||
|
||||
def _env_int(name: str, default: int) -> int:
|
||||
value = os.environ.get(name)
|
||||
if value is None or value == "":
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
I2C_BUS = _env_int("SPOOLBUDDY_I2C_BUS", 1)
|
||||
NAU7802_ADDR = 0x2A
|
||||
|
||||
# Register addresses
|
||||
@@ -37,93 +52,109 @@ PU_OSCS = 0x40 # Oscillator select
|
||||
PU_AVDDS = 0x80 # AVDD source select
|
||||
|
||||
|
||||
class NAU7802:
|
||||
def __init__(self, bus=I2C_BUS, addr=NAU7802_ADDR):
|
||||
self._bus = smbus2.SMBus(bus)
|
||||
self._addr = addr
|
||||
|
||||
def close(self):
|
||||
self._bus.close()
|
||||
|
||||
def read_reg(self, reg: int) -> int:
|
||||
return self._bus.read_byte_data(self._addr, reg)
|
||||
|
||||
def write_reg(self, reg: int, val: int):
|
||||
self._bus.write_byte_data(self._addr, reg, val & 0xFF)
|
||||
|
||||
def init(self):
|
||||
"""Initialize NAU7802 — matches Rust firmware init sequence."""
|
||||
revision = self.read_reg(REG_REVISION)
|
||||
print(f" Revision: 0x{revision:02X}")
|
||||
|
||||
# Reset
|
||||
self.write_reg(REG_PU_CTRL, PU_RR)
|
||||
time.sleep(0.010)
|
||||
self.write_reg(REG_PU_CTRL, 0x00)
|
||||
|
||||
# Power up digital + analog
|
||||
self.write_reg(REG_PU_CTRL, PU_PUD | PU_PUA)
|
||||
|
||||
# Wait for power-up ready
|
||||
for _ in range(100):
|
||||
status = self.read_reg(REG_PU_CTRL)
|
||||
if status & PU_PUR:
|
||||
print(" Power-up ready")
|
||||
break
|
||||
time.sleep(0.001)
|
||||
else:
|
||||
raise TimeoutError("NAU7802 power-up timeout")
|
||||
|
||||
# Sample rate: 10 SPS (bits 6:4 of CTRL2 = 0b000)
|
||||
ctrl2 = self.read_reg(REG_CTRL2)
|
||||
self.write_reg(REG_CTRL2, (ctrl2 & 0x8F) | (0 << 4))
|
||||
print(" Sample rate: 10 SPS")
|
||||
|
||||
# Gain: 128x (bits 2:0 of CTRL1 = 0b111)
|
||||
ctrl1 = self.read_reg(REG_CTRL1)
|
||||
self.write_reg(REG_CTRL1, (ctrl1 & 0xF8) | 7)
|
||||
print(" Gain: 128x")
|
||||
|
||||
# LDO: 3.3V (bits 5:3 of CTRL1 = 0b100)
|
||||
ctrl1 = self.read_reg(REG_CTRL1)
|
||||
self.write_reg(REG_CTRL1, (ctrl1 & 0xC7) | (0b100 << 3))
|
||||
|
||||
# Enable internal LDO (bit 7 of CTRL1)
|
||||
ctrl1 = self.read_reg(REG_CTRL1)
|
||||
self.write_reg(REG_CTRL1, ctrl1 | 0x80)
|
||||
print(" LDO: 3.3V (internal)")
|
||||
|
||||
# Start conversion cycle
|
||||
pu_ctrl = self.read_reg(REG_PU_CTRL)
|
||||
self.write_reg(REG_PU_CTRL, pu_ctrl | PU_CS)
|
||||
print(" Conversion started")
|
||||
|
||||
def data_ready(self) -> bool:
|
||||
return bool(self.read_reg(REG_PU_CTRL) & PU_CR)
|
||||
|
||||
def read_raw(self) -> int:
|
||||
"""Read 24-bit signed ADC value."""
|
||||
b2 = self.read_reg(REG_ADCO_B2)
|
||||
b1 = self.read_reg(REG_ADCO_B1)
|
||||
b0 = self.read_reg(REG_ADCO_B0)
|
||||
raw = (b2 << 16) | (b1 << 8) | b0
|
||||
# Sign extend 24-bit to 32-bit
|
||||
if raw & 0x800000:
|
||||
raw |= 0xFF000000
|
||||
raw = struct.unpack("i", struct.pack("I", raw))[0]
|
||||
return raw
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("NAU7802 Scale Diagnostic")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"Configured bus: {I2C_BUS}, address: 0x{NAU7802_ADDR:02X}")
|
||||
|
||||
# Probe both common I2C buses and show where devices are actually visible.
|
||||
found_by_bus: dict[int, list[int]] = {}
|
||||
for bus_num in (0, 1):
|
||||
found_by_bus[bus_num] = []
|
||||
try:
|
||||
with smbus2.SMBus(bus_num) as probe_bus:
|
||||
for addr in range(0x03, 0x78):
|
||||
try:
|
||||
probe_bus.read_byte(addr)
|
||||
found_by_bus[bus_num].append(addr)
|
||||
except OSError:
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except PermissionError:
|
||||
continue
|
||||
|
||||
for bus_num, addrs in found_by_bus.items():
|
||||
if addrs:
|
||||
pretty = " ".join(f"0x{a:02X}" for a in addrs)
|
||||
print(f"Bus {bus_num} devices: {pretty}")
|
||||
else:
|
||||
print(f"Bus {bus_num} devices: (none)")
|
||||
|
||||
if NAU7802_ADDR not in found_by_bus.get(I2C_BUS, []):
|
||||
for alt in (1, 0):
|
||||
if alt != I2C_BUS and NAU7802_ADDR in found_by_bus.get(alt, []):
|
||||
print(f"\nHint: NAU7802 (0x{NAU7802_ADDR:02X}) appears on bus {alt}, not configured bus {I2C_BUS}.")
|
||||
print(f"Try: SPOOLBUDDY_I2C_BUS={alt} .../scale_diag.py")
|
||||
break
|
||||
|
||||
scale = NAU7802()
|
||||
try:
|
||||
print("[1] Initializing...")
|
||||
|
||||
scale.init()
|
||||
|
||||
# Print key interpreted config values
|
||||
revision = scale.read_reg(REG_REVISION)
|
||||
revision_id = revision & 0x0F
|
||||
print(f" Revision ID: {revision_id}")
|
||||
|
||||
# PU_CTRL bit 7: AVDD source select
|
||||
pu_ctrl = scale.read_reg(REG_PU_CTRL)
|
||||
avdds = (pu_ctrl >> 7) & 0x1
|
||||
avdds_str = "Internal LDO" if avdds == 1 else "AVDD pin input"
|
||||
print(f" AVDD source: {avdds_str}")
|
||||
ctrl1 = scale.read_reg(REG_CTRL1)
|
||||
vldo = (ctrl1 >> 3) & 0b111
|
||||
vldo_map = {
|
||||
0b111: "2.4V",
|
||||
0b110: "2.7V",
|
||||
0b101: "3.0V",
|
||||
0b100: "3.3V",
|
||||
0b011: "3.6V",
|
||||
0b010: "3.9V",
|
||||
0b001: "4.2V",
|
||||
0b000: "4.5V",
|
||||
}
|
||||
vldo_str = vldo_map.get(vldo, f"Unknown ({vldo})")
|
||||
gain = ctrl1 & 0b111
|
||||
gain_map = {
|
||||
0b000: "1x",
|
||||
0b001: "2x",
|
||||
0b010: "4x",
|
||||
0b011: "8x",
|
||||
0b100: "16x",
|
||||
0b101: "32x",
|
||||
0b110: "64x",
|
||||
0b111: "128x",
|
||||
}
|
||||
gain_str = gain_map.get(gain, f"Unknown ({gain})")
|
||||
print(f" LDO setting (VLDO): {vldo_str}")
|
||||
print(f" Gain setting: {gain_str}")
|
||||
ctrl2 = scale.read_reg(REG_CTRL2)
|
||||
sps = (ctrl2 >> 4) & 0b111
|
||||
sps_map = {
|
||||
0b000: "10 SPS",
|
||||
0b001: "20 SPS",
|
||||
0b010: "40 SPS",
|
||||
0b011: "80 SPS",
|
||||
0b100: "320 SPS",
|
||||
}
|
||||
sps_str = sps_map.get(sps, f"Unknown ({sps})")
|
||||
print(f" Sample rate: {sps_str}")
|
||||
adc = scale.read_reg(REG_ADC)
|
||||
chopper = (adc >> 4) & 0b11
|
||||
chopper_str = {0b00: "Enabled", 0b01: "Enabled", 0b10: "Enabled", 0b11: "Disabled"}.get(
|
||||
chopper, f"Unknown ({chopper})"
|
||||
)
|
||||
print(f" ADC chopper: {chopper_str}")
|
||||
pga = scale.read_reg(REG_PGA)
|
||||
low_esr = (pga >> 6) & 0x1
|
||||
low_esr_str = "Enabled" if low_esr == 0 else "Disabled"
|
||||
print(f" PGA low-ESR caps: {low_esr_str}")
|
||||
|
||||
print("[2] Waiting for first reading...")
|
||||
for _ in range(200):
|
||||
if scale.data_ready():
|
||||
@@ -133,7 +164,7 @@ def main():
|
||||
print(" Timeout waiting for data ready")
|
||||
sys.exit(1)
|
||||
|
||||
print("[3] Reading 10 samples (10 SPS = ~1 second)...")
|
||||
print(" Reading 10 samples (10 SPS = ~1 second)...")
|
||||
readings = []
|
||||
for i in range(10):
|
||||
# Wait for data ready
|
||||
@@ -158,9 +189,36 @@ def main():
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
import traceback
|
||||
is_known_error = False
|
||||
|
||||
if isinstance(e, OSError):
|
||||
if e.errno == 16: # Device or resource busy
|
||||
is_known_error = True
|
||||
print("\nI2C DEVICE BUSY (Errno 16): Another process is using the I2C bus.")
|
||||
print("This typically means the SpoolBuddy daemon is already reading the scale.")
|
||||
print("\nTo run this diagnostic, stop the daemon first:")
|
||||
print(" sudo systemctl stop bambuddy")
|
||||
print(" # Run diagnostic")
|
||||
print(" .../scale_diag.py")
|
||||
print(" # Restart daemon when done:")
|
||||
print(" sudo systemctl start bambuddy")
|
||||
elif e.errno == 121:
|
||||
is_known_error = True
|
||||
print("\nI2C NACK (Errno 121): the device did not acknowledge reads at 0x2A.")
|
||||
print("Check:")
|
||||
print(" - NAU7802 SDA/SCL are on the configured bus pins")
|
||||
print(" - 3.3V and GND are correct and stable")
|
||||
print(" - Sensor address is really 0x2A")
|
||||
print(" - No loose wire or swapped SDA/SCL")
|
||||
else:
|
||||
print(f"\nI2C Error (Errno {e.errno}): {e}")
|
||||
|
||||
# Only print full traceback for unexpected errors
|
||||
if not is_known_error:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
scale.close()
|
||||
|
||||
Reference in New Issue
Block a user