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.
This commit is contained in:
maziggy
2026-06-28 11:39:47 +02:00
parent b6fd226ed4
commit b5a2f56cca
+17 -4
View File
@@ -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