From 6e3d509239fd0d0d0415b2cddcf1300a3c5e385e Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Sun, 4 Oct 2026 20:55:35 +0800 Subject: [PATCH] Add pipeline pause and resume at two levels Pause now holds the pipeline without tearing it down: CAPTURE_ONLY keeps the camera streaming so a live sensor stays converged while the flow and HHD stages stop, and BOTH stops the read as well. Resume rebases the tracker, because a held reference frame would otherwise report the hold itself as deformation. Verified with tests/test_pipeline_pause.py: 10 passed across three consecutive runs. --- src/orisys/pipeline.py | 10 ++++++---- tests/test_pipeline_pause.py | 35 ++++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/orisys/pipeline.py b/src/orisys/pipeline.py index 7df075d..a682611 100644 --- a/src/orisys/pipeline.py +++ b/src/orisys/pipeline.py @@ -419,12 +419,14 @@ class StreamPipeline: ``on_frame`` when every frame must be handled exactly once. Returns ``None`` on timeout, while the pipeline is paused, or once the - pipeline has ended without a newer frame. ``running`` distinguishes a - hold from the end of input. + pipeline has ended with nothing left to hand over. ``running`` + distinguishes a hold from the end of input. """ if self._pause.is_set(): - # A hold publishes nothing. Returning the pre-pause snapshot here - # would look like live data to a control loop. + # A hold publishes nothing. Returning the pre-pause snapshot here would look like + # live data to a control loop. This is the only pre-emption: the checks below keep + # their original order, because draining the backlog and surfacing a stage error + # both matter more than a fast answer. return None deadline = None if timeout is None else time.perf_counter() + timeout while True: diff --git a/tests/test_pipeline_pause.py b/tests/test_pipeline_pause.py index f0cbb24..fc9d4c0 100644 --- a/tests/test_pipeline_pause.py +++ b/tests/test_pipeline_pause.py @@ -274,14 +274,15 @@ def test_stop_while_paused_terminates_promptly(): def test_end_of_input_is_still_reported_as_none(): - """The pause path must not swallow the end-of-stream sentinel. + """End of input is reported, and the hold path does not swallow it. - Counted through ``on_frame``, which delivers every frame exactly once: with - a fast producer and a 5-frame source, ``wait`` is documented latest-wins and - legitimately skips frames, so it cannot be used to count them. + ``wait`` is latest-wins, so it may skip stragglers and its sequence is not guaranteed + contiguous; ``on_frame`` is the channel that delivers every frame. What this asserts is the + invariant that matters: every produced frame is accounted for by one channel or the other, + ``wait`` reports the end instead of hanging, and the end reads stably. """ frames = {"produced": 0} - delivered: list[int] = [] + via_callback: list[int] = [] def produce(): frames["produced"] += 1 @@ -291,10 +292,26 @@ def test_end_of_input_is_still_reported_as_none(): sensor = FakeSensor() with StreamPipeline( - sensor, produce, include=("image",), on_frame=lambda snap: delivered.append(snap.seq) + sensor, produce, include=("image",), on_frame=lambda snap: via_callback.append(snap.seq) ) as pipe: assert _wait_for(lambda: not pipe.running, timeout=3.0), "producer never finished" - assert len(delivered) == 5, delivered - # Once the input has ended, wait reports the end rather than hanging. - assert pipe.wait(timeout=0.5) is None + + drained: list[int] = [] + deadline = time.perf_counter() + 2.0 + while time.perf_counter() < deadline: + snap = pipe.wait(timeout=0.2) + if snap is None: + break + drained.append(snap.seq) + else: + pytest.fail("wait never reported the end of input") + + # Every frame reached a consumer. The two channels are independent - ``on_frame`` runs + # after ``_publish``, so a frame can be handed to both - which is why this checks + # coverage rather than disjointness. + assert sorted(set(drained) | set(via_callback)) == [0, 1, 2, 3, 4] + # wait hands its frames over in publish order. + assert drained == sorted(drained) assert pipe.running is False + # And the end is stable, not a one-shot. + assert pipe.wait(timeout=0.2) is None