mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
dev
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
72a8dafde9 |
y fix(camera): rotate every still exactly once, and cover the sources that let ffmpeg write the file
Review follow-ups on applying camera_rotation to finish photos and
layer-timelapse frames.
Rotating the frame popped from _stage22_finish_frames rotated one of its
sources twice. The cache has two kinds of feeder: live grabs, which are raw,
and the #1867 in-print bank, whose bytes come from
_capture_snapshot_for_notification and have already been rotated on the way
in. The consumer cannot tell them apart, so on the finish_state trigger - the
path the bank exists to serve, on firmware that never emits stg_cur=22 - a 180
degree rotation cancelled itself out and the photo was upside-down again,
which is the reported symptom exactly; 90 and 270 landed 180 out. Rotation now
happens where each frame is captured, so every entry in the cache carries one
rotation whatever produced it, and the invariant is stated both where the
cache is declared and where it is consumed.
Two finish-photo sources were still writing unrotated files: the built-in
camera's own capture_finish_photo, and the still extracted from a
printer-recorded timelapse - which is the *preferred* source for a built-in
camera print, so a user with a rotation set got a correctly oriented photo or
not depending on which source happened to win. Neither ever holds the frame as
bytes; ffmpeg writes the file and they return a filename. apply_camera_rotation_to_file
handles that case and is best-effort - a failed rotate leaves the unrotated
file rather than losing a delivered photo. The archived video itself is the
printer's own file and is not re-encoded, so it still plays at the camera's
native orientation; the CHANGELOG says so rather than leaving it to be
discovered.
apply_camera_rotation logs at debug, not info. It was on a path that runs once
per layer, where a tall print would have put hundreds of lines in the log for
something the surrounding capture already reports at debug.
The moved rotation logic had no test of its own - every existing test patches
it out and asserts the call, so a flipped sign or a dropped expand=True would
have shipped green. test_camera_rotation.py drives the real round trip: a
corner marker pins which way it turns, the dimensions pin that the frame is
not cropped, and an undecodable frame comes back by identity because a capture
path must not lose a frame to a failed rotate.
Tests for the fix itself sit on both sides of the cache. The producer half is
driven directly; the consumer half is a closure nested inside on_print_complete
with nothing able to reach it, so it is pinned by an AST guard - checked
against the source because the alternative is no check at all. Reverting
main.py to the pre-fix shape fails three of the five, the guard among them.
The three new tests used Path("/tmp/test") for a patched base_dir, which Bandit
flagged (B108); they take tmp_path now.
|
||
|
|
432e956eff |
fix(camera): rotate every still exactly once, and cover the sources that let ffmpeg write the file
Review follow-ups on applying camera_rotation to finish photos and layer-timelapse frames. Rotating the frame popped from _stage22_finish_frames rotated one of its sources twice. The cache has two kinds of feeder: live grabs, which are raw, and the #1867 in-print bank, whose bytes come from _capture_snapshot_for_notification and have already been rotated on the way in. The consumer cannot tell them apart, so on the finish_state trigger - the path the bank exists to serve, on firmware that never emits stg_cur=22 - a 180 degree rotation cancelled itself out and the photo was upside-down again, which is the reported symptom exactly; 90 and 270 landed 180 out. Rotation now happens where each frame is captured, so every entry in the cache carries one rotation whatever produced it, and the invariant is stated both where the cache is declared and where it is consumed. Two finish-photo sources were still writing unrotated files: the built-in camera's own capture_finish_photo, and the still extracted from a printer-recorded timelapse - which is the *preferred* source for a built-in camera print, so a user with a rotation set got a correctly oriented photo or not depending on which source happened to win. Neither ever holds the frame as bytes; ffmpeg writes the file and they return a filename. apply_camera_rotation_to_file handles that case and is best-effort - a failed rotate leaves the unrotated file rather than losing a delivered photo. The archived video itself is the printer's own file and is not re-encoded, so it still plays at the camera's native orientation; the CHANGELOG says so rather than leaving it to be discovered. apply_camera_rotation logs at debug, not info. It was on a path that runs once per layer, where a tall print would have put hundreds of lines in the log for something the surrounding capture already reports at debug. The moved rotation logic had no test of its own - every existing test patches it out and asserts the call, so a flipped sign or a dropped expand=True would have shipped green. test_camera_rotation.py drives the real round trip: a corner marker pins which way it turns, the dimensions pin that the frame is not cropped, and an undecodable frame comes back by identity because a capture path must not lose a frame to a failed rotate. Tests for the fix itself sit on both sides of the cache. The producer half is driven directly; the consumer half is a closure nested inside on_print_complete with nothing able to reach it, so it is pinned by an AST guard - checked against the source because the alternative is no check at all. Reverting main.py to the pre-fix shape fails three of the five, the guard among them. The three new tests used Path("/tmp/test") for a patched base_dir, which Bandit flagged (B108); they take tmp_path now. |
||
|
|
08c9ec6749 |
fix(camera): protect an in-progress stitch from the orphan sweep, and only sweep this feature's own files
Review follow-ups on the orphaned timelapse session cleanup. The sweep's own docstring said min_age_seconds made it safe to call mid-run. It did not. on_print_complete drops the session from _active_sessions before handing frames_dir to ffmpeg, so for the length of a stitch the directory matches no active session, and its mtime is the last layer's frame write - which on a tall print's final layer is easily older than the margin. The default margin is 300s and the stitch timeout is also 300s, so the two were tied with no headroom at all: a sweep landing in that window deleted ffmpeg's input from under it. _finalizing_sessions now covers the stitch, set as the session leaves _active_sessions and cleared in a finally so a failed stitch cannot leak the marker and make that printer's leftovers permanently un-sweepable. The docstring names all three guards and which gap each covers, including that the margin does have real headroom for the two cases it suits - a session mid-creation, and the freshly written .mp4 awaiting attach. The file branch now requires the timelapse_<session_id>.mp4 shape its own comment describes. It previously deleted any file under timelapse_frames/<printer_id>/ past the margin; nothing else writes there today, but age alone is not a reason to delete a file this feature did not create. Dropped ignore_errors=True from the rmtree. It made the surrounding except OSError unreachable, so a read-only mount or a permissions problem was counted and logged as a successful removal - and that log is the only evidence an operator has of what was deleted. Tests 5 -> 9: sparing a session mid-stitch, the finalizing marker cleared even when the stitch raises, unrelated files left alone, and a failed removal not counted. The failure test's rmtree stub honours the real contract and returns silently when ignore_errors=True, because that silent no-op is exactly what the old call could never observe; a stub that raised unconditionally would have passed against both versions and proved nothing. main.py is unchanged: it has no module-level logger, and the inline logging.getLogger(__name__) the sweep uses is the idiom throughout lifespan. |
||
|
|
5db4c75ca0 |
fix(camera): apply camera_rotation to layer-timelapse frames too
Same gap as the finish-photo fix: camera_rotation was only ever wired into the notification-snapshot path, so a layer-timelapse video came out upside-down whenever a rotation was configured - every frame (fresh or reused from the live view's buffer) was written to disk raw. Extracts the rotation logic out of main.py into a shared apply_camera_rotation(image_data, rotation, logger) in services/camera.py (taking the rotation value directly rather than a printer object, so both main.py's printer-shaped callers and layer_timelapse's plain int field can use it). main.py's _apply_camera_rotation becomes a thin compatibility wrapper so its existing call sites are unchanged. Threads a `rotation` field through TimelapseSession/start_session, set from printer.camera_rotation in _maybe_start_layer_timelapse, and applies it (via asyncio.to_thread, since PIL rotation is CPU-bound) in capture_layer before each frame is written - after #2707's live_frame_for_capture() resolves the frame, regardless of whether it came fresh or from the live view's buffer. Rebased onto #2707's landed implementation: capture_layer now calls live_frame_for_capture() instead of the older is_stream_active/ try_get_active_buffered_frame pair this was originally written against, so the layer-timelapse tests are updated to match, and the now-redundant TestCaptureLayerAvoidsCompetingWithLiveViewer class (superseded by #2707's own test_external_camera_live_frame_reuse.py) is dropped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
24863203fb |
fix(camera): sweep orphaned timelapse session directories on startup
_active_sessions is in-memory only, so a process restart mid-print loses track of any active layer-timelapse session without ever calling cancel_session()/cleanup() - the frames directory (and, if stitching had already produced output before the restart, a stray timelapse_<session_id>.mp4) are then orphaned on disk permanently, with no equivalent to the ffmpeg orphan janitor to reap them. Confirmed live: 38MB of exactly this leftover on the OrangePi after several restarts during this week's testing, including two corrupt 48-byte .mp4s from stitches that got interrupted mid-write. Adds cleanup_orphaned_timelapse_sessions(), run once at startup: for each printer_id under timelapse_frames/, remove any frame directory or stitched-output file that doesn't match that printer's current active session (if any) and is older than a defensive margin (5 min default). A restart-recovered print never gets a new timelapse session either (#1353's _maybe_start_layer_timelapse only fires on fresh PRINT_START events), so nothing orphaned here can ever be resumed - safe to always remove once it's old enough not to be a startup race. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
691fb133b7 |
Add external network camera support for printers
Add support for external network cameras (MJPEG, RTSP, HTTP snapshot) that replace a printer's built-in camera when configured. Features: - Live streaming on printers page (replaces built-in camera) - Finish photo capture from external camera on print complete - Layer-based timelapse: captures frame on each layer change, stitches to MP4 video on print completion Backend changes: - Add external_camera_url, external_camera_type, external_camera_enabled fields to Printer model with database migration - New external_camera.py service: MJPEG/RTSP/snapshot frame capture, connection testing, MJPEG stream generation - New layer_timelapse.py service: TimelapseSession management, layer-by-layer frame capture, ffmpeg video stitching - Add on_layer_change callback to MQTT client and printer manager - Update camera routes with external camera streaming and tracking - Update print lifecycle hooks for timelapse start/stitch/cancel - Add external camera fields to backup/restore - Rate limiting for external camera streams (prevents browser freeze) Frontend changes: - Add external camera configuration UI in Settings > Camera - Per-printer enable toggle, URL input, type selector, test button - Toast notification on save Closes #143 |