diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b7f0370..287911dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to Bambuddy will be documented in this file. - **Library files now display the filename, not the embedded 3MF Title (#1489, reported by @needo37)** — File Manager cards, search and sort keyed off `file_metadata.print_name`, which `ThreeMFParser` lifts from the 3MF's ``. That title is the in-app project title — generic `"Exported 3D Model"` for any Bambu Studio "Save As", a marketing title for a MakerWorld download — and almost never the filename the user actually saved. So a card for `Whatever.3mf` showed `Exported 3D Model`, and the only way to correct it was a rename round-trip (the Rename dialog's Save button is disabled while the name is unchanged, so the user had to rename to a different name and back). The slicer-output write path already dropped `print_name` for exactly this reason; the **four** other write paths that store parsed 3MF metadata onto a `LibraryFile` did not — external-folder scan, managed multipart upload, the multi-file ZIP-upload branch, and MakerWorld import. **Fix**: a shared `_without_print_name()` helper strips `print_name` from library-file metadata, applied at all four import paths (and the slicer path switched to it, so there is one rule). A `LibraryFile`'s display name is its filename; only `PrintArchive` carries a real `print_name`, and that is untouched. The now-redundant filename→`print_name` mirroring in the rename route is removed. A one-time data migration (`_migrate_drop_library_print_name`, idempotent, SQLite `json_remove` / PostgreSQL `jsonb` key-removal branched on `is_sqlite()`) clears `print_name` from rows imported before the fix, so existing libraries correct themselves without the rename workaround. No frontend change — `print_name || filename` naturally yields the filename once `print_name` is gone. **Tests**: 6 new in `test_library_print_name.py` — `_without_print_name` (strips, keeps siblings, `None` pass-through, no-op identity return, no input mutation, print-name-only → `{}`) and the migration (clears `print_name`, leaves siblings and metadata-free rows alone, idempotent). 109 library + dialect tests green; the migration's PostgreSQL branch additionally ran live against real Postgres during the integration-test app boot. Backend ruff clean. - **Camera: ffmpeg's stderr is now captured when an RTSP stream stalls instead of only when ffmpeg crashes (#1395, reported by @Tschipel)** — A P2S support bundle taken on 0.2.5b1 (the per-model probesize fix already applied) showed the camera still failing: ffmpeg connects, stays alive 30+ seconds, emits zero JPEG bytes, the stream's 30 s `stdout.read` times out, reconnect loop repeats — but with **no ffmpeg stderr anywhere in the log** to say why. Root cause was a diagnostic bug, not the camera path: `_read_ffmpeg_stderr` called `process.stderr.read()` (read-to-EOF). A stalled-but-still-alive ffmpeg — exactly the P2S RTSP failure mode — never closes stderr, so the read blocked until the 2 s `wait_for` timeout and returned `None`, discarding the banner + stream-analysis lines ffmpeg had already printed. ffmpeg's stderr was therefore captured *only* when it fully exited; the earlier "not enough frames to estimate rate" smoking gun was available only because ffmpeg crashed back then, and once the probesize bump turned the crash into a hang the diagnostic went dark. **Fix**: `_read_ffmpeg_stderr` now drains stderr incrementally in bounded 8 KB chunks (64 KB cap), returning whatever ffmpeg has printed so far whether or not it has exited — so a hung stream is self-describing in the next support bundle. Additionally, `generate_rtsp_mjpeg_stream` now logs the resolved per-model `probesize` / `analyzeduration` on the info-level "Starting RTSP camera stream" line (verifiable without debug logging), and the debug-level ffmpeg-command line logs the full argv with only the credential-bearing camera URL redacted, instead of hiding the entire command. No behaviour change to streaming itself — this makes the still-unresolved P2S RTSP stall diagnosable. **Tests**: 4 new in `test_camera_stderr_summary.py` cover `_read_ffmpeg_stderr` capturing output from a *running* (un-exited, no-EOF) ffmpeg — the regression — as well as the exited case, the no-stderr-pipe case, and banner-only output summarizing to `None`. 9 camera-stderr tests green; backend ruff clean. - **Camera diagnostic (stethoscope) was missing from the pop-out camera window (#1395, reported by @Tschipel)** — The #1395 camera-diagnostic follow-up — stethoscope icon in the control bar, **Diagnose** button in the stream-error state, `CameraDiagnoseModal` — shipped wired into `EmbeddedCameraViewer.tsx` only, the *embedded* camera mode. It was never added to `CameraPage.tsx`, the standalone window that opens at `/camera/{id}` when `camera_view_mode` is `window` (the default). The reporter's support bundle had `"camera_view_mode": "window"`, so they were on `CameraPage` the whole time and genuinely could not see the stethoscope no matter how many container rebuilds or cache clears they tried — the JS bundle did contain the `camera.diagnose` strings (they come from `EmbeddedCameraViewer`), but that component never renders in window mode. Switching to overlay mode made it appear instantly, exactly as the reporter found. **Fix**: ported the diagnostic into `CameraPage.tsx` — the `Stethoscope` control-bar button (between **Refresh** and **Fullscreen**, matching the embedded viewer), a **Diagnose** button next to **Retry** in the `streamError` block, and the `CameraDiagnoseModal` render. No new i18n keys — `camera.diagnose.*` already exist in all 9 locales. The backend per-model camera-profile fix from the same issue is view-mode-agnostic and already applied; this only makes the diagnostic reachable in the default window mode. Frontend build clean. +- **Camera: P2S RTSP stream no longer drops every frame after the first (#1395, reported by @Tschipel)** — With the stderr-capture diagnostic fix in place, a fresh P2S support bundle finally showed ffmpeg's reason for the stall: `frame=1 time=00:00:00.06 dup=0 drop=526 speed=0.0037x`. ffmpeg connects fine and frames *are* arriving (the `drop` counter climbs steadily, ~15/s) — but it emits exactly one output frame and the output clock freezes at 0.06 s. **Root cause**: the streaming ffmpeg command ends with `-r 15`, which puts ffmpeg in CFR (constant-frame-rate) mode — it drops/dupes input frames to hit 15 fps *based on the source's timestamps*. P2S firmware 01.02.00.00 sends an RTSP stream whose RTP timestamps don't advance (every frame is stamped ~0.06 s), so CFR sees every frame after the first as a same-timestamp duplicate and drops it. The browser gets one frame, then nothing → "connection lost", reconnect, repeat. This is why snapshot capture works on the same printer (that path has no `-r`, so no CFR conversion — timestamps are irrelevant) and why X1/H2 are unaffected (their firmware sends correct, advancing timestamps). The earlier "increase probesize" fix was real but had been masking this second bug — once ffmpeg got past the probe, the timestamp bug surfaced. **Fix**: the P2S camera profile gains `-use_wallclock_as_timestamps 1` as an ffmpeg input arg (via the existing `extra_ffmpeg_input_args` hook — no dataclass change, no other model touched). ffmpeg then rebuilds each packet's PTS from arrival wall-clock time, the output clock advances normally, and `-r 15` CFR conversion works as intended. **Tests**: 2 new in `test_camera_profiles.py` — the P2S profile splices the flag and value as an adjacent pair, and the default profile keeps an empty `extra_ffmpeg_input_args` so the override never leaks to X1/H2. 13 camera-profile tests green; backend ruff clean. - **A backend restart mid-print no longer duplicates the job in the archive (#1485, reported by @pwostran)** — When the server running Bambuddy restarted during an active print, the running job was duplicated in the archive — and deleting the duplicate didn't help: every subsequent restart while the print was still running spawned a fresh one. Both support bundles confirmed it: `WARNING Found stale 'printing' archive 3 (age: 9:46:23), marking as cancelled and creating new archive` → `Created archive 4`. On reconnect `on_print_start` fires (Bambuddy sees the printer running) and tries to re-attach to the existing archive in `main.py`. The reliable match is by `subtask_id`; the fallback is a name match plus — and this was the bug — a **4-hour staleness heuristic**: a name-matched `printing` archive older than 4h was assumed dead, marked `cancelled`, and a new archive created. Bambu prints routinely run far longer than 4h, so a genuine long print's *live* archive was destroyed and duplicated on every restart. **Two root causes, both fixed.** **(1) Queue/scheduled archives never persisted a restart-stable `subtask_id`.** Bambuddy mints a per-job id (`project_id`/`subtask_id`/`task_id`) inside `start_print` when it sends the `project_file` command, and the printer echoes it back — but often not within the ~10s before `on_print_start` first fires, so the expected-print branch's `if subtask_id and not archive.subtask_id` write got an empty value and the archive was left with no id. A later restart then had nothing to match on and fell through to the fragile name path. Fix: `BambuMQTTClient.start_print` now records the minted id on `last_dispatch_subtask_id`, and `on_print_start` falls back to it when the printer hasn't echoed `subtask_id` yet — so every dispatched archive persists a stable id and a restart resumes it by id, age-independent. **(2) The 4-hour cutoff itself.** Replaced with a progress-aware check: when a name-matched `printing` archive is found on restart, the printer's *current* reported progress decides resume-vs-stale, not wall-clock age. Real progress (or unknown progress — printer offline) always resumes the existing archive. It is only treated as a stale leftover when the printer clearly shows a *different, freshly-started* print — under 1% progress on an archive more than 2h old, a state a real in-progress print is never in. The arbitrary 4h constant is gone. **Net effect**: a restart mid-print resumes the existing archive (`started_at`, energy, timelapse intact) instead of ever cancelling it and creating a duplicate. **Tests**: 2 new in `test_bambu_mqtt.py` (`start_print` records `last_dispatch_subtask_id`, and updates it per submission); new `TestStaleVsResume` in `test_subtask_archive_resume.py` — 6 cases pinning the progress-aware decision (long print mid-run resumes; barely-started long print resumes; ~0% + old archive is stale; ~0% + young archive resumes; unknown progress never cancels; the sub-1%/2h boundary). 472 print-start / MQTT / scheduler / dispatch tests green; backend ruff clean. - **File Manager no longer polls the printer over FTPS every 30 seconds while open (#1480, reported by @OscarsWorldTech)** — The reporter's P1S churned through MQTT disconnect/reconnect cycles and timelapse downloads silently failed. The support bundle showed the real picture: during the churn windows, MQTT (`Connection stale - no message for 60.2s`), FTPS (`_ssl.c:1015: The handshake operation timed out`) and the camera all timed out *together* and recovered together — the P1S's embedded controller saturating, not a network fault (wifi -44 dBm, Docker host networking). A visible contributor on Bambuddy's side: `FileManagerModal.tsx` ran its `getPrinterFiles` query with `refetchInterval: 30000`, so every 30 s while the File Manager modal sat open it opened a *fresh* FTPS connection — full TLS handshake — to re-list the current directory. A printer's file list doesn't change on its own; it only changes on upload / delete (the modal's mutations already `invalidateQueries`) or when a print finishes. The blind 30 s poll was pure load, and on a fragile controller like the P1S it was enough to tip MQTT and FTP into the timeouts above. **Fix**: the `refetchInterval` is removed. The listing still refreshes on modal open, on directory / tab change (the path is in the query key), after every upload / delete, and via the existing manual Refresh button — so nothing stops updating, the printer just isn't hammered. Reduces steady-state FTPS connection load while the modal is open from one handshake every 30 s to zero. 19 FileManagerModal tests green; frontend build clean. - **STL thumbnail generation failures now log a full traceback** — Surfaced by the #1480 support bundle: every STL in the reporter's library failed thumbnail generation with `unsupported operand type(s) for /: 'str' and 'str'`, but `generate_stl_thumbnail`'s `except` handler logged only the bare exception message — no traceback, no line number. The fault could not be reproduced from a clean STL across path shapes (`#` and spaces in the path), `str` vs `Path` arguments, or large meshes that exercise `simplify_quadric_decimation`, so it is data- or environment-specific and the message alone is not enough to locate it. `stl_thumbnail.py` now passes `exc_info=True` on that warning, so the next support bundle carries the traceback and the exact failing line. No behaviour change to thumbnail generation itself. diff --git a/backend/app/services/camera_profiles.py b/backend/app/services/camera_profiles.py index 98e771188..3834e24e4 100644 --- a/backend/app/services/camera_profiles.py +++ b/backend/app/services/camera_profiles.py @@ -77,13 +77,24 @@ DEFAULT_PROFILE = CameraProfile() # AFTER alias normalisation, so internal SSDP codes ("N7") resolve via # ``_MODEL_ALIASES`` below. _PROFILES: dict[str, CameraProfile] = { - # P2S firmware 01.02.00.00 RTSP keyframe pacing is slow enough that - # ffmpeg's "32-byte probe + zero analyze" combo can't estimate the - # frame rate. ffmpeg's own stderr literally says "consider increasing - # probesize" (#1395 follow-up). + # P2S firmware 01.02.00.00 has two RTSP quirks, both surfaced by #1395: + # + # 1. Slow keyframe pacing — ffmpeg's "32-byte probe + zero analyze" + # combo can't estimate the frame rate ("consider increasing + # probesize"). Fixed by the relaxed probesize/analyzeduration below. + # + # 2. Non-advancing RTP timestamps — every frame is stamped at ~t=0.06s. + # With ffmpeg's default CFR rate conversion (`-r 15`), this freezes + # the output clock after the first frame and drops every subsequent + # frame as a same-timestamp duplicate (ffmpeg stderr: `frame=1 + # time=00:00:00.06 dup=0 drop=526`). `-use_wallclock_as_timestamps 1` + # regenerates each packet's PTS from arrival wall-clock time, so the + # output clock advances and CFR conversion works. X1/H2 send correct + # timestamps and need no override. "P2S": CameraProfile( probesize=1_000_000, analyzeduration=500_000, + extra_ffmpeg_input_args=("-use_wallclock_as_timestamps", "1"), ), } diff --git a/backend/tests/unit/services/test_camera_profiles.py b/backend/tests/unit/services/test_camera_profiles.py index a9f764eee..4969e8386 100644 --- a/backend/tests/unit/services/test_camera_profiles.py +++ b/backend/tests/unit/services/test_camera_profiles.py @@ -50,6 +50,24 @@ class TestGetCameraProfile: assert profile.probesize >= 1_000_000 assert profile.analyzeduration >= 500_000 + def test_p2s_regenerates_timestamps_from_wallclock(self): + """P2S firmware 01.02.00.00 sends non-advancing RTP timestamps; + ffmpeg's default CFR conversion (`-r 15`) then freezes the output + clock after frame 1 and drops everything else (#1395). The profile + must splice `-use_wallclock_as_timestamps 1` into the input args so + ffmpeg rebuilds PTS from arrival time. Order matters — the flag and + its value must be adjacent so they reach ffmpeg as a pair.""" + args = get_camera_profile("P2S").extra_ffmpeg_input_args + assert "-use_wallclock_as_timestamps" in args + idx = args.index("-use_wallclock_as_timestamps") + assert args[idx + 1] == "1" + + def test_default_profile_has_no_timestamp_override(self): + """X1/H2 send correct, advancing RTP timestamps — they must NOT get + the wallclock override, which would needlessly re-stamp a healthy + stream. Guards against the P2S fix leaking into the default.""" + assert DEFAULT_PROFILE.extra_ffmpeg_input_args == () + def test_p2s_internal_code_resolves_to_p2s_profile(self): """SSDP internal codes (e.g. `N7` for P2S) must resolve to the same profile as their display name. Otherwise printers freshly