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