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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user