Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

automatic unpack of custom extensionObjects(Structures) WORK IN PROGRESS #388

Merged
merged 14 commits into from
Mar 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@ python:
# command to install dependencies
install:
- pip install python-dateutil
- pip install pytz
- pip install lxml
- if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install cryptography ; fi
- if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install pytz ; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install futures ; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install cryptography ; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install trollius ; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install enum34 ; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install pytz ; fi
#- if [[ $TRAVIS_PYTHON_VERSION == 'pypy3' ]]; then pip install cryptography ; fi
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install futures ; fi
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install trollius ; fi
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install enum34 ; fi
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install pytz ; fi
# command to run tests
script: ./run-tests.sh
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ OPC UA binary protocol implementation is quasi complete and has been tested agai

Most low level code is autogenerated from xml specification, thus adding missing functionality to client or server is often trivial.

Using Python > 3.4 the dependencies are cryptography, dateutil and pytz. If using python 2.7 or pypy < 3 you also need to install enum34, trollius(asyncio), and futures(concurrent.futures), with pip for example.
```
pip install enum34 trollius futures
```

Using Python > 3.4 the dependencies are cryptography, dateutil, lxml and pytz. If using python 2.7 or pypy < 3 you also need to install enum34, trollius(asyncio), and futures(concurrent.futures), with pip for example.

coveryage.py reports a test coverage of over 90% of code, most of non-tested code is autogenerated code that is not used yet.

Expand Down
36 changes: 36 additions & 0 deletions examples/test_perf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import sys
sys.path.insert(0, "..")
import time


from opcua import ua, Server

import cProfile
import re


def mymain():

# setup our server
server = Server()
server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")

# setup our own namespace, not really necessary but should as spec
uri = "http://examples.freeopcua.github.io"
idx = server.register_namespace(uri)

# get Objects node, this is where we should put our nodes
objects = server.get_objects_node()

# populating our address space
myobj = objects.add_object(idx, "MyObject")
myvar = myobj.add_variable(idx, "MyVariable", 6.7)
myvar.set_writable() # Set MyVariable to be writable by clients

# starting!
server.start()
server.stop()


if __name__ == "__main__":
cProfile.run('mymain()')
44 changes: 43 additions & 1 deletion opcua/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from opcua.common import utils
from opcua.crypto import security_policies
from opcua.common.shortcuts import Shortcuts
from opcua.common.structures_generator import StructGenerator
use_crypto = True
try:
from opcua.crypto import uacrypto
Expand Down Expand Up @@ -550,4 +551,45 @@ def register_namespace(self, uri):
ns_node.set_value(uries)
return len(uries) - 1


def import_and_register_structures(self, nodes=None):
"""
Download xml from given variable node defining custom structures.
If no no node is given, attemps to import variables from all nodes under
"0:OPC Binary"
the code is generated and imported on the fly. If you know the structures
are not going to be modified it might be interresting to copy the generated files
and include them in you code
"""
if nodes is None:
nodes = []
for desc in self.nodes.opc_binary.get_children_descriptions():
if desc.BrowseName != ua.QualifiedName("Opc.Ua"):
nodes.append(self.get_node(desc.NodeId))
self.logger.info("Importing structures from nodes: %s", nodes)

structs_dict = {}
for node in nodes:
xml = node.get_value()
xml = xml.decode("utf-8")
name = "structures_" + node.get_browse_name().Name
gen = StructGenerator()
gen.make_model_from_string(xml)
gen.save_and_import(name + ".py", append_to=structs_dict)

# register classes
for desc in self.nodes.base_structure_type.get_children_descriptions():
# FIXME: maybe we should look recursively at children
# FIXME: we should get enoding and description but this is too
# expensive. we take a shorcut and assume that browsename of struct
# is the same as the name of the data type structure
if desc.BrowseName.Name in structs_dict:
struct_node = self.get_node(desc.NodeId)
refs = struct_node.get_references(ua.ObjectIds.HasEncoding, ua.BrowseDirection.Forward)
for ref in refs:
if "Binary" in ref.BrowseName.Name:
ua.register_extension_object(desc.BrowseName.Name, ref.NodeId, structs_dict[desc.BrowseName.Name])





6 changes: 6 additions & 0 deletions opcua/common/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ def get_methods(self):
def get_children_descriptions(self, refs=ua.ObjectIds.HierarchicalReferences, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
return self.get_references(refs, ua.BrowseDirection.Forward, nodeclassmask, includesubtypes)

def get_encoding_refs(self):
return self.get_referenced_nodes(ua.ObjectIds.HasEncoding, ua.BrowseDirection.Forward)

def get_description_refs(self):
return self.get_referenced_nodes(ua.ObjectIds.HasDescription, ua.BrowseDirection.Forward)

def get_references(self, refs=ua.ObjectIds.References, direction=ua.BrowseDirection.Both, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
"""
returns references of the node based on specific filter defined with:
Expand Down
2 changes: 2 additions & 0 deletions opcua/common/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ def __init__(self, server):
self.variable_types = Node(server, ObjectIds.VariableTypesFolder)
self.object_types = Node(server, ObjectIds.ObjectTypesFolder)
self.namespace_array = Node(server, ObjectIds.Server_NamespaceArray)
self.opc_binary = Node(server, ObjectIds.OPCBinarySchema_TypeSystem)
self.base_structure_type = Node(server, ObjectIds.Structure)
221 changes: 221 additions & 0 deletions opcua/common/structures_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
"""
parse simple structures from an xml tree
We only support a subset of features but should be enough
for custom structures
"""

import os
import importlib

from lxml import objectify


from opcua.ua.ua_binary import Primitives


def get_default_value(uatype):
if uatype == "String":
return "None"
elif uatype == "Guid":
return "uuid.uuid4()"
elif uatype in ("ByteString", "CharArray", "Char"):
return None
elif uatype == "Boolean":
return "True"
elif uatype == "DateTime":
return "datetime.utcnow()"
elif uatype in ("Int8", "Int16", "Int32", "Int64", "UInt8", "UInt16", "UInt32", "UInt64", "Double", "Float", "Byte", "SByte"):
return 0
else:
return "ua." + uatype + "()"


class Struct(object):
def __init__(self, name):
self.name = name
self.fields = []
self.code = ""

def get_code(self):
if not self.fields:
return """

class {}(object):
pass

""".format(self.name)
self._make_constructor()
self._make_from_binary()
self._make_to_binary()
return self.code

def _make_constructor(self):
self.code = """


class {0}(object):
'''
{0} structure autogenerated from xml
'''
def __init__(self, data=None):
if data is not None:
self._binary_init(data)
return
""".format(self.name)
for field in self.fields:
self.code += " self.{} = {}\n".format(field.name, field.value)

def _make_from_binary(self):
self.code += '''
@staticmethod
def from_binary(data):
return {}(data=data)

def _binary_init(self, data):
'''.format(self.name)
for field in self.fields:
if hasattr(Primitives, field.uatype):
if field.array:
self.code += ' self.{} = ua.ua_binary.Primitives.{}.unpack_array(data)\n'.format(field.name, field.uatype)
else:
self.code += ' self.{} = ua.ua_binary.Primitives.{}.unpack(data)\n'.format(field.name, field.uatype)
else:
if field.array:
self.code += '''
length = ua.ua_binary.Primitives.Int32.unpack(data)
if length == -1:
self.{0} = None
else:
self.{0} = [ua.{1}.from_binary(data) for _ in range(length)]
'''.format(field.name, field.uatype)
else:
self.code += " self.{} = ua.{}.from_binary(data)\n".format(field.name, field.uatype)

def _make_to_binary(self):
self.code += '''
def to_binary(self):
packet = []
'''
for field in self.fields:
if hasattr(Primitives, field.uatype):
if field.array:
self.code += ' packet.append(ua.ua_binary.Primitives.{}.pack_array(self.{}))\n'.format(field.uatype, field.name)
else:
self.code += ' packet.append(ua.ua_binary.Primitives.{}.pack(self.{}))\n'.format(field.uatype, field.name)
else:
if field.array:
self.code += '''
if self.{0} is None:
packet.append(ua.ua_binary.Primitives.Int32.pack(-1))
else:
packet.append(ua.ua_binary.Primitives.Int32.pack(len(self.{0})))
for element in self.{0}:
packet.append(element.to_binary())
'''.format(field.name)
else:
self.code += " packet.append(self.{}.to_binary())\n".format(field.name)
self.code += ' return b"".join(packet)'


class Field(object):
def __init__(self, name):
self.name = name
self.uatype = None
self.value = None
self.array = False


class StructGenerator(object):
def __init__(self):
self.model = []

def make_model_from_string(self, xml):
obj = objectify.fromstring(xml)
self._make_model(obj)

def make_model_from_file(self, path):
obj = objectify.parse(path)
root = obj.getroot()
self._make_model(root)

def _make_model(self, root):
for child in root.iter("{*}StructuredType"):
struct = Struct(child.get("Name"))
array = False
for xmlfield in child.iter("{*}Field"):
name = xmlfield.get("Name")
if name.startswith("NoOf"):
array = True
continue
field = Field(name)
field.uatype = xmlfield.get("TypeName")
if ":" in field.uatype:
field.uatype = field.uatype.split(":")[1]
field.value = get_default_value(field.uatype)
if array:
field.array = True
field.value = []
array = False
struct.fields.append(field)
self.model.append(struct)

def save_to_file(self, path):
_file = open(path, "wt")
self._make_header(_file)
for struct in self.model:
_file.write(struct.get_code())
_file.close()

def save_and_import(self, path, append_to=None):
"""
save the new structures to a python file which be used later
import the result and return resulting classes in a dict
if append_to is a dict, the classes are added to the dict
"""
self.save_to_file(path)
name = os.path.basename(path)
name = os.path.splitext(name)[0]
mymodule = importlib.import_module(name)
if append_to is None:
result = {}
else:
result = append_to
for struct in self.model:
result[struct.name] = getattr(mymodule, struct.name)
return result

def get_structures(self):
ld = {}
for struct in self.model:
exec(struct.get_code(), ld)
return ld

def _make_header(self, _file):
_file.write("""
'''
THIS FILE IS AUTOGENERATED, DO NOT EDIT!!!
'''

from datetime import datetime
import uuid

from opcua import ua
""")




if __name__ == "__main__":
import sys
from IPython import embed
sys.path.insert(0, ".") # necessary for import in current dir

#xmlpath = "schemas/Opc.Ua.Types.bsd"
xmlpath = "schemas/example.bsd"
c = StructGenerator(xmlpath, "structures.py")
c.run()
import structures as s


#sts = c.get_structures()
embed()
11 changes: 11 additions & 0 deletions opcua/common/ua_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,14 @@ def get_nodes_of_namespace(server, namespaces=None):
nodes = [server.get_node(nodeid) for nodeid in server.iserver.aspace.keys()
if nodeid.NamespaceIndex != 0 and nodeid.NamespaceIndex in namespace_indexes]
return nodes


def get_default_value(uatype):
if isinstance(uatype, ua.VariantType):
return ua.get_default_values(uatype)
elif hasattr(ua.VariantType, uatype):
return ua.get_default_value(getattr(ua.VariantType, uatype))
else:
return getattr(ua, uatype)()


6 changes: 5 additions & 1 deletion opcua/common/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""
Helper function and classes that do not rely on opcua library.
Helper function and classes depending on ua object are in ua_utils.py
"""

import logging
import os
from concurrent.futures import Future
Expand All @@ -6,7 +11,6 @@
from socket import error as SocketError

try:
# we prefer to use bundles asyncio version, otherwise fallback to trollius
import asyncio
except ImportError:
import trollius as asyncio
Expand Down
Loading