[{"id":144,"web_url":"https://patchwork.libcamera.org/comment/144/","msgid":"<20181230223948.GG31866@bigcity.dyn.berto.se>","date":"2018-12-30T22:39:48","subject":"Re: [libcamera-devel] [PATCH v3 2/3] libcamera: Add MediaDevice\n\tclass","submitter":{"id":5,"url":"https://patchwork.libcamera.org/api/people/5/","name":"Niklas Söderlund","email":"niklas.soderlund@ragnatech.se"},"content":"Hi Jacopo,\n\nThanks for your work.\n\nOn 2018-12-30 15:23:13 +0100, Jacopo Mondi wrote:\n> The MediaDevice object implements handling and configuration of the media\n> graph associated with a media device.\n> \n> The class allows enumeration of all pads, links and entities registered in\n> the media graph\n> \n> Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>\n> ---\n>  src/libcamera/include/media_device.h |  61 +++++\n>  src/libcamera/media_device.cpp       | 370 +++++++++++++++++++++++++++\n>  src/libcamera/meson.build            |   2 +\n>  3 files changed, 433 insertions(+)\n>  create mode 100644 src/libcamera/include/media_device.h\n>  create mode 100644 src/libcamera/media_device.cpp\n> \n> diff --git a/src/libcamera/include/media_device.h b/src/libcamera/include/media_device.h\n> new file mode 100644\n> index 0000000..6a9260e\n> --- /dev/null\n> +++ b/src/libcamera/include/media_device.h\n> @@ -0,0 +1,61 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2018, Google Inc.\n> + *\n> + * media_device.h - Media device handler\n> + */\n> +#ifndef __LIBCAMERA_MEDIA_DEVICE_H__\n> +#define __LIBCAMERA_MEDIA_DEVICE_H__\n> +\n> +#include <map>\n> +#include <sstream>\n> +#include <string>\n> +#include <vector>\n> +\n> +#include <linux/media.h>\n> +\n> +#include \"media_object.h\"\n> +\n> +namespace libcamera {\n> +\n> +class MediaDevice\n> +{\n> +public:\n> +\tMediaDevice() : fd_(-1) { };\n> +\t~MediaDevice();\n> +\n> +\tconst std::string driver() const { return driver_; }\n> +\tconst std::string devnode() const { return devnode_; }\n> +\n> +\tint open(const std::string &devnode);\n> +\tvoid close();\n\nThis bothers me somewhat. Would it be a valid use-case for a MediaDevice \nobject to be around while the fd is closed after it have been open()?\n\nWhy not rename open() to init() and move all of close() to \n~MediaDevice()? Looking at the code bellow there seems to be overlap \nbetween the two.\n\n> +\n> +\tconst std::vector<MediaEntity *> &entities() const { return entities_; }\n> +\n> +\tint populate();\n> +\n> +private:\n> +\tstd::string driver_;\n> +\tstd::string devnode_;\n> +\tint fd_;\n> +\n> +\t/*\n> +\t * Global map of media objects (entities, pads, links) associated to\n> +\t * their globally unique id.\n> +\t */\n> +\tstd::map<unsigned int, MediaObject *> objects_;\n> +\tMediaObject *getObject(unsigned int id);\n> +\tint addObject(MediaObject *obj);\n> +\tvoid deleteObjects();\n> +\n> +\t/* Global list of media entities in the media graph: lookup by name. */\n> +\tstd::vector<MediaEntity *> entities_;\n> +\tMediaEntity *getEntityByName(const std::string &name);\n> +\n> +\tint populateEntities(const struct media_v2_topology &topology);\n> +\tint populatePads(const struct media_v2_topology &topology);\n> +\tint populateLinks(const struct media_v2_topology &topology);\n> +};\n> +\n> +} /* namespace libcamera */\n> +#endif /* __LIBCAMERA_MEDIA_DEVICE_H__ */\n> diff --git a/src/libcamera/media_device.cpp b/src/libcamera/media_device.cpp\n> new file mode 100644\n> index 0000000..497b12d\n> --- /dev/null\n> +++ b/src/libcamera/media_device.cpp\n> @@ -0,0 +1,370 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2018, Google Inc.\n> + *\n> + * media_device.cpp - Media device handler\n> + */\n> +\n> +#include <errno.h>\n> +#include <fcntl.h>\n> +#include <string.h>\n> +#include <sys/ioctl.h>\n> +#include <unistd.h>\n> +\n> +#include <string>\n> +#include <vector>\n> +\n> +#include <linux/media.h>\n> +\n> +#include \"log.h\"\n> +#include \"media_device.h\"\n> +\n> +/**\n> + * \\file media_device.h\n> + */\n> +namespace libcamera {\n> +\n> +/**\n> + * \\class MediaDevice\n> + * \\brief Media device handler\n> + *\n> + * MediaDevice represents the graph of media objects associated with a\n> + * media device as exposed by the kernel through the media controller APIs.\n> + *\n> + * The caller is responsible for opening the MediaDevice explicitly before\n> + * operating on it, and shall close it when not needed anymore, as access\n> + * to the MediaDevice is exclusive.\n> + *\n> + * A MediaDevice is created empty and gets populated by inspecting the media\n> + * graph topology using the MEDIA_IOC_G_TOPOLOGY ioctls. Representation\n> + * of each entity, pad and link described are created using MediaObject\n> + * derived classes.\n> + *\n> + * All MediaObject are stored in a global pool, where they could be retrieved\n> + * from by their globally unique id.\n> + *\n> + * References to MediaEntity registered in the graph are stored in a vector\n> + * to allow easier by-name lookup, and the list of MediaEntities is accessible.\n\nAccessible for whom?\n\n> + */\n> +\n> +/**\n> + * \\brief Close the media device file descriptor and delete all \n> object\n> + */\n> +MediaDevice::~MediaDevice()\n> +{\n> +\tif (fd_ != -1)\n> +\t\t::close(fd_);\n> +\tdeleteObjects();\n> +}\n> +\n> +/**\n> + * \\fn MediaDevice::driver()\n> + * \\brief Return the driver name that handles the media graph this object\n> + * represents.\n> + */\n> +\n> +/**\n> + * \\fn MediaDevice::devnode()\n> + * \\brief Return the media device devnode node associated with this MediaDevice.\n> + */\n\nIs it useful to document these two simple getters?\n\n> +\n> +/**\n> + * \\brief Delete all media objects in the MediaDevice.\n> + *\n> + * Delete all MediaEntities; entities will then delete their pads,\n> + * and each source pad will delete links.\n> + *\n> + * After this function has been called, the media graph will be unpopulated\n> + * and its media objects deleted. The media device has to be populated\n> + * before it could be used again.\n\nIs there a use-case to delete all objects and re-populate() the graph?  \nIf not can this not be moved to the destructor?\n\n> + */\n> +void MediaDevice::deleteObjects()\n> +{\n> +\tfor (auto const &e : entities_)\n> +\t\tdelete e;\n> +\n> +\tobjects_.clear();\n> +\tentities_.clear();\n> +}\n> +\n> +/**\n> + * \\brief Open a media device and retrieve informations from it.\n> + * \\param devnode The media device node path.\n> + *\n> + * The function fails if the media device is already open or if either\n> + * open or the media device information retrieval operations fail.\n> + *\n> + * \\return Returns 0 for success or a negative error number otherwise.\n> + */\n> +int MediaDevice::open(const std::string &devnode)\n> +{\n> +\tif (fd_ != -1) {\n> +\t\tLOG(Error) << \"MediaDevice already open\";\n> +\t\treturn -EBUSY;\n> +\t}\n> +\n> +\tint ret = ::open(devnode.c_str(), O_RDWR);\n> +\tif (ret < 0) {\n> +\t\tret = -errno;\n> +\t\tLOG(Error) << \"Failed to open media device at \" << devnode\n> +\t\t\t   << \": \" << strerror(-ret);\n> +\t\treturn ret;\n> +\t}\n> +\tfd_ = ret;\n> +\tdevnode_ = devnode;\n> +\n> +\tstruct media_device_info info = { };\n> +\tret = ioctl(fd_, MEDIA_IOC_DEVICE_INFO, &info);\n> +\tif (ret) {\n> +\t\tret = -errno;\n> +\t\tLOG(Error) << \"Failed to get media device info \"\n> +\t\t\t   << \": \" << strerror(-ret);\n\nAs you have easy access to devnode here I would add it to the error \nmessage.\n\n> +\t\treturn ret;\n> +\t}\n> +\n> +\tdriver_ = info.driver;\n> +\n> +\treturn 0;\n> +}\n> +\n> +/**\n> + * \\brief Close the file descriptor associated with the media device.\n> + */\n> +void MediaDevice::close()\n> +{\n> +\tif (fd_ == -1)\n> +\t\treturn;\n> +\n> +\t::close(fd_);\n> +\tfd_ = -1;\n> +}\n\nI think this should be moved/merged to the destructor and this function \nremoved. If not possible/desirable the code duplication should be \nreduced by calling close() in the destructor.\n\n> +\n> +/**\n> + * \\fn MediaDevice::entities()\n> + * \\brief Return the list of MediaEntity references.\n> + */\n\nNeeded?\n\n> +\n> +/*\n> + * Add a new object to the global objects pool and fail if the object\n> + * has already been registered.\n> + */\n> +int MediaDevice::addObject(MediaObject *obj)\n> +{\n> +\n> +\tif (objects_.find(obj->id()) != objects_.end()) {\n> +\t\tLOG(Error) << \"Element with id \" << obj->id()\n> +\t\t\t   << \" already enumerated.\";\n> +\t\treturn -EEXIST;\n> +\t}\n> +\n> +\tobjects_[obj->id()] = obj;\n> +\n> +\treturn 0;\n> +}\n> +\n> +/*\n> + * MediaObject pool lookup by id.\n> + */\n> +MediaObject *MediaDevice::getObject(unsigned int id)\n> +{\n> +\tauto it = objects_.find(id);\n> +\treturn (it == objects_.end()) ? nullptr : it->second;\n> +}\n> +\n> +/**\n> + * \\brief Return the MediaEntity with name \\a name.\n> + * \\param name The entity name.\n> + * \\return The entity with \\a name.\n> + * \\return nullptr if no entity with \\a name is found.\n> + */\n> +MediaEntity *MediaDevice::getEntityByName(const std::string &name)\n> +{\n> +\tfor (MediaEntity *e : entities_)\n> +\t\tif (e->name() == name)\n> +\t\t\treturn e;\n> +\n> +\treturn nullptr;\n> +}\n> +\n> +int MediaDevice::populateLinks(const struct media_v2_topology &topology)\n> +{\n> +\tmedia_v2_link *mediaLinks = reinterpret_cast<media_v2_link *>\n> +\t\t\t\t    (topology.ptr_links);\n> +\n> +\tfor (unsigned int i = 0; i < topology.num_links; ++i) {\n> +\t\t/*\n> +\t\t * Skip links between entities and interfaces: we only care\n> +\t\t * about pad-2-pad links here.\n> +\t\t */\n> +\t\tif ((mediaLinks[i].flags & MEDIA_LNK_FL_LINK_TYPE) ==\n> +\t\t    MEDIA_LNK_FL_INTERFACE_LINK)\n> +\t\t\tcontinue;\n> +\n> +\t\t/* Store references to source and sink pads in the link. */\n> +\t\tunsigned int source_id = mediaLinks[i].source_id;\n> +\t\tMediaPad *source = dynamic_cast<MediaPad *>\n> +\t\t\t\t   (getObject(source_id));\n> +\t\tif (!source) {\n> +\t\t\tLOG(Error) << \"Failed to find pad with id: \"\n> +\t\t\t\t   << source_id;\n> +\t\t\treturn -ENODEV;\n> +\t\t}\n> +\n> +\t\tunsigned int sink_id = mediaLinks[i].sink_id;\n> +\t\tMediaPad *sink = dynamic_cast<MediaPad *>\n> +\t\t\t\t (getObject(sink_id));\n> +\t\tif (!sink) {\n> +\t\t\tLOG(Error) << \"Failed to find pad with id: \"\n> +\t\t\t\t   << sink_id;\n> +\t\t\treturn -ENODEV;\n> +\t\t}\n> +\n> +\t\tMediaLink *link = new MediaLink(&mediaLinks[i], source, sink);\n> +\t\tsource->addLink(link);\n> +\t\tsink->addLink(link);\n> +\n> +\t\taddObject(link);\n\nI would put addObject() before foo->addLink();\n\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +int MediaDevice::populatePads(const struct media_v2_topology &topology)\n> +{\n> +\tmedia_v2_pad *mediaPads = reinterpret_cast<media_v2_pad *>\n> +\t\t\t\t  (topology.ptr_pads);\n> +\n> +\tfor (unsigned int i = 0; i < topology.num_pads; ++i) {\n> +\t\tunsigned int entity_id = mediaPads[i].entity_id;\n> +\n> +\t\t/* Store a reference to this MediaPad in entity. */\n> +\t\tMediaEntity *mediaEntity = dynamic_cast<MediaEntity *>\n> +\t\t\t\t\t   (getObject(entity_id));\n> +\t\tif (!mediaEntity) {\n> +\t\t\tLOG(Error) << \"Failed to find entity with id: \"\n> +\t\t\t\t   << entity_id;\n> +\t\t\treturn -ENODEV;\n> +\t\t}\n> +\n> +\t\tMediaPad *pad = new MediaPad(&mediaPads[i], mediaEntity);\n> +\t\tmediaEntity->addPad(pad);\n> +\n> +\t\taddObject(pad);\n\nI would put addObject() before foo->addPad();\n\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +/*\n> + * For each entity in the media graph create a MediaEntity and store a\n> + * reference in the MediaObject global pool and in the global vector of\n> + * entities.\n> + */\n> +int MediaDevice::populateEntities(const struct media_v2_topology &topology)\n\nThis function can't fail maybe make it void?\n\n> +{\n> +\tmedia_v2_entity *mediaEntities = reinterpret_cast<media_v2_entity *>\n> +\t\t\t\t\t (topology.ptr_entities);\n> +\n> +\tfor (unsigned int i = 0; i < topology.num_entities; ++i) {\n> +\t\tMediaEntity *entity = new MediaEntity(&mediaEntities[i]);\n> +\n> +\t\taddObject(entity);\n> +\t\tentities_.push_back(entity);\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +/**\n> + * \\brief Populate the media graph with media objects.\n> + *\n> + * This function enumerates all media objects in the media device graph and\n> + * creates their MediaObject representations. All entities, pads and links are\n> + * stored as MediaEntity, MediaPad and MediaLink respectively, with cross-\n> + * references between objects. Interfaces are not processed.\n> + *\n> + * MediaEntities are stored in a global list in the MediaDevice itself to ease\n> + * lookup, while MediaPads are accessible from the MediaEntity they belong\n> + * to only and MediaLinks from the MediaPad they connect.\n> + *\n> + * \\return Return 0 on success or a negative error code on error.\n\ns/Return//\n\n> + */\n> +int MediaDevice::populate()\n> +{\n> +\tstruct media_v2_topology topology;\n> +\tstruct media_v2_entity *ents;\n> +\tstruct media_v2_link *links;\n> +\tstruct media_v2_pad *pads;\n> +\tint ret;\n> +\n> +\t/*\n> +\t * Keep calling G_TOPOLOGY until the version number stays stable.\n> +\t */\n> +\twhile (true) {\n> +\t\ttopology = {};\n> +\n> +\t\tret = ioctl(fd_, MEDIA_IOC_G_TOPOLOGY, &topology);\n> +\t\tif (ret < 0) {\n> +\t\t\tret = -errno;\n> +\t\t\tLOG(Error) << \"Failed to enumerate topology: \"\n> +\t\t\t\t   << strerror(-ret);\n> +\t\t\treturn ret;\n> +\t\t}\n> +\n> +\t\t__u64 version = topology.topology_version;\n> +\n> +\t\tents = new media_v2_entity[topology.num_entities];\n> +\t\tlinks = new media_v2_link[topology.num_links];\n> +\t\tpads = new media_v2_pad[topology.num_pads];\n> +\n> +\t\ttopology.ptr_entities = reinterpret_cast<__u64>(ents);\n> +\t\ttopology.ptr_links = reinterpret_cast<__u64>(links);\n> +\t\ttopology.ptr_pads = reinterpret_cast<__u64>(pads);\n> +\n> +\t\tret = ioctl(fd_, MEDIA_IOC_G_TOPOLOGY, &topology);\n> +\t\tif (ret < 0) {\n> +\t\t\tret = -errno;\n> +\t\t\tLOG(Error) << \"Failed to enumerate topology: \"\n> +\t\t\t\t   << strerror(-ret);\n> +\t\t\tgoto error_free_mem;\n> +\t\t}\n> +\n> +\t\tif (version == topology.topology_version)\n> +\t\t\tbreak;\n> +\n> +\t\tdelete[] links;\n> +\t\tdelete[] ents;\n> +\t\tdelete[] pads;\n> +\t}\n> +\n> +\t/* Populate entities, pads and links. */\n> +\tret = populateEntities(topology);\n> +\tif (ret)\n> +\t\tgoto error_free_mem;\n\npopulateEntities() can't fail.\n\n> +\n> +\tret = populatePads(topology);\n> +\tif (ret)\n> +\t\tgoto error_free_objs;\n> +\n> +\tret = populateLinks(topology);\n> +\tif (ret)\n> +\t\tgoto error_free_objs;\n> +\n\nCode bellow here can be reduced.\n\n> +\tdelete[] links;\n> +\tdelete[] ents;\n> +\tdelete[] pads;\n> +\n> +\treturn 0;\n> +\n> +error_free_objs:\n> +\tdeleteObjects();\n> +\n> +error_free_mem:\n> +\tdelete[] links;\n> +\tdelete[] ents;\n> +\tdelete[] pads;\n> +\n> +\treturn ret;\n\nAs there is no harm in calling deleteObjects() even if no object have \nbeen added. How about using only one error label:\n\nerror:\n    if (ret)\n        deleteObjects();\n\n    delete[] links;\n    delete[] ents;\n    delete[] pads;\n\n    return ret;\n\n> +}\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\n> index 01d321c..39a0464 100644\n> --- a/src/libcamera/meson.build\n> +++ b/src/libcamera/meson.build\n> @@ -2,10 +2,12 @@ libcamera_sources = files([\n>      'log.cpp',\n>      'main.cpp',\n>      'media_object.cpp',\n> +    'media_device.cpp',\n>  ])\n>  \n>  libcamera_headers = files([\n>      'include/log.h',\n> +    'include/media_device.h',\n>      'include/media_object.h',\n>      'include/utils.h',\n>  ])\n> -- \n> 2.20.1\n> \n> _______________________________________________\n> libcamera-devel mailing list\n> libcamera-devel@lists.libcamera.org\n> https://lists.libcamera.org/listinfo/libcamera-devel","headers":{"Return-Path":"<niklas.soderlund@ragnatech.se>","Received":["from mail-lj1-x244.google.com (mail-lj1-x244.google.com\n\t[IPv6:2a00:1450:4864:20::244])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B25A3600CC\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSun, 30 Dec 2018 23:39:50 +0100 (CET)","by mail-lj1-x244.google.com with SMTP id k19-v6so22608145lji.11\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSun, 30 Dec 2018 14:39:50 -0800 (PST)","from localhost (89-233-230-99.cust.bredband2.com. [89.233.230.99])\n\tby smtp.gmail.com with ESMTPSA id\n\tt9-v6sm9874323ljj.87.2018.12.30.14.39.49\n\t(version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256);\n\tSun, 30 Dec 2018 14:39:49 -0800 (PST)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=ragnatech-se.20150623.gappssmtp.com; s=20150623;\n\th=date:from:to:cc:subject:message-id:references:mime-version\n\t:content-disposition:content-transfer-encoding:in-reply-to\n\t:user-agent; bh=SLJcnloH1/AYULtJx0VuDMpR+uLWu+2scPnRtbMijJE=;\n\tb=PwWVDefDOqO12nr/E/bHXtQCZhT5u6U8h5QIvvftNzBrm+/eEifo8KuXKgaVK1mvs8\n\tEoVTzRgSbONg/rjAYI/s/MkKcVtdjwWgjUJlJXo4KteEEzkxuthsRKgnVJXzQ5++f7oz\n\tiyAJ483JV6wTUE3zqw7zsiLGMgHfhgsCzkWqPmDC1dYJIBHuyO3JtKp69EnIko/id6BX\n\tzzKc//aR+8fhMM1l8BjqsofZ+X2k+rcVkzBtOzkE2hj55hg1YDaVA426eG80qluVuYyT\n\teKbWF1xGLApPvXSoiDCUsp9sE0fyn8sNDaIbNvLYS+lRwx9RaXeIo2lf2MfyoHvWO1sf\n\tf1mQ==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20161025;\n\th=x-gm-message-state:date:from:to:cc:subject:message-id:references\n\t:mime-version:content-disposition:content-transfer-encoding\n\t:in-reply-to:user-agent;\n\tbh=SLJcnloH1/AYULtJx0VuDMpR+uLWu+2scPnRtbMijJE=;\n\tb=RKz7gfHPeCYnryxzaT6+O5BV7O52Qy6SAApHjsWYHGjgpi+1UJY4FFlmOzxWMUSZUf\n\tCFu+BHRiyP4+jUihjwjVtiJSrEptd8U1cpUgiJJ5OVvYgmJMIxI6HDsYcRefCWbRrTeQ\n\t9Hlsy2TPUbCmtoEd/6jH60s05IU//qK95lxTqhPQ8L1oZG4gED5srqjHh3C6aKF77Cvi\n\tIL1Kw8KqqlBvkGfbyquaKJxJwgZSMytBIDE4XYPVFmxnnxR8NlqzAnKqzsxuapspJitT\n\t2PMDiYIH6pkfXBC/Y0Po6MBCMONuiez4ZRtdp1apayGHU88bBdN2QGeEMGCTh4lijS56\n\tMJ0g==","X-Gm-Message-State":"AJcUukfcAvdBX0mVHW3sXlw/iw5HNMHmpXgga5900BoXbLWcR0wnG3HD\n\tk3wHeI3NaEvoeKjhYnFnAIfnVA==","X-Google-Smtp-Source":"ALg8bN6TI2OwXednK54a9/KGYDAEb62ZOOaB2M0suHR+J52UvzekAw57ANCt8DiymyOraGIlbGF12w==","X-Received":"by 2002:a2e:5d12:: with SMTP id\n\tr18-v6mr22723730ljb.89.1546209589935; \n\tSun, 30 Dec 2018 14:39:49 -0800 (PST)","Date":"Sun, 30 Dec 2018 23:39:48 +0100","From":"Niklas =?iso-8859-1?q?S=F6derlund?= <niklas.soderlund@ragnatech.se>","To":"Jacopo Mondi <jacopo@jmondi.org>","Cc":"libcamera-devel@lists.libcamera.org","Message-ID":"<20181230223948.GG31866@bigcity.dyn.berto.se>","References":"<20181230142314.16263-1-jacopo@jmondi.org>\n\t<20181230142314.16263-3-jacopo@jmondi.org>","MIME-Version":"1.0","Content-Type":"text/plain; charset=iso-8859-1","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<20181230142314.16263-3-jacopo@jmondi.org>","User-Agent":"Mutt/1.10.1 (2018-07-13)","Subject":"Re: [libcamera-devel] [PATCH v3 2/3] libcamera: Add MediaDevice\n\tclass","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.23","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>","X-List-Received-Date":"Sun, 30 Dec 2018 22:39:51 -0000"}},{"id":154,"web_url":"https://patchwork.libcamera.org/comment/154/","msgid":"<20181231083343.GB997@uno.localdomain>","date":"2018-12-31T08:33:43","subject":"Re: [libcamera-devel] [PATCH v3 2/3] libcamera: Add MediaDevice\n\tclass","submitter":{"id":3,"url":"https://patchwork.libcamera.org/api/people/3/","name":"Jacopo Mondi","email":"jacopo@jmondi.org"},"content":"Hi Niklas,\n\nOn Sun, Dec 30, 2018 at 11:39:48PM +0100, Niklas Söderlund wrote:\n> Hi Jacopo,\n>\n> Thanks for your work.\n>\n> On 2018-12-30 15:23:13 +0100, Jacopo Mondi wrote:\n> > The MediaDevice object implements handling and configuration of the media\n> > graph associated with a media device.\n> >\n> > The class allows enumeration of all pads, links and entities registered in\n> > the media graph\n> >\n> > Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>\n> > ---\n> >  src/libcamera/include/media_device.h |  61 +++++\n> >  src/libcamera/media_device.cpp       | 370 +++++++++++++++++++++++++++\n> >  src/libcamera/meson.build            |   2 +\n> >  3 files changed, 433 insertions(+)\n> >  create mode 100644 src/libcamera/include/media_device.h\n> >  create mode 100644 src/libcamera/media_device.cpp\n> >\n> > diff --git a/src/libcamera/include/media_device.h b/src/libcamera/include/media_device.h\n> > new file mode 100644\n> > index 0000000..6a9260e\n> > --- /dev/null\n> > +++ b/src/libcamera/include/media_device.h\n> > @@ -0,0 +1,61 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2018, Google Inc.\n> > + *\n> > + * media_device.h - Media device handler\n> > + */\n> > +#ifndef __LIBCAMERA_MEDIA_DEVICE_H__\n> > +#define __LIBCAMERA_MEDIA_DEVICE_H__\n> > +\n> > +#include <map>\n> > +#include <sstream>\n> > +#include <string>\n> > +#include <vector>\n> > +\n> > +#include <linux/media.h>\n> > +\n> > +#include \"media_object.h\"\n> > +\n> > +namespace libcamera {\n> > +\n> > +class MediaDevice\n> > +{\n> > +public:\n> > +\tMediaDevice() : fd_(-1) { };\n> > +\t~MediaDevice();\n> > +\n> > +\tconst std::string driver() const { return driver_; }\n> > +\tconst std::string devnode() const { return devnode_; }\n> > +\n> > +\tint open(const std::string &devnode);\n> > +\tvoid close();\n>\n> This bothers me somewhat. Would it be a valid use-case for a MediaDevice\n> object to be around while the fd is closed after it have been open()?\n>\n> Why not rename open() to init() and move all of close() to\n> ~MediaDevice()? Looking at the code bellow there seems to be overlap\n> between the two.\n>\n\nI think it is a valid use case. In example the enumerator might\nopen/populate the media device and then close it (wihtout deleting its\nmedia objects) and open it again once a match is requested or once the\nmedia device is given to the pipeline handler that operates on it.\n\n> > +\n> > +\tconst std::vector<MediaEntity *> &entities() const { return entities_; }\n> > +\n> > +\tint populate();\n> > +\n> > +private:\n> > +\tstd::string driver_;\n> > +\tstd::string devnode_;\n> > +\tint fd_;\n> > +\n> > +\t/*\n> > +\t * Global map of media objects (entities, pads, links) associated to\n> > +\t * their globally unique id.\n> > +\t */\n> > +\tstd::map<unsigned int, MediaObject *> objects_;\n> > +\tMediaObject *getObject(unsigned int id);\n> > +\tint addObject(MediaObject *obj);\n> > +\tvoid deleteObjects();\n> > +\n> > +\t/* Global list of media entities in the media graph: lookup by name. */\n> > +\tstd::vector<MediaEntity *> entities_;\n> > +\tMediaEntity *getEntityByName(const std::string &name);\n> > +\n> > +\tint populateEntities(const struct media_v2_topology &topology);\n> > +\tint populatePads(const struct media_v2_topology &topology);\n> > +\tint populateLinks(const struct media_v2_topology &topology);\n> > +};\n> > +\n> > +} /* namespace libcamera */\n> > +#endif /* __LIBCAMERA_MEDIA_DEVICE_H__ */\n> > diff --git a/src/libcamera/media_device.cpp b/src/libcamera/media_device.cpp\n> > new file mode 100644\n> > index 0000000..497b12d\n> > --- /dev/null\n> > +++ b/src/libcamera/media_device.cpp\n> > @@ -0,0 +1,370 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2018, Google Inc.\n> > + *\n> > + * media_device.cpp - Media device handler\n> > + */\n> > +\n> > +#include <errno.h>\n> > +#include <fcntl.h>\n> > +#include <string.h>\n> > +#include <sys/ioctl.h>\n> > +#include <unistd.h>\n> > +\n> > +#include <string>\n> > +#include <vector>\n> > +\n> > +#include <linux/media.h>\n> > +\n> > +#include \"log.h\"\n> > +#include \"media_device.h\"\n> > +\n> > +/**\n> > + * \\file media_device.h\n> > + */\n> > +namespace libcamera {\n> > +\n> > +/**\n> > + * \\class MediaDevice\n> > + * \\brief Media device handler\n> > + *\n> > + * MediaDevice represents the graph of media objects associated with a\n> > + * media device as exposed by the kernel through the media controller APIs.\n> > + *\n> > + * The caller is responsible for opening the MediaDevice explicitly before\n> > + * operating on it, and shall close it when not needed anymore, as access\n> > + * to the MediaDevice is exclusive.\n> > + *\n> > + * A MediaDevice is created empty and gets populated by inspecting the media\n> > + * graph topology using the MEDIA_IOC_G_TOPOLOGY ioctls. Representation\n> > + * of each entity, pad and link described are created using MediaObject\n> > + * derived classes.\n> > + *\n> > + * All MediaObject are stored in a global pool, where they could be retrieved\n> > + * from by their globally unique id.\n> > + *\n> > + * References to MediaEntity registered in the graph are stored in a vector\n> > + * to allow easier by-name lookup, and the list of MediaEntities is accessible.\n>\n> Accessible for whom?\n>\n\nI meant that a public method to access entities is available.\n\n> > + */\n> > +\n> > +/**\n> > + * \\brief Close the media device file descriptor and delete all\n> > object\n> > + */\n> > +MediaDevice::~MediaDevice()\n> > +{\n> > +\tif (fd_ != -1)\n> > +\t\t::close(fd_);\n> > +\tdeleteObjects();\n> > +}\n> > +\n> > +/**\n> > + * \\fn MediaDevice::driver()\n> > + * \\brief Return the driver name that handles the media graph this object\n> > + * represents.\n> > + */\n> > +\n> > +/**\n> > + * \\fn MediaDevice::devnode()\n> > + * \\brief Return the media device devnode node associated with this MediaDevice.\n> > + */\n>\n> Is it useful to document these two simple getters?\n>\n> > +\n> > +/**\n> > + * \\brief Delete all media objects in the MediaDevice.\n> > + *\n> > + * Delete all MediaEntities; entities will then delete their pads,\n> > + * and each source pad will delete links.\n> > + *\n> > + * After this function has been called, the media graph will be unpopulated\n> > + * and its media objects deleted. The media device has to be populated\n> > + * before it could be used again.\n>\n> Is there a use-case to delete all objects and re-populate() the graph?\n> If not can this not be moved to the destructor?\n>\n\ndeleteObjects() is private anyhow, so that's just an internal\nconvenience function, which I can remove if it bothers you.\n\n> > + */\n> > +void MediaDevice::deleteObjects()\n> > +{\n> > +\tfor (auto const &e : entities_)\n> > +\t\tdelete e;\n> > +\n> > +\tobjects_.clear();\n> > +\tentities_.clear();\n> > +}\n> > +\n> > +/**\n> > + * \\brief Open a media device and retrieve informations from it.\n> > + * \\param devnode The media device node path.\n> > + *\n> > + * The function fails if the media device is already open or if either\n> > + * open or the media device information retrieval operations fail.\n> > + *\n> > + * \\return Returns 0 for success or a negative error number otherwise.\n> > + */\n> > +int MediaDevice::open(const std::string &devnode)\n> > +{\n> > +\tif (fd_ != -1) {\n> > +\t\tLOG(Error) << \"MediaDevice already open\";\n> > +\t\treturn -EBUSY;\n> > +\t}\n> > +\n> > +\tint ret = ::open(devnode.c_str(), O_RDWR);\n> > +\tif (ret < 0) {\n> > +\t\tret = -errno;\n> > +\t\tLOG(Error) << \"Failed to open media device at \" << devnode\n> > +\t\t\t   << \": \" << strerror(-ret);\n> > +\t\treturn ret;\n> > +\t}\n> > +\tfd_ = ret;\n> > +\tdevnode_ = devnode;\n> > +\n> > +\tstruct media_device_info info = { };\n> > +\tret = ioctl(fd_, MEDIA_IOC_DEVICE_INFO, &info);\n> > +\tif (ret) {\n> > +\t\tret = -errno;\n> > +\t\tLOG(Error) << \"Failed to get media device info \"\n> > +\t\t\t   << \": \" << strerror(-ret);\n>\n> As you have easy access to devnode here I would add it to the error\n> message.\n>\n> > +\t\treturn ret;\n> > +\t}\n> > +\n> > +\tdriver_ = info.driver;\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +/**\n> > + * \\brief Close the file descriptor associated with the media device.\n> > + */\n> > +void MediaDevice::close()\n> > +{\n> > +\tif (fd_ == -1)\n> > +\t\treturn;\n> > +\n> > +\t::close(fd_);\n> > +\tfd_ = -1;\n> > +}\n>\n> I think this should be moved/merged to the destructor and this function\n> removed. If not possible/desirable the code duplication should be\n> reduced by calling close() in the destructor.\n>\n\nFor now (aka we don't merge with the enumerator) I would keep this\nopen/close interfce. If we find out this is not required we can merge\nthis in a single init() that does open+enumeration.\n\n> > +\n> > +/**\n> > + * \\fn MediaDevice::entities()\n> > + * \\brief Return the list of MediaEntity references.\n> > + */\n>\n> Needed?\n>\n\nYes, there is not way to access entities otherwise, and if I want to\ndo any testing I had to do so.\n\nMe and Laurent briefly discussed this, I don't like to much giving all\nentities away and I would have liked a parametrized interface to access\nentities (byName, byId etc). But he convinced me there are a lot of use\ncases where the list of entities is needed (the point that convinced\nme the most is that a pipeline handler might not know the name of the\nimage sensor installed, and to find it out, it has to access all\nentities).\n\n> > +\n> > +/*\n> > + * Add a new object to the global objects pool and fail if the object\n> > + * has already been registered.\n> > + */\n> > +int MediaDevice::addObject(MediaObject *obj)\n> > +{\n> > +\n> > +\tif (objects_.find(obj->id()) != objects_.end()) {\n> > +\t\tLOG(Error) << \"Element with id \" << obj->id()\n> > +\t\t\t   << \" already enumerated.\";\n> > +\t\treturn -EEXIST;\n> > +\t}\n> > +\n> > +\tobjects_[obj->id()] = obj;\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +/*\n> > + * MediaObject pool lookup by id.\n> > + */\n> > +MediaObject *MediaDevice::getObject(unsigned int id)\n> > +{\n> > +\tauto it = objects_.find(id);\n> > +\treturn (it == objects_.end()) ? nullptr : it->second;\n> > +}\n> > +\n> > +/**\n> > + * \\brief Return the MediaEntity with name \\a name.\n> > + * \\param name The entity name.\n> > + * \\return The entity with \\a name.\n> > + * \\return nullptr if no entity with \\a name is found.\n> > + */\n> > +MediaEntity *MediaDevice::getEntityByName(const std::string &name)\n> > +{\n> > +\tfor (MediaEntity *e : entities_)\n> > +\t\tif (e->name() == name)\n> > +\t\t\treturn e;\n> > +\n> > +\treturn nullptr;\n> > +}\n> > +\n> > +int MediaDevice::populateLinks(const struct media_v2_topology &topology)\n> > +{\n> > +\tmedia_v2_link *mediaLinks = reinterpret_cast<media_v2_link *>\n> > +\t\t\t\t    (topology.ptr_links);\n> > +\n> > +\tfor (unsigned int i = 0; i < topology.num_links; ++i) {\n> > +\t\t/*\n> > +\t\t * Skip links between entities and interfaces: we only care\n> > +\t\t * about pad-2-pad links here.\n> > +\t\t */\n> > +\t\tif ((mediaLinks[i].flags & MEDIA_LNK_FL_LINK_TYPE) ==\n> > +\t\t    MEDIA_LNK_FL_INTERFACE_LINK)\n> > +\t\t\tcontinue;\n> > +\n> > +\t\t/* Store references to source and sink pads in the link. */\n> > +\t\tunsigned int source_id = mediaLinks[i].source_id;\n> > +\t\tMediaPad *source = dynamic_cast<MediaPad *>\n> > +\t\t\t\t   (getObject(source_id));\n> > +\t\tif (!source) {\n> > +\t\t\tLOG(Error) << \"Failed to find pad with id: \"\n> > +\t\t\t\t   << source_id;\n> > +\t\t\treturn -ENODEV;\n> > +\t\t}\n> > +\n> > +\t\tunsigned int sink_id = mediaLinks[i].sink_id;\n> > +\t\tMediaPad *sink = dynamic_cast<MediaPad *>\n> > +\t\t\t\t (getObject(sink_id));\n> > +\t\tif (!sink) {\n> > +\t\t\tLOG(Error) << \"Failed to find pad with id: \"\n> > +\t\t\t\t   << sink_id;\n> > +\t\t\treturn -ENODEV;\n> > +\t\t}\n> > +\n> > +\t\tMediaLink *link = new MediaLink(&mediaLinks[i], source, sink);\n> > +\t\tsource->addLink(link);\n> > +\t\tsink->addLink(link);\n> > +\n> > +\t\taddObject(link);\n>\n> I would put addObject() before foo->addLink();\n>\n\nAs that's just a matter of tastes, I'll keep it as it is.\n\n> > +\t}\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +int MediaDevice::populatePads(const struct media_v2_topology &topology)\n> > +{\n> > +\tmedia_v2_pad *mediaPads = reinterpret_cast<media_v2_pad *>\n> > +\t\t\t\t  (topology.ptr_pads);\n> > +\n> > +\tfor (unsigned int i = 0; i < topology.num_pads; ++i) {\n> > +\t\tunsigned int entity_id = mediaPads[i].entity_id;\n> > +\n> > +\t\t/* Store a reference to this MediaPad in entity. */\n> > +\t\tMediaEntity *mediaEntity = dynamic_cast<MediaEntity *>\n> > +\t\t\t\t\t   (getObject(entity_id));\n> > +\t\tif (!mediaEntity) {\n> > +\t\t\tLOG(Error) << \"Failed to find entity with id: \"\n> > +\t\t\t\t   << entity_id;\n> > +\t\t\treturn -ENODEV;\n> > +\t\t}\n> > +\n> > +\t\tMediaPad *pad = new MediaPad(&mediaPads[i], mediaEntity);\n> > +\t\tmediaEntity->addPad(pad);\n> > +\n> > +\t\taddObject(pad);\n>\n> I would put addObject() before foo->addPad();\n>\n> > +\t}\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +/*\n> > + * For each entity in the media graph create a MediaEntity and store a\n> > + * reference in the MediaObject global pool and in the global vector of\n> > + * entities.\n> > + */\n> > +int MediaDevice::populateEntities(const struct media_v2_topology &topology)\n>\n> This function can't fail maybe make it void?\n>\n> > +{\n> > +\tmedia_v2_entity *mediaEntities = reinterpret_cast<media_v2_entity *>\n> > +\t\t\t\t\t (topology.ptr_entities);\n> > +\n> > +\tfor (unsigned int i = 0; i < topology.num_entities; ++i) {\n> > +\t\tMediaEntity *entity = new MediaEntity(&mediaEntities[i]);\n> > +\n> > +\t\taddObject(entity);\n> > +\t\tentities_.push_back(entity);\n> > +\t}\n> > +\n> > +\treturn 0;\n> > +}\n> > +\n> > +/**\n> > + * \\brief Populate the media graph with media objects.\n> > + *\n> > + * This function enumerates all media objects in the media device graph and\n> > + * creates their MediaObject representations. All entities, pads and links are\n> > + * stored as MediaEntity, MediaPad and MediaLink respectively, with cross-\n> > + * references between objects. Interfaces are not processed.\n> > + *\n> > + * MediaEntities are stored in a global list in the MediaDevice itself to ease\n> > + * lookup, while MediaPads are accessible from the MediaEntity they belong\n> > + * to only and MediaLinks from the MediaPad they connect.\n> > + *\n> > + * \\return Return 0 on success or a negative error code on error.\n>\n> s/Return//\n>\n> > + */\n> > +int MediaDevice::populate()\n> > +{\n> > +\tstruct media_v2_topology topology;\n> > +\tstruct media_v2_entity *ents;\n> > +\tstruct media_v2_link *links;\n> > +\tstruct media_v2_pad *pads;\n> > +\tint ret;\n> > +\n> > +\t/*\n> > +\t * Keep calling G_TOPOLOGY until the version number stays stable.\n> > +\t */\n> > +\twhile (true) {\n> > +\t\ttopology = {};\n> > +\n> > +\t\tret = ioctl(fd_, MEDIA_IOC_G_TOPOLOGY, &topology);\n> > +\t\tif (ret < 0) {\n> > +\t\t\tret = -errno;\n> > +\t\t\tLOG(Error) << \"Failed to enumerate topology: \"\n> > +\t\t\t\t   << strerror(-ret);\n> > +\t\t\treturn ret;\n> > +\t\t}\n> > +\n> > +\t\t__u64 version = topology.topology_version;\n> > +\n> > +\t\tents = new media_v2_entity[topology.num_entities];\n> > +\t\tlinks = new media_v2_link[topology.num_links];\n> > +\t\tpads = new media_v2_pad[topology.num_pads];\n> > +\n> > +\t\ttopology.ptr_entities = reinterpret_cast<__u64>(ents);\n> > +\t\ttopology.ptr_links = reinterpret_cast<__u64>(links);\n> > +\t\ttopology.ptr_pads = reinterpret_cast<__u64>(pads);\n> > +\n> > +\t\tret = ioctl(fd_, MEDIA_IOC_G_TOPOLOGY, &topology);\n> > +\t\tif (ret < 0) {\n> > +\t\t\tret = -errno;\n> > +\t\t\tLOG(Error) << \"Failed to enumerate topology: \"\n> > +\t\t\t\t   << strerror(-ret);\n> > +\t\t\tgoto error_free_mem;\n> > +\t\t}\n> > +\n> > +\t\tif (version == topology.topology_version)\n> > +\t\t\tbreak;\n> > +\n> > +\t\tdelete[] links;\n> > +\t\tdelete[] ents;\n> > +\t\tdelete[] pads;\n> > +\t}\n> > +\n> > +\t/* Populate entities, pads and links. */\n> > +\tret = populateEntities(topology);\n> > +\tif (ret)\n> > +\t\tgoto error_free_mem;\n>\n> populateEntities() can't fail.\n>\n> > +\n> > +\tret = populatePads(topology);\n> > +\tif (ret)\n> > +\t\tgoto error_free_objs;\n> > +\n> > +\tret = populateLinks(topology);\n> > +\tif (ret)\n> > +\t\tgoto error_free_objs;\n> > +\n>\n> Code bellow here can be reduced.\n>\n> > +\tdelete[] links;\n> > +\tdelete[] ents;\n> > +\tdelete[] pads;\n> > +\n> > +\treturn 0;\n> > +\n> > +error_free_objs:\n> > +\tdeleteObjects();\n> > +\n> > +error_free_mem:\n> > +\tdelete[] links;\n> > +\tdelete[] ents;\n> > +\tdelete[] pads;\n> > +\n> > +\treturn ret;\n>\n> As there is no harm in calling deleteObjects() even if no object have\n> been added. How about using only one error label:\n>\n> error:\n>     if (ret)\n>         deleteObjects();\n>\n>     delete[] links;\n>     delete[] ents;\n>     delete[] pads;\n>\n>     return ret;\n\nThis might simplify the code, thanks.\n\nThanks\n   j\n\n>\n> > +}\n> > +\n> > +} /* namespace libcamera */\n> > diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\n> > index 01d321c..39a0464 100644\n> > --- a/src/libcamera/meson.build\n> > +++ b/src/libcamera/meson.build\n> > @@ -2,10 +2,12 @@ libcamera_sources = files([\n> >      'log.cpp',\n> >      'main.cpp',\n> >      'media_object.cpp',\n> > +    'media_device.cpp',\n> >  ])\n> >\n> >  libcamera_headers = files([\n> >      'include/log.h',\n> > +    'include/media_device.h',\n> >      'include/media_object.h',\n> >      'include/utils.h',\n> >  ])\n> > --\n> > 2.20.1\n> >\n> > _______________________________________________\n> > libcamera-devel mailing list\n> > libcamera-devel@lists.libcamera.org\n> > https://lists.libcamera.org/listinfo/libcamera-devel\n>\n> --\n> Regards,\n> Niklas Söderlund","headers":{"Return-Path":"<jacopo@jmondi.org>","Received":["from relay2-d.mail.gandi.net (relay2-d.mail.gandi.net\n\t[217.70.183.194])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 9EC3160B2E\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 31 Dec 2018 09:33:42 +0100 (CET)","from uno.localdomain\n\t(host54-51-dynamic.16-87-r.retail.telecomitalia.it [87.16.51.54])\n\t(Authenticated sender: jacopo@jmondi.org)\n\tby relay2-d.mail.gandi.net (Postfix) with ESMTPSA id 38B6740007;\n\tMon, 31 Dec 2018 08:33:41 +0000 (UTC)"],"X-Originating-IP":"87.16.51.54","Date":"Mon, 31 Dec 2018 09:33:43 +0100","From":"Jacopo Mondi <jacopo@jmondi.org>","To":"Niklas =?utf-8?q?S=C3=B6derlund?= <niklas.soderlund@ragnatech.se>","Cc":"libcamera-devel@lists.libcamera.org","Message-ID":"<20181231083343.GB997@uno.localdomain>","References":"<20181230142314.16263-1-jacopo@jmondi.org>\n\t<20181230142314.16263-3-jacopo@jmondi.org>\n\t<20181230223948.GG31866@bigcity.dyn.berto.se>","MIME-Version":"1.0","Content-Type":"multipart/signed; micalg=pgp-sha256;\n\tprotocol=\"application/pgp-signature\"; boundary=\"8X7/QrJGcKSMr1RN\"","Content-Disposition":"inline","In-Reply-To":"<20181230223948.GG31866@bigcity.dyn.berto.se>","User-Agent":"Mutt/1.11.1 (2018-12-01)","Subject":"Re: [libcamera-devel] [PATCH v3 2/3] libcamera: Add MediaDevice\n\tclass","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.23","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>","X-List-Received-Date":"Mon, 31 Dec 2018 08:33:42 -0000"}}]