-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
205 lines (178 loc) · 8.79 KB
/
server.py
File metadata and controls
205 lines (178 loc) · 8.79 KB
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
#Import pymodbus components
from pymodbus.server.sync import StartTcpServer, StartSerialServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSparseDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.transaction import ModbusRtuFramer
#Import local scripts componenets
import xml.etree.ElementTree as ET
from threading import Thread
import time, logging, sys
from client import validateXml
class Server:
"""Server simulator that serves on given ip and port and generates signals based on xml file"""
def __init__(self, xmlFile):
self.xml = xmlFile
validationInt = validateXml(self.xml)
if validationInt == -1: raise Exception('XML File Error: devicData node missing')
elif validationInt == -2: raise Exception('XML File Error: modbus type not set')
elif validationInt == -3: raise Exception('XML File Error: ip address missing')
elif validationInt == -4: raise Exception('XML File Error: comm port missing')
elif validationInt == -5: raise Exception('XML File Error: baud rate missing')
elif validationInt == -10: raise Exception('XML File Error: No register mappings')
elif validationInt == -11: raise Exception('XML File Error: Duplicated Input register mapping')
elif validationInt == -12: raise Exception('XML File Error: Duplicated Discrete input mapping')
elif validationInt == -13: raise Exception('XML File Error: Duplicated Holding register mapping')
elif validationInt == -14: raise Exception('XML File Error: Duplicated Coil mapping')
self.xmlData = self._parseXml()
store = ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, self.xmlData.get('registers').get('di')),
ir=ModbusSequentialDataBlock(0, self.xmlData.get('registers').get('ir')),
co=ModbusSequentialDataBlock(0, self.xmlData.get('registers').get('co')),
hr=ModbusSequentialDataBlock(0, self.xmlData.get('registers').get('hr')),
zero_mode=True)
self.context = ModbusServerContext(slaves=store, single=True)
self.deviceIdentity = ModbusDeviceIdentification()
self.deviceIdentity.VendorName = self.xmlData.get("vendorName")
self.deviceIdentity.ProductCode = self.xmlData.get("productCode")
self.deviceIdentity.VendorUrl = self.xmlData.get("vendorUrl")
self.deviceIdentity.ProductName = self.xmlData.get("productName")
self.deviceIdentity.ModelName = self.xmlData.get("modelName")
self.deviceIdentity.MajorMinorRevision = self.xmlData.get("Version")
def _parseXml(self):
"""Parses xml file and validates the registers"""
data = {}
tree = ET.parse(self.xml)
root = tree.getroot()
registers = root.find('registers')
#Discrete inputs (booleans)
diNode = registers.find('di')
if diNode != None:
di = [0]*65535
for mapping in diNode.findall('mapping'):
ix = int(mapping.get('register'))
if mapping.get('initialValue') != None:
di[ix] = int(mapping.get('initialValue'))
else:
di = [0]
#Input registers (analogs)
irNode = registers.find('ir')
if irNode != None:
ir = [0]*65535
for mapping in irNode.findall('mapping'):
ix = int(mapping.get('register'))
if mapping.get('initialValue') != None:
ir[ix] = int(mapping.get('initialValue'))
else:
ir = [0]
#Holding registers (analogs)
hrNode = registers.find('hr')
if hrNode != None:
hr = [0]*65535
for mapping in hrNode.findall('mapping'):
ix = int(mapping.get('register'))
if mapping.get('initialValue') != None:
hr[ix] = int(mapping.get('initialValue'))
else:
hr = [0]
#Coils (booleans)
coNode = registers.find('co')
if coNode != None:
co = [0]*65535
for mapping in coNode.findall('mapping'):
ix = int(mapping.get('register'))
if mapping.get('initialValue') != None:
co[ix] = int(mapping.get('initialValue'))
else:
co = [0]
data['registers'] = {
'di' : di,
'ir' : ir,
'hr' : hr,
'co' : co
}
#Parse device data
deviceData = root.find('deviceData')
data['vendorName'] = deviceData.get("vendorName", '')
data['productCode'] = deviceData.get("productCode", '')
data['vendorUrl'] = deviceData.get("vendorUrl", '')
data['productName'] = deviceData.get("productName", '')
data['modelName'] = deviceData.get("modelName", '')
data['version'] = deviceData.get("version", '0.0-1')
data['modbusType'] = deviceData.get('modbusType')
data['com'] = deviceData.get("com", None)
data['baud'] = int(deviceData.get("baud", "9600"))
data['stopbits'] = int(deviceData.get("stopbits", "1"))
data['bytesize'] = int(deviceData.get("bytesize", "8"))
data['parity'] = deviceData.get("parity", "E")
data['ip'] = deviceData.get("ip", "localhost")
data['port'] = int(deviceData.get("port", 502))
data['timeout'] = int(deviceData.get('timeout', "2"))
return data
def run_server(self, callback=None, debug = True):
"""Runs the modbus tcp or rtu server with given register information. if increment is true, the register values are dynamic and incrementing by one
every interval provided in cycle_s argument"""
if debug:
logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.DEBUG)
try:
#Data callback function will be executed as a separate thread
if callback != None:
thread = Thread(target=callback, args=(self.context,), daemon=True)
thread.start()
if self.xmlData.get('modbusType') == 'tcp/ip':
print(f"Running server on IP: {self.xmlData.get('ip')} and port {self.xmlData.get('port')}")
StartTcpServer(self.context, identity=self.deviceIdentity, address=(self.xmlData.get('ip'), self.xmlData.get('port')))
elif self.xmlData.get('modbusType') == 'rtu':
print(f"Running server on COM: {self.xmlData.get('com')} and baudrate {self.xmlData.get('baud')}")
StartSerialServer(self.context, timeout=self.xmlData.get('timeout'), framer=ModbusRtuFramer, identity=self.deviceIdentity, port=self.xmlData.get('com'), stopbits=self.xmlData.get('stopbits'), bytesize=self.xmlData.get('bytesize'), parity=self.xmlData.get('parity'), baudrate=self.xmlData.get('baud'))
except KeyboardInterrupt:
print('Server stopped')
#Helpfull data simulators
def incrementer(context):
""" A worker process that runs on a given cycle and
updates live values of the context.
"""
while True:
updateStartTime = time.perf_counter()
#Get values from only the first slave/ multiple slaves unsupported
#Toggle values of coils and digital inputs
di_values = context[0].getValues(2, 0, count=65535)
new_values = [v - 1 if v == 1 else v + 1 for v in di_values]
context[0].setValues(2, 0, new_values)
co_values = context[0].getValues(1, 0, count=65535)
new_values = [v - 1 if v == 1 else v + 1 for v in co_values]
context[0].setValues(1, 0, new_values)
hr_values = context[0].getValues(3, 0, count=65535)
new_values = [v + 1 for v in hr_values]
context[0].setValues(3, 0, new_values)
ir_values = context[0].getValues(4, 0, count=65535)
new_values = [v + 1 for v in ir_values]
context[0].setValues(4, 0, new_values)
#Calculate the latency
latency_ms = int((time.perf_counter() - updateStartTime) * 1000)
#if cycle time is faster than latency, go to sleep to match the cycle time
if latency_ms < 2000:
time.sleep((2000 - latency_ms) / 1000)
####MAIN APP#######
if __name__ == '__main__':
#handle arguments to the script
#Default arguments
increment = False
debug = False
callback = None
opts = [opt for opt in sys.argv[1:] if opt.startswith("-")]
args = [arg for arg in sys.argv[1:] if not arg.startswith("-")]
#xml file path must be first
xmlFilePath = args[0]
if '-i' in opts:
callback = incrementer
if '--increment' in opts:
callback = incrementer
if '-d' in opts:
debug = True
if '--debug' in opts:
debug = True
sim = Server(xmlFilePath)
sim.run_server(callback=callback, debug=debug)