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) |