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.
This commit is contained in:
ykcheungab
2026-10-04 20:55:35 +08:00
parent 333fa8fcff
commit 6e3d509239
2 changed files with 32 additions and 13 deletions
+6 -4
View File
@@ -419,12 +419,14 @@ class StreamPipeline:
``on_frame`` when every frame must be handled exactly once. ``on_frame`` when every frame must be handled exactly once.
Returns ``None`` on timeout, while the pipeline is paused, or once the Returns ``None`` on timeout, while the pipeline is paused, or once the
pipeline has ended without a newer frame. ``running`` distinguishes a pipeline has ended with nothing left to hand over. ``running``
hold from the end of input. distinguishes a hold from the end of input.
""" """
if self._pause.is_set(): if self._pause.is_set():
# A hold publishes nothing. Returning the pre-pause snapshot here # A hold publishes nothing. Returning the pre-pause snapshot here would look like
# would look like live data to a control loop. # 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 return None
deadline = None if timeout is None else time.perf_counter() + timeout deadline = None if timeout is None else time.perf_counter() + timeout
while True: while True:
+26 -9
View File
@@ -274,14 +274,15 @@ def test_stop_while_paused_terminates_promptly():
def test_end_of_input_is_still_reported_as_none(): 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 ``wait`` is latest-wins, so it may skip stragglers and its sequence is not guaranteed
a fast producer and a 5-frame source, ``wait`` is documented latest-wins and contiguous; ``on_frame`` is the channel that delivers every frame. What this asserts is the
legitimately skips frames, so it cannot be used to count them. 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} frames = {"produced": 0}
delivered: list[int] = [] via_callback: list[int] = []
def produce(): def produce():
frames["produced"] += 1 frames["produced"] += 1
@@ -291,10 +292,26 @@ def test_end_of_input_is_still_reported_as_none():
sensor = FakeSensor() sensor = FakeSensor()
with StreamPipeline( 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: ) as pipe:
assert _wait_for(lambda: not pipe.running, timeout=3.0), "producer never finished" 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. drained: list[int] = []
assert pipe.wait(timeout=0.5) is None 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 assert pipe.running is False
# And the end is stable, not a one-shot.
assert pipe.wait(timeout=0.2) is None