diff --git a/spoolbuddy/README.md b/spoolbuddy/README.md index a65a8f774..c6dfeff77 100644 --- a/spoolbuddy/README.md +++ b/spoolbuddy/README.md @@ -27,13 +27,14 @@ ### Setup Steps -#### 1. Enable SPI +#### 1. Enable SPI and I2C -After a fresh Raspberry Pi OS install, SPI is disabled by default. +After a fresh Raspberry Pi OS install, SPI and I2C are disabled by default. ```bash sudo raspi-config # Navigate to: Interface Options -> SPI -> Enable +# Navigate to: Interface Options -> I2C -> Enable sudo reboot ``` @@ -42,40 +43,64 @@ Verify after reboot: ```bash ls /dev/spidev0.* # Should show: /dev/spidev0.0 /dev/spidev0.1 + +ls /dev/i2c-* +# Should include: /dev/i2c-1 ``` -#### 2. Disable automatic CS (kernel SPI chip-select) +#### 2. Configure `/boot/firmware/config.txt` -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`: +Add the following lines under the `[all]` section: ``` +# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1) +dtparam=i2c_vc=on + +# SpoolBuddy: Disable SPI auto CS (manual CS on GPIO23 for PN5180) dtoverlay=spi0-0cs ``` +- `i2c_vc=on` enables I2C bus 0 (GPIO0/GPIO1). The default `i2c_arm` only + enables bus 1 (GPIO2/GPIO3). The NAU7802 is wired to bus 0. +- `spi0-0cs` disables the kernel SPI driver's automatic chip-select. We use + manual CS on GPIO23 because the driver's CS timing doesn't meet the PN5180's + requirements. + Then reboot: ```bash sudo reboot ``` +Verify after reboot: + +```bash +ls /dev/i2c-0 +# Should exist + +sudo i2cdetect -y 0 +# Should show 0x2A (NAU7802) +``` + #### 3. Install system packages ```bash -sudo apt install python3-spidev python3-libgpiod gpiod libgpiod3 +sudo apt install python3-spidev python3-libgpiod gpiod libgpiod3 i2c-tools ``` - `python3-spidev` / `libgpiod3` — system libraries for SPI and GPIO access - `gpiod` — command-line GPIO tools (useful for debugging) +- `i2c-tools` — I2C diagnostic tools (`i2cdetect`, `i2cget`, etc.) #### 4. Install Python dependencies (in venv) ```bash -pip install spidev gpiod +pip install spidev gpiod smbus2 ``` -- `spidev` — Python SPI bindings +- `spidev` — Python SPI bindings (PN5180 NFC reader) - `gpiod` — Python GPIO bindings via libgpiod (works on both RPi 4 and RPi 5) +- `smbus2` — Python I2C bindings (NAU7802 scale) #### 5. Solder all connections @@ -144,3 +169,19 @@ Place a tag on the reader. Supported tag types: | SDA | Pin 27 | GPIO 0 | Yellow | | SCL | Pin 28 | GPIO 1 | White | | GND | Pin 30 | — | Black | + +> **I2C Bus:** Uses I2C bus 0 (GPIO0/GPIO1), enabled via `dtparam=i2c_vc=on` +> in config.txt. Bus 1 (GPIO2/GPIO3) is the default but those pins are not +> used here. + +### Verify + +```bash +sudo i2cdetect -y 0 +# Should show 0x2A + +sudo python3 spoolbuddy/scale_diag.py +``` + +The diagnostic reads 10 samples at 10 SPS and shows raw ADC values, average, +and spread. Typical idle readings are around ~500k with a spread under 20k. diff --git a/spoolbuddy/scale_diag.py b/spoolbuddy/scale_diag.py new file mode 100644 index 000000000..1495923da --- /dev/null +++ b/spoolbuddy/scale_diag.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""NAU7802 Scale Diagnostic — ported from SpoolBuddy Rust firmware. + +I2C address: 0x2A +Bus: /dev/i2c-0 (GPIO0/GPIO1 on RPi) +""" + +import struct +import sys +import time + +import smbus2 + +I2C_BUS = 0 +NAU7802_ADDR = 0x2A + +# Register addresses +REG_PU_CTRL = 0x00 +REG_CTRL1 = 0x01 +REG_CTRL2 = 0x02 +REG_ADCO_B2 = 0x12 # ADC output MSB +REG_ADCO_B1 = 0x13 +REG_ADCO_B0 = 0x14 # ADC output LSB +REG_ADC = 0x15 +REG_PGA = 0x1B +REG_PWR_CTRL = 0x1C +REG_REVISION = 0x1F + +# PU_CTRL bits +PU_RR = 0x01 # Register reset +PU_PUD = 0x02 # Power up digital +PU_PUA = 0x04 # Power up analog +PU_PUR = 0x08 # Power up ready (read-only) +PU_CS = 0x10 # Cycle start +PU_CR = 0x20 # Cycle ready (read-only) +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) + + scale = NAU7802() + try: + print("[1] Initializing...") + scale.init() + + print("[2] Waiting for first reading...") + for _ in range(200): + if scale.data_ready(): + break + time.sleep(0.010) + else: + print(" Timeout waiting for data ready") + sys.exit(1) + + print("[3] Reading 10 samples (10 SPS = ~1 second)...") + readings = [] + for i in range(10): + # Wait for data ready + for _ in range(200): + if scale.data_ready(): + break + time.sleep(0.010) + raw = scale.read_raw() + readings.append(raw) + print(f" Sample {i + 1:2d}: {raw:>10d}") + + avg = sum(readings) / len(readings) + spread = max(readings) - min(readings) + print(f"\n Average: {avg:>10.0f}") + print(f" Min: {min(readings):>10d}") + print(f" Max: {max(readings):>10d}") + print(f" Spread: {spread:>10d}") + + print("\n" + "=" * 60) + print("Diagnostic complete!") + print("=" * 60) + + except Exception as e: + print(f"\nERROR: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + finally: + scale.close() + + +if __name__ == "__main__": + main()