Patch Detail
Show a patch.
GET /api/1.1/patches/10518/?format=api
{ "id": 10518, "url": "https://patchwork.libcamera.org/api/1.1/patches/10518/?format=api", "web_url": "https://patchwork.libcamera.org/patch/10518/", "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": "<20201127133738.880859-4-tomi.valkeinen@iki.fi>", "date": "2020-11-27T13:37:37", "name": "[libcamera-devel,RFC,v2,3/4] libcamera python bindings", "commit_ref": null, "pull_url": null, "state": "rfc", "archived": false, "hash": "7751d92b5f5cc3c6dc23b0f2aa5552c449f40b42", "submitter": { "id": 70, "url": "https://patchwork.libcamera.org/api/1.1/people/70/?format=api", "name": "Tomi Valkeinen", "email": "tomi.valkeinen@iki.fi" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/10518/mbox/", "series": [ { "id": 1487, "url": "https://patchwork.libcamera.org/api/1.1/series/1487/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=1487", "date": "2020-11-27T13:37:34", "name": "prototype python bindings", "version": 2, "mbox": "https://patchwork.libcamera.org/series/1487/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/10518/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/10518/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 23F8EBE176\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 27 Nov 2020 13:38:03 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E5AE863479;\n\tFri, 27 Nov 2020 14:38:02 +0100 (CET)", "from mail-lj1-x234.google.com (mail-lj1-x234.google.com\n\t[IPv6:2a00:1450:4864:20::234])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 5D2F26346E\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 27 Nov 2020 14:38:01 +0100 (CET)", "by mail-lj1-x234.google.com with SMTP id i17so5967622ljd.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 27 Nov 2020 05:38:01 -0800 (PST)", "from deskari.ti.com (91-152-83-50.elisa-laajakaista.fi.\n\t[91.152.83.50]) by smtp.gmail.com with ESMTPSA id\n\tz199sm690892lfc.42.2020.11.27.05.37.58\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tFri, 27 Nov 2020 05:37:59 -0800 (PST)" ], "Authentication-Results": "lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (2048-bit key;\n\tunprotected) header.d=gmail.com header.i=@gmail.com\n\theader.b=\"DRw2pb2w\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025;\n\th=sender:from:to:cc:subject:date:message-id:in-reply-to:references\n\t:mime-version:content-transfer-encoding;\n\tbh=X1NhXv9PP6ESJ7mesaBZIBSusjVxP7hNf+Z1Luxfzgw=;\n\tb=DRw2pb2w+rSTcLQj0kDbJTbuM314m0hd2WAGmpONqBRkf4ycURyzxQ2+/7M4boOCjG\n\tgWT5Xv8OfzQVjF08jPHaa553n+VrUMi1BA1beWWaXCNql0j9hGmUtKh7l4Xd0cwTDDNQ\n\t1GGTwGJRBFNqVEMKmMNjAY5IkqldA/gEgotxDEzjo+wxSiQSNB4yVvFd9F4yJy7/zRGb\n\tKX/Uka/iYt1J6gMsihmCXWITZf9olLK4MdH4k6Lez7Xc/ldKBhWk4XxrSQNQyyZRTdeT\n\tZcBJ51qqAC4ECyHXoGD4/+mPHgMw2p4X0j4ZOEmnVQ9iumWkgqWXZWW5JaHc4PXuxk+T\n\tSTjw==", "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20161025;\n\th=x-gm-message-state:sender:from:to:cc:subject:date:message-id\n\t:in-reply-to:references:mime-version:content-transfer-encoding;\n\tbh=X1NhXv9PP6ESJ7mesaBZIBSusjVxP7hNf+Z1Luxfzgw=;\n\tb=ehkGgyy48KWcc4Hozo0MarCBrJ062pgzmuI9bAP4+HXQk6TD84lf0mypppxrIU9+b1\n\tL2yvZF5jqr25H7qBR6ECvuzpHCivbyIdi69e4pvlqngbbTWN7gS100HDgKenL0iO37aN\n\tsTMVk+/fkRORTIOu/44K5Qg4deo+WNjJe3icLyIlTQN9WsA0BXTD2DfBJoBBeTe6mxXT\n\tC6JbHvHqGK39pvbpB3ZfkS7xxUOQwob7WTPElOJztxzDjEUikOa0dkCTEzzyH3NAq4zw\n\thNc2LFXhCC9l75fvI4sDqsY1xb8p89Q0C80KkC7Ujt5uYXUxaC4093lUFqcbZhG/zQW7\n\t2gDQ==", "X-Gm-Message-State": "AOAM530ieQeRr051ohOhA651+s9J5xKTvVqkenzeQa4POqCZZtfVx/tk\n\tzVDvGAzqzaVFVrMpzTKMzqsJlmFMh2w=", "X-Google-Smtp-Source": "ABdhPJwhlHpA+AyuDzFD32miXiy7TvmlJfSHcyEZaU5kTDoBGLYqPGIweLPbrooHFWNT6OKRyL7m1A==", "X-Received": "by 2002:a2e:9a98:: with SMTP id\n\tp24mr3459994lji.418.1606484279765; \n\tFri, 27 Nov 2020 05:37:59 -0800 (PST)", "From": "Tomi Valkeinen <tomi.valkeinen@iki.fi>", "To": "libcamera-devel@lists.libcamera.org", "Date": "Fri, 27 Nov 2020 15:37:37 +0200", "Message-Id": "<20201127133738.880859-4-tomi.valkeinen@iki.fi>", "X-Mailer": "git-send-email 2.25.1", "In-Reply-To": "<20201127133738.880859-1-tomi.valkeinen@iki.fi>", "References": "<20201127133738.880859-1-tomi.valkeinen@iki.fi>", "MIME-Version": "1.0", "Subject": "[libcamera-devel] [RFC v2 3/4] libcamera 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>", "Cc": "Tomi Valkeinen <tomi.valkeinen@iki.fi>", "Content-Type": "text/plain; charset=\"us-ascii\"", "Content-Transfer-Encoding": "7bit", "Errors-To": "libcamera-devel-bounces@lists.libcamera.org", "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>" }, "content": "Main issues:\n\n- Memory management in general. Who owns the object, how to pass\n ownership, etc.\n\n- Specifically, Request is currently broken. We can't, afaik, pass\n ownership around. So currently Python never frees a Request, and if\n the Request is not given to Camera::queueRequest, it will leak.\n\n- Need public Camera destructor. It is not clear to me why C++ allows it\n to be private, but pybind11 doesn't.\n\nSigned-off-by: Tomi Valkeinen <tomi.valkeinen@iki.fi>\n---\n .gitignore | 2 +\n meson_options.txt | 2 +\n src/meson.build | 1 +\n src/py/meson.build | 1 +\n src/py/pycamera/__init__.py | 11 +\n src/py/pycamera/meson.build | 38 +++\n src/py/pycamera/pymain.cpp | 382 +++++++++++++++++++++++++++++\n src/py/test/drmtest.py | 129 ++++++++++\n src/py/test/icam.py | 154 ++++++++++++\n src/py/test/run-valgrind.sh | 6 +\n src/py/test/run.sh | 3 +\n src/py/test/simplecamera.py | 198 +++++++++++++++\n src/py/test/test.py | 210 ++++++++++++++++\n src/py/test/valgrind-pycamera.supp | 17 ++\n subprojects/pybind11.wrap | 10 +\n 15 files changed, 1164 insertions(+)\n create mode 100644 src/py/meson.build\n create mode 100644 src/py/pycamera/__init__.py\n create mode 100644 src/py/pycamera/meson.build\n create mode 100644 src/py/pycamera/pymain.cpp\n create mode 100755 src/py/test/drmtest.py\n create mode 100755 src/py/test/icam.py\n create mode 100755 src/py/test/run-valgrind.sh\n create mode 100755 src/py/test/run.sh\n create mode 100644 src/py/test/simplecamera.py\n create mode 100755 src/py/test/test.py\n create mode 100644 src/py/test/valgrind-pycamera.supp\n create mode 100644 subprojects/pybind11.wrap", "diff": "diff --git a/.gitignore b/.gitignore\nindex d3d73615..1f9dc7d1 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -5,3 +5,5 @@ build/\n patches/\n *.patch\n *.pyc\n+subprojects/packagecache/\n+subprojects/pybind11-2.3.0/\ndiff --git a/meson_options.txt b/meson_options.txt\nindex 53f2675e..ef995527 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -37,3 +37,5 @@ option('v4l2',\n type : 'boolean',\n value : false,\n description : 'Compile the V4L2 compatibility layer')\n+\n+option('pycamera', type : 'feature', value : 'auto')\ndiff --git a/src/meson.build b/src/meson.build\nindex b9c7e759..61ec3991 100644\n--- a/src/meson.build\n+++ b/src/meson.build\n@@ -23,3 +23,4 @@ if get_option('v4l2')\n endif\n \n subdir('gstreamer')\n+subdir('py')\ndiff --git a/src/py/meson.build b/src/py/meson.build\nnew file mode 100644\nindex 00000000..42ffa221\n--- /dev/null\n+++ b/src/py/meson.build\n@@ -0,0 +1 @@\n+subdir('pycamera')\ndiff --git a/src/py/pycamera/__init__.py b/src/py/pycamera/__init__.py\nnew file mode 100644\nindex 00000000..ddb70096\n--- /dev/null\n+++ b/src/py/pycamera/__init__.py\n@@ -0,0 +1,11 @@\n+from .pycamera import *\n+from enum import Enum\n+import os\n+import struct\n+import mmap\n+\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/pycamera/meson.build b/src/py/pycamera/meson.build\nnew file mode 100644\nindex 00000000..9ff9b8ee\n--- /dev/null\n+++ b/src/py/pycamera/meson.build\n@@ -0,0 +1,38 @@\n+# SPDX-License-Identifier: CC0-1.0\n+\n+py3_dep = dependency('python3', required : get_option('pycamera'))\n+\n+if py3_dep.found() == false\n+ subdir_done()\n+endif\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_dep,\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/pycamera'\n+\n+pycamera = shared_module('pycamera',\n+ pycamera_sources,\n+ install : true,\n+ install_dir : destdir,\n+ name_prefix : '',\n+ dependencies : pycamera_deps,\n+ cpp_args : pycamera_args)\n+\n+# Copy __init__.py to build dir so that we can run without installing\n+configure_file(input: '__init__.py', output: '__init__.py', copy: true)\n+\n+install_data(['__init__.py'], install_dir: destdir)\ndiff --git a/src/py/pycamera/pymain.cpp b/src/py/pycamera/pymain.cpp\nnew file mode 100644\nindex 00000000..bd1b9bdd\n--- /dev/null\n+++ b/src/py/pycamera/pymain.cpp\n@@ -0,0 +1,382 @@\n+#include <chrono>\n+#include <thread>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/mman.h>\n+#include <sys/eventfd.h>\n+#include <mutex>\n+\n+#include <pybind11/pybind11.h>\n+#include <pybind11/stl.h>\n+#include <pybind11/stl_bind.h>\n+#include <pybind11/functional.h>\n+\n+#include <libcamera/libcamera.h>\n+\n+namespace py = pybind11;\n+\n+using namespace std;\n+using namespace libcamera;\n+\n+static py::object ControlValueToPy(const ControlValue &cv)\n+{\n+\t//assert(!cv.isArray());\n+\t//assert(cv.numElements() == 1);\n+\n+\tswitch (cv.type()) {\n+\tcase ControlTypeBool:\n+\t\treturn py::cast(cv.get<bool>());\n+\tcase ControlTypeByte:\n+\t\treturn py::cast(cv.get<uint8_t>());\n+\tcase ControlTypeInteger32:\n+\t\treturn py::cast(cv.get<int32_t>());\n+\tcase ControlTypeInteger64:\n+\t\treturn py::cast(cv.get<int64_t>());\n+\tcase ControlTypeFloat:\n+\t\treturn py::cast(cv.get<float>());\n+\tcase ControlTypeString:\n+\t\treturn py::cast(cv.get<string>());\n+\tcase ControlTypeRectangle:\n+\tcase ControlTypeSize:\n+\tcase ControlTypeNone:\n+\tdefault:\n+\t\tthrow runtime_error(\"Unsupported ControlValue type\");\n+\t}\n+}\n+\n+static ControlValue PyToControlValue(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+struct CameraEvent\n+{\n+\tshared_ptr<Camera> camera;\n+\tRequest::Status status;\n+\tmap<const Stream *, FrameBuffer *> bufmap;\n+\tControlList metadata;\n+\tuint64_t cookie;\n+};\n+\n+static int g_eventfd;\n+static mutex g_buflist_mutex;\n+static vector<CameraEvent> g_buflist;\n+\n+static void handle_request_completed(Request *req)\n+{\n+\tCameraEvent ev;\n+\tev.camera = req->camera();\n+\tev.status = req->status();\n+\tev.bufmap = req->buffers();\n+\tev.metadata = req->metadata();\n+\tev.cookie = req->cookie();\n+\n+\t{\n+\t\tlock_guard guard(g_buflist_mutex);\n+\t\tg_buflist.push_back(ev);\n+\t}\n+\n+\tuint64_t v = 1;\n+\twrite(g_eventfd, &v, 8);\n+}\n+\n+PYBIND11_MODULE(pycamera, m)\n+{\n+\tpy::class_<CameraEvent>(m, \"CameraEvent\")\n+\t\t.def_readonly(\"camera\", &CameraEvent::camera)\n+\t\t.def_readonly(\"status\", &CameraEvent::status)\n+\t\t.def_readonly(\"buffers\", &CameraEvent::bufmap)\n+\t\t.def_property_readonly(\"metadata\", [](const CameraEvent& 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 = 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+\t\t.def_readonly(\"cookie\", &CameraEvent::cookie)\n+\t;\n+\n+\tpy::class_<CameraManager>(m, \"CameraManager\")\n+\t\t/*\n+\t\t * CameraManager::stop() cannot be called, as CameraManager expects all Camera\n+\t\t * instances to be released before calling stop and we can't have such requirement\n+\t\t * in python, especially as we have a keep-alive from Camera to CameraManager.\n+\t\t * So we rely on GC and the keep-alives, and call CameraManager::start() from\n+\t\t * the constructor.\n+\t\t */\n+\n+\t\t.def(py::init([]() {\n+\t\t\tg_eventfd = eventfd(0, 0);\n+\n+\t\t\tauto cm = make_unique<CameraManager>();\n+\t\t\tcm->start();\n+\t\t\treturn cm;\n+\t\t}))\n+\n+\t\t.def_property_readonly(\"efd\", [](CameraManager &) {\n+\t\t\treturn g_eventfd;\n+\t\t})\n+\n+\t\t.def(\"get_ready_requests\", [](CameraManager &) {\n+\t\t\tvector<CameraEvent> v;\n+\n+\t\t\t{\n+\t\t\t\tlock_guard guard(g_buflist_mutex);\n+\t\t\t\tswap(v, g_buflist);\n+\t\t\t}\n+\n+\t\t\treturn v;\n+\t\t})\n+\n+\t\t.def(\"get\", py::overload_cast<const string &>(&CameraManager::get))\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})\n+\t\t.def_property_readonly(\"version\", &CameraManager::version)\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+\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+\t\t\treturn l;\n+\t\t});\n+\n+\tpy::class_<Camera, shared_ptr<Camera>>(m, \"Camera\", py::dynamic_attr())\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(handle_request_completed);\n+\n+\t\t\tself->start();\n+\t\t})\n+\n+\t\t.def(\"stop\", [](shared_ptr<Camera> &self) {\n+\t\t\tself->stop();\n+\n+\t\t\tself->requestCompleted.disconnect(handle_request_completed);\n+\t\t})\n+\n+\t\t.def(\"__repr__\", [](shared_ptr<Camera> &self) {\n+\t\t\treturn \"<pycamera.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// XXX created requests MUST be queued to be freed, python will not free them\n+\t\t.def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0, py::return_value_policy::reference_internal)\n+\t\t.def(\"queueRequest\", &Camera::queueRequest)\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::class_<CameraConfiguration>(m, \"CameraConfiguration\")\n+\t\t.def(\"at\", py::overload_cast<unsigned int>(&CameraConfiguration::at), py::return_value_policy::reference_internal)\n+\t\t.def(\"validate\", &CameraConfiguration::validate)\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\"fmt\",\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(\"StillCaptureRaw\", 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(\"free\", &FrameBufferAllocator::free)\n+\t\t.def_property_readonly(\"allocated\", &FrameBufferAllocator::allocated)\n+\t\t// Create a list of FrameBuffer, where each FrameBuffer has a keep-alive to FrameBufferAllocator\n+\t\t.def(\"buffers\", [](FrameBufferAllocator &self, Stream *stream) {\n+\t\t\tpy::list l;\n+\t\t\tfor (auto &ub : self.buffers(stream)) {\n+\t\t\t\tpy::object py_fa = py::cast(self);\n+\t\t\t\tpy::object py_buf = py::cast(ub.get());\n+\t\t\t\tpy::detail::keep_alive_impl(py_buf, py_fa);\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, unique_ptr<FrameBuffer, py::nodelete>>(m, \"FrameBuffer\")\n+\t\t// XXX who frees this\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({FileDescriptor(get<0>(t)), 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.fd();\n+\t\t})\n+\t\t.def_property(\"cookie\", &FrameBuffer::cookie, &FrameBuffer::setCookie);\n+\n+\tpy::class_<Stream, unique_ptr<Stream, py::nodelete>>(m, \"Stream\")\n+\t\t.def_property_readonly(\"configuration\", &Stream::configuration);\n+\n+\tpy::class_<Request, unique_ptr<Request, py::nodelete>>(m, \"Request\")\n+\t\t.def(\"addBuffer\", &Request::addBuffer)\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+\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.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+\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+}\ndiff --git a/src/py/test/drmtest.py b/src/py/test/drmtest.py\nnew file mode 100755\nindex 00000000..f7a6cc48\n--- /dev/null\n+++ b/src/py/test/drmtest.py\n@@ -0,0 +1,129 @@\n+#!/usr/bin/python3\n+\n+from simplecamera import SimpleCameraManager, SimpleCamera\n+import pykms\n+import pycamera as pycam\n+import time\n+import argparse\n+import selectors\n+import sys\n+\n+card = pykms.Card()\n+\n+res = pykms.ResourceManager(card)\n+conn = res.reserve_connector()\n+crtc = res.reserve_crtc(conn)\n+plane = res.reserve_generic_plane(crtc)\n+mode = conn.get_default_mode()\n+modeb = mode.to_blob(card)\n+\n+req = pykms.AtomicReq(card)\n+req.add_connector(conn, crtc)\n+req.add_crtc(crtc, modeb)\n+req.commit_sync(allow_modeset = True)\n+\n+class ScreenHandler:\n+\tdef __init__(self, card, crtc, plane):\n+\t\tself.card = card\n+\t\tself.crtc = crtc\n+\t\tself.plane = plane\n+\t\tself.bufqueue = []\n+\t\tself.current = None\n+\t\tself.next = None\n+\n+\tdef handle_page_flip(self, frame, time):\n+\t\told = self.current\n+\t\tself.current = self.next\n+\n+\t\tif len(self.bufqueue) > 0:\n+\t\t\tself.next = self.bufqueue.pop(0)\n+\t\telse:\n+\t\t\tself.next = None\n+\n+\t\tif self.next:\n+\t\t\treq = pykms.AtomicReq(self.card)\n+\t\t\treq.add_plane(self.plane, fb, self.crtc, dst=(0, 0, fb.width, fb.height))\n+\t\t\treq.commit()\n+\n+\t\treturn old\n+\n+\tdef queue(self, fb):\n+\t\tif not self.next:\n+\t\t\tself.next = fb\n+\n+\t\t\treq = pykms.AtomicReq(self.card)\n+\t\t\treq.add_plane(self.plane, fb, self.crtc, dst=(0, 0, fb.width, fb.height))\n+\t\t\treq.commit()\n+\t\telse:\n+\t\t\tself.bufqueue.append(fb)\n+\n+\n+\n+\n+screen = ScreenHandler(card, crtc, plane)\n+\n+\n+\n+def handle_camera_frame(camera, stream, fb):\n+\tscreen.queue(cam_2_drm_map[fb])\n+\n+cm = SimpleCameraManager()\n+cam = cm.find(\"imx219\")\n+cam.open()\n+\n+cam.format = \"ARGB8888\"\n+cam.resolution = (1920, 1080)\n+\n+cam.callback = lambda stream, fb, camera=cam: handle_camera_frame(camera, stream, fb)\n+\n+cam_2_drm_map = {}\n+drm_2_cam_map = {}\n+\n+cam.xxx_config()\n+\n+drmbuffers = []\n+stream_cfg = cam.stream_config\n+for fb in cam.buffers:\n+\tw, h = stream_cfg.size\n+\tstride = stream_cfg.stride\n+\tdrmfb = pykms.DmabufFramebuffer(card, w, h, pykms.PixelFormat.ARGB8888,\n+\t\t\t\t\t\t\t\t\t[fb.fd(0)], [stride], [0])\n+\tdrmbuffers.append(drmfb)\n+\n+\tcam_2_drm_map[fb] = drmfb\n+\tdrm_2_cam_map[drmfb] = fb\n+\n+\n+cam.start()\n+\n+def readdrm(fileobj, mask):\n+\tfor ev in card.read_events():\n+\t\tif ev.type == pykms.DrmEventType.FLIP_COMPLETE:\n+\t\t\told = screen.handle_page_flip(ev.seq, ev.time)\n+\n+\t\t\tif old:\n+\t\t\t\tfb = drm_2_cam_map[old]\n+\t\t\t\tcam.queue_fb(fb)\n+\n+running = True\n+\n+def readkey(fileobj, mask):\n+\tglobal running\n+\tsys.stdin.readline()\n+\trunning = False\n+\n+sel = selectors.DefaultSelector()\n+sel.register(card.fd, selectors.EVENT_READ, readdrm)\n+sel.register(sys.stdin, selectors.EVENT_READ, readkey)\n+\n+print(\"Press enter to exit\")\n+\n+while running:\n+\tevents = sel.select()\n+\tfor key, mask in events:\n+\t\tcallback = key.data\n+\t\tcallback(key.fileobj, mask)\n+\n+cam.stop()\n+\n+print(\"Done\")\ndiff --git a/src/py/test/icam.py b/src/py/test/icam.py\nnew file mode 100755\nindex 00000000..2a8205ed\n--- /dev/null\n+++ b/src/py/test/icam.py\n@@ -0,0 +1,154 @@\n+#!/usr/bin/python3 -i\n+\n+from simplecamera import SimpleCameraManager, SimpleCamera\n+from PyQt5 import QtCore, QtGui, QtWidgets\n+import pycamera as pycam\n+import argparse\n+\n+parser = argparse.ArgumentParser()\n+parser.add_argument(\"-c\", \"--cameras\", type=str, default=None)\n+args = parser.parse_args()\n+\n+format_map = {\n+\t\"YUYV\": QtGui.QImage.Format.Format_RGB16,\n+\t\"BGR888\": QtGui.QImage.Format.Format_RGB888,\n+\t\"MJPEG\": QtGui.QImage.Format.Format_RGB888,\n+}\n+\n+\n+class MainWindow(QtWidgets.QWidget):\n+\trequestDone = QtCore.pyqtSignal(pycam.Stream, pycam.FrameBuffer)\n+\n+\tdef __init__(self, camera):\n+\t\tsuper().__init__()\n+\n+\t\t# Use signal to handle request, so that the execution is transferred to the main thread\n+\t\tself.requestDone.connect(self.handle_request)\n+\t\tcamera.callback = lambda stream, fb: self.requestDone.emit(stream, fb)\n+\n+\t\tcamera.xxx_config()\n+\n+\t\tself.camera = camera\n+\n+\t\tself.label = QtWidgets.QLabel()\n+\n+\t\twindowLayout = QtWidgets.QHBoxLayout()\n+\t\tself.setLayout(windowLayout)\n+\n+\t\twindowLayout.addWidget(self.label)\n+\n+\t\tcontrolsLayout = QtWidgets.QVBoxLayout()\n+\t\twindowLayout.addLayout(controlsLayout)\n+\n+\t\twindowLayout.addStretch()\n+\n+\t\tgroup = QtWidgets.QGroupBox(\"Info\")\n+\t\tgroupLayout = QtWidgets.QVBoxLayout()\n+\t\tgroup.setLayout(groupLayout)\n+\t\tcontrolsLayout.addWidget(group)\n+\n+\t\tlab = QtWidgets.QLabel(camera.id)\n+\t\tgroupLayout.addWidget(lab)\n+\n+\t\tself.frameLabel = QtWidgets.QLabel()\n+\t\tgroupLayout.addWidget(self.frameLabel)\n+\n+\n+\t\tgroup = QtWidgets.QGroupBox(\"Properties\")\n+\t\tgroupLayout = QtWidgets.QVBoxLayout()\n+\t\tgroup.setLayout(groupLayout)\n+\t\tcontrolsLayout.addWidget(group)\n+\n+\t\tfor k, v in camera.properties.items():\n+\t\t\tlab = QtWidgets.QLabel()\n+\t\t\tlab.setText(k + \" = \" + str(v))\n+\t\t\tgroupLayout.addWidget(lab)\n+\n+\t\tgroup = QtWidgets.QGroupBox(\"Controls\")\n+\t\tgroupLayout = QtWidgets.QVBoxLayout()\n+\t\tgroup.setLayout(groupLayout)\n+\t\tcontrolsLayout.addWidget(group)\n+\n+\t\tfor k, (min, max, default) in camera.controls.items():\n+\t\t\tlab = QtWidgets.QLabel()\n+\t\t\tlab.setText(\"{} = {}/{}/{}\".format(k, min, max, default))\n+\t\t\tgroupLayout.addWidget(lab)\n+\n+\t\tcontrolsLayout.addStretch()\n+\n+\t\tself.camera.start()\n+\n+\tdef closeEvent(self, event):\n+\t\tself.camera.stop()\n+\t\tsuper().closeEvent(event)\n+\n+\tdef handle_request(self, stream, fb):\n+\t\tglobal format_map\n+\n+\t\t#meta = fb.metadata\n+\t\t#print(\"Buf seq {}, bytes {}\".format(meta.sequence, meta.bytesused))\n+\n+\t\twith fb.mmap(0) as b:\n+\t\t\tcfg = stream.configuration\n+\t\t\tqfmt = format_map[cfg.fmt]\n+\t\t\tw, h = cfg.size\n+\t\t\tpitch = cfg.stride\n+\t\t\timg = QtGui.QImage(b, w, h, pitch, qfmt)\n+\t\t\tself.label.setPixmap(QtGui.QPixmap.fromImage(img))\n+\n+\t\tself.frameLabel.setText(\"Queued: {}\\nDone: {}\".format(camera.reqs_queued, camera.reqs_completed))\n+\n+\t\tself.camera.queue_fb(fb)\n+\n+\n+app = QtWidgets.QApplication([])\n+cm = SimpleCameraManager()\n+\n+notif = QtCore.QSocketNotifier(cm.cm.efd, QtCore.QSocketNotifier.Read)\n+notif.activated.connect(lambda x: cm.read_events())\n+\n+if not args.cameras:\n+\tcameras = cm.cameras\n+else:\n+\tcameras = []\n+\tfor name in args.cameras.split(\",\"):\n+\t\tc = cm.find(name)\n+\t\tif not c:\n+\t\t\tprint(\"Camera not found: \", name)\n+\t\t\texit(-1)\n+\t\tcameras.append(c)\n+\n+windows = []\n+\n+i = 0\n+for camera in cameras:\n+\tglobals()[\"cam\" + str(i)] = camera\n+\ti += 1\n+\n+\tcamera.open()\n+\n+\tfmts = camera.formats\n+\tif \"BGR888\" in fmts:\n+\t\tcamera.format = \"BGR888\"\n+\telif \"YUYV\" in fmts:\n+\t\tcamera.format = \"YUYV\"\n+\telse:\n+\t\traise Exception(\"Unsupported pixel format\")\n+\n+\tcamera.resolution = (640, 480)\n+\n+\twindow = MainWindow(camera)\n+\twindow.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)\n+\twindow.show()\n+\twindows.append(window)\n+\n+def cleanup():\n+\tfor w in windows:\n+\t\tw.close()\n+\n+\tfor camera in cameras:\n+\t\tcamera.close()\n+\tprint(\"Done\")\n+\n+import atexit\n+atexit.register(cleanup)\ndiff --git a/src/py/test/run-valgrind.sh b/src/py/test/run-valgrind.sh\nnew file mode 100755\nindex 00000000..7537e507\n--- /dev/null\n+++ b/src/py/test/run-valgrind.sh\n@@ -0,0 +1,6 @@\n+#!/bin/bash\n+\n+export PYTHONMALLOC=malloc\n+export PYTHONPATH=../../../build/debug/src/py\n+\n+valgrind --suppressions=valgrind-pycamera.supp --leak-check=full --show-leak-kinds=definite --gen-suppressions=yes python3 test.py $*\ndiff --git a/src/py/test/run.sh b/src/py/test/run.sh\nnew file mode 100755\nindex 00000000..96f68dcb\n--- /dev/null\n+++ b/src/py/test/run.sh\n@@ -0,0 +1,3 @@\n+#!/bin/bash\n+\n+PYTHONPATH=../../../build/debug/src/py python3 test.py $*\ndiff --git a/src/py/test/simplecamera.py b/src/py/test/simplecamera.py\nnew file mode 100644\nindex 00000000..2051dbeb\n--- /dev/null\n+++ b/src/py/test/simplecamera.py\n@@ -0,0 +1,198 @@\n+import pycamera as pycam\n+import os\n+\n+class SimpleCameraManager:\n+\tdef __init__(self):\n+\t\tself.cm = pycam.CameraManager()\n+\n+\t\tself.cameras = []\n+\t\tfor c in self.cm.cameras:\n+\t\t\tself.cameras.append(SimpleCamera(c))\n+\n+\tdef find(self, name):\n+\t\tfor c in self.cameras:\n+\t\t\tif name.lower() in c.id.lower():\n+\t\t\t\treturn c\n+\n+\t\treturn None\n+\n+\tdef read_events(self):\n+\t\tdata = os.read(self.cm.efd, 8)\n+\n+\t\treqs = self.cm.get_ready_requests()\n+\n+\t\tfor req in reqs:\n+\t\t\tfor c in self.cameras:\n+\t\t\t\tif c.pycam == req.camera:\n+\t\t\t\t\tc.req_complete_cb(req)\n+\n+class SimpleCamera:\n+\tdef __init__(self, camera):\n+\t\tself.pycam = camera\n+\n+\t\tself.callback = None\n+\n+\t\tself.control_values = {}\n+\t\t#for k, (min, max, default) in self.pycam.controls.items():\n+\t\t#\tself.control_values[k] = default\n+\n+\t\tself.running = False\n+\n+\tdef __repr__(self):\n+\t\treturn \"<SimpleCamera '\" + self.pycam.id + \"'>\"\n+\n+\t@property\n+\tdef id(self):\n+\t\treturn self.pycam.id\n+\n+\t@property\n+\tdef formats(self):\n+\t\treturn self.stream_config.formats.pixelFormats\n+\n+\tdef open(self):\n+\t\tself.pycam.acquire()\n+\n+\t\tself.camera_config = self.pycam.generateConfiguration([pycam.StreamRole.Viewfinder])\n+\t\tself.stream_config = self.camera_config.at(0)\n+\n+\tdef close(self):\n+\t\tself.pycam.release()\n+\n+\t@property\n+\tdef properties(self):\n+\t\treturn self.pycam.properties\n+\n+\t@property\n+\tdef controls(self):\n+\t\treturn self.pycam.controls\n+\n+\tdef xxx_config(self):\n+\t\tself.configure_camera()\n+\t\tself.alloc_buffers()\n+\n+\tdef start(self):\n+\t\tself.reqs_queued = 0\n+\t\tself.reqs_completed = 0\n+\n+\t\tself.running = True\n+\t\tself.pycam.start()\n+\n+\t\tself.queue_initial_fbs()\n+\n+\tdef stop(self):\n+\t\tself.running = False\n+\n+\t\tself.pycam.stop()\n+\n+\t\tself.buffers = None\n+\n+\t@property\n+\tdef resolution(self):\n+\t\treturn self.stream_config.size\n+\n+\t@resolution.setter\n+\tdef resolution(self, val):\n+\t\trunning = self.running\n+\t\tif running:\n+\t\t\tself.stop()\n+\n+\t\tself.stream_config.size = val\n+\t\tself.camera_config.validate()\n+\n+\t\tif running:\n+\t\t\tself.start()\n+\n+\t@property\n+\tdef format(self):\n+\t\treturn self.stream_config.fmt\n+\n+\t@format.setter\n+\tdef format(self, val):\n+\t\trunning = self.running\n+\t\tif running:\n+\t\t\tself.stop()\n+\n+\t\tself.stream_config.fmt = val\n+\t\tself.camera_config.validate()\n+\n+\t\tif running:\n+\t\t\tself.start()\n+\n+\tdef configure_camera(self):\n+\t\tcamera = self.pycam\n+\n+\t\tstatus = self.camera_config.validate()\n+\n+\t\tif status == pycam.ConfigurationStatus.Invalid:\n+\t\t\traise Exception(\"Invalid configuration\")\n+\n+\t\tprint(\"Cam: config {}\".format(self.stream_config.toString()))\n+\n+\t\tcamera.configure(self.camera_config);\n+\n+\tdef alloc_buffers(self):\n+\t\tcamera = self.pycam\n+\t\tstream = self.stream_config.stream\n+\n+\t\tallocator = pycam.FrameBufferAllocator(camera);\n+\t\tret = allocator.allocate(stream)\n+\t\tif ret < 0:\n+\t\t\traise Exception(\"Can't allocate buffers\")\n+\n+\t\tself.buffers = allocator.buffers(stream)\n+\n+\t\tprint(\"Cam: Allocated {} buffers for stream\".format(len(self.buffers)))\n+\n+\tdef queue_initial_fbs(self):\n+\t\tbuffers = self.buffers\n+\n+\t\tfor fb in buffers:\n+\t\t\tself.queue_fb(fb)\n+\n+\tdef queue_fb(self, fb):\n+\t\tcamera = self.pycam\n+\t\tstream = self.stream_config.stream\n+\n+\t\trequest = camera.createRequest()\n+\n+\t\tif request == None:\n+\t\t\traise Exception(\"Can't create request\")\n+\n+\t\tret = request.addBuffer(stream, fb)\n+\t\tif ret < 0:\n+\t\t\traise Exception(\"Can't set buffer for request\")\n+\n+\t\t# XXX: ExposureTime cannot be set if AeEnable == True\n+\t\tskip_exp_time = \"AeEnable\" in self.control_values and self.control_values[\"AeEnable\"] == True\n+\n+\t\tfor k, v in self.control_values.items():\n+\t\t\tif k == \"ExposureTime\" and skip_exp_time:\n+\t\t\t\tcontinue\n+\t\t\trequest.set_control(k, v)\n+\n+\t\tcontrol_values = {}\n+\n+\t\tcamera.queueRequest(request)\n+\n+\t\tself.reqs_queued += 1\n+\n+\n+\tdef req_complete_cb(self, req):\n+\t\tcamera = self.pycam\n+\n+\t\tassert(len(req.buffers) == 1)\n+\n+\t\tstream, fb = next(iter(req.buffers.items()))\n+\n+\t\tself.reqs_completed += 1\n+\n+\t\tif self.running and self.callback:\n+\t\t\tself.callback(stream, fb)\n+\n+\tdef set_control(self, control, value):\n+\t\tif not control in self.pycam.controls:\n+\t\t\tfor k in self.pycam.controls:\n+\t\t\t\tif control.lower() == k.lower():\n+\t\t\t\t\tcontrol = k\n+\n+\t\tself.control_values[control] = value\ndiff --git a/src/py/test/test.py b/src/py/test/test.py\nnew file mode 100755\nindex 00000000..86e86043\n--- /dev/null\n+++ b/src/py/test/test.py\n@@ -0,0 +1,210 @@\n+#!/usr/bin/python3\n+\n+import pycamera as pycam\n+import time\n+import binascii\n+import argparse\n+import selectors\n+import os\n+\n+parser = argparse.ArgumentParser()\n+parser.add_argument(\"-n\", \"--num-frames\", type=int, default=10)\n+parser.add_argument(\"-c\", \"--print-crc\", action=\"store_true\")\n+parser.add_argument(\"-s\", \"--save-frames\", action=\"store_true\")\n+parser.add_argument(\"-m\", \"--max-cameras\", type=int, default=1)\n+args = parser.parse_args()\n+\n+cm = pycam.CameraManager()\n+\n+cameras = cm.cameras\n+\n+if len(cameras) == 0:\n+\tprint(\"No cameras\")\n+\texit(0)\n+\n+print(\"Cameras:\")\n+for c in cameras:\n+\tprint(\" {}\".format(c.id))\n+\tprint(\" Properties:\", c.properties)\n+\tprint(\" Controls:\", c.controls)\n+\n+contexts = []\n+\n+for i in range(len(cameras)):\n+\tcontexts.append({ \"camera\": cameras[i], \"id\": i })\n+\tif args.max_cameras and args.max_cameras - 1 == i:\n+\t\tbreak\n+\n+for ctx in contexts:\n+\tctx[\"camera\"].acquire()\n+\n+def configure_camera(ctx):\n+\tcamera = ctx[\"camera\"]\n+\n+\t# Configure\n+\n+\tconfig = camera.generateConfiguration([pycam.StreamRole.Viewfinder])\n+\tstream_config = config.at(0)\n+\n+\t#stream_config.size = (1920, 480)\n+\t#stream_config.fmt = \"BGR888\"\n+\n+\tprint(\"Cam {}: stream config {}\".format(ctx[\"id\"], stream_config.toString()))\n+\n+\tcamera.configure(config);\n+\n+\tctx[\"config\"] = config\n+\n+def alloc_buffers(ctx):\n+\tcamera = ctx[\"camera\"]\n+\tstream = ctx[\"config\"].at(0).stream\n+\n+\tallocator = pycam.FrameBufferAllocator(camera);\n+\tret = allocator.allocate(stream)\n+\tif ret < 0:\n+\t\tprint(\"Can't allocate buffers\")\n+\t\texit(-1)\n+\n+\tallocated = len(allocator.buffers(stream))\n+\tprint(\"Cam {}: Allocated {} buffers for stream\".format(ctx[\"id\"], allocated))\n+\n+\tctx[\"allocator\"] = allocator\n+\n+def create_requests(ctx):\n+\tcamera = ctx[\"camera\"]\n+\tstream = ctx[\"config\"].at(0).stream\n+\tbuffers = ctx[\"allocator\"].buffers(stream)\n+\n+\trequests = []\n+\n+\tb = -1\n+\n+\tfor buffer in buffers:\n+\t\trequest = camera.createRequest()\n+\t\tif request == None:\n+\t\t\tprint(\"Can't create request\")\n+\t\t\texit(-1)\n+\n+\t\tret = request.addBuffer(stream, buffer)\n+\t\tif ret < 0:\n+\t\t\tprint(\"Can't set buffer for request\")\n+\t\t\texit(-1)\n+\n+\t\t#request.set_control(\"Brightness\", b)\n+\t\tb += 0.25\n+\n+\t\trequests.append(request)\n+\n+\tctx[\"requests\"] = requests\n+\n+\n+def req_complete_cb(ctx, req):\n+\tcamera = ctx[\"camera\"]\n+\n+\tprint(\"Cam {}: Req {} Complete: {}\".format(ctx[\"id\"], ctx[\"reqs_completed\"], req.status))\n+\n+\tbufs = req.buffers\n+\tfor stream, fb in bufs.items():\n+\t\tmeta = fb.metadata\n+\t\tprint(\"Cam {}: Buf seq {}, bytes {}\".format(ctx[\"id\"], meta.sequence, meta.bytesused))\n+\n+\t\twith fb.mmap(0) as b:\n+\t\t\tif args.print_crc:\n+\t\t\t\tcrc = binascii.crc32(b)\n+\t\t\t\tprint(\"Cam {}: CRC {:#x}\".format(ctx[\"id\"], crc))\n+\n+\t\t\tif args.save_frames:\n+\t\t\t\tid = ctx[\"id\"]\n+\t\t\t\tnum = ctx[\"reqs_completed\"]\n+\t\t\t\tfilename = \"frame-{}-{}.data\".format(id, num)\n+\t\t\t\twith open(filename, \"wb\") as f:\n+\t\t\t\t\tf.write(b)\n+\t\t\t\tprint(\"Cam {}: Saved {}\".format(ctx[\"id\"], filename))\n+\n+\tctx[\"reqs_completed\"] += 1\n+\n+\tif ctx[\"reqs_queued\"] < args.num_frames:\n+\t\trequest = camera.createRequest()\n+\t\tif request == None:\n+\t\t\tprint(\"Can't create request\")\n+\t\t\texit(-1)\n+\n+\t\tfor stream, fb in bufs.items():\n+\t\t\tret = request.addBuffer(stream, fb)\n+\t\t\tif ret < 0:\n+\t\t\t\tprint(\"Can't set buffer for request\")\n+\t\t\t\texit(-1)\n+\n+\t\tcamera.queueRequest(request)\n+\t\tctx[\"reqs_queued\"] += 1\n+\n+\n+def setup_callbacks(ctx):\n+\tcamera = ctx[\"camera\"]\n+\n+\tctx[\"reqs_queued\"] = 0\n+\tctx[\"reqs_completed\"] = 0\n+\n+def queue_requests(ctx):\n+\tcamera = ctx[\"camera\"]\n+\trequests = ctx[\"requests\"]\n+\n+\tcamera.start()\n+\n+\tfor request in requests:\n+\t\tcamera.queueRequest(request)\n+\t\tctx[\"reqs_queued\"] += 1\n+\n+\n+\n+for ctx in contexts:\n+\tconfigure_camera(ctx)\n+\talloc_buffers(ctx)\n+\tcreate_requests(ctx)\n+\tsetup_callbacks(ctx)\n+\n+for ctx in contexts:\n+\tqueue_requests(ctx)\n+\n+\n+print(\"Processing...\")\n+\n+# Need to release GIL here, so that callbacks can be called\n+#while any(ctx[\"reqs_completed\"] < args.num_frames for ctx in contexts):\n+#\tpycam.sleep(0.1)\n+\n+running = True\n+\n+def readcam(fileobj, mask):\n+\tglobal running\n+\tdata = os.read(fileobj, 8)\n+\n+\treqs = cm.get_ready_requests()\n+\n+\tctx = contexts[0]\n+\tfor req in reqs:\n+\t\tctx = next(ctx for ctx in contexts if ctx[\"camera\"] == req.camera)\n+\t\treq_complete_cb(ctx, req)\n+\n+\trunning = any(ctx[\"reqs_completed\"] < args.num_frames for ctx in contexts)\n+\n+\n+sel = selectors.DefaultSelector()\n+sel.register(cm.efd, selectors.EVENT_READ, readcam)\n+\n+print(\"Press enter to exit\")\n+\n+while running:\n+\tevents = sel.select()\n+\tfor key, mask in events:\n+\t\tcallback = key.data\n+\t\tcallback(key.fileobj, mask)\n+\n+print(\"Exiting...\")\n+\n+for ctx in contexts:\n+\tcamera = ctx[\"camera\"]\n+\tcamera.stop()\n+\tcamera.release()\n+\n+print(\"Done\")\ndiff --git a/src/py/test/valgrind-pycamera.supp b/src/py/test/valgrind-pycamera.supp\nnew file mode 100644\nindex 00000000..98c693f2\n--- /dev/null\n+++ b/src/py/test/valgrind-pycamera.supp\n@@ -0,0 +1,17 @@\n+{\n+ <insert_a_suppression_name_here>\n+ Memcheck:Leak\n+ match-leak-kinds: definite\n+ fun:_Znwm\n+ fun:_ZN8pybind116moduleC1EPKcS2_\n+ fun:PyInit_pycamera\n+ fun:_PyImport_LoadDynamicModuleWithSpec\n+ obj:/usr/bin/python3.8\n+ obj:/usr/bin/python3.8\n+ fun:PyVectorcall_Call\n+ fun:_PyEval_EvalFrameDefault\n+ fun:_PyEval_EvalCodeWithName\n+ fun:_PyFunction_Vectorcall\n+ fun:_PyEval_EvalFrameDefault\n+ fun:_PyFunction_Vectorcall\n+}\ndiff --git a/subprojects/pybind11.wrap b/subprojects/pybind11.wrap\nnew file mode 100644\nindex 00000000..a76ddb1b\n--- /dev/null\n+++ b/subprojects/pybind11.wrap\n@@ -0,0 +1,10 @@\n+[wrap-file]\n+directory = pybind11-2.3.0\n+\n+source_url = https://github.com/pybind/pybind11/archive/v2.3.0.zip\n+source_filename = pybind11-2.3.0.zip\n+source_hash = 1f844c071d9d98f5bb08458f128baa0aa08a9e5939a6b42276adb1bacd8b483e\n+\n+patch_url = https://wrapdb.mesonbuild.com/v1/projects/pybind11/2.3.0/2/get_zip\n+patch_filename = pybind11-2.3.0-2-wrap.zip\n+patch_hash = f3bed4bfc8961b3b985ff1e63fc6e57c674f35b353f0d42adbc037f9416381fb\n", "prefixes": [ "libcamera-devel", "RFC", "v2", "3/4" ] }