Patch Detail
Show a patch.
GET /api/1.1/patches/15445/?format=api
{ "id": 15445, "url": "https://patchwork.libcamera.org/api/1.1/patches/15445/?format=api", "web_url": "https://patchwork.libcamera.org/patch/15445/", "project": { "id": 1, "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api", "name": "libcamera", "link_name": "libcamera", "list_id": "libcamera_core", "list_email": "libcamera-devel@lists.libcamera.org", "web_url": "", "scm_url": "", "webscm_url": "" }, "msgid": "<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>", "date": "2022-03-14T15:46:31", "name": "[libcamera-devel,v5,1/3] Add Python bindings", "commit_ref": null, "pull_url": null, "state": "accepted", "archived": false, "hash": "ce00e2b7a6821c5a11b63011faacb8a7b1f5007e", "submitter": { "id": 109, "url": "https://patchwork.libcamera.org/api/1.1/people/109/?format=api", "name": "Tomi Valkeinen", "email": "tomi.valkeinen@ideasonboard.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/15445/mbox/", "series": [ { "id": 2960, "url": "https://patchwork.libcamera.org/api/1.1/series/2960/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=2960", "date": "2022-03-14T15:46:30", "name": "Python bindings", "version": 5, "mbox": "https://patchwork.libcamera.org/series/2960/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/15445/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/15445/checks/", "tags": {}, "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\r\n\t[92.243.16.209])\r\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 44091BF415\r\n\tfor <parsemail@patchwork.libcamera.org>;\r\n\tMon, 14 Mar 2022 15:47:00 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\r\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id CD3B8632E6;\r\n\tMon, 14 Mar 2022 16:46:58 +0100 (CET)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\r\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\r\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 41CD0604EA\r\n\tfor <libcamera-devel@lists.libcamera.org>;\r\n\tMon, 14 Mar 2022 16:46:57 +0100 (CET)", "from deskari.lan (91-156-85-209.elisa-laajakaista.fi\r\n\t[91.156.85.209])\r\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id A714230B;\r\n\tMon, 14 Mar 2022 16:46:56 +0100 (CET)" ], "DKIM-Signature": [ "v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\r\n\ts=mail; t=1647272818;\r\n\tbh=9h9puR1p8Ohv5Zh7QQicbwYZkzp92RG6dfnzmnpgXSE=;\r\n\th=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe:\r\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:\r\n\tFrom;\r\n\tb=Ex1OBjw0y8iRNbYQ2ijocqumL9Kx7g9za0mCgNivGUV8PbpHX1mlghdkvlKTk851i\r\n\taiNbzJVI/0v9EdQympg3PDvY+qcJkc9R/M6ZgOoHLhLgJpyeUSvXHKk1XhacAzVMdr\r\n\t0qF7TXGntbWLtaSe6r1pbQ4yviO/EV+urINnqGCOOmp7/CLffqbUYck6iwNnIlAxwS\r\n\tx9cT3B0Hh3OBoli5IW+UKvnK3bFPxJ0vkIZ/RH86iIBV+VriX39c7Sh3CsOYAo4LfY\r\n\tHDz/RZVRBZGqpDnAYxqSskaLX3Gj27Pqb627hXtWExt4f8ZTQt0cO7F8+5YUw/Zsou\r\n\tkeRg+ap0gO4gg==", "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\r\n\ts=mail; t=1647272817;\r\n\tbh=9h9puR1p8Ohv5Zh7QQicbwYZkzp92RG6dfnzmnpgXSE=;\r\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\r\n\tb=S1ZVHbEeSfxaLnVxrxmfvVPUahY5DjkVYNlMervpAuLjebRBns3mSfnE/E5W/+0Zh\r\n\t8hdeBaSmnZptKoXBzBf/oyNKC4q6Oyq/K0zHL90/0D+fsz+OZ9kXtoV5ymuB8mpBo8\r\n\tY28FXjCmeHMME0ajKNlHYaWiib13dcAUVPuOxAqM=" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key; \r\n\tunprotected) header.d=ideasonboard.com\r\n\theader.i=@ideasonboard.com\r\n\theader.b=\"S1ZVHbEe\"; dkim-atps=neutral", "To": "libcamera-devel@lists.libcamera.org,\r\n\tDavid Plowman <david.plowman@raspberrypi.com>,\r\n\tKieran Bingham <kieran.bingham@ideasonboard.com>,\r\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>", "Date": "Mon, 14 Mar 2022 17:46:31 +0200", "Message-Id": "<20220314154633.506026-2-tomi.valkeinen@ideasonboard.com>", "X-Mailer": "git-send-email 2.25.1", "In-Reply-To": "<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>", "References": "<20220314154633.506026-1-tomi.valkeinen@ideasonboard.com>", "MIME-Version": "1.0", "Content-Transfer-Encoding": "8bit", "Subject": "[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>,\r\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>,\r\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>", "From": "Tomi Valkeinen via libcamera-devel\r\n\t<libcamera-devel@lists.libcamera.org>", "Reply-To": "Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>", "Errors-To": "libcamera-devel-bounces@lists.libcamera.org", "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>" }, "content": "Add libcamera Python bindings. pybind11 is used to generate the C++ <->\nPython layer.\n\nWe use pybind11 'smart_holder' version to avoid issues with private\ndestructors and shared_ptr. There is also an alternative solution here:\n\nhttps://github.com/pybind/pybind11/pull/2067\n\nOnly a subset of libcamera classes are exposed. Implementing and testing\nthe wrapper classes is challenging, and as such only classes that I have\nneeded have been added so far.\n\nSigned-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", "diff": "diff --git a/meson.build b/meson.build\r\nindex 29d8542d..ff6c2ad6 100644\r\n--- a/meson.build\r\n+++ b/meson.build\r\n@@ -179,6 +179,7 @@ summary({\r\n 'Tracing support': tracing_enabled,\r\n 'Android support': android_enabled,\r\n 'GStreamer support': gst_enabled,\r\n+ 'Python bindings': pycamera_enabled,\r\n 'V4L2 emulation support': v4l2_enabled,\r\n 'cam application': cam_enabled,\r\n 'qcam application': qcam_enabled,\r\ndiff --git a/meson_options.txt b/meson_options.txt\r\nindex 2c80ad8b..ca00c78e 100644\r\n--- a/meson_options.txt\r\n+++ b/meson_options.txt\r\n@@ -58,3 +58,8 @@ option('v4l2',\r\n type : 'boolean',\r\n value : false,\r\n description : 'Compile the V4L2 compatibility layer')\r\n+\r\n+option('pycamera',\r\n+ type : 'feature',\r\n+ value : 'auto',\r\n+ description : 'Enable libcamera Python bindings (experimental)')\r\ndiff --git a/src/meson.build b/src/meson.build\r\nindex e0ea9c35..34663a6f 100644\r\n--- a/src/meson.build\r\n+++ b/src/meson.build\r\n@@ -37,4 +37,5 @@ subdir('cam')\r\n subdir('qcam')\r\n \r\n subdir('gstreamer')\r\n+subdir('py')\r\n subdir('v4l2')\r\ndiff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py\r\nnew file mode 100644\r\nindex 00000000..b30bf33a\r\n--- /dev/null\r\n+++ b/src/py/libcamera/__init__.py\r\n@@ -0,0 +1,10 @@\r\n+# SPDX-License-Identifier: LGPL-2.1-or-later\r\n+# Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\r\n+\r\n+from ._libcamera import *\r\n+import mmap\r\n+\r\n+def __FrameBuffer__mmap(self, plane):\r\n+\treturn mmap.mmap(self.fd(plane), self.length(plane), mmap.MAP_SHARED, mmap.PROT_READ)\r\n+\r\n+FrameBuffer.mmap = __FrameBuffer__mmap\r\ndiff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\r\nnew file mode 100644\r\nindex 00000000..82388efb\r\n--- /dev/null\r\n+++ b/src/py/libcamera/meson.build\r\n@@ -0,0 +1,43 @@\r\n+# SPDX-License-Identifier: CC0-1.0\r\n+\r\n+py3_dep = dependency('python3', required : get_option('pycamera'))\r\n+\r\n+if not py3_dep.found()\r\n+ pycamera_enabled = false\r\n+ subdir_done()\r\n+endif\r\n+\r\n+pycamera_enabled = true\r\n+\r\n+pybind11_proj = subproject('pybind11')\r\n+pybind11_dep = pybind11_proj.get_variable('pybind11_dep')\r\n+\r\n+pycamera_sources = files([\r\n+ 'pymain.cpp',\r\n+ 'pyenums.cpp',\r\n+])\r\n+\r\n+pycamera_deps = [\r\n+ libcamera_public,\r\n+ py3_dep,\r\n+ pybind11_dep,\r\n+]\r\n+\r\n+pycamera_args = ['-fvisibility=hidden']\r\n+pycamera_args += ['-Wno-shadow']\r\n+pycamera_args += ['-DPYBIND11_USE_SMART_HOLDER_AS_DEFAULT']\r\n+\r\n+destdir = get_option('libdir') + '/python' + py3_dep.version() + '/site-packages/libcamera'\r\n+\r\n+pycamera = shared_module('_libcamera',\r\n+ pycamera_sources,\r\n+ install : true,\r\n+ install_dir : destdir,\r\n+ name_prefix : '',\r\n+ dependencies : pycamera_deps,\r\n+ cpp_args : pycamera_args)\r\n+\r\n+run_command('ln', '-fsT', '../../../../src/py/libcamera/__init__.py',\r\n+ meson.current_build_dir() / '__init__.py')\r\n+\r\n+install_data(['__init__.py'], install_dir : destdir)\r\ndiff --git a/src/py/libcamera/pyenums.cpp b/src/py/libcamera/pyenums.cpp\r\nnew file mode 100644\r\nindex 00000000..af6151c8\r\n--- /dev/null\r\n+++ b/src/py/libcamera/pyenums.cpp\r\n@@ -0,0 +1,53 @@\r\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\r\n+/*\r\n+ * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\r\n+ *\r\n+ * Python bindings\r\n+ */\r\n+\r\n+#include <libcamera/libcamera.h>\r\n+\r\n+#include <pybind11/smart_holder.h>\r\n+\r\n+namespace py = pybind11;\r\n+\r\n+using namespace libcamera;\r\n+\r\n+void init_pyenums(py::module& m)\r\n+{\r\n+\tpy::enum_<CameraConfiguration::Status>(m, \"ConfigurationStatus\")\r\n+\t\t.value(\"Valid\", CameraConfiguration::Valid)\r\n+\t\t.value(\"Adjusted\", CameraConfiguration::Adjusted)\r\n+\t\t.value(\"Invalid\", CameraConfiguration::Invalid);\r\n+\r\n+\tpy::enum_<StreamRole>(m, \"StreamRole\")\r\n+\t\t.value(\"StillCapture\", StreamRole::StillCapture)\r\n+\t\t.value(\"Raw\", StreamRole::Raw)\r\n+\t\t.value(\"VideoRecording\", StreamRole::VideoRecording)\r\n+\t\t.value(\"Viewfinder\", StreamRole::Viewfinder);\r\n+\r\n+\tpy::enum_<Request::Status>(m, \"RequestStatus\")\r\n+\t\t.value(\"Pending\", Request::RequestPending)\r\n+\t\t.value(\"Complete\", Request::RequestComplete)\r\n+\t\t.value(\"Cancelled\", Request::RequestCancelled);\r\n+\r\n+\tpy::enum_<FrameMetadata::Status>(m, \"FrameMetadataStatus\")\r\n+\t\t.value(\"Success\", FrameMetadata::FrameSuccess)\r\n+\t\t.value(\"Error\", FrameMetadata::FrameError)\r\n+\t\t.value(\"Cancelled\", FrameMetadata::FrameCancelled);\r\n+\r\n+\tpy::enum_<Request::ReuseFlag>(m, \"ReuseFlag\")\r\n+\t\t.value(\"Default\", Request::ReuseFlag::Default)\r\n+\t\t.value(\"ReuseBuffers\", Request::ReuseFlag::ReuseBuffers);\r\n+\r\n+\tpy::enum_<ControlType>(m, \"ControlType\")\r\n+\t\t.value(\"None\", ControlType::ControlTypeNone)\r\n+\t\t.value(\"Bool\", ControlType::ControlTypeBool)\r\n+\t\t.value(\"Byte\", ControlType::ControlTypeByte)\r\n+\t\t.value(\"Integer32\", ControlType::ControlTypeInteger32)\r\n+\t\t.value(\"Integer64\", ControlType::ControlTypeInteger64)\r\n+\t\t.value(\"Float\", ControlType::ControlTypeFloat)\r\n+\t\t.value(\"String\", ControlType::ControlTypeString)\r\n+\t\t.value(\"Rectangle\", ControlType::ControlTypeRectangle)\r\n+\t\t.value(\"Size\", ControlType::ControlTypeSize);\r\n+}\r\ndiff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp\r\nnew file mode 100644\r\nindex 00000000..7701da40\r\n--- /dev/null\r\n+++ b/src/py/libcamera/pymain.cpp\r\n@@ -0,0 +1,453 @@\r\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\r\n+/*\r\n+ * Copyright (C) 2021, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\r\n+ *\r\n+ * Python bindings\r\n+ */\r\n+\r\n+/*\r\n+ * To generate pylibcamera stubs:\r\n+ * PYTHONPATH=build/src/py pybind11-stubgen --no-setup-py -o build/src/py libcamera\r\n+ */\r\n+\r\n+#include <chrono>\r\n+#include <fcntl.h>\r\n+#include <mutex>\r\n+#include <sys/eventfd.h>\r\n+#include <sys/mman.h>\r\n+#include <thread>\r\n+#include <unistd.h>\r\n+\r\n+#include <libcamera/libcamera.h>\r\n+\r\n+#include <pybind11/smart_holder.h>\r\n+#include <pybind11/functional.h>\r\n+#include <pybind11/stl.h>\r\n+#include <pybind11/stl_bind.h>\r\n+\r\n+namespace py = pybind11;\r\n+\r\n+using namespace std;\r\n+using namespace libcamera;\r\n+\r\n+template<typename T>\r\n+static py::object ValueOrTuple(const ControlValue &cv)\r\n+{\r\n+\tif (cv.isArray()) {\r\n+\t\tconst T *v = reinterpret_cast<const T *>(cv.data().data());\r\n+\t\tauto t = py::tuple(cv.numElements());\r\n+\r\n+\t\tfor (size_t i = 0; i < cv.numElements(); ++i)\r\n+\t\t\tt[i] = v[i];\r\n+\r\n+\t\treturn t;\r\n+\t}\r\n+\r\n+\treturn py::cast(cv.get<T>());\r\n+}\r\n+\r\n+static py::object ControlValueToPy(const ControlValue &cv)\r\n+{\r\n+\tswitch (cv.type()) {\r\n+\tcase ControlTypeBool:\r\n+\t\treturn ValueOrTuple<bool>(cv);\r\n+\tcase ControlTypeByte:\r\n+\t\treturn ValueOrTuple<uint8_t>(cv);\r\n+\tcase ControlTypeInteger32:\r\n+\t\treturn ValueOrTuple<int32_t>(cv);\r\n+\tcase ControlTypeInteger64:\r\n+\t\treturn ValueOrTuple<int64_t>(cv);\r\n+\tcase ControlTypeFloat:\r\n+\t\treturn ValueOrTuple<float>(cv);\r\n+\tcase ControlTypeString:\r\n+\t\treturn py::cast(cv.get<string>());\r\n+\tcase ControlTypeRectangle: {\r\n+\t\tconst Rectangle *v = reinterpret_cast<const Rectangle *>(cv.data().data());\r\n+\t\treturn py::make_tuple(v->x, v->y, v->width, v->height);\r\n+\t}\r\n+\tcase ControlTypeSize: {\r\n+\t\tconst Size *v = reinterpret_cast<const Size *>(cv.data().data());\r\n+\t\treturn py::make_tuple(v->width, v->height);\r\n+\t}\r\n+\tcase ControlTypeNone:\r\n+\tdefault:\r\n+\t\tthrow runtime_error(\"Unsupported ControlValue type\");\r\n+\t}\r\n+}\r\n+\r\n+static ControlValue PyToControlValue(const py::object &ob, ControlType type)\r\n+{\r\n+\tswitch (type) {\r\n+\tcase ControlTypeBool:\r\n+\t\treturn ControlValue(ob.cast<bool>());\r\n+\tcase ControlTypeByte:\r\n+\t\treturn ControlValue(ob.cast<uint8_t>());\r\n+\tcase ControlTypeInteger32:\r\n+\t\treturn ControlValue(ob.cast<int32_t>());\r\n+\tcase ControlTypeInteger64:\r\n+\t\treturn ControlValue(ob.cast<int64_t>());\r\n+\tcase ControlTypeFloat:\r\n+\t\treturn ControlValue(ob.cast<float>());\r\n+\tcase ControlTypeString:\r\n+\t\treturn ControlValue(ob.cast<string>());\r\n+\tcase ControlTypeRectangle:\r\n+\tcase ControlTypeSize:\r\n+\tcase ControlTypeNone:\r\n+\tdefault:\r\n+\t\tthrow runtime_error(\"Control type not implemented\");\r\n+\t}\r\n+}\r\n+\r\n+static weak_ptr<CameraManager> g_camera_manager;\r\n+static int g_eventfd;\r\n+static mutex g_reqlist_mutex;\r\n+static vector<Request *> g_reqlist;\r\n+\r\n+static void handleRequestCompleted(Request *req)\r\n+{\r\n+\t{\r\n+\t\tlock_guard guard(g_reqlist_mutex);\r\n+\t\tg_reqlist.push_back(req);\r\n+\t}\r\n+\r\n+\tuint64_t v = 1;\r\n+\twrite(g_eventfd, &v, 8);\r\n+}\r\n+\r\n+void init_pyenums(py::module& m);\r\n+\r\n+PYBIND11_MODULE(_libcamera, m)\r\n+{\r\n+\tinit_pyenums(m);\r\n+\r\n+\t/* Forward declarations */\r\n+\r\n+\t/*\r\n+\t * We need to declare all the classes here so that Python docstrings\r\n+\t * can be generated correctly.\r\n+\t * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\r\n+\t */\r\n+\r\n+\tauto pyCameraManager = py::class_<CameraManager>(m, \"CameraManager\");\r\n+\tauto pyCamera = py::class_<Camera>(m, \"Camera\");\r\n+\tauto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\r\n+\tauto pyStreamConfiguration = py::class_<StreamConfiguration>(m, \"StreamConfiguration\");\r\n+\tauto pyStreamFormats = py::class_<StreamFormats>(m, \"StreamFormats\");\r\n+\tauto pyFrameBufferAllocator = py::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\");\r\n+\tauto pyFrameBuffer = py::class_<FrameBuffer>(m, \"FrameBuffer\");\r\n+\tauto pyStream = py::class_<Stream>(m, \"Stream\");\r\n+\tauto pyControlId = py::class_<ControlId>(m, \"ControlId\");\r\n+\tauto pyRequest = py::class_<Request>(m, \"Request\");\r\n+\tauto pyFrameMetadata = py::class_<FrameMetadata>(m, \"FrameMetadata\");\r\n+\r\n+\t/* Global functions */\r\n+\tm.def(\"logSetLevel\", &logSetLevel);\r\n+\r\n+\t/* Classes */\r\n+\tpyCameraManager\r\n+\t\t.def_static(\"singleton\", []() {\r\n+\t\t\tshared_ptr<CameraManager> cm = g_camera_manager.lock();\r\n+\t\t\tif (cm)\r\n+\t\t\t\treturn cm;\r\n+\r\n+\t\t\tint fd = eventfd(0, 0);\r\n+\t\t\tif (fd == -1)\r\n+\t\t\t\tthrow std::system_error(errno, std::generic_category(), \"Failed to create eventfd\");\r\n+\r\n+\t\t\tcm = shared_ptr<CameraManager>(new CameraManager, [](auto p) {\r\n+\t\t\t\tclose(g_eventfd);\r\n+\t\t\t\tg_eventfd = -1;\r\n+\t\t\t\tdelete p;\r\n+\t\t\t});\r\n+\r\n+\t\t\tg_eventfd = fd;\r\n+\t\t\tg_camera_manager = cm;\r\n+\r\n+\t\t\tint ret = cm->start();\r\n+\t\t\tif (ret)\r\n+\t\t\t\tthrow std::system_error(-ret, std::generic_category(), \"Failed to start CameraManager\");\r\n+\r\n+\t\t\treturn cm;\r\n+\t\t})\r\n+\r\n+\t\t.def_property_readonly(\"version\", &CameraManager::version)\r\n+\r\n+\t\t.def_property_readonly(\"efd\", [](CameraManager &) {\r\n+\t\t\treturn g_eventfd;\r\n+\t\t})\r\n+\r\n+\t\t.def(\"getReadyRequests\", [](CameraManager &) {\r\n+\t\t\tvector<Request *> v;\r\n+\r\n+\t\t\t{\r\n+\t\t\t\tlock_guard guard(g_reqlist_mutex);\r\n+\t\t\t\tswap(v, g_reqlist);\r\n+\t\t\t}\r\n+\r\n+\t\t\tvector<py::object> ret;\r\n+\r\n+\t\t\tfor (Request *req : v) {\r\n+\t\t\t\tpy::object o = py::cast(req);\r\n+\t\t\t\t/* decrease the ref increased in Camera::queueRequest() */\r\n+\t\t\t\to.dec_ref();\r\n+\t\t\t\tret.push_back(o);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t})\r\n+\r\n+\t\t.def(\"get\", py::overload_cast<const string &>(&CameraManager::get), py::keep_alive<0, 1>())\r\n+\r\n+\t\t.def(\"find\", [](CameraManager &self, string str) {\r\n+\t\t\tstd::transform(str.begin(), str.end(), str.begin(), ::tolower);\r\n+\r\n+\t\t\tfor (auto c : self.cameras()) {\r\n+\t\t\t\tstring id = c->id();\r\n+\r\n+\t\t\t\tstd::transform(id.begin(), id.end(), id.begin(), ::tolower);\r\n+\r\n+\t\t\t\tif (id.find(str) != string::npos)\r\n+\t\t\t\t\treturn c;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn shared_ptr<Camera>();\r\n+\t\t}, py::keep_alive<0, 1>())\r\n+\r\n+\t\t/* Create a list of Cameras, where each camera has a keep-alive to CameraManager */\r\n+\t\t.def_property_readonly(\"cameras\", [](CameraManager &self) {\r\n+\t\t\tpy::list l;\r\n+\r\n+\t\t\tfor (auto &c : self.cameras()) {\r\n+\t\t\t\tpy::object py_cm = py::cast(self);\r\n+\t\t\t\tpy::object py_cam = py::cast(c);\r\n+\t\t\t\tpy::detail::keep_alive_impl(py_cam, py_cm);\r\n+\t\t\t\tl.append(py_cam);\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn l;\r\n+\t\t});\r\n+\r\n+\tpyCamera\r\n+\t\t.def_property_readonly(\"id\", &Camera::id)\r\n+\t\t.def(\"acquire\", &Camera::acquire)\r\n+\t\t.def(\"release\", &Camera::release)\r\n+\t\t.def(\"start\", [](Camera &self) {\r\n+\t\t\tself.requestCompleted.connect(handleRequestCompleted);\r\n+\r\n+\t\t\tint ret = self.start();\r\n+\t\t\tif (ret)\r\n+\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t})\r\n+\r\n+\t\t.def(\"stop\", [](Camera &self) {\r\n+\t\t\tint ret = self.stop();\r\n+\t\t\tif (!ret)\r\n+\t\t\t\tself.requestCompleted.disconnect(handleRequestCompleted);\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t})\r\n+\r\n+\t\t.def(\"__repr__\", [](Camera &self) {\r\n+\t\t\treturn \"<libcamera.Camera '\" + self.id() + \"'>\";\r\n+\t\t})\r\n+\r\n+\t\t/* Keep the camera alive, as StreamConfiguration contains a Stream* */\r\n+\t\t.def(\"generateConfiguration\", &Camera::generateConfiguration, py::keep_alive<0, 1>())\r\n+\t\t.def(\"configure\", &Camera::configure)\r\n+\r\n+\t\t.def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0)\r\n+\r\n+\t\t.def(\"queueRequest\", [](Camera &self, Request *req) {\r\n+\t\t\tpy::object py_req = py::cast(req);\r\n+\r\n+\t\t\tpy_req.inc_ref();\r\n+\r\n+\t\t\tint ret = self.queueRequest(req);\r\n+\t\t\tif (ret)\r\n+\t\t\t\tpy_req.dec_ref();\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t})\r\n+\r\n+\t\t.def_property_readonly(\"streams\", [](Camera &self) {\r\n+\t\t\tpy::set set;\r\n+\t\t\tfor (auto &s : self.streams()) {\r\n+\t\t\t\tpy::object py_self = py::cast(self);\r\n+\t\t\t\tpy::object py_s = py::cast(s);\r\n+\t\t\t\tpy::detail::keep_alive_impl(py_s, py_self);\r\n+\t\t\t\tset.add(py_s);\r\n+\t\t\t}\r\n+\t\t\treturn set;\r\n+\t\t})\r\n+\r\n+\t\t.def(\"find_control\", [](Camera &self, const string &name) {\r\n+\t\t\tconst auto &controls = self.controls();\r\n+\r\n+\t\t\tauto it = find_if(controls.begin(), controls.end(),\r\n+\t\t\t\t\t [&name](const auto &kvp) { return kvp.first->name() == name; });\r\n+\r\n+\t\t\tif (it == controls.end())\r\n+\t\t\t\tthrow runtime_error(\"Control not found\");\r\n+\r\n+\t\t\treturn it->first;\r\n+\t\t}, py::return_value_policy::reference_internal)\r\n+\r\n+\t\t.def_property_readonly(\"controls\", [](Camera &self) {\r\n+\t\t\tpy::dict ret;\r\n+\r\n+\t\t\tfor (const auto &[id, ci] : self.controls()) {\r\n+\t\t\t\tret[id->name().c_str()] = make_tuple<py::object>(ControlValueToPy(ci.min()),\r\n+\t\t\t\t\t\t\t\t\t\t ControlValueToPy(ci.max()),\r\n+\t\t\t\t\t\t\t\t\t\t ControlValueToPy(ci.def()));\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t})\r\n+\r\n+\t\t.def_property_readonly(\"properties\", [](Camera &self) {\r\n+\t\t\tpy::dict ret;\r\n+\r\n+\t\t\tfor (const auto &[key, cv] : self.properties()) {\r\n+\t\t\t\tconst ControlId *id = properties::properties.at(key);\r\n+\t\t\t\tpy::object ob = ControlValueToPy(cv);\r\n+\r\n+\t\t\t\tret[id->name().c_str()] = ob;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t});\r\n+\r\n+\tpyCameraConfiguration\r\n+\t\t.def(\"__iter__\", [](CameraConfiguration &self) {\r\n+\t\t\treturn py::make_iterator<py::return_value_policy::reference_internal>(self);\r\n+\t\t}, py::keep_alive<0, 1>())\r\n+\t\t.def(\"__len__\", [](CameraConfiguration &self) {\r\n+\t\t\treturn self.size();\r\n+\t\t})\r\n+\t\t.def(\"validate\", &CameraConfiguration::validate)\r\n+\t\t.def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at), py::return_value_policy::reference_internal)\r\n+\t\t.def_property_readonly(\"size\", &CameraConfiguration::size)\r\n+\t\t.def_property_readonly(\"empty\", &CameraConfiguration::empty);\r\n+\r\n+\tpyStreamConfiguration\r\n+\t\t.def(\"toString\", &StreamConfiguration::toString)\r\n+\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream, py::return_value_policy::reference_internal)\r\n+\t\t.def_property(\r\n+\t\t\t\"size\",\r\n+\t\t\t[](StreamConfiguration &self) { return make_tuple(self.size.width, self.size.height); },\r\n+\t\t\t[](StreamConfiguration &self, tuple<uint32_t, uint32_t> size) { self.size.width = get<0>(size); self.size.height = get<1>(size); })\r\n+\t\t.def_property(\r\n+\t\t\t\"pixelFormat\",\r\n+\t\t\t[](StreamConfiguration &self) { return self.pixelFormat.toString(); },\r\n+\t\t\t[](StreamConfiguration &self, string fmt) { self.pixelFormat = PixelFormat::fromString(fmt); })\r\n+\t\t.def_readwrite(\"stride\", &StreamConfiguration::stride)\r\n+\t\t.def_readwrite(\"frameSize\", &StreamConfiguration::frameSize)\r\n+\t\t.def_readwrite(\"bufferCount\", &StreamConfiguration::bufferCount)\r\n+\t\t.def_property_readonly(\"formats\", &StreamConfiguration::formats, py::return_value_policy::reference_internal);\r\n+\t;\r\n+\r\n+\tpyStreamFormats\r\n+\t\t.def_property_readonly(\"pixelFormats\", [](StreamFormats &self) {\r\n+\t\t\tvector<string> fmts;\r\n+\t\t\tfor (auto &fmt : self.pixelformats())\r\n+\t\t\t\tfmts.push_back(fmt.toString());\r\n+\t\t\treturn fmts;\r\n+\t\t})\r\n+\t\t.def(\"sizes\", [](StreamFormats &self, const string &pixelFormat) {\r\n+\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\r\n+\t\t\tvector<tuple<uint32_t, uint32_t>> fmts;\r\n+\t\t\tfor (const auto &s : self.sizes(fmt))\r\n+\t\t\t\tfmts.push_back(make_tuple(s.width, s.height));\r\n+\t\t\treturn fmts;\r\n+\t\t})\r\n+\t\t.def(\"range\", [](StreamFormats &self, const string &pixelFormat) {\r\n+\t\t\tauto fmt = PixelFormat::fromString(pixelFormat);\r\n+\t\t\tconst auto &range = self.range(fmt);\r\n+\t\t\treturn make_tuple(make_tuple(range.hStep, range.vStep),\r\n+\t\t\t\t\t make_tuple(range.min.width, range.min.height),\r\n+\t\t\t\t\t make_tuple(range.max.width, range.max.height));\r\n+\t\t});\r\n+\r\n+\tpyFrameBufferAllocator\r\n+\t\t.def(py::init<shared_ptr<Camera>>(), py::keep_alive<1, 2>())\r\n+\t\t.def(\"allocate\", &FrameBufferAllocator::allocate)\r\n+\t\t.def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\r\n+\t\t/* Create a list of FrameBuffers, where each FrameBuffer has a keep-alive to FrameBufferAllocator */\r\n+\t\t.def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\r\n+\t\t\tpy::object py_self = py::cast(self);\r\n+\t\t\tpy::list l;\r\n+\t\t\tfor (auto &ub : self.buffers(stream)) {\r\n+\t\t\t\tpy::object py_buf = py::cast(ub.get(), py::return_value_policy::reference_internal, py_self);\r\n+\t\t\t\tl.append(py_buf);\r\n+\t\t\t}\r\n+\t\t\treturn l;\r\n+\t\t});\r\n+\r\n+\tpyFrameBuffer\r\n+\t\t/* TODO: implement FrameBuffer::Plane properly */\r\n+\t\t.def(py::init([](vector<tuple<int, unsigned int>> planes, unsigned int cookie) {\r\n+\t\t\tvector<FrameBuffer::Plane> v;\r\n+\t\t\tfor (const auto &t : planes)\r\n+\t\t\t\tv.push_back({ SharedFD(get<0>(t)), FrameBuffer::Plane::kInvalidOffset, get<1>(t) });\r\n+\t\t\treturn new FrameBuffer(v, cookie);\r\n+\t\t}))\r\n+\t\t.def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\r\n+\t\t.def(\"length\", [](FrameBuffer &self, uint32_t idx) {\r\n+\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\r\n+\t\t\treturn plane.length;\r\n+\t\t})\r\n+\t\t.def(\"fd\", [](FrameBuffer &self, uint32_t idx) {\r\n+\t\t\tconst FrameBuffer::Plane &plane = self.planes()[idx];\r\n+\t\t\treturn plane.fd.get();\r\n+\t\t})\r\n+\t\t.def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\r\n+\r\n+\tpyStream\r\n+\t\t.def_property_readonly(\"configuration\", &Stream::configuration);\r\n+\r\n+\tpyControlId\r\n+\t\t.def_property_readonly(\"id\", &ControlId::id)\r\n+\t\t.def_property_readonly(\"name\", &ControlId::name)\r\n+\t\t.def_property_readonly(\"type\", &ControlId::type);\r\n+\r\n+\tpyRequest\r\n+\t\t/* Fence is not supported, so we cannot expose addBuffer() directly */\r\n+\t\t.def(\"addBuffer\", [](Request &self, const Stream *stream, FrameBuffer *buffer) {\r\n+\t\t\treturn self.addBuffer(stream, buffer);\r\n+\t\t}, py::keep_alive<1, 3>()) /* Request keeps Framebuffer alive */\r\n+\t\t.def_property_readonly(\"status\", &Request::status)\r\n+\t\t.def_property_readonly(\"buffers\", &Request::buffers)\r\n+\t\t.def_property_readonly(\"cookie\", &Request::cookie)\r\n+\t\t.def_property_readonly(\"hasPendingBuffers\", &Request::hasPendingBuffers)\r\n+\t\t.def(\"set_control\", [](Request &self, ControlId& id, py::object value) {\r\n+\t\t\tself.controls().set(id.id(), PyToControlValue(value, id.type()));\r\n+\t\t})\r\n+\t\t.def_property_readonly(\"metadata\", [](Request &self) {\r\n+\t\t\tpy::dict ret;\r\n+\r\n+\t\t\tfor (const auto &[key, cv] : self.metadata()) {\r\n+\t\t\t\tconst ControlId *id = controls::controls.at(key);\r\n+\t\t\t\tpy::object ob = ControlValueToPy(cv);\r\n+\r\n+\t\t\t\tret[id->name().c_str()] = ob;\r\n+\t\t\t}\r\n+\r\n+\t\t\treturn ret;\r\n+\t\t})\r\n+\t\t/* As we add a keep_alive to the fb in addBuffers(), we can only allow reuse with ReuseBuffers. */\r\n+\t\t.def(\"reuse\", [](Request &self) { self.reuse(Request::ReuseFlag::ReuseBuffers); });\r\n+\r\n+\tpyFrameMetadata\r\n+\t\t.def_readonly(\"status\", &FrameMetadata::status)\r\n+\t\t.def_readonly(\"sequence\", &FrameMetadata::sequence)\r\n+\t\t.def_readonly(\"timestamp\", &FrameMetadata::timestamp)\r\n+\t\t/* temporary helper, to be removed */\r\n+\t\t.def_property_readonly(\"bytesused\", [](FrameMetadata &self) {\r\n+\t\t\tvector<unsigned int> v;\r\n+\t\t\tv.resize(self.planes().size());\r\n+\t\t\ttransform(self.planes().begin(), self.planes().end(), v.begin(), [](const auto &p) { return p.bytesused; });\r\n+\t\t\treturn v;\r\n+\t\t});\r\n+}\r\ndiff --git a/src/py/meson.build b/src/py/meson.build\r\nnew file mode 100644\r\nindex 00000000..4ce9668c\r\n--- /dev/null\r\n+++ b/src/py/meson.build\r\n@@ -0,0 +1 @@\r\n+subdir('libcamera')\r\ndiff --git a/subprojects/.gitignore b/subprojects/.gitignore\r\nindex 391fde2c..757bb072 100644\r\n--- a/subprojects/.gitignore\r\n+++ b/subprojects/.gitignore\r\n@@ -1,3 +1,4 @@\r\n /googletest-release*\r\n /libyuv\r\n-/packagecache\r\n\\ No newline at end of file\r\n+/packagecache\r\n+/pybind11*/\r\ndiff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap\r\nnew file mode 100644\r\nindex 00000000..ebf942ff\r\n--- /dev/null\r\n+++ b/subprojects/pybind11.wrap\r\n@@ -0,0 +1,6 @@\r\n+[wrap-git]\r\n+url = https://github.com/tomba/pybind11.git\r\n+revision = smart_holder\r\n+\r\n+[provide]\r\n+pybind11 = pybind11_dep", "prefixes": [ "libcamera-devel", "v5", "1/3" ] }