Skip to content

Commit 7f788a4

Browse files
authored
Merge pull request #300 from riptideio/dev
pymodbus v1.5.1
2 parents 8f3fc71 + 36d5fd8 commit 7f788a4

13 files changed

+446
-36
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ test/__pycache__/
3030
/doc/sphinx/
3131
/doc/html/
3232
/doc/_build/
33+
.pytest_cache/

CHANGELOG.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
Version 1.5.1
2+
------------------------------------------------------------
3+
* Fix device information selectors
4+
* Fixed behaviour of the MEI device information command as a server when an invalid object_id is provided by an external client.
5+
* Add support for repeated MEI device information Object IDs (client/server)
6+
* Added support for encoding device information when it requires more than one PDU to pack.
7+
* Added REPR statements for all syncchronous clients
8+
* Added `isError` method to exceptions, Any response received can be tested for success before proceeding.
9+
10+
```
11+
res = client.read_holding_registers(...)
12+
if not res.isError():
13+
# proceed
14+
else:
15+
# handle error or raise
16+
```
17+
* Add examples for MEI read device information request
18+
119
Version 1.5.0
220
------------------------------------------------------------
321
* Improve transaction speeds for sync clients (RTU/ASCII), now retry on empty happens only when retry_on_empty kwarg is passed to client during intialization
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/client/sync.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ def __str__(self):
270270
"""
271271
return "ModbusTcpClient(%s:%s)" % (self.host, self.port)
272272

273+
def __repr__(self):
274+
return (
275+
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
276+
"port={self.port}, timeout={self.timeout}>"
277+
).format(self.__class__.__name__, hex(id(self)), self=self)
278+
279+
273280

274281
# --------------------------------------------------------------------------- #
275282
# Modbus UDP Client Transport Implementation
@@ -360,6 +367,11 @@ def __str__(self):
360367
"""
361368
return "ModbusUdpClient(%s:%s)" % (self.host, self.port)
362369

370+
def __repr__(self):
371+
return (
372+
"<{} at {} socket={self.socket}, ipaddr={self.host}, "
373+
"port={self.port}, timeout={self.timeout}>"
374+
).format(self.__class__.__name__, hex(id(self)), self=self)
363375

364376
# --------------------------------------------------------------------------- #
365377
# Modbus Serial Client Transport Implementation
@@ -527,6 +539,12 @@ def __str__(self):
527539
"""
528540
return "ModbusSerialClient(%s baud[%s])" % (self.method, self.baudrate)
529541

542+
def __repr__(self):
543+
return (
544+
"<{} at {} socket={self.socket}, method={self.method}, "
545+
"timeout={self.timeout}>"
546+
).format(self.__class__.__name__, hex(id(self)), self=self)
547+
530548
# --------------------------------------------------------------------------- #
531549
# Exported symbols
532550
# --------------------------------------------------------------------------- #

pymodbus/device.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def __init__(self, info=None):
220220
'''
221221
if isinstance(info, dict):
222222
for key in info:
223-
if (0x06 >= key >= 0x00) or (0x80 > key > 0x08):
223+
if (0x06 >= key >= 0x00) or (0xFF >= key >= 0x80):
224224
self.__data[key] = info[key]
225225

226226
def __iter__(self):
@@ -287,10 +287,14 @@ class DeviceInformationFactory(Singleton):
287287
'''
288288

289289
__lookup = {
290-
DeviceInformation.Basic: lambda c,r,i: c.__gets(r, list(range(0x00, 0x03))),
291-
DeviceInformation.Regular: lambda c,r,i: c.__gets(r, list(range(0x00, 0x08))),
292-
DeviceInformation.Extended: lambda c,r,i: c.__gets(r, list(range(0x80, i))),
293-
DeviceInformation.Specific: lambda c,r,i: c.__get(r, i),
290+
DeviceInformation.Basic: lambda c, r, i: c.__gets(r, list(range(i, 0x03))),
291+
DeviceInformation.Regular: lambda c, r, i: c.__gets(r, list(range(i, 0x07))
292+
if c.__get(r, i)[i] else list(range(0, 0x07))),
293+
DeviceInformation.Extended: lambda c, r, i: c.__gets(r,
294+
[x for x in range(i, 0x100) if x not in range(0x07, 0x80)]
295+
if c.__get(r, i)[i] else
296+
[x for x in range(0, 0x100) if x not in range(0x07, 0x80)]),
297+
DeviceInformation.Specific: lambda c, r, i: c.__get(r, i),
294298
}
295299

296300
@classmethod
@@ -323,7 +327,7 @@ def __gets(cls, identity, object_ids):
323327
:param object_ids: The specific object ids to read
324328
:returns: The requested data (id, length, value)
325329
'''
326-
return dict((oid, identity[oid]) for oid in object_ids)
330+
return dict((oid, identity[oid]) for oid in object_ids if identity[oid])
327331

328332

329333
#---------------------------------------------------------------------------#

pymodbus/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def __init__(self, string):
1818
def __str__(self):
1919
return 'Modbus Error: %s' % self.string
2020

21+
def isError(self):
22+
"""Error"""
23+
return True
24+
2125

2226
class ModbusIOException(ModbusException):
2327
''' Error resulting from data i/o '''

0 commit comments

Comments
 (0)