Skip to content

Commit da7e73b

Browse files
authored
fix(v4l2):: show bool controls and fix control name mismatch with Motion (#3391)
v4l2-ctl reports bool controls without min/max, so they were filtered out before reaching the UI and never rendered as a checkbox. Capture the control type from the parenthesized field and default bool controls to min=0 and max=1. Control values also often never reached the camera. Motion matches video_params entries against the driver's raw control name, which v4l2-ctl never prints: it collapses runs of non-alphanumeric characters into underscores, so "White Balance, Automatic" is shown as "white_balance_automatic". That substitution cannot be reversed, and on a typical UVC webcam it breaks every control whose driver name is more than one word -- half of them on the camera this was found on. Write the control ID instead, in Motion's own "ID%08d" format, which Motion builds and matches on in video_v4l2.c. IDs are stable UAPI constants, whereas control names have changed between kernel versions. Matching by ID also survives Motion upgrades: 5.x made name matching case-sensitive, breaking even the single-word names that work on 4.6. Existing configs with plain names are read back unchanged and migrate to IDs on the next save.
1 parent 2d62cce commit da7e73b

2 files changed

Lines changed: 35 additions & 8 deletions

File tree

motioneye/config.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,12 +1120,16 @@ def motion_camera_ui_to_dict(ui, prev_config=None):
11201120
threshold = int(float(ui['frame_change_threshold']) * width * height / 100)
11211121

11221122
if proto == 'v4l2':
1123-
# video controls
1124-
vid_control_params = (
1125-
('{}={}'.format(n, c['value']))
1126-
for n, c in list(ui['video_controls'].items())
1127-
)
1128-
data['vid_control_params'] = ','.join(vid_control_params)
1123+
# video controls, control_id is the ID Motion actually matches against (see v4l2-ctl --list-ctrls)
1124+
video_controls = v4l2ctl.list_ctrls(prev_config.get('videodevice', ''))
1125+
if video_controls:
1126+
vid_control_params = (
1127+
'{}={}'.format(
1128+
video_controls.get(n, {}).get('control_id', n), c['value']
1129+
)
1130+
for n, c in list(ui['video_controls'].items())
1131+
)
1132+
data['vid_control_params'] = ','.join(vid_control_params)
11291133

11301134
else: # assuming netcam
11311135
if match(
@@ -1682,6 +1686,12 @@ def motion_camera_dict_to_ui(data): # noqa: C901
16821686
ui['resolution'] = str(data['width']) + 'x' + str(data['height'])
16831687

16841688
video_controls = v4l2ctl.list_ctrls(data['videodevice'])
1689+
1690+
# video_params is keyed by control_id, map it back to our UI name
1691+
name_by_control_id = {
1692+
c.get('control_id', n): n for n, c in video_controls.items()
1693+
}
1694+
16851695
video_controls = [
16861696
(n, c)
16871697
for (n, c) in list(video_controls.items())
@@ -1701,6 +1711,7 @@ def motion_camera_dict_to_ui(data): # noqa: C901
17011711
else:
17021712
continue # ignore any other kind of param
17031713

1714+
name = name_by_control_id.get(name, name)
17041715
vid_control_values[name] = value
17051716

17061717
ui['video_controls'] = {

motioneye/controls/v4l2ctl.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,30 @@ def list_ctrls(device):
179179
if not line:
180180
continue
181181

182-
match = re.match(r'^\s*(\w+)\s+([a-f0-9x\s]+)?\(\w+\)\s*:\s*(.+)\s*', line)
182+
match = re.match(r'^\s*(\w+)\s+([a-f0-9x\s]+)?\((\w+)\)\s*:\s*(.+)\s*', line)
183183
if not match:
184184
continue
185185

186-
control, _, properties = match.groups()
186+
control, id_hex, ctrl_type, properties = match.groups()
187187
properties = dict(
188188
[v.split('=', 1) for v in properties.split(' ') if v.count('=')]
189189
)
190+
if ctrl_type == 'bool':
191+
# v4l2-ctl --list-ctrls gives no min/max for bool controls, so set them here
192+
properties.setdefault('min', '0')
193+
properties.setdefault('max', '1')
194+
195+
# Motion matches controls by ID. We use ID because the driver's raw
196+
# name doesn't always match the name v4l2-ctl gives (e.g. driver:
197+
# "White Balance, Automatic" vs. v4l2-ctl: "white_balance_automatic")
198+
if id_hex:
199+
try:
200+
properties['control_id'] = f'ID{int(id_hex, 16):08d}'
201+
except ValueError:
202+
logging.warning(
203+
f'could not parse v4l2 control id "{id_hex}" for "{control}"'
204+
)
205+
190206
controls[control] = properties
191207

192208
_ctrls_cache[device] = controls

0 commit comments

Comments
 (0)