56 Commits
Author SHA1 Message Date
maziggy c1123365da fix(spoolbuddy): tolerate SPI_NO_CS rejection on Pi 5 (#1424)
Reporter on a Raspberry Pi 5 couldn't read NFC tags — gauge worked,
  SPI bus and wiring fine, but PN5180 transfers didn't complete.
  Manually commenting out `self._spi.no_cs = True` restored
  communication. Root cause: Pi 5's RP1 southbridge SPI driver
  (spi-rp1) doesn't honour the SPI_NO_CS ioctl the way the historical
  Broadcom driver on Pi 4 did.

  Safe to relax Pi-wide. SpoolBuddy's PN5180 NSS line is wired to
  GPIO23 (manual CS in _cs_low / _cs_high — the kernel's default
  auto-CS timing doesn't meet the PN5180's 5µs setup / 100µs hold
  spec). The hardware CE0 line (GPIO8) is not connected to the
  reader, so whether the kernel auto-toggles it is electrically
  invisible. The no_cs = True call was always cosmetic on this
  hardware.

  Wraps the assignment in try/except OSError in both the daemon and
  the diagnostic script; the daemon logs at debug level so future
  Pi-5-specific triage is greppable. README updated to drop the
  "spidev.no_cs = True resolves this" sentence and explain the
  manual GPIO23 CS scheme carries the timing on its own.
2026-05-19 12:33:33 +02:00
maziggy 18238cfeb2 fix(spoolbuddy): apply screen-blank timeout changes live without kiosk restart
Picking a new "Screen Blank Timeout" in SpoolBuddy Settings → Display
  didn't change actual blanking behaviour: whatever value was active when
  the kiosk last booted continued to fire. A user who started with the
  10m preset and switched to 1m or "Off" still saw the screen blank at 10m.

  Cause: blanking is driven by swayidle, started once by spoolbuddy-idle.sh
  at labwc autostart with the timeout passed as a command-line argument.
  The script fetched blank_timeout from the backend exactly once at startup
  and swayidle has no runtime control surface for changing its timeout.
  The daemon's display.set_blank_timeout() updated an in-memory variable
  that was never reached by swayidle, so UI changes were silently discarded
  until the next kiosk restart.

  Fix: extend the wake FIFO protocol with a "reload-timeout N" message.
  The daemon writes it whenever set_blank_timeout() sees a value that
  differs from the current one (the very first call is suppressed because
  the watchdog already fetched the same value at its own startup —
  signalling there would just thrash swayidle on every cold boot). The
  script's FIFO loop is restructured around start_swayidle / stop_swayidle
  helpers and a case statement: wake → wlopm --on + arm a re-blank at the
  current timeout; reload-timeout N → kill running swayidle, set TIMEOUT=N,
  restart swayidle, wlopm --on so the user sees the change took effect
  even if the screen was already blanked. The script de-dupes too — a
  reload-timeout whose N matches the current value is a no-op. Going from
  any positive timeout to 0 ("Off") stops swayidle and doesn't restart
  it; going from 0 to a positive value starts a fresh swayidle. Both work
  without a kiosk restart.

  The script's main loop opens the FIFO read+write (exec 3<>"$WAKE_FIFO")
  so bash read never sees EOF when the daemon momentarily disconnects
  between writes. A cleanup trap on TERM/INT/HUP stops swayidle, removes
  the FIFO, and exits cleanly.
2026-05-05 13:50:08 +02:00
maziggy 66a3604d9b Draft commit message:
fix(spoolbuddy): gate display wake on stable scale reading

  A noisy load cell that bounced ≥50 g around its midpoint kept the kiosk
  screen permanently lit. The wake threshold check ran against last_wake_grams
  which itself advanced to noisy values, so every bounce back across 50 g
  re-fired display.wake(), keeping the FIFO reader pumping wlopm --on
  faster than swayidle could trigger wlopm --off.

  The fix gates wake on the scale's `stable` flag — only readings that
  have settled within 2 g over a 1 s window count as a real spool
  placement / removal. Unstable noise can't fire wake and can't advance
  last_wake_grams, so the next genuine settled change is still measured
  against the right baseline.

  Three regression tests pin the contract: noisy ±60 g unstable readings
  never wake, a settled >50 g jump wakes, a noise burst between two
  settled readings doesn't poison last_wake_grams.
2026-05-03 09:22:35 +02:00
maziggy 2aabbe5d37 fix(spoolbuddy): sync SSH key over heartbeat to survive Bambuddy keypair rotation
Bambuddy's SSH keypair under <DATA_DIR>/spoolbuddy/ssh/ regenerates whenever
  the data dir is recreated (volume remount, container recreate, fresh deploy).
  The daemon previously only fetched the pubkey at registration, so any
  rotation after a successful boot left ~/.ssh/authorized_keys pointing at
  a stale public half — every Update click then failed with "Connection
  closed by authenticating user spoolbuddy [preauth]" until the daemon was
  restarted by hand. Each prior registration also appended a fresh entry
  without pruning, accumulating stale Bambuddy-tagged keys indefinitely.

  - HeartbeatResponse now carries ssh_public_key; the heartbeat route reads
    it via the same try/except shape as the register route so a missing or
    unreadable backend key doesn't break telemetry.
  - _deploy_ssh_key() strips lines tagged bambuddy-spoolbuddy and writes
    the current key once. No-op when already in sync (no mtime churn on
    every heartbeat). User-managed entries are preserved.
  - Daemon heartbeat handler calls _deploy_ssh_key when the response
    carries a key, so rotations propagate within one heartbeat instead
    of requiring a service restart.

  Tests: 5 unit (creates-when-missing, replace-stale-pileup, preserve-user-keys,
  idempotent, swallows-write-errors) + 2 backend integration (heartbeat carries
  the key; backend key-read failure leaves ssh_public_key None but the
  heartbeat still 200s).
2026-05-01 10:44:01 +02:00
maziggy 749257e97a . 2026-04-12 11:12:05 +02:00
maziggy a25f200529 . 2026-04-12 11:09:59 +02:00
maziggy 13d759c7ed . 2026-04-12 11:08:26 +02:00
maziggy 969174cc81 . 2026-04-12 11:05:22 +02:00
maziggy 2d6ddc431b feat(spoolbuddy): auto-wake display on NFC tag scan or scale activity (#945)
Display now powers on via wlopm when the daemon detects an NFC tag or
  a significant weight change (>=50g, i.e. spool placed/removed) while
  the screen is blanked.  Minor scale fluctuations no longer wake the
  display or prevent blanking.  The daemon only re-blanks screens it woke
  itself — touch-based wake/blank stays with swayidle so the two don't
  conflict.  Daemon discovers the Wayland session from the shared runtime
  dir since it runs as a systemd service outside the compositor.
2026-04-12 10:56:44 +02:00
maziggy 25203b3491 feat(spoolbuddy): auto-wake display on NFC tag scan or scale activity (#945)
Display now powers on via wlopm when the daemon detects an NFC tag or
  a significant weight change (>=50g, i.e. spool placed/removed) while
  the screen is blanked.  Minor scale fluctuations no longer wake the
  display or prevent blanking.  The daemon only re-blanks screens it woke
  itself — touch-based wake/blank stays with swayidle so the two don't
  conflict.  Daemon discovers the Wayland session from the shared runtime
  dir since it runs as a systemd service outside the compositor.
2026-04-12 10:51:11 +02:00
maziggy f135605908 feat(spoolbuddy): auto-wake display on NFC tag scan or scale activity (#945)
Display now powers on via wlopm when the daemon detects an NFC tag or
  weight change while the screen is blanked.  The daemon only re-blanks
  screens it woke itself — touch-based wake/blank stays with swayidle so
  the two don't conflict.  Daemon discovers the Wayland session from the
  shared runtime dir since it runs as a systemd service outside the
  compositor.
2026-04-12 10:47:04 +02:00
maziggy ca1b0a2cd5 feat(spoolbuddy): auto-wake display on NFC tag scan or scale activity (#945)
Display now powers on via wlopm when the daemon detects an NFC tag or
  weight change while the screen is blanked.  Daemon discovers the Wayland
  session from the shared runtime dir and coexists with swayidle which
  continues to handle touch-based wake independently.
2026-04-12 10:41:57 +02:00
maziggy b76d6210cf Add SpoolBuddy quick menu with power control and system commands (#893)
Swipe down from the top of the SpoolBuddy display to open a quick-access
  menu for toggling printer smart plugs and managing the device (restart
  daemon, restart browser, reboot, shutdown). All destructive actions
  require confirmation.

  Backend: new POST /spoolbuddy/devices/{id}/system/command endpoint
  queuing reboot/shutdown/restart_daemon/restart_browser commands.
  Daemon: handles commands via subprocess (sudo reboot, systemctl restart).
  Frontend: SpoolBuddyQuickMenu component, swipe-down gesture detection,
  i18n keys for all 7 locales.
2026-04-08 11:29:44 +02:00
Keybored eb3450b1d0 Spoolbuddy init and diagnostic improvements (#814)
Spoolbuddy init and diagnostic improvements (#814)
2026-03-26 16:16:09 +01:00
maziggy a321a709b5 Fix Read Tag diagnostic failing on NTAG tags
The read_tag.py diagnostic script had stale PN5180 NTAG methods: TX CRC
  was off (should be on), no Crypto1 clear, no IDLE→TRANSCEIVE state
  reset, and the PN5180 drops the card after each READ batch requiring
  reactivation between 4-page reads. Also rejected SAK 0x04 as
  unsupported. Synced register setup with daemon and added per-batch
  card reactivation.
2026-03-26 10:23:07 +01:00
maziggy 77cb7158d2 Add SpoolBuddy System tab with live OS stats from Raspberry Pi
The daemon now collects CPU temp, core count, load average, memory/disk
  usage, OS info, and system uptime every heartbeat using stdlib-only reads
  from /proc and /sys. Stats are sent as a JSON blob in the heartbeat
  payload, stored in a new system_stats TEXT column, and displayed in a
  new "System" tab in SpoolBuddy Settings with color-coded usage bars.
2026-03-26 09:57:09 +01:00
maziggy c1c31d8d91 Skip NTAG post-write verification — PN5180 read-back unreliable
The PN5180 cannot read more than 4 NTAG pages after a batch write:
  the second READ command (page 8+) returns rx_status=0 regardless of
  state machine reset strategy. The write itself succeeds (tag ACKs
  via SOF on every page). Remove verification and trust the writes.
2026-03-25 13:34:53 +01:00
maziggy a59c81d6a6 Fix NTAG read loop: IDLE→TRANSCEIVE once, set_transceive_mode after
Repeated IDLE→TRANSCEIVE resets inside the read loop broke the card
  session after the first READ (page 8 returned rx_status=0). Match
  the activate_type_a pattern: IDLE→TRANSCEIVE once before the loop,
  set_transceive_mode() for subsequent iterations.
2026-03-25 13:33:04 +01:00
maziggy 85ffb9e01f Fix NTAG read: apply same IDLE→TRANSCEIVE state machine reset
The verification read after writing failed because ntag_read_pages()
  used set_transceive_mode() which was a no-op when already in
  TRANSCEIVE. Apply the same IDLE→TRANSCEIVE reset pattern used in the
  write path, clear Crypto1, and add diagnostic logging.
2026-03-25 13:30:09 +01:00
maziggy 918dec4c19 Skip NTAG write ACK check — PN5180 can't capture 4-bit response
IRQ logging revealed the tag IS responding (RX_SOF_DET set) but the
  PN5180 cannot capture the 4-bit ACK as a complete frame — RX_IRQ
  never fires and RX_STATUS stays zero. Skip per-page ACK checking
  and rely on the read-back verification in ntag_write_pages().
2026-03-25 13:27:31 +01:00
maziggy 31ca2e7ddf Fix NTAG write: reset PN5180 state machine before WRITE command
The PN5180 transceive state machine wasn't being reset between the
  SELECT (from reactivate_card) and the WRITE command — just re-setting
  the TRANSCEIVE bits was a no-op. Use IDLE→TRANSCEIVE transition like
  activate_type_a does. Also clear Crypto1 bit and add IRQ status
  logging.
2026-03-25 13:24:57 +01:00
maziggy bae394d277 Add debug logging to NTAG write/verify path
Temporary diagnostics to identify which step of the NTAG write
  fails: per-page ACK status, reactivation, read-back, or data
  mismatch. Enable DEBUG level for pn5180 module.
2026-03-25 13:21:27 +01:00
maziggy 72536c93a0 Fix NTAG write ACK detection for 4-bit response
The NTAG WRITE ACK is 4 bits (0x0A), not a full byte. The PN5180
  RX_STATUS register reports 0 complete bytes with 4 extra bits, but
  the check only looked at the byte count — returning False on every
  successful write. Check both byte and bit fields of RX_STATUS.
  Also bump post-write delay from 5ms to 10ms for margin.
2026-03-25 13:18:41 +01:00
maziggy f4d8e1ecd3 Fix SpoolBuddy NFC write failing on NTAG 213/215/216 tags
Three issues:

  1. SAK gate too strict — NTAG chips can report SAK 0x04 (MIFARE
     Ultralight family) instead of 0x00. Accept both in nfc_reader.py
     and main.py.

  2. TX CRC disabled for NTAG WRITE — the NTAG spec requires CRC on
     the WRITE command frame. Enable TX CRC in ntag_write_page().

  3. TX CRC disabled for NTAG READ — the post-write verification read
     also failed because ntag_read_pages() sent the READ command
     without CRC. Enable TX CRC there too.
2026-03-25 13:15:45 +01:00
maziggy 9d4f57d54d Fix SpoolBuddy NFC write failing on NTAG 213/215/216 tags
Two issues:

  1. SAK gate too strict — NTAG chips can report SAK 0x04 (MIFARE
     Ultralight family) instead of 0x00. Accept both in nfc_reader.py
     and main.py.

  2. TX CRC disabled during WRITE — the NTAG WRITE command (0xA2)
     requires a CRC on the frame. The PN5180 was sending without CRC,
     causing the tag to NAK every write. Enable TX CRC for writes
     (RX CRC stays off for the 4-bit ACK).
2026-03-25 13:12:57 +01:00
maziggy f71367bb4e Fix SpoolBuddy NFC write rejecting NTAG 215 tags with SAK=0x04
NTAG 213/215/216 chips can report SAK 0x04 (MIFARE Ultralight family)
  instead of 0x00 during anticollision. Accept both values for NTAG
  detection and write operations in nfc_reader.py and main.py.
2026-03-25 13:10:11 +01:00
maziggy d5c520dd71 Added back debug logs to Spoolbuddy test scripts 2026-03-25 12:15:29 +01:00
maziggy 2b1fe03159 Fix SpoolBuddy scale first reading and extract hardware drivers
The NAU7802 ADC returns a stale max-scale value (0x7FFFFF) on its
  first conversion after power-up, polluting the moving average and
  making the initial weight report wildly inaccurate. Flush the first
  reading during init().

  Also extract both hardware drivers out of diagnostic scripts into
  proper daemon modules:
  - NAU7802 scale driver: scripts/scale_diag.py -> daemon/nau7802.py
  - PN5180 NFC driver: scripts/read_tag.py -> daemon/pn5180.py

  The production daemon was importing driver classes from test scripts
  since the original SpoolBuddy commit. Diagnostic scripts now import
  from the driver modules. Removed the sys.path hack from main.py.
2026-03-25 11:57:28 +01:00
Keybored 6e648804fc [Feature] Spoolbuddy Fixes and Improvements (#787)
[Feature] Spoolbuddy Fixes and Improvements (#787)
2026-03-24 11:56:33 +01:00
maziggy 5e9481859b Replace SpoolBuddy self-update with SSH-based updates from Bambuddy
The daemon's self-update mechanism (git fetch/reset on its own code) was
  fragile: .git permission errors, self-modifying code mid-run, hardcoded
  main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
  update remotely — matching its own branch, with step-by-step progress
  via WebSocket.

  SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
  returns the public key in the registration response. The daemon deploys
  it to authorized_keys on first connect — no manual setup needed.

  - New: backend/app/services/spoolbuddy_ssh.py (keypair, SSH commands, update orchestration)
  - Rewritten: trigger_daemon_update endpoint uses SSH instead of pending_command
  - New: GET /spoolbuddy/ssh/public-key endpoint for manual pairing
  - Removed: daemon _perform_update() and cmd=="update" heartbeat handler
  - Updated: install.sh — bash shell, sudoers for systemctl restart, .ssh/ setup
  - Updated: Dockerfile — added openssh-client
  - Updated: frontend — SSH key display, force update button
  - Fixed: update check now compares against APP_VERSION, not GitHub releases
2026-03-23 13:32:42 +01:00
maziggy bc1bba078b Replace SpoolBuddy self-update with SSH-based updates from Bambuddy
The daemon's self-update mechanism (git fetch/reset on its own code) was
  fragile: .git permission errors, self-modifying code mid-run, hardcoded
  main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
  update remotely — matching its own branch, with step-by-step progress
  via WebSocket. Install script updated with SSH access, sudoers entry,
  and --ssh-pubkey flag for pairing.
2026-03-23 12:51:21 +01:00
maziggy 18700cf5da Fix SpoolBuddy daemon reporting stale version in update check
The daemon had a hardcoded __version__ = "0.2.2b1" that was never
  bumped, causing the update check to always show an update available.
  Changed to read APP_VERSION from backend/app/core/config.py at import
  time so the daemon version stays in sync automatically.
2026-03-22 13:08:45 +01:00
maziggy cf2203914d [Feature] SpoolBuddy OTA updates via Bambuddy UI
SpoolBuddy devices can now be updated from Settings → Updates without
  SSH access. The daemon picks up an "update" command via its existing
  heartbeat, runs git fetch/reset + pip install, reports progress back
  to the backend, then exits for systemd to restart with the new code.

  Backend: update_status/update_message fields, trigger + status endpoints
  Daemon: _perform_update() handler, report_update_status() API method
  Frontend: "Apply Update" button with live progress in UpdatesTab
2026-03-22 12:40:25 +01:00
maziggy f943420ea2 Add SpoolBuddy NFC tag writing with OpenTag3D format
Write NTAG213/215/216 tags for third-party spools via the SpoolBuddy
  kiosk UI. New "Write" page with three workflows: existing spool, new
  spool creation, and tag replacement. Backend encodes 133-byte OpenTag3D
  NDEF payloads (material, color, brand, weight, temp). Daemon writes
  page-by-page via PN5180 NTAG WRITE command with read-back verification.
  Write commands flow through heartbeat polling with WebSocket status
  updates. Includes 39 new tests and translations for all 6 languages.
2026-03-02 13:56:15 +01:00
maziggy 48b492f619 Spoolbuddy
1. Device tab — repo URL unlinked

  Replaced the <a> tag with a <span> so the GitHub URL is displayed as plain text (no clickable link in kiosk mode).

  2. Display controls — moved to frontend

  Root cause: The daemon runs as a system user without Wayland access, and the kiosk uses an HDMI display (no sysfs backlight). Neither wlopm, vcgencmd, nor sysfs writes
  can work in this setup.

  Fix — frontend-controlled display in SpoolBuddyLayout.tsx:
  - Brightness: CSS filter: brightness(X) applied to the layout root. Immediate visual dimming that works on any display type.
  - Screen blanking: Full-screen black overlay (z-[9999]) shown after the configured inactivity timeout. Touch anywhere to wake.
  - Activity tracking: Resets on pointerdown/keydown events AND on WebSocket-driven NFC/scale changes (weight update, tag scan).
  - Queries device data every 15s to read the current brightness and blank timeout values.

  Daemon display_control.py — Simplified:
  - Removed vcgencmd/wlopm/subprocess usage entirely
  - _blank/_unblank are now state-tracking no-ops (log only)
  - DSI backlight brightness via sysfs is preserved as a bonus for DSI displays
  - Updated docstring to explain the architecture

  3. Scale tab — redesigned with step indicator

  Idle state: Same as before — weight card with tare/calibrate buttons.

  Calibration wizard: Two-column layout:
  - Left column (~64px): Vertical step indicator with numbered circles, checkmark for completed steps, and a connecting line
  - Right column: Live weight bar (with stable/settling indicator), step-specific content (instructions or numpad), and action buttons pinned to bottom via mt-auto

  The numpad buttons are min-h-[52px] with text-lg — large enough for touch on 1024x600. The weight info card is hidden during the weight entry step to maximize vertical
  space.
2026-03-02 11:58:10 +01:00
maziggy 28fda7b1a0 Display Controls (brightness + blanking)
Root cause: The daemon used wlopm for screen blanking, but wlopm was never installed. Additionally, the daemon runs as the spoolbuddy system user which has no access to
   the Wayland socket, so Wayland-based tools can't work.

  Fix in display_control.py:
  - Replaced wlopm --off/--on with vcgencmd display_power 0/1 — this is a Raspberry Pi firmware-level command that's pre-installed and works without Wayland socket access
  - Added shutil.which("vcgencmd") check at init to avoid repeated failures on non-RPi hardware
  - Added explicit PermissionError handling for brightness writes with a helpful message about the video group

  Fix in install.sh:
  - Added video group to the spoolbuddy service user's groups (was: gpio, spi, i2c → now: gpio, spi, i2c, video), which grants access to both vcgencmd and sysfs backlight
   files

  Scale Tab Numpad

  Root cause: On the 1024x600 kiosk screen (~376px available content height), the weight info card + numpad + action buttons exceeded the space, causing tiny buttons and
  overlapping.

  Fix in SpoolBuddySettingsPage.tsx:
  - Hide the weight info card during weight entry step (calStep !== 'weight'), reclaiming ~70px
  - Compact inline weight reading in the step header (small dot + monospace text) so users can still see the live scale value
  - Larger numpad buttons: min-h-[56px] with text-lg font size (was no min-height, text-sm)
  - Added active:scale-95 for tactile touch feedback
  - mt-auto on action buttons to push them to the bottom, preventing overlap
  - Removed the wrapping card around the calibration flow to save vertical padding
2026-03-02 11:40:45 +01:00
maziggy f2d28a0ef8 Spoolbuddy frontend fixes 2026-03-02 11:22:52 +01:00
maziggy 4fb4e5faac Fix SpoolBuddy scale calibration lost after reboot
_get_mac_id() used Path.iterdir() to find a network interface for the
  device ID, but iteration order is non-deterministic. On different boots
  it could pick eth0 (MAC …3100) or wlan0 (MAC …3102), producing a
  different device_id each time. Since calibration is stored per device ID
  in the backend DB, a new ID meant registering as an uncalibrated device.

  Sort interfaces alphabetically so the same one is always selected.
2026-03-01 13:27:04 +01:00
maziggy bf33dccfb4 Fix SpoolBuddy scale tare & calibration not being applied
The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to three broken links:

  1. Daemon received tare command via heartbeat but never called
     scale.tare() — the ScaleReader was available in shared dict
     but unused
  2. No API endpoint for the daemon to report the new tare offset
     back to the backend DB, so tare results were lost
  3. Heartbeat updated config but never called
     scale.update_calibration(), so ScaleReader kept initial values

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, and propagate calibration
  changes to the ScaleReader instance. Skip calibration sync on the
  heartbeat cycle that delivers a tare command, since the response
  predates the tare and would overwrite it with stale values.
2026-02-28 09:35:16 +01:00
maziggy 7bc549e985 Fix SpoolBuddy scale tare & calibration not being applied
The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to three broken links:

  1. Daemon received tare command via heartbeat but never called
     scale.tare() — the ScaleReader was available in shared dict
     but unused
  2. No API endpoint for the daemon to report the new tare offset
     back to the backend DB, so tare results were lost
  3. Heartbeat updated config but never called
     scale.update_calibration(), so ScaleReader kept initial values

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, and propagate calibration
  changes to the ScaleReader instance.
2026-02-28 09:31:39 +01:00
maziggy d466a4a86e 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.
2026-02-28 09:17:04 +01:00
maziggy 7a5c0b7a82 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 maintain tag detection. After a
  successful SELECT the card stays in ACTIVE state and ignores subsequent
  WUPA/REQA. Added a conditional RF off/on cycle (13ms) before each poll,
  but only when a tag is present — resets card from ACTIVE to IDLE for
  re-selection. The cycle is skipped when idle to avoid degrading reader
  state with continuous RF toggling, which prevented new tags from being
  detected after removal. Also added a preventive full hardware reset every
  60s when idle to recover from deeper stuck states, error-based recovery
  after 10 consecutive exceptions, and accurate heartbeat reporting of
  NFC/scale health.
2026-02-28 09:12:41 +01:00
maziggy 2d814ea5f4 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 maintain tag detection. After a
  successful SELECT the card stays in ACTIVE state and ignores subsequent
  WUPA/REQA, causing immediate false "tag removed" events. Added a brief
  RF off/on cycle (13ms) before each poll to force cards back to IDLE
  state. Also added a preventive full hardware reset every 60s when idle
  to recover from deeper stuck states where activate_type_a() silently
  returns None without exceptions. Heartbeat now reports actual nfc_ok/
  scale_ok instead of hardcoded True.
2026-02-28 09:07:31 +01:00
maziggy 464c5969ca Add SpoolBuddy tag detection modal and fix NFC reader silent failure
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 silently stopping tag detection. The reader
  drifts into a stuck state where activate_type_a() returns None without
  raising exceptions, making error-based recovery useless. Changed the
  preventive idle maintenance from a lightweight RF off/on cycle to a full
  hardware reset (RST pin toggle + RF re-init) every 60s — the same reset
  that a service restart performs. Also added error-based auto-recovery
  after 10 consecutive exceptions, promoted poll errors to WARNING, added
  periodic status logging, and fixed heartbeat to report actual nfc_ok/
  scale_ok instead of hardcoded True.
2026-02-28 09:02:46 +01:00
maziggy 7767851975 Add SpoolBuddy tag detection modal and fix NFC reader silent failure
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 silently stopping tag detection when the
  reader drifts into a stuck state. Add auto-recovery (full hardware reset
  after 10 consecutive errors), preventive RF cycling every 60s when idle,
  and periodic status logging. Heartbeat now reports actual nfc_ok/scale_ok
  from reader instances instead of hardcoded True.
2026-02-28 08:54:39 +01:00
maziggy 2811e2ec34 Remove stability flipping as scale report trigger
When ADC noise kept the spread hovering around the 2g stability
threshold, the stable flag toggled every cycle, forcing a report with
a slightly different weight each time. Now only actual weight changes
of >=2g trigger reports. The stable flag is still included in each
report for consumers that need it.
2026-02-27 13:31:51 +01:00
maziggy 12fe0741b5 Increase scale moving average window to reduce weight bouncing
5 samples at 100ms (500ms window) wasn't enough to smooth NAU7802 ADC
noise — the averaged value still varied by >2g between 1s report
intervals, and the stability state kept flipping, triggering a report
every cycle. Increased to 20 samples (2s window) so noise is smoothed
before reaching the reporting layer.
2026-02-27 13:31:51 +01:00
maziggy 541f8926b4 Fix SpoolBuddy daemon failing to import hardware drivers
The daemon imports read_tag and scale_diag as bare modules, but they
live in spoolbuddy/scripts/ which isn't on sys.path when systemd runs
the daemon. Added scripts/ to sys.path at startup, resolved relative
to the module file. Also moved the read_tag import inside NFCReader's
try/except (was crashing the daemon instead of skipping gracefully)
and demoted hardware-not-available messages from ERROR to INFO.
2026-02-27 13:31:51 +01:00
maziggy 8f3d194596 Fix SpoolBuddy daemon crash when read_tag module is missing
NFCReader.__init__ imported read_tag and instantiated PN5180() outside
the try/except block, so a missing module crashed the entire daemon.
Moved the import inside the existing try/except so the daemon gracefully
skips NFC polling — matching the scale reader's existing behavior.
2026-02-27 13:31:51 +01:00
maziggy 46f209e2ac Add SpoolBuddy AMS slot config, external slots, and dashboard redesign
- AMS page: external spool slots (Ext/Ext-L/Ext-R), click-to-configure
  modal on all slots, temperature/humidity threshold-colored indicators,
  nozzle L/R badges for dual-nozzle printers, compact AMS-HT layout
- Dashboard: two-column layout with device status + printers list (left)
  and current spool card (right), state-colored scale/NFC icons, dashed
  border card styling
- Daemon: suppress redundant scale reports (±2g threshold + stability
  state change detection) to prevent weight display bouncing
- TopBar: auto-select online printers only, SpoolBuddy logo
2026-02-27 13:31:48 +01:00