Issues (2)

uaclient/mainwindow.py (2 issues)

1
#! /usr/bin/env python3
2
3
import sys
4
5
from datetime import datetime
6
import inspect
7
from enum import Enum
8
import logging
9
10
from PyQt5.QtCore import pyqtSignal, QTimer, Qt, QObject, QSettings, QItemSelection, QMimeData, QCoreApplication
11
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QIcon
12
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication, QAbstractItemView, QMenu, QAction
13
14
from opcua import ua
15
from opcua import Node
16
17
from uaclient.uaclient import UaClient
18
from uaclient.mainwindow_ui import Ui_MainWindow
19
from uaclient.connection_dialog import ConnectionDialog
20
from uaclient.graphwidget import GraphUI
21
22
from uawidgets import resources
23
from uawidgets.attrs_widget import AttrsWidget
24
from uawidgets.tree_widget import TreeWidget
25
from uawidgets.refs_widget import RefsWidget
26
from uawidgets.utils import trycatchslot
27
from uawidgets.logger import QtHandler
28
from uawidgets.call_method_dialog import CallMethodDialog
29
30
31
logger = logging.getLogger(__name__)
32
33
34
class DataChangeHandler(QObject):
35
    data_change_fired = pyqtSignal(object, str, str)
36
37
    def datachange_notification(self, node, val, data):
38
        if data.monitored_item.Value.SourceTimestamp:
39
            dato = data.monitored_item.Value.SourceTimestamp.isoformat()
40
        elif data.monitored_item.Value.ServerTimestamp:
41
            dato = data.monitored_item.Value.ServerTimestamp.isoformat()
42
        else:
43
            dato = datetime.now().isoformat()
44
        self.data_change_fired.emit(node, str(val), dato)
45
46
47
class EventHandler(QObject):
48
    event_fired = pyqtSignal(object)
49
50
    def event_notification(self, event):
51
        self.event_fired.emit(event)
52
53
54
class EventUI(object):
55
56 View Code Duplication
    def __init__(self, window, uaclient):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
57
        self.window = window
58
        self.uaclient = uaclient
59
        self._handler = EventHandler()
60
        self._subscribed_nodes = []  # FIXME: not really needed
61
        self.model = QStandardItemModel()
62
        self.window.ui.evView.setModel(self.model)
63
        self.window.ui.actionSubscribeEvent.triggered.connect(self._subscribe)
64
        self.window.ui.actionUnsubscribeEvents.triggered.connect(self._unsubscribe)
65
        # context menu
66
        self.window.addAction(self.window.ui.actionSubscribeEvent)
67
        self.window.addAction(self.window.ui.actionUnsubscribeEvents)
68
        self.window.addAction(self.window.ui.actionAddToGraph)
69
        self._handler.event_fired.connect(self._update_event_model, type=Qt.QueuedConnection)
70
71
        # accept drops
72
        self.model.canDropMimeData = self.canDropMimeData
73
        self.model.dropMimeData = self.dropMimeData
74
75
    def canDropMimeData(self, mdata, action, row, column, parent):
76
        return True
77
78
    def show_error(self, *args):
79
        self.window.show_error(*args)
80
81
    def dropMimeData(self, mdata, action, row, column, parent):
82
        node = self.uaclient.client.get_node(mdata.text())
83
        self._subscribe(node)
84
        return True
85
86
    def clear(self):
87
        self._subscribed_nodes = []
88
        self.model.clear()
89
90
    @trycatchslot
91
    def _subscribe(self, node=None):
92
        logger.info("Subscribing to %s", node)
93
        if not node:
94
            node = self.window.get_current_node()
95
            if node is None:
96
                return
97
        if node in self._subscribed_nodes:
98
            logger.info("already subscribed to event for node: %s", node)
99
            return
100
        logger.info("Subscribing to events for %s", node)
101
        self.window.ui.evDockWidget.raise_()
102
        try:
103
            self.uaclient.subscribe_events(node, self._handler)
104
        except Exception as ex:
105
            self.window.show_error(ex)
106
            raise
107
        else:
108
            self._subscribed_nodes.append(node)
109
110
    @trycatchslot
111
    def _unsubscribe(self):
112
        node = self.window.get_current_node()
113
        if node is None:
114
            return
115
        self._subscribed_nodes.remove(node)
116
        self.uaclient.unsubscribe_events(node)
117
118
    @trycatchslot
119
    def _update_event_model(self, event):
120
        self.model.appendRow([QStandardItem(str(event))])
121
122
123
class DataChangeUI(object):
124
125 View Code Duplication
    def __init__(self, window, uaclient):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
126
        self.window = window
127
        self.uaclient = uaclient
128
        self._subhandler = DataChangeHandler()
129
        self._subscribed_nodes = []
130
        self.model = QStandardItemModel()
131
        self.window.ui.subView.setModel(self.model)
132
        self.window.ui.subView.horizontalHeader().setSectionResizeMode(1)
133
134
        self.window.ui.actionSubscribeDataChange.triggered.connect(self._subscribe)
135
        self.window.ui.actionUnsubscribeDataChange.triggered.connect(self._unsubscribe)
136
137
        # populate contextual menu
138
        self.window.addAction(self.window.ui.actionSubscribeDataChange)
139
        self.window.addAction(self.window.ui.actionUnsubscribeDataChange)
140
141
        # handle subscriptions
142
        self._subhandler.data_change_fired.connect(self._update_subscription_model, type=Qt.QueuedConnection)
143
        
144
        # accept drops
145
        self.model.canDropMimeData = self.canDropMimeData
146
        self.model.dropMimeData = self.dropMimeData
147
148
    def canDropMimeData(self, mdata, action, row, column, parent):
149
        return True
150
151
    def dropMimeData(self, mdata, action, row, column, parent):
152
        node = self.uaclient.client.get_node(mdata.text())
153
        self._subscribe(node)
154
        return True
155
156
    def clear(self):
157
        self._subscribed_nodes = []
158
        self.model.clear()
159
160
    def show_error(self, *args):
161
        self.window.show_error(*args)
162
163
    @trycatchslot
164
    def _subscribe(self, node=None):
165
        if not isinstance(node, Node):
166
            node = self.window.get_current_node()
167
            if node is None:
168
                return
169
        if node in self._subscribed_nodes:
170
            logger.warning("allready subscribed to node: %s ", node)
171
            return
172
        self.model.setHorizontalHeaderLabels(["DisplayName", "Value", "Timestamp"])
173
        text = str(node.get_display_name().Text)
174
        row = [QStandardItem(text), QStandardItem("No Data yet"), QStandardItem("")]
175
        row[0].setData(node)
176
        self.model.appendRow(row)
177
        self._subscribed_nodes.append(node)
178
        self.window.ui.subDockWidget.raise_()
179
        try:
180
            self.uaclient.subscribe_datachange(node, self._subhandler)
181
        except Exception as ex:
182
            self.window.show_error(ex)
183
            idx = self.model.indexFromItem(row[0])
184
            self.model.takeRow(idx.row())
185
            raise
186
187
    @trycatchslot
188
    def _unsubscribe(self):
189
        node = self.window.get_current_node()
190
        if node is None:
191
            return
192
        self.uaclient.unsubscribe_datachange(node)
193
        self._subscribed_nodes.remove(node)
194
        i = 0
195
        while self.model.item(i):
196
            item = self.model.item(i)
197
            if item.data() == node:
198
                self.model.removeRow(i)
199
            i += 1
200
201
    def _update_subscription_model(self, node, value, timestamp):
202
        i = 0
203
        while self.model.item(i):
204
            item = self.model.item(i)
205
            if item.data() == node:
206
                it = self.model.item(i, 1)
207
                it.setText(value)
208
                it_ts = self.model.item(i, 2)
209
                it_ts.setText(timestamp)
210
            i += 1
211
212
213
class Window(QMainWindow):
214
215
    def __init__(self):
216
        QMainWindow.__init__(self)
217
        self.ui = Ui_MainWindow()
218
        self.ui.setupUi(self)
219
        self.setWindowIcon(QIcon(":/network.svg"))
220
221
        # fix stuff imposible to do in qtdesigner
222
        # remove dock titlebar for addressbar
223
        w = QWidget()
224
        self.ui.addrDockWidget.setTitleBarWidget(w)
225
        # tabify some docks
226
        self.tabifyDockWidget(self.ui.evDockWidget, self.ui.subDockWidget)
227
        self.tabifyDockWidget(self.ui.subDockWidget, self.ui.refDockWidget)
228
        self.tabifyDockWidget(self.ui.refDockWidget, self.ui.graphDockWidget)
229
230
        # we only show statusbar in case of errors
231
        self.ui.statusBar.hide()
232
233
        # setup QSettings for application and get a settings object
234
        QCoreApplication.setOrganizationName("FreeOpcUa")
235
        QCoreApplication.setApplicationName("OpcUaClient")
236
        self.settings = QSettings()
237
238
        self._address_list = self.settings.value("address_list", ["opc.tcp://localhost:4840", "opc.tcp://localhost:53530/OPCUA/SimulationServer/"])
239
        print("ADR", self._address_list)
240
        self._address_list_max_count = int(self.settings.value("address_list_max_count", 10))
241
242
        # init widgets
243
        for addr in self._address_list:
244
            self.ui.addrComboBox.insertItem(100, addr)
245
246
        self.uaclient = UaClient()
247
248
        self.tree_ui = TreeWidget(self.ui.treeView)
249
        self.tree_ui.error.connect(self.show_error)
250
        self.setup_context_menu_tree()
251
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
252
253
        self.refs_ui = RefsWidget(self.ui.refView)
254
        self.refs_ui.error.connect(self.show_error)
255
        self.attrs_ui = AttrsWidget(self.ui.attrView)
256
        self.attrs_ui.error.connect(self.show_error)
257
        self.datachange_ui = DataChangeUI(self, self.uaclient)
258
        self.event_ui = EventUI(self, self.uaclient)
259
        self.graph_ui = GraphUI(self, self.uaclient)
260
261
        self.ui.addrComboBox.currentTextChanged.connect(self._uri_changed)
262
        self._uri_changed(self.ui.addrComboBox.currentText())  # force update for current value at startup
263
264
        self.ui.treeView.selectionModel().selectionChanged.connect(self.show_refs)
265
        self.ui.actionCopyPath.triggered.connect(self.tree_ui.copy_path)
266
        self.ui.actionCopyNodeId.triggered.connect(self.tree_ui.copy_nodeid)
267
        self.ui.actionCall.triggered.connect(self.call_method)
268
269
        self.ui.treeView.selectionModel().selectionChanged.connect(self.show_attrs)
270
        self.ui.attrRefreshButton.clicked.connect(self.show_attrs)
271
272
        self.resize(int(self.settings.value("main_window_width", 800)), int(self.settings.value("main_window_height", 600)))
273
        data = self.settings.value("main_window_state", None)
274
        if data:
275
            self.restoreState(data)
276
277
        self.ui.connectButton.clicked.connect(self.connect)
278
        self.ui.disconnectButton.clicked.connect(self.disconnect)
279
        # self.ui.treeView.expanded.connect(self._fit)
280
281
        self.ui.actionConnect.triggered.connect(self.connect)
282
        self.ui.actionDisconnect.triggered.connect(self.disconnect)
283
284
        self.ui.connectOptionButton.clicked.connect(self.show_connection_dialog)
285
286
    def _uri_changed(self, uri):
287
        self.uaclient.load_security_settings(uri)
288
289
    def show_connection_dialog(self):
290
        dia = ConnectionDialog(self, self.ui.addrComboBox.currentText())
291
        dia.security_mode = self.uaclient.security_mode
292
        dia.security_policy = self.uaclient.security_policy
293
        dia.certificate_path = self.uaclient.certificate_path
294
        dia.private_key_path = self.uaclient.private_key_path
295
        ret = dia.exec_()
296
        if ret:
297
            self.uaclient.security_mode = dia.security_mode
298
            self.uaclient.security_policy = dia.security_policy
299
            self.uaclient.certificate_path = dia.certificate_path
300
            self.uaclient.private_key_path = dia.private_key_path
301
302
    @trycatchslot
303
    def show_refs(self, selection):
304
        if isinstance(selection, QItemSelection):
305
            if not selection.indexes(): # no selection
306
                return
307
308
        node = self.get_current_node()
309
        if node:
310
            self.refs_ui.show_refs(node)
311
    
312
    @trycatchslot
313
    def show_attrs(self, selection):
314
        if isinstance(selection, QItemSelection):
315
            if not selection.indexes(): # no selection
316
                return
317
318
        node = self.get_current_node()
319
        if node:
320
            self.attrs_ui.show_attrs(node)
321
322
    def show_error(self, msg):
323
        logger.warning("showing error: %s")
324
        self.ui.statusBar.show()
325
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
326
        self.ui.statusBar.showMessage(str(msg))
327
        QTimer.singleShot(1500, self.ui.statusBar.hide)
328
329
    def get_current_node(self, idx=None):
330
        return self.tree_ui.get_current_node(idx)
331
332
    def get_uaclient(self):
333
        return self.uaclient
334
335
    @trycatchslot
336
    def connect(self):
337
        uri = self.ui.addrComboBox.currentText()
338
        try:
339
            self.uaclient.connect(uri)
340
        except Exception as ex:
341
            self.show_error(ex)
342
            raise
343
344
        self._update_address_list(uri)
345
        self.tree_ui.set_root_node(self.uaclient.client.get_root_node())
346
        self.ui.treeView.setFocus()
347
        self.load_current_node()
348
349
    def _update_address_list(self, uri):
350
        if uri == self._address_list[0]:
351
            return
352
        if uri in self._address_list:
353
            self._address_list.remove(uri)
354
        self._address_list.insert(0, uri)
355
        if len(self._address_list) > self._address_list_max_count:
356
            self._address_list.pop(-1)
357
358
    def disconnect(self):
359
        try:
360
            self.uaclient.disconnect()
361
        except Exception as ex:
362
            self.show_error(ex)
363
            raise
364
        finally:
365
            self.save_current_node()
366
            self.tree_ui.clear()
367
            self.refs_ui.clear()
368
            self.attrs_ui.clear()
369
            self.datachange_ui.clear()
370
            self.event_ui.clear()
371
372
373
    def closeEvent(self, event):
374
        self.tree_ui.save_state()
375
        self.attrs_ui.save_state()
376
        self.refs_ui.save_state()
377
        self.settings.setValue("main_window_width", self.size().width())
378
        self.settings.setValue("main_window_height", self.size().height())
379
        self.settings.setValue("main_window_state", self.saveState())
380
        self.settings.setValue("address_list", self._address_list)
381
        self.disconnect()
382
        event.accept()
383
384
    def save_current_node(self):
385
        current_node = self.tree_ui.get_current_node()
386
        if current_node:
387
            mysettings = self.settings.value("current_node", None)
388
            if mysettings is None:
389
                mysettings = {}
390
            uri = self.ui.addrComboBox.currentText()
391
            mysettings[uri] = current_node.nodeid.to_string()
392
            self.settings.setValue("current_node", mysettings)
393
394
    def load_current_node(self):
395
        mysettings = self.settings.value("current_node", None)
396
        if mysettings is None:
397
            return
398
        uri = self.ui.addrComboBox.currentText()
399
        if uri in mysettings:
400
            nodeid = ua.NodeId.from_string(mysettings[uri])
401
            node = self.uaclient.client.get_node(nodeid)
402
            self.tree_ui.expand_to_node(node)
403
404
    def setup_context_menu_tree(self):
405
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
406
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
407
        self._contextMenu = QMenu()
408
        self.addAction(self.ui.actionCopyPath)
409
        self.addAction(self.ui.actionCopyNodeId)
410
        self._contextMenu.addSeparator()
411
        self._contextMenu.addAction(self.ui.actionCall)
412
        self._contextMenu.addSeparator()
413
414
    def addAction(self, action):
415
        self._contextMenu.addAction(action)
416
417
    @trycatchslot
418
    def _update_actions_state(self, current, previous):
419
        node = self.get_current_node(current)
420
        self.ui.actionCall.setEnabled(False)
421
        if node:
422
            if node.get_node_class() == ua.NodeClass.Method:
423
                self.ui.actionCall.setEnabled(True)
424
425
    def _show_context_menu_tree(self, position):
426
        node = self.tree_ui.get_current_node()
427
        if node:
428
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
429
430
    def call_method(self):
431
        node = self.get_current_node()
432
        dia = CallMethodDialog(self, self.uaclient.client, node)
433
        dia.show()
434
435
436
def main():
437
    app = QApplication(sys.argv)
438
    client = Window()
439
    handler = QtHandler(client.ui.logTextEdit)
440
    logging.getLogger().addHandler(handler)
441
    logging.getLogger("uaclient").setLevel(logging.INFO)
442
    logging.getLogger("uawidgets").setLevel(logging.INFO)
443
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua client library
444
   
445
    client.show()
446
    sys.exit(app.exec_())
447
448
449
if __name__ == "__main__":
450
    main()
451