Skip to content

Commit 4ed484d

Browse files
authoredMar 19, 2017
automatic unpack of custom extensionObjects(Structures)
* first attempt to decode custom extension objects, broken! * add new generator for structures * add test for online structure generator from xml * improve custom struct test and fix bug in ua_binary * add lxml dependency * remove dead code * pylint fixes * more robust parsing * start work on client side * add script to check init perf * cleanup extension object code for encoding/decoding and add support for registring on the fly * finish client implementation for custom structures * make automatic decoding of custom structs really work on client * add missing ua_types class member to ExtensionObject class
1 parent 440d064 commit 4ed484d

16 files changed

+1398
-299
lines changed
 

‎.travis.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ python:
66
# command to install dependencies
77
install:
88
- pip install python-dateutil
9+
- pip install pytz
10+
- pip install lxml
911
- if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install cryptography ; fi
10-
- if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then pip install pytz ; fi
1112
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install futures ; fi
1213
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install cryptography ; fi
1314
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install trollius ; fi
1415
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install enum34 ; fi
15-
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install pytz ; fi
1616
#- if [[ $TRAVIS_PYTHON_VERSION == 'pypy3' ]]; then pip install cryptography ; fi
1717
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install futures ; fi
1818
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install trollius ; fi
1919
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install enum34 ; fi
20-
- if [[ $TRAVIS_PYTHON_VERSION == 'pypy' ]]; then pip install pytz ; fi
2120
# command to run tests
2221
script: ./run-tests.sh

‎README.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ OPC UA binary protocol implementation is quasi complete and has been tested agai
1313

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

16-
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.
17-
```
18-
pip install enum34 trollius futures
19-
```
20-
16+
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.
2117

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

‎examples/test_perf.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import sys
2+
sys.path.insert(0, "..")
3+
import time
4+
5+
6+
from opcua import ua, Server
7+
8+
import cProfile
9+
import re
10+
11+
12+
def mymain():
13+
14+
# setup our server
15+
server = Server()
16+
server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")
17+
18+
# setup our own namespace, not really necessary but should as spec
19+
uri = "http://examples.freeopcua.github.io"
20+
idx = server.register_namespace(uri)
21+
22+
# get Objects node, this is where we should put our nodes
23+
objects = server.get_objects_node()
24+
25+
# populating our address space
26+
myobj = objects.add_object(idx, "MyObject")
27+
myvar = myobj.add_variable(idx, "MyVariable", 6.7)
28+
myvar.set_writable() # Set MyVariable to be writable by clients
29+
30+
# starting!
31+
server.start()
32+
server.stop()
33+
34+
35+
if __name__ == "__main__":
36+
cProfile.run('mymain()')

‎opcua/client/client.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from opcua.common import utils
1717
from opcua.crypto import security_policies
1818
from opcua.common.shortcuts import Shortcuts
19+
from opcua.common.structures_generator import StructGenerator
1920
use_crypto = True
2021
try:
2122
from opcua.crypto import uacrypto
@@ -550,4 +551,45 @@ def register_namespace(self, uri):
550551
ns_node.set_value(uries)
551552
return len(uries) - 1
552553

553-
554+
def import_and_register_structures(self, nodes=None):
555+
"""
556+
Download xml from given variable node defining custom structures.
557+
If no no node is given, attemps to import variables from all nodes under
558+
"0:OPC Binary"
559+
the code is generated and imported on the fly. If you know the structures
560+
are not going to be modified it might be interresting to copy the generated files
561+
and include them in you code
562+
"""
563+
if nodes is None:
564+
nodes = []
565+
for desc in self.nodes.opc_binary.get_children_descriptions():
566+
if desc.BrowseName != ua.QualifiedName("Opc.Ua"):
567+
nodes.append(self.get_node(desc.NodeId))
568+
self.logger.info("Importing structures from nodes: %s", nodes)
569+
570+
structs_dict = {}
571+
for node in nodes:
572+
xml = node.get_value()
573+
xml = xml.decode("utf-8")
574+
name = "structures_" + node.get_browse_name().Name
575+
gen = StructGenerator()
576+
gen.make_model_from_string(xml)
577+
gen.save_and_import(name + ".py", append_to=structs_dict)
578+
579+
# register classes
580+
for desc in self.nodes.base_structure_type.get_children_descriptions():
581+
# FIXME: maybe we should look recursively at children
582+
# FIXME: we should get enoding and description but this is too
583+
# expensive. we take a shorcut and assume that browsename of struct
584+
# is the same as the name of the data type structure
585+
if desc.BrowseName.Name in structs_dict:
586+
struct_node = self.get_node(desc.NodeId)
587+
refs = struct_node.get_references(ua.ObjectIds.HasEncoding, ua.BrowseDirection.Forward)
588+
for ref in refs:
589+
if "Binary" in ref.BrowseName.Name:
590+
ua.register_extension_object(desc.BrowseName.Name, ref.NodeId, structs_dict[desc.BrowseName.Name])
591+
592+
593+
594+
595+

‎opcua/common/node.py

+6
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ def get_methods(self):
308308
def get_children_descriptions(self, refs=ua.ObjectIds.HierarchicalReferences, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
309309
return self.get_references(refs, ua.BrowseDirection.Forward, nodeclassmask, includesubtypes)
310310

311+
def get_encoding_refs(self):
312+
return self.get_referenced_nodes(ua.ObjectIds.HasEncoding, ua.BrowseDirection.Forward)
313+
314+
def get_description_refs(self):
315+
return self.get_referenced_nodes(ua.ObjectIds.HasDescription, ua.BrowseDirection.Forward)
316+
311317
def get_references(self, refs=ua.ObjectIds.References, direction=ua.BrowseDirection.Both, nodeclassmask=ua.NodeClass.Unspecified, includesubtypes=True):
312318
"""
313319
returns references of the node based on specific filter defined with:

‎opcua/common/shortcuts.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ def __init__(self, server):
2424
self.variable_types = Node(server, ObjectIds.VariableTypesFolder)
2525
self.object_types = Node(server, ObjectIds.ObjectTypesFolder)
2626
self.namespace_array = Node(server, ObjectIds.Server_NamespaceArray)
27+
self.opc_binary = Node(server, ObjectIds.OPCBinarySchema_TypeSystem)
28+
self.base_structure_type = Node(server, ObjectIds.Structure)

‎opcua/common/structures_generator.py

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""
2+
parse simple structures from an xml tree
3+
We only support a subset of features but should be enough
4+
for custom structures
5+
"""
6+
7+
import os
8+
import importlib
9+
10+
from lxml import objectify
11+
12+
13+
from opcua.ua.ua_binary import Primitives
14+
15+
16+
def get_default_value(uatype):
17+
if uatype == "String":
18+
return "None"
19+
elif uatype == "Guid":
20+
return "uuid.uuid4()"
21+
elif uatype in ("ByteString", "CharArray", "Char"):
22+
return None
23+
elif uatype == "Boolean":
24+
return "True"
25+
elif uatype == "DateTime":
26+
return "datetime.utcnow()"
27+
elif uatype in ("Int8", "Int16", "Int32", "Int64", "UInt8", "UInt16", "UInt32", "UInt64", "Double", "Float", "Byte", "SByte"):
28+
return 0
29+
else:
30+
return "ua." + uatype + "()"
31+
32+
33+
class Struct(object):
34+
def __init__(self, name):
35+
self.name = name
36+
self.fields = []
37+
self.code = ""
38+
39+
def get_code(self):
40+
if not self.fields:
41+
return """
42+
43+
class {}(object):
44+
pass
45+
46+
""".format(self.name)
47+
self._make_constructor()
48+
self._make_from_binary()
49+
self._make_to_binary()
50+
return self.code
51+
52+
def _make_constructor(self):
53+
self.code = """
54+
55+
56+
class {0}(object):
57+
'''
58+
{0} structure autogenerated from xml
59+
'''
60+
def __init__(self, data=None):
61+
if data is not None:
62+
self._binary_init(data)
63+
return
64+
""".format(self.name)
65+
for field in self.fields:
66+
self.code += " self.{} = {}\n".format(field.name, field.value)
67+
68+
def _make_from_binary(self):
69+
self.code += '''
70+
@staticmethod
71+
def from_binary(data):
72+
return {}(data=data)
73+
74+
def _binary_init(self, data):
75+
'''.format(self.name)
76+
for field in self.fields:
77+
if hasattr(Primitives, field.uatype):
78+
if field.array:
79+
self.code += ' self.{} = ua.ua_binary.Primitives.{}.unpack_array(data)\n'.format(field.name, field.uatype)
80+
else:
81+
self.code += ' self.{} = ua.ua_binary.Primitives.{}.unpack(data)\n'.format(field.name, field.uatype)
82+
else:
83+
if field.array:
84+
self.code += '''
85+
length = ua.ua_binary.Primitives.Int32.unpack(data)
86+
if length == -1:
87+
self.{0} = None
88+
else:
89+
self.{0} = [ua.{1}.from_binary(data) for _ in range(length)]
90+
'''.format(field.name, field.uatype)
91+
else:
92+
self.code += " self.{} = ua.{}.from_binary(data)\n".format(field.name, field.uatype)
93+
94+
def _make_to_binary(self):
95+
self.code += '''
96+
def to_binary(self):
97+
packet = []
98+
'''
99+
for field in self.fields:
100+
if hasattr(Primitives, field.uatype):
101+
if field.array:
102+
self.code += ' packet.append(ua.ua_binary.Primitives.{}.pack_array(self.{}))\n'.format(field.uatype, field.name)
103+
else:
104+
self.code += ' packet.append(ua.ua_binary.Primitives.{}.pack(self.{}))\n'.format(field.uatype, field.name)
105+
else:
106+
if field.array:
107+
self.code += '''
108+
if self.{0} is None:
109+
packet.append(ua.ua_binary.Primitives.Int32.pack(-1))
110+
else:
111+
packet.append(ua.ua_binary.Primitives.Int32.pack(len(self.{0})))
112+
for element in self.{0}:
113+
packet.append(element.to_binary())
114+
'''.format(field.name)
115+
else:
116+
self.code += " packet.append(self.{}.to_binary())\n".format(field.name)
117+
self.code += ' return b"".join(packet)'
118+
119+
120+
class Field(object):
121+
def __init__(self, name):
122+
self.name = name
123+
self.uatype = None
124+
self.value = None
125+
self.array = False
126+
127+
128+
class StructGenerator(object):
129+
def __init__(self):
130+
self.model = []
131+
132+
def make_model_from_string(self, xml):
133+
obj = objectify.fromstring(xml)
134+
self._make_model(obj)
135+
136+
def make_model_from_file(self, path):
137+
obj = objectify.parse(path)
138+
root = obj.getroot()
139+
self._make_model(root)
140+
141+
def _make_model(self, root):
142+
for child in root.iter("{*}StructuredType"):
143+
struct = Struct(child.get("Name"))
144+
array = False
145+
for xmlfield in child.iter("{*}Field"):
146+
name = xmlfield.get("Name")
147+
if name.startswith("NoOf"):
148+
array = True
149+
continue
150+
field = Field(name)
151+
field.uatype = xmlfield.get("TypeName")
152+
if ":" in field.uatype:
153+
field.uatype = field.uatype.split(":")[1]
154+
field.value = get_default_value(field.uatype)
155+
if array:
156+
field.array = True
157+
field.value = []
158+
array = False
159+
struct.fields.append(field)
160+
self.model.append(struct)
161+
162+
def save_to_file(self, path):
163+
_file = open(path, "wt")
164+
self._make_header(_file)
165+
for struct in self.model:
166+
_file.write(struct.get_code())
167+
_file.close()
168+
169+
def save_and_import(self, path, append_to=None):
170+
"""
171+
save the new structures to a python file which be used later
172+
import the result and return resulting classes in a dict
173+
if append_to is a dict, the classes are added to the dict
174+
"""
175+
self.save_to_file(path)
176+
name = os.path.basename(path)
177+
name = os.path.splitext(name)[0]
178+
mymodule = importlib.import_module(name)
179+
if append_to is None:
180+
result = {}
181+
else:
182+
result = append_to
183+
for struct in self.model:
184+
result[struct.name] = getattr(mymodule, struct.name)
185+
return result
186+
187+
def get_structures(self):
188+
ld = {}
189+
for struct in self.model:
190+
exec(struct.get_code(), ld)
191+
return ld
192+
193+
def _make_header(self, _file):
194+
_file.write("""
195+
'''
196+
THIS FILE IS AUTOGENERATED, DO NOT EDIT!!!
197+
'''
198+
199+
from datetime import datetime
200+
import uuid
201+
202+
from opcua import ua
203+
""")
204+
205+
206+
207+
208+
if __name__ == "__main__":
209+
import sys
210+
from IPython import embed
211+
sys.path.insert(0, ".") # necessary for import in current dir
212+
213+
#xmlpath = "schemas/Opc.Ua.Types.bsd"
214+
xmlpath = "schemas/example.bsd"
215+
c = StructGenerator(xmlpath, "structures.py")
216+
c.run()
217+
import structures as s
218+
219+
220+
#sts = c.get_structures()
221+
embed()

‎opcua/common/ua_utils.py

+11
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,14 @@ def get_nodes_of_namespace(server, namespaces=None):
258258
nodes = [server.get_node(nodeid) for nodeid in server.iserver.aspace.keys()
259259
if nodeid.NamespaceIndex != 0 and nodeid.NamespaceIndex in namespace_indexes]
260260
return nodes
261+
262+
263+
def get_default_value(uatype):
264+
if isinstance(uatype, ua.VariantType):
265+
return ua.get_default_values(uatype)
266+
elif hasattr(ua.VariantType, uatype):
267+
return ua.get_default_value(getattr(ua.VariantType, uatype))
268+
else:
269+
return getattr(ua, uatype)()
270+
271+

‎opcua/common/utils.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""
2+
Helper function and classes that do not rely on opcua library.
3+
Helper function and classes depending on ua object are in ua_utils.py
4+
"""
5+
16
import logging
27
import os
38
from concurrent.futures import Future
@@ -6,7 +11,6 @@
611
from socket import error as SocketError
712

813
try:
9-
# we prefer to use bundles asyncio version, otherwise fallback to trollius
1014
import asyncio
1115
except ImportError:
1216
import trollius as asyncio

0 commit comments

Comments
 (0)
Please sign in to comment.