-
Notifications
You must be signed in to change notification settings - Fork 0
/
opcua_ioc
executable file
·247 lines (213 loc) · 10.2 KB
/
opcua_ioc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# -*- coding: utf8 -*-:
######################################################################################################################################
# EPICS IOC developed with DLS Softioc Framework with support for OPC UA communication based on FreeOpcUa python package #
# Brazilian Synchroton Light Laboratory - Campinas, 06/xx/2018 #
# Author: Allan Serra Braga Bugyi (allan.bugyi@lnls.br) #
# In response to Ocommon's occurrence: 6134 #
# Version: 1.0 #
# Tested - Siemens PLC S7-1500 #
######################################################################################################################################
import sys
import time
import subprocess
from sys import argv
import os
import logging
import threading
from socket import error as socket_error
#python packages from DLS which are dependencies for SoftIioc. Set the 'cothread' and 'epicsdbbuilder' to the right version showed on distribution (in this case, '2.14' and 'development', respectively)
from pkg_resources import require
require('cothread==2.14')
require('epicsdbbuilder==development')
# Import basic DLS softIoc framework
from softioc import softioc, builder, device
#FreeOpcUa python package
from opcua import Client, ua
from opcua.common.subscription import SubHandler
if(((len(argv)) > 1) and argv[1] == "help"):
print(" opcua_ioc <'xml'> <'IP:port'>\n \
Provide an OPC UA XML as the first argument for importing nodes (e.g., 'opcua_ioc OPCUA_NODES.xml' )\n \
You can also provide the IP:port from the OPC UA server to connect. (e.g., 'opcua_ioc OPCUA_NODES.xml 192.168.0.1:4840' )")
sys.exit()
#Set IOC's name
builder.SetDeviceName('CP_HVAC')
# ------- Automated PV generation from OPC UA XML schema -------
#Extracting only the nodes, which means all the <UAVariable> items
try:
nodeExtraction_xml = subprocess.check_output(['sed', '-n', '/^<UAVariable.*BrowseName=".*".*ParentNodeId="ns=.*;s=.*".*DataType=".*"/p', argv[1]])
except (subprocess.CalledProcessError, IndexError):
sys.exit("Invalid file input. Please provide an OPC UA XML for importing nodes.")
if (nodeExtraction_xml == ""):
sys.exit("Invalid file input. Please provide an OPC UA XML for importing nodes.")
#Some variable initialization
pvs_To_NodesOPCUA_mapping = {}
#Extracting NodeId, BrowseName, ParentNodeId, DataType, AccessLevel from the selected lines
for line in nodeExtraction_xml.splitlines():
#Some pre-processing due to Siemens PLC broken XML
line_str = line.decode()
newline = line_str.replace('"', '"')
left_tag_exclusion = newline[20:]
split_on_whitespaces = left_tag_exclusion.split(" ")
#Extracting NodeId
nodeId = split_on_whitespaces[0]
#print nodeId
if((nodeId[((len(nodeId))-1)] == '"') and (nodeId[((len(nodeId))-2)] == '"')):
nodeId = nodeId[:-1]
else:
nodeId = nodeId.rstrip('"')
#print nodeId
#Extracting BrowseName
browseName = split_on_whitespaces[1].replace('BrowseName="', "")
browseName = browseName.rstrip('"')
#Extracting ParentNodeId
parentNodeId = split_on_whitespaces[2].replace('ParentNodeId="', "")
parentNodeId = parentNodeId.rstrip('"')
parentNodeId_test = ""
if ((len(parentNodeId) > 8)):
if (parentNodeId[7] != '"'):
parentNodeId_test = parentNodeId[7:]
else:
parentNodeId_test = parentNodeId[8:]
#Deciding whether all the PV creation should continue based on the ParentNode. This avoids the creation of unecessary PVs (or actually, PVs that
#reference not desired Nodes (e.g., Nodes without attributes), which are without any use to EPICS)
if((parentNodeId_test == "Inputs") or (parentNodeId_test == "Outputs") or (parentNodeId_test == "HMI") or (parentNodeId_test == "PID_memories")):#The ParentNodes which hold PVs
#Continues extracting properties from XML ...
#Extracting DataType
dataType = split_on_whitespaces[3].replace('DataType="', "")
dataType = dataType.rstrip('"')
dataType = dataType.rstrip('">')
#Extracting AccessLevel
accessLevel = ""
if (len(split_on_whitespaces) > 4):
accessLevel = split_on_whitespaces[4].replace('AccessLevel="', "")
accessLevel = accessLevel.rstrip('"')
accessLevel = accessLevel.rstrip('">')
#The PV's finals name for inside python referencing
pvName = "pv_" + parentNodeId_test + "_" + browseName[2:]#Notice that for python softIoc we use the convention '_' instead of EPICS' separator ':',
#which is a special character for Python
#Decision making based on which datatype PV will be set
if (dataType == 'bool' or dataType == 'BOOL'):
nodeOPCUA_properties_list = [nodeId, browseName, parentNodeId, dataType, accessLevel]
#PV creation
pvItself = builder.boolIn((parentNodeId_test+browseName[1:]))#PV's name fiting EPICS convention, with the ':' separating PVs' names, so 'caget' works fine
linkInformation_array = [nodeOPCUA_properties_list, pvItself]
pvs_To_NodesOPCUA_mapping.update({pvName: linkInformation_array})
elif (dataType == "Int16" or dataType == "INT" or dataType == "UInt16" or dataType == "UINT" or dataType == "Int32"
or dataType == "UInt32" or dataType == "UDINT"):
nodeOPCUA_properties_list = [nodeId, browseName, parentNodeId, dataType, accessLevel]
#PV creation
pvItself = builder.longIn((parentNodeId_test+browseName[1:]))#PV's name fiting EPICS convention, with the ':' separating PVs' names,so 'caget' works fine
linkInformation_array = [nodeOPCUA_properties_list, pvItself]
pvs_To_NodesOPCUA_mapping.update({pvName: linkInformation_array})
elif (dataType == "Float" or dataType == "REAL"):
nodeOPCUA_properties_list = [nodeId, browseName, parentNodeId, dataType, accessLevel]
#PV creation
pvItself = builder.aIn((parentNodeId_test+browseName[1:]))#PV's name fiting EPICS convention, with the ':' separating PVs' names, so 'caget' works fine
linkInformation_array = [nodeOPCUA_properties_list, pvItself]
pvs_To_NodesOPCUA_mapping.update({pvName: linkInformation_array})
else:
continue
# ---------------------------------------------------------
#Create OPCUA client using the OPC UA server's 'IP:port' address
if((len(argv)) > 2):
client = Client("opc.tcp://" + argv[2])
else:
client = Client("opc.tcp://10.2.121.202:4840") #Fixed server address and port
try:
#Establish connection with OPC UA Server
client.connect()
#function for monitoring the specified PV based on scan period
def monitorPV (pv_name, scan_period):
try:
arrayMappedPV = pvs_To_NodesOPCUA_mapping[pv_name]
nodeOPCUA_properties_list = arrayMappedPV[0]
pv = arrayMappedPV[1]
nodeId = nodeOPCUA_properties_list[0]
opcua_node_pv = client.get_node(nodeId)
oldValue = opcua_node_pv.get_value()
while True:
currentValue = opcua_node_pv.get_value()
if (currentValue == oldValue):
print(" " + (pv.name) + "\t" + str(opcua_node_pv.get_value()))
else:
print("**" + (pv.name) + "\t" + str(opcua_node_pv.get_value()) + "\tData change event detected!**")
pv.set(opcua_node_pv.get_value())
oldValue = currentValue
time.sleep(scan_period)
except KeyboardInterrupt:
print ("\n- Stopped Monitoring -")
''' Created for FreeOpcUa Event subscription
class mySubHandler(SubHandler):
#def __init__(self, obj):
# self.obj = obj
def datachange_notification(self, node, val, data):
print (node.name)
#print("* " + (str(node.get_display_name())) + "\t" + str(node.get_value()) + " *")
#def event_notification(self, event):
'''
#function for monitoring the specified PV based on datachange event subscription (self-implementation using threads and network I/O operation)
def monitorPV (pv_name, stop_event):
''' FreeOpcUa Event Subscription: supposed to work, but after many tries, apparently not.
arrayMappedPV = pvs_To_NodesOPCUA_mapping[pv_name]
nodeOPCUA_properties_list = arrayMappedPV[0]
nodeId = nodeOPCUA_properties_list[0]
pvNode = client.get_node(nodeId)
handler = mySubHandler()
sub = client.create_subscription(1, handler)
handle = sub.subscribe_data_change(pvNode)
#handle = sub.unsubscribe(handle)
'''
arrayMappedPV = pvs_To_NodesOPCUA_mapping[pv_name]
nodeOPCUA_properties_list = arrayMappedPV[0]
pv = arrayMappedPV[1]
nodeId = nodeOPCUA_properties_list[0]
opcua_node_pv = client.get_node(nodeId)
oldValue = opcua_node_pv.get_value()
while not stop_event.is_set():
currentValue = opcua_node_pv.get_value()
if (currentValue != oldValue):
print("**" + (pv.name) + "\t" + str(opcua_node_pv.get_value()) + "\tData change event detected!**")
pv.set(opcua_node_pv.get_value())
oldValue = currentValue
stop_event.clear()
return
#readPV and writePV: functions to read and write to specific PVs, equivalent to caget and caput Channel Access commands
def readPV (pv_name):
arrayMappedPV = pvs_To_NodesOPCUA_mapping[pv_name]
nodeOPCUA_properties_list = arrayMappedPV[0]
pv = arrayMappedPV[1]
nodeId = nodeOPCUA_properties_list[0]
opcua_node_pv = client.get_node(nodeId)
pv.set(opcua_node_pv.get_value())
print((pv.name) + "\t" + str(pv.get()))
def writePV (pv_name, value):
arrayMappedPV = pvs_To_NodesOPCUA_mapping[pv_name]
nodeOPCUA_properties_list = arrayMappedPV[0]
pv = arrayMappedPV[1]
nodeId = nodeOPCUA_properties_list[0]
dataType = nodeOPCUA_properties_list[3]
opcua_node_pv = client.get_node(nodeId)
var = ua.Variant(value, (opcua_node_pv.get_data_type_as_variant_type()))
print((pv.name) + "\tOld value: " + str(opcua_node_pv.get_value()))
opcua_node_pv.set_value(var)
pv.set(opcua_node_pv.get_value())
print((pv.name) + "\tNew value: " + str(pv.get()))
stop_event = threading.Event()
threads_dic = {}
def subscribeDataChange(pv_name):
if (pv_name not in threads_dic):
threads_dic[pv_name] = (threading.Thread(target = monitorPV, kwargs = {'pv_name': pv_name, 'stop_event': stop_event}))
thread = threads_dic[pv_name]
thread.start()
def unsubscribeDataChange(pv_name):
if (pv_name in threads_dic):
stop_event.set()
del threads_dic[pv_name]
# Run the IOC. This is boilerplate, and must always be done in this order,
# and must always be done after creating all PVs.
builder.LoadDatabase()
softioc.iocInit()
softioc.interactive_ioc(globals())
client.disconnect()
except socket_error:
sys.exit("Invalid IP address")