mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
5d00a93f521150b7dd7e26a700b12c4cf8ce4e79
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
5680f5d34b |
fix(scheduler): watchdogs no longer falsely treat FINISH->IDLE as "print landed" (#1370)
Both the queue-side _watchdog_print_start and the direct-dispatch _verify_print_response used `status.state != pre_state` to decide whether a project_file command had been accepted. When a printer was in FINISH at dispatch time (un-dismissed post-print prompt from a prior job), the firmware silently rejected the new command; if the user then dismissed the screen prompt, the printer moved FINISH -> IDLE and the watchdog returned early as "command landed" — leaving the queue row stuck at status='printing' indefinitely and the scheduler permanently marking the printer as busy. Narrow the "command landed" check in both verifiers to an allow-list of active-print states (PREPARE / SLICING / RUNNING / PAUSE). Inactive transitions (FINISH -> IDLE, etc.) no longer short-circuit the revert. The subtask_id-advance signal stays in place for H2D's slow FINISH -> PREPARE transition (#1078). Also wrap _watchdog_print_start's revert commit and printer_manager._persist_awaiting_plate_clear in run_with_retry so SQLite single-writer contention can't silently drop these writes. The revert path returns a tristate sentinel so the post-revert MQTT session-recovery logic only runs when we actually reverted (or the commit failed) — not when on_print_complete had already cleared the row, where a forced reconnect could break a healthy concurrent print. |
||
|
|
69b6b5a334 |
fix(#1150): skip MQTT reconnect on watchdog timeout when project_file landed
Background: P1P firmware can take ~135 s after a project_file MQTT publish
to actually start parsing the uploaded .3mf — gcode_state stays IDLE and
subtask_id doesn't advance until parse completes. The dispatch watchdogs
treated the missed transition as a #887/#936 half-broken session and called
force_reconnect_stale_session, which interrupts the printer's in-progress
parse and triggers 0500_4003 ("can't parse print file") on the printer side.
Both #1150 (slow parse) and #887/#936 (zombie session) look identical from
state and subtask_id alone — both have stale state and stale subtask_id with
fresh telemetry. The distinguishing signal is the printer's gcode_file
field: it updates in push_status when the project_file command actually
lands on the printer, but stays unchanged when the publish was silently
swallowed.
Both watchdogs (_verify_print_response in background_dispatch and
_watchdog_print_start in print_scheduler) now capture pre_gcode_file from
printer_manager.get_status() before sending the publish, then on timeout
compare it against the last good status seen during the poll loop. If the
file changed, the command landed → log a #1150 warning, skip the forced
reconnect to avoid 0500_4003 mid-parse. If unchanged, fall through to the
original force_reconnect_stale_session call so the half-broken-session
recovery is preserved exactly.
Caveat documented in code: in a retry-same-file slow-parse scenario the
gcode_file looks identical pre/post-publish, so the watchdog falls through
to the reconnect path and the user still hits 0500_4003 on that retry.
Accepted to avoid breaking the half-broken-session recovery, which is the
more impactful regression of the two.
The new pre_gcode_file kwarg has a default of None on both watchdog
functions, so any caller that doesn't pass it keeps the original
reconnect-on-timeout behavior verbatim.
4 new unit tests cover both watchdogs: skip on gcode_file change (#1150
fix), reconnect when unchanged (#936 protection preserved), skip when
pre=None and current is non-None (printer just connected), reconnect when
pre_gcode_file arg is omitted (backward-compat). All 439 existing
dispatch / scheduler / mqtt tests pass unchanged.
|
||
|
|
9d0418688c |
fix(#1134): propagate background-dispatch watchdog timeout as job failure
Follow-up to #1042. The post-dispatch watchdog _verify_print_response was fire-and-forget — it correctly detected when the printer never transitioned (HMS error pending, half-broken MQTT session, plate-clear gate, SD card fault) and force-reconnected the MQTT session, but the dispatch job had already been marked successful on the optimistic MQTT-publish-acknowledged path. The UI carried on showing "Print started successfully" while the printer sat idle. The watchdog now returns bool and is awaited inline by both call sites in _run_reprint_archive and _run_print_library_file. On False the call sites raise a RuntimeError carrying a user-actionable message ("Printer did not acknowledge print command — state still {pre_state}. Check the printer for a pending error...") which routes through the existing _run_active_job → _mark_job_finished(failed=True) → background_dispatch WS broadcast path. Library-file flow rolls back the freshly-created archive on timeout so no phantom row is left behind for a print that never started. The watchdog now also accepts subtask_id advancing past pre_subtask_id as a definitive "command landed" signal — same as the queue-side watchdog at print_scheduler.py:1992 — so slow H2D FINISH→PREPARE transitions (~50 s observed) don't false-fail when the printer has clearly accepted the project_file but is still in FINISH. Default timeout raised from 15 s to 90 s to match the queue-side watchdog and give the same headroom on both dispatch paths. Brief mid-window MQTT disconnects keep polling instead of immediately failing — matches what the queue watchdog already does and avoids false-failing on transient telemetry gaps. 11 new tests in test_background_dispatch_watchdog.py: state-change pickup, subtask_id-change pickup with state still FINISH, neither-changed timeout plus force_reconnect_stale_session call, pre_subtask_id=None backwards- compat, post-dispatch subtask_id=None not counting as a change, brief disconnect not short-circuiting the window, persistent disconnect for the full window returning False, default-timeout=90s contract, _run_reprint_archive raises RuntimeError with the captured pre-state args on watchdog False, _run_reprint_archive happy path doesn't rollback, _run_active_job marks the job failed with the message when _process_job raises RuntimeError. |