"""Pin Logitech C920 exposure so 360p stays 30 fps. Aperture-priority + exposure_dynamic_framerate=1 lets the sensor sit at ~66 ms / gain 255 and drop to 15 fps. Manual 333 (0.1 ms units = 1/30 s) with dyn-fps off holds the gst 30 fps request. """ from __future__ import annotations import logging import os import subprocess log = logging.getLogger("cameras.uvc-exposure") # UVC: 1 = Manual Mode, 3 = Aperture Priority. Time unit is 0.1 ms. AUTO_EXPOSURE_MANUAL = 1 EXPOSURE_TIME_30FPS = 333 def pin_cmd( device: str, *, auto_exposure: int = AUTO_EXPOSURE_MANUAL, exposure_time: int = EXPOSURE_TIME_30FPS, dynamic_framerate: int = 0, ) -> list[str]: cmd = [ "v4l2-ctl", "-d", str(device), f"--set-ctrl=auto_exposure={int(auto_exposure)}", f"--set-ctrl=exposure_dynamic_framerate={int(dynamic_framerate)}", ] # Time is inactive under aperture-priority (3); setting it fails the whole pin. if int(auto_exposure) == AUTO_EXPOSURE_MANUAL: cmd.insert(4, f"--set-ctrl=exposure_time_absolute={int(exposure_time)}") return cmd def pin_from_env() -> tuple[int, int, int]: ae = int(os.environ.get("VIDEO_AUTO_EXPOSURE", str(AUTO_EXPOSURE_MANUAL))) exp = int(os.environ.get("VIDEO_EXPOSURE_TIME", str(EXPOSURE_TIME_30FPS))) dyn = int(os.environ.get("VIDEO_EXPOSURE_DYNAMIC", "0")) return ae, exp, dyn def pin_c920_exposure(device: str) -> None: ae, exp, dyn = pin_from_env() cmd = pin_cmd(device, auto_exposure=ae, exposure_time=exp, dynamic_framerate=dyn) try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=5) except OSError as exc: log.warning("pin %s failed: %s", device, exc) return if r.returncode: log.warning("pin %s rc=%s %s", device, r.returncode, (r.stderr or "").strip()) return log.info("pinned %s ae=%s time=%s dynfps=%s", device, ae, exp, dyn) def pin_devices(devices: list[str]) -> None: for dev in devices: pin_c920_exposure(dev)