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
Files added to a project from the archive (status="archived") were
incorrectly counted in completed_prints and parts_progress stats.
Only status="completed" (actually printed) now counts toward completion.
All filament mapping dropdowns (single-printer, multi-printer, and
"Print to Any" model-based) showed only the base material type (e.g.
"PLA") without distinguishing subtypes like "PLA Basic" vs "PLA Matte",
making entries with different subtypes but same color look identical.
Backend: Add tray_sub_brands to available-filaments response and include
it in the dedup key so different subtypes of the same color appear as
separate entries.
Frontend: Show tray_sub_brands in FilamentMapping, FilamentOverride, and
PrinterSelector dropdowns, falling back to base type when unset. Add
traySubBrands field to LoadedFilament interface.
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.
- Add 12 backend integration tests for AMS labels API (GET/PUT/DELETE,
serial resolution, synthetic key fallback, whitespace handling, validation)
- Add 10 frontend tests for FilamentSlotCircle component (rendering,
border styles, background colors, text contrast inversion)
- Fix ruff W293 trailing whitespace in inventory.py from contributor fix
- Add ams_label model import to test conftest.py
- Update CHANGELOG, README, website features page, and wiki AMS docs
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 previous fix only filtered status codes (< 0x4000) from the
print_error field. Firmware can also send the same false positive
through the hms array in MQTT, which had no such filter. Apply the
same < 0x4000 check to the HMS parser so status/phase indicators
are skipped regardless of which MQTT field carries them.
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
* feat(queue): show spool grams left in filament slot mapping
* Bumped version
* Add SpoolBuddy AMS slot config, external slots, and dashboard redesign
- AMS page: external spool slots (Ext/Ext-L/Ext-R), click-to-configure
modal on all slots, temperature/humidity threshold-colored indicators,
nozzle L/R badges for dual-nozzle printers, compact AMS-HT layout
- Dashboard: two-column layout with device status + printers list (left)
and current spool card (right), state-colored scale/NFC icons, dashed
border card styling
- Daemon: suppress redundant scale reports (±2g threshold + stability
state change detection) to prevent weight display bouncing
- TopBar: auto-select online printers only, SpoolBuddy logo
* Fix SpoolBuddy daemon crash when read_tag module is missing
NFCReader.__init__ imported read_tag and instantiated PN5180() outside
the try/except block, so a missing module crashed the entire daemon.
Moved the import inside the existing try/except so the daemon gracefully
skips NFC polling — matching the scale reader's existing behavior.
* Fix SpoolBuddy daemon failing to import hardware drivers
The daemon imports read_tag and scale_diag as bare modules, but they
live in spoolbuddy/scripts/ which isn't on sys.path when systemd runs
the daemon. Added scripts/ to sys.path at startup, resolved relative
to the module file. Also moved the read_tag import inside NFCReader's
try/except (was crashing the daemon instead of skipping gracefully)
and demoted hardware-not-available messages from ERROR to INFO.
* Increase scale moving average window to reduce weight bouncing
5 samples at 100ms (500ms window) wasn't enough to smooth NAU7802 ADC
noise — the averaged value still varied by >2g between 1s report
intervals, and the stability state kept flipping, triggering a report
every cycle. Increased to 20 samples (2s window) so noise is smoothed
before reaching the reporting layer.
* Remove stability flipping as scale report trigger
When ADC noise kept the spread hovering around the 2g stability
threshold, the stable flag toggled every cycle, forcing a report with
a slightly different weight each time. Now only actual weight changes
of >=2g trigger reports. The stable flag is still included in each
report for consumers that need it.
* Fix formatting of option elements in FilamentMapping
* Make low stock threshold editable
* Add new filter for low spools
* Update bug report template to require additional fields
* Added toast for invalid imputs with locales, updated inputb field restrictions
* Minor Spoolbuddy frontend improvements
* Updated test_backend.sh
* Updated Spoolbuddy install script to strip down Raspbian
* Updated Spoolbuddy install script
* Add API key auth support to /auth/me for SpoolBuddy kiosk
When Bambuddy auth is enabled, the SpoolBuddy kiosk gets redirected to
the login page because ProtectedRoute requires a user from GET /auth/me,
which only handled JWT tokens. The kiosk daemon already has an API key
but couldn't use it to satisfy the frontend auth check.
- Backend: /auth/me now accepts API keys (Bearer bb_xxx or X-API-Key)
and returns a synthetic admin UserResponse with all permissions
- Frontend: AuthContext reads ?token= from URL on first load, stores in
localStorage, and strips from URL (prevents history/referrer leakage)
- Install script: kiosk URL now includes ?token=${API_KEY}
- Tests: 3 new integration tests (Bearer API key, X-API-Key header,
invalid key rejection)
* SpoolBuddy touch-friendly UI overhaul for 1024x600 kiosk display
Enlarge all interactive elements across 9 SpoolBuddy components to meet
44px minimum tap targets on the RPi touchscreen. Increase nav icons
(20→24px), labels (10→12px), bar heights, section headers, printer
buttons, spool visualizations, fill bars, and status indicators.
Compact the dashboard stats bar and remove the printers card. Add
fullScreen prop to ConfigureAmsSlotModal with two-column layout
(filament list left, K-profile + color right) to eliminate scrolling.
* Minor changes, CSS fixes
* Refactor usageFilter state to remove 'lowstock' option for clarity
* Move var saving to API, add test coverage
* fix: threshold validation and cleanup
* Change test input from '150' to '0'
---------
Co-authored-by: tridev <c.tripod@gmx.ch>
Co-authored-by: MartinNYHC <mz@v8w.de>
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.
SpoolBuddy's "Link to Spool" used the generic updateSpool API which only
set tag_uid, leaving tag_type and data_origin empty. Now uses linkTagToSpool
with tag_type='generic' and data_origin='nfc_link'.
Added a "Weight Check" inventory column (hidden by default) that compares
each spool's last scale measurement against calculated gross weight with
±50g tolerance. Shows green check for match, yellow warning + sync button
for mismatch. Backend stores last_scale_weight and last_weighed_at on each
spool when weight is synced via SpoolBuddy. Includes edge case handling
when scale weight < core weight. i18n keys added for all 6 locales.
- Add lightweight GET /archives/slim endpoint with column-level SELECT
(13 fields vs 46), skipping duplicate detection for ~70-80% payload
reduction on the stats dashboard
- Add ArchiveSlim Pydantic schema and TypeScript type
- Switch StatsPage and FilamentTrends to use ArchiveSlim
- Fix critical bug in failure_analysis.py: use effective_days for week
count, separate non-date filters, build fresh week_filter per loop
- Fix busiestDay timezone bug: parse YYYY-MM-DD with split() + local
Date constructor instead of new Date() which creates UTC midnight
- Fix success streak ordering: sort by completed_at || created_at
instead of created_at alone
- Add 6 integration tests for /archives/slim endpoint
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.
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.
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.
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
When Bambuddy auth is enabled, the SpoolBuddy kiosk gets redirected to
the login page because ProtectedRoute requires a user from GET /auth/me,
which only handled JWT tokens. The kiosk daemon already has an API key
but couldn't use it to satisfy the frontend auth check.
- Backend: /auth/me now accepts API keys (Bearer bb_xxx or X-API-Key)
and returns a synthetic admin UserResponse with all permissions
- Frontend: AuthContext reads ?token= from URL on first load, stores in
localStorage, and strips from URL (prevents history/referrer leakage)
- Install script: kiosk URL now includes ?token=${API_KEY}
- Tests: 3 new integration tests (Bearer API key, X-API-Key header,
invalid key rejection)
When scheduling a print to "Any {model}", a redundant "Target Model"
dropdown appeared even though the G-code is already sliced for a
specific printer model. Changing it would lead to print failures.
Hide the dropdown when slicedForModel is known — the tab label already
communicates the target. The dropdown still appears as a fallback for
legacy files without model metadata.
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.
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)
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
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.