Skip to content

Commit 4b57def

Browse files
committed
Fix REPL server documentation, support multiple unit id's, handle response manipulators in serial servers
1 parent e88c41d commit 4b57def

File tree

7 files changed

+45
-15
lines changed

7 files changed

+45
-15
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ version 2.5.0
88
* Fix sync client and processing of incomplete frames with rtu framers
99
* Support synchronous diagnostic client (TCP)
1010
* Server updates (REPL and async)
11+
* Handle Memory leak in sync servers due to socketserver memory leak
1112

1213
version 2.5.0rc3
1314
----------------------------------------------------------

pymodbus/repl/server/cli.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,24 @@
4545
"help": None,
4646
"clear": None
4747
}
48-
USAGE = "manipulate response_type=|normal|error|delayed| " \
49-
"error_code=<int> delay_by=<in seconds>"
48+
USAGE = "manipulator response_type=|normal|error|delayed|empty|stray \n" \
49+
"\tAdditional parameters\n" \
50+
"\t\terror_code=<int> \n\t\tdelay_by=<in seconds> \n\t\t" \
51+
"clear_after=<clear after n messages int>" \
52+
"\n\t\tdata_len=<length of stray data (int)>\n" \
53+
"\n\tExample usage: \n\t" \
54+
"1. Send error response 3 for 4 requests\n\t" \
55+
" <ansiblue>manipulator response_type=error error_code=3 clear_after=4</ansiblue>\n\t" \
56+
"2. Delay outgoing response by 5 seconds indefinitely\n\t" \
57+
" <ansiblue>manipulator response_type=delayed delay_by=5</ansiblue>\n\t" \
58+
"3. Send empty response\n\t" \
59+
" <ansiblue>manipulator response_type=empty</ansiblue>\n\t" \
60+
"4. Send stray response of lenght 12 and revert to normal after 2 responses\n\t" \
61+
" <ansiblue>manipulator response_type=stray data_len=11 clear_after=2</ansiblue>\n\t" \
62+
"5. To disable response manipulation\n\t" \
63+
" <ansiblue>manipulator response_type=normal</ansiblue>"
5064
COMMAND_HELPS = {
51-
"manipulator": "Manipulate response from server.\nUsage: '{}'".format(USAGE),
65+
"manipulator": "Manipulate response from server.\nUsage: {}".format(USAGE),
5266
"clear": "Clears screen"
5367

5468
}

pymodbus/repl/server/main.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
import asyncio
66
import json
77
import click
8-
from pymodbus.utilities import IS_PYTHON3
8+
from pymodbus.compat import IS_PYTHON3, PYTHON_VERSION
99
from pymodbus.framer.socket_framer import ModbusSocketFramer
1010
from pymodbus.server.reactive.main import (
1111
ReactiveServer, DEFAULT_FRAMER, DEFUALT_HANDLERS)
1212
from pymodbus.server.reactive.default_config import DEFUALT_CONFIG
1313
from pymodbus.repl.server.cli import run_repl
1414

15+
if IS_PYTHON3 and PYTHON_VERSION > (3, 7):
16+
CANCELLED_ERROR = asyncio.exceptions.CancelledError
17+
else:
18+
CANCELLED_ERROR = asyncio.CancelledError
19+
1520

1621
@click.group("ReactiveModbusServer")
1722
@click.option("--host", default="localhost", help="Host address")
@@ -38,7 +43,7 @@ def server(ctx, host, web_port, broadcast_support, repl, verbose):
3843
pymodbus_logger.setLevel(logging.ERROR)
3944
logger.setLevel(logging.ERROR)
4045

41-
ctx.obj = {"repl": repl, "host": host, "port": web_port,
46+
ctx.obj = {"repl": repl, "host": host, "web_port": web_port,
4247
"broadcast": broadcast_support}
4348

4449

@@ -52,11 +57,17 @@ def server(ctx, host, web_port, broadcast_support, repl, verbose):
5257
case_sensitive=False),
5358
help="Modbus framer to use")
5459
@click.option("--modbus-port", default="5020", help="Modbus port")
55-
@click.option("--modbus-unit-id", default=1, help="Modbus unit id")
60+
@click.option("--modbus-unit-id", default=[1], multiple=True, help="Modbus unit id")
5661
@click.option("--modbus-config", type=click.Path(exists=True),
5762
help="Path to additional modbus server config")
63+
@click.option("-r", "--randomize", default=0, help="Randomize every `r` reads."
64+
" 0=never, 1=always, "
65+
"2=every-second-read, "
66+
"and so on. "
67+
"Applicable IR and DI.")
5868
@click.pass_context
59-
def run(ctx, modbus_server, modbus_framer, modbus_port, modbus_unit_id, modbus_config):
69+
def run(ctx, modbus_server, modbus_framer, modbus_port, modbus_unit_id,
70+
modbus_config, randomize):
6071
"""
6172
Run Reactive Modbus server exposing REST endpoint
6273
for response manipulation.
@@ -82,6 +93,7 @@ def run(ctx, modbus_server, modbus_framer, modbus_port, modbus_unit_id, modbus_c
8293
handler = DEFUALT_HANDLERS.get(handler.strip())
8394

8495
modbus_config["handler"] = handler
96+
modbus_config["randomize"] = randomize
8597
app = ReactiveServer.factory(modbus_server, framer,
8698
modbus_port=modbus_port,
8799
unit=modbus_unit_id,
@@ -96,7 +108,7 @@ def run(ctx, modbus_server, modbus_framer, modbus_port, modbus_unit_id, modbus_c
96108
else:
97109
app.run()
98110

99-
except asyncio.exceptions.CancelledError:
111+
except CANCELLED_ERROR:
100112
print("Done!!!!!")
101113

102114

pymodbus/server/async_io.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ def _log_exception(self):
5656
"been canceled" % self.client_address[:2])
5757
elif isinstance(self, ModbusSingleRequestHandler):
5858
_logger.error(
59-
"Handler for serial port [%s] has been "
60-
"cancelled" % self.transport.serial.port)
59+
"Handler for serial port has been cancelled")
6160
else:
6261
sock_name = self.protocol._sock.getsockname()
6362
_logger.error("Handler for UDP socket [%s] has "

pymodbus/server/reactive/main.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"ModbusDisconnectedRequestHandler": ModbusDisconnectedRequestHandler
7272
}
7373
DEFAULT_MODBUS_MAP = {"start_offset": 0,
74-
"count": 10, "value": 0, "sparse": False}
74+
"count": 100,
75+
"value": 0, "sparse": False}
7576
DEFAULT_DATA_BLOCK = {
7677
"co": DEFAULT_MODBUS_MAP,
7778
"di": DEFAULT_MODBUS_MAP,
@@ -318,7 +319,9 @@ def create_context(cls, data_block=None, unit=1,
318319

319320
slave_context = ModbusSlaveContext(**block, zero_mode=True)
320321
if not single:
321-
slaves = {unit: slave_context}
322+
slaves = {}
323+
for i in unit:
324+
slaves[i] = slave_context
322325
else:
323326
slaves = slave_context
324327
server_context = ModbusServerContext(slaves, single=single)

pymodbus/server/sync.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,10 @@ def serve_forever(self):
581581
if not self.handler:
582582
self._build_handler()
583583
while self.is_running:
584-
self.handler.response_manipulator()
584+
if hasattr(self.handler, "response_manipulator"):
585+
self.handler.response_manipulator()
586+
else:
587+
self.handler.handle()
585588
else:
586589
_logger.error("Error opening serial port , "
587590
"Unable to start server!!")

pymodbus/transaction.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ def execute(self, request):
217217
"/Unable to decode response")
218218
response = ModbusIOException(last_exception,
219219
request.function_code)
220-
self.client.close()
221220
if hasattr(self.client, "state"):
222221
_logger.debug("Changing transaction state from "
223222
"'PROCESSING REPLY' to "
@@ -228,7 +227,6 @@ def execute(self, request):
228227
return response
229228
except ModbusIOException as ex:
230229
# Handle decode errors in processIncomingPacket method
231-
self.client.close()
232230
_logger.exception(ex)
233231
self.client.close()
234232
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE

0 commit comments

Comments
 (0)