From 000696e9110de5cd5eda4989d722e12aa8f4bc8a Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Wed, 22 Mar 2023 03:00:30 +0100 Subject: [PATCH 01/45] Control: change is_readable/is_writable to is_writeonly/is_readonly. These two methods were not well-thought. While a control that has the read-only flag set is certainly not writeable, a control with the write-only flag set could still be unwriteable because of being inactive. The original intention for these two methods was to check for the flags, so adjust their behaviour and naming accordingly. --- v4l2py/device.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index c31ad43..d99fa1c 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -872,7 +872,7 @@ def __repr__(self): 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): + if not self.is_writeonly: repr += f" value={self.value}" flags = [flag.name.lower() for flag in ControlFlag if (self.info.flags & flag)] @@ -894,14 +894,14 @@ def config_name(self): @property def value(self): - if self.is_readable: + if not self.is_writeonly: return get_control(self.device, self.id) else: return None @value.setter def value(self, value): - if not self.is_writeable: + if self.is_readonly: raise AttributeError(f"Control {self.config_name} is read-only") if self.is_inactive: raise AttributeError(f"Control {self.config_name} is currently inactive") @@ -914,12 +914,12 @@ def value(self, value): set_control(self.device, self.id, v) @property - def is_readable(self): - return not (self.info.flags & ControlFlag.WRITE_ONLY) + def is_writeonly(self): + return (self.info.flags & ControlFlag.WRITE_ONLY) @property - def is_writeable(self): - return not (self.info.flags & ControlFlag.READ_ONLY) + def is_readonly(self): + return (self.info.flags & ControlFlag.READ_ONLY) @property def is_inactive(self): From b798ed89935d9b373229553c4831f6a0cff62b47 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Wed, 22 Mar 2023 03:23:53 +0100 Subject: [PATCH 02/45] Control: update is_... methods/properties to actually return bools --- v4l2py/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index d99fa1c..362a14f 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -915,19 +915,19 @@ def value(self, value): @property def is_writeonly(self): - return (self.info.flags & ControlFlag.WRITE_ONLY) + return (self.info.flags & ControlFlag.WRITE_ONLY) == ControlFlag.WRITE_ONLY @property def is_readonly(self): - return (self.info.flags & ControlFlag.READ_ONLY) + return (self.info.flags & ControlFlag.READ_ONLY) == ControlFlag.READ_ONLY @property def is_inactive(self): - return self.info.flags & ControlFlag.INACTIVE + return (self.info.flags & ControlFlag.INACTIVE) == ControlFlag.INACTIVE @property def is_grabbed(self): - return self.info.flags & ControlFlag.GRABBED + return (self.info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED def set_to_minimum(self): self.value = self.info.minimum From 61442139efee872d82c7208777ed80dd2f169195 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Wed, 22 Mar 2023 03:27:27 +0100 Subject: [PATCH 03/45] Control: reimplement method is_writeable, with correct behaviour. Before 000696e, a control was assumed (and reported by this method) to be writeable when the read-only was not set. However, such a control would still be unwriteable when being flagged as inactive, which was not taken into account before. In other words, is_writeable now checks that a control can *actually* be written to. --- v4l2py/device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v4l2py/device.py b/v4l2py/device.py index 362a14f..2e3c3e9 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -925,6 +925,10 @@ def is_readonly(self): def is_inactive(self): return (self.info.flags & ControlFlag.INACTIVE) == ControlFlag.INACTIVE + @property + def is_writeable(self): + return (not self.is_readonly and not self.is_inactive) + @property def is_grabbed(self): return (self.info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED From b8d3a155e32520bed8a0baf6a7767fab4703fe8f Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 24 Mar 2023 04:20:34 +0100 Subject: [PATCH 04/45] Controls are also not writeable while grabbed or when disabled. --- v4l2py/device.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 2e3c3e9..d5a94b2 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -901,10 +901,17 @@ def value(self): @value.setter def value(self, value): - if self.is_readonly: - raise AttributeError(f"Control {self.config_name} is read-only") - if self.is_inactive: - raise AttributeError(f"Control {self.config_name} is currently inactive") + if not self.is_writeable: + reasons = [] + if self.is_readonly: + reasons.append("read-only") + if self.is_inactive: + reasons.append("inactive") + if self.is_disabled: + reasons.append("disabled") + if self.is_grabbed: + reasons.append("grabbed") + raise AttributeError(f"Control {self.config_name} is not writeable: {','.join(reasons)}") if value < self.info.minimum: v = self.info.minimum elif value > self.info.maximum: @@ -925,14 +932,19 @@ def is_readonly(self): def is_inactive(self): return (self.info.flags & ControlFlag.INACTIVE) == ControlFlag.INACTIVE - @property - def is_writeable(self): - return (not self.is_readonly and not self.is_inactive) - @property def is_grabbed(self): return (self.info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED + @property + def is_disabled(self): + return (self.info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED + + @property + def is_writeable(self): + return not (self.is_readonly or self.is_inactive + or self.is_disabled or self.is_grabbed) + def set_to_minimum(self): self.value = self.info.minimum From 88cabd54c5b6e79ed5cca95fd1ab9e62f3e9e657 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 24 Mar 2023 04:42:04 +0100 Subject: [PATCH 05/45] Fix flake8 complaint --- v4l2py/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index d5a94b2..7b8ee5a 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -943,7 +943,7 @@ def is_disabled(self): @property def is_writeable(self): return not (self.is_readonly or self.is_inactive - or self.is_disabled or self.is_grabbed) + or self.is_disabled or self.is_grabbed) def set_to_minimum(self): self.value = self.info.minimum From d5e9dde492d01c153a3221d957bf7f30e2031cad Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 25 Mar 2023 00:37:41 +0100 Subject: [PATCH 06/45] Controls: add alternative constructor from_device. This will be used later to facilitate instantiation of a Controls object with diverse Control sub-classes. --- v4l2py/device.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 7b8ee5a..087412c 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -638,7 +638,7 @@ class Device(ReentrantContextManager): def __init__(self, name_or_file, read_write=True, io=IO): 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) @@ -675,7 +675,7 @@ 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}) + self.controls = Controls.from_device(self) def open(self): if not self._fobj: @@ -778,6 +778,13 @@ def write(self, data: bytes) -> None: class Controls(dict): + @classmethod + def from_device(cls, device): + ctrl_dict = dict() + for ctrl in device.info.controls: + ctrl_dict[ctrl.id] = Control(device, ctrl) + return cls(ctrl_dict) + def __getattr__(self, key): try: return self[key] From f8629d9b85ad628f6f0387f47258fa7478452f71 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 25 Mar 2023 15:46:34 +0100 Subject: [PATCH 07/45] Remove stand-alone function config_name; the functionality has been moved into a method of class Control --- v4l2py/device.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 087412c..b689822 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -846,13 +846,6 @@ def __repr__(self): return f"<{type(self).__name__} index={self.index} name={self.name}>" -def config_name(name: str) -> str: - res = name.lower() - for r in (", ", " "): - res = res.replace(r, "_") - return res - - class Control: def __init__(self, device, info): self.device = device From 79fd14dd85b07194aad643e4a6a651d4ea15063e Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 25 Mar 2023 16:45:29 +0100 Subject: [PATCH 08/45] Control: two minor changes to flags gathering in __repr__ to satisfy PEP8 --- v4l2py/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index b689822..3f4cc54 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -875,8 +875,8 @@ def __repr__(self): if not self.is_writeonly: repr += f" value={self.value}" - 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}>" From 20a8cda2f90727c0544cd9ba20ce85810e9df8e3 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Tue, 28 Mar 2023 05:17:07 +0200 Subject: [PATCH 09/45] Begin implementation of inheritance-based class model for controls. This commit covers the part up to LegacyControl, as discussed in #15. The upgrade path for existing code is: Use LegacyControl instead of Control for instantiations, and use either BaseControl or BaseSingleControl for isinstance() checks (see Controls). --- v4l2py/device.py | 147 +++++++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 55 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 3f4cc54..872e806 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -782,7 +782,7 @@ class Controls(dict): def from_device(cls, device): ctrl_dict = dict() for ctrl in device.info.controls: - ctrl_dict[ctrl.id] = Control(device, ctrl) + ctrl_dict[ctrl.id] = LegacyControl(device, ctrl) return cls(ctrl_dict) def __getattr__(self, key): @@ -803,12 +803,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 set([v.control_class for v in self.values() if isinstance(v, Control)]) + return set([v.control_class for v in self.values() if isinstance(v, BaseControl)]) def with_class(self, control_class): if isinstance(control_class, ControlClass): @@ -822,12 +822,12 @@ def with_class(self, control_class): raise TypeError(f"control_class expected as ControlClass or str, not {control_class.__class__.__name__}") 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, BaseSingleControl): continue try: @@ -846,43 +846,42 @@ def __repr__(self): return f"<{type(self).__name__} index={self.index} name={self.name}>" -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}" + repr = f"{self.config_name}" + + addrepr = self._get_repr() + addrepr = addrepr.strip() + if addrepr: + repr += f" {addrepr}" + if not self.is_writeonly: repr += f" value={self.value}" - flags = [flag.name.lower() for flag in ControlFlag if ((self.info.flags & flag) == flag)] + 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 "" + @property - def config_name(self): + def config_name(self) -> str: if self._config_name is None: res = self.name.lower() for r in ("(", ")"): @@ -892,6 +891,45 @@ def config_name(self): self._config_name = res return self._config_name + @property + def is_writeonly(self) -> bool: + return (self._info.flags & ControlFlag.WRITE_ONLY) == ControlFlag.WRITE_ONLY + + @property + def is_readonly(self) -> bool: + return (self._info.flags & ControlFlag.READ_ONLY) == ControlFlag.READ_ONLY + + @property + def is_inactive(self) -> bool: + return (self._info.flags & ControlFlag.INACTIVE) == ControlFlag.INACTIVE + + @property + def is_grabbed(self) -> bool: + return (self._info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED + + @property + def is_disabled(self) -> bool: + return (self._info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED + + @property + def is_writeable(self) -> bool: + return not (self.is_readonly or self.is_inactive + or self.is_disabled or self.is_grabbed) + + +class BaseSingleControl(BaseControl): + def __init__(self, device, info): + super().__init__(device, info) + self.minimum = self._info.minimum + self.maximum = self._info.maximum + self.step = self._info.step + self.default = self._info.default_value + + def _get_repr(self) -> str: + repr = f" min={self.minimum} max={self.maximum}" + repr += f" step={self.step} default={self.default}" + return repr + @property def value(self): if not self.is_writeonly: @@ -911,54 +949,53 @@ def value(self, value): reasons.append("disabled") if self.is_grabbed: reasons.append("grabbed") - raise AttributeError(f"Control {self.config_name} is not writeable: {','.join(reasons)}") - if value < self.info.minimum: - v = self.info.minimum - elif value > self.info.maximum: - v = self.info.maximum + raise AttributeError(f"Control {self.config_name} is not writeable: {', '.join(reasons)}") + if value < self.minimum: + v = self.minimum + elif value > self.maximum: + v = self.maximum else: v = value set_control(self.device, self.id, v) - @property - def is_writeonly(self): - return (self.info.flags & ControlFlag.WRITE_ONLY) == ControlFlag.WRITE_ONLY + def set_to_default(self): + self.value = self.default - @property - def is_readonly(self): - return (self.info.flags & ControlFlag.READ_ONLY) == ControlFlag.READ_ONLY + def set_to_minimum(self): + self.value = self.minimum - @property - def is_inactive(self): - return (self.info.flags & ControlFlag.INACTIVE) == ControlFlag.INACTIVE + def set_to_maximum(self): + self.value = self.maximum - @property - def is_grabbed(self): - return (self.info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED - @property - def is_disabled(self): - return (self.info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED +class BaseCompoundControl(BaseControl): + def __init__(self, device, info): + raise NotImplementedError() - @property - def is_writeable(self): - return not (self.is_readonly or self.is_inactive - or self.is_disabled or self.is_grabbed) - def set_to_minimum(self): - self.value = self.info.minimum +class LegacyControl(BaseSingleControl): + def __init__(self, device, info): + super().__init__(device, info) - def set_to_default(self): - self.value = self.info.default_value + self.info = self._info + 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 set_to_maximum(self): - self.value = self.info.maximum + def _get_repr(self) -> str: + repr = f"type={self.type.name.lower()}" + repr += super()._get_repr() + return repr 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: From dea5cbe3e219f5b3619be5143966caf325c0c99c Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Tue, 28 Mar 2023 05:30:06 +0200 Subject: [PATCH 10/45] Adjust example output in README.md to the changes in commit 20a8cda --- README.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 19be688..5424407 100644 --- a/README.md +++ b/README.md @@ -72,33 +72,32 @@ 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 - + ``` ### asyncio From 5640d4ee6e4d593e464c4ed8846ba6c45613dc63 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Wed, 5 Apr 2023 23:04:14 +0200 Subject: [PATCH 11/45] Dissolve BaseSingleControl into BaseControl and BaseNumericControl. See https://github.com/otaku42/v4l2py/issues/15#issuecomment-1498146221 . --- v4l2py/device.py | 74 +++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 872e806..15a0b95 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -827,7 +827,7 @@ def with_class(self, control_class): def set_to_default(self): for v in self.values(): - if not isinstance(v, BaseSingleControl): + if not isinstance(v, BaseControl): continue try: @@ -855,11 +855,14 @@ def __init__(self, device, info): self._config_name = None self.control_class = ControlClass(raw.V4L2_CTRL_ID2CLASS(self.id)) self.type = ControlType(self._info.type) + try: self.standard = ControlID(self.id) except ValueError: self.standard = None + self.default = self._info.default_value + def __repr__(self): repr = f"{self.config_name}" @@ -891,6 +894,34 @@ def config_name(self) -> str: self._config_name = res return self._config_name + @property + def value(self): + if not self.is_writeonly: + return get_control(self.device, self.id) + else: + return None + + @value.setter + def value(self, value): + if not self.is_writeable: + reasons = [] + if self.is_readonly: + reasons.append("read-only") + if self.is_inactive: + reasons.append("inactive") + if self.is_disabled: + reasons.append("disabled") + if self.is_grabbed: + reasons.append("grabbed") + raise AttributeError(f"Control {self.config_name} is not writeable: {', '.join(reasons)}") + if value < self.minimum: + v = self.minimum + elif value > self.maximum: + v = self.maximum + else: + v = value + set_control(self.device, self.id, v) + @property def is_writeonly(self) -> bool: return (self._info.flags & ControlFlag.WRITE_ONLY) == ControlFlag.WRITE_ONLY @@ -916,50 +947,27 @@ def is_writeable(self) -> bool: return not (self.is_readonly or self.is_inactive or self.is_disabled or self.is_grabbed) + def set_to_default(self): + self.value = self.default + -class BaseSingleControl(BaseControl): +class BaseNumericControl(BaseControl): def __init__(self, device, info): super().__init__(device, info) self.minimum = self._info.minimum self.maximum = self._info.maximum self.step = self._info.step - self.default = self._info.default_value def _get_repr(self) -> str: repr = f" min={self.minimum} max={self.maximum}" repr += f" step={self.step} default={self.default}" return repr - @property - def value(self): - if not self.is_writeonly: - return get_control(self.device, self.id) - else: - return None - - @value.setter - def value(self, value): - if not self.is_writeable: - reasons = [] - if self.is_readonly: - reasons.append("read-only") - if self.is_inactive: - reasons.append("inactive") - if self.is_disabled: - reasons.append("disabled") - if self.is_grabbed: - reasons.append("grabbed") - raise AttributeError(f"Control {self.config_name} is not writeable: {', '.join(reasons)}") - if value < self.minimum: - v = self.minimum - elif value > self.maximum: - v = self.maximum - else: - v = value - set_control(self.device, self.id, v) + def increase(self, steps: int = 1): + self.value += (steps * self.step) - def set_to_default(self): - self.value = self.default + def decrease(self, steps: int = 1): + self.value -= (steps * self.step) def set_to_minimum(self): self.value = self.minimum @@ -973,7 +981,7 @@ def __init__(self, device, info): raise NotImplementedError() -class LegacyControl(BaseSingleControl): +class LegacyControl(BaseNumericControl): def __init__(self, device, info): super().__init__(device, info) From 2b57f1cfc7ee39a46078da2756ca5d4587423e4a Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Thu, 6 Apr 2023 01:01:06 +0200 Subject: [PATCH 12/45] Add support for checking all currently defined control flags. is_ is working for many of the flags, but is weird for stuff like V4L2_CTRL_FLAG_HAS_PAYLOAD or V4L2_CTRL_FLAG_UPDATE. Switching to is_flagged_ to check if a flag is set, e.g. is_flagged_has_payload, is still not perfect, but better. --- v4l2py/device.py | 68 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 15a0b95..22cdfd1 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -923,29 +923,57 @@ def value(self, value): set_control(self.device, self.id, v) @property - def is_writeonly(self) -> bool: - return (self._info.flags & ControlFlag.WRITE_ONLY) == ControlFlag.WRITE_ONLY + def is_flagged_disabled(self) -> bool: + return (self._info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED @property - def is_readonly(self) -> bool: + 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_inactive(self) -> bool: + 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_grabbed(self) -> bool: - return (self._info.flags & ControlFlag.GRABBED) == ControlFlag.GRABBED + def is_flagged_slider(self) -> bool: + return (self._info.flags & ControlFlag.SLIDER) == ControlFlag.SLIDER @property - def is_disabled(self) -> bool: - return (self._info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED + 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_readonly or self.is_inactive - or self.is_disabled or self.is_grabbed) + return not (self.is_flagged_read_only or self.is_flagged_inactive + or self.is_flagged_disabled or self.is_flagged_grabbed) def set_to_default(self): self.value = self.default @@ -999,6 +1027,26 @@ def _get_repr(self) -> str: repr += super()._get_repr() return repr + @property + def is_writeonly(self) -> bool: + return self.is_flagged_write_only + + @property + def is_readonly(self) -> bool: + return self.is_flagged_read_only + + @property + def is_inactive(self) -> bool: + return self.is_flagged_inactive + + @property + def is_grabbed(self) -> bool: + return self.is_flagged_grabbed + + @property + def is_disabled(self) -> bool: + return self.is_flagged_disabled + def increase(self, steps: int = 1): self.value += (steps * self.step) From 2d68a2c633867dd228e67a522c84a82bb2d22349 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 7 Apr 2023 16:20:44 +0200 Subject: [PATCH 13/45] is_writeonly now is called is_flagged_write_only --- v4l2py/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 22cdfd1..66a5fe4 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -871,7 +871,7 @@ def __repr__(self): if addrepr: repr += f" {addrepr}" - if not self.is_writeonly: + if not self.is_flagged_write_only: repr += f" value={self.value}" flags = [flag.name.lower() for flag in ControlFlag if ((self._info.flags & flag) == flag)] @@ -896,7 +896,7 @@ def config_name(self) -> str: @property def value(self): - if not self.is_writeonly: + if not self.is_flagged_write_only: return get_control(self.device, self.id) else: return None From 5580d4be48d2d21f693d664c956c8534310ed51b Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 7 Apr 2023 22:06:53 +0200 Subject: [PATCH 14/45] After default has been moved to BaseControl, it should be part BaseControl's __repr__, too --- v4l2py/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 66a5fe4..a6eccb6 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -871,6 +871,7 @@ def __repr__(self): if addrepr: repr += f" {addrepr}" + repr += f" default={self.default}" if not self.is_flagged_write_only: repr += f" value={self.value}" @@ -987,8 +988,7 @@ def __init__(self, device, info): self.step = self._info.step def _get_repr(self) -> str: - repr = f" min={self.minimum} max={self.maximum}" - repr += f" step={self.step} default={self.default}" + repr = f" min={self.minimum} max={self.maximum} step={self.step}" return repr def increase(self, steps: int = 1): From 685acc2c54d4acef96df5c7468fecf5864ecc544 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 7 Apr 2023 22:13:06 +0200 Subject: [PATCH 15/45] Make default a property; this is required for an upcoming change --- v4l2py/device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index a6eccb6..15c3db9 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -861,8 +861,6 @@ def __init__(self, device, info): except ValueError: self.standard = None - self.default = self._info.default_value - def __repr__(self): repr = f"{self.config_name}" @@ -895,6 +893,10 @@ def config_name(self) -> str: self._config_name = res return self._config_name + @property + def default(self): + return self._info.default_value + @property def value(self): if not self.is_flagged_write_only: From 99d79b34cab882383391cd25878226634b9a3d1a Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 7 Apr 2023 22:33:13 +0200 Subject: [PATCH 16/45] Use actual control type in this exception message --- v4l2py/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 15c3db9..5fecb70 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -916,7 +916,7 @@ def value(self, value): reasons.append("disabled") if self.is_grabbed: reasons.append("grabbed") - raise AttributeError(f"Control {self.config_name} is not writeable: {', '.join(reasons)}") + raise AttributeError(f"{self.__class__.__name__} {self.config_name} is not writeable: {', '.join(reasons)}") if value < self.minimum: v = self.minimum elif value > self.maximum: From ee38673503d25c6f8a234c1db5ec8551a5e41ff3 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 00:40:19 +0200 Subject: [PATCH 17/45] Introduce some 'hooks' meant to be overwritten by upcoming subclasses --- v4l2py/device.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 5fecb70..1ed975f 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -893,17 +893,27 @@ def config_name(self) -> str: self._config_name = res return self._config_name + def _convert_read(self, value): + return value + @property def default(self): - return self._info.default_value + return self._convert_read(self._info.default_value) @property def value(self): if not self.is_flagged_write_only: - return get_control(self.device, self.id) + v = get_control(self.device, self.id) + return self._convert_read(v) else: return None + def _mangle_write(self, value): + return value + + def _convert_write(self, value): + return value + @value.setter def value(self, value): if not self.is_writeable: @@ -923,6 +933,8 @@ def value(self, value): v = self.maximum else: v = value + v = self._mangle_write(value) + v = self._convert_write(v) set_control(self.device, self.id, v) @property From ff66c0270aa4c3843c5b710c4dba88cfd4c007be Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 00:48:56 +0200 Subject: [PATCH 18/45] These checks need to be moved to BaseNumericControl, since BaseControl doesn't have the attributes minimum and maximum. --- v4l2py/device.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 1ed975f..c738bcc 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -927,12 +927,6 @@ def value(self, value): if self.is_grabbed: reasons.append("grabbed") raise AttributeError(f"{self.__class__.__name__} {self.config_name} is not writeable: {', '.join(reasons)}") - if value < self.minimum: - v = self.minimum - elif value > self.maximum: - v = self.maximum - else: - v = value v = self._mangle_write(value) v = self._convert_write(v) set_control(self.device, self.id, v) @@ -1005,6 +999,14 @@ def _get_repr(self) -> str: repr = f" min={self.minimum} max={self.maximum} step={self.step}" return repr + def _mangle_write(self, value): + if value < self.minimum: + return self.minimum + elif value > self.maximum: + return self.maximum + else: + return value + def increase(self, steps: int = 1): self.value += (steps * self.step) From 395f4ec76f3f1eeea96149e876d9a56bb33da83c Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 01:22:21 +0200 Subject: [PATCH 19/45] Introduce LegacyControls as backward-compatible controls 'factory', which is used by default. --- v4l2py/device.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index c738bcc..8f94683 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -635,7 +635,7 @@ 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=True): super().__init__() self.info = None self.controls = None @@ -656,6 +656,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}>" @@ -675,7 +676,10 @@ def from_id(cls, did: int, **kwargs): def _init(self): self.info = read_info(self.fileno()) - self.controls = Controls.from_device(self) + if self.legacy_controls: + self.controls = LegacyControls.from_device(self) + else: + self.controls = Controls.from_device(self) def open(self): if not self._fobj: @@ -836,6 +840,15 @@ def set_to_default(self): pass +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 MenuItem: def __init__(self, item): self.item = item From 877709cb61a50f80550a1418b209e10a4ce3bc7f Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 02:07:02 +0200 Subject: [PATCH 20/45] Controls: introduce a ControlType to class mapping, to support instantiation of different control class objects without a messy if/elif construct. --- v4l2py/device.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 8f94683..b0d2498 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -784,9 +784,14 @@ def write(self, data: bytes) -> None: class Controls(dict): @classmethod def from_device(cls, device): + ctrl_type_map = {} ctrl_dict = dict() + for ctrl in device.info.controls: - ctrl_dict[ctrl.id] = LegacyControl(device, ctrl) + ctrl_type = ControlType(ctrl.type) + ctrl_class = ctrl_type_map.get(ctrl_type, LegacyControl) + ctrl_dict[ctrl.id] = ctrl_class(device, ctrl) + return cls(ctrl_dict) def __getattr__(self, key): From ed234744114f8de1fec1fd17bf8576eea74f9df0 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 02:19:46 +0200 Subject: [PATCH 21/45] Initial implementation of class BooleanControl. --- v4l2py/device.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index b0d2498..643e5a5 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -784,7 +784,9 @@ def write(self, data: bytes) -> None: class Controls(dict): @classmethod def from_device(cls, device): - ctrl_type_map = {} + ctrl_type_map = { + ControlType.BOOLEAN: BooleanControl, + } ctrl_dict = dict() for ctrl in device.info.controls: @@ -1038,6 +1040,31 @@ def set_to_maximum(self): self.value = self.maximum +class BooleanControl(BaseControl): + _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 BaseCompoundControl(BaseControl): def __init__(self, device, info): raise NotImplementedError() From 6c9532f780a8c0b2f96ea4379b3eee0a544686da Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 15:41:08 +0200 Subject: [PATCH 22/45] BaseNumericControl: make clipping of written values to minimum/maximum optional (default: on) --- v4l2py/device.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 643e5a5..796b0f5 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -1009,23 +1009,29 @@ def set_to_default(self): class BaseNumericControl(BaseControl): - def __init__(self, device, info): + 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 def _get_repr(self) -> str: repr = f" min={self.minimum} max={self.maximum} step={self.step}" return repr def _mangle_write(self, value): - if value < self.minimum: - return self.minimum - elif value > self.maximum: - return self.maximum + if self.clipping: + if value < self.minimum: + return self.minimum + elif value > self.maximum: + return self.maximum else: - return value + 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 def increase(self, steps: int = 1): self.value += (steps * self.step) From bd9dd8bc19c9729ee8cf3720e3d0c7f67f0948b2 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 15:55:20 +0200 Subject: [PATCH 23/45] BaseControl: value to be written should be converted to target type before trying to mangle it --- v4l2py/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 796b0f5..b98252a 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -928,10 +928,10 @@ def value(self): else: return None - def _mangle_write(self, value): + def _convert_write(self, value): return value - def _convert_write(self, value): + def _mangle_write(self, value): return value @value.setter @@ -947,8 +947,8 @@ def value(self, value): if self.is_grabbed: reasons.append("grabbed") raise AttributeError(f"{self.__class__.__name__} {self.config_name} is not writeable: {', '.join(reasons)}") - v = self._mangle_write(value) - v = self._convert_write(v) + v = self._convert_write(value) + v = self._mangle_write(v) set_control(self.device, self.id, v) @property From 608954e123c07acffb665b2be7e55c6efeb186eb Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 8 Apr 2023 16:20:46 +0200 Subject: [PATCH 24/45] Initial implementation of classes IntegerControl and Integer64Control. --- v4l2py/device.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/v4l2py/device.py b/v4l2py/device.py index b98252a..de0e743 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -786,6 +786,8 @@ class Controls(dict): def from_device(cls, device): ctrl_type_map = { ControlType.BOOLEAN: BooleanControl, + ControlType.INTEGER: IntegerControl, + ControlType.INTEGER64: Integer64Control, } ctrl_dict = dict() @@ -1009,6 +1011,9 @@ def set_to_default(self): class BaseNumericControl(BaseControl): + lower_bound = -2 ** 31 + upper_bound = 2 ** 31 + def __init__(self, device, info, clipping=True): super().__init__(device, info) self.minimum = self._info.minimum @@ -1016,10 +1021,30 @@ def __init__(self, device, info, clipping=True): 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}" return repr + def _convert_read(self, value): + return int(value) + + def _convert_write(self, value): + if isinstance(value, int): + return value + else: + try: + v = int(value) + except Exception: + pass + else: + return v + raise ValueError(f"Failed to coerce {value.__class__.__name__} '{value}' to int") + def _mangle_write(self, value): if self.clipping: if value < self.minimum: @@ -1046,6 +1071,16 @@ def set_to_maximum(self): self.value = self.maximum +class IntegerControl(BaseNumericControl): + lower_bound = -2 ** 31 + upper_bound = 2 ** 31 + + +class Integer64Control(BaseNumericControl): + lower_bound = -2 ** 63 + upper_bound = 2 ** 63 + + class BooleanControl(BaseControl): _true = ["true", "1", "yes", "on", "enable"] _false = ["false", "0", "no", "off", "disable"] From 7e47ec006c67675def08780fbb3e0e7bfa41280d Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Mon, 17 Apr 2023 10:54:23 +0200 Subject: [PATCH 25/45] iter_read_menu(): use ctrl._info to support all subclasses of BaseControl --- v4l2py/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index de0e743..891400e 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -307,9 +307,9 @@ def iter_read_menu(fd, ctrl): fd, IOC.QUERYMENU, menu, - 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) From 6e0f5f686637cdd717bdfb981d8b5ee6393b39d9 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Tue, 18 Apr 2023 00:25:33 +0200 Subject: [PATCH 26/45] Initial implementation of class MenuControl. This class inherits from UserDict and thus can be used like a dict to access the items defined for the menu. For the sake of this dict-like behaviour the MenuItem class is not used in MenuControl; the names of the menu items will be transformed straight to either string or integer, depending on the menu type. MenuItem is kept for now, but renamed to LegacyMenuItem to signal it should not be used except for legacy code. --- v4l2py/device.py | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 891400e..7622ac0 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -19,6 +19,7 @@ import typing from io import IOBase +from collections import UserDict from . import raw from .io import IO, fopen @@ -788,6 +789,8 @@ def from_device(cls, device): ControlType.BOOLEAN: BooleanControl, ControlType.INTEGER: IntegerControl, ControlType.INTEGER64: Integer64Control, + ControlType.MENU: MenuControl, + ControlType.INTEGER_MENU: MenuControl, } ctrl_dict = dict() @@ -858,16 +861,6 @@ def from_device(cls, device): return cls(ctrl_dict) -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}>" - - class BaseControl: def __init__(self, device, info): self.device = device @@ -1106,11 +1099,40 @@ def _convert_write(self, value): raise ValueError(f"Failed to coerce {value.__class__.__name__} '{value}' to bool") +class MenuControl(BaseControl, 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}") + + 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) @@ -1118,7 +1140,7 @@ def __init__(self, device, info): self.info = self._info if self.type == ControlType.MENU: self.menu = { - menu.index: MenuItem(menu) + menu.index: LegacyMenuItem(menu) for menu in iter_read_menu(self.device._fobj, self) } else: From bbac3648d6326743e739dbb3d2d032f3ce1d0310 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Tue, 18 Apr 2023 01:47:34 +0200 Subject: [PATCH 27/45] Add some explanation of and demonstration for the improved controls API. --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/README.md b/README.md index 5424407..1ecc41d 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,94 @@ 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. For +now the legacy interface is still the default, the new interface has to be +explicitly requested: `Device.from_id(x, legacy_controls=False)`. + +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, legacy_controls=False) +>>> 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 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. From 832a4a23d680760429835e239d6c14e26da59519 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Thu, 20 Apr 2023 02:14:51 +0200 Subject: [PATCH 28/45] BaseControl: fix checks for flags being set --- v4l2py/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 7622ac0..1127785 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -933,13 +933,13 @@ def _mangle_write(self, value): def value(self, value): if not self.is_writeable: reasons = [] - if self.is_readonly: + if self.is_flagged_read_only: reasons.append("read-only") - if self.is_inactive: + if self.is_flagged_inactive: reasons.append("inactive") - if self.is_disabled: + if self.is_flagged_disabled: reasons.append("disabled") - if self.is_grabbed: + if self.is_flagged_grabbed: reasons.append("grabbed") raise AttributeError(f"{self.__class__.__name__} {self.config_name} is not writeable: {', '.join(reasons)}") v = self._convert_write(value) From d2fc7ff2d3a7ead60fa7226f09c40cf3c07b4ccc Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Thu, 20 Apr 2023 02:59:36 +0200 Subject: [PATCH 29/45] Controls: add set_clipping(), allows clipping to be changed for all numeric controls --- v4l2py/device.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/v4l2py/device.py b/v4l2py/device.py index 1127785..ee351bb 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -851,6 +851,11 @@ def set_to_default(self): except AttributeError: pass + def set_clipping(self, clipping: bool) -> None: + for v in self.values(): + if isinstance(v, BaseNumericControl): + v.clipping = clipping + class LegacyControls(Controls): @classmethod From 9392f169beefdf028647378159871360b249d777 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Thu, 20 Apr 2023 03:20:09 +0200 Subject: [PATCH 30/45] MenuControl: menu controls expect written values to be integers --- v4l2py/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v4l2py/device.py b/v4l2py/device.py index ee351bb..47f2f98 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -1122,6 +1122,9 @@ def __init__(self, device, info): 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 BaseCompoundControl(BaseControl): def __init__(self, device, info): From 43c33e7c5bbc539eae113ed80175183976b25f4a Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 21 Apr 2023 00:38:07 +0200 Subject: [PATCH 31/45] Add example to demonstrate usage of controls --- examples/v4l2py-ctl.py | 190 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 examples/v4l2py-ctl.py diff --git a/examples/v4l2py-ctl.py b/examples/v4l2py-ctl.py new file mode 100644 index 0000000..95631a5 --- /dev/null +++ b/examples/v4l2py-ctl.py @@ -0,0 +1,190 @@ +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.") From d18bf00b414efe7ca105cd6fe79aa21ff47c2460 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 21 Apr 2023 00:58:37 +0200 Subject: [PATCH 32/45] Use new control interface by default now. --- README.md | 43 +++++++++++++++++++++++-------------------- v4l2py/device.py | 2 +- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1ecc41d..ba41671 100644 --- a/README.md +++ b/README.md @@ -208,9 +208,9 @@ with Device.from_id(0) as cam: ## Improved device controls -Device controls have been improved to provide a more pythonic interface. For -now the legacy interface is still the default, the new interface has to be -explicitly requested: `Device.from_id(x, legacy_controls=False)`. +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 @@ -221,25 +221,25 @@ Before: ... 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) @@ -248,7 +248,7 @@ Before: Now: ```python >>> from v4l2py.device import Device, MenuControl ->>> cam = Device.from_id(0, legacy_controls=False) +>>> cam = Device.from_id(0) >>> cam.open() >>> for ctrl in cam.controls.values(): ... print(ctrl) @@ -291,8 +291,11 @@ Now: ``` -The initial upgrade path for existing code is to 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`. +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 diff --git a/v4l2py/device.py b/v4l2py/device.py index 47f2f98..741dc44 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -636,7 +636,7 @@ def __exit__(self, *exc): class Device(ReentrantContextManager): - def __init__(self, name_or_file, read_write=True, io=IO, legacy_controls=True): + def __init__(self, name_or_file, read_write=True, io=IO, legacy_controls=False): super().__init__() self.info = None self.controls = None From 430d9478feb5d3bc20f7d9141c5ad7a5493ee666 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 21 Apr 2023 01:25:15 +0200 Subject: [PATCH 33/45] Fix flake8's 'over-indented' complaint. --- examples/v4l2py-ctl.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/v4l2py-ctl.py b/examples/v4l2py-ctl.py index 95631a5..e453ac0 100644 --- a/examples/v4l2py-ctl.py +++ b/examples/v4l2py-ctl.py @@ -4,17 +4,17 @@ 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 + 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: From d6aa81153001432b96e251e6234a13de9ea27bb9 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 21 Apr 2023 01:33:27 +0200 Subject: [PATCH 34/45] Update examples in README.md for new control interface. This will most probably be the last patch in the current series of the improve_controls branch for now. This series closes #15 and closes #12. --- README.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ba41671..acda457 100644 --- a/README.md +++ b/README.md @@ -72,34 +72,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 examples/v4l2py-ctl.py) + ### asyncio v4l2py is asyncio friendly: From 8e4ca97b16e420c8a2bf5ff05ec8a3076444f011 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sun, 23 Apr 2023 00:40:56 +0200 Subject: [PATCH 35/45] Add link to v4l2py-ctl.py example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index acda457..fef4a21 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Format(width=640, height=480, pixelformat=} ``` -(see also examples/v4l2py-ctl.py) +(see also [v4l2py-ctl](examples/v4l2py-ctl.py) example) ### asyncio From 62bef5727ba377428e77528b7742776c5bfb2061 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Wed, 26 Apr 2023 00:05:35 +0200 Subject: [PATCH 36/45] Apply some black magic --- examples/v4l2py-ctl.py | 32 +++++++++++------ v4l2py/device.py | 78 +++++++++++++++++++++++++++++------------- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/examples/v4l2py-ctl.py b/examples/v4l2py-ctl.py index e453ac0..9bbcb09 100644 --- a/examples/v4l2py-ctl.py +++ b/examples/v4l2py-ctl.py @@ -28,7 +28,7 @@ def show_control_status(device: str, legacy_controls: bool) -> None: for ctrl in cam.controls.with_class(cc): print("0x%08x:" % ctrl.id, ctrl) if isinstance(ctrl, MenuControl): - for (key, value) in ctrl.items(): + for key, value in ctrl.items(): print(11 * " ", f" +-- {key}: {value}") elif isinstance(ctrl, LegacyControl): for item in ctrl.menu.values(): @@ -53,15 +53,19 @@ def get_controls(device: str, controls: list, legacy_controls: bool) -> None: 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)) +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: + for control, value_new in controls: ctrl = _get_ctrl(cam, control) if not ctrl: print(f"{control}: unknown control") @@ -85,7 +89,9 @@ def set_controls(device: str, controls: list, legacy_controls: bool, clipping: b if success: print(f"{result} {control}: {value_old} -> {value_new}\n") else: - print(f"{result} {control}: {value_old} -> {value_new}\n{result} {reason}\n") + print( + f"{result} {control}: {value_old} -> {value_new}\n{result} {reason}\n" + ) def reset_controls(device: str, controls: list, legacy_controls: bool) -> None: @@ -136,29 +142,33 @@ def csv(string: str) -> list: "--clipping", default=False, action="store_true", - help="when changing numeric controls, enforce the written value to be within allowed range (default: %(default)s)" + 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", + 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=[], + type=csv, + default=[], metavar="[,...]", help="get the values of the specified controls", ) parser.add_argument( "--set-ctrl", - type=csv, default=[], + type=csv, + default=[], metavar="=[,=...]", help="set the values of the specified controls", ) parser.add_argument( "--reset-ctrl", - type=csv, default=[], + type=csv, + default=[], metavar="[,...]", help="reset the specified controls to their default values", ) diff --git a/v4l2py/device.py b/v4l2py/device.py index 31a6e56..0b605f0 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -899,7 +899,11 @@ def __repr__(self): if not self.is_flagged_write_only: repr += f" value={self.value}" - flags = [flag.name.lower() for flag in ControlFlag if ((self._info.flags & flag) == flag)] + flags = [ + flag.name.lower() + for flag in ControlFlag + if ((self._info.flags & flag) == flag) + ] if flags: repr += " flags=" + ",".join(flags) @@ -952,7 +956,9 @@ def value(self, value): 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)}") + raise AttributeError( + f"{self.__class__.__name__} {self.config_name} is not writeable: {', '.join(reasons)}" + ) v = self._convert_write(value) v = self._mangle_write(v) set_control(self.device, self.id, v) @@ -995,28 +1001,38 @@ def is_flagged_has_payload(self) -> bool: @property def is_flagged_execute_on_write(self) -> bool: - return (self._info.flags & ControlFlag.EXECUTE_ON_WRITE) == ControlFlag.EXECUTE_ON_WRITE + 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 + 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 + 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) + return not ( + self.is_flagged_read_only + or self.is_flagged_inactive + or self.is_flagged_disabled + or self.is_flagged_grabbed + ) def set_to_default(self): self.value = self.default class BaseNumericControl(BaseControl): - lower_bound = -2 ** 31 - upper_bound = 2 ** 31 + lower_bound = -(2**31) + upper_bound = 2**31 def __init__(self, device, info, clipping=True): super().__init__(device, info) @@ -1026,9 +1042,13 @@ def __init__(self, device, info, clipping=True): 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__}") + 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__}") + 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}" @@ -1047,7 +1067,9 @@ def _convert_write(self, value): pass else: return v - raise ValueError(f"Failed to coerce {value.__class__.__name__} '{value}' to int") + raise ValueError( + f"Failed to coerce {value.__class__.__name__} '{value}' to int" + ) def _mangle_write(self, value): if self.clipping: @@ -1057,16 +1079,20 @@ def _mangle_write(self, value): return self.maximum else: if value < self.minimum: - raise ValueError(f"Control {self.config_name}: {value} exceeds allowed minimum {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}") + raise ValueError( + f"Control {self.config_name}: {value} exceeds allowed maximum {self.maximum}" + ) return value def increase(self, steps: int = 1): - self.value += (steps * self.step) + self.value += steps * self.step def decrease(self, steps: int = 1): - self.value -= (steps * self.step) + self.value -= steps * self.step def set_to_minimum(self): self.value = self.minimum @@ -1076,13 +1102,13 @@ def set_to_maximum(self): class IntegerControl(BaseNumericControl): - lower_bound = -2 ** 31 - upper_bound = 2 ** 31 + lower_bound = -(2**31) + upper_bound = 2**31 class Integer64Control(BaseNumericControl): - lower_bound = -2 ** 63 - upper_bound = 2 ** 63 + lower_bound = -(2**63) + upper_bound = 2**63 class BooleanControl(BaseControl): @@ -1107,7 +1133,9 @@ def _convert_write(self, value): pass else: return v - raise ValueError(f"Failed to coerce {value.__class__.__name__} '{value}' to bool") + raise ValueError( + f"Failed to coerce {value.__class__.__name__} '{value}' to bool" + ) class MenuControl(BaseControl, UserDict): @@ -1126,7 +1154,9 @@ def __init__(self, device, info): 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}") + 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) @@ -1186,10 +1216,10 @@ def is_disabled(self) -> bool: return self.is_flagged_disabled def increase(self, steps: int = 1): - self.value += (steps * self.step) + self.value += steps * self.step def decrease(self, steps: int = 1): - self.value -= (steps * self.step) + self.value -= steps * self.step class DeviceHelper: From d485978202c0a20925690d1b9707a1351bf1e6e7 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 28 Apr 2023 22:19:00 +0200 Subject: [PATCH 37/45] Correct the upper_bound values as per https://github.com/tiagocoutinho/v4l2py/pull/42#discussion_r1180649581 --- v4l2py/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 0b605f0..b991a44 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -1032,7 +1032,7 @@ def set_to_default(self): class BaseNumericControl(BaseControl): lower_bound = -(2**31) - upper_bound = 2**31 + upper_bound = 2**31 - 1 def __init__(self, device, info, clipping=True): super().__init__(device, info) @@ -1103,12 +1103,12 @@ def set_to_maximum(self): class IntegerControl(BaseNumericControl): lower_bound = -(2**31) - upper_bound = 2**31 + upper_bound = 2**31 - 1 class Integer64Control(BaseNumericControl): lower_bound = -(2**63) - upper_bound = 2**63 + upper_bound = 2**63 - 1 class BooleanControl(BaseControl): From e910562a462ee678b09cc70e1b6d5e19c5d3e748 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 29 Apr 2023 15:22:59 +0200 Subject: [PATCH 38/45] Initial implementation of U8/U16/U32Control --- v4l2py/device.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/v4l2py/device.py b/v4l2py/device.py index b991a44..fe1486d 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -793,6 +793,9 @@ def from_device(cls, device): ControlType.INTEGER64: Integer64Control, ControlType.MENU: MenuControl, ControlType.INTEGER_MENU: MenuControl, + ControlType.U8: U8Control, + ControlType.U16: U16Control, + ControlType.U32: U32Control, } ctrl_dict = dict() @@ -1111,6 +1114,21 @@ class Integer64Control(BaseNumericControl): 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(BaseControl): _true = ["true", "1", "yes", "on", "enable"] _false = ["false", "0", "no", "off", "disable"] From 22c17d9f2900ba79b9f9d6120018d1a7561fbc4f Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Sat, 29 Apr 2023 15:59:23 +0200 Subject: [PATCH 39/45] Initial "implementation" of GenericControl. Use this now instead of LegacyControl for control types not found in ctrl_type_map. --- v4l2py/device.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index fe1486d..3821c23 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -801,7 +801,7 @@ def from_device(cls, device): for ctrl in device.info.controls: ctrl_type = ControlType(ctrl.type) - ctrl_class = ctrl_type_map.get(ctrl_type, LegacyControl) + ctrl_class = ctrl_type_map.get(ctrl_type, GenericControl) ctrl_dict[ctrl.id] = ctrl_class(device, ctrl) return cls(ctrl_dict) @@ -1033,6 +1033,10 @@ def set_to_default(self): self.value = self.default +class GenericControl(BaseControl): + pass + + class BaseNumericControl(BaseControl): lower_bound = -(2**31) upper_bound = 2**31 - 1 From 654e47d2f730bec73cf0e707e6c7e120c5ec9680 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Tue, 2 May 2023 22:33:36 +0200 Subject: [PATCH 40/45] Reduce BaseControl to the bare minimum of control features, and move the rest to the new derived class BaseMonoControl. The latter is more or less a reprise of BaseSingleControl, which had been removed in 5640d4e. It's similar but not identical, and used to prepare for the upcoming ButtonControl. --- v4l2py/device.py | 101 +++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index 3821c23..cd2ffdf 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -898,10 +898,6 @@ def __repr__(self): if addrepr: repr += f" {addrepr}" - repr += f" default={self.default}" - if not self.is_flagged_write_only: - repr += f" value={self.value}" - flags = [ flag.name.lower() for flag in ControlFlag @@ -926,46 +922,6 @@ def config_name(self) -> str: self._config_name = res return self._config_name - def _convert_read(self, value): - return value - - @property - def default(self): - return self._convert_read(self._info.default_value) - - @property - def value(self): - if not self.is_flagged_write_only: - v = get_control(self.device, self.id) - 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: - 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)}" - ) - v = self._convert_write(value) - v = self._mangle_write(v) - set_control(self.device, self.id, v) - @property def is_flagged_disabled(self) -> bool: return (self._info.flags & ControlFlag.DISABLED) == ControlFlag.DISABLED @@ -1029,15 +985,63 @@ def is_writeable(self) -> bool: 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 not self.is_flagged_write_only: + v = get_control(self.device, self.id) + 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: + 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)}" + ) + v = self._convert_write(value) + v = self._mangle_write(v) + set_control(self.device, self.id, v) + def set_to_default(self): self.value = self.default -class GenericControl(BaseControl): +class GenericControl(BaseMonoControl): pass -class BaseNumericControl(BaseControl): +class BaseNumericControl(BaseMonoControl): lower_bound = -(2**31) upper_bound = 2**31 - 1 @@ -1059,6 +1063,7 @@ def __init__(self, device, info, clipping=True): 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): @@ -1133,7 +1138,7 @@ class U32Control(BaseNumericControl): upper_bound = 2**32 -class BooleanControl(BaseControl): +class BooleanControl(BaseMonoControl): _true = ["true", "1", "yes", "on", "enable"] _false = ["false", "0", "no", "off", "disable"] @@ -1160,7 +1165,7 @@ def _convert_write(self, value): ) -class MenuControl(BaseControl, UserDict): +class MenuControl(BaseMonoControl, UserDict): def __init__(self, device, info): BaseControl.__init__(self, device, info) UserDict.__init__(self) From da53ed6521d54e7ff4fd2cb31eaf0640b79796e1 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Tue, 2 May 2023 23:15:26 +0200 Subject: [PATCH 41/45] Initial implementation of ButtonControl. Not yet tested, since I have no real device that provides a button control and trying vivid did not work (see otaku42/v4l2py#17). --- v4l2py/device.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/v4l2py/device.py b/v4l2py/device.py index cd2ffdf..a897317 100644 --- a/v4l2py/device.py +++ b/v4l2py/device.py @@ -911,6 +911,26 @@ def __repr__(self): 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) -> str: if self._config_name is None: @@ -1003,7 +1023,7 @@ def default(self): @property def value(self): if not self.is_flagged_write_only: - v = get_control(self.device, self.id) + v = self._get_control() return self._convert_read(v) else: return None @@ -1016,22 +1036,9 @@ def _mangle_write(self, value): @value.setter def value(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)}" - ) v = self._convert_write(value) v = self._mangle_write(v) - set_control(self.device, self.id, v) + self._set_control(v) def set_to_default(self): self.value = self.default @@ -1189,6 +1196,11 @@ 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() From a9d5f3a15c4ed89d2078e8d92f30bdd5773dc0b4 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Wed, 3 May 2023 00:04:49 +0200 Subject: [PATCH 42/45] Add new compound control types as per kernel v6.3. Closes otaku42/v4l2py#13. --- v4l2py/raw.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 ( From 67a6e3b8c0c6b26ba00aa99e51eb813c24e27e5e Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 19 May 2023 02:01:34 +0200 Subject: [PATCH 43/45] Make the web example work with Python < 3.10 (again). Closes otaku42/v4l2py#21. --- examples/web/common.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/examples/web/common.py b/examples/web/common.py index 9e87cfc..3216bea 100644 --- a/examples/web/common.py +++ b/examples/web/common.py @@ -35,22 +35,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) From 398f9b3646a2e07f7d58c27f79809fc9a8726233 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 19 May 2023 02:45:32 +0200 Subject: [PATCH 44/45] Update installation instructions for extra dependencies: OpenCV package is named opencv-python; call pip as module, as suggested in the pip manual. Closes otaku42/v4l2py#22. --- examples/qt/widget.py | 7 +++++-- examples/web.py | 3 +++ examples/web/async.py | 3 ++- examples/web/common.py | 6 ------ examples/web/sync.py | 2 +- examples/web_async.py | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) 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/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..af5ee50 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 diff --git a/examples/web/common.py b/examples/web/common.py index 3216bea..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 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 From a98df12f1cdc84ec8d4ba169c9e1d86bcba2c880 Mon Sep 17 00:00:00 2001 From: Michael Renzmann Date: Fri, 19 May 2023 21:52:31 +0200 Subject: [PATCH 45/45] Fix error reported by @tiagocoutinho at tiagocoutinho/v4l2py#42. --- examples/web/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/web/async.py b/examples/web/async.py index af5ee50..e0cd1fe 100644 --- a/examples/web/async.py +++ b/examples/web/async.py @@ -89,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