import logging import os import xml.etree.ElementTree as Et from collections import OrderedDict from PyQt5.QtCore import pyqtSignal, QObject, QSettings from opcua import ua from opcua import copy_node from opcua import Node from opcua.common.instantiate import instantiate from opcua.common.ua_utils import data_type_to_variant_type from opcua.common.structures import Struct, StructGenerator from opcua.common.type_dictionary_buider import DataTypeDictionaryBuilder, get_ua_class from uawidgets.utils import trycatchslot from uamodeler.server_manager import ServerManager logger = logging.getLogger(__name__) class _Struct: def __init__(self, name, typename): self.name = name self.typename = typename self.fields = [] class ModelManager(QObject): """ Manage our model. loads xml, start and close, add nodes No dialogs at that level, only api """ error = pyqtSignal(Exception) titleChanged = pyqtSignal(str) modelChanged = pyqtSignal() def __init__(self, modeler): QObject.__init__(self, modeler) self.modeler = modeler self.server_mgr = ServerManager(self.modeler.ui.actionUseOpenUa) self.new_nodes = [] # the added nodes we will save self.current_path = None self.settings = QSettings() self.modified = False self.modeler.attrs_ui.attr_written.connect(self._attr_written) def delete_node(self, node, interactive=True): logger.warning("Deleting: %s", node) if node: deleted_nodes = node.delete(delete_references=True, recursive=True) for dn in deleted_nodes: # make sure we remove ALL instances of node self.new_nodes[:] = (node for node in self.new_nodes if node != dn) if interactive: self.modeler.tree_ui.remove_current_item() def paste_node(self, node): parent = self.modeler.get_current_node() try: added_nodes = copy_node(parent, node) except Exception as ex: self.show_error(ex) raise self.new_nodes.extend(added_nodes) self.modeler.tree_ui.reload_current() self.modeler.show_refs() self.modified = True def close_model(self, force=False): if not force and self.modified: raise RuntimeError("Model is modified, use force to close it") self.modeler.actions.disable_all_actions() self.server_mgr.stop_server() self.current_path = None self.modified = False self.titleChanged.emit("") self.modeler.clear_all_widgets() def new_model(self): if self.modified: raise RuntimeError("Model is modified, cannot create new model") del (self.new_nodes[:]) # empty list while keeping reference endpoint = "opc.tcp://0.0.0.0:48400/freeopcua/uamodeler/" logger.info("Starting server on %s", endpoint) self.server_mgr.start_server(endpoint) self.modeler.tree_ui.set_root_node(self.server_mgr.nodes.root) self.modeler.idx_ui.set_node(self.server_mgr.get_node(ua.ObjectIds.Server_NamespaceArray)) self.modeler.nodesets_ui.set_server_mgr(self.server_mgr) self.modified = False self.modeler.actions.enable_model_actions() self.current_path = None self.titleChanged.emit("No Name") return True def import_xml(self, path): new_nodes = self.server_mgr.import_xml(path) self.new_nodes.extend([self.server_mgr.get_node(node) for node in new_nodes]) self.modified = True # we maybe should only reload the imported nodes self.modeler.tree_ui.reload() self.modeler.idx_ui.reload() return path def open_xml(self, path): self.new_model() try: self._open_xml(path) except: self.close_model(force=True) raise def _open_xml(self, path): path = self.import_xml(path) self.server_mgr.load_enums() self.server_mgr.load_type_definitions() self._show_structs() self.modified = False self.current_path = path self.titleChanged.emit(self.current_path) def _show_structs(self): base_struct = self.server_mgr.get_node(ua.ObjectIds.Structure) opc_binary = self.server_mgr.get_node(ua.ObjectIds.OPCBinarySchema_TypeSystem) opc_schema = self.server_mgr.get_node(ua.ObjectIds.OpcUa_BinarySchema) for node in opc_binary.get_children(): if node == opc_schema: continue # This is standard namespace structures try: ns = node.get_child("0:NamespaceUri").get_value() ar = self.server_mgr.get_namespace_array() idx = ar.index(ns) except ua.UaError: idx = 1 xml = node.get_value() if not xml: return xml = xml.decode("utf-8") generator = StructGenerator() generator.make_model_from_string(xml) for el in generator.model: # we only care about structs, ignoring enums if isinstance(el, Struct): self._add_design_node(base_struct, idx, el) def _add_design_node(self, base_struct, idx, el): try: struct_node = base_struct.get_child(f"{idx}:{el.name}") except ua.UaError: logger.warning("Could not find struct %s under %s", el.name, base_struct) return for field in el.fields: if hasattr(ua.ObjectIds, field.uatype): dtype = self.server_mgr.get_node(getattr(ua.ObjectIds, field.uatype)) else: dtype = self._get_datatype_from_string(idx, field.uatype) if not dtype: logger.warning("Could not find datatype of name %s %s", field.uatype, type(field.uatype)) return vtype = data_type_to_variant_type(dtype) struct_node.add_variable(idx, field.name, field.value, varianttype=vtype, datatype=dtype.nodeid) def _get_datatype_from_string(self, idx, name): #FIXME: this is very heavy and missing recusion, what is the correct way to do that? for node in self.server_mgr.get_node(ua.ObjectIds.BaseDataType).get_children(): try: dtype = node.get_child(f'{idx}:{name}') except ua.UaError: continue return dtype return None def open(self, path): if path.endswith(".xml"): self.open_xml(path) else: self.open_ua_model(path) def open_ua_model(self, path): self.new_model() try: self._open_ua_model(path) except: self.close_model(force=True) raise def _open_ua_model(self, path): tree = Et.parse(path) root = tree.getroot() for ref_el in root.findall("Reference"): refpath = ref_el.attrib['path'] self.modeler.nodesets_ui.import_nodeset(refpath) mod_el = root.find("Model") dirname = os.path.dirname(path) xmlpath = os.path.join(dirname, mod_el.attrib['path']) self._open_xml(xmlpath) if "current_node" in mod_el.attrib: current_node_str = mod_el.attrib['current_node'] nodeid = ua.NodeId.from_string(current_node_str) current_node = self.server_mgr.get_node(nodeid) self.modeler.tree_ui.expand_to_node(current_node) def _get_path(self, path): if path is None: path = self.current_path if path is None: raise ValueError("No path is defined") self.current_path = os.path.splitext(path)[0] self.titleChanged.emit(self.current_path) return self.current_path def save_xml(self, path=None): self._save_structs() path = self._get_path(path) path += ".xml" logger.info("Saving nodes to %s", path) logger.info("Exporting %s nodes: %s", len(self.new_nodes), self.new_nodes) logger.info("and namespaces: %s ", self.server_mgr.get_namespace_array()[1:]) uris = self.server_mgr.get_namespace_array()[1:] self.new_nodes = list(OrderedDict.fromkeys(self.new_nodes)) # remove any potential duplicate self.server_mgr.export_xml(self.new_nodes, uris, path) self.modified = False logger.info("%s saved", path) self._show_structs() #_save_structs has delete our design nodes for structure, we need to recreate them def save_ua_model(self, path=None): path = self._get_path(path) model_path = path + ".uamodel" logger.info("Saving model to %s", model_path) etree = Et.ElementTree(Et.Element('UAModel')) node_el = Et.SubElement(etree.getroot(), "Model") node_el.attrib["path"] = os.path.basename(path) + ".xml" c_node = self.modeler.tree_ui.get_current_node() if c_node: node_el.attrib["current_node"] = c_node.nodeid.to_string() for refpath in self.modeler.nodesets_ui.nodesets: node_el = Et.SubElement(etree.getroot(), "Reference") node_el.attrib["path"] = refpath etree.write(model_path, encoding='utf-8', xml_declaration=True) return model_path def _after_add(self, new_nodes): if isinstance(new_nodes, (list, tuple)): for node in new_nodes: if node not in self.new_nodes: self.new_nodes.append(node) else: if new_nodes not in self.new_nodes: self.new_nodes.append(new_nodes) self.modeler.tree_ui.reload_current() self.modeler.show_refs() self.modified = True def add_method(self, *args): logger.info("Creating method type with args: %s", args) parent = self.modeler.tree_ui.get_current_node() new_nodes = [] new_node = parent.add_method(*args) new_nodes.append(new_node) new_nodes.extend(new_node.get_children()) self._after_add(new_nodes) return new_nodes def add_object_type(self, *args): logger.info("Creating object type with args: %s", args) parent = self.modeler.tree_ui.get_current_node() new_node = parent.add_object_type(*args) self._after_add(new_node) return new_node def add_folder(self, *args): parent = self.modeler.tree_ui.get_current_node() logger.info("Creating folder with args: %s", args) new_node = parent.add_folder(*args) self._after_add(new_node) return new_node def add_object(self, *args): parent = self.modeler.tree_ui.get_current_node() logger.info("Creating object with args: %s", args) nodeid, bname, otype = args new_nodes = instantiate(parent, otype, bname=bname, nodeid=nodeid, dname=ua.LocalizedText(bname.Name)) self._after_add(new_nodes) return new_nodes def add_data_type(self, *args): parent = self.modeler.tree_ui.get_current_node() logger.info("Creating data type with args: %s", args) new_node = parent.add_data_type(*args) self._after_add(new_node) return new_node def add_variable(self, *args): parent = self.modeler.tree_ui.get_current_node() logger.info("Creating variable with args: %s", args) new_node = parent.add_variable(*args) self._after_add(new_node) return new_node def add_property(self, *args): parent = self.modeler.tree_ui.get_current_node() logger.info("Creating property with args: %s", args) new_node = parent.add_property(*args) self._after_add(new_node) return new_node def add_variable_type(self, *args): parent = self.modeler.tree_ui.get_current_node() logger.info("Creating variable type with args: %s", args) nodeid, bname, datatype = args new_node = parent.add_variable_type(nodeid, bname, datatype.nodeid) self._after_add(new_node) return new_node @trycatchslot def _attr_written(self, attr, dv): self.modified = True if attr == ua.AttributeIds.BrowseName: self.modeler.tree_ui.update_browse_name_current_item(dv.Value.Value) elif attr == ua.AttributeIds.DisplayName: self.modeler.tree_ui.update_display_name_current_item(dv.Value.Value) def _create_type_dict_node(self, idx, urn, name): node_id = None # first delete current dict node and its children try: opc_binary = self.server_mgr.get_node(ua.ObjectIds.OPCBinarySchema_TypeSystem) dnode = opc_binary.get_child(f"{idx}:{name}") node_id = dnode.nodeid except ua.UaError: logger.warning("Dictionary node does not exist, creating it: %s", name) builder = DataTypeDictionaryBuilder(self.server_mgr, idx, urn, name, dict_node_id=node_id) if builder.dict_id not in self.new_nodes: self.new_nodes.append(self.server_mgr.get_node(builder.dict_id)) return builder def _save_structs(self): """ Save struct and delete our design nodes. They will need to be recreated """ struct_node = self.server_mgr.get_node(ua.ObjectIds.Structure) dict_name = "TypeDictionary" idx = 1 urn = self.server_mgr.get_namespace_array()[1] to_delete = [] have_structs = False to_add = [] for node in self.new_nodes: # FIXME: we do not support inheritance parent = node.get_parent() if parent == struct_node: if not have_structs: dict_builder = self._create_type_dict_node(idx, urn, dict_name) dict_node = self.server_mgr.get_node(dict_builder.dict_id) have_structs = True bname = node.get_browse_name() try: dict_node.get_child(f"{idx}:{bname.Name}") struct = dict_builder.create_data_type(bname.Name, node.nodeid, init=False) except ua.UaError: logger.warning("DataType %s has not been initialized, doing it", bname) struct = dict_builder.create_data_type(bname.Name, node.nodeid, init=True) childs = node.get_children() for child in childs: bname = child.get_browse_name() try: dtype = child.get_data_type() except ua.UaError: logger.warning("could not get data type for node %s, %s, skipping", child, child.get_browse_name()) continue array = False if isinstance(child.get_value(), list) or child.get_array_dimensions(): array = True dtype_name = Node(node.server, dtype).get_browse_name() struct.add_field(bname.Name, dtype_name.Name, is_array=array) to_delete.append(child) to_add.extend([self.server_mgr.get_node(nodeid) for nodeid in struct.node_ids]) if have_structs: dict_builder.set_dict_byte_string() self.new_nodes.extend(to_add) for node in to_delete: self.delete_node(node, False)