mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 03:31:25 +02:00
b23cb69a663ea35339d54bf45d3f94c19dd6ca01
721
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3ef197e4e0 |
feat(slicer): Pipelines — multi-copy + class targeting + fanout + runs dashboard + retry-failed + WS updates (#1425 PR C — completes the v3 design)
PR A/B turned the slice modal's preset bundle into a one-click dispatch
with a pinned target printer. PR C closes the original issue: operators
type in a number of copies, Bambuddy slices once and distributes prints
across a fleet per the pipeline's chosen fanout strategy. A new dashboard
surfaces every run with filters, expandable per-copy status, cancel,
and retry-failed-copies. WS pushes keep everything live.
Backend
- copies field on POST /run, capped by new pipeline_max_copies setting
(default 50, hard cap 1000). PipelineRun.parent_run_id chains retries.
- SlicerPipelineUpdate accepts target_kind (specific_printer /
printer_class), target_model_class, fanout_strategy.
- Eligibility matcher branches: class-targeting enumerates matching
Printer rows, runs per-printer checks via a status_lookup closure,
returns printer_reports[]. New issue kinds: no_class_matches,
class_not_set.
- _pick_assignments distributes copies per strategy:
- max_parallel: target_model set, printer_id None — scheduler picks
- round_robin: copy i → eligible[i % N], fixed printer_id
- fill_one_first: all copies pinned to eligible[0]
All three reuse the slice-once path through slice_dispatch.enqueue.
- New routes:
- GET /pipeline-runs (paginated, filterable by pipeline + status)
- POST /pipeline-runs/{id}/retry-failed (creates child run with
copies = failed+cancelled count, parent_run_id set)
- Cancel cascades to all N queue entries (only pending/queued)
- _roll_up_run_status computes run-level status from per-job statuses;
introduces partial_failure for "some completed, some failed".
- ws_manager.broadcast_to_user emits pipeline_run_updated on every
state transition with the full materialised response.
Frontend
- Pipeline editor: target_kind radio + class picker (filtered to
installed models) + fanout-strategy radio. Read-only row shows
"X1C · Round robin" for class pipelines.
- RunWithPipelineModal: copies number input bounded by
settings.pipeline_max_copies. Accepts class-targeted pipelines.
- Settings → Workflow → Queue & Dispatch: new "Slicer Pipeline limits"
card with the max-copies input.
- New /pipelines/runs dashboard page (sidebar entry, gated on
pipelines:read). Two-filter dropdown, 25-per-page pagination, per-row
expandable to job list, Cancel + Retry-failed buttons.
- useWebSocket case for pipeline_run_updated invalidates both
pipeline-runs-all and pipeline-runs/{id} query keys.
|
||
|
|
4bbf0f031e |
feat(slicer): Pipelines — archive entry point + slicer progress toast (#1425 PR B follow-up)
Two real gaps from the PR B drop:
1. Run-with-pipeline only existed in the file manager. Operators who keep
working files in archives had to copy them to the library to use a
pipeline.
2. Triggering a slice via a pipeline produced a silent multi-second-to-
minute wait. The manual SliceModal flow shows the sticky
"Slicing X - Generating G-code 75%" persistent toast; the pipeline
path went through asyncio.create_task directly and never registered
with SliceJobTracker.
Archive entry point
- POST /slicer-pipelines/{id}/check-eligibility and /run accept
source_archive_id as an alternative to source_library_file_id (XOR,
enforced by Pydantic validator).
- PipelineRun.source_archive_id is a new nullable FK column with the
ALTER TABLE migration in run_migrations (idempotent via _safe_execute,
works on SQLite + Postgres).
- _resolve_source branches: archive path reads source_3mf_path with
fallback to file_path, mirroring routes/archives.py.
- ArchiveCard's context menu picks up a "Run with pipeline" item next to
Slice (only on source archives), gated on useSlicerApi + pipelines:run.
Slice (only on source archives), gated on useSlicerApi + pipelines:run.
- Path-safety: SEC-PATH-OK markers added at both LibraryFile.file_path
and archive.source_3mf_path join sites, citing the upload-time
validators.
Progress toast
- Pipeline orchestration is now the `run` callable of a
slice_dispatch.enqueue call — the same dispatcher SliceModal uses —
instead of a bare asyncio.create_task. The SliceJob lifecycle drives
the existing progress toast end to end with no separate notification
surface for pipeline runs.
- PipelineRun.slice_job_id is set before the 202 returns.
- RunWithPipelineModal calls useSliceJobTracker().trackJob() from
runMutation.onSuccess.
- RunWithPipelineModal source prop is now {kind, id, filename}
mirroring SliceModal.SliceSource; api.checkPipelineEligibility +
api.runPipeline take a discriminated-union source argument.
|
||
|
|
9033b0f81e |
feat(toast): restore upload-progress toast for scheduler dispatches (#1625 follow-up)
FTP push to the printer into the server-side scheduler tick. That
removed the browser-side upload the old XHR-progress modal listened
to — users only saw the queue item flip to "active" with no visibility
into the FTP push + the H2D/H2D Pro 80-210 s project_file digestion
window before the printer actually started.
Port the legacy bg-dispatch toast rendering from
0b43ac0d:frontend/src/contexts/ToastContext.tsx lines 510-650 back in
place verbatim — same DOM tree, same Tailwind classes, same
formatFileSize bytes line, same uppercase status chip, same collapse
chevron, same awaitingPrinter derivation, same auto-dismiss. The only
adapt is the event ingestion: a useEffect maps the four scheduler-side
WS events to the legacy DispatchToastJob shape.
The toast materializes when the FTP push to the printer ACTUALLY
STARTS (queue_item_uploading) — NOT on POST /queue. A draft that
fired at queue-add made the toast jump to "Dispatched" before any
upload had happened.
Four backend WS events drive it: uploading (carries printer_name +
total_bytes), upload_progress (throttled at 200 ms / 256 KB to match
legacy background_dispatch.py:614-615 1:1, first call always emits,
completion always emits; an _UploadProgressBridge bridges from the
FTP executor thread to the asyncio loop), acked (printer transitioned
out of pre_state), failed (with a reason key the toast looks up as
dispatchToast.failed.{reason}). No queue_item_dispatched event: the
legacy path kept status=processing from upload start until printer
ack, "Awaiting printer..." derives from upload_progress_pct >= 99.9
(legacy uploadDoneAwaitingPrinter trick).
Per-user routing: WS connect resolves the principal username to
User.id once and stashes it on websocket.state, so
ws_manager.broadcast_to_user filters O(connections). Auth-disabled
installs route user_id=None to all connections — matches the legacy
single-user behaviour. The watchdog receives created_by_id through a
new kwarg so the static method can still emit acked without
re-fetching the queue item.
|
||
|
|
d4ad41d850 |
fix(hms): action buttons actually reach the printer (#1830)
Three distinct bugs combined into one user-facing failure: clicking
Stop / Problem-solved-and-resume / Ignore-and-resume returned 200 OK
but the printer didn't act, modal stayed up, print stayed paused.
Verified by injecting candidate command shapes on device/<sn>/request
against a live H2D paused on a wrong-plate HMS (print_error=0x05008051).
(1) hms_resume / hms_stop dispatched the "err"-bearing shape that
BambuStudio doesn't actually send; Bambu firmware silently rejects it.
Both now send the plain shape ({"print":{"command":"<x>","param":"",
"sequence_id":"0"}}). PAUSE -> FAILED in 1.7s for stop, PAUSE -> RUNNING
in <2s for resume.
(2) IGNORE_RESUME mapped to idle_ignore, which is BambuStudio's
"dismiss a warning" command and only works for non-pause warnings.
hms_ignore now branches on state.state == "PAUSE": paused -> plain
resume; not-paused -> idle_ignore with the full-length err.
(3) 64-bit hms[]-array faults were truncated to a non-matching err.
short_code in _parse_status discarded 32 of the 64 identifier bits, so
the firmware didn't match it to the active fault. HMSError.full_code
now carries the canonical hex identifier (16 chars for hms[] faults,
8 chars for print_error faults). Catalog lookup tries 16-char first,
falls back to 8-char. HmsActionBody.print_error pattern relaxed to
^[0-9A-Fa-f]{8}([0-9A-Fa-f]{8})?$.
(4) execute_hms_action returned publish-success as success, masking
every silent-rejection bug above as 200 OK. Route now snapshots
(state.state, len(state.hms_errors)) before dispatch, awaits
HMS_ACTION_ACK_WAIT_SECONDS (default 2.5s, module-level so tests
override), and returns 502 with "Printer did not acknowledge HMS
action within 2.5s" if state didn't move.
|
||
|
|
31e61cebd7 |
feat(sponsor-prompt): lower print/archive/cost thresholds to fire for typical new installs
The toast was calibrated for power users — lowest bars were 100 prints,
50 archives, 100 cost. Most installs never crossed any of them, especially
with the install base ~doubling since March. Matomo confirms: only 4
prints-100 and 3 archives-50 deeplink visits to /sponsors.html in a 7-day
window despite tens of thousands of weekly pulls.
Adds lower thresholds without changing priority order or cooldown:
PRINT_MILESTONES = (10, 25, ...)
ARCHIVE_MILESTONES = (5, 10, ...)
COST_MILESTONES = (25, 50, ...)
Existing toast copy uses {count}/{total} interpolation in all 11 locales,
so no i18n changes. Tests rebalanced so "below the floor" still tests
with the new floor; new test_fires_at_lowest_threshold pins prints-10.
|
||
|
|
510005f043 |
fix(printers): cam wall — offline tile chip + don't kill shared
streams when one viewer closes
1) Offline tiles now show OFF (not LIVE)
CameraWall.modeByPrinter assigned 'live' to any visible printer
without considering status.connected, so a disconnected X1C wasted
a live-budget slot AND rendered the red LIVE chip on top of the
WifiOff placeholder. Disconnected printers now map to 'paused' and
don't decrement liveBudget — the existing WifiOff + Off chip
rendering takes over.
2) /camera/stop no longer kills other viewers' streams
The cam-wall tile, EmbeddedCameraViewer, and the /camera/:id popup
all subscribe to the same fan-out broadcaster for a printer.
/camera/stop used to unconditionally shutdown_broadcaster() + kill
every ffmpeg process for the printer, so closing the embedded viewer
while the cam-wall tile of the same printer was live force-killed
the source the tile was pulling from — the tile's <img> errored.
New get_subscriber_count(key) accessor in camera_fanout.py exposes
the broadcaster's subscriber list length. /camera/stop now reads
that first; when >= 1 subscriber is still attached, return
{stopped: 0, skipped: true} and leave the broadcaster + ffmpeg
processes alone. The leaving viewer's HTTP teardown still runs the
natural iter_subscriber.finally -> unsubscribe path, so its slot is
released; the broadcaster keeps serving the other viewers. Single-
viewer close still hits the immediate force-teardown (count is 0).
|
||
|
|
6f727d300a | Post work PR #1743 | ||
|
|
3ddf8d847e | [Feature]: HMS Actions (#1743) | ||
|
|
4c67d8a4e1 | feat: Unify print dispatch through the scheduler (#1625) | ||
|
|
fd61812d01 |
feat(drying): show active-cycle filament + target temperature on the AMS drying badge
Bambu's per-tick AMS push carries only the dry_time countdown — the
filament name and target temperature the user chose are never echoed on
the wire. The AMS card had no source of truth for them and rendered the
bare "Drying · 11h 35m left". The badge now shows
"Drying · PETG @ 65°C · 11h 35m left", matching the cycle the user
actually started.
BambuMQTTClient caches {ams_id: {filament, temp}} on send_drying_command
(mode=1), clears on mode=0 and on the dry_time falling edge to 0 — the
same per-AMS edge detector that drives the smart-plug-after-drying
callback. PrinterManager.get_drying_targets exposes it, the four
printer_state_to_dict call sites thread it through, AMS schema gains
dry_target_temp + dry_filament, and routes/printers.py builds the same
fields into the manually-constructed AMSUnit response.
When no cached target exists (drying started in a previous backend
lifetime, or initiated outside Bambuddy), the badge falls back to the
first loaded tray's tray_type + RFID-recommended drying_temp — the
heuristic the popover already uses to seed defaults.
i18n: printers.drying.targetSummary = "{{filament}} @ {{temp}}°C" in
all 11 locales. Parity check 5356 leaves per locale.
Note: a user reported the H2D's own physical display still labels the
cycle by the loaded tray's filament (e.g. "PLA" instead of the
Bambuddy-requested "PETG"). The wire payload is correct end-to-end —
journalctl shows filament: "PETG" sent and result: success ACKed — and
the badge in Bambuddy's own UI now reflects what we actually sent,
independent of the firmware's display choice.
|
||
|
|
8d6f701f1d |
feat(drying): continue drying while printing + gate rotate-spool when tray loaded (issue #1816)
Continue Auto-Drying while a print is running on capable hardware. New Settings > Print Queue > "Continue drying while printing" toggle (default OFF). Extends _check_auto_drying in print_scheduler.py to evaluate running printers when supports_drying_while_printing(model, firmware) returns true. Strict allowlist verified per Bambu wiki release notes for "Print While Drying" / "printing while filament is drying": H2D 01.03.00.00+, H2C/H2S/P2S/H2D Pro 01.02.00.00+, X2D/A2L 01.01.00.00+, X1C 01.11.02.00+. P1*, A1, A1 Mini, X1 (non-C), X1E intentionally excluded. Mid-print drying temperature is capped at max(40, preset_temp - 5) to protect spools from heat damage inside the hot enclosure during a print, matching Bambu's own "lower drying temperature during printing" guidance. Rotate-spool toggle in the drying popover is now disabled when any tray in the targeted AMS has filament threaded into the feed tube (tray.state === 11). The whole AMS rotates as one mechanism, so a single loaded slot locks the entire unit. Previously the toggle was always clickable and the firmware rejected with dry_sf_reason=[3] (ConsumableAtAmsOutlet) after the click. The first cut keyed on the printer-level tray_now but missed the H2D's typical post-print state where tray_now resets to 255 while filament stays in the tube — the per-tray state field reports it correctly. Submission also clamps rotateTray off so a stale-true state from a previous AMS can't leak through. Backend: supports_drying_while_printing in printer_manager.py covers display names and internal SSDP/MQTT codes (O1D, O1E/O2D, O1C/O1C2, O1S, N6, BL-P001, N7, N9). New print_drying_enabled boolean in settings schema. Frontend: toggle on SettingsPage, gate + clamp on PrintersPage drying popover using existing amsData cache. i18n: 3 new keys x 11 locales, no English fallback. Tests: 7 cases on the gate matrix (TestSupportsDryingWhilePrinting), 4 cases on the scheduler mid-print path (TestMidPrintDrying), 9 cases on the rotate gate state transitions. Full backend pytest -n 30 green (4251/4251), ruff clean, frontend npm run build clean, i18n parity 5355 leaves per locale. |
||
|
|
e09a33be16 |
feat(spoolman): remain%-delta fallback for no-3MF "Untitled" prints (#1820)
Brings the Spoolman writer up to parity with the internal-inventory side, which has had this fallback since #1119. When a Bambu print starts without a retrievable .gcode.3mf on the printer (typically an unsaved BambuStudio project, subtask_name='Untitled'), Spoolman no longer silently skips the print's filament consumption. - ActivePrintSpoolman.filament_usage now nullable; new tray_remain_start column captures per-slot {remain, tray_uuid} at print start. - store_print_data: always snapshots remain, even when 3MF is present (mirrors usage_tracker.on_print_start), so partial-3MF prints can also fall back per-slot. - report_usage: 3MF path stays primary; new _report_remain_delta_for_slots handles slots the 3MF didn't cover via delta * Filament.weight / 100, resolving the spool via the existing slot-assignment table. - _report_partial_usage: same fallback for aborted no-3MF prints. #1119 invariant preserved: per-slot, per-print, gated on a valid start/current remain AND a resolvable Spoolman spool. Uses curated Filament.weight (not MQTT's unreliable tray_weight) — same trick the internal-inventory side uses. Mid-print spool swap detected via tray_uuid mismatch → slot skipped. Double-charge prevented via handled_global_tray_ids dedup. |
||
|
|
261c376d1f |
fix(spoolbuddy): close #1815 — preserve PFUS/PFCN setting_id in resolver
SpoolBuddy "Assign to AMS" with a Bambu Cloud user preset (PFUS) left Bambu Studio showing "Generic <Material>" instead of the user's custom preset. Root cause: the defensive filter that catches PFUS/PFCN leaks into tray_info_idx also cleared setting_id — but PFUS/PFCN are VALID setting_id values, just not valid tray_info_idx values. When the cloud detail lookup didn't return a filament_id (cloud unauth on the on_ams_change replay path, transient failure, or older custom presets), both fields got cleared and the caller's generic-material fallback overwrote setting_id with GFSG99 — slicer resolved to Generic PETG. Fix: the filter still clears tray_info_idx for PFUS/PFCN/material- name leaks, but preserves setting_id when it's a valid slicer reference (PFUS / PFCN / GFS). Material-name leaks still clear both. Post-fix MQTT carries tray_info_idx=GFG99 (firmware-acceptable for HMS/drying/colour) AND setting_id=PFUS<hash> (slicer uses this to load the actual user preset). What stays the same: Bambuddy's own AMS card still displays the generic material on cloud-unauth paths — same fundamental limitation as today. Fixing that needs a deeper layered fallback (LocalPreset name match, printer kprofile query, cloud-detail cache) and is out of scope for this drop. Slicer-side fix is the reporter's explicit ask. |
||
|
|
4b0150b0ec |
fix(mqtt): close #1822 — promote H2S tray_now to 254 on all-external prints
H2S firmware reports tray_now=0 (the AMS's idle slot) throughout external-spool prints instead of 254 like X1C/P1S/A1 do, so the single-nozzle branch's 0-3 passthrough landed state.tray_now on slot 0 — UI highlighted AMS SLOT 1 instead of the external spool. Usage credit was unaffected (#1276 covers that via ams_mapping). The single-nozzle branch now checks _captured_ams_mapping (slicer- captured per-filament mapping that the request-topic intercept already tracks) before the existing P2S multi-AMS resolver. When every entry is -1, the print uses ONLY the external spool, so state.tray_now is promoted to 254. Narrow on purpose: AMS-only [5] and mixed [5, -1] are NOT overridden — we have no evidence H2S misreports mid-print swaps, and trusting the firmware preserves correctness for users with multi- filament setups. No-mapping prints (printer-screen start) fall through unchanged. |
||
|
|
2fe9896917 |
fix(queue): close #1818 — Resume after failure clears the gate
Single failure on a printer with require_previous_success queue items
permanently skipped every downstream + every new item — the
_check_previous_success lookback always walked back to the original
failed row (skipped is excluded from the lookback), and no code path
could dismiss that failure.
Three pieces:
1. PrintQueueItem.gate_acknowledged Boolean column (default False).
SQLite/Postgres-safe ALTER, dialect-branched DEFAULT.
2. _check_previous_success skips rows where gate_acknowledged=True so
acknowledged failures walk past the lookback. Fresh post-resume
failures still gate independently.
3. POST /api/v1/queue/printer/{printer_id}/resume — gated on
QUEUE_UPDATE_ALL — acknowledges failed/aborted items for that
printer AND restores items where
status='skipped' AND error_message='Previous print failed or was
aborted' back to pending in one transaction. Returns
{acknowledged, restored}.
Frontend banner above the active Queue tab surfaces blocked printers,
fires a warning-variant ConfirmModal, and shows a precise toast on
success.
|
||
|
|
38b8a87c11 |
fix(vp): close #1780 race — bump slicer-MQTT wait to 5s + retroactive stamp
Round 2 (
|
||
|
|
7cb905ad0c |
feat(inventory): toggle to disable auto-add of unknown RFID spools + global confirmation modal (issue #1764)
New setting "Auto-add unknown RFID spools" under Settings -> Filament -> Filament Tracking,
default ON for back-compat. When turned off, the backend stops auto-creating an inventory
record for an unknown RFID tag and instead broadcasts an unknown_tag WS event that pops
a global confirmation modal in the Bambuddy UI showing the printer / AMS-X label / slot /
material / colour. Add or Cancel; no nag on every MQTT push.
Backend
- Module-level _unknown_tag_last_broadcast dict dedupes per (printer, slot, tag). Set is
committed AFTER ws_manager.broadcast() returns so a crashed broadcast doesn't poison
the dedup and permanently silence the slot.
- Empty-slot MQTT push clears that slot's entry, so remove+reinsert reliably re-prompts.
- Successful matches via get_spool_by_tag / find_matching_untagged_spool / create_spool
also clear the entry so a future tag swap re-prompts.
- Tray data (tray_type, tray_color, tray_sub_brands, tray_count) shipped in the WS payload
directly so the modal renders the real material / colour instead of relying on the
React Query cache that lags the WS event by several seconds.
- Two new endpoints back the modal's confirm action:
POST /api/v1/inventory/spools/from-slot (INVENTORY_UPDATE)
POST /api/v1/spoolman/spools/from-slot (FILAMENTS_UPDATE)
Both look up the slot's tray data server-side and create + auto-assign atomically.
- Spoolman /from-slot now raises HTTP 500 when the slot-assignment INSERT fails instead
of returning success while the DB rolled back the binding.
- sync_ams_tray gained an optional auto_add_unknown_rfid kwarg (default True so existing
callers are unaffected); auto-sync and both manual sync routes thread the setting.
Frontend
- useUnknownTagPrompt hook listens for the unknown-tag CustomEvent, reads the tray fields
out of the event detail, and feeds a single-modal queue. No long-lived dismissed set;
the backend dedup handles spam suppression.
- UnknownSpoolModal wraps the existing ConfirmModal with a material + colour-swatch
preview block.
- Mounted in Layout.tsx alongside useSponsorPrompt so SpoolBuddy kiosk / login / setup
routes are excluded.
- getAmsLabel moved to utils/amsHelpers.ts; ConfigureAmsSlotModal.tsx and PrintersPage.tsx
both import the shared version (canonical AMS-A / HT-A / External labels).
- AppSettings TS interface gained spoolman_enabled, auto_add_unknown_rfid, spoolman_url
so the runtime cast in the hook is no longer needed.
- SpoolmanSettings.tsx gets a new toggle row in the Filament Tracking card, visible in
both built-in and Spoolman branches; auto-save + toast already wired.
|
||
|
|
271560f7cb | Fix camera port diagnostic for A1/P1 printers (#1799) | ||
|
|
7e6b390d74 |
feat(notifications): template-driven finish-photo email embed + user_print_* rename (#1792)
Reporter (email provider, "Reason: unknown" failures) wanted a camera snapshot in failure emails. The finish-photo capture path shipped in 0.2.5b1 (#1397) already loads JPEG bytes into archive_data["image_data"] for terminal print events, and pushover/telegram/discord/ntfy users have been getting them. Email was the one provider that dropped the bytes on the floor. Reporter separately flagged that the Message Templates list shows "Print Completed" and "User Print Completed" with no visual cue they're different dispatches. Both fixes in one commit because they touch the same UI surface (Message Templates) and both stem from the same reporter conversation. 1) Inline finish-photo embed in email — template-driven, opt-in. _send_email now accepts finish_photo_url alongside image_data. Inline embed fires only when bytes are present AND URL is set AND the rendered body contains that URL — i.e. the user's template referenced the existing {finish_photo_url} variable. The multipart/related shape wraps a multipart/alternative (plain + HTML) plus an inline MIMEImage with Content-ID: <bambuddy-finish-photo>. The HTML part replaces the escaped URL in-place with the cid <img>, so the image appears WHERE the user put the variable in the template, not stapled to the bottom. Plain-text part keeps the URL as a clickable link for non-HTML clients. First draft of this fix unconditionally inlined the photo whenever image_data was present, which bypassed the template system. Reverted to the template-driven contract: default templates unchanged, opt-in by editing the template body to include {finish_photo_url}. 2) user_print_* template name disambiguation. The four user_print_* templates are the per-user SMTP emails sent to the print's submitter (advanced-auth-only path). They shared the "Print Completed" / "Print Failed" / etc. short names with the broadcast provider templates, so the Message Templates list was indistinguishable. The EVENT_NAMES display map in routes/notification_templates.py already used the disambiguated "… Email" labels, but the seed wrote the short name to the DB. DEFAULT_TEMPLATES now seeds the four user_print_* rows with " Email" suffix so fresh installs are correctly labelled. New _migrate_rename_user_print_template_names runs on startup and updates existing rows where the name still matches the old default. Admin-edited names are preserved. Standard SQL UPDATE works on both SQLite and Postgres without dialect branching. |
||
|
|
9d74f9281b |
feat(deficit): backup-aware filament deficit check, colour-strict (#1762)
When the printer reports ams_filament_backup=True, compute_deficit_for_queue_item pools remaining_grams across spools matching (preset, colour) on the same printer (scoped per extruder on dual-nozzle) before declaring a per-slot shortfall. Identity is strict: same slicer_filament preset AND same colour (alpha-normalised). Two PETG HF spools in different colours are NOT pooled — the firmware would swap correctly but the print would change colour mid-run. Spoolman side mirrors the rule via filament.id + color_hex. Backup OFF falls back to the pre-PR per-slot accounting line-for-line. 8 new test cases in TestFilamentDeficitBackupAware pin pool covers, pool insufficient, different presets, backup-OFF regression, dual- extruder side scoping, no-preset never pairs, colour-strict, and alpha-hex normalisation. The 8 pre-existing test_filament_deficit.py cases stay green. feat(printers): AMS Filament Backup modal with BS-style ring per pair Badge click on the Filaments section header (#1766) now opens a modal: filament-colour ring per backup pair, material name + rotation count in the centre, slot labels distributed around the colour band on contrast-aware pills. Closely modelled on Bambu Studio's Auto Refill widget. Lone slots are intentionally not listed. R / L badges per ring when the extruder map carries two distinct values; collapses to no- badge rendering for single-nozzle printers misflagged as dual. Esc keypress closes the modal. Theme-aware via CSS variables matching AMSHistoryModal. computeBackupGroups helper in utils/amsHelpers defensively dedupes duplicate ams.id entries observed on switch-VP aggregations. 10 modal render cases pin: Esc closes / unmount nulls the listener / ring renders for pairs and omits lone slots / R-L badges only when extruder map has distinct values / empty state / toggle gating. 13 frontend cases pin computeBackupGroups identity rules. feat(printers): active-print P-N pill on AMS slot tiles during RUNNING While the printer is mid-print, each AMS slot tile referenced by status.ams_mapping carries a small "P1 / P2 / P3" pill in the top- right corner, naming which print-slot is mapped to that AMS slot. Catches the #1762 comment-2 scenario: a queue job set for "any X1C" staged to a printer with mismatched filament, no way to verify mid- print. Same wire data (status.ams_mapping is already on the wire) — the addition is purely surface. The existing ring-bambu-green highlight for effectiveTrayNow keeps its meaning (currently extruding RIGHT NOW); the pill is the per-slot static assignment for the active print. chore(scheduler): log Print Anyway short-circuit at INFO _block_on_filament_deficit logs at INFO when it honours item.skip_filament_check, so a future "Print Anyway didn't work" report (third commenter on #1762 hit this shape) has actionable evidence in the standard support bundle without DEBUG. Bundled because the deficit fix makes the original symptom disappear for users with backup ON. |
||
|
|
4206d675eb |
feat(notifications): dedicate AI Failure Detection notification event (#1794)
Split Obico failure-detection dispatch out of the multiplexed
on_printer_error event onto its own on_ai_failure_detection event so
users can subscribe to AI alerts without also enabling HMS hardware-
error pages, and so the discoverable label "AI Failure Detection" is
what subscribes them rather than the unrelated "Printer Error" toggle.
New column on notification_providers (default False, branched
SQLite/Postgres migration), new notification_service.on_ai_failure_detection
method, new ai_failure_detection template, obico_actions._notify swap.
Frontend gets a summary badge, a toggle row with description, and ntfy
priority surfacing. 14 new tests pin the routing + the regression guard
("Printer Error" alone must NOT receive AI notifications now). 11 locales
covered.
Existing providers keep working: HMS hardware errors continue to ride
on_printer_error unchanged; users who want spaghetti alerts opt in via
the new toggle.
|
||
|
|
166e9f9ef2 |
fix(vp): correct #1780 root cause — VP intake key mismatch dropped every slicer field
First-attempt fix ( |
||
|
|
68b9d741d9 |
feat(humidity): per-filament humidity threshold for auto-drying + alarms (#1605)
Reporter @thenewguy runs an engineering farm with one AMS per material
(PLA, ASA, Nylon, PVB, HIPS) — Bambuddy's single global ams_humidity_fair
threshold (default 60%) was driving both the queue / ambient auto-drying
trigger AND the hourly humidity alarm uniformly, which is wrong for
multi-material setups where Nylon wants <10% and PLA is fine at 60%.
Drying RUN parameters were already per-filament via drying_presets;
this commit adds the missing per-filament TRIGGER.
New setting ams_humidity_thresholds — JSON map of filament-type to
threshold percent with a "default" key for unknown / unmapped types.
Empty / unset → both consumers fall back to ams_humidity_fair so the
upgrade is silent.
Resolver lives in PrintScheduler.resolve_humidity_threshold(trays,
thresholds, fallback) — picks the lowest (most-restrictive) threshold
across all loaded tray types, matching the conservative-params strategy
_get_conservative_drying_params already uses for temp / hours. Empty
tray slots contribute no constraint; all-empty AMS falls through to the
"default" key. Filament names normalized to uppercase base (so
"PLA Basic" / "pla basic" both map to PLA).
Two consumer sites rewired through the same resolver so the scheduler
and the alarm path can never disagree about whether an AMS is "too
humid":
- print_scheduler.py::_check_auto_drying — per-AMS humidity comparison
for start / stop / skip decisions.
- main.py AMS sensor / alarm worker — hourly humidity alarm notifier.
UI: new table in Settings → Workflow → Auto-Drying, below the existing
Drying Presets table. Default row + 8 default filament types
(PLA / PETG / TPU / ABS / ASA / PA / PC / PVA) pre-filled from the
current ams_humidity_fair value so the editor starts sensibly.
Input pattern: draft-on-edit / commit-on-blur (transient humidityDrafts
state per row). onChange only updates the draft; onBlur (and Enter)
parses + clamps to [5, 95] + commits. Empty value on blur clears the
override and falls back to default. Caught mid-PR via a typing test:
the naive per-keystroke clamp snapped "3" → 5 before the user could
type the second digit of "30".
Setting is in the public _UI_PREFERENCE_FIELDS allowlist (same rationale
as drying_presets and ams_humidity_fair — non-sensitive integer map,
no SETTINGS_READ permission required for badge-color rendering).
|
||
|
|
a5fe5cb3d4 |
feat(sponsor-prompt): in-app toast at earned milestones
ghcr.io pull baseline (~10k/day rising → ~8-12k active installs) puts
sponsor conversion at 0.08% — roughly an order of magnitude under
industry-benchmark for OSS with visible CTA. The Settings banner from
|
||
|
|
a53dc20ca3 |
fix(usage-tracker): split mid-print AMS-Backup spool switch correctly (#1771)
Reporter forcefully started a print needing ~260 g with 180 g on the
first spool and a backup spool in the AMS. Printer correctly consumed
spool 1, AMS Backup switched, spool 2 finished the print. Bambuddy
attributed all 260 g to spool 2 -- spool 1 untouched in inventory.
Two stacking bugs produced the exact "all to second spool" symptom for
prints without per-layer 3MF gcode data:
1. bambu_mqtt.py:2135 wrote state.total_layers = int(data["total_layer_num"])
unconditionally. P1S firmware pushes total_layer_num=0 at print end
(same reset pattern other models do for layer_num / progress). The
unconditional write clobbered the slicer's actual total to 0 before
the usage tracker read it.
2. usage_tracker.py:1129-1137 linear-fallback dumped EVERYTHING onto the
last segment when total_layers was 0:
if total_layers > 0:
segment_grams = total_weight * (seg_end_layer - seg_start_layer) / total_layers
else:
segment_grams = 0.0 # <- entire print weight ends up on last segment
Path 2 (AMS remain% delta) couldn't recover because (a) the emptied
spool reported remain=-1 and (b) Bug-A had already added the second
spool's key to handled_trays, suppressing the Path 2 lookup.
Fix:
- bambu_mqtt.py: only overwrite state.total_layers when the incoming
value is positive (mirror of the existing _last_valid_layer_num
pattern at line 2127). Explicit reset on new print start at
_handle_print_start so the previous print's total can't bleed in.
- usage_tracker.py: cascade the linear-fallback denominator -
state.total_layers, then last_layer_num (already threaded in for
the last_progress fallback), then equal-split as a bounded fence.
Equal-split is still wrong but never dumps the whole print on the
last segment, which was strictly worse.
|
||
|
|
b1cb26f6ee |
feat(ams-backup): add status badge + toggle, fix prefer-lowest (#1766)
Two tightly-coupled deliverables in one drop -- a new AMS Filament Backup status/control surface, and the #1766 fix that depends on it. Added -- AMS Filament Backup status + control - Parse bit 18 of top-level print.cfg into PrinterState.ams_filament_backup on every push_status. Verified against OrcaSlicer source (DeviceManager.cpp:4961) and a live H2D ON/OFF capture. Tri-state (None = A1 family / pre-cfg push) preserves today's behaviour. - Hold-timer guard (3 s) prevents stale frames from flickering the badge back to the printer's old cfg after a user-initiated toggle. - POST /printers/{id}/ams-backup toggle, set_ams_filament_backup() client method calling _set_print_option("auto_switch_filament", enabled). - GET /printers/{id}/inventory-remain endpoint exposes the same map the dispatcher uses (internal and Spoolman modes both work uniformly). - Small icon badge in the printer card's "Filaments" section header (placement reads as printer-wide because the cfg bit is printer-wide, not per-AMS). Click to toggle, success toast. - 5 i18n keys x 11 locales for the badge UI. Fixed -- #1766: prefer_lowest didn't pick lowest, ignored backup state - Backend gate in _compute_ams_mapping_for_printer: coerce prefer_lowest to False when status.ams_filament_backup is False; log the skip. - New effectivePreferLowest(setting, backup) helper applied at every frontend sort entry point: single-printer PrintModal, multi-printer hook per-printer, PrinterSelector InlineMappingEditor, FilamentMapping standalone editor (the last had NO preferLowest awareness at all before this change). - New preferLowestSortKey(f, inventoryByTrayId) mirrors backend's two-tier key exactly, including the banding tie-break (regular AMS < AMS-HT < external) so the client-side pre-compute matches the dispatch-time pick. An earlier draft used a flat `amsId * 4 + trayId` priority which gave external slots (ams_id = -1) a NEGATIVE priority -- caught in code review before commit. - Settings -> Filament -> "Prefer lowest remaining filament" gets an explanatory note about the printer-side AMS Backup dependency, with i18n key in all 11 locales. |
||
|
|
d196cfc500 |
fix(virtual-printer): forward H2C rack-swap nozzle pick from slicer to dispatch (#1780)
BambuStudio's project_file MQTT command for O1C2 (the H2C dual- nozzle-rack variant) carries nozzle_mapping (per-filament physical nozzle position IDs) and nozzles_info (per-extruder rack metadata). The VP intake was dropping both, so the H2C firmware fell back to "last matching nozzle type" auto-pick and ignored the user's slicer choice — every HF print landed on R2, every standard print landed on R4. Carry both fields through the VP intake → queue item → MQTT dispatch path. New nullable TEXT columns on print_queue, non- branched ALTER (matches ams_mapping / filament_overrides precedent). Dual-nozzle gate at start_print() keeps the fields off single-nozzle dispatches. Fail-open on malformed JSON — firmware auto-picks, never worse than pre-fix. Stamps both fields on every plate in the multi-plate Send All loop (#1697 / #1188 precedent). ams_mapping2 still handles H2D/X2D dual-extruder routing unchanged; this fix is scoped to the O1C2 rack-swap mechanism. |
||
|
|
b414af6b1c | feat(gcode-injection): per-VP opt-in auto-print injection toggle (#1516) (#1656) | ||
|
|
3ef5119b7d |
fix(archives): render plate thumbnails server-side when sidecar slice skips them (#1759)
Bambuddy's archive cards were blank for every print sliced through the BS or Orca docker sidecars. The "Some recent prints couldn't be archived with thumbnails" banner pointed at install step 4 which is unrelated — that flag only fires on FTP-fetch failures, not on missing-thumb in the sliced 3MF. Root cause is upstream of Bambuddy: neither slicer CLI renders Metadata/plate_N.png when invoked headlessly with --slice --export-3mf. That render is a separate code path triggered by --export-png, which is mutually exclusive with --export-3mf and additionally needs a working display backend (BS 02.07.x's bundled GLFW is hard-locked to Wayland — even XDG_SESSION_TYPE=x11 + GDK_BACKEND=x11 + QT_QPA_PLATFORM=xcb don't switch it back). An Xvfb display in the sidecar wouldn't help even if we wired the second-pass call. The Orca sidecar has been silently shipping thumbnail-less 3MFs from STL inputs since launch; nobody noticed. Fill the gap on the Bambuddy side: new plate_thumbnail.py renders the missing thumbnails after the slice returns. inject_plate_thumbnails_if_missing parses the sliced zip, finds every Metadata/plate_N.gcode entry that doesn't have a matching plate_N.png, loads 3D/3dmodel.model via trimesh, renders an isometric Bambu-green-on-dark view at 512x512 + 128x128 via the same matplotlib Agg pipeline as stl_thumbnail.py, and re-packs the zip with the PNGs injected. Visual style matches Bambuddy's existing library thumbnails — archive cards stay consistent inside Bambuddy rather than chasing parity with desktop Studio's plate render. Best-effort: input bytes are returned unchanged on any failure so the slice flow itself can never fail because of a missing thumbnail. Idempotent: re-running on an already-injected 3MF returns the input verbatim. Wired into both library.py slice paths via result._replace; covers the cross-class merged-multi-plate path automatically (merged bytes flow into the same write site). No sidecar Dockerfile change required — an earlier attempt to install Xvfb in Dockerfile.bambu-studio was a false start and is not part of this drop. Dependencies: trimesh's 3MF loader uses networkx (scene-graph traversal) and lxml (model.xml parse) lazily inside the 3MF code path — both added to requirements.txt because they aren't strict trimesh transitives. |
||
|
|
0f99b54d7e | feat: Update printer card UI for structure and readability (#1661) | ||
|
|
9d1f04b89d |
log(scheduler): emit prefer-lowest dispatch decision at INFO so #1766 can be triaged from a bundle
The matcher's tray_info_idx vs color vs type_only bucket choice was debug-only, so a bug report's bundle never showed which path won. Emit the sorted candidate trays and the picked bucket per filament req when prefer_lowest=True. Behaviour-neutral; existing 89 matcher tests pass. |
||
|
|
af5d24e289 | feat(inventory): structured storage locations catalog (#1505) | ||
|
|
2cf6f29503 |
fix(vp): Send All enqueues one item per plate; archive delete cascades to queue
VP queue-mode multi-plate Send All
==========================================
BambuStudio / OrcaSlicer "Send All" of a multi-plate project uploads ONE
3MF containing every plate (one FTP STOR, single filename) — slice_info.config
inside the file lists N <plate> blocks with their own index metadata and
their own Metadata/plate_N.gcode payload. Pre-#1733 the VP queue path
called _extract_plate_id which returned only the FIRST plate index, and
_add_to_print_queue built exactly one PrintQueueItem from it. Plates 2..N
silently dropped on the floor. From the user's perspective: Send All of a
3-plate project produced 1 queue item, indistinguishable from a regular
single-plate Send, with no log line to explain the discrepancy.
The wire was confirmed against the live H2D-1 Proxy VP: the same file
ships whether the user clicked Send or Send All; the only intent signal
is the count of <plate> blocks inside slice_info.config.
Fix: replaced _extract_plate_id (-> int | None) with _extract_plate_ids
(-> list[int]). The list contains every <plate> block's index in order;
falls back to [1] when slice_info.config is missing / unparseable so the
single-plate case is preserved. _add_to_print_queue now loops over the
list and creates one PrintQueueItem per plate, with:
- plate-specific position = MAX(position) + iteration_number, so the
items inherit consecutive positions and the slicer's plate order
becomes the queue execution order.
- per-plate required_filament_types / filament_overrides via
extract_filament_requirements(file_path, plate_id) — the plate-aware
filter shipped with #1697 — so the scheduler's per-printer "Any X"
matching dispatches each plate onto a printer with the right
colours loaded for THAT plate, not for plate 1's filament set.
- shared archive_id across all plates (one upload = one archive row).
- the VP's auto_dispatch + manual_start posture inherited unchanged.
Net behaviour: single-plate Send hits the loop once → exactly today's
result (one queue item, plate_id from the slicer, one archive). Multi-
plate Send All of a 3-plate file → 3 queue items, plate_id 1/2/3,
consecutive positions, all referencing the same backing archive.
Archive delete cascades to queue rows
=============================================
Previously the soft-delete path (the default the trash-can button uses)
called _cancel_pending_queue_items which only flipped queue rows with
status='pending' to status='cancelled' while leaving every other status
alone AND leaving every row in the DB. The Send All multi-plate work
above made this much more visible: deleting an archive backed by N
queue items now had to clean up N rows, and what users saw instead was
N "cancelled" rows lingering in the queue history.
Backend:
- Replaced _cancel_pending_queue_items with _delete_related_queue_items
(db, archive_id) -> int. DELETEs every queue row where
archive_id = X regardless of status. Matches what the hard-delete
path already did via the ON DELETE CASCADE FK on
print_queue.archive_id — both paths now produce the same end state.
- Print history lives in PrintLogEntry (FK ON DELETE SET NULL) and is
untouched; Quick Stats / accuracy bands are preserved across both
delete paths.
- 409 guard on archives.py::delete_archive when any related queue
item is currently status='printing'. Both soft and hard delete are
gated; deleting the archive while a print is live would strip the
dispatcher's metadata trail (filament / plate / ams_mapping) out
from under the running print.
- New GET /archives/{id}/delete-impact endpoint returns
{related_queue_items: N, currently_printing: M}. Cheap, single
endpoint, deliberately NOT folded into the archive list response
so the much larger list endpoint isn't forced to run the same
query per row.
Frontend:
- ArchivesPage delete-confirm modal queries the new endpoint when the
modal opens (useQuery with enabled: showDeleteConfirm) and renders
an amber "N queue items linked to this archive will also be removed"
line when total > 0 AND printing = 0, OR a red "Cannot delete —
M queue items are currently printing" line when printing > 0
(confirm button disabled in that case so the user can't bonk the
409 on submit).
- ConfirmModal gained an optional confirmDisabled?: boolean prop —
isLoading was the only disable knob before; this adds the external-
precondition path.
- 2 new i18n keys (deleteQueueItemsWarning, deleteBlockedByPrinting)
translated across all 11 locales per feedback_translate_dont_fallback —
no English fallbacks.
No DB migration — the CASCADE FK was already in place; only the helper's
semantics changed.
|
||
|
|
be7e85344c |
fix(finish-photo): drive capture from stg_cur=22, drop dispatch force-on (#1721)
capture_finish_photo (default-on) was forcing the timelapse MQTT field to true on every print, even when the user explicitly unchecked Timelapse in the slicer send dialog. On profiles with Timelapse Type = Smooth, that flipped the printer's timelapse_record_flag and un-gated the per-layer M622 J1 wipe blocks the slicer had baked in — toolhead parked off the part every layer, on prints the user opted out of recording. Root cause: #1397 implemented the finish-photo feature as a side channel of "force the printer into timelapse-recording mode at dispatch" so the last-frame extractor had a video to pull from. That conflated recording a timelapse with snapping a finish photo, and the per-layer side effects were decided at slice time by the user's timelapse_type, which Bambuddy has no visibility into post-slice. Fix: replace the force-on with a clean MQTT-state-driven trigger. bambu_mqtt.py fires a new on_finish_photo_moment callback when stg_cur transitions INTO 22 ("Filament unloading") while _was_running AND end-of-print gate matches (progress >= 99 OR layer_num >= total_layers OR remaining_time <= 0). The gate disambiguates from mid-print color swaps (which also transit stage 22 but at progress < 99). FINISH-state fallback in the same handler fires the callback at the existing transition if stage 22 never arrived (cancel, external-spool-only, HMS halt, firmware variants). main.py registers on_finish_photo_moment as a top-level handler. It pre-captures one camera frame at the trigger edge (external cam → buffered RTSP → fresh RTSP via capture_camera_frame_bytes) and caches the JPEG bytes in _stage22_finish_frames[printer_id]. _background_finish_photo consumes the cached bytes before its existing live-grab chain, so the saved photo has the better framing (toolhead parked, before bed drop) without restructuring the archive-resolution / fallback / notification wiring. When a timelapse IS actively recording (user explicitly opted in), pre-capture is skipped — _capture_finish_photo_from_timelapse still extracts the last frame, which is still the best framing and now has no force-on side effects because the user wanted the video. Removed: resolve_effective_timelapse, _resolve_effective_timelapse wrapper, both background_dispatch call sites, the print_scheduler call site, the archive.bambuddy_forced_timelapse write, _cleanup_forced_timelapse (~75 lines including the FTP-DELE walk across /timelapse, /timelapse/video, /record, /recording) and its call site. All paths now read bool(item.timelapse) / bool(job.options.get("timelapse", False)) directly. The archive.bambuddy_forced_timelapse DB column stays defined (default False) for back-compat with existing rows — no consumer reads it anymore. |
||
|
|
43adb6f964 | Security hardening (security #2) | ||
|
|
8a63fcbf57 |
fix(vp): apply tray_exist_bits empty-slot cleanup to slicer-facing cache (#1726)
VP bridges bound to a target printer (Proxy mode, Queue mode with specific target) forwarded the printer's raw AMS push_status to the slicer untouched. bambu_mqtt.py::_handle_ams_data applies a tray_exist_bits-driven cleanup to Bambuddy's internal state (promote empty slots to state=9, wipe stale tray_type / tray_color / tray_info_idx / tag_uid / tray_uuid / remain) so the AMS card renders empty slots as Empty, but the VP bridge cache never ran the same cleanup. Net result on real hardware: a printer with 3 loaded filaments and several previously-loaded-now-empty slots had Bambuddy's AMS card render those slots correctly as Empty, but BambuStudio after Sync painted them as phantom loaded filaments with stale color and material from before the slot went empty. Root cause: two consumers of the same payload, only one wired to the cleanup. _handle_ams_data ran it on every push; mqtt_bridge.py:: _on_printer_raw merged the ams blob via _merge_ams_dict but copied tray_exist_bits through as an opaque scalar without acting on it. Fix: factored the bit-clear logic out of _handle_ams_data into a module-level helper apply_tray_exist_bits(units, tray_exist_bits_str, *, power_on_flag, log_label). Internal path replaced with a single call. Bridge calls it after _merge_ams_dict on the merged ams dict, before the merged state is stored as the 1 Hz cached-as-base source. Shared shutdown guard kept on both sides: all-zero bits + power_on_flag=False is the printer-off pattern (#765, would propagate phantom empties on every reconnect); nonzero bits + power-off is valid idle-printer state (#1365, X1C between prints) and still applies. AMS-HT units (id >= 128) skipped on both sides. Tests: new TestApplyTrayExistBitsHelper (10 cases) pins the helper contract directly. 3 new bridge regression tests reproduce the #1726 wire shape, the shutdown guard, and the AMS-HT skip on the cached slicer-facing state. Existing internal-state tests for the bit-clear logic (covers state=9 promotion, loaded-slot preserve, genuine-removal-with-power-on) continue to pass against the refactored path. One pre-existing bridge fixture had an inconsistent tray_exist_bits ('3' for 2 AMS units each with slot 0 loaded — bit 4 missing). The shared cleanup exposed it; corrected to '11' (bits 0 + 4) to match real-printer wire shape. Reported by @needo37 with full code-level analysis including the suggested fix shape and the BAMBUDDY_VP_DUMP_WIRE diagnostic to verify on a live system. |
||
|
|
f15e54c383 |
fix(windows): _local_zone falls back to stdlib utc when zoneinfo DB is missing
The Windows installer's embedded Python doesn't carry an IANA tz
database, and the stdlib zoneinfo has no system DB to read on Windows.
ZoneInfo("UTC") raises ZoneInfoNotFoundError on those installs, and
the new /api/local-backup/status endpoint 500s on the resulting
uncaught exception. Surfaced via a Windows traceback from a user's log:
File "...\backend\app\services\local_backup.py", line 32, in _local_zone
return ZoneInfo("UTC")
zoneinfo._common.ZoneInfoNotFoundError: 'No time zone found with key UTC'
_local_zone()'s try/except only covered the TZ-env branch — both
fallbacks unconditionally called ZoneInfo("UTC") and re-raised.
Fix (two parts):
1. services/local_backup.py — return type widened from ZoneInfo to
tzinfo, the UTC fallback is wrapped in its own try, and the
last-resort fallback returns datetime.timezone.utc (stdlib, no
IANA DB needed). str(timezone.utc) == "UTC" so the response shape
on /api/local-backup/status is unchanged. The astimezone call in
_calculate_next_run accepts any tzinfo — no other call sites
affected.
2. requirements.txt — pin tzdata>=2024.1; sys_platform == "win32" so
the next Windows installer build ships the IANA DB, and any non-
UTC TZ value (e.g. Europe/Berlin) resolves correctly. The stdlib
fallback can only ever give UTC. Linux/macOS unaffected by the
platform marker — they already have the system tz database.
|
||
|
|
0de71ca412 |
fix(printer): "off" flow_cali / nozzle_offset_cali now actually suppress the stage
The Re-print and Schedule modal toggles for Flow Calibration and Nozzle Offset Calibration accepted "off" correctly and flowed it through to the project_file MQTT publish — Bambuddy sent extrude_cali_flag: 2 and nozzle_offset_cali: 2 per the "1 = run, 2 = skip" reading inherited from the #1478 / #1682 work. Live test on H2D 01.x: with both toggles off, the stg queue still scheduled stage 8 ("Calibrating dynamic flow") and stage 39 ("Nozzle offset calibration"), and the printer ran both at print start. Root cause: 2 means "skip the explicit pass but still apply / verify stored PA via the calibration stage" — close to a no-op K-factor wise but the per-print physical sequence still runs. 0 is the encoding that actually drops the stage from stg. A BambuStudio Send-dialog capture on the same firmware showed 0 for both fields when the user unchecked the calibrations — contradicting the #1478 commit's read of "BambuStudio never sends 0." Fix: - extrude_cali_flag = 1 if flow_cali else 0 (was: else 2) - nozzle_offset_cali = 1 if (nozzle_offset_cali and is_dual_nozzle) else 0 (was: else 2) Dual-nozzle gate stays; single-nozzle prints continue to force-skip the nozzle-offset cali their head doesn't support (#1682). The 1 (run) branch is unchanged. Verified live on the same H2D after the patch: stg dropped to [29, 13, 4, 14, 3] (cooling, homing, filament change, nozzle cleaning, vibration comp). Stages 8 and 39 gone. Vibration compensation is NOT fixed by this commit: vibration_cali is a bool in our and BambuStudio's wire format, and the H2D firmware queues stage 3 regardless of the false value. Firmware-side, not solvable at the dispatch layer with the current field. Filed as a follow-up. |
||
|
|
7190fc2d13 |
fix(logs): demote benign "not connected" + "may linger" warnings
Two warnings polluting every A1 support bundle on healthy prints, both
unrelated to the timelapse-default behaviour the issue actually reports.
1. mqtt_bridge.py's post-bind nudge calls request_status_update on the
real printer's MQTT client to populate the bridge cache without
waiting for the next periodic pushall. The bind frequently races the
TLS handshake, especially on A1 firmware. Skip the nudge when
state.connected is False — the periodic pushall fills the cache
anyway. The WARNING in bambu_mqtt.py stays for the genuinely-
actionable callers (refresh-status API, bug reporter).
2. Post-finish SD-card cleanup (and the symmetric forced-timelapse dir
walk) used delete_file_async's bool return to drive a WARNING when
all candidates failed. A1 firmware self-cleans the SD card before
our cleanup runs — every candidate FTP-DELE returns 550, we burn
the retry budget, then WARN on a successful print. Introduce
DeleteResult.{DELETED,NOT_FOUND,FAILED} so the helpers only WARN
on real network/auth/transient failures. NOT_FOUND advances to the
next candidate without consuming the 2s backoff. User-facing delete
endpoint returns 404 on NOT_FOUND.
|
||
|
|
9c4252911b |
fix(vp): overlay incoming dict-shaped push_status fields onto cache instead of replacing (#1622 round 5)
Right after the slicer picks a filament for the external spool (vt_tray, ams_id=255),
Bambu firmware pushes a partial vt_tray carrying just {tray_info_idx, tray_color} -
~18 fields shorter than the pushall shape the slicer expects. The #1622 round-4
per-field accumulate (
|
||
|
|
1c42a9f1fd |
remove(slicer): drop bundle import; fix cloud preset type/from for CLI (#1712)
Bundle import never delivered what it implied: BambuStudio's .bbscfg export
strips system processes/filaments, so importing a bundle left users without
process presets and slicing fell back to embedded settings on STL. Bundle
mode also hid the standard tier behind a constrained dropdown, the actual
trap reported here.
Removed end-to-end:
- backend: POST/GET/DELETE /slicer/bundles*, SliceRequest.bundle,
SliceBundleSpec, dispatch fork in library.py, bundle-context params on
the filament-requirements endpoints, bundle-fingerprint cache key in
slice_preview.py, SlicerApiService.{import,list,get,delete}_bundle and
slice_with_bundle, BundleSummary / BundleNotFoundError.
- frontend: BundlePicker + BundleStringDropdown, isBundleMode + every
branch, bundle state/queries/dispatch in SliceModal.tsx, SlicerBundle /
SliceBundleSpec types, three bundle API methods. buildCompatibilityIndex
loses its bundle path; presetCompatibility keeps compatible_printers
plus the @BBL fallback.
- SlicerBundlesPanel turns into a permanent static notice explaining the
removal, alternative import paths, and the new slice-time lookup order
(Imported > Orca Cloud > Bambu Cloud > Standard sidecar fallback).
- i18n: slicerBundlesRemoved.{title,description,alternatives,lookupOrder}
translated across all 11 locales; slice.bundle*, slicerBundles.* keys
removed.
Fixed (surfaced by removing bundle mode):
- _resolve_cloud and _resolve_orca_cloud now force type per slot and pin
from: "system" on the payload before json.dumps. Bambu Cloud ships
type as "printer"/"print" and routinely empty `from`; the BS CLI's
--load-settings parser rejects both with return -5 / "input preset
file invalid". Standard tier already did this; cloud paths now match.
|
||
|
|
da799447f6 |
fix(vp): accumulate cached push_status per-field instead of allowlist (#1622)
Bridge cache replaced prev state wholesale on each incremental, re-merging only a 14-key allowlist. Capability/lifecycle fields (cali_version, print_type, mc_print_stage, device, ...) drained out within one 1Hz tick, greying out BambuStudio's Device-tab UIs (manage-calibration, AMS-slot dropdown) once the cache thinned. Most P1S users miss it by timing — they click Device tab while the cache is still fat from the connect pushall. Switch to per-field accumulate matching bambu_mqtt.py's internal state handler: prev keys carry over verbatim when not present in the incoming push, new values overwrite when present. _merge_ams_dict for partial AMS blobs unchanged (#1387 / #1371 regression guards stay green). _SLICER_VISIBLE_STICKY_KEYS removed — new logic is a strict superset. |
||
|
|
282aefc564 | Sync and housekeeping | ||
|
|
5644f11495 |
debug(vp): env-flagged bridge-synthesised reply trace for slicer↔printer #1622 round-3 triage
Round-2 cmd.jsonl from shaddowlink proves the bridge forwards both commands and responses correctly: ams_filament_setting round-trips with result=success on P1S, the cached push_status carries tray_info_idx=GFA11/tray_type=PLA-AERO/K-n/cali_idx intact, and the visible "unload" symptom comes from the slicer's choice of extrusion_cali_set (push K direct, P1S firmware rejects) vs extrusion_cali_sel (select by id, both H2D and P1S accept). The open question is what makes the slicer pick _set vs _sel — likely the info.get_version response Bambuddy synthesises or the first cached pushall reply the slicer reads at connect. Round 2 captured neither; the JSONL had slicer_to_bridge and printer_to_slicer but no direction for the bridge's own synthesised replies. Same BAMBUDDY_VP_DUMP_WIRE=1 flag now also appends a bridge_to_slicer line for every bridge-synthesised reply (info.get_version answer, project_file ack, on-demand pushall response). Capture is in _publish_to_report — the single chokepoint — gated on a new log_event param; the 1Hz periodic push threads log_event=False so the JSONL isn't flooded (~60 lines/min/VP) because dump_wire already covers cache shape per tick. Diagnostic-only, no data-path change. Default param preserves every existing call site's behaviour. |
||
|
|
19eed8eba0 |
debug(vp): env-flagged command-flow trace for slicer↔printer #1622 round-2 triage
The shape-of-payload dump shipped earlier rules out cache wipes — shaddowlink's round-1 captures show AMS data reaches the slicer byte-identical to what the printer sent. The remaining symptom (picking a generic filament in archive mode "unloads" the slot) lives on the command path, which the snapshot dump doesn't see: it writes only the cached _latest_print_state and the periodic 1Hz push. Add append_event() in _debug.py — same env flag, separate file at <log_dir>/vp_wire/<vp>_cmd.jsonl. One JSONL line per event with UTC iso timestamp, direction (slicer_to_bridge / printer_to_slicer), MQTT topic, <channel>.<command> grep handle, and parsed payload. Wired at two points: mqtt_server._handle_publish for slicer publishes (after JSON decode so the trace matches what the bridge actually parsed) and mqtt_bridge._on_printer_raw "everything else" branch for printer responses (after serial rewrite so the trace matches what the slicer sees on the wire). Pushall / get_version stay out — both are handled locally and never round-trip through the bridge. Bytes payloads get the same \x00-tolerance fix from #927 so OrcaSlicer's C-string-null publishes parse cleanly; un-parseable bytes fall back to {"raw": "..."} so every line stays valid JSON. |
||
|
|
4ab7339fe4 |
debug(vp): env-flagged wire-payload dump for slicer-mirror triage (#1622)
Add a BAMBUDDY_VP_DUMP_WIRE=1 escape hatch that writes the bridge's cached push_status (in) and the 1Hz slicer-facing copy (out) to <log_dir>/vp_wire/<vp_name>_<direction>.json, overwritten each tick. #1622's symptom — empty filament dropdown in slicer's AMS slot details for P1S/A1 but not H2D in non-proxy VP modes — needs visibility into the actual wire bytes flowing through the bridge to bisect between "cache is missing fields" and "_send_status_report strips them on copy." The existing logs prove the bridge is bound and pushing at 1Hz, but not what's in the payload. Off by default, single env flag, single file per VP per direction (bounded disk footprint), failures swallowed at debug so a broken dump can never break the 1Hz loop. 21 tests pin the helper contract: disabled-by-default, atomic writes, sanitized vp_name (no path escape), per-call env check so toggling without restart works. |
||
|
|
4b2babe752 |
fix(network-utils): enumerate interfaces on Windows via psutil
get_network_interfaces() used fcntl ioctls and get_all_interface_ips() shelled out to `ip -j addr show` — both Linux-only. On Windows the fcntl path raised ImportError and returned [], so the VP "bind IP" dropdown showed no interfaces. Add a Windows branch using psutil.net_if_addrs() + net_if_stats() (psutil is already a dep). Same dict shape as the Linux path, filters loopback / link-local / down interfaces by address class instead of by name prefix. No name-based exclusion — users may legitimately bind a VP to a Hyper-V / WSL / Tailscale virtual adapter. The Linux fcntl path is untouched. |
||
|
|
defec334c4 |
feat(a2l): add Bambu Lab A2L support (#1684)
Internal code N9 (from BambuStudio resources/profiles/BBL/machine/Bambu Lab A2L.json), serial prefix 26A19 (5-char, same shape as H2C's late 31B8B). Capabilities from Bambu's official A2L specs page: linear rail, single FDM extruder + integrated cutter/plotter, no Ethernet (2.4 GHz Wi-Fi only), low-rate chamber camera on port 6000. The BambuStudio profile's use_double_extruder_default_texture: true flag describes two TOOL HEADS (FDM + cutter), not dual filament extrusion — A2L must NOT be classified as dual-nozzle or AMS routing will target the deputy slot and firmware rejects with 07FF_8012. Registry updates: printer_models.py, firmware_check.py, virtual_printer/manager.py, virtual_printer/mqtt_server.py, PrintersPage.tsx, SpoolBuddyAmsPage.tsx. 12 new test cases in TestA2LModel pin every dimension. |
||
|
|
e737c84c6e |
fix(diagnostic): skip external_storage check on A1 / A1 Mini (#1703)
A1 and A1 Mini ship without a MicroSD slot at all - there is no firmware-side "Store sent files on external storage" toggle and the slicers don't surface a slicer-side equivalent either. The connection diagnostic was reading state.store_to_sdcard (home_flag bit 11), which is never set on these models, so the check fell through to fail for every A1-series user. Combined with the absent slicer UI it left users thinking Bambuddy was wrong about a setting their hardware does not have. New NO_EXTERNAL_STORAGE_MODELS frozenset in utils/printer_models.py enumerates A1, A1 Mini, and their internal codes (N1, N2S, A04, A11, A12). has_external_storage() returns False for those, True for everything else. Unknown models default to True so the check stays active for future Bambu lineup additions - new no-slot models must be added to the set explicitly. The diagnostic now short-circuits to skip before reading store_to_sdcard when printer.model is in the set. X1, P1, P2S, H2, and X2D are unchanged - the bit-off -> fail signal is still the right read for them. The companion FTP-upload-timeout symptom in the same bug report (ftp code 28 from BambuStudio when sending to the proxy VP) is a separate Docker-bridge-mode networking constraint, not addressed here. |
||
|
|
0793022818 |
fix(ams): keep slot card preset name in sync after spool swaps
The slot card on PrintersPage shows slot_preset_mappings.preset_name first in its display fallback chain. Three write paths swap which spool occupies a given slot: - internal manual assign (inventory.apply_spool_to_slot_via_mqtt) - internal RFID auto-assign (spool_tag_matcher.auto_assign_spool) - Spoolman RFID sync (main.auto_sync_spoolman_ams_trays) Only the first one was reconciling the row. After an RFID-driven spool change, the card kept surfacing the previous spool's preset name until the user opened Configure Slot manually. Reporter saw H2D-1 / AMS-B3 displaying "Bambu PLA Silk+" for a freshly-inserted Bambu PLA-CF spool. The matching row in slot_preset_mappings was last written in March when a PLA Silk+ spool had been in that slot - confirmed live in the database. New backend/app/services/slot_preset_writer.py exposes a primitive upsert_slot_preset plus two derivation wrappers: one for the internal Spool ORM object, one for the Spoolman API dict shape. All three call sites now go through the helper, so the row stays in lockstep with the assigned spool regardless of inventory mode. Bug shape exists in both inventory modes and the patch fixes both per feedback_inventory_modes_parity. The Spoolman path was latent for users who'd never manually picked a slot preset; the same "stale row overrides correct catalog name" symptom appeared for those who had. Existing stale rows self-heal on the next RFID-driven swap. |