Three interrelated bugs in AMS info field parsing caused AMS units
to appear on the wrong nozzle on H2D dual-nozzle printers:
1. Info field parsed as decimal instead of hex (BambuStudio uses
std::stoull with base 16)
2. Extruder ID extracted as 1-bit instead of 4-bit field (bits 8-11)
3. Partial MQTT updates replaced the full extruder map instead of
merging into it
Virtual printers in Queue mode now have an "Auto-dispatch" setting.
When enabled (default), prints start automatically — preserving current
behavior. When disabled, prints are added with manual_start so they
wait for manual dispatch from the queue UI.
Checkbox selection + "Delete Selected" button in Settings > Filament
for both Spool Catalog and Color Catalog. Adds POST bulk-delete
endpoints and translations for all 7 locales.
P2S uses hardened steel rods, not carbon fiber. Move P2S from
carbon rod classification to new steel_rod category with its own
"Lubricate Steel Rods" / "Clean Steel Rods" maintenance tasks.
Add {eta} template variable showing wall-clock completion time
(e.g. "15:53" or "3:53 PM") to print_start, print_progress, and
queue_job_started notifications. Respects the user's time_format
setting (12h/24h). The existing {estimated_time} variable continues
to show duration ("1h 23m").
visibility into AMS spool assignments on printer cards without exposing
the full Inventory page (#635). The list_assignments endpoint now
requires this new permission instead of inventory:read. All default
groups (Administrators, Operators, Viewers) include it for backward
compatibility.
Closes#634
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.
FTP upload: reduce chunk size from 1MB to 64KB for smooth progress bar
updates (~1s intervals instead of 20+ second gaps). Skip voidresp() for
all printer models — H2D delays the 226 response by 30+ seconds after
data transfer, causing a hang at 100%. Add transfer speed and TLS
handshake timing to logs for diagnosing slow connections.
Print/Schedule modal: widen from max-w-lg (512px) to max-w-2xl (672px)
to accommodate long filament profile names like "PLA Support for PETG
PETG Basic @Bambu Lab H2D 0.4 nozzle".
Profiles like "PLA Support for PETG PETG Basic @Bambu Lab H2D" have
filament_type PETG, but both the frontend and backend name parsers
found "PLA" first (iterating material types in order). The MQTT command
sent tray_type=PLA and tray_info_idx=GFL99 (PLA generic), so the
slicer displayed PLA instead of PETG.
- Frontend parsePresetName(): detect "X Support for Y" pattern, extract
material after "Support for"
- Frontend ConfigureAmsSlotModal: prefer corrected parsed material over
stored localPreset.filament_type for tray_type, tray_info_idx, and
temperature fallback
- Backend _parse_material_from_name(): same "Support for" handling for
future profile imports
- Backend assign_spool: prioritize spool.material over lp.filament_type
for generic filament ID lookup
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.
BambuStudio uses TLS on port 3002 for certain printer models (e.g. A1
Mini / N1). The bind server only spoke plain TCP on both ports, so the
TLS ClientHello was rejected as "invalid frame" and the slicer could
never discover or connect to the virtual printer.
Port 3002 now uses TLS (reusing the VP's existing certificate), port
3000 remains plain TCP. Also updated proxy-mode to use TLSProxy for
the port 3002 bind proxy instead of raw TCPProxy.
sync_from_db() skipped VPs already in self._instances without checking
if their config had changed. Mode, model, access code, bind IP, remote
interface IP, and target printer changes were silently ignored until
manual toggle off/on or full restart. Now detects config drift and
restarts affected instances.
Four support package improvements:
1. Mask first two octets of subnet IPs in support info
(192.168.1.0/24 → x.x.1.0/24) to avoid leaking private network
addresses.
2. Fix Docker network_mode_hint detection. The old heuristic
(interface count > 2) always reported "bridge" on single-NIC
hosts because get_network_interfaces() excludes Docker
interfaces. Now checks for docker0/br-*/veth* visibility via
socket.if_nameindex() — these are only visible in host mode.
3. Parse MQTT "fun" field at top level of payload (not just inside
"print" key). Some firmware versions send it there, which
explains why developer_mode was null for most users.
4. Add virtual_printers section to support info with mode, model,
enabled/running status, and pending file count.
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.
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)
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
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.
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 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
When the snow field hadn't arrived yet, the ams_extruder_map fallback
computed ams_id * 4 + slot for AMS-HT units (128-135), producing
invalid tray IDs like 512+. AMS-HT has a single slot; global ID = unit
ID.
- Single-AMS fallback: return ams_id directly for AMS-HT (128-135)
- Multi-AMS matching: decompose AMS-HT trays correctly (slot=0)
- Multi-AMS narrowing: slot > 0 eliminates AMS-HT candidates
- last_loaded_tray: tighten to valid IDs only (0-15, 128-135, 254)
- Add 10 tests for AMS-HT fallback and last_loaded_tray validation
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
Add a new `printers:clear_plate` permission so admins can grant
plate-clearing ability without full `printers:control` access.
Existing groups with `printers:control` automatically receive the
new permission on startup via migration.
Replace the cramped permission modal in Settings with a dedicated
full-page group editor (`/groups/new`, `/groups/:id/edit`) featuring
search filtering, select all/clear all, category-level toggles,
and a responsive 2-column permission grid. Remove dead GroupsPage
code that was never routed.
- Backend: new Permission enum, category, defaults, endpoint guard, migration
- Frontend: GroupEditPage, updated routes, removed modal from SettingsPage
- Tests: 10 backend + 10 frontend tests for new permission and editor
- Docs: updated wiki (auth, printer-control, API), website, changelog
- i18n: added group editor keys to all 6 locale files (en/de/ja/fr/pt-BR/it)
Add color-matching fallback for slot-to-tray mapping on printers that
don't provide the MQTT mapping field (snow encoding) or reject request
topic subscription. Matches 3MF filament slot colors against AMS tray
colors to resolve the correct physical tray. Returns no match when
colors are ambiguous (duplicate tray colors) to safely fall through
to existing fallbacks.
Mapping priority chain: print_cmd → MQTT snow → queue → color match →
tray_now → slot_id-1.
On dual-nozzle printers (H2C, H2D Pro) with multiple AMS units, usage
tracking attributed filament consumption to the wrong spools. The MQTT
mapping field — a snow-encoded array mapping slicer slots to physical
AMS trays — was preserved in state but never parsed.
Add _decode_mqtt_mapping() to convert snow encoding (ams_hw_id * 256 +
slot) to bambuddy global tray IDs. Insert as priority 2 in the mapping
resolution chain, after print_cmd ams_mapping but before queue/tray_now
fallbacks. Works for all printer models and print sources.
Recent BambuStudio/OrcaSlicer updates require a bind/detect handshake on
port 3000 before connecting via MQTT/FTP. Without this, slicers cannot
discover or connect to the virtual printer in any mode.
- Add BindServer for server modes (immediate/review/print_queue)
- Add TCPProxy for raw TCP forwarding (proxy mode)
- Update Dockerfile (EXPOSE 3000) and docker-compose.yml (bridge port)
- Add 10 new tests for BindServer protocol and integration
Some printer MQTT brokers (e.g. P1S) reject subscriptions to the request
topic by killing the TCP connection, causing a disconnect loop. The request
topic subscription for ams_mapping capture is now fail-safe with two
detection paths:
- SUBACK rejection: on_subscribe callback detects failure code 0x80
- Broker kills connection: on_disconnect detects disconnect within 10s of
unconfirmed subscription attempt
After detection, the request topic is disabled for that printer and
reconnection succeeds with only the report topic. Printers that support
the request topic (e.g. X1E, H2D) get ams_mapping capture; printers that
don't (e.g. P1S) gracefully degrade to tray_now-based tracking.
When the auto-scan attached the wrong timelapse (e.g. from a different
print), there was no way to fix it — the file couldn't be removed and
re-scanning was disabled once a timelapse was attached.
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.
Usage tracking works on X1E but fails on H2D Pro — the wrong tray
is identified, attributing usage to the wrong spool. The root cause
is unknown because we don't know what MQTT mapping data each printer
model reports.
Adds INFO-level logging at print start and completion:
- state.raw_data["mapping"] (unknown format, preserved but never parsed)
- tray_now, last_loaded_tray values
- All raw_data keys containing "map" (discovery)
- Per-AMS-tray summary: tray_type, tray_color, tray_now, tray_tar
This enables comparing MQTT behavior across printer models without
requiring DEBUG mode, so testers can send normal logs.
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.
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.