diff --git a/tangostationcontrol/tangostationcontrol/clients/opcua_client.py b/tangostationcontrol/tangostationcontrol/clients/opcua_client.py index ce4447de3b9c8339bd36cc1e75a04ec6867b04de..f0c246a691ea163a7549715bee5d15393cc6674d 100644 --- a/tangostationcontrol/tangostationcontrol/clients/opcua_client.py +++ b/tangostationcontrol/tangostationcontrol/clients/opcua_client.py @@ -58,6 +58,9 @@ class OPCUAConnection(AsyncCommClient): # prefix path to all nodes with this. this allows the user to switch trees more easily. self.node_path_prefix = [] + # cache of looked up child node lists for each comma-separated parent path + self._node_cache = {} + super().__init__(fault_func, event_loop) def _servername(self): @@ -130,13 +133,41 @@ class OPCUAConnection(AsyncCommClient): return path + async def get_node(self, path): + """ Retrieve an OPC-UA node from either the cache, or the server. """ + + if not path: + return self.obj + + cache_key = ",".join(path) + + # lookup in cache + if cache_key in self._node_cache: + return self._node_cache[cache_key] + + # cache it and all of its siblings to save us the round trips for them later on. + parent_path = path[:-1] + parent_node = await self.obj.get_child(parent_path) if parent_path else self.obj + child_nodes = await parent_node.get_children_descriptions() + + for child_node in child_nodes: + # add node to the cache + child_path = parent_path + [f"{self.name_space_index}:{child_node.DisplayName.Text}"] + self._node_cache[",".join(child_path)] = self.client.get_node(child_node.NodeId) + + # lookup in cache again. if the name is valid, it should be in there. + if cache_key in self._node_cache: + return self._node_cache[cache_key] + + # we couldnt find the requested child, ask server directly to get the appropriate error + return await self.obj.get_child(path) async def setup_protocol_attribute(self, annotation, attribute): # process the annotation path = self.get_node_path(annotation) try: - node = await self.obj.get_child(path) + node = await self.get_node(path) except Exception as e: logger.exception("Could not get node: %s on server %s", path, self._servername()) raise Exception("Could not get node: %s on server %s", path, self._servername()) from e @@ -180,7 +211,7 @@ class OPCUAConnection(AsyncCommClient): try: # call method in its parent node - node = await self.obj.get_child(method_path[:-1]) if len(method_path) > 1 else self.obj + node = await self.get_node(method_path[:-1]) result = await node.call_method(method_path[-1], *args) except Exception as e: raise Exception(f"Calling method {method_path} failed") from e diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py index c1c29ee04279bab3c943ccc35d4e3a5071345607..25968deded93d8d45160c5a1949521784ab59e7a 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py @@ -86,6 +86,7 @@ class TestOPCua(base.AsyncTestCase): m_opc_client_members.send_hello = asynctest.asynctest.CoroutineMock() m_objects_node = asynctest.Mock() m_objects_node.get_child = asynctest.asynctest.CoroutineMock() + m_objects_node.get_children_descriptions = asynctest.asynctest.CoroutineMock() m_opc_client_members.get_objects_node = asynctest.Mock(return_value=m_objects_node) m_opc_client.return_value = m_opc_client_members @@ -108,7 +109,7 @@ class TestOPCua(base.AsyncTestCase): m_attribute = mock_attr(i.numpy_type, dim_x, dim_y) # pretend like there is a running OPCua server with a node that has this name - m_annotation = ["2:PCC", f"2:testNode_{str(i.numpy_type)}_{str(dim_x)}_{str(dim_y)}"] + m_annotation = [f"2:testNode_{str(i.numpy_type)}_{str(dim_x)}_{str(dim_y)}"] test_client = OPCUAConnection("opc.tcp://localhost:4874/freeopcua/server/", "http://lofar.eu", 5, mock.Mock(), self.loop) try: