Add camera selection diagnostics

resolve_device now prints each by-id candidate it scans, which entry
matched, and the resolved device path; the no-match error lists all
by-id entries with their targets. Camera startup logs the raw config
setting, resolved path, the node's sysfs name, the OpenCV version, and
the negotiated resolution. 'python3 camera.py' now dumps every detected
camera before opening, to troubleshoot machines where the wrong camera
gets used.
This commit is contained in:
2026-07-18 15:21:32 -04:00
parent 8af59ac11f
commit 13b210935f
2 changed files with 62 additions and 3 deletions

View File

@ -109,6 +109,12 @@ Test the camera on its own (uses the device from `config.toml`):
python3 camera.py
```
It first prints a pre-flight dump of every camera the system sees
(`/dev/v4l/by-id/` names, device nodes, and what each node identifies as),
then reports which device was matched, resolved, and opened, plus the OpenCV
version. If the booth ever uses the wrong camera, run this and read the
output — it shows exactly where the selection went sideways.
**If it reports black frames** (or the live preview shows a warning): the
Macally cam's chipset sometimes wedges and streams pure black until it is
power-cycled. **Unplug the camera, plug it back in, and restart the

View File

@ -14,6 +14,16 @@ import cv2
import numpy as np
def sysfs_name(device_path):
"""Human-readable name of a /dev/videoN node from sysfs, e.g.
'Macally USB2.0Camera: Macally U'. Empty string if unknown."""
try:
with open(f"/sys/class/video4linux/{os.path.basename(device_path)}/name") as f:
return f.read().strip()
except OSError:
return ""
def resolve_device(setting, wait_seconds=15):
"""Turn the config's camera device setting into something cv2 accepts.
@ -33,16 +43,34 @@ def resolve_device(setting, wait_seconds=15):
needle = setting[len("auto-"):].lower()
deadline = time.monotonic() + wait_seconds
waited = False
last_seen = None
while True:
candidates = sorted(glob.glob("/dev/v4l/by-id/*-video-index0"))
# Re-print the scan only when the device set changes, so a slow
# USB camera showing up mid-wait doesn't spam the log.
if candidates != last_seen:
last_seen = candidates
if candidates:
print(f"Scanning /dev/v4l/by-id for '{needle}':")
for path in candidates:
print(f" {os.path.basename(path)} -> "
f"{os.path.realpath(path)}")
else:
print("Scanning /dev/v4l/by-id: no cameras found yet.")
for path in candidates:
if needle in os.path.basename(path).lower():
return os.path.realpath(path)
device = os.path.realpath(path)
print(f"Matched '{needle}': "
f"{os.path.basename(path)} -> {device}")
return device
if time.monotonic() > deadline:
names = [os.path.basename(p) for p in candidates]
all_entries = [
f"{os.path.basename(p)} -> {os.path.realpath(p)}"
for p in sorted(glob.glob("/dev/v4l/by-id/*"))
]
raise RuntimeError(
f"No camera matching '{needle}' found. "
f"Available cameras: {names or 'none'}"
f"Available cameras: {all_entries or 'none'}"
)
if not waited:
waited = True
@ -68,6 +96,12 @@ def crop_4x3(frame):
class Camera:
def __init__(self, device_setting, width, height):
device = resolve_device(device_setting)
dev_path = device if isinstance(device, str) else f"/dev/video{device}"
name = sysfs_name(dev_path)
print(f"Camera: setting={device_setting!r} resolved={dev_path!r} "
f"cv2={cv2.__version__}")
if name:
print(f"Camera: {dev_path} identifies as '{name}' (sysfs)")
self.cap = cv2.VideoCapture(device, cv2.CAP_V4L2)
if not self.cap.isOpened():
raise RuntimeError(f"Could not open camera {device!r}")
@ -75,6 +109,9 @@ class Camera:
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
actual_w = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_h = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"Camera: opened {dev_path}, negotiated {actual_w}x{actual_h}")
self._lock = threading.Lock()
self._frame = None
@ -140,6 +177,22 @@ if __name__ == "__main__":
import tomllib
from pathlib import Path
print("== Camera pre-flight ==")
print(f"OpenCV version: {cv2.__version__}")
by_id = sorted(glob.glob("/dev/v4l/by-id/*"))
if by_id:
print("/dev/v4l/by-id entries:")
for p in by_id:
print(f" {os.path.basename(p)} -> {os.path.realpath(p)}")
else:
print("/dev/v4l/by-id: (empty or missing)")
nodes = sorted(glob.glob("/dev/video*"))
if nodes:
print("Video device nodes:")
for n in nodes:
print(f" {n} {sysfs_name(n)}")
print("=======================")
if len(sys.argv) > 1:
device = sys.argv[1]
else: