Show a patch.

GET /api/patches/15330/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 15330,
    "url": "https://patchwork.libcamera.org/api/patches/15330/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/15330/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/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": "<20220204133814.303217-4-tomi.valkeinen@ideasonboard.com>",
    "date": "2022-02-04T13:38:12",
    "name": "[libcamera-devel,v4,3/5] Add Python bindings",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "8f06f029d76eb254a5528310eab0e662e7125d11",
    "submitter": {
        "id": 109,
        "url": "https://patchwork.libcamera.org/api/people/109/?format=api",
        "name": "Tomi Valkeinen",
        "email": "tomi.valkeinen@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/15330/mbox/",
    "series": [
        {
            "id": 2916,
            "url": "https://patchwork.libcamera.org/api/series/2916/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=2916",
            "date": "2022-02-04T13:38:09",
            "name": "Python bindings",
            "version": 4,
            "mbox": "https://patchwork.libcamera.org/series/2916/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/15330/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/15330/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\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 57E75C3257\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  4 Feb 2022 13:38:47 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E37EA60AF6;\n\tFri,  4 Feb 2022 14:38:46 +0100 (CET)",
            "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 E4EE4609F3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  4 Feb 2022 14:38:43 +0100 (CET)",
            "from deskari.lan (91-156-85-209.elisa-laajakaista.fi\n\t[91.156.85.209])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 45693472;\n\tFri,  4 Feb 2022 14:38:43 +0100 (CET)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"KJQR1z4N\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1643981923;\n\tbh=0DhLwgABRH5w87AuWDlG/YK/vJA5atHlrbz8Uv7EqDY=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=KJQR1z4NuBz8v7QGAKx6JtWgr47wXdOHpHO30BN2XxWQWP1i1EadW4afPKvvC4P/r\n\tWRsMoHewfKPFBDFZ9nwlME672XtuVaHq8z/pjeSXGI33laOEN8CV6Wmj1e5q47iSvU\n\tWwGWijxCsdQQuWMVR/f7ZT9UKlZQUBGz7X1xWzm8=",
        "From": "Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>",
        "To": "libcamera-devel@lists.libcamera.org,\n\tDavid Plowman <david.plowman@raspberrypi.com>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>,\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>",
        "Date": "Fri,  4 Feb 2022 15:38:12 +0200",
        "Message-Id": "<20220204133814.303217-4-tomi.valkeinen@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.25.1",
        "In-Reply-To": "<20220204133814.303217-1-tomi.valkeinen@ideasonboard.com>",
        "References": "<20220204133814.303217-1-tomi.valkeinen@ideasonboard.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Subject": "[libcamera-devel] [PATCH v4 3/5] 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>",
        "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\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 |  41 ++++\n src/py/libcamera/pymain.cpp  | 440 +++++++++++++++++++++++++++++++++++\n src/py/meson.build           |   1 +\n subprojects/.gitignore       |   3 +-\n subprojects/pybind11.wrap    |  12 +\n 9 files changed, 513 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/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\nindex 9684d562..fe1a09e1 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -180,6 +180,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,\ndiff --git a/meson_options.txt b/meson_options.txt\nindex 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)')\ndiff --git a/src/meson.build b/src/meson.build\nindex 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')\ndiff --git a/src/py/libcamera/__init__.py b/src/py/libcamera/__init__.py\nnew file mode 100644\nindex 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\ndiff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build\nnew file mode 100644\nindex 00000000..0926b995\n--- /dev/null\n+++ b/src/py/libcamera/meson.build\n@@ -0,0 +1,41 @@\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+])\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+\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)\ndiff --git a/src/py/libcamera/pymain.cpp b/src/py/libcamera/pymain.cpp\nnew file mode 100644\nindex 00000000..d35ec2f2\n--- /dev/null\n+++ b/src/py/libcamera/pymain.cpp\n@@ -0,0 +1,440 @@\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 <chrono>\n+#include <fcntl.h>\n+#include <mutex>\n+#include <sys/eventfd.h>\n+#include <sys/mman.h>\n+#include <thread>\n+#include <unistd.h>\n+\n+#include <libcamera/libcamera.h>\n+\n+#include <pybind11/functional.h>\n+#include <pybind11/pybind11.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+PYBIND11_MODULE(_libcamera, m)\n+{\n+\tm.def(\"logSetLevel\", &logSetLevel);\n+\n+\tpy::class_<CameraManager, std::shared_ptr<CameraManager>>(m, \"CameraManager\")\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+\tpy::class_<Camera, shared_ptr<Camera>>(m, \"Camera\")\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\", [](shared_ptr<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\", [](shared_ptr<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__\", [](shared_ptr<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_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+\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::class_<CameraConfiguration>(m, \"CameraConfiguration\")\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+\tpy::class_<StreamConfiguration>(m, \"StreamConfiguration\")\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+\tpy::class_<StreamFormats>(m, \"StreamFormats\")\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+\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::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\")\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+\tpy::class_<FrameBuffer>(m, \"FrameBuffer\")\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+\tpy::class_<Stream>(m, \"Stream\")\n+\t\t.def_property_readonly(\"configuration\", &Stream::configuration);\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::class_<Request>(m, \"Request\")\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, string &control, py::object value) {\n+\t\t\tconst auto &controls = self.camera()->controls();\n+\n+\t\t\tauto it = find_if(controls.begin(), controls.end(),\n+\t\t\t\t\t  [&control](const auto &kvp) { return kvp.first->name() == control; });\n+\n+\t\t\tif (it == controls.end())\n+\t\t\t\tthrow runtime_error(\"Control not found\");\n+\n+\t\t\tconst auto &id = it->first;\n+\n+\t\t\tself.controls().set(id->id(), PyToControlValue(value, id->type()));\n+\t\t})\n+\t\t.def_property_readonly(\"metadata\", [](Request &self) {\n+\t\t\tpy::dict ret;\n+\n+\t\t\tfor (const auto &[key, cv] : self.metadata()) {\n+\t\t\t\tconst ControlId *id = controls::controls.at(key);\n+\t\t\t\tpy::object ob = ControlValueToPy(cv);\n+\n+\t\t\t\tret[id->name().c_str()] = ob;\n+\t\t\t}\n+\n+\t\t\treturn ret;\n+\t\t})\n+\t\t/* 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+\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::class_<FrameMetadata>(m, \"FrameMetadata\")\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+}\ndiff --git a/src/py/meson.build b/src/py/meson.build\nnew file mode 100644\nindex 00000000..4ce9668c\n--- /dev/null\n+++ b/src/py/meson.build\n@@ -0,0 +1 @@\n+subdir('libcamera')\ndiff --git a/subprojects/.gitignore b/subprojects/.gitignore\nindex 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*/\ndiff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap\nnew file mode 100644\nindex 00000000..9d6e7acb\n--- /dev/null\n+++ b/subprojects/pybind11.wrap\n@@ -0,0 +1,12 @@\n+[wrap-file]\n+directory = pybind11-2.6.1\n+source_url = https://github.com/pybind/pybind11/archive/v2.6.1.tar.gz\n+source_filename = pybind11-2.6.1.tar.gz\n+source_hash = cdbe326d357f18b83d10322ba202d69f11b2f49e2d87ade0dc2be0c5c34f8e2a\n+patch_url = https://wrapdb.mesonbuild.com/v2/pybind11_2.6.1-1/get_patch\n+patch_filename = pybind11-2.6.1-1-wrap.zip\n+patch_hash = 6de5477598b56c8a2e609196420c783ac35b79a31d6622121602e6ade6b3cee8\n+\n+[provide]\n+pybind11 = pybind11_dep\n+\n",
    "prefixes": [
        "libcamera-devel",
        "v4",
        "3/5"
    ]
}