Commit 78183016 authored by Philipp's avatar Philipp

feat: Implement client functionality (opcua-to-http-gw.py)

- Add login to OPC UA server via credentials
- Establish interval connection for reading nodes from OPC UA server
- Implement monitoring function to subscribe and monitor data changes on OPC UA server nodes
feat: Implement server functionality hosting data and randomly chaning it (minimal-server.py)
parent 16279aac
#!//usr/bin/python
"""
Basic server with minimal data
Sever simulating/emulating hosting data.
"""
# from https://opcua-asyncio.readthedocs.io/en/latest/usage/get-started/minimal-client.html
import asyncio
import logging
from asyncua import Server, ua
from asyncua.common.methods import uamethod
from asyncua.common.manage_nodes import create_object_type
from asyncua.server.users import User, UserRole
import argparse
import random
import csv
import sys
import string
# command line handling
parser = argparse.ArgumentParser(description='Run OPCUA Server.')
a = parser.add_argument
a('--ipv4', help='The IPv4 address on which the OPCUA Server runs', default="0.0.0.0")
a('--ipv6', help='The IPv6 address on which the OPCUA Server runs', default="::")
a('--ipv6-enabled', help='The IPv6 address check whether it is enabled or disabled', default="1")
a('--port', help='The port on which the OPCUA Server runs', default="4840")
a('--xml', help='Path of XML to configure Server. See asyncua doc for more details.', default=None)
args = parser.parse_args()
ipv4 = args.ipv4
ipv6 = args.ipv6
ipv6_enabled = args.ipv6_enabled
port = args.port
xml = args.xml
ApplicationUri = "Powerfuse_DataConnect"
# For local development
ipv6 = "::"
users_db = {
"Powerfuse": "password"
}
class UserManager:
def get_user(self, iserver, username=None, password=None, certificate=None):
if username in users_db and password == users_db[username].encode():
return User(role=UserRole.Admin)
return None
def read_nodes(input_interface_description):
import csv
'''
Reads the nodes defined in .csv style description as list of dictionaries with key values paris.
keys represent the header of the .csv file
input_interface_description: filename of the interface description
'''
csv_data = []
with open(input_interface_description, 'r') as file:
reader = csv.DictReader(file, delimiter=',')
for row in reader:
csv_data.append(row)
return csv_data
# Class sub handler - with function that notes datachanges
class SubHandler(object):
def datachange_notification(self, node, val, data):
print(f"New data change event for node {node} with value {val}")
async def shuffle_based_on_csv_data(server, _logger, csv_data):
'''
# The values of the nodes are again set randomly.
# Import the nodes from the CSV data
# server: opcua server
# csv_data: data in the original format of csv now in the format of list of dictionary - iterating over the rows
'''
data_type_mapping = {
'BOOL': (ua.VariantType.Boolean, lambda: random.choice([True, False])),
'INT': (ua.VariantType.Int32, lambda: random.randint(-2**31, 2**31 - 1)),
'STRING': (ua.VariantType.String, lambda: ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(random.randint(5, 20)))),
'UDINT': (ua.VariantType.UInt32, lambda: random.randint(0, 2**32 - 1)),
'UINT': (ua.VariantType.UInt16, lambda: random.randint(0, 2**16 - 1)),
'REAL': (ua.VariantType.Float, lambda: random.uniform(-1e6, 1e6)),
}
object_type_node_class_map = {}
for row in csv_data:
idx = 1
object_type_node_class = row['object_type_node_class']
s = row['s']
data_type = row['data_type']
description = row['description']
access_level = row['access_level']
cloud_transmission = row['cloud_transmission']
if data_type not in data_type_mapping:
_logger.error(f"Unsupported data type: {data_type} for node {s}")
continue
root = server.nodes.root
# Get the object node
myobj = await root.get_child(["0:Objects", f"{idx}:{object_type_node_class}"])
# Get the variable nodes
myvar = await myobj.get_child(f"{idx}:{s}")
variant_type, value_func = data_type_mapping[data_type]
value = value_func()
await myvar.write_value(value)
async def import_nodes_from_csv_data(server, _logger, csv_data):
# Import the nodes from the CSV data
# server: opcua server
# csv_data: data in the original format of csv now in the format of list of dictionary - iterating over the rows
data_type_mapping = {
'BOOL': (ua.VariantType.Boolean, lambda: random.choice([True, False])),
'INT': (ua.VariantType.Int32, lambda: random.randint(-2**31, 2**31 - 1)),
'STRING': (ua.VariantType.String, lambda: ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(random.randint(5, 20)))),
'UDINT': (ua.VariantType.UInt32, lambda: random.randint(0, 2**32 - 1)),
'UINT': (ua.VariantType.UInt16, lambda: random.randint(0, 2**16 - 1)),
'REAL': (ua.VariantType.Float, lambda: random.uniform(-1e6, 1e6)),
}
object_type_node_class_map = {}
for row in csv_data:
idx = 1
object_type_node_class = row['object_type_node_class']
s = row['s']
data_type = row['data_type']
description = row['description']
access_level = row['access_level']
cloud_transmission = row['cloud_transmission']
if data_type not in data_type_mapping:
_logger.error(f"Unsupported data type: {data_type} for node {s}")
continue
if object_type_node_class not in object_type_node_class_map:
myobj = await server.nodes.objects.add_object(idx, object_type_node_class)
object_type_node_class_map[object_type_node_class] = myobj
else:
myobj = object_type_node_class_map[object_type_node_class]
variant_type, value_func = data_type_mapping[data_type]
myvar = await myobj.add_variable(idx, s, ua.Variant([], variant_type), description)
value = value_func()
await myvar.set_writable()
await myvar.set_writable(access_level == access_level)
await myvar.set_value(value)
# Create a subscription with a publishing interval of 500 milliseconds and assign it to the SubHandler class for handling subscription events
sub = await server.create_subscription(500, handler=SubHandler())
# Subscribe the variable node to the subscription for data change notifications
await sub.subscribe_data_change(myvar)
async def main():
_logger = logging.getLogger(__name__)
bool_vialogin = True
# setup our server
server = Server()
await server.init()
if bool_vialogin:
# server = Server()
user_manager = UserManager()
server = Server(user_manager = user_manager)
await server.init()
else:
server = Server()
await server.init()
if bool(int(ipv6_enabled)):
server.set_endpoint(f"opc.tcp://[{ipv6}]:{port}/PwDC")
_logger.info(f"Set endpoint to: opc.tcp://[{ipv6}]:{port}/{ApplicationUri}")
server.set_endpoint(f"opc.tcp://[{ipv6}]:{port}/{ApplicationUri}")
else:
_logger.debug(f"Setting endpoint to: opc.tcp://[{ipv4}]:{port}/{ApplicationUri}")
server.set_endpoint(f"opc.tcp://{ipv4}:{port}/{ApplicationUri}")
#from asyncua.crypto.permission_rules import SimpleRoleRuleset
#server.set_security_policy([ua.SecurityPolicyType.NoSecurity],permission_ruleset=SimpleRoleRuleset())
#,
#ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
#ua.SecurityPolicyType.Basic256Sha256_Sign
# ]
if xml is not None:
await server.import_xml(xml)
idx = 1
# Create an object to hold the variables
objects = server.get_objects_node()
powerfuse_obj = await objects.add_object(idx, "Powerfuse")
# Create variables
machine_status_rot = await powerfuse_obj.add_variable(idx, "Powerfuse_Maschinenstatus_Status_Rot", False)
machine_status_gelb = await powerfuse_obj.add_variable(idx, "Powerfuse_Maschinenstatus_Status_Gelb", False)
# Read the interface description defining the nodes
nodes_data = read_nodes(input_interface_description = "Powerfuse_Interface_definition_import.csv")
# Set the variables as writable
await machine_status_rot.set_writable()
await machine_status_gelb.set_writable()
# Create nodes defined in the interface description
await import_nodes_from_csv_data(server = server, _logger = _logger, csv_data = nodes_data)
# Start the server
_logger.info("Starting server!")
# Trigger data change events
async with server:
while True:
# Randomly change the machine status
new_status_rot = random.choice([True, False])
new_status_gelb = random.choice([True, False])
await machine_status_rot.write_value(new_status_rot)
await machine_status_gelb.write_value(new_status_gelb)
val_machine_status_rot = await machine_status_rot.get_value()
val_machine_status_gelb = await machine_status_gelb.get_value()
_logger.info("New machine status: Rot=%r, Gelb=%r",
val_machine_status_rot,
val_machine_status_gelb)
await asyncio.sleep(1)
await shuffle_based_on_csv_data(server = server, _logger = _logger, csv_data = nodes_data)
await asyncio.sleep(10)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.ERROR)
asyncio.run(main(), debug=False)
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment