mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
[Feature] Spoolbuddy Fixes and Improvements (#787)
[Feature] Spoolbuddy Fixes and Improvements (#787)
This commit is contained in:
@@ -10,7 +10,7 @@ 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)
|
||||
"""
|
||||
@@ -26,6 +26,7 @@ import spidev
|
||||
# ---------------------------------------------------------------------------
|
||||
BUSY_PIN = 25 # Pin 22
|
||||
RST_PIN = 24 # Pin 18
|
||||
NSS_PIN = 23 # Pin 16 (manual CS)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPI command instruction codes (NXP PN5180 datasheet Table 5)
|
||||
@@ -109,37 +110,67 @@ def _find_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):
|
||||
def __init__(
|
||||
self,
|
||||
spi_bus=0,
|
||||
spi_device=0,
|
||||
spi_speed_hz=500_000,
|
||||
busy_pin=BUSY_PIN,
|
||||
rst_pin=RST_PIN,
|
||||
nss_pin=NSS_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,
|
||||
)
|
||||
},
|
||||
)
|
||||
try:
|
||||
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._nss_line = self._chip.request_lines(
|
||||
consumer="pn5180-diag",
|
||||
config={
|
||||
nss_pin: gpiod.LineSettings(
|
||||
direction=gpiod.line.Direction.OUTPUT,
|
||||
output_value=gpiod.line.Value.ACTIVE,
|
||||
)
|
||||
},
|
||||
)
|
||||
except OSError as e:
|
||||
self._chip.close()
|
||||
if getattr(e, "errno", None) == 16:
|
||||
raise RuntimeError(
|
||||
"GPIO line is busy (another process owns PN5180 pins). "
|
||||
"Stop spoolbuddy service before running diagnostics: "
|
||||
"sudo systemctl stop spoolbuddy"
|
||||
) from e
|
||||
raise
|
||||
self._busy_pin = busy_pin
|
||||
self._rst_pin = rst_pin
|
||||
self._nss_pin = nss_pin
|
||||
|
||||
# SPI setup – mode 0 (CPOL=0, CPHA=0), MSB first
|
||||
# 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
|
||||
self._spi.no_cs = True
|
||||
|
||||
def close(self):
|
||||
self._spi.close()
|
||||
self._busy_line.release()
|
||||
self._rst_line.release()
|
||||
self._nss_line.release()
|
||||
self._chip.close()
|
||||
|
||||
# -- low-level helpers --------------------------------------------------
|
||||
@@ -155,6 +186,14 @@ class PN5180:
|
||||
raise TimeoutError("PN5180 BUSY line did not go low")
|
||||
time.sleep(0.001)
|
||||
|
||||
def _cs_low(self):
|
||||
self._nss_line.set_value(self._nss_pin, gpiod.line.Value.INACTIVE)
|
||||
time.sleep(0.000005)
|
||||
|
||||
def _cs_high(self):
|
||||
self._nss_line.set_value(self._nss_pin, gpiod.line.Value.ACTIVE)
|
||||
time.sleep(0.000100)
|
||||
|
||||
def _send_command(self, tx_data, rx_len=0):
|
||||
"""Send an SPI command frame and optionally read a response frame.
|
||||
|
||||
@@ -165,11 +204,13 @@ class PN5180:
|
||||
"""
|
||||
self._wait_busy()
|
||||
|
||||
# Transmit command
|
||||
# Transmit command (manual CS)
|
||||
self._cs_low()
|
||||
self._spi.xfer2(list(tx_data))
|
||||
self._cs_high()
|
||||
|
||||
if rx_len == 0:
|
||||
# Write-only command – wait for processing
|
||||
# Write-only command - wait for processing
|
||||
time.sleep(0.001)
|
||||
self._wait_busy()
|
||||
return None
|
||||
@@ -178,8 +219,10 @@ class PN5180:
|
||||
time.sleep(0.001)
|
||||
self._wait_busy()
|
||||
|
||||
# Read response
|
||||
# Read response (manual CS)
|
||||
self._cs_low()
|
||||
rx = self._spi.xfer2([0xFF] * rx_len)
|
||||
self._cs_high()
|
||||
time.sleep(0.001)
|
||||
self._wait_busy()
|
||||
return bytes(rx)
|
||||
@@ -270,8 +313,9 @@ def run_diagnostics():
|
||||
print("PN5180 NFC Reader Diagnostics")
|
||||
print("=" * 60)
|
||||
|
||||
nfc = PN5180()
|
||||
nfc = None
|
||||
try:
|
||||
nfc = PN5180()
|
||||
# Reset
|
||||
print("\n[1] Hardware reset...")
|
||||
nfc.reset()
|
||||
@@ -339,7 +383,8 @@ def run_diagnostics():
|
||||
print(f"\nERROR: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
nfc.close()
|
||||
if nfc is not None:
|
||||
nfc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -547,7 +562,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)
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NAU7802 Scale Diagnostic — ported from SpoolBuddy Rust firmware.
|
||||
"""NAU7802 Scale Diagnostic - ported from SpoolBuddy Rust firmware.
|
||||
|
||||
I2C address: 0x2A
|
||||
Bus: /dev/i2c-0 (GPIO0/GPIO1 on RPi)
|
||||
Bus: /dev/i2c-1 (GPIO2/GPIO3 on RPi)
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import smbus2
|
||||
|
||||
I2C_BUS = 0
|
||||
|
||||
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
|
||||
@@ -39,6 +51,7 @@ PU_AVDDS = 0x80 # AVDD source select
|
||||
|
||||
class NAU7802:
|
||||
def __init__(self, bus=I2C_BUS, addr=NAU7802_ADDR):
|
||||
self._bus_num = bus
|
||||
self._bus = smbus2.SMBus(bus)
|
||||
self._addr = addr
|
||||
|
||||
@@ -51,20 +64,37 @@ class NAU7802:
|
||||
def write_reg(self, reg: int, val: int):
|
||||
self._bus.write_byte_data(self._addr, reg, val & 0xFF)
|
||||
|
||||
def _update_bits(self, reg: int, mask: int, value: int):
|
||||
cur = self.read_reg(reg)
|
||||
self.write_reg(reg, (cur & ~mask) | (value & mask))
|
||||
|
||||
def _set_bit(self, reg: int, bit: int, enabled: bool):
|
||||
mask = 1 << bit
|
||||
self._update_bits(reg, mask, mask if enabled else 0)
|
||||
|
||||
def _set_field(self, reg: int, shift: int, width: int, value: int):
|
||||
mask = ((1 << width) - 1) << shift
|
||||
self._update_bits(reg, mask, value << shift)
|
||||
|
||||
def init(self):
|
||||
"""Initialize NAU7802 — matches Rust firmware init sequence."""
|
||||
revision = self.read_reg(REG_REVISION)
|
||||
print(f" Revision: 0x{revision:02X}")
|
||||
"""Initialize NAU7802 using the Adafruit library startup sequence."""
|
||||
|
||||
# Reset
|
||||
self.write_reg(REG_PU_CTRL, PU_RR)
|
||||
self._set_bit(REG_PU_CTRL, 0, True) # RR=1
|
||||
time.sleep(0.010)
|
||||
self.write_reg(REG_PU_CTRL, 0x00)
|
||||
self._set_bit(REG_PU_CTRL, 0, False) # RR=0
|
||||
self._set_bit(REG_PU_CTRL, 1, True) # PUD=1
|
||||
time.sleep(0.001)
|
||||
|
||||
# Power up digital + analog
|
||||
self.write_reg(REG_PU_CTRL, PU_PUD | PU_PUA)
|
||||
# Enable digital + analog and allow analog section to settle.
|
||||
self._set_bit(REG_PU_CTRL, 1, True) # PUD=1
|
||||
self._set_bit(REG_PU_CTRL, 2, True) # PUA=1
|
||||
time.sleep(0.600)
|
||||
|
||||
# Wait for power-up ready
|
||||
# Start conversion cycle (PU_CS bit 4) after power-up.
|
||||
self._set_bit(REG_PU_CTRL, 4, True)
|
||||
|
||||
# Wait for power-up ready (PU_PUR bit 3)
|
||||
for _ in range(100):
|
||||
status = self.read_reg(REG_PU_CTRL)
|
||||
if status & PU_PUR:
|
||||
@@ -74,28 +104,33 @@ class NAU7802:
|
||||
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")
|
||||
# Check revision register low nibble (Adafruit expects 0xF).
|
||||
revision = self.read_reg(REG_REVISION)
|
||||
print(f" Revision: 0x{revision:02X}")
|
||||
if (revision & 0x0F) != 0x0F:
|
||||
raise RuntimeError(f"Unexpected NAU7802 revision register: 0x{revision:02X}")
|
||||
|
||||
# Internal LDO enable is PU_CTRL.AVDDS (bit 7); set LDO voltage to 3.0V.
|
||||
self._set_bit(REG_PU_CTRL, 7, True) # AVDDS=1 (internal LDO)
|
||||
self._set_field(REG_CTRL1, shift=3, width=3, value=0b101) # VLDO=3.0V
|
||||
print(" LDO: 3.0V (internal)")
|
||||
|
||||
# Gain: 128x (bits 2:0 of CTRL1 = 0b111)
|
||||
ctrl1 = self.read_reg(REG_CTRL1)
|
||||
self.write_reg(REG_CTRL1, (ctrl1 & 0xF8) | 7)
|
||||
self._set_field(REG_CTRL1, shift=0, width=3, value=0b111)
|
||||
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))
|
||||
# Sample rate: 10 SPS (CTRL2 bits 6:4 = 0b000)
|
||||
self._set_field(REG_CTRL2, shift=4, width=3, value=0b000)
|
||||
print(" Sample rate: 10 SPS")
|
||||
|
||||
# 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)")
|
||||
# Adafruit tuning: disable ADC chopper clock (ADC bits 5:4 = 0b11)
|
||||
self._set_field(REG_ADC, shift=4, width=2, value=0b11)
|
||||
|
||||
# Adafruit tuning: use low ESR caps (PGA bit 6 = 0)
|
||||
self._set_bit(REG_PGA, 6, False)
|
||||
|
||||
# Start conversion cycle
|
||||
pu_ctrl = self.read_reg(REG_PU_CTRL)
|
||||
self.write_reg(REG_PU_CTRL, pu_ctrl | PU_CS)
|
||||
self._set_bit(REG_PU_CTRL, 4, True)
|
||||
print(" Conversion started")
|
||||
|
||||
def data_ready(self) -> bool:
|
||||
@@ -119,6 +154,39 @@ def main():
|
||||
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...")
|
||||
@@ -158,9 +226,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