--- 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 @@
+from threading import Thread -from opcua import Client
+from asyncua import Client 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()
- 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)) - 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)) - 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)) - nsid = node.nodeid.NamespaceIndex
- nid = node.nodeid.Identifier
- nid_type = type(nid).__name__
+ nid_type = type(properties.nid).__name__
+ value = [properties.dname,
@@ -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 + 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):
wx.SplitterWindow.__init__(self, parent, -1)
- self.ordered_nodes = []
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.config_getter = config_getter
@@ -279,31 +300,67 @@
if self.client is not None:
- self.client.disconnect()
+ asyncio.run(self.client.disconnect()) + 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 + @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"]) + await client.set_security_string( + "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config)) + # 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) + async def DisconnectAsyncUAClient(self): + if self.client is not None: + await self.client.disconnect() + 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"])
- self.client.set_security_string(
- "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config))
+ rootnode, rootnodeproperties = self.ConnectAsyncUAClient(config) - self.log("OPCUA browser: "+str(e)+"\n")
+ self.log("Exception in OPCUA browser: "+repr(e)+"\n") self.connect_button.SetValue(False)
@@ -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)
- self.client.disconnect()
+ 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) - for subnode in node.get_children():
- self.AddNodeItem(lambda n: self.tree.AppendItem(item, n), subnode)
- self.tree.SetPyData(item,(node, True))
+ 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):
- nsid = node.nodeid.NamespaceIndex
- nid = node.nodeid.Identifier
- dname = node.get_display_name().Text
- cname = node.get_node_class().name
+ def AddNodeItem(self, item_creation_func, node, properties): + item = item_creation_func(properties.dname) - item = item_creation_func(dname)
- if cname == "Variable":
- access = node.get_access_level()
+ if properties.cname == "Variable": + access = properties.access r = ua.AccessLevel.CurrentRead in access
w = ua.AccessLevel.CurrentWrite in access
@@ -387,14 +435,14 @@
ext = "WO" # not sure this one exist
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 + ")" - 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
- if node not in self.ordered_nodes:
- self.ordered_nodes.append(node)
+ if np not in self.ordered_nps: + self.ordered_nps.append(np) # filter out vanished items
- for node in self.ordered_nodes
+ for np in self.ordered_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
# 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)
- 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:])
- 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: