From eb3450b1d02bb419aca01bc0e24c811339d08362 Mon Sep 17 00:00:00 2001 From: Keybored <78739882+Keybored02@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:16:09 +0100 Subject: [PATCH] Spoolbuddy init and diagnostic improvements (#814) Spoolbuddy init and diagnostic improvements (#814) --- spoolbuddy/daemon/nau7802.py | 147 ++++++++--- spoolbuddy/daemon/pn5180.py | 12 + spoolbuddy/scripts/pn5180_diag.py | 395 ++++++++++-------------------- spoolbuddy/scripts/scale_diag.py | 178 +++++--------- 4 files changed, 316 insertions(+), 416 deletions(-) diff --git a/spoolbuddy/daemon/nau7802.py b/spoolbuddy/daemon/nau7802.py index 826bbc221..f0bc99a74 100644 --- a/spoolbuddy/daemon/nau7802.py +++ b/spoolbuddy/daemon/nau7802.py @@ -56,6 +56,10 @@ class NAU7802: self._bus = smbus2.SMBus(bus) self._addr = addr + # CTRL2 bits for AFE calibration + _CTRL2_CALS = 1 << 2 + _CTRL2_CAL_ERROR = 1 << 3 + def close(self): self._bus.close() @@ -78,70 +82,145 @@ class NAU7802: self._update_bits(reg, mask, value << shift) def init(self): - """Initialize NAU7802 using the Adafruit library startup sequence.""" + """Initialize NAU7802 per datasheet power-on sequencing (Section 8.1). - # Reset + Datasheet steps: + 1. RR=1 (reset all registers) + 2. RR=0, PUD=1 (enter normal operation; PUD auto-starts AD conversion) + 3. Wait ~200µs for PUR=1 + 4. Configure (LDO, gain, rate, etc.) + 5. Tuning (ADC chopper, PGA caps) + 6. (Optional) calibration and flush transients + """ + + # Step 1: Reset (set RR=1, then RR=0) self._set_bit(REG_PU_CTRL, 0, True) # RR=1 time.sleep(0.010) - self._set_bit(REG_PU_CTRL, 0, False) # RR=0 - self._set_bit(REG_PU_CTRL, 1, True) # PUD=1 + self._set_bit(REG_PU_CTRL, 0, False) # RR=0 exits reset + # Datasheet says "about 200 microseconds" before PUR is set time.sleep(0.001) - # Enable digital + analog and allow analog section to settle. + # Step 2: Power up digital (PUD=1 auto-starts AD conversion) self._set_bit(REG_PU_CTRL, 1, True) # PUD=1 + # Step 2b: Power up analog (PUA=1) self._set_bit(REG_PU_CTRL, 2, True) # PUA=1 - time.sleep(0.600) + time.sleep(0.600) # Wait for LDO and analog section to stabilize - # Start conversion cycle (PU_CS bit 4) after power-up. - self._set_bit(REG_PU_CTRL, 4, True) + # Step 3: Wait for power-up ready (PUR bit 3) - # Wait for power-up ready (PU_PUR bit 3) for _ in range(100): status = self.read_reg(REG_PU_CTRL) if status & PU_PUR: - logger.debug("Power-up ready") + logger.debug(" Power-up ready") break time.sleep(0.001) else: - raise TimeoutError("NAU7802 power-up timeout") + raise TimeoutError("NAU7802 power-up timeout (PUR bit not set)") + + # Check revision register low nibble (datasheet expects 0xF). - # Check revision register low nibble (Adafruit expects 0xF). revision = self.read_reg(REG_REVISION) + logger.debug(f" Revision: 0x{revision:02X}") if (revision & 0x0F) != 0x0F: - raise RuntimeError(f"Unexpected NAU7802 revision register: 0x{revision:02X}") - logger.debug("Revision: 0x%02X", revision) + raise RuntimeError(f"Unexpected NAU7802 revision: 0x{revision:02X} (expected 0x_F)") - # 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) + # Step 4: Configure device + # Internal LDO enable (AVDDS=1, bit 7) and set voltage to 3.0V + + self._set_bit(REG_PU_CTRL, 7, True) # AVDDS=1 self._set_field(REG_CTRL1, shift=3, width=3, value=0b101) # VLDO=3.0V - logger.debug("LDO: 3.0V (internal)") + logger.debug(" LDO: 3.0V (internal)") + + # Set gain to 128x (CTRL1 bits 2:0 = 0b111) - # Gain: 128x (bits 2:0 of CTRL1 = 0b111) self._set_field(REG_CTRL1, shift=0, width=3, value=0b111) - logger.debug("Gain: 128x") + logger.debug(" Gain: 128x") + + # Set sample rate to 10 SPS (CTRL2 bits 6:4 = 0b000) + # Note: At 10 SPS, each sample takes ~100ms; first 4 samples = ~400ms to settle - # Sample rate: 10 SPS (CTRL2 bits 6:4 = 0b000) self._set_field(REG_CTRL2, shift=4, width=3, value=0b000) - logger.debug("Sample rate: 10 SPS") + logger.debug(" Sample rate: 10 SPS") - # Adafruit tuning: disable ADC chopper clock (ADC bits 5:4 = 0b11) + # Step 5: Tuning per application notes + # 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) + # Enable low-ESR caps on PGA (PGA bit 6 = 0 for improved accuracy) self._set_bit(REG_PGA, 6, False) - # Start conversion cycle - self._set_bit(REG_PU_CTRL, 4, True) - logger.debug("Conversion started") + # Step 6: Trigger fresh AD conversion and wait for first result + # CS bit transition 0→1 starts fresh conversion; takes ~4-sample time for result - # Flush the first reading — the NAU7802 always returns a stale - # max-scale value (0x7FFFFF) on the first conversion after power-up. - for _ in range(200): + self._set_bit(REG_PU_CTRL, 4, True) # CS=1 + logger.debug(" Conversion started") + + # Flush startup transients before calibration + # At 10 SPS, initial 4 samples may contain settling artifacts + + self.flush_readings(count=4, timeout_s=1.5) + + # Run AFE calibration (internal mode), then flush result + self.calibrate_afe(timeout_ms=1000, mode=0) + self.flush_readings(count=2, timeout_s=1.0) + logger.debug(" Initialization complete") + + def begin_calibrate_afe(self, mode: int = 0) -> None: + """Start asynchronous AFE calibration. + + mode values match NAU7802 CALMOD: 0=internal, 1=offset, 2=gain. + """ + ctrl2 = self.read_reg(REG_CTRL2) + ctrl2 &= 0xFC # clear CALMOD bits[1:0] + ctrl2 |= mode & 0x03 + self.write_reg(REG_CTRL2, ctrl2) + + # Set CALS (bit 2) to start calibration. + self.write_reg(REG_CTRL2, self.read_reg(REG_CTRL2) | self._CTRL2_CALS) + + def wait_for_calibrate_afe(self, timeout_ms: int = 1000) -> bool: + deadline = time.monotonic() + (timeout_ms / 1000.0) if timeout_ms > 0 else None + + while True: + ctrl2 = self.read_reg(REG_CTRL2) + if (ctrl2 & self._CTRL2_CALS) == 0: + return (ctrl2 & self._CTRL2_CAL_ERROR) == 0 + + if deadline is not None and time.monotonic() >= deadline: + return False + time.sleep(0.001) + + def calibrate_afe(self, timeout_ms: int = 1000, mode: int = 0) -> None: + """Run AFE calibration per datasheet CTRL2[2] CALS bit sequence. + + Datasheet says: + - Write 1 to CALS to start (mode in CALMOD bits [1:0]) + - CALS=1 during calibration, 0 when complete + - Check CAL_ERR bit after completion + """ + self.begin_calibrate_afe(mode=mode) + if not self.wait_for_calibrate_afe(timeout_ms=timeout_ms): + raise RuntimeError(f"NAU7802 AFE calibration timed out after {timeout_ms}ms") + # Check CAL_ERR bit to ensure no error during calibration + ctrl2 = self.read_reg(REG_CTRL2) + if ctrl2 & self._CTRL2_CAL_ERROR: + raise RuntimeError("NAU7802 AFE calibration completed with CAL_ERR set") + logger.debug(" AFE calibration: OK") + + def wait_data_ready(self, timeout_s: float = 1.0) -> bool: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: if self.data_ready(): - self.read_raw() # discard - logger.debug("First reading flushed") - break - time.sleep(0.010) + return True + time.sleep(0.001) + return False + + def flush_readings(self, count: int = 4, timeout_s: float = 1.0) -> None: + flushed = 0 + while flushed < count: + if not self.wait_data_ready(timeout_s=timeout_s): + raise TimeoutError("Timeout while flushing startup scale readings") + _ = self.read_raw() + flushed += 1 def data_ready(self) -> bool: return bool(self.read_reg(REG_PU_CTRL) & PU_CR) diff --git a/spoolbuddy/daemon/pn5180.py b/spoolbuddy/daemon/pn5180.py index 624596221..7cbda5fa9 100644 --- a/spoolbuddy/daemon/pn5180.py +++ b/spoolbuddy/daemon/pn5180.py @@ -208,6 +208,18 @@ class PN5180: self._cmd([0x17, 0x00]) time.sleep(0.005) + def set_pin(self, pin: int, value: bool) -> None: + """Set the state of a control pin (NSS or RST). Value: True=ACTIVE, False=INACTIVE.""" + if pin not in (NSS_PIN, RST_PIN): + raise ValueError("Only NSS_PIN and RST_PIN can be set via set_pin().") + self._lines.set_value(pin, gpiod.line.Value.ACTIVE if value else gpiod.line.Value.INACTIVE) + + def get_pin(self, pin: int) -> bool: + """Get the state of a control pin (NSS or RST). Returns True if ACTIVE, False if INACTIVE.""" + if pin not in (NSS_PIN, RST_PIN): + raise ValueError("Only NSS_PIN and RST_PIN can be read via get_pin().") + return self._lines.get_value(pin) == gpiod.line.Value.ACTIVE + def set_transceive_mode(self): """Set SYSTEM_CONFIG command bits to TRANSCEIVE (0x03) — CRITICAL!""" sys_cfg = self.read_reg(0x00) diff --git a/spoolbuddy/scripts/pn5180_diag.py b/spoolbuddy/scripts/pn5180_diag.py index 381a0c48f..1494fa3d0 100644 --- a/spoolbuddy/scripts/pn5180_diag.py +++ b/spoolbuddy/scripts/pn5180_diag.py @@ -15,38 +15,23 @@ Wiring (from spoolbuddy/README.md): 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 -NSS_PIN = 23 # Pin 16 (manual CS) +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 @@ -60,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 # --------------------------------------------------------------------------- @@ -89,224 +58,39 @@ 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=500_000, - busy_pin=BUSY_PIN, - rst_pin=RST_PIN, - nss_pin=NSS_PIN, - ): - # GPIO setup via libgpiod - self._chip = _find_gpio_chip() - - 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 - 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 -------------------------------------------------- - - 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 _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. - - 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 (manual CS) - self._cs_low() - self._spi.xfer2(list(tx_data)) - self._cs_high() - - 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 (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) - - # -- 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, - ] + print( + f" {pin_name} pin {pin_num}: " + f"ACTIVE->{'ACTIVE' if active_state else 'INACTIVE'}, " + f"INACTIVE->{'ACTIVE' if inactive_state else 'INACTIVE'}" ) - 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) @@ -315,28 +99,71 @@ def run_diagnostics(): 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"), @@ -356,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.") diff --git a/spoolbuddy/scripts/scale_diag.py b/spoolbuddy/scripts/scale_diag.py index f0f9e3700..c88829697 100644 --- a/spoolbuddy/scripts/scale_diag.py +++ b/spoolbuddy/scripts/scale_diag.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 -"""NAU7802 Scale Diagnostic - ported from SpoolBuddy Rust firmware. +"""NAU7802 Scale Diagnostic. I2C address: 0x2A Bus: /dev/i2c-1 (GPIO2/GPIO3 on RPi) """ import os -import struct import sys import time import smbus2 +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) @@ -49,115 +52,6 @@ PU_OSCS = 0x40 # Oscillator select 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 - - 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 _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 using the Adafruit library startup sequence.""" - - # Reset - self._set_bit(REG_PU_CTRL, 0, True) # RR=1 - time.sleep(0.010) - self._set_bit(REG_PU_CTRL, 0, False) # RR=0 - self._set_bit(REG_PU_CTRL, 1, True) # PUD=1 - time.sleep(0.001) - - # 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) - - # 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: - print(" Power-up ready") - break - time.sleep(0.001) - else: - raise TimeoutError("NAU7802 power-up timeout") - - # 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) - self._set_field(REG_CTRL1, shift=0, width=3, value=0b111) - print(" Gain: 128x") - - # 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") - - # 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 - self._set_bit(REG_PU_CTRL, 4, True) - print(" Conversion started") - - # Flush the first reading — the NAU7802 always returns a stale - # max-scale value (0x7FFFFF) on the first conversion after power-up. - for _ in range(200): - if self.data_ready(): - self.read_raw() # discard - print(" First reading flushed") - break - time.sleep(0.010) - - 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") @@ -199,8 +93,68 @@ def main(): 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(): @@ -210,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