diff --git a/README.md b/README.md index ee5701d..ec5e655 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/camera.py b/camera.py index 43de183..271fc88 100644 --- a/camera.py +++ b/camera.py @@ -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: