[{"id":23117,"web_url":"https://patchwork.libcamera.org/comment/23117/","msgid":"<20220521162449.ljjzcqj5bu5d2pay@uno.localdomain>","date":"2022-05-21T16:24:49","subject":"Re: [libcamera-devel] [PATCH v2 1/3] cam: Add a parser for capture\n\tscripts","submitter":{"id":3,"url":"https://patchwork.libcamera.org/api/people/3/","name":"Jacopo Mondi","email":"jacopo@jmondi.org"},"content":"Hi Laurent,\n\nOn Fri, May 20, 2022 at 07:52:32PM +0300, Laurent Pinchart wrote:\n> From: Jacopo Mondi <jacopo@jmondi.org>\n>\n> Add a parser class to the cam test application to control the capture\n> operations through a yaml script.\n>\n> The capture script currently allow to specify a list of controls and\n> their associated values to be applied per-frame.\n>\n> Also add a trivial capture script example to showcase the intended\n> script structure.\n>\n> Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>\n\nYou sob is missing here.\nYou could almost change the authorship too :)\n\n> ---\n> Changes since v1:\n>\n> - Add SPDX license identifier to capture-script.yaml\n> - Drop gcc 7 support as libcamera requires gcc 8 or newer\n> - Make scriptPath_ a local variable in CaptureScript constructor\n> - Drop file check as we want to accept symlinks too\n> - Pass const reference to fileName to CaptureScript constructor\n> - Avoid manual deletes of tokens\n> - Simplify and improve error message handling\n> - Print line and column number in error message\n> - Simplify handling of key/value parsing\n> - Simplify parser deletion\n> - Don't consume last tokens as that's not needed.\n> - Simplify CaptureScript::unpackFailure()\n> - Switch to the simpler event API\n> - Typo fixes\n> ---\n>  src/cam/capture-script.yaml |  46 +++++\n>  src/cam/capture_script.cpp  | 334 ++++++++++++++++++++++++++++++++++++\n>  src/cam/capture_script.h    |  60 +++++++\n>  src/cam/meson.build         |   2 +\n>  4 files changed, 442 insertions(+)\n>  create mode 100644 src/cam/capture-script.yaml\n>  create mode 100644 src/cam/capture_script.cpp\n>  create mode 100644 src/cam/capture_script.h\n>\n> diff --git a/src/cam/capture-script.yaml b/src/cam/capture-script.yaml\n> new file mode 100644\n> index 000000000000..6a749bc60cf7\n> --- /dev/null\n> +++ b/src/cam/capture-script.yaml\n> @@ -0,0 +1,46 @@\n> +# SPDX-License-Identifier: CC0-1.0\n> +\n> +# Capture script example\n> +#\n> +# A capture script allows to associate a list of controls and their values\n> +# to frame numbers.\n> +\n> +# \\todo Formally define the capture script structure with a schema\n> +\n> +# Notes:\n> +# - Controls have to be specified by name, as defined in the\n> +#   libcamera::controls:: enumeration\n> +# - Controls not supported by the camera currently operated are ignored\n> +# - Frame numbers shall be monotonically incrementing, gaps are allowed\n> +\n> +# Example:\n> +frames:\n> +  - 1:\n> +      Brightness: 0.0\n> +\n> +  - 40:\n> +      Brightness: 0.2\n> +\n> +  - 80:\n> +      Brightness: 0.4\n> +\n> +  - 120:\n> +      Brightness: 0.8\n> +\n> +  - 160:\n> +      Brightness: 0.4\n> +\n> +  - 200:\n> +      Brightness: 0.2\n> +\n> +  - 240:\n> +      Brightness: 0.0\n> +\n> +  - 280:\n> +      Brightness: -0.2\n> +\n> +  - 300:\n> +      Brightness: -0.4\n> +\n> +  - 340:\n> +      Brightness: -0.8\n> diff --git a/src/cam/capture_script.cpp b/src/cam/capture_script.cpp\n> new file mode 100644\n> index 000000000000..8e9120321a25\n> --- /dev/null\n> +++ b/src/cam/capture_script.cpp\n> @@ -0,0 +1,334 @@\n> +/* SPDX-License-Identifier: GPL-2.0-or-later */\n> +/*\n> + * capture_script.cpp - Capture session configuration script\n\nMissing copyright. Can you add IoB one ?\n\n> + */\n> +\n> +#include \"capture_script.h\"\n> +\n> +#include <iostream>\n> +#include <stdio.h>\n> +#include <stdlib.h>\n> +\n> +using namespace libcamera;\n> +\n> +CaptureScript::CaptureScript(std::shared_ptr<Camera> camera,\n> +\t\t\t     const std::string &fileName)\n> +\t: camera_(camera), valid_(false)\n> +{\n> +\tFILE *fh = fopen(fileName.c_str(), \"r\");\n> +\tif (!fh) {\n> +\t\tint ret = -errno;\n> +\t\tstd::cerr << \"Failed to open capture script \" << fileName\n> +\t\t\t  << \": \" << strerror(-ret) << std::endl;\n> +\t\treturn;\n> +\t}\n> +\n> +\t/*\n> +\t * Map the camera's controls to their name so that they can be\n> +\t * easily identified when parsing the script file.\n> +\t */\n> +\tfor (const auto &[control, info] : camera_->controls())\n> +\t\tcontrols_[control->name()] = control;\n> +\n> +\tint ret = parseScript(fh);\n> +\tfclose(fh);\n> +\tif (ret)\n> +\t\treturn;\n> +\n> +\tvalid_ = true;\n> +}\n> +\n> +/* Retrieve the control list associated with a frame number. */\n> +const ControlList &CaptureScript::frameControls(unsigned int frame)\n> +{\n> +\tstatic ControlList controls{};\n> +\n> +\tauto it = frameControls_.find(frame);\n> +\tif (it == frameControls_.end())\n> +\t\treturn controls;\n> +\n> +\treturn it->second;\n> +}\n> +\n> +CaptureScript::EventPtr CaptureScript::nextEvent(yaml_event_type_t expectedType)\n> +{\n> +\tEventPtr event(new yaml_event_t);\n> +\n> +\tif (!yaml_parser_parse(&parser_, event.get()))\n> +\t\treturn nullptr;\n> +\n> +\tif (expectedType != YAML_NO_EVENT && !checkEvent(event, expectedType))\n\nCould you move the expectedType != YAML_NO_EVENT check in checkEvent() ?\n\nIt's probably nicer to read, at the expense of an unconditional\nfunction call\n\n\n> +\t\treturn nullptr;\n> +\n> +\treturn event;\n> +}\n> +\n> +bool CaptureScript::checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const\n> +{\n> +\tif (event->type != expectedType) {\n> +\t\tstd::cerr << \"Capture script error on line \" << event->start_mark.line\n\nTried to insert a random error in the config file and I get line == 0,\ncolumn == 0\n\nApart from these two minors, the automatic deletion and usage of\nevent-based parsing is much nicer! Thanks\n\nReviewed-by: Jacopo Mondi <jacopo@jmondi.org>\n\nThanks\n  j\n\n> +\t\t\t  << \" column \" << event->start_mark.column << \": \"\n> +\t\t\t  << \"Expected \" << eventTypeName(expectedType)\n> +\t\t\t  << \" event, got \" << eventTypeName(event->type)\n> +\t\t\t  << std::endl;\n> +\t\treturn false;\n> +\t}\n> +\n> +\treturn true;\n> +}\n> +\n> +std::string CaptureScript::eventScalarValue(const EventPtr &event)\n> +{\n> +\treturn std::string(reinterpret_cast<char *>(event->data.scalar.value),\n> +\t\t\t   event->data.scalar.length);\n> +}\n> +\n> +std::string CaptureScript::eventTypeName(yaml_event_type_t type)\n> +{\n> +\tstatic const std::map<yaml_event_type_t, std::string> typeNames = {\n> +\t\t{ YAML_STREAM_START_EVENT, \"stream-start\" },\n> +\t\t{ YAML_STREAM_END_EVENT, \"stream-end\" },\n> +\t\t{ YAML_DOCUMENT_START_EVENT, \"document-start\" },\n> +\t\t{ YAML_DOCUMENT_END_EVENT, \"document-end\" },\n> +\t\t{ YAML_ALIAS_EVENT, \"alias\" },\n> +\t\t{ YAML_SCALAR_EVENT, \"scalar\" },\n> +\t\t{ YAML_SEQUENCE_START_EVENT, \"sequence-start\" },\n> +\t\t{ YAML_SEQUENCE_END_EVENT, \"sequence-end\" },\n> +\t\t{ YAML_MAPPING_START_EVENT, \"mapping-start\" },\n> +\t\t{ YAML_MAPPING_END_EVENT, \"mapping-end\" },\n> +\t};\n> +\n> +\tauto it = typeNames.find(type);\n> +\tif (it == typeNames.end())\n> +\t\treturn \"[type \" + std::to_string(type) + \"]\";\n> +\n> +\treturn it->second;\n> +}\n> +\n> +int CaptureScript::parseScript(FILE *script)\n> +{\n> +\tint ret = yaml_parser_initialize(&parser_);\n> +\tif (!ret) {\n> +\t\tstd::cerr << \"Failed to initialize yaml parser\" << std::endl;\n> +\t\treturn ret;\n> +\t}\n> +\n> +\t/* Delete the parser upon function exit. */\n> +\tstruct ParserDeleter {\n> +\t\tParserDeleter(yaml_parser_t *parser) : parser_(parser) { }\n> +\t\t~ParserDeleter() { yaml_parser_delete(parser_); }\n> +\t\tyaml_parser_t *parser_;\n> +\t} deleter(&parser_);\n> +\n> +\tyaml_parser_set_input_file(&parser_, script);\n> +\n> +\tEventPtr event = nextEvent(YAML_STREAM_START_EVENT);\n> +\tif (!event)\n> +\t\treturn -EINVAL;\n> +\n> +\tevent = nextEvent(YAML_DOCUMENT_START_EVENT);\n> +\tif (!event)\n> +\t\treturn -EINVAL;\n> +\n> +\tevent = nextEvent(YAML_MAPPING_START_EVENT);\n> +\tif (!event)\n> +\t\treturn -EINVAL;\n> +\n> +\twhile (1) {\n> +\t\tevent = nextEvent();\n> +\t\tif (!event)\n> +\t\t\treturn -EINVAL;\n> +\n> +\t\tif (event->type == YAML_MAPPING_END_EVENT)\n> +\t\t\treturn 0;\n> +\n> +\t\tif (!checkEvent(event, YAML_SCALAR_EVENT))\n> +\t\t\treturn -EINVAL;\n> +\n> +\t\tstd::string section = eventScalarValue(event);\n> +\n> +\t\tif (section == \"frames\") {\n> +\t\t\tparseFrames();\n> +\t\t} else {\n> +\t\t\tstd::cerr << \"Unsupported section '\" << section << \"'\"\n> +\t\t\t\t  << std::endl;\n> +\t\t\treturn -EINVAL;\n> +\t\t}\n> +\t}\n3> +}\n> +\n> +int CaptureScript::parseFrames()\n> +{\n> +\tEventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);\n> +\tif (!event)\n> +\t\treturn -EINVAL;\n> +\n> +\twhile (1) {\n> +\t\tevent = nextEvent();\n> +\t\tif (!event)\n> +\t\t\treturn -EINVAL;\n> +\n> +\t\tif (event->type == YAML_SEQUENCE_END_EVENT)\n> +\t\t\treturn 0;\n> +\n> +\t\tint ret = parseFrame(std::move(event));\n> +\t\tif (ret)\n> +\t\t\treturn ret;\n> +\t}\n> +}\n> +\n> +int CaptureScript::parseFrame(EventPtr event)\n> +{\n> +\tif (!checkEvent(event, YAML_MAPPING_START_EVENT))\n> +\t\treturn -EINVAL;\n> +\n> +\tstd::string key = parseScalar();\n> +\tif (key.empty())\n> +\t\treturn -EINVAL;\n> +\n> +\tunsigned int frameId = atoi(key.c_str());\n> +\n> +\tevent = nextEvent(YAML_MAPPING_START_EVENT);\n> +\tif (!event)\n> +\t\treturn -EINVAL;\n> +\n> +\tControlList controls{};\n> +\n> +\twhile (1) {\n> +\t\tevent = nextEvent();\n> +\t\tif (!event)\n> +\t\t\treturn -EINVAL;\n> +\n> +\t\tif (event->type == YAML_MAPPING_END_EVENT)\n> +\t\t\tbreak;\n> +\n> +\t\tint ret = parseControl(std::move(event), controls);\n> +\t\tif (ret)\n> +\t\t\treturn ret;\n> +\t}\n> +\n> +\tframeControls_[frameId] = std::move(controls);\n> +\n> +\tevent = nextEvent(YAML_MAPPING_END_EVENT);\n> +\tif (!event)\n> +\t\treturn -EINVAL;\n> +\n> +\treturn 0;\n> +}\n> +\n> +int CaptureScript::parseControl(EventPtr event, ControlList &controls)\n> +{\n> +\t/* We expect a value after a key. */\n> +\tstd::string name = eventScalarValue(event);\n> +\tif (name.empty())\n> +\t\treturn -EINVAL;\n> +\n> +\t/* If the camera does not support the control just ignore it. */\n> +\tauto it = controls_.find(name);\n> +\tif (it == controls_.end()) {\n> +\t\tstd::cerr << \"Unsupported control '\" << name << \"'\" << std::endl;\n> +\t\treturn -EINVAL;\n> +\t}\n> +\n> +\tstd::string value = parseScalar();\n> +\tif (value.empty())\n> +\t\treturn -EINVAL;\n> +\n> +\tconst ControlId *controlId = it->second;\n> +\tControlValue val = unpackControl(controlId, value);\n> +\tcontrols.set(controlId->id(), val);\n> +\n> +\treturn 0;\n> +}\n> +\n> +std::string CaptureScript::parseScalar()\n> +{\n> +\tEventPtr event = nextEvent(YAML_SCALAR_EVENT);\n> +\tif (!event)\n> +\t\treturn \"\";\n> +\n> +\treturn eventScalarValue(event);\n> +}\n> +\n> +void CaptureScript::unpackFailure(const ControlId *id, const std::string &repr)\n> +{\n> +\tstatic const std::map<unsigned int, const char *> typeNames = {\n> +\t\t{ ControlTypeNone, \"none\" },\n> +\t\t{ ControlTypeBool, \"bool\" },\n> +\t\t{ ControlTypeByte, \"byte\" },\n> +\t\t{ ControlTypeInteger32, \"int32\" },\n> +\t\t{ ControlTypeInteger64, \"int64\" },\n> +\t\t{ ControlTypeFloat, \"float\" },\n> +\t\t{ ControlTypeString, \"string\" },\n> +\t\t{ ControlTypeRectangle, \"Rectangle\" },\n> +\t\t{ ControlTypeSize, \"Size\" },\n> +\t};\n> +\n> +\tconst char *typeName;\n> +\tauto it = typeNames.find(id->type());\n> +\tif (it != typeNames.end())\n> +\t\ttypeName = it->second;\n> +\telse\n> +\t\ttypeName = \"unknown\";\n> +\n> +\tstd::cerr << \"Unsupported control '\" << repr << \"' for \"\n> +\t\t  << typeName << \" control \" << id->name() << std::endl;\n> +}\n> +\n> +ControlValue CaptureScript::unpackControl(const ControlId *id,\n> +\t\t\t\t\t  const std::string &repr)\n> +{\n> +\tControlValue value{};\n> +\n> +\tswitch (id->type()) {\n> +\tcase ControlTypeNone:\n> +\t\tbreak;\n> +\tcase ControlTypeBool: {\n> +\t\tbool val;\n> +\n> +\t\tif (repr == \"true\") {\n> +\t\t\tval = true;\n> +\t\t} else if (repr == \"false\") {\n> +\t\t\tval = false;\n> +\t\t} else {\n> +\t\t\tunpackFailure(id, repr);\n> +\t\t\treturn value;\n> +\t\t}\n> +\n> +\t\tvalue.set<bool>(val);\n> +\t\tbreak;\n> +\t}\n> +\tcase ControlTypeByte: {\n> +\t\tuint8_t val = strtol(repr.c_str(), NULL, 10);\n> +\t\tvalue.set<uint8_t>(val);\n> +\t\tbreak;\n> +\t}\n> +\tcase ControlTypeInteger32: {\n> +\t\tint32_t val = strtol(repr.c_str(), NULL, 10);\n> +\t\tvalue.set<int32_t>(val);\n> +\t\tbreak;\n> +\t}\n> +\tcase ControlTypeInteger64: {\n> +\t\tint64_t val = strtoll(repr.c_str(), NULL, 10);\n> +\t\tvalue.set<int64_t>(val);\n> +\t\tbreak;\n> +\t}\n> +\tcase ControlTypeFloat: {\n> +\t\tfloat val = strtof(repr.c_str(), NULL);\n> +\t\tvalue.set<float>(val);\n> +\t\tbreak;\n> +\t}\n> +\tcase ControlTypeString: {\n> +\t\tvalue.set<std::string>(repr);\n> +\t\tbreak;\n> +\t}\n> +\tcase ControlTypeRectangle:\n> +\t\t/* \\todo Parse rectangles. */\n> +\t\tbreak;\n> +\tcase ControlTypeSize:\n> +\t\t/* \\todo Parse Sizes. */\n> +\t\tbreak;\n> +\t}\n> +\n> +\treturn value;\n> +}\n> diff --git a/src/cam/capture_script.h b/src/cam/capture_script.h\n> new file mode 100644\n> index 000000000000..44368f63f34c\n> --- /dev/null\n> +++ b/src/cam/capture_script.h\n> @@ -0,0 +1,60 @@\n> +/* SPDX-License-Identifier: GPL-2.0-or-later */\n> +/*\n> + * capture_script.h - Capture session configuration script\n> + */\n> +\n> +#pragma once\n> +\n> +#include <map>\n> +#include <memory>\n> +#include <string>\n> +\n> +#include <libcamera/camera.h>\n> +#include <libcamera/controls.h>\n> +\n> +#include <yaml.h>\n> +\n> +class CaptureScript\n> +{\n> +public:\n> +\tCaptureScript(std::shared_ptr<libcamera::Camera> camera,\n> +\t\t      const std::string &fileName);\n> +\n> +\tbool valid() const { return valid_; }\n> +\n> +\tconst libcamera::ControlList &frameControls(unsigned int frame);\n> +\n> +private:\n> +\tstruct EventDeleter {\n> +\t\tvoid operator()(yaml_event_t *event) const\n> +\t\t{\n> +\t\t\tyaml_event_delete(event);\n> +\t\t\tdelete event;\n> +\t\t}\n> +\t};\n> +\tusing EventPtr = std::unique_ptr<yaml_event_t, EventDeleter>;\n> +\n> +\tstd::map<std::string, const libcamera::ControlId *> controls_;\n> +\tstd::map<unsigned int, libcamera::ControlList> frameControls_;\n> +\tstd::shared_ptr<libcamera::Camera> camera_;\n> +\tyaml_parser_t parser_;\n> +\tbool valid_;\n> +\n> +\tEventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT);\n> +\tbool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const;\n> +\tstatic std::string eventScalarValue(const EventPtr &event);\n> +\tstatic std::string eventTypeName(yaml_event_type_t type);\n> +\n> +\tint parseScript(FILE *script);\n> +\n> +\tint parseFrames();\n> +\tint parseFrame(EventPtr event);\n> +\tint parseControl(EventPtr event, libcamera::ControlList &controls);\n> +\n> +\tstd::string parseScalar();\n> +\n> +\tvoid unpackFailure(const libcamera::ControlId *id,\n> +\t\t\t   const std::string &repr);\n> +\tlibcamera::ControlValue unpackControl(const libcamera::ControlId *id,\n> +\t\t\t\t\t      const std::string &repr);\n> +};\n> diff --git a/src/cam/meson.build b/src/cam/meson.build\n> index 5bab8c9e331b..c47a85592478 100644\n> --- a/src/cam/meson.build\n> +++ b/src/cam/meson.build\n> @@ -11,6 +11,7 @@ cam_enabled = true\n>\n>  cam_sources = files([\n>      'camera_session.cpp',\n> +    'capture_script.cpp',\n>      'event_loop.cpp',\n>      'file_sink.cpp',\n>      'frame_sink.cpp',\n> @@ -38,6 +39,7 @@ cam  = executable('cam', cam_sources,\n>                        libcamera_public,\n>                        libdrm,\n>                        libevent,\n> +                      libyaml,\n>                    ],\n>                    cpp_args : cam_cpp_args,\n>                    install : true)\n> --\n> Regards,\n>\n> Laurent Pinchart\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 BE5E7C0F2A\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat, 21 May 2022 16:24:54 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 7385865663;\n\tSat, 21 May 2022 18:24:53 +0200 (CEST)","from relay12.mail.gandi.net (relay12.mail.gandi.net\n\t[217.70.178.232])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id AC5D765654\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat, 21 May 2022 18:24:51 +0200 (CEST)","(Authenticated sender: jacopo@jmondi.org)\n\tby mail.gandi.net (Postfix) with ESMTPSA id E72C9200003;\n\tSat, 21 May 2022 16:24:50 +0000 (UTC)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1653150293;\n\tbh=dYC06GbVMXd5m9cid7lzvl88302+OWFChTKFf+eFlg4=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=uFpZI6OOWCzo9AokQEpvCCRXEFl1v1urwCC8nk0UJDMH+zXfS8FCheTRVw5vJL0zL\n\tZ0CbG2/XDixTgmuXfyl2n4EtHhh7QYg3N2gd6PuaGsoSSqRsOglPLlS1mPOaJZktvz\n\tKArCuLosJaZOBmZfd9p5o7iCixKRVGbfuVkLpqRVUQCBWH31EHJXKPrORwe/0UyTPH\n\tpykz0k7sDzdilqgGAFrTe8gKTkhX86nguiEHyZKymbEIX38hiJUbURL45hK1Ezsk0K\n\tOrLcma00+L7FokOQyuf3fAQ8lKrtRjYJtLKUJcDzvdwvo/4PV0rirIL/JVmDtcRqiO\n\t4x6j6gcVFnERA==","Date":"Sat, 21 May 2022 18:24:49 +0200","To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Message-ID":"<20220521162449.ljjzcqj5bu5d2pay@uno.localdomain>","References":"<20220520165234.7642-1-laurent.pinchart@ideasonboard.com>\n\t<20220520165234.7642-2-laurent.pinchart@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220520165234.7642-2-laurent.pinchart@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v2 1/3] cam: Add a parser for capture\n\tscripts","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>","From":"Jacopo Mondi via libcamera-devel <libcamera-devel@lists.libcamera.org>","Reply-To":"Jacopo Mondi <jacopo@jmondi.org>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":23121,"web_url":"https://patchwork.libcamera.org/comment/23121/","msgid":"<YooHANSCbruIytwr@pendragon.ideasonboard.com>","date":"2022-05-22T09:48:48","subject":"Re: [libcamera-devel] [PATCH v2 1/3] cam: Add a parser for capture\n\tscripts","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Jacopo,\n\nOn Sat, May 21, 2022 at 06:24:49PM +0200, Jacopo Mondi wrote:\n> On Fri, May 20, 2022 at 07:52:32PM +0300, Laurent Pinchart wrote:\n> > From: Jacopo Mondi <jacopo@jmondi.org>\n> >\n> > Add a parser class to the cam test application to control the capture\n> > operations through a yaml script.\n> >\n> > The capture script currently allow to specify a list of controls and\n> > their associated values to be applied per-frame.\n> >\n> > Also add a trivial capture script example to showcase the intended\n> > script structure.\n> >\n> > Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>\n> \n> You sob is missing here.\n> You could almost change the authorship too :)\n\nNo no, you authored the original version and the structure is still the\nsame :-)\n\n> > ---\n> > Changes since v1:\n> >\n> > - Add SPDX license identifier to capture-script.yaml\n> > - Drop gcc 7 support as libcamera requires gcc 8 or newer\n> > - Make scriptPath_ a local variable in CaptureScript constructor\n> > - Drop file check as we want to accept symlinks too\n> > - Pass const reference to fileName to CaptureScript constructor\n> > - Avoid manual deletes of tokens\n> > - Simplify and improve error message handling\n> > - Print line and column number in error message\n> > - Simplify handling of key/value parsing\n> > - Simplify parser deletion\n> > - Don't consume last tokens as that's not needed.\n> > - Simplify CaptureScript::unpackFailure()\n> > - Switch to the simpler event API\n> > - Typo fixes\n> > ---\n> >  src/cam/capture-script.yaml |  46 +++++\n> >  src/cam/capture_script.cpp  | 334 ++++++++++++++++++++++++++++++++++++\n> >  src/cam/capture_script.h    |  60 +++++++\n> >  src/cam/meson.build         |   2 +\n> >  4 files changed, 442 insertions(+)\n> >  create mode 100644 src/cam/capture-script.yaml\n> >  create mode 100644 src/cam/capture_script.cpp\n> >  create mode 100644 src/cam/capture_script.h\n> >\n> > diff --git a/src/cam/capture-script.yaml b/src/cam/capture-script.yaml\n> > new file mode 100644\n> > index 000000000000..6a749bc60cf7\n> > --- /dev/null\n> > +++ b/src/cam/capture-script.yaml\n> > @@ -0,0 +1,46 @@\n> > +# SPDX-License-Identifier: CC0-1.0\n> > +\n> > +# Capture script example\n> > +#\n> > +# A capture script allows to associate a list of controls and their values\n> > +# to frame numbers.\n> > +\n> > +# \\todo Formally define the capture script structure with a schema\n> > +\n> > +# Notes:\n> > +# - Controls have to be specified by name, as defined in the\n> > +#   libcamera::controls:: enumeration\n> > +# - Controls not supported by the camera currently operated are ignored\n> > +# - Frame numbers shall be monotonically incrementing, gaps are allowed\n> > +\n> > +# Example:\n> > +frames:\n> > +  - 1:\n> > +      Brightness: 0.0\n> > +\n> > +  - 40:\n> > +      Brightness: 0.2\n> > +\n> > +  - 80:\n> > +      Brightness: 0.4\n> > +\n> > +  - 120:\n> > +      Brightness: 0.8\n> > +\n> > +  - 160:\n> > +      Brightness: 0.4\n> > +\n> > +  - 200:\n> > +      Brightness: 0.2\n> > +\n> > +  - 240:\n> > +      Brightness: 0.0\n> > +\n> > +  - 280:\n> > +      Brightness: -0.2\n> > +\n> > +  - 300:\n> > +      Brightness: -0.4\n> > +\n> > +  - 340:\n> > +      Brightness: -0.8\n> > diff --git a/src/cam/capture_script.cpp b/src/cam/capture_script.cpp\n> > new file mode 100644\n> > index 000000000000..8e9120321a25\n> > --- /dev/null\n> > +++ b/src/cam/capture_script.cpp\n> > @@ -0,0 +1,334 @@\n> > +/* SPDX-License-Identifier: GPL-2.0-or-later */\n> > +/*\n> > + * capture_script.cpp - Capture session configuration script\n> \n> Missing copyright. Can you add IoB one ?\n\nOK.\n\n> > + */\n> > +\n> > +#include \"capture_script.h\"\n> > +\n> > +#include <iostream>\n> > +#include <stdio.h>\n> > +#include <stdlib.h>\n> > +\n> > +using namespace libcamera;\n> > +\n> > +CaptureScript::CaptureScript(std::shared_ptr<Camera> camera,\n> > +\t\t\t     const std::string &fileName)\n> > +\t: camera_(camera), valid_(false)\n> > +{\n> > +\tFILE *fh = fopen(fileName.c_str(), \"r\");\n> > +\tif (!fh) {\n> > +\t\tint ret = -errno;\n> > +\t\tstd::cerr << \"Failed to open capture script \" << fileName\n> > +\t\t\t  << \": \" << strerror(-ret) << std::endl;\n> > +\t\treturn;\n> > +\t}\n> > +\n> > +\t/*\n> > +\t * Map the camera's controls to their name so that they can be\n> > +\t * easily identified when parsing the script file.\n> > +\t */\n> > +\tfor (const auto &[control, info] : camera_->controls())\n> > +\t\tcontrols_[control->name()] = control;\n> > +\n> > +\tint ret = parseScript(fh);\n> > +\tfclose(fh);\n> > +\tif (ret)\n> > +\t\treturn;\n> > +\n> > +\tvalid_ = true;\n> > +}\n> > +\n> > +/* Retrieve the control list associated with a frame number. */\n> > +const ControlList &CaptureScript::frameControls(unsigned int frame)\n> > +{\n> > +\tstatic ControlList controls{};\n> > +\n> > +\tauto it = frameControls_.find(frame);\n> > +\tif (it == frameControls_.end())\n> > +\t\treturn controls;\n> > +\n> > +\treturn it->second;\n> > +}\n> > +\n> > +CaptureScript::EventPtr CaptureScript::nextEvent(yaml_event_type_t expectedType)\n> > +{\n> > +\tEventPtr event(new yaml_event_t);\n> > +\n> > +\tif (!yaml_parser_parse(&parser_, event.get()))\n> > +\t\treturn nullptr;\n> > +\n> > +\tif (expectedType != YAML_NO_EVENT && !checkEvent(event, expectedType))\n> \n> Could you move the expectedType != YAML_NO_EVENT check in checkEvent() ?\n> \n> It's probably nicer to read, at the expense of an unconditional\n> function call\n\nI thought about it, but thought it would unnecessarily burden the other\ncallers of checkEvent() that never call the function with YAML_NO_EVENT.\n\n> > +\t\treturn nullptr;\n> > +\n> > +\treturn event;\n> > +}\n> > +\n> > +bool CaptureScript::checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const\n> > +{\n> > +\tif (event->type != expectedType) {\n> > +\t\tstd::cerr << \"Capture script error on line \" << event->start_mark.line\n> \n> Tried to insert a random error in the config file and I get line == 0,\n> column == 0\n\nThat's weird, I've just tested the same, and I'm getting\n\nCapture script error on line 23 column 10: Expected mapping-start event, got scalar\n\n> Apart from these two minors, the automatic deletion and usage of\n> event-based parsing is much nicer! Thanks\n> \n> Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>\n> \n> > +\t\t\t  << \" column \" << event->start_mark.column << \": \"\n> > +\t\t\t  << \"Expected \" << eventTypeName(expectedType)\n> > +\t\t\t  << \" event, got \" << eventTypeName(event->type)\n> > +\t\t\t  << std::endl;\n> > +\t\treturn false;\n> > +\t}\n> > +\n> > +\treturn true;\n> > +}\n> > +\n> > +std::string CaptureScript::eventScalarValue(const EventPtr &event)\n> > +{\n> > +\treturn std::string(reinterpret_cast<char *>(event->data.scalar.value),\n> > +\t\t\t   event->data.scalar.length);\n> > +}\n> > +\n> > +std::string CaptureScript::eventTypeName(yaml_event_type_t type)\n> > +{\n> > +\tstatic const std::map<yaml_event_type_t, std::string> typeNames = {\n> > +\t\t{ YAML_STREAM_START_EVENT, \"stream-start\" },\n> > +\t\t{ YAML_STREAM_END_EVENT, \"stream-end\" },\n> > +\t\t{ YAML_DOCUMENT_START_EVENT, \"document-start\" },\n> > +\t\t{ YAML_DOCUMENT_END_EVENT, \"document-end\" },\n> > +\t\t{ YAML_ALIAS_EVENT, \"alias\" },\n> > +\t\t{ YAML_SCALAR_EVENT, \"scalar\" },\n> > +\t\t{ YAML_SEQUENCE_START_EVENT, \"sequence-start\" },\n> > +\t\t{ YAML_SEQUENCE_END_EVENT, \"sequence-end\" },\n> > +\t\t{ YAML_MAPPING_START_EVENT, \"mapping-start\" },\n> > +\t\t{ YAML_MAPPING_END_EVENT, \"mapping-end\" },\n> > +\t};\n> > +\n> > +\tauto it = typeNames.find(type);\n> > +\tif (it == typeNames.end())\n> > +\t\treturn \"[type \" + std::to_string(type) + \"]\";\n> > +\n> > +\treturn it->second;\n> > +}\n> > +\n> > +int CaptureScript::parseScript(FILE *script)\n> > +{\n> > +\tint ret = yaml_parser_initialize(&parser_);\n> > +\tif (!ret) {\n> > +\t\tstd::cerr << \"Failed to initialize yaml parser\" << std::endl;\n> > +\t\treturn ret;\n> > +\t}\n> > +\n> > +\t/* Delete the parser upon function exit. */\n> > +\tstruct ParserDeleter {\n> > +\t\tParserDeleter(yaml_parser_t *parser) : parser_(parser) { }\n> > +\t\t~ParserDeleter() { yaml_parser_delete(parser_); }\n> > +\t\tyaml_parser_t *parser_;\n> > +\t} deleter(&parser_);\n> > +\n> > +\tyaml_parser_set_input_file(&parser_, script);\n> > +\n> > +\tEventPtr event = nextEvent(YAML_STREAM_START_EVENT);\n> > +\tif (!event)\n> > +\t\treturn -EINVAL;\n> > +\n> > +\tevent = nextEvent(YAML_DOCUMENT_START_EVENT);\n> > +\tif (!event)\n> > +\t\treturn -EINVAL;\n> > +\n> > +\tevent = nextEvent(YAML_MAPPING_START_EVENT);\n> > +\tif (!event)\n> > +\t\treturn -EINVAL;\n> > +\n> > +\twhile (1) {\n> > +\t\tevent = nextEvent();\n> > +\t\tif (!event)\n> > +\t\t\treturn -EINVAL;\n> > +\n> > +\t\tif (event->type == YAML_MAPPING_END_EVENT)\n> > +\t\t\treturn 0;\n> > +\n> > +\t\tif (!checkEvent(event, YAML_SCALAR_EVENT))\n> > +\t\t\treturn -EINVAL;\n> > +\n> > +\t\tstd::string section = eventScalarValue(event);\n> > +\n> > +\t\tif (section == \"frames\") {\n> > +\t\t\tparseFrames();\n> > +\t\t} else {\n> > +\t\t\tstd::cerr << \"Unsupported section '\" << section << \"'\"\n> > +\t\t\t\t  << std::endl;\n> > +\t\t\treturn -EINVAL;\n> > +\t\t}\n> > +\t}\n> 3> +}\n> > +\n> > +int CaptureScript::parseFrames()\n> > +{\n> > +\tEventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);\n> > +\tif (!event)\n> > +\t\treturn -EINVAL;\n> > +\n> > +\twhile (1) {\n> > +\t\tevent = nextEvent();\n> > +\t\tif (!event)\n> > +\t\t\treturn -EINVAL;\n> > +\n> > +\t\tif (event->type == YAML_SEQUENCE_END_EVENT)\n> > +\t\t\treturn 0;\n> > +\n> > +\t\tint ret = parseFrame(std::move(event));\n> > +\t\tif (ret)\n> > +\t\t\treturn ret;\n> > +\t}\n> > +}\n> > +\n> > +int CaptureScript::parseFrame(EventPtr event)\n> > +{\n> > +\tif (!checkEvent(event, YAML_MAPPING_START_EVENT))\n> > +\t\treturn -EINVAL;\n> > +\n> > +\tstd::string key = parseScalar();\n> > +\tif (key.empty())\n> > +\t\treturn -EINVAL;\n> > +\n> > +\tunsigned int frameId = atoi(key.c_str());\n> > +\n> > +\tevent = nextEvent(YAML_MAPPING_START_EVENT);\n> > +\tif (!event)\n> > +\t\treturn -EINVAL;\n> > +\n> > +\tControlList controls{};\n> > +\n> > +\twhile (1) {\n> > +\t\tevent = nextEvent();\n> > +\t\tif (!event)\n> > +\t\t\treturn -EINVAL;\n> > +\n> > +\t\tif (event->type == YAML_MAPPING_END_EVENT)\n> > +\t\t\tbreak;\n> > +\n> > +\t\tint ret = parseControl(std::move(event), controls);\n> > +\t\tif (ret)\n> > +\t\t\treturn ret;\n> > +\t}\n> > +\n> > +\tframeControls_[frameId] = std::move(controls);\n> > +\n> > +\tevent = nextEvent(YAML_MAPPING_END_EVENT);\n> > +\tif (!event)\n> > +\t\treturn -EINVAL;\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +int CaptureScript::parseControl(EventPtr event, ControlList &controls)\n> > +{\n> > +\t/* We expect a value after a key. */\n> > +\tstd::string name = eventScalarValue(event);\n> > +\tif (name.empty())\n> > +\t\treturn -EINVAL;\n> > +\n> > +\t/* If the camera does not support the control just ignore it. */\n> > +\tauto it = controls_.find(name);\n> > +\tif (it == controls_.end()) {\n> > +\t\tstd::cerr << \"Unsupported control '\" << name << \"'\" << std::endl;\n> > +\t\treturn -EINVAL;\n> > +\t}\n> > +\n> > +\tstd::string value = parseScalar();\n> > +\tif (value.empty())\n> > +\t\treturn -EINVAL;\n> > +\n> > +\tconst ControlId *controlId = it->second;\n> > +\tControlValue val = unpackControl(controlId, value);\n> > +\tcontrols.set(controlId->id(), val);\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +std::string CaptureScript::parseScalar()\n> > +{\n> > +\tEventPtr event = nextEvent(YAML_SCALAR_EVENT);\n> > +\tif (!event)\n> > +\t\treturn \"\";\n> > +\n> > +\treturn eventScalarValue(event);\n> > +}\n> > +\n> > +void CaptureScript::unpackFailure(const ControlId *id, const std::string &repr)\n> > +{\n> > +\tstatic const std::map<unsigned int, const char *> typeNames = {\n> > +\t\t{ ControlTypeNone, \"none\" },\n> > +\t\t{ ControlTypeBool, \"bool\" },\n> > +\t\t{ ControlTypeByte, \"byte\" },\n> > +\t\t{ ControlTypeInteger32, \"int32\" },\n> > +\t\t{ ControlTypeInteger64, \"int64\" },\n> > +\t\t{ ControlTypeFloat, \"float\" },\n> > +\t\t{ ControlTypeString, \"string\" },\n> > +\t\t{ ControlTypeRectangle, \"Rectangle\" },\n> > +\t\t{ ControlTypeSize, \"Size\" },\n> > +\t};\n> > +\n> > +\tconst char *typeName;\n> > +\tauto it = typeNames.find(id->type());\n> > +\tif (it != typeNames.end())\n> > +\t\ttypeName = it->second;\n> > +\telse\n> > +\t\ttypeName = \"unknown\";\n> > +\n> > +\tstd::cerr << \"Unsupported control '\" << repr << \"' for \"\n> > +\t\t  << typeName << \" control \" << id->name() << std::endl;\n> > +}\n> > +\n> > +ControlValue CaptureScript::unpackControl(const ControlId *id,\n> > +\t\t\t\t\t  const std::string &repr)\n> > +{\n> > +\tControlValue value{};\n> > +\n> > +\tswitch (id->type()) {\n> > +\tcase ControlTypeNone:\n> > +\t\tbreak;\n> > +\tcase ControlTypeBool: {\n> > +\t\tbool val;\n> > +\n> > +\t\tif (repr == \"true\") {\n> > +\t\t\tval = true;\n> > +\t\t} else if (repr == \"false\") {\n> > +\t\t\tval = false;\n> > +\t\t} else {\n> > +\t\t\tunpackFailure(id, repr);\n> > +\t\t\treturn value;\n> > +\t\t}\n> > +\n> > +\t\tvalue.set<bool>(val);\n> > +\t\tbreak;\n> > +\t}\n> > +\tcase ControlTypeByte: {\n> > +\t\tuint8_t val = strtol(repr.c_str(), NULL, 10);\n> > +\t\tvalue.set<uint8_t>(val);\n> > +\t\tbreak;\n> > +\t}\n> > +\tcase ControlTypeInteger32: {\n> > +\t\tint32_t val = strtol(repr.c_str(), NULL, 10);\n> > +\t\tvalue.set<int32_t>(val);\n> > +\t\tbreak;\n> > +\t}\n> > +\tcase ControlTypeInteger64: {\n> > +\t\tint64_t val = strtoll(repr.c_str(), NULL, 10);\n> > +\t\tvalue.set<int64_t>(val);\n> > +\t\tbreak;\n> > +\t}\n> > +\tcase ControlTypeFloat: {\n> > +\t\tfloat val = strtof(repr.c_str(), NULL);\n> > +\t\tvalue.set<float>(val);\n> > +\t\tbreak;\n> > +\t}\n> > +\tcase ControlTypeString: {\n> > +\t\tvalue.set<std::string>(repr);\n> > +\t\tbreak;\n> > +\t}\n> > +\tcase ControlTypeRectangle:\n> > +\t\t/* \\todo Parse rectangles. */\n> > +\t\tbreak;\n> > +\tcase ControlTypeSize:\n> > +\t\t/* \\todo Parse Sizes. */\n> > +\t\tbreak;\n> > +\t}\n> > +\n> > +\treturn value;\n> > +}\n> > diff --git a/src/cam/capture_script.h b/src/cam/capture_script.h\n> > new file mode 100644\n> > index 000000000000..44368f63f34c\n> > --- /dev/null\n> > +++ b/src/cam/capture_script.h\n> > @@ -0,0 +1,60 @@\n> > +/* SPDX-License-Identifier: GPL-2.0-or-later */\n> > +/*\n> > + * capture_script.h - Capture session configuration script\n> > + */\n> > +\n> > +#pragma once\n> > +\n> > +#include <map>\n> > +#include <memory>\n> > +#include <string>\n> > +\n> > +#include <libcamera/camera.h>\n> > +#include <libcamera/controls.h>\n> > +\n> > +#include <yaml.h>\n> > +\n> > +class CaptureScript\n> > +{\n> > +public:\n> > +\tCaptureScript(std::shared_ptr<libcamera::Camera> camera,\n> > +\t\t      const std::string &fileName);\n> > +\n> > +\tbool valid() const { return valid_; }\n> > +\n> > +\tconst libcamera::ControlList &frameControls(unsigned int frame);\n> > +\n> > +private:\n> > +\tstruct EventDeleter {\n> > +\t\tvoid operator()(yaml_event_t *event) const\n> > +\t\t{\n> > +\t\t\tyaml_event_delete(event);\n> > +\t\t\tdelete event;\n> > +\t\t}\n> > +\t};\n> > +\tusing EventPtr = std::unique_ptr<yaml_event_t, EventDeleter>;\n> > +\n> > +\tstd::map<std::string, const libcamera::ControlId *> controls_;\n> > +\tstd::map<unsigned int, libcamera::ControlList> frameControls_;\n> > +\tstd::shared_ptr<libcamera::Camera> camera_;\n> > +\tyaml_parser_t parser_;\n> > +\tbool valid_;\n> > +\n> > +\tEventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT);\n> > +\tbool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const;\n> > +\tstatic std::string eventScalarValue(const EventPtr &event);\n> > +\tstatic std::string eventTypeName(yaml_event_type_t type);\n> > +\n> > +\tint parseScript(FILE *script);\n> > +\n> > +\tint parseFrames();\n> > +\tint parseFrame(EventPtr event);\n> > +\tint parseControl(EventPtr event, libcamera::ControlList &controls);\n> > +\n> > +\tstd::string parseScalar();\n> > +\n> > +\tvoid unpackFailure(const libcamera::ControlId *id,\n> > +\t\t\t   const std::string &repr);\n> > +\tlibcamera::ControlValue unpackControl(const libcamera::ControlId *id,\n> > +\t\t\t\t\t      const std::string &repr);\n> > +};\n> > diff --git a/src/cam/meson.build b/src/cam/meson.build\n> > index 5bab8c9e331b..c47a85592478 100644\n> > --- a/src/cam/meson.build\n> > +++ b/src/cam/meson.build\n> > @@ -11,6 +11,7 @@ cam_enabled = true\n> >\n> >  cam_sources = files([\n> >      'camera_session.cpp',\n> > +    'capture_script.cpp',\n> >      'event_loop.cpp',\n> >      'file_sink.cpp',\n> >      'frame_sink.cpp',\n> > @@ -38,6 +39,7 @@ cam  = executable('cam', cam_sources,\n> >                        libcamera_public,\n> >                        libdrm,\n> >                        libevent,\n> > +                      libyaml,\n> >                    ],\n> >                    cpp_args : cam_cpp_args,\n> >                    install : true)","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 F0F58BD161\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSun, 22 May 2022 09:48:56 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 23CA365663;\n\tSun, 22 May 2022 11:48:56 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 36F7B60422\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSun, 22 May 2022 11:48:54 +0200 (CEST)","from pendragon.ideasonboard.com\n\t(static-11-127-145-212.ipcom.comunitel.net [212.145.127.11])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 571E2563;\n\tSun, 22 May 2022 11:48:53 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1653212936;\n\tbh=EC2flUdat3kzMce4yNcuSNUXt6rxUKlUFMUXHBLvLNg=;\n\th=Date:To:References:In-Reply-To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=JV8TNcRtOgPl1d4aqtVstv5B1N/Dv/Nq3/wUrfocDmKUpw/i3lf5h/c+tt+Gd21dq\n\ta+v/LXdfGGQsq3HTNeuTLrQk0cuJraV1t2EX+HqRs7FygjYrB00YV8jJqseUuRvsft\n\tKPp2276o2PPtg91lRm6mBJXljmCrJ23GZGpBJZM8naq6Saxcr7+3bJzfuUO8P1EKVV\n\t1H2rXrsOeIiOp7tPt+RF1sF4VJr3bac0nsqwUhsHnC4/V6v3/PXoSMatoGDO+RtbZy\n\tpB+8nCEf6DrbQZl1Pkz4bAP7sw8IaiZAhgLpVClBRFBQqOSiPtnew7isgoHDDqR8BR\n\tqGaF1VXaCkiTA==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1653212933;\n\tbh=EC2flUdat3kzMce4yNcuSNUXt6rxUKlUFMUXHBLvLNg=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=vMHjzJkhdplZHeDOvmjD3HzUg1vQ3loRtymgfGV+V6FdEpgJaguRn3BR5eTPOhU1T\n\tlkzJWMipN5mkqS2S7Ckd1NfhEFq+ijUhi3ykiSfL8mZUQ0JT/+KRqm5JIBtSQhebQC\n\tiIRsIYAytSF26vb0tc9IP1Ax6F7u/vNMZBFG5B9c="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"vMHjzJkh\"; dkim-atps=neutral","Date":"Sun, 22 May 2022 12:48:48 +0300","To":"Jacopo Mondi <jacopo@jmondi.org>","Message-ID":"<YooHANSCbruIytwr@pendragon.ideasonboard.com>","References":"<20220520165234.7642-1-laurent.pinchart@ideasonboard.com>\n\t<20220520165234.7642-2-laurent.pinchart@ideasonboard.com>\n\t<20220521162449.ljjzcqj5bu5d2pay@uno.localdomain>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220521162449.ljjzcqj5bu5d2pay@uno.localdomain>","Subject":"Re: [libcamera-devel] [PATCH v2 1/3] cam: Add a parser for capture\n\tscripts","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>","From":"Laurent Pinchart via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]