Skip to content

Commit c410f5b

Browse files
committed
Merge branch '#357-Custom-Function' into pymodbus-2.2.0
2 parents 2e169b6 + a2d79e1 commit c410f5b

16 files changed

+577
-154
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ from pymodbus.client.asynchronous import ModbusTcpClient
1111
* Fix SQLDbcontext always returning InvalidAddress error.
1212
* Fix SQLDbcontext update failure
1313
* Fix Binary payload example for endianess.
14+
* Add support to register custom requests in clients and server instances.
15+
1416

1517
Version 2.1.0
1618
-----------------------------------------------------------

examples/common/asynchronous_server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pymodbus.transaction import (ModbusRtuFramer,
2121
ModbusAsciiFramer,
2222
ModbusBinaryFramer)
23+
from custom_message import CustomModbusRequest
2324

2425
# --------------------------------------------------------------------------- #
2526
# configure the service logging
@@ -92,6 +93,8 @@ def run_async_server():
9293
co=ModbusSequentialDataBlock(0, [17]*100),
9394
hr=ModbusSequentialDataBlock(0, [17]*100),
9495
ir=ModbusSequentialDataBlock(0, [17]*100))
96+
store.register(CustomModbusRequest.function_code, 'cm',
97+
ModbusSequentialDataBlock(0, [17] * 100))
9598
context = ModbusServerContext(slaves=store, single=True)
9699

97100
# ----------------------------------------------------------------------- #
@@ -113,7 +116,8 @@ def run_async_server():
113116

114117
# TCP Server
115118

116-
StartTcpServer(context, identity=identity, address=("localhost", 5020))
119+
StartTcpServer(context, identity=identity, address=("localhost", 5020),
120+
custom_functions=[CustomModbusRequest])
117121

118122
# TCP Server with deferred reactor run
119123

examples/common/custom_message.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pymodbus.pdu import ModbusRequest, ModbusResponse, ModbusExceptions
2121
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
2222
from pymodbus.bit_read_message import ReadCoilsRequest
23+
from pymodbus.compat import int2byte, byte2int
2324
# --------------------------------------------------------------------------- #
2425
# configure the client logging
2526
# --------------------------------------------------------------------------- #
@@ -40,15 +41,41 @@
4041

4142

4243
class CustomModbusResponse(ModbusResponse):
43-
pass
44+
function_code = 55
45+
_rtu_byte_count_pos = 2
46+
47+
def __init__(self, values=None, **kwargs):
48+
ModbusResponse.__init__(self, **kwargs)
49+
self.values = values or []
50+
51+
def encode(self):
52+
""" Encodes response pdu
53+
54+
:returns: The encoded packet message
55+
"""
56+
result = int2byte(len(self.values) * 2)
57+
for register in self.values:
58+
result += struct.pack('>H', register)
59+
return result
60+
61+
def decode(self, data):
62+
""" Decodes response pdu
63+
64+
:param data: The packet data to decode
65+
"""
66+
byte_count = byte2int(data[0])
67+
self.values = []
68+
for i in range(1, byte_count + 1, 2):
69+
self.values.append(struct.unpack('>H', data[i:i + 2])[0])
4470

4571

4672
class CustomModbusRequest(ModbusRequest):
4773

48-
function_code = 1
74+
function_code = 55
75+
_rtu_frame_size = 8
4976

50-
def __init__(self, address):
51-
ModbusRequest.__init__(self)
77+
def __init__(self, address=None, **kwargs):
78+
ModbusRequest.__init__(self, **kwargs)
5279
self.address = address
5380
self.count = 16
5481

@@ -74,12 +101,12 @@ def execute(self, context):
74101

75102
class Read16CoilsRequest(ReadCoilsRequest):
76103

77-
def __init__(self, address):
104+
def __init__(self, address, **kwargs):
78105
""" Initializes a new instance
79106
80107
:param address: The address to start reading from
81108
"""
82-
ReadCoilsRequest.__init__(self, address, 16)
109+
ReadCoilsRequest.__init__(self, address, 16, **kwargs)
83110

84111
# --------------------------------------------------------------------------- #
85112
# execute the request with your client
@@ -90,7 +117,8 @@ def __init__(self, address):
90117

91118

92119
if __name__ == "__main__":
93-
with ModbusClient('127.0.0.1') as client:
94-
request = CustomModbusRequest(0)
120+
with ModbusClient(host='localhost', port=5020) as client:
121+
client.register(CustomModbusResponse)
122+
request = CustomModbusRequest(1, unit=1)
95123
result = client.execute(request)
96-
print(result)
124+
print(result.values)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env python
2+
"""
3+
Pymodbus Synchronous Client Examples
4+
--------------------------------------------------------------------------
5+
6+
The following is an example of how to use the synchronous modbus client
7+
implementation from pymodbus.
8+
9+
It should be noted that the client can also be used with
10+
the guard construct that is available in python 2.5 and up::
11+
12+
with ModbusClient('127.0.0.1') as client:
13+
result = client.read_coils(1,10)
14+
print result
15+
"""
16+
import struct
17+
# --------------------------------------------------------------------------- #
18+
# import the various server implementations
19+
# --------------------------------------------------------------------------- #
20+
from pymodbus.pdu import ModbusRequest, ModbusResponse, ModbusExceptions
21+
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
22+
from pymodbus.bit_read_message import ReadCoilsRequest
23+
from pymodbus.compat import int2byte, byte2int
24+
# --------------------------------------------------------------------------- #
25+
# configure the client logging
26+
# --------------------------------------------------------------------------- #
27+
import logging
28+
logging.basicConfig()
29+
log = logging.getLogger()
30+
log.setLevel(logging.DEBUG)
31+
32+
# --------------------------------------------------------------------------- #
33+
# create your custom message
34+
# --------------------------------------------------------------------------- #
35+
# The following is simply a read coil request that always reads 16 coils.
36+
# Since the function code is already registered with the decoder factory,
37+
# this will be decoded as a read coil response. If you implement a new
38+
# method that is not currently implemented, you must register the request
39+
# and response with a ClientDecoder factory.
40+
# --------------------------------------------------------------------------- #
41+
42+
43+
class CustomModbusResponse(ModbusResponse):
44+
function_code = 55
45+
_rtu_byte_count_pos = 2
46+
47+
def __init__(self, values=None, **kwargs):
48+
ModbusResponse.__init__(self, **kwargs)
49+
self.values = values or []
50+
51+
def encode(self):
52+
""" Encodes response pdu
53+
54+
:returns: The encoded packet message
55+
"""
56+
result = int2byte(len(self.values) * 2)
57+
for register in self.values:
58+
result += struct.pack('>H', register)
59+
return result
60+
61+
def decode(self, data):
62+
""" Decodes response pdu
63+
64+
:param data: The packet data to decode
65+
"""
66+
byte_count = byte2int(data[0])
67+
self.values = []
68+
for i in range(1, byte_count + 1, 2):
69+
self.values.append(struct.unpack('>H', data[i:i + 2])[0])
70+
71+
72+
class CustomModbusRequest(ModbusRequest):
73+
74+
function_code = 55
75+
_rtu_frame_size = 8
76+
77+
def __init__(self, address=None, **kwargs):
78+
ModbusRequest.__init__(self, **kwargs)
79+
self.address = address
80+
self.count = 16
81+
82+
def encode(self):
83+
return struct.pack('>HH', self.address, self.count)
84+
85+
def decode(self, data):
86+
self.address, self.count = struct.unpack('>HH', data)
87+
88+
def execute(self, context):
89+
if not (1 <= self.count <= 0x7d0):
90+
return self.doException(ModbusExceptions.IllegalValue)
91+
if not context.validate(self.function_code, self.address, self.count):
92+
return self.doException(ModbusExceptions.IllegalAddress)
93+
values = context.getValues(self.function_code, self.address,
94+
self.count)
95+
return CustomModbusResponse(values)
96+
97+
# --------------------------------------------------------------------------- #
98+
# This could also have been defined as
99+
# --------------------------------------------------------------------------- #
100+
101+
102+
class Read16CoilsRequest(ReadCoilsRequest):
103+
104+
def __init__(self, address, **kwargs):
105+
""" Initializes a new instance
106+
107+
:param address: The address to start reading from
108+
"""
109+
ReadCoilsRequest.__init__(self, address, 16, **kwargs)
110+
111+
# --------------------------------------------------------------------------- #
112+
# execute the request with your client
113+
# --------------------------------------------------------------------------- #
114+
# using the with context, the client will automatically be connected
115+
# and closed when it leaves the current scope.
116+
# --------------------------------------------------------------------------- #
117+
118+
119+
if __name__ == "__main__":
120+
with ModbusClient(host='localhost', port=5020) as client:
121+
client.register(CustomModbusResponse)
122+
request = CustomModbusRequest(1, unit=1)
123+
result = client.execute(request)
124+
print(result.values)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python
2+
"""
3+
Pymodbus Synchronous Server Example with Custom functions
4+
--------------------------------------------------------------------------
5+
6+
Implements a custom function code not in standard modbus function code list
7+
and its response which otherwise would throw `IllegalFunction (0x1)` error.
8+
9+
Steps:
10+
1. Create CustomModbusRequest class derived from ModbusRequest
11+
```class CustomModbusRequest(ModbusRequest):
12+
function_code = 75 # Value less than 0x80)
13+
_rtu_frame_size = <some_value> # Required only For RTU support
14+
15+
def __init__(custom_arg=None, **kwargs)
16+
# Make sure the arguments has default values, will error out
17+
# while decoding otherwise
18+
ModbusRequest.__init__(self, **kwargs)
19+
self.custom_request_arg = custom_arg
20+
21+
def encode(self):
22+
# Implement encoding logic
23+
24+
def decode(self, data):
25+
# implement decoding logic
26+
27+
def execute(self, context):
28+
# Implement execute logic
29+
...
30+
return CustomModbusResponse(..)
31+
32+
```
33+
2. Create CustomModbusResponse class derived from ModbusResponse
34+
```class CustomModbusResponse(ModbusResponse):
35+
function_code = 75 # Value less than 0x80)
36+
_rtu_byte_count_pos = <some_value> # Required only For RTU support
37+
38+
def __init__(self, custom_args=None, **kwargs):
39+
# Make sure the arguments has default values, will error out
40+
# while decoding otherwise
41+
ModbusResponse.__init__(self, **kwargs)
42+
self.custom_reponse_values = values
43+
44+
def encode(self):
45+
# Implement encoding logic
46+
def decode(self, data):
47+
# Implement decoding logic
48+
```
49+
3. Register with ModbusSlaveContext,
50+
if your request has to access some values from the data-store.
51+
```store = ModbusSlaveContext(...)
52+
store.register(CustomModbusRequest.function_code, 'dummy_context_name')
53+
```
54+
4. Pass CustomModbusRequest class as argument to Start<protocol>Server
55+
```
56+
StartTcpServer(..., custom_functions=[CustomModbusRequest]..)
57+
```
58+
59+
"""
60+
# --------------------------------------------------------------------------- #
61+
# import the various server implementations
62+
# --------------------------------------------------------------------------- #
63+
from pymodbus.server.sync import StartTcpServer
64+
65+
from pymodbus.device import ModbusDeviceIdentification
66+
from pymodbus.datastore import ModbusSequentialDataBlock
67+
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
68+
from custom_message import CustomModbusRequest
69+
70+
# --------------------------------------------------------------------------- #
71+
# configure the service logging
72+
# --------------------------------------------------------------------------- #
73+
import logging
74+
75+
FORMAT = ('%(asctime)-15s %(threadName)-15s'
76+
' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
77+
logging.basicConfig(format=FORMAT)
78+
log = logging.getLogger()
79+
log.setLevel(logging.DEBUG)
80+
81+
82+
def run_server():
83+
store = ModbusSlaveContext(
84+
di=ModbusSequentialDataBlock(0, [17] * 100),
85+
co=ModbusSequentialDataBlock(0, [17] * 100),
86+
hr=ModbusSequentialDataBlock(0, [17] * 100),
87+
ir=ModbusSequentialDataBlock(0, [17] * 100))
88+
89+
store.register(CustomModbusRequest.function_code, 'cm',
90+
ModbusSequentialDataBlock(0, [17] * 100))
91+
context = ModbusServerContext(slaves=store, single=True)
92+
93+
# ----------------------------------------------------------------------- #
94+
# initialize the server information
95+
# ----------------------------------------------------------------------- #
96+
# If you don't set this or any fields, they are defaulted to empty strings.
97+
# ----------------------------------------------------------------------- #
98+
identity = ModbusDeviceIdentification()
99+
identity.VendorName = 'Pymodbus'
100+
identity.ProductCode = 'PM'
101+
identity.VendorUrl = 'http://github.com/riptideio/pymodbus/'
102+
identity.ProductName = 'Pymodbus Server'
103+
identity.ModelName = 'Pymodbus Server'
104+
identity.MajorMinorRevision = '2.1.0'
105+
106+
# ----------------------------------------------------------------------- #
107+
# run the server you want
108+
# ----------------------------------------------------------------------- #
109+
# Tcp:
110+
StartTcpServer(context, identity=identity, address=("localhost", 5020),
111+
custom_functions=[CustomModbusRequest])
112+
113+
114+
if __name__ == "__main__":
115+
run_server()
116+

pymodbus/client/sync.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ def _dump(self, data, direction):
157157
self._logger.debug(hexlify_packets(data))
158158
self._logger.exception(e)
159159

160+
def register(self, function):
161+
"""
162+
Registers a function and sub function class with the decoder
163+
:param function: Custom function class to register
164+
:return:
165+
"""
166+
self.framer.decoder.register(function)
167+
160168
def __str__(self):
161169
""" Builds a string representation of the connection
162170

pymodbus/datastore/context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ def setValues(self, fx, address, values):
8888
_logger.debug("setValues[%d] %d:%d" % (fx, address, len(values)))
8989
self.store[self.decode(fx)].setValues(address, values)
9090

91+
def register(self, fc, fx, datablock=None):
92+
"""
93+
Registers a datablock with the slave context
94+
:param fc: function code (int)
95+
:param fx: string representation of function code (e.g 'cf' )
96+
:param datablock: datablock to associate with this function code
97+
:return:
98+
"""
99+
self.store[fx] = datablock or ModbusSequentialDataBlock.create()
100+
self._IModbusSlaveContext__fx_mapper[fc] = fx
101+
91102

92103
class ModbusServerContext(object):
93104
''' This represents a master collection of slave contexts.

0 commit comments

Comments
 (0)