|
| 1 | +import sys |
| 2 | +sys.path.insert(0, "..") |
| 3 | +import time |
| 4 | + |
| 5 | + |
| 6 | +from opcua import ua, Server |
| 7 | + |
| 8 | +# INFO: The concept in this example is that the software model is first built in OPC UA via XML. After that, matching |
| 9 | +# python objects are created based on the UA address space design. Do not use this example to build a UA address space |
| 10 | +# from the python design. |
| 11 | + |
| 12 | +# The advantage of this is that the software can be designed in a tool like UA Modeler. Then with minimal setup, a |
| 13 | +# python program will import the XML and mirror all the objects in the software design. After this mirroring is achieved |
| 14 | +# the user can focus on programming in python knowing that all all data from UA clients will reach the python object, |
| 15 | +# and all data that needs to be output to the server can be published from the python object. |
| 16 | +# |
| 17 | +# Be aware that subscription calls are asynchronous. |
| 18 | + |
| 19 | + |
| 20 | +class SubHandler(object): |
| 21 | + """ |
| 22 | + Subscription Handler. To receive events from server for a subscription. |
| 23 | + The handler forwards updates to it's referenced python object |
| 24 | + """ |
| 25 | + |
| 26 | + def __init__(self, obj): |
| 27 | + self.obj = obj |
| 28 | + |
| 29 | + def datachange_notification(self, node, val, data): |
| 30 | + # print("Python: New data change event", node, val, data) |
| 31 | + |
| 32 | + _node_name = node.get_browse_name() |
| 33 | + setattr(self.obj, _node_name.Name, data.monitored_item.Value.Value.Value) |
| 34 | + |
| 35 | + |
| 36 | +class UaObject(object): |
| 37 | + """ |
| 38 | + Python object which mirrors an OPC UA object |
| 39 | + Child UA variables/properties are auto subscribed to to synchronize python with UA server |
| 40 | + Python can write to children via write method, which will trigger an update for UA clients |
| 41 | + """ |
| 42 | + def __init__(self, opcua_server, ua_node): |
| 43 | + self.opcua_server = opcua_server |
| 44 | + self.nodes = {} |
| 45 | + self.b_name = ua_node.get_browse_name().Name |
| 46 | + |
| 47 | + # keep track of the children of this object (in case python needs to write, or get more info from UA server) |
| 48 | + for _child in ua_node.get_children(): |
| 49 | + _child_name = _child.get_browse_name() |
| 50 | + self.nodes[_child_name.Name] = _child |
| 51 | + |
| 52 | + # find all children which can be subscribed to (python object is kept up to date via subscription) |
| 53 | + sub_children = ua_node.get_properties() |
| 54 | + sub_children.extend(ua_node.get_variables()) |
| 55 | + |
| 56 | + # subscribe to properties/variables |
| 57 | + handler = SubHandler(self) |
| 58 | + sub = opcua_server.create_subscription(500, handler) |
| 59 | + handle = sub.subscribe_data_change(sub_children) |
| 60 | + |
| 61 | + def write(self, attr=None): |
| 62 | + # if a specific attr isn't passed to write, write all OPC UA children |
| 63 | + if attr is None: |
| 64 | + for k, node in self.nodes.items(): |
| 65 | + node_class = node.get_node_class() |
| 66 | + if node_class == ua.NodeClass.Variable: |
| 67 | + node.set_value(getattr(self, k)) |
| 68 | + # only update a specific attr |
| 69 | + else: |
| 70 | + self.nodes[attr].set_value(getattr(self, attr)) |
| 71 | + |
| 72 | + |
| 73 | +class MyObj(UaObject): |
| 74 | + """ |
| 75 | + Definition of OPC UA object which represents a object to be mirrored in python |
| 76 | + This class mirrors it's UA counterpart and semi-configures itself according to the UA model (generally from XML) |
| 77 | + """ |
| 78 | + def __init__(self, opcua_server, ua_node): |
| 79 | + |
| 80 | + # properties and variables; must mirror UA model (based on browsename!) |
| 81 | + self.MyVariable = 0 |
| 82 | + self.MyProperty = 0 |
| 83 | + self.MyClientWrite = 0 |
| 84 | + |
| 85 | + # init the UaObject super class to connect the python object to the UA object |
| 86 | + super().__init__(opcua_server, ua_node) |
| 87 | + |
| 88 | + # local values only for use inside python |
| 89 | + self.testval = 'python only' |
| 90 | + |
| 91 | + # If the object has other objects as children it is best to search by type and instantiate more |
| 92 | + # mirrored python classes so that your UA object tree matches your python object tree |
| 93 | + |
| 94 | + # ADD CUSTOM OBJECT INITIALIZATION BELOW |
| 95 | + # find children by type and instantiate them as sub-objects of this class |
| 96 | + # NOT PART OF THIS EXAMPLE |
| 97 | + |
| 98 | + |
| 99 | +if __name__ == "__main__": |
| 100 | + |
| 101 | + # setup our server |
| 102 | + server = Server() |
| 103 | + server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/") |
| 104 | + |
| 105 | + # setup our own namespace, not really necessary but should as spec |
| 106 | + uri = "http://examples.freeopcua.github.io" |
| 107 | + idx = server.register_namespace(uri) |
| 108 | + |
| 109 | + # get Objects node, this is where we should put our nodes |
| 110 | + objects = server.get_objects_node() |
| 111 | + |
| 112 | + # populating our address space; in most real use cases this should be imported from UA spec XML |
| 113 | + myobj = objects.add_object(idx, "MyObject") |
| 114 | + myvar = myobj.add_variable(idx, "MyVariable", 0.0) |
| 115 | + myprop = myobj.add_property(idx, "MyProperty", 0) |
| 116 | + mywritevar = myobj.add_variable(idx, "MyClientWrite", 0) |
| 117 | + mywritevar.set_writable() # Set MyVariable to be writable by clients |
| 118 | + |
| 119 | + # starting! |
| 120 | + server.start() |
| 121 | + |
| 122 | + # after the UA server is started initialize the mirrored object |
| 123 | + my_python_obj = MyObj(server, myobj) |
| 124 | + |
| 125 | + try: |
| 126 | + while True: |
| 127 | + # from an OPC UA client write a value to this node to see it show up in the python object |
| 128 | + print('Python mirror of MyClientWrite is: ' + str(my_python_obj.MyClientWrite)) |
| 129 | + |
| 130 | + # write a single attr to OPC UA |
| 131 | + my_python_obj.MyVariable = 12.3 |
| 132 | + my_python_obj.MyProperty = 55 # this value will not be visible to clients because write is not called |
| 133 | + my_python_obj.write('MyVariable') |
| 134 | + |
| 135 | + time.sleep(3) |
| 136 | + |
| 137 | + # write all attr of the object to OPC UA |
| 138 | + my_python_obj.MyVariable = 98.1 |
| 139 | + my_python_obj.MyProperty = 99 |
| 140 | + my_python_obj.write() |
| 141 | + |
| 142 | + time.sleep(3) |
| 143 | + |
| 144 | + # write directly to the OPC UA node of the object |
| 145 | + dv = ua.DataValue(ua.Variant(5.5, ua.VariantType.Double)) |
| 146 | + my_python_obj.nodes['MyVariable'].set_value(dv) |
| 147 | + dv = ua.DataValue(ua.Variant(4, ua.VariantType.UInt64)) |
| 148 | + my_python_obj.nodes['MyVariable'].set_value(dv) |
| 149 | + |
| 150 | + time.sleep(3) |
| 151 | + |
| 152 | + finally: |
| 153 | + # close connection, remove subscriptions, etc |
| 154 | + server.stop() |
0 commit comments