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().
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.
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.
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.
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.
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).
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.
Remove branding/about card, fold Device ID into Device Info card,
place Backend/Auth config and diagnostic buttons side by side in a
2-column grid, remove redundant online/offline status from Device
Info, and tighten spacing throughout.
Remove the branding/about card, fold Device ID into the Device Info
card, make diagnostic buttons a horizontal 3-column row, and put the
API token input and Save button on the same line. Tighten spacing
throughout so the tab fits on the small touchscreen without scrolling.
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.
BambuStudio converts virtual tray IDs (254/255) to -1 in the flat
ams_mapping and relies on ams_mapping2 for external spool routing.
Bambuddy was passing raw 254/255 in the flat array, which H2D firmware
rejects with "Failed to get AMS mapping table".
- Convert external tray IDs to -1 in flat ams_mapping (match BambuStudio)
- Fix ams_mapping2 for external trays: each virtual tray is its own AMS
unit with slot_id 0, not a shared unit differentiated by slot
- Fix main/deputy nozzle ID comment (255=main, 254=deputy per BambuStudio)
- Add 8 unit tests for start_print() mapping construction
Install script now runs apt-get upgrade -y after installing system
packages. A WiFi safeguard (APT hook + helper script) is installed
first, backing up NetworkManager connections before dpkg and restoring
them if wiped — prevents headless Pis from losing WiFi during upgrades.
Runs apt-get upgrade -y after installing system packages and the WiFi
safeguard hook. Ensures the Pi is fully up to date before deploying
SpoolBuddy, and the WiFi safeguard protects NM connections during
the upgrade.
APT hook backs up NetworkManager WiFi connections before dpkg runs
and restores them if they get wiped. Prevents headless SpoolBuddy
Pis from losing WiFi after apt upgrade (observed with Bookworm
kernel/raspi-config updates clearing system-connections/).
External folder scan generated thumbnails for 3mf/stl/gcode files but
skipped image files. Added IMAGE_EXTENSIONS check with
create_image_thumbnail() to the scan loop.
Some printers (e.g. H2D) only send {id, state} in incremental MQTT
tray updates — no tray_type, tray_color, or other fields. When
filament is unloaded (state changes from 11 to 10), the old tray
data persisted indefinitely because the merge logic only updates
fields present in the incoming message.
Backend:
- Detect tray state != 11 without tray_type in incremental updates
and clear stale tray data (bambu_mqtt.py)
- Expose tray `state` field via REST API and WebSocket broadcasts
(printer.py schema, printers.py route, printer_manager.py)
- Include AMS tray content in WebSocket dedup key so load/unload
transitions trigger broadcasts (main.py)
Frontend:
- Add `state` to AMSTray interface (client.ts)
- Show configure/assign buttons for state=10 (spool present, not
loaded) but hide for state=9 (truly empty) on AMS/HT slots
- Hide fill level bar on empty slots
Tests:
- 5 new tests covering state-based clearing, preservation, reload,
and idempotency
When a Bambu Lab spool is detected in the AMS but no tag match exists,
check for an untagged inventory spool with matching material/subtype/color
before creating a new entry. Links the RFID tag to the existing spool
(data_origin="rfid_linked") to prevent duplicate inventory entries.
Host directories (NAS, USB, network shares) can now be mounted
into the File Manager without copying files. Files are indexed
into the database on scan but read directly from their original
location. Supports read-only mode, hidden file filtering, and
automatic thumbnail extraction for 3MF/STL/gcode.
- POST /library/folders/external — create with path validation
- POST /library/folders/{id}/scan — discover/sync files
- Block uploads, moves, and deletes for read-only external folders
- Never delete actual files from external paths (DB-only removal)
- Purple folder icon + info bar with rescan button in UI
- i18n for all 7 languages
- 19 backend + 11 frontend tests
When a user closed the camera viewer, the stop endpoint killed the
ffmpeg process but never signaled the stream generator's disconnect
event. The generator saw "process died" as a dropped RTSP session
and respawned ffmpeg — up to 30 times per stream. The orphan cleanup
couldn't catch these because they were still tracked as active.
- Add per-stream disconnect events dict so stop endpoint can signal
generators to stop reconnecting before killing the process
- Check if stream_id was removed from _active_streams before
reconnecting (belt-and-suspenders with the event)
- Track frame timestamps per stream_id instead of per printer_id
so stale detection isn't fooled by newer streams for the same
printer
- Reduce stale thresholds from 120s+60s to 60s+30s
- Signal disconnect events from cleanup when killing stale streams
Diagnostic log added in 0.2.2.1 fired on every MQTT update while a
printer was in FINISH/FAILED state, not just on the state transition.
For farms with multiple idle printers, this produced thousands of log
lines per minute. Guard the log to only fire once when the state first
changes to FINISH/FAILED.
15 backend unit tests covering keypair management, branch detection,
SSH command execution (success/failure/timeout), and full update flow
with error handling. 4 new frontend tests for the Updates tab (version
display, buttons, SSH setup, update available state).
Just unregistering the SW wasn't enough — the old SW still controlled
the page for that load. Now detects if any SW exists, nukes all SWs
and caches, then reloads once to get a clean fetch from the server.
Subsequent loads skip this since no SW is registered.
The kiosk touchscreen has no way to hard-refresh, and the service worker
served stale cached JS after updates. SpoolBuddy pages now unregister
any existing SW and skip registration entirely. Regular desktop/mobile
users still get the SW. Restored kiosk restart in SSH update flow since
SW is no longer an obstacle.
The SW used stale-while-revalidate for JS/CSS, serving old cached
bundles even after a new build. Changed to network-first (Vite already
content-hashes filenames), bumped cache version v24→v25, and added
Cache-Control: no-cache to the sw.js endpoint so browsers always fetch
the latest service worker.
Rewrote update button states: single `busy` flag stays set from click
through to device pickup — no more gap with no feedback. Buttons hide
while any operation is in progress. Listens for spoolbuddy-online
WebSocket event to reload the page after daemon re-registers, ensuring
fresh version and status. Set staleTime to 0 on update check queries.
Check button had no visual feedback because isFetching doesn't trigger
reliably with cached data — replaced with manual loading state. Removed
kiosk restart via getty; the frontend now detects daemon re-registration
via WebSocket and calls window.location.reload(), keeping the user on
the same page and fetching all fresh data.
- Check button now shows spinner on refetch (isFetching vs isLoading)
- Force Update button shows spinner while triggering
- Reduced staleTime from 4 minutes to 30 seconds so the UI picks up
version changes after an update without a long delay
Merged version, status, and update check into a single card. Buttons
are side by side when up to date. Reduced padding and font sizes.
Removed separate "complete" banner since status is now cleared on
re-registration. SSH Setup section is smaller and more compact.
The SSH update set status to "complete" after the daemon had already
restarted and re-registered, overwriting the cleared state so it stuck
forever. Removed the post-restart "complete" write — daemon
re-registration is now the completion signal, clearing any update status.
After updates, the kiosk browser showed stale frontend assets from
Chromium's disk cache even after restarting. Added --disk-cache-size=0
to the launch flags — the kiosk loads a single page from the local
network so caching provides no benefit.
The getty@tty1 autologin had no network dependency, so the labwc/Chromium
kiosk chain started before connectivity was up — showing a connection
error for 10-15 seconds. Added After=network-online.target to the
autologin override so the browser has network when it launches.
After an SSH update completed, the UI kept showing "Update complete,
daemon restarting..." and the old "update available" banner because
nothing cleared the stale state. Registration now resets update_status
when the daemon comes back, and WebSocket handlers for spoolbuddy_update
and spoolbuddy_online invalidate the frontend queries immediately.