[{"id":31797,"web_url":"https://patchwork.libcamera.org/comment/31797/","msgid":"<172926120694.3353069.16813507965031089089@ping.linuxembedded.co.uk>","date":"2024-10-18T14:20:06","subject":"Re: [PATCH v3 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Jaslo Ziska (2024-10-17 18:04:15)\n> This commit implements gstreamer controls for the libcamera element by\n> generating the controls from the control_ids_*.yaml files using a new\n> gen-gst-controls.py script. The appropriate meson files are also changed\n> to automatically run the script when building.\n> \n> The gen-gst-controls.py script works similar to the gen-controls.py\n> script by parsing the control_ids_*.yaml files and generating C++ code\n> for each exposed control.\n> For the controls to be used as gstreamer properties the type for each\n> control needs to be translated to the appropriate glib type and a\n> GEnumValue is generated for each enum control. Then a\n> g_object_install_property(), _get_property() and _set_property()\n> function is generated for each control.\n> The vendor controls get prefixed with \"$vendor-\" in the final gstreamer\n> property name.\n> \n> The C++ code generated by the gen-gst-controls.py script is written into\n> the template gstlibcamerasrc-controls.cpp.in file. The matching\n> gstlibcamerasrc-controls.h header defines the GstCameraControls class\n> which handles the installation of the gstreamer properties as well as\n> keeping track of the control values and setting and getting the\n> controls. The content of these functions is generated in the Python\n> script.\n> \n> Finally the libcamerasrc element itself is edited to make use of the new\n> GstCameraControls class. The way this works is by defining a PROP_LAST\n> enum variant which is passed to the installProperties() function so the\n> properties are defined with the appropriate offset. When getting or\n> setting a property PROP_LAST is subtracted from the requested property\n> to translate the control back into a libcamera::controls:: enum\n> variant.\n> \n> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>\n\nI know there was a build failure in CI to sort out - but I'll be very\nhappy to hear this 'works' from some testing once the build is fixed in\nthe next version. There's so much templating in here I'm not sure how\neffectively I can review it, but I'd be fine merging when we know it\nproduces the correct results!\n\n--\nKieran\n\n\n> ---\n>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++\n>  src/gstreamer/gstlibcamera-controls.h      |  43 +++\n>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-\n>  src/gstreamer/meson.build                  |  10 +\n>  utils/codegen/controls.py                  |   8 +\n>  utils/codegen/gen-gst-controls.py          | 166 +++++++++++\n>  utils/codegen/meson.build                  |   1 +\n>  7 files changed, 579 insertions(+), 3 deletions(-)\n>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in\n>  create mode 100644 src/gstreamer/gstlibcamera-controls.h\n>  create mode 100755 utils/codegen/gen-gst-controls.py\n> \n> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in\n> new file mode 100644\n> index 00000000..6861f5d1\n> --- /dev/null\n> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in\n> @@ -0,0 +1,332 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2024, Jaslo Ziska\n> + *\n> + * GStreamer Camera Controls\n> + *\n> + * This file is auto-generated. Do not edit.\n> + */\n> +\n> +#include <vector>\n> +\n> +#include <libcamera/control_ids.h>\n> +#include <libcamera/controls.h>\n> +#include <libcamera/geometry.h>\n> +\n> +#include \"gstlibcamera-controls.h\"\n> +\n> +using namespace libcamera;\n> +\n> +static void value_set_rectangle(GValue *value, const Rectangle &rect)\n> +{\n> +       Point top_left = rect.topLeft();\n> +       Size size = rect.size();\n> +\n> +       GValue x = G_VALUE_INIT;\n> +       g_value_init(&x, G_TYPE_INT);\n> +       g_value_set_int(&x, top_left.x);\n> +       gst_value_array_append_and_take_value(value, &x);\n> +\n> +       GValue y = G_VALUE_INIT;\n> +       g_value_init(&y, G_TYPE_INT);\n> +       g_value_set_int(&y, top_left.y);\n> +       gst_value_array_append_and_take_value(value, &y);\n> +\n> +       GValue width = G_VALUE_INIT;\n> +       g_value_init(&width, G_TYPE_INT);\n> +       g_value_set_int(&width, size.width);\n> +       gst_value_array_append_and_take_value(value, &width);\n> +\n> +       GValue height = G_VALUE_INIT;\n> +       g_value_init(&height, G_TYPE_INT);\n> +       g_value_set_int(&x, size.height);\n> +       gst_value_array_append_and_take_value(value, &height);\n> +}\n> +\n> +static Rectangle value_get_rectangle(const GValue *value)\n> +{\n> +       const GValue *r;\n> +       r = gst_value_array_get_value(value, 0);\n> +       int x = g_value_get_int(r);\n> +       r = gst_value_array_get_value(value, 1);\n> +       int y = g_value_get_int(r);\n> +       r = gst_value_array_get_value(value, 2);\n> +       int w = g_value_get_int(r);\n> +       r = gst_value_array_get_value(value, 3);\n> +       int h = g_value_get_int(r);\n> +\n> +       return Rectangle(x, y, w, h);\n> +}\n> +\n> +{% for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls if ctrl.is_enum %}\n> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {\n> +{%- for enum in ctrl.enum_values %}\n> +       {\n> +               controls::{{ ctrl.namespace }}{{ enum.name }},\n> +               {{ enum.description|format_description|indent('\\t\\t') }},\n> +               \"{{ enum.gst_name }}\"\n> +       },\n> +{%- endfor %}\n> +       {0, NULL, NULL}\n> +};\n> +\n> +#define TYPE_{{ ctrl.name|snake_case|upper }} \\\n> +       ({{ ctrl.name|snake_case }}_get_type())\n> +static GType {{ ctrl.name|snake_case }}_get_type()\n> +{\n> +       static GType {{ ctrl.name|snake_case }}_type = 0;\n> +\n> +       if (!{{ ctrl.name|snake_case }}_type)\n> +               {{ ctrl.name|snake_case }}_type =\n> +                       g_enum_register_static(\"{{ ctrl.name }}\",\n> +                                              {{ ctrl.name|snake_case }}_types);\n> +\n> +       return {{ ctrl.name|snake_case }}_type;\n> +}\n> +{% endfor %}\n> +{%- endfor %}\n> +\n> +void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)\n> +{\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +{%- set spec %}\n> +{%- if ctrl.is_rectangle -%}\n> +gst_param_spec_array(\n> +{%- else -%}\n> +g_param_spec_{{ ctrl.gtype }}(\n> +{%- endif -%}\n> +{%- if ctrl.is_array %}\n> +       \"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value\",\n> +       \"{{ ctrl.name }} Value\",\n> +       \"One {{ ctrl.name }} element value\",\n> +{%- else %}\n> +       \"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n> +       \"{{ ctrl.name }}\",\n> +       {{ ctrl.description|format_description|indent('\\t') }},\n> +{%- endif %}\n> +{%- if ctrl.is_enum %}\n> +       TYPE_{{ ctrl.name|snake_case|upper }},\n> +       {{ ctrl.default }},\n> +{%- elif ctrl.is_rectangle %}\n> +       g_param_spec_int(\n> +               \"rectangle-value\",\n> +               \"Rectangle Value\",\n> +               \"One rectangle value, either x, y, width or height.\",\n> +               {{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n> +               (GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n> +                              G_PARAM_STATIC_STRINGS)\n> +       ),\n> +{%- elif ctrl.gtype == 'boolean' %}\n> +       {{ ctrl.default }},\n> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}\n> +       {{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n> +{%- endif %}\n> +       (GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n> +                      G_PARAM_STATIC_STRINGS)\n> +)\n> +{%- endset %}\n> +\n> +       g_object_class_install_property(\n> +               klass,\n> +               lastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},\n> +{%- if ctrl.is_array %}\n> +               gst_param_spec_array(\n> +                       \"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n> +                       \"{{ ctrl.name }}\",\n> +                       {{ ctrl.description|format_description|indent('\\t\\t\\t') }},\n> +                       {{ spec|indent('\\t\\t\\t') }},\n> +                       (GParamFlags) (GST_PARAM_CONTROLLABLE |\n> +                                      G_PARAM_READWRITE |\n> +                                      G_PARAM_STATIC_STRINGS)\n> +               )\n> +{%- else %}\n> +               {{ spec|indent('\\t\\t') }}\n> +{%- endif %}\n> +       );\n> +{%- endfor %}\n> +{%- endfor %}\n> +}\n> +\n> +bool GstCameraControls::getProperty(guint propId, GValue *value,\n> +                                   [[maybe_unused]] GParamSpec *pspec)\n> +{\n> +       if (!controls_acc_.contains(propId)) {\n> +               GST_WARNING(\"Control '%s' is not available, default value will \"\n> +                           \"be returned\",\n> +                           controls::controls.at(propId)->name().c_str());\n> +               return true;\n> +       }\n> +       const ControlValue &cv = controls_acc_.get(propId);\n> +\n> +       switch (propId) {\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +       case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n> +               auto control = cv.get<{{ ctrl.type }}>();\n> +\n> +{%- if ctrl.is_array %}\n> +               for (size_t i = 0; i < control.size(); ++i) {\n> +                       GValue element = G_VALUE_INIT;\n> +{%- if ctrl.is_rectangle %}\n> +                       g_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);\n> +                       value_set_rectangle(&element, control[i]);\n> +{%- else %}\n> +                       g_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});\n> +                       g_value_set_{{ ctrl.gtype }}(&element, control[i]);\n> +{%- endif %}\n> +                       gst_value_array_append_and_take_value(value, &element);\n> +               }\n> +{%- else %}\n> +{%- if ctrl.is_rectangle %}\n> +               value_set_rectangle(value, control);\n> +{%- else %}\n> +               g_value_set_{{ ctrl.gtype }}(value, control);\n> +{%- endif %}\n> +{%- endif %}\n> +\n> +               return true;\n> +       }\n> +{%- endfor %}\n> +{%- endfor %}\n> +\n> +       default:\n> +               return false;\n> +       }\n> +}\n> +\n> +bool GstCameraControls::setProperty(guint propId, const GValue *value,\n> +                                   [[maybe_unused]] GParamSpec *pspec)\n> +{\n> +       /*\n> +        * Check whether the camera capabilities are already available.\n> +        * They might not be available if the pipeline has not started yet.\n> +        */\n> +       if (!capabilities_.empty()) {\n> +               /* If so, check that the control is supported by the camera. */\n> +               const ControlId *cid = capabilities_.idmap().at(propId);\n> +               auto info = capabilities_.find(cid);\n> +\n> +               if (info == capabilities_.end()) {\n> +                       GST_WARNING(\"Control '%s' is not supported by the \"\n> +                                   \"camera and will be ignored\",\n> +                                   cid->name().c_str());\n> +                       return true;\n> +               }\n> +       }\n> +\n> +       switch (propId) {\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +       case controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n> +               ControlValue control;\n> +{%- if ctrl.is_array %}\n> +               size_t size = gst_value_array_get_size(value);\n> +{%- if ctrl.size != 0 %}\n> +               if (size != {{ ctrl.size }}) {\n> +                       GST_ERROR(\"Incorrect array size for control \"\n> +                                 \"'{{ ctrl.name|kebab_case }}', must be of \"\n> +                                 \"size {{ ctrl.size }}\");\n> +                       return true;\n> +               }\n> +{%- endif %}\n> +\n> +               std::vector<{{ ctrl.element_type }}> values(size);\n> +               for (size_t i = 0; i < size; ++i) {\n> +                       const GValue *element =\n> +                               gst_value_array_get_value(value, i);\n> +{%- if ctrl.is_rectangle %}\n> +                       if (gst_value_array_get_size(element) != 4) {\n> +                               GST_ERROR(\"Rectangle in control \"\n> +                                         \"'{{ ctrl.name|kebab_case }}' at\"\n> +                                         \"index %lu must be an array of size 4\",\n> +                                         i);\n> +                               return true;\n> +                       }\n> +                       values[i] = value_get_rectangle(element);\n> +{%- else %}\n> +                       values[i] = g_value_get_{{ ctrl.gtype }}(element);\n> +{%- endif %}\n> +               }\n> +\n> +{%- if ctrl.size == 0 %}\n> +               control.set(Span<const {{ ctrl.element_type }}>(values.data(),\n> +                                                               size));\n> +{%- else %}\n> +               control.set(Span<const {{ ctrl.element_type }},\n> +                                {{ ctrl.size }}>(values.data(),\n> +                                                 {{ ctrl.size }}));\n> +{%- endif %}\n> +{%- else %}\n> +{%- if ctrl.is_rectangle %}\n> +               if (gst_value_array_get_size(value) != 4) {\n> +                       GST_ERROR(\"Rectangle in control \"\n> +                                 \"'{{ ctrl.name|kebab_case }}' must be an \"\n> +                                 \"array of size 4\");\n> +                       return true;\n> +               }\n> +               Rectangle val = value_get_rectangle(value);\n> +{%- else %}\n> +               auto val = g_value_get_{{ ctrl.gtype }}(value);\n> +{%- endif %}\n> +               control.set(val);\n> +{%- endif %}\n> +               controls_.set(propId, control);\n> +               controls_acc_.set(propId, control);\n> +               return true;\n> +       }\n> +{%- endfor %}\n> +{%- endfor %}\n> +\n> +       default:\n> +               return false;\n> +       }\n> +}\n> +\n> +void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)\n> +{\n> +       capabilities_ = cam->controls();\n> +\n> +       /*\n> +        * Check the controls which were set before the camera capabilities were\n> +        * known. This is required because GStreamer may set properties before\n> +        * the pipeline has started and thus before the camera was known.\n> +        */\n> +       ControlList new_controls;\n> +       for (auto control = controls_acc_.begin();\n> +            control != controls_acc_.end();\n> +            ++control) {\n> +               unsigned int id = control->first;\n> +               ControlValue value = control->second;\n> +\n> +               const ControlId *cid = capabilities_.idmap().at(id);\n> +               auto info = capabilities_.find(cid);\n> +\n> +               /* Only add controls which are supported. */\n> +               if (info != capabilities_.end())\n> +                       new_controls.set(id, value);\n> +               else\n> +                       GST_WARNING(\"Control '%s' is not supported by the \"\n> +                                   \"camera and will be ignored\",\n> +                                   cid->name().c_str());\n> +       }\n> +\n> +       controls_acc_ = new_controls;\n> +       controls_ = new_controls;\n> +}\n> +\n> +void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)\n> +{\n> +       request->controls().merge(controls_);\n> +       controls_.clear();\n> +}\n> +\n> +void GstCameraControls::readMetadata(libcamera::Request *request)\n> +{\n> +       controls_acc_.merge(request->metadata(),\n> +                           ControlList::MergePolicy::OverwriteExisting);\n> +}\n> diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h\n> new file mode 100644\n> index 00000000..749220b5\n> --- /dev/null\n> +++ b/src/gstreamer/gstlibcamera-controls.h\n> @@ -0,0 +1,43 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2023, Collabora Ltd.\n> + *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>\n> + *\n> + * GStreamer Camera Controls\n> + */\n> +\n> +#pragma once\n> +\n> +#include <memory>\n> +\n> +#include <libcamera/camera.h>\n> +#include <libcamera/controls.h>\n> +#include <libcamera/request.h>\n> +\n> +#include \"gstlibcamerasrc.h\"\n> +\n> +namespace libcamera {\n> +\n> +class GstCameraControls\n> +{\n> +public:\n> +       static void installProperties(GObjectClass *klass, int lastProp);\n> +\n> +       bool getProperty(guint propId, GValue *value, GParamSpec *pspec);\n> +       bool setProperty(guint propId, const GValue *value, GParamSpec *pspec);\n> +\n> +       void setCamera(const std::shared_ptr<libcamera::Camera> &cam);\n> +\n> +       void applyControls(std::unique_ptr<libcamera::Request> &request);\n> +       void readMetadata(libcamera::Request *request);\n> +\n> +private:\n> +       /* Supported controls and limits of camera. */\n> +       ControlInfoMap capabilities_;\n> +       /* Set of user modified controls. */\n> +       ControlList controls_;\n> +       /* Accumulator of all controls ever set and metadata returned by camera */\n> +       ControlList controls_acc_;\n> +};\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp\n> index 40b787c8..8efa25f4 100644\n> --- a/src/gstreamer/gstlibcamerasrc.cpp\n> +++ b/src/gstreamer/gstlibcamerasrc.cpp\n> @@ -37,10 +37,11 @@\n>  \n>  #include <gst/base/base.h>\n>  \n> +#include \"gstlibcamera-controls.h\"\n> +#include \"gstlibcamera-utils.h\"\n>  #include \"gstlibcameraallocator.h\"\n>  #include \"gstlibcamerapad.h\"\n>  #include \"gstlibcamerapool.h\"\n> -#include \"gstlibcamera-utils.h\"\n>  \n>  using namespace libcamera;\n>  \n> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {\n>  \n>         ControlList initControls_;\n>         guint group_id_;\n> +       GstCameraControls controls_;\n>  \n>         int queueRequest();\n>         void requestCompleted(Request *request);\n> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {\n>  enum {\n>         PROP_0,\n>         PROP_CAMERA_NAME,\n> +       PROP_LAST\n>  };\n>  \n>  static void gst_libcamera_src_child_proxy_init(gpointer g_iface,\n> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()\n>         if (!request)\n>                 return -ENOMEM;\n>  \n> +       /* Apply controls */\n> +       controls_.applyControls(request);\n> +\n>         std::unique_ptr<RequestWrap> wrap =\n>                 std::make_unique<RequestWrap>(std::move(request));\n>  \n> @@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)\n>  \n>         {\n>                 GLibLocker locker(&lock_);\n> +\n> +               controls_.readMetadata(request);\n> +\n>                 wrap = std::move(queuedRequests_.front());\n>                 queuedRequests_.pop();\n>         }\n> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)\n>                 return false;\n>         }\n>  \n> +       self->state->controls_.setCamera(cam);\n> +\n>         cam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);\n>  \n>         /* No need to lock here, we didn't start our threads yet. */\n> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n>  {\n>         GLibLocker lock(GST_OBJECT(object));\n>         GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n> +       GstLibcameraSrcState *state = self->state;\n>  \n>         switch (prop_id) {\n>         case PROP_CAMERA_NAME:\n> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n>                 self->camera_name = g_value_dup_string(value);\n>                 break;\n>         default:\n> -               G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n> +               if (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))\n> +                       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n>                 break;\n>         }\n>  }\n> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,\n>  {\n>         GLibLocker lock(GST_OBJECT(object));\n>         GstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n> +       GstLibcameraSrcState *state = self->state;\n>  \n>         switch (prop_id) {\n>         case PROP_CAMERA_NAME:\n>                 g_value_set_string(value, self->camera_name);\n>                 break;\n>         default:\n> -               G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n> +               if (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))\n> +                       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n>                 break;\n>         }\n>  }\n> @@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)\n>                                                              | G_PARAM_STATIC_STRINGS));\n>         g_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);\n>  \n> +       GstCameraControls::installProperties(object_class, PROP_LAST);\n>  }\n>  \n>  /* GstChildProxy implementation */\n> diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build\n> index c2a01e7b..6b7e53b5 100644\n> --- a/src/gstreamer/meson.build\n> +++ b/src/gstreamer/meson.build\n> @@ -25,6 +25,16 @@ libcamera_gst_sources = [\n>      'gstlibcamerasrc.cpp',\n>  ]\n>  \n> +# Generate gstreamer control properties\n> +\n> +gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')\n> +libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',\n> +                                       input : controls_files,\n> +                                       output : 'gstlibcamera-controls.cpp',\n> +                                       command : [gen_gst_controls, '-o', '@OUTPUT@',\n> +                                                  '-t', gen_gst_controls_template, '@INPUT@'],\n> +                                       env : py_build_env)\n> +\n>  libcamera_gst_cpp_args = [\n>      '-DVERSION=\"@0@\"'.format(libcamera_git_version),\n>      '-DPACKAGE=\"@0@\"'.format(meson.project_name()),\n> diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py\n> index 7bafee59..03c77cc6 100644\n> --- a/utils/codegen/controls.py\n> +++ b/utils/codegen/controls.py\n> @@ -110,3 +110,11 @@ class Control(object):\n>              return f\"Span<const {typ}, {self.__size}>\"\n>          else:\n>              return f\"Span<const {typ}>\"\n> +\n> +    @property\n> +    def element_type(self):\n> +        return self.__data.get('type')\n> +\n> +    @property\n> +    def size(self):\n> +        return self.__size\n> diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py\n> new file mode 100755\n> index 00000000..0b899258\n> --- /dev/null\n> +++ b/utils/codegen/gen-gst-controls.py\n> @@ -0,0 +1,166 @@\n> +#!/usr/bin/env python3\n> +# SPDX-License-Identifier: GPL-2.0-or-later\n> +# Copyright (C) 2019, Google Inc.\n> +# Copyright (C) 2024, Jaslo Ziska\n> +#\n> +# Authors:\n> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> +# Jaslo Ziska <jaslo@ziska.de>\n> +#\n> +# Generate gstreamer control properties from YAML\n> +\n> +import argparse\n> +import jinja2\n> +import re\n> +import sys\n> +import yaml\n> +\n> +from controls import Control\n> +\n> +\n> +exposed_controls = [\n> +    \"AeEnable\", \"AeLocked\", \"AeMeteringMode\", \"AeConstraintMode\",\n> +    \"AeExposureMode\", \"ExposureValue\", \"ExposureTime\", \"AnalogueGain\",\n> +    \"AeFlickerMode\", \"AeFlickerPeriod\", \"AeFlickerDetected\", \"Brightness\",\n> +    \"Contrast\", \"Lux\", \"AwbEnable\", \"AwbMode\", \"AwbLocked\", \"ColourGains\",\n> +    \"ColourTemperature\", \"Saturation\", \"SensorBlackLevels\", \"Sharpness\",\n> +    \"FocusFoM\", \"ColourCorrectionMatrix\", \"ScalerCrop\", \"DigitalGain\",\n> +    \"FrameDuration\", \"SensorTemperature\", \"SensorTimestamp\", \"AfMode\",\n> +    \"AfRange\", \"AfSpeed\", \"AfMetering\", \"AfWindows\", \"LensPosition\", \"AfState\",\n> +    \"AfPauseState\", \"Gamma\",\n> +]\n> +\n> +\n> +def find_common_prefix(strings):\n> +    prefix = strings[0]\n> +\n> +    for string in strings[1:]:\n> +        while string[:len(prefix)] != prefix and prefix:\n> +            prefix = prefix[:len(prefix) - 1]\n> +        if not prefix:\n> +            break\n> +\n> +    return prefix\n> +\n> +\n> +def format_description(description):\n> +    # Substitute doxygen keywords \\sa (see also) and \\todo\n> +    description = re.sub(r'\\\\sa((?: \\w+)+)',\n> +                         lambda match: 'See also: ' + ', '.join(\n> +                             map(kebab_case, match.group(1).strip().split(' '))\n> +                         ) + '.', description)\n> +    description = re.sub(r'\\\\todo', 'Todo:', description)\n> +\n> +    description = description.strip().split('\\n')\n> +    return '\\n'.join([\n> +        '\"' + line.replace('\\\\', r'\\\\').replace('\"', r'\\\"') + ' \"' for line in description if line\n> +    ]).rstrip()\n> +\n> +\n> +def snake_case(s):\n> +    return ''.join([\n> +        c.isupper() and ('_' + c.lower()) or c for c in s\n> +    ]).strip('_')\n> +\n> +\n> +def kebab_case(s):\n> +    return snake_case(s).replace('_', '-')\n> +\n> +\n> +def extend_control(ctrl):\n> +    if ctrl.vendor != 'libcamera':\n> +        ctrl.namespace = f'{ctrl.vendor}::'\n> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'\n> +    else:\n> +        ctrl.namespace = ''\n> +        ctrl.vendor_prefix = ''\n> +\n> +    ctrl.is_array = ctrl.size is not None\n> +\n> +    if ctrl.is_enum:\n> +        # Remove common prefix from enum variant names\n> +        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])\n> +        for enum in ctrl.enum_values:\n> +            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))\n> +\n> +        ctrl.gtype = 'enum'\n> +        ctrl.default = '0'\n> +    elif ctrl.element_type == 'bool':\n> +        ctrl.gtype = 'boolean'\n> +        ctrl.default = 'false'\n> +    elif ctrl.element_type == 'float':\n> +        ctrl.gtype = 'float'\n> +        ctrl.default = '0'\n> +        ctrl.min = '-G_MAXFLOAT'\n> +        ctrl.max = 'G_MAXFLOAT'\n> +    elif ctrl.element_type == 'int32_t':\n> +        ctrl.gtype = 'int'\n> +        ctrl.default = '0'\n> +        ctrl.min = 'G_MININT'\n> +        ctrl.max = 'G_MAXINT'\n> +    elif ctrl.element_type == 'int64_t':\n> +        ctrl.gtype = 'int64'\n> +        ctrl.default = '0'\n> +        ctrl.min = 'G_MININT64'\n> +        ctrl.max = 'G_MAXINT64'\n> +    elif ctrl.element_type == 'uint8_t':\n> +        ctrl.gtype = 'uchar'\n> +        ctrl.default = '0'\n> +        ctrl.min = '0'\n> +        ctrl.max = 'G_MAXUINT8'\n> +    elif ctrl.element_type == 'Rectangle':\n> +        ctrl.is_rectangle = True\n> +        ctrl.default = '0'\n> +        ctrl.min = '0'\n> +        ctrl.max = 'G_MAXINT'\n> +    else:\n> +        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')\n> +\n> +    return ctrl\n> +\n> +\n> +def main(argv):\n> +    # Parse command line arguments\n> +    parser = argparse.ArgumentParser()\n> +    parser.add_argument('--output', '-o', metavar='file', type=str,\n> +                        help='Output file name. Defaults to standard output if not specified.')\n> +    parser.add_argument('--template', '-t', dest='template', type=str, required=True,\n> +                        help='Template file name.')\n> +    parser.add_argument('input', type=str, nargs='+',\n> +                        help='Input file name.')\n> +\n> +    args = parser.parse_args(argv[1:])\n> +\n> +    controls = {}\n> +    for input in args.input:\n> +        data = yaml.safe_load(open(input, 'rb').read())\n> +\n> +        vendor = data['vendor']\n> +        ctrls = controls.setdefault(vendor, [])\n> +\n> +        for ctrl in data['controls']:\n> +            ctrl = Control(*ctrl.popitem(), vendor)\n> +\n> +            if ctrl.name in exposed_controls:\n> +                ctrls.append(extend_control(ctrl))\n> +\n> +    data = {'controls': list(controls.items())}\n> +\n> +    env = jinja2.Environment()\n> +    env.filters['format_description'] = format_description\n> +    env.filters['snake_case'] = snake_case\n> +    env.filters['kebab_case'] = kebab_case\n> +    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())\n> +    string = template.render(data)\n> +\n> +    if args.output:\n> +        with open(args.output, 'w', encoding='utf-8') as output:\n> +            output.write(string)\n> +    else:\n> +        sys.stdout.write(string)\n> +\n> +    return 0\n> +\n> +\n> +if __name__ == '__main__':\n> +    sys.exit(main(sys.argv))\n> diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build\n> index adf33bba..904dd66d 100644\n> --- a/utils/codegen/meson.build\n> +++ b/utils/codegen/meson.build\n> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']\n>  \n>  gen_controls = files('gen-controls.py')\n>  gen_formats = files('gen-formats.py')\n> +gen_gst_controls = files('gen-gst-controls.py')\n>  gen_header = files('gen-header.sh')\n>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')\n>  gen_tracepoints = files('gen-tp-header.py')\n> -- \n> 2.46.2\n>","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 7A752C32FE\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 18 Oct 2024 14:20:12 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 9A0F06538A;\n\tFri, 18 Oct 2024 16:20:11 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 16950633C6\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 18 Oct 2024 16:20:10 +0200 (CEST)","from pendragon.ideasonboard.com\n\t(cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 23A9F514;\n\tFri, 18 Oct 2024 16:18:26 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"X43VM1BK\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1729261106;\n\tbh=A2nWaJoROiRU7sbttfCSkJ4+IAsFx6Z0tvijhlesU/c=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=X43VM1BKXfPk7eNNiY2Jio80Ld3CutEf59u0Q828sxX/gtuSUfOOVzeaH9APRrHID\n\tK/hQuCD10FXETWR/i7OuhQaaFXOqyQB7SgGQXIQU5K79RWHrnYzM3WL+O5JmwsbBFb\n\tFmYCwCfRUjeOvHg4DxErNOJZx5s6AMnv9bj3ypBo=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20241017172331.16377-4-jaslo@ziska.de>","References":"<20241017172331.16377-1-jaslo@ziska.de>\n\t<20241017172331.16377-4-jaslo@ziska.de>","Subject":"Re: [PATCH v3 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Jaslo Ziska <jaslo@ziska.de>","To":"Jaslo Ziska <jaslo@ziska.de>, libcamera-devel@lists.libcamera.org","Date":"Fri, 18 Oct 2024 15:20:06 +0100","Message-ID":"<172926120694.3353069.16813507965031089089@ping.linuxembedded.co.uk>","User-Agent":"alot/0.10","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":31804,"web_url":"https://patchwork.libcamera.org/comment/31804/","msgid":"<e189b0ca3b20f9a698022ed296e4fa4b2ca5fd62.camel@ndufresne.ca>","date":"2024-10-18T18:19:20","subject":"Re: [PATCH v3 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","submitter":{"id":30,"url":"https://patchwork.libcamera.org/api/people/30/","name":"Nicolas Dufresne","email":"nicolas@ndufresne.ca"},"content":"Le jeudi 17 octobre 2024 à 19:04 +0200, Jaslo Ziska a écrit :\n> This commit implements gstreamer controls for the libcamera element by\n> generating the controls from the control_ids_*.yaml files using a new\n> gen-gst-controls.py script. The appropriate meson files are also changed\n> to automatically run the script when building.\n> \n> The gen-gst-controls.py script works similar to the gen-controls.py\n> script by parsing the control_ids_*.yaml files and generating C++ code\n> for each exposed control.\n> For the controls to be used as gstreamer properties the type for each\n> control needs to be translated to the appropriate glib type and a\n> GEnumValue is generated for each enum control. Then a\n> g_object_install_property(), _get_property() and _set_property()\n> function is generated for each control.\n> The vendor controls get prefixed with \"$vendor-\" in the final gstreamer\n> property name.\n> \n> The C++ code generated by the gen-gst-controls.py script is written into\n> the template gstlibcamerasrc-controls.cpp.in file. The matching\n> gstlibcamerasrc-controls.h header defines the GstCameraControls class\n> which handles the installation of the gstreamer properties as well as\n> keeping track of the control values and setting and getting the\n> controls. The content of these functions is generated in the Python\n> script.\n> \n> Finally the libcamerasrc element itself is edited to make use of the new\n> GstCameraControls class. The way this works is by defining a PROP_LAST\n> enum variant which is passed to the installProperties() function so the\n> properties are defined with the appropriate offset. When getting or\n> setting a property PROP_LAST is subtracted from the requested property\n> to translate the control back into a libcamera::controls:: enum\n> variant.\n> \n> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>\n> ---\n>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 +++++++++++++++++++++\n>  src/gstreamer/gstlibcamera-controls.h      |  43 +++\n>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-\n>  src/gstreamer/meson.build                  |  10 +\n>  utils/codegen/controls.py                  |   8 +\n>  utils/codegen/gen-gst-controls.py          | 166 +++++++++++\n>  utils/codegen/meson.build                  |   1 +\n>  7 files changed, 579 insertions(+), 3 deletions(-)\n>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in\n>  create mode 100644 src/gstreamer/gstlibcamera-controls.h\n>  create mode 100755 utils/codegen/gen-gst-controls.py\n> \n> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in b/src/gstreamer/gstlibcamera-controls.cpp.in\n> new file mode 100644\n> index 00000000..6861f5d1\n> --- /dev/null\n> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in\n> @@ -0,0 +1,332 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2024, Jaslo Ziska\n> + *\n> + * GStreamer Camera Controls\n> + *\n> + * This file is auto-generated. Do not edit.\n> + */\n> +\n> +#include <vector>\n> +\n> +#include <libcamera/control_ids.h>\n> +#include <libcamera/controls.h>\n> +#include <libcamera/geometry.h>\n> +\n> +#include \"gstlibcamera-controls.h\"\n> +\n> +using namespace libcamera;\n> +\n> +static void value_set_rectangle(GValue *value, const Rectangle &rect)\n> +{\n> +\tPoint top_left = rect.topLeft();\n> +\tSize size = rect.size();\n> +\n> +\tGValue x = G_VALUE_INIT;\n> +\tg_value_init(&x, G_TYPE_INT);\n> +\tg_value_set_int(&x, top_left.x);\n> +\tgst_value_array_append_and_take_value(value, &x);\n> +\n> +\tGValue y = G_VALUE_INIT;\n> +\tg_value_init(&y, G_TYPE_INT);\n> +\tg_value_set_int(&y, top_left.y);\n> +\tgst_value_array_append_and_take_value(value, &y);\n> +\n> +\tGValue width = G_VALUE_INIT;\n> +\tg_value_init(&width, G_TYPE_INT);\n> +\tg_value_set_int(&width, size.width);\n> +\tgst_value_array_append_and_take_value(value, &width);\n> +\n> +\tGValue height = G_VALUE_INIT;\n> +\tg_value_init(&height, G_TYPE_INT);\n> +\tg_value_set_int(&x, size.height);\n> +\tgst_value_array_append_and_take_value(value, &height);\n> +}\n> +\n> +static Rectangle value_get_rectangle(const GValue *value)\n> +{\n> +\tconst GValue *r;\n> +\tr = gst_value_array_get_value(value, 0);\n> +\tint x = g_value_get_int(r);\n> +\tr = gst_value_array_get_value(value, 1);\n> +\tint y = g_value_get_int(r);\n> +\tr = gst_value_array_get_value(value, 2);\n> +\tint w = g_value_get_int(r);\n> +\tr = gst_value_array_get_value(value, 3);\n> +\tint h = g_value_get_int(r);\n> +\n> +\treturn Rectangle(x, y, w, h);\n> +}\n> +\n> +{% for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls if ctrl.is_enum %}\n> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {\n> +{%- for enum in ctrl.enum_values %}\n> +\t{\n> +\t\tcontrols::{{ ctrl.namespace }}{{ enum.name }},\n> +\t\t{{ enum.description|format_description|indent('\\t\\t') }},\n> +\t\t\"{{ enum.gst_name }}\"\n> +\t},\n> +{%- endfor %}\n> +\t{0, NULL, NULL}\n> +};\n> +\n> +#define TYPE_{{ ctrl.name|snake_case|upper }} \\\n> +\t({{ ctrl.name|snake_case }}_get_type())\n> +static GType {{ ctrl.name|snake_case }}_get_type()\n> +{\n> +\tstatic GType {{ ctrl.name|snake_case }}_type = 0;\n> +\n> +\tif (!{{ ctrl.name|snake_case }}_type)\n> +\t\t{{ ctrl.name|snake_case }}_type =\n> +\t\t\tg_enum_register_static(\"{{ ctrl.name }}\",\n> +\t\t\t\t\t       {{ ctrl.name|snake_case }}_types);\n> +\n> +\treturn {{ ctrl.name|snake_case }}_type;\n> +}\n> +{% endfor %}\n> +{%- endfor %}\n> +\n> +void GstCameraControls::installProperties(GObjectClass *klass, int lastPropId)\n> +{\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +{%- set spec %}\n> +{%- if ctrl.is_rectangle -%}\n> +gst_param_spec_array(\n> +{%- else -%}\n> +g_param_spec_{{ ctrl.gtype }}(\n> +{%- endif -%}\n> +{%- if ctrl.is_array %}\n> +\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}-value\",\n> +\t\"{{ ctrl.name }} Value\",\n> +\t\"One {{ ctrl.name }} element value\",\n> +{%- else %}\n> +\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n> +\t\"{{ ctrl.name }}\",\n> +\t{{ ctrl.description|format_description|indent('\\t') }},\n> +{%- endif %}\n> +{%- if ctrl.is_enum %}\n> +\tTYPE_{{ ctrl.name|snake_case|upper }},\n> +\t{{ ctrl.default }},\n> +{%- elif ctrl.is_rectangle %}\n> +\tg_param_spec_int(\n> +\t\t\"rectangle-value\",\n> +\t\t\"Rectangle Value\",\n> +\t\t\"One rectangle value, either x, y, width or height.\",\n> +\t\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n> +\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n> +\t\t\t       G_PARAM_STATIC_STRINGS)\n> +\t),\n> +{%- elif ctrl.gtype == 'boolean' %}\n> +\t{{ ctrl.default }},\n> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}\n> +\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n> +{%- endif %}\n> +\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE |\n> +\t\t       G_PARAM_STATIC_STRINGS)\n> +)\n> +{%- endset %}\n> +\n> +\tg_object_class_install_property(\n> +\t\tklass,\n> +\t\tlastPropId + controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }},\n> +{%- if ctrl.is_array %}\n> +\t\tgst_param_spec_array(\n> +\t\t\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n> +\t\t\t\"{{ ctrl.name }}\",\n> +\t\t\t{{ ctrl.description|format_description|indent('\\t\\t\\t') }},\n> +\t\t\t{{ spec|indent('\\t\\t\\t') }},\n> +\t\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE |\n> +\t\t\t\t       G_PARAM_READWRITE |\n> +\t\t\t\t       G_PARAM_STATIC_STRINGS)\n> +\t\t)\n> +{%- else %}\n> +\t\t{{ spec|indent('\\t\\t') }}\n> +{%- endif %}\n> +\t);\n> +{%- endfor %}\n> +{%- endfor %}\n> +}\n> +\n> +bool GstCameraControls::getProperty(guint propId, GValue *value,\n> +\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n> +{\n> +\tif (!controls_acc_.contains(propId)) {\n> +\t\tGST_WARNING(\"Control '%s' is not available, default value will \"\n> +\t\t\t    \"be returned\",\n> +\t\t\t    controls::controls.at(propId)->name().c_str());\n> +\t\treturn true;\n> +\t}\n> +\tconst ControlValue &cv = controls_acc_.get(propId);\n> +\n> +\tswitch (propId) {\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +\tcase controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n> +\t\tauto control = cv.get<{{ ctrl.type }}>();\n> +\n> +{%- if ctrl.is_array %}\n> +\t\tfor (size_t i = 0; i < control.size(); ++i) {\n> +\t\t\tGValue element = G_VALUE_INIT;\n> +{%- if ctrl.is_rectangle %}\n> +\t\t\tg_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);\n> +\t\t\tvalue_set_rectangle(&element, control[i]);\n> +{%- else %}\n> +\t\t\tg_value_init(&element, G_TYPE_{{ ctrl.gtype|upper }});\n> +\t\t\tg_value_set_{{ ctrl.gtype }}(&element, control[i]);\n> +{%- endif %}\n> +\t\t\tgst_value_array_append_and_take_value(value, &element);\n> +\t\t}\n> +{%- else %}\n> +{%- if ctrl.is_rectangle %}\n> +\t\tvalue_set_rectangle(value, control);\n> +{%- else %}\n> +\t\tg_value_set_{{ ctrl.gtype }}(value, control);\n> +{%- endif %}\n> +{%- endif %}\n> +\n> +\t\treturn true;\n> +\t}\n> +{%- endfor %}\n> +{%- endfor %}\n> +\n> +\tdefault:\n> +\t\treturn false;\n> +\t}\n> +}\n> +\n> +bool GstCameraControls::setProperty(guint propId, const GValue *value,\n> +\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n> +{\n> +\t/*\n> +\t * Check whether the camera capabilities are already available.\n> +\t * They might not be available if the pipeline has not started yet.\n> +\t */\n> +\tif (!capabilities_.empty()) {\n> + \t\t/* If so, check that the control is supported by the camera. */\n> +\t\tconst ControlId *cid = capabilities_.idmap().at(propId);\n> +\t\tauto info = capabilities_.find(cid);\n> +\n> +\t\tif (info == capabilities_.end()) {\n> +\t\t\tGST_WARNING(\"Control '%s' is not supported by the \"\n> +\t\t\t\t    \"camera and will be ignored\",\n> +\t\t\t\t    cid->name().c_str());\n> +\t\t\treturn true;\n> +\t\t}\n> +\t}\n> +\n> +\tswitch (propId) {\n> +{%- for vendor, ctrls in controls %}\n> +{%- for ctrl in ctrls %}\n> +\n> +\tcase controls::{{ ctrl.namespace }}{{ ctrl.name|snake_case|upper }}: {\n> +\t\tControlValue control;\n> +{%- if ctrl.is_array %}\n> +\t\tsize_t size = gst_value_array_get_size(value);\n> +{%- if ctrl.size != 0 %}\n> +\t\tif (size != {{ ctrl.size }}) {\n> +\t\t\tGST_ERROR(\"Incorrect array size for control \"\n> +\t\t\t\t  \"'{{ ctrl.name|kebab_case }}', must be of \"\n> +\t\t\t\t  \"size {{ ctrl.size }}\");\n> +\t\t\treturn true;\n> +\t\t}\n> +{%- endif %}\n> +\n> +\t\tstd::vector<{{ ctrl.element_type }}> values(size);\n> +\t\tfor (size_t i = 0; i < size; ++i) {\n> +\t\t\tconst GValue *element =\n> +\t\t\t\tgst_value_array_get_value(value, i);\n> +{%- if ctrl.is_rectangle %}\n> +\t\t\tif (gst_value_array_get_size(element) != 4) {\n> +\t\t\t\tGST_ERROR(\"Rectangle in control \"\n> +\t\t\t\t\t  \"'{{ ctrl.name|kebab_case }}' at\"\n> +\t\t\t\t\t  \"index %lu must be an array of size 4\",\n> +\t\t\t\t\t  i);\n> +\t\t\t\treturn true;\n> +\t\t\t}\n> +\t\t\tvalues[i] = value_get_rectangle(element);\n> +{%- else %}\n> +\t\t\tvalues[i] = g_value_get_{{ ctrl.gtype }}(element);\n> +{%- endif %}\n> +\t\t}\n> +\n> +{%- if ctrl.size == 0 %}\n> +\t\tcontrol.set(Span<const {{ ctrl.element_type }}>(values.data(),\n> +\t\t\t\t\t\t\t\tsize));\n> +{%- else %}\n> +\t\tcontrol.set(Span<const {{ ctrl.element_type }},\n> +\t\t\t         {{ ctrl.size }}>(values.data(),\n> +\t\t\t\t\t\t  {{ ctrl.size }}));\n> +{%- endif %}\n> +{%- else %}\n> +{%- if ctrl.is_rectangle %}\n> +\t\tif (gst_value_array_get_size(value) != 4) {\n> +\t\t\tGST_ERROR(\"Rectangle in control \"\n> +\t\t\t\t  \"'{{ ctrl.name|kebab_case }}' must be an \"\n> +\t\t\t\t  \"array of size 4\");\n> +\t\t\treturn true;\n> +\t\t}\n> +\t\tRectangle val = value_get_rectangle(value);\n> +{%- else %}\n> +\t\tauto val = g_value_get_{{ ctrl.gtype }}(value);\n> +{%- endif %}\n> +\t\tcontrol.set(val);\n> +{%- endif %}\n> +\t\tcontrols_.set(propId, control);\n> +\t\tcontrols_acc_.set(propId, control);\n> +\t\treturn true;\n> +\t}\n> +{%- endfor %}\n> +{%- endfor %}\n> +\n> +\tdefault:\n> +\t\treturn false;\n> +\t}\n> +}\n> +\n> +void GstCameraControls::setCamera(const std::shared_ptr<libcamera::Camera> &cam)\n> +{\n> +\tcapabilities_ = cam->controls();\n> +\n> +\t/*\n> +\t * Check the controls which were set before the camera capabilities were\n> +\t * known. This is required because GStreamer may set properties before\n> +\t * the pipeline has started and thus before the camera was known.\n> +\t */\n> +\tControlList new_controls;\n> +\tfor (auto control = controls_acc_.begin();\n> +\t     control != controls_acc_.end();\n> +\t     ++control) {\n> +\t\tunsigned int id = control->first;\n> +\t\tControlValue value = control->second;\n> +\n> +\t\tconst ControlId *cid = capabilities_.idmap().at(id);\n> +\t\tauto info = capabilities_.find(cid);\n> +\n> +\t\t/* Only add controls which are supported. */\n> +\t\tif (info != capabilities_.end())\n> +\t\t\tnew_controls.set(id, value);\n> +\t\telse\n> +\t\t\tGST_WARNING(\"Control '%s' is not supported by the \"\n> +\t\t\t\t    \"camera and will be ignored\",\n> +\t\t\t\t    cid->name().c_str());\n> +\t}\n> +\n> +\tcontrols_acc_ = new_controls;\n> +\tcontrols_ = new_controls;\n> +}\n> +\n> +void GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> &request)\n> +{\n> +\trequest->controls().merge(controls_);\n> +\tcontrols_.clear();\n> +}\n> +\n> +void GstCameraControls::readMetadata(libcamera::Request *request)\n> +{\n> +\tcontrols_acc_.merge(request->metadata(),\n> +\t\t\t    ControlList::MergePolicy::OverwriteExisting);\n> +}\n> diff --git a/src/gstreamer/gstlibcamera-controls.h b/src/gstreamer/gstlibcamera-controls.h\n> new file mode 100644\n> index 00000000..749220b5\n> --- /dev/null\n> +++ b/src/gstreamer/gstlibcamera-controls.h\n> @@ -0,0 +1,43 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2023, Collabora Ltd.\n> + *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>\n> + *\n> + * GStreamer Camera Controls\n> + */\n> +\n> +#pragma once\n> +\n> +#include <memory>\n> +\n> +#include <libcamera/camera.h>\n> +#include <libcamera/controls.h>\n> +#include <libcamera/request.h>\n> +\n> +#include \"gstlibcamerasrc.h\"\n> +\n> +namespace libcamera {\n> +\n> +class GstCameraControls\n> +{\n> +public:\n> +\tstatic void installProperties(GObjectClass *klass, int lastProp);\n> +\n> +\tbool getProperty(guint propId, GValue *value, GParamSpec *pspec);\n> +\tbool setProperty(guint propId, const GValue *value, GParamSpec *pspec);\n> +\n> +\tvoid setCamera(const std::shared_ptr<libcamera::Camera> &cam);\n> +\n> +\tvoid applyControls(std::unique_ptr<libcamera::Request> &request);\n> +\tvoid readMetadata(libcamera::Request *request);\n> +\n> +private:\n> +\t/* Supported controls and limits of camera. */\n> +\tControlInfoMap capabilities_;\n> +\t/* Set of user modified controls. */\n> +\tControlList controls_;\n> +\t/* Accumulator of all controls ever set and metadata returned by camera */\n> +\tControlList controls_acc_;\n> +};\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp\n> index 40b787c8..8efa25f4 100644\n> --- a/src/gstreamer/gstlibcamerasrc.cpp\n> +++ b/src/gstreamer/gstlibcamerasrc.cpp\n> @@ -37,10 +37,11 @@\n>  \n>  #include <gst/base/base.h>\n>  \n> +#include \"gstlibcamera-controls.h\"\n> +#include \"gstlibcamera-utils.h\"\n>  #include \"gstlibcameraallocator.h\"\n>  #include \"gstlibcamerapad.h\"\n>  #include \"gstlibcamerapool.h\"\n> -#include \"gstlibcamera-utils.h\"\n>  \n>  using namespace libcamera;\n>  \n> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {\n>  \n>  \tControlList initControls_;\n>  \tguint group_id_;\n> +\tGstCameraControls controls_;\n>  \n>  \tint queueRequest();\n>  \tvoid requestCompleted(Request *request);\n> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {\n>  enum {\n>  \tPROP_0,\n>  \tPROP_CAMERA_NAME,\n> +\tPROP_LAST\n>  };\n>  \n>  static void gst_libcamera_src_child_proxy_init(gpointer g_iface,\n> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()\n>  \tif (!request)\n>  \t\treturn -ENOMEM;\n>  \n> +\t/* Apply controls */\n> +\tcontrols_.applyControls(request);\n> +\n>  \tstd::unique_ptr<RequestWrap> wrap =\n>  \t\tstd::make_unique<RequestWrap>(std::move(request));\n>  \n> @@ -226,6 +232,9 @@ GstLibcameraSrcState::requestCompleted(Request *request)\n>  \n>  \t{\n>  \t\tGLibLocker locker(&lock_);\n> +\n> +\t\tcontrols_.readMetadata(request);\n> +\n>  \t\twrap = std::move(queuedRequests_.front());\n>  \t\tqueuedRequests_.pop();\n>  \t}\n> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc *self)\n>  \t\treturn false;\n>  \t}\n>  \n> +\tself->state->controls_.setCamera(cam);\n> +\n>  \tcam->requestCompleted.connect(self->state, &GstLibcameraSrcState::requestCompleted);\n>  \n>  \t/* No need to lock here, we didn't start our threads yet. */\n> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n>  {\n>  \tGLibLocker lock(GST_OBJECT(object));\n>  \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n> +\tGstLibcameraSrcState *state = self->state;\n>  \n>  \tswitch (prop_id) {\n>  \tcase PROP_CAMERA_NAME:\n> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject *object, guint prop_id,\n>  \t\tself->camera_name = g_value_dup_string(value);\n>  \t\tbreak;\n>  \tdefault:\n> -\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n> +\t\tif (!state->controls_.setProperty(prop_id - PROP_LAST, value, pspec))\n> +\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n>  \t\tbreak;\n>  \t}\n>  }\n> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject *object, guint prop_id, GValue *value,\n>  {\n>  \tGLibLocker lock(GST_OBJECT(object));\n>  \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n> +\tGstLibcameraSrcState *state = self->state;\n>  \n>  \tswitch (prop_id) {\n>  \tcase PROP_CAMERA_NAME:\n>  \t\tg_value_set_string(value, self->camera_name);\n>  \t\tbreak;\n>  \tdefault:\n> -\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n> +\t\tif (!state->controls_.getProperty(prop_id - PROP_LAST, value, pspec))\n> +\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);\n>  \t\tbreak;\n>  \t}\n>  }\n> @@ -947,6 +962,7 @@ gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)\n>  \t\t\t\t\t\t\t     | G_PARAM_STATIC_STRINGS));\n>  \tg_object_class_install_property(object_class, PROP_CAMERA_NAME, spec);\n>  \n> +\tGstCameraControls::installProperties(object_class, PROP_LAST);\n>  }\n>  \n>  /* GstChildProxy implementation */\n> diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build\n> index c2a01e7b..6b7e53b5 100644\n> --- a/src/gstreamer/meson.build\n> +++ b/src/gstreamer/meson.build\n> @@ -25,6 +25,16 @@ libcamera_gst_sources = [\n>      'gstlibcamerasrc.cpp',\n>  ]\n>  \n> +# Generate gstreamer control properties\n> +\n> +gen_gst_controls_template = files('gstlibcamera-controls.cpp.in')\n> +libcamera_gst_sources += custom_target('gstlibcamera-controls.cpp',\n> +                                       input : controls_files,\n> +                                       output : 'gstlibcamera-controls.cpp',\n> +                                       command : [gen_gst_controls, '-o', '@OUTPUT@',\n> +                                                  '-t', gen_gst_controls_template, '@INPUT@'],\n> +                                       env : py_build_env)\n> +\n>  libcamera_gst_cpp_args = [\n>      '-DVERSION=\"@0@\"'.format(libcamera_git_version),\n>      '-DPACKAGE=\"@0@\"'.format(meson.project_name()),\n> diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py\n> index 7bafee59..03c77cc6 100644\n> --- a/utils/codegen/controls.py\n> +++ b/utils/codegen/controls.py\n> @@ -110,3 +110,11 @@ class Control(object):\n>              return f\"Span<const {typ}, {self.__size}>\"\n>          else:\n>              return f\"Span<const {typ}>\"\n> +\n> +    @property\n> +    def element_type(self):\n> +        return self.__data.get('type')\n> +\n> +    @property\n> +    def size(self):\n> +        return self.__size\n> diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py\n> new file mode 100755\n> index 00000000..0b899258\n> --- /dev/null\n> +++ b/utils/codegen/gen-gst-controls.py\n> @@ -0,0 +1,166 @@\n> +#!/usr/bin/env python3\n> +# SPDX-License-Identifier: GPL-2.0-or-later\n> +# Copyright (C) 2019, Google Inc.\n> +# Copyright (C) 2024, Jaslo Ziska\n> +#\n> +# Authors:\n> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n> +# Jaslo Ziska <jaslo@ziska.de>\n> +#\n> +# Generate gstreamer control properties from YAML\n> +\n> +import argparse\n> +import jinja2\n> +import re\n> +import sys\n> +import yaml\n> +\n> +from controls import Control\n> +\n> +\n> +exposed_controls = [\n> +    \"AeEnable\", \"AeLocked\", \"AeMeteringMode\", \"AeConstraintMode\",\n\nAeLocked is not settable, until we can expose it correctly I'd drop. We might\nalso want something more accurate, so we can match an exact frame?\n\n> +    \"AeExposureMode\", \"ExposureValue\", \"ExposureTime\", \"AnalogueGain\",\n> +    \"AeFlickerMode\", \"AeFlickerPeriod\", \"AeFlickerDetected\", \"Brightness\",\n\nI'm not sure about AeFlickerMode and AeFlickerDetected, and its supposed to be\nread-only. Without a use case I cannot tell the proper way to expose these, I\nwould drop that.\n\n> +    \"Contrast\", \"Lux\", \"AwbEnable\", \"AwbMode\", \"AwbLocked\", \"ColourGains\",\n\nAwbLocked is similar to AeLocked. Same goes for ColourGains and\nColourTemperature, probably fine as property, but read-only.\n\n> \n> +    \"ColourTemperature\", \"Saturation\", \"SensorBlackLevels\", \"Sharpness\",\n> +    \"FocusFoM\", \"ColourCorrectionMatrix\", \"ScalerCrop\", \"DigitalGain\",\n> +    \"FrameDuration\", \"SensorTemperature\", \"SensorTimestamp\", \"AfMode\",\n\nFrameDuration and SensorTimestamp should be meta, and attached to GstBuffer, I\nwould drop it from the list in this serie. SensorTimestamp is clearly a fit for\nGstReferenceTimestampMeta. I would need to read more about FrameDuration to see\nif we should use it, perhaps could be set to GstBuffer.duration ?\n\n> +    \"AfRange\", \"AfSpeed\", \"AfMetering\", \"AfWindows\", \"LensPosition\", \"AfState\",\n> +    \"AfPauseState\", \"Gamma\",\n\nAfState, AfPausedState are not settable, I'd remove for now until we can\ngenerate read-only properties. \n\ncheers\nNicolas\n\n> +]\n> +\n> +\n> +def find_common_prefix(strings):\n> +    prefix = strings[0]\n> +\n> +    for string in strings[1:]:\n> +        while string[:len(prefix)] != prefix and prefix:\n> +            prefix = prefix[:len(prefix) - 1]\n> +        if not prefix:\n> +            break\n> +\n> +    return prefix\n> +\n> +\n> +def format_description(description):\n> +    # Substitute doxygen keywords \\sa (see also) and \\todo\n> +    description = re.sub(r'\\\\sa((?: \\w+)+)',\n> +                         lambda match: 'See also: ' + ', '.join(\n> +                             map(kebab_case, match.group(1).strip().split(' '))\n> +                         ) + '.', description)\n> +    description = re.sub(r'\\\\todo', 'Todo:', description)\n> +\n> +    description = description.strip().split('\\n')\n> +    return '\\n'.join([\n> +        '\"' + line.replace('\\\\', r'\\\\').replace('\"', r'\\\"') + ' \"' for line in description if line\n> +    ]).rstrip()\n> +\n> +\n> +def snake_case(s):\n> +    return ''.join([\n> +        c.isupper() and ('_' + c.lower()) or c for c in s\n> +    ]).strip('_')\n> +\n> +\n> +def kebab_case(s):\n> +    return snake_case(s).replace('_', '-')\n> +\n> +\n> +def extend_control(ctrl):\n> +    if ctrl.vendor != 'libcamera':\n> +        ctrl.namespace = f'{ctrl.vendor}::'\n> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'\n> +    else:\n> +        ctrl.namespace = ''\n> +        ctrl.vendor_prefix = ''\n> +\n> +    ctrl.is_array = ctrl.size is not None\n> +\n> +    if ctrl.is_enum:\n> +        # Remove common prefix from enum variant names\n> +        prefix = find_common_prefix([enum.name for enum in ctrl.enum_values])\n> +        for enum in ctrl.enum_values:\n> +            enum.gst_name = kebab_case(enum.name.removeprefix(prefix))\n> +\n> +        ctrl.gtype = 'enum'\n> +        ctrl.default = '0'\n> +    elif ctrl.element_type == 'bool':\n> +        ctrl.gtype = 'boolean'\n> +        ctrl.default = 'false'\n> +    elif ctrl.element_type == 'float':\n> +        ctrl.gtype = 'float'\n> +        ctrl.default = '0'\n> +        ctrl.min = '-G_MAXFLOAT'\n> +        ctrl.max = 'G_MAXFLOAT'\n> +    elif ctrl.element_type == 'int32_t':\n> +        ctrl.gtype = 'int'\n> +        ctrl.default = '0'\n> +        ctrl.min = 'G_MININT'\n> +        ctrl.max = 'G_MAXINT'\n> +    elif ctrl.element_type == 'int64_t':\n> +        ctrl.gtype = 'int64'\n> +        ctrl.default = '0'\n> +        ctrl.min = 'G_MININT64'\n> +        ctrl.max = 'G_MAXINT64'\n> +    elif ctrl.element_type == 'uint8_t':\n> +        ctrl.gtype = 'uchar'\n> +        ctrl.default = '0'\n> +        ctrl.min = '0'\n> +        ctrl.max = 'G_MAXUINT8'\n> +    elif ctrl.element_type == 'Rectangle':\n> +        ctrl.is_rectangle = True\n> +        ctrl.default = '0'\n> +        ctrl.min = '0'\n> +        ctrl.max = 'G_MAXINT'\n> +    else:\n> +        raise RuntimeError(f'The type `{ctrl.element_type}` is unknown')\n> +\n> +    return ctrl\n> +\n> +\n> +def main(argv):\n> +    # Parse command line arguments\n> +    parser = argparse.ArgumentParser()\n> +    parser.add_argument('--output', '-o', metavar='file', type=str,\n> +                        help='Output file name. Defaults to standard output if not specified.')\n> +    parser.add_argument('--template', '-t', dest='template', type=str, required=True,\n> +                        help='Template file name.')\n> +    parser.add_argument('input', type=str, nargs='+',\n> +                        help='Input file name.')\n> +\n> +    args = parser.parse_args(argv[1:])\n> +\n> +    controls = {}\n> +    for input in args.input:\n> +        data = yaml.safe_load(open(input, 'rb').read())\n> +\n> +        vendor = data['vendor']\n> +        ctrls = controls.setdefault(vendor, [])\n> +\n> +        for ctrl in data['controls']:\n> +            ctrl = Control(*ctrl.popitem(), vendor)\n> +\n> +            if ctrl.name in exposed_controls:\n> +                ctrls.append(extend_control(ctrl))\n> +\n> +    data = {'controls': list(controls.items())}\n> +\n> +    env = jinja2.Environment()\n> +    env.filters['format_description'] = format_description\n> +    env.filters['snake_case'] = snake_case\n> +    env.filters['kebab_case'] = kebab_case\n> +    template = env.from_string(open(args.template, 'r', encoding='utf-8').read())\n> +    string = template.render(data)\n> +\n> +    if args.output:\n> +        with open(args.output, 'w', encoding='utf-8') as output:\n> +            output.write(string)\n> +    else:\n> +        sys.stdout.write(string)\n> +\n> +    return 0\n> +\n> +\n> +if __name__ == '__main__':\n> +    sys.exit(main(sys.argv))\n> diff --git a/utils/codegen/meson.build b/utils/codegen/meson.build\n> index adf33bba..904dd66d 100644\n> --- a/utils/codegen/meson.build\n> +++ b/utils/codegen/meson.build\n> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']\n>  \n>  gen_controls = files('gen-controls.py')\n>  gen_formats = files('gen-formats.py')\n> +gen_gst_controls = files('gen-gst-controls.py')\n>  gen_header = files('gen-header.sh')\n>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')\n>  gen_tracepoints = files('gen-tp-header.py')","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id A5E43C32FE\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 18 Oct 2024 18:19:27 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 6B398633C7;\n\tFri, 18 Oct 2024 20:19:26 +0200 (CEST)","from mail-qk1-x72c.google.com (mail-qk1-x72c.google.com\n\t[IPv6:2607:f8b0:4864:20::72c])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 9D88C633C6\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 18 Oct 2024 20:19:23 +0200 (CEST)","by mail-qk1-x72c.google.com with SMTP id\n\taf79cd13be357-7b15eadee87so8639485a.2\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 18 Oct 2024 11:19:23 -0700 (PDT)","from nicolas-tpx395.lan ([2606:6d00:15:862e::7a9])\n\tby smtp.gmail.com with ESMTPSA id\n\taf79cd13be357-7b156f8b33csm90829285a.21.2024.10.18.11.19.21\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tFri, 18 Oct 2024 11:19:21 -0700 (PDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=ndufresne-ca.20230601.gappssmtp.com\n\theader.i=@ndufresne-ca.20230601.gappssmtp.com\n\theader.b=\"CPs0NCnG\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=ndufresne-ca.20230601.gappssmtp.com; s=20230601; t=1729275562;\n\tx=1729880362; darn=lists.libcamera.org; \n\th=mime-version:user-agent:content-transfer-encoding:references\n\t:in-reply-to:date:to:from:subject:message-id:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=rpYOb8sK55HS+tMLUG5dE3azVk6aI0EmdFIBF+I2Ucg=;\n\tb=CPs0NCnGayoCBrk/8TyuQ7Uq/ad2sRzJRjqX+JUnNetG/0jlBorJ8tsanpTmVqIRZ0\n\t8AHtEHmi9VCty+12HwAvzF9xwn2Tmt1x9fVZhO4rZX7aWp7veDKTchLyrOkIKsJkHpvO\n\tpmSVL1xfAEEJ8IhyEaaXw9OMfJFLq69VRS0EJUbC2Ncp8FdLMlqxucJYAVn6/GvgvqlY\n\tjsRIa26Jh6gM6H4QRtQErSmSW8zS4U/AQ4Iu6K3oQwKXJHH/fR1Sf9lTBYr2utTIl6uT\n\ts86azKu8inEGSmKSQJmQTGEefNKAXXP21OrwUYIde1Ju7ALbZdc4QFkg/B5+VhouKNIk\n\tuMXQ==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1729275562; x=1729880362;\n\th=mime-version:user-agent:content-transfer-encoding:references\n\t:in-reply-to:date:to:from:subject:message-id:x-gm-message-state:from\n\t:to:cc:subject:date:message-id:reply-to;\n\tbh=rpYOb8sK55HS+tMLUG5dE3azVk6aI0EmdFIBF+I2Ucg=;\n\tb=dQTzMucRlWk9gF/CLlt1Eag0rMcVPL1oZm8JBxIMwDWINvo7rxhyTH5wrCSbKmf8tk\n\tJNSahHrxDXe5mrdJshVtKZcRz7IH7VjrEB+lcTlnc6a6mST8GjBHLgWIUn4xqr8sIrma\n\tD4qOq2X18zfWfOHBqKNPLHbtTDXAhWnsHqwJBZoR67q/8hfJAyGbbsqZ+rFCDwJ3u4Vi\n\t8Vn9tVPMI2mQ3vRFDbaH43BpGqhcHcM2n7PObG/Dd9XyhHCj7kvxdK/NXiZ1qpP/D+Eo\n\tz/Abls+W1ezngkKBE+oGqleveGaANGSLK7tRgm8ZXR+WxZfcL6Rguuftl8E8eVoExg+0\n\tNv+Q==","X-Forwarded-Encrypted":"i=1;\n\tAJvYcCW+Kq7Ax1C8r63dmabCIPc39gM732y59NCLm3YiiZ8CYMlUtq41SglM4yGhwgGVjEriGxmrzSvsEsPm7UyPuxM=@lists.libcamera.org","X-Gm-Message-State":"AOJu0Yzz59MDymg+oI/h95J6Sgt3eqsochg5avD4yWgAMXmkq3ybxdNg\n\tLKpfgKBQ6BznNWZEpkUy4fk6oFQ7lo3jOH/AIvMvl/1lpklwRqI3tMOYcPStMbszPJBKeARmnHp\n\t0","X-Google-Smtp-Source":"AGHT+IEpHII4lvoF5dMyZNaR+c+j3TjouddGB6X+MxNsY8CKDbenzDIJIZSy9d/6lVk/m3uxE3bLpg==","X-Received":"by 2002:a05:620a:1a0f:b0:7b1:532c:ee32 with SMTP id\n\taf79cd13be357-7b157c0ade9mr369151385a.54.1729275561902; \n\tFri, 18 Oct 2024 11:19:21 -0700 (PDT)","Message-ID":"<e189b0ca3b20f9a698022ed296e4fa4b2ca5fd62.camel@ndufresne.ca>","Subject":"Re: [PATCH v3 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","From":"Nicolas Dufresne <nicolas@ndufresne.ca>","To":"Jaslo Ziska <jaslo@ziska.de>, libcamera-devel@lists.libcamera.org","Date":"Fri, 18 Oct 2024 14:19:20 -0400","In-Reply-To":"<20241017172331.16377-4-jaslo@ziska.de>","References":"<20241017172331.16377-1-jaslo@ziska.de>\n\t<20241017172331.16377-4-jaslo@ziska.de>","Content-Type":"text/plain; charset=\"UTF-8\"","Content-Transfer-Encoding":"quoted-printable","User-Agent":"Evolution 3.52.4 (3.52.4-1.fc40) ","MIME-Version":"1.0","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":31864,"web_url":"https://patchwork.libcamera.org/comment/31864/","msgid":"<875xpl8mnd.fsf@ziska.de>","date":"2024-10-21T16:07:50","subject":"Re: [PATCH v3 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","submitter":{"id":173,"url":"https://patchwork.libcamera.org/api/people/173/","name":"Jaslo Ziska","email":"jaslo@ziska.de"},"content":"Hi Nicolas,\n\nthanks for the review.\n\nNicolas Dufresne <nicolas@ndufresne.ca> writes:\n> Le jeudi 17 octobre 2024 à 19:04 +0200, Jaslo Ziska a écrit :\n>> This commit implements gstreamer controls for the libcamera \n>> element by\n>> generating the controls from the control_ids_*.yaml files using \n>> a new\n>> gen-gst-controls.py script. The appropriate meson files are \n>> also changed\n>> to automatically run the script when building.\n>> \n>> The gen-gst-controls.py script works similar to the \n>> gen-controls.py\n>> script by parsing the control_ids_*.yaml files and generating \n>> C++ code\n>> for each exposed control.\n>> For the controls to be used as gstreamer properties the type \n>> for each\n>> control needs to be translated to the appropriate glib type and \n>> a\n>> GEnumValue is generated for each enum control. Then a\n>> g_object_install_property(), _get_property() and \n>> _set_property()\n>> function is generated for each control.\n>> The vendor controls get prefixed with \"$vendor-\" in the final \n>> gstreamer\n>> property name.\n>> \n>> The C++ code generated by the gen-gst-controls.py script is \n>> written into\n>> the template gstlibcamerasrc-controls.cpp.in file. The matching\n>> gstlibcamerasrc-controls.h header defines the GstCameraControls \n>> class\n>> which handles the installation of the gstreamer properties as \n>> well as\n>> keeping track of the control values and setting and getting the\n>> controls. The content of these functions is generated in the \n>> Python\n>> script.\n>> \n>> Finally the libcamerasrc element itself is edited to make use \n>> of the new\n>> GstCameraControls class. The way this works is by defining a \n>> PROP_LAST\n>> enum variant which is passed to the installProperties() \n>> function so the\n>> properties are defined with the appropriate offset. When \n>> getting or\n>> setting a property PROP_LAST is subtracted from the requested \n>> property\n>> to translate the control back into a libcamera::controls:: enum\n>> variant.\n>> \n>> Signed-off-by: Jaslo Ziska <jaslo@ziska.de>\n>> ---\n>>  src/gstreamer/gstlibcamera-controls.cpp.in | 332 \n>>  +++++++++++++++++++++\n>>  src/gstreamer/gstlibcamera-controls.h      |  43 +++\n>>  src/gstreamer/gstlibcamerasrc.cpp          |  22 +-\n>>  src/gstreamer/meson.build                  |  10 +\n>>  utils/codegen/controls.py                  |   8 +\n>>  utils/codegen/gen-gst-controls.py          | 166 +++++++++++\n>>  utils/codegen/meson.build                  |   1 +\n>>  7 files changed, 579 insertions(+), 3 deletions(-)\n>>  create mode 100644 src/gstreamer/gstlibcamera-controls.cpp.in\n>>  create mode 100644 src/gstreamer/gstlibcamera-controls.h\n>>  create mode 100755 utils/codegen/gen-gst-controls.py\n>> \n>> diff --git a/src/gstreamer/gstlibcamera-controls.cpp.in \n>> b/src/gstreamer/gstlibcamera-controls.cpp.in\n>> new file mode 100644\n>> index 00000000..6861f5d1\n>> --- /dev/null\n>> +++ b/src/gstreamer/gstlibcamera-controls.cpp.in\n>> @@ -0,0 +1,332 @@\n>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>> +/*\n>> + * Copyright (C) 2024, Jaslo Ziska\n>> + *\n>> + * GStreamer Camera Controls\n>> + *\n>> + * This file is auto-generated. Do not edit.\n>> + */\n>> +\n>> +#include <vector>\n>> +\n>> +#include <libcamera/control_ids.h>\n>> +#include <libcamera/controls.h>\n>> +#include <libcamera/geometry.h>\n>> +\n>> +#include \"gstlibcamera-controls.h\"\n>> +\n>> +using namespace libcamera;\n>> +\n>> +static void value_set_rectangle(GValue *value, const Rectangle \n>> &rect)\n>> +{\n>> +\tPoint top_left = rect.topLeft();\n>> +\tSize size = rect.size();\n>> +\n>> +\tGValue x = G_VALUE_INIT;\n>> +\tg_value_init(&x, G_TYPE_INT);\n>> +\tg_value_set_int(&x, top_left.x);\n>> +\tgst_value_array_append_and_take_value(value, &x);\n>> +\n>> +\tGValue y = G_VALUE_INIT;\n>> +\tg_value_init(&y, G_TYPE_INT);\n>> +\tg_value_set_int(&y, top_left.y);\n>> +\tgst_value_array_append_and_take_value(value, &y);\n>> +\n>> +\tGValue width = G_VALUE_INIT;\n>> +\tg_value_init(&width, G_TYPE_INT);\n>> +\tg_value_set_int(&width, size.width);\n>> +\tgst_value_array_append_and_take_value(value, &width);\n>> +\n>> +\tGValue height = G_VALUE_INIT;\n>> +\tg_value_init(&height, G_TYPE_INT);\n>> +\tg_value_set_int(&x, size.height);\n>> +\tgst_value_array_append_and_take_value(value, &height);\n>> +}\n>> +\n>> +static Rectangle value_get_rectangle(const GValue *value)\n>> +{\n>> +\tconst GValue *r;\n>> +\tr = gst_value_array_get_value(value, 0);\n>> +\tint x = g_value_get_int(r);\n>> +\tr = gst_value_array_get_value(value, 1);\n>> +\tint y = g_value_get_int(r);\n>> +\tr = gst_value_array_get_value(value, 2);\n>> +\tint w = g_value_get_int(r);\n>> +\tr = gst_value_array_get_value(value, 3);\n>> +\tint h = g_value_get_int(r);\n>> +\n>> +\treturn Rectangle(x, y, w, h);\n>> +}\n>> +\n>> +{% for vendor, ctrls in controls %}\n>> +{%- for ctrl in ctrls if ctrl.is_enum %}\n>> +static const GEnumValue {{ ctrl.name|snake_case }}_types[] = {\n>> +{%- for enum in ctrl.enum_values %}\n>> +\t{\n>> +\t\tcontrols::{{ ctrl.namespace }}{{ enum.name }},\n>> +\t\t{{ enum.description|format_description|indent('\\t\\t') \n>> }},\n>> +\t\t\"{{ enum.gst_name }}\"\n>> +\t},\n>> +{%- endfor %}\n>> +\t{0, NULL, NULL}\n>> +};\n>> +\n>> +#define TYPE_{{ ctrl.name|snake_case|upper }} \\\n>> +\t({{ ctrl.name|snake_case }}_get_type())\n>> +static GType {{ ctrl.name|snake_case }}_get_type()\n>> +{\n>> +\tstatic GType {{ ctrl.name|snake_case }}_type = 0;\n>> +\n>> +\tif (!{{ ctrl.name|snake_case }}_type)\n>> +\t\t{{ ctrl.name|snake_case }}_type =\n>> +\t\t\tg_enum_register_static(\"{{ ctrl.name }}\",\n>> +\t\t\t\t\t       {{ ctrl.name|snake_case }}_types);\n>> +\n>> +\treturn {{ ctrl.name|snake_case }}_type;\n>> +}\n>> +{% endfor %}\n>> +{%- endfor %}\n>> +\n>> +void GstCameraControls::installProperties(GObjectClass *klass, \n>> int lastPropId)\n>> +{\n>> +{%- for vendor, ctrls in controls %}\n>> +{%- for ctrl in ctrls %}\n>> +\n>> +{%- set spec %}\n>> +{%- if ctrl.is_rectangle -%}\n>> +gst_param_spec_array(\n>> +{%- else -%}\n>> +g_param_spec_{{ ctrl.gtype }}(\n>> +{%- endif -%}\n>> +{%- if ctrl.is_array %}\n>> +\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case \n>> }}-value\",\n>> +\t\"{{ ctrl.name }} Value\",\n>> +\t\"One {{ ctrl.name }} element value\",\n>> +{%- else %}\n>> +\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case }}\",\n>> +\t\"{{ ctrl.name }}\",\n>> +\t{{ ctrl.description|format_description|indent('\\t') }},\n>> +{%- endif %}\n>> +{%- if ctrl.is_enum %}\n>> +\tTYPE_{{ ctrl.name|snake_case|upper }},\n>> +\t{{ ctrl.default }},\n>> +{%- elif ctrl.is_rectangle %}\n>> +\tg_param_spec_int(\n>> +\t\t\"rectangle-value\",\n>> +\t\t\"Rectangle Value\",\n>> +\t\t\"One rectangle value, either x, y, width or height.\",\n>> +\t\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n>> +\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE | \n>> G_PARAM_READWRITE |\n>> +\t\t\t       G_PARAM_STATIC_STRINGS)\n>> +\t),\n>> +{%- elif ctrl.gtype == 'boolean' %}\n>> +\t{{ ctrl.default }},\n>> +{%- elif ctrl.gtype in ['float', 'int', 'int64', 'uchar'] %}\n>> +\t{{ ctrl.min }}, {{ ctrl.max }}, {{ ctrl.default }},\n>> +{%- endif %}\n>> +\t(GParamFlags) (GST_PARAM_CONTROLLABLE | G_PARAM_READWRITE \n>> |\n>> +\t\t       G_PARAM_STATIC_STRINGS)\n>> +)\n>> +{%- endset %}\n>> +\n>> +\tg_object_class_install_property(\n>> +\t\tklass,\n>> +\t\tlastPropId + controls::{{ ctrl.namespace }}{{ \n>> ctrl.name|snake_case|upper }},\n>> +{%- if ctrl.is_array %}\n>> +\t\tgst_param_spec_array(\n>> +\t\t\t\"{{ ctrl.vendor_prefix }}{{ ctrl.name|kebab_case \n>> }}\",\n>> +\t\t\t\"{{ ctrl.name }}\",\n>> +\t\t\t{{ \n>> ctrl.description|format_description|indent('\\t\\t\\t') }},\n>> +\t\t\t{{ spec|indent('\\t\\t\\t') }},\n>> +\t\t\t(GParamFlags) (GST_PARAM_CONTROLLABLE |\n>> +\t\t\t\t       G_PARAM_READWRITE |\n>> +\t\t\t\t       G_PARAM_STATIC_STRINGS)\n>> +\t\t)\n>> +{%- else %}\n>> +\t\t{{ spec|indent('\\t\\t') }}\n>> +{%- endif %}\n>> +\t);\n>> +{%- endfor %}\n>> +{%- endfor %}\n>> +}\n>> +\n>> +bool GstCameraControls::getProperty(guint propId, GValue \n>> *value,\n>> +\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n>> +{\n>> +\tif (!controls_acc_.contains(propId)) {\n>> +\t\tGST_WARNING(\"Control '%s' is not available, default \n>> value will \"\n>> +\t\t\t    \"be returned\",\n>> + \n>> controls::controls.at(propId)->name().c_str());\n>> +\t\treturn true;\n>> +\t}\n>> +\tconst ControlValue &cv = controls_acc_.get(propId);\n>> +\n>> +\tswitch (propId) {\n>> +{%- for vendor, ctrls in controls %}\n>> +{%- for ctrl in ctrls %}\n>> +\n>> +\tcase controls::{{ ctrl.namespace }}{{ \n>> ctrl.name|snake_case|upper }}: {\n>> +\t\tauto control = cv.get<{{ ctrl.type }}>();\n>> +\n>> +{%- if ctrl.is_array %}\n>> +\t\tfor (size_t i = 0; i < control.size(); ++i) {\n>> +\t\t\tGValue element = G_VALUE_INIT;\n>> +{%- if ctrl.is_rectangle %}\n>> +\t\t\tg_value_init(&element, GST_TYPE_PARAM_ARRAY_LIST);\n>> +\t\t\tvalue_set_rectangle(&element, control[i]);\n>> +{%- else %}\n>> +\t\t\tg_value_init(&element, G_TYPE_{{ ctrl.gtype|upper \n>> }});\n>> +\t\t\tg_value_set_{{ ctrl.gtype }}(&element, \n>> control[i]);\n>> +{%- endif %}\n>> +\t\t\tgst_value_array_append_and_take_value(value, \n>> &element);\n>> +\t\t}\n>> +{%- else %}\n>> +{%- if ctrl.is_rectangle %}\n>> +\t\tvalue_set_rectangle(value, control);\n>> +{%- else %}\n>> +\t\tg_value_set_{{ ctrl.gtype }}(value, control);\n>> +{%- endif %}\n>> +{%- endif %}\n>> +\n>> +\t\treturn true;\n>> +\t}\n>> +{%- endfor %}\n>> +{%- endfor %}\n>> +\n>> +\tdefault:\n>> +\t\treturn false;\n>> +\t}\n>> +}\n>> +\n>> +bool GstCameraControls::setProperty(guint propId, const GValue \n>> *value,\n>> +\t\t\t\t    [[maybe_unused]] GParamSpec *pspec)\n>> +{\n>> +\t/*\n>> +\t * Check whether the camera capabilities are already \n>> available.\n>> +\t * They might not be available if the pipeline has not \n>> started yet.\n>> +\t */\n>> +\tif (!capabilities_.empty()) {\n>> + \t\t/* If so, check that the control is supported by the \n>> camera. */\n>> +\t\tconst ControlId *cid = \n>> capabilities_.idmap().at(propId);\n>> +\t\tauto info = capabilities_.find(cid);\n>> +\n>> +\t\tif (info == capabilities_.end()) {\n>> +\t\t\tGST_WARNING(\"Control '%s' is not supported by the \n>> \"\n>> +\t\t\t\t    \"camera and will be ignored\",\n>> +\t\t\t\t    cid->name().c_str());\n>> +\t\t\treturn true;\n>> +\t\t}\n>> +\t}\n>> +\n>> +\tswitch (propId) {\n>> +{%- for vendor, ctrls in controls %}\n>> +{%- for ctrl in ctrls %}\n>> +\n>> +\tcase controls::{{ ctrl.namespace }}{{ \n>> ctrl.name|snake_case|upper }}: {\n>> +\t\tControlValue control;\n>> +{%- if ctrl.is_array %}\n>> +\t\tsize_t size = gst_value_array_get_size(value);\n>> +{%- if ctrl.size != 0 %}\n>> +\t\tif (size != {{ ctrl.size }}) {\n>> +\t\t\tGST_ERROR(\"Incorrect array size for control \"\n>> +\t\t\t\t  \"'{{ ctrl.name|kebab_case }}', must be of \"\n>> +\t\t\t\t  \"size {{ ctrl.size }}\");\n>> +\t\t\treturn true;\n>> +\t\t}\n>> +{%- endif %}\n>> +\n>> +\t\tstd::vector<{{ ctrl.element_type }}> values(size);\n>> +\t\tfor (size_t i = 0; i < size; ++i) {\n>> +\t\t\tconst GValue *element =\n>> +\t\t\t\tgst_value_array_get_value(value, i);\n>> +{%- if ctrl.is_rectangle %}\n>> +\t\t\tif (gst_value_array_get_size(element) != 4) {\n>> +\t\t\t\tGST_ERROR(\"Rectangle in control \"\n>> +\t\t\t\t\t  \"'{{ ctrl.name|kebab_case }}' at\"\n>> +\t\t\t\t\t  \"index %lu must be an array of size 4\",\n>> +\t\t\t\t\t  i);\n>> +\t\t\t\treturn true;\n>> +\t\t\t}\n>> +\t\t\tvalues[i] = value_get_rectangle(element);\n>> +{%- else %}\n>> +\t\t\tvalues[i] = g_value_get_{{ ctrl.gtype }}(element);\n>> +{%- endif %}\n>> +\t\t}\n>> +\n>> +{%- if ctrl.size == 0 %}\n>> +\t\tcontrol.set(Span<const {{ ctrl.element_type \n>> }}>(values.data(),\n>> +\t\t\t\t\t\t\t\tsize));\n>> +{%- else %}\n>> +\t\tcontrol.set(Span<const {{ ctrl.element_type }},\n>> +\t\t\t         {{ ctrl.size }}>(values.data(),\n>> +\t\t\t\t\t\t  {{ ctrl.size }}));\n>> +{%- endif %}\n>> +{%- else %}\n>> +{%- if ctrl.is_rectangle %}\n>> +\t\tif (gst_value_array_get_size(value) != 4) {\n>> +\t\t\tGST_ERROR(\"Rectangle in control \"\n>> +\t\t\t\t  \"'{{ ctrl.name|kebab_case }}' must be an \"\n>> +\t\t\t\t  \"array of size 4\");\n>> +\t\t\treturn true;\n>> +\t\t}\n>> +\t\tRectangle val = value_get_rectangle(value);\n>> +{%- else %}\n>> +\t\tauto val = g_value_get_{{ ctrl.gtype }}(value);\n>> +{%- endif %}\n>> +\t\tcontrol.set(val);\n>> +{%- endif %}\n>> +\t\tcontrols_.set(propId, control);\n>> +\t\tcontrols_acc_.set(propId, control);\n>> +\t\treturn true;\n>> +\t}\n>> +{%- endfor %}\n>> +{%- endfor %}\n>> +\n>> +\tdefault:\n>> +\t\treturn false;\n>> +\t}\n>> +}\n>> +\n>> +void GstCameraControls::setCamera(const \n>> std::shared_ptr<libcamera::Camera> &cam)\n>> +{\n>> +\tcapabilities_ = cam->controls();\n>> +\n>> +\t/*\n>> +\t * Check the controls which were set before the camera \n>> capabilities were\n>> +\t * known. This is required because GStreamer may set \n>> properties before\n>> +\t * the pipeline has started and thus before the camera was \n>> known.\n>> +\t */\n>> +\tControlList new_controls;\n>> +\tfor (auto control = controls_acc_.begin();\n>> +\t     control != controls_acc_.end();\n>> +\t     ++control) {\n>> +\t\tunsigned int id = control->first;\n>> +\t\tControlValue value = control->second;\n>> +\n>> +\t\tconst ControlId *cid = capabilities_.idmap().at(id);\n>> +\t\tauto info = capabilities_.find(cid);\n>> +\n>> +\t\t/* Only add controls which are supported. */\n>> +\t\tif (info != capabilities_.end())\n>> +\t\t\tnew_controls.set(id, value);\n>> +\t\telse\n>> +\t\t\tGST_WARNING(\"Control '%s' is not supported by the \n>> \"\n>> +\t\t\t\t    \"camera and will be ignored\",\n>> +\t\t\t\t    cid->name().c_str());\n>> +\t}\n>> +\n>> +\tcontrols_acc_ = new_controls;\n>> +\tcontrols_ = new_controls;\n>> +}\n>> +\n>> +void \n>> GstCameraControls::applyControls(std::unique_ptr<libcamera::Request> \n>> &request)\n>> +{\n>> +\trequest->controls().merge(controls_);\n>> +\tcontrols_.clear();\n>> +}\n>> +\n>> +void GstCameraControls::readMetadata(libcamera::Request \n>> *request)\n>> +{\n>> +\tcontrols_acc_.merge(request->metadata(),\n>> +\t\t\t    ControlList::MergePolicy::OverwriteExisting);\n>> +}\n>> diff --git a/src/gstreamer/gstlibcamera-controls.h \n>> b/src/gstreamer/gstlibcamera-controls.h\n>> new file mode 100644\n>> index 00000000..749220b5\n>> --- /dev/null\n>> +++ b/src/gstreamer/gstlibcamera-controls.h\n>> @@ -0,0 +1,43 @@\n>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>> +/*\n>> + * Copyright (C) 2023, Collabora Ltd.\n>> + *     Author: Nicolas Dufresne \n>> <nicolas.dufresne@collabora.com>\n>> + *\n>> + * GStreamer Camera Controls\n>> + */\n>> +\n>> +#pragma once\n>> +\n>> +#include <memory>\n>> +\n>> +#include <libcamera/camera.h>\n>> +#include <libcamera/controls.h>\n>> +#include <libcamera/request.h>\n>> +\n>> +#include \"gstlibcamerasrc.h\"\n>> +\n>> +namespace libcamera {\n>> +\n>> +class GstCameraControls\n>> +{\n>> +public:\n>> +\tstatic void installProperties(GObjectClass *klass, int \n>> lastProp);\n>> +\n>> +\tbool getProperty(guint propId, GValue *value, GParamSpec \n>> *pspec);\n>> +\tbool setProperty(guint propId, const GValue *value, \n>> GParamSpec *pspec);\n>> +\n>> +\tvoid setCamera(const std::shared_ptr<libcamera::Camera> \n>> &cam);\n>> +\n>> +\tvoid applyControls(std::unique_ptr<libcamera::Request> \n>> &request);\n>> +\tvoid readMetadata(libcamera::Request *request);\n>> +\n>> +private:\n>> +\t/* Supported controls and limits of camera. */\n>> +\tControlInfoMap capabilities_;\n>> +\t/* Set of user modified controls. */\n>> +\tControlList controls_;\n>> +\t/* Accumulator of all controls ever set and metadata \n>> returned by camera */\n>> +\tControlList controls_acc_;\n>> +};\n>> +\n>> +} /* namespace libcamera */\n>> diff --git a/src/gstreamer/gstlibcamerasrc.cpp \n>> b/src/gstreamer/gstlibcamerasrc.cpp\n>> index 40b787c8..8efa25f4 100644\n>> --- a/src/gstreamer/gstlibcamerasrc.cpp\n>> +++ b/src/gstreamer/gstlibcamerasrc.cpp\n>> @@ -37,10 +37,11 @@\n>>  \n>>  #include <gst/base/base.h>\n>>  \n>> +#include \"gstlibcamera-controls.h\"\n>> +#include \"gstlibcamera-utils.h\"\n>>  #include \"gstlibcameraallocator.h\"\n>>  #include \"gstlibcamerapad.h\"\n>>  #include \"gstlibcamerapool.h\"\n>> -#include \"gstlibcamera-utils.h\"\n>>  \n>>  using namespace libcamera;\n>>  \n>> @@ -128,6 +129,7 @@ struct GstLibcameraSrcState {\n>>  \n>>  \tControlList initControls_;\n>>  \tguint group_id_;\n>> +\tGstCameraControls controls_;\n>>  \n>>  \tint queueRequest();\n>>  \tvoid requestCompleted(Request *request);\n>> @@ -153,6 +155,7 @@ struct _GstLibcameraSrc {\n>>  enum {\n>>  \tPROP_0,\n>>  \tPROP_CAMERA_NAME,\n>> +\tPROP_LAST\n>>  };\n>>  \n>>  static void gst_libcamera_src_child_proxy_init(gpointer \n>>  g_iface,\n>> @@ -183,6 +186,9 @@ int GstLibcameraSrcState::queueRequest()\n>>  \tif (!request)\n>>  \t\treturn -ENOMEM;\n>>  \n>> +\t/* Apply controls */\n>> +\tcontrols_.applyControls(request);\n>> +\n>>  \tstd::unique_ptr<RequestWrap> wrap =\n>>  \t\tstd::make_unique<RequestWrap>(std::move(request));\n>>  \n>> @@ -226,6 +232,9 @@ \n>> GstLibcameraSrcState::requestCompleted(Request *request)\n>>  \n>>  \t{\n>>  \t\tGLibLocker locker(&lock_);\n>> +\n>> +\t\tcontrols_.readMetadata(request);\n>> +\n>>  \t\twrap = std::move(queuedRequests_.front());\n>>  \t\tqueuedRequests_.pop();\n>>  \t}\n>> @@ -408,6 +417,8 @@ gst_libcamera_src_open(GstLibcameraSrc \n>> *self)\n>>  \t\treturn false;\n>>  \t}\n>>  \n>> +\tself->state->controls_.setCamera(cam);\n>> +\n>>  \tcam->requestCompleted.connect(self->state, \n>>  &GstLibcameraSrcState::requestCompleted);\n>>  \n>>  \t/* No need to lock here, we didn't start our threads yet. \n>>  */\n>> @@ -722,6 +733,7 @@ gst_libcamera_src_set_property(GObject \n>> *object, guint prop_id,\n>>  {\n>>  \tGLibLocker lock(GST_OBJECT(object));\n>>  \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n>> +\tGstLibcameraSrcState *state = self->state;\n>>  \n>>  \tswitch (prop_id) {\n>>  \tcase PROP_CAMERA_NAME:\n>> @@ -729,7 +741,8 @@ gst_libcamera_src_set_property(GObject \n>> *object, guint prop_id,\n>>  \t\tself->camera_name = g_value_dup_string(value);\n>>  \t\tbreak;\n>>  \tdefault:\n>> -\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, \n>> pspec);\n>> +\t\tif (!state->controls_.setProperty(prop_id - PROP_LAST, \n>> value, pspec))\n>> +\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, \n>> pspec);\n>>  \t\tbreak;\n>>  \t}\n>>  }\n>> @@ -740,13 +753,15 @@ gst_libcamera_src_get_property(GObject \n>> *object, guint prop_id, GValue *value,\n>>  {\n>>  \tGLibLocker lock(GST_OBJECT(object));\n>>  \tGstLibcameraSrc *self = GST_LIBCAMERA_SRC(object);\n>> +\tGstLibcameraSrcState *state = self->state;\n>>  \n>>  \tswitch (prop_id) {\n>>  \tcase PROP_CAMERA_NAME:\n>>  \t\tg_value_set_string(value, self->camera_name);\n>>  \t\tbreak;\n>>  \tdefault:\n>> -\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, \n>> pspec);\n>> +\t\tif (!state->controls_.getProperty(prop_id - PROP_LAST, \n>> value, pspec))\n>> +\t\t\tG_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, \n>> pspec);\n>>  \t\tbreak;\n>>  \t}\n>>  }\n>> @@ -947,6 +962,7 @@ \n>> gst_libcamera_src_class_init(GstLibcameraSrcClass *klass)\n>>  \t\t\t\t\t\t\t     | G_PARAM_STATIC_STRINGS));\n>>  \tg_object_class_install_property(object_class, \n>>  PROP_CAMERA_NAME, spec);\n>>  \n>> +\tGstCameraControls::installProperties(object_class, \n>> PROP_LAST);\n>>  }\n>>  \n>>  /* GstChildProxy implementation */\n>> diff --git a/src/gstreamer/meson.build \n>> b/src/gstreamer/meson.build\n>> index c2a01e7b..6b7e53b5 100644\n>> --- a/src/gstreamer/meson.build\n>> +++ b/src/gstreamer/meson.build\n>> @@ -25,6 +25,16 @@ libcamera_gst_sources = [\n>>      'gstlibcamerasrc.cpp',\n>>  ]\n>>  \n>> +# Generate gstreamer control properties\n>> +\n>> +gen_gst_controls_template = \n>> files('gstlibcamera-controls.cpp.in')\n>> +libcamera_gst_sources += \n>> custom_target('gstlibcamera-controls.cpp',\n>> +                                       input : controls_files,\n>> +                                       output : \n>> 'gstlibcamera-controls.cpp',\n>> +                                       command : \n>> [gen_gst_controls, '-o', '@OUTPUT@',\n>> +                                                  '-t', \n>> gen_gst_controls_template, '@INPUT@'],\n>> +                                       env : py_build_env)\n>> +\n>>  libcamera_gst_cpp_args = [\n>>      '-DVERSION=\"@0@\"'.format(libcamera_git_version),\n>>      '-DPACKAGE=\"@0@\"'.format(meson.project_name()),\n>> diff --git a/utils/codegen/controls.py \n>> b/utils/codegen/controls.py\n>> index 7bafee59..03c77cc6 100644\n>> --- a/utils/codegen/controls.py\n>> +++ b/utils/codegen/controls.py\n>> @@ -110,3 +110,11 @@ class Control(object):\n>>              return f\"Span<const {typ}, {self.__size}>\"\n>>          else:\n>>              return f\"Span<const {typ}>\"\n>> +\n>> +    @property\n>> +    def element_type(self):\n>> +        return self.__data.get('type')\n>> +\n>> +    @property\n>> +    def size(self):\n>> +        return self.__size\n>> diff --git a/utils/codegen/gen-gst-controls.py \n>> b/utils/codegen/gen-gst-controls.py\n>> new file mode 100755\n>> index 00000000..0b899258\n>> --- /dev/null\n>> +++ b/utils/codegen/gen-gst-controls.py\n>> @@ -0,0 +1,166 @@\n>> +#!/usr/bin/env python3\n>> +# SPDX-License-Identifier: GPL-2.0-or-later\n>> +# Copyright (C) 2019, Google Inc.\n>> +# Copyright (C) 2024, Jaslo Ziska\n>> +#\n>> +# Authors:\n>> +# Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n>> +# Jaslo Ziska <jaslo@ziska.de>\n>> +#\n>> +# Generate gstreamer control properties from YAML\n>> +\n>> +import argparse\n>> +import jinja2\n>> +import re\n>> +import sys\n>> +import yaml\n>> +\n>> +from controls import Control\n>> +\n>> +\n>> +exposed_controls = [\n>> +    \"AeEnable\", \"AeLocked\", \"AeMeteringMode\", \n>> \"AeConstraintMode\",\n>\n> AeLocked is not settable, until we can expose it correctly I'd \n> drop. We might\n> also want something more accurate, so we can match an exact \n> frame?\n\nYou are right, I forgot that the read-only properties should not \nbe exposed yet, I will fix that.\n\n>> +    \"AeExposureMode\", \"ExposureValue\", \"ExposureTime\", \n>> \"AnalogueGain\",\n>> +    \"AeFlickerMode\", \"AeFlickerPeriod\", \"AeFlickerDetected\", \n>> \"Brightness\",\n>\n> I'm not sure about AeFlickerMode and AeFlickerDetected, and its \n> supposed to be\n> read-only. Without a use case I cannot tell the proper way to \n> expose these, I\n> would drop that.\n\nI think only AeFlickerDetected is read-only but I can leave them \nout for now.\n\n>> +    \"Contrast\", \"Lux\", \"AwbEnable\", \"AwbMode\", \"AwbLocked\", \n>> \"ColourGains\",\n>\n> AwbLocked is similar to AeLocked. Same goes for ColourGains and\n> ColourTemperature, probably fine as property, but read-only.\n\nColourGains is writeable, I tested that. But you are correct about \nColourTemperature.\n\n>> \n>> +    \"ColourTemperature\", \"Saturation\", \"SensorBlackLevels\", \n>> \"Sharpness\",\n>> +    \"FocusFoM\", \"ColourCorrectionMatrix\", \"ScalerCrop\", \n>> \"DigitalGain\",\n>> +    \"FrameDuration\", \"SensorTemperature\", \"SensorTimestamp\", \n>> \"AfMode\",\n>\n> FrameDuration and SensorTimestamp should be meta, and attached \n> to GstBuffer, I\n> would drop it from the list in this serie. SensorTimestamp is \n> clearly a fit for\n> GstReferenceTimestampMeta. I would need to read more about \n> FrameDuration to see\n> if we should use it, perhaps could be set to GstBuffer.duration \n> ?\n\nI will just drop them.\n\n>> +    \"AfRange\", \"AfSpeed\", \"AfMetering\", \"AfWindows\", \n>> \"LensPosition\", \"AfState\",\n>> +    \"AfPauseState\", \"Gamma\",\n>\n> AfState, AfPausedState are not settable, I'd remove for now \n> until we can\n> generate read-only properties.\n\nAdding the readable/writeable property and integrating that into \nthe Gstreamer element will be my next goal once this is merged.\n\nBest regards,\n\nJaslo\n\n> cheers\n> Nicolas\n>\n>> +]\n>> +\n>> +\n>> +def find_common_prefix(strings):\n>> +    prefix = strings[0]\n>> +\n>> +    for string in strings[1:]:\n>> +        while string[:len(prefix)] != prefix and prefix:\n>> +            prefix = prefix[:len(prefix) - 1]\n>> +        if not prefix:\n>> +            break\n>> +\n>> +    return prefix\n>> +\n>> +\n>> +def format_description(description):\n>> +    # Substitute doxygen keywords \\sa (see also) and \\todo\n>> +    description = re.sub(r'\\\\sa((?: \\w+)+)',\n>> +                         lambda match: 'See also: ' + ', \n>> '.join(\n>> +                             map(kebab_case, \n>> match.group(1).strip().split(' '))\n>> +                         ) + '.', description)\n>> +    description = re.sub(r'\\\\todo', 'Todo:', description)\n>> +\n>> +    description = description.strip().split('\\n')\n>> +    return '\\n'.join([\n>> +        '\"' + line.replace('\\\\', r'\\\\').replace('\"', r'\\\"') + \n>> ' \"' for line in description if line\n>> +    ]).rstrip()\n>> +\n>> +\n>> +def snake_case(s):\n>> +    return ''.join([\n>> +        c.isupper() and ('_' + c.lower()) or c for c in s\n>> +    ]).strip('_')\n>> +\n>> +\n>> +def kebab_case(s):\n>> +    return snake_case(s).replace('_', '-')\n>> +\n>> +\n>> +def extend_control(ctrl):\n>> +    if ctrl.vendor != 'libcamera':\n>> +        ctrl.namespace = f'{ctrl.vendor}::'\n>> +        ctrl.vendor_prefix = f'{ctrl.vendor}-'\n>> +    else:\n>> +        ctrl.namespace = ''\n>> +        ctrl.vendor_prefix = ''\n>> +\n>> +    ctrl.is_array = ctrl.size is not None\n>> +\n>> +    if ctrl.is_enum:\n>> +        # Remove common prefix from enum variant names\n>> +        prefix = find_common_prefix([enum.name for enum in \n>> ctrl.enum_values])\n>> +        for enum in ctrl.enum_values:\n>> +            enum.gst_name = \n>> kebab_case(enum.name.removeprefix(prefix))\n>> +\n>> +        ctrl.gtype = 'enum'\n>> +        ctrl.default = '0'\n>> +    elif ctrl.element_type == 'bool':\n>> +        ctrl.gtype = 'boolean'\n>> +        ctrl.default = 'false'\n>> +    elif ctrl.element_type == 'float':\n>> +        ctrl.gtype = 'float'\n>> +        ctrl.default = '0'\n>> +        ctrl.min = '-G_MAXFLOAT'\n>> +        ctrl.max = 'G_MAXFLOAT'\n>> +    elif ctrl.element_type == 'int32_t':\n>> +        ctrl.gtype = 'int'\n>> +        ctrl.default = '0'\n>> +        ctrl.min = 'G_MININT'\n>> +        ctrl.max = 'G_MAXINT'\n>> +    elif ctrl.element_type == 'int64_t':\n>> +        ctrl.gtype = 'int64'\n>> +        ctrl.default = '0'\n>> +        ctrl.min = 'G_MININT64'\n>> +        ctrl.max = 'G_MAXINT64'\n>> +    elif ctrl.element_type == 'uint8_t':\n>> +        ctrl.gtype = 'uchar'\n>> +        ctrl.default = '0'\n>> +        ctrl.min = '0'\n>> +        ctrl.max = 'G_MAXUINT8'\n>> +    elif ctrl.element_type == 'Rectangle':\n>> +        ctrl.is_rectangle = True\n>> +        ctrl.default = '0'\n>> +        ctrl.min = '0'\n>> +        ctrl.max = 'G_MAXINT'\n>> +    else:\n>> +        raise RuntimeError(f'The type `{ctrl.element_type}` is \n>> unknown')\n>> +\n>> +    return ctrl\n>> +\n>> +\n>> +def main(argv):\n>> +    # Parse command line arguments\n>> +    parser = argparse.ArgumentParser()\n>> +    parser.add_argument('--output', '-o', metavar='file', \n>> type=str,\n>> +                        help='Output file name. Defaults to \n>> standard output if not specified.')\n>> +    parser.add_argument('--template', '-t', dest='template', \n>> type=str, required=True,\n>> +                        help='Template file name.')\n>> +    parser.add_argument('input', type=str, nargs='+',\n>> +                        help='Input file name.')\n>> +\n>> +    args = parser.parse_args(argv[1:])\n>> +\n>> +    controls = {}\n>> +    for input in args.input:\n>> +        data = yaml.safe_load(open(input, 'rb').read())\n>> +\n>> +        vendor = data['vendor']\n>> +        ctrls = controls.setdefault(vendor, [])\n>> +\n>> +        for ctrl in data['controls']:\n>> +            ctrl = Control(*ctrl.popitem(), vendor)\n>> +\n>> +            if ctrl.name in exposed_controls:\n>> +                ctrls.append(extend_control(ctrl))\n>> +\n>> +    data = {'controls': list(controls.items())}\n>> +\n>> +    env = jinja2.Environment()\n>> +    env.filters['format_description'] = format_description\n>> +    env.filters['snake_case'] = snake_case\n>> +    env.filters['kebab_case'] = kebab_case\n>> +    template = env.from_string(open(args.template, 'r', \n>> encoding='utf-8').read())\n>> +    string = template.render(data)\n>> +\n>> +    if args.output:\n>> +        with open(args.output, 'w', encoding='utf-8') as \n>> output:\n>> +            output.write(string)\n>> +    else:\n>> +        sys.stdout.write(string)\n>> +\n>> +    return 0\n>> +\n>> +\n>> +if __name__ == '__main__':\n>> +    sys.exit(main(sys.argv))\n>> diff --git a/utils/codegen/meson.build \n>> b/utils/codegen/meson.build\n>> index adf33bba..904dd66d 100644\n>> --- a/utils/codegen/meson.build\n>> +++ b/utils/codegen/meson.build\n>> @@ -11,6 +11,7 @@ py_modules += ['jinja2', 'yaml']\n>>  \n>>  gen_controls = files('gen-controls.py')\n>>  gen_formats = files('gen-formats.py')\n>> +gen_gst_controls = files('gen-gst-controls.py')\n>>  gen_header = files('gen-header.sh')\n>>  gen_ipa_pub_key = files('gen-ipa-pub-key.py')\n>>  gen_tracepoints = files('gen-tp-header.py')","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 7C520BD808\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 21 Oct 2024 16:07:55 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 75E3565391;\n\tMon, 21 Oct 2024 18:07:54 +0200 (CEST)","from mo4-p00-ob.smtp.rzone.de (mo4-p00-ob.smtp.rzone.de\n\t[81.169.146.217])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id C05636538B\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 21 Oct 2024 18:07:52 +0200 (CEST)","from archlinux by smtp.strato.de (RZmta 51.2.11 AUTH)\n\twith ESMTPSA id zf9ba109LG7pVKx\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256 bits))\n\t(Client did not present a certificate);\n\tMon, 21 Oct 2024 18:07:51 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (2048-bit key;\n\tunprotected) header.d=ziska.de header.i=@ziska.de header.b=\"bZQsNQzE\";\n\tdkim=permerror (0-bit key) header.d=ziska.de header.i=@ziska.de\n\theader.b=\"ZMWWmym9\"; dkim-atps=neutral","ARC-Seal":"i=1; a=rsa-sha256; t=1729526871; cv=none;\n\td=strato.com; s=strato-dkim-0002;\n\tb=B3ngB+wSXOcpQtFhFSkt5AD3DRdrFDJa0x4HA57MLWWwYMjTpztWDawJ4ikU/3W7A7\n\tSoRqe/HgH5ef4h5oJlK0MUVN/UaJAFgRtYtPalmaLVLIkQ3lMZkVrQawg+xaUb8IT2TZ\n\tbYKHKdceoGD33FgHyQw//fTMhDkmtmqmaEgZjRM9khOtD+7ZXKpxdkX5CS0Hx2H6RAaZ\n\t85On632kLILgwBchtPjaOqnVUtMWn1m7zHtfoIbF70WOJKHOQE6aHvVQXl4xGaEVYYKa\n\tiMXZPBgDjpq+toVdaXbKGKL337LH1n0IUunFxL0hhhT2dye+OriMeGbaMdHpn7D8qWT4\n\ti/3g==","ARC-Message-Signature":"i=1; a=rsa-sha256; c=relaxed/relaxed; t=1729526871;\n\ts=strato-dkim-0002; d=strato.com;\n\th=Message-ID:Date:References:In-Reply-To:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=0OLzL0tV1QRwXh+TdpqOdbFjzvEkK7V0Gb3p8gga8MM=;\n\tb=kAuhDqUKkMvjdkT66RzRu5taADtK7HX5VLhHgp1j4t2t9yOJ69/TTPl1BLqG8PzrJa\n\t4yOdXvP+b3rOC5U655pqPV+bz2dr3Lnbnlea2gx/lDcGjB3xbEfJIWehgz0Fkdf9tbxP\n\tZJKS5ik28Xb0qmZT0bKwyC27Tw49owi8WdhK3ZwWOt6lc7dhhbR8gm3ByGV1+re8CJMo\n\tjYMNiXb8wTxdp5LPdAklduiUhLjydGYZhUjSVzXvceoJBJT7o8EOf1xHKtz5WtogcEF+\n\tFqO4Ekt2biV0UrlRtD+jDPLFnShv4bd5yxL59OiF40YrGT/7YWJTsDYBLiLlkQGLr8da\n\tE1NA==","ARC-Authentication-Results":"i=1; strato.com;\n    arc=none;\n    dkim=none","X-RZG-CLASS-ID":"mo00","DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/relaxed; t=1729526871;\n\ts=strato-dkim-0002; d=ziska.de;\n\th=Message-ID:Date:References:In-Reply-To:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=0OLzL0tV1QRwXh+TdpqOdbFjzvEkK7V0Gb3p8gga8MM=;\n\tb=bZQsNQzEklP/ulYAjVI8lqu+kUS8dAJjKinsbfBbcumxFkpiE6dOUKG+TZBPKQTlZd\n\tl2URK1VlIP2X2Cy1uKkhnZh1E/BHevC8YNMF2AJQ0D14jD7t2ZkeHB9cqUvzHd/eef4w\n\tQRZIaxm7iKMY3X5d3ynsWz8F1stG3tdlPdFG8PjPIciRckTqh4P3LLkQFdb/4BqonSea\n\tOPA8Zd1nIgIj70NDEZouRCLendSiY1jV+ckGCusoBRMxUc/Sz9dYNWcTzWuwonYM+EMx\n\tmgXuG7lPWO3ht4iyWV/7rPxOn7PF04I1LMmu8Hm+u2zSOVA7b+KMdHtITOUQ3RTq+SgN\n\tQskw==","v=1; a=ed25519-sha256; c=relaxed/relaxed; t=1729526871;\n\ts=strato-dkim-0003; d=ziska.de;\n\th=Message-ID:Date:References:In-Reply-To:Subject:Cc:To:From:Cc:Date:\n\tFrom:Subject:Sender;\n\tbh=0OLzL0tV1QRwXh+TdpqOdbFjzvEkK7V0Gb3p8gga8MM=;\n\tb=ZMWWmym98HlLG3h/FzoiZdmmH2fHFAmGVH5ba6d7QtLBJATZmMCCm3Uvv9OQ2pxx5h\n\tgRQiPkwwtFK4DxmtqABA=="],"X-RZG-AUTH":"\":Jm0XeU+IYfb0x77LHmrjN5Wlb7TBwusDqIM6Hizy8VdfzvKi4yoFC9cEiIqwXfJa0UVFIOBpUYAtVOpHIoJG1qZybSPb\"","From":"Jaslo Ziska <jaslo@ziska.de>","To":"Nicolas Dufresne <nicolas@ndufresne.ca>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v3 3/3] gstreamer: Generate controls from\n\tcontrol_ids_*.yaml files","In-Reply-To":"<e189b0ca3b20f9a698022ed296e4fa4b2ca5fd62.camel@ndufresne.ca>\n\t(Nicolas Dufresne's message of \"Fri, 18 Oct 2024 14:19:20 -0400\")","References":"<20241017172331.16377-1-jaslo@ziska.de>\n\t<20241017172331.16377-4-jaslo@ziska.de>\n\t<e189b0ca3b20f9a698022ed296e4fa4b2ca5fd62.camel@ndufresne.ca>","Date":"Mon, 21 Oct 2024 18:07:50 +0200","Message-ID":"<875xpl8mnd.fsf@ziska.de>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8; format=flowed","Content-Transfer-Encoding":"quoted-printable","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]