diff --git a/README.md b/README.md index c5f3a35..c3a2301 100644 --- a/README.md +++ b/README.md @@ -87,35 +87,37 @@ Getting information about the device: Format(width=640, height=480, pixelformat=} >>> for ctrl in cam.controls.values(): print(ctrl) - - - - - - - - - - - - - -``` + + + + + + + + + + + + + >>> cam.controls["saturation"] - + + >>> cam.controls["saturation"].id 9963778 >>> cam.controls[9963778] - + >>> cam.controls.brightness - ->>> cam.controls.brightness.value = 128 + +>>> cam.controls.brightness.value = 64 >>> cam.controls.brightness - + ``` +(see also [v4l2py-ctl](examples/v4l2py-ctl.py) example) + ### asyncio v4l2py is asyncio friendly: @@ -222,6 +224,97 @@ with Device.from_id(0) as cam: buff = io.BytesIO(frame) ``` +## Improved device controls + +Device controls have been improved to provide a more pythonic interface. The +new interface is the default now; however, the legacy interface can be +requested: `Device.from_id(x, legacy_controls=True)`. + +Before: +```python +>>> from v4l2py import Device +>>> cam = Device.from_id(0) +>>> cam.open() +>>> for ctrl in cam.controls.values(): +... print(ctrl) +... for item in ctrl.menu.values(): +... print(f" - {item.index}: {item.name}") + + + + + + + + + - 0: Disabled + - 1: 50 Hz + - 2: 60 Hz + + + + + - 1: Manual Mode + - 3: Aperture Priority Mode + + + +>>> type(cam.controls.exposure_dynamic_framerate.value) + +``` + +Now: +```python +>>> from v4l2py.device import Device, MenuControl +>>> cam = Device.from_id(0) +>>> cam.open() +>>> for ctrl in cam.controls.values(): +... print(ctrl) +... if isinstance(ctrl, MenuControl): +... for (index, name) in ctrl.items(): +... print(f" - {index}: {name}") + + + + + + + + + - 0: Disabled + - 1: 50 Hz + - 2: 60 Hz + + + + + - 1: Manual Mode + - 3: Aperture Priority Mode + + + +>>> type(cam.controls.white_balance_automatic.value) + +>>> cam.controls.white_balance_automatic.value + +>>> cam.controls.white_balance_automatic.value = False + + +>>> wba = cam.controls.white_balance_automatic +>>> wba.value = "enable" # or "on", "1", "true", "yes" +>>> wba + +>>> wba.value = "off" # or "disable", "0", "false", "no" +>>> wba + +``` + +The initial upgrade path for existing code is to request the legacy interface +by passing `legacy_controls=True` when instantiating the `Device` object, use +`LegacyControl` instead of `Control` for instantiations, and `BaseControl` +for isinstance() checks. And in the unlikely case your code does isinstance() +checks for `MenuItem`, these should be changed to `LegacyMenuItem`. + ## References See the ``linux/videodev2.h`` header file for details. diff --git a/examples/qt/widget.py b/examples/qt/widget.py index b11ff09..b72e529 100644 --- a/examples/qt/widget.py +++ b/examples/qt/widget.py @@ -4,8 +4,11 @@ # Copyright (c) 2021 Tiago Coutinho # Distributed under the GPLv3 license. See LICENSE for more info. -# install requirements: pip install cv2 qtpy pyqt6 -# run with: QT_API=pyqt6 python widget.py +# install extra requirements: +# python3 -m pip install opencv-python qtpy pyqt6 + +# run from this directory with: +# QT_API=pyqt6 python widget.py import cv2 from qtpy import QtCore, QtGui, QtWidgets diff --git a/examples/v4l2py-ctl.py b/examples/v4l2py-ctl.py new file mode 100644 index 0000000..9bbcb09 --- /dev/null +++ b/examples/v4l2py-ctl.py @@ -0,0 +1,200 @@ +import argparse + +from v4l2py.device import Device, MenuControl, LegacyControl + + +def _get_ctrl(cam, control): + if control.isdigit() or control.startswith("0x"): + _ctrl = int(control, 0) + else: + _ctrl = control + + try: + ctrl = cam.controls[_ctrl] + except KeyError: + return None + else: + return ctrl + + +def show_control_status(device: str, legacy_controls: bool) -> None: + with Device(device, legacy_controls=legacy_controls) as cam: + print("Showing current status of all controls ...\n") + print(f"*** {cam.info.card} ***") + + for cc in cam.controls.used_classes(): + print(f"\n{cc.name.title()} Controls\n") + + for ctrl in cam.controls.with_class(cc): + print("0x%08x:" % ctrl.id, ctrl) + if isinstance(ctrl, MenuControl): + for key, value in ctrl.items(): + print(11 * " ", f" +-- {key}: {value}") + elif isinstance(ctrl, LegacyControl): + for item in ctrl.menu.values(): + print(11 * " ", f" +-- {item}") + print("") + + +def get_controls(device: str, controls: list, legacy_controls: bool) -> None: + with Device(device, legacy_controls=legacy_controls) as cam: + print("Showing current value of given controls ...\n") + + for control in controls: + ctrl = _get_ctrl(cam, control) + if not ctrl: + print(f"{control}: unknown control") + continue + + if not ctrl.is_flagged_write_only: + print(f"{control} = {ctrl.value}") + else: + print(f"{control} is write-only, thus cannot be read") + print("") + + +def set_controls( + device: str, controls: list, legacy_controls: bool, clipping: bool +) -> None: + controls = ( + (ctrl.strip(), value.strip()) + for (ctrl, value) in (c.split("=") for c in controls) + ) + + with Device(device, legacy_controls=legacy_controls) as cam: + print("Changing value of given controls ...\n") + + cam.controls.set_clipping(clipping) + for control, value_new in controls: + ctrl = _get_ctrl(cam, control) + if not ctrl: + print(f"{control}: unknown control") + continue + + if not ctrl.is_flagged_write_only: + value_old = ctrl.value + else: + value_old = "(write-only)" + + try: + ctrl.value = value_new + except Exception as err: + success = False + reason = f"{err}" + else: + success = True + + result = "%-5s" % ("OK" if success else "ERROR") + + if success: + print(f"{result} {control}: {value_old} -> {value_new}\n") + else: + print( + f"{result} {control}: {value_old} -> {value_new}\n{result} {reason}\n" + ) + + +def reset_controls(device: str, controls: list, legacy_controls: bool) -> None: + with Device(device, legacy_controls=legacy_controls) as cam: + print("Resetting given controls to default ...\n") + + for control in controls: + ctrl = _get_ctrl(cam, control) + if not ctrl: + print(f"{control}: unknown control") + continue + + try: + ctrl.set_to_default() + except Exception as err: + success = False + reason = f"{err}" + else: + success = True + + result = "%-5s" % ("OK" if success else "ERROR") + + if success: + print(f"{result} {control} reset to {ctrl.default}\n") + else: + print(f"{result} {control}:\n{result} {reason}\n") + + +def reset_all_controls(device: str, legacy_controls: bool) -> None: + with Device(device, legacy_controls=legacy_controls) as cam: + print("Resetting all controls to default ...\n") + cam.controls.set_to_default() + + +def csv(string: str) -> list: + return [v.strip() for v in string.split(",")] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--legacy", + default=False, + action="store_true", + help="use legacy controls (default: %(default)s)", + ) + parser.add_argument( + "--clipping", + default=False, + action="store_true", + help="when changing numeric controls, enforce the written value to be within allowed range (default: %(default)s)", + ) + parser.add_argument( + "--device", + type=str, + default="0", + metavar="", + help="use device instead of /dev/video0; if starts with a digit, then /dev/video is used", + ) + parser.add_argument( + "--get-ctrl", + type=csv, + default=[], + metavar="[,...]", + help="get the values of the specified controls", + ) + parser.add_argument( + "--set-ctrl", + type=csv, + default=[], + metavar="=[,=...]", + help="set the values of the specified controls", + ) + parser.add_argument( + "--reset-ctrl", + type=csv, + default=[], + metavar="[,...]", + help="reset the specified controls to their default values", + ) + parser.add_argument( + "--reset-all", + default=False, + action="store_true", + help="reset all controls to their default value", + ) + + args = parser.parse_args() + + if args.device.isdigit(): + dev = f"/dev/video{args.device}" + else: + dev = args.device + + if args.reset_all: + reset_all_controls(dev, args.legacy) + elif args.reset_ctrl: + reset_controls(dev, args.reset_ctrl, args.legacy) + elif args.get_ctrl: + get_controls(dev, args.get_ctrl, args.legacy) + elif args.set_ctrl: + set_controls(dev, args.set_ctrl, args.legacy, args.clipping) + else: + show_control_status(dev, args.legacy) + + print("Done.") diff --git a/examples/web.py b/examples/web.py index 4fc9a5b..4b6cd76 100644 --- a/examples/web.py +++ b/examples/web.py @@ -4,6 +4,9 @@ # Copyright (c) 2021 Tiago Coutinho # Distributed under the GPLv3 license. See LICENSE for more info. +# Extra dependency required to run this example: +# python3 -m pip install flask + # run from this directory with: FLASK_APP=web flask run -h 0.0.0.0 import flask diff --git a/examples/web/async.py b/examples/web/async.py index cac586e..e0cd1fe 100644 --- a/examples/web/async.py +++ b/examples/web/async.py @@ -5,7 +5,8 @@ # Distributed under the GPLv3 license. See LICENSE for more info. # Extra dependencies required to run this example: -# pip install fastapi jinja2 python-multipart cv2 pillow uvicorn +# python3 -m pip install fastapi jinja2 python-multipart opencv-python \ +# pillow uvicorn # run from this directory with: # uvicorn async:app @@ -88,7 +89,7 @@ def cameras() -> list[Camera]: global CAMERAS if CAMERAS is None: cameras = {} - for device in iter_video_capture_devices(): + for device in iter_video_capture_devices(legacy_controls=True): cameras[device.index] = Camera(device) CAMERAS = cameras return CAMERAS diff --git a/examples/web/common.py b/examples/web/common.py index 9e87cfc..2245995 100644 --- a/examples/web/common.py +++ b/examples/web/common.py @@ -4,12 +4,6 @@ # Copyright (c) 2023 Tiago Coutinho # Distributed under the GPLv3 license. See LICENSE for more info. -# Extra dependencies required to run this example: -# pip install fastapi jinja2 python-multipart cv2 pillow uvicorn - -# run from this directory with: -# uvicorn async:app - """Common tools for async and sync web app examples""" import io @@ -35,22 +29,21 @@ def __init__(self, device: Device) -> None: def frame_to_image(frame, output="jpeg"): - match frame.pixel_format: - case PixelFormat.JPEG | PixelFormat.MJPEG: - if output == "jpeg": - return to_image_send(frame.data, type=output) - else: - buff = io.BytesIO() - image = PIL.Image.open(io.BytesIO(frame.data)) - case PixelFormat.GREY: - data = frame.array - data.shape = frame.height, frame.width, -1 - image = PIL.Image.frombuffer("L", (frame.width, frame.height), data) - case PixelFormat.YUYV: - data = frame.array - data.shape = frame.height, frame.width, -1 - rgb = cv2.cvtColor(data, cv2.COLOR_YUV2RGB_YUYV) - image = PIL.Image.fromarray(rgb) + if frame.pixel_format in (PixelFormat.JPEG, PixelFormat.MJPEG): + if output == "jpeg": + return to_image_send(frame.data, type=output) + else: + buff = io.BytesIO() + image = PIL.Image.open(io.BytesIO(frame.data)) + elif frame.pixel_format == PixelFormat.GREY: + data = frame.array + data.shape = frame.height, frame.width, -1 + image = PIL.Image.frombuffer("L", (frame.width, frame.height), data) + elif frame.pixel_format == PixelFormat.YUYV: + data = frame.array + data.shape = frame.height, frame.width, -1 + rgb = cv2.cvtColor(data, cv2.COLOR_YUV2RGB_YUYV) + image = PIL.Image.fromarray(rgb) buff = io.BytesIO() image.save(buff, output) diff --git a/examples/web/sync.py b/examples/web/sync.py index bbe1601..72a5ef0 100644 --- a/examples/web/sync.py +++ b/examples/web/sync.py @@ -5,7 +5,7 @@ # Distributed under the GPLv3 license. See LICENSE for more info. # Extra dependencies required to run this example: -# pip install pillow cv2 flask gunicorn gevent +# python3 -m pip install pillow opencv-python flask gunicorn gevent # run from this directory with: # gunicorn --bind=0.0.0.0:8000 --log-level=debug --worker-class=gevent sync:app diff --git a/examples/web_async.py b/examples/web_async.py index e7420db..cdf6c49 100644 --- a/examples/web_async.py +++ b/examples/web_async.py @@ -5,7 +5,7 @@ # Distributed under the GPLv3 license. See LICENSE for more info. # install dependencies with: -# pip install uvicorn fastapi +# python3 -m pip install uvicorn fastapi # # run from this directory with: # uvicorn web_async:app diff --git a/v4l2py/device.py b/v4l2py/device.py index e76834e..a897317 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -18,6 +18,7 @@ import pathlib import typing from io import IOBase +from collections import UserDict from . import raw from .io import IO, fopen @@ -306,9 +307,9 @@ def iter_read_menu(fd, ctrl): fd, IOC.QUERYMENU, qmenu, - start=ctrl.info.minimum, - stop=ctrl.info.maximum + 1, - step=ctrl.info.step, + start=ctrl._info.minimum, + stop=ctrl._info.maximum + 1, + step=ctrl._info.step, ignore_einval=True, ): yield copy.deepcopy(menu) @@ -635,10 +636,10 @@ def __exit__(self, *exc): class Device(ReentrantContextManager): - def __init__(self, name_or_file, read_write=True, io=IO): + def __init__(self, name_or_file, read_write=True, io=IO, legacy_controls=False): super().__init__() self.info = None - self.controls = Controls() + self.controls = None self.io = io if isinstance(name_or_file, (str, pathlib.Path)): filename = pathlib.Path(name_or_file) @@ -658,6 +659,7 @@ def __init__(self, name_or_file, read_write=True, io=IO): self.log = log.getChild(filename.stem) self.filename = filename self.index = device_number(filename) + self.legacy_controls = legacy_controls def __repr__(self): return f"<{type(self).__name__} name={self.filename}, closed={self.closed}>" @@ -677,9 +679,10 @@ def from_id(cls, did: int, **kwargs): def _init(self): self.info = read_info(self.fileno()) - self.controls = Controls( - {ctrl.id: Control(self, ctrl) for ctrl in self.info.controls} - ) + if self.legacy_controls: + self.controls = LegacyControls.from_device(self) + else: + self.controls = Controls.from_device(self) def open(self): if not self._fobj: @@ -782,6 +785,27 @@ def write(self, data: bytes) -> None: class Controls(dict): + @classmethod + def from_device(cls, device): + ctrl_type_map = { + ControlType.BOOLEAN: BooleanControl, + ControlType.INTEGER: IntegerControl, + ControlType.INTEGER64: Integer64Control, + ControlType.MENU: MenuControl, + ControlType.INTEGER_MENU: MenuControl, + ControlType.U8: U8Control, + ControlType.U16: U16Control, + ControlType.U32: U32Control, + } + ctrl_dict = dict() + + for ctrl in device.info.controls: + ctrl_type = ControlType(ctrl.type) + ctrl_class = ctrl_type_map.get(ctrl_type, GenericControl) + ctrl_dict[ctrl.id] = ctrl_class(device, ctrl) + + return cls(ctrl_dict) + def __getattr__(self, key): try: return self[key] @@ -802,12 +826,12 @@ def __delattr__(self, key): def __missing__(self, key): for v in self.values(): - if isinstance(v, Control) and (v.config_name == key): + if isinstance(v, BaseControl) and (v.config_name == key): return v raise KeyError(key) def used_classes(self): - return {v.control_class for v in self.values() if isinstance(v, Control)} + return {v.control_class for v in self.values() if isinstance(v, BaseControl)} def with_class(self, control_class): if isinstance(control_class, ControlClass): @@ -823,12 +847,12 @@ def with_class(self, control_class): ) for v in self.values(): - if isinstance(v, Control) and (v.control_class == control_class): + if isinstance(v, BaseControl) and (v.control_class == control_class): yield v def set_to_default(self): for v in self.values(): - if not isinstance(v, Control): + if not isinstance(v, BaseControl): continue try: @@ -836,61 +860,79 @@ def set_to_default(self): except AttributeError: pass - -class MenuItem: - def __init__(self, item): - self.item = item - self.index = item.index - self.name = item.name.decode() - - def __repr__(self): - return f"<{type(self).__name__} index={self.index} name={self.name}>" + def set_clipping(self, clipping: bool) -> None: + for v in self.values(): + if isinstance(v, BaseNumericControl): + v.clipping = clipping -def config_name(name: str) -> str: - res = name.lower() - for r in (", ", " "): - res = res.replace(r, "_") - return res +class LegacyControls(Controls): + @classmethod + def from_device(cls, device): + ctrl_dict = dict() + for ctrl in device.info.controls: + ctrl_dict[ctrl.id] = LegacyControl(device, ctrl) + return cls(ctrl_dict) -class Control: +class BaseControl: def __init__(self, device, info): self.device = device - self.info = info - self.id = self.info.id - self.name = info.name.decode() + self._info = info + self.id = self._info.id + self.name = self._info.name.decode() self._config_name = None self.control_class = ControlClass(raw.V4L2_CTRL_ID2CLASS(self.id)) - self.type = ControlType(self.info.type) + self.type = ControlType(self._info.type) + try: self.standard = ControlID(self.id) except ValueError: self.standard = None - if self.type == ControlType.MENU: - self.menu = { - menu.index: MenuItem(menu) - for menu in iter_read_menu(self.device._fobj, self) - } - else: - self.menu = {} def __repr__(self): - repr = f"{self.config_name} type={self.type.name.lower()}" - if self.type != ControlType.BOOLEAN: - repr += f" min={self.info.minimum} max={self.info.maximum} step={self.info.step}" - repr += f" default={self.info.default_value}" - if not (self.info.flags & ControlFlag.WRITE_ONLY): - repr += f" value={self.value}" + repr = f"{self.config_name}" + + addrepr = self._get_repr() + addrepr = addrepr.strip() + if addrepr: + repr += f" {addrepr}" - flags = [flag.name.lower() for flag in ControlFlag if (self.info.flags & flag)] - if len(flags): + flags = [ + flag.name.lower() + for flag in ControlFlag + if ((self._info.flags & flag) == flag) + ] + if flags: repr += " flags=" + ",".join(flags) return f"<{type(self).__name__} {repr}>" + def _get_repr(self) -> str: + return "" + + def _get_control(self): + value = get_control(self.device, self.id) + return value + + def _set_control(self, value): + if not self.is_writeable: + reasons = [] + if self.is_flagged_read_only: + reasons.append("read-only") + if self.is_flagged_inactive: + reasons.append("inactive") + if self.is_flagged_disabled: + reasons.append("disabled") + if self.is_flagged_grabbed: + reasons.append("grabbed") + raise AttributeError( + f"{self.__class__.__name__} {self.config_name} is not writeable: {', '.join(reasons)}" + ) + set_control(self.device, self.id, value) + @property - def config_name(self): + def config_name(self) -> str: if self._config_name is None: res = self.name.lower() for r in ("(", ")"): @@ -900,55 +942,323 @@ def config_name(self): self._config_name = res return self._config_name + @property + def is_flagged_disabled(self) -> bool: + return (self._info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED + + @property + def is_flagged_grabbed(self) -> bool: + return (self._info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED + + @property + def is_flagged_read_only(self) -> bool: + return (self._info.flags & ControlFlag.READ_ONLY) == ControlFlag.READ_ONLY + + @property + def is_flagged_update(self) -> bool: + return (self._info.flags & ControlFlag.UPDATE) == ControlFlag.UPDATE + + @property + def is_flagged_inactive(self) -> bool: + return (self._info.flags & ControlFlag.INACTIVE) == ControlFlag.INACTIVE + + @property + def is_flagged_slider(self) -> bool: + return (self._info.flags & ControlFlag.SLIDER) == ControlFlag.SLIDER + + @property + def is_flagged_write_only(self) -> bool: + return (self._info.flags & ControlFlag.WRITE_ONLY) == ControlFlag.WRITE_ONLY + + @property + def is_flagged_volatile(self) -> bool: + return (self._info.flags & ControlFlag.VOLATILE) == ControlFlag.VOLATILE + + @property + def is_flagged_has_payload(self) -> bool: + return (self._info.flags & ControlFlag.HAS_PAYLOAD) == ControlFlag.HAS_PAYLOAD + + @property + def is_flagged_execute_on_write(self) -> bool: + return ( + self._info.flags & ControlFlag.EXECUTE_ON_WRITE + ) == ControlFlag.EXECUTE_ON_WRITE + + @property + def is_flagged_modify_layout(self) -> bool: + return ( + self._info.flags & ControlFlag.MODIFY_LAYOUT + ) == ControlFlag.MODIFY_LAYOUT + + @property + def is_flagged_dynamic_array(self) -> bool: + return ( + self._info.flags & ControlFlag.DYNAMIC_ARRAY + ) == ControlFlag.DYNAMIC_ARRAY + + @property + def is_writeable(self) -> bool: + return not ( + self.is_flagged_read_only + or self.is_flagged_inactive + or self.is_flagged_disabled + or self.is_flagged_grabbed + ) + + +class BaseMonoControl(BaseControl): + def _get_repr(self) -> str: + repr = f" default={self.default}" + if not self.is_flagged_write_only: + repr += f" value={self.value}" + return repr + + def _convert_read(self, value): + return value + + @property + def default(self): + return self._convert_read(self._info.default_value) + @property def value(self): - if self.is_readable: - return get_control(self.device, self.id) + if not self.is_flagged_write_only: + v = self._get_control() + return self._convert_read(v) else: return None + def _convert_write(self, value): + return value + + def _mangle_write(self, value): + return value + @value.setter def value(self, value): - if not self.is_writeable: - raise AttributeError(f"Control {self.config_name} is read-only") - if value < self.info.minimum: - v = self.info.minimum - elif value > self.info.maximum: - v = self.info.maximum + v = self._convert_write(value) + v = self._mangle_write(v) + self._set_control(v) + + def set_to_default(self): + self.value = self.default + + +class GenericControl(BaseMonoControl): + pass + + +class BaseNumericControl(BaseMonoControl): + lower_bound = -(2**31) + upper_bound = 2**31 - 1 + + def __init__(self, device, info, clipping=True): + super().__init__(device, info) + self.minimum = self._info.minimum + self.maximum = self._info.maximum + self.step = self._info.step + self.clipping = clipping + + if self.minimum < self.lower_bound: + raise RuntimeWarning( + f"Control {self.config_name}'s claimed minimum value {self.minimum} exceeds lower bound of {self.__class__.__name__}" + ) + if self.maximum > self.upper_bound: + raise RuntimeWarning( + f"Control {self.config_name}'s claimed maximum value {self.maximum} exceeds upper bound of {self.__class__.__name__}" + ) + + def _get_repr(self) -> str: + repr = f" min={self.minimum} max={self.maximum} step={self.step}" + repr += super()._get_repr() + return repr + + def _convert_read(self, value): + return int(value) + + def _convert_write(self, value): + if isinstance(value, int): + return value else: - v = value - set_control(self.device, self.id, v) + try: + v = int(value) + except Exception: + pass + else: + return v + raise ValueError( + f"Failed to coerce {value.__class__.__name__} '{value}' to int" + ) - @property - def is_readable(self): - return not (self.info.flags & ControlFlag.WRITE_ONLY) + def _mangle_write(self, value): + if self.clipping: + if value < self.minimum: + return self.minimum + elif value > self.maximum: + return self.maximum + else: + if value < self.minimum: + raise ValueError( + f"Control {self.config_name}: {value} exceeds allowed minimum {self.minimum}" + ) + elif value > self.maximum: + raise ValueError( + f"Control {self.config_name}: {value} exceeds allowed maximum {self.maximum}" + ) + return value - @property - def is_writeable(self): - return not (self.info.flags & ControlFlag.READ_ONLY) + def increase(self, steps: int = 1): + self.value += steps * self.step + + def decrease(self, steps: int = 1): + self.value -= steps * self.step + + def set_to_minimum(self): + self.value = self.minimum + + def set_to_maximum(self): + self.value = self.maximum + + +class IntegerControl(BaseNumericControl): + lower_bound = -(2**31) + upper_bound = 2**31 - 1 + + +class Integer64Control(BaseNumericControl): + lower_bound = -(2**63) + upper_bound = 2**63 - 1 + + +class U8Control(BaseNumericControl): + lower_bound = 0 + upper_bound = 2**8 + + +class U16Control(BaseNumericControl): + lower_bound = 0 + upper_bound = 2**16 + + +class U32Control(BaseNumericControl): + lower_bound = 0 + upper_bound = 2**32 + + +class BooleanControl(BaseMonoControl): + _true = ["true", "1", "yes", "on", "enable"] + _false = ["false", "0", "no", "off", "disable"] + + def _convert_read(self, value): + return bool(value) + + def _convert_write(self, value): + if isinstance(value, bool): + return value + elif isinstance(value, str): + if value in self._true: + return True + elif value in self._false: + return False + else: + try: + v = bool(value) + except Exception: + pass + else: + return v + raise ValueError( + f"Failed to coerce {value.__class__.__name__} '{value}' to bool" + ) + + +class MenuControl(BaseMonoControl, UserDict): + def __init__(self, device, info): + BaseControl.__init__(self, device, info) + UserDict.__init__(self) + + if self.type == ControlType.MENU: + self.data = { + item.index: item.name.decode() + for item in iter_read_menu(self.device._fobj, self) + } + elif self.type == ControlType.INTEGER_MENU: + self.data = { + item.index: int(item.name) + for item in iter_read_menu(self.device._fobj, self) + } + else: + raise TypeError( + f"MenuControl only supports control types MENU or INTEGER_MENU, but not {self.type.name}" + ) + + def _convert_write(self, value): + return int(value) + + +class ButtonControl(BaseControl): + def push(self): + self._set_control(1) + + +class BaseCompoundControl(BaseControl): + def __init__(self, device, info): + raise NotImplementedError() + + +class LegacyMenuItem: + def __init__(self, item: raw.v4l2_querymenu): + self.item = item + self.index = item.index + self.name = item.name.decode() + + def __repr__(self) -> str: + return f"<{type(self).__name__} index={self.index} name={self.name}>" + + +class LegacyControl(BaseNumericControl): + def __init__(self, device, info): + super().__init__(device, info) + + self.info = self._info + if self.type == ControlType.MENU: + self.menu = { + menu.index: LegacyMenuItem(menu) + for menu in iter_read_menu(self.device._fobj, self) + } + else: + self.menu = {} + + def _get_repr(self) -> str: + repr = f"type={self.type.name.lower()}" + repr += super()._get_repr() + return repr @property - def is_inactive(self): - return self.info.flags & ControlFlag.INACTIVE + def is_writeonly(self) -> bool: + return self.is_flagged_write_only @property - def is_grabbed(self): - return self.info.flags & ControlFlag.GRABBED + def is_readonly(self) -> bool: + return self.is_flagged_read_only - def set_to_minimum(self): - self.value = self.info.minimum + @property + def is_inactive(self) -> bool: + return self.is_flagged_inactive - def set_to_default(self): - self.value = self.info.default_value + @property + def is_grabbed(self) -> bool: + return self.is_flagged_grabbed - def set_to_maximum(self): - self.value = self.info.maximum + @property + def is_disabled(self) -> bool: + return self.is_flagged_disabled def increase(self, steps: int = 1): - self.value += steps * self.info.step + self.value += steps * self.step def decrease(self, steps: int = 1): - self.value -= steps * self.info.step + self.value -= steps * self.step class DeviceHelper: diff --git a/v4l2py/raw.py b/v4l2py/raw.py index fa3f59c..fe2245c 100644 --- a/v4l2py/raw.py +++ b/v4l2py/raw.py @@ -190,7 +190,27 @@ def V4L2_FIELD_HAS_BOTH(field): V4L2_CTRL_TYPE_U8 = 0x0100 V4L2_CTRL_TYPE_U16 = 0x0101 V4L2_CTRL_TYPE_U32 = 0x0102 - +V4L2_CTRL_TYPE_AREA = 0x0106 +V4L2_CTRL_TYPE_HDR10_CLL_INFO = 0x0110 +V4L2_CTRL_TYPE_HDR10_MASTERING_DISPLAY = 0x0111 +V4L2_CTRL_TYPE_H264_SPS = 0x0200 +V4L2_CTRL_TYPE_H264_PPS = 0x0201 +V4L2_CTRL_TYPE_H264_SCALING_MATRIX = 0x0202 +V4L2_CTRL_TYPE_H264_SLICE_PARAMS = 0x0203 +V4L2_CTRL_TYPE_H264_DECODE_PARAMS = 0x0204 +V4L2_CTRL_TYPE_H264_PRED_WEIGHTS = 0x0205 +V4L2_CTRL_TYPE_FWHT_PARAMS = 0x0220 +V4L2_CTRL_TYPE_VP8_FRAME = 0x0240 +V4L2_CTRL_TYPE_MPEG2_QUANTISATION = 0x0250 +V4L2_CTRL_TYPE_MPEG2_SEQUENCE = 0x0251 +V4L2_CTRL_TYPE_MPEG2_PICTURE = 0x0252 +V4L2_CTRL_TYPE_VP9_COMPRESSED_HDR = 0x0260 +V4L2_CTRL_TYPE_VP9_FRAME = 0x0261 +V4L2_CTRL_TYPE_HEVC_SPS = 0x0270 +V4L2_CTRL_TYPE_HEVC_PPS = 0x0271 +V4L2_CTRL_TYPE_HEVC_SLICE_PARAMS = 0x0272 +V4L2_CTRL_TYPE_HEVC_SCALING_MATRIX = 0x0273 +V4L2_CTRL_TYPE_HEVC_DECODE_PARAMS = 0x0274 v4l2_tuner_type = enum (