mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 06:02:05 +02:00
0e8b5e032b97e2fc97cd5947ae80b56238daa0ee
484
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
af5d24e289 | feat(inventory): structured storage locations catalog (#1505) | ||
|
|
ead6c37147 |
fix(notifications): wire on_printer_offline dispatch on disconnect edge (#1752)
The provider toggle, schema, template, and NotificationService.on_printer_offline all shipped, but no caller invoked the dispatcher — the offline event was an orphan toggle. Edge detection in on_printer_status_change now schedules a debounced (60s) background task on the connected→disconnected transition; reconnect before the window elapses cancels it. Covers both upstream paths (smart-plug power-off via mark_printer_offline, and MQTT staleness via check_staleness), both of which already route through the status callback. The "back online" channel is the existing print-failure notification on firmware FAILED report — no symmetric on_printer_online needed. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
1bcd5c8ba5 |
feat(support): bundle redacted cached push_status per connected printer
The support bundle shipped support-info.json + bambuddy.log, but the raw
shape of the printer's MQTT push_status — the field that blocks per-model
work like AMS Backup detection (deferred in
|
||
|
|
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. |
||
|
|
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. |
||
|
|
d459b6eabb |
fix(slicer): preset visibility + lookup precedence + signed-out banner + AMS slot badges (#1712)
Four #1712 issues from the 2026-06-04 Orca Cloud integration: (1) Tier order put Orca Cloud above everything across SliceModal, auto-pick scoring, dropdown groups, the AMS slot picker, and the backend precedence. Bambu-Cloud-only users saw their profiles deprioritised behind an empty Orca tier. (2) Cross-tier dedup hid a same-named preset in all but the highest- priority tier. A user with both a local-imported and an Orca-synced "Bambu PLA Basic" couldn't see the Orca copy as a picker option. (3) CloudStatusBanner nagged signed-out users with a permanent "Sign in to Orca Cloud" line at the top of every slice -- even after explicit logout. Bambu Cloud had the symmetric problem. (4) ConfigureAmsSlotModal source badges were inconsistent: Orca rows showed only "Custom" (no source identity), Bambu Cloud built-in rows had no badge at all, and the orthogonal isUser-driven "Custom" badge collided with the source badge for cloud user presets. Order is local > orca_cloud > cloud > standard everywhere it lives (SliceModal SLICE_MODAL_TIER_ORDER + TIER_BONUS + dropdown tier list, ConfigureAmsSlotModal sourceOrder, and backend precedence). The order drives auto-pick + visual group rendering; it does NOT hide profiles. _dedupe_by_name replaced with _enrich_cloud_metadata: every tier returns its full list across all three slots (printer / process / filament). The function still backfills Bambu Cloud filament metadata from same-named local / orca_cloud / standard entries so cloud filaments score in pickFilamentForSlot. CloudStatusBanner silently no-ops on not_authenticated for both clouds; expired / unreachable still surface. The not_authenticated i18n keys stay in the locale files dormant. ConfigureAmsSlotModal: one source badge per row, one colour per source (green Local / purple Orca Cloud / bambu-blue Bambu Cloud / amber Built-in). The legacy isUser-driven "Custom" badge is gone; every row identifies its tier consistently. |
||
|
|
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. |
||
|
|
bf781e0566 |
fix(notifications): reprint-from-archive sent original print's finish photo (#1707)
Reprints reuse the source archive row via the expected-print promotion branch, but that branch never reset archive.timelapse_path. The stale path made _scan_for_timelapse_with_retries early-return (so the new run's MP4 was never downloaded), and _capture_finish_photo_from_timelapse then extracted the *original* run's last frame and shipped it to Telegram as the new run's finish photo. Surface was specific to the timelapse-prefer path (data.timelapse_was_active=true and no external camera) — fallback live-camera paths were unaffected, which is why this took a P2S reporter to surface. Clear archive.timelapse_path at promotion and unlink the stale on-disk MP4 (best-effort; missing files are logged and skipped). The orphan unlink also kills a long-standing file-leak: every reprint used to leave its predecessor's timelapse on disk forever. archive.photos is deliberately left alone — accumulating one finish photo per run is correct. |
||
|
|
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. |
||
|
|
e01d3a7979 |
feat(mqtt): one-shot device identification probe for unknown models
Logs device.dev_model_name / dev_product_name / dev_id / project_name at INFO level once per client session, falling back to device.keys() if none of the known fields are present. The MQTT push_status carries the model code in device.dev_model_name on every message, but nothing in bambu_mqtt.py reads or logs that field — so adding a new printer model meant chasing the code through either Bambu cloud or a manual mosquitto_sub. A2L (#1684) was the case that surfaced this: get_version also failed because the firmware disconnected right after request topic subscription, so the support bundle had no way to disclose the model. INFO level so the line lands in support bundles without enabling debug. One-shot via _device_id_logged, mirroring the existing _nozzle_fields_logged flag at line 2095, so push_status spam is avoided. Future-proofs against Bambu renaming the field (the fallback dumps device.keys() so a rename like model_name without the dev_ prefix is still observable). 3 unit tests in TestDeviceIdentificationProbe pin all three branches. |
||
|
|
2bb69cb079 |
fix(printer-models): swap A1 / A1 Mini in PRINTER_MODEL_ID_MAP
N1 and N2S were flipped relative to every other registry that names them - firmware_check.py (N2S -> "a1"), virtual_printer/manager.py (both the model map and the serial-prefix map: N2S -> 039 = A1, N1 -> 030 = A1 Mini), and printer_manager.py A1_MODELS all agree on N2S = A1, N1 = A1 Mini. Only printer_models.py had it backwards, so any path that resolved an A1-family printer by internal code rather than serial prefix would silently misclassify. Also fixes the matching comments in LINEAR_RAIL_MODELS - cosmetic only (both codes were already in the frozenset) but kept the file self-consistent. New TestA1SeriesModelIds regression test pins both directions so a future re-flip fails loudly. |
||
|
|
60e31634b8 |
feat(diagnostic): add "Store sent files on external storage" check (install step 4)
Detects the printer-side variant of install step 4 — many users (esp. on clean installs) forget to enable this and only notice when their archive cards have no thumbnails. The diagnostic now catches it upfront. Detection: read state.store_to_sdcard, which Bambuddy already parses from MQTT push_status home_flag bit 11 (bambu_mqtt.py:153). Instant, no I/O. An FTP upload-and-verify probe was tried first and rejected. /cache is always writable from Bambuddy regardless of the slicer setting — only BambuStudio's own behaviour changes when the toggle flips, not the printer's acceptance policy. Confirmed empirically against X1C + H2D with the slicer option toggled off: probe succeeded, home_flag bit 11 stayed True. So the only reliable signal is what the printer actually reports about its own state. Limitation: the printer-side variant only exists on newer firmware (P2S 01.02 / Bambu Studio 2.6+). On older versions the toggle lives only in the slicer and the printer never hears about it, so this check will pass even when the user is missing step 4 in BambuStudio. The skip-text and the wiki call this out explicitly. A reactive banner on the no-3MF archive-fallback path is planned as a follow-up to cover that case. Statuses: - pass: state.store_to_sdcard is True - fail: state.store_to_sdcard is False (-> overall escalates to problems) - skip: no live state, disconnected, or field never populated |
||
|
|
85fbd7fc35 |
fix(queue): persist "Print Anyway" so scheduler stops re-flagging it
When the user clicked Print Anyway on a filament-deficit warning, the
acknowledgement was one-shot. The route cleared manual_start and
filament_short, then the next scheduler tick re-ran
compute_deficit_for_queue_item against identical spool state, found
the same deficit, and re-set both flags. The item bounced between
"user said anyway" and "scheduler re-blocked" — every Play click
returned 409, every confirm got rolled back on the next tick.
Add a persistent acknowledgement flag on the queue item:
- New column `skip_filament_check` on print_queue. SQLite + Postgres
migration branched on is_sqlite() so Postgres doesn't reject
DEFAULT 0 on BOOLEAN.
- PrintQueueItemCreate + PrintQueueItemResponse schemas + the
TypeScript types carry the field.
- POST /print-queue/{id}/start with skip_filament_check=true now
ALSO sets item.skip_filament_check = True (not just clearing
manual_start / filament_short).
- PrintScheduler._block_on_filament_deficit short-circuits to
False — no compute, no flag-setting, no notification — when
item.skip_filament_check is True. We trust the operator's
decision and stop fighting them.
- PrintModal at queue-creation time threads
skip_filament_check=true into the create payload when the user
clicks Print Anyway on the frontend deficit warning, so a print
that was warned-then-acknowledged at add-to-queue time goes in
pre-acknowledged — scheduler never blocks it on first tick.
Flag is not auto-cleared on spool swap by design: if remaining is
now sufficient, the check returns no deficit anyway, so the flag
is moot. Auto-clearing would add lifecycle complexity without
changing behaviour.
AMS Backup awareness (the other half of the discussion) intentionally
NOT included — verified the H2D's bit-26 of print.cfg toggles with
the printer-side AMS Backup setting, but the X1C's cfg has a
different shape entirely and verifying every model family isn't
realistic. Silently under-warning would be worse than always
per-slot. The check stays single-slot for now.
|
||
|
|
72044e3a53 |
fix(usage): scope 3MF filament tracking to dispatched plate (#1697)
When a print targets a single plate from a multi-plate 3MF, both the
internal Filament Inventory tracker and the Spoolman-mode tracker parsed
the 3MF without a plate filter and summed every plate's filament — so a
single lid print debited the spool the entire file's grey + black totals.
The 3MF parser already supports plate_id (queue pre-flight uses it at
print_queue.py:254/:286). Plumbed it through both dispatch paths:
Queue path:
- PrintSession gains a plate_id field; on_print_start queries the
printer's currently-printing queue row and records queue_item.plate_id
onto the session.
- _track_from_3mf accepts plate_id and passes it to the extractor.
- store_print_data moves its existing queue-item lookup above the
extract and uses queue_item.plate_id as the plate filter.
Direct-Print path (reprintArchive / printLibraryFile — never goes
through the queue):
- _print_plate_ids dict added in main.py, parallel to _print_ams_mappings.
- register_expected_print accepts plate_id and stores it; the 2 sites in
background_dispatch.py and the 1 site in print_scheduler.py now pass
it (resolve was already happening, just needed reordering before the
register call so the value is available).
- Expected-print promotion in main.py injects _print_plate_ids[archive_id]
into the session, guarded so a queue capture wins over the dict.
- _get_start_plate_id helper feeds plate_id into all 3
_store_spoolman_print_data call sites; spoolman_tracking.store_print_data
takes the caller value first, falls back to queue_item.plate_id.
PrintArchive.filament_used_grams stays file-level summed by design
(#1593's contract — the archive describes the file, not the run); only
the per-run usage attribution becomes plate-aware. Single-plate direct
prints resolve to plate_id=1 → plate 1 = whole file, identical to the
prior no-filter behaviour.
|
||
|
|
7a9c78c32c |
● fix(vp): stop enforcing MQTT keepalive 1.5x to match real Bambu firmware (#1548)
Round 1 ( |
||
|
|
bfbf5d59af |
fix(profiles): add PLA-CF + Bambu CF/GF/specialty variants to filament_type select (#1686)
The Profiles page edit modal (shared by BL Cloud / Orca Cloud / Local Profiles) renders filament_type from backend/app/data/filament_fields.json via GET /cloud/fields/filament. The curated 11-option list pre-dated Bambu's CF/GF lineup expansion, so PLA-CF and most carbon/glass-fiber variants were unselectable — saved presets carried the wrong material code at dispatch. Expanded to 25 BambuStudio-aligned options grouped by family: PLA (+ CF/GF/ AERO), PETG (+ CF), ABS (+ GF), ASA (+ CF/GF), PC, PCTG, PA family (+ CF/ PAHT-CF/PA6-CF/PA6-GF), PET-CF, TPU, PPS family (+ CF/GF for X1E), PVA, HIPS. K-profiles editor unaffected (picks filament_id, not filament_type). |
||
|
|
432080d500 |
feat(queue): show build plate type on queue items + print modal (#1281)
Reporter on a multi-printer farm with 40+-plate runs needed to walk to
the printer with the right physical plate, but the queue and the
scheduling modal didn't surface curr_bed_type the way the archive card
already did.
New utils/threemf_tools.extract_bed_type_from_3mf helper (per-plate) so
both the queue API and the /plates endpoint can return per-plate values.
PrintQueueItemResponse.bed_type populated from archive.bed_type /
library_file.file_metadata['bed_type'] as the default, then overridden
per-plate via the helper when item.plate_id is set. /archives/{id}/plates
(and library equivalent) include bed_type in each plate object.
Per-plate accuracy matters because archive.bed_type is captured at
ingest as only the first plate's value (services/archive.py:235) -
a 40-plate 3MF mixing PEI + Engineering returns PEI at the archive
level for every plate. The helper re-reads the 3MF and returns the
truth.
Frontend: queue card meta row + PlateSelector per-plate row + PrintModal
header all render bed icon + canonical label via the existing
getBedTypeInfo() helper, same as the archive card.
|
||
|
|
2c2725cb53 |
fix(print): expose nozzle_offset_cali toggle for dual-nozzle printers (#1682)
Bambuddy's project_file MQTT payload hardcoded "nozzle_offset_cali": 2 (skip), giving users on H2D / H2D Pro / H2C / X2D no way to control the same toggle BambuStudio exposes. Critical for diamond-nozzle setups that must keep the calibration off. start_print() now takes a nozzle_offset_cali kwarg; the value is encoded as 1 (run) or 2 (skip) and gated on is_dual_nozzle so single-nozzle machines always send 2 even if a stale flag arrives. The kwarg threads through printer_manager, both background_dispatch sites, and print_scheduler so every dispatch path respects the per-item setting. print_queue gains a nozzle_offset_cali column (DEFAULT TRUE, is_sqlite() branch for Postgres BOOLEAN). Settings default key default_nozzle_offset_cali defaults to TRUE to match BambuStudio. Schemas updated across print_queue, library FilePrintRequest, archive ReprintRequest, settings. PrintModal renders the new toggle only when the selected printer is dual- nozzle (printer-mode: nozzle_count===2; model-mode: DUAL_NOZZLE_MODELS). SettingsPage default-print-options row + QueuePage bulk-edit tri-state both hide unless any registered printer is dual-nozzle. Labels reuse the existing settings.default* keys so the only new i18n strings are settings.defaultNozzleOffsetCali / Desc and queue.bulkEdit.nozzleOffsetCali - real translations in all 11 locales. |
||
|
|
d0d6a659e9 |
fix(reconcile): don't synthesize aborted PRINT COMPLETE on the bare-connect edge before push_status arrives (#1679)
on_printer_status_change fires from MQTT _on_connect BEFORE the first push_status round-trips, when PrinterState is still on construction defaults (state="unknown", subtask_name=""). The connected-edge reconcile was treating that degenerate state as evidence and synthesising aborted PRINT COMPLETE for every in-flight archive on every Bambuddy restart. The reactive PRINT COMPLETE then created a duplicate archive (lookup misses on cleared _active_prints), so filament got deducted twice. Two-layer guard: gate the reconcile spawn on a real state.state, and make _is_active_archive_stale return not-stale on unknown/empty input as defensive fallback. #1542 behaviour preserved — real stale archives still get caught the moment a real push_status arrives. |
||
|
|
fdaff37975 |
fix(queue): two-phase dispatch watchdog so a printer that accepts project_file but never starts doesn't wedge the queue (#1678)
_watchdog_print_start now treats subtask_id-advance as Phase A "command landed", not as final success. Phase B (180s) keeps watching for the active-state transition; if it never arrives — printer accepted the file but stalled (cloud+LAN re-auth after a power cycle on old firmware was the reported trigger) — revert the queue item to 'pending' instead of leaving it stuck in 'printing' until container restart. Phase B skips force_reconnect because subtask_id-advance proves the project_file landed; a reconnect mid-parse would trigger 0500_4003 (#1150). Phase A's H2D 50s FINISH→PREPARE tolerance (#1078) is preserved by Phase B's 180s headroom. |
||
|
|
96ce403554 |
fix(archive): HTML-unescape 3MF Title metadata; correct VP name tooltip (#1658)
ThreeMFParser._parse_3dmodel left XML-escaped values raw, so a Title of "PCB Vise & Solder Station" landed in the DB as the literal "&" and React re-escaped it on render to "&amp;". Apply the same loop-until-stable html.unescape() the sibling ProjectPageParser already uses, uniformly across all <metadata> values. Same drop: rewrite the VP archive-name-source tooltip in all 11 locales. BambuStudio 2.7.x (PrintJob.cpp:314-325) overwrites the user-typed Send-dialog name with the slugified 3MF Title field whenever one is present, so the previous "handy if you renamed the job in the send dialog" copy was false. New text spells out the actual behavior; both Filename and Metadata modes often produce the same string for that reason. |
||
|
|
d597d36d5b |
fix(vp): slice FTP passive ports per VP, drop bridge-mode RAM by 95% (#1646)
The 50000-51000 docker-compose port range spawned ~2000 docker-proxy host processes (~3.5 GB RSS) under Docker's default userland-proxy. The 1001-port pool was symptom treatment — collisions only matter for multi-VP-on-shared-bind, but the cost was paid by every install. Each VP now gets a non-overlapping 10-port slice computed from its id (VP 1 -> 50000-50009, VP 2 -> 50010-50019, ...). Class constants are gone; VirtualPrinterFTPServer takes passive_port_min/max instance args. Wraps modulo PASSIVE_MAX_SLOTS = 100, with the existing 10-attempt random retry as same-slot collision fallback. Compose default narrowed to 50000-50029 (3 VPs). Proxy-mode VPs forward the real printer's full range and stay on the separate TCPProxy constants. Compose comment rewritten to acknowledge Linux multi-service hosts as a primary bridge-mode audience and drop an over-stated "confirmed by reporter" claim about userland-proxy=false. |
||
|
|
edaf7c4559 |
fix(queue): cancelled prints no longer block require_previous_success chain (#1667)
Two bugs in PrintScheduler._check_previous_success: - Lookback excluded 'cancelled' so user cancellations were walked past - Lookback included 'skipped', so one skip cascaded indefinitely Swap to ['completed', 'failed', 'cancelled', 'aborted'] and accept both 'completed' and 'cancelled' as predecessor success. Real 'failed' / 'aborted' still gate. One-shot migration in run_migrations resets only the skipped items whose true predecessor was cancelled — surgical reversal of the exact bug fingerprint, leaves genuine failure-gated skips alone. Portable across SQLite and Postgres, idempotent on re-run. |
||
|
|
4bcab89eee |
fix(firmware-check): bypass Cloudflare TLS-fingerprint gate via curl_cffi (#1666)
Cloudflare on bambulab.com now serves cf-mitigated=challenge to plain Python TLS handshakes. Use curl_cffi.AsyncSession with impersonate="chrome" for the two bambulab.com fetches (index page + per-model JSON); wiki and CDN paths stay on httpx. HTTP User-Agent stays honest "Bambuddy/1.0" — only TLS-handshake bytes match Chrome, per the compliance commitment. Soft dependency — falls back to httpx with a startup warning when curl_cffi isn't importable; wiki-based version detection still works. |
||
|
|
f243e4e598 |
fix(asyncio): track strong refs on orphan create_task sites
asyncio holds only a weak reference to tasks returned by ``create_task``. Fire-and-forget callers that discard the return value let the event loop GC the task before it finishes, logging ``Task was destroyed but it is pending!`` with no traceback. The #1648 support-bundle review surfaced 94 such warnings in 8 days of v0.2.4.5 -- the silently-vanished exceptions reach support bundles as opaque GC notices instead of actionable errors. New backend/app/core/tasks.py::spawn_background_task(coro, *, name=None) is the one place in the codebase that calls asyncio.create_task. It stores the task in a module-level set, attaches a done-callback that auto-removes on completion AND surfaces any uncaught exception via the logger with the originating traceback, and accepts name= so a leak source is traceable through /tracebacks and the log line. Cancelled tasks don't log (a shutting-down service is not an error). Migrated the 16 truly-orphan create_task call sites to the helper: main.py (8): reconcile-stale, cooldown-poweroff, energy calc, smart-plug, maintenance-check, photo-then-notify, layer-timelapse, scan-timelapse, print-scheduler, notify-no-archive (the last one was hand-rolling the same pattern with task + no-op done_callback) printers.py:3123 apply-pa-after-refresh print_queue.py:1034 queue cooldown-poweroff firmware_update.py:261 firmware upload archive.py:1514 timelapse mp4 convert print_scheduler.py:2199 watchdog print-start library.py:1614 STL backfill smart_plugs.py:259 tasmota scan discovery.py:159 subnet scan smart_plug_manager.py x3 plug auto-off-pending background_dispatch.py x2 (lambda-wrapped inside loop.call_soon_threadsafe) upload progress Sites that already kept strong refs are unchanged: self._tasks.append(asyncio.create_task(...)) -- VP manager, tcp_proxy, mqtt_server self._x_task = asyncio.create_task(...) on service instances -- mqtt_bridge, obico_detection, github_backup, archive_purge, local_backup, library_trash, discovery service Locally assigned + awaited/gathered -- tcp_proxy bidirectional pumps, camera_fanout, slice_dispatch, slicer_api progress_task, manager._finish_release_task, main.py module-level cleanup loops |
||
|
|
e895d8350a |
fix(vp): re-fire FINISH after project_file ack so slicer releases (#1658)
Reporter on Bambu Studio 2.7.1.57 + X1C saw the Send modal stuck at
"Downloading" after sending to a Queue-mode VP. Delete-from-queue and
even Auto-Dispatch ON + a successful real print didn't release it.
Root cause: BS 2.7.x flipped the Send sequence from
MQTT project_file -> FTP upload -> done
to
FTP verify_job -> FTP .3mf -> MQTT project_file.
The #1280 fix sets gcode_state=FINISH in on_file_received (after the
FTP upload). Under the new order, the synthetic project_file ack in
_send_print_response then runs and overwrites _gcode_state back to
PREPARE. The 1 Hz cached-as-base push stream carries PREPARE forever,
the slicer never sees the FINISH transition it waits for, and the
modal sits stuck. Auto-Dispatch ON shares the cause: the real
printer's PREPARE->RUNNING->FINISH on the bridge gets masked by the
local _gcode_state override in _send_status_report.
Re-fire set_gcode_state("FINISH", filename, prepare_percent="100")
1.5 s after the project_file ack for every non-proxy mode (queue /
archive / review). The 1.5 s window lets the slicer see at least one
PREPARE push on the 1 Hz cycle so the transition reads as
PREPARE -> FINISH, matching what the slicer expects. Proxy mode is
exempt -- there the real printer drives the bridge state and a
synthetic FINISH would clobber a real PREPARE/RUNNING transition.
The scheduler cancels any in-flight timer when a new project_file
arrives so a retrying slicer doesn't end with two competing FINISH
timers. The pending timer is also cancelled on stop_server.
|
||
|
|
19073f5c84 |
fix(vp): auto-derive access code from target printer in non-proxy modes
Non-proxy VPs (Archive / Review / Queue) with a target printer set up a live-mirror bridge that forwards the slicer's MQTT and RTSPS auth bytes through to the real printer. The slicer holds one code in its profile (the one it bound the VP with), and that code has to satisfy both the VP listener and the real printer at the far end of the bridge. If the codes diverge the bridge silently fails at the second hop — slicer reaches .49:8883, FINs before sending a ClientHello, retries identically. The wiki framed the code-match requirement as a camera-only concern; it isn't, all bridged protocols inherit. Fix removes the foot-gun instead of re-documenting it. When a target is selected on a non-proxy VP the access-code field switches to a read-only display showing the target's code with an Eye-toggle reveal; the backend auto-inherits on every create / update (any explicit access_code submitted alongside a target is silently overridden as belt-and-braces for non-UI clients). The required-when- enabling check now treats target-set as satisfying the access-code requirement. Standalone (no-target) non-proxy VPs still get the editable input + Save button. One-shot startup migration corrects any pre-existing mismatched rows: SELECTs diverged VPs and logs one INFO line per row for the audit trail, then UPDATEs via correlated subquery. Idempotent and portable between SQLite and Postgres. |
||
|
|
12d17bfbe7 |
fix(photo): source finish photo from forced timelapse + cleanup (#1397)
Bambu's end-gcode lowers the bed at gcode_state=FINISH. Bambuddy's live-camera grab captured the bed already dropped, ruining the photo framing. Source the photo from a brief Bambu timelapse instead — firmware stops timelapse recording AFTER toolhead parks but BEFORE bed-drop runs, so the last frame frames the finished print correctly. When capture_finish_photo is on AND the user did not opt in to timelapse for this print, force timelapse=True at dispatch + mark the new PrintArchive.bambuddy_forced_timelapse column. After extraction (success or failure), cleanup deletes the locally-attached file, clears archive.timelapse_path, and walks the four scanner directories (/timelapse, /timelapse/video, /record, /recording) trying FTP DELE against the original filename. User-opted-in timelapses pass through unchanged. Resolver lives at services/background_dispatch.py::resolve_effective_timelapse (module-level so the print queue can reuse it). Both dispatch paths wired: background_dispatch.py (Print Now / Reprint) AND print_scheduler.py:_start_print (the queue). Field testing caught the scheduler gap on the first round — AST regression test now asserts start_print(timelapse=...) references effective_timelapse, not the raw item.timelapse, so a future refactor can't silently drop it. Extractor: ffmpeg -i input.mp4 -update 1 -q:v 2 out.jpg. Decoded frames overwrite the same output file, so the file left on disk is the literal last frame regardless of duration. Bambu records one frame per layer-change, so a 16-layer cube produces a 0.6 s timelapse — the original -sseof -1.0 approach seeked before the start of the file and returned frame 0 (empty bed). Decoding every frame is fine; Bambu timelapses are short by construction even on hours-long prints. Migration adds bambuddy_forced_timelapse branched on is_sqlite() (DEFAULT 0 / DEFAULT FALSE — PG rejects DEFAULT 0 for BOOLEAN). Verified live on postgres:16-alpine. Photo-task wait_for budget extends 45s -> 75s when timelapse_was_active so the notification carries the bed-up photo instead of falling back to the live-cam grab on slow links. Scope limit, documented in the camera wiki: prints started directly on the printer touchscreen / Bambu Handy / Bambu Studio Send bypass both dispatch paths, so the override doesn't fire there. Future option: mid-print M981 S1 P20000 MQTT toggle in on_print_start. Setting description rewritten in all 11 locales to drop the "only works when timelapse enabled" caveat (Bambuddy now forces it) and explain the kept-or-deleted behaviour. |
||
|
|
aed01f875a |
fix(vp): resolve hostname/FQDN targets in MQTT bridge IP encoding (#1429 follow-up)
Printers added to Bambuddy by hostname/FQDN (e.g. p1s.fritz.box) hit 'invalid IPv4' in _ip_to_uint32_le, so the net.info[*].ip rewrite never armed and BambuStudio Send went straight to the real printer instead of the Bambuddy archive whenever the printer was powered on. Add _resolve_target_to_ipv4(target): IPv4 pass-through, else socket.getaddrinfo(target, family=AF_INET). AF_INET filter is load-bearing because net.info[*].ip is uint32 LE and IPv6 can't round-trip. OSError returns None so a transient DNS failure recovers on the next 30s refresh tick via the existing not-armed throttle. Apply the resolver to both the encode call and the host-interface picker (which also assumes dotted-quad). Armed log line now carries configured->resolved when they differ, so bad-DNS regressions stay legible in 'docker logs'. The unresolvable not-armed reason now names the configured value rather than parroting 'invalid IPv4', distinguishing 'DNS gave a v6 result' from 'user typed garbage'. Root-caused by @Mape6; @TrickShotMLG02 confirmed the FQDN workaround on the same release. Pre-0.2.4 these setups worked by accident because there was no net.info[].ip rewrite at all. |
||
|
|
54389a54aa |
fix(library): MakerWorld URL import honours external folder destinations (#1645)
Reported and root-caused by @needo37. Importing a model via the MakerWorld URL-download feature into a writable external folder (e.g. SMB/NFS-mounted NAS) saved the 3MF into Bambuddy's internal managed library dir, not the external mount. The file card showed in the File Manager under the external folder, but the bytes never landed on the NAS, and the on-disk copy was UUID-renamed so a find by the original basename matched nothing. Root cause was save_3mf_bytes_to_library at backend/app/api/routes/library.py:422: it accepted folder_id but never loaded the folder, never inspected is_external / external_path, hardcoded the destination to get_library_files_dir() with a UUID name, and left the LibraryFile row with is_external=False. So the row's folder_id pointed at the external folder while its bytes and is_external flag both said "managed/internal". Same class of bug as #1112, which had been fixed for the multipart-upload and move paths but never applied to this byte-import path. Fix mirrors the multipart-upload path directly: - Load target_folder from folder_id when non-None. - Feed it to _resolve_upload_destination(target_folder, filename), which already returns (dest, is_external) and enforces the 403-read-only / 400-unwritable-or-missing / 409-collision rejections. - Write bytes to dest (real filename for external, UUID for managed). - Persist the row with file_path=_stored_file_path(dest, is_external) and is_external=is_external. The route-layer read-only guard at makerworld.py:256-260 is preserved - it returns the friendlier error before the upstream download burns bandwidth - and _resolve_upload_destination's identical check stays as defence-in-depth for any future caller that skips the route gate. Thumbnails continue to live under the managed get_library_thumbnails_dir() regardless of the 3MF's location, matching the upload path. |
||
|
|
e802bfc806 |
fix(ftp): cap TLS to v1.2 for X2D FTPS to dodge WRONG_VERSION_NUMBER on firmware 01.01.00.00 (#1638)
Reporter @vasmarfas saw X2D archive cards land almost empty - only print time, no filament weight / layers / MakerWorld link / thumbnail - and Spoolman filament-usage tracking went silent on the same printer. Support bundle traces the end-to-end: at print start backend/app/main.py::on_print_start tries the usual FTP-download dance for the 3MF, every implicit-FTPS connect attempt to the X2D fails with `[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1032)`, ~2 minutes later "Could not find 3MF file for print" -> "Created fallback archive". Fallback path writes file_path="", file_size=0, content_hash=NULL, no layers / filament / model-link fields. Spoolman tracking degrades from the same root cause - both depend on the 3MF metadata parser. Proximate cause: Python 3.13's default ssl.create_default_context() negotiates TLS 1.3, the X2D's implicit-FTPS server on port 990 rejects the ClientHello. Same family as the P2S 01.02.00.00 bug from #1401 (post-Python-3.13 TLS-1.3 breakage), different wire-level failure mode (P2S completes the handshake and truncates with 426; X2D fails the handshake outright). Same fix shape: add X2D to backend/app/services/ftp_profiles.py with cap_tls_v1_2=True, plus N6 -> X2D SSDP alias. Every other model stays on negotiated TLS 1.3. Honest caveat: hypothesis-driven trial, not a confirmed root-cause fix. WRONG_VERSION_NUMBER could equally describe the X2D switching to explicit FTPS (AUTH TLS on plaintext greeting) or moving FTPS to a different port - either would need a different code path. Reporter has been asked to test this build; if the cap doesn't clear it the registry slot stays useful and the next diagnostic round goes to openssl s_client from a network-adjacent host. |
||
|
|
18d534c945 |
feat(orca-cloud): integrate Orca Cloud profile sync across UI, slicer and SpoolBuddy
Reads, lists, and slices with profiles from a user's Orca Cloud account (OrcaSlicer 2.4.0-alpha's Supabase-backed sync) alongside the existing Bambu Cloud integration. Four sign-in providers (Google / Apple / GitHub / email+password); password defaults. Paste-flow PKCE because Orca's Supabase project only allowlists localhost redirect_to — open feature request at OrcaSlicer/OrcaSlicer#14028. Surfaces: - Profiles tab: new "Orca Cloud" tab next to "Bambu Cloud" with the same rich layout (search + 5 filter dropdowns + 3-column grouped grid + read-only detail modal) - SliceModal: 4-tier preset picker (orca_cloud > local > bambu cloud > standard); separate status banner per cloud; metadata-aware pre-pick scores Orca filaments above local (Orca's sync_pull returns full content inline so filament_type / filament_colour come for free, no per-setting fetch rate-limit dance) - ConfigureAmsSlotModal: orca_cloud as a new preset source (prefixed orca_<UUID> to match local_/builtin_); generic Bambu filament-ID derivation from parsed material (printer firmware can't grok Orca UUIDs); slot mapping persists preset_source='orca_cloud' - SpoolForm / SpoolBuddyWriteTagPage: Orca filaments merge into the cloud preset list via Promise.allSettled (OrcaProfileMeta is structurally identical to SlicerSetting) Backend: - services/orca_cloud.py: OrcaCloudService with PKCE / token exchange / single-use refresh rotation / get_user_info / list_profiles via the bare /sync/pull bootstrap path - routes/orca_cloud.py: 7 endpoints (auth/start, auth/finish, auth/password, status, logout, profiles, profiles/{id}); router-level _cloud_api_key_gate + per-route cloud_caller() so API-keyed callers (SpoolBuddy kiosk) properly resolve their owner User; just-in-time refresh with atomic persist-before-API-call - routes/slicer_presets.py: _fetch_orca_cloud_presets mirrors the Bambu Cloud fetcher (status vocabulary, 5min cache, permission shortcut); _dedupe_by_name extended to 4 tiers; UnifiedPresetsResponse gains orca_cloud + orca_cloud_status - services/preset_resolver.py: PresetRef.source extended with "orca_cloud"; _resolve_orca_cloud walks list + filters - 8 columns on users table for tokens (5 persistent) + transient PKCE handshake state with 10-min TTL (3); dialect-branched DATETIME / TIMESTAMP; auth-disabled mode falls back to Settings table - orca_cloud:auth permission folded into can_access_cloud API-key scope (same trust dimension) |
||
|
|
51730a7bf1 |
feat(ams): gate humidity/temperature alarms on AMS-has-filament (#1619)
The hourly AMS sensor recorder dispatched humidity and temperature alarms for every unit above threshold without checking whether the unit was actually loaded. Empty AMS units still report ambient readings, so users with one loaded + one empty AMS got useful alarms for the loaded one and hourly noise for the empty one. Disabling the whole alarm category killed both — not a real choice. New _ams_has_filament helper inspects tray_exist_bits (hex bitmap, "0" = empty) with fallback to the tray array's tray_type strings for shapes where the bitmap is missing. The recorder gates the alarm dispatch on this check per-AMS-unit, so a multi-AMS printer with one loaded + one empty still alarms on the loaded one. Sensor history still records regardless of the gate so the System page humidity charts stay continuous — only the outbound notification is suppressed. 9 unit tests cover the bitmap-zero case, bitmap-missing fallback, garbage/blank/int bitmap edges, and defensive malformed tray. |
||
|
|
db1c664fce |
feat(vp): surface why MQTT bridge IP encoding didn't arm (#1429 defensive)
_refresh_ip_encoding had 4 silent early-returns. When the rewrite silently no-op'd on a user's setup, the only signal was the absence of the "armed" INFO line, and diagnosing which path was firing meant grepping the source. Each path now emits one INFO line naming the specific reason. A _not_armed_reason dedup field throttles to one line per state change, so an idle unarmed bridge doesn't spam every 30s refresh tick. Cleared on successful arm so regressions re-emit. Not a fix for #1429 itself — the bridge logic is unchanged; this just turns the silent failure into visible signal so the next "fix didn't work for me" report can be triaged in one round-trip. |
||
|
|
51abc4b7a8 |
feat(drying): enable AMS drying for H2C at firmware 01.02.00.00+ (issue #1624)
H2C was in _DRYING_UNSUPPORTED_MODELS. Move to _DRYING_MIN_FIRMWARE with the same 01.02.00.00 floor as H2S / P2S. Both SSDP model codes the H2C advertises (O1C single-nozzle, O1C2 dual-nozzle) get the same gate so supports_drying() fires correctly regardless of which form is stored on the printer record. |
||
|
|
c571ad86dd |
feat(diagnostic): printer_publishing check + countdown UI (#1622)
The existing connection diagnostic proved TCP + TLS + auth + SUBSCRIBE but not that the printer was actually publishing reports. A wrong-cased serial passes mqtt_auth because the broker accepts the subscription regardless; the user-visible symptom is empty AMS / no K-profiles / no custom filaments in the slicer Device tab because the VP cached state is empty. Bambuddy already logged the actionable hint at bambu_mqtt.py:498 but only to container logs. New printer_publishing check turns that warning into a structured diagnostic result. Pass = bridge has seen at least one report since the last (re)connect; fail = zero reports across the wait window with fix-text pointing at the case-sensitive serial. Bounded 10s poll on the on-demand UI route, no wait on the support-package gathering path so bundling stays fast. Exits the moment a message arrives — typical wall-clock is 1-2s. Frontend renders an elapsed-seconds counter plus a "Listening for status report — up to 10s" hint during the pending state so the wait doesn't look hung. PUBLISH_WAIT_DEFAULT_SECONDS pinned on both sides. report_messages_since_connect exposed as a public property on BambuMQTTClient so the diagnostic doesn't reach into private state. |
||
|
|
a1cb5d5b4d |
fix(backup): interpret scheduled-backup HH:MM as local time, not UTC (#1602 follow-up)
The Scheduled Local Backups time-of-day picker was interpreted as UTC by _calculate_next_run, so a UTC+3 user had to enter 18:00 to get a 21:00 local backup. The UI labeled the field "UTC" but it was still surprising. Picker is now interpreted in the container's local timezone, resolved from the TZ env var via zoneinfo.ZoneInfo (same source the Support page's environment.timezone shows). UTC fallback when TZ is unset or unrecognised. The /local-backup/status endpoint exposes the resolved zone, and the UI renders it next to the field via a new backup.localTimeHint i18n key with real translations in all 10 non-English locales. One-time behaviour change for users who entered a UTC time as a workaround: the first scheduled cycle after upgrade will run at their local TZ offset earlier than expected. Re-enter the time as local once and it is correct from then on. No migration is shipped; migrating around a DST boundary would be ambiguous. |
||
|
|
cc25cbe774 |
fix(archives): #1608 suppress card time-accuracy badge for multi-run archives
compute_time_accuracy in routes/archives.py compares the archive row's own started_at / completed_at (which reflect the latest run only) against archive.print_time_seconds (which the #1593 parser fix correctly stores as the sum across plates). For a 3-plate file printed plate-by-plate the ratio is ~300%, producing a "+188%" card badge that means nothing — apples to oranges. The 5-500% sanity band catches truly broken values but lets this deterministic N×100% shape through. Reporter's archive #65 was 3 plates over 9 runs. compute_time_accuracy gains an optional run_aggregate argument and returns both actual_time_seconds and time_accuracy as null when the aggregate reports more than one logged run. The frontend already falls through to print_time_seconds for the time display (actual_time_seconds || print_time_seconds) and gates the badge on time_accuracy being truthy, so multi-run archives now show the slicer estimate with no badge. Single-run archives keep the original behaviour verbatim. The fix is applied at every call site that renders an archive card: archive_to_response now threads run_aggregate through, and the three endpoints that previously didn't load the aggregate (archives.py search fast-path and FTS path, single-archive PATCH, and projects.list_project_archives) now batch-load it via the existing _load_run_aggregates helper. The stats endpoint's per-run accuracy aggregation at archives.py:940 already uses PrintLogEntry.duration_seconds with its own 50-200% band filter and is untouched. |
||
|
|
9cc4b6aa60 |
fix(virtual-printer): #1610 add Bambu cipher pin to every slicer-facing TLS context
The #620 patch fixed the OpenSSL-3.x-strips-plain-RSA-AES-GCM cipher mismatch on the printer-facing TLSProxy client context. The same fix was never applied to the four other slicer-facing TLS contexts. On hardened distros (Fedora / RHEL with update-crypto-policies, hardened Alpine builds) where the system narrows DEFAULT to forward-secrecy only, the slicer's ClientHello finds no overlap with what Bambuddy offers and the handshake aborts with the slicer reporting code=-1 before any application data flows. The reporter pinpointed the missing set_ciphers call in bind_server.py against the #620 lineage; the audit-wide sweep here extends the same fix to mqtt_server.py, tcp_proxy._create_server_ssl_context (the missing other half of #620), and ftp_server.py. For the three new contexts (bind / mqtt / proxy-server) the cipher string is DEFAULT:AES256-GCM-SHA384:AES128-GCM-SHA256 — verbatim match with the #620 client-side fix. For FTPS the original HIGH baseline is kept (HIGH:AES256-GCM-SHA384:AES128-GCM-SHA256:!aNULL:!MD5:!RC4) so the cipher set stays a strict superset of what shipped before — HIGH offers ~58 suites DEFAULT doesn't (CCM / ARIA / CAMELLIA / DSS) that no Bambu slicer is known to pick, but narrowing a compat surface without proof would violate the existing don't-remove-compat-pinning rule. TLS version pins (TLSv1_2 minimum across all four, TLSv1_2 max on FTPS for the BambuStudio PSK-reuse compat) and verify-mode settings are unchanged — only the cipher list is widened. |