Queue endpoints now return filament_type, filament_color, layer_height,
nozzle_diameter, and sliced_for_model from the archive or library file.
Previously this data was only available via the archive API.
Virtual printer "print_queue" mode created queue items with no printer
assignment. Now sets target_model from the VP's SSDP model code (e.g.
P1S, X1C) for "Any Printer" scheduling, or uses target_printer_id if
configured.
Settings page auto-save onSuccess overwrote localSettings with the
server response, discarding characters typed during the save request.
Removed the stale state overwrite so in-progress input is preserved.
When scheduling a print to "any printer" with a filament color override,
the scheduler picked a printer with the correct filament type but wrong
color. _find_idle_printer_for_model() used color matching only for
ranking candidates, not filtering — a printer with 0 color matches was
still selected if it had the right types.
Now requires at least 1 color match when filament overrides specify
colors. Printers with 0 matches are skipped and reported in the
"waiting for filament" reason instead of being treated as candidates.
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.
H2D firmware reports stg_cur=74 during print preparation, which was
not in the STAGE_NAMES lookup table (sourced from BambuStudio, which
only goes up to 66). This caused "Unknown stage (74)" to display on
the printer card. Also adds stage 77 ("Preparing AMS") which exists
in BambuStudio source but was missing from the lookup.
When scheduling a print to "any printer" (model-based assignment),
users can now override the 3MF's original filament choices per slot.
The override dropdown shows compatible filaments loaded across all
printers of the selected model, filtered by type and nozzle (H2D).
The scheduler matches against overridden type/color instead of the
original 3MF values, preferring printers with exact color matches.
Backend:
- New GET /printers/available-filaments endpoint (aggregates loaded
filaments across printers of a model, includes extruder_id for
dual-nozzle nozzle-aware filtering)
- New filament_overrides JSON column on print_queue table
- Scheduler applies overrides before AMS mapping, clears tray_info_idx
on overridden slots so color matching takes priority
- Printer selection prefers printers with most override color matches
Frontend:
- New FilamentOverride component with per-slot override dropdowns
- Type-filtered options (PLA slots only show PLA)
- Nozzle-aware filtering (H2D: only shows filaments on correct extruder)
- Integrated into PrintModal for model-based queue mode
Tests:
- 11 backend unit tests (color match counting, override application)
- 12 frontend tests (rendering, type/nozzle filtering, interactions)
i18n: All 6 locales (en, de, fr, it, ja, pt-BR)
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.
The rename endpoint updated `filename` but not `file_metadata.print_name`,
which the UI uses as the primary display name. Since print_name is extracted
from the 3MF at upload, it always took precedence over the renamed filename.
Now also updates print_name in file metadata when present.
The MQTT "fun" field is an integer in the JSON payload, but the
parser used int(value, 16) which requires a string. This raised
TypeError on every message, silently swallowed by the except clause,
so developer_mode was never set.
Two bugs caused spool assignments to always configure AMS slots with
generic Bambu filament IDs (e.g. GFB99 "Generic ABS") instead of the
spool's actual slicer preset:
1. PFUS* IDs (cloud-synced custom presets) were blanket-rejected and
replaced with generic IDs in both assign_spool and configure_ams_slot
2. Generic fallback IDs (GFB99, GFL99, etc.) were treated as "good"
presets by the slot-reuse logic, making them sticky once set
New priority: spool's own slicer_filament > slot's non-generic preset
(same material) > generic fallback.
Parse the MQTT "fun" field bit 0x20000000 to detect whether connected
printers have Developer LAN Mode enabled. Show a persistent orange
warning banner when any printer lacks it, since newer firmware silently
rejects MQTT write commands without developer mode.
- Parse fun field into developer_mode on PrinterState
- Add /printers/developer-mode-warnings lightweight polling endpoint
- Include developer_mode in printer status API and support bundle
- Orange banner with affected printer names and wiki link
- Translations for all 6 locales (en, de, fr, it, ja, pt-BR)
- 7 backend + 4 frontend tests
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.
P2S firmware sends local slot IDs (0-3) in tray_now, not global tray
IDs, when multiple AMS units are connected. The UI highlighted the
wrong AMS unit (e.g., AMS-A instead of AMS-B). Usage tracking was
unaffected because it uses the MQTT mapping field.
Added disambiguation in _handle_ams_data for single-nozzle printers
with ams_exist_bits indicating >1 AMS: cross-references tray_now with
the MQTT mapping field (snow-encoded) to resolve the correct global
tray ID. Falls back to raw value when no mapping is available or
ambiguous. No behavior change for single-AMS printers or dual-nozzle
(H2D) printers.
Add Quick Add mode to the spool form for simplified entry (material +
color + weight only), and a quantity field (1-100) for creating multiple
identical spools at once. Stock spools (no slicer profile) are computed
rather than stored — any spool without slicer_filament shows an amber
"Stock" badge. New filter chips (All/Stock/Configured) on the inventory
page. Backend bulk endpoint creates N spools in one transaction.
Backend:
- SpoolBulkCreate schema with quantity validation (1-100)
- POST /inventory/spools/bulk endpoint
Frontend:
- Quick Add toggle in SpoolFormModal (create mode only)
- Quantity field in FilamentSection (both modes)
- bulkCreateSpools API method and bulkCreateMutation
- Stock column (hidden by default) and stock filter chips
- Inline error rendering moved into FilamentSection
- CellCtx extended with t() for translatable cell content
Tests:
- 13 backend tests (schema validation + endpoint logic)
- 10 frontend tests (validateForm quickAdd + UI behavior)
i18n: 6 locales (en, de, fr, it, ja, pt-BR)
Docs: CHANGELOG, README, website features, wiki inventory
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.
When a spool runs empty mid-print and the AMS auto-switches to a backup,
the entire print weight was attributed to the original spool. Track tray
changes during the print via tray_change_log on PrinterState, then split
the 3MF weight across trays using per-layer gcode segment boundaries at
print completion. Falls back to linear layer-ratio splitting when gcode
data is unavailable.
When a print was started from BambuStudio (not Bambuddy), the
auto-archive has an empty file_path. The photo save code uses
base_dir / Path("").parent which resolves correctly, but the
serve/upload/delete endpoints used (base_dir / "").parent which
goes one directory level too high — returning 404 despite the
photo existing on disk.
ntfy servers with attachments disabled reject thumbnail uploads with
"attachments not allowed" (HTTP 400 / code 40014), causing the entire
notification to fail. Now automatically retries without the image so
the notification still gets delivered.
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
The log sanitizer only used regex patterns, missing arbitrary user-chosen
strings (printer names, usernames). Tasmota smart plug credentials were
logged verbatim in URLs by httpx.
- Make _sanitize_log_content() database-aware: query Printer names/serials,
User usernames, and Bambu Cloud email for exact-string replacement
(longest-first, skip <3 chars to prevent over-redaction)
- Fix serial regex leaking first 3 chars (remove capture group partial
redaction), add case-insensitive flag
- Move Tasmota credentials from URL-embedded (http://user:pass@host) to
httpx auth= parameter so they never appear in logs
- Add URL credentials regex as defense-in-depth for user:pass@ in logs
- Add 'username' and 'path' to settings sensitive_keys filter (catches
smtp_username, slicer_binary_path in support-info.json)
When sending notifications with image attachments (progress/error
events), the message body was placed in an HTTP Message header. Messages
containing newlines (e.g. "Printer: file.stl\nRemaining: 4h 52m") were
rejected by httpx since HTTP headers cannot contain newline characters.
Escape newlines to literal \n which ntfy renders as line breaks.
* 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().
MQTT raw data returns AMS unit and tray IDs as strings (e.g. "0"),
but _build_loaded_filaments() compared them to integers without
casting. After the scheduler assigned an idle printer to an "Any
Model" job, it crashed computing AMS mapping with:
'>=' not supported between instances of 'str' and 'int'
The exception prevented the assignment from committing, so the
scheduler retried every 30 seconds in an infinite loop. The job
stayed in "Waiting" and the "Clear Plate" button kept reappearing.
Cast ams_id and tray_id to int(), matching the pattern already used
for external spool IDs on line 550.
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.
The MQTT callback thread checked self._pending_kprofile_response before
calling .set(), but the asyncio thread could clear it to None between
the check and the use (TOCTOU race after get_kprofiles() timeout).
Capture the event reference in a local variable to eliminate the race.
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
The previous fix passed target_model from the frontend, but this relied
on printer.model being set on the client. When a printer was added
without selecting a model (the field is optional), the frontend sent no
target_model parameter, causing the OR logic to be bypassed entirely —
the widget only queried printer_id=X and missed unassigned model-based
items.
The backend now looks up the printer's model from the database when
target_model isn't provided by the client, ensuring "Clear Plate &
Start Next" appears for model-based queue jobs regardless of how the
printer was configured.