Show a patch.

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

{
    "id": 18688,
    "url": "https://patchwork.libcamera.org/api/patches/18688/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/18688/",
    "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": "<20230603075615.20663-4-tomi.valkeinen@ideasonboard.com>",
    "date": "2023-06-03T07:56:05",
    "name": "[libcamera-devel,v5,03/13] py: New event handling",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "45db000748723f58b8bab2b24ec493f7a0d38e21",
    "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/18688/mbox/",
    "series": [
        {
            "id": 3904,
            "url": "https://patchwork.libcamera.org/api/series/3904/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=3904",
            "date": "2023-06-03T07:56:02",
            "name": "py: New python bindings event handling",
            "version": 5,
            "mbox": "https://patchwork.libcamera.org/series/3904/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/18688/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/18688/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 73741C328F\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat,  3 Jun 2023 07:57:17 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 29AE562881;\n\tSat,  3 Jun 2023 09:57:13 +0200 (CEST)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 7DE616038F\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSat,  3 Jun 2023 09:57:10 +0200 (CEST)",
            "from desky.lan (91-154-35-171.elisa-laajakaista.fi [91.154.35.171])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 03318B2A;\n\tSat,  3 Jun 2023 09:56:46 +0200 (CEST)"
        ],
        "DKIM-Signature": [
            "v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1685779033;\n\tbh=Zqj/mpGYwoku8B9aPVRIEzQwfIZZ8PoQiko92pYY0sY=;\n\th=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:\n\tFrom;\n\tb=1W8l86rPZaehrTdzbHbc4avfxgGOWIYiKvmO1xVomooJiuO1uZzoiqsNLD6H7uSzg\n\trAH+YtqjMv+5F3arz5lQdPZHQvQ9zwHXT6cbXQE+Bp5ooDy3X1MIZG3aFKlcOoyyYt\n\tM1NIlkRilNzDTTN0cDhJRDohAQnX0g3zxDoye6LLJ/sVZwJ6/vozrHr8Zv9UeMooJK\n\tiCE1aH8kJ2rCxLvLk56gkT+ILppCFZwLU+q8zreB6X2kYvCuWBs38vFraGmpClcVvP\n\tKXo81I9Wufuf5RD7WRX5nfdRjivX7iYYDJlDZNxtP7wBgLc0ZAgXIAIBqK3Wg9IcjL\n\tVw+6aqVlBp8OQ==",
            "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1685779007;\n\tbh=Zqj/mpGYwoku8B9aPVRIEzQwfIZZ8PoQiko92pYY0sY=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=toHcUtLvHKAQ+VRwFUt+Ebje2cibU/v9V8UOYTUpgMvjn1wqUcibqc24RmPEq/b60\n\tFxdrKa8xWgc4CqmWA5dxoV7ZtgT57TYjMpS8CCroguT83u7hfE4jig2nPeCp8A31fb\n\tp+h+xr0CajLhckC87r4laN371BBwce3O5ptnqY3U="
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key; \n\tunprotected) header.d=ideasonboard.com\n\theader.i=@ideasonboard.com\n\theader.b=\"toHcUtLv\"; dkim-atps=neutral",
        "To": "libcamera-devel@lists.libcamera.org",
        "Date": "Sat,  3 Jun 2023 10:56:05 +0300",
        "Message-Id": "<20230603075615.20663-4-tomi.valkeinen@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20230603075615.20663-1-tomi.valkeinen@ideasonboard.com>",
        "References": "<20230603075615.20663-1-tomi.valkeinen@ideasonboard.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Subject": "[libcamera-devel] [PATCH v5 03/13] py: New event handling",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "From": "Tomi Valkeinen via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>",
        "Reply-To": "Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "At the moment the Python bindings only handle the requestCompleted\nevents. But we have others, bufferCompleted and disconnect from the\nCamera, and cameraAdded and cameraRemoved from the CameraManager.\n\nThis patch makes all those events available to the users.\n\nThe get_ready_requests() method is now deprecated, but available to keep\nbackward compatibility.\n\nThe new event handling works like get_ready_requests(), but instead of\nreturning only Requests from requestCompleted events, it returns all\nevent types using a new Event class.\n\nAdditionally camera.stop() has been changed to return events for that\nspecific camera. This serves two purposes: first, it removes the\nRequestCompleted and BufferCompleted events from the event queue, thus\npreventing the old events being returned when the camera is started\nagain, and second, it allows the user to process those events if it so\nwants.\n\nThe CameraManager events (CameraAdded and CameraRemoved) are always\nenabled, but of the Camera events only RequestCompleted is enabled by\ndefault. Other Camera events can be enabled\nCamera.enable_camera_event().\n\nSigned-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>\n---\n Documentation/python-bindings.rst      |  24 ++-\n src/py/libcamera/py_camera_manager.cpp | 275 +++++++++++++++++++++++--\n src/py/libcamera/py_camera_manager.h   |  73 ++++++-\n src/py/libcamera/py_main.cpp           |  54 ++++-\n 4 files changed, 386 insertions(+), 40 deletions(-)",
    "diff": "diff --git a/Documentation/python-bindings.rst b/Documentation/python-bindings.rst\nindex bac5cd34..8f2d76c3 100644\n--- a/Documentation/python-bindings.rst\n+++ b/Documentation/python-bindings.rst\n@@ -43,17 +43,23 @@ CameraManager\n The Python API provides a singleton CameraManager via ``CameraManager.singleton()``.\n There is no need to start or stop the CameraManager.\n \n-Handling Completed Requests\n----------------------------\n+Event Handling\n+--------------\n \n-The Python bindings do not expose the ``Camera::requestCompleted`` signal\n-directly as the signal is invoked from another thread and it has real-time\n-constraints. Instead the bindings queue the completed requests internally and\n-use an eventfd to inform the user that there are completed requests.\n+The Python bindings do not expose the signals from the C++ side directly as the\n+signals are invoked from another thread and they may have real-time\n+constraints. Instead the bindings queue the received events internally and use\n+an eventfd to inform the user that there are events to be handled.\n \n-The user can wait on the eventfd, and upon getting an event, use\n-``CameraManager.get_ready_requests()`` to clear the eventfd event and to get\n-the completed requests.\n+The user can wait on the eventfd (e.g. by using Python Selector), and use\n+``CameraManager.get_events()`` to reset the eventfd and get the events.\n+\n+The CameraManager events (CameraAdded and CameraRemoved) are always enabled, but\n+of the Camera events only RequestCompleted is enabled by default. To enable\n+Disconnect or BufferCompleted event, use ``Camera.enable_camera_event()``.\n+\n+The ``Camera.stop()`` method will return all events related to that Camera from\n+the event queue.\n \n Controls & Properties\n ---------------------\ndiff --git a/src/py/libcamera/py_camera_manager.cpp b/src/py/libcamera/py_camera_manager.cpp\nindex 9ccb7aad..83c2b063 100644\n--- a/src/py/libcamera/py_camera_manager.cpp\n+++ b/src/py/libcamera/py_camera_manager.cpp\n@@ -5,6 +5,7 @@\n \n #include \"py_camera_manager.h\"\n \n+#include <algorithm>\n #include <errno.h>\n #include <memory>\n #include <sys/eventfd.h>\n@@ -35,11 +36,24 @@ PyCameraManager::PyCameraManager()\n \tif (ret)\n \t\tthrow std::system_error(-ret, std::generic_category(),\n \t\t\t\t\t\"Failed to start CameraManager\");\n+\n+\tcameraManager_->cameraAdded.connect(this, &PyCameraManager::handleCameraAdded);\n+\tcameraManager_->cameraRemoved.connect(this, &PyCameraManager::handleCameraRemoved);\n }\n \n PyCameraManager::~PyCameraManager()\n {\n \tLOG(Python, Debug) << \"~PyCameraManager()\";\n+\n+\tcameraManager_->cameraAdded.disconnect();\n+\tcameraManager_->cameraRemoved.disconnect();\n+}\n+\n+void PyCameraManager::init()\n+{\n+\t/* Always enable RequestCompleted for all cameras by default */\n+\tfor (auto cam : cameraManager_->cameras())\n+\t\tsetCameraEventFlag(cam, CameraEventType::RequestCompleted, true);\n }\n \n py::list PyCameraManager::cameras()\n@@ -60,6 +74,43 @@ py::list PyCameraManager::cameras()\n \treturn l;\n }\n \n+PyCameraEvent PyCameraManager::convertEvent(const CameraEvent &event)\n+{\n+\t/*\n+\t * We need to set a keep-alive here so that the camera keeps the\n+\t * camera manager alive.\n+\t */\n+\tpy::object py_cm = py::cast(this);\n+\tpy::object py_cam = py::cast(event.camera_);\n+\tpy::detail::keep_alive_impl(py_cam, py_cm);\n+\n+\tPyCameraEvent pyevent(event.type_, py_cam);\n+\n+\tswitch (event.type_) {\n+\tcase CameraEventType::CameraAdded:\n+\tcase CameraEventType::CameraRemoved:\n+\tcase CameraEventType::Disconnect:\n+\t\t/* No additional parameters to add */\n+\t\tbreak;\n+\n+\tcase CameraEventType::BufferCompleted:\n+\t\tpyevent.request_ = py::cast(event.request_);\n+\t\tpyevent.fb_ = py::cast(event.fb_);\n+\t\tbreak;\n+\n+\tcase CameraEventType::RequestCompleted:\n+\t\tpyevent.request_ = py::cast(event.request_);\n+\n+\t\t/* Decrease the ref increased in Camera.queue_request() */\n+\t\tpyevent.request_.dec_ref();\n+\n+\t\tbreak;\n+\t}\n+\n+\treturn pyevent;\n+}\n+\n+/* DEPRECATED */\n std::vector<py::object> PyCameraManager::getReadyRequests()\n {\n \tint ret = readFd();\n@@ -72,21 +123,207 @@ std::vector<py::object> PyCameraManager::getReadyRequests()\n \n \tstd::vector<py::object> py_reqs;\n \n-\tfor (Request *request : getCompletedRequests()) {\n-\t\tpy::object o = py::cast(request);\n-\t\t/* Decrease the ref increased in Camera.queue_request() */\n-\t\to.dec_ref();\n-\t\tpy_reqs.push_back(o);\n+\tfor (const auto &ev : getEvents()) {\n+\t\tif (ev.type_ != CameraEventType::RequestCompleted)\n+\t\t\tcontinue;\n+\n+\t\tPyCameraEvent pyev = convertEvent(ev);\n+\t\tpy_reqs.push_back(pyev.request_);\n \t}\n \n \treturn py_reqs;\n }\n \n+std::vector<PyCameraEvent> PyCameraManager::getPyEvents()\n+{\n+\tint ret = readFd();\n+\n+\tif (ret == EAGAIN) {\n+\t\tLOG(Python, Debug) << \"No events\";\n+\t\treturn {};\n+\t}\n+\n+\tif (ret != 0)\n+\t\tthrow std::system_error(ret, std::generic_category());\n+\n+\tstd::vector<CameraEvent> events = getEvents();\n+\n+\tLOG(Python, Debug) << \"Got \" << events.size() << \" events\";\n+\n+\tstd::vector<PyCameraEvent> pyevents;\n+\tpyevents.reserve(events.size());\n+\n+\tstd::transform(events.begin(), events.end(), std::back_inserter(pyevents),\n+\t\t       [this](const CameraEvent &ev) {\n+\t\t\t       return convertEvent(ev);\n+\t\t       });\n+\n+\treturn pyevents;\n+}\n+\n+static bool isCameraSpecificEvent(const CameraEvent &event, std::shared_ptr<Camera> &camera)\n+{\n+\treturn event.camera_ == camera &&\n+\t       (event.type_ == CameraEventType::RequestCompleted ||\n+\t\tevent.type_ == CameraEventType::BufferCompleted ||\n+\t\tevent.type_ == CameraEventType::Disconnect);\n+}\n+\n+std::vector<PyCameraEvent> PyCameraManager::getPyCameraEvents(std::shared_ptr<Camera> camera)\n+{\n+\tstd::vector<CameraEvent> events;\n+\tsize_t unhandled_size;\n+\n+\t{\n+\t\tMutexLocker guard(eventsMutex_);\n+\n+\t\t/*\n+\t\t * Collect events related to the given camera and remove them\n+\t\t * from the events_ vector.\n+\t\t */\n+\n+\t\tauto it = events_.begin();\n+\t\twhile (it != events_.end()) {\n+\t\t\tif (isCameraSpecificEvent(*it, camera)) {\n+\t\t\t\tevents.push_back(*it);\n+\t\t\t\tit = events_.erase(it);\n+\t\t\t} else {\n+\t\t\t\tit++;\n+\t\t\t}\n+\t\t}\n+\n+\t\tunhandled_size = events_.size();\n+\t}\n+\n+\t/* Convert events to Python events */\n+\n+\tstd::vector<PyCameraEvent> pyevents;\n+\n+\tfor (const auto &event : events) {\n+\t\tPyCameraEvent pyev = convertEvent(event);\n+\t\tpyevents.push_back(pyev);\n+\t}\n+\n+\tLOG(Python, Debug) << \"Got \" << pyevents.size() << \" camera events, \"\n+\t\t\t   << unhandled_size << \" unhandled events left\";\n+\n+\treturn pyevents;\n+}\n+\n /* Note: Called from another thread */\n-void PyCameraManager::handleRequestCompleted(Request *req)\n+void PyCameraManager::handleBufferCompleted(std::shared_ptr<Camera> cam, Request *req, FrameBuffer *fb)\n {\n-\tpushRequest(req);\n-\twriteFd();\n+\tCameraEvent ev(CameraEventType::BufferCompleted, cam, req, fb);\n+\n+\tpushEvent(ev);\n+}\n+\n+/* Note: Called from another thread */\n+void PyCameraManager::handleRequestCompleted(std::shared_ptr<Camera> cam, Request *req)\n+{\n+\tCameraEvent ev(CameraEventType::RequestCompleted, cam, req);\n+\n+\tpushEvent(ev);\n+}\n+\n+/* Note: Called from another thread */\n+void PyCameraManager::handleDisconnected(std::shared_ptr<Camera> cam)\n+{\n+\tCameraEvent ev(CameraEventType::Disconnect, cam);\n+\n+\tpushEvent(ev);\n+}\n+\n+/* Note: Called from another thread */\n+void PyCameraManager::handleCameraAdded(std::shared_ptr<Camera> cam)\n+{\n+\tCameraEvent ev(CameraEventType::CameraAdded, cam);\n+\n+\tpushEvent(ev);\n+}\n+\n+/* Note: Called from another thread */\n+void PyCameraManager::handleCameraRemoved(std::shared_ptr<Camera> cam)\n+{\n+\tCameraEvent ev(CameraEventType::CameraRemoved, cam);\n+\n+\tpushEvent(ev);\n+}\n+\n+bool PyCameraManager::getCameraEventFlag(std::shared_ptr<Camera> camera, CameraEventType event_type)\n+{\n+\tconst uint32_t evbit = 1 << (uint32_t)event_type;\n+\tuint32_t mask = 0;\n+\n+\tif (auto it = camera_event_masks_.find(camera); it != camera_event_masks_.end())\n+\t\tmask = it->second;\n+\n+\treturn !!(mask & evbit);\n+}\n+\n+void PyCameraManager::setCameraEventFlag(std::shared_ptr<Camera> camera, CameraEventType event_type, bool value)\n+{\n+\tswitch (event_type) {\n+\tcase CameraEventType::RequestCompleted:\n+\tcase CameraEventType::BufferCompleted:\n+\tcase CameraEventType::Disconnect:\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tthrow std::runtime_error(\"Bad camera event type\");\n+\t}\n+\n+\tconst uint32_t evbit = 1 << (uint32_t)event_type;\n+\tuint32_t mask = 0;\n+\n+\tif (auto it = camera_event_masks_.find(camera); it != camera_event_masks_.end())\n+\t\tmask = it->second;\n+\n+\tbool old_val = !!(mask & evbit);\n+\n+\tif (old_val == value)\n+\t\treturn;\n+\n+\tif (value)\n+\t\tmask |= evbit;\n+\telse\n+\t\tmask &= ~evbit;\n+\n+\tcamera_event_masks_[camera] = mask;\n+\n+\tswitch (event_type) {\n+\tcase CameraEventType::RequestCompleted:\n+\t\tif (value) {\n+\t\t\tcamera->requestCompleted.connect(camera.get(), [cm = this->shared_from_this(), camera](Request *req) {\n+\t\t\t\tcm->handleRequestCompleted(camera, req);\n+\t\t\t});\n+\t\t} else {\n+\t\t\tcamera->requestCompleted.disconnect();\n+\t\t}\n+\t\tbreak;\n+\n+\tcase CameraEventType::BufferCompleted:\n+\t\tif (value) {\n+\t\t\tcamera->bufferCompleted.connect(camera.get(), [cm = this->shared_from_this(), camera](Request *req, FrameBuffer *fb) {\n+\t\t\t\tcm->handleBufferCompleted(camera, req, fb);\n+\t\t\t});\n+\t\t} else {\n+\t\t\tcamera->bufferCompleted.disconnect();\n+\t\t}\n+\t\tbreak;\n+\n+\tcase CameraEventType::Disconnect:\n+\t\tif (value) {\n+\t\t\tcamera->disconnected.connect(camera.get(), [cm = this->shared_from_this(), camera]() {\n+\t\t\t\tcm->handleDisconnected(camera);\n+\t\t\t});\n+\t\t} else {\n+\t\t\tcamera->disconnected.disconnect();\n+\t\t}\n+\t\tbreak;\n+\tdefault:\n+\t\tASSERT(false);\n+\t}\n }\n \n void PyCameraManager::writeFd()\n@@ -116,16 +353,24 @@ int PyCameraManager::readFd()\n \t\treturn -EIO;\n }\n \n-void PyCameraManager::pushRequest(Request *req)\n+void PyCameraManager::pushEvent(const CameraEvent &ev)\n {\n-\tMutexLocker guard(completedRequestsMutex_);\n-\tcompletedRequests_.push_back(req);\n+\t{\n+\t\tMutexLocker guard(eventsMutex_);\n+\t\tevents_.push_back(ev);\n+\t}\n+\n+\twriteFd();\n+\n+\tLOG(Python, Debug) << \"Queued events: \" << events_.size();\n }\n \n-std::vector<Request *> PyCameraManager::getCompletedRequests()\n+std::vector<CameraEvent> PyCameraManager::getEvents()\n {\n-\tstd::vector<Request *> v;\n-\tMutexLocker guard(completedRequestsMutex_);\n-\tswap(v, completedRequests_);\n+\tstd::vector<CameraEvent> v;\n+\n+\tMutexLocker guard(eventsMutex_);\n+\tswap(v, events_);\n+\n \treturn v;\n }\ndiff --git a/src/py/libcamera/py_camera_manager.h b/src/py/libcamera/py_camera_manager.h\nindex 3574db23..31747547 100644\n--- a/src/py/libcamera/py_camera_manager.h\n+++ b/src/py/libcamera/py_camera_manager.h\n@@ -13,12 +13,56 @@\n \n using namespace libcamera;\n \n-class PyCameraManager\n+enum class CameraEventType {\n+\tCameraAdded,\n+\tCameraRemoved,\n+\tDisconnect,\n+\tRequestCompleted,\n+\tBufferCompleted,\n+};\n+\n+/*\n+ * This event struct is used internally to queue the events we receive from\n+ * other threads.\n+ */\n+struct CameraEvent {\n+\tCameraEvent(CameraEventType type, std::shared_ptr<Camera> camera,\n+\t\t    Request *request = nullptr, FrameBuffer *fb = nullptr)\n+\t\t: type_(type), camera_(camera), request_(request), fb_(fb)\n+\t{\n+\t}\n+\n+\tCameraEventType type_;\n+\tstd::shared_ptr<Camera> camera_;\n+\tRequest *request_;\n+\tFrameBuffer *fb_;\n+};\n+\n+/*\n+ * This event struct is passed to Python. We need to use pybind11::object here\n+ * instead of a C++ pointer so that we keep a ref to the Request, and a\n+ * keep-alive from the camera to the camera manager.\n+ */\n+struct PyCameraEvent {\n+\tPyCameraEvent(CameraEventType type, pybind11::object camera)\n+\t\t: type_(type), camera_(camera)\n+\t{\n+\t}\n+\n+\tCameraEventType type_;\n+\tpybind11::object camera_;\n+\tpybind11::object request_;\n+\tpybind11::object fb_;\n+};\n+\n+class PyCameraManager : public std::enable_shared_from_this<PyCameraManager>\n {\n public:\n \tPyCameraManager();\n \t~PyCameraManager();\n \n+\tvoid init();\n+\n \tpybind11::list cameras();\n \tstd::shared_ptr<Camera> get(const std::string &name) { return cameraManager_->get(name); }\n \n@@ -26,20 +70,33 @@ public:\n \n \tint eventFd() const { return eventFd_.get(); }\n \n-\tstd::vector<pybind11::object> getReadyRequests();\n+\tstd::vector<pybind11::object> getReadyRequests(); /* DEPRECATED */\n+\tstd::vector<PyCameraEvent> getPyEvents();\n+\tstd::vector<PyCameraEvent> getPyCameraEvents(std::shared_ptr<Camera> camera);\n+\n+\tvoid handleBufferCompleted(std::shared_ptr<Camera> cam, Request *req, FrameBuffer *fb);\n+\tvoid handleRequestCompleted(std::shared_ptr<Camera> cam, Request *req);\n+\tvoid handleDisconnected(std::shared_ptr<Camera> cam);\n+\tvoid handleCameraAdded(std::shared_ptr<Camera> cam);\n+\tvoid handleCameraRemoved(std::shared_ptr<Camera> cam);\n \n-\tvoid handleRequestCompleted(Request *req);\n+\tbool getCameraEventFlag(std::shared_ptr<Camera> camera, CameraEventType event_type);\n+\tvoid setCameraEventFlag(std::shared_ptr<Camera> camera, CameraEventType event_type, bool value);\n \n private:\n \tstd::unique_ptr<CameraManager> cameraManager_;\n \n \tUniqueFD eventFd_;\n-\tlibcamera::Mutex completedRequestsMutex_;\n-\tstd::vector<Request *> completedRequests_\n-\t\tLIBCAMERA_TSA_GUARDED_BY(completedRequestsMutex_);\n+\tlibcamera::Mutex eventsMutex_;\n+\tstd::vector<CameraEvent> events_\n+\t\tLIBCAMERA_TSA_GUARDED_BY(eventsMutex_);\n \n \tvoid writeFd();\n \tint readFd();\n-\tvoid pushRequest(Request *req);\n-\tstd::vector<Request *> getCompletedRequests();\n+\tvoid pushEvent(const CameraEvent &ev);\n+\tstd::vector<CameraEvent> getEvents();\n+\n+\tPyCameraEvent convertEvent(const CameraEvent &event);\n+\n+\tstd::map<std::shared_ptr<Camera>, uint32_t> camera_event_masks_;\n };\ndiff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp\nindex 5a5f1a37..981a3070 100644\n--- a/src/py/libcamera/py_main.cpp\n+++ b/src/py/libcamera/py_main.cpp\n@@ -110,6 +110,7 @@ PYBIND11_MODULE(_libcamera, m)\n \t * https://pybind11.readthedocs.io/en/latest/advanced/misc.html#avoiding-c-types-in-docstrings\n \t */\n \n+\tauto pyEvent = py::class_<PyCameraEvent>(m, \"Event\");\n \tauto pyCameraManager = py::class_<PyCameraManager, std::shared_ptr<PyCameraManager>>(m, \"CameraManager\");\n \tauto pyCamera = py::class_<Camera, PyCameraSmartPtr<Camera>>(m, \"Camera\");\n \tauto pyCameraConfiguration = py::class_<CameraConfiguration>(m, \"CameraConfiguration\");\n@@ -136,12 +137,27 @@ PYBIND11_MODULE(_libcamera, m)\n \tm.def(\"log_set_level\", &logSetLevel);\n \n \t/* Classes */\n+\n+\tpy::enum_<CameraEventType>(pyEvent, \"Type\")\n+\t\t.value(\"CameraAdded\", CameraEventType::CameraAdded)\n+\t\t.value(\"CameraRemoved\", CameraEventType::CameraRemoved)\n+\t\t.value(\"Disconnect\", CameraEventType::Disconnect)\n+\t\t.value(\"RequestCompleted\", CameraEventType::RequestCompleted)\n+\t\t.value(\"BufferCompleted\", CameraEventType::BufferCompleted);\n+\n+\tpyEvent\n+\t\t.def_readonly(\"type\", &PyCameraEvent::type_)\n+\t\t.def_readonly(\"camera\", &PyCameraEvent::camera_)\n+\t\t.def_readonly(\"request\", &PyCameraEvent::request_)\n+\t\t.def_readonly(\"fb\", &PyCameraEvent::fb_);\n+\n \tpyCameraManager\n \t\t.def_static(\"singleton\", []() {\n \t\t\tstd::shared_ptr<PyCameraManager> cm = gCameraManager.lock();\n \n \t\t\tif (!cm) {\n \t\t\t\tcm = std::make_shared<PyCameraManager>();\n+\t\t\t\tcm->init();\n \t\t\t\tgCameraManager = cm;\n \t\t\t}\n \n@@ -153,10 +169,33 @@ PYBIND11_MODULE(_libcamera, m)\n \t\t.def_property_readonly(\"cameras\", &PyCameraManager::cameras)\n \n \t\t.def_property_readonly(\"event_fd\", &PyCameraManager::eventFd)\n-\t\t.def(\"get_ready_requests\", &PyCameraManager::getReadyRequests);\n+\n+\t\t/* DEPRECATED */\n+\t\t.def(\"get_ready_requests\", &PyCameraManager::getReadyRequests)\n+\n+\t\t.def(\"get_events\", &PyCameraManager::getPyEvents);\n \n \tpyCamera\n \t\t.def_property_readonly(\"id\", &Camera::id)\n+\n+\t\t.def(\"get_camera_event_enabled\",\n+\t\t\t[](Camera &self, CameraEventType type) {\n+\t\t\t\tauto cm = gCameraManager.lock();\n+\t\t\t\treturn cm->getCameraEventFlag(self.shared_from_this(), type);\n+\t\t\t})\n+\n+\t\t.def(\"enable_camera_event\",\n+\t\t\t[](Camera &self, CameraEventType type) {\n+\t\t\t\tauto cm = gCameraManager.lock();\n+\t\t\t\tcm->setCameraEventFlag(self.shared_from_this(), type, true);\n+\t\t\t})\n+\n+\t\t.def(\"disable_camera_event\",\n+\t\t\t[](Camera &self, CameraEventType type) {\n+\t\t\t\tauto cm = gCameraManager.lock();\n+\t\t\t\tcm->setCameraEventFlag(self.shared_from_this(), type, false);\n+\t\t\t})\n+\n \t\t.def(\"acquire\", [](Camera &self) {\n \t\t\tint ret = self.acquire();\n \t\t\tif (ret)\n@@ -173,11 +212,6 @@ PYBIND11_MODULE(_libcamera, m)\n \t\t                 const std::unordered_map<const ControlId *, py::object> &controls) {\n \t\t\t/* \\todo What happens if someone calls start() multiple times? */\n \n-\t\t\tauto cm = gCameraManager.lock();\n-\t\t\tASSERT(cm);\n-\n-\t\t\tself.requestCompleted.connect(cm.get(), &PyCameraManager::handleRequestCompleted);\n-\n \t\t\tControlList controlList(self.controls());\n \n \t\t\tfor (const auto &[id, obj] : controls) {\n@@ -187,7 +221,6 @@ PYBIND11_MODULE(_libcamera, m)\n \n \t\t\tint ret = self.start(&controlList);\n \t\t\tif (ret) {\n-\t\t\t\tself.requestCompleted.disconnect();\n \t\t\t\tthrow std::system_error(-ret, std::generic_category(),\n \t\t\t\t\t\t\t\"Failed to start camera\");\n \t\t\t}\n@@ -196,11 +229,16 @@ PYBIND11_MODULE(_libcamera, m)\n \t\t.def(\"stop\", [](Camera &self) {\n \t\t\tint ret = self.stop();\n \n-\t\t\tself.requestCompleted.disconnect();\n+\t\t\tauto cm = gCameraManager.lock();\n+\t\t\tASSERT(cm);\n+\n+\t\t\tauto events = cm->getPyCameraEvents(self.shared_from_this());\n \n \t\t\tif (ret)\n \t\t\t\tthrow std::system_error(-ret, std::generic_category(),\n \t\t\t\t\t\t\t\"Failed to stop camera\");\n+\n+\t\t\treturn events;\n \t\t})\n \n \t\t.def(\"__str__\", [](Camera &self) {\n",
    "prefixes": [
        "libcamera-devel",
        "v5",
        "03/13"
    ]
}