libcamera/utils/gen-controls.py
Naushir Patuck bd6658943a controls: Add vendor control/property support to generation scripts
Add support for vendor-specific controls and properties to libcamera.
The controls/properties are defined by a "vendor" tag in the YAML
control description file, for example:

vendor: rpi
controls:
  - MyExampleControl:
      type: string
      description: |
        Test for libcamera vendor-specific controls.

This will now generate a control id in the libcamera::controls::rpi
namespace, ensuring no id conflict between different vendors, core or
draft libcamera controls. Similarly, a ControlIdMap control is generated
in the libcamera::controls::rpi namespace.

A #define LIBCAMERA_HAS_RPI_VENDOR_CONTROLS is also generated to allow
applications to conditionally compile code if the specific vendor
controls are present. For the python bindings, the control is available
with libcamera.controls.rpi.MyExampleControl. The above controls
example applies similarly to properties.

Existing libcamera controls defined in control_ids.yaml are given the
"libcamera" vendor tag.

A new --mode flag is added to gen-controls.py to specify the mode of
operation, either 'controls' or 'properties' to allow the code generator
to correctly set the #define string.

As a drive-by, sort and redefine the output command line argument in
gen-controls.py and gen-py-controls.py to ('--output', '-o') for
consistency.

Signed-off-by: Naushir Patuck <naush@raspberrypi.com>
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
2023-11-29 09:05:37 +00:00

375 lines
11 KiB
Python
Executable file

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2019, Google Inc.
#
# Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
#
# gen-controls.py - Generate control definitions from YAML
import argparse
from functools import reduce
import operator
import string
import sys
import yaml
class ControlEnum(object):
def __init__(self, data):
self.__data = data
@property
def description(self):
"""The enum description"""
return self.__data.get('description')
@property
def name(self):
"""The enum name"""
return self.__data.get('name')
@property
def value(self):
"""The enum value"""
return self.__data.get('value')
class Control(object):
def __init__(self, name, data, vendor):
self.__name = name
self.__data = data
self.__enum_values = None
self.__size = None
self.__vendor = vendor
enum_values = data.get('enum')
if enum_values is not None:
self.__enum_values = [ControlEnum(enum) for enum in enum_values]
size = self.__data.get('size')
if size is not None:
if len(size) == 0:
raise RuntimeError(f'Control `{self.__name}` size must have at least one dimension')
# Compute the total number of elements in the array. If any of the
# array dimension is a string, the array is variable-sized.
num_elems = 1
for dim in size:
if type(dim) is str:
num_elems = 0
break
dim = int(dim)
if dim <= 0:
raise RuntimeError(f'Control `{self.__name}` size must have positive values only')
num_elems *= dim
self.__size = num_elems
@property
def description(self):
"""The control description"""
return self.__data.get('description')
@property
def enum_values(self):
"""The enum values, if the control is an enumeration"""
if self.__enum_values is None:
return
for enum in self.__enum_values:
yield enum
@property
def is_enum(self):
"""Is the control an enumeration"""
return self.__enum_values is not None
@property
def is_draft(self):
"""Is the control a draft control"""
return self.__data.get('draft') is not None
@property
def vendor(self):
"""The vendor string, or None"""
return self.__vendor
@property
def name(self):
"""The control name (CamelCase)"""
return self.__name
@property
def q_name(self):
"""The control name, qualified with a namespace"""
ns = 'draft::' if self.is_draft else ''
return ns + self.__name
@property
def type(self):
typ = self.__data.get('type')
size = self.__data.get('size')
if typ == 'string':
return 'std::string'
if self.__size is None:
return typ
if self.__size:
return f"Span<const {typ}, {self.__size}>"
else:
return f"Span<const {typ}>"
def snake_case(s):
return ''.join([c.isupper() and ('_' + c) or c for c in s]).strip('_')
def format_description(description):
description = description.strip('\n').split('\n')
description[0] = '\\brief ' + description[0]
return '\n'.join([(line and ' * ' or ' *') + line for line in description])
def generate_cpp(controls):
enum_doc_start_template = string.Template('''/**
* \\enum ${name}Enum
* \\brief Supported ${name} values''')
enum_doc_value_template = string.Template(''' * \\var ${value}
${description}''')
doc_template = string.Template('''/**
* \\var ${name}
${description}
*/''')
def_template = string.Template('extern const Control<${type}> ${name}(${id_name}, "${name}");')
enum_values_doc = string.Template('''/**
* \\var ${name}Values
* \\brief List of all $name supported values
*/''')
enum_values_start = string.Template('''extern const std::array<const ControlValue, ${size}> ${name}Values = {''')
enum_values_values = string.Template('''\tstatic_cast<int32_t>(${name}),''')
ctrls_doc = {}
ctrls_def = {}
ctrls_map = []
for ctrl in controls:
id_name = snake_case(ctrl.name).upper()
vendor = 'draft' if ctrl.is_draft else ctrl.vendor
if vendor not in ctrls_doc:
ctrls_doc[vendor] = []
ctrls_def[vendor] = []
info = {
'name': ctrl.name,
'type': ctrl.type,
'description': format_description(ctrl.description),
'id_name': id_name,
}
target_doc = ctrls_doc[vendor]
target_def = ctrls_def[vendor]
if ctrl.is_enum:
enum_doc = []
enum_doc.append(enum_doc_start_template.substitute(info))
num_entries = 0
for enum in ctrl.enum_values:
value_info = {
'name': ctrl.name,
'value': enum.name,
'description': format_description(enum.description),
}
enum_doc.append(enum_doc_value_template.substitute(value_info))
num_entries += 1
enum_doc = '\n *\n'.join(enum_doc)
enum_doc += '\n */'
target_doc.append(enum_doc)
values_info = {
'name': info['name'],
'size': num_entries,
}
target_doc.append(enum_values_doc.substitute(values_info))
target_def.append(enum_values_start.substitute(values_info))
for enum in ctrl.enum_values:
value_info = {
'name': enum.name
}
target_def.append(enum_values_values.substitute(value_info))
target_def.append("};")
target_doc.append(doc_template.substitute(info))
target_def.append(def_template.substitute(info))
ctrls_map.append('\t{ ' + id_name + ', &' + ctrl.q_name + ' },')
vendor_ctrl_doc_sub = []
vendor_ctrl_template = string.Template('''
/**
* \\brief Namespace for ${vendor} controls
*/
namespace ${vendor} {
${vendor_controls_str}
} /* namespace ${vendor} */''')
for vendor in [v for v in ctrls_doc.keys() if v not in ['libcamera', 'draft']]:
vendor_ctrl_doc_sub.append(vendor_ctrl_template.substitute({'vendor': vendor, 'vendor_controls_str': '\n\n'.join(ctrls_doc[vendor])}))
vendor_ctrl_def_sub = []
for vendor in [v for v in ctrls_def.keys() if v not in ['libcamera', 'draft']]:
vendor_ctrl_def_sub.append(vendor_ctrl_template.substitute({'vendor': vendor, 'vendor_controls_str': '\n'.join(ctrls_def[vendor])}))
return {
'controls_doc': '\n\n'.join(ctrls_doc['libcamera']),
'controls_def': '\n'.join(ctrls_def['libcamera']),
'draft_controls_doc': '\n\n'.join(ctrls_doc['draft']),
'draft_controls_def': '\n\n'.join(ctrls_def['draft']),
'controls_map': '\n'.join(ctrls_map),
'vendor_controls_doc': '\n'.join(vendor_ctrl_doc_sub),
'vendor_controls_def': '\n'.join(vendor_ctrl_def_sub),
}
def generate_h(controls, mode):
enum_template_start = string.Template('''enum ${name}Enum {''')
enum_value_template = string.Template('''\t${name} = ${value},''')
enum_values_template = string.Template('''extern const std::array<const ControlValue, ${size}> ${name}Values;''')
template = string.Template('''extern const Control<${type}> ${name};''')
ctrls = {}
ids = {}
id_value = {}
for ctrl in controls:
id_name = snake_case(ctrl.name).upper()
vendor = 'draft' if ctrl.is_draft else ctrl.vendor
if vendor not in ctrls:
ids[vendor] = []
id_value[vendor] = 1
ctrls[vendor] = []
# Core and draft controls use the same ID value
target_ids = ids['libcamera'] if vendor in ['libcamera', 'draft'] else ids[vendor]
target_ids.append('\t' + id_name + ' = ' + str(id_value[vendor]) + ',')
info = {
'name': ctrl.name,
'type': ctrl.type,
}
target_ctrls = ctrls['libcamera']
if ctrl.is_draft:
target_ctrls = ctrls['draft']
elif vendor != 'libcamera':
target_ctrls = ctrls[vendor]
if ctrl.is_enum:
target_ctrls.append(enum_template_start.substitute(info))
num_entries = 0
for enum in ctrl.enum_values:
value_info = {
'name': enum.name,
'value': enum.value,
}
target_ctrls.append(enum_value_template.substitute(value_info))
num_entries += 1
target_ctrls.append("};")
values_info = {
'name': info['name'],
'size': num_entries,
}
target_ctrls.append(enum_values_template.substitute(values_info))
target_ctrls.append(template.substitute(info))
id_value[vendor] += 1
vendor_template = string.Template('''
namespace ${vendor} {
#define LIBCAMERA_HAS_${vendor_def}_VENDOR_${mode}
enum {
${vendor_enums}
};
${vendor_controls}
} /* namespace ${vendor} */
''')
vendor_sub = []
for vendor in [v for v in ctrls.keys() if v not in ['libcamera', 'draft']]:
vendor_sub.append(vendor_template.substitute({'mode': mode.upper(),
'vendor': vendor,
'vendor_def': vendor.upper(),
'vendor_enums': '\n'.join(ids[vendor]),
'vendor_controls': '\n'.join(ctrls[vendor])}))
return {
'ids': '\n'.join(ids['libcamera']),
'controls': '\n'.join(ctrls['libcamera']),
'draft_controls': '\n'.join(ctrls['draft']),
'vendor_controls': '\n'.join(vendor_sub)
}
def fill_template(template, data):
template = open(template, 'rb').read()
template = template.decode('utf-8')
template = string.Template(template)
return template.substitute(data)
def main(argv):
# Parse command line arguments
parser = argparse.ArgumentParser()
parser.add_argument('--mode', '-m', type=str, required=True, choices=['controls', 'properties'],
help='Mode of operation')
parser.add_argument('--output', '-o', metavar='file', type=str,
help='Output file name. Defaults to standard output if not specified.')
parser.add_argument('input', type=str,
help='Input file name.')
parser.add_argument('template', type=str,
help='Template file name.')
args = parser.parse_args(argv[1:])
data = open(args.input, 'rb').read()
vendor = yaml.safe_load(data)['vendor']
controls = yaml.safe_load(data)['controls']
controls = [Control(*ctrl.popitem(), vendor) for ctrl in controls]
if args.template.endswith('.cpp.in'):
data = generate_cpp(controls)
elif args.template.endswith('.h.in'):
data = generate_h(controls, args.mode)
else:
raise RuntimeError('Unknown template type')
data = fill_template(args.template, data)
if args.output:
output = open(args.output, 'wb')
output.write(data.encode('utf-8'))
output.close()
else:
sys.stdout.write(data)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))