From b5a2f56cca564f80d45863cd2a27430c34e28bdb Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 28 Jun 2026 11:39:47 +0200 Subject: [PATCH] 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