Patch Detail
Show a patch.
GET /api/1.1/patches/9671/?format=api
{ "id": 9671, "url": "https://patchwork.libcamera.org/api/1.1/patches/9671/?format=api", "web_url": "https://patchwork.libcamera.org/patch/9671/", "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": "<20200918152019.784315-5-tomi.valkeinen@iki.fi>", "date": "2020-09-18T15:20:19", "name": "[libcamera-devel,RFC,4/4] libcamera python bindings", "commit_ref": null, "pull_url": null, "state": "superseded", "archived": false, "hash": "d766f5376d03c30e2adee40457e6f079588abcea", "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/9671/mbox/", "series": [ { "id": 1297, "url": "https://patchwork.libcamera.org/api/1.1/series/1297/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=1297", "date": "2020-09-18T15:20:15", "name": "prototype python bindings", "version": 1, "mbox": "https://patchwork.libcamera.org/series/1297/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/9671/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/9671/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 3A8DEC3B5D\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 18 Sep 2020 15:21:03 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E5DF362FCD;\n\tFri, 18 Sep 2020 17:21:02 +0200 (CEST)", "from mail-lf1-x136.google.com (mail-lf1-x136.google.com\n\t[IPv6:2a00:1450:4864:20::136])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 7411B62FAB\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 18 Sep 2020 17:21:01 +0200 (CEST)", "by mail-lf1-x136.google.com with SMTP id d15so6511496lfq.11\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 18 Sep 2020 08:21:01 -0700 (PDT)", "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\ti63sm666472lji.66.2020.09.18.08.20.59\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tFri, 18 Sep 2020 08:20:59 -0700 (PDT)" ], "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=\"qV8sDXVM\"; 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=1E1P1JfouIvoqAcSQcz8mxEV25Kx5h+CZg32vriOSpY=;\n\tb=qV8sDXVMUf3mfv7Oqjk/UJNowN9u628qXbnEHmoNHTI8z2s1oPp2Nxkw3UhHoWWa4e\n\twXqhqwRVwt+Foo7355MwM7iE6LciWAlxDcHYa8VTPBLBZ8Ll7n2O1ZD8jW6Mw3Ju65KJ\n\toS2SMxwuM44c5t859GpxwSMNP1CC+3+IXI+GuLvH82rn1y6NUEfG6ZomDGEXT9+Q4I7S\n\t/GKwcTXcqchV8tdrMmhs5DvjMWWXQ1Kjbi8eEtp7FxSA1yVeRAlCTDD9aVH7c2rMzvCf\n\tNKiRIJslvuQPi6ej40T3ICqGjmazhHqcFpYeoNcqPFPTMH5E8j2aGRmV64qm4f4caXXd\n\tXgGw==", "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=1E1P1JfouIvoqAcSQcz8mxEV25Kx5h+CZg32vriOSpY=;\n\tb=LW2OJz7JKSA969AvcFquHa2OIrKU8cWsvEqdr2PWR6ElIV4y0nbW6rgdsLL3cak5ji\n\t0riDSyZOmAA62GhL0R21QeKywiDpmQst1mQjpV2iqWaiSKXpeBB09ccuWL6i/4CkZyZg\n\t7T8iUpD9oG2+8qj5Meuh93vapj0++LJ1gauCb260pkonbgdjfOzIH3tAdk6lIV6jOWyp\n\tTVIEKHrtvBtgUGT6NCuGHB6UkCBKAqA0B3kXnIpZYxEs7XfRukk2Kenuj4W8YEv+jrGE\n\t8IkUEtIO3eyUtu76TU8reIEJys/YW/BnemKj0O/OGi5vhzf813g5+TxvQn+D8fOoB7J/\n\tTKlw==", "X-Gm-Message-State": "AOAM531HYelRHwS6i1H9nDvYQyQ2MYxCahnIN+mwxLePy8OfK+ADh7pN\n\t+16EmvD66asmBu2VgXzZY7Io8Lvg07Ytmg==", "X-Google-Smtp-Source": "ABdhPJyp2/BRuDvRBCqWi9hQEZ2tWCTBSdQ+pb1a5MZWLE8eswextC1isIre11DB8avLEKGjBaFbvg==", "X-Received": "by 2002:a05:6512:1051:: with SMTP id\n\tc17mr12286412lfb.20.1600442460389; \n\tFri, 18 Sep 2020 08:21:00 -0700 (PDT)", "From": "Tomi Valkeinen <tomi.valkeinen@iki.fi>", "To": "libcamera-devel@lists.libcamera.org", "Date": "Fri, 18 Sep 2020 18:20:19 +0300", "Message-Id": "<20200918152019.784315-5-tomi.valkeinen@iki.fi>", "X-Mailer": "git-send-email 2.25.1", "In-Reply-To": "<20200918152019.784315-1-tomi.valkeinen@iki.fi>", "References": "<20200918152019.784315-1-tomi.valkeinen@iki.fi>", "MIME-Version": "1.0", "Subject": "[libcamera-devel] [RFC 4/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- The forced threading causes some headache. Need to take care to use\n gil_scoped_release when C++ context can invoke callbacks, and\n gil_scoped_acquire at the invoke wrapper.\n\n- Callbacks. Difficult to attach context to the callbacks. I solved it\n with BoundMethodFunction and using lambda captures\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 meson.build | 1 +\n meson_options.txt | 2 +\n py/meson.build | 1 +\n py/pycamera/__init__.py | 29 ++++++\n py/pycamera/meson.build | 35 +++++++\n py/pycamera/pymain.cpp | 169 +++++++++++++++++++++++++++++++\n py/test/run-valgrind.sh | 3 +\n py/test/run.sh | 3 +\n py/test/test.py | 177 +++++++++++++++++++++++++++++++++\n py/test/valgrind-pycamera.supp | 17 ++++\n 10 files changed, 437 insertions(+)\n create mode 100644 py/meson.build\n create mode 100644 py/pycamera/__init__.py\n create mode 100644 py/pycamera/meson.build\n create mode 100644 py/pycamera/pymain.cpp\n create mode 100755 py/test/run-valgrind.sh\n create mode 100755 py/test/run.sh\n create mode 100755 py/test/test.py\n create mode 100644 py/test/valgrind-pycamera.supp", "diff": "diff --git a/meson.build b/meson.build\nindex c58d458..3d1c797 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -104,6 +104,7 @@ libcamera_includes = include_directories('include')\n subdir('include')\n subdir('src')\n subdir('utils')\n+subdir('py')\n \n # The documentation and test components are optional and can be disabled\n # through configuration values. They are enabled by default.\ndiff --git a/meson_options.txt b/meson_options.txt\nindex d2e07ef..45b88b6 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -32,3 +32,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/py/meson.build b/py/meson.build\nnew file mode 100644\nindex 0000000..42ffa22\n--- /dev/null\n+++ b/py/meson.build\n@@ -0,0 +1 @@\n+subdir('pycamera')\ndiff --git a/py/pycamera/__init__.py b/py/pycamera/__init__.py\nnew file mode 100644\nindex 0000000..c37571b\n--- /dev/null\n+++ b/py/pycamera/__init__.py\n@@ -0,0 +1,29 @@\n+from .pycamera import *\n+from enum import Enum\n+import os\n+import struct\n+import mmap\n+\n+# Add a wrapper which returns an array of Cameras, which have keep-alive to the CameraManager\n+def __CameraManager__cameras(self):\n+\tcameras = []\n+\tfor i in range(self.num_cameras):\n+\t\tcameras.append(self.at(i))\n+\treturn cameras\n+\n+\n+CameraManager.cameras = property(__CameraManager__cameras)\n+\n+# Add a wrapper which returns an array of buffers, which have keep-alive to the FB allocator\n+def __FrameBufferAllocator__buffers(self, stream):\n+\tbuffers = []\n+\tfor i in range(self.num_buffers(stream)):\n+\t\tbuffers.append(self.at(stream, i))\n+\treturn buffers\n+\n+FrameBufferAllocator.buffers = __FrameBufferAllocator__buffers\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/py/pycamera/meson.build b/py/pycamera/meson.build\nnew file mode 100644\nindex 0000000..50bdfb8\n--- /dev/null\n+++ b/py/pycamera/meson.build\n@@ -0,0 +1,35 @@\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+pycamera_sources = files([\n+ 'pymain.cpp',\n+])\n+\n+pycamera_deps = [\n+ libcamera_dep,\n+ py3_dep,\n+]\n+\n+includes = [\n+ '../../ext/pybind11/include',\n+]\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+ include_directories : includes,\n+ dependencies : pycamera_deps)\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/py/pycamera/pymain.cpp b/py/pycamera/pymain.cpp\nnew file mode 100644\nindex 0000000..569423a\n--- /dev/null\n+++ b/py/pycamera/pymain.cpp\n@@ -0,0 +1,169 @@\n+#include <chrono>\n+#include <thread>\n+#include <fcntl.h>\n+#include <unistd.h>\n+#include <sys/mman.h>\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+PYBIND11_MODULE(pycamera, m) {\n+\tm.def(\"sleep\", [](double s) {\n+\t\tpy::gil_scoped_release release;\n+\t\tthis_thread::sleep_for(std::chrono::duration<double>(s));\n+\t});\n+\n+\tpy::class_<CameraManager>(m, \"CameraManager\")\n+\t\t\t// Call cm->start implicitly, as we can't use stop() either\n+\t\t\t.def(py::init([]() {\n+\t\t\t\tauto cm = make_unique<CameraManager>();\n+\t\t\t\tcm->start();\n+\t\t\t\treturn cm;\n+\t\t\t}))\n+\n+\t\t\t//.def(\"start\", &CameraManager::start)\n+\n+\t\t\t// stop() cannot be called, as CameraManager expects all Camera instances to be released before calling stop\n+\t\t\t// and we can't have such requirement in python, especially as we have a keep-alive from Camera to CameraManager.\n+\t\t\t// So we rely on GC and the keep-alives.\n+\t\t\t//.def(\"stop\", &CameraManager::stop)\n+\n+\t\t\t.def_property_readonly(\"num_cameras\", [](CameraManager& cm) { return cm.cameras().size(); })\n+\t\t\t.def(\"at\", [](CameraManager& cm, unsigned int idx) { return cm.cameras()[idx]; }, py::keep_alive<0, 1>())\n+\t;\n+\n+\tpy::class_<Camera, shared_ptr<Camera>>(m, \"Camera\")\n+\t\t\t.def_property_readonly(\"id\", &Camera::id)\n+\t\t\t.def(\"acquire\", &Camera::acquire)\n+\t\t\t.def(\"release\", &Camera::release)\n+\t\t\t.def(\"start\", &Camera::start)\n+\t\t\t.def(\"stop\", [](shared_ptr<Camera>& self) {\n+\t\t\t\t// Camera::stop can cause callbacks to be invoked, so we must release GIL\n+\t\t\t\tpy::gil_scoped_release release;\n+\t\t\t\tself->stop();\n+\t\t\t})\n+\t\t\t.def(\"generateConfiguration\", &Camera::generateConfiguration)\n+\t\t\t.def(\"configure\", &Camera::configure)\n+\n+\t\t\t// XXX created requests MUST be queued to be freed, python will not free them\n+\t\t\t.def(\"createRequest\", &Camera::createRequest, py::arg(\"cookie\") = 0, py::return_value_policy::reference_internal)\n+\t\t\t.def(\"queueRequest\", &Camera::queueRequest)\n+\n+\t\t\t.def_property(\"requestCompleted\",\n+\t\t\t\t nullptr,\n+\t\t\t\t [](shared_ptr<Camera>& self, function<void(Request*)> f) {\n+\t\t\t\t\t\tif (f) {\n+\t\t\t\t\t\t\tself->requestCompleted.connect(function<void(Request*)>([f = move(f)](Request* req) {\n+\t\t\t\t\t\t\t\t// Called from libcamera's internal thread, so need to get GIL\n+\t\t\t\t\t\t\t\tpy::gil_scoped_acquire acquire;\n+\t\t\t\t\t\t\t\tf(req);\n+\t\t\t\t\t\t\t}));\n+\t\t\t\t\t\t} else {\n+\t\t\t\t\t\t\t// XXX Disconnects all, as we have no means to disconnect the specific std::function\n+\t\t\t\t\t\t\tself->requestCompleted.disconnect();\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t)\n+\n+\t\t\t.def_property(\"bufferCompleted\",\n+\t\t\t\t nullptr,\n+\t\t\t\t [](shared_ptr<Camera>& self, function<void(Request*, FrameBuffer*)> f) {\n+\t\t\t\t\t\tif (f) {\n+\t\t\t\t\t\t\tself->bufferCompleted.connect(function<void(Request*, FrameBuffer* fb)>([f = move(f)](Request* req, FrameBuffer* fb) {\n+\t\t\t\t\t\t\t\t// Called from libcamera's internal thread, so need to get GIL\n+\t\t\t\t\t\t\t\tpy::gil_scoped_acquire acquire;\n+\t\t\t\t\t\t\t\tf(req, fb);\n+\t\t\t\t\t\t\t}));\n+\t\t\t\t\t\t} else {\n+\t\t\t\t\t\t\t// XXX Disconnects all, as we have no means to disconnect the specific std::function\n+\t\t\t\t\t\t\tself->bufferCompleted.disconnect();\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t)\n+\n+\t\t\t;\n+\n+\tpy::class_<CameraConfiguration>(m, \"CameraConfiguration\")\n+\t\t\t.def(\"at\", (StreamConfiguration& (CameraConfiguration::*)(unsigned int))&CameraConfiguration::at,\n+\t\t\t py::return_value_policy::reference_internal)\n+\t\t\t.def(\"validate\", &CameraConfiguration::validate)\n+\t\t\t.def_property_readonly(\"size\", &CameraConfiguration::size)\n+\t\t\t.def_property_readonly(\"empty\", &CameraConfiguration::empty)\n+\t\t\t;\n+\n+\tpy::class_<StreamConfiguration>(m, \"StreamConfiguration\")\n+\t\t\t.def(\"toString\", &StreamConfiguration::toString)\n+\t\t\t.def_property_readonly(\"stream\", &StreamConfiguration::stream,\n+\t\t\t py::return_value_policy::reference_internal)\n+\t\t\t.def_property(\"width\",\n+\t\t\t\t[](StreamConfiguration& c) { return c.size.width; },\n+\t\t\t\t[](StreamConfiguration& c, unsigned int w) { c.size.width = w; }\n+\t\t\t)\n+\t\t\t.def_property(\"height\",\n+\t\t\t\t[](StreamConfiguration& c) { return c.size.height; },\n+\t\t\t\t[](StreamConfiguration& c, unsigned int h) { c.size.height = h; }\n+\t\t\t)\n+\t\t\t.def_property(\"fmt\",\n+\t\t\t\t[](StreamConfiguration& c) { return c.pixelFormat.toString(); },\n+\t\t\t\t[](StreamConfiguration& c, string fmt) { c.pixelFormat = PixelFormat::fromString(fmt); }\n+\t\t\t)\n+\t\t\t;\n+\n+\tpy::enum_<StreamRole>(m, \"StreamRole\")\n+\t\t\t.value(\"StillCapture\", StreamRole::StillCapture)\n+\t\t\t.value(\"StillCaptureRaw\", StreamRole::StillCaptureRaw)\n+\t\t\t.value(\"VideoRecording\", StreamRole::VideoRecording)\n+\t\t\t.value(\"Viewfinder\", StreamRole::Viewfinder)\n+\t\t\t;\n+\n+\tpy::class_<FrameBufferAllocator>(m, \"FrameBufferAllocator\")\n+\t\t\t.def(py::init<shared_ptr<Camera>>(), py::keep_alive<1, 2>())\n+\t\t\t.def(\"allocate\", &FrameBufferAllocator::allocate)\n+\t\t\t.def(\"free\", &FrameBufferAllocator::free)\n+\t\t\t.def(\"num_buffers\", [](FrameBufferAllocator& fa, Stream* stream) { return fa.buffers(stream).size(); })\n+\t\t\t.def(\"at\", [](FrameBufferAllocator& fa, Stream* stream, unsigned int idx) { return fa.buffers(stream).at(idx).get(); },\n+\t\t\t\tpy::return_value_policy::reference_internal)\n+\t\t\t;\n+\n+\tpy::class_<FrameBuffer, unique_ptr<FrameBuffer, py::nodelete>>(m, \"FrameBuffer\")\n+\t\t\t.def_property_readonly(\"metadata\", &FrameBuffer::metadata, py::return_value_policy::reference_internal)\n+\t\t\t.def(\"length\", [](FrameBuffer& fb, uint32_t idx) {\n+\t\t\t\tconst FrameBuffer::Plane &plane = fb.planes()[idx];\n+\t\t\t\treturn plane.length;\n+\t\t\t})\n+\t\t\t.def(\"fd\", [](FrameBuffer& fb, uint32_t idx) {\n+\t\t\t\tconst FrameBuffer::Plane &plane = fb.planes()[idx];\n+\t\t\t\treturn plane.fd.fd();\n+\t\t\t})\n+\t\t\t;\n+\n+\tpy::class_<Stream, unique_ptr<Stream, py::nodelete>>(m, \"Stream\")\n+\t\t\t;\n+\n+\tpy::class_<Request, unique_ptr<Request, py::nodelete>>(m, \"Request\")\n+\t\t\t.def(\"addBuffer\", &Request::addBuffer)\n+\t\t\t.def_property_readonly(\"status\", &Request::status)\n+\t\t\t.def_property_readonly(\"buffers\", &Request::buffers)\n+\t\t\t;\n+\n+\n+\tpy::enum_<Request::Status>(m, \"RequestStatus\")\n+\t\t\t.value(\"Pending\", Request::RequestPending)\n+\t\t\t.value(\"Complete\", Request::RequestComplete)\n+\t\t\t.value(\"Cancelled\", Request::RequestCancelled)\n+\t\t\t;\n+\n+\tpy::class_<FrameMetadata>(m, \"FrameMetadata\")\n+\t\t\t.def_property_readonly(\"sequence\", [](FrameMetadata& data) { return data.sequence; })\n+\t\t\t.def(\"bytesused\", [](FrameMetadata& data, uint32_t idx) { return data.planes[idx].bytesused; })\n+\t\t\t;\n+}\ndiff --git a/py/test/run-valgrind.sh b/py/test/run-valgrind.sh\nnew file mode 100755\nindex 0000000..623188e\n--- /dev/null\n+++ b/py/test/run-valgrind.sh\n@@ -0,0 +1,3 @@\n+#!/bin/bash\n+\n+PYTHONMALLOC=malloc PYTHONPATH=../../build/debug/py valgrind --suppressions=valgrind-pycamera.supp --leak-check=full --show-leak-kinds=definite --gen-suppressions=yes python3 test.py $*\ndiff --git a/py/test/run.sh b/py/test/run.sh\nnew file mode 100755\nindex 0000000..035d3ea\n--- /dev/null\n+++ b/py/test/run.sh\n@@ -0,0 +1,3 @@\n+#!/bin/bash\n+\n+PYTHONPATH=../../build/debug/py python3 test.py $*\ndiff --git a/py/test/test.py b/py/test/test.py\nnew file mode 100755\nindex 0000000..0f874d3\n--- /dev/null\n+++ b/py/test/test.py\n@@ -0,0 +1,177 @@\n+#!/usr/bin/python3\n+\n+import pycamera as pycam\n+import time\n+import binascii\n+import argparse\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)\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+\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.width = 160;\n+\t#stream_config.height = 120;\n+\t#stream_config.fmt = \"YUYV\"\n+\n+\tprint(\"Cam {}: stream config {}\".format(ctx[\"id\"], stream_config.toString()))\n+\n+\tcamera.configure(config);\n+\n+\t# Allocate buffers\n+\n+\tstream = stream_config.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 = allocator.num_buffers(stream)\n+\tprint(\"Cam {}: Allocated {} buffers for stream\".format(ctx[\"id\"], allocated))\n+\n+\t# Create Requests\n+\n+\trequests = []\n+\tbuffers = allocator.buffers(stream)\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\trequests.append(request)\n+\n+\tctx[\"allocator\"] = allocator\n+\tctx[\"requests\"] = requests\n+\n+\n+def buffer_complete_cb(ctx, req, fb):\n+\tprint(\"Cam {}: Buf {} Complete: {}\".format(ctx[\"id\"], ctx[\"bufs_completed\"], req.status))\n+\n+\twith fb.mmap(0) as b:\n+\t\tif args.print_crc:\n+\t\t\tcrc = binascii.crc32(b)\n+\t\t\tprint(\"Cam {}: CRC {:#x}\".format(ctx[\"id\"], crc))\n+\n+\t\tif args.save_frames:\n+\t\t\tid = ctx[\"id\"]\n+\t\t\tnum = ctx[\"bufs_completed\"]\n+\t\t\tfilename = \"frame-{}-{}.data\".format(id, num)\n+\t\t\twith open(filename, \"wb\") as f:\n+\t\t\t\tf.write(b)\n+\t\t\tprint(\"Cam {}: Saved {}\".format(ctx[\"id\"], filename))\n+\n+\tctx[\"bufs_completed\"] += 1\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(0)))\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+\tctx[\"bufs_completed\"] = 0\n+\n+\tcamera.requestCompleted = lambda req, ctx = ctx: req_complete_cb(ctx, req)\n+\tcamera.bufferCompleted = lambda req, fb, ctx = ctx: buffer_complete_cb(ctx, req, fb)\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+\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+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/py/test/valgrind-pycamera.supp b/py/test/valgrind-pycamera.supp\nnew file mode 100644\nindex 0000000..98c693f\n--- /dev/null\n+++ b/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+}\n", "prefixes": [ "libcamera-devel", "RFC", "4/4" ] }