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.
This commit is contained in:
maziggy
2026-02-28 09:17:04 +01:00
parent 7a5c0b7a82
commit d466a4a86e
2 changed files with 11 additions and 31 deletions
+10 -30
View File
@@ -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)