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:
@ -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
|
||||
|
||||
59
camera.py
59
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:
|
||||
|
||||
Reference in New Issue
Block a user