diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ff9f0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +server/__pycache__ +uaxplorer/ui_methods/__pycache__ +uaxplorer/ui_methods/__pycache__/discovery.cpython-39.pyc +uaxplorer/ui_methods/__pycache__/server_discovery.cpython-39.pyc +*.pyc diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eaefbe4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.enabled": true, + "python.pythonPath": "/usr/sbin/python" +} \ No newline at end of file diff --git a/server/__pycache__/server.cpython-38.pyc b/server/__pycache__/server.cpython-38.pyc new file mode 100644 index 0000000..535f52e Binary files /dev/null and b/server/__pycache__/server.cpython-38.pyc differ diff --git a/server/announce_service.py b/server/announce_service.py new file mode 100644 index 0000000..da062eb --- /dev/null +++ b/server/announce_service.py @@ -0,0 +1,38 @@ +from zeroconf import ServiceInfo, Zeroconf +import socket + +# This file handles the announcement of the OPCUA +# service for zeroconf discovery to work. +# +# Usage: +# Call the start_service_announcement() function. +# Input is name of the device and the port it's using. +# The device name should take the form "_NAME.". +# The beginning underscore and ending dot is important. + +# Standard OPCUA server port, per IANA. +ZC_PORT = 4840 +# Test +DEV_NAME = "_testopc." + +def start_service_announcement(device_name=DEV_NAME, iport=ZC_PORT): + info = ServiceInfo("_opcua-tcp._tcp.local.", + device_name + "_opcua-tcp._tcp.local.", + port = iport, + server=device_name, + addresses=[socket.inet_pton(socket.AF_INET, get_ip_address())]) + zeroconf = Zeroconf() + zeroconf.register_service(info) + +# Hacky way to get ip address which isn't localhost under linux. +# The google dns ip does not have to be routeable. +def get_ip_address(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + +if __name__ == "__main__": + print("Starting service announcement for testing purposes!") + print("Press enter to exit.") + start_service_announcement() + input() diff --git a/server/localclient.py b/server/localclient.py new file mode 100644 index 0000000..63b385a --- /dev/null +++ b/server/localclient.py @@ -0,0 +1,30 @@ +from asyncua import Client + +# Example of url: +# 'opc.tcp://localhost:4840/freeopcua/server/' +class LocalClient(): + def __init__(self, url): + self.nodes = {} + self.values = {} + self.url = url + + def run(self): + async with Client(self.url) as client: + while True: + for n in self.nodes.keys(): + node = client.get_node(n) + value = await node.read_value() + self.values[self.nodes[n]] = value + + def add_node(self, node, name): + self.nodes[node] = name + + def remove_node(self, node, name): + self.values.pop(name) + self.nodes.pop(node) + + def get_value(self, name): + return self.values[name]) + +if __name__=="__main__": + c = LocalClient() diff --git a/server/py.server.py b/server/py.server.py new file mode 100644 index 0000000..c88ad6f --- /dev/null +++ b/server/py.server.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +from opcua import Server +from random import randint +import datetime +import time, sys +sys.path.insert(1, '/Users/FKV/Desktop/simpysim/OPCUA-Communication/server') +import announce_service as sa + +server = Server() + +url = "opc.tcp://192.168.10.196:4840" +server.set_endpoint(url) + +name = "vvvvvvvvvvvvvvv" + +addspace = server.register_namespace(name) + +node = server.get_objects_node() +t = node.add_variable(addspace,"Lol", 0) +Params = node.add_object(addspace, "Parameters") +New_object = Params.add_object(addspace, "Ddad") +tt = New_object.add_object(addspace, "Hi") +ff = tt.add_variable(addspace, "Yoo", 0) +Params2 = node.add_object(addspace, "Params2") +Params3 = Params2.add_object(addspace, "Params5") +Params4 = Params3.add_object(addspace, "Params6") +Params5 = Params4.add_object(addspace, "Params8") +Ff = Params3.add_variable(addspace, "Hello", 0) +tt = Params4.add_variable(addspace, "ff", 0) +ttr = Params5.add_variable(addspace, "feeef", 0) +Temp = Params.add_variable(addspace, "ixTemperature",0) +Press = Params.add_variable(addspace, "ixPressure",0) +Nothing = Params.add_variable(addspace, "qxNothing",0) +Time = Params.add_variable(addspace, "ixTime",0) +Test = Params.add_variable(addspace, "ixTest",0) +Test3 = Params.add_variable(addspace, "qxTesting",0) +Test4 = Params2.add_variable(addspace, "qxTrue", 0) + +Temp.set_writable() +Press.set_writable() +Time.set_writable() +Test.set_writable() +#starting server +server.start() +sa.start_service_announcement(device_name="kfd347-server.", +iport=4840) + +while True: + Temperature = randint(0,35) + Pressure = randint(100,250) + TIME = datetime.datetime.now() + + print(Temperature,Pressure,TIME) + + Temp.set_value(Temperature) + Press.set_value(Pressure) + Time.set_value(TIME) + + time.sleep(5) diff --git a/server/server.py b/server/server.py new file mode 100644 index 0000000..3c96a2c --- /dev/null +++ b/server/server.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# Credits to http://courses.compute.dtu.dk/02619/software/opcda_to_opcua.py +# Helped with the conversion of OPCDA to OPCUA + +import sys +import asyncio +import OpenOPC +import decimal +import time +import pywintypes +from datetime import datetime +from asyncua import ua, Server, uamethod +from zeroconf import ServiceInfo, Zeroconf +import socket + +# local imports +import announce_service as sa + +pywintypes.datetime = pywintypes.TimeType + +UA_URI = 'https://hv.se' +OPCDA_SERVER_STRING = "" +readable_variables = {} +writeable_variables = {} +tree = {} +obj_in_node = {} + + +# Constants +ITEM_ACCESS_RIGHTS = 5 +ACCESS_READ = 0 +ACCESS_WRITE = 1 +ACCESS_READ_WRITE = 2 +ITEM_VALUE = 2 + + +class SubHandler(object): + """ + Subscription handler to receive events from the server. + """ + + def __init__(self, n, opcda_string): + self.i = 0 + self.opcda_string = opcda_string + self.n = n + + async def final_datachange_notification(self, node, val, data): + # Get a list containing root, objects, opc da server + p_a_string = await node.get_path(as_string=True) + print(p_a_string) + #print(da.servers()[0]) + da_address = '.'.join([a.split(':')[1] for a in p_a_string[3:]]) + + da = OpenOPC.client() + + da.connect(self.opcda_string) + print('Datachanged ', da_address, val) + da.write((da_address, val,)) + da.close() + + def datachange_notification(self, node, val, data): + self.i = self.i + 1 + + if(self.i == self.n): + self.datachange_notification = self.final_datachange_notification + +def read_value(value): + value = value[0] + if isinstance(value,decimal.Decimal): + value = float(value) + elif isinstance(value,list): + if len(value) == 0: + value = None + elif isinstance(value,tuple): + if len(value) == 0: + value = None + + return value + +async def sort_nodes_list(list, idx, root, da): + + for node in list: + parts = node.split('.') #We split it into parts to separate each "part" + folders = parts[:-1] # The first part is the folder, typically multiple ones + file = parts[-1] #Then we have the file that is in the folder + for i, folder in enumerate(folders,1): + if i == 1: + parent = root + + else: + parent = tree[path] + path = '.'.join(folders[0:i]) + if path not in tree.keys(): + tree[path] = await parent.add_folder(idx, folder) + + for id, description_of_id, value in da.properties(node): + if id is ITEM_ACCESS_RIGHTS: + if value == 'Read': + value = ACCESS_READ + elif value == 'Write': + value = ACCESS_WRITE + elif value == 'Read/Write': + value = ACCESS_READ_WRITE + obj_in_node[id] = value + curr_value = read_value((obj_in_node[ITEM_VALUE],)) + if type(curr_value) != int: + curr_value = 0 + + opcua_node = await tree[path].add_variable(idx, file, ua.Variant(curr_value, ua.VariantType.UInt16)) + + if obj_in_node[ITEM_ACCESS_RIGHTS] in [ACCESS_READ]: + readable_variables[node] = opcua_node + # print(opcua_node) + if obj_in_node[ITEM_ACCESS_RIGHTS] in [ACCESS_WRITE, ACCESS_READ_WRITE]: + await opcua_node.set_writable() + writeable_variables[node] = opcua_node + # print(opcua_node) + + +async def main(): + + # We connect to the OPC-DA server (I assume there will only be one, else this will have to be changed) + # We can also check if there is a server and if there isn't one we'll run only an OPC UA server without conversion + da = OpenOPC.client() + OPCDA_SERVER_STRING = da.servers()[0] + da.connect(OPCDA_SERVER_STRING) #Connect to the first server in the array + print(OPCDA_SERVER_STRING) + + # Setup the server for UA + server = Server() + await server.init() + server.set_endpoint("opc.tcp://192.168.10.196:55341") + + server.set_server_name("SErverRER") + + idx = await server.register_namespace(UA_URI) + root = await server.nodes.objects.add_object(idx,OPCDA_SERVER_STRING) + + # We want to find the OPC-DA server nodes in aliases + nodes_list = da.list('*', recursive=True) #A list of dot-delimited strings + await sort_nodes_list(nodes_list, idx, root, da) + + + + async with server: #Starting the server + + handler = SubHandler(len(writeable_variables), OPCDA_SERVER_STRING) #Subscribing to datachanges coming from the UA clients + sub = await server.create_subscription(500, handler) + await sub.subscribe_data_change(writeable_variables.values()) + # In Robotstudio all variables are writeable, so the readable variables are empty + # This should be changed when tried in a real environment, so temporary for now + readable_vars = list(writeable_variables.keys()) #readable_variables + # print(readable_vars) + while True: + + for i in da.read(readable_vars): + print(i) + da_id = i[0] + var_handler = writeable_variables[da_id] # Due to change + await var_handler.set_value(read_value(i[1:])) + await asyncio.sleep(1) + + + + +if __name__ == "__main__": + sa.start_service_announcement(device_name="_adda-server.", iport=55341) + asyncio.run(main()) diff --git a/server/serverpi.py b/server/serverpi.py new file mode 100644 index 0000000..72a718e --- /dev/null +++ b/server/serverpi.py @@ -0,0 +1,169 @@ +#/usr/bin/env python3 + +# This file contains a set of classes and methods to handle +# running a OPC UA server on a raspberry pi. + + +import asyncio +import random +import time +from asyncua import ua, uamethod, Server, Client +import announce_service as sa + +SERVER_NAME = "OPC UA Server" +FLAT_NAME = SERVER_NAME.replace(" ", "") +SERVER_PORT = 4840 +SERVER_ENDPOINT = "opc.tcp://0.0.0.0:" +UA_NAMESPACE = "hvproj:ua:" +DISCOVERY_NAME="_"+FLAT_NAME[:10]+"." + +class SubHandler(): + """ + Subscription Handler. To receive events from server for a subscription + data_change and event methods are called directly from receiving thread. + Do not do expensive, slow or network operation there. Create another + thread if you need to do such a thing + """ + def __init__(self, variables, client, localserver): + self.vars = variables + self.cl = client + self.srv = localserver + + def datachange_notification(self, node, val, data): + n = self.srv.get_node(self.vars[str(node)]) + n.set_value(val) + print("Python: New data change event", n, val) + + def event_notification(self, event): + print("Python: New event", event) + +class ServerPI: + + def __init__(self, name=SERVER_NAME, port=SERVER_PORT): + self.server_name = name + self.flat_name = name.replace(" ", "") + self.server = None + self.server_port = port + self.ua_namespace = UA_NAMESPACE+self.flat_name + self.discovery_name = "_"+self.flat_name[:10]+"." + self.varfolder = None + self.idx = None + self.objects = None + self.clients = {} + self.localvars = {} + self.callback_func = None + self.callback_vars = None + + async def init_server(self): + print("Inside init_server") + sa.start_service_announcement(device_name=self.discovery_name, iport=self.server_port) + self.server = Server() + await self.server.init() + self.server.set_endpoint(SERVER_ENDPOINT+str(self.server_port)) + self.server.set_server_name(self.server_name) + #server.set_security_policy([ + # ua.SecurityPolicyType.NoSecurity, + # ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt, + # ua.SecurityPolicyType.Basic256Sha256_Sign]) + + self.idx = await self.server.register_namespace(self.ua_namespace) + self.objects = self.server.nodes.objects + self.varfolder = await self.objects.add_folder(self.idx, "Variables") + print(type(self.varfolder)) + await self.setup_sub_method() + + # This ua method is used to subscribe to a variable on + # another server. + # Inputs: + # endpoint(Server url as string): the path to server to subscribe from. + # qx(NodeId in string format): The variable to subscribe to + # ix(NodeId in string fromat): The variable to connect subscription to + # Returns void. + @uamethod + async def subscribe(self, parent, endpoint, qx, ix): + # The client tuple consists of a Client object, a SubHandler object, + # a Subscription, a list of subscribed variables, and a dictionary of + # subscribed and connected variables as strings. + server = str(endpoint) + if server not in self.clients.keys(): + try: + client = Client(server) + print("After Client(server)") + await client.connect() + print("After client.connect()") + await client.load_data_type_definitions() + print("After client.loaddatattype") + tmpvariables = {} + tmphandler = SubHandler(tmpvariables, client, self.server) + tmpsubscription = await client.create_subscription(50, tmphandler) + self.clients[server] = (client, tmphandler, tmpsubscription, [], tmpvariables) + except: + return "Could not reach the server specified." + else: + client = self.clients[server][0] + + qxvar = client.get_node(qx) + if qx not in self.clients[server][4].keys(): + self.clients[server][4][qx] = ix + subbedvar = await self.clients[server][2].subscribe_data_change(qxvar) + self.clients[server][3].append(subbedvar) + time.sleep(0.1) + return "Successfully subscribed to the specified variable!" + + # Helper method for setting up a UA Argument, in this case + # for use in the subscription method. + def method_var(self, name, description): + arg = ua.Argument() + arg.Name = name + arg.DataType = ua.NodeId(ua.ObjectIds.Int64) #NodeId, and not datatype of value. We use Int64 ID's. + arg.ValueRank = -1 + arg.ArrayDimensions = [] + arg.Description = ua.LocalizedText(description) + return arg + + # Helper method for setting up the subscription method. + async def setup_sub_method(self): + methods = await self.objects.add_object(self.idx, "Methods") + print(methods) + endp = self.method_var("Endpoint", "Address to tendpoint") + qxvar = self.method_var("qx", "Output variable to connect to server.") + ixvar = self.method_var("ix", "Input variable that is to be connected to.") + ret = self.method_var("ret", "Return message for information of what happend.") + await methods.add_method(self.idx, "subscribe", self.subscribe, [endp, qxvar, ixvar], [ret]) + + # Add a local variable to the UA Server. + async def add_variable(self, name, startvalue): + variable = await self.varfolder.add_variable(self.idx, name, startvalue) + variable.set_writable() + self.localvars[name] = [variable, startvalue] + + # Used to set a value of a variable, requires the + # name of the variable as a string and the corresponding + # python datatype value. + async def write_variable(self, name, value): + await self.localvars[name][0].write_value(value) + + # Returns the current value of the local + # server variable. + def get_variable_value(self, name): + return self.localvars[name][1] + + async def update_vars(self): + for n in self.localvars.keys(): + self.localvars[n][1] = await self.localvars[n][0].get_value() + + def add_callback(self, fnc, vars=None): + self.callback_func = fnc + self.callback_vars = vars + + async def go(self): + async with self.server: + while True: + await asyncio.sleep(0.1) + await self.update_vars() + if self.callback_func is not None: + await self.callback_func(self.callback_vars) + +if __name__ == "__main__": + sp = ServerPI() + asyncio.run(sp.go()) diff --git a/server/serverpi2.py b/server/serverpi2.py new file mode 100644 index 0000000..a9195ee --- /dev/null +++ b/server/serverpi2.py @@ -0,0 +1,145 @@ +#/usr/bin/env python3 + +# This file contains a set of classes and methods to handle +# running a OPC UA server on a raspberry pi. + + +import asyncio +import random +import time +from asyncua import ua, uamethod, Server, Client +import announce_service as sa + +SERVER_NAME = "2RaspPI OPC UA Server" +FLAT_NAME = SERVER_NAME.replace(" ", "") +SERVER_PORT = 4841 +SERVER_ENDPOINT = "opc.tcp://0.0.0.0:"+str(SERVER_PORT) +UA_NAMESPACE = "hvproj:ua:"+FLAT_NAME +DISCOVERY_NAME="_"+FLAT_NAME[:10]+"." +TEMP = 19 + +class SubHandler(): + """ + Subscription Handler. To receive events from server for a subscription + data_change and event methods are called directly from receiving thread. + Do not do expensive, slow or network operation there. Create another + thread if you need to do such a thing + """ + + def datachange_notification(self, node, val, data): + print("Python: New data change event", node, val) + + def event_notification(self, event): + print("Python: New event", event) + +client = None +handler = SubHandler() +sub = None +handle = None + + +class ServerPI: + + def __init__(self): + self.temp = TEMP + self.qxBall = True + self.qxBarrel = True + self.clients = {} + + # This ua method is used to subscribe to a variable on + # another server. + # Inputs: + # endpoint(Server url as string): the path to server to subscribe from. + # qx(NodeId in string format): The variable to subscribe to + # ix(NodeId in string fromat): The variable to connect subscription to + # Returns void. + @uamethod + async def subscribe(self, parent, endpoint, qx, ix): + server = str(endpoint) + if server not in self.clients: + try: + client = Client(server) + await client.connect() + await client.load_data_type_definitions() + self.clients[server] = (client,set()) + except: + return "Could not reach the server specified." + else: + client = self.clients[server][0] + + #root = client.get_root_node( + #uri = "http://examples.freeopcua.github.io" + #idx = client.get_namespace_index(uri) + + qxvar = client.get_node(qx) + if qx not in self.clients[server][1]: + self.clients[server][1].add(qx) + print(len(self.clients[server][1])) + sub = await client.create_subscription(500, handler) + handle = await sub.subscribe_data_change(qxvar) + time.sleep(0.1) + return "Successfully subscribed to the specified variable!" + + def method_var(self, name, description): + arg = ua.Argument() + arg.Name = name + arg.DataType = ua.NodeId(ua.ObjectIds.Int64) #NodeId, and not datatype of value. We use Int64 ID's. + arg.ValueRank = -1 + arg.ArrayDimensions = [] + arg.Description = ua.LocalizedText(description) + return arg + + async def go(self): + server = Server() + await server.init() + server.set_endpoint(SERVER_ENDPOINT) + server.set_server_name(SERVER_NAME) + #server.set_security_policy([ + # ua.SecurityPolicyType.NoSecurity, + # ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt, + # ua.SecurityPolicyType.Basic256Sha256_Sign]) + + idx = await server.register_namespace(UA_NAMESPACE) + + objects = server.nodes.objects + + #dev = await server.nodes.base_object_type.add_object_type(idx, FLAT_NAME) + + lFolder = await objects.add_folder(idx, "Sensors") + print("Sensors folder: ", lFolder) + zobj = await objects.add_object(idx, "Methods") + print("Methods object:", zobj) + + xvar = await lFolder.add_variable(idx, "qxBarrel", self.qxBarrel) + yvar = await lFolder.add_variable(idx, "qxBall", self.qxBall) + zvar = await lFolder.add_variable(idx, "qxTemperature", self.temp) + print("Temp var: ", zvar) + print("qxBall var:", yvar) + print("qxBarrel var:", xvar) + + endp = self.method_var("Endpoint", "Address to tendpoint") + qxvar = self.method_var("qx", "Output variable to connect to server.") + ixvar = self.method_var("ix", "Input variable that is to be connected to.") + ret = self.method_var("ret", "Return message for information of what happend.") + + await zobj.add_method(idx, "subscribe", self.subscribe, [endp, qxvar, ixvar], [ret]) + + async with server: + while True: + await asyncio.sleep(1.2) + await zvar.write_value(self.temp) + await yvar.write_value(self.qxBall) + await xvar.write_value(self.qxBarrel) + self.temp = 19 + random.random() + self.qxBall = not self.qxBall + self.qxBarrel = not self.qxBarrel + print(self.temp) + print(self.qxBall) + print(self.qxBarrel) + + +if __name__ == "__main__": + print(DISCOVERY_NAME) + sa.start_service_announcement(device_name=DISCOVERY_NAME, iport=SERVER_PORT) + sp = ServerPI() + asyncio.run(sp.go()) diff --git a/server/specialized_server.py b/server/specialized_server.py new file mode 100644 index 0000000..05c2cea --- /dev/null +++ b/server/specialized_server.py @@ -0,0 +1,33 @@ +import asyncio +import serverpi + +async def go(): + + #Variables to be sued + inBall = False + outBall = False + inBarrel = False + outBarrel = False + var_list = [inBall, outBall, inBarrel, outBarrel] + + sp = serverpi.ServerPI(name="Firsta test", port=4840) + await sp.init_server() + # Callback function to update variables outside of the OPCUA server. + # the vars is a list of variables that are available inside the OPCUA server. + async def cb_func(vars): + vars[1] = not vars[1] + vars[3] = not vars[3] + await sp.write_variable("qxBall", vars[1]) + await sp.write_variable("qxBarrel", vars[3]) + vars[2] = sp.get_variable_value("ixBarrel") + vars[0] = sp.get_variable_value("ixBall") + + await sp.add_variable("ixBall", inBall) + await sp.add_variable("ixBarrel", inBarrel) + await sp.add_variable("qxBall", outBall) + await sp.add_variable("qxBarrel", outBarrel) + sp.add_callback(cb_func, var_list) + await sp.go() + +if __name__ == "__main__": + asyncio.run(go()) diff --git a/server/specialized_server2.py b/server/specialized_server2.py new file mode 100644 index 0000000..9c2b6df --- /dev/null +++ b/server/specialized_server2.py @@ -0,0 +1,33 @@ +import asyncio +import serverpi + +async def go(): + + #Variables to be sued + inBall = False + outBall = False + inBarrel = False + outBarrel = False + var_list = [inBall, outBall, inBarrel, outBarrel] + + sp = serverpi.ServerPI(name="2Andra tvo", port=4841) + await sp.init_server() + # Callback function to update variables outside of the OPCUA server. + # the vars is a list of variables that are available inside the OPCUA server. + async def cb_func(vars): + vars[1] = not vars[1] + vars[3] = not vars[3] + await sp.write_variable("qxBall", vars[1]) + await sp.write_variable("qxBarrel", vars[3]) + vars[2] = sp.get_variable_value("ixBarrel") + vars[0] = sp.get_variable_value("ixBall") + + await sp.add_variable("ixBall", inBall) + await sp.add_variable("ixBarrel", inBarrel) + await sp.add_variable("qxBall", outBall) + await sp.add_variable("qxBarrel", outBarrel) + sp.add_callback(cb_func, var_list) + await sp.go() + +if __name__ == "__main__": + asyncio.run(go()) diff --git a/server/test_opcua_client.py b/server/test_opcua_client.py new file mode 100644 index 0000000..dc797a9 --- /dev/null +++ b/server/test_opcua_client.py @@ -0,0 +1,68 @@ + +import asyncio +import logging + +from asyncua import Client + +logging.basicConfig(level=logging.INFO) +_logger = logging.getLogger('asyncua') + + +class SubHandler(object): + """ + Subscription Handler. To receive events from server for a subscription + data_change and event methods are called directly from receiving thread. + Do not do expensive, slow or network operation there. Create another + thread if you need to do such a thing + """ + def datachange_notification(self, node, val, data): + print("New data change event", node, val) + + def event_notification(self, event): + print("New event", event) + + +async def main(): + url = "opc.tcp://localhost:4840/freeopcua/server/" + async with Client(url=url) as client: + _logger.info("Root node is: %r", client.nodes.root) + _logger.info("Objects node is: %r", client.nodes.objects) + + # Node objects have methods to read and write node attributes as well as browse or populate address space + _logger.info("Children of root are: %r", await client.nodes.root.get_children()) + + uri = "http://examples.freeopcua.github.io" + idx = await client.get_namespace_index(uri) + _logger.info("index of our namespace is %s", idx) + # get a specific node knowing its node id + #var = client.get_node(ua.NodeId(1002, 2)) + #var = client.get_node("ns=3;i=2002") + #print(var) + #await var.read_data_value() # get value of node as a DataValue object + #await var.read_value() # get value of node as a python builtin + #await var.write_value(ua.Variant([23], ua.VariantType.Int64)) #set node value using explicit data type + #await var.write_value(3.9) # set node value using implicit data type + + # Now getting a variable node using its browse path + myvar = await client.nodes.root.get_child(["0:Objects", "2:MyObject", "2:MyVariable"]) + obj = await client.nodes.root.get_child(["0:Objects", "2:MyObject"]) + _logger.info("myvar is: %r", myvar) + + # subscribing to a variable node + handler = SubHandler() + sub = await client.create_subscription(500, handler) + handle = await sub.subscribe_data_change(myvar) + await asyncio.sleep(0.1) + + # we can also subscribe to events from server + await sub.subscribe_events() + # await sub.unsubscribe(handle) + # await sub.delete() + + # calling a method on server + res = await obj.call_method("2:multiply", 3, "klk", 4) + _logger.info("method result is: %r", res) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/server/test_serverpi.py b/server/test_serverpi.py new file mode 100644 index 0000000..020aea5 --- /dev/null +++ b/server/test_serverpi.py @@ -0,0 +1,116 @@ +import asyncio + +from asyncua import ua, uamethod, Server + + +# method to be exposed through server +def func(parent, variant): + print("func method call with parameters: ", variant.Value) + ret = False + if variant.Value % 2 == 0: + ret = True + return [ua.Variant(ret, ua.VariantType.Boolean)] + + +# method to be exposed through server +async def func_async(parent, variant): + if variant.Value % 2 == 0: + print("Sleeping asynchronously for 1 second") + await asyncio.sleep(1) + else: + print("Not sleeping!") + + +# method to be exposed through server +# uses a decorator to automatically convert to and from variants + + +@uamethod +def multiply(parent, x, y, z): + print("multiply method call with parameters: ", x, y) + return x * y * z + + +@uamethod +async def multiply_async(parent, x, y): + sleep_time = x * y + print(f"Sleeping asynchronously for {x * y} seconds") + await asyncio.sleep(sleep_time) + + +async def main(): + # optional: setup logging + #logging.basicConfig(level=logging.WARN) + # logger = logging.getLogger("asyncua.address_space") + # logger.setLevel(logging.DEBUG) + # logger = logging.getLogger("asyncua.internal_server") + # logger.setLevel(logging.DEBUG) + # logger = logging.getLogger("asyncua.binary_server_asyncio") + # logger.setLevel(logging.DEBUG) + # logger = logging.getLogger("asyncua.uaprocessor") + # logger.setLevel(logging.DEBUG) + # logger = logging.getLogger("asyncua.subscription_service") + # logger.setLevel(logging.DEBUG) + + # now setup our server + server = Server() + await server.init() + # server.set_endpoint("opc.tcp://localhost:4840/freeopcua/server/") + server.set_endpoint("opc.tcp://0.0.0.0:4840") + server.set_server_name("FreeOpcUa Example Server") + + # setup our own namespace + uri = "http://examples.freeopcua.github.io" + idx = await server.register_namespace(uri) + + # get Objects node, this is where we should put our custom stuff + objects = server.nodes.objects + + # populating our address space + await objects.add_folder(idx, "myEmptyFolder") + myobj = await objects.add_object(idx, "MyObject") + myvar = await myobj.add_variable(idx, "MyVariable", 6.7) + await myvar.set_writable() # Set MyVariable to be writable by clients + myarrayvar = await myobj.add_variable(idx, "myarrayvar", [6.7, 7.9]) + await myobj.add_variable( + idx, "myStronglytTypedVariable", ua.Variant([], ua.VariantType.UInt32) + ) + await myobj.add_property(idx, "myproperty", "I am a property") + await myobj.add_method(idx, "mymethod", func, [ua.VariantType.Int64], [ua.VariantType.Boolean]) + + inargx = ua.Argument() + inargx.Name = "x" + inargx.DataType = ua.NodeId(ua.ObjectIds.Int64) + inargx.ValueRank = -1 + inargx.ArrayDimensions = [] + inargx.Description = ua.LocalizedText("First number x") + inargy = ua.Argument() + inargy.Name = "y" + inargy.DataType = ua.NodeId(ua.ObjectIds.Int64) + inargy.ValueRank = -1 + inargy.ArrayDimensions = [] + inargy.Description = ua.LocalizedText("Second number y") + inargz = ua.Argument() + inargz.Name = "z" + inargz.DataType = ua.NodeId(ua.ObjectIds.Int64) + inargz.ValueRank = -1 + inargz.ArrayDimensions = [] + inargz.Description = ua.LocalizedText("Third number z") + outarg = ua.Argument() + outarg.Name = "Result" + outarg.DataType = ua.NodeId(ua.ObjectIds.Int64) + outarg.ValueRank = -1 + outarg.ArrayDimensions = [] + outarg.Description = ua.LocalizedText("Multiplication result") + + await myobj.add_method(idx, "multiply", multiply, [inargx, inargy, inargz], [outarg]) + await myobj.add_method(idx, "multiply_async", multiply_async, [inargx, inargy], []) + await myobj.add_method(idx, "func_async", func_async, [ua.VariantType.Int64], []) + + async with server: + while True: + await asyncio.sleep(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/server/test_serverpi_client.py b/server/test_serverpi_client.py new file mode 100644 index 0000000..3807329 --- /dev/null +++ b/server/test_serverpi_client.py @@ -0,0 +1,43 @@ +import asyncio + +from asyncua import Client + +SERVER_NAME = "RaspPI OPC UA Server" +FLAT_NAME = SERVER_NAME.replace(" ", "") +SERVER_ENDPOINT = "opc.tcp://0.0.0.0:4840" +UA_NAMESPACE = "hvproj:ua:"+FLAT_NAME +TEMP = 19 + +class SubHandler(object): + """ + Subscription Handler. To receive events from server for a subscription + data_change and event methods are called directly from receiving thread. + Do not do expensive, slow or network operation there. Create another + thread if you need to do such a thing + """ + def datachange_notification(self, node, val, data): + print("New data change event", node, val) + + def event_notification(self, event): + print("New event", event) + + +async def main(): + url = "opc.tcp://0.0.0.0:4840" + async with Client(url=url) as client: + #obj = await client.nodes.root.get_child(["0:Objects", "2:Methods"]) + obj = await client.nodes.root.get_child("ns=2; i=2") + await asyncio.sleep(0.1) + + print("Here be calling da meffod!") + # calling a method on server + + ret = await obj.call_method("2:subscribe", "opc.tcp://0.0.0.0:4841", "ns=2;i=4", "ns=2;i=2") + ret2 = await obj.call_method("2:subscribe", "opc.tcp://0.0.0.0:4841", "ns=2;i=5", "ns=2;i=3") + + print(ret) + print(ret2) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/uaxplorer/__pycache__/uaxplorer.cpython-39.pyc b/uaxplorer/__pycache__/uaxplorer.cpython-39.pyc new file mode 100644 index 0000000..af35695 Binary files /dev/null and b/uaxplorer/__pycache__/uaxplorer.cpython-39.pyc differ diff --git a/uaxplorer/ui_methods/MainUI.py b/uaxplorer/ui_methods/MainUI.py new file mode 100644 index 0000000..5fb1894 --- /dev/null +++ b/uaxplorer/ui_methods/MainUI.py @@ -0,0 +1,530 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'zuo.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets, Qt +from PyQt5.QtWidgets import QMenu +from zeroconf import ServiceBrowser, Zeroconf +import server_discovery as dsc +import client_nodes as cl_node +from opcua import client, Client +from client_nodes import StandardItem as StItem +import navigating_nodes as nav +import sys + + +class Node_storage: # Necessary for keeping track of every standarditem and where it belongs too and information that is good for references. + def __init__(self, server_name, node_name, node_id, standarditem): + self.server_name = server_name + self.node_name = node_name + self.node_id = node_id + self.standarditem = standarditem + + +# Used for when an user subscribes to a node and wants information from it (Middle widget) +class Subscription_storage: + def __init__(self, server_name, node_name, node_id, server_address, row, tablewidget): + self.server_name = server_name + self.node_name = node_name + self.node_id = node_id + # Keep track of what row to append the information into. + self.row = row + self.server_address = server_address + self.tableWidget = tablewidget + + self.tableWidget.setItem(self.row, 0, QtWidgets.QTableWidgetItem( + self.server_name)) # Set the server name and append into the widget + self.tableWidget.setItem(self.row, 1, QtWidgets.QTableWidgetItem( + self.node_name)) # Set node name into the widget + + # Node_id converted to string and appended. + self.tableWidget.setItem( + self.row, 2, QtWidgets.QTableWidgetItem(str(self.node_id))) + + self.client = Client("opc.tcp://" + self.server_address) + + self.client.connect() + # Get the data values from the node we are subscribing to + var = self.client.get_node(node_id) + # DataValue(Value:Variant(val:19,type:VariantType.Int64), StatusCode:StatusCode(Good), SourceTimestamp:2021-03-13 12:36:58.621994) + # Example of outprint from var + + # Create a subscription handelr to update the variables value outprint. Also send in the widget and row + handler = SubHandler(self.row, self.tableWidget) + sub = self.client.create_subscription( + 500, handler) # Refresh every 500 ms + handle = sub.subscribe_data_change(var) # Send in the variable object. + + +class SubHandler(object): + def __init__(self, row, tablewidget): + self.row = row # Keep track of the row to know where to append + # Get the tablewidget as this is a class it requires it. + self.tableWidget = tablewidget + + """ + Subscription Handler. To receive events from server for a subscription + data_change and event methods are called directly from receiving thread. + Do not do expensive, slow or network operation there. Create another + thread if you need to do such a thing + """ + + def datachange_notification(self, node, val, data): + # print("Python: New data change event", node, val) + # When a data change happens we append the value to the widget. + self.tableWidget.setItem( + self.row, 3, QtWidgets.QTableWidgetItem(str(val))) + self.tableWidget.setItem(self.row, 4, QtWidgets.QTableWidgetItem( + str(data.monitored_item.Value.Value.VariantType))) # Get the variant type of the node + self.tableWidget.setItem(self.row, 5, QtWidgets.QTableWidgetItem( + str(data.monitored_item.Value.SourceTimestamp))) # Get the time stamp and display + self.tableWidget.setItem(self.row, 6, QtWidgets.QTableWidgetItem( + str(data.monitored_item.Value.StatusCode))) # Get the statuscode Good, Bad + # We update the tableWidget, so that the update becomes present in the user client. + self.tableWidget.viewport().update() + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + self.clients = list() + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1014, 513) + MainWindow.setStyleSheet("background-color: rgb(200, 200, 200;") + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + + self.Connect = QtWidgets.QPushButton(self.centralwidget) + self.Connect.setGeometry(QtCore.QRect(780, 0, 81, 21)) + self.Connect.setStyleSheet("color: rgb(0, 0, 0);") + self.Connect.clicked.connect(self.manual_connection) + self.Connect.setObjectName("Connect") + + self.Discover = QtWidgets.QPushButton(self.centralwidget) + self.Discover.setGeometry(QtCore.QRect(690, 0, 81, 21)) + self.Discover.setStyleSheet("color: rgb(0, 0, 0);") + self.Discover.clicked.connect(self.discover_servers) + self.Discover.setObjectName("Discover") + + self.lineEdit = QtWidgets.QLineEdit(self.centralwidget) + self.lineEdit.setGeometry(QtCore.QRect(10, 0, 681, 21)) + self.lineEdit.setStyleSheet("color: rgb(0, 0, 0);") + self.lineEdit.setObjectName("lineEdit") + + self.tableWidget = QtWidgets.QTableWidget(self.centralwidget) + self.tableWidget.setGeometry(QtCore.QRect(340, 26, 612, 325)) + self.tableWidget.setStyleSheet("color: rgb(0, 0, 0);") + self.tableWidget.setObjectName("tableWidget") + self.tableWidget.setColumnCount(7) + self.tableWidget.setRowCount(0) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(2, item) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(3, item) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(4, item) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(5, item) + item = QtWidgets.QTableWidgetItem() + self.tableWidget.setHorizontalHeaderItem(6, item) + + self.textBrowser = QtWidgets.QTextBrowser(self.centralwidget) + self.textBrowser.setGeometry(QtCore.QRect(10, 360, 942, 111)) + self.textBrowser.setObjectName("textBrowser") + + self.pushButton = QtWidgets.QPushButton(self.centralwidget) + self.pushButton.setGeometry(QtCore.QRect(870, 0, 81, 21)) + self.pushButton.setStyleSheet("color: rgb(0, 0, 0);") + self.pushButton.setObjectName("pushButton") + + self.pairingbutton = QtWidgets.QPushButton(self.centralwidget) + self.pairingbutton.setGeometry(QtCore.QRect(960, 0, 81, 21)) + self.pairingbutton.setStyleSheet("color: rgb(0, 0, 0);") + self.pairingbutton.setObjectName("pairButton") + self.pairingbutton.hide() + self.pairingbutton.clicked.connect(self.linking_servers) + + self.treeView = QtWidgets.QTreeView(self.centralwidget) + self.treeView.setGeometry(QtCore.QRect(10, 26, 331, 325)) + self.treeView.setObjectName("treeView") + self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.treeView.customContextMenuRequested.connect(self.contextMenuEvent) + + self.right_treeView = QtWidgets.QTreeView(self.centralwidget) + self.right_treeView.setGeometry(QtCore.QRect(951, 26, 331, 325)) + self.right_treeView.setObjectName("Right_treeview") + self.right_treeView.hide() + + self.Connect.raise_() + self.Discover.raise_() + self.lineEdit.raise_() + self.textBrowser.raise_() + self.pushButton.raise_() + self.treeView.raise_() + self.tableWidget.raise_() + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 912, 21)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 914, 21)) + self.menubar.setObjectName("menubar") + self.menuFile = QtWidgets.QMenu(self.menubar) + self.menuFile.setObjectName("menuFile") + self.menuView = QtWidgets.QMenu(self.menubar) + self.menuView.setObjectName("menuView") + MainWindow.setMenuBar(self.menubar) + self.closing_app = QtWidgets.QAction(MainWindow) + self.closing_app.setObjectName("Closing Application") + self.closing_app.setShortcut("CTRL+Q") + self.closing_app.triggered.connect(self.closing_application) + self.actionRight_Hand_tree = QtWidgets.QAction(MainWindow) + self.actionRight_Hand_tree.setObjectName("actionRight_Hand_tree") + self.actionRight_Hand_tree.setShortcut("CTRL+N") + self.actionRight_Hand_tree.triggered.connect( + self.creating_right_window) + self.hide_Right_Hand_tree = QtWidgets.QAction(MainWindow) + self.hide_Right_Hand_tree.setObjectName("Hiding tree") + self.hide_Right_Hand_tree.setShortcut("CTRL+M") + self.hide_Right_Hand_tree.triggered.connect(self.closing_right_window) + + self.menuFile.addAction(self.closing_app) + self.menuView.addAction(self.actionRight_Hand_tree) + self.menuView.addAction(self.hide_Right_Hand_tree) + + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuView.menuAction()) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + ############################### Discovery ################################ + + self.treeModel = Qt.QStandardItemModel() + self.treeView.setHeaderHidden(True) + self.rootNode = self.treeModel.invisibleRootItem() + self.treeView.setModel(self.treeModel) + self.treeView.doubleClicked.connect(self.getValueLeft) + ###### Right hand tree ####### + self.right_treeView.setModel(self.treeModel) + self.right_treeView.doubleClicked.connect(self.getValueLeft) + + ## VARIABLES ##### + self.ROOT_CHILDREN_NODES = [] + + ## TABLE WIDGET - MIDDLE PART - ##### + self.row = 0 + self.subscriptions_array = list() + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "CustomOPC")) + self.Connect.setText(_translate("MainWindow", "Connect")) + self.Discover.setText(_translate("MainWindow", "Discover")) + item = self.tableWidget.horizontalHeaderItem(0) + item.setText(_translate("MainWindow", "Server name")) + item = self.tableWidget.horizontalHeaderItem(1) + item.setText(_translate("MainWindow", "Node Name")) + item = self.tableWidget.horizontalHeaderItem(2) + item.setText(_translate("MainWindow", "Node Id")) + item = self.tableWidget.horizontalHeaderItem(3) + item.setText(_translate("MainWindow", "Value")) + item = self.tableWidget.horizontalHeaderItem(4) + item.setText(_translate("MainWindow", "DataType")) + item = self.tableWidget.horizontalHeaderItem(5) + item.setText(_translate("MainWindow", "TimeStamp")) + item = self.tableWidget.horizontalHeaderItem(6) + item.setText(_translate("MainWindow", "Quality")) + + self.textBrowser.setHtml(_translate("MainWindow", "\n" + "\n" + "


")) + self.pushButton.setText(_translate("MainWindow", "Disconnect")) + self.pairingbutton.setText(_translate("MainWindow", "Pair")) + + self.menuFile.setTitle(_translate("MainWindow", "File")) + self.menuView.setTitle(_translate("MainWindow", "View")) + self.actionRight_Hand_tree.setText(_translate( + "MainWindow", "Expand a right hand tree")) + self.hide_Right_Hand_tree.setText(_translate( + "MainWindow", "Hide the right hand tree")) + self.closing_app.setText(_translate("MainWindow", "Quit")) + + def onitemclicked(self): + item = self.treeView.currentIndex() + print(item.text(0)) + + def closing_application(self): + QtWidgets.qApp.quit() + + # Linking the servers and navigating all the nodes and finding the correct qx ix subscription. + def linking_servers(self): + server1 = None + server2 = None + nav1 = None + nav2 = None + for i in self.clients: + if(i.ROOT_NODE.checkState() == 2 and server1 == None): + server1 = i + i.client.connect() + n = nav.Navigating_nodes(i.client) + nav1 = n.get_children_nodes_name(n.get_root_nodes()) + i.client.disconnect() + self.textBrowser.append("Linked server 1: " + i.server_name) + if(i.ROOT_NODE.checkState() == 2 and i.server_name != server1.server_name): + server2 = i + i.client.connect() + n = nav.Navigating_nodes(i.client) + nav2 = n.get_children_nodes_name(n.get_root_nodes()) + i.client.disconnect() + self.textBrowser.append("Linked server 2: " + i.server_name) + + if(server2 != None and server1 != None): + for dict1_key, dict1_values in nav1.items(): + for value in dict1_values: + for key2, values2 in nav2.items(): + for value2 in values2: + if value[2:] == value2[2:]: + + if 'ix' in value and 'qx' in value2: + server1.client.connect() + get_obj = server1.client.get_objects_node().get_children() + for i in get_obj: + function_name = server1.client.get_node( + i).get_display_name()._text + print(function_name) + if('Methods' == function_name): + bl = server1.client.get_node(i).call_method( + "2:subscribe", "opc.tcp://" + server2.Server, str(dict1_key), str(key2)) + self.textBrowser.append(bl) + server1.client.disconnect() + if 'qx' in value and 'ix' in value2: + server2.client.connect() + get_obj = server2.client.get_objects_node().get_children() + for i in get_obj: + function_name = server2.client.get_node( + i).get_display_name()._text + + if('Methods' == function_name): + bl = server2.client.get_node(i).call_method( + "2:subscribe", "opc.tcp://" + server1.Server, str(key2), str(dict1_key)) + self.textBrowser.append(bl) + server2.client.disconnect() + + # server1.client.connect() + # f = server1.client.get_objects_node().get_children() + # for i in f: + # kkk = server1.client.get_node(i).get_display_name().__dict__['_text'] + # print(server1.client.get_node(i).get_display_name()._text) + + def getValueLeft(self, val): + node_name = val.data() + bool_continue = True + root = val.parent() # The root node of the node index that the user double clicks + + # We loop till we get None as data (We're at the end of the hierarchy) + while(root.parent().data() != None): + root = root.parent() + + for i in self.clients: # Loop through our servers and make sure it already isn't in the treeview + if(i.server_name == node_name): + for k in i.NODE_ID: + i.client.connect() + for j in self.ROOT_CHILDREN_NODES: + if(i.server_name == j.server_name): + children_name = i.client.get_node( + k).get_browse_name().__dict__['Name'] + if(children_name == j.node_name): + bool_continue = False + break + i.client.disconnect() + + for i in self.clients: # Loop through our servers and make sure it already isn't in the treeview + if(root.data() == i.server_name): + for j in self.ROOT_CHILDREN_NODES: + if(node_name == j.node_name and root.data() == j.server_name): + i.client.connect() + for d in i.client.get_node(j.node_id).get_children(): + children_name = i.client.get_node( + d).get_browse_name().__dict__['Name'] + for k in self.ROOT_CHILDREN_NODES: + if(children_name == k.node_name and root.data() == k.server_name): + self.textBrowser.append( + "Children_name already exists! " + children_name + " " + k.node_name) + bool_continue = False + break + i.client.disconnect() + + for i in self.clients: # We add the first children to the root node + if(i.server_name == node_name and bool_continue == True): + + for j in i.NODE_ID: + + i.client.connect() + children_name = i.client.get_node( + j).get_browse_name().__dict__['Name'] + qtitem = StItem(children_name, 8, + color=QtGui.QColor(180, 180, 180)) + i.ROOT_NODE.appendRow(qtitem) + server_name = node_name + self.ROOT_CHILDREN_NODES.append(Node_storage( + server_name, children_name, j, qtitem)) + i.client.disconnect() + + # We add the first children to the first children of the root node (and etc) + for i in self.clients: + if(root.data() == i.server_name): + + for j in self.ROOT_CHILDREN_NODES: + if(node_name == j.node_name and root.data() == j.server_name and bool_continue == True): + + i.client.connect() + for d in i.client.get_node(j.node_id).get_children(): + children_name = i.client.get_node( + d).get_browse_name().__dict__['Name'] + qtitem = StItem(children_name, 8, + color=QtGui.QColor(180, 180, 180)) + j.standarditem.appendRow(qtitem) + server_name = root.data() + self.ROOT_CHILDREN_NODES.append(Node_storage( + server_name, children_name, d, qtitem)) + i.client.disconnect() + + def creating_right_window(self): + # resizing the window to be able to fit the new treeview + MainWindow.resize(1300, 513) + # making the textbox bigger to be able to display more information + self.textBrowser.resize(1272, 111) + self.right_treeView.show() + self.pairingbutton.show() + + def closing_right_window(self): + self.right_treeView.hide() + self.textBrowser.resize(942, 111) + self.pairingbutton.hide() + + # Manually create a connection to a server and append it to the arrays for navigation. + def manual_connection(self): + adress = self.lineEdit.text() + self.clients.append(cl_node.Client_nodes(adress, adress)) + self.rootNode.appendRow(self.clients[-1].ROOT_NODE) + self.textBrowser.append("Manually added service: " + + adress + " !Note: name will be set to the address:port!") + + def discover_servers(self): # For discovering and displaying the OPC UA servers in the UA interface. + if len(self.clients) > 0: # We check if clients have been added and remove them if so to not create duplicate clients. + self.clients.clear() + self.treeModel.removeRows(0, self.treeModel.rowCount()) + + # We also remove the children nodes. + if len(self.ROOT_CHILDREN_NODES) > 0: + self.ROOT_CHILDREN_NODES.clear() + + # We create an instance of Server_discovery. + url=dsc.Server_Discovery() + # We call the get servers function to get the servers. + url.get_servers() + # We get the server names (in a list) from the servers and store it. + self.SERVER_ARR=url.get_all(0) + # We get a list of the addresses and port ex : [192.168.1.10:3249, 192.168.1.5:3249] + servers=url.get_all_as_address() + print(servers) + + j=0 + for i in servers: # We iterate through the server list + # We append a Client_nodes instance into an array for further reference. + self.clients.append(cl_node.Client_nodes(i, self.SERVER_ARR[j])) + # Client_nodes arguments require the IP:PORT and server name which it stores. + self.textBrowser.append( + "Service added: " + self.SERVER_ARR[j] + "- At address: " + i) # Logger for the User interface. + + # We iterate through all servers but we also need to add the correct server_name which is also an array. Best to just use a counter here. + j += 1 + + for i in self.clients: # We add the server name to the treeview. + self.rootNode.appendRow(i.ROOT_NODE) + #print() + + def contextMenuEvent(self, pos): + indexes = self.treeView.indexAt(pos) + print(indexes.data()) + + root = indexes.parent() # The root node of the node index that the user double clicks + + # We loop till we get None as data (We're at the beginning of the hierarchy) + while(root.parent().data() != None): + root = root.parent() + + menu = QMenu() + # Note that it creates a button to any widget. Fail-safes have not been added. + subscribe = menu.addAction("Subscribe to variable") + + # Display the action at correct position. + action = menu.exec_(self.treeView.viewport().mapToGlobal(pos)) + + # We add another count to the tablewidget. + self.tableWidget.setRowCount(self.row + 1) + + server_address = None + nodes_id_and_name = None + node_id = None + + # If someone presses the button then we want to iterate and find that variable and display it. + if(action == subscribe): + + # Find the right client and if the server_name is equals to the first treeview node (The root node) then continue + for i in self.clients: + if(i.server_name == root.data()): + # Add the server to our local variable. + server_address = i.Server + # Connect to the UA client for the UA server. + i.client.connect() + n = nav.Navigating_nodes(i.client) + nodes_id_and_name = n.get_children_nodes_name( + n.get_root_nodes()) # Lets recursively get all the nodes and its name : id + i.client.disconnect() # Disconnect from the UA client after we're done + break + + # Let us find the node id for the variable name that we're trying to subscribe to + for dict1_key, dict1_values in nodes_id_and_name.items(): + for value in dict1_values: + # indexes.data tells us what variable we are trying to subscribe to. + if(value == indexes.data()): + # Lets set the local node id variable to the actual variables node id. + node_id = dict1_key + # dict1_key is the node id and dict1_values is the string name of the node. + + self.subscriptions_array.append(Subscription_storage( + root.data(), indexes.data(), node_id, server_address, self.row, self.tableWidget)) # We also store all the information (For future reference) also to create our subscription handler. + # As we have subscribed to a variable, next subscription needs to be on the next row. + self.row += 1 + self.textBrowser.append("Created a subscription to: " + str(indexes.data())) + + + + +# Set up the User Interface window. +if __name__ == "__main__": + + app=QtWidgets.QApplication(sys.argv) + MainWindow=QtWidgets.QMainWindow() + ui=Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + + sys.exit(app.exec_()) diff --git a/uaxplorer/ui_methods/__pycache__/Ui_client.cpython-38.pyc b/uaxplorer/ui_methods/__pycache__/Ui_client.cpython-38.pyc new file mode 100644 index 0000000..5364739 Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/Ui_client.cpython-38.pyc differ diff --git a/uaxplorer/ui_methods/__pycache__/client_nodes.cpython-38.pyc b/uaxplorer/ui_methods/__pycache__/client_nodes.cpython-38.pyc new file mode 100644 index 0000000..150598c Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/client_nodes.cpython-38.pyc differ diff --git a/uaxplorer/ui_methods/__pycache__/discovery.cpython-38.pyc b/uaxplorer/ui_methods/__pycache__/discovery.cpython-38.pyc new file mode 100644 index 0000000..9ceef99 Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/discovery.cpython-38.pyc differ diff --git a/uaxplorer/ui_methods/__pycache__/discovery.cpython-39.pyc b/uaxplorer/ui_methods/__pycache__/discovery.cpython-39.pyc new file mode 100644 index 0000000..4d33f17 Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/discovery.cpython-39.pyc differ diff --git a/uaxplorer/ui_methods/__pycache__/navigating_nodes.cpython-38.pyc b/uaxplorer/ui_methods/__pycache__/navigating_nodes.cpython-38.pyc new file mode 100644 index 0000000..675714d Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/navigating_nodes.cpython-38.pyc differ diff --git a/uaxplorer/ui_methods/__pycache__/server_discovery.cpython-38.pyc b/uaxplorer/ui_methods/__pycache__/server_discovery.cpython-38.pyc new file mode 100644 index 0000000..c481688 Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/server_discovery.cpython-38.pyc differ diff --git a/uaxplorer/ui_methods/__pycache__/server_discovery.cpython-39.pyc b/uaxplorer/ui_methods/__pycache__/server_discovery.cpython-39.pyc new file mode 100644 index 0000000..98b1aa0 Binary files /dev/null and b/uaxplorer/ui_methods/__pycache__/server_discovery.cpython-39.pyc differ diff --git a/uaxplorer/ui_methods/client_nodes.py b/uaxplorer/ui_methods/client_nodes.py new file mode 100644 index 0000000..df2125b --- /dev/null +++ b/uaxplorer/ui_methods/client_nodes.py @@ -0,0 +1,48 @@ +from PyQt5 import QtCore, QtGui, QtWidgets, Qt +from opcua import Client, ua +from navigating_nodes import Navigating_nodes as navNodes + +class StandardItem(Qt.QStandardItem): + def __init__(self, txt='', font_size=10, check_box=False, set_bold=False, color=QtGui.QColor(0, 0, 0)): + super().__init__() + self.setEditable(False) + self.setForeground(color) + self.setText(txt) + + fnt = QtGui.QFont('Open Sans', font_size) + fnt.setBold(set_bold) + self.setFont(fnt) + self.setCheckable(check_box) + + + + +class Client_nodes: + def __init__(self, server, server_name): + self.server_name = server_name + self.Server = server + self.client = Client("opc.tcp://" + server) + + self.ROOT_NODE = StandardItem(self.server_name, 10, True, True) + + self.MAP_VALUE_NODES = {} + self.FOLDER_NODE = [] + self.NODE_MAP = {} + self.NODE_ID = None + nav_nodes = navNodes(self.client) + try: + self.client.connect() + + self.NODE_ID = nav_nodes.get_root_nodes() + self.NODE_MAP.update(nav_nodes.get_name_from_nodes(nav_nodes.get_children_nodes(nav_nodes.get_root_nodes()))) + finally: + self.client.disconnect() + + for key, values in self.NODE_MAP.items(): + for value in values: + if key not in self.MAP_VALUE_NODES: + self.MAP_VALUE_NODES[key] = list() + self.MAP_VALUE_NODES[key].append(StandardItem(value, 8, color=QtGui.QColor(0, 0, 0))) + + for key in self.MAP_VALUE_NODES: + self.FOLDER_NODE.append(StandardItem(key, 9, color=QtGui.QColor(200, 200, 200))) diff --git a/uaxplorer/ui_methods/discovery.py b/uaxplorer/ui_methods/discovery.py new file mode 100644 index 0000000..f30ecc7 --- /dev/null +++ b/uaxplorer/ui_methods/discovery.py @@ -0,0 +1,86 @@ +from zeroconf import ServiceBrowser, Zeroconf + + +# This is the Discovery implementation. +# +# Usage: Create an instance of the Discovery class. +# As input it takes a list of strings of the services +# it should search for. E.g : ["_opcua-tcp._tcp.local."] +# To get a list of all found servers, call +# the get_services() method and it will return a list of +# tuples of the following format: ('name', 'ip address', 'port') + + +INAME = 0 +IADDR = 1 +IPORT = 2 +DADDR = 0 +DPORT = 1 + +def get_info(zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + if info is not None: + name = info.get_name() + addresses = info.parsed_addresses() + port = str(info.port) + return (name, addresses, port) + return None + +class DiscoveryListener: + def __init__(self, servicedict): + self.servicedict = servicedict + + def remove_service(self, zeroconf, type, name): + info = get_info(zeroconf, type, name) + if info: + print("Service %s removed" % (name,)) + del self.servicedict[info[INAME]] + + def add_service(self, zeroconf, type, name): + info = get_info(zeroconf, type, name) + if info: + print("Service %s added, addresses: %s" % (info[INAME], info[IADDR])) + self.servicedict[info[INAME]] = (info[IADDR], info[IPORT]) + + def update_service(self, zeroconf, type, name): + info = get_info(zeroconf, type, name) + if info: + print("Service %s updated, addresses: %s" % (info[INAME], info[IADDR])) + self.servicedict[info[INAME]] = (info[IADDR], info[IPORT]) + +""" Vi ska kolla efter dessa tjänster: + opc.tcp" = "_opcua-tcp._tcp" + "opc.https" eller "https" = "_opcua-https._tcp" + "opc.wss" = "_opcua-wss._tcp" + enligt https://github.com/OPCFoundation/UA-LDS/blob/master/zeroconf.c + rad 222-238. """ + +class Discovery: + def __init__(self, types): + self.sdict = {} + self.zeroconf = Zeroconf() + self.types = types + self.listener = DiscoveryListener(self.sdict) + browser = ServiceBrowser(self.zeroconf, self.types, self.listener) + + def get_services(self): + l = [] + if not self.sdict: + return l + for k in list(self.sdict.keys()): + name = k + port = self.sdict[k][DPORT] + for addr in self.sdict[k][DADDR]: + l.append((name, addr, port)) + return l + + + +if __name__ == "__main__": + Test_types = ["_opcua-tcp._tcp.local.", + "_opcua-https._tcp.local.", + "_opcua-wss._tcp.local."] + d = Discovery(Test_types) + while True: + print(d.get_services()) + input() diff --git a/uaxplorer/ui_methods/navigating_nodes.py b/uaxplorer/ui_methods/navigating_nodes.py new file mode 100644 index 0000000..03ff9ba --- /dev/null +++ b/uaxplorer/ui_methods/navigating_nodes.py @@ -0,0 +1,108 @@ +#Class for navigating the nodes + +from opcua import Client, ua + +#How to use the class in your own folder +#This is the import -> from ui_methods.navigating_nodes import Navigating_nodes as navNodes + +#Create an instance with the client -> navigating = navNodes(client) +#Run one of the functions with your instance -> navigating.get_root_nodes() + +#You could also run all of them at the same time like so: +#print(navigating.get_name_from_nodes(navigating.get_children_nodes(navigating.get_root_nodes()))) +#To get the names, but best is to save every node (Cleaner code) + +class Navigating_nodes: + + def __init__(self, client): + self.client = client + self.all_nodes = list() + + def get_root_nodes(self): + TEMP_ARRAY = [] + + for i in self.client.get_objects_node().get_children()[1:]: # Skipping first element as it is unnecessary, we grab all objects in a server + TEMP_ARRAY.append(i) + + + #self.client.get_objects_node().get_children( + return TEMP_ARRAY + + def get_children_nodes(self, object_array): #Gets the children nodes from the root node and adds them into a dictonary + children_dict = {} + + for i in object_array: + for j in self.client.get_node(i).get_children(): + if i not in children_dict: + children_dict[i] = list() + children_dict[i].append(j) + + return children_dict + + def get_children_nodes_name(self, object_array): #We get children name from the ROOT nodes and then run it recursively if necessary. + nodes_name = {} + temp_dict = {} + for i in object_array: #Check in the root dictionary. + if i not in nodes_name: #We append if the root children nodes isnt in our dictionary + nodes_name[i] = list() #Add the node id as key + nodes_name[i].append(self.client.get_node(i).get_browse_name().__dict__['Name']) #Add the variable name as value + for j in self.client.get_node(i).get_children(): #We check the children in root node + if j not in nodes_name: + nodes_name[j] = list() + nodes_name[j].append(self.client.get_node(j).get_browse_name().__dict__['Name']) + if len(self.client.get_node(j).get_children()) > 0: #If the children in the root node has children of its own, then we need to run it recursively + if j not in temp_dict: + temp_dict[j] = list() + temp_dict[j].append(self.client.get_node(j).get_children()) + #print(nodes_name) + if bool(temp_dict): #If we found children inside of children, we will run through it recursively till there is no more to check. + nodes_name = self.run_recursively(temp_dict, nodes_name) + + return nodes_name + + + def run_recursively(self, map_dict, nodes_name): + nodes_name = nodes_name + new_dict2 = {} + for key, values in map_dict.items(): #Loop through the dictionary and append it to nodes_name and if there is more children append it + # to our new recursive dict + for value in values: + for value1 in value: + if value1 not in nodes_name: + nodes_name[value1] = list() + nodes_name[value1].append(self.client.get_node(value1).get_browse_name().__dict__['Name']) + if len(self.client.get_node(value1).get_children()) > 0: + if value1 not in new_dict2: + new_dict2[value1] = list() + new_dict2[value1].append(self.client.get_node(value1).get_children()) + + if bool(new_dict2): #If there is even more children, run it again! + self.run_recursively(new_dict2, nodes_name) + + return nodes_name + + + + def get_rootnode_nodeid_from_name(self, root_array, children_list): + root_childrenid_dict = {} + for key, values in root_array.items(): #We iterate over all the root nodes and its children nodes. + for i in children_list: # We iterate over the children string array + for value in values: # We check the values in the root node + if key not in root_childrenid_dict: + root_childrenid_dict[key] = list() + if self.client.get_node(value).get_browse_name().__dict__['Name'] == i: + root_childrenid_dict[key].append(value) + return root_childrenid_dict + + def get_name_from_nodes(self, dict_list): #Takes in a dictonary with the Objects : values that are only path values and converts to the name + name_dict = {} + for key, values in dict_list.items(): + for value in values: + + if key not in name_dict: + name_dict.setdefault(self.client.get_node(key).get_browse_name().__dict__['Name'], []) + + name_dict[self.client.get_node(key).get_browse_name().__dict__['Name']].append(self.client.get_node(value).get_browse_name().__dict__['Name']) + + return name_dict + \ No newline at end of file diff --git a/uaxplorer/ui_methods/server_discovery.py b/uaxplorer/ui_methods/server_discovery.py new file mode 100644 index 0000000..32de2e0 --- /dev/null +++ b/uaxplorer/ui_methods/server_discovery.py @@ -0,0 +1,44 @@ +#DISCOVER ALL SERVERS WITH THIS METHOD. +import discovery as disc +import time +# For testing: +#d = Server_Discovery() # Create an instance + +#print(d.get_servers()) #Get all servers available in a tuple + +#print(d.get_all(1)) # Get all ip addresses from all servers + +class Server_Discovery(): + def __init__(self): + self.DISCOVERY_OUTPUT = [] + + def get_servers(self): # To find the servers available + + Test_types = ["_opcua-tcp._tcp.local.", + "_opcua-https._tcp.local.", + "_opcua-wss._tcp.local."] + + d = disc.Discovery(Test_types) + time_to_end = time.time() + 5 + while(time.time() < time_to_end): + if(len(d.get_services()) > 0): + self.DISCOVERY_OUTPUT = d.get_services() + + return self.DISCOVERY_OUTPUT + + def get_all(self, type): #0 for server name, 1 for ip address, 2 for port number + temp_array = [] + for i in self.DISCOVERY_OUTPUT: + temp_array.append(i[type]) + + return temp_array + + def combine(self, server): + return server[1] + ":" + server[2] + + def get_all_as_address(self): + arr = [] + for s in self.DISCOVERY_OUTPUT: + arr.append(self.combine(s)) + + return arr \ No newline at end of file