[{"id":22680,"web_url":"https://patchwork.libcamera.org/comment/22680/","msgid":"<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","date":"2022-04-12T17:49:53","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":3,"url":"https://patchwork.libcamera.org/api/people/3/","name":"Jacopo Mondi","email":"jacopo@jmondi.org"},"content":"Hi Tomi,\n   I've been using the bindings in the last days, they're nice to\nwork! Great job!\n\nOnce the question about a Request belonging to a Camera is\nclarified I think we should merge these soon, even if incomplete, to\nbuild on top.\n\nMy understanding of python is very limited so I have just a few minor\ncomments and one larger question about controls.\n\nOn Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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 |  10 +\n>  src/py/libcamera/meson.build |  43 ++++\n>  src/py/libcamera/pyenums.cpp |  53 ++++\n>  src/py/libcamera/pymain.cpp  | 453 +++++++++++++++++++++++++++++++++++\n>  src/py/meson.build           |   1 +\n>  subprojects/.gitignore       |   3 +-\n>  subprojects/pybind11.wrap    |   6 +\n>  10 files changed, 575 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/pybind11.wrap\n>\n> diff --git a/meson.build b/meson.build\n> index 29d8542d..ff6c2ad6 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -179,6 +179,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..b30bf33a\n> --- /dev/null\n> +++ b/src/py/libcamera/__init__.py\n> @@ -0,0 +1,10 @@\n> +# SPDX-License-Identifier: LGPL-2.1-or-later\n> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> +\n> +from ._libcamera import *\n> +import mmap\n> +\n> +def __FrameBuffer__mmap(self, plane):\n> +\treturn mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)\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..82388efb\n> --- /dev/null\n> +++ b/src/py/libcamera/meson.build\n> @@ -0,0 +1,43 @@\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> +    'pymain.cpp',\n> +    'pyenums.cpp',\n> +])\n> +\n> +pycamera_deps = [\n> +    libcamera_public,\n> +    py3_dep,\n> +    pybind11_dep,\n> +]\n> +\n> +pycamera_args = ['-fvisibility=hidden']\n> +pycamera_args += ['-Wno-shadow']\n> +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']\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> +\n> +install_data(['__init__.py'], install_dir : destdir)\n> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n> new file mode 100644\n> index 00000000..af6151c8\n> --- /dev/null\n> +++ b/src/py/libcamera/pyenums.cpp\n> @@ -0,0 +1,53 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> + *\n> + * Python bindings\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_<CameraConfiguration::Status>(m, \"ConfigurationStatus\")\n> +\t\t.value(\"Valid\", CameraConfiguration::Valid)\n> +\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\n> +\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\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_<Request::Status>(m, \"RequestStatus\")\n> +\t\t.value(\"Pending\", Request::RequestPending)\n> +\t\t.value(\"Complete\", Request::RequestComplete)\n> +\t\t.value(\"Cancelled\", Request::RequestCancelled);\n> +\n> +\tpy::enum_<FrameMetadata::Status>(m, \"FrameMetadataStatus\")\n> +\t\t.value(\"Success\", FrameMetadata::FrameSuccess)\n> +\t\t.value(\"Error\", FrameMetadata::FrameError)\n> +\t\t.value(\"Cancelled\", FrameMetadata::FrameCancelled);\n> +\n> +\tpy::enum_<Request::ReuseFlag>(m, \"ReuseFlag\")\n> +\t\t.value(\"Default\", Request::ReuseFlag::Default)\n> +\t\t.value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\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..7701da40\n> --- /dev/null\n> +++ b/src/py/libcamera/pymain.cpp\n> @@ -0,0 +1,453 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> + *\n> + * Python bindings\n> + */\n> +\n> +/*\n> + * To generate pylibcamera stubs:\n> + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\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/smart_holder.h>\n> +#include <pybind11/functional.h>\n> +#include <pybind11/stl.h>\n> +#include <pybind11/stl_bind.h>\n> +\n> +namespace py = pybind11;\n> +\n> +using namespace std;\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<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 runtime_error(\"Unsupported ControlValue type\");\n> +\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 ControlValue(ob.cast<uint8_t>());\n> +\tcase ControlTypeInteger32:\n> +\t\treturn ControlValue(ob.cast<int32_t>());\n> +\tcase ControlTypeInteger64:\n> +\t\treturn ControlValue(ob.cast<int64_t>());\n> +\tcase ControlTypeFloat:\n> +\t\treturn ControlValue(ob.cast<float>());\n> +\tcase ControlTypeString:\n> +\t\treturn ControlValue(ob.cast<string>());\n> +\tcase ControlTypeRectangle:\n> +\tcase ControlTypeSize:\n> +\tcase ControlTypeNone:\n> +\tdefault:\n> +\t\tthrow runtime_error(\"Control type not implemented\");\n> +\t}\n> +}\n> +\n> +static weak_ptr<CameraManager> g_camera_manager;\n> +static int g_eventfd;\n> +static mutex g_reqlist_mutex;\n> +static vector<Request *> g_reqlist;\n> +\n> +static void handleRequestCompleted(Request *req)\n> +{\n> +\t{\n> +\t\tlock_guard guard(g_reqlist_mutex);\n> +\t\tg_reqlist.push_back(req);\n> +\t}\n> +\n> +\tuint64_t v = 1;\n> +\twrite(g_eventfd, &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 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 pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n> +\n> +\t/* Global functions */\n> +\tm.def(\"logSetLevel\", &logSetLevel);\n> +\n> +\t/* Classes */\n> +\tpyCameraManager\n> +\t\t.def_static(\"singleton\", []() {\n> +\t\t\tshared_ptr<CameraManager> cm = g_camera_manager.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(), \"Failed to create eventfd\");\n> +\n> +\t\t\tcm = shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n> +\t\t\t\tclose(g_eventfd);\n> +\t\t\t\tg_eventfd = -1;\n> +\t\t\t\tdelete p;\n> +\t\t\t});\n> +\n> +\t\t\tg_eventfd = fd;\n> +\t\t\tg_camera_manager = 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(), \"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 g_eventfd;\n> +\t\t})\n> +\n> +\t\t.def(\"getReadyRequests\", [](CameraManager &) {\n> +\t\t\tvector<Request *> v;\n> +\n> +\t\t\t{\n> +\t\t\t\tlock_guard guard(g_reqlist_mutex);\n> +\t\t\t\tswap(v, g_reqlist);\n> +\t\t\t}\n> +\n> +\t\t\tvector<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::queueRequest() */\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 string &>(&CameraManager::get), py::keep_alive<0, 1>())\n> +\n> +\t\t.def(\"find\", [](CameraManager &self, string str) {\n> +\t\t\tstd::transform(str.begin(), str.end(), str.begin(), ::tolower);\n> +\n> +\t\t\tfor (auto c : self.cameras()) {\n> +\t\t\t\tstring id = c->id();\n> +\n> +\t\t\t\tstd::transform(id.begin(), id.end(), id.begin(), ::tolower);\n> +\n> +\t\t\t\tif (id.find(str) != string::npos)\n> +\t\t\t\t\treturn c;\n> +\t\t\t}\n> +\n> +\t\t\treturn shared_ptr<Camera>();\n> +\t\t}, 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) {\n> +\t\t\tself.requestCompleted.connect(handleRequestCompleted);\n> +\n> +\t\t\tint ret = self.start();\n> +\t\t\tif (ret)\n> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n> +\n> +\t\t\treturn ret;\n> +\t\t})\n> +\n> +\t\t.def(\"stop\", [](Camera &self) {\n> +\t\t\tint ret = self.stop();\n> +\t\t\tif (!ret)\n> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n> +\n> +\t\t\treturn ret;\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(\"generateConfiguration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n> +\t\t.def(\"configure\", &Camera::configure)\n> +\n> +\t\t.def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n> +\n> +\t\t.def(\"queueRequest\", [](Camera &self, Request *req) {\n> +\t\t\tpy::object py_req = py::cast(req);\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 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> +\t\t\tif (it == controls.end())\n> +\t\t\t\tthrow runtime_error(\"Control not found\");\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()] = 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), 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> +\n> +\tpyStreamConfiguration\n> +\t\t.def(\"toString\", &StreamConfiguration::toString)\n> +\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream, py::return_value_policy::reference_internal)\n> +\t\t.def_property(\n> +\t\t\t\"size\",\n> +\t\t\t[](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },\n> +\t\t\t[](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })\n> +\t\t.def_property(\n> +\t\t\t\"pixelFormat\",\n> +\t\t\t[](StreamConfiguration &self) { return self.pixelFormat.toString(); },\n> +\t\t\t[](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })\n> +\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\n> +\t\t.def_readwrite(\"frameSize\", &StreamConfiguration::frameSize)\n> +\t\t.def_readwrite(\"bufferCount\", &StreamConfiguration::bufferCount)\n> +\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats, py::return_value_policy::reference_internal);\n> +\t;\n> +\n> +\tpyStreamFormats\n> +\t\t.def_property_readonly(\"pixelFormats\", [](StreamFormats &self) {\n> +\t\t\tvector<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 string &pixelFormat) {\n> +\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\n> +\t\t\tvector<tuple<uint32_t, uint32_t>> fmts;\n> +\t\t\tfor (const auto &s : self.sizes(fmt))\n> +\t\t\t\tfmts.push_back(make_tuple(s.width, s.height));\n> +\t\t\treturn fmts;\n> +\t\t})\n> +\t\t.def(\"range\", [](StreamFormats &self, const 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(make_tuple(range.hStep, range.vStep),\n> +\t\t\t\t\t  make_tuple(range.min.width, range.min.height),\n> +\t\t\t\t\t  make_tuple(range.max.width, range.max.height));\n> +\t\t});\n> +\n> +\tpyFrameBufferAllocator\n> +\t\t.def(py::init<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([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {\n> +\t\t\tvector<FrameBuffer::Plane> v;\n> +\t\t\tfor (const auto &t : planes)\n> +\t\t\t\tv.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, 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(\"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_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/* Fence is not supported, so we cannot expose addBuffer() directly */\n> +\t\t.def(\"addBuffer\", [](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(\"hasPendingBuffers\", &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\nI see a mixture of camel case (\"addBuffer\") and snake case\n(\"set_controls\"). If there's a preferred coding style for Python\nshould we uniform on it ?\n\nMinor comments apart, there's a thing which is missing:\nsupport for setting controls that accept a Span<> of values.\n\nI tried several solutions and I got the following to compile\n\n        .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n                py::bytearray bytes = py::bytearray(values);\n                py::buffer_info info(py::buffer(bytes).request());\n\n                const int *data = reinterpret_cast<const int *>(info.ptr);\n                size_t length = static_cast<size_t>(info.size);\n\n                self.controls().set(id.id(), Span<const int>{data, length});\n         })\n\n(All types in casts<> should depend on id.type(), but that's for later).\n\nUnfortunately, while length is correct, the values I access with\n'data' seems invalid, which makes me think\n\n                py::bytearray bytes = py::bytearray(values);\n\nDosn't do what I think it does, or maybe Tuple in Python are simply\nnot backed by a contigous memory buffer , hence there's no way to wrap\ntheir memory in a Span<>).\n\nPlease note I also tried to instrument:\n\n        .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n\nRelying on pybind11 type casting, and it seems to work, but I see two issues:\n\n1) Converting from python to C++ types goes through a copy.\nPerformances reasons apart, the vector lifetime is limited to the\nfunction scope. This isn't be a problem for now, as control\nvalues are copied in the Request's control list, but that would\nprevent passing controls values as pointers, if a control transports a\nlarge chunk of data (ie gamma tables). Not sure we'll ever want this\n(I don't think so as it won't play well with serialization between the\nIPA and the pipeline handler) but you never know.\n\n2) my understanding is that python does not support methods signature\noverloading, hence we would have\n\n        .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n        .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n        ....\n\nthere are surely smart ways to handle this, but in my quick experiment\nI haven't found one yet :)\n\nDavid: Is the issue with Span<> controls addressed by picamera2 ?\n\nThanks, I hope we can merge this soon!\n\n\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/* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */\n> +\t\t.def(\"reuse\", [](Request &self) { self.reuse(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/* temporary helper, to be removed */\n> +\t\t.def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n> +\t\t\tvector<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> 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..757bb072 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/pybind11.wrap b/subprojects/pybind11.wrap\n> new file mode 100644\n> index 00000000..ebf942ff\n> --- /dev/null\n> +++ b/subprojects/pybind11.wrap\n> @@ -0,0 +1,6 @@\n> +[wrap-git]\n> +url = https://github.com/tomba/pybind11.git\n> +revision = smart_holder\n> +\n> +[provide]\n> +pybind11 = pybind11_dep\n> --\n> 2.25.1\n>","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 99445C0F1B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 12 Apr 2022 17:49:58 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id BDE7D65640;\n\tTue, 12 Apr 2022 19:49:57 +0200 (CEST)","from relay4-d.mail.gandi.net (relay4-d.mail.gandi.net\n\t[217.70.183.196])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id CC973604B7\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 12 Apr 2022 19:49:56 +0200 (CEST)","(Authenticated sender: jacopo@jmondi.org)\n\tby mail.gandi.net (Postfix) with ESMTPSA id 92FD7E0006;\n\tTue, 12 Apr 2022 17:49:55 +0000 (UTC)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649785797;\n\tbh=tMqXZZyDLFER3K4rEbrx3F0u42aXel75qP1gCCl3rDI=;\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=FYE5cAzNp8Eoe4Q191o2jwvReL/iTw4Doo9kcgfNfKpmxLozJgGLxcL6vi6NLieXN\n\tyHur4R2Y95WaULD6D39OMeD8LOQEclFEo5psmuvBFiRcZ8IFJU9uGgdBQRmli5t7tq\n\t5JeUDmzyMso0HfctNsPtCjPjQWjDto6ZDZs/D+EOxPFn9U8SDf4YTncfoLhwN3yvBm\n\tmWOi4XbMlXV2ImASciuPrNVJISB3zAXD7gwJfKBrij8uvtfWMhtNJy7IUQ0HmFqTPy\n\tZaLBkjhGPbYMQo72FXMMsK3ND9mMnJlHSGbcA8GPXeUhbNMFtGM4Xs7NLjraQ4iH4M\n\tg3JIUZFjJfFPA==","Date":"Tue, 12 Apr 2022 19:49:53 +0200","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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":"Jacopo Mondi via libcamera-devel <libcamera-devel@lists.libcamera.org>","Reply-To":"Jacopo Mondi <jacopo@jmondi.org>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":22681,"web_url":"https://patchwork.libcamera.org/comment/22681/","msgid":"<CAHW6GYJ3S2kT-kS=RZWPne0=L+3DSafc74qPswaR+3RH-O=1+w@mail.gmail.com>","date":"2022-04-12T18:44:19","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":42,"url":"https://patchwork.libcamera.org/api/people/42/","name":"David Plowman","email":"david.plowman@raspberrypi.com"},"content":"Hi Jacopo, everyone\n\nOn our \"picamera2\" branch of libcamera we have all Tomi's work, plus\nanother 8 commits on top which implement the features that I was\nmissing (https://github.com/raspberrypi/libcamera/commits/picamera2).\nMost of it is to do with Transforms, ColorSpaces, array controls and\none or two other things. I was planning to start submitting some of\nthese once the bulk of the work has been merged. Most of it shouldn't\nbe too surprising, though there's another\n\"multi-plane-isn't-really-multi-plane\" thing (just like we had in the\nC++ world some time ago) which needs a proper solution.\n\nDavid\n\nOn Tue, 12 Apr 2022 at 18:49, Jacopo Mondi <jacopo@jmondi.org> wrote:\n>\n> Hi Tomi,\n>    I've been using the bindings in the last days, they're nice to\n> work! Great job!\n>\n> Once the question about a Request belonging to a Camera is\n> clarified I think we should merge these soon, even if incomplete, to\n> build on top.\n>\n> My understanding of python is very limited so I have just a few minor\n> comments and one larger question about controls.\n>\n> On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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 |  10 +\n> >  src/py/libcamera/meson.build |  43 ++++\n> >  src/py/libcamera/pyenums.cpp |  53 ++++\n> >  src/py/libcamera/pymain.cpp  | 453 +++++++++++++++++++++++++++++++++++\n> >  src/py/meson.build           |   1 +\n> >  subprojects/.gitignore       |   3 +-\n> >  subprojects/pybind11.wrap    |   6 +\n> >  10 files changed, 575 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/pybind11.wrap\n> >\n> > diff --git a/meson.build b/meson.build\n> > index 29d8542d..ff6c2ad6 100644\n> > --- a/meson.build\n> > +++ b/meson.build\n> > @@ -179,6 +179,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..b30bf33a\n> > --- /dev/null\n> > +++ b/src/py/libcamera/__init__.py\n> > @@ -0,0 +1,10 @@\n> > +# SPDX-License-Identifier: LGPL-2.1-or-later\n> > +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> > +\n> > +from ._libcamera import *\n> > +import mmap\n> > +\n> > +def __FrameBuffer__mmap(self, plane):\n> > +     return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)\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..82388efb\n> > --- /dev/null\n> > +++ b/src/py/libcamera/meson.build\n> > @@ -0,0 +1,43 @@\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> > +    'pymain.cpp',\n> > +    'pyenums.cpp',\n> > +])\n> > +\n> > +pycamera_deps = [\n> > +    libcamera_public,\n> > +    py3_dep,\n> > +    pybind11_dep,\n> > +]\n> > +\n> > +pycamera_args = ['-fvisibility=hidden']\n> > +pycamera_args += ['-Wno-shadow']\n> > +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']\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> > +\n> > +install_data(['__init__.py'], install_dir : destdir)\n> > diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n> > new file mode 100644\n> > index 00000000..af6151c8\n> > --- /dev/null\n> > +++ b/src/py/libcamera/pyenums.cpp\n> > @@ -0,0 +1,53 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> > + *\n> > + * Python bindings\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> > +     py::enum_<CameraConfiguration::Status>(m, \"ConfigurationStatus\")\n> > +             .value(\"Valid\", CameraConfiguration::Valid)\n> > +             .value(\"Adjusted\", CameraConfiguration::Adjusted)\n> > +             .value(\"Invalid\", CameraConfiguration::Invalid);\n> > +\n> > +     py::enum_<StreamRole>(m, \"StreamRole\")\n> > +             .value(\"StillCapture\", StreamRole::StillCapture)\n> > +             .value(\"Raw\", StreamRole::Raw)\n> > +             .value(\"VideoRecording\", StreamRole::VideoRecording)\n> > +             .value(\"Viewfinder\", StreamRole::Viewfinder);\n> > +\n> > +     py::enum_<Request::Status>(m, \"RequestStatus\")\n> > +             .value(\"Pending\", Request::RequestPending)\n> > +             .value(\"Complete\", Request::RequestComplete)\n> > +             .value(\"Cancelled\", Request::RequestCancelled);\n> > +\n> > +     py::enum_<FrameMetadata::Status>(m, \"FrameMetadataStatus\")\n> > +             .value(\"Success\", FrameMetadata::FrameSuccess)\n> > +             .value(\"Error\", FrameMetadata::FrameError)\n> > +             .value(\"Cancelled\", FrameMetadata::FrameCancelled);\n> > +\n> > +     py::enum_<Request::ReuseFlag>(m, \"ReuseFlag\")\n> > +             .value(\"Default\", Request::ReuseFlag::Default)\n> > +             .value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\n> > +\n> > +     py::enum_<ControlType>(m, \"ControlType\")\n> > +             .value(\"None\", ControlType::ControlTypeNone)\n> > +             .value(\"Bool\", ControlType::ControlTypeBool)\n> > +             .value(\"Byte\", ControlType::ControlTypeByte)\n> > +             .value(\"Integer32\", ControlType::ControlTypeInteger32)\n> > +             .value(\"Integer64\", ControlType::ControlTypeInteger64)\n> > +             .value(\"Float\", ControlType::ControlTypeFloat)\n> > +             .value(\"String\", ControlType::ControlTypeString)\n> > +             .value(\"Rectangle\", ControlType::ControlTypeRectangle)\n> > +             .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..7701da40\n> > --- /dev/null\n> > +++ b/src/py/libcamera/pymain.cpp\n> > @@ -0,0 +1,453 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> > + *\n> > + * Python bindings\n> > + */\n> > +\n> > +/*\n> > + * To generate pylibcamera stubs:\n> > + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\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/smart_holder.h>\n> > +#include <pybind11/functional.h>\n> > +#include <pybind11/stl.h>\n> > +#include <pybind11/stl_bind.h>\n> > +\n> > +namespace py = pybind11;\n> > +\n> > +using namespace std;\n> > +using namespace libcamera;\n> > +\n> > +template<typename T>\n> > +static py::object ValueOrTuple(const ControlValue &cv)\n> > +{\n> > +     if (cv.isArray()) {\n> > +             const T *v = reinterpret_cast<const T *>(cv.data().data());\n> > +             auto t = py::tuple(cv.numElements());\n> > +\n> > +             for (size_t i = 0; i < cv.numElements(); ++i)\n> > +                     t[i] = v[i];\n> > +\n> > +             return t;\n> > +     }\n> > +\n> > +     return py::cast(cv.get<T>());\n> > +}\n> > +\n> > +static py::object ControlValueToPy(const ControlValue &cv)\n> > +{\n> > +     switch (cv.type()) {\n> > +     case ControlTypeBool:\n> > +             return ValueOrTuple<bool>(cv);\n> > +     case ControlTypeByte:\n> > +             return ValueOrTuple<uint8_t>(cv);\n> > +     case ControlTypeInteger32:\n> > +             return ValueOrTuple<int32_t>(cv);\n> > +     case ControlTypeInteger64:\n> > +             return ValueOrTuple<int64_t>(cv);\n> > +     case ControlTypeFloat:\n> > +             return ValueOrTuple<float>(cv);\n> > +     case ControlTypeString:\n> > +             return py::cast(cv.get<string>());\n> > +     case ControlTypeRectangle: {\n> > +             const Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n> > +             return py::make_tuple(v->x, v->y, v->width, v->height);\n> > +     }\n> > +     case ControlTypeSize: {\n> > +             const Size *v = reinterpret_cast<const Size *>(cv.data().data());\n> > +             return py::make_tuple(v->width, v->height);\n> > +     }\n> > +     case ControlTypeNone:\n> > +     default:\n> > +             throw runtime_error(\"Unsupported ControlValue type\");\n> > +     }\n> > +}\n> > +\n> > +static ControlValue PyToControlValue(const py::object &ob, ControlType type)\n> > +{\n> > +     switch (type) {\n> > +     case ControlTypeBool:\n> > +             return ControlValue(ob.cast<bool>());\n> > +     case ControlTypeByte:\n> > +             return ControlValue(ob.cast<uint8_t>());\n> > +     case ControlTypeInteger32:\n> > +             return ControlValue(ob.cast<int32_t>());\n> > +     case ControlTypeInteger64:\n> > +             return ControlValue(ob.cast<int64_t>());\n> > +     case ControlTypeFloat:\n> > +             return ControlValue(ob.cast<float>());\n> > +     case ControlTypeString:\n> > +             return ControlValue(ob.cast<string>());\n> > +     case ControlTypeRectangle:\n> > +     case ControlTypeSize:\n> > +     case ControlTypeNone:\n> > +     default:\n> > +             throw runtime_error(\"Control type not implemented\");\n> > +     }\n> > +}\n> > +\n> > +static weak_ptr<CameraManager> g_camera_manager;\n> > +static int g_eventfd;\n> > +static mutex g_reqlist_mutex;\n> > +static vector<Request *> g_reqlist;\n> > +\n> > +static void handleRequestCompleted(Request *req)\n> > +{\n> > +     {\n> > +             lock_guard guard(g_reqlist_mutex);\n> > +             g_reqlist.push_back(req);\n> > +     }\n> > +\n> > +     uint64_t v = 1;\n> > +     write(g_eventfd, &v, 8);\n> > +}\n> > +\n> > +void init_pyenums(py::module& m);\n> > +\n> > +PYBIND11_MODULE(_libcamera, m)\n> > +{\n> > +     init_pyenums(m);\n> > +\n> > +     /* Forward declarations */\n> > +\n> > +     /*\n> > +      * We need to declare all the classes here so that Python docstrings\n> > +      * can be generated correctly.\n> > +      * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n> > +      */\n> > +\n> > +     auto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n> > +     auto pyCamera = py::class_<Camera>(m, \"Camera\");\n> > +     auto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n> > +     auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n> > +     auto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n> > +     auto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n> > +     auto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n> > +     auto pyStream = py::class_<Stream>(m, \"Stream\");\n> > +     auto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n> > +     auto pyRequest = py::class_<Request>(m, \"Request\");\n> > +     auto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n> > +\n> > +     /* Global functions */\n> > +     m.def(\"logSetLevel\", &logSetLevel);\n> > +\n> > +     /* Classes */\n> > +     pyCameraManager\n> > +             .def_static(\"singleton\", []() {\n> > +                     shared_ptr<CameraManager> cm = g_camera_manager.lock();\n> > +                     if (cm)\n> > +                             return cm;\n> > +\n> > +                     int fd = eventfd(0, 0);\n> > +                     if (fd == -1)\n> > +                             throw std::system_error(errno, std::generic_category(), \"Failed to create eventfd\");\n> > +\n> > +                     cm = shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n> > +                             close(g_eventfd);\n> > +                             g_eventfd = -1;\n> > +                             delete p;\n> > +                     });\n> > +\n> > +                     g_eventfd = fd;\n> > +                     g_camera_manager = cm;\n> > +\n> > +                     int ret = cm->start();\n> > +                     if (ret)\n> > +                             throw std::system_error(-ret, std::generic_category(), \"Failed to start CameraManager\");\n> > +\n> > +                     return cm;\n> > +             })\n> > +\n> > +             .def_property_readonly(\"version\", &CameraManager::version)\n> > +\n> > +             .def_property_readonly(\"efd\", [](CameraManager &) {\n> > +                     return g_eventfd;\n> > +             })\n> > +\n> > +             .def(\"getReadyRequests\", [](CameraManager &) {\n> > +                     vector<Request *> v;\n> > +\n> > +                     {\n> > +                             lock_guard guard(g_reqlist_mutex);\n> > +                             swap(v, g_reqlist);\n> > +                     }\n> > +\n> > +                     vector<py::object> ret;\n> > +\n> > +                     for (Request *req : v) {\n> > +                             py::object o = py::cast(req);\n> > +                             /* decrease the ref increased in Camera::queueRequest() */\n> > +                             o.dec_ref();\n> > +                             ret.push_back(o);\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def(\"get\", py::overload_cast<const string &>(&CameraManager::get), py::keep_alive<0, 1>())\n> > +\n> > +             .def(\"find\", [](CameraManager &self, string str) {\n> > +                     std::transform(str.begin(), str.end(), str.begin(), ::tolower);\n> > +\n> > +                     for (auto c : self.cameras()) {\n> > +                             string id = c->id();\n> > +\n> > +                             std::transform(id.begin(), id.end(), id.begin(), ::tolower);\n> > +\n> > +                             if (id.find(str) != string::npos)\n> > +                                     return c;\n> > +                     }\n> > +\n> > +                     return shared_ptr<Camera>();\n> > +             }, py::keep_alive<0, 1>())\n> > +\n> > +             /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n> > +             .def_property_readonly(\"cameras\", [](CameraManager &self) {\n> > +                     py::list l;\n> > +\n> > +                     for (auto &c : self.cameras()) {\n> > +                             py::object py_cm = py::cast(self);\n> > +                             py::object py_cam = py::cast(c);\n> > +                             py::detail::keep_alive_impl(py_cam, py_cm);\n> > +                             l.append(py_cam);\n> > +                     }\n> > +\n> > +                     return l;\n> > +             });\n> > +\n> > +     pyCamera\n> > +             .def_property_readonly(\"id\", &Camera::id)\n> > +             .def(\"acquire\", &Camera::acquire)\n> > +             .def(\"release\", &Camera::release)\n> > +             .def(\"start\", [](Camera &self) {\n> > +                     self.requestCompleted.connect(handleRequestCompleted);\n> > +\n> > +                     int ret = self.start();\n> > +                     if (ret)\n> > +                             self.requestCompleted.disconnect(handleRequestCompleted);\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def(\"stop\", [](Camera &self) {\n> > +                     int ret = self.stop();\n> > +                     if (!ret)\n> > +                             self.requestCompleted.disconnect(handleRequestCompleted);\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def(\"__repr__\", [](Camera &self) {\n> > +                     return \"<libcamera.Camera '\" + self.id() + \"'>\";\n> > +             })\n> > +\n> > +             /* Keep the camera alive, as StreamConfiguration contains a Stream* */\n> > +             .def(\"generateConfiguration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n> > +             .def(\"configure\", &Camera::configure)\n> > +\n> > +             .def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n> > +\n> > +             .def(\"queueRequest\", [](Camera &self, Request *req) {\n> > +                     py::object py_req = py::cast(req);\n> > +\n> > +                     py_req.inc_ref();\n> > +\n> > +                     int ret = self.queueRequest(req);\n> > +                     if (ret)\n> > +                             py_req.dec_ref();\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def_property_readonly(\"streams\", [](Camera &self) {\n> > +                     py::set set;\n> > +                     for (auto &s : self.streams()) {\n> > +                             py::object py_self = py::cast(self);\n> > +                             py::object py_s = py::cast(s);\n> > +                             py::detail::keep_alive_impl(py_s, py_self);\n> > +                             set.add(py_s);\n> > +                     }\n> > +                     return set;\n> > +             })\n> > +\n> > +             .def(\"find_control\", [](Camera &self, const string &name) {\n> > +                     const auto &controls = self.controls();\n> > +\n> > +                     auto it = find_if(controls.begin(), controls.end(),\n> > +                                       [&name](const auto &kvp) { return kvp.first->name() == name; });\n> > +\n> > +                     if (it == controls.end())\n> > +                             throw runtime_error(\"Control not found\");\n> > +\n> > +                     return it->first;\n> > +             }, py::return_value_policy::reference_internal)\n> > +\n> > +             .def_property_readonly(\"controls\", [](Camera &self) {\n> > +                     py::dict ret;\n> > +\n> > +                     for (const auto &[id, ci] : self.controls()) {\n> > +                             ret[id->name().c_str()] = make_tuple<py::object>(ControlValueToPy(ci.min()),\n> > +                                                                              ControlValueToPy(ci.max()),\n> > +                                                                              ControlValueToPy(ci.def()));\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def_property_readonly(\"properties\", [](Camera &self) {\n> > +                     py::dict ret;\n> > +\n> > +                     for (const auto &[key, cv] : self.properties()) {\n> > +                             const ControlId *id = properties::properties.at(key);\n> > +                             py::object ob = ControlValueToPy(cv);\n> > +\n> > +                             ret[id->name().c_str()] = ob;\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             });\n> > +\n> > +     pyCameraConfiguration\n> > +             .def(\"__iter__\", [](CameraConfiguration &self) {\n> > +                     return py::make_iterator<py::return_value_policy::reference_internal>(self);\n> > +             }, py::keep_alive<0, 1>())\n> > +             .def(\"__len__\", [](CameraConfiguration &self) {\n> > +                     return self.size();\n> > +             })\n> > +             .def(\"validate\", &CameraConfiguration::validate)\n> > +             .def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at), py::return_value_policy::reference_internal)\n> > +             .def_property_readonly(\"size\", &CameraConfiguration::size)\n> > +             .def_property_readonly(\"empty\", &CameraConfiguration::empty);\n> > +\n> > +     pyStreamConfiguration\n> > +             .def(\"toString\", &StreamConfiguration::toString)\n> > +             .def_property_readonly(\"stream\", &StreamConfiguration::stream, py::return_value_policy::reference_internal)\n> > +             .def_property(\n> > +                     \"size\",\n> > +                     [](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },\n> > +                     [](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })\n> > +             .def_property(\n> > +                     \"pixelFormat\",\n> > +                     [](StreamConfiguration &self) { return self.pixelFormat.toString(); },\n> > +                     [](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })\n> > +             .def_readwrite(\"stride\", &StreamConfiguration::stride)\n> > +             .def_readwrite(\"frameSize\", &StreamConfiguration::frameSize)\n> > +             .def_readwrite(\"bufferCount\", &StreamConfiguration::bufferCount)\n> > +             .def_property_readonly(\"formats\", &StreamConfiguration::formats, py::return_value_policy::reference_internal);\n> > +     ;\n> > +\n> > +     pyStreamFormats\n> > +             .def_property_readonly(\"pixelFormats\", [](StreamFormats &self) {\n> > +                     vector<string> fmts;\n> > +                     for (auto &fmt : self.pixelformats())\n> > +                             fmts.push_back(fmt.toString());\n> > +                     return fmts;\n> > +             })\n> > +             .def(\"sizes\", [](StreamFormats &self, const string &pixelFormat) {\n> > +                     auto fmt = PixelFormat::fromString(pixelFormat);\n> > +                     vector<tuple<uint32_t, uint32_t>> fmts;\n> > +                     for (const auto &s : self.sizes(fmt))\n> > +                             fmts.push_back(make_tuple(s.width, s.height));\n> > +                     return fmts;\n> > +             })\n> > +             .def(\"range\", [](StreamFormats &self, const string &pixelFormat) {\n> > +                     auto fmt = PixelFormat::fromString(pixelFormat);\n> > +                     const auto &range = self.range(fmt);\n> > +                     return make_tuple(make_tuple(range.hStep, range.vStep),\n> > +                                       make_tuple(range.min.width, range.min.height),\n> > +                                       make_tuple(range.max.width, range.max.height));\n> > +             });\n> > +\n> > +     pyFrameBufferAllocator\n> > +             .def(py::init<shared_ptr<Camera>>(), py::keep_alive<1, 2>())\n> > +             .def(\"allocate\", &FrameBufferAllocator::allocate)\n> > +             .def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\n> > +             /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */\n> > +             .def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\n> > +                     py::object py_self = py::cast(self);\n> > +                     py::list l;\n> > +                     for (auto &ub : self.buffers(stream)) {\n> > +                             py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);\n> > +                             l.append(py_buf);\n> > +                     }\n> > +                     return l;\n> > +             });\n> > +\n> > +     pyFrameBuffer\n> > +             /* TODO: implement FrameBuffer::Plane properly */\n> > +             .def(py::init([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {\n> > +                     vector<FrameBuffer::Plane> v;\n> > +                     for (const auto &t : planes)\n> > +                             v.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, get<1>(t) });\n> > +                     return new FrameBuffer(v, cookie);\n> > +             }))\n> > +             .def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\n> > +             .def(\"length\", [](FrameBuffer &self, uint32_t idx) {\n> > +                     const FrameBuffer::Plane &plane = self.planes()[idx];\n> > +                     return plane.length;\n> > +             })\n> > +             .def(\"fd\", [](FrameBuffer &self, uint32_t idx) {\n> > +                     const FrameBuffer::Plane &plane = self.planes()[idx];\n> > +                     return plane.fd.get();\n> > +             })\n> > +             .def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\n> > +\n> > +     pyStream\n> > +             .def_property_readonly(\"configuration\", &Stream::configuration);\n> > +\n> > +     pyControlId\n> > +             .def_property_readonly(\"id\", &ControlId::id)\n> > +             .def_property_readonly(\"name\", &ControlId::name)\n> > +             .def_property_readonly(\"type\", &ControlId::type);\n> > +\n> > +     pyRequest\n> > +             /* Fence is not supported, so we cannot expose addBuffer() directly */\n> > +             .def(\"addBuffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n> > +                     return self.addBuffer(stream, buffer);\n> > +             }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n> > +             .def_property_readonly(\"status\", &Request::status)\n> > +             .def_property_readonly(\"buffers\", &Request::buffers)\n> > +             .def_property_readonly(\"cookie\", &Request::cookie)\n> > +             .def_property_readonly(\"hasPendingBuffers\", &Request::hasPendingBuffers)\n> > +             .def(\"set_control\", [](Request &self, ControlId& id, py::object value) {\n> > +                     self.controls().set(id.id(), PyToControlValue(value, id.type()));\n> > +             })\n>\n> I see a mixture of camel case (\"addBuffer\") and snake case\n> (\"set_controls\"). If there's a preferred coding style for Python\n> should we uniform on it ?\n>\n> Minor comments apart, there's a thing which is missing:\n> support for setting controls that accept a Span<> of values.\n>\n> I tried several solutions and I got the following to compile\n>\n>         .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n>                 py::bytearray bytes = py::bytearray(values);\n>                 py::buffer_info info(py::buffer(bytes).request());\n>\n>                 const int *data = reinterpret_cast<const int *>(info.ptr);\n>                 size_t length = static_cast<size_t>(info.size);\n>\n>                 self.controls().set(id.id(), Span<const int>{data, length});\n>          })\n>\n> (All types in casts<> should depend on id.type(), but that's for later).\n>\n> Unfortunately, while length is correct, the values I access with\n> 'data' seems invalid, which makes me think\n>\n>                 py::bytearray bytes = py::bytearray(values);\n>\n> Dosn't do what I think it does, or maybe Tuple in Python are simply\n> not backed by a contigous memory buffer , hence there's no way to wrap\n> their memory in a Span<>).\n>\n> Please note I also tried to instrument:\n>\n>         .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n>\n> Relying on pybind11 type casting, and it seems to work, but I see two issues:\n>\n> 1) Converting from python to C++ types goes through a copy.\n> Performances reasons apart, the vector lifetime is limited to the\n> function scope. This isn't be a problem for now, as control\n> values are copied in the Request's control list, but that would\n> prevent passing controls values as pointers, if a control transports a\n> large chunk of data (ie gamma tables). Not sure we'll ever want this\n> (I don't think so as it won't play well with serialization between the\n> IPA and the pipeline handler) but you never know.\n>\n> 2) my understanding is that python does not support methods signature\n> overloading, hence we would have\n>\n>         .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n>         .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n>         ....\n>\n> there are surely smart ways to handle this, but in my quick experiment\n> I haven't found one yet :)\n>\n> David: Is the issue with Span<> controls addressed by picamera2 ?\n>\n> Thanks, I hope we can merge this soon!\n>\n>\n> > +             .def_property_readonly(\"metadata\", [](Request &self) {\n> > +                     py::dict ret;\n> > +\n> > +                     for (const auto &[key, cv] : self.metadata()) {\n> > +                             const ControlId *id = controls::controls.at(key);\n> > +                             py::object ob = ControlValueToPy(cv);\n> > +\n> > +                             ret[id->name().c_str()] = ob;\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             })\n> > +             /* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */\n> > +             .def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\n> > +\n> > +     pyFrameMetadata\n> > +             .def_readonly(\"status\", &FrameMetadata::status)\n> > +             .def_readonly(\"sequence\", &FrameMetadata::sequence)\n> > +             .def_readonly(\"timestamp\", &FrameMetadata::timestamp)\n> > +             /* temporary helper, to be removed */\n> > +             .def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n> > +                     vector<unsigned int> v;\n> > +                     v.resize(self.planes().size());\n> > +                     transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\n> > +                     return v;\n> > +             });\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..757bb072 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/pybind11.wrap b/subprojects/pybind11.wrap\n> > new file mode 100644\n> > index 00000000..ebf942ff\n> > --- /dev/null\n> > +++ b/subprojects/pybind11.wrap\n> > @@ -0,0 +1,6 @@\n> > +[wrap-git]\n> > +url = https://github.com/tomba/pybind11.git\n> > +revision = smart_holder\n> > +\n> > +[provide]\n> > +pybind11 = pybind11_dep\n> > --\n> > 2.25.1\n> >","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 931F6C3256\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 12 Apr 2022 18:44:32 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D897B65646;\n\tTue, 12 Apr 2022 20:44:31 +0200 (CEST)","from mail-wm1-x32a.google.com (mail-wm1-x32a.google.com\n\t[IPv6:2a00:1450:4864:20::32a])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id BA63E604B7\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 12 Apr 2022 20:44:30 +0200 (CEST)","by mail-wm1-x32a.google.com with SMTP id p189so12482348wmp.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 12 Apr 2022 11:44:30 -0700 (PDT)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649789071;\n\tbh=ggIc43zT6w4a4YYA2JsDldZojK9/pPZb4tER0VHFOYI=;\n\th=References:In-Reply-To:Date:To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=ZycMq9TFuZUKYBkrsIaJqRNktDB91WB7JulYE+DVc7FzfAmkUidGHzyPxvs7jic5P\n\t5zSWoHZ5w0SBeRFt6dlNuZ3j4NqE0B+Ifmgt2xt6ZHifkny3TIAKxrJaJNIE3rjPyE\n\tDkmN9XGbZbMc+QWAP3+uEuV7s/0m1SNfa/Fak24lTVHoocHvTwLfbQr71kVGspg0IC\n\tBdaum83txBHDmv547PS8rxSuR5ar/6KbZuHbITWzbXdLestt5Di4LI5E1wgTYbLw1V\n\tkZcpiIbNhWeeYH96q8tgBI2Iy8zBpl9nihszYbMyX+cBwyygy+jnrZ5So7+aVnaKb1\n\tEa3A2+BM122WQ==","v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google;\n\th=mime-version:references:in-reply-to:from:date:message-id:subject:to\n\t:cc; bh=pltfVV6CGko9sc49+jHUZ0bZ1RhS2mFepjFUsb6OEYM=;\n\tb=cBt8ro1kuT+4e0p3QZr0V/Mo+/OALzTFYXVHqGTgAMoWGedAWWhsSRkoVLEEOTgyUA\n\tVzRNpdF/HlIts3cP6pmgbBvfgYo35e4tJgHBTKl0uOuI7gVe82RMhMAZiaOiSuZ3POAG\n\tbTVeuF0jCVBa7IXrDkeZm7AQbwJz3noWc9wpcnBAydV4LY7EmhN01X1P2rHJSqWALntp\n\tt5pSrG1NR4BFdyMjwcFlvTq6syAs651YtK7mdW81nhHncKHP1VMqALYtKsUfOleHytID\n\ttTB1rkx2pCUjkfNtNLGYMAui3F18J5LN1n/dJQXWkRnTz/kytmHFoDDlZIc5BhfMxw8h\n\t5YFg=="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"cBt8ro1k\"; dkim-atps=neutral","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20210112;\n\th=x-gm-message-state:mime-version:references:in-reply-to:from:date\n\t:message-id:subject:to:cc;\n\tbh=pltfVV6CGko9sc49+jHUZ0bZ1RhS2mFepjFUsb6OEYM=;\n\tb=MMIynmWnwXmkPBN1a1dTXV0YdFuuCkR27ryUWR82oX3QqJNDBB3sWdm4iEqaFEclXu\n\tNnOvTnDpz4RY1Hwn+YhDwKCHTDmvqb/Cd96D1SwLtyjEGWQXb3dac3PCziuzT0MCqSzX\n\tpGf5ZJDQFWFY7oq+oRPmRO10uh5QSlGh0G9ldqh/UamqK43uw3vVuxxw0ufkOcnTFNDw\n\tt990kpor8J2qNV/EzkINt4khESGpRzokoWIHlxPpfAnS+Aqu7do/EYfiMOIayxOTYrL1\n\tLaZ8oLQU5ZXH5KUTYkiaWHXpI5yrMsYx5dcC08onF0G1G/d+Io15mBRSBHG4nQMeSfPx\n\tY8eA==","X-Gm-Message-State":"AOAM532qukJk9mYdDtof4bB1lAm0QGw0So9F3BcQsLFpZJO1Mzw+0vFI\n\t4/3FTYEJudJc69Sut0yMlSys383EIW2ReVna/OBvJw==","X-Google-Smtp-Source":"ABdhPJyKiXEai2ji8WMKSm7WWvmKYC7cxIn+n8hPMrhgtB0baLVlzZn+KGO3ltWXiMyE2Vi+tylStI3U0h6CyFAUB9E=","X-Received":"by 2002:a1c:f207:0:b0:38e:9aac:df41 with SMTP id\n\ts7-20020a1cf207000000b0038e9aacdf41mr5329437wmc.14.1649789070151;\n\tTue, 12 Apr 2022 11:44:30 -0700 (PDT)","MIME-Version":"1.0","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>\n\t<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","In-Reply-To":"<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","Date":"Tue, 12 Apr 2022 19:44:19 +0100","Message-ID":"<CAHW6GYJ3S2kT-kS=RZWPne0=L+3DSafc74qPswaR+3RH-O=1+w@mail.gmail.com>","To":"Jacopo Mondi <jacopo@jmondi.org>","Content-Type":"text/plain; charset=\"UTF-8\"","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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":"David Plowman via libcamera-devel <libcamera-devel@lists.libcamera.org>","Reply-To":"David Plowman <david.plowman@raspberrypi.com>","Cc":"libcamera devel <libcamera-devel@lists.libcamera.org>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":22682,"web_url":"https://patchwork.libcamera.org/comment/22682/","msgid":"<44b6afd4-23ee-00f6-5c0b-16c3f21dbb6d@ideasonboard.com>","date":"2022-04-13T07:24:45","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":109,"url":"https://patchwork.libcamera.org/api/people/109/","name":"Tomi Valkeinen","email":"tomi.valkeinen@ideasonboard.com"},"content":"On 12/04/2022 20:49, Jacopo Mondi wrote:\n> Hi Tomi,\n>     I've been using the bindings in the last days, they're nice to\n> work! Great job!\n\nNice to hear =).\n\n> Once the question about a Request belonging to a Camera is\n> clarified I think we should merge these soon, even if incomplete, to\n> build on top.\n> \n> My understanding of python is very limited so I have just a few minor\n> comments and one larger question about controls.\n> \n> On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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 |  10 +\n>>   src/py/libcamera/meson.build |  43 ++++\n>>   src/py/libcamera/pyenums.cpp |  53 ++++\n>>   src/py/libcamera/pymain.cpp  | 453 +++++++++++++++++++++++++++++++++++\n>>   src/py/meson.build           |   1 +\n>>   subprojects/.gitignore       |   3 +-\n>>   subprojects/pybind11.wrap    |   6 +\n>>   10 files changed, 575 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/pybind11.wrap\n>>\n>> diff --git a/meson.build b/meson.build\n>> index 29d8542d..ff6c2ad6 100644\n>> --- a/meson.build\n>> +++ b/meson.build\n>> @@ -179,6 +179,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..b30bf33a\n>> --- /dev/null\n>> +++ b/src/py/libcamera/__init__.py\n>> @@ -0,0 +1,10 @@\n>> +# SPDX-License-Identifier: LGPL-2.1-or-later\n>> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> +\n>> +from ._libcamera import *\n>> +import mmap\n>> +\n>> +def __FrameBuffer__mmap(self, plane):\n>> +\treturn mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)\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..82388efb\n>> --- /dev/null\n>> +++ b/src/py/libcamera/meson.build\n>> @@ -0,0 +1,43 @@\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>> +    'pymain.cpp',\n>> +    'pyenums.cpp',\n>> +])\n>> +\n>> +pycamera_deps = [\n>> +    libcamera_public,\n>> +    py3_dep,\n>> +    pybind11_dep,\n>> +]\n>> +\n>> +pycamera_args = ['-fvisibility=hidden']\n>> +pycamera_args += ['-Wno-shadow']\n>> +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']\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>> +\n>> +install_data(['__init__.py'], install_dir : destdir)\n>> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n>> new file mode 100644\n>> index 00000000..af6151c8\n>> --- /dev/null\n>> +++ b/src/py/libcamera/pyenums.cpp\n>> @@ -0,0 +1,53 @@\n>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>> +/*\n>> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> + *\n>> + * Python bindings\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_<CameraConfiguration::Status>(m, \"ConfigurationStatus\")\n>> +\t\t.value(\"Valid\", CameraConfiguration::Valid)\n>> +\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\n>> +\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\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_<Request::Status>(m, \"RequestStatus\")\n>> +\t\t.value(\"Pending\", Request::RequestPending)\n>> +\t\t.value(\"Complete\", Request::RequestComplete)\n>> +\t\t.value(\"Cancelled\", Request::RequestCancelled);\n>> +\n>> +\tpy::enum_<FrameMetadata::Status>(m, \"FrameMetadataStatus\")\n>> +\t\t.value(\"Success\", FrameMetadata::FrameSuccess)\n>> +\t\t.value(\"Error\", FrameMetadata::FrameError)\n>> +\t\t.value(\"Cancelled\", FrameMetadata::FrameCancelled);\n>> +\n>> +\tpy::enum_<Request::ReuseFlag>(m, \"ReuseFlag\")\n>> +\t\t.value(\"Default\", Request::ReuseFlag::Default)\n>> +\t\t.value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\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..7701da40\n>> --- /dev/null\n>> +++ b/src/py/libcamera/pymain.cpp\n>> @@ -0,0 +1,453 @@\n>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>> +/*\n>> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>> + *\n>> + * Python bindings\n>> + */\n>> +\n>> +/*\n>> + * To generate pylibcamera stubs:\n>> + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\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/smart_holder.h>\n>> +#include <pybind11/functional.h>\n>> +#include <pybind11/stl.h>\n>> +#include <pybind11/stl_bind.h>\n>> +\n>> +namespace py = pybind11;\n>> +\n>> +using namespace std;\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<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 runtime_error(\"Unsupported ControlValue type\");\n>> +\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 ControlValue(ob.cast<uint8_t>());\n>> +\tcase ControlTypeInteger32:\n>> +\t\treturn ControlValue(ob.cast<int32_t>());\n>> +\tcase ControlTypeInteger64:\n>> +\t\treturn ControlValue(ob.cast<int64_t>());\n>> +\tcase ControlTypeFloat:\n>> +\t\treturn ControlValue(ob.cast<float>());\n>> +\tcase ControlTypeString:\n>> +\t\treturn ControlValue(ob.cast<string>());\n>> +\tcase ControlTypeRectangle:\n>> +\tcase ControlTypeSize:\n>> +\tcase ControlTypeNone:\n>> +\tdefault:\n>> +\t\tthrow runtime_error(\"Control type not implemented\");\n>> +\t}\n>> +}\n>> +\n>> +static weak_ptr<CameraManager> g_camera_manager;\n>> +static int g_eventfd;\n>> +static mutex g_reqlist_mutex;\n>> +static vector<Request *> g_reqlist;\n>> +\n>> +static void handleRequestCompleted(Request *req)\n>> +{\n>> +\t{\n>> +\t\tlock_guard guard(g_reqlist_mutex);\n>> +\t\tg_reqlist.push_back(req);\n>> +\t}\n>> +\n>> +\tuint64_t v = 1;\n>> +\twrite(g_eventfd, &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 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 pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n>> +\n>> +\t/* Global functions */\n>> +\tm.def(\"logSetLevel\", &logSetLevel);\n>> +\n>> +\t/* Classes */\n>> +\tpyCameraManager\n>> +\t\t.def_static(\"singleton\", []() {\n>> +\t\t\tshared_ptr<CameraManager> cm = g_camera_manager.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(), \"Failed to create eventfd\");\n>> +\n>> +\t\t\tcm = shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n>> +\t\t\t\tclose(g_eventfd);\n>> +\t\t\t\tg_eventfd = -1;\n>> +\t\t\t\tdelete p;\n>> +\t\t\t});\n>> +\n>> +\t\t\tg_eventfd = fd;\n>> +\t\t\tg_camera_manager = 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(), \"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 g_eventfd;\n>> +\t\t})\n>> +\n>> +\t\t.def(\"getReadyRequests\", [](CameraManager &) {\n>> +\t\t\tvector<Request *> v;\n>> +\n>> +\t\t\t{\n>> +\t\t\t\tlock_guard guard(g_reqlist_mutex);\n>> +\t\t\t\tswap(v, g_reqlist);\n>> +\t\t\t}\n>> +\n>> +\t\t\tvector<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::queueRequest() */\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 string &>(&CameraManager::get), py::keep_alive<0, 1>())\n>> +\n>> +\t\t.def(\"find\", [](CameraManager &self, string str) {\n>> +\t\t\tstd::transform(str.begin(), str.end(), str.begin(), ::tolower);\n>> +\n>> +\t\t\tfor (auto c : self.cameras()) {\n>> +\t\t\t\tstring id = c->id();\n>> +\n>> +\t\t\t\tstd::transform(id.begin(), id.end(), id.begin(), ::tolower);\n>> +\n>> +\t\t\t\tif (id.find(str) != string::npos)\n>> +\t\t\t\t\treturn c;\n>> +\t\t\t}\n>> +\n>> +\t\t\treturn shared_ptr<Camera>();\n>> +\t\t}, 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) {\n>> +\t\t\tself.requestCompleted.connect(handleRequestCompleted);\n>> +\n>> +\t\t\tint ret = self.start();\n>> +\t\t\tif (ret)\n>> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n>> +\n>> +\t\t\treturn ret;\n>> +\t\t})\n>> +\n>> +\t\t.def(\"stop\", [](Camera &self) {\n>> +\t\t\tint ret = self.stop();\n>> +\t\t\tif (!ret)\n>> +\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\n>> +\n>> +\t\t\treturn ret;\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(\"generateConfiguration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n>> +\t\t.def(\"configure\", &Camera::configure)\n>> +\n>> +\t\t.def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n>> +\n>> +\t\t.def(\"queueRequest\", [](Camera &self, Request *req) {\n>> +\t\t\tpy::object py_req = py::cast(req);\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 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>> +\t\t\tif (it == controls.end())\n>> +\t\t\t\tthrow runtime_error(\"Control not found\");\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()] = 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), 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>> +\n>> +\tpyStreamConfiguration\n>> +\t\t.def(\"toString\", &StreamConfiguration::toString)\n>> +\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream, py::return_value_policy::reference_internal)\n>> +\t\t.def_property(\n>> +\t\t\t\"size\",\n>> +\t\t\t[](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },\n>> +\t\t\t[](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })\n>> +\t\t.def_property(\n>> +\t\t\t\"pixelFormat\",\n>> +\t\t\t[](StreamConfiguration &self) { return self.pixelFormat.toString(); },\n>> +\t\t\t[](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })\n>> +\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\n>> +\t\t.def_readwrite(\"frameSize\", &StreamConfiguration::frameSize)\n>> +\t\t.def_readwrite(\"bufferCount\", &StreamConfiguration::bufferCount)\n>> +\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats, py::return_value_policy::reference_internal);\n>> +\t;\n>> +\n>> +\tpyStreamFormats\n>> +\t\t.def_property_readonly(\"pixelFormats\", [](StreamFormats &self) {\n>> +\t\t\tvector<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 string &pixelFormat) {\n>> +\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\n>> +\t\t\tvector<tuple<uint32_t, uint32_t>> fmts;\n>> +\t\t\tfor (const auto &s : self.sizes(fmt))\n>> +\t\t\t\tfmts.push_back(make_tuple(s.width, s.height));\n>> +\t\t\treturn fmts;\n>> +\t\t})\n>> +\t\t.def(\"range\", [](StreamFormats &self, const 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(make_tuple(range.hStep, range.vStep),\n>> +\t\t\t\t\t  make_tuple(range.min.width, range.min.height),\n>> +\t\t\t\t\t  make_tuple(range.max.width, range.max.height));\n>> +\t\t});\n>> +\n>> +\tpyFrameBufferAllocator\n>> +\t\t.def(py::init<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([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {\n>> +\t\t\tvector<FrameBuffer::Plane> v;\n>> +\t\t\tfor (const auto &t : planes)\n>> +\t\t\t\tv.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, 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(\"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_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/* Fence is not supported, so we cannot expose addBuffer() directly */\n>> +\t\t.def(\"addBuffer\", [](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(\"hasPendingBuffers\", &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> \n> I see a mixture of camel case (\"addBuffer\") and snake case\n> (\"set_controls\"). If there's a preferred coding style for Python\n> should we uniform on it ?\n\nYes, the names should be consistent. I'm not sure what the naming should \nbe, though.\n\nWe try to expose the C++ API as transparently as possible (while keeping \nit usable) to the Python side, and thus it makes sense to keep the \nnaming same/similar to the C++ naming so that it's clear how the Py \nmethods map to the C++ methods.\n\nThen again, we can't follow the C++ API very strictly as some things \nhave to be implemented in a different way, and possibly it would be \nbetter to hide a bit more if there's no possible performance harm. And \nif it's not quite the same as the C++ API anymore, maybe we don't need \nto follow the C++ naming...\n\nAny convenience library on top of these bindings, like picamera2, should \nof course be \"pure\" Python.\n\n> Minor comments apart, there's a thing which is missing:\n> support for setting controls that accept a Span<> of values.\n> \n> I tried several solutions and I got the following to compile\n> \n>          .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n>                  py::bytearray bytes = py::bytearray(values);\n>                  py::buffer_info info(py::buffer(bytes).request());\n> \n>                  const int *data = reinterpret_cast<const int *>(info.ptr);\n>                  size_t length = static_cast<size_t>(info.size);\n> \n>                  self.controls().set(id.id(), Span<const int>{data, length});\n>           })\n> \n> (All types in casts<> should depend on id.type(), but that's for later).\n> \n> Unfortunately, while length is correct, the values I access with\n> 'data' seems invalid, which makes me think\n> \n>                  py::bytearray bytes = py::bytearray(values);\n> \n> Dosn't do what I think it does, or maybe Tuple in Python are simply\n> not backed by a contigous memory buffer , hence there's no way to wrap\n> their memory in a Span<>).\n> \n> Please note I also tried to instrument:\n> \n>          .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n> \n> Relying on pybind11 type casting, and it seems to work, but I see two issues:\n> \n> 1) Converting from python to C++ types goes through a copy.\n> Performances reasons apart, the vector lifetime is limited to the\n> function scope. This isn't be a problem for now, as control\n> values are copied in the Request's control list, but that would\n> prevent passing controls values as pointers, if a control transports a\n> large chunk of data (ie gamma tables). Not sure we'll ever want this\n> (I don't think so as it won't play well with serialization between the\n> IPA and the pipeline handler) but you never know.\n> \n> 2) my understanding is that python does not support methods signature\n> overloading, hence we would have\n> \n>          .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n>          .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n>          ....\n> \n> there are surely smart ways to handle this, but in my quick experiment\n> I haven't found one yet :)\n\nI think using the same name should work:\n\nhttps://pybind11.readthedocs.io/en/stable/advanced/functions.html?highlight=template#binding-functions-with-template-parameters\n\nI would just go with this latter way. We can try to optimize it later.\n\nAvoiding copies means managing the lifetimes of the python objects, \nwhich can get rather tricky.\n\nAlso, I'm not sure if we can access the underlying array from a python \nlist or tuple. It could mean that we'd need to take in a bytearray, and \nthat would then be a rather annoying API for the user. Then again, a \nbyte array for a gamma table sounds fine, so possibly would could have \nthe simple templated methods and additionally a method that takes a byte \narray (or rather, a buffer_protocol).\n\nThat said, maybe it still wouldn't work, as if we just pass the \nunderlying buffer to the C++ side, and even if we manage to keep the \nbuffer alive, nothing stops the py side from modifying or maybe even \nresizing the buffer (depending on the object, I guess).\n\nTo safely do this we should transfer the ownership from the Python side \nto the C++ side (or the binding layer in this case). Which, I think, the \npybind11 'smart_holders' branch used in this version should support, but \nI haven't looked at it.\n\nIn any case, as I said, I think just do it with the templated methods, \nwhich sound easy to write and easy to validate.\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 7FEB4C0F1B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 13 Apr 2022 07:24:51 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id B353A65644;\n\tWed, 13 Apr 2022 09:24:50 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 8EECD604B3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Apr 2022 09:24:48 +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 7045D25B;\n\tWed, 13 Apr 2022 09:24:47 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649834690;\n\tbh=pW4vjUjGek9rQA7iWDbzYaDfbA8XiRe+FoyyLpIm2n8=;\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=3u1y6Pe8QhjFTCrWCiyM4wgtwFoW3q/C0iWkF/bj5hmdot8d/oxoXlHIZSudrdD3n\n\t00AhLHxJgA5n/PpXurw0FkgeBfBGCb8RW6QlMem0HBC+dvhJMqIjEO5M5o8biXxUWm\n\teKNFhrZMjLriuf3vtmAgSwQEk0lyDVbrx1kO8O/pilCXhbXCu3h0i1qcrj77nj+SJB\n\tL4oIGpzw2JMmB5dLz6GU7t06GyP0s6u27f4hiD+gzyHbvV1OYwHjPirmKmy0XxLlwF\n\thIUNJpLVt7zbNAow+YkHFTusG7/3AjNvBhvqjCSmyv/w6zCgyszkuENhaQYSjBr+Mx\n\troimLhSZcXzEg==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1649834687;\n\tbh=pW4vjUjGek9rQA7iWDbzYaDfbA8XiRe+FoyyLpIm2n8=;\n\th=Date:Subject:To:Cc:References:From:In-Reply-To:From;\n\tb=aFnGRk4ZF5Zg4ZSbz/7zrhrPhL4IOqLUG5wMmvBSUcX3fv3Vt5zqDTRWqPOaHuWjE\n\t9YjoMenlhnJIuQb4NMJytl0LiYSPO9DYzgMRmv/vZ2EIAAQl3LeIa7RGRx8VsuGl3i\n\tIhhD3cn6/7ymekh1mMH0UxGRdTDbtyaKPpc9wMog="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"aFnGRk4Z\"; dkim-atps=neutral","Message-ID":"<44b6afd4-23ee-00f6-5c0b-16c3f21dbb6d@ideasonboard.com>","Date":"Wed, 13 Apr 2022 10:24:45 +0300","MIME-Version":"1.0","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101\n\tThunderbird/91.7.0","Content-Language":"en-US","To":"Jacopo Mondi <jacopo@jmondi.org>","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>\n\t<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","In-Reply-To":"<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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":22683,"web_url":"https://patchwork.libcamera.org/comment/22683/","msgid":"<164983663775.22830.12723401575260170305@Monstersaurus>","date":"2022-04-13T07:57:17","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Jacopo Mondi (2022-04-12 18:49:53)\n> Hi Tomi,\n>    I've been using the bindings in the last days, they're nice to\n> work! Great job!\n> \n> Once the question about a Request belonging to a Camera is\n> clarified I think we should merge these soon, even if incomplete, to\n> build on top.\n> \n> My understanding of python is very limited so I have just a few minor\n> comments and one larger question about controls.\n> \n> On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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\nWhy is the 'smart_holder' a branch of pybind11 ? I can't see a PR or\nanything such that indicates how or when it might get merged.\n\nIs there any other reference to the developments here?\n\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\nI think that's reasonable\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 |  10 +\n> >  src/py/libcamera/meson.build |  43 ++++\n> >  src/py/libcamera/pyenums.cpp |  53 ++++\n> >  src/py/libcamera/pymain.cpp  | 453 +++++++++++++++++++++++++++++++++++\n> >  src/py/meson.build           |   1 +\n> >  subprojects/.gitignore       |   3 +-\n> >  subprojects/pybind11.wrap    |   6 +\n> >  10 files changed, 575 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/pybind11.wrap\n> >\n> > diff --git a/meson.build b/meson.build\n> > index 29d8542d..ff6c2ad6 100644\n> > --- a/meson.build\n> > +++ b/meson.build\n> > @@ -179,6 +179,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..b30bf33a\n> > --- /dev/null\n> > +++ b/src/py/libcamera/__init__.py\n> > @@ -0,0 +1,10 @@\n> > +# SPDX-License-Identifier: LGPL-2.1-or-later\n> > +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> > +\n> > +from ._libcamera import *\n> > +import mmap\n> > +\n> > +def __FrameBuffer__mmap(self, plane):\n> > +     return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)\n> > +\n> > +FrameBuffer.mmap = __FrameBuffer__mmap\n\nThis seems like an odd place to have this. Is it a workaround ? Can it\nlive with other FrameBuffer code? or a comment to say why it's here?\n\nShould it live in something like framebuffer.py?\n\n> > diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\n> > new file mode 100644\n> > index 00000000..82388efb\n> > --- /dev/null\n> > +++ b/src/py/libcamera/meson.build\n> > @@ -0,0 +1,43 @@\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> > +    'pymain.cpp',\n> > +    'pyenums.cpp',\n> > +])\n> > +\n> > +pycamera_deps = [\n> > +    libcamera_public,\n> > +    py3_dep,\n> > +    pybind11_dep,\n> > +]\n> > +\n> > +pycamera_args = ['-fvisibility=hidden']\n> > +pycamera_args += ['-Wno-shadow']\n> > +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']\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\nIs this to support running from the build dir? or is it part of the\nrequirements to build?\n\nA quick comment above could help make it clear.\n\nI'm not sure I like the ../../../../ relative path ... is there a meson\nvariable we can use to identify that location to build the correct /\nrequired path?\n\nI can look the other way on the ../../ path though...\n\n> > +\n> > +install_data(['__init__.py'], install_dir : destdir)\n> > diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n> > new file mode 100644\n> > index 00000000..af6151c8\n> > --- /dev/null\n> > +++ b/src/py/libcamera/pyenums.cpp\n> > @@ -0,0 +1,53 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> > + *\n> > + * Python bindings\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> > +     py::enum_<CameraConfiguration::Status>(m, \"ConfigurationStatus\")\n> > +             .value(\"Valid\", CameraConfiguration::Valid)\n> > +             .value(\"Adjusted\", CameraConfiguration::Adjusted)\n> > +             .value(\"Invalid\", CameraConfiguration::Invalid);\n> > +\n> > +     py::enum_<StreamRole>(m, \"StreamRole\")\n> > +             .value(\"StillCapture\", StreamRole::StillCapture)\n> > +             .value(\"Raw\", StreamRole::Raw)\n> > +             .value(\"VideoRecording\", StreamRole::VideoRecording)\n> > +             .value(\"Viewfinder\", StreamRole::Viewfinder);\n> > +\n> > +     py::enum_<Request::Status>(m, \"RequestStatus\")\n> > +             .value(\"Pending\", Request::RequestPending)\n> > +             .value(\"Complete\", Request::RequestComplete)\n> > +             .value(\"Cancelled\", Request::RequestCancelled);\n> > +\n> > +     py::enum_<FrameMetadata::Status>(m, \"FrameMetadataStatus\")\n> > +             .value(\"Success\", FrameMetadata::FrameSuccess)\n> > +             .value(\"Error\", FrameMetadata::FrameError)\n> > +             .value(\"Cancelled\", FrameMetadata::FrameCancelled);\n> > +\n> > +     py::enum_<Request::ReuseFlag>(m, \"ReuseFlag\")\n> > +             .value(\"Default\", Request::ReuseFlag::Default)\n> > +             .value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\n> > +\n> > +     py::enum_<ControlType>(m, \"ControlType\")\n> > +             .value(\"None\", ControlType::ControlTypeNone)\n> > +             .value(\"Bool\", ControlType::ControlTypeBool)\n> > +             .value(\"Byte\", ControlType::ControlTypeByte)\n> > +             .value(\"Integer32\", ControlType::ControlTypeInteger32)\n> > +             .value(\"Integer64\", ControlType::ControlTypeInteger64)\n> > +             .value(\"Float\", ControlType::ControlTypeFloat)\n> > +             .value(\"String\", ControlType::ControlTypeString)\n> > +             .value(\"Rectangle\", ControlType::ControlTypeRectangle)\n> > +             .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..7701da40\n> > --- /dev/null\n> > +++ b/src/py/libcamera/pymain.cpp\n> > @@ -0,0 +1,453 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n> > + *\n> > + * Python bindings\n> > + */\n> > +\n> > +/*\n> > + * To generate pylibcamera stubs:\n> > + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\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/smart_holder.h>\n> > +#include <pybind11/functional.h>\n> > +#include <pybind11/stl.h>\n> > +#include <pybind11/stl_bind.h>\n\nsort order? Did checkstyle not catch this? \n(Do you have the post-commit hook enabled?)\n\n> > +\n> > +namespace py = pybind11;\n> > +\n> > +using namespace std;\n> > +using namespace libcamera;\n> > +\n> > +template<typename T>\n> > +static py::object ValueOrTuple(const ControlValue &cv)\n> > +{\n> > +     if (cv.isArray()) {\n> > +             const T *v = reinterpret_cast<const T *>(cv.data().data());\n> > +             auto t = py::tuple(cv.numElements());\n> > +\n> > +             for (size_t i = 0; i < cv.numElements(); ++i)\n> > +                     t[i] = v[i];\n> > +\n> > +             return t;\n> > +     }\n> > +\n> > +     return py::cast(cv.get<T>());\n> > +}\n> > +\n> > +static py::object ControlValueToPy(const ControlValue &cv)\n> > +{\n> > +     switch (cv.type()) {\n> > +     case ControlTypeBool:\n> > +             return ValueOrTuple<bool>(cv);\n> > +     case ControlTypeByte:\n> > +             return ValueOrTuple<uint8_t>(cv);\n> > +     case ControlTypeInteger32:\n> > +             return ValueOrTuple<int32_t>(cv);\n> > +     case ControlTypeInteger64:\n> > +             return ValueOrTuple<int64_t>(cv);\n> > +     case ControlTypeFloat:\n> > +             return ValueOrTuple<float>(cv);\n> > +     case ControlTypeString:\n> > +             return py::cast(cv.get<string>());\n> > +     case ControlTypeRectangle: {\n> > +             const Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n> > +             return py::make_tuple(v->x, v->y, v->width, v->height);\n> > +     }\n> > +     case ControlTypeSize: {\n> > +             const Size *v = reinterpret_cast<const Size *>(cv.data().data());\n> > +             return py::make_tuple(v->width, v->height);\n> > +     }\n> > +     case ControlTypeNone:\n> > +     default:\n> > +             throw runtime_error(\"Unsupported ControlValue type\");\n> > +     }\n> > +}\n> > +\n> > +static ControlValue PyToControlValue(const py::object &ob, ControlType type)\n> > +{\n> > +     switch (type) {\n> > +     case ControlTypeBool:\n> > +             return ControlValue(ob.cast<bool>());\n> > +     case ControlTypeByte:\n> > +             return ControlValue(ob.cast<uint8_t>());\n> > +     case ControlTypeInteger32:\n> > +             return ControlValue(ob.cast<int32_t>());\n> > +     case ControlTypeInteger64:\n> > +             return ControlValue(ob.cast<int64_t>());\n> > +     case ControlTypeFloat:\n> > +             return ControlValue(ob.cast<float>());\n> > +     case ControlTypeString:\n> > +             return ControlValue(ob.cast<string>());\n> > +     case ControlTypeRectangle:\n> > +     case ControlTypeSize:\n> > +     case ControlTypeNone:\n> > +     default:\n> > +             throw runtime_error(\"Control type not implemented\");\n> > +     }\n> > +}\n> > +\n> > +static weak_ptr<CameraManager> g_camera_manager;\n> > +static int g_eventfd;\n> > +static mutex g_reqlist_mutex;\n> > +static vector<Request *> g_reqlist;\n> > +\n> > +static void handleRequestCompleted(Request *req)\n> > +{\n> > +     {\n> > +             lock_guard guard(g_reqlist_mutex);\n> > +             g_reqlist.push_back(req);\n> > +     }\n> > +\n> > +     uint64_t v = 1;\n> > +     write(g_eventfd, &v, 8);\n> > +}\n> > +\n> > +void init_pyenums(py::module& m);\n> > +\n> > +PYBIND11_MODULE(_libcamera, m)\n> > +{\n\nDo these all have to be in a single definition file to maintain this\nscope? Or could the classes be broken down to one file per class? I\ncould imagine these growing rapidly - so maintaining them would likely\nbe easier to parse if they matched the file hierarchy of the\ncorresponding libcamera impelemttion.\n\n> > +     init_pyenums(m);\n> > +\n> > +     /* Forward declarations */\n> > +\n> > +     /*\n> > +      * We need to declare all the classes here so that Python docstrings\n> > +      * can be generated correctly.\n> > +      * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n> > +      */\n> > +\n> > +     auto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n> > +     auto pyCamera = py::class_<Camera>(m, \"Camera\");\n> > +     auto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n> > +     auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n> > +     auto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n> > +     auto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n> > +     auto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n> > +     auto pyStream = py::class_<Stream>(m, \"Stream\");\n> > +     auto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n> > +     auto pyRequest = py::class_<Request>(m, \"Request\");\n> > +     auto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n> > +\n> > +     /* Global functions */\n> > +     m.def(\"logSetLevel\", &logSetLevel);\n> > +\n> > +     /* Classes */\n> > +     pyCameraManager\n> > +             .def_static(\"singleton\", []() {\n> > +                     shared_ptr<CameraManager> cm = g_camera_manager.lock();\n> > +                     if (cm)\n> > +                             return cm;\n> > +\n> > +                     int fd = eventfd(0, 0);\n> > +                     if (fd == -1)\n> > +                             throw std::system_error(errno, std::generic_category(), \"Failed to create eventfd\");\n> > +\n> > +                     cm = shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n> > +                             close(g_eventfd);\n> > +                             g_eventfd = -1;\n> > +                             delete p;\n> > +                     });\n> > +\n> > +                     g_eventfd = fd;\n> > +                     g_camera_manager = cm;\n> > +\n> > +                     int ret = cm->start();\n> > +                     if (ret)\n> > +                             throw std::system_error(-ret, std::generic_category(), \"Failed to start CameraManager\");\n> > +\n> > +                     return cm;\n> > +             })\n> > +\n> > +             .def_property_readonly(\"version\", &CameraManager::version)\n> > +\n> > +             .def_property_readonly(\"efd\", [](CameraManager &) {\n> > +                     return g_eventfd;\n> > +             })\n> > +\n> > +             .def(\"getReadyRequests\", [](CameraManager &) {\n> > +                     vector<Request *> v;\n> > +\n> > +                     {\n> > +                             lock_guard guard(g_reqlist_mutex);\n> > +                             swap(v, g_reqlist);\n> > +                     }\n> > +\n> > +                     vector<py::object> ret;\n> > +\n> > +                     for (Request *req : v) {\n> > +                             py::object o = py::cast(req);\n> > +                             /* decrease the ref increased in Camera::queueRequest() */\n> > +                             o.dec_ref();\n> > +                             ret.push_back(o);\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def(\"get\", py::overload_cast<const string &>(&CameraManager::get), py::keep_alive<0, 1>())\n> > +\n> > +             .def(\"find\", [](CameraManager &self, string str) {\n> > +                     std::transform(str.begin(), str.end(), str.begin(), ::tolower);\n> > +\n> > +                     for (auto c : self.cameras()) {\n> > +                             string id = c->id();\n> > +\n> > +                             std::transform(id.begin(), id.end(), id.begin(), ::tolower);\n> > +\n> > +                             if (id.find(str) != string::npos)\n> > +                                     return c;\n> > +                     }\n> > +\n> > +                     return shared_ptr<Camera>();\n> > +             }, py::keep_alive<0, 1>())\n> > +\n> > +             /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n> > +             .def_property_readonly(\"cameras\", [](CameraManager &self) {\n> > +                     py::list l;\n> > +\n> > +                     for (auto &c : self.cameras()) {\n> > +                             py::object py_cm = py::cast(self);\n> > +                             py::object py_cam = py::cast(c);\n> > +                             py::detail::keep_alive_impl(py_cam, py_cm);\n> > +                             l.append(py_cam);\n> > +                     }\n> > +\n> > +                     return l;\n> > +             });\n> > +\n> > +     pyCamera\n> > +             .def_property_readonly(\"id\", &Camera::id)\n> > +             .def(\"acquire\", &Camera::acquire)\n> > +             .def(\"release\", &Camera::release)\n> > +             .def(\"start\", [](Camera &self) {\n> > +                     self.requestCompleted.connect(handleRequestCompleted);\n> > +\n> > +                     int ret = self.start();\n> > +                     if (ret)\n> > +                             self.requestCompleted.disconnect(handleRequestCompleted);\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def(\"stop\", [](Camera &self) {\n> > +                     int ret = self.stop();\n> > +                     if (!ret)\n> > +                             self.requestCompleted.disconnect(handleRequestCompleted);\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def(\"__repr__\", [](Camera &self) {\n> > +                     return \"<libcamera.Camera '\" + self.id() + \"'>\";\n> > +             })\n> > +\n> > +             /* Keep the camera alive, as StreamConfiguration contains a Stream* */\n> > +             .def(\"generateConfiguration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n> > +             .def(\"configure\", &Camera::configure)\n> > +\n> > +             .def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n> > +\n> > +             .def(\"queueRequest\", [](Camera &self, Request *req) {\n> > +                     py::object py_req = py::cast(req);\n> > +\n> > +                     py_req.inc_ref();\n> > +\n> > +                     int ret = self.queueRequest(req);\n> > +                     if (ret)\n> > +                             py_req.dec_ref();\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def_property_readonly(\"streams\", [](Camera &self) {\n> > +                     py::set set;\n> > +                     for (auto &s : self.streams()) {\n> > +                             py::object py_self = py::cast(self);\n> > +                             py::object py_s = py::cast(s);\n> > +                             py::detail::keep_alive_impl(py_s, py_self);\n> > +                             set.add(py_s);\n> > +                     }\n> > +                     return set;\n> > +             })\n> > +\n> > +             .def(\"find_control\", [](Camera &self, const string &name) {\n> > +                     const auto &controls = self.controls();\n> > +\n> > +                     auto it = find_if(controls.begin(), controls.end(),\n> > +                                       [&name](const auto &kvp) { return kvp.first->name() == name; });\n> > +\n> > +                     if (it == controls.end())\n> > +                             throw runtime_error(\"Control not found\");\n> > +\n> > +                     return it->first;\n> > +             }, py::return_value_policy::reference_internal)\n> > +\n> > +             .def_property_readonly(\"controls\", [](Camera &self) {\n> > +                     py::dict ret;\n> > +\n> > +                     for (const auto &[id, ci] : self.controls()) {\n> > +                             ret[id->name().c_str()] = make_tuple<py::object>(ControlValueToPy(ci.min()),\n> > +                                                                              ControlValueToPy(ci.max()),\n> > +                                                                              ControlValueToPy(ci.def()));\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             })\n> > +\n> > +             .def_property_readonly(\"properties\", [](Camera &self) {\n> > +                     py::dict ret;\n> > +\n> > +                     for (const auto &[key, cv] : self.properties()) {\n> > +                             const ControlId *id = properties::properties.at(key);\n> > +                             py::object ob = ControlValueToPy(cv);\n> > +\n> > +                             ret[id->name().c_str()] = ob;\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             });\n> > +\n> > +     pyCameraConfiguration\n> > +             .def(\"__iter__\", [](CameraConfiguration &self) {\n> > +                     return py::make_iterator<py::return_value_policy::reference_internal>(self);\n> > +             }, py::keep_alive<0, 1>())\n> > +             .def(\"__len__\", [](CameraConfiguration &self) {\n> > +                     return self.size();\n> > +             })\n> > +             .def(\"validate\", &CameraConfiguration::validate)\n> > +             .def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at), py::return_value_policy::reference_internal)\n> > +             .def_property_readonly(\"size\", &CameraConfiguration::size)\n> > +             .def_property_readonly(\"empty\", &CameraConfiguration::empty);\n> > +\n> > +     pyStreamConfiguration\n> > +             .def(\"toString\", &StreamConfiguration::toString)\n> > +             .def_property_readonly(\"stream\", &StreamConfiguration::stream, py::return_value_policy::reference_internal)\n> > +             .def_property(\n> > +                     \"size\",\n> > +                     [](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },\n> > +                     [](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })\n> > +             .def_property(\n> > +                     \"pixelFormat\",\n> > +                     [](StreamConfiguration &self) { return self.pixelFormat.toString(); },\n> > +                     [](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })\n> > +             .def_readwrite(\"stride\", &StreamConfiguration::stride)\n> > +             .def_readwrite(\"frameSize\", &StreamConfiguration::frameSize)\n> > +             .def_readwrite(\"bufferCount\", &StreamConfiguration::bufferCount)\n> > +             .def_property_readonly(\"formats\", &StreamConfiguration::formats, py::return_value_policy::reference_internal);\n> > +     ;\n\nIs this a stray ; ?\n\nHow does checkstyle cope with all of this? does clang-format get\nanywhere close?\n\nI expect the syntax and chaining probably breaks a lot of assumptions\nused by clang-format :-(\n\n> > +\n> > +     pyStreamFormats\n> > +             .def_property_readonly(\"pixelFormats\", [](StreamFormats &self) {\n> > +                     vector<string> fmts;\n> > +                     for (auto &fmt : self.pixelformats())\n> > +                             fmts.push_back(fmt.toString());\n> > +                     return fmts;\n> > +             })\n> > +             .def(\"sizes\", [](StreamFormats &self, const string &pixelFormat) {\n> > +                     auto fmt = PixelFormat::fromString(pixelFormat);\n> > +                     vector<tuple<uint32_t, uint32_t>> fmts;\n> > +                     for (const auto &s : self.sizes(fmt))\n> > +                             fmts.push_back(make_tuple(s.width, s.height));\n> > +                     return fmts;\n> > +             })\n> > +             .def(\"range\", [](StreamFormats &self, const string &pixelFormat) {\n> > +                     auto fmt = PixelFormat::fromString(pixelFormat);\n> > +                     const auto &range = self.range(fmt);\n> > +                     return make_tuple(make_tuple(range.hStep, range.vStep),\n> > +                                       make_tuple(range.min.width, range.min.height),\n> > +                                       make_tuple(range.max.width, range.max.height));\n> > +             });\n> > +\n> > +     pyFrameBufferAllocator\n> > +             .def(py::init<shared_ptr<Camera>>(), py::keep_alive<1, 2>())\n> > +             .def(\"allocate\", &FrameBufferAllocator::allocate)\n> > +             .def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\n> > +             /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */\n> > +             .def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\n> > +                     py::object py_self = py::cast(self);\n> > +                     py::list l;\n> > +                     for (auto &ub : self.buffers(stream)) {\n> > +                             py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);\n> > +                             l.append(py_buf);\n> > +                     }\n> > +                     return l;\n> > +             });\n> > +\n> > +     pyFrameBuffer\n> > +             /* TODO: implement FrameBuffer::Plane properly */\n> > +             .def(py::init([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {\n> > +                     vector<FrameBuffer::Plane> v;\n> > +                     for (const auto &t : planes)\n> > +                             v.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, get<1>(t) });\n> > +                     return new FrameBuffer(v, cookie);\n> > +             }))\n> > +             .def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\n> > +             .def(\"length\", [](FrameBuffer &self, uint32_t idx) {\n> > +                     const FrameBuffer::Plane &plane = self.planes()[idx];\n> > +                     return plane.length;\n> > +             })\n> > +             .def(\"fd\", [](FrameBuffer &self, uint32_t idx) {\n> > +                     const FrameBuffer::Plane &plane = self.planes()[idx];\n> > +                     return plane.fd.get();\n> > +             })\n> > +             .def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\n> > +\n> > +     pyStream\n> > +             .def_property_readonly(\"configuration\", &Stream::configuration);\n> > +\n> > +     pyControlId\n> > +             .def_property_readonly(\"id\", &ControlId::id)\n> > +             .def_property_readonly(\"name\", &ControlId::name)\n> > +             .def_property_readonly(\"type\", &ControlId::type);\n> > +\n> > +     pyRequest\n> > +             /* Fence is not supported, so we cannot expose addBuffer() directly */\n> > +             .def(\"addBuffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n> > +                     return self.addBuffer(stream, buffer);\n> > +             }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n> > +             .def_property_readonly(\"status\", &Request::status)\n> > +             .def_property_readonly(\"buffers\", &Request::buffers)\n> > +             .def_property_readonly(\"cookie\", &Request::cookie)\n> > +             .def_property_readonly(\"hasPendingBuffers\", &Request::hasPendingBuffers)\n> > +             .def(\"set_control\", [](Request &self, ControlId& id, py::object value) {\n> > +                     self.controls().set(id.id(), PyToControlValue(value, id.type()));\n> > +             })\n> \n> I see a mixture of camel case (\"addBuffer\") and snake case\n> (\"set_controls\"). If there's a preferred coding style for Python\n> should we uniform on it ?\n> \n> Minor comments apart, there's a thing which is missing:\n> support for setting controls that accept a Span<> of values.\n> \n> I tried several solutions and I got the following to compile\n> \n>         .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n>                 py::bytearray bytes = py::bytearray(values);\n>                 py::buffer_info info(py::buffer(bytes).request());\n> \n>                 const int *data = reinterpret_cast<const int *>(info.ptr);\n>                 size_t length = static_cast<size_t>(info.size);\n> \n>                 self.controls().set(id.id(), Span<const int>{data, length});\n>          })\n> \n> (All types in casts<> should depend on id.type(), but that's for later).\n> \n> Unfortunately, while length is correct, the values I access with\n> 'data' seems invalid, which makes me think\n> \n>                 py::bytearray bytes = py::bytearray(values);\n> \n> Dosn't do what I think it does, or maybe Tuple in Python are simply\n> not backed by a contigous memory buffer , hence there's no way to wrap\n> their memory in a Span<>).\n> \n> Please note I also tried to instrument:\n> \n>         .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n> \n> Relying on pybind11 type casting, and it seems to work, but I see two issues:\n> \n> 1) Converting from python to C++ types goes through a copy.\n> Performances reasons apart, the vector lifetime is limited to the\n> function scope. This isn't be a problem for now, as control\n> values are copied in the Request's control list, but that would\n> prevent passing controls values as pointers, if a control transports a\n> large chunk of data (ie gamma tables). Not sure we'll ever want this\n> (I don't think so as it won't play well with serialization between the\n> IPA and the pipeline handler) but you never know.\n\nI don't think we could ever let an application pass a pointer to an\narbitrary location into libcamera for us to parse.\n\n> 2) my understanding is that python does not support methods signature\n> overloading, hence we would have\n> \n>         .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n>         .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n>         ....\n> \n> there are surely smart ways to handle this, but in my quick experiment\n> I haven't found one yet :)\n\nGiven how dynamically typed python is I would expect there would be some\nform of wrapping here that could be done?\n\n> David: Is the issue with Span<> controls addressed by picamera2 ?\n> \n> Thanks, I hope we can merge this soon!\n\n+1 ..\n\n> \n> \n> > +             .def_property_readonly(\"metadata\", [](Request &self) {\n> > +                     py::dict ret;\n> > +\n> > +                     for (const auto &[key, cv] : self.metadata()) {\n> > +                             const ControlId *id = controls::controls.at(key);\n> > +                             py::object ob = ControlValueToPy(cv);\n> > +\n> > +                             ret[id->name().c_str()] = ob;\n> > +                     }\n> > +\n> > +                     return ret;\n> > +             })\n> > +             /* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */\n> > +             .def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\n> > +\n> > +     pyFrameMetadata\n> > +             .def_readonly(\"status\", &FrameMetadata::status)\n> > +             .def_readonly(\"sequence\", &FrameMetadata::sequence)\n> > +             .def_readonly(\"timestamp\", &FrameMetadata::timestamp)\n> > +             /* temporary helper, to be removed */\n> > +             .def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n> > +                     vector<unsigned int> v;\n> > +                     v.resize(self.planes().size());\n> > +                     transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\n> > +                     return v;\n> > +             });\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..757bb072 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/pybind11.wrap b/subprojects/pybind11.wrap\n> > new file mode 100644\n> > index 00000000..ebf942ff\n> > --- /dev/null\n> > +++ b/subprojects/pybind11.wrap\n> > @@ -0,0 +1,6 @@\n> > +[wrap-git]\n> > +url = https://github.com/tomba/pybind11.git\n> > +revision = smart_holder\n\nCan this point to the upstream branch? or have you made local\nmodifications to the branch?\n\nI second Jacopo's desire to merge this soon. I've only had some minor (I\nhope) comments above, and with those resolved or answered I'd happily\nsee this merged - It will be much easier to develop on top, than to\niterate here.\n\nI know I had some comments about the tests previously - I hope they're\nsufficient for now anyway.\n\n\n> > +\n> > +[provide]\n> > +pybind11 = pybind11_dep\n> > --\n> > 2.25.1\n> >","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 6A28AC0F1B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 13 Apr 2022 07:57:22 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id A08FB65646;\n\tWed, 13 Apr 2022 09:57:21 +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 69505604B3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Apr 2022 09:57:20 +0200 (CEST)","from pendragon.ideasonboard.com\n\t(cpc89244-aztw30-2-0-cust3082.18-1.cable.virginm.net [86.31.172.11])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id D62B125B;\n\tWed, 13 Apr 2022 09:57:19 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649836641;\n\tbh=DWvXrpnCByPAk3/hv5Neaj/FV+Oe6zi8XhOht0dDyFQ=;\n\th=In-Reply-To:References:To:Date:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=cZjkXvfzGNO3seLtljQPXphYdGqAyN/Kt/0R7WkGo9uFbR6PWpGUGHQDmfVCFKwV4\n\twzS56smcgrfIzI2otej2pw8oLa5MfNjZck0aMJIR5jimeLSNrtv0eaADSC77lxBMZM\n\tZJumnaF0kIp1gc6xAhmfrw0FXqJauciASVx4xHlOLtB4Wu1JkgBf97azAZUYeaT2X5\n\tQoFFhBONy9mI9whJ8q5GdEc7y7Jt9goWhsDb3/y1ebGTD3LSfGU51Wz8Fss8o3tTwe\n\tMGJxd2wbdrHTQ0Pae7G+tkSfJoxmPnunkViKlfntTf1YWY8bXX6k7jDZMiJmYxF74r\n\tNczo8SrTJNgmQ==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1649836640;\n\tbh=DWvXrpnCByPAk3/hv5Neaj/FV+Oe6zi8XhOht0dDyFQ=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=U+ho+7WDxV8prMZu/a/C18LzI2cIYcK1BGUK6Opu1kUfIw8IL1iI0xPLPzd15IPEx\n\tRq5i+aQfJW4M9uSQIT1oqveNgRqLhmlGdMv5D6uYGBMYUUddZY5MdnkyemrlSCcEbR\n\th8m/WqeMGK3WX2j7CchUp3blmFplLovMdG6NJAiE="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"U+ho+7WD\"; dkim-atps=neutral","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>\n\t<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>","To":"Jacopo Mondi <jacopo@jmondi.org>,\n\tTomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Date":"Wed, 13 Apr 2022 08:57:17 +0100","Message-ID":"<164983663775.22830.12723401575260170305@Monstersaurus>","User-Agent":"alot/0.10","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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":"Kieran Bingham via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Kieran Bingham <kieran.bingham@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":22691,"web_url":"https://patchwork.libcamera.org/comment/22691/","msgid":"<5d3e9b08-bdc7-9d43-904c-22706e54f0ed@ideasonboard.com>","date":"2022-04-13T09:10:45","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":109,"url":"https://patchwork.libcamera.org/api/people/109/","name":"Tomi Valkeinen","email":"tomi.valkeinen@ideasonboard.com"},"content":"On 13/04/2022 10:57, Kieran Bingham wrote:\n> Quoting Jacopo Mondi (2022-04-12 18:49:53)\n>> Hi Tomi,\n>>     I've been using the bindings in the last days, they're nice to\n>> work! Great job!\n>>\n>> Once the question about a Request belonging to a Camera is\n>> clarified I think we should merge these soon, even if incomplete, to\n>> build on top.\n>>\n>> My understanding of python is very limited so I have just a few minor\n>> comments and one larger question about controls.\n>>\n>> On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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> Why is the 'smart_holder' a branch of pybind11 ? I can't see a PR or\n> anything such that indicates how or when it might get merged.\n> \n> Is there any other reference to the developments here?\n\nThere's a readme:\n\nhttps://github.com/pybind/pybind11/blob/49f8f60ec4254e0f55db3c095c945210bcb43ad2/README_smart_holder.rst\n\nI haven't studied the smart_holder discussions to find out if there's a \nclear plan on how and if it gets merged to the main pybind11.\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> I think that's reasonable\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 |  10 +\n>>>   src/py/libcamera/meson.build |  43 ++++\n>>>   src/py/libcamera/pyenums.cpp |  53 ++++\n>>>   src/py/libcamera/pymain.cpp  | 453 +++++++++++++++++++++++++++++++++++\n>>>   src/py/meson.build           |   1 +\n>>>   subprojects/.gitignore       |   3 +-\n>>>   subprojects/pybind11.wrap    |   6 +\n>>>   10 files changed, 575 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/pybind11.wrap\n>>>\n>>> diff --git a/meson.build b/meson.build\n>>> index 29d8542d..ff6c2ad6 100644\n>>> --- a/meson.build\n>>> +++ b/meson.build\n>>> @@ -179,6 +179,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..b30bf33a\n>>> --- /dev/null\n>>> +++ b/src/py/libcamera/__init__.py\n>>> @@ -0,0 +1,10 @@\n>>> +# SPDX-License-Identifier: LGPL-2.1-or-later\n>>> +# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>>> +\n>>> +from ._libcamera import *\n>>> +import mmap\n>>> +\n>>> +def __FrameBuffer__mmap(self, plane):\n>>> +     return mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)\n>>> +\n>>> +FrameBuffer.mmap = __FrameBuffer__mmap\n> \n> This seems like an odd place to have this. Is it a workaround ? Can it\n> live with other FrameBuffer code? or a comment to say why it's here?\n> \n> Should it live in something like framebuffer.py?\n\nYes, I should have added a comment. It was a quick way to get mmap \nworking. I don't see why it couldn't be done in the cpp code, but I \nrecall having some issues figuring out how to do that:\n\n- If the cpp code would use native mmap, then it would also need to wrap \nit somehow so that the mmaped area is usable in the python side\n\n- Using python's mmap (like I do above) is possible from the cpp code, \nbut I think I didn't figure it out right away, so I just did it with python.\n\n>>> diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\n>>> new file mode 100644\n>>> index 00000000..82388efb\n>>> --- /dev/null\n>>> +++ b/src/py/libcamera/meson.build\n>>> @@ -0,0 +1,43 @@\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>>> +    'pymain.cpp',\n>>> +    'pyenums.cpp',\n>>> +])\n>>> +\n>>> +pycamera_deps = [\n>>> +    libcamera_public,\n>>> +    py3_dep,\n>>> +    pybind11_dep,\n>>> +]\n>>> +\n>>> +pycamera_args = ['-fvisibility=hidden']\n>>> +pycamera_args += ['-Wno-shadow']\n>>> +pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']\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> \n> Is this to support running from the build dir? or is it part of the\n> requirements to build?\n> \n> A quick comment above could help make it clear.\n\nFor running from the build dir.\n\n> I'm not sure I like the ../../../../ relative path ... is there a meson\n> variable we can use to identify that location to build the correct /\n> required path?\n> \n> I can look the other way on the ../../ path though...\n\nI guess it depends, but usually a relative path is better with sym \nlinks, so that they continue working even if the whole dir structure is \nmoved somewhere else.\n\n>>> +\n>>> +install_data(['__init__.py'], install_dir : destdir)\n>>> diff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\n>>> new file mode 100644\n>>> index 00000000..af6151c8\n>>> --- /dev/null\n>>> +++ b/src/py/libcamera/pyenums.cpp\n>>> @@ -0,0 +1,53 @@\n>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>>> +/*\n>>> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>>> + *\n>>> + * Python bindings\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>>> +     py::enum_<CameraConfiguration::Status>(m, \"ConfigurationStatus\")\n>>> +             .value(\"Valid\", CameraConfiguration::Valid)\n>>> +             .value(\"Adjusted\", CameraConfiguration::Adjusted)\n>>> +             .value(\"Invalid\", CameraConfiguration::Invalid);\n>>> +\n>>> +     py::enum_<StreamRole>(m, \"StreamRole\")\n>>> +             .value(\"StillCapture\", StreamRole::StillCapture)\n>>> +             .value(\"Raw\", StreamRole::Raw)\n>>> +             .value(\"VideoRecording\", StreamRole::VideoRecording)\n>>> +             .value(\"Viewfinder\", StreamRole::Viewfinder);\n>>> +\n>>> +     py::enum_<Request::Status>(m, \"RequestStatus\")\n>>> +             .value(\"Pending\", Request::RequestPending)\n>>> +             .value(\"Complete\", Request::RequestComplete)\n>>> +             .value(\"Cancelled\", Request::RequestCancelled);\n>>> +\n>>> +     py::enum_<FrameMetadata::Status>(m, \"FrameMetadataStatus\")\n>>> +             .value(\"Success\", FrameMetadata::FrameSuccess)\n>>> +             .value(\"Error\", FrameMetadata::FrameError)\n>>> +             .value(\"Cancelled\", FrameMetadata::FrameCancelled);\n>>> +\n>>> +     py::enum_<Request::ReuseFlag>(m, \"ReuseFlag\")\n>>> +             .value(\"Default\", Request::ReuseFlag::Default)\n>>> +             .value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\n>>> +\n>>> +     py::enum_<ControlType>(m, \"ControlType\")\n>>> +             .value(\"None\", ControlType::ControlTypeNone)\n>>> +             .value(\"Bool\", ControlType::ControlTypeBool)\n>>> +             .value(\"Byte\", ControlType::ControlTypeByte)\n>>> +             .value(\"Integer32\", ControlType::ControlTypeInteger32)\n>>> +             .value(\"Integer64\", ControlType::ControlTypeInteger64)\n>>> +             .value(\"Float\", ControlType::ControlTypeFloat)\n>>> +             .value(\"String\", ControlType::ControlTypeString)\n>>> +             .value(\"Rectangle\", ControlType::ControlTypeRectangle)\n>>> +             .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..7701da40\n>>> --- /dev/null\n>>> +++ b/src/py/libcamera/pymain.cpp\n>>> @@ -0,0 +1,453 @@\n>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n>>> +/*\n>>> + * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n>>> + *\n>>> + * Python bindings\n>>> + */\n>>> +\n>>> +/*\n>>> + * To generate pylibcamera stubs:\n>>> + * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\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/smart_holder.h>\n>>> +#include <pybind11/functional.h>\n>>> +#include <pybind11/stl.h>\n>>> +#include <pybind11/stl_bind.h>\n> \n> sort order? Did checkstyle not catch this?\n> (Do you have the post-commit hook enabled?)\n\nWe have a checkstyle? =) Yes, I see a script in utils. I'll try it out.\n\n>>> +\n>>> +namespace py = pybind11;\n>>> +\n>>> +using namespace std;\n>>> +using namespace libcamera;\n>>> +\n>>> +template<typename T>\n>>> +static py::object ValueOrTuple(const ControlValue &cv)\n>>> +{\n>>> +     if (cv.isArray()) {\n>>> +             const T *v = reinterpret_cast<const T *>(cv.data().data());\n>>> +             auto t = py::tuple(cv.numElements());\n>>> +\n>>> +             for (size_t i = 0; i < cv.numElements(); ++i)\n>>> +                     t[i] = v[i];\n>>> +\n>>> +             return t;\n>>> +     }\n>>> +\n>>> +     return py::cast(cv.get<T>());\n>>> +}\n>>> +\n>>> +static py::object ControlValueToPy(const ControlValue &cv)\n>>> +{\n>>> +     switch (cv.type()) {\n>>> +     case ControlTypeBool:\n>>> +             return ValueOrTuple<bool>(cv);\n>>> +     case ControlTypeByte:\n>>> +             return ValueOrTuple<uint8_t>(cv);\n>>> +     case ControlTypeInteger32:\n>>> +             return ValueOrTuple<int32_t>(cv);\n>>> +     case ControlTypeInteger64:\n>>> +             return ValueOrTuple<int64_t>(cv);\n>>> +     case ControlTypeFloat:\n>>> +             return ValueOrTuple<float>(cv);\n>>> +     case ControlTypeString:\n>>> +             return py::cast(cv.get<string>());\n>>> +     case ControlTypeRectangle: {\n>>> +             const Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\n>>> +             return py::make_tuple(v->x, v->y, v->width, v->height);\n>>> +     }\n>>> +     case ControlTypeSize: {\n>>> +             const Size *v = reinterpret_cast<const Size *>(cv.data().data());\n>>> +             return py::make_tuple(v->width, v->height);\n>>> +     }\n>>> +     case ControlTypeNone:\n>>> +     default:\n>>> +             throw runtime_error(\"Unsupported ControlValue type\");\n>>> +     }\n>>> +}\n>>> +\n>>> +static ControlValue PyToControlValue(const py::object &ob, ControlType type)\n>>> +{\n>>> +     switch (type) {\n>>> +     case ControlTypeBool:\n>>> +             return ControlValue(ob.cast<bool>());\n>>> +     case ControlTypeByte:\n>>> +             return ControlValue(ob.cast<uint8_t>());\n>>> +     case ControlTypeInteger32:\n>>> +             return ControlValue(ob.cast<int32_t>());\n>>> +     case ControlTypeInteger64:\n>>> +             return ControlValue(ob.cast<int64_t>());\n>>> +     case ControlTypeFloat:\n>>> +             return ControlValue(ob.cast<float>());\n>>> +     case ControlTypeString:\n>>> +             return ControlValue(ob.cast<string>());\n>>> +     case ControlTypeRectangle:\n>>> +     case ControlTypeSize:\n>>> +     case ControlTypeNone:\n>>> +     default:\n>>> +             throw runtime_error(\"Control type not implemented\");\n>>> +     }\n>>> +}\n>>> +\n>>> +static weak_ptr<CameraManager> g_camera_manager;\n>>> +static int g_eventfd;\n>>> +static mutex g_reqlist_mutex;\n>>> +static vector<Request *> g_reqlist;\n>>> +\n>>> +static void handleRequestCompleted(Request *req)\n>>> +{\n>>> +     {\n>>> +             lock_guard guard(g_reqlist_mutex);\n>>> +             g_reqlist.push_back(req);\n>>> +     }\n>>> +\n>>> +     uint64_t v = 1;\n>>> +     write(g_eventfd, &v, 8);\n>>> +}\n>>> +\n>>> +void init_pyenums(py::module& m);\n>>> +\n>>> +PYBIND11_MODULE(_libcamera, m)\n>>> +{\n> \n> Do these all have to be in a single definition file to maintain this\n> scope? Or could the classes be broken down to one file per class? I\n> could imagine these growing rapidly - so maintaining them would likely\n> be easier to parse if they matched the file hierarchy of the\n> corresponding libcamera impelemttion.\n\nThese can be split, as I have done with the enums (see the \ninit_pyenums() call below).\n\nI don't know if file per class makes sense, as there's an overhead. But \nit is possible. When using pybind11 I've done a split when I feel the \nbindings grow too big, and there's a clear set of code to separate.\n\n>>> +     init_pyenums(m);\n>>> +\n>>> +     /* Forward declarations */\n>>> +\n>>> +     /*\n>>> +      * We need to declare all the classes here so that Python docstrings\n>>> +      * can be generated correctly.\n>>> +      * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n>>> +      */\n>>> +\n>>> +     auto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\n>>> +     auto pyCamera = py::class_<Camera>(m, \"Camera\");\n>>> +     auto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n>>> +     auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\n>>> +     auto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\n>>> +     auto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\n>>> +     auto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\n>>> +     auto pyStream = py::class_<Stream>(m, \"Stream\");\n>>> +     auto pyControlId = py::class_<ControlId>(m, \"ControlId\");\n>>> +     auto pyRequest = py::class_<Request>(m, \"Request\");\n>>> +     auto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\n>>> +\n>>> +     /* Global functions */\n>>> +     m.def(\"logSetLevel\", &logSetLevel);\n>>> +\n>>> +     /* Classes */\n>>> +     pyCameraManager\n>>> +             .def_static(\"singleton\", []() {\n>>> +                     shared_ptr<CameraManager> cm = g_camera_manager.lock();\n>>> +                     if (cm)\n>>> +                             return cm;\n>>> +\n>>> +                     int fd = eventfd(0, 0);\n>>> +                     if (fd == -1)\n>>> +                             throw std::system_error(errno, std::generic_category(), \"Failed to create eventfd\");\n>>> +\n>>> +                     cm = shared_ptr<CameraManager>(new CameraManager, [](auto p) {\n>>> +                             close(g_eventfd);\n>>> +                             g_eventfd = -1;\n>>> +                             delete p;\n>>> +                     });\n>>> +\n>>> +                     g_eventfd = fd;\n>>> +                     g_camera_manager = cm;\n>>> +\n>>> +                     int ret = cm->start();\n>>> +                     if (ret)\n>>> +                             throw std::system_error(-ret, std::generic_category(), \"Failed to start CameraManager\");\n>>> +\n>>> +                     return cm;\n>>> +             })\n>>> +\n>>> +             .def_property_readonly(\"version\", &CameraManager::version)\n>>> +\n>>> +             .def_property_readonly(\"efd\", [](CameraManager &) {\n>>> +                     return g_eventfd;\n>>> +             })\n>>> +\n>>> +             .def(\"getReadyRequests\", [](CameraManager &) {\n>>> +                     vector<Request *> v;\n>>> +\n>>> +                     {\n>>> +                             lock_guard guard(g_reqlist_mutex);\n>>> +                             swap(v, g_reqlist);\n>>> +                     }\n>>> +\n>>> +                     vector<py::object> ret;\n>>> +\n>>> +                     for (Request *req : v) {\n>>> +                             py::object o = py::cast(req);\n>>> +                             /* decrease the ref increased in Camera::queueRequest() */\n>>> +                             o.dec_ref();\n>>> +                             ret.push_back(o);\n>>> +                     }\n>>> +\n>>> +                     return ret;\n>>> +             })\n>>> +\n>>> +             .def(\"get\", py::overload_cast<const string &>(&CameraManager::get), py::keep_alive<0, 1>())\n>>> +\n>>> +             .def(\"find\", [](CameraManager &self, string str) {\n>>> +                     std::transform(str.begin(), str.end(), str.begin(), ::tolower);\n>>> +\n>>> +                     for (auto c : self.cameras()) {\n>>> +                             string id = c->id();\n>>> +\n>>> +                             std::transform(id.begin(), id.end(), id.begin(), ::tolower);\n>>> +\n>>> +                             if (id.find(str) != string::npos)\n>>> +                                     return c;\n>>> +                     }\n>>> +\n>>> +                     return shared_ptr<Camera>();\n>>> +             }, py::keep_alive<0, 1>())\n>>> +\n>>> +             /* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\n>>> +             .def_property_readonly(\"cameras\", [](CameraManager &self) {\n>>> +                     py::list l;\n>>> +\n>>> +                     for (auto &c : self.cameras()) {\n>>> +                             py::object py_cm = py::cast(self);\n>>> +                             py::object py_cam = py::cast(c);\n>>> +                             py::detail::keep_alive_impl(py_cam, py_cm);\n>>> +                             l.append(py_cam);\n>>> +                     }\n>>> +\n>>> +                     return l;\n>>> +             });\n>>> +\n>>> +     pyCamera\n>>> +             .def_property_readonly(\"id\", &Camera::id)\n>>> +             .def(\"acquire\", &Camera::acquire)\n>>> +             .def(\"release\", &Camera::release)\n>>> +             .def(\"start\", [](Camera &self) {\n>>> +                     self.requestCompleted.connect(handleRequestCompleted);\n>>> +\n>>> +                     int ret = self.start();\n>>> +                     if (ret)\n>>> +                             self.requestCompleted.disconnect(handleRequestCompleted);\n>>> +\n>>> +                     return ret;\n>>> +             })\n>>> +\n>>> +             .def(\"stop\", [](Camera &self) {\n>>> +                     int ret = self.stop();\n>>> +                     if (!ret)\n>>> +                             self.requestCompleted.disconnect(handleRequestCompleted);\n>>> +\n>>> +                     return ret;\n>>> +             })\n>>> +\n>>> +             .def(\"__repr__\", [](Camera &self) {\n>>> +                     return \"<libcamera.Camera '\" + self.id() + \"'>\";\n>>> +             })\n>>> +\n>>> +             /* Keep the camera alive, as StreamConfiguration contains a Stream* */\n>>> +             .def(\"generateConfiguration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\n>>> +             .def(\"configure\", &Camera::configure)\n>>> +\n>>> +             .def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0)\n>>> +\n>>> +             .def(\"queueRequest\", [](Camera &self, Request *req) {\n>>> +                     py::object py_req = py::cast(req);\n>>> +\n>>> +                     py_req.inc_ref();\n>>> +\n>>> +                     int ret = self.queueRequest(req);\n>>> +                     if (ret)\n>>> +                             py_req.dec_ref();\n>>> +\n>>> +                     return ret;\n>>> +             })\n>>> +\n>>> +             .def_property_readonly(\"streams\", [](Camera &self) {\n>>> +                     py::set set;\n>>> +                     for (auto &s : self.streams()) {\n>>> +                             py::object py_self = py::cast(self);\n>>> +                             py::object py_s = py::cast(s);\n>>> +                             py::detail::keep_alive_impl(py_s, py_self);\n>>> +                             set.add(py_s);\n>>> +                     }\n>>> +                     return set;\n>>> +             })\n>>> +\n>>> +             .def(\"find_control\", [](Camera &self, const string &name) {\n>>> +                     const auto &controls = self.controls();\n>>> +\n>>> +                     auto it = find_if(controls.begin(), controls.end(),\n>>> +                                       [&name](const auto &kvp) { return kvp.first->name() == name; });\n>>> +\n>>> +                     if (it == controls.end())\n>>> +                             throw runtime_error(\"Control not found\");\n>>> +\n>>> +                     return it->first;\n>>> +             }, py::return_value_policy::reference_internal)\n>>> +\n>>> +             .def_property_readonly(\"controls\", [](Camera &self) {\n>>> +                     py::dict ret;\n>>> +\n>>> +                     for (const auto &[id, ci] : self.controls()) {\n>>> +                             ret[id->name().c_str()] = make_tuple<py::object>(ControlValueToPy(ci.min()),\n>>> +                                                                              ControlValueToPy(ci.max()),\n>>> +                                                                              ControlValueToPy(ci.def()));\n>>> +                     }\n>>> +\n>>> +                     return ret;\n>>> +             })\n>>> +\n>>> +             .def_property_readonly(\"properties\", [](Camera &self) {\n>>> +                     py::dict ret;\n>>> +\n>>> +                     for (const auto &[key, cv] : self.properties()) {\n>>> +                             const ControlId *id = properties::properties.at(key);\n>>> +                             py::object ob = ControlValueToPy(cv);\n>>> +\n>>> +                             ret[id->name().c_str()] = ob;\n>>> +                     }\n>>> +\n>>> +                     return ret;\n>>> +             });\n>>> +\n>>> +     pyCameraConfiguration\n>>> +             .def(\"__iter__\", [](CameraConfiguration &self) {\n>>> +                     return py::make_iterator<py::return_value_policy::reference_internal>(self);\n>>> +             }, py::keep_alive<0, 1>())\n>>> +             .def(\"__len__\", [](CameraConfiguration &self) {\n>>> +                     return self.size();\n>>> +             })\n>>> +             .def(\"validate\", &CameraConfiguration::validate)\n>>> +             .def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at), py::return_value_policy::reference_internal)\n>>> +             .def_property_readonly(\"size\", &CameraConfiguration::size)\n>>> +             .def_property_readonly(\"empty\", &CameraConfiguration::empty);\n>>> +\n>>> +     pyStreamConfiguration\n>>> +             .def(\"toString\", &StreamConfiguration::toString)\n>>> +             .def_property_readonly(\"stream\", &StreamConfiguration::stream, py::return_value_policy::reference_internal)\n>>> +             .def_property(\n>>> +                     \"size\",\n>>> +                     [](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },\n>>> +                     [](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })\n>>> +             .def_property(\n>>> +                     \"pixelFormat\",\n>>> +                     [](StreamConfiguration &self) { return self.pixelFormat.toString(); },\n>>> +                     [](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })\n>>> +             .def_readwrite(\"stride\", &StreamConfiguration::stride)\n>>> +             .def_readwrite(\"frameSize\", &StreamConfiguration::frameSize)\n>>> +             .def_readwrite(\"bufferCount\", &StreamConfiguration::bufferCount)\n>>> +             .def_property_readonly(\"formats\", &StreamConfiguration::formats, py::return_value_policy::reference_internal);\n>>> +     ;\n> \n> Is this a stray ; ?\n> \n> How does checkstyle cope with all of this? does clang-format get\n> anywhere close?\n> \n> I expect the syntax and chaining probably breaks a lot of assumptions\n> used by clang-format :-(\n\nRight... I have never gotten clang-format to do exactly what I want. But \nthis ; looks extra. I think I originally used a style where the chain \nends with a ; on its own line like here.\n\n>>> +\n>>> +     pyStreamFormats\n>>> +             .def_property_readonly(\"pixelFormats\", [](StreamFormats &self) {\n>>> +                     vector<string> fmts;\n>>> +                     for (auto &fmt : self.pixelformats())\n>>> +                             fmts.push_back(fmt.toString());\n>>> +                     return fmts;\n>>> +             })\n>>> +             .def(\"sizes\", [](StreamFormats &self, const string &pixelFormat) {\n>>> +                     auto fmt = PixelFormat::fromString(pixelFormat);\n>>> +                     vector<tuple<uint32_t, uint32_t>> fmts;\n>>> +                     for (const auto &s : self.sizes(fmt))\n>>> +                             fmts.push_back(make_tuple(s.width, s.height));\n>>> +                     return fmts;\n>>> +             })\n>>> +             .def(\"range\", [](StreamFormats &self, const string &pixelFormat) {\n>>> +                     auto fmt = PixelFormat::fromString(pixelFormat);\n>>> +                     const auto &range = self.range(fmt);\n>>> +                     return make_tuple(make_tuple(range.hStep, range.vStep),\n>>> +                                       make_tuple(range.min.width, range.min.height),\n>>> +                                       make_tuple(range.max.width, range.max.height));\n>>> +             });\n>>> +\n>>> +     pyFrameBufferAllocator\n>>> +             .def(py::init<shared_ptr<Camera>>(), py::keep_alive<1, 2>())\n>>> +             .def(\"allocate\", &FrameBufferAllocator::allocate)\n>>> +             .def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\n>>> +             /* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */\n>>> +             .def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\n>>> +                     py::object py_self = py::cast(self);\n>>> +                     py::list l;\n>>> +                     for (auto &ub : self.buffers(stream)) {\n>>> +                             py::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);\n>>> +                             l.append(py_buf);\n>>> +                     }\n>>> +                     return l;\n>>> +             });\n>>> +\n>>> +     pyFrameBuffer\n>>> +             /* TODO: implement FrameBuffer::Plane properly */\n>>> +             .def(py::init([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {\n>>> +                     vector<FrameBuffer::Plane> v;\n>>> +                     for (const auto &t : planes)\n>>> +                             v.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, get<1>(t) });\n>>> +                     return new FrameBuffer(v, cookie);\n>>> +             }))\n>>> +             .def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\n>>> +             .def(\"length\", [](FrameBuffer &self, uint32_t idx) {\n>>> +                     const FrameBuffer::Plane &plane = self.planes()[idx];\n>>> +                     return plane.length;\n>>> +             })\n>>> +             .def(\"fd\", [](FrameBuffer &self, uint32_t idx) {\n>>> +                     const FrameBuffer::Plane &plane = self.planes()[idx];\n>>> +                     return plane.fd.get();\n>>> +             })\n>>> +             .def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\n>>> +\n>>> +     pyStream\n>>> +             .def_property_readonly(\"configuration\", &Stream::configuration);\n>>> +\n>>> +     pyControlId\n>>> +             .def_property_readonly(\"id\", &ControlId::id)\n>>> +             .def_property_readonly(\"name\", &ControlId::name)\n>>> +             .def_property_readonly(\"type\", &ControlId::type);\n>>> +\n>>> +     pyRequest\n>>> +             /* Fence is not supported, so we cannot expose addBuffer() directly */\n>>> +             .def(\"addBuffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n>>> +                     return self.addBuffer(stream, buffer);\n>>> +             }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n>>> +             .def_property_readonly(\"status\", &Request::status)\n>>> +             .def_property_readonly(\"buffers\", &Request::buffers)\n>>> +             .def_property_readonly(\"cookie\", &Request::cookie)\n>>> +             .def_property_readonly(\"hasPendingBuffers\", &Request::hasPendingBuffers)\n>>> +             .def(\"set_control\", [](Request &self, ControlId& id, py::object value) {\n>>> +                     self.controls().set(id.id(), PyToControlValue(value, id.type()));\n>>> +             })\n>>\n>> I see a mixture of camel case (\"addBuffer\") and snake case\n>> (\"set_controls\"). If there's a preferred coding style for Python\n>> should we uniform on it ?\n>>\n>> Minor comments apart, there's a thing which is missing:\n>> support for setting controls that accept a Span<> of values.\n>>\n>> I tried several solutions and I got the following to compile\n>>\n>>          .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n>>                  py::bytearray bytes = py::bytearray(values);\n>>                  py::buffer_info info(py::buffer(bytes).request());\n>>\n>>                  const int *data = reinterpret_cast<const int *>(info.ptr);\n>>                  size_t length = static_cast<size_t>(info.size);\n>>\n>>                  self.controls().set(id.id(), Span<const int>{data, length});\n>>           })\n>>\n>> (All types in casts<> should depend on id.type(), but that's for later).\n>>\n>> Unfortunately, while length is correct, the values I access with\n>> 'data' seems invalid, which makes me think\n>>\n>>                  py::bytearray bytes = py::bytearray(values);\n>>\n>> Dosn't do what I think it does, or maybe Tuple in Python are simply\n>> not backed by a contigous memory buffer , hence there's no way to wrap\n>> their memory in a Span<>).\n>>\n>> Please note I also tried to instrument:\n>>\n>>          .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n>>\n>> Relying on pybind11 type casting, and it seems to work, but I see two issues:\n>>\n>> 1) Converting from python to C++ types goes through a copy.\n>> Performances reasons apart, the vector lifetime is limited to the\n>> function scope. This isn't be a problem for now, as control\n>> values are copied in the Request's control list, but that would\n>> prevent passing controls values as pointers, if a control transports a\n>> large chunk of data (ie gamma tables). Not sure we'll ever want this\n>> (I don't think so as it won't play well with serialization between the\n>> IPA and the pipeline handler) but you never know.\n> \n> I don't think we could ever let an application pass a pointer to an\n> arbitrary location into libcamera for us to parse.\n> \n>> 2) my understanding is that python does not support methods signature\n>> overloading, hence we would have\n>>\n>>          .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n>>          .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n>>          ....\n>>\n>> there are surely smart ways to handle this, but in my quick experiment\n>> I haven't found one yet :)\n> \n> Given how dynamically typed python is I would expect there would be some\n> form of wrapping here that could be done?\n\nBut the C++ templates are not dynamic, so we have to have all the \npossible Span<T> variations here somehow so that we get the code for them.\n\n>> David: Is the issue with Span<> controls addressed by picamera2 ?\n>>\n>> Thanks, I hope we can merge this soon!\n> \n> +1 ..\n> \n>>\n>>\n>>> +             .def_property_readonly(\"metadata\", [](Request &self) {\n>>> +                     py::dict ret;\n>>> +\n>>> +                     for (const auto &[key, cv] : self.metadata()) {\n>>> +                             const ControlId *id = controls::controls.at(key);\n>>> +                             py::object ob = ControlValueToPy(cv);\n>>> +\n>>> +                             ret[id->name().c_str()] = ob;\n>>> +                     }\n>>> +\n>>> +                     return ret;\n>>> +             })\n>>> +             /* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */\n>>> +             .def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\n>>> +\n>>> +     pyFrameMetadata\n>>> +             .def_readonly(\"status\", &FrameMetadata::status)\n>>> +             .def_readonly(\"sequence\", &FrameMetadata::sequence)\n>>> +             .def_readonly(\"timestamp\", &FrameMetadata::timestamp)\n>>> +             /* temporary helper, to be removed */\n>>> +             .def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n>>> +                     vector<unsigned int> v;\n>>> +                     v.resize(self.planes().size());\n>>> +                     transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\n>>> +                     return v;\n>>> +             });\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..757bb072 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/pybind11.wrap b/subprojects/pybind11.wrap\n>>> new file mode 100644\n>>> index 00000000..ebf942ff\n>>> --- /dev/null\n>>> +++ b/subprojects/pybind11.wrap\n>>> @@ -0,0 +1,6 @@\n>>> +[wrap-git]\n>>> +url = https://github.com/tomba/pybind11.git\n>>> +revision = smart_holder\n> \n> Can this point to the upstream branch? or have you made local\n> modifications to the branch?\n\nI add meson build support. Possibly that could be done with a suitable \nmeson wrap file to fetch the upstream smart_holder branch as the main \nrepo, and get the patch from my repo.\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 BDBBCC3260\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 13 Apr 2022 09:10:51 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 0991965645;\n\tWed, 13 Apr 2022 11:10:51 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 1649B604B3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Apr 2022 11:10:50 +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 4E50C25B;\n\tWed, 13 Apr 2022 11:10:49 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649841051;\n\tbh=GNvFd42Zw1OjQDIK5kZwJwTa0biuIGiSfTw+BfcPdxk=;\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=sEZZA7jINllE+WzmU6VzD9V1lFj8+agNVZLDU0ESAKAxMdJkkcVsmgjDOy/I+6qZt\n\tHrNHiF6K81znJDo+rhOgCJl1Z6E/qK9qqA8q+q3xinc8eOF/SMi/QEUUYW47XFF5RU\n\tyJchhC+9dWkV1Wk2+/IHonMYGOmggX7nfINFKtnkBYAUKQoYs3mXu2KLkOpbmswSbC\n\tUzQnce8lCl83tB/ttV2ASIPyXvPpZQ/OoC5b+M3kt2kScL0h2jJwWklrf4ZSlJXIr0\n\tUyOOAFCzK0hd74eH+BVwSAeS+kGRhkL5v0rPLXdx3T6J6fQLoBMqScViNf5HsQB7bJ\n\tnVlKjYXhhWduA==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1649841049;\n\tbh=GNvFd42Zw1OjQDIK5kZwJwTa0biuIGiSfTw+BfcPdxk=;\n\th=Date:Subject:To:Cc:References:From:In-Reply-To:From;\n\tb=eU1HUE3IW7Aq8RRMPgY54Chs5an+1cqi7RZ/sTGhWqREL6C5s3wehHlBygjO9Qpe2\n\t4zBJbI+L2Gb5A4IqZ4rN6MCvHB3WD7vC3oEPqQtapWk5ba/17y3SkOR6QuQBSLYQJY\n\tPRu5PdOFLNCXHqE74blWnz9g75Iy0NZM2UJV+Gc0="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"eU1HUE3I\"; dkim-atps=neutral","Message-ID":"<5d3e9b08-bdc7-9d43-904c-22706e54f0ed@ideasonboard.com>","Date":"Wed, 13 Apr 2022 12:10:45 +0300","MIME-Version":"1.0","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101\n\tThunderbird/91.7.0","Content-Language":"en-US","To":"Kieran Bingham <kieran.bingham@ideasonboard.com>,\n\tJacopo Mondi <jacopo@jmondi.org>","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>\n\t<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>\n\t<164983663775.22830.12723401575260170305@Monstersaurus>","In-Reply-To":"<164983663775.22830.12723401575260170305@Monstersaurus>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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":22696,"web_url":"https://patchwork.libcamera.org/comment/22696/","msgid":"<20220413095326.x3o66cc4f6zptxuc@uno.localdomain>","date":"2022-04-13T09:53:26","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":3,"url":"https://patchwork.libcamera.org/api/people/3/","name":"Jacopo Mondi","email":"jacopo@jmondi.org"},"content":"Hi\n\nOn Wed, Apr 13, 2022 at 12:10:45PM +0300, Tomi Valkeinen wrote:\n> On 13/04/2022 10:57, Kieran Bingham wrote:\n> > Quoting Jacopo Mondi (2022-04-12 18:49:53)\n> > > Hi Tomi,\n> > >     I've been using the bindings in the last days, they're nice to\n> > > work! Great job!\n> > >\n> > > Once the question about a Request belonging to a Camera is\n> > > clarified I think we should merge these soon, even if incomplete, to\n> > > build on top.\n> > >\n> > > My understanding of python is very limited so I have just a few minor\n> > > comments and one larger question about controls.\n> > >\n> > > On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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> > Why is the 'smart_holder' a branch of pybind11 ? I can't see a PR or\n> > anything such that indicates how or when it might get merged.\n> >\n\n[snip]\n\n> > > > +\n> > > > +     pyRequest\n> > > > +             /* Fence is not supported, so we cannot expose addBuffer() directly */\n> > > > +             .def(\"addBuffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n> > > > +                     return self.addBuffer(stream, buffer);\n> > > > +             }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n> > > > +             .def_property_readonly(\"status\", &Request::status)\n> > > > +             .def_property_readonly(\"buffers\", &Request::buffers)\n> > > > +             .def_property_readonly(\"cookie\", &Request::cookie)\n> > > > +             .def_property_readonly(\"hasPendingBuffers\", &Request::hasPendingBuffers)\n> > > > +             .def(\"set_control\", [](Request &self, ControlId& id, py::object value) {\n> > > > +                     self.controls().set(id.id(), PyToControlValue(value, id.type()));\n> > > > +             })\n> > >\n> > > Minor comments apart, there's a thing which is missing:\n> > > support for setting controls that accept a Span<> of values.\n> > >\n> > > I tried several solutions and I got the following to compile\n> > >\n> > >          .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n> > >                  py::bytearray bytes = py::bytearray(values);\n> > >                  py::buffer_info info(py::buffer(bytes).request());\n> > >\n> > >                  const int *data = reinterpret_cast<const int *>(info.ptr);\n> > >                  size_t length = static_cast<size_t>(info.size);\n> > >\n> > >                  self.controls().set(id.id(), Span<const int>{data, length});\n> > >           })\n> > >\n> > > (All types in casts<> should depend on id.type(), but that's for later).\n> > >\n> > > Unfortunately, while length is correct, the values I access with\n> > > 'data' seems invalid, which makes me think\n> > >\n> > >                  py::bytearray bytes = py::bytearray(values);\n> > >\n> > > Dosn't do what I think it does, or maybe Tuple in Python are simply\n> > > not backed by a contigous memory buffer , hence there's no way to wrap\n> > > their memory in a Span<>).\n> > >\n> > > Please note I also tried to instrument:\n> > >\n> > >          .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n> > >\n> > > Relying on pybind11 type casting, and it seems to work, but I see two issues:\n> > >\n> > > 1) Converting from python to C++ types goes through a copy.\n> > > Performances reasons apart, the vector lifetime is limited to the\n> > > function scope. This isn't be a problem for now, as control\n> > > values are copied in the Request's control list, but that would\n> > > prevent passing controls values as pointers, if a control transports a\n> > > large chunk of data (ie gamma tables). Not sure we'll ever want this\n> > > (I don't think so as it won't play well with serialization between the\n> > > IPA and the pipeline handler) but you never know.\n> >\n> > I don't think we could ever let an application pass a pointer to an\n> > arbitrary location into libcamera for us to parse.\n> >\n> > > 2) my understanding is that python does not support methods signature\n> > > overloading, hence we would have\n> > >\n> > >          .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n> > >          .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n> > >          ....\n> > >\n> > > there are surely smart ways to handle this, but in my quick experiment\n> > > I haven't found one yet :)\n> >\n> > Given how dynamically typed python is I would expect there would be some\n> > form of wrapping here that could be done?\n>\n> But the C++ templates are not dynamic, so we have to have all the possible\n> Span<T> variations here somehow so that we get the code for them.\n>\n> > > David: Is the issue with Span<> controls addressed by picamera2 ?\n\nI have tested with picamera2, and this commit allows the usage of\ncontrols which accepts multiple values\n\nhttps://github.com/raspberrypi/libcamera/commit/43cbfd5289aedafa40758312e944db13a5afdf14\n\nand it works here.\n\nIt goes through a forceful ob.cast<vector>() which goes through a copy\nhttps://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html\n\nI had looked a bit into using PYBIND11_MAKE_OPAQUE() to define a\ncustom caster, but if the raw memory of a Python Tuple cannot be\naccessed as a contiguous block, it won't help much.\n\nIs this something that can be brought in already in your next series\nTomi ?\n\nThanks\n  j\n\n> > >\n> > > Thanks, I hope we can merge this soon!\n> >\n> > +1 ..\n> >\n> > >\n> > >\n> > > > +             .def_property_readonly(\"metadata\", [](Request &self) {\n> > > > +                     py::dict ret;\n> > > > +\n> > > > +                     for (const auto &[key, cv] : self.metadata()) {\n> > > > +                             const ControlId *id = controls::controls.at(key);\n> > > > +                             py::object ob = ControlValueToPy(cv);\n> > > > +\n> > > > +                             ret[id->name().c_str()] = ob;\n> > > > +                     }\n> > > > +\n> > > > +                     return ret;\n> > > > +             })\n> > > > +             /* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */\n> > > > +             .def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\n> > > > +\n> > > > +     pyFrameMetadata\n> > > > +             .def_readonly(\"status\", &FrameMetadata::status)\n> > > > +             .def_readonly(\"sequence\", &FrameMetadata::sequence)\n> > > > +             .def_readonly(\"timestamp\", &FrameMetadata::timestamp)\n> > > > +             /* temporary helper, to be removed */\n> > > > +             .def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\n> > > > +                     vector<unsigned int> v;\n> > > > +                     v.resize(self.planes().size());\n> > > > +                     transform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\n> > > > +                     return v;\n> > > > +             });\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..757bb072 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/pybind11.wrap b/subprojects/pybind11.wrap\n> > > > new file mode 100644\n> > > > index 00000000..ebf942ff\n> > > > --- /dev/null\n> > > > +++ b/subprojects/pybind11.wrap\n> > > > @@ -0,0 +1,6 @@\n> > > > +[wrap-git]\n> > > > +url = https://github.com/tomba/pybind11.git\n> > > > +revision = smart_holder\n> >\n> > Can this point to the upstream branch? or have you made local\n> > modifications to the branch?\n>\n> I add meson build support. Possibly that could be done with a suitable meson\n> wrap file to fetch the upstream smart_holder branch as the main repo, and\n> get the patch from my repo.\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 32809C326D\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 13 Apr 2022 09:53:33 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 7D8DA6563F;\n\tWed, 13 Apr 2022 11:53:32 +0200 (CEST)","from relay12.mail.gandi.net (relay12.mail.gandi.net\n\t[IPv6:2001:4b98:dc4:8::232])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id A8EFA65646\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Apr 2022 11:53:29 +0200 (CEST)","(Authenticated sender: jacopo@jmondi.org)\n\tby mail.gandi.net (Postfix) with ESMTPSA id 2EDBB200012;\n\tWed, 13 Apr 2022 09:53:27 +0000 (UTC)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649843612;\n\tbh=h1wpEV2xB1jnFjcI8N2ORc+CHNFf5st4BCNqY4fd17A=;\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=UgKQ/IhQLX3HoNJfjFq3hdhkQRYwSk0p1fn0hBml0lC92uepMJzKuW/pQcurWpok1\n\tPFSL4jYeFxdIVMx85rW2OJydlZo9EidjKiJ+p5oPaiQJ2C1XEAwnNUZ/t+AzxJRvyw\n\tyTK0nlGQtn0C5Q/Y0fQEZeCmYx2eH+H6VEYws8CzYUuV3rAT0eq0L+l2zcKX2Rc9y7\n\tYmOmdpsZnfc3IP6c4djEYYLxjREE044mDq0xPKG7TRl1qJsCqkTjcJKCtAX7Gr0T7Y\n\t4LxrkBH/GopMdwGMLllWslXlspYNdGWuSe7gANoi1BEdQgkrjGcoaYdMKeZOva5yir\n\tvqIl7ok+9FjtA==","Date":"Wed, 13 Apr 2022 11:53:26 +0200","To":"Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>","Message-ID":"<20220413095326.x3o66cc4f6zptxuc@uno.localdomain>","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>\n\t<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>\n\t<164983663775.22830.12723401575260170305@Monstersaurus>\n\t<5d3e9b08-bdc7-9d43-904c-22706e54f0ed@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<5d3e9b08-bdc7-9d43-904c-22706e54f0ed@ideasonboard.com>","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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":"Jacopo Mondi via libcamera-devel <libcamera-devel@lists.libcamera.org>","Reply-To":"Jacopo Mondi <jacopo@jmondi.org>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":22697,"web_url":"https://patchwork.libcamera.org/comment/22697/","msgid":"<a001074c-7427-f5c3-ad4a-045f5a1f088a@ideasonboard.com>","date":"2022-04-13T10:01:39","subject":"Re: [libcamera-devel] [PATCH v5 1/3] Add Python bindings","submitter":{"id":109,"url":"https://patchwork.libcamera.org/api/people/109/","name":"Tomi Valkeinen","email":"tomi.valkeinen@ideasonboard.com"},"content":"On 13/04/2022 12:53, Jacopo Mondi wrote:\n> Hi\n> \n> On Wed, Apr 13, 2022 at 12:10:45PM +0300, Tomi Valkeinen wrote:\n>> On 13/04/2022 10:57, Kieran Bingham wrote:\n>>> Quoting Jacopo Mondi (2022-04-12 18:49:53)\n>>>> Hi Tomi,\n>>>>      I've been using the bindings in the last days, they're nice to\n>>>> work! Great job!\n>>>>\n>>>> Once the question about a Request belonging to a Camera is\n>>>> clarified I think we should merge these soon, even if incomplete, to\n>>>> build on top.\n>>>>\n>>>> My understanding of python is very limited so I have just a few minor\n>>>> comments and one larger question about controls.\n>>>>\n>>>> On Mon, Mar 14, 2022 at 05:46:31PM +0200, Tomi Valkeinen via libcamera-devel 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>>> Why is the 'smart_holder' a branch of pybind11 ? I can't see a PR or\n>>> anything such that indicates how or when it might get merged.\n>>>\n> \n> [snip]\n> \n>>>>> +\n>>>>> +     pyRequest\n>>>>> +             /* Fence is not supported, so we cannot expose addBuffer() directly */\n>>>>> +             .def(\"addBuffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\n>>>>> +                     return self.addBuffer(stream, buffer);\n>>>>> +             }, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\n>>>>> +             .def_property_readonly(\"status\", &Request::status)\n>>>>> +             .def_property_readonly(\"buffers\", &Request::buffers)\n>>>>> +             .def_property_readonly(\"cookie\", &Request::cookie)\n>>>>> +             .def_property_readonly(\"hasPendingBuffers\", &Request::hasPendingBuffers)\n>>>>> +             .def(\"set_control\", [](Request &self, ControlId& id, py::object value) {\n>>>>> +                     self.controls().set(id.id(), PyToControlValue(value, id.type()));\n>>>>> +             })\n>>>>\n>>>> Minor comments apart, there's a thing which is missing:\n>>>> support for setting controls that accept a Span<> of values.\n>>>>\n>>>> I tried several solutions and I got the following to compile\n>>>>\n>>>>           .def(\"set_control_array\", [](Request &self, ControlId& id, py::tuple &values) {\n>>>>                   py::bytearray bytes = py::bytearray(values);\n>>>>                   py::buffer_info info(py::buffer(bytes).request());\n>>>>\n>>>>                   const int *data = reinterpret_cast<const int *>(info.ptr);\n>>>>                   size_t length = static_cast<size_t>(info.size);\n>>>>\n>>>>                   self.controls().set(id.id(), Span<const int>{data, length});\n>>>>            })\n>>>>\n>>>> (All types in casts<> should depend on id.type(), but that's for later).\n>>>>\n>>>> Unfortunately, while length is correct, the values I access with\n>>>> 'data' seems invalid, which makes me think\n>>>>\n>>>>                   py::bytearray bytes = py::bytearray(values);\n>>>>\n>>>> Dosn't do what I think it does, or maybe Tuple in Python are simply\n>>>> not backed by a contigous memory buffer , hence there's no way to wrap\n>>>> their memory in a Span<>).\n>>>>\n>>>> Please note I also tried to instrument:\n>>>>\n>>>>           .def(\"set_control_array\", [](Request &self, ControlId& id, vector<int> values)\n>>>>\n>>>> Relying on pybind11 type casting, and it seems to work, but I see two issues:\n>>>>\n>>>> 1) Converting from python to C++ types goes through a copy.\n>>>> Performances reasons apart, the vector lifetime is limited to the\n>>>> function scope. This isn't be a problem for now, as control\n>>>> values are copied in the Request's control list, but that would\n>>>> prevent passing controls values as pointers, if a control transports a\n>>>> large chunk of data (ie gamma tables). Not sure we'll ever want this\n>>>> (I don't think so as it won't play well with serialization between the\n>>>> IPA and the pipeline handler) but you never know.\n>>>\n>>> I don't think we could ever let an application pass a pointer to an\n>>> arbitrary location into libcamera for us to parse.\n>>>\n>>>> 2) my understanding is that python does not support methods signature\n>>>> overloading, hence we would have\n>>>>\n>>>>           .def(\"set_control_array_int\", [](Request &self, ControlId& id, vector<int> values)\n>>>>           .def(\"set_control_array_float\", [](Request &self, ControlId& id, vector<float> values)\n>>>>           ....\n>>>>\n>>>> there are surely smart ways to handle this, but in my quick experiment\n>>>> I haven't found one yet :)\n>>>\n>>> Given how dynamically typed python is I would expect there would be some\n>>> form of wrapping here that could be done?\n>>\n>> But the C++ templates are not dynamic, so we have to have all the possible\n>> Span<T> variations here somehow so that we get the code for them.\n>>\n>>>> David: Is the issue with Span<> controls addressed by picamera2 ?\n> \n> I have tested with picamera2, and this commit allows the usage of\n> controls which accepts multiple values\n> \n> https://github.com/raspberrypi/libcamera/commit/43cbfd5289aedafa40758312e944db13a5afdf14\n> \n> and it works here.\n> \n> It goes through a forceful ob.cast<vector>() which goes through a copy\n> https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html\n> \n> I had looked a bit into using PYBIND11_MAKE_OPAQUE() to define a\n> custom caster, but if the raw memory of a Python Tuple cannot be\n> accessed as a contiguous block, it won't help much.\n> \n> Is this something that can be brought in already in your next series\n> Tomi ?\n\nI really don't like having exceptions thrown when everything goes fine. \nUnfortunately I haven't had time to look at how to do this properly.\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 966FAC0F1B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 13 Apr 2022 10:01:45 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 04EF96563E;\n\tWed, 13 Apr 2022 12:01:44 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B8D06604B4\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 13 Apr 2022 12:01:42 +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 CEE0725B;\n\tWed, 13 Apr 2022 12:01:41 +0200 (CEST)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1649844105;\n\tbh=bnPzGqti6IQzaA+/ASx9JoIzZ2ew9K94irhZwuCWg/g=;\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=xnShKjPgZQc6UkdzxvCkekttfknjpcsouM8Vkc6D2Dw/N13q8j8TOzVpPEnKbUleD\n\t71zJ9OVaUCh+rLWodYKRZ14JDQtqdBNoiRP1axjeeHKgmAR/oVKzetqCyL3JlQOcB6\n\tG5Vx+XRzRrYsKc67n4SuWZYOE66S/61zWqu29+aGWmbUbGvgrUSASJSKJ2MFBUMyAT\n\t0QXytxJa/HfAGmU2w718XooiC0g1Tp0Kr2Tx4fE6Hh8N+Q9NAWRwKtNt6ZX79F92+T\n\tHO6kD8D31kQUbtUTFfFAFNIl26KtO/AXtY0Yh7Rw2b8+SAVSzFuyJdx4mdTYmvKKRM\n\tHk04hblhA5fkg==","v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1649844102;\n\tbh=bnPzGqti6IQzaA+/ASx9JoIzZ2ew9K94irhZwuCWg/g=;\n\th=Date:Subject:To:Cc:References:From:In-Reply-To:From;\n\tb=mppz6a9G+E07JOOmD3M8p9su3b1aNihntFRxi5RIfUsKFRpCbjhuGMbNKBa6rg+ri\n\tNTnnueIa3fmPRcGFkIBbjA4+ESXWBlvisK/jUOf5d+i4/DS/ak7l7g2hWF7BJQdMHg\n\t2x2ASTwhj0DnIoIiPIcPrIrtsQVRf5JFV07p1JQE="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"mppz6a9G\"; dkim-atps=neutral","Message-ID":"<a001074c-7427-f5c3-ad4a-045f5a1f088a@ideasonboard.com>","Date":"Wed, 13 Apr 2022 13:01:39 +0300","MIME-Version":"1.0","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101\n\tThunderbird/91.7.0","Content-Language":"en-US","To":"Jacopo Mondi <jacopo@jmondi.org>","References":"<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>\n\t<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>\n\t<20220412174953.kmuv7dfyetx6xhvw@uno.localdomain>\n\t<164983663775.22830.12723401575260170305@Monstersaurus>\n\t<5d3e9b08-bdc7-9d43-904c-22706e54f0ed@ideasonboard.com>\n\t<20220413095326.x3o66cc4f6zptxuc@uno.localdomain>","In-Reply-To":"<20220413095326.x3o66cc4f6zptxuc@uno.localdomain>","Content-Type":"text/plain; charset=UTF-8; format=flowed","Content-Transfer-Encoding":"7bit","Subject":"Re: [libcamera-devel] [PATCH v5 1/3] 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>"}}]