diff -r 4582f0fcf4c4 -r 46f3ca3f0157 opc_ua/opcua_client_maker.py --- a/opc_ua/opcua_client_maker.py Wed May 31 23:16:29 2023 +0200 +++ b/opc_ua/opcua_client_maker.py Sun Jun 18 16:28:42 2023 +0200 @@ -2,9 +2,12 @@ import csv - -from opcua import Client -from opcua import ua +import asyncio +import functools +from threading import Thread + +from asyncua import Client +from asyncua import ua import wx import wx.lib.gizmos as gizmos # Formerly wx.gizmos in Classic @@ -182,33 +185,28 @@ # splitter. panel. splitter ClientPanel = self.GetParent().GetParent().GetParent() nodes = ClientPanel.GetSelectedNodes() - for node in nodes: - cname = node.get_node_class().name - dname = node.get_display_name().Text - if cname != "Variable": - self.log("Node {} ignored (not a variable)".format(dname)) + for node, properties in nodes: + if properties.cname != "Variable": + self.log("Node {} ignored (not a variable)".format(properties.dname)) continue - tname = node.get_data_type_as_variant_type().name + tname = properties.variant_type if tname not in UA_IEC_types: - self.log("Node {} ignored (unsupported type)".format(dname)) + self.log("Node {} ignored (unsupported type)".format(properties.dname)) continue - access = node.get_access_level() if {"input":ua.AccessLevel.CurrentRead, - "output":ua.AccessLevel.CurrentWrite}[self.direction] not in access: - self.log("Node {} ignored because of insuficient access rights".format(dname)) + "output":ua.AccessLevel.CurrentWrite}[self.direction] not in properties.access: + self.log("Node {} ignored because of insuficient access rights".format(properties.dname)) continue - nsid = node.nodeid.NamespaceIndex - nid = node.nodeid.Identifier - nid_type = type(nid).__name__ - iecid = nid - - value = [dname, - nsid, + nid_type = type(properties.nid).__name__ + iecid = properties.nid + + value = [properties.dname, + properties.nsid, nid_type, - nid, + properties.nid, tname, iecid] self.model.AddRow(value) @@ -239,18 +237,41 @@ smileidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_OTHER, isz)) +AsyncUAClientLoop = None +def AsyncUAClientLoopProc(): + asyncio.set_event_loop(AsyncUAClientLoop) + AsyncUAClientLoop.run_forever() + +def ExecuteSychronously(func, timeout=1): + def AsyncSychronizer(*args, **kwargs): + global AsyncUAClientLoop + # create asyncio loop + if AsyncUAClientLoop is None: + AsyncUAClientLoop = asyncio.new_event_loop() + Thread(target=AsyncUAClientLoopProc, daemon=True).start() + # schedule work in this loop + future = asyncio.run_coroutine_threadsafe(func(*args, **kwargs), AsyncUAClientLoop) + # wait max 5sec until connection completed + return future.result(timeout) + return AsyncSychronizer + +def ExecuteSychronouslyWithTimeout(timeout): + return functools.partial(ExecuteSychronously,timeout=timeout) + + class OPCUAClientPanel(wx.SplitterWindow): def __init__(self, parent, modeldata, log, config_getter): self.log = log wx.SplitterWindow.__init__(self, parent, -1) - self.ordered_nodes = [] + self.ordered_nps = [] self.inout_panel = wx.Panel(self) self.inout_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) self.inout_sizer.AddGrowableCol(0) self.inout_sizer.AddGrowableRow(1) + self.clientloop = None self.client = None self.config_getter = config_getter @@ -279,31 +300,67 @@ def OnClose(self): if self.client is not None: - self.client.disconnect() + asyncio.run(self.client.disconnect()) self.client = None def __del__(self): self.OnClose() + async def GetAsyncUANodeProperties(self, node): + properties = type("UANodeProperties",(),dict( + nsid = node.nodeid.NamespaceIndex, + nid = node.nodeid.Identifier, + dname = (await node.read_display_name()).Text, + cname = (await node.read_node_class()).name, + )) + if properties.cname == "Variable": + properties.access = await node.get_access_level() + properties.variant_type = (await node.read_data_type_as_variant_type()).name + return properties + + @ExecuteSychronouslyWithTimeout(5) + async def ConnectAsyncUAClient(self, config): + client = Client(config["URI"]) + + AuthType = config["AuthType"] + if AuthType=="UserPasword": + await client.set_user(config["User"]) + await client.set_password(config["Password"]) + elif AuthType=="x509": + await client.set_security_string( + "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config)) + + await client.connect() + self.client = client + + # load definition of server specific structures/extension objects + await self.client.load_type_definitions() + + # returns root node object and its properties + rootnode = self.client.get_root_node() + return rootnode, await self.GetAsyncUANodeProperties(rootnode) + + @ExecuteSychronously + async def DisconnectAsyncUAClient(self): + if self.client is not None: + await self.client.disconnect() + self.client = None + + @ExecuteSychronously + async def GetAsyncUANodeChildren(self, node): + children = await node.get_children() + return [ (child, await self.GetAsyncUANodeProperties(child)) for child in children] + def OnConnectButton(self, event): if self.connect_button.GetValue(): config = self.config_getter() - self.client = Client(config["URI"]) self.log("OPCUA browser: connecting to {}\n".format(config["URI"])) - AuthType = config["AuthType"] - if AuthType=="UserPasword": - self.client.set_user(config["User"]) - self.client.set_password(config["Password"]) - elif AuthType=="x509": - self.client.set_security_string( - "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config)) - try : - self.client.connect() + rootnode, rootnodeproperties = self.ConnectAsyncUAClient(config) except Exception as e: - self.log("OPCUA browser: "+str(e)+"\n") + self.log("Exception in OPCUA browser: "+repr(e)+"\n") self.client = None self.connect_button.SetValue(False) return @@ -328,10 +385,7 @@ self.tree.SetMainColumn(0) - self.client.load_type_definitions() # load definition of server specific structures/extension objects - rootnode = self.client.get_root_node() - - rootitem = self.AddNodeItem(self.tree.AddRoot, rootnode) + rootitem = self.AddNodeItem(self.tree.AddRoot, rootnode, rootnodeproperties) # Populate first level so that root can be expanded self.CreateSubItems(rootitem) @@ -343,7 +397,7 @@ self.tree.Expand(rootitem) - hint = wx.StaticText(self, label = "Drag'n'drop desired variables from tree to Input or Output list") + hint = wx.StaticText(self.tree_panel, label = "Drag'n'drop desired variables from tree to Input or Output list") self.tree_sizer.Add(self.tree, flag=wx.GROW) self.tree_sizer.Add(hint, flag=wx.GROW) @@ -353,29 +407,23 @@ self.SplitVertically(self.tree_panel, self.inout_panel, 500) else: - self.client.disconnect() - self.client = None + self.DisconnectAsyncUAClient() self.Unsplit(self.tree_panel) self.tree_panel.Destroy() - def CreateSubItems(self, item): - node, browsed = self.tree.GetPyData(item) + node, properties, browsed = self.tree.GetPyData(item) if not browsed: - for subnode in node.get_children(): - self.AddNodeItem(lambda n: self.tree.AppendItem(item, n), subnode) - self.tree.SetPyData(item,(node, True)) - - def AddNodeItem(self, item_creation_func, node): - nsid = node.nodeid.NamespaceIndex - nid = node.nodeid.Identifier - dname = node.get_display_name().Text - cname = node.get_node_class().name - - item = item_creation_func(dname) - - if cname == "Variable": - access = node.get_access_level() + children = self.GetAsyncUANodeChildren(node) + for subnode, subproperties in children: + self.AddNodeItem(lambda n: self.tree.AppendItem(item, n), subnode, subproperties) + self.tree.SetPyData(item,(node, properties, True)) + + def AddNodeItem(self, item_creation_func, node, properties): + item = item_creation_func(properties.dname) + + if properties.cname == "Variable": + access = properties.access normalidx = fileidx r = ua.AccessLevel.CurrentRead in access w = ua.AccessLevel.CurrentWrite in access @@ -387,14 +435,14 @@ ext = "WO" # not sure this one exist else: ext = "no access" # not sure this one exist - cname = "Var "+node.get_data_type_as_variant_type().name+" (" + ext + ")" + cname = "Var "+properties.variant_type+" (" + ext + ")" else: normalidx = fldridx - self.tree.SetPyData(item,(node, False)) - self.tree.SetItemText(item, cname, 1) - self.tree.SetItemText(item, str(nsid), 2) - self.tree.SetItemText(item, type(nid).__name__+": "+str(nid), 3) + self.tree.SetPyData(item,(node, properties, False)) + self.tree.SetItemText(item, properties.cname, 1) + self.tree.SetItemText(item, str(properties.nsid), 2) + self.tree.SetItemText(item, type(properties.nid).__name__+": "+str(properties.nid), 3) self.tree.SetItemImage(item, normalidx, which = wx.TreeItemIcon_Normal) self.tree.SetItemImage(item, fldropenidx, which = wx.TreeItemIcon_Expanded) @@ -412,28 +460,28 @@ items = self.tree.GetSelections() items_pydata = [self.tree.GetPyData(item) for item in items] - nodes = [node for node, _unused in items_pydata] + nps = [(node,properties) for node, properties, unused in items_pydata] # append new nodes to ordered list - for node in nodes: - if node not in self.ordered_nodes: - self.ordered_nodes.append(node) + for np in nps: + if np not in self.ordered_nps: + self.ordered_nps.append(np) # filter out vanished items - self.ordered_nodes = [ - node - for node in self.ordered_nodes - if node in nodes] + self.ordered_nps = [ + np + for np in self.ordered_nps + if np in nps] def GetSelectedNodes(self): - return self.ordered_nodes + return self.ordered_nps def OnTreeBeginDrag(self, event): """ Called when a drag is started in tree @param event: wx.TreeEvent """ - if self.ordered_nodes: + if self.ordered_nps: # Just send a recognizable mime-type, drop destination # will get python data from parent data = wx.CustomDataObject(OPCUAClientDndMagicWord) @@ -496,7 +544,7 @@ self[direction] = OPCUAClientList(log, change_callback) def LoadCSV(self,path): - with open(path, 'rb') as csvfile: + with open(path, 'r') as csvfile: reader = csv.reader(csvfile, delimiter=',', quotechar='"') buf = {direction:[] for direction, _model in self.items()} for direction, model in self.items(): @@ -507,7 +555,7 @@ list.append(self[direction],row[1:]) def SaveCSV(self,path): - with open(path, 'wb') as csvfile: + with open(path, 'w') as csvfile: for direction, data in self.items(): writer = csv.writer(csvfile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) @@ -806,7 +854,7 @@ } """ - with open(path, 'wb') as Cfile: + with open(path, 'w') as Cfile: Cfile.write(Ccode)