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.
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.
Print complete notifications were chained behind the finish photo
capture task with no timeout. If photo capture hung, the notification
would never send. Added a 45-second timeout so notifications always
fire regardless of photo outcome.
Also added diagnostic logging to MQTT state detection and upgraded
notification error logging to include stack traces for easier
debugging.
Per-printer camera rotation (0°/90°/180°/270°) for cameras mounted
in portrait or upside-down. CSS rotation for live views, Pillow
rotation for notification snapshots. Setting visible in external
camera config when enabled.
Add tests, docs, and ruff fixes for per-user email notifications
- Fix missing timezone import in email_service.py (F821)
- Fix unused lambda arg in main.py asyncio done_callback (ARG005)
- Fix E302 blank line spacing for mark_printer_stopped_by_user
- Fix F821/UP037 forward reference in user_email_pref model
- Fix SettingsPage test for duplicate "Notifications" text
- Add backend unit tests for permissions, schemas, and templates
- Add backend integration tests for user-notifications API
- Add frontend tests for NotificationsPage
- Add user_email_pref model import to test conftest
- Update CHANGELOG and README
Printers send many undocumented HMS codes (e.g. 0501_0002, 0501_0004,
0300_0001) that don't correspond to real errors — calibration status,
firmware state, etc. The frontend already filters these out (only shows
codes in ERROR_DESCRIPTIONS), but the backend notified for all codes
with severity >= 2, causing spurious email/push notifications on every
backend restart when a printer has active phantom codes.
Flip the logic: only send notifications for HMS codes with a known
description in the backend's HMS_ERROR_DESCRIPTIONS dictionary. Also
fix the log message to report the actual number of notifications sent,
not the pre-filter count.
Notify users when the first layer finishes printing so they can check
adhesion remotely. Triggers once per print when layer 2 begins
(layer_num >= 2, capped at <= 5 to handle reconnects). Includes a
camera snapshot attachment. Adds the on_first_layer_complete toggle
to all notification providers, with backend/frontend/i18n support
across all 7 locales.
Start, monitor, and stop drying sessions for AMS 2 Pro (n3f) and
AMS-HT (n3s) directly from the Printers page. Flame icon in AMS card
header opens a popover with filament-based temperature/duration presets
from BambuStudio. Live countdown status bar shows time remaining.
Backend: cache module_type from MQTT get_version, expose dry_time and
module_type in both WebSocket and REST paths, add supports_drying()
with firmware gating, add server-side guard on start_drying endpoint.
Frontend: drying button, popover with filament select + temp/duration
sliders, status bar, start/stop mutations. i18n for all 7 locales.
Supported: X1/X1C (fw 01.09+), P1P/P1S (fw 01.08+), H2D (fw 01.02.30+),
H2D Pro, X1E. Not supported: P2S, A1, A1 Mini, H2S, H2C.
Two bugs prevented bed cooldown notifications from working:
1. Stale temperature data: After print completion, the printer sends
partial MQTT updates without bed_temper, leaving the cached value
frozen at the end-of-print temperature. The monitor polled for 30
minutes seeing the stale value above threshold, then timed out.
Fix: send periodic pushall commands to force fresh temperature data.
2. Missing fields in provider create endpoint: on_bed_cooled and all 7
queue event toggles were omitted from the NotificationProvider
constructor, silently discarding user selections on create (update
worked fine via dynamic setattr).
Also added debug logging to the bed cooldown polling loop.
Two lazy-load bugs caused greenlet_spawn errors in the RFID auto-assign
flow:
1. create_spool_from_tray set spool.k_profiles=[] AFTER db.flush(),
but assigning to a relationship on a persistent object triggers a
lazy load (SQLAlchemy loads the current collection before replacing).
Moved initialization to BEFORE db.add().
2. spool.assignments was never initialized or eagerly loaded, so
db.add(SpoolAssignment) triggered a back_populates lazy load
outside the async greenlet. Added assignments=[] in create and
selectinload(Spool.assignments) in get_spool_by_tag.
Also added exc_info=True to error handlers for full tracebacks, and
19 new tests including greenlet regression tests that reproduce the
exact failure.
Floating bug report button submits issues via bambuddy.cool relay (no GitHub
token needed locally). Collects 30s debug logs with printer push_all, sanitizes
all sensitive data, uploads logs as files to GitHub. Screenshot upload/paste/drag
with JPEG compression. Translated into all 7 languages. Includes 21 tests.
The MQTT completion handler stored "aborted" as the queue item status
when a print was cancelled mid-print, but the response schema only
allows "cancelled". Pydantic validation failed on the invalid status
when listing all queue items, returning 500. Filtering by specific
status excluded the bad row so those still worked.
Normalise "aborted" → "cancelled" before storing. A startup fixup
also converts any existing "aborted" rows in the database.
Fix camera button permissions & ffmpeg process leak (#550)
Camera button on printer card was clickable without camera:view
permission. ffmpeg processes (~240MB each) accumulated after closing
camera streams because: (1) stop endpoint called terminate() without
wait()/kill(), (2) HTTP disconnect detection only ran between frames
so was blocked when the generator was stuck on stdout read, and
(3) no mechanism caught processes orphaned by generator abandonment
or app restarts.
- Add camera:view permission check + tooltip to camera button
- Fix stop endpoint: terminate() → wait(2s) → kill() → wait()
- Add background disconnect monitor (polls every 2s, kills ffmpeg
directly on disconnect)
- Add periodic /proc scan (every 60s) that SIGKILLs any ffmpeg
with rtsps://bblp: not in an active stream
- Add noCamera i18n key to all 6 locales
- Fix camera API test mocks for async wait() and pid attribute
The SQLite WAL was never checkpointed during container shutdown, leaving
uncommitted data (including schema migrations) in the -wal file. On
restart, partial WAL recovery could cause inconsistent query results,
making the queue appear empty until a filter was applied. Add
PRAGMA wal_checkpoint(TRUNCATE) and engine.dispose() to the lifespan
shutdown handler.
Add weight_locked flag to spools that auto-sets when weight_used is
explicitly updated via the API. Both the MQTT AMS remain% auto-sync and
the manual force-sync endpoint skip locked spools. Usage tracker delta
tracking is unaffected. Users can re-enable AMS sync by setting
weight_locked to false.
Three independent code paths wrote to archive.cost with conflicting
strategies, causing the same model to produce different prices on each
reprint (e.g. £0.77, £1.54, £2.03).
- Remove add_reprint_cost (Path 3) — redundant cost accumulation that
double-counted on top of usage tracker
- Fix usage tracker (Path 2) — compute cost from current session's
results only, not a SUM of all historical SpoolUsageHistory rows
- Remove _reprint_archives tracking set from main.py
- Update test mocks to match simplified query pattern
archive.cost now always reflects the cost of a single print.
All backend timestamps used datetime.now() (server local time) or the
deprecated datetime.utcnow(). The frontend's parseUTCDate() assumes
timestamps without timezone indicators are UTC and appends 'Z', so
stored timestamps were off by the timezone offset when the container's
timezone wasn't UTC.
Backend: replaced datetime.now() and datetime.utcnow() with
datetime.now(timezone.utc) across 16 files (~80 call sites) for all
database fields and DB comparisons. Cosmetic timestamps (filenames,
user-facing local time formatting) intentionally left as local time.
Frontend: replaced 13 new Date(backendTimestamp) calls with
parseUTCDate() across 8 files to correctly interpret UTC timestamps.
The bed cooldown monitor was defined at the end of on_print_complete,
after an early return that exits when no archive is found. Prints
started from BambuStudio or the printer's touchscreen have no archive,
so the function returned before the bed cooldown task was ever created.
Moved the bed cooldown block (function def + task creation) to before
the archive_id early-return so it fires for all completed prints.
Also hardened the temperature dict check from truthiness to isinstance.
External spool assignments (ams_id=255) were silently deleted on every
AMS change because _find_tray_in_ams_data only searches AMS units, not
vt_tray. Now looks up external assignments in the printer's vt_tray data
instead, and preserves assignments when vt_tray hasn't arrived yet.
SpoolBuddy turns a Raspberry Pi 4B with a PN5180 NFC reader and
NAU7802 scale into a filament management station that integrates
with Bambuddy via REST API and WebSocket.
Backend: SpoolBuddyDevice model, 10 REST endpoints (/spoolbuddy/*),
6 WebSocket broadcast types, background offline-detection watchdog.
RPi daemon: asyncio service with concurrent NFC polling (300ms,
MIFARE Classic + Bambu HKDF key derivation), scale reading (10 SPS,
5-sample moving average, stability detection), and 10s heartbeat
with exponential backoff reconnect.
Frontend: kiosk-optimized 1024x600 UI at /spoolbuddy with Dashboard
(live weight + NFC spool detection), AMS Overview, Inventory,
Printers, and Settings pages. useSpoolBuddyState reducer hook
driven by WebSocket CustomEvents.
i18n: all 6 locales (en, de, fr, it, ja, pt-BR).
When the 3MF source file wasn't downloaded from the printer (e.g. FTP
failure on P2S), archive.file_path was null. The finish photo capture
silently skipped because it derived the save directory from file_path.
Now falls back to archive/{id}/ directory and logs a warning.
When FTP download of a 3MF fails (e.g. BambuStudio-initiated prints),
the fallback archive has file_path="". Path.exists() on
settings.base_dir / "" resolves to the base directory itself and
returns True, so ZipFile() then fails with [Errno 21] Is a directory.
Replace .exists() with .is_file() across all 15 archive route checks
and 1 in main.py. Add file_path truthiness guard for finish photo
capture to prevent saving photos under the base directory.
Three bugs allowed uploaded gcode files to survive on the printer's SD
card, causing firmware restarts to auto-start old prints hours later.
1. The post-print cleanup retry loop always broke after attempt 1
regardless of delete success — delete_file_async returns False on
failure instead of raising, so the except-based retry never fired.
2. When start_print() failed after uploading, the file was never cleaned
up since on_print_complete never fires for prints that never started.
3. Cleanup failures were logged at DEBUG level, invisible in production.
Infrastructure/auth HMS error codes like 0500_0007 (MQTT command
verification failed) triggered false-alarm printer error notifications
even though they don't indicate actual print problems.
- Add suppress list for non-actionable error codes: 0500_0007 (MQTT
auth failure), 0500_4001 (cloud connection failure), 0500_400E
(print cancelled by user)
- Skip suppressed codes in the notification loop so they are still
tracked in HMS state and shown in the UI, but don't fire notifications
* Adding the energy cost from 2 to 3 decimal precision
* Complying with pr
* Complying with PR
* Complying with PR
* Fix energy cost display precision in ProjectDetailPage
* Change energy cost formatting to two decimal places
* Change decimal precision for energy cost display
---------
Co-authored-by: MartinNYHC <mz@v8w.de>
The post-print SD card cleanup used printer_manager.get_printer(),
which returns a PrinterInfo object with only name and serial_number.
The code then accessed .ip_address, .access_code, and .model, which
don't exist on PrinterInfo — raising AttributeError on every print
completion. The outer except Exception handler silently swallowed it
at debug level.
Result: uploaded gcode files were never deleted from the printer's
SD card root. On some printers (e.g. P1S), files left in root
auto-start on power cycle, causing phantom prints.
Replaced with a DB query for the Printer model, matching the pattern
used everywhere else in on_print_complete().
When a spool ran empty during a print and the AMS auto-switched to a
backup, on_ams_change eagerly deleted the SpoolAssignment (fingerprint
mismatch). on_print_complete then queried the now-deleted assignment
and silently dropped usage.
Snapshot all spool assignments into PrintSession at print start. At
completion, prefer the snapshot over live DB queries in both the 3MF
primary path and the AMS remain% delta fallback. Fall back to live
query for backward compatibility with in-flight sessions started
before upgrade.
This allows users sending particularly large prints to slow printers such as the P1-series to start a print and then move onto another task in Bambuddy immediately (such as starting more prints on more printers). It also gives the user visibility into what's happening instead of a loading indicator appearing for an indefinite period of time.
The new toast-based UI uses websockets to update in real time. It will also appear for other users / instances of Bambuddy, not just the user who started the prints, allowing more transparency and handling cases where the user closes the page and then comes back wanting to know the status of the dispatching.
fix: review fixes for background dispatch PR #408
- Restore missing imports in main.py (inventory, print_log, virtual_printers, mqtt_smart_plug_service)
- Guard voidresp() for A1 printers to prevent hang after upload
- Don't fail upload on voidresp() error since data transfer already completed
- Add ams_mapping to register_expected_print calls for Spoolman usage tracking
- Fix cancel_job TOCTOU race by using single lock acquisition
- Fix batch counter reset TOCTOU by re-checking condition inside second lock
- Add backgroundDispatch translations to fr.ts and pt-BR.ts
- Remove dead upload_progress_callback definitions
- Skip redundant "Print queued" toast in reprint mode (dispatch toast handles it)
- 5 backend tests: cancel_job single-lock TOCTOU, batch reset re-check,
job lifecycle
- 2 FTP regression tests: voidresp error handling (upload-loop fix),
A1 model voidresp skip
- 1 frontend test: reprint toast suppression
- CHANGELOG: background dispatch feature + test coverage entries
- README: add background dispatch to Scheduling & Automation
- Website: add feature item to Automation section
- Wiki: add Background Print Dispatch section to print-queue.md
Multiple Virtual Printers:
- Each VP gets a dedicated bind IP with independent FTP, MQTT, SSDP, and Bind services
- New VirtualPrinter DB model, CRUD API (/api/virtual-printers), React UI
- VirtualPrinterList, VirtualPrinterCard, VirtualPrinterAddDialog components
- Per-instance TLS certificates (shared CA), 11 printer models, all 4 modes
- Auto-incremented serial suffixes, network interface override per VP
Dual Bind/Detect Ports (#445):
- Listen on both ports 3000 and 3002 for slicer bind/detect handshake
- Different BambuStudio/OrcaSlicer versions use different ports
- Applies to BindServer (server mode) and SlicerProxyManager (proxy mode)
- Updated Dockerfile, docker-compose.yml, firewall rules in wiki
Also:
- Rewrote VP test suite for new multi-instance architecture (75 tests)
- Rewritten "How it works" section with 3-step workflow explanation
- Updated all 5 locales (en, de, ja, fr, it)
- Updated wiki and website for multi-VP + dual ports
- New multi-VP screenshot
* Debounce HMS error tracking to prevent flapping notifications
HMS errors on newer printers (e.g. H2S) can flicker on and off
every few seconds as conditions fluctuate around thresholds. For
example, chamber temperature regulation during PETG prints sends
HMS code 0300A70000030001 repeatedly as temps oscillate. Previously
each reappearance after a brief hms:[] gap was treated as a new
error, triggering duplicate notifications. This adds a 30-second
grace period before clearing the notification tracking state so
flapping errors are only notified once per episode.
* build(deps): bump aquasecurity/trivy-action
Bumps the github_actions group with 1 update in the /.github/workflows directory: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).
Updates `aquasecurity/trivy-action` from 0.33.1 to 0.34.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0)
---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
dependency-version: 0.34.0
dependency-type: direct:production
dependency-group: github_actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* Updated CI
* build(deps): bump aquasecurity/trivy-action
Bumps the github_actions group with 1 update in the /.github/workflows directory: [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).
Updates `aquasecurity/trivy-action` from 0.33.1 to 0.34.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0)
---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
dependency-version: 0.34.0
dependency-type: direct:production
dependency-group: github_actions
...
Signed-off-by: dependabot[bot] <support@github.com>
* Updated CI
* Updated CI
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: MartinNYHC <mz@v8w.de>
Slicer protocol handlers (bambustudio://, orcaslicer://) launch the
slicer app which fetches files via HTTP but cannot send auth headers.
The global auth middleware returned 401, causing "importing failed."
Add short-lived, single-use download tokens: the frontend fetches a
token via authenticated POST, then builds a /dl/{token}/{filename} URL
the slicer can access without auth headers. Tokens are validated
server-side (5-min expiry, single-use). Applies to archive files,
source 3MFs, and library files.
Also fix platform-specific URL formats to match slicer source code:
- macOS: bambustudioopen:// with encodeURIComponent
- Windows/Linux: bambustudio://open?file= (was wrongly using macOS scheme on Linux)
- OrcaSlicer: orcaslicer://open?file=
The queue item status update (printing → completed/failed) was placed
after `if not archive_id: return` in on_print_complete. When archive
lookup failed (app restart mid-print, manual deletion), the function
returned early and the queue item stayed stuck in "printing" forever.
Over multiple print cycles, stale items accumulated — showing double
the actual printer count in the "Printing" stat and completed prints
remaining in the "Currently Printing" section.
Move the entire queue item status update block (status transition,
MQTT relay, queue-completed notification, auto-power-off) to before
the archive_id early return, matching the pattern used for SD card
cleanup.
The SD card file cleanup (which prevents phantom prints on power cycle)
was placed after the archive_id early-return in on_print_complete. Users
with auto-archiving disabled never reached the cleanup code, so ghost
prints persisted on every reboot.
- Move cleanup before the archive_id check so it always runs
- Add retry loop (3 attempts, 2s delay) for printers that briefly lock
the filesystem after a print ends
- Try both .3mf and .gcode extensions
Reported by @DIYofThings in #374.
Subscribe to the MQTT request topic (device/{serial}/request) to intercept
print commands from any source (BambuStudio, OrcaSlicer, Bambu Handy, or
Bambuddy itself). Captures ams_mapping from project_file commands and passes
it through print start/complete callbacks. Fixes wrong tray attribution on
H2D Pro when prints are started from external slicers.
Three fixes for inventory spool usage not updating after prints:
1. Store ams_mapping from print command (reprint, library print, queue)
so the usage tracker maps 3MF slots to the actual physical trays
the user selected, not the default slicer mapping.
2. Track last_loaded_tray on printer state — the last valid tray_now
(0-253) seen during printing. On H2D printers, tray_now is always
255 in AMS data; the real tray resolves via snow field ~44s after
print start but reverts to "unloaded" at completion. The fallback
chain is: tray_now_at_start > current tray_now > last_loaded_tray.
3. Use color similarity (Euclidean RGB distance, threshold 50) instead
of exact hex match for auto-unlink fingerprint checks. RFID sensors
report slightly different shades across reads (e.g. distance ~43.6
for the same spool), causing false assignment unlinks after prints.
The print scheduler uploads .3mf files to the printer's SD card root
but never cleans them up. Some printers (e.g. P1S) auto-start files
found in root on power cycle, causing phantom/ghost prints on every
reboot. Now deletes the uploaded file via FTP after print completion
(best-effort, non-blocking).
Three bugs fixed and one recovery feature added:
1. AMS remain=0 zeroed all spools on printer power-off. The weight sync
treated 0% remain as "fully consumed," setting weight_used to
label_weight for every assigned spool. Fix: skip remain=0 in AMS
weight sync — empty spools are tracked by the usage tracker.
2. Editing any spool field sent stale weight_used from the React Query
cache back to the server, resetting usage-tracked weight. Fix: only
include weight_used in PATCH when the user explicitly changes it.
3. K-profile auto-select crashed on dual-nozzle printers for non-BL
spools with 'SpoolKProfile has no attribute extruder_id'. The model
attribute is 'extruder', not 'extruder_id'.
4. New "Sync Weights from AMS" button in Settings > Filament Tracking
(built-in inventory mode) to force-recover spool weights from live
AMS sensor data. Bypasses the "only increase" guard for explicit
user-initiated recovery.