Skip to content

Commit 08fa109

Browse files
chintaldhoomakethu
authored andcommitted
Add support for MEI device identification encoding respecting PDU size and repeated Object IDs in the device identification (#293)
* Add support for repeated MEI device information Object IDs * Add server-side support for repeated MEI device information Object IDs * Updated unit tests for repeated MEI device information Object ID support * Added simple examples to showcase the usage of the device information commands. * Added support for encoding device information when it requires more than one PDU to pack. * Added unit test for long device identification encoding"
1 parent d159a44 commit 08fa109

File tree

4 files changed

+341
-23
lines changed

4 files changed

+341
-23
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python
2+
"""
3+
Pymodbus Synchronous Client Example to showcase Device Information
4+
--------------------------------------------------------------------------
5+
6+
This client demonstrates the use of Device Information to get information
7+
about servers connected to the client. This is part of the MODBUS specification,
8+
and uses the MEI 0x2B 0x0E request / response.
9+
"""
10+
# --------------------------------------------------------------------------- #
11+
# import the various server implementations
12+
# --------------------------------------------------------------------------- #
13+
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
14+
# from pymodbus.client.sync import ModbusUdpClient as ModbusClient
15+
# from pymodbus.client.sync import ModbusSerialClient as ModbusClient
16+
17+
# --------------------------------------------------------------------------- #
18+
# import the request
19+
# --------------------------------------------------------------------------- #
20+
from pymodbus.mei_message import ReadDeviceInformationRequest
21+
from pymodbus.device import ModbusDeviceIdentification
22+
23+
# --------------------------------------------------------------------------- #
24+
# configure the client logging
25+
# --------------------------------------------------------------------------- #
26+
import logging
27+
FORMAT = ('%(asctime)-15s %(threadName)-15s '
28+
'%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
29+
logging.basicConfig(format=FORMAT)
30+
log = logging.getLogger()
31+
log.setLevel(logging.DEBUG)
32+
33+
UNIT = 0x1
34+
35+
36+
def run_sync_client():
37+
# ------------------------------------------------------------------------#
38+
# choose the client you want
39+
# ------------------------------------------------------------------------#
40+
# make sure to start an implementation to hit against. For this
41+
# you can use an existing device, the reference implementation in the tools
42+
# directory, or start a pymodbus server.
43+
#
44+
# If you use the UDP or TCP clients, you can override the framer being used
45+
# to use a custom implementation (say RTU over TCP). By default they use
46+
# the socket framer::
47+
#
48+
# client = ModbusClient('localhost', port=5020, framer=ModbusRtuFramer)
49+
#
50+
# It should be noted that you can supply an ipv4 or an ipv6 host address
51+
# for both the UDP and TCP clients.
52+
#
53+
# There are also other options that can be set on the client that controls
54+
# how transactions are performed. The current ones are:
55+
#
56+
# * retries - Specify how many retries to allow per transaction (default=3)
57+
# * retry_on_empty - Is an empty response a retry (default = False)
58+
# * source_address - Specifies the TCP source address to bind to
59+
#
60+
# Here is an example of using these options::
61+
#
62+
# client = ModbusClient('localhost', retries=3, retry_on_empty=True)
63+
# ------------------------------------------------------------------------#
64+
client = ModbusClient('localhost', port=5020)
65+
# from pymodbus.transaction import ModbusRtuFramer
66+
# client = ModbusClient('localhost', port=5020, framer=ModbusRtuFramer)
67+
# client = ModbusClient(method='binary', port='/dev/ptyp0', timeout=1)
68+
# client = ModbusClient(method='ascii', port='/dev/ptyp0', timeout=1)
69+
# client = ModbusClient(method='rtu', port='/dev/ptyp0', timeout=1,
70+
# baudrate=9600)
71+
client.connect()
72+
73+
# ------------------------------------------------------------------------#
74+
# specify slave to query
75+
# ------------------------------------------------------------------------#
76+
# The slave to query is specified in an optional parameter for each
77+
# individual request. This can be done by specifying the `unit` parameter
78+
# which defaults to `0x00`
79+
# ----------------------------------------------------------------------- #
80+
log.debug("Reading Device Information")
81+
information = {}
82+
rr = None
83+
84+
while not rr or rr.more_follows:
85+
next_object_id = rr.next_object_id if rr else 0
86+
rq = ReadDeviceInformationRequest(read_code=0x03, unit=UNIT,
87+
object_id=next_object_id)
88+
rr = client.execute(rq)
89+
information.update(rr.information)
90+
log.debug(rr)
91+
92+
print("Device Information : ")
93+
for key in information.keys():
94+
print(key, information[key])
95+
96+
# ----------------------------------------------------------------------- #
97+
# You can also have the information parsed through the
98+
# ModbusDeviceIdentificiation class, which gets you a more usable way
99+
# to access the Basic and Regular device information objects which are
100+
# specifically listed in the Modbus specification
101+
# ----------------------------------------------------------------------- #
102+
di = ModbusDeviceIdentification(info=information)
103+
print('Product Name : ', di.ProductName)
104+
105+
# ----------------------------------------------------------------------- #
106+
# close the client
107+
# ----------------------------------------------------------------------- #
108+
client.close()
109+
110+
111+
if __name__ == "__main__":
112+
run_sync_client()
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python
2+
"""
3+
Pymodbus Synchronous Server Example to showcase Device Information
4+
--------------------------------------------------------------------------
5+
6+
This server demonstrates the use of Device Information to provide information
7+
to clients about the device. This is part of the MODBUS specification, and
8+
uses the MEI 0x2B 0x0E request / response. This example creates an otherwise
9+
empty server.
10+
"""
11+
# --------------------------------------------------------------------------- #
12+
# import the various server implementations
13+
# --------------------------------------------------------------------------- #
14+
from pymodbus.server.sync import StartTcpServer
15+
from pymodbus.server.sync import StartUdpServer
16+
from pymodbus.server.sync import StartSerialServer
17+
18+
from pymodbus.device import ModbusDeviceIdentification
19+
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
20+
21+
from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer
22+
23+
# --------------------------------------------------------------------------- #
24+
# import versions of libraries which we will use later on for the example
25+
# --------------------------------------------------------------------------- #
26+
from pymodbus import __version__ as pymodbus_version
27+
from serial import __version__ as pyserial_version
28+
29+
# --------------------------------------------------------------------------- #
30+
# configure the service logging
31+
# --------------------------------------------------------------------------- #
32+
import logging
33+
FORMAT = ('%(asctime)-15s %(threadName)-15s'
34+
' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
35+
logging.basicConfig(format=FORMAT)
36+
log = logging.getLogger()
37+
log.setLevel(logging.DEBUG)
38+
39+
40+
def run_server():
41+
# ----------------------------------------------------------------------- #
42+
# initialize your data store
43+
# ----------------------------------------------------------------------- #
44+
store = ModbusSlaveContext()
45+
context = ModbusServerContext(slaves=store, single=True)
46+
47+
# ----------------------------------------------------------------------- #
48+
# initialize the server information
49+
# ----------------------------------------------------------------------- #
50+
# If you don't set this or any fields, they are defaulted to empty strings.
51+
# ----------------------------------------------------------------------- #
52+
identity = ModbusDeviceIdentification()
53+
identity.VendorName = 'Pymodbus'
54+
identity.ProductCode = 'PM'
55+
identity.VendorUrl = 'http://github.com/riptideio/pymodbus/'
56+
identity.ProductName = 'Pymodbus Server'
57+
identity.ModelName = 'Pymodbus Server'
58+
identity.MajorMinorRevision = '1.5'
59+
60+
# ----------------------------------------------------------------------- #
61+
# Add an example which is long enough to force the ReadDeviceInformation
62+
# request / response to require multiple responses to send back all of the
63+
# information.
64+
# ----------------------------------------------------------------------- #
65+
66+
identity[0x80] = "Lorem ipsum dolor sit amet, consectetur adipiscing " \
67+
"elit. Vivamus rhoncus massa turpis, sit amet " \
68+
"ultrices orci semper ut. Aliquam tristique sapien in " \
69+
"lacus pharetra, in convallis nunc consectetur. Nunc " \
70+
"velit elit, vehicula tempus tempus sed. "
71+
72+
# ----------------------------------------------------------------------- #
73+
# Add an example with repeated object IDs. The MODBUS specification is
74+
# entirely silent on whether or not this is allowed. In practice, this
75+
# should be assumed to be contrary to the MODBUS specification and other
76+
# clients (other than pymodbus) might behave differently when presented
77+
# with an object ID occurring twice in the returned information.
78+
#
79+
# Use this at your discretion, and at the very least ensure that all
80+
# objects which share a single object ID can fit together within a single
81+
# ADU unit. In the case of Modbus RTU, this is about 240 bytes or so. In
82+
# other words, when the spec says "An object is indivisible, therefore
83+
# any object must have a size consistent with the size of transaction
84+
# response", if you use repeated OIDs, apply that rule to the entire
85+
# grouping of objects with the repeated OID.
86+
# ----------------------------------------------------------------------- #
87+
identity[0x81] = ['pymodbus {0}'.format(pymodbus_version),
88+
'pyserial {0}'.format(pyserial_version)]
89+
90+
# ----------------------------------------------------------------------- #
91+
# run the server you want
92+
# ----------------------------------------------------------------------- #
93+
# Tcp:
94+
StartTcpServer(context, identity=identity, address=("localhost", 5020))
95+
96+
# TCP with different framer
97+
# StartTcpServer(context, identity=identity,
98+
# framer=ModbusRtuFramer, address=("0.0.0.0", 5020))
99+
100+
# Udp:
101+
# StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020))
102+
103+
# Ascii:
104+
# StartSerialServer(context, identity=identity,
105+
# port='/dev/ttyp0', timeout=1)
106+
107+
# RTU:
108+
# StartSerialServer(context, framer=ModbusRtuFramer, identity=identity,
109+
# port='/dev/ttyp0', timeout=.005, baudrate=9600)
110+
111+
# Binary
112+
# StartSerialServer(context,
113+
# identity=identity,
114+
# framer=ModbusBinaryFramer,
115+
# port='/dev/ttyp0',
116+
# timeout=1)
117+
118+
119+
if __name__ == "__main__":
120+
run_server()

pymodbus/mei_message.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@
1515
_MCB = ModbusControlBlock()
1616

1717

18+
class _OutOfSpaceException(Exception):
19+
# This exception exists here as a simple, local way to manage response
20+
# length control for the only MODBUS command which requires it under
21+
# standard, non-error conditions. It and the structures associated with
22+
# it should ideally be refactored and applied to all responses, however,
23+
# since a Client can make requests which result in disallowed conditions,
24+
# such as, for instance, requesting a register read of more registers
25+
# than will fit in a single PDU. As per the specification, the PDU is
26+
# restricted to 253 bytes, irrespective of the transport used.
27+
#
28+
# See Page 5/50 of MODBUS Application Protocol Specification V1.1b3.
29+
def __init__(self, oid):
30+
self.oid = oid
31+
32+
1833
#---------------------------------------------------------------------------#
1934
# Read Device Information
2035
#---------------------------------------------------------------------------#
@@ -114,31 +129,50 @@ def __init__(self, read_code=None, information=None, **kwargs):
114129
ModbusResponse.__init__(self, **kwargs)
115130
self.read_code = read_code or DeviceInformation.Basic
116131
self.information = information or {}
117-
self.number_of_objects = len(self.information)
118-
self.conformity = 0x83 # I support everything right now
119-
120-
# TODO calculate
121-
self.next_object_id = 0x00 # self.information[-1](0)
132+
self.number_of_objects = 0
133+
self.conformity = 0x83 # I support everything right now
134+
self.next_object_id = 0x00
122135
self.more_follows = MoreData.Nothing
136+
self.space_left = None
137+
138+
def _encode_object(self, object_id, data):
139+
self.space_left -= (2 + len(data))
140+
if self.space_left <= 0:
141+
raise _OutOfSpaceException(object_id)
142+
encoded_obj = struct.pack('>BB', object_id, len(data))
143+
if IS_PYTHON3:
144+
if isinstance(data, bytes):
145+
encoded_obj += data
146+
else:
147+
encoded_obj += data.encode()
148+
else:
149+
encoded_obj += data.encode()
150+
self.number_of_objects += 1
151+
return encoded_obj
123152

124153
def encode(self):
125154
''' Encodes the response
126155
127156
:returns: The byte encoded message
128157
'''
129-
packet = struct.pack('>BBBBBB', self.sub_function_code,
130-
self.read_code, self.conformity, self.more_follows,
131-
self.next_object_id, self.number_of_objects)
132-
133-
for (object_id, data) in iteritems(self.information):
134-
packet += struct.pack('>BB', object_id, len(data))
135-
if IS_PYTHON3:
136-
if isinstance(data, bytes):
137-
packet += data
158+
packet = struct.pack('>BBB', self.sub_function_code,
159+
self.read_code, self.conformity)
160+
self.space_left = 253 - 6
161+
objects = b''
162+
try:
163+
for (object_id, data) in iteritems(self.information):
164+
if isinstance(data, list):
165+
for item in data:
166+
objects += self._encode_object(object_id, item)
138167
else:
139-
packet += data.encode()
140-
else:
141-
packet += data.encode()
168+
objects += self._encode_object(object_id, data)
169+
except _OutOfSpaceException as e:
170+
self.next_object_id = e.oid
171+
self.more_follows = MoreData.KeepReading
172+
173+
packet += struct.pack('>BBB', self.more_follows, self.next_object_id,
174+
self.number_of_objects)
175+
packet += objects
142176
return packet
143177

144178
def decode(self, data):
@@ -155,7 +189,14 @@ def decode(self, data):
155189
while count < len(data):
156190
object_id, object_length = struct.unpack('>BB', data[count:count+2])
157191
count += object_length + 2
158-
self.information[object_id] = data[count-object_length:count]
192+
if object_id not in self.information.keys():
193+
self.information[object_id] = data[count-object_length:count]
194+
else:
195+
if isinstance(self.information[object_id], list):
196+
self.information[object_id].append(data[count-object_length:count])
197+
else:
198+
self.information[object_id] = [self.information[object_id],
199+
data[count - object_length:count]]
159200

160201
def __str__(self):
161202
''' Builds a representation of the response

0 commit comments

Comments
 (0)