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" + "