Browse Source

Add SpoolBuddy tag detection modal and fix NFC reader polling

  Frontend: Replace inline SpoolInfoCard/UnknownTagCard with full-screen
  TagDetectedModal that auto-opens on NFC tag detection. Known spools show
  remaining weight, fill bar, and offer "Assign to AMS" (new sub-modal with
  printer selector + AMS slot grid) and "Sync Weight". Unknown tags offer
  "Add to Inventory" and "Link to Spool". Modal stays open on tag removal,
  won't re-open for dismissed tags, reopens on re-place after removal.

  Daemon: Fix PN5180 NFC reader failing to detect tags. Each
  activate_type_a() call returning None corrupts the PN5180 transceive
  state, silently preventing all subsequent tag detection. Fixed by
  performing a full hardware reset (RST pin toggle + RF re-init) before
  every idle poll (~240ms, ~1.8 Hz rate). When a tag is present, a light
  RF off/on cycle (13ms) resets the card from ACTIVE to IDLE state for
  continuous re-selection. Also added error-based recovery, periodic
  status logging, and accurate heartbeat NFC/scale health reporting.
maziggy 2 months ago
parent
commit
d466a4a86e
2 changed files with 11 additions and 31 deletions
  1. 1 1
      CHANGELOG.md
  2. 10 30
      spoolbuddy/daemon/nfc_reader.py

+ 1 - 1
CHANGELOG.md

@@ -17,7 +17,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Windows Install Fails With "Syntax of the Command Is Incorrect"** ([#544](https://github.com/maziggy/bambuddy/issues/544)) — The `start_bambuddy.bat` launcher had Unix (LF) line endings instead of Windows (CRLF). When a user's git config has `core.autocrlf=false` or `input`, the file is checked out with LF endings and `cmd.exe` cannot parse it. Added a `.gitattributes` file that forces CRLF for all `.bat` files regardless of git config.
 - **Queue Badge Shows on Incompatible Printers** ([#486](https://github.com/maziggy/bambuddy/issues/486)) — The purple queue counter badge in the printer card header showed on all printers of the same model when a job was scheduled for "any [model]", even if the printer didn't have the matching filament color loaded. The `PrinterQueueWidget` (which shows "Clear Plate & Start") already filtered by filament type and color, but the badge count used the raw unfiltered queue length. Now applies the same filament compatibility filter to the badge count.
 - **SpoolBuddy Daemon Can't Find Hardware Drivers** — The daemon's `nfc_reader.py` and `scale_reader.py` import `read_tag` and `scale_diag` as bare modules, but these files live in `spoolbuddy/scripts/` which isn't on Python's module search path. The systemd service sets `WorkingDirectory` to `spoolbuddy/` and runs `python -m daemon.main`, so only the `spoolbuddy/` and `daemon/` directories are on `sys.path`. Added `scripts/` to `sys.path` at daemon startup, resolved relative to the module file so it works regardless of install path. Also moved the `read_tag` import inside `NFCReader.__init__`'s try/except block — it was previously outside, so a missing module crashed the entire daemon instead of gracefully skipping NFC polling. Demoted hardware-not-available log messages from ERROR to INFO since missing modules are expected when hardware isn't connected.
-- **SpoolBuddy NFC Reader Silently Stops Detecting Tags** — Two PN5180 NFC reader issues caused tags to stop being detected. First, after a successful `activate_type_a()` + SELECT, the card remained in ACTIVE state and would not respond to the next WUPA/REQA, causing every subsequent poll to return `None`. The tag appeared to be "removed" after ~1 second (3 consecutive misses) despite still being physically present, and was never re-detected. Added a conditional RF field off/on cycle (13ms) before each poll, but only when a tag is present — this resets the card from ACTIVE back to IDLE for re-selection. The cycle is skipped when idle to avoid degrading the reader state with continuous unnecessary RF toggling, which prevented new tags from being detected after removal. Second, the reader could drift into a deeper stuck state where `activate_type_a()` silently returned `None` perpetually without raising exceptions, indistinguishable from "no tag present" and invisible in logs (poll failures were at DEBUG level). Added a preventive full hardware reset (RST pin toggle + RF re-init) every 60 seconds when idle — the same reset a service restart performs. Also added error-based auto-recovery (full hardware reset after 10 consecutive poll exceptions) for exception-raising failures. Poll errors are now logged at WARNING level, a periodic status line logs every 60 seconds, and the heartbeat reports actual `nfc.ok` and `scale.ok` from the reader instances instead of hardcoded `True`.
+- **SpoolBuddy NFC Reader Fails to Detect Tags** — The PN5180 NFC reader had two polling issues. First, each `activate_type_a()` call that returned `None` (no tag) corrupted the PN5180 transceive state — subsequent calls silently failed even when a tag was physically present, making it impossible to detect tags placed after startup (only tags already on the reader during init were detected). Fixed by performing a full hardware reset (RST pin toggle + RF re-init, ~240ms) before every idle poll, giving a ~1.8 Hz effective poll rate. Second, after a successful SELECT the card stayed in ACTIVE state and ignored subsequent WUPA/REQA, causing false "tag removed" events after ~1 second. Fixed with a light RF off/on cycle (13ms) before each poll when a tag is present, resetting the card to IDLE for re-selection. Also added error-based auto-recovery (full hardware reset after 10 consecutive poll exceptions), periodic status logging every 60 seconds, and accurate heartbeat reporting of NFC/scale health.
 
 ### Improved
 - **SpoolBuddy Scale Value Stabilization** — The SpoolBuddy daemon now suppresses redundant scale weight reports: only sends updates when the weight changes by ≥2g. Previously every 1-second report interval sent a reading regardless of change, and stability state flips (stable ↔ unstable) also triggered reports — when ADC noise kept the spread hovering around the 2g stability threshold, the flag toggled every cycle, forcing a report with a slightly different weight each time. Removed stability flipping as a report trigger (the stable flag is still included in each report for consumers). Also increased the NAU7802 moving average window from 5 to 20 samples (500ms → 2s) to smooth ADC noise. The frontend also applies a 3g display threshold as defense-in-depth.

+ 10 - 30
spoolbuddy/daemon/nfc_reader.py

@@ -8,7 +8,6 @@ logger = logging.getLogger(__name__)
 
 MISS_THRESHOLD = 3  # Consecutive misses before declaring tag removed
 ERROR_RECOVERY_THRESHOLD = 10  # Consecutive errors before attempting RF reset
-RF_CYCLE_INTERVAL = 60.0  # Seconds between preventive RF cycles (when idle)
 
 
 class NFCState(Enum):
@@ -25,7 +24,6 @@ class NFCReader:
         self._miss_count = 0
         self._ok = False
         self._error_count = 0
-        self._last_rf_cycle = 0.0
         self._poll_count = 0
         self._last_status_log = 0.0
 
@@ -47,23 +45,6 @@ class NFCReader:
         self._nfc.rf_on()
         time.sleep(0.030)
         self._nfc.set_transceive_mode()
-        self._last_rf_cycle = time.monotonic()
-
-    def _rf_cycle(self):
-        """RF off/on cycle to recover from stuck state."""
-        try:
-            self._nfc.rf_off()
-            time.sleep(0.010)
-            self._nfc.load_rf_config(0x00, 0x80)
-            time.sleep(0.005)
-            self._nfc.rf_on()
-            time.sleep(0.020)
-            self._nfc.set_transceive_mode()
-            self._last_rf_cycle = time.monotonic()
-            return True
-        except Exception as e:
-            logger.warning("NFC RF cycle failed: %s", e)
-            return False
 
     def _full_reset(self):
         """Full hardware reset + RF init to recover from stuck state."""
@@ -114,20 +95,19 @@ class NFCReader:
             )
             self._last_status_log = now
 
-        # Preventive full hardware reset when idle (prevents reader drift into
-        # stuck state where activate_type_a() silently returns None without errors)
-        if self._state == NFCState.IDLE and now - self._last_rf_cycle >= RF_CYCLE_INTERVAL:
+        if self._state == NFCState.IDLE:
+            # Full hardware reset before every idle poll. Each activate_type_a()
+            # call that returns None corrupts the PN5180 state — subsequent calls
+            # silently fail even when a tag is present. Only a full RST pin toggle
+            # recovers the reader. ~240ms overhead per poll, giving ~1.8 Hz poll
+            # rate which is fine for a spool tag reader.
             try:
                 self._init_rf()
-                logger.debug("Preventive NFC hardware reset completed")
             except Exception as e:
-                logger.warning("Preventive NFC reset failed: %s", e)
-
-        # RF field cycle only when a tag is present — after a successful SELECT,
-        # the card stays in ACTIVE state and won't respond to the next WUPA/REQA.
-        # Toggling RF forces it back to IDLE. Skip when idle (no prior SELECT)
-        # to avoid degrading the reader state with continuous unnecessary cycling.
-        if self._state == NFCState.TAG_PRESENT:
+                logger.warning("NFC pre-poll reset failed: %s", e)
+        else:
+            # Tag present: light RF cycle to reset card from ACTIVE back to IDLE
+            # state after previous SELECT, so it responds to the next WUPA/REQA.
             try:
                 self._nfc.rf_off()
                 time.sleep(0.003)