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
+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():
"""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