Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further improval of controls #42

Merged
merged 47 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
000696e
Control: change is_readable/is_writable to is_writeonly/is_readonly.
otaku42 Mar 22, 2023
b798ed8
Control: update is_... methods/properties to actually return bools
otaku42 Mar 22, 2023
6144213
Control: reimplement method is_writeable, with correct behaviour.
otaku42 Mar 22, 2023
b8d3a15
Controls are also not writeable while grabbed or when disabled.
otaku42 Mar 24, 2023
88cabd5
Fix flake8 complaint
otaku42 Mar 24, 2023
d5e9dde
Controls: add alternative constructor from_device.
otaku42 Mar 24, 2023
f8629d9
Remove stand-alone function config_name; the functionality has been m…
otaku42 Mar 25, 2023
79fd14d
Control: two minor changes to flags gathering in __repr__ to satisfy …
otaku42 Mar 25, 2023
20a8cda
Begin implementation of inheritance-based class model for controls.
otaku42 Mar 28, 2023
dea5cbe
Adjust example output in README.md to the changes in commit 20a8cda
otaku42 Mar 28, 2023
5640d4e
Dissolve BaseSingleControl into BaseControl and BaseNumericControl.
otaku42 Apr 5, 2023
2b57f1c
Add support for checking all currently defined control flags.
otaku42 Apr 5, 2023
2d68a2c
is_writeonly now is called is_flagged_write_only
otaku42 Apr 7, 2023
5580d4b
After default has been moved to BaseControl, it should be part BaseCo…
otaku42 Apr 7, 2023
685acc2
Make default a property; this is required for an upcoming change
otaku42 Apr 7, 2023
99d79b3
Use actual control type in this exception message
otaku42 Apr 7, 2023
ee38673
Introduce some 'hooks' meant to be overwritten by upcoming subclasses
otaku42 Apr 7, 2023
ff66c02
These checks need to be moved to BaseNumericControl, since BaseContro…
otaku42 Apr 7, 2023
395f4ec
Introduce LegacyControls as backward-compatible controls 'factory', w…
otaku42 Apr 7, 2023
877709c
Controls: introduce a ControlType to class mapping, to support instan…
otaku42 Apr 8, 2023
ed23474
Initial implementation of class BooleanControl.
otaku42 Apr 8, 2023
6c9532f
BaseNumericControl: make clipping of written values to minimum/maximu…
otaku42 Apr 8, 2023
bd9dd8b
BaseControl: value to be written should be converted to target type b…
otaku42 Apr 8, 2023
608954e
Initial implementation of classes IntegerControl and Integer64Control.
otaku42 Apr 8, 2023
7e47ec0
iter_read_menu(): use ctrl._info to support all subclasses of BaseCon…
otaku42 Apr 17, 2023
6e0f5f6
Initial implementation of class MenuControl.
otaku42 Apr 17, 2023
bbac364
Add some explanation of and demonstration for the improved controls API.
otaku42 Apr 17, 2023
832a4a2
BaseControl: fix checks for flags being set
otaku42 Apr 20, 2023
d2fc7ff
Controls: add set_clipping(), allows clipping to be changed for all n…
otaku42 Apr 20, 2023
9392f16
MenuControl: menu controls expect written values to be integers
otaku42 Apr 20, 2023
43c33e7
Add example to demonstrate usage of controls
otaku42 Apr 20, 2023
d18bf00
Use new control interface by default now.
otaku42 Apr 20, 2023
430d947
Fix flake8's 'over-indented' complaint.
otaku42 Apr 20, 2023
d6aa811
Update examples in README.md for new control interface.
otaku42 Apr 20, 2023
8e4ca97
Add link to v4l2py-ctl.py example
otaku42 Apr 22, 2023
10eadd4
Merge remote-tracking branch 'upstream/master' into improve_controls
otaku42 Apr 25, 2023
62bef57
Apply some black magic
otaku42 Apr 25, 2023
d485978
Correct the upper_bound values as per https://github.com/tiagocoutinh…
otaku42 Apr 28, 2023
e910562
Initial implementation of U8/U16/U32Control
otaku42 Apr 29, 2023
22c17d9
Initial "implementation" of GenericControl. Use this now instead of L…
otaku42 Apr 29, 2023
654e47d
Reduce BaseControl to the bare minimum of control features, and move
otaku42 May 2, 2023
da53ed6
Initial implementation of ButtonControl. Not yet tested, since I have
otaku42 May 2, 2023
a9d5f3a
Add new compound control types as per kernel v6.3. Closes otaku42/v4l…
otaku42 May 2, 2023
93fc9b3
Merge branch 'tiagocoutinho:master' into improve_controls
otaku42 May 15, 2023
67a6e3b
Make the web example work with Python < 3.10 (again). Closes otaku42/…
otaku42 May 19, 2023
398f9b3
Update installation instructions for extra dependencies: OpenCV package
otaku42 May 19, 2023
a98df12
Fix error reported by @tiagocoutinho at tiagocoutinho/v4l2py#42.
otaku42 May 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 112 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,35 +87,37 @@ Getting information about the device:
Format(width=640, height=480, pixelformat=<PixelFormat.MJPEG: 1196444237>}

>>> for ctrl in cam.controls.values(): print(ctrl)
<Control brightness type=integer min=0 max=255 step=1 default=128 value=64>
<Control contrast type=integer min=0 max=255 step=1 default=32 value=32>
<Control saturation type=integer min=0 max=100 step=1 default=64 value=64>
<Control hue type=integer min=-180 max=180 step=1 default=0 value=0>
<Control white_balance_automatic type=boolean default=1 value=1>
<Control gamma type=integer min=90 max=150 step=1 default=120 value=120>
<Control gain type=integer min=1 max=7 step=1 default=1 value=1>
<Control power_line_frequency type=menu min=0 max=2 step=1 default=2 value=2>
<Control white_balance_temperature type=integer min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<Control sharpness type=integer min=0 max=7 step=1 default=2 value=2>
<Control backlight_compensation type=integer min=0 max=1 step=1 default=0 value=0>
<Control auto_exposure type=menu min=0 max=3 step=1 default=3 value=3>
<Control exposure_time_absolute type=integer min=10 max=333 step=1 default=156 value=156 flags=inactive>
<Control exposure_dynamic_framerate type=boolean default=0 value=1>```
<IntegerControl brightness min=0 max=255 step=1 default=128 value=128>
<IntegerControl contrast min=0 max=255 step=1 default=32 value=32>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=64>
<IntegerControl hue min=-180 max=180 step=1 default=0 value=0>
<BooleanControl white_balance_automatic default=True value=True>
<IntegerControl gamma min=90 max=150 step=1 default=120 value=120>
<MenuControl power_line_frequency default=1 value=1>
<IntegerControl white_balance_temperature min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<IntegerControl sharpness min=0 max=7 step=1 default=2 value=2>
<IntegerControl backlight_compensation min=0 max=2 step=1 default=1 value=1>
<MenuControl auto_exposure default=3 value=3>
<IntegerControl exposure_time_absolute min=4 max=1250 step=1 default=156 value=156 flags=inactive>
<BooleanControl exposure_dynamic_framerate default=False value=False>

>>> cam.controls["saturation"]
<Control saturation type=integer min=0 max=100 step=1 default=64 value=64>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=64>

>>> cam.controls["saturation"].id
9963778
>>> cam.controls[9963778]
<Control saturation type=integer min=0 max=100 step=1 default=64 value=64>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=64>

>>> cam.controls.brightness
<Control brightness type=integer min=0 max=255 step=1 default=128 value=64>
>>> cam.controls.brightness.value = 128
<IntegerControl brightness min=0 max=255 step=1 default=128 value=128>
>>> cam.controls.brightness.value = 64
>>> cam.controls.brightness
<Control brightness type=integer min=0 max=255 step=1 default=128 value=128>
<IntegerControl brightness min=0 max=255 step=1 default=128 value=64>
```

(see also [v4l2py-ctl](examples/v4l2py-ctl.py) example)

### asyncio

v4l2py is asyncio friendly:
Expand Down Expand Up @@ -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}")
<Control brightness type=integer min=0 max=255 step=1 default=128 value=255>
<Control contrast type=integer min=0 max=255 step=1 default=32 value=255>
<Control saturation type=integer min=0 max=100 step=1 default=64 value=100>
<Control hue type=integer min=-180 max=180 step=1 default=0 value=0>
<Control white_balance_automatic type=boolean min=0 max=1 step=1 default=1 value=1>
<Control gamma type=integer min=90 max=150 step=1 default=120 value=150>
<Control gain type=integer min=1 max=7 step=1 default=1 value=1>
<Control power_line_frequency type=menu min=0 max=2 step=1 default=2 value=2>
- 0: Disabled
- 1: 50 Hz
- 2: 60 Hz
<Control white_balance_temperature type=integer min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<Control sharpness type=integer min=0 max=7 step=1 default=2 value=7>
<Control backlight_compensation type=integer min=0 max=1 step=1 default=0 value=1>
<Control auto_exposure type=menu min=0 max=3 step=1 default=3 value=3>
- 1: Manual Mode
- 3: Aperture Priority Mode
<Control exposure_time_absolute type=integer min=10 max=333 step=1 default=156 value=156 flags=inactive>
<Control exposure_dynamic_framerate type=boolean min=0 max=1 step=1 default=0 value=1>

>>> type(cam.controls.exposure_dynamic_framerate.value)
<class 'int'>
```

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}")
<IntegerControl brightness min=0 max=255 step=1 default=128 value=255>
<IntegerControl contrast min=0 max=255 step=1 default=32 value=255>
<IntegerControl saturation min=0 max=100 step=1 default=64 value=100>
<IntegerControl hue min=-180 max=180 step=1 default=0 value=0>
<BooleanControl white_balance_automatic default=True value=True>
<IntegerControl gamma min=90 max=150 step=1 default=120 value=150>
<IntegerControl gain min=1 max=7 step=1 default=1 value=1>
<MenuControl power_line_frequency default=2 value=2>
- 0: Disabled
- 1: 50 Hz
- 2: 60 Hz
<IntegerControl white_balance_temperature min=2800 max=6500 step=1 default=4000 value=4000 flags=inactive>
<IntegerControl sharpness min=0 max=7 step=1 default=2 value=7>
<IntegerControl backlight_compensation min=0 max=1 step=1 default=0 value=1>
<MenuControl auto_exposure default=3 value=3>
- 1: Manual Mode
- 3: Aperture Priority Mode
<IntegerControl exposure_time_absolute min=10 max=333 step=1 default=156 value=156 flags=inactive>
<BooleanControl exposure_dynamic_framerate default=False value=True>

>>> type(cam.controls.white_balance_automatic.value)
<class 'bool'>
>>> cam.controls.white_balance_automatic.value
<BooleanControl white_balance_automatic default=True value=True>
>>> cam.controls.white_balance_automatic.value = False
<BooleanControl white_balance_automatic default=True value=False>

>>> wba = cam.controls.white_balance_automatic
>>> wba.value = "enable" # or "on", "1", "true", "yes"
>>> wba
<BooleanControl white_balance_automatic default=True value=True>
>>> wba.value = "off" # or "disable", "0", "false", "no"
>>> wba
<BooleanControl white_balance_automatic default=True value=False>
```

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.
Expand Down
200 changes: 200 additions & 0 deletions examples/v4l2py-ctl.py
Original file line number Diff line number Diff line change
@@ -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="<dev>",
help="use device <dev> instead of /dev/video0; if <dev> starts with a digit, then /dev/video<dev> is used",
)
parser.add_argument(
"--get-ctrl",
type=csv,
default=[],
metavar="<ctrl>[,<ctrl>...]",
help="get the values of the specified controls",
)
parser.add_argument(
"--set-ctrl",
type=csv,
default=[],
metavar="<ctrl>=<val>[,<ctrl>=<val>...]",
help="set the values of the specified controls",
)
parser.add_argument(
"--reset-ctrl",
type=csv,
default=[],
metavar="<ctrl>[,<ctrl>...]",
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.")
Loading