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 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
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.
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.
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.
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.
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. After updating the daemon, the kiosk browser is also
restarted so it loads the updated frontend.
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.
Changes:
- New: backend/app/services/spoolbuddy_ssh.py
- Rewritten: trigger_daemon_update endpoint (SSH instead of pending_command)
- New: GET /spoolbuddy/ssh/public-key endpoint
- Auto SSH key deployment via registration response + daemon
- Removed: daemon _perform_update() and cmd=="update" handler
- Install script: bash shell, sudoers for daemon + kiosk restart, .ssh/ setup
- Dockerfile: added openssh-client
- Frontend: SSH key display, force update button
- Fixed: update check compares APP_VERSION, not GitHub releases
- Fixed: kiosk browser restart after update
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
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.
The SpoolBuddy daemon update endpoint fetched GitHub releases and compared
them against device.firmware_version, which always showed "up to date"
because the comparison source was wrong. Now compares directly against
APP_VERSION from the running backend, so a daemon at 0.2.3b1 correctly
sees 0.2.3 as an available update.
BambuStudio connects to undocumented proprietary ports 2024-2026
on A1/P1S models during the print flow. The proxy wasn't forwarding
these ports, causing connection refused (RST) and triggering the
access code dialog instead of printing. MQTT and FTP worked fine —
port 2024 was the sole blocker.
Added transparent TCP pass-through proxies for ports 2024-2026,
following the same pattern as the existing FileTransfer (6000) and
RTSP (322) proxies. Silently ignored on models that don't use them.
Remove "Assign Spool" and "Configure" buttons from empty AMS slot
hover popups (standard AMS and HT AMS). Assigning a spool to a
physically empty slot created a stuck state — no unassign button
exists for empty slots, so the assignment couldn't be removed.
External spool holders are unaffected.
The OTA update feature added update_status and update_message to the
SpoolBuddyDevice model but no ALTER TABLE migration, causing
"no such column: spoolbuddy_devices.update_status" on existing databases.
pyOpenSSL 25.3.0 → 26.0.0 (CVE-2026-27448, CVE-2026-27459)
pyasn1 0.6.2 → 0.6.3 (CVE-2026-30922)
No breaking changes — Python 3.7 drop is irrelevant (we use 3.13),
cryptography >=46.0.0 requirement already satisfied (we have 46.0.5),
and we don't use set_tlsext_servername_callback (the behavioral change).
The SpoolBuddy layout now auto-checks for daemon updates every 5
minutes and shows "Update available: v{version}" in the status bar.
Removed the beta toggle since SpoolBuddy follows Bambuddy's release
channel. The daemon version is now read from backend APP_VERSION
instead of a stale hardcoded string.
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.
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
When targeting a specific printer, the scheduler's power-on-wait loop
created new MQTT clients on each attempt. Each new client re-tried the
request topic subscription, which some brokers (e.g. A1) reject by
disconnecting. This caused a 170s thrash loop leaving the connection
fragile, so the eventual print command silently failed to reach the
printer.
Cache request topic support per serial number at the class level so
new client instances inherit the knowledge and skip the subscription.
Multi-plate 3MF files now support selecting a subset of plates to queue
via checkboxes, instead of the binary "one plate" or "all plates" toggle.
In add-to-queue mode, each plate gets a checkbox for multi-select with a
Select All / Deselect All toggle. Reprint and edit modes remain single-select.
The saved preset bypassed the search filter entirely, so when a slot's
DB mapping was stale (e.g. previously Matte, now physically Silk), the
old preset always appeared regardless of search query. Remove the search
bypass — saved/current presets still bypass the printer model filter but
must match the search text like all other presets.
Add visual indicators so printers with HMS errors stand out in large
print farms:
- Red "Problem" counter in the status summary bar
- Status pip turns red (fatal/serious) or amber (warning) for HMS errors
- Progress bar turns amber when a print is paused
- Status sort prioritizes printers with HMS errors at the top
When a printer shuts down it sends a final MQTT message with
tray_exist_bits=0 and power_on_flag=false. The tray_exist_bits
clearing code processed this all-zero value, wiping every AMS
slot's filament data. On reconnect, the auto-unlink check saw
empty tray data (no color, no type) and deleted all spool
assignments as "fingerprint mismatch".
Fix: skip tray_exist_bits slot clearing when power_on_flag is
false. Defaults to true when absent for backwards compatibility.
Adds 3 regression tests covering shutdown preservation, genuine
removal still working, and missing power_on_flag fallback.
Replace fixed 30-second debug log collection with an interactive
3-step flow: start logging, reproduce the issue, stop & submit.
Users now control timing instead of racing a countdown.
Backend: split _collect_debug_logs() into POST /start-logging and
POST /stop-logging endpoints; add debug_logs field to submit request.
Frontend: 3-step progress indicator with elapsed timer, pulsing
active state, and 5-minute auto-stop. Updated all 7 locale files.
Add a "Rotate spool during drying" checkbox to the manual drying popover
for AMS 2 Pro and AMS-HT units. The firmware-level rotate_tray field was
already sent (hardcoded to false) — this makes it user-configurable.
The checkbox defaults to unchecked and resets each time the popover opens.
Firmware silently disables rotation if filament is currently loaded.