From 13b210935fae210b481deaa1c182154659385309 Mon Sep 17 00:00:00 2001 From: trav Date: Sat, 18 Jul 2026 15:21:32 -0400 Subject: [PATCH] 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. --- README.md | 6 ++++++ camera.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 3 deletions(-) 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: