[{"id":22895,"web_url":"https://patchwork.libcamera.org/comment/22895/","msgid":"<YnVY/KR03cB/TPri@pendragon.ideasonboard.com>","date":"2022-05-06T17:21:00","subject":"Re: [libcamera-devel] [PATCH v8 4/7] Add Python bindings","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Tomi,\n\nThank you for the patch.\n\nOn Fri, May 06, 2022 at 05:54:11PM +0300, Tomi Valkeinen wrote:\n> Add libcamera Python bindings. pybind11 is used to generate the C++ <->\n> Python layer.\n> \n> We use pybind11 'smart_holder' version to avoid issues with private\n> destructors and shared_ptr. There is also an alternative solution here:\n> \n> https://github.com/pybind/pybind11/pull/2067\n> \n> Only a subset of libcamera classes are exposed. Implementing and testing\n> the wrapper classes is challenging, and as such only classes that I have\n> needed have been added so far.\n> \n> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> ---\n>  meson.build                                   |   1 +\n>  meson_options.txt                             |   5 +\n>  src/meson.build                               |   1 +\n>  src/py/libcamera/__init__.py                  |  84 +++\n>  src/py/libcamera/meson.build                  |  51 ++\n>  src/py/libcamera/pyenums.cpp                  |  34 +\n>  src/py/libcamera/pymain.cpp                   | 640 ++++++++++++++++++\n>  src/py/meson.build                            |   1 +\n>  subprojects/.gitignore                        |   3 +-\n>  subprojects/packagefiles/pybind11/meson.build |   7 +\n>  subprojects/pybind11.wrap                     |   9 +\n>  11 files changed, 835 insertions(+), 1 deletion(-)\n>  create mode 100644 src/py/libcamera/__init__.py\n>  create mode 100644 src/py/libcamera/meson.build\n>  create mode 100644 src/py/libcamera/pyenums.cpp\n>  create mode 100644 src/py/libcamera/pymain.cpp\n>  create mode 100644 src/py/meson.build\n>  create mode 100644 subprojects/packagefiles/pybind11/meson.build\n>  create mode 100644 subprojects/pybind11.wrap\n> \n> diff --git a/meson.build b/meson.build\n> index 0124e7d3..60a911e0 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -177,6 +177,7 @@ summary({\n>              'Tracing support': tracing_enabled,\n>              'Android support': android_enabled,\n>              'GStreamer support': gst_enabled,\n> +            'Python bindings': pycamera_enabled,\n>              'V4L2 emulation support': v4l2_enabled,\n>              'cam application': cam_enabled,\n>              'qcam application': qcam_enabled,\n> diff --git a/meson_options.txt b/meson_options.txt\n> index 2c80ad8b..ca00c78e 100644\n> --- a/meson_options.txt\n> +++ b/meson_options.txt\n> @@ -58,3 +58,8 @@ option('v4l2',\n>          type : 'boolean',\n>          value : false,\n>          description : 'Compile the V4L2 compatibility layer')\n> +\n> +option('pycamera',\n> +        type : 'feature',\n> +        value : 'auto',\n> +        description : 'Enable libcamera Python bindings (experimental)')\n> diff --git a/src/meson.build b/src/meson.build\n> index e0ea9c35..34663a6f 100644\n> --- a/src/meson.build\n> +++ b/src/meson.build\n> @@ -37,4 +37,5 @@ subdir('cam')\n>  subdir('qcam')\n>  \n>  subdir('gstreamer')\n> +subdir('py')\n>  subdir('v4l2')\n> diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py\n> new file mode 100644\n> index 00000000..6b330890\n> --- /dev/null\n> +++ b/src/py/libcamera/__init__.py\n> @@ -0,0 +1,84 @@\n> +# SPDX-License-Identifier: LGPL-2.1-or-later\n> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> +\n> +from ._libcamera import *\n> +\n> +\n> +class MappedFrameBuffer:\n> +    def __init__(self, fb):\n> +        self.__fb = fb\n> +\n> +    def __enter__(self):\n> +        from os import lseek, SEEK_END\n\nAs it's local to the function it doesn't matter much, but I would have\njust imported os and used os.lseek and os.SEEK_END.\n\n> +        import mmap\n> +\n> +        fb = self.__fb\n> +\n> +        # Collect information about the buffers\n> +\n> +        bufinfos = {}\n> +\n> +        for i in range(fb.num_planes):\n> +            fd = fb.fd(i)\n> +\n> +            if fd not in bufinfos:\n> +                buflen = lseek(fd, 0, SEEK_END)\n> +                bufinfos[fd] = {'maplen': 0, 'buflen': buflen}\n> +            else:\n> +                buflen = bufinfos[fd]['buflen']\n> +\n> +            if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen:\n> +                raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' +\n> +                                   f'plane offset={fb.offset(i)}, plane length={fb.length(i)}')\n> +\n> +            bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i))\n> +\n> +        # mmap the buffers\n> +\n> +        maps = []\n> +\n> +        for fd, info in bufinfos.items():\n> +            map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)\n> +            info['map'] = map\n> +            maps.append(map)\n> +\n> +        self.__maps = tuple(maps)\n> +\n> +        # Create memoryviews for the planes\n> +\n> +        planes = []\n> +\n> +        for i in range(fb.num_planes):\n> +            fd = fb.fd(i)\n> +            info = bufinfos[fd]\n> +\n> +            mv = memoryview(info['map'])\n> +\n> +            start = fb.offset(i)\n> +            end = fb.offset(i) + fb.length(i)\n> +\n> +            mv = mv[start:end]\n> +\n> +            planes.append(mv)\n> +\n> +        self.__planes = tuple(planes)\n> +\n> +        return self\n> +\n> +    def __exit__(self, exc_type, exc_value, exc_traceback):\n> +        for p in self.__planes:\n> +            p.release()\n> +\n> +        for mm in self.__maps:\n> +            mm.close()\n> +\n> +    @property\n> +    def planes(self):\n> +        return self.__planes\n> +\n> +\n> +def __FrameBuffer__mmap(self):\n> +    return MappedFrameBuffer(self)\n> +\n> +\n> +FrameBuffer.mmap = __FrameBuffer__mmap\n> diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\n> new file mode 100644\n> index 00000000..e4abc34a\n> --- /dev/null\n> +++ b/src/py/libcamera/meson.build\n> @@ -0,0 +1,51 @@\n> +# SPDX-License-Identifier: CC0-1.0\n> +\n> +py3_dep = dependency('python3', required : get_option('pycamera'))\n> +\n> +if not py3_dep.found()\n> +    pycamera_enabled = false\n> +    subdir_done()\n> +endif\n> +\n> +pycamera_enabled = true\n> +\n> +pybind11_proj = subproject('pybind11')\n> +pybind11_dep = pybind11_proj.get_variable('pybind11_dep')\n> +\n> +pycamera_sources = files([\n> +    'pyenums.cpp',\n> +    'pymain.cpp',\n> +])\n> +\n> +pycamera_deps = [\n> +    libcamera_public,\n> +    py3_dep,\n> +    pybind11_dep,\n> +]\n> +\n> +pycamera_args = [\n> +    '-fvisibility=hidden',\n> +    '-Wno-shadow',\n> +    '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT',\n> +]\n> +\n> +destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera'\n> +\n> +pycamera = shared_module('_libcamera',\n> +                         pycamera_sources,\n> +                         install : true,\n> +                         install_dir : destdir,\n> +                         name_prefix : '',\n> +                         dependencies : pycamera_deps,\n> +                         cpp_args : pycamera_args)\n> +\n> +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',\n> +            meson.current_build_dir() / '__init__.py',\n> +            check: true)\n> +\n> +install_data(['__init__.py'], install_dir : destdir)\n> +\n> +# \\todo: Generate stubs when building. Depends on pybind11-stubgen. Sometimes\n\ns/todo:/todo/\n\nI'm still not sure what this is for :-) Do we need to generate stubs\nlater ? What are they for ?\n\n> +# this works, sometimes doesn't... To generate pylibcamera stubs.\n> +# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\n> +# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/\n> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n> new file mode 100644\n> index 00000000..b655e622\n> --- /dev/null\n> +++ b/src/py/libcamera/pyenums.cpp\n> @@ -0,0 +1,34 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> + *\n> + * Python bindings - Enumerations\n> + */\n> +\n> +#include <libcamera/libcamera.h>\n> +\n> +#include <pybind11/smart_holder.h>\n> +\n> +namespace py = pybind11;\n> +\n> +using namespace libcamera;\n> +\n> +void init_pyenums(py::module &m)\n> +{\n> +\tpy::enum_<StreamRole>(m, \"StreamRole\")\n> +\t\t.value(\"StillCapture\", StreamRole::StillCapture)\n> +\t\t.value(\"Raw\", StreamRole::Raw)\n> +\t\t.value(\"VideoRecording\", StreamRole::VideoRecording)\n> +\t\t.value(\"Viewfinder\", StreamRole::Viewfinder);\n> +\n> +\tpy::enum_<ControlType>(m, \"ControlType\")\n> +\t\t.value(\"None\", ControlType::ControlTypeNone)\n> +\t\t.value(\"Bool\", ControlType::ControlTypeBool)\n> +\t\t.value(\"Byte\", ControlType::ControlTypeByte)\n> +\t\t.value(\"Integer32\", ControlType::ControlTypeInteger32)\n> +\t\t.value(\"Integer64\", ControlType::ControlTypeInteger64)\n> +\t\t.value(\"Float\", ControlType::ControlTypeFloat)\n> +\t\t.value(\"String\", ControlType::ControlTypeString)\n> +\t\t.value(\"Rectangle\", ControlType::ControlTypeRectangle)\n> +\t\t.value(\"Size\", ControlType::ControlTypeSize);\n> +}\n> diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp\n> new file mode 100644\n> index 00000000..8c3be8f4\n> --- /dev/null\n> +++ b/src/py/libcamera/pymain.cpp\n> @@ -0,0 +1,640 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> + *\n> + * Python bindings\n> + */\n> +\n> +/*\n> + * \\todo Add geometry classes (Point, Rectangle...)\n> + * \\todo Add bindings for the ControlInfo class\n> + */\n> +\n> +#include <chrono>\n> +#include <fcntl.h>\n> +#include <mutex>\n> +#include <sys/eventfd.h>\n> +#include <sys/mman.h>\n> +#include <thread>\n> +#include <unistd.h>\n> +\n> +#include <libcamera/libcamera.h>\n> +\n> +#include <pybind11/functional.h>\n> +#include <pybind11/smart_holder.h>\n> +#include <pybind11/stl.h>\n> +#include <pybind11/stl_bind.h>\n> +\n> +namespace py = pybind11;\n> +\n> +using namespace libcamera;\n> +\n> +template<typename T>\n> +static py::object valueOrTuple(const ControlValue &cv)\n> +{\n> +\tif (cv.isArray()) {\n> +\t\tconst T *v = reinterpret_cast<const T *>(cv.data().data());\n> +\t\tauto t = py::tuple(cv.numElements());\n> +\n> +\t\tfor (size_t i = 0; i < cv.numElements(); ++i)\n> +\t\t\tt[i] = v[i];\n> +\n> +\t\treturn t;\n> +\t}\n> +\n> +\treturn py::cast(cv.get<T>());\n> +}\n> +\n> +static py::object controlValueToPy(const ControlValue &cv)\n> +{\n> +\tswitch (cv.type()) {\n> +\tcase ControlTypeBool:\n> +\t\treturn valueOrTuple<bool>(cv);\n> +\tcase ControlTypeByte:\n> +\t\treturn valueOrTuple<uint8_t>(cv);\n> +\tcase ControlTypeInteger32:\n> +\t\treturn valueOrTuple<int32_t>(cv);\n> +\tcase ControlTypeInteger64:\n> +\t\treturn valueOrTuple<int64_t>(cv);\n> +\tcase ControlTypeFloat:\n> +\t\treturn valueOrTuple<float>(cv);\n> +\tcase ControlTypeString:\n> +\t\treturn py::cast(cv.get<std::string>());\n> +\tcase ControlTypeRectangle: {\n> +\t\tconst Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n> +\t\treturn py::make_tuple(v->x, v->y, v->width, v->height);\n> +\t}\n> +\tcase ControlTypeSize: {\n> +\t\tconst Size *v = reinterpret_cast<const Size *>(cv.data().data());\n> +\t\treturn py::make_tuple(v->width, v->height);\n> +\t}\n> +\tcase ControlTypeNone:\n> +\tdefault:\n> +\t\tthrow std::runtime_error(\"Unsupported ControlValue type\");\n> +\t}\n> +}\n> +\n> +template<typename T>\n> +static ControlValue controlValueMaybeArray(const py::object &ob)\n> +{\n> +\tif (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) {\n> +\t\tstd::vector<T> vec = ob.cast<std::vector<T>>();\n> +\t\treturn ControlValue(Span<const T>(vec));\n> +\t}\n> +\n> +\treturn ControlValue(ob.cast<T>());\n> +}\n> +\n> +static ControlValue pyToControlValue(const py::object &ob, ControlType type)\n> +{\n> +\tswitch (type) {\n> +\tcase ControlTypeBool:\n> +\t\treturn ControlValue(ob.cast<bool>());\n> +\tcase ControlTypeByte:\n> +\t\treturn controlValueMaybeArray<uint8_t>(ob);\n> +\tcase ControlTypeInteger32:\n> +\t\treturn controlValueMaybeArray<int32_t>(ob);\n> +\tcase ControlTypeInteger64:\n> +\t\treturn controlValueMaybeArray<int64_t>(ob);\n> +\tcase ControlTypeFloat:\n> +\t\treturn controlValueMaybeArray<float>(ob);\n> +\tcase ControlTypeString:\n> +\t\treturn ControlValue(ob.cast<std::string>());\n> +\tcase ControlTypeRectangle: {\n> +\t\tauto array = ob.cast<std::array<int32_t, 4>>();\n> +\t\treturn ControlValue(Rectangle(array[0], array[1], array[2], array[3]));\n> +\t}\n> +\tcase ControlTypeSize: {\n> +\t\tauto array = ob.cast<std::array<int32_t, 2>>();\n> +\t\treturn ControlValue(Size(array[0], array[1]));\n> +\t}\n> +\tcase ControlTypeNone:\n> +\tdefault:\n> +\t\tthrow std::runtime_error(\"Control type not implemented\");\n> +\t}\n> +}\n> +\n> +static std::weak_ptr<CameraManager> gCameraManager;\n> +static int gEventfd;\n> +static std::mutex gReqlistMutex;\n> +static std::vector<Request *> gReqList;\n> +\n> +static void handleRequestCompleted(Request *req)\n> +{\n> +\t{\n> +\t\tstd::lock_guard guard(gReqlistMutex);\n> +\t\tgReqList.push_back(req);\n> +\t}\n> +\n> +\tuint64_t v = 1;\n> +\twrite(gEventfd, &v, 8);\n> +}\n> +\n> +void init_pyenums(py::module &m);\n> +\n> +PYBIND11_MODULE(_libcamera, m)\n> +{\n> +\tinit_pyenums(m);\n> +\n> +\t/* Forward declarations */\n> +\n> +\t/*\n> +\t * We need to declare all the classes here so that Python docstrings\n> +\t * can be generated correctly.\n> +\t * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n> +\t */\n> +\n> +\tauto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n> +\tauto pyCamera = py::class_<Camera>(m, \"Camera\");\n> +\tauto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n> +\tauto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, \"Status\");\n> +\tauto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n> +\tauto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n> +\tauto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n> +\tauto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n> +\tauto pyStream = py::class_<Stream>(m, \"Stream\");\n> +\tauto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n> +\tauto pyRequest = py::class_<Request>(m, \"Request\");\n> +\tauto pyRequestStatus = py::enum_<Request::Status>(pyRequest, \"Status\");\n> +\tauto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, \"Reuse\");\n> +\tauto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n> +\tauto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, \"Status\");\n> +\tauto pyTransform = py::class_<Transform>(m, \"Transform\");\n> +\tauto pyColorSpace = py::class_<ColorSpace>(m, \"ColorSpace\");\n> +\tauto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, \"Primaries\");\n> +\tauto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, \"TransferFunction\");\n> +\tauto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, \"YcbcrEncoding\");\n> +\tauto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, \"Range\");\n> +\n> +\t/* Global functions */\n> +\tm.def(\"log_set_level\", &logSetLevel);\n> +\n> +\t/* Classes */\n> +\tpyCameraManager\n> +\t\t.def_static(\"singleton\", []() {\n> +\t\t\tstd::shared_ptr<CameraManager> cm = gCameraManager.lock();\n> +\t\t\tif (cm)\n> +\t\t\t\treturn cm;\n> +\n> +\t\t\tint fd = eventfd(0, 0);\n> +\t\t\tif (fd == -1)\n> +\t\t\t\tthrow std::system_error(errno, std::generic_category(),\n> +\t\t\t\t                   \"Failed to create eventfd\");\n\nThere should be tabs instead of spaces, and the '\"' should be aligned\nunder errno. Same below.\n\n> +\n> +\t\t\tcm = std::shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n> +\t\t\t\tclose(gEventfd);\n> +\t\t\t\tgEventfd = -1;\n> +\t\t\t\tdelete p;\n> +\t\t\t});\n> +\n> +\t\t\tgEventfd = fd;\n> +\t\t\tgCameraManager = cm;\n> +\n> +\t\t\tint ret = cm->start();\n> +\t\t\tif (ret)\n> +\t\t\t\tthrow std::system_error(-ret, std::generic_category(),\n> +\t\t\t\t                   \"Failed to start CameraManager\");\n> +\n> +\t\t\treturn cm;\n> +\t\t})\n> +\n> +\t\t.def_property_readonly(\"version\", &CameraManager::version)\n> +\n> +\t\t.def_property_readonly(\"efd\", [](CameraManager &) {\n> +\t\t\treturn gEventfd;\n> +\t\t})\n> +\n> +\t\t.def(\"get_ready_requests\", [](CameraManager &) {\n> +\t\t\tstd::vector<Request *> v;\n> +\n> +\t\t\t{\n> +\t\t\t\tstd::lock_guard guard(gReqlistMutex);\n> +\t\t\t\tswap(v, gReqList);\n> +\t\t\t}\n> +\n> +\t\t\tstd::vector<py::object> ret;\n> +\n> +\t\t\tfor (Request *req : v) {\n> +\t\t\t\tpy::object o = py::cast(req);\n> +\t\t\t\t/* Decrease the ref increased in Camera.queue_request() */\n> +\t\t\t\to.dec_ref();\n> +\t\t\t\tret.push_back(o);\n> +\t\t\t}\n> +\n> +\t\t\treturn ret;\n> +\t\t})\n> +\n> +\t\t.def(\"get\", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>())\n> +\n> +\t\t/* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n> +\t\t.def_property_readonly(\"cameras\", [](CameraManager &self) {\n> +\t\t\tpy::list l;\n> +\n> +\t\t\tfor (auto &c : self.cameras()) {\n> +\t\t\t\tpy::object py_cm = py::cast(self);\n> +\t\t\t\tpy::object py_cam = py::cast(c);\n> +\t\t\t\tpy::detail::keep_alive_impl(py_cam, py_cm);\n> +\t\t\t\tl.append(py_cam);\n> +\t\t\t}\n> +\n> +\t\t\treturn l;\n> +\t\t});\n> +\n> +\tpyCamera\n> +\t\t.def_property_readonly(\"id\", &Camera::id)\n> +\t\t.def(\"acquire\", &Camera::acquire)\n> +\t\t.def(\"release\", &Camera::release)\n> +\t\t.def(\"start\", [](Camera &self, py::dict controls) {\n> +\t\t\t/* \\todo What happens if someone calls start() multiple times? */\n> +\n> +\t\t\tself.requestCompleted.connect(handleRequestCompleted);\n> +\n> +\t\t\tconst ControlInfoMap &controlMap = self.controls();\n> +\t\t\tControlList controlList(controlMap);\n> +\t\t\tfor (const auto& [hkey, hval]: controls) {\n> +\t\t\t\tauto key = hkey.cast<std::string>();\n> +\n> +\t\t\t\tauto it = find_if(controlMap.begin(), controlMap.end(),\n> +\t\t\t\t\t\t  [&key](const auto &kvp) {\n> +\t\t\t\t\t\t\t  return kvp.first->name() == key; });\n\n\t\t\t\tauto it = std::find_if(controlMap.begin(), controlMap.end(),\n\t\t\t\t\t\t       [&key](const auto &kvp) {\n\t\t\t\t\t\t\t       return kvp.first->name() == key;\n\t\t\t\t\t\t       });\n\n> +\n> +\t\t\t\tif (it == controlMap.end())\n> +\t\t\t\t\tthrow std::runtime_error(\"Control \" + key + \" not found\");\n> +\n> +\t\t\t\tconst auto &id = it->first;\n> +\t\t\t\tauto obj = py::cast<py::object>(hval);\n> +\n> +\t\t\t\tcontrolList.set(id->id(), pyToControlValue(obj, id->type()));\n> +\t\t\t}\n> +\n> +\t\t\tint ret = self.start(&controlList);\n> +\t\t\tif (ret) {\n> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n> +\t\t\t\treturn ret;\n> +\t\t\t}\n> +\n> +\t\t\treturn 0;\n> +\t\t}, py::arg(\"controls\") = py::dict())\n> +\n> +\t\t.def(\"stop\", [](Camera &self) {\n> +\t\t\tint ret = self.stop();\n> +\t\t\tif (ret)\n> +\t\t\t\treturn ret;\n> +\n> +\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n> +\n> +\t\t\treturn 0;\n> +\t\t})\n> +\n> +\t\t.def(\"__repr__\", [](Camera &self) {\n> +\t\t\treturn \"<libcamera.Camera '\" + self.id() + \"'>\";\n> +\t\t})\n> +\n> +\t\t/* Keep the camera alive, as StreamConfiguration contains a Stream* */\n> +\t\t.def(\"generate_configuration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n> +\t\t.def(\"configure\", &Camera::configure)\n> +\n> +\t\t.def(\"create_request\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n> +\n> +\t\t.def(\"queue_request\", [](Camera &self, Request *req) {\n> +\t\t\tpy::object py_req = py::cast(req);\n> +\n> +\t\t\t/*\n> +\t\t\t * Increase the reference count, will be dropped in\n> +\t\t\t * CameraManager.get_ready_requests().\n> +\t\t\t */\n> +\n> +\t\t\tpy_req.inc_ref();\n> +\n> +\t\t\tint ret = self.queueRequest(req);\n> +\t\t\tif (ret)\n> +\t\t\t\tpy_req.dec_ref();\n> +\n> +\t\t\treturn ret;\n> +\t\t})\n> +\n> +\t\t.def_property_readonly(\"streams\", [](Camera &self) {\n> +\t\t\tpy::set set;\n> +\t\t\tfor (auto &s : self.streams()) {\n> +\t\t\t\tpy::object py_self = py::cast(self);\n> +\t\t\t\tpy::object py_s = py::cast(s);\n> +\t\t\t\tpy::detail::keep_alive_impl(py_s, py_self);\n> +\t\t\t\tset.add(py_s);\n> +\t\t\t}\n> +\t\t\treturn set;\n> +\t\t})\n> +\n> +\t\t.def(\"find_control\", [](Camera &self, const std::string &name) {\n> +\t\t\tconst auto &controls = self.controls();\n> +\n> +\t\t\tauto it = find_if(controls.begin(), controls.end(),\n> +\t\t\t\t\t  [&name](const auto &kvp) { return kvp.first->name() == name; });\n\nMissing std:: here too (and I would also wrap the line).\n\n> +\n> +\t\t\tif (it == controls.end())\n> +\t\t\t\tthrow std::runtime_error(\"Control not found\");\n\n\t\t\t\tthrow std::runtime_error(\"Control '\" + name + \"' not found\");\n\ncould be nicer to debug issues.\n\n> +\n> +\t\t\treturn it->first;\n> +\t\t}, py::return_value_policy::reference_internal)\n> +\n> +\t\t.def_property_readonly(\"controls\", [](Camera &self) {\n> +\t\t\tpy::dict ret;\n> +\n> +\t\t\tfor (const auto &[id, ci] : self.controls()) {\n> +\t\t\t\tret[id->name().c_str()] = std::make_tuple<py::object>(controlValueToPy(ci.min()),\n> +\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.max()),\n> +\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.def()));\n> +\t\t\t}\n> +\n> +\t\t\treturn ret;\n> +\t\t})\n> +\n> +\t\t.def_property_readonly(\"properties\", [](Camera &self) {\n> +\t\t\tpy::dict ret;\n> +\n> +\t\t\tfor (const auto &[key, cv] : self.properties()) {\n> +\t\t\t\tconst ControlId *id = properties::properties.at(key);\n> +\t\t\t\tpy::object ob = controlValueToPy(cv);\n> +\n> +\t\t\t\tret[id->name().c_str()] = ob;\n> +\t\t\t}\n> +\n> +\t\t\treturn ret;\n> +\t\t});\n> +\n> +\tpyCameraConfiguration\n> +\t\t.def(\"__iter__\", [](CameraConfiguration &self) {\n> +\t\t\treturn py::make_iterator<py::return_value_policy::reference_internal>(self);\n> +\t\t}, py::keep_alive<0, 1>())\n> +\t\t.def(\"__len__\", [](CameraConfiguration &self) {\n> +\t\t\treturn self.size();\n> +\t\t})\n> +\t\t.def(\"validate\", &CameraConfiguration::validate)\n> +\t\t.def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at),\n> +\t\t     py::return_value_policy::reference_internal)\n> +\t\t.def_property_readonly(\"size\", &CameraConfiguration::size)\n> +\t\t.def_property_readonly(\"empty\", &CameraConfiguration::empty)\n> +\t\t.def_readwrite(\"transform\", &CameraConfiguration::transform);\n> +\n> +\tpyCameraConfigurationStatus\n> +\t\t.value(\"Valid\", CameraConfiguration::Valid)\n> +\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\n> +\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\n> +\n> +\tpyStreamConfiguration\n> +\t\t.def(\"to_string\", &StreamConfiguration::toString)\n\nShould this be __str__ ?\n\n> +\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream,\n> +\t\t                       py::return_value_policy::reference_internal)\n> +\t\t.def_property(\n> +\t\t\t\"size\",\n> +\t\t\t[](StreamConfiguration &self) {\n> +\t\t\t\treturn std::make_tuple(self.size.width, self.size.height);\n> +\t\t\t},\n> +\t\t\t[](StreamConfiguration &self, std::tuple<uint32_t, uint32_t> size) {\n> +\t\t\t\tself.size.width = std::get<0>(size);\n> +\t\t\t\tself.size.height = std::get<1>(size);\n> +\t\t\t})\n> +\t\t.def_property(\n> +\t\t\t\"pixel_format\",\n> +\t\t\t[](StreamConfiguration &self) {\n> +\t\t\t\treturn self.pixelFormat.toString();\n> +\t\t\t},\n> +\t\t\t[](StreamConfiguration &self, std::string fmt) {\n> +\t\t\t\tself.pixelFormat = PixelFormat::fromString(fmt);\n> +\t\t\t})\n> +\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\n> +\t\t.def_readwrite(\"frame_size\", &StreamConfiguration::frameSize)\n> +\t\t.def_readwrite(\"buffer_count\", &StreamConfiguration::bufferCount)\n> +\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats,\n> +\t\t                       py::return_value_policy::reference_internal)\n> +\t\t.def_readwrite(\"colorSpace\", &StreamConfiguration::colorSpace);\n\ncolor_space\n\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\n> +\n> +\tpyStreamFormats\n> +\t\t.def_property_readonly(\"pixel_formats\", [](StreamFormats &self) {\n> +\t\t\tstd::vector<std::string> fmts;\n> +\t\t\tfor (auto &fmt : self.pixelformats())\n> +\t\t\t\tfmts.push_back(fmt.toString());\n> +\t\t\treturn fmts;\n> +\t\t})\n> +\t\t.def(\"sizes\", [](StreamFormats &self, const std::string &pixelFormat) {\n> +\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\n> +\t\t\tstd::vector<std::tuple<uint32_t, uint32_t>> fmts;\n> +\t\t\tfor (const auto &s : self.sizes(fmt))\n> +\t\t\t\tfmts.push_back(std::make_tuple(s.width, s.height));\n> +\t\t\treturn fmts;\n> +\t\t})\n> +\t\t.def(\"range\", [](StreamFormats &self, const std::string &pixelFormat) {\n> +\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\n> +\t\t\tconst auto &range = self.range(fmt);\n> +\t\t\treturn make_tuple(std::make_tuple(range.hStep, range.vStep),\n> +\t\t\t\t\t  std::make_tuple(range.min.width, range.min.height),\n> +\t\t\t\t\t  std::make_tuple(range.max.width, range.max.height));\n> +\t\t});\n> +\n> +\tpyFrameBufferAllocator\n> +\t\t.def(py::init<std::shared_ptr<Camera>>(), py::keep_alive<1, 2>())\n> +\t\t.def(\"allocate\", &FrameBufferAllocator::allocate)\n> +\t\t.def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\n> +\t\t/* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */\n> +\t\t.def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\n> +\t\t\tpy::object py_self = py::cast(self);\n> +\t\t\tpy::list l;\n> +\t\t\tfor (auto &ub : self.buffers(stream)) {\n> +\t\t\t\tpy::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);\n> +\t\t\t\tl.append(py_buf);\n> +\t\t\t}\n> +\t\t\treturn l;\n> +\t\t});\n> +\n> +\tpyFrameBuffer\n> +\t\t/* \\todo implement FrameBuffer::Plane properly */\n> +\t\t.def(py::init([](std::vector<std::tuple<int, unsigned int>> planes, unsigned int cookie) {\n> +\t\t\tstd::vector<FrameBuffer::Plane> v;\n> +\t\t\tfor (const auto &t : planes)\n> +\t\t\t\tv.push_back({ SharedFD(std::get<0>(t)), FrameBuffer::Plane::kInvalidOffset, std::get<1>(t) });\n> +\t\t\treturn new FrameBuffer(v, cookie);\n> +\t\t}))\n> +\t\t.def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\n> +\t\t.def_property_readonly(\"num_planes\", [](const FrameBuffer &self) {\n> +\t\t\treturn self.planes().size();\n> +\t\t})\n> +\t\t.def(\"length\", [](FrameBuffer &self, uint32_t idx) {\n> +\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\n> +\t\t\treturn plane.length;\n> +\t\t})\n> +\t\t.def(\"fd\", [](FrameBuffer &self, uint32_t idx) {\n> +\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\n> +\t\t\treturn plane.fd.get();\n> +\t\t})\n> +\t\t.def(\"offset\", [](FrameBuffer &self, uint32_t idx) {\n> +\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\n> +\t\t\treturn plane.offset;\n> +\t\t})\n> +\t\t.def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\n> +\n> +\tpyStream\n> +\t\t.def_property_readonly(\"configuration\", &Stream::configuration);\n> +\n> +\tpyControlId\n> +\t\t.def_property_readonly(\"id\", &ControlId::id)\n> +\t\t.def_property_readonly(\"name\", &ControlId::name)\n> +\t\t.def_property_readonly(\"type\", &ControlId::type);\n> +\n> +\tpyRequest\n> +\t\t/* \\todo Fence is not supported, so we cannot expose addBuffer() directly */\n> +\t\t.def(\"add_buffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n> +\t\t\treturn self.addBuffer(stream, buffer);\n> +\t\t}, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n> +\t\t.def_property_readonly(\"status\", &Request::status)\n> +\t\t.def_property_readonly(\"buffers\", &Request::buffers)\n> +\t\t.def_property_readonly(\"cookie\", &Request::cookie)\n> +\t\t.def_property_readonly(\"has_pending_buffers\", &Request::hasPendingBuffers)\n> +\t\t.def(\"set_control\", [](Request &self, ControlId &id, py::object value) {\n> +\t\t\tself.controls().set(id.id(), pyToControlValue(value, id.type()));\n> +\t\t})\n> +\t\t.def_property_readonly(\"metadata\", [](Request &self) {\n> +\t\t\tpy::dict ret;\n> +\n> +\t\t\tfor (const auto &[key, cv] : self.metadata()) {\n> +\t\t\t\tconst ControlId *id = controls::controls.at(key);\n> +\t\t\t\tpy::object ob = controlValueToPy(cv);\n> +\n> +\t\t\t\tret[id->name().c_str()] = ob;\n> +\t\t\t}\n> +\n> +\t\t\treturn ret;\n> +\t\t})\n> +\t\t/*\n> +\t\t * \\todo As we add a keep_alive to the fb in addBuffers(), we\n> +\t\t * can only allow reuse with ReuseBuffers.\n> +\t\t */\n> +\t\t.def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\n> +\n> +\tpyRequestStatus\n> +\t\t.value(\"Pending\", Request::RequestPending)\n> +\t\t.value(\"Complete\", Request::RequestComplete)\n> +\t\t.value(\"Cancelled\", Request::RequestCancelled);\n> +\n> +\tpyRequestReuse\n> +\t\t.value(\"Default\", Request::ReuseFlag::Default)\n> +\t\t.value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\n> +\n> +\tpyFrameMetadata\n> +\t\t.def_readonly(\"status\", &FrameMetadata::status)\n> +\t\t.def_readonly(\"sequence\", &FrameMetadata::sequence)\n> +\t\t.def_readonly(\"timestamp\", &FrameMetadata::timestamp)\n> +\t\t/* \\todo Implement FrameMetadata::Plane properly */\n> +\t\t.def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n> +\t\t\tstd::vector<unsigned int> v;\n> +\t\t\tv.resize(self.planes().size());\n> +\t\t\ttransform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\n> +\t\t\treturn v;\n> +\t\t});\n> +\n> +\tpyFrameMetadataStatus\n> +\t\t.value(\"Success\", FrameMetadata::FrameSuccess)\n> +\t\t.value(\"Error\", FrameMetadata::FrameError)\n> +\t\t.value(\"Cancelled\", FrameMetadata::FrameCancelled);\n> +\n> +\tpyTransform\n> +\t\t.def(py::init([](int rotation, bool hflip, bool vflip, bool transpose) {\n> +\t\t\tbool ok;\n> +\n> +\t\t\tTransform t = transformFromRotation(rotation, &ok);\n> +\t\t\tif (!ok)\n> +\t\t\t\tthrow std::invalid_argument(\"Invalid rotation\");\n> +\n> +\t\t\tif (hflip)\n> +\t\t\t\tt ^= Transform::HFlip;\n> +\t\t\tif (vflip)\n> +\t\t\t\tt ^= Transform::VFlip;\n> +\t\t\tif (transpose)\n> +\t\t\t\tt ^= Transform::Transpose;\n> +\t\t\treturn t;\n> +\t\t}), py::arg(\"rotation\") = 0, py::arg(\"hflip\") = false,\n> +\t\t    py::arg(\"vflip\") = false, py::arg(\"transpose\") = false)\n> +\t\t.def(py::init([](Transform &other) { return other; }))\n> +\t\t.def(\"__repr__\", [](Transform &self) {\n> +\t\t\treturn \"<libcamera.Transform '\" + std::string(transformToString(self)) + \"'>\";\n> +\t\t})\n> +\t\t.def_property(\"hflip\",\n> +\t\t\t      [](Transform &self) {\n> +\t\t\t\t      return !!(self & Transform::HFlip);\n> +\t\t\t      },\n> +\t\t\t      [](Transform &self, bool hflip) {\n> +\t\t\t\t      if (hflip)\n> +\t\t\t\t\t      self |= Transform::HFlip;\n> +\t\t\t\t      else\n> +\t\t\t\t\t      self &= ~Transform::HFlip;\n> +\t\t\t      })\n> +\t\t.def_property(\"vflip\",\n> +\t\t\t      [](Transform &self) {\n> +\t\t\t\t      return !!(self & Transform::VFlip);\n> +\t\t\t      },\n> +\t\t\t      [](Transform &self, bool vflip) {\n> +\t\t\t\t      if (vflip)\n> +\t\t\t\t\t      self |= Transform::VFlip;\n> +\t\t\t\t      else\n> +\t\t\t\t\t      self &= ~Transform::VFlip;\n> +\t\t\t      })\n> +\t\t.def_property(\"transpose\",\n> +\t\t\t      [](Transform &self) {\n> +\t\t\t\t      return !!(self & Transform::Transpose);\n> +\t\t\t      },\n> +\t\t\t      [](Transform &self, bool transpose) {\n> +\t\t\t\t      if (transpose)\n> +\t\t\t\t\t      self |= Transform::Transpose;\n> +\t\t\t\t      else\n> +\t\t\t\t\t      self &= ~Transform::Transpose;\n> +\t\t\t      })\n> +\t\t.def(\"inverse\", [](Transform &self) { return -self; })\n> +\t\t.def(\"invert\", [](Transform &self) {\n> +\t\t\tself = -self;\n> +\t\t})\n> +\t\t.def(\"compose\", [](Transform &self, Transform &other) {\n> +\t\t\tself = self * other;\n> +\t\t});\n> +\n> +\tpyColorSpace\n> +\t\t.def(py::init([](ColorSpace::Primaries primaries,\n> +\t\t\t\t ColorSpace::TransferFunction transferFunction,\n> +\t\t\t\t ColorSpace::YcbcrEncoding ycbcrEncoding,\n> +\t\t\t\t ColorSpace::Range range) {\n> +\t\t\treturn ColorSpace(primaries, transferFunction, ycbcrEncoding, range);\n> +\t\t}), py::arg(\"primaries\"), py::arg(\"transferFunction\"),\n> +\t\t    py::arg(\"ycbcrEncoding\"), py::arg(\"range\"))\n> +\t\t.def(py::init([](ColorSpace &other) { return other; }))\n> +\t\t.def(\"__repr__\", [](ColorSpace &self) {\n> +\t\t\treturn \"<libcamera.ColorSpace '\" + self.toString() + \"'>\";\n> +\t\t})\n> +\t\t.def_readwrite(\"primaries\", &ColorSpace::primaries)\n> +\t\t.def_readwrite(\"transferFunction\", &ColorSpace::transferFunction)\n> +\t\t.def_readwrite(\"ycbcrEncoding\", &ColorSpace::ycbcrEncoding)\n> +\t\t.def_readwrite(\"range\", &ColorSpace::range)\n> +\t\t.def_static(\"Raw\", []() { return ColorSpace::Raw; })\n> +\t\t.def_static(\"Jpeg\", []() { return ColorSpace::Jpeg; })\n> +\t\t.def_static(\"Srgb\", []() { return ColorSpace::Srgb; })\n> +\t\t.def_static(\"Smpte170m\", []() { return ColorSpace::Smpte170m; })\n> +\t\t.def_static(\"Rec709\", []() { return ColorSpace::Rec709; })\n> +\t\t.def_static(\"Rec2020\", []() { return ColorSpace::Rec2020; });\n> +\n> +\tpyColorSpacePrimaries\n> +\t\t.value(\"Raw\", ColorSpace::Primaries::Raw)\n> +\t\t.value(\"Smpte170m\", ColorSpace::Primaries::Smpte170m)\n> +\t\t.value(\"Rec709\", ColorSpace::Primaries::Rec709)\n> +\t\t.value(\"Rec2020\", ColorSpace::Primaries::Rec2020);\n> +\n> +\tpyColorSpaceTransferFunction\n> +\t\t.value(\"Linear\", ColorSpace::TransferFunction::Linear)\n> +\t\t.value(\"Srgb\", ColorSpace::TransferFunction::Srgb)\n> +\t\t.value(\"Rec709\", ColorSpace::TransferFunction::Rec709);\n> +\n> +\tpyColorSpaceYcbcrEncoding\n> +\t\t.value(\"Null\", ColorSpace::YcbcrEncoding::None)\n> +\t\t.value(\"Rec601\", ColorSpace::YcbcrEncoding::Rec601)\n> +\t\t.value(\"Rec709\", ColorSpace::YcbcrEncoding::Rec709)\n> +\t\t.value(\"Rec2020\", ColorSpace::YcbcrEncoding::Rec2020);\n> +\n> +\tpyColorSpaceRange\n> +\t\t.value(\"Full\", ColorSpace::Range::Full)\n> +\t\t.value(\"Limited\", ColorSpace::Range::Limited);\n> +}\n> diff --git a/src/py/meson.build b/src/py/meson.build\n> new file mode 100644\n> index 00000000..4ce9668c\n> --- /dev/null\n> +++ b/src/py/meson.build\n> @@ -0,0 +1 @@\n> +subdir('libcamera')\n> diff --git a/subprojects/.gitignore b/subprojects/.gitignore\n> index 391fde2c..0e194289 100644\n> --- a/subprojects/.gitignore\n> +++ b/subprojects/.gitignore\n> @@ -1,3 +1,4 @@\n>  /googletest-release*\n>  /libyuv\n> -/packagecache\n> \\ No newline at end of file\n> +/packagecache\n> +/pybind11\n> diff --git a/subprojects/packagefiles/pybind11/meson.build b/subprojects/packagefiles/pybind11/meson.build\n> new file mode 100644\n> index 00000000..1be47ca4\n> --- /dev/null\n> +++ b/subprojects/packagefiles/pybind11/meson.build\n> @@ -0,0 +1,7 @@\n> +project('pybind11', 'cpp',\n> +    version : '2.9.1',\n> +    license : 'BSD-3-Clause')\n> +\n> +pybind11_incdir = include_directories('include')\n> +\n> +pybind11_dep = declare_dependency(include_directories : pybind11_incdir)\n> diff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap\n> new file mode 100644\n> index 00000000..43c0608d\n> --- /dev/null\n> +++ b/subprojects/pybind11.wrap\n> @@ -0,0 +1,9 @@\n> +[wrap-git]\n> +url = https://github.com/pybind/pybind11.git\n> +# This is the head of 'smart_holder' branch\n> +revision = 82734801f23314b4c34d70a79509e060a2648e04\n> +depth = 1\n> +patch_directory = pybind11\n> +\n> +[provide]\n> +pybind11 = pybind11_dep","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 8ADEAC3256\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  6 May 2022 17:21:07 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D6A4C65648;\n\tFri,  6 May 2022 19:21:06 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id BFAC3604A3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  6 May 2022 19:21:05 +0200 (CEST)","from pendragon.ideasonboard.com (62-78-145-57.bb.dnainternet.fi\n\t[62.78.145.57])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id EAA15492;\n\tFri,  6 May 2022 19:21:04 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651857666;\n\tbh=NYIwviWEqiAgwuu2Kzs3Zc6I8wmNjhzrO++yctAQGgQ=;\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=1qxoQUL8uJ49B9m+FcjCKMv9L03T+kmuGF2a/f0IIv6noZQjDmaeY0ZKXH2niM5PH\n\tA/xVATyP1PP//7JVLriprIdGN/fGhcYz9sBIOgZcQ/Qn0oS9WYLVzsmGOAQ7ACkOfg\n\tElnIQy6orcnTYndsNg+LOaQYU5Ihf2B6Dy5FAqYmI5g6XHC9eKxAgye8o6Uy5+aO9u\n\tiShBCmyModpx89JGpsQr4eg/nKVaCpTYeXcJPQzLdgm0KQbHa3PFXPVr2H6SrsvPv3\n\tOQnVilB9TwlDFcWhOFyJaQrlGNj0AR+MmtUCKF1VHP2pAVJX7qQ29qFajvuaW84vYx\n\ta5DNy9dh0Gn9g==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651857665;\n\tbh=NYIwviWEqiAgwuu2Kzs3Zc6I8wmNjhzrO++yctAQGgQ=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=TsEEFfN7nAIJzYqcwAuFbgUjNlmfDvRkm1PByD8AgoM4k8IdhYoNJpSNT+tsJ29ix\n\tPT3kN0eEHARkXRFCuYFf091eFzbfCe0AJN3NYLLIdLpzDW37HEbPYptmaVKRAOT1XH\n\tppjL2pPczmzwTwhCH+HGoG09u4ZIT/hXkrAEqcew="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"TsEEFfN7\"; dkim-atps=neutral","Date":"Fri, 6 May 2022 20:21:00 +0300","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<YnVY/KR03cB/TPri@pendragon.ideasonboard.com>","References":"<20220506145414.99039-1-tomi.valkeinen@ideasonboard.com>\n\t<20220506145414.99039-5-tomi.valkeinen@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220506145414.99039-5-tomi.valkeinen@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v8 4/7] Add Python bindings","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>"}},{"id":22896,"web_url":"https://patchwork.libcamera.org/comment/22896/","msgid":"<65545abe-b56f-c3c6-9f9f-0cc6567c8866@ideasonboard.com>","date":"2022-05-06T17:50:20","subject":"Re: [libcamera-devel] [PATCH v8 4/7] Add Python bindings","submitter":{"id":109,"url":"https://patchwork.libcamera.org/api/people/109/","name":"Tomi Valkeinen","email":"tomi.valkeinen@ideasonboard.com"},"content":"On 06/05/2022 20:21, Laurent Pinchart wrote:\n> Hi Tomi,\n> \n> Thank you for the patch.\n> \n> On Fri, May 06, 2022 at 05:54:11PM +0300, Tomi Valkeinen wrote:\n>> Add libcamera Python bindings. pybind11 is used to generate the C++ <->\n>> Python layer.\n>>\n>> We use pybind11 'smart_holder' version to avoid issues with private\n>> destructors and shared_ptr. There is also an alternative solution here:\n>>\n>> https://github.com/pybind/pybind11/pull/2067\n>>\n>> Only a subset of libcamera classes are exposed. Implementing and testing\n>> the wrapper classes is challenging, and as such only classes that I have\n>> needed have been added so far.\n>>\n>> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> ---\n>>   meson.build                                   |   1 +\n>>   meson_options.txt                             |   5 +\n>>   src/meson.build                               |   1 +\n>>   src/py/libcamera/__init__.py                  |  84 +++\n>>   src/py/libcamera/meson.build                  |  51 ++\n>>   src/py/libcamera/pyenums.cpp                  |  34 +\n>>   src/py/libcamera/pymain.cpp                   | 640 ++++++++++++++++++\n>>   src/py/meson.build                            |   1 +\n>>   subprojects/.gitignore                        |   3 +-\n>>   subprojects/packagefiles/pybind11/meson.build |   7 +\n>>   subprojects/pybind11.wrap                     |   9 +\n>>   11 files changed, 835 insertions(+), 1 deletion(-)\n>>   create mode 100644 src/py/libcamera/__init__.py\n>>   create mode 100644 src/py/libcamera/meson.build\n>>   create mode 100644 src/py/libcamera/pyenums.cpp\n>>   create mode 100644 src/py/libcamera/pymain.cpp\n>>   create mode 100644 src/py/meson.build\n>>   create mode 100644 subprojects/packagefiles/pybind11/meson.build\n>>   create mode 100644 subprojects/pybind11.wrap\n>>\n>> diff --git a/meson.build b/meson.build\n>> index 0124e7d3..60a911e0 100644\n>> --- a/meson.build\n>> +++ b/meson.build\n>> @@ -177,6 +177,7 @@ summary({\n>>               'Tracing support': tracing_enabled,\n>>               'Android support': android_enabled,\n>>               'GStreamer support': gst_enabled,\n>> +            'Python bindings': pycamera_enabled,\n>>               'V4L2 emulation support': v4l2_enabled,\n>>               'cam application': cam_enabled,\n>>               'qcam application': qcam_enabled,\n>> diff --git a/meson_options.txt b/meson_options.txt\n>> index 2c80ad8b..ca00c78e 100644\n>> --- a/meson_options.txt\n>> +++ b/meson_options.txt\n>> @@ -58,3 +58,8 @@ option('v4l2',\n>>           type : 'boolean',\n>>           value : false,\n>>           description : 'Compile the V4L2 compatibility layer')\n>> +\n>> +option('pycamera',\n>> +        type : 'feature',\n>> +        value : 'auto',\n>> +        description : 'Enable libcamera Python bindings (experimental)')\n>> diff --git a/src/meson.build b/src/meson.build\n>> index e0ea9c35..34663a6f 100644\n>> --- a/src/meson.build\n>> +++ b/src/meson.build\n>> @@ -37,4 +37,5 @@ subdir('cam')\n>>   subdir('qcam')\n>>   \n>>   subdir('gstreamer')\n>> +subdir('py')\n>>   subdir('v4l2')\n>> diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py\n>> new file mode 100644\n>> index 00000000..6b330890\n>> --- /dev/null\n>> +++ b/src/py/libcamera/__init__.py\n>> @@ -0,0 +1,84 @@\n>> +# SPDX-License-Identifier: LGPL-2.1-or-later\n>> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> +\n>> +from ._libcamera import *\n>> +\n>> +\n>> +class MappedFrameBuffer:\n>> +    def __init__(self, fb):\n>> +        self.__fb = fb\n>> +\n>> +    def __enter__(self):\n>> +        from os import lseek, SEEK_END\n> \n> As it's local to the function it doesn't matter much, but I would have\n> just imported os and used os.lseek and os.SEEK_END.\n\nYep, makes sense.\n\n>> +        import mmap\n>> +\n>> +        fb = self.__fb\n>> +\n>> +        # Collect information about the buffers\n>> +\n>> +        bufinfos = {}\n>> +\n>> +        for i in range(fb.num_planes):\n>> +            fd = fb.fd(i)\n>> +\n>> +            if fd not in bufinfos:\n>> +                buflen = lseek(fd, 0, SEEK_END)\n>> +                bufinfos[fd] = {'maplen': 0, 'buflen': buflen}\n>> +            else:\n>> +                buflen = bufinfos[fd]['buflen']\n>> +\n>> +            if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen:\n>> +                raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' +\n>> +                                   f'plane offset={fb.offset(i)}, plane length={fb.length(i)}')\n>> +\n>> +            bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i))\n>> +\n>> +        # mmap the buffers\n>> +\n>> +        maps = []\n>> +\n>> +        for fd, info in bufinfos.items():\n>> +            map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)\n>> +            info['map'] = map\n>> +            maps.append(map)\n>> +\n>> +        self.__maps = tuple(maps)\n>> +\n>> +        # Create memoryviews for the planes\n>> +\n>> +        planes = []\n>> +\n>> +        for i in range(fb.num_planes):\n>> +            fd = fb.fd(i)\n>> +            info = bufinfos[fd]\n>> +\n>> +            mv = memoryview(info['map'])\n>> +\n>> +            start = fb.offset(i)\n>> +            end = fb.offset(i) + fb.length(i)\n>> +\n>> +            mv = mv[start:end]\n>> +\n>> +            planes.append(mv)\n>> +\n>> +        self.__planes = tuple(planes)\n>> +\n>> +        return self\n>> +\n>> +    def __exit__(self, exc_type, exc_value, exc_traceback):\n>> +        for p in self.__planes:\n>> +            p.release()\n>> +\n>> +        for mm in self.__maps:\n>> +            mm.close()\n>> +\n>> +    @property\n>> +    def planes(self):\n>> +        return self.__planes\n>> +\n>> +\n>> +def __FrameBuffer__mmap(self):\n>> +    return MappedFrameBuffer(self)\n>> +\n>> +\n>> +FrameBuffer.mmap = __FrameBuffer__mmap\n>> diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\n>> new file mode 100644\n>> index 00000000..e4abc34a\n>> --- /dev/null\n>> +++ b/src/py/libcamera/meson.build\n>> @@ -0,0 +1,51 @@\n>> +# SPDX-License-Identifier: CC0-1.0\n>> +\n>> +py3_dep = dependency('python3', required : get_option('pycamera'))\n>> +\n>> +if not py3_dep.found()\n>> +    pycamera_enabled = false\n>> +    subdir_done()\n>> +endif\n>> +\n>> +pycamera_enabled = true\n>> +\n>> +pybind11_proj = subproject('pybind11')\n>> +pybind11_dep = pybind11_proj.get_variable('pybind11_dep')\n>> +\n>> +pycamera_sources = files([\n>> +    'pyenums.cpp',\n>> +    'pymain.cpp',\n>> +])\n>> +\n>> +pycamera_deps = [\n>> +    libcamera_public,\n>> +    py3_dep,\n>> +    pybind11_dep,\n>> +]\n>> +\n>> +pycamera_args = [\n>> +    '-fvisibility=hidden',\n>> +    '-Wno-shadow',\n>> +    '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT',\n>> +]\n>> +\n>> +destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera'\n>> +\n>> +pycamera = shared_module('_libcamera',\n>> +                         pycamera_sources,\n>> +                         install : true,\n>> +                         install_dir : destdir,\n>> +                         name_prefix : '',\n>> +                         dependencies : pycamera_deps,\n>> +                         cpp_args : pycamera_args)\n>> +\n>> +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',\n>> +            meson.current_build_dir() / '__init__.py',\n>> +            check: true)\n>> +\n>> +install_data(['__init__.py'], install_dir : destdir)\n>> +\n>> +# \\todo: Generate stubs when building. Depends on pybind11-stubgen. Sometimes\n> \n> s/todo:/todo/\n> \n> I'm still not sure what this is for :-) Do we need to generate stubs\n> later ? What are they for ?\n\nOh, I see. https://peps.python.org/pep-0484/#stub-files\n\nI'm not very familiar with them, but my editor is able to introspect \npure python code, but not the pybind11 module. A stub file can provide \nthe pure-python view to the module's API.\n\nI haven't gotten them to work too well, thought. Earlier today it \nworked, then later it didn't. I haven't figured out the exact method on \nhow the stub files are searched, etc...\n\n>> +# this works, sometimes doesn't... To generate pylibcamera stubs.\n>> +# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\n>> +# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/\n>> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n>> new file mode 100644\n>> index 00000000..b655e622\n>> --- /dev/null\n>> +++ b/src/py/libcamera/pyenums.cpp\n>> @@ -0,0 +1,34 @@\n>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>> +/*\n>> + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> + *\n>> + * Python bindings - Enumerations\n>> + */\n>> +\n>> +#include <libcamera/libcamera.h>\n>> +\n>> +#include <pybind11/smart_holder.h>\n>> +\n>> +namespace py = pybind11;\n>> +\n>> +using namespace libcamera;\n>> +\n>> +void init_pyenums(py::module &m)\n>> +{\n>> +\tpy::enum_<StreamRole>(m, \"StreamRole\")\n>> +\t\t.value(\"StillCapture\", StreamRole::StillCapture)\n>> +\t\t.value(\"Raw\", StreamRole::Raw)\n>> +\t\t.value(\"VideoRecording\", StreamRole::VideoRecording)\n>> +\t\t.value(\"Viewfinder\", StreamRole::Viewfinder);\n>> +\n>> +\tpy::enum_<ControlType>(m, \"ControlType\")\n>> +\t\t.value(\"None\", ControlType::ControlTypeNone)\n>> +\t\t.value(\"Bool\", ControlType::ControlTypeBool)\n>> +\t\t.value(\"Byte\", ControlType::ControlTypeByte)\n>> +\t\t.value(\"Integer32\", ControlType::ControlTypeInteger32)\n>> +\t\t.value(\"Integer64\", ControlType::ControlTypeInteger64)\n>> +\t\t.value(\"Float\", ControlType::ControlTypeFloat)\n>> +\t\t.value(\"String\", ControlType::ControlTypeString)\n>> +\t\t.value(\"Rectangle\", ControlType::ControlTypeRectangle)\n>> +\t\t.value(\"Size\", ControlType::ControlTypeSize);\n>> +}\n>> diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp\n>> new file mode 100644\n>> index 00000000..8c3be8f4\n>> --- /dev/null\n>> +++ b/src/py/libcamera/pymain.cpp\n>> @@ -0,0 +1,640 @@\n>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>> +/*\n>> + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> + *\n>> + * Python bindings\n>> + */\n>> +\n>> +/*\n>> + * \\todo Add geometry classes (Point, Rectangle...)\n>> + * \\todo Add bindings for the ControlInfo class\n>> + */\n>> +\n>> +#include <chrono>\n>> +#include <fcntl.h>\n>> +#include <mutex>\n>> +#include <sys/eventfd.h>\n>> +#include <sys/mman.h>\n>> +#include <thread>\n>> +#include <unistd.h>\n>> +\n>> +#include <libcamera/libcamera.h>\n>> +\n>> +#include <pybind11/functional.h>\n>> +#include <pybind11/smart_holder.h>\n>> +#include <pybind11/stl.h>\n>> +#include <pybind11/stl_bind.h>\n>> +\n>> +namespace py = pybind11;\n>> +\n>> +using namespace libcamera;\n>> +\n>> +template<typename T>\n>> +static py::object valueOrTuple(const ControlValue &cv)\n>> +{\n>> +\tif (cv.isArray()) {\n>> +\t\tconst T *v = reinterpret_cast<const T *>(cv.data().data());\n>> +\t\tauto t = py::tuple(cv.numElements());\n>> +\n>> +\t\tfor (size_t i = 0; i < cv.numElements(); ++i)\n>> +\t\t\tt[i] = v[i];\n>> +\n>> +\t\treturn t;\n>> +\t}\n>> +\n>> +\treturn py::cast(cv.get<T>());\n>> +}\n>> +\n>> +static py::object controlValueToPy(const ControlValue &cv)\n>> +{\n>> +\tswitch (cv.type()) {\n>> +\tcase ControlTypeBool:\n>> +\t\treturn valueOrTuple<bool>(cv);\n>> +\tcase ControlTypeByte:\n>> +\t\treturn valueOrTuple<uint8_t>(cv);\n>> +\tcase ControlTypeInteger32:\n>> +\t\treturn valueOrTuple<int32_t>(cv);\n>> +\tcase ControlTypeInteger64:\n>> +\t\treturn valueOrTuple<int64_t>(cv);\n>> +\tcase ControlTypeFloat:\n>> +\t\treturn valueOrTuple<float>(cv);\n>> +\tcase ControlTypeString:\n>> +\t\treturn py::cast(cv.get<std::string>());\n>> +\tcase ControlTypeRectangle: {\n>> +\t\tconst Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n>> +\t\treturn py::make_tuple(v->x, v->y, v->width, v->height);\n>> +\t}\n>> +\tcase ControlTypeSize: {\n>> +\t\tconst Size *v = reinterpret_cast<const Size *>(cv.data().data());\n>> +\t\treturn py::make_tuple(v->width, v->height);\n>> +\t}\n>> +\tcase ControlTypeNone:\n>> +\tdefault:\n>> +\t\tthrow std::runtime_error(\"Unsupported ControlValue type\");\n>> +\t}\n>> +}\n>> +\n>> +template<typename T>\n>> +static ControlValue controlValueMaybeArray(const py::object &ob)\n>> +{\n>> +\tif (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) {\n>> +\t\tstd::vector<T> vec = ob.cast<std::vector<T>>();\n>> +\t\treturn ControlValue(Span<const T>(vec));\n>> +\t}\n>> +\n>> +\treturn ControlValue(ob.cast<T>());\n>> +}\n>> +\n>> +static ControlValue pyToControlValue(const py::object &ob, ControlType type)\n>> +{\n>> +\tswitch (type) {\n>> +\tcase ControlTypeBool:\n>> +\t\treturn ControlValue(ob.cast<bool>());\n>> +\tcase ControlTypeByte:\n>> +\t\treturn controlValueMaybeArray<uint8_t>(ob);\n>> +\tcase ControlTypeInteger32:\n>> +\t\treturn controlValueMaybeArray<int32_t>(ob);\n>> +\tcase ControlTypeInteger64:\n>> +\t\treturn controlValueMaybeArray<int64_t>(ob);\n>> +\tcase ControlTypeFloat:\n>> +\t\treturn controlValueMaybeArray<float>(ob);\n>> +\tcase ControlTypeString:\n>> +\t\treturn ControlValue(ob.cast<std::string>());\n>> +\tcase ControlTypeRectangle: {\n>> +\t\tauto array = ob.cast<std::array<int32_t, 4>>();\n>> +\t\treturn ControlValue(Rectangle(array[0], array[1], array[2], array[3]));\n>> +\t}\n>> +\tcase ControlTypeSize: {\n>> +\t\tauto array = ob.cast<std::array<int32_t, 2>>();\n>> +\t\treturn ControlValue(Size(array[0], array[1]));\n>> +\t}\n>> +\tcase ControlTypeNone:\n>> +\tdefault:\n>> +\t\tthrow std::runtime_error(\"Control type not implemented\");\n>> +\t}\n>> +}\n>> +\n>> +static std::weak_ptr<CameraManager> gCameraManager;\n>> +static int gEventfd;\n>> +static std::mutex gReqlistMutex;\n>> +static std::vector<Request *> gReqList;\n>> +\n>> +static void handleRequestCompleted(Request *req)\n>> +{\n>> +\t{\n>> +\t\tstd::lock_guard guard(gReqlistMutex);\n>> +\t\tgReqList.push_back(req);\n>> +\t}\n>> +\n>> +\tuint64_t v = 1;\n>> +\twrite(gEventfd, &v, 8);\n>> +}\n>> +\n>> +void init_pyenums(py::module &m);\n>> +\n>> +PYBIND11_MODULE(_libcamera, m)\n>> +{\n>> +\tinit_pyenums(m);\n>> +\n>> +\t/* Forward declarations */\n>> +\n>> +\t/*\n>> +\t * We need to declare all the classes here so that Python docstrings\n>> +\t * can be generated correctly.\n>> +\t * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n>> +\t */\n>> +\n>> +\tauto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n>> +\tauto pyCamera = py::class_<Camera>(m, \"Camera\");\n>> +\tauto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n>> +\tauto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, \"Status\");\n>> +\tauto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n>> +\tauto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n>> +\tauto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n>> +\tauto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n>> +\tauto pyStream = py::class_<Stream>(m, \"Stream\");\n>> +\tauto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n>> +\tauto pyRequest = py::class_<Request>(m, \"Request\");\n>> +\tauto pyRequestStatus = py::enum_<Request::Status>(pyRequest, \"Status\");\n>> +\tauto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, \"Reuse\");\n>> +\tauto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n>> +\tauto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, \"Status\");\n>> +\tauto pyTransform = py::class_<Transform>(m, \"Transform\");\n>> +\tauto pyColorSpace = py::class_<ColorSpace>(m, \"ColorSpace\");\n>> +\tauto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, \"Primaries\");\n>> +\tauto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, \"TransferFunction\");\n>> +\tauto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, \"YcbcrEncoding\");\n>> +\tauto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, \"Range\");\n>> +\n>> +\t/* Global functions */\n>> +\tm.def(\"log_set_level\", &logSetLevel);\n>> +\n>> +\t/* Classes */\n>> +\tpyCameraManager\n>> +\t\t.def_static(\"singleton\", []() {\n>> +\t\t\tstd::shared_ptr<CameraManager> cm = gCameraManager.lock();\n>> +\t\t\tif (cm)\n>> +\t\t\t\treturn cm;\n>> +\n>> +\t\t\tint fd = eventfd(0, 0);\n>> +\t\t\tif (fd == -1)\n>> +\t\t\t\tthrow std::system_error(errno, std::generic_category(),\n>> +\t\t\t\t                   \"Failed to create eventfd\");\n> \n> There should be tabs instead of spaces, and the '\"' should be aligned\n> under errno. Same below.\n\nOk.\n\n>> +\n>> +\t\t\tcm = std::shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n>> +\t\t\t\tclose(gEventfd);\n>> +\t\t\t\tgEventfd = -1;\n>> +\t\t\t\tdelete p;\n>> +\t\t\t});\n>> +\n>> +\t\t\tgEventfd = fd;\n>> +\t\t\tgCameraManager = cm;\n>> +\n>> +\t\t\tint ret = cm->start();\n>> +\t\t\tif (ret)\n>> +\t\t\t\tthrow std::system_error(-ret, std::generic_category(),\n>> +\t\t\t\t                   \"Failed to start CameraManager\");\n>> +\n>> +\t\t\treturn cm;\n>> +\t\t})\n>> +\n>> +\t\t.def_property_readonly(\"version\", &CameraManager::version)\n>> +\n>> +\t\t.def_property_readonly(\"efd\", [](CameraManager &) {\n>> +\t\t\treturn gEventfd;\n>> +\t\t})\n>> +\n>> +\t\t.def(\"get_ready_requests\", [](CameraManager &) {\n>> +\t\t\tstd::vector<Request *> v;\n>> +\n>> +\t\t\t{\n>> +\t\t\t\tstd::lock_guard guard(gReqlistMutex);\n>> +\t\t\t\tswap(v, gReqList);\n>> +\t\t\t}\n>> +\n>> +\t\t\tstd::vector<py::object> ret;\n>> +\n>> +\t\t\tfor (Request *req : v) {\n>> +\t\t\t\tpy::object o = py::cast(req);\n>> +\t\t\t\t/* Decrease the ref increased in Camera.queue_request() */\n>> +\t\t\t\to.dec_ref();\n>> +\t\t\t\tret.push_back(o);\n>> +\t\t\t}\n>> +\n>> +\t\t\treturn ret;\n>> +\t\t})\n>> +\n>> +\t\t.def(\"get\", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>())\n>> +\n>> +\t\t/* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n>> +\t\t.def_property_readonly(\"cameras\", [](CameraManager &self) {\n>> +\t\t\tpy::list l;\n>> +\n>> +\t\t\tfor (auto &c : self.cameras()) {\n>> +\t\t\t\tpy::object py_cm = py::cast(self);\n>> +\t\t\t\tpy::object py_cam = py::cast(c);\n>> +\t\t\t\tpy::detail::keep_alive_impl(py_cam, py_cm);\n>> +\t\t\t\tl.append(py_cam);\n>> +\t\t\t}\n>> +\n>> +\t\t\treturn l;\n>> +\t\t});\n>> +\n>> +\tpyCamera\n>> +\t\t.def_property_readonly(\"id\", &Camera::id)\n>> +\t\t.def(\"acquire\", &Camera::acquire)\n>> +\t\t.def(\"release\", &Camera::release)\n>> +\t\t.def(\"start\", [](Camera &self, py::dict controls) {\n>> +\t\t\t/* \\todo What happens if someone calls start() multiple times? */\n>> +\n>> +\t\t\tself.requestCompleted.connect(handleRequestCompleted);\n>> +\n>> +\t\t\tconst ControlInfoMap &controlMap = self.controls();\n>> +\t\t\tControlList controlList(controlMap);\n>> +\t\t\tfor (const auto& [hkey, hval]: controls) {\n>> +\t\t\t\tauto key = hkey.cast<std::string>();\n>> +\n>> +\t\t\t\tauto it = find_if(controlMap.begin(), controlMap.end(),\n>> +\t\t\t\t\t\t  [&key](const auto &kvp) {\n>> +\t\t\t\t\t\t\t  return kvp.first->name() == key; });\n> \n> \t\t\t\tauto it = std::find_if(controlMap.begin(), controlMap.end(),\n> \t\t\t\t\t\t       [&key](const auto &kvp) {\n> \t\t\t\t\t\t\t       return kvp.first->name() == key;\n> \t\t\t\t\t\t       });\n\nOk.\n\n>> +\n>> +\t\t\t\tif (it == controlMap.end())\n>> +\t\t\t\t\tthrow std::runtime_error(\"Control \" + key + \" not found\");\n>> +\n>> +\t\t\t\tconst auto &id = it->first;\n>> +\t\t\t\tauto obj = py::cast<py::object>(hval);\n>> +\n>> +\t\t\t\tcontrolList.set(id->id(), pyToControlValue(obj, id->type()));\n>> +\t\t\t}\n>> +\n>> +\t\t\tint ret = self.start(&controlList);\n>> +\t\t\tif (ret) {\n>> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n>> +\t\t\t\treturn ret;\n>> +\t\t\t}\n>> +\n>> +\t\t\treturn 0;\n>> +\t\t}, py::arg(\"controls\") = py::dict())\n>> +\n>> +\t\t.def(\"stop\", [](Camera &self) {\n>> +\t\t\tint ret = self.stop();\n>> +\t\t\tif (ret)\n>> +\t\t\t\treturn ret;\n>> +\n>> +\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n>> +\n>> +\t\t\treturn 0;\n>> +\t\t})\n>> +\n>> +\t\t.def(\"__repr__\", [](Camera &self) {\n>> +\t\t\treturn \"<libcamera.Camera '\" + self.id() + \"'>\";\n>> +\t\t})\n>> +\n>> +\t\t/* Keep the camera alive, as StreamConfiguration contains a Stream* */\n>> +\t\t.def(\"generate_configuration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n>> +\t\t.def(\"configure\", &Camera::configure)\n>> +\n>> +\t\t.def(\"create_request\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n>> +\n>> +\t\t.def(\"queue_request\", [](Camera &self, Request *req) {\n>> +\t\t\tpy::object py_req = py::cast(req);\n>> +\n>> +\t\t\t/*\n>> +\t\t\t * Increase the reference count, will be dropped in\n>> +\t\t\t * CameraManager.get_ready_requests().\n>> +\t\t\t */\n>> +\n>> +\t\t\tpy_req.inc_ref();\n>> +\n>> +\t\t\tint ret = self.queueRequest(req);\n>> +\t\t\tif (ret)\n>> +\t\t\t\tpy_req.dec_ref();\n>> +\n>> +\t\t\treturn ret;\n>> +\t\t})\n>> +\n>> +\t\t.def_property_readonly(\"streams\", [](Camera &self) {\n>> +\t\t\tpy::set set;\n>> +\t\t\tfor (auto &s : self.streams()) {\n>> +\t\t\t\tpy::object py_self = py::cast(self);\n>> +\t\t\t\tpy::object py_s = py::cast(s);\n>> +\t\t\t\tpy::detail::keep_alive_impl(py_s, py_self);\n>> +\t\t\t\tset.add(py_s);\n>> +\t\t\t}\n>> +\t\t\treturn set;\n>> +\t\t})\n>> +\n>> +\t\t.def(\"find_control\", [](Camera &self, const std::string &name) {\n>> +\t\t\tconst auto &controls = self.controls();\n>> +\n>> +\t\t\tauto it = find_if(controls.begin(), controls.end(),\n>> +\t\t\t\t\t  [&name](const auto &kvp) { return kvp.first->name() == name; });\n> \n> Missing std:: here too (and I would also wrap the line).\n\nInteresting... Why does it compile...\n\n>> +\n>> +\t\t\tif (it == controls.end())\n>> +\t\t\t\tthrow std::runtime_error(\"Control not found\");\n> \n> \t\t\t\tthrow std::runtime_error(\"Control '\" + name + \"' not found\");\n> \n> could be nicer to debug issues.\n\nOk.\n\n>> +\n>> +\t\t\treturn it->first;\n>> +\t\t}, py::return_value_policy::reference_internal)\n>> +\n>> +\t\t.def_property_readonly(\"controls\", [](Camera &self) {\n>> +\t\t\tpy::dict ret;\n>> +\n>> +\t\t\tfor (const auto &[id, ci] : self.controls()) {\n>> +\t\t\t\tret[id->name().c_str()] = std::make_tuple<py::object>(controlValueToPy(ci.min()),\n>> +\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.max()),\n>> +\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.def()));\n>> +\t\t\t}\n>> +\n>> +\t\t\treturn ret;\n>> +\t\t})\n>> +\n>> +\t\t.def_property_readonly(\"properties\", [](Camera &self) {\n>> +\t\t\tpy::dict ret;\n>> +\n>> +\t\t\tfor (const auto &[key, cv] : self.properties()) {\n>> +\t\t\t\tconst ControlId *id = properties::properties.at(key);\n>> +\t\t\t\tpy::object ob = controlValueToPy(cv);\n>> +\n>> +\t\t\t\tret[id->name().c_str()] = ob;\n>> +\t\t\t}\n>> +\n>> +\t\t\treturn ret;\n>> +\t\t});\n>> +\n>> +\tpyCameraConfiguration\n>> +\t\t.def(\"__iter__\", [](CameraConfiguration &self) {\n>> +\t\t\treturn py::make_iterator<py::return_value_policy::reference_internal>(self);\n>> +\t\t}, py::keep_alive<0, 1>())\n>> +\t\t.def(\"__len__\", [](CameraConfiguration &self) {\n>> +\t\t\treturn self.size();\n>> +\t\t})\n>> +\t\t.def(\"validate\", &CameraConfiguration::validate)\n>> +\t\t.def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at),\n>> +\t\t     py::return_value_policy::reference_internal)\n>> +\t\t.def_property_readonly(\"size\", &CameraConfiguration::size)\n>> +\t\t.def_property_readonly(\"empty\", &CameraConfiguration::empty)\n>> +\t\t.def_readwrite(\"transform\", &CameraConfiguration::transform);\n>> +\n>> +\tpyCameraConfigurationStatus\n>> +\t\t.value(\"Valid\", CameraConfiguration::Valid)\n>> +\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\n>> +\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\n>> +\n>> +\tpyStreamConfiguration\n>> +\t\t.def(\"to_string\", &StreamConfiguration::toString)\n> \n> Should this be __str__ ?\n\nYes. And we seem to have a few __repr__, which should be __str__. I'll \nchange those too.\n\n>> +\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream,\n>> +\t\t                       py::return_value_policy::reference_internal)\n>> +\t\t.def_property(\n>> +\t\t\t\"size\",\n>> +\t\t\t[](StreamConfiguration &self) {\n>> +\t\t\t\treturn std::make_tuple(self.size.width, self.size.height);\n>> +\t\t\t},\n>> +\t\t\t[](StreamConfiguration &self, std::tuple<uint32_t, uint32_t> size) {\n>> +\t\t\t\tself.size.width = std::get<0>(size);\n>> +\t\t\t\tself.size.height = std::get<1>(size);\n>> +\t\t\t})\n>> +\t\t.def_property(\n>> +\t\t\t\"pixel_format\",\n>> +\t\t\t[](StreamConfiguration &self) {\n>> +\t\t\t\treturn self.pixelFormat.toString();\n>> +\t\t\t},\n>> +\t\t\t[](StreamConfiguration &self, std::string fmt) {\n>> +\t\t\t\tself.pixelFormat = PixelFormat::fromString(fmt);\n>> +\t\t\t})\n>> +\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\n>> +\t\t.def_readwrite(\"frame_size\", &StreamConfiguration::frameSize)\n>> +\t\t.def_readwrite(\"buffer_count\", &StreamConfiguration::bufferCount)\n>> +\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats,\n>> +\t\t                       py::return_value_policy::reference_internal)\n>> +\t\t.def_readwrite(\"colorSpace\", &StreamConfiguration::colorSpace);\n> \n> color_space\n\nOk.\n\n> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\n  Tomi","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 1BD3DC0F2A\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  6 May 2022 17:50:33 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 53C8861655;\n\tFri,  6 May 2022 19:50:32 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 165D3604A3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  6 May 2022 19:50:25 +0200 (CEST)","from [192.168.1.111] (91-156-85-209.elisa-laajakaista.fi\n\t[91.156.85.209])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 20DE9492;\n\tFri,  6 May 2022 19:50:24 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651859432;\n\tbh=IpDsGZQgcLYy7/UpQVh4y4h9+qjd3amX+NosIjC9bDk=;\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=KxKRTlFG1I/ARIs6QK6tnYTj6+wu7aGy+BHjhDjvTHJEMjJ1+IPcsRDd/PT3CURBf\n\ty37e9Gk2XCCis8+eOe4DQLL5M+Uo6NYhSo4xEexZXvbadLJ7r5IO1pOy4uI7oH+XbF\n\tyuJqK4wE4QQctPcnKokEK+8xkfTvpOPZ+WiN8eKSYts4XwAYcSwY5f3KDh3+NX3etN\n\tH5J/Q30upSx02ej7G4lNoutQCxAbF59n9k2shGjSSOGeQwhQP5o9oeQQeU6DXPpPQR\n\tXFuniNEDbAavjo+x+Jr3D8WkfUzD1f6YwByASE+wZ1oTbucE7nxp9UE+PYYPENOEwD\n\tIo48UeJiwnIWw==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651859424;\n\tbh=IpDsGZQgcLYy7/UpQVh4y4h9+qjd3amX+NosIjC9bDk=;\n\th=Date:Subject:To:Cc:References:From:In-Reply-To:From;\n\tb=geNqXXW3/kjcnHH8DN6VktyaYj9Wp8iWyVNjUmDmEBht7LFyjxDtOUPZ40zWxzxXQ\n\tzBHpdloX9UXa0D+fVxMb4cjc60+m2XtrnDsvQHS0KClCbXR6nx0hjsmXPK8WgX1JpQ\n\tRhy+b8MYyLKkZAxdCC3DaEMvdL4wAOmlGTbQqBPs="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"geNqXXW3\"; dkim-atps=neutral","Message-ID":"<65545abe-b56f-c3c6-9f9f-0cc6567c8866@ideasonboard.com>","Date":"Fri, 6 May 2022 20:50:20 +0300","MIME-Version":"1.0","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101\n\tThunderbird/91.8.0","Content-Language":"en-US","To":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","References":"<20220506145414.99039-1-tomi.valkeinen@ideasonboard.com>\n\t<20220506145414.99039-5-tomi.valkeinen@ideasonboard.com>\n\t<YnVY/KR03cB/TPri@pendragon.ideasonboard.com>","In-Reply-To":"<YnVY/KR03cB/TPri@pendragon.ideasonboard.com>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","Subject":"Re: [libcamera-devel] [PATCH v8 4/7] Add Python bindings","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":"Tomi Valkeinen via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Tomi Valkeinen <tomi.valkeinen@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>"}},{"id":22899,"web_url":"https://patchwork.libcamera.org/comment/22899/","msgid":"<YnV8mQDNCfeGsjbI@pendragon.ideasonboard.com>","date":"2022-05-06T19:52:57","subject":"Re: [libcamera-devel] [PATCH v8 4/7] Add Python bindings","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Tomi,\n\nOn Fri, May 06, 2022 at 08:50:20PM +0300, Tomi Valkeinen wrote:\n> On 06/05/2022 20:21, Laurent Pinchart wrote:\n> > On Fri, May 06, 2022 at 05:54:11PM +0300, Tomi Valkeinen wrote:\n> >> Add libcamera Python bindings. pybind11 is used to generate the C++ <->\n> >> Python layer.\n> >>\n> >> We use pybind11 'smart_holder' version to avoid issues with private\n> >> destructors and shared_ptr. There is also an alternative solution here:\n> >>\n> >> https://github.com/pybind/pybind11/pull/2067\n> >>\n> >> Only a subset of libcamera classes are exposed. Implementing and testing\n> >> the wrapper classes is challenging, and as such only classes that I have\n> >> needed have been added so far.\n\nThere's another issue I'm afraid :-( Compiling with clang-13, I get\n\nIn file included from ../../src/py/libcamera/pyenums.cpp:10:\nIn file included from ../../subprojects/pybind11/include/pybind11/smart_holder.h:7:\nIn file included from ../../subprojects/pybind11/include/pybind11/pybind11.h:13:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/class.h:12:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/../attr.h:14:\nIn file included from ../../subprojects/pybind11/include/pybind11/cast.h:33:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/smart_holder_type_casters.h:9:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/../trampoline_self_life_support.h:8:\n../../subprojects/pybind11/include/pybind11/detail/smart_holder_poc.h:109:2: error: extra ';' outside of a function is incompatible with C++98 [-Werror,-Wc++98-compat-extra-semi]\n};\n ^\n1 error generated.\n\nWith clang-9, there's additionally\n\nIn file included from ../../src/py/libcamera/pymain.cpp:23:\nIn file included from ../../subprojects/pybind11/include/pybind11/functional.h:12:\nIn file included from ../../subprojects/pybind11/include/pybind11/pybind11.h:13:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/class.h:12:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/../attr.h:14:\nIn file included from ../../subprojects/pybind11/include/pybind11/cast.h:33:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/smart_holder_type_casters.h:9:\nIn file included from ../../subprojects/pybind11/include/pybind11/detail/../trampoline_self_life_support.h:8:\n../../subprojects/pybind11/include/pybind11/detail/smart_holder_poc.h:109:2: error: extra ';' outside of a function is incompatible with C++98 [-Werror,-Wc++98-compat-extra-semi]\n};\n ^\n../../src/py/libcamera/pymain.cpp:42:10: error: local variable 't' will be copied despite being returned by name [-Werror,-Wreturn-std-move]\n                return t;\n                       ^\n../../src/py/libcamera/pymain.cpp:42:10: note: call 'std::move' explicitly to avoid copying\n                return t;\n                       ^\n                       std::move(t)\n../../src/py/libcamera/pymain.cpp:42:10: error: local variable 't' will be copied despite being returned by name [-Werror,-Wreturn-std-move]\n                return t;\n                       ^\n../../src/py/libcamera/pymain.cpp:52:10: note: in instantiation of function template specialization 'valueOrTuple<bool>' requested here\n                return valueOrTuple<bool>(cv);\n                       ^\n../../src/py/libcamera/pymain.cpp:42:10: note: call 'std::move' explicitly to avoid copying\n                return t;\n                       ^\n                       std::move(t)\n../../src/py/libcamera/pymain.cpp:42:10: error: local variable 't' will be copied despite being returned by name [-Werror,-Wreturn-std-move]\n                return t;\n                       ^\n../../src/py/libcamera/pymain.cpp:54:10: note: in instantiation of function template specialization 'valueOrTuple<unsigned char>' requested here\n                return valueOrTuple<uint8_t>(cv);\n                       ^\n../../src/py/libcamera/pymain.cpp:42:10: note: call 'std::move' explicitly to avoid copying\n                return t;\n                       ^\n                       std::move(t)\n../../src/py/libcamera/pymain.cpp:42:10: error: local variable 't' will be copied despite being returned by name [-Werror,-Wreturn-std-move]\n                return t;\n                       ^\n../../src/py/libcamera/pymain.cpp:56:10: note: in instantiation of function template specialization 'valueOrTuple<int>' requested here\n                return valueOrTuple<int32_t>(cv);\n                       ^\n../../src/py/libcamera/pymain.cpp:42:10: note: call 'std::move' explicitly to avoid copying\n                return t;\n                       ^\n                       std::move(t)\n../../src/py/libcamera/pymain.cpp:42:10: error: local variable 't' will be copied despite being returned by name [-Werror,-Wreturn-std-move]\n                return t;\n                       ^\n../../src/py/libcamera/pymain.cpp:58:10: note: in instantiation of function template specialization 'valueOrTuple<long>' requested here\n                return valueOrTuple<int64_t>(cv);\n                       ^\n../../src/py/libcamera/pymain.cpp:42:10: note: call 'std::move' explicitly to avoid copying\n                return t;\n                       ^\n                       std::move(t)\n../../src/py/libcamera/pymain.cpp:42:10: error: local variable 't' will be copied despite being returned by name [-Werror,-Wreturn-std-move]\n                return t;\n                       ^\n../../src/py/libcamera/pymain.cpp:60:10: note: in instantiation of function template specialization 'valueOrTuple<float>' requested here\n                return valueOrTuple<float>(cv);\n                       ^\n../../src/py/libcamera/pymain.cpp:42:10: note: call 'std::move' explicitly to avoid copying\n                return t;\n                       ^\n                       std::move(t)\n7 errors generated.\n\nWith gcc-10 and gcc-11, errors are different:\n\n../../src/py/libcamera/pymain.cpp: In function ‘void handleRequestCompleted(libcamera::Request*)’:\n../../src/py/libcamera/pymain.cpp:130:14: error: ignoring return value of ‘ssize_t write(int, const void*, size_t)’ declared with attribute ‘warn_unused_result’ [-Werror=unused-result]\n  130 |         write(gEventfd, &v, 8);\n      |         ~~~~~^~~~~~~~~~~~~~~~~\n\n> >> Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> >> ---\n> >>   meson.build                                   |   1 +\n> >>   meson_options.txt                             |   5 +\n> >>   src/meson.build                               |   1 +\n> >>   src/py/libcamera/__init__.py                  |  84 +++\n> >>   src/py/libcamera/meson.build                  |  51 ++\n> >>   src/py/libcamera/pyenums.cpp                  |  34 +\n> >>   src/py/libcamera/pymain.cpp                   | 640 ++++++++++++++++++\n> >>   src/py/meson.build                            |   1 +\n> >>   subprojects/.gitignore                        |   3 +-\n> >>   subprojects/packagefiles/pybind11/meson.build |   7 +\n> >>   subprojects/pybind11.wrap                     |   9 +\n> >>   11 files changed, 835 insertions(+), 1 deletion(-)\n> >>   create mode 100644 src/py/libcamera/__init__.py\n> >>   create mode 100644 src/py/libcamera/meson.build\n> >>   create mode 100644 src/py/libcamera/pyenums.cpp\n> >>   create mode 100644 src/py/libcamera/pymain.cpp\n> >>   create mode 100644 src/py/meson.build\n> >>   create mode 100644 subprojects/packagefiles/pybind11/meson.build\n> >>   create mode 100644 subprojects/pybind11.wrap\n> >>\n> >> diff --git a/meson.build b/meson.build\n> >> index 0124e7d3..60a911e0 100644\n> >> --- a/meson.build\n> >> +++ b/meson.build\n> >> @@ -177,6 +177,7 @@ summary({\n> >>               'Tracing support': tracing_enabled,\n> >>               'Android support': android_enabled,\n> >>               'GStreamer support': gst_enabled,\n> >> +            'Python bindings': pycamera_enabled,\n> >>               'V4L2 emulation support': v4l2_enabled,\n> >>               'cam application': cam_enabled,\n> >>               'qcam application': qcam_enabled,\n> >> diff --git a/meson_options.txt b/meson_options.txt\n> >> index 2c80ad8b..ca00c78e 100644\n> >> --- a/meson_options.txt\n> >> +++ b/meson_options.txt\n> >> @@ -58,3 +58,8 @@ option('v4l2',\n> >>           type : 'boolean',\n> >>           value : false,\n> >>           description : 'Compile the V4L2 compatibility layer')\n> >> +\n> >> +option('pycamera',\n> >> +        type : 'feature',\n> >> +        value : 'auto',\n> >> +        description : 'Enable libcamera Python bindings (experimental)')\n> >> diff --git a/src/meson.build b/src/meson.build\n> >> index e0ea9c35..34663a6f 100644\n> >> --- a/src/meson.build\n> >> +++ b/src/meson.build\n> >> @@ -37,4 +37,5 @@ subdir('cam')\n> >>   subdir('qcam')\n> >>   \n> >>   subdir('gstreamer')\n> >> +subdir('py')\n> >>   subdir('v4l2')\n> >> diff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py\n> >> new file mode 100644\n> >> index 00000000..6b330890\n> >> --- /dev/null\n> >> +++ b/src/py/libcamera/__init__.py\n> >> @@ -0,0 +1,84 @@\n> >> +# SPDX-License-Identifier: LGPL-2.1-or-later\n> >> +# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> >> +\n> >> +from ._libcamera import *\n> >> +\n> >> +\n> >> +class MappedFrameBuffer:\n> >> +    def __init__(self, fb):\n> >> +        self.__fb = fb\n> >> +\n> >> +    def __enter__(self):\n> >> +        from os import lseek, SEEK_END\n> > \n> > As it's local to the function it doesn't matter much, but I would have\n> > just imported os and used os.lseek and os.SEEK_END.\n> \n> Yep, makes sense.\n> \n> >> +        import mmap\n> >> +\n> >> +        fb = self.__fb\n> >> +\n> >> +        # Collect information about the buffers\n> >> +\n> >> +        bufinfos = {}\n> >> +\n> >> +        for i in range(fb.num_planes):\n> >> +            fd = fb.fd(i)\n> >> +\n> >> +            if fd not in bufinfos:\n> >> +                buflen = lseek(fd, 0, SEEK_END)\n> >> +                bufinfos[fd] = {'maplen': 0, 'buflen': buflen}\n> >> +            else:\n> >> +                buflen = bufinfos[fd]['buflen']\n> >> +\n> >> +            if fb.offset(i) > buflen or fb.offset(i) + fb.length(i) > buflen:\n> >> +                raise RuntimeError(f'plane is out of buffer: buffer length={buflen}, ' +\n> >> +                                   f'plane offset={fb.offset(i)}, plane length={fb.length(i)}')\n> >> +\n> >> +            bufinfos[fd]['maplen'] = max(bufinfos[fd]['maplen'], fb.offset(i) + fb.length(i))\n> >> +\n> >> +        # mmap the buffers\n> >> +\n> >> +        maps = []\n> >> +\n> >> +        for fd, info in bufinfos.items():\n> >> +            map = mmap.mmap(fd, info['maplen'], mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)\n> >> +            info['map'] = map\n> >> +            maps.append(map)\n> >> +\n> >> +        self.__maps = tuple(maps)\n> >> +\n> >> +        # Create memoryviews for the planes\n> >> +\n> >> +        planes = []\n> >> +\n> >> +        for i in range(fb.num_planes):\n> >> +            fd = fb.fd(i)\n> >> +            info = bufinfos[fd]\n> >> +\n> >> +            mv = memoryview(info['map'])\n> >> +\n> >> +            start = fb.offset(i)\n> >> +            end = fb.offset(i) + fb.length(i)\n> >> +\n> >> +            mv = mv[start:end]\n> >> +\n> >> +            planes.append(mv)\n> >> +\n> >> +        self.__planes = tuple(planes)\n> >> +\n> >> +        return self\n> >> +\n> >> +    def __exit__(self, exc_type, exc_value, exc_traceback):\n> >> +        for p in self.__planes:\n> >> +            p.release()\n> >> +\n> >> +        for mm in self.__maps:\n> >> +            mm.close()\n> >> +\n> >> +    @property\n> >> +    def planes(self):\n> >> +        return self.__planes\n> >> +\n> >> +\n> >> +def __FrameBuffer__mmap(self):\n> >> +    return MappedFrameBuffer(self)\n> >> +\n> >> +\n> >> +FrameBuffer.mmap = __FrameBuffer__mmap\n> >> diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\n> >> new file mode 100644\n> >> index 00000000..e4abc34a\n> >> --- /dev/null\n> >> +++ b/src/py/libcamera/meson.build\n> >> @@ -0,0 +1,51 @@\n> >> +# SPDX-License-Identifier: CC0-1.0\n> >> +\n> >> +py3_dep = dependency('python3', required : get_option('pycamera'))\n> >> +\n> >> +if not py3_dep.found()\n> >> +    pycamera_enabled = false\n> >> +    subdir_done()\n> >> +endif\n> >> +\n> >> +pycamera_enabled = true\n> >> +\n> >> +pybind11_proj = subproject('pybind11')\n> >> +pybind11_dep = pybind11_proj.get_variable('pybind11_dep')\n> >> +\n> >> +pycamera_sources = files([\n> >> +    'pyenums.cpp',\n> >> +    'pymain.cpp',\n> >> +])\n> >> +\n> >> +pycamera_deps = [\n> >> +    libcamera_public,\n> >> +    py3_dep,\n> >> +    pybind11_dep,\n> >> +]\n> >> +\n> >> +pycamera_args = [\n> >> +    '-fvisibility=hidden',\n> >> +    '-Wno-shadow',\n> >> +    '-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT',\n> >> +]\n> >> +\n> >> +destdir = get_option('libdir') / ('python' + py3_dep.version()) / 'site-packages' / 'libcamera'\n> >> +\n> >> +pycamera = shared_module('_libcamera',\n> >> +                         pycamera_sources,\n> >> +                         install : true,\n> >> +                         install_dir : destdir,\n> >> +                         name_prefix : '',\n> >> +                         dependencies : pycamera_deps,\n> >> +                         cpp_args : pycamera_args)\n> >> +\n> >> +run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',\n> >> +            meson.current_build_dir() / '__init__.py',\n> >> +            check: true)\n> >> +\n> >> +install_data(['__init__.py'], install_dir : destdir)\n> >> +\n> >> +# \\todo: Generate stubs when building. Depends on pybind11-stubgen. Sometimes\n> > \n> > s/todo:/todo/\n> > \n> > I'm still not sure what this is for :-) Do we need to generate stubs\n> > later ? What are they for ?\n> \n> Oh, I see. https://peps.python.org/pep-0484/#stub-files\n> \n> I'm not very familiar with them, but my editor is able to introspect \n> pure python code, but not the pybind11 module. A stub file can provide \n> the pure-python view to the module's API.\n> \n> I haven't gotten them to work too well, thought. Earlier today it \n> worked, then later it didn't. I haven't figured out the exact method on \n> how the stub files are searched, etc...\n\nThanks for the explanation. Adding the above link to the comment would\nbe enough for me.\n\n> >> +# this works, sometimes doesn't... To generate pylibcamera stubs.\n> >> +# $ PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\n> >> +# $ mv build/src/py/libcamera-stubs/* build/src/py/libcamera/\n> >> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n> >> new file mode 100644\n> >> index 00000000..b655e622\n> >> --- /dev/null\n> >> +++ b/src/py/libcamera/pyenums.cpp\n> >> @@ -0,0 +1,34 @@\n> >> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> >> +/*\n> >> + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> >> + *\n> >> + * Python bindings - Enumerations\n> >> + */\n> >> +\n> >> +#include <libcamera/libcamera.h>\n> >> +\n> >> +#include <pybind11/smart_holder.h>\n> >> +\n> >> +namespace py = pybind11;\n> >> +\n> >> +using namespace libcamera;\n> >> +\n> >> +void init_pyenums(py::module &m)\n> >> +{\n> >> +\tpy::enum_<StreamRole>(m, \"StreamRole\")\n> >> +\t\t.value(\"StillCapture\", StreamRole::StillCapture)\n> >> +\t\t.value(\"Raw\", StreamRole::Raw)\n> >> +\t\t.value(\"VideoRecording\", StreamRole::VideoRecording)\n> >> +\t\t.value(\"Viewfinder\", StreamRole::Viewfinder);\n> >> +\n> >> +\tpy::enum_<ControlType>(m, \"ControlType\")\n> >> +\t\t.value(\"None\", ControlType::ControlTypeNone)\n> >> +\t\t.value(\"Bool\", ControlType::ControlTypeBool)\n> >> +\t\t.value(\"Byte\", ControlType::ControlTypeByte)\n> >> +\t\t.value(\"Integer32\", ControlType::ControlTypeInteger32)\n> >> +\t\t.value(\"Integer64\", ControlType::ControlTypeInteger64)\n> >> +\t\t.value(\"Float\", ControlType::ControlTypeFloat)\n> >> +\t\t.value(\"String\", ControlType::ControlTypeString)\n> >> +\t\t.value(\"Rectangle\", ControlType::ControlTypeRectangle)\n> >> +\t\t.value(\"Size\", ControlType::ControlTypeSize);\n> >> +}\n> >> diff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp\n> >> new file mode 100644\n> >> index 00000000..8c3be8f4\n> >> --- /dev/null\n> >> +++ b/src/py/libcamera/pymain.cpp\n> >> @@ -0,0 +1,640 @@\n> >> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> >> +/*\n> >> + * Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> >> + *\n> >> + * Python bindings\n> >> + */\n> >> +\n> >> +/*\n> >> + * \\todo Add geometry classes (Point, Rectangle...)\n> >> + * \\todo Add bindings for the ControlInfo class\n> >> + */\n> >> +\n> >> +#include <chrono>\n> >> +#include <fcntl.h>\n> >> +#include <mutex>\n> >> +#include <sys/eventfd.h>\n> >> +#include <sys/mman.h>\n> >> +#include <thread>\n> >> +#include <unistd.h>\n> >> +\n> >> +#include <libcamera/libcamera.h>\n> >> +\n> >> +#include <pybind11/functional.h>\n> >> +#include <pybind11/smart_holder.h>\n> >> +#include <pybind11/stl.h>\n> >> +#include <pybind11/stl_bind.h>\n> >> +\n> >> +namespace py = pybind11;\n> >> +\n> >> +using namespace libcamera;\n> >> +\n> >> +template<typename T>\n> >> +static py::object valueOrTuple(const ControlValue &cv)\n> >> +{\n> >> +\tif (cv.isArray()) {\n> >> +\t\tconst T *v = reinterpret_cast<const T *>(cv.data().data());\n> >> +\t\tauto t = py::tuple(cv.numElements());\n> >> +\n> >> +\t\tfor (size_t i = 0; i < cv.numElements(); ++i)\n> >> +\t\t\tt[i] = v[i];\n> >> +\n> >> +\t\treturn t;\n> >> +\t}\n> >> +\n> >> +\treturn py::cast(cv.get<T>());\n> >> +}\n> >> +\n> >> +static py::object controlValueToPy(const ControlValue &cv)\n> >> +{\n> >> +\tswitch (cv.type()) {\n> >> +\tcase ControlTypeBool:\n> >> +\t\treturn valueOrTuple<bool>(cv);\n> >> +\tcase ControlTypeByte:\n> >> +\t\treturn valueOrTuple<uint8_t>(cv);\n> >> +\tcase ControlTypeInteger32:\n> >> +\t\treturn valueOrTuple<int32_t>(cv);\n> >> +\tcase ControlTypeInteger64:\n> >> +\t\treturn valueOrTuple<int64_t>(cv);\n> >> +\tcase ControlTypeFloat:\n> >> +\t\treturn valueOrTuple<float>(cv);\n> >> +\tcase ControlTypeString:\n> >> +\t\treturn py::cast(cv.get<std::string>());\n> >> +\tcase ControlTypeRectangle: {\n> >> +\t\tconst Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n> >> +\t\treturn py::make_tuple(v->x, v->y, v->width, v->height);\n> >> +\t}\n> >> +\tcase ControlTypeSize: {\n> >> +\t\tconst Size *v = reinterpret_cast<const Size *>(cv.data().data());\n> >> +\t\treturn py::make_tuple(v->width, v->height);\n> >> +\t}\n> >> +\tcase ControlTypeNone:\n> >> +\tdefault:\n> >> +\t\tthrow std::runtime_error(\"Unsupported ControlValue type\");\n> >> +\t}\n> >> +}\n> >> +\n> >> +template<typename T>\n> >> +static ControlValue controlValueMaybeArray(const py::object &ob)\n> >> +{\n> >> +\tif (py::isinstance<py::list>(ob) || py::isinstance<py::tuple>(ob)) {\n> >> +\t\tstd::vector<T> vec = ob.cast<std::vector<T>>();\n> >> +\t\treturn ControlValue(Span<const T>(vec));\n> >> +\t}\n> >> +\n> >> +\treturn ControlValue(ob.cast<T>());\n> >> +}\n> >> +\n> >> +static ControlValue pyToControlValue(const py::object &ob, ControlType type)\n> >> +{\n> >> +\tswitch (type) {\n> >> +\tcase ControlTypeBool:\n> >> +\t\treturn ControlValue(ob.cast<bool>());\n> >> +\tcase ControlTypeByte:\n> >> +\t\treturn controlValueMaybeArray<uint8_t>(ob);\n> >> +\tcase ControlTypeInteger32:\n> >> +\t\treturn controlValueMaybeArray<int32_t>(ob);\n> >> +\tcase ControlTypeInteger64:\n> >> +\t\treturn controlValueMaybeArray<int64_t>(ob);\n> >> +\tcase ControlTypeFloat:\n> >> +\t\treturn controlValueMaybeArray<float>(ob);\n> >> +\tcase ControlTypeString:\n> >> +\t\treturn ControlValue(ob.cast<std::string>());\n> >> +\tcase ControlTypeRectangle: {\n> >> +\t\tauto array = ob.cast<std::array<int32_t, 4>>();\n> >> +\t\treturn ControlValue(Rectangle(array[0], array[1], array[2], array[3]));\n> >> +\t}\n> >> +\tcase ControlTypeSize: {\n> >> +\t\tauto array = ob.cast<std::array<int32_t, 2>>();\n> >> +\t\treturn ControlValue(Size(array[0], array[1]));\n> >> +\t}\n> >> +\tcase ControlTypeNone:\n> >> +\tdefault:\n> >> +\t\tthrow std::runtime_error(\"Control type not implemented\");\n> >> +\t}\n> >> +}\n> >> +\n> >> +static std::weak_ptr<CameraManager> gCameraManager;\n> >> +static int gEventfd;\n> >> +static std::mutex gReqlistMutex;\n> >> +static std::vector<Request *> gReqList;\n> >> +\n> >> +static void handleRequestCompleted(Request *req)\n> >> +{\n> >> +\t{\n> >> +\t\tstd::lock_guard guard(gReqlistMutex);\n> >> +\t\tgReqList.push_back(req);\n> >> +\t}\n> >> +\n> >> +\tuint64_t v = 1;\n> >> +\twrite(gEventfd, &v, 8);\n> >> +}\n> >> +\n> >> +void init_pyenums(py::module &m);\n> >> +\n> >> +PYBIND11_MODULE(_libcamera, m)\n> >> +{\n> >> +\tinit_pyenums(m);\n> >> +\n> >> +\t/* Forward declarations */\n> >> +\n> >> +\t/*\n> >> +\t * We need to declare all the classes here so that Python docstrings\n> >> +\t * can be generated correctly.\n> >> +\t * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n> >> +\t */\n> >> +\n> >> +\tauto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n> >> +\tauto pyCamera = py::class_<Camera>(m, \"Camera\");\n> >> +\tauto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n> >> +\tauto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, \"Status\");\n> >> +\tauto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n> >> +\tauto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n> >> +\tauto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n> >> +\tauto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n> >> +\tauto pyStream = py::class_<Stream>(m, \"Stream\");\n> >> +\tauto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n> >> +\tauto pyRequest = py::class_<Request>(m, \"Request\");\n> >> +\tauto pyRequestStatus = py::enum_<Request::Status>(pyRequest, \"Status\");\n> >> +\tauto pyRequestReuse = py::enum_<Request::ReuseFlag>(pyRequest, \"Reuse\");\n> >> +\tauto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n> >> +\tauto pyFrameMetadataStatus = py::enum_<FrameMetadata::Status>(pyFrameMetadata, \"Status\");\n> >> +\tauto pyTransform = py::class_<Transform>(m, \"Transform\");\n> >> +\tauto pyColorSpace = py::class_<ColorSpace>(m, \"ColorSpace\");\n> >> +\tauto pyColorSpacePrimaries = py::enum_<ColorSpace::Primaries>(pyColorSpace, \"Primaries\");\n> >> +\tauto pyColorSpaceTransferFunction = py::enum_<ColorSpace::TransferFunction>(pyColorSpace, \"TransferFunction\");\n> >> +\tauto pyColorSpaceYcbcrEncoding = py::enum_<ColorSpace::YcbcrEncoding>(pyColorSpace, \"YcbcrEncoding\");\n> >> +\tauto pyColorSpaceRange = py::enum_<ColorSpace::Range>(pyColorSpace, \"Range\");\n> >> +\n> >> +\t/* Global functions */\n> >> +\tm.def(\"log_set_level\", &logSetLevel);\n> >> +\n> >> +\t/* Classes */\n> >> +\tpyCameraManager\n> >> +\t\t.def_static(\"singleton\", []() {\n> >> +\t\t\tstd::shared_ptr<CameraManager> cm = gCameraManager.lock();\n> >> +\t\t\tif (cm)\n> >> +\t\t\t\treturn cm;\n> >> +\n> >> +\t\t\tint fd = eventfd(0, 0);\n> >> +\t\t\tif (fd == -1)\n> >> +\t\t\t\tthrow std::system_error(errno, std::generic_category(),\n> >> +\t\t\t\t                   \"Failed to create eventfd\");\n> > \n> > There should be tabs instead of spaces, and the '\"' should be aligned\n> > under errno. Same below.\n> \n> Ok.\n> \n> >> +\n> >> +\t\t\tcm = std::shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n> >> +\t\t\t\tclose(gEventfd);\n> >> +\t\t\t\tgEventfd = -1;\n> >> +\t\t\t\tdelete p;\n> >> +\t\t\t});\n> >> +\n> >> +\t\t\tgEventfd = fd;\n> >> +\t\t\tgCameraManager = cm;\n> >> +\n> >> +\t\t\tint ret = cm->start();\n> >> +\t\t\tif (ret)\n> >> +\t\t\t\tthrow std::system_error(-ret, std::generic_category(),\n> >> +\t\t\t\t                   \"Failed to start CameraManager\");\n> >> +\n> >> +\t\t\treturn cm;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def_property_readonly(\"version\", &CameraManager::version)\n> >> +\n> >> +\t\t.def_property_readonly(\"efd\", [](CameraManager &) {\n> >> +\t\t\treturn gEventfd;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def(\"get_ready_requests\", [](CameraManager &) {\n> >> +\t\t\tstd::vector<Request *> v;\n> >> +\n> >> +\t\t\t{\n> >> +\t\t\t\tstd::lock_guard guard(gReqlistMutex);\n> >> +\t\t\t\tswap(v, gReqList);\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\tstd::vector<py::object> ret;\n> >> +\n> >> +\t\t\tfor (Request *req : v) {\n> >> +\t\t\t\tpy::object o = py::cast(req);\n> >> +\t\t\t\t/* Decrease the ref increased in Camera.queue_request() */\n> >> +\t\t\t\to.dec_ref();\n> >> +\t\t\t\tret.push_back(o);\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\treturn ret;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def(\"get\", py::overload_cast<const std::string &>(&CameraManager::get), py::keep_alive<0, 1>())\n> >> +\n> >> +\t\t/* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n> >> +\t\t.def_property_readonly(\"cameras\", [](CameraManager &self) {\n> >> +\t\t\tpy::list l;\n> >> +\n> >> +\t\t\tfor (auto &c : self.cameras()) {\n> >> +\t\t\t\tpy::object py_cm = py::cast(self);\n> >> +\t\t\t\tpy::object py_cam = py::cast(c);\n> >> +\t\t\t\tpy::detail::keep_alive_impl(py_cam, py_cm);\n> >> +\t\t\t\tl.append(py_cam);\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\treturn l;\n> >> +\t\t});\n> >> +\n> >> +\tpyCamera\n> >> +\t\t.def_property_readonly(\"id\", &Camera::id)\n> >> +\t\t.def(\"acquire\", &Camera::acquire)\n> >> +\t\t.def(\"release\", &Camera::release)\n> >> +\t\t.def(\"start\", [](Camera &self, py::dict controls) {\n> >> +\t\t\t/* \\todo What happens if someone calls start() multiple times? */\n> >> +\n> >> +\t\t\tself.requestCompleted.connect(handleRequestCompleted);\n> >> +\n> >> +\t\t\tconst ControlInfoMap &controlMap = self.controls();\n> >> +\t\t\tControlList controlList(controlMap);\n> >> +\t\t\tfor (const auto& [hkey, hval]: controls) {\n> >> +\t\t\t\tauto key = hkey.cast<std::string>();\n> >> +\n> >> +\t\t\t\tauto it = find_if(controlMap.begin(), controlMap.end(),\n> >> +\t\t\t\t\t\t  [&key](const auto &kvp) {\n> >> +\t\t\t\t\t\t\t  return kvp.first->name() == key; });\n> > \n> > \t\t\t\tauto it = std::find_if(controlMap.begin(), controlMap.end(),\n> > \t\t\t\t\t\t       [&key](const auto &kvp) {\n> > \t\t\t\t\t\t\t       return kvp.first->name() == key;\n> > \t\t\t\t\t\t       });\n> \n> Ok.\n> \n> >> +\n> >> +\t\t\t\tif (it == controlMap.end())\n> >> +\t\t\t\t\tthrow std::runtime_error(\"Control \" + key + \" not found\");\n> >> +\n> >> +\t\t\t\tconst auto &id = it->first;\n> >> +\t\t\t\tauto obj = py::cast<py::object>(hval);\n> >> +\n> >> +\t\t\t\tcontrolList.set(id->id(), pyToControlValue(obj, id->type()));\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\tint ret = self.start(&controlList);\n> >> +\t\t\tif (ret) {\n> >> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n> >> +\t\t\t\treturn ret;\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\treturn 0;\n> >> +\t\t}, py::arg(\"controls\") = py::dict())\n> >> +\n> >> +\t\t.def(\"stop\", [](Camera &self) {\n> >> +\t\t\tint ret = self.stop();\n> >> +\t\t\tif (ret)\n> >> +\t\t\t\treturn ret;\n> >> +\n> >> +\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n> >> +\n> >> +\t\t\treturn 0;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def(\"__repr__\", [](Camera &self) {\n> >> +\t\t\treturn \"<libcamera.Camera '\" + self.id() + \"'>\";\n> >> +\t\t})\n> >> +\n> >> +\t\t/* Keep the camera alive, as StreamConfiguration contains a Stream* */\n> >> +\t\t.def(\"generate_configuration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n> >> +\t\t.def(\"configure\", &Camera::configure)\n> >> +\n> >> +\t\t.def(\"create_request\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n> >> +\n> >> +\t\t.def(\"queue_request\", [](Camera &self, Request *req) {\n> >> +\t\t\tpy::object py_req = py::cast(req);\n> >> +\n> >> +\t\t\t/*\n> >> +\t\t\t * Increase the reference count, will be dropped in\n> >> +\t\t\t * CameraManager.get_ready_requests().\n> >> +\t\t\t */\n> >> +\n> >> +\t\t\tpy_req.inc_ref();\n> >> +\n> >> +\t\t\tint ret = self.queueRequest(req);\n> >> +\t\t\tif (ret)\n> >> +\t\t\t\tpy_req.dec_ref();\n> >> +\n> >> +\t\t\treturn ret;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def_property_readonly(\"streams\", [](Camera &self) {\n> >> +\t\t\tpy::set set;\n> >> +\t\t\tfor (auto &s : self.streams()) {\n> >> +\t\t\t\tpy::object py_self = py::cast(self);\n> >> +\t\t\t\tpy::object py_s = py::cast(s);\n> >> +\t\t\t\tpy::detail::keep_alive_impl(py_s, py_self);\n> >> +\t\t\t\tset.add(py_s);\n> >> +\t\t\t}\n> >> +\t\t\treturn set;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def(\"find_control\", [](Camera &self, const std::string &name) {\n> >> +\t\t\tconst auto &controls = self.controls();\n> >> +\n> >> +\t\t\tauto it = find_if(controls.begin(), controls.end(),\n> >> +\t\t\t\t\t  [&name](const auto &kvp) { return kvp.first->name() == name; });\n> > \n> > Missing std:: here too (and I would also wrap the line).\n> \n> Interesting... Why does it compile...\n\nIf you figure it out, I'm interested in knowing :-)\n\n> >> +\n> >> +\t\t\tif (it == controls.end())\n> >> +\t\t\t\tthrow std::runtime_error(\"Control not found\");\n> > \n> > \t\t\t\tthrow std::runtime_error(\"Control '\" + name + \"' not found\");\n> > \n> > could be nicer to debug issues.\n> \n> Ok.\n> \n> >> +\n> >> +\t\t\treturn it->first;\n> >> +\t\t}, py::return_value_policy::reference_internal)\n> >> +\n> >> +\t\t.def_property_readonly(\"controls\", [](Camera &self) {\n> >> +\t\t\tpy::dict ret;\n> >> +\n> >> +\t\t\tfor (const auto &[id, ci] : self.controls()) {\n> >> +\t\t\t\tret[id->name().c_str()] = std::make_tuple<py::object>(controlValueToPy(ci.min()),\n> >> +\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.max()),\n> >> +\t\t\t\t\t\t\t\t\t\t      controlValueToPy(ci.def()));\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\treturn ret;\n> >> +\t\t})\n> >> +\n> >> +\t\t.def_property_readonly(\"properties\", [](Camera &self) {\n> >> +\t\t\tpy::dict ret;\n> >> +\n> >> +\t\t\tfor (const auto &[key, cv] : self.properties()) {\n> >> +\t\t\t\tconst ControlId *id = properties::properties.at(key);\n> >> +\t\t\t\tpy::object ob = controlValueToPy(cv);\n> >> +\n> >> +\t\t\t\tret[id->name().c_str()] = ob;\n> >> +\t\t\t}\n> >> +\n> >> +\t\t\treturn ret;\n> >> +\t\t});\n> >> +\n> >> +\tpyCameraConfiguration\n> >> +\t\t.def(\"__iter__\", [](CameraConfiguration &self) {\n> >> +\t\t\treturn py::make_iterator<py::return_value_policy::reference_internal>(self);\n> >> +\t\t}, py::keep_alive<0, 1>())\n> >> +\t\t.def(\"__len__\", [](CameraConfiguration &self) {\n> >> +\t\t\treturn self.size();\n> >> +\t\t})\n> >> +\t\t.def(\"validate\", &CameraConfiguration::validate)\n> >> +\t\t.def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at),\n> >> +\t\t     py::return_value_policy::reference_internal)\n> >> +\t\t.def_property_readonly(\"size\", &CameraConfiguration::size)\n> >> +\t\t.def_property_readonly(\"empty\", &CameraConfiguration::empty)\n> >> +\t\t.def_readwrite(\"transform\", &CameraConfiguration::transform);\n> >> +\n> >> +\tpyCameraConfigurationStatus\n> >> +\t\t.value(\"Valid\", CameraConfiguration::Valid)\n> >> +\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\n> >> +\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\n> >> +\n> >> +\tpyStreamConfiguration\n> >> +\t\t.def(\"to_string\", &StreamConfiguration::toString)\n> > \n> > Should this be __str__ ?\n> \n> Yes. And we seem to have a few __repr__, which should be __str__. I'll \n> change those too.\n\nI see three __repr__ implementations. I'm not familiar enough with the\ndifference between __str__ and __repr__ to tell which one would be best\nhere.\n\n> >> +\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream,\n> >> +\t\t                       py::return_value_policy::reference_internal)\n> >> +\t\t.def_property(\n> >> +\t\t\t\"size\",\n> >> +\t\t\t[](StreamConfiguration &self) {\n> >> +\t\t\t\treturn std::make_tuple(self.size.width, self.size.height);\n> >> +\t\t\t},\n> >> +\t\t\t[](StreamConfiguration &self, std::tuple<uint32_t, uint32_t> size) {\n> >> +\t\t\t\tself.size.width = std::get<0>(size);\n> >> +\t\t\t\tself.size.height = std::get<1>(size);\n> >> +\t\t\t})\n> >> +\t\t.def_property(\n> >> +\t\t\t\"pixel_format\",\n> >> +\t\t\t[](StreamConfiguration &self) {\n> >> +\t\t\t\treturn self.pixelFormat.toString();\n> >> +\t\t\t},\n> >> +\t\t\t[](StreamConfiguration &self, std::string fmt) {\n> >> +\t\t\t\tself.pixelFormat = PixelFormat::fromString(fmt);\n> >> +\t\t\t})\n> >> +\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\n> >> +\t\t.def_readwrite(\"frame_size\", &StreamConfiguration::frameSize)\n> >> +\t\t.def_readwrite(\"buffer_count\", &StreamConfiguration::bufferCount)\n> >> +\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats,\n> >> +\t\t                       py::return_value_policy::reference_internal)\n> >> +\t\t.def_readwrite(\"colorSpace\", &StreamConfiguration::colorSpace);\n> > \n> > color_space\n> \n> Ok.\n> \n> > Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>","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 2CB5EC3256\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  6 May 2022 19:53:04 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 6EB2865649;\n\tFri,  6 May 2022 21:53:03 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 72EAB61655\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  6 May 2022 21:53:02 +0200 (CEST)","from pendragon.ideasonboard.com (62-78-145-57.bb.dnainternet.fi\n\t[62.78.145.57])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id AEFE3492;\n\tFri,  6 May 2022 21:53:01 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1651866783;\n\tbh=8Ztw97NxlyP+anMdObzq/FMNPlKQjyGk4uxIyRE/WZA=;\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=pnB0ukxSKPpp3IrZIdzK27FIoFO0xt7tHxT+L27iy/muc360PCu1PjnSohOksJ3UD\n\tvxdVWx7bLGpfJSiJ3CnAYQd9d0tvgqnJ1C4nhvsSnHCL2gwIrxC3kKVyKYvnSw0d0f\n\tJfSq9UnynaXHSwdHtiEW7SclhxS9UTE/2PIHAJwtETGVh0l21vLgW4qh+dpyVuhJ84\n\t0P1pw0yC01qHybcCBPdEBdOUEtZEdQ0iUYVe4tfo+Uu/6G71qHnPCQ4/P9w0uSPq0X\n\taZtPTsgS87vFhc+A6dVHEsiTlcooEknm7B+Pz8Nrf6UwmU8Nmm5YT2Wy5/ccPqegeQ\n\ty0NhPR3BsVraA==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1651866782;\n\tbh=8Ztw97NxlyP+anMdObzq/FMNPlKQjyGk4uxIyRE/WZA=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=hDhQnl4d+uWVp16I6SFOIqOa0XaYV+Sv30rvUsgPA7RKQIepy07km/4ghZLpR9Sa+\n\tTODDp2/bqaqeKO5A27t2I9QtRvfRWAxBtB/SmHywuAoimx4QGLCqnrC4U83t6q/EDC\n\tHJU2T3jUWlrwru43b2Cd8Tjtx/No8jyJOnq7OjxE="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"hDhQnl4d\"; dkim-atps=neutral","Date":"Fri, 6 May 2022 22:52:57 +0300","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<YnV8mQDNCfeGsjbI@pendragon.ideasonboard.com>","References":"<20220506145414.99039-1-tomi.valkeinen@ideasonboard.com>\n\t<20220506145414.99039-5-tomi.valkeinen@ideasonboard.com>\n\t<YnVY/KR03cB/TPri@pendragon.ideasonboard.com>\n\t<65545abe-b56f-c3c6-9f9f-0cc6567c8866@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<65545abe-b56f-c3c6-9f9f-0cc6567c8866@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v8 4/7] Add Python bindings","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>"}}]