From c959864619888b8e9d2199658d0413394bca4560 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 28 Mar 2024 10:59:54 +0100 Subject: [PATCH] Add patch which implements async GetConnectedDevice The call GetConnectedDeviceSync may block for an extended period of time, especially when the CASE session needs to get reestablished. This adds an implementation of GetConnectedDeviceSync which uses asyncio to keep the asyncio event loop running. --- ...-Use-Python-logging-everywhere-32383.patch | 0 ...nt-async-friendly-GetConnectedDevice.patch | 115 ++++++++++++++++++ 2 files changed, 115 insertions(+) rename 0010-Python-Use-Python-logging-everywhere-32383.patch => 0011-Python-Use-Python-logging-everywhere-32383.patch (100%) create mode 100644 0012-Python-Implement-async-friendly-GetConnectedDevice.patch diff --git a/0010-Python-Use-Python-logging-everywhere-32383.patch b/0011-Python-Use-Python-logging-everywhere-32383.patch similarity index 100% rename from 0010-Python-Use-Python-logging-everywhere-32383.patch rename to 0011-Python-Use-Python-logging-everywhere-32383.patch diff --git a/0012-Python-Implement-async-friendly-GetConnectedDevice.patch b/0012-Python-Implement-async-friendly-GetConnectedDevice.patch new file mode 100644 index 0000000..a6535de --- /dev/null +++ b/0012-Python-Implement-async-friendly-GetConnectedDevice.patch @@ -0,0 +1,115 @@ +From eeaecf615bda4192c31d3cb569f951bede052caa Mon Sep 17 00:00:00 2001 +From: Stefan Agner +Date: Wed, 27 Mar 2024 22:13:19 +0100 +Subject: [PATCH] [Python] Implement async friendly GetConnectedDevice + +Currently GetConnectedDeviceSync() is blocking e.g. when a new session +needs to be created. This is not asyncio friendly as it blocks the +whole event loop. + +Implement a asyncio friendly variant GetConnectedDevice() which is +a co-routine function which can be awaited. +--- + src/controller/python/chip/ChipDeviceCtrl.py | 58 ++++++++++++++++++-- + 1 file changed, 54 insertions(+), 4 deletions(-) + +diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py +index 4a1b3af3e2..08dbdff224 100644 +--- a/src/controller/python/chip/ChipDeviceCtrl.py ++++ b/src/controller/python/chip/ChipDeviceCtrl.py +@@ -780,6 +780,56 @@ class ChipDeviceControllerBase(): + + return DeviceProxyWrapper(returnDevice, self._dmLib) + ++ async def GetConnectedDevice(self, nodeid, allowPASE=True, timeoutMs: int = None): ++ ''' Returns DeviceProxyWrapper upon success.''' ++ self.CheckIsActive() ++ ++ if allowPASE: ++ returnDevice = c_void_p(None) ++ res = self._ChipStack.Call(lambda: self._dmLib.pychip_GetDeviceBeingCommissioned( ++ self.devCtrl, nodeid, byref(returnDevice)), timeoutMs) ++ if res.is_success: ++ logging.info('Using PASE connection') ++ return DeviceProxyWrapper(returnDevice) ++ ++ eventLoop = asyncio.get_running_loop() ++ future = eventLoop.create_future() ++ ++ class DeviceAvailableClosure(): ++ def __init__(self, loop, future: asyncio.Future): ++ self._returnDevice = c_void_p(None) ++ self._returnErr = None ++ self._event_loop = loop ++ self._future = future ++ ++ def _deviceAvailable(self): ++ if self._returnDevice.value is not None: ++ self._future.set_result(self._returnDevice) ++ else: ++ self._future.set_exception(self._returnErr.to_exception()) ++ ++ def deviceAvailable(self, device, err): ++ self._returnDevice = c_void_p(device) ++ self._returnErr = err ++ self._event_loop.call_soon_threadsafe(self._deviceAvailable) ++ ctypes.pythonapi.Py_DecRef(ctypes.py_object(self)) ++ ++ closure = DeviceAvailableClosure(eventLoop, future) ++ ctypes.pythonapi.Py_IncRef(ctypes.py_object(closure)) ++ self._ChipStack.Call(lambda: self._dmLib.pychip_GetConnectedDeviceByNodeId( ++ self.devCtrl, nodeid, ctypes.py_object(closure), _DeviceAvailableCallback), ++ timeoutMs).raise_on_error() ++ ++ # The callback might have been received synchronously (during self._ChipStack.Call()). ++ # In that case the Future has already been set it will return immediately ++ if (timeoutMs): ++ timeout = float(timeoutMs) / 1000 ++ await asyncio.wait_for(future, timeout=timeout) ++ else: ++ await future ++ ++ return DeviceProxyWrapper(future.result(), self._dmLib) ++ + def ComputeRoundTripTimeout(self, nodeid, upperLayerProcessingTimeoutMs: int = 0): + ''' Returns a computed timeout value based on the round-trip time it takes for the peer at the other end of the session to + receive a message, process it and send it back. This is computed based on the session type, the type of transport, +@@ -804,7 +854,7 @@ class ChipDeviceControllerBase(): + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + +- device = self.GetConnectedDeviceSync(nodeid, timeoutMs=None) ++ device = await self.GetConnectedDevice(nodeid, timeoutMs=None) + ClusterCommand.TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke( + future, eventLoop, responseType, device.deviceProxy, ClusterCommand.CommandPath( + EndpointId=endpoint, +@@ -831,7 +881,7 @@ class ChipDeviceControllerBase(): + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + +- device = self.GetConnectedDeviceSync(nodeid, timeoutMs=interactionTimeoutMs) ++ device = await self.GetConnectedDevice(nodeid, timeoutMs=interactionTimeoutMs) + ClusterCommand.SendCommand( + future, eventLoop, responseType, device.deviceProxy, ClusterCommand.CommandPath( + EndpointId=endpoint, +@@ -876,7 +926,7 @@ class ChipDeviceControllerBase(): + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + +- device = self.GetConnectedDeviceSync(nodeid, timeoutMs=interactionTimeoutMs) ++ device = await self.GetConnectedDevice(nodeid, timeoutMs=interactionTimeoutMs) + + attrs = [] + for v in attributes: +@@ -1097,7 +1147,7 @@ class ChipDeviceControllerBase(): + eventLoop = asyncio.get_running_loop() + future = eventLoop.create_future() + +- device = self.GetConnectedDeviceSync(nodeid) ++ device = await self.GetConnectedDevice(nodeid) + attributePaths = [self._parseAttributePathTuple( + v) for v in attributes] if attributes else None + clusterDataVersionFilters = [self._parseDataVersionFilterTuple( +-- +2.44.0 +