scale_diag.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. #!/usr/bin/env python3
  2. """NAU7802 Scale Diagnostic - ported from SpoolBuddy Rust firmware.
  3. I2C address: 0x2A
  4. Bus: /dev/i2c-1 (GPIO2/GPIO3 on RPi)
  5. """
  6. import os
  7. import struct
  8. import sys
  9. import time
  10. import smbus2
  11. def _env_int(name: str, default: int) -> int:
  12. value = os.environ.get(name)
  13. if value is None or value == "":
  14. return default
  15. try:
  16. return int(value)
  17. except ValueError:
  18. return default
  19. I2C_BUS = _env_int("SPOOLBUDDY_I2C_BUS", 1)
  20. NAU7802_ADDR = 0x2A
  21. # Register addresses
  22. REG_PU_CTRL = 0x00
  23. REG_CTRL1 = 0x01
  24. REG_CTRL2 = 0x02
  25. REG_ADCO_B2 = 0x12 # ADC output MSB
  26. REG_ADCO_B1 = 0x13
  27. REG_ADCO_B0 = 0x14 # ADC output LSB
  28. REG_ADC = 0x15
  29. REG_PGA = 0x1B
  30. REG_PWR_CTRL = 0x1C
  31. REG_REVISION = 0x1F
  32. # PU_CTRL bits
  33. PU_RR = 0x01 # Register reset
  34. PU_PUD = 0x02 # Power up digital
  35. PU_PUA = 0x04 # Power up analog
  36. PU_PUR = 0x08 # Power up ready (read-only)
  37. PU_CS = 0x10 # Cycle start
  38. PU_CR = 0x20 # Cycle ready (read-only)
  39. PU_OSCS = 0x40 # Oscillator select
  40. PU_AVDDS = 0x80 # AVDD source select
  41. class NAU7802:
  42. def __init__(self, bus=I2C_BUS, addr=NAU7802_ADDR):
  43. self._bus_num = bus
  44. self._bus = smbus2.SMBus(bus)
  45. self._addr = addr
  46. def close(self):
  47. self._bus.close()
  48. def read_reg(self, reg: int) -> int:
  49. return self._bus.read_byte_data(self._addr, reg)
  50. def write_reg(self, reg: int, val: int):
  51. self._bus.write_byte_data(self._addr, reg, val & 0xFF)
  52. def _update_bits(self, reg: int, mask: int, value: int):
  53. cur = self.read_reg(reg)
  54. self.write_reg(reg, (cur & ~mask) | (value & mask))
  55. def _set_bit(self, reg: int, bit: int, enabled: bool):
  56. mask = 1 << bit
  57. self._update_bits(reg, mask, mask if enabled else 0)
  58. def _set_field(self, reg: int, shift: int, width: int, value: int):
  59. mask = ((1 << width) - 1) << shift
  60. self._update_bits(reg, mask, value << shift)
  61. def init(self):
  62. """Initialize NAU7802 using the Adafruit library startup sequence."""
  63. # Reset
  64. self._set_bit(REG_PU_CTRL, 0, True) # RR=1
  65. time.sleep(0.010)
  66. self._set_bit(REG_PU_CTRL, 0, False) # RR=0
  67. self._set_bit(REG_PU_CTRL, 1, True) # PUD=1
  68. time.sleep(0.001)
  69. # Enable digital + analog and allow analog section to settle.
  70. self._set_bit(REG_PU_CTRL, 1, True) # PUD=1
  71. self._set_bit(REG_PU_CTRL, 2, True) # PUA=1
  72. time.sleep(0.600)
  73. # Start conversion cycle (PU_CS bit 4) after power-up.
  74. self._set_bit(REG_PU_CTRL, 4, True)
  75. # Wait for power-up ready (PU_PUR bit 3)
  76. for _ in range(100):
  77. status = self.read_reg(REG_PU_CTRL)
  78. if status & PU_PUR:
  79. print(" Power-up ready")
  80. break
  81. time.sleep(0.001)
  82. else:
  83. raise TimeoutError("NAU7802 power-up timeout")
  84. # Check revision register low nibble (Adafruit expects 0xF).
  85. revision = self.read_reg(REG_REVISION)
  86. print(f" Revision: 0x{revision:02X}")
  87. if (revision & 0x0F) != 0x0F:
  88. raise RuntimeError(f"Unexpected NAU7802 revision register: 0x{revision:02X}")
  89. # Internal LDO enable is PU_CTRL.AVDDS (bit 7); set LDO voltage to 3.0V.
  90. self._set_bit(REG_PU_CTRL, 7, True) # AVDDS=1 (internal LDO)
  91. self._set_field(REG_CTRL1, shift=3, width=3, value=0b101) # VLDO=3.0V
  92. print(" LDO: 3.0V (internal)")
  93. # Gain: 128x (bits 2:0 of CTRL1 = 0b111)
  94. self._set_field(REG_CTRL1, shift=0, width=3, value=0b111)
  95. print(" Gain: 128x")
  96. # Sample rate: 10 SPS (CTRL2 bits 6:4 = 0b000)
  97. self._set_field(REG_CTRL2, shift=4, width=3, value=0b000)
  98. print(" Sample rate: 10 SPS")
  99. # Adafruit tuning: disable ADC chopper clock (ADC bits 5:4 = 0b11)
  100. self._set_field(REG_ADC, shift=4, width=2, value=0b11)
  101. # Adafruit tuning: use low ESR caps (PGA bit 6 = 0)
  102. self._set_bit(REG_PGA, 6, False)
  103. # Start conversion cycle
  104. self._set_bit(REG_PU_CTRL, 4, True)
  105. print(" Conversion started")
  106. def data_ready(self) -> bool:
  107. return bool(self.read_reg(REG_PU_CTRL) & PU_CR)
  108. def read_raw(self) -> int:
  109. """Read 24-bit signed ADC value."""
  110. b2 = self.read_reg(REG_ADCO_B2)
  111. b1 = self.read_reg(REG_ADCO_B1)
  112. b0 = self.read_reg(REG_ADCO_B0)
  113. raw = (b2 << 16) | (b1 << 8) | b0
  114. # Sign extend 24-bit to 32-bit
  115. if raw & 0x800000:
  116. raw |= 0xFF000000
  117. raw = struct.unpack("i", struct.pack("I", raw))[0]
  118. return raw
  119. def main():
  120. print("=" * 60)
  121. print("NAU7802 Scale Diagnostic")
  122. print("=" * 60)
  123. print(f"Configured bus: {I2C_BUS}, address: 0x{NAU7802_ADDR:02X}")
  124. # Probe both common I2C buses and show where devices are actually visible.
  125. found_by_bus: dict[int, list[int]] = {}
  126. for bus_num in (0, 1):
  127. found_by_bus[bus_num] = []
  128. try:
  129. with smbus2.SMBus(bus_num) as probe_bus:
  130. for addr in range(0x03, 0x78):
  131. try:
  132. probe_bus.read_byte(addr)
  133. found_by_bus[bus_num].append(addr)
  134. except OSError:
  135. continue
  136. except FileNotFoundError:
  137. continue
  138. except PermissionError:
  139. continue
  140. for bus_num, addrs in found_by_bus.items():
  141. if addrs:
  142. pretty = " ".join(f"0x{a:02X}" for a in addrs)
  143. print(f"Bus {bus_num} devices: {pretty}")
  144. else:
  145. print(f"Bus {bus_num} devices: (none)")
  146. if NAU7802_ADDR not in found_by_bus.get(I2C_BUS, []):
  147. for alt in (1, 0):
  148. if alt != I2C_BUS and NAU7802_ADDR in found_by_bus.get(alt, []):
  149. print(f"\nHint: NAU7802 (0x{NAU7802_ADDR:02X}) appears on bus {alt}, not configured bus {I2C_BUS}.")
  150. print(f"Try: SPOOLBUDDY_I2C_BUS={alt} .../scale_diag.py")
  151. break
  152. scale = NAU7802()
  153. try:
  154. print("[1] Initializing...")
  155. scale.init()
  156. print("[2] Waiting for first reading...")
  157. for _ in range(200):
  158. if scale.data_ready():
  159. break
  160. time.sleep(0.010)
  161. else:
  162. print(" Timeout waiting for data ready")
  163. sys.exit(1)
  164. print("[3] Reading 10 samples (10 SPS = ~1 second)...")
  165. readings = []
  166. for i in range(10):
  167. # Wait for data ready
  168. for _ in range(200):
  169. if scale.data_ready():
  170. break
  171. time.sleep(0.010)
  172. raw = scale.read_raw()
  173. readings.append(raw)
  174. print(f" Sample {i + 1:2d}: {raw:>10d}")
  175. avg = sum(readings) / len(readings)
  176. spread = max(readings) - min(readings)
  177. print(f"\n Average: {avg:>10.0f}")
  178. print(f" Min: {min(readings):>10d}")
  179. print(f" Max: {max(readings):>10d}")
  180. print(f" Spread: {spread:>10d}")
  181. print("\n" + "=" * 60)
  182. print("Diagnostic complete!")
  183. print("=" * 60)
  184. except Exception as e:
  185. print(f"\nERROR: {e}")
  186. is_known_error = False
  187. if isinstance(e, OSError):
  188. if e.errno == 16: # Device or resource busy
  189. is_known_error = True
  190. print("\nI2C DEVICE BUSY (Errno 16): Another process is using the I2C bus.")
  191. print("This typically means the SpoolBuddy daemon is already reading the scale.")
  192. print("\nTo run this diagnostic, stop the daemon first:")
  193. print(" sudo systemctl stop bambuddy")
  194. print(" # Run diagnostic")
  195. print(" .../scale_diag.py")
  196. print(" # Restart daemon when done:")
  197. print(" sudo systemctl start bambuddy")
  198. elif e.errno == 121:
  199. is_known_error = True
  200. print("\nI2C NACK (Errno 121): the device did not acknowledge reads at 0x2A.")
  201. print("Check:")
  202. print(" - NAU7802 SDA/SCL are on the configured bus pins")
  203. print(" - 3.3V and GND are correct and stable")
  204. print(" - Sensor address is really 0x2A")
  205. print(" - No loose wire or swapped SDA/SCL")
  206. else:
  207. print(f"\nI2C Error (Errno {e.errno}): {e}")
  208. # Only print full traceback for unexpected errors
  209. if not is_known_error:
  210. import traceback
  211. traceback.print_exc()
  212. sys.exit(1)
  213. finally:
  214. scale.close()
  215. if __name__ == "__main__":
  216. main()