opc_ua/opcua_client_maker.py
changeset 3820 46f3ca3f0157
parent 3750 f62625418bff
equal deleted inserted replaced
3819:4582f0fcf4c4 3820:46f3ca3f0157
     1 
     1 
     2 
     2 
     3 
     3 
     4 import csv
     4 import csv
     5 
     5 import asyncio
     6 from opcua import Client
     6 import functools
     7 from opcua import ua
     7 from threading import Thread
       
     8 
       
     9 from asyncua import Client
       
    10 from asyncua import ua
     8 
    11 
     9 import wx
    12 import wx
    10 import wx.lib.gizmos as gizmos  # Formerly wx.gizmos in Classic
    13 import wx.lib.gizmos as gizmos  # Formerly wx.gizmos in Classic
    11 import wx.dataview as dv
    14 import wx.dataview as dv
    12 
    15 
   180         # Have to find OPC-UA client extension panel from here 
   183         # Have to find OPC-UA client extension panel from here 
   181         # in order to avoid keeping reference (otherwise __del__ isn't called)
   184         # in order to avoid keeping reference (otherwise __del__ isn't called)
   182         #             splitter.        panel.      splitter
   185         #             splitter.        panel.      splitter
   183         ClientPanel = self.GetParent().GetParent().GetParent()
   186         ClientPanel = self.GetParent().GetParent().GetParent()
   184         nodes = ClientPanel.GetSelectedNodes()
   187         nodes = ClientPanel.GetSelectedNodes()
   185         for node in nodes:
   188         for node, properties in nodes:
   186             cname = node.get_node_class().name
   189             if properties.cname != "Variable":
   187             dname = node.get_display_name().Text
   190                 self.log("Node {} ignored (not a variable)".format(properties.dname))
   188             if cname != "Variable":
       
   189                 self.log("Node {} ignored (not a variable)".format(dname))
       
   190                 continue
   191                 continue
   191 
   192 
   192             tname = node.get_data_type_as_variant_type().name
   193             tname = properties.variant_type
   193             if tname not in UA_IEC_types:
   194             if tname not in UA_IEC_types:
   194                 self.log("Node {} ignored (unsupported type)".format(dname))
   195                 self.log("Node {} ignored (unsupported type)".format(properties.dname))
   195                 continue
   196                 continue
   196 
   197 
   197             access = node.get_access_level()
       
   198             if {"input":ua.AccessLevel.CurrentRead,
   198             if {"input":ua.AccessLevel.CurrentRead,
   199                 "output":ua.AccessLevel.CurrentWrite}[self.direction] not in access:
   199                 "output":ua.AccessLevel.CurrentWrite}[self.direction] not in properties.access:
   200                 self.log("Node {} ignored because of insuficient access rights".format(dname))
   200                 self.log("Node {} ignored because of insuficient access rights".format(properties.dname))
   201                 continue
   201                 continue
   202 
   202 
   203             nsid = node.nodeid.NamespaceIndex
   203             nid_type =  type(properties.nid).__name__
   204             nid =  node.nodeid.Identifier
   204             iecid = properties.nid
   205             nid_type =  type(nid).__name__
   205 
   206             iecid = nid
   206             value = [properties.dname,
   207 
   207                      properties.nsid,
   208             value = [dname,
       
   209                      nsid,
       
   210                      nid_type,
   208                      nid_type,
   211                      nid,
   209                      properties.nid,
   212                      tname,
   210                      tname,
   213                      iecid]
   211                      iecid]
   214             self.model.AddRow(value)
   212             self.model.AddRow(value)
   215 
   213 
   216 
   214 
   237     fldropenidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN,   wx.ART_OTHER, isz))
   235     fldropenidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN,   wx.ART_OTHER, isz))
   238     fileidx     = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, isz))
   236     fileidx     = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, isz))
   239     smileidx    = il.Add(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_OTHER, isz))
   237     smileidx    = il.Add(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_OTHER, isz))
   240 
   238 
   241 
   239 
       
   240 AsyncUAClientLoop = None
       
   241 def AsyncUAClientLoopProc():
       
   242     asyncio.set_event_loop(AsyncUAClientLoop)
       
   243     AsyncUAClientLoop.run_forever()
       
   244 
       
   245 def ExecuteSychronously(func, timeout=1):
       
   246     def AsyncSychronizer(*args, **kwargs):
       
   247         global AsyncUAClientLoop
       
   248         # create asyncio loop
       
   249         if AsyncUAClientLoop is None:
       
   250             AsyncUAClientLoop = asyncio.new_event_loop()
       
   251             Thread(target=AsyncUAClientLoopProc, daemon=True).start()
       
   252         # schedule work in this loop
       
   253         future = asyncio.run_coroutine_threadsafe(func(*args, **kwargs), AsyncUAClientLoop)
       
   254         # wait max 5sec until connection completed
       
   255         return future.result(timeout)
       
   256     return AsyncSychronizer
       
   257 
       
   258 def ExecuteSychronouslyWithTimeout(timeout):
       
   259     return functools.partial(ExecuteSychronously,timeout=timeout)
       
   260 
       
   261 
   242 class OPCUAClientPanel(wx.SplitterWindow):
   262 class OPCUAClientPanel(wx.SplitterWindow):
   243     def __init__(self, parent, modeldata, log, config_getter):
   263     def __init__(self, parent, modeldata, log, config_getter):
   244         self.log = log
   264         self.log = log
   245         wx.SplitterWindow.__init__(self, parent, -1)
   265         wx.SplitterWindow.__init__(self, parent, -1)
   246 
   266 
   247         self.ordered_nodes = []
   267         self.ordered_nps = []
   248 
   268 
   249         self.inout_panel = wx.Panel(self)
   269         self.inout_panel = wx.Panel(self)
   250         self.inout_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
   270         self.inout_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
   251         self.inout_sizer.AddGrowableCol(0)
   271         self.inout_sizer.AddGrowableCol(0)
   252         self.inout_sizer.AddGrowableRow(1)
   272         self.inout_sizer.AddGrowableRow(1)
   253 
   273 
       
   274         self.clientloop = None
   254         self.client = None
   275         self.client = None
   255         self.config_getter = config_getter
   276         self.config_getter = config_getter
   256 
   277 
   257         self.connect_button = wx.ToggleButton(self.inout_panel, -1, "Browse Server")
   278         self.connect_button = wx.ToggleButton(self.inout_panel, -1, "Browse Server")
   258 
   279 
   277 
   298 
   278         self.Bind(wx.EVT_TOGGLEBUTTON, self.OnConnectButton, self.connect_button)
   299         self.Bind(wx.EVT_TOGGLEBUTTON, self.OnConnectButton, self.connect_button)
   279 
   300 
   280     def OnClose(self):
   301     def OnClose(self):
   281         if self.client is not None:
   302         if self.client is not None:
   282             self.client.disconnect()
   303             asyncio.run(self.client.disconnect())
   283             self.client = None
   304             self.client = None
   284 
   305 
   285     def __del__(self):
   306     def __del__(self):
   286         self.OnClose()
   307         self.OnClose()
       
   308 
       
   309     async def GetAsyncUANodeProperties(self, node):
       
   310         properties = type("UANodeProperties",(),dict(
       
   311                 nsid = node.nodeid.NamespaceIndex,
       
   312                 nid =  node.nodeid.Identifier,
       
   313                 dname = (await node.read_display_name()).Text,
       
   314                 cname = (await node.read_node_class()).name,
       
   315             ))
       
   316         if properties.cname == "Variable":
       
   317             properties.access = await node.get_access_level()
       
   318             properties.variant_type = (await node.read_data_type_as_variant_type()).name
       
   319         return properties
       
   320 
       
   321     @ExecuteSychronouslyWithTimeout(5)
       
   322     async def ConnectAsyncUAClient(self, config):
       
   323         client = Client(config["URI"])
       
   324         
       
   325         AuthType = config["AuthType"]
       
   326         if AuthType=="UserPasword":
       
   327             await client.set_user(config["User"])
       
   328             await client.set_password(config["Password"])
       
   329         elif AuthType=="x509":
       
   330             await client.set_security_string(
       
   331                 "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config))
       
   332 
       
   333         await client.connect()
       
   334         self.client = client
       
   335 
       
   336         # load definition of server specific structures/extension objects
       
   337         await self.client.load_type_definitions()
       
   338 
       
   339         # returns root node object and its properties
       
   340         rootnode = self.client.get_root_node()
       
   341         return rootnode, await self.GetAsyncUANodeProperties(rootnode)
       
   342 
       
   343     @ExecuteSychronously
       
   344     async def DisconnectAsyncUAClient(self):
       
   345         if self.client is not None:
       
   346             await self.client.disconnect()
       
   347             self.client = None
       
   348 
       
   349     @ExecuteSychronously
       
   350     async def GetAsyncUANodeChildren(self, node):
       
   351         children = await node.get_children()
       
   352         return [ (child, await self.GetAsyncUANodeProperties(child)) for child in children]
   287 
   353 
   288     def OnConnectButton(self, event):
   354     def OnConnectButton(self, event):
   289         if self.connect_button.GetValue():
   355         if self.connect_button.GetValue():
   290             
   356             
   291             config = self.config_getter()
   357             config = self.config_getter()
   292             self.client = Client(config["URI"])
       
   293             self.log("OPCUA browser: connecting to {}\n".format(config["URI"]))
   358             self.log("OPCUA browser: connecting to {}\n".format(config["URI"]))
   294             
   359             
   295             AuthType = config["AuthType"]
       
   296             if AuthType=="UserPasword":
       
   297                 self.client.set_user(config["User"])
       
   298                 self.client.set_password(config["Password"])
       
   299             elif AuthType=="x509":
       
   300                 self.client.set_security_string(
       
   301                     "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config))
       
   302 
       
   303             try :
   360             try :
   304                 self.client.connect()
   361                 rootnode, rootnodeproperties = self.ConnectAsyncUAClient(config)
   305             except Exception as e:
   362             except Exception as e:
   306                 self.log("OPCUA browser: "+str(e)+"\n")
   363                 self.log("Exception in OPCUA browser: "+repr(e)+"\n")
   307                 self.client = None
   364                 self.client = None
   308                 self.connect_button.SetValue(False)
   365                 self.connect_button.SetValue(False)
   309                 return
   366                 return
   310 
   367 
   311             self.tree_panel = wx.Panel(self)
   368             self.tree_panel = wx.Panel(self)
   326                 self.tree.AddColumn(colname)
   383                 self.tree.AddColumn(colname)
   327                 self.tree.SetColumnWidth(idx, width)
   384                 self.tree.SetColumnWidth(idx, width)
   328 
   385 
   329             self.tree.SetMainColumn(0)
   386             self.tree.SetMainColumn(0)
   330 
   387 
   331             self.client.load_type_definitions()  # load definition of server specific structures/extension objects
   388             rootitem = self.AddNodeItem(self.tree.AddRoot, rootnode, rootnodeproperties)
   332             rootnode = self.client.get_root_node()
       
   333 
       
   334             rootitem = self.AddNodeItem(self.tree.AddRoot, rootnode)
       
   335 
   389 
   336             # Populate first level so that root can be expanded
   390             # Populate first level so that root can be expanded
   337             self.CreateSubItems(rootitem)
   391             self.CreateSubItems(rootitem)
   338 
   392 
   339             self.tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self.OnExpand)
   393             self.tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self.OnExpand)
   341             self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeNodeSelection)
   395             self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeNodeSelection)
   342             self.tree.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag)
   396             self.tree.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag)
   343 
   397 
   344             self.tree.Expand(rootitem)
   398             self.tree.Expand(rootitem)
   345 
   399 
   346             hint = wx.StaticText(self, label = "Drag'n'drop desired variables from tree to Input or Output list")
   400             hint = wx.StaticText(self.tree_panel, label = "Drag'n'drop desired variables from tree to Input or Output list")
   347 
   401 
   348             self.tree_sizer.Add(self.tree, flag=wx.GROW)
   402             self.tree_sizer.Add(self.tree, flag=wx.GROW)
   349             self.tree_sizer.Add(hint, flag=wx.GROW)
   403             self.tree_sizer.Add(hint, flag=wx.GROW)
   350             self.tree_sizer.Layout()
   404             self.tree_sizer.Layout()
   351             self.tree_panel.SetAutoLayout(True)
   405             self.tree_panel.SetAutoLayout(True)
   352             self.tree_panel.SetSizer(self.tree_sizer)
   406             self.tree_panel.SetSizer(self.tree_sizer)
   353 
   407 
   354             self.SplitVertically(self.tree_panel, self.inout_panel, 500)
   408             self.SplitVertically(self.tree_panel, self.inout_panel, 500)
   355         else:
   409         else:
   356             self.client.disconnect()
   410             self.DisconnectAsyncUAClient()
   357             self.client = None
       
   358             self.Unsplit(self.tree_panel)
   411             self.Unsplit(self.tree_panel)
   359             self.tree_panel.Destroy()
   412             self.tree_panel.Destroy()
   360 
   413 
   361 
       
   362     def CreateSubItems(self, item):
   414     def CreateSubItems(self, item):
   363         node, browsed = self.tree.GetPyData(item)
   415         node, properties, browsed = self.tree.GetPyData(item)
   364         if not browsed:
   416         if not browsed:
   365             for subnode in node.get_children():
   417             children = self.GetAsyncUANodeChildren(node)
   366                 self.AddNodeItem(lambda n: self.tree.AppendItem(item, n), subnode)
   418             for subnode, subproperties in children:
   367             self.tree.SetPyData(item,(node, True))
   419                 self.AddNodeItem(lambda n: self.tree.AppendItem(item, n), subnode, subproperties)
   368 
   420             self.tree.SetPyData(item,(node, properties, True))
   369     def AddNodeItem(self, item_creation_func, node):
   421 
   370         nsid = node.nodeid.NamespaceIndex
   422     def AddNodeItem(self, item_creation_func, node, properties):
   371         nid =  node.nodeid.Identifier
   423         item = item_creation_func(properties.dname)
   372         dname = node.get_display_name().Text
   424 
   373         cname = node.get_node_class().name
   425         if properties.cname == "Variable":
   374 
   426             access = properties.access
   375         item = item_creation_func(dname)
       
   376 
       
   377         if cname == "Variable":
       
   378             access = node.get_access_level()
       
   379             normalidx = fileidx
   427             normalidx = fileidx
   380             r = ua.AccessLevel.CurrentRead in access
   428             r = ua.AccessLevel.CurrentRead in access
   381             w = ua.AccessLevel.CurrentWrite in access
   429             w = ua.AccessLevel.CurrentWrite in access
   382             if r and w:
   430             if r and w:
   383                 ext = "RW"
   431                 ext = "RW"
   385                 ext = "RO"
   433                 ext = "RO"
   386             elif w:
   434             elif w:
   387                 ext = "WO"  # not sure this one exist
   435                 ext = "WO"  # not sure this one exist
   388             else:
   436             else:
   389                 ext = "no access"  # not sure this one exist
   437                 ext = "no access"  # not sure this one exist
   390             cname = "Var "+node.get_data_type_as_variant_type().name+" (" + ext + ")"
   438             cname = "Var "+properties.variant_type+" (" + ext + ")"
   391         else:
   439         else:
   392             normalidx = fldridx
   440             normalidx = fldridx
   393 
   441 
   394         self.tree.SetPyData(item,(node, False))
   442         self.tree.SetPyData(item,(node, properties, False))
   395         self.tree.SetItemText(item, cname, 1)
   443         self.tree.SetItemText(item, properties.cname, 1)
   396         self.tree.SetItemText(item, str(nsid), 2)
   444         self.tree.SetItemText(item, str(properties.nsid), 2)
   397         self.tree.SetItemText(item, type(nid).__name__+": "+str(nid), 3)
   445         self.tree.SetItemText(item, type(properties.nid).__name__+": "+str(properties.nid), 3)
   398         self.tree.SetItemImage(item, normalidx, which = wx.TreeItemIcon_Normal)
   446         self.tree.SetItemImage(item, normalidx, which = wx.TreeItemIcon_Normal)
   399         self.tree.SetItemImage(item, fldropenidx, which = wx.TreeItemIcon_Expanded)
   447         self.tree.SetItemImage(item, fldropenidx, which = wx.TreeItemIcon_Expanded)
   400 
   448 
   401         return item
   449         return item
   402 
   450 
   410 
   458 
   411     def OnTreeNodeSelection(self, event):
   459     def OnTreeNodeSelection(self, event):
   412         items = self.tree.GetSelections()
   460         items = self.tree.GetSelections()
   413         items_pydata = [self.tree.GetPyData(item) for item in items]
   461         items_pydata = [self.tree.GetPyData(item) for item in items]
   414 
   462 
   415         nodes = [node for node, _unused in items_pydata]
   463         nps = [(node,properties) for node, properties, unused in items_pydata]
   416 
   464 
   417         # append new nodes to ordered list
   465         # append new nodes to ordered list
   418         for node in nodes:
   466         for np in nps:
   419             if node not in self.ordered_nodes:
   467             if np not in self.ordered_nps:
   420                 self.ordered_nodes.append(node)
   468                 self.ordered_nps.append(np)
   421 
   469 
   422         # filter out vanished items
   470         # filter out vanished items
   423         self.ordered_nodes = [
   471         self.ordered_nps = [
   424             node 
   472             np 
   425             for node in self.ordered_nodes 
   473             for np in self.ordered_nps 
   426             if node in nodes]
   474             if np in nps]
   427 
   475 
   428     def GetSelectedNodes(self):
   476     def GetSelectedNodes(self):
   429         return self.ordered_nodes 
   477         return self.ordered_nps 
   430 
   478 
   431     def OnTreeBeginDrag(self, event):
   479     def OnTreeBeginDrag(self, event):
   432         """
   480         """
   433         Called when a drag is started in tree
   481         Called when a drag is started in tree
   434         @param event: wx.TreeEvent
   482         @param event: wx.TreeEvent
   435         """
   483         """
   436         if self.ordered_nodes:
   484         if self.ordered_nps:
   437             # Just send a recognizable mime-type, drop destination
   485             # Just send a recognizable mime-type, drop destination
   438             # will get python data from parent
   486             # will get python data from parent
   439             data = wx.CustomDataObject(OPCUAClientDndMagicWord)
   487             data = wx.CustomDataObject(OPCUAClientDndMagicWord)
   440             dragSource = wx.DropSource(self)
   488             dragSource = wx.DropSource(self)
   441             dragSource.SetData(data)
   489             dragSource.SetData(data)
   494         super(OPCUAClientModel, self).__init__()
   542         super(OPCUAClientModel, self).__init__()
   495         for direction in directions:
   543         for direction in directions:
   496             self[direction] = OPCUAClientList(log, change_callback)
   544             self[direction] = OPCUAClientList(log, change_callback)
   497 
   545 
   498     def LoadCSV(self,path):
   546     def LoadCSV(self,path):
   499         with open(path, 'rb') as csvfile:
   547         with open(path, 'r') as csvfile:
   500             reader = csv.reader(csvfile, delimiter=',', quotechar='"')
   548             reader = csv.reader(csvfile, delimiter=',', quotechar='"')
   501             buf = {direction:[] for direction, _model in self.items()}
   549             buf = {direction:[] for direction, _model in self.items()}
   502             for direction, model in self.items():
   550             for direction, model in self.items():
   503                 self[direction][:] = []
   551                 self[direction][:] = []
   504             for row in reader:
   552             for row in reader:
   505                 direction = row[0]
   553                 direction = row[0]
   506                 # avoids calling change callback whe loading CSV
   554                 # avoids calling change callback whe loading CSV
   507                 list.append(self[direction],row[1:])
   555                 list.append(self[direction],row[1:])
   508 
   556 
   509     def SaveCSV(self,path):
   557     def SaveCSV(self,path):
   510         with open(path, 'wb') as csvfile:
   558         with open(path, 'w') as csvfile:
   511             for direction, data in self.items():
   559             for direction, data in self.items():
   512                 writer = csv.writer(csvfile, delimiter=',',
   560                 writer = csv.writer(csvfile, delimiter=',',
   513                                 quotechar='"', quoting=csv.QUOTE_MINIMAL)
   561                                 quotechar='"', quoting=csv.QUOTE_MINIMAL)
   514                 for row in data:
   562                 for row in data:
   515                     writer.writerow([direction] + row)
   563                     writer.writerow([direction] + row)
   804 
   852 
   805     return EXIT_SUCCESS;
   853     return EXIT_SUCCESS;
   806 }
   854 }
   807 """
   855 """
   808 
   856 
   809             with open(path, 'wb') as Cfile:
   857             with open(path, 'w') as Cfile:
   810                 Cfile.write(Ccode)
   858                 Cfile.write(Ccode)
   811 
   859 
   812 
   860 
   813         dlg.Destroy()
   861         dlg.Destroy()
   814 
   862