build_runtimes() logs-and-skips a camera it cannot open, so a wrong /dev/video index or an unseated ribbon produced a green systemd unit that silently monitored one camera less. - edge/probe.py: opens each camera in CAMERAS, retries while the sensor settles, reports frame geometry, optionally saves preview PNGs, and exits non-zero with a backend-specific hint (v4l2-ctl / rpicam-hello) - setup_online.sh: new step [8/9] runs it against the installed package and the CAMERAS line actually written to edge.env, and refuses to enable the unit on failure. SKIP_CAMERA_CHECK=1 opts out. Tests: 174 pass, 2 skipped (7 new).
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
"""Camera probe (edge/probe.py) — all with fakes, no hardware."""
|
|
|
|
import pytest
|
|
|
|
from edge import probe
|
|
|
|
|
|
class FakeFrame:
|
|
def __init__(self, shape):
|
|
self.shape = shape
|
|
|
|
|
|
class FakeSource:
|
|
"""Yields `blanks` empty reads, then frames. Records that close() was called."""
|
|
|
|
def __init__(self, blanks=0, shape=(480, 640, 3)):
|
|
self._blanks = blanks
|
|
self._shape = shape
|
|
self.closed = False
|
|
|
|
def read(self):
|
|
if self._blanks > 0:
|
|
self._blanks -= 1
|
|
return False, None
|
|
return True, FakeFrame(self._shape)
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
|
|
def test_probe_reports_frame_geometry():
|
|
r = probe.probe_one("uvc", "0", "cam-02", build_source=lambda b, d: FakeSource())
|
|
assert r.ok
|
|
assert "640x480" in r.detail and "3 channel" in r.detail
|
|
|
|
|
|
def test_probe_retries_while_the_sensor_settles():
|
|
# A UVC camera commonly returns nothing for the first reads; that is not a failure.
|
|
src = FakeSource(blanks=probe._READ_ATTEMPTS - 1)
|
|
r = probe.probe_one("uvc", "0", "cam-02", build_source=lambda b, d: src)
|
|
assert r.ok
|
|
|
|
|
|
def test_probe_fails_when_no_frame_ever_arrives(monkeypatch):
|
|
monkeypatch.setattr(probe, "_READ_DELAY_S", 0)
|
|
src = FakeSource(blanks=probe._READ_ATTEMPTS)
|
|
r = probe.probe_one("csi", "0", "cam-01", build_source=lambda b, d: src)
|
|
assert not r.ok
|
|
assert "no frame" in r.detail
|
|
|
|
|
|
def test_probe_fails_when_the_device_cannot_be_opened():
|
|
def boom(backend, device):
|
|
raise RuntimeError("UVC device 0 failed to open")
|
|
|
|
r = probe.probe_one("uvc", "0", "cam-02", build_source=boom)
|
|
assert not r.ok
|
|
assert "open failed" in r.detail
|
|
|
|
|
|
def test_probe_closes_the_source_even_on_failure(monkeypatch):
|
|
monkeypatch.setattr(probe, "_READ_DELAY_S", 0)
|
|
src = FakeSource(blanks=probe._READ_ATTEMPTS)
|
|
probe.probe_one("uvc", "0", "cam-02", build_source=lambda b, d: src)
|
|
assert src.closed, "a probe that holds the device open would block the daemon start"
|
|
|
|
|
|
def test_probe_all_covers_every_configured_camera():
|
|
env = {"CAMERAS": "csi:0:cam-01,uvc:0:cam-02"}
|
|
results = probe.probe_all(env, build_source=lambda b, d: FakeSource())
|
|
assert [r.camera_id for r in results] == ["cam-01", "cam-02"]
|
|
assert [r.backend for r in results] == ["csi", "uvc"]
|
|
assert all(r.ok for r in results)
|
|
|
|
|
|
def test_probe_all_rejects_a_malformed_cameras_value():
|
|
with pytest.raises(ValueError):
|
|
probe.probe_all({"CAMERAS": "csi:0"}, build_source=lambda b, d: FakeSource())
|