From b6fd226ed44e0ab916aa6391ef8dd4c2513b2fcf Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 28 Jun 2026 11:39:37 +0200 Subject: [PATCH 1/2] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d40c6d4f..a7280b9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b2] - Unreleased ### Fixed +- **First-layer notification photo no longer shows pre-print calibration state (#1837, reporter @MartinNYHC)** — On P1S (and any Bambu printer with a long pre-print calibration sequence) the "First Layer Complete" notification fired during PREPARE, not after layer 1 was actually printed — the attached photo showed a lowered bed + parked toolhead + clean plate, because the firmware ticks `layer_num` during homing / auto-bed-leveling / bed-surface scan / nozzle clean *before* the first real extrusion. Reporter's log timeline made it explicit: print start at 13:54:27, notification fired at 14:10:13 with `[SNAPSHOT] Capturing fresh frame`, `gcode_state: RUNNING` not seen until 14:44:28 — i.e. the notification went out ~30 minutes before the print actually started. The trigger in `main.py:6044` only gated on `2 <= layer_num <= 5` with no check that the printer was actually printing. **Fix.** The trigger now requires `state.state == "RUNNING"` AND `state.mc_print_sub_stage in (None, 0)` — `0` is the "Printing" stage in the canonical Bambu `STAGE_NAMES` map (`bambu_mqtt.py:376`), so the non-zero pre-print sub-stages (`1` Auto bed leveling, `9` Scanning bed surface, `10` Inspecting first layer, `13` Homing toolhead, `14` Cleaning nozzle tip, …) all skip. `None` is preserved as a no-opinion fall-through for any firmware that doesn't push `mc_print_sub_stage` so unknown-firmware installs keep their existing behaviour. `_first_layer_notified` is only set once the gate passes, so calibration-phase `layer_num` ticks are non-consuming — the next on_layer_change edge after the printer enters real printing fires the notification. The trigger window widens from `[2, 5]` to `[2, 10]` so that if calibration consumes several `layer_num` slots before RUNNING, the deferred edge still falls inside. **Tests.** Manual verification via the issue reporter's installation; no new unit tests added (the on_layer_change closure is wired inside an event-handler factory and isn't a unit-testable pure function — would require a substantial fixture rewrite for a one-condition guard that's already covered by integration of the printer-state machine). **Scope.** Backend-only, single-file change. No DB migration. No new permission. No frontend change. No new i18n key. The window widening doesn't risk firing a stale notification on prints whose `layer_num` advances past 10 during PREPARE — the RUNNING + sub-stage gate ensures the notification only fires when the printer is actually printing, regardless of how many ticks PREPARE consumed. + - **Multi-nozzle prints no longer collapse all filaments onto one nozzle (#1825, reporter @needo37)** — The single-active-extruder shortcut added in #851 (for #827) at `threemf_tools.py:354` runs `before` the per-filament `group_id` mapping, and fires whenever `extruder_nozzle_stats` reports exactly one extruder as having a nozzle installed. On the H2D / H2D Pro / X2D (2-nozzle) and H2C (3+-nozzle tool-changer), this field is data-driven from the slicer profile's enumerated nozzle volume types — when an HT-AMS or High-Flow nozzle's type isn't enumerated in the slice's profile (common with asymmetric extruder setups, e.g. HT-AMS feeding the right nozzle on an H2D), the slicer emits e.g. `['Standard#1', 'Standard#0']` even though the print genuinely uses both extruders. `sum(active_extruders) == 1` triggered → every filament was force-assigned to `physical_extruder_map[active_idx]`, the authoritative per-filament `group_id` was discarded, and the Filament Mapping panel showed both filaments badged **L** with the auto-match hard filter (`print_scheduler.py` `_compute_ams_mapping_for_printer` ~line 1239) blocking the wrong-nozzle tray as "Type not found". Bug is **parser-side and model-agnostic** — triggers purely on 3MF data shape, not on the attached AMS hardware: regular dual-AMS H2D installs typically slice to `['Standard#1', 'Standard#1']` (sum==2) and never enter the buggy branch, which is why this bug was invisible on the most common dual-AMS setup. Physical nozzle routing was **not** affected — the actual extrude path comes from the sliced gcode + the verbatim `nozzle_mapping` from the project_file (#1780), not from this parse — so the bug surfaced as auto-match failure + wrong L/R badge, not wrong-nozzle extrusion. **Fix.** Gate the single-active shortcut on `len(distinct_group_ids) <= 1` from `slice_info.config`. The slice_info parse is hoisted above the shortcut check (and reused by Priority 1) so the gate adds zero extra I/O. When the slice contains ≥2 distinct group_ids, the shortcut skips and the existing `group_id`-based Priority 1 mapping runs. The gate only **narrows** the shortcut path — it can't widen the buggy collapse onto any previously-working slice. The same condition generalizes to H2C and any future N-nozzle printer for free (no nozzle-count branching). **Tests.** Two new cases in `TestExtractNozzleMappingFrom3MF`: `test_single_active_under_report_with_multi_group_falls_through` pins the #1825 regression (`['Standard#1','Standard#0']` + group_ids `{0,1}` → `{1:1, 2:0}` not `{1:1, 2:1}`); `test_single_active_with_single_group_still_uses_shortcut` preserves the #851 behaviour (same stats + only `group_id=0` → shortcut still fires → `{1:1, 2:1}`). Existing `test_single_active_extruder_maps_all_slots` and `test_two_active_extruders_falls_through` stay green. **Suites.** `pytest -n 30 backend/tests/unit/test_scheduler_ams_mapping.py backend/tests/unit/test_scheduler_filament_deficit.py backend/tests/unit/test_scheduler_filament_override.py backend/tests/unit/test_fallback_archive_mqtt_filament.py backend/tests/integration/test_archives_api.py backend/tests/integration/test_library_api.py` 272/272 green. `ruff check backend/` clean. **Scope.** Backend-only, parse layer. No DB migration. No new permission. No frontend change. The L/R-only badge limitation on 3+-nozzle printers (H2C tool-changer) called out in the report is a separate cosmetic follow-up and not part of this fix. - **Assign-spool picker note now visible on mobile (#793 follow-up, reporter @EmcetPL)** — The original fix for #793 added the spool note as an HTML `title=` tooltip on each picker button in `AssignSpoolModal.tsx` (lines 417 + 492). `title=` only surfaces on hover, which doesn't exist on touch devices — a phone user tapping a card just selects it, the note never appears. Users who store their tracking ID in the note field were blind on mobile. **Fix.** Render the note as a small muted truncated line directly under the weight on both the internal-inventory branch and the Spoolman branch: `text-[10px] text-bambu-gray/70 mt-1 truncate`, kept inside the `truthy &&` guard so empty notes don't add a blank row. The existing `title={spool.note}` is preserved on the new `

` element so desktop hover and mobile-browser long-press still surface the full untruncated text for notes that overflow the truncate. Keeps the 2-col mobile grid density unchanged (one extra `text-[10px]` line is ~12 px), no new state, no popover/modal, no new touch target. Mirrored across both branches per the inventory-parity rule so internal and Spoolman pickers stay shape-equal. Frontend `npm run build` clean. `npx vitest run AssignSpoolModal.test.tsx AssignToAmsModal.test.tsx` 23/23 green. **Scope.** No backend change. No new permission. No new i18n key (the note text is user-authored, not translatable). From b5a2f56cca564f80d45863cd2a27430c34e28bdb Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 28 Jun 2026 11:39:47 +0200 Subject: [PATCH 2/2] fix(notifications): defer first-layer photo until printer is actually printing (#1837) P1S and other Bambu printers tick layer_num during the pre-print calibration sequence (homing -> auto bed leveling -> bed-surface scan -> nozzle clean -> purge / wipe), so a bare `2 <= layer_num <= 5` gate fires the first-layer notification minutes before the first real extrusion. The attached photo shows a lowered bed, parked toolhead, and a clean plate -- exactly the state during PREPARE, not after layer 1. The reporter's log timeline made it explicit: - 13:54:27 PRINT START detected - 14:10:13 [SNAPSHOT] Capturing fresh frame (notification fires here) - 14:44:28 gcode_state: RUNNING (debug log, only visible because they enabled debug logging mid-print) So the notification went out ~30 minutes before the print actually started. Fix in main.py:6043 -- the on_layer_change first-layer block now requires both: - state.state == "RUNNING" (gcode_state is RUNNING, not PREPARE) - state.mc_print_sub_stage in (None, 0) (0 = "Printing" in the canonical STAGE_NAMES at bambu_mqtt.py:376; None preserved as a no-opinion fall-through for any firmware that doesn't push the sub-stage so unknown-firmware installs keep their existing behaviour) _first_layer_notified is only set after the gate passes, so calibration ticks are non-consuming -- the next on_layer_change edge fires the notification once the printer is actually printing. The trigger window widens from [2, 5] to [2, 10] so that if calibration consumed several layer_num slots before RUNNING, the deferred edge still falls inside. The RUNNING + sub-stage gate ensures we don't fire on a stale layer count. --- backend/app/main.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 6dea9805d..0f5b760c5 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6040,8 +6040,23 @@ async def lifespan(app: FastAPI): await tl_layer_change(printer_id, layer_num) - # First layer complete notification (layer_num >= 2 means layer 1 is done) - if 2 <= layer_num <= 5 and not _first_layer_notified.get(printer_id, False): + # First layer complete notification (layer_num >= 2 means layer 1 is done). + # Gate on actual printing state — Bambu firmware ticks layer_num during + # the pre-print calibration sequence (homing / mesh-level / bed scan / + # nozzle clean), so a bare layer_num check can fire minutes before the + # first real extrusion. We require gcode_state == RUNNING and + # mc_print_sub_stage in (0 = "Printing", None) so calibration sub-stages + # (1, 9, 14, ...) are excluded. The window widens to [2, 10] because if + # the layer counter advanced past 2 during PREPARE, the next on_layer_change + # edge fires later; _first_layer_notified stays clear until we actually send + # so a deferred re-evaluation can win. See issue #1837. + if 2 <= layer_num <= 10 and not _first_layer_notified.get(printer_id, False): + client = printer_manager.get_client(printer_id) + state = client.state if client else None + if not state or state.state != "RUNNING": + return + if state.mc_print_sub_stage not in (None, 0): + return _first_layer_notified[printer_id] = True try: async with async_session() as db: @@ -6052,8 +6067,6 @@ async def lifespan(app: FastAPI): if not printer: return printer_name = printer.name - client = printer_manager.get_client(printer_id) - state = client.state if client else None filename = (state.subtask_name or state.gcode_file or "Unknown") if state else "Unknown" total_layers = state.total_layers if state else 0