Skip to content

Commit e6da559

Browse files
memetbdhoomakethu
authored andcommitted
asyncio server implementation (#400)
* #357 Support registration of custom requests * #368 Fixes write to broadcast address When writing to broadcast address (unit_id=0) there should be no response according to the Modbus spec. This fix changes expected_response_length to 0 when writing to unit_id=0. This will break any existing code that is improperly using unit_id 0 for a slave address. * Bump version to 2.2.0 Fix #366 Update failures in sql context Update Changelog Fix major minor version in example codes * Fix #371 pymodbus repl on python3 * 1. Fix tornado async serial client `TypeError` while processing incoming packet. 2. Fix asyncio examples. 3. Minor update in factory.py, now server logs prints received request instead of only function cod * [fix v3] poprawa sprawdzania timeout * Release candidate for pymodbus 2.2.0 * Fix #377 when invalid port is supplied and minor updates in logging * #368 adds broadcast support for sync client and server Adds broadcast_enable parameter to client and server, default value is False. When true it will treat unit_id 0 as broadcast and execute requests on all server slave contexts and not send a response and on the client side will send the request and not try to receive a response. * #368 Fixes minor bug in broadcast support code * Fixed erronous CRC handling If the CRC recieved is not correct in my case my slave got caught in a deadlock, not taking any new requests. This addition fixed that. * Update Changelog * Fix test coverage * Fix #387 Transactions failing on 2.2.0rc2. * Task Cancellation and CRC Errors Alternate solution for #356 and #360. Changes the RTU to make the transaction ID as the unit ID instead of an ever incrementing number. Previously this transaction ID was always 0 on the receiving end but was the unique transaction ID on sending. As such the FIFO buffer made the most sense. By tying it to the unit ID, we can recover from failure modes such as: - - Asyncio task cancellations (eg. timeouts) #360 - Skipped responses from slaves. (hangs on master #360) - CRC Errors #356 - Busy response * Cherry pick commit from PR #367 , Update changelog , bump version to 2.2.0rc4 * native asyncio implementation of ModbusTcpServer and ModbusUdpServer * preliminary asyncio server examples * move serial module dependency into class instantiation * unittests for asyncio based server implementation * induce exception in execute method by mock patching the request object's execute method * move serial module dependency into class instantiation * added asynctest depency to requirements-tests.txt * add unittest skip condition for unsupported targets, remove failing assertion from unsupported targets, use lower asynctest version * remove logger setLevel call since doing so may override library consumers' already set log level * remove async def/await keywords from unittest so that the ast can be loaded in py2 even if the test is to be skipped
1 parent c645605 commit e6da559

File tree

5 files changed

+1354
-1
lines changed

5 files changed

+1354
-1
lines changed

examples/common/asyncio_server.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python
2+
"""
3+
Pymodbus Asyncio Server Example
4+
--------------------------------------------------------------------------
5+
6+
The asyncio server is implemented in pure python without any third
7+
party libraries (unless you need to use the serial protocols which require
8+
asyncio-pyserial). This is helpful in constrained or old environments where using
9+
twisted is just not feasible. What follows is an example of its use:
10+
"""
11+
# --------------------------------------------------------------------------- #
12+
# import the various server implementations
13+
# --------------------------------------------------------------------------- #
14+
from pymodbus.server.asyncio import StartTcpServer
15+
from pymodbus.server.asyncio import StartUdpServer
16+
from pymodbus.server.asyncio import StartSerialServer
17+
18+
from pymodbus.device import ModbusDeviceIdentification
19+
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSparseDataBlock
20+
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
21+
22+
from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer
23+
# --------------------------------------------------------------------------- #
24+
# configure the service 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+
34+
async def run_server():
35+
# ----------------------------------------------------------------------- #
36+
# initialize your data store
37+
# ----------------------------------------------------------------------- #
38+
# The datastores only respond to the addresses that they are initialized to
39+
# Therefore, if you initialize a DataBlock to addresses of 0x00 to 0xFF, a
40+
# request to 0x100 will respond with an invalid address exception. This is
41+
# because many devices exhibit this kind of behavior (but not all)::
42+
#
43+
# block = ModbusSequentialDataBlock(0x00, [0]*0xff)
44+
#
45+
# Continuing, you can choose to use a sequential or a sparse DataBlock in
46+
# your data context. The difference is that the sequential has no gaps in
47+
# the data while the sparse can. Once again, there are devices that exhibit
48+
# both forms of behavior::
49+
#
50+
# block = ModbusSparseDataBlock({0x00: 0, 0x05: 1})
51+
# block = ModbusSequentialDataBlock(0x00, [0]*5)
52+
#
53+
# Alternately, you can use the factory methods to initialize the DataBlocks
54+
# or simply do not pass them to have them initialized to 0x00 on the full
55+
# address range::
56+
#
57+
# store = ModbusSlaveContext(di = ModbusSequentialDataBlock.create())
58+
# store = ModbusSlaveContext()
59+
#
60+
# Finally, you are allowed to use the same DataBlock reference for every
61+
# table or you may use a separate DataBlock for each table.
62+
# This depends if you would like functions to be able to access and modify
63+
# the same data or not::
64+
#
65+
# block = ModbusSequentialDataBlock(0x00, [0]*0xff)
66+
# store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block)
67+
#
68+
# The server then makes use of a server context that allows the server to
69+
# respond with different slave contexts for different unit ids. By default
70+
# it will return the same context for every unit id supplied (broadcast
71+
# mode).
72+
# However, this can be overloaded by setting the single flag to False and
73+
# then supplying a dictionary of unit id to context mapping::
74+
#
75+
# slaves = {
76+
# 0x01: ModbusSlaveContext(...),
77+
# 0x02: ModbusSlaveContext(...),
78+
# 0x03: ModbusSlaveContext(...),
79+
# }
80+
# context = ModbusServerContext(slaves=slaves, single=False)
81+
#
82+
# The slave context can also be initialized in zero_mode which means that a
83+
# request to address(0-7) will map to the address (0-7). The default is
84+
# False which is based on section 4.4 of the specification, so address(0-7)
85+
# will map to (1-8)::
86+
#
87+
# store = ModbusSlaveContext(..., zero_mode=True)
88+
# ----------------------------------------------------------------------- #
89+
store = ModbusSlaveContext(
90+
di=ModbusSequentialDataBlock(0, [17]*100),
91+
co=ModbusSequentialDataBlock(0, [17]*100),
92+
hr=ModbusSequentialDataBlock(0, [17]*100),
93+
ir=ModbusSequentialDataBlock(0, [17]*100))
94+
95+
context = ModbusServerContext(slaves=store, single=True)
96+
97+
# ----------------------------------------------------------------------- #
98+
# initialize the server information
99+
# ----------------------------------------------------------------------- #
100+
# If you don't set this or any fields, they are defaulted to empty strings.
101+
# ----------------------------------------------------------------------- #
102+
identity = ModbusDeviceIdentification()
103+
identity.VendorName = 'Pymodbus'
104+
identity.ProductCode = 'PM'
105+
identity.VendorUrl = 'http://github.com/riptideio/pymodbus/'
106+
identity.ProductName = 'Pymodbus Server'
107+
identity.ModelName = 'Pymodbus Server'
108+
identity.MajorMinorRevision = '2.2.0'
109+
110+
# ----------------------------------------------------------------------- #
111+
# run the server you want
112+
# ----------------------------------------------------------------------- #
113+
# Tcp:
114+
# immediately start serving:
115+
server = await StartTcpServer(context, identity=identity, address=("0.0.0.0", 5020),
116+
allow_reuse_address=True)
117+
118+
# deferred start:
119+
# server = await StartTcpServer(context, identity=identity, address=("0.0.0.0", 5020),
120+
# allow_reuse_address=True, defer_start=True)
121+
122+
# asyncio.get_event_loop().call_later(20, lambda : server.)
123+
# await server.serve_forever()
124+
125+
126+
127+
128+
# TCP with different framer
129+
# StartTcpServer(context, identity=identity,
130+
# framer=ModbusRtuFramer, address=("0.0.0.0", 5020))
131+
132+
# Udp:
133+
# server = await StartUdpServer(context, identity=identity, address=("0.0.0.0", 5020),
134+
# allow_reuse_address=True, defer_start=True)
135+
#
136+
# await server.serve_forever()
137+
138+
139+
# !!! SERIAL SERVER NOT IMPLEMENTED !!!
140+
# Ascii:
141+
# StartSerialServer(context, identity=identity,
142+
# port='/dev/ttyp0', timeout=1)
143+
144+
# RTU:
145+
# StartSerialServer(context, framer=ModbusRtuFramer, identity=identity,
146+
# port='/dev/ttyp0', timeout=.005, baudrate=9600)
147+
148+
# Binary
149+
# StartSerialServer(context,
150+
# identity=identity,
151+
# framer=ModbusBinaryFramer,
152+
# port='/dev/ttyp0',
153+
# timeout=1)
154+
155+
156+
if __name__ == "__main__":
157+
asyncio.run(run_server())
158+

pymodbus/client/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import socket
22
import select
3-
import serial
43
import time
54
import sys
65
from functools import partial
@@ -425,6 +424,7 @@ def __init__(self, method='ascii', **kwargs):
425424
:param strict: Use Inter char timeout for baudrates <= 19200 (adhere
426425
to modbus standards)
427426
"""
427+
import serial
428428
self.method = method
429429
self.socket = None
430430
BaseModbusClient.__init__(self, self.__implementation(method, self),

0 commit comments

Comments
 (0)