Skip to content

Commit f3762bb

Browse files
authored
Add background parameter to servers. (#2529)
1 parent 4eccc75 commit f3762bb

File tree

9 files changed

+118
-128
lines changed

9 files changed

+118
-128
lines changed

pymodbus/client/base.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Base for all clients."""
22
from __future__ import annotations
33

4+
import asyncio
45
from abc import abstractmethod
56
from collections.abc import Awaitable, Callable
67

@@ -57,7 +58,9 @@ async def connect(self) -> bool:
5758
self.ctx.comm_params.host,
5859
self.ctx.comm_params.port,
5960
)
60-
return await self.ctx.connect()
61+
rc = await self.ctx.connect()
62+
await asyncio.sleep(0.1)
63+
return rc
6164

6265
def register(self, custom_response_class: type[ModbusPDU]) -> None:
6366
"""Register a custom response class with the decoder (call **sync**).

pymodbus/pdu/pdu.py

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import struct
66
from abc import abstractmethod
77

8+
from pymodbus.datastore import ModbusSlaveContext
89
from pymodbus.exceptions import NotImplementedException
910
from pymodbus.logging import Log
1011

@@ -79,6 +80,11 @@ def encode(self) -> bytes:
7980
def decode(self, data: bytes) -> None:
8081
"""Decode data part of the message."""
8182

83+
async def update_datastore(self, context: ModbusSlaveContext) -> ModbusPDU:
84+
"""Run request against a datastore."""
85+
_ = context
86+
return ExceptionResponse(0, 0)
87+
8288
@classmethod
8389
def calculateRtuFrameSize(cls, data: bytes) -> int:
8490
"""Calculate the size of a PDU."""

pymodbus/server/base.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import asyncio
55
from collections.abc import Callable
6+
from contextlib import suppress
67

78
from pymodbus.datastore import ModbusServerContext
89
from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification
@@ -67,25 +68,27 @@ async def shutdown(self):
6768
self.serving.set_result(True)
6869
self.close()
6970

70-
async def serve_forever(self):
71+
async def serve_forever(self, *, background: bool = False):
7172
"""Start endless loop."""
7273
if self.transport:
7374
raise RuntimeError(
7475
"Can't call serve_forever on an already running server object"
7576
)
7677
await self.listen()
7778
Log.info("Server listening.")
78-
await self.serving
79-
Log.info("Server graceful shutdown.")
79+
if not background:
80+
with suppress(asyncio.exceptions.CancelledError):
81+
await self.serving
82+
Log.info("Server graceful shutdown.")
8083

8184
def callback_connected(self) -> None:
8285
"""Call when connection is succcesfull."""
86+
raise RuntimeError("callback_new_connection should never be called")
8387

8488
def callback_disconnected(self, exc: Exception | None) -> None:
8589
"""Call when connection is lost."""
86-
Log.debug("callback_disconnected called: {}", exc)
90+
raise RuntimeError("callback_disconnected should never be called")
8791

8892
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
8993
"""Handle received data."""
90-
Log.debug("callback_data called: {} addr={}", data, ":hex", addr)
91-
return 0
94+
raise RuntimeError("callback_data should never be called")

pymodbus/server/requesthandler.py

+33-73
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import asyncio
55
import traceback
66

7-
from pymodbus.exceptions import NoSuchSlaveException
7+
from pymodbus.exceptions import ModbusIOException, NoSuchSlaveException
88
from pymodbus.logging import Log
99
from pymodbus.pdu.pdu import ExceptionResponse
1010
from pymodbus.transaction import TransactionManager
@@ -29,9 +29,6 @@ def __init__(self, owner):
2929
self.server = owner
3030
self.framer = self.server.framer(self.server.decoder)
3131
self.running = False
32-
self.handler_task = None # coroutine to be run on asyncio loop
33-
self.databuffer = b''
34-
self.loop = asyncio.get_running_loop()
3532
super().__init__(
3633
params,
3734
self.framer,
@@ -44,8 +41,7 @@ def __init__(self, owner):
4441

4542
def callback_new_connection(self) -> ModbusProtocol:
4643
"""Call when listener receive new connection request."""
47-
Log.debug("callback_new_connection called")
48-
return ServerRequestHandler(self)
44+
raise RuntimeError("callback_new_connection should never be called")
4945

5046
def callback_connected(self) -> None:
5147
"""Call when connection is succcesfull."""
@@ -54,27 +50,11 @@ def callback_connected(self) -> None:
5450
if self.server.broadcast_enable:
5551
if 0 not in slaves:
5652
slaves.append(0)
57-
try:
58-
self.running = True
59-
60-
# schedule the connection handler on the event loop
61-
self.handler_task = asyncio.create_task(self.handle())
62-
self.handler_task.set_name("server connection handler")
63-
except Exception as exc: # pylint: disable=broad-except
64-
Log.error(
65-
"Server callback_connected exception: {}; {}",
66-
exc,
67-
traceback.format_exc(),
68-
)
6953

7054
def callback_disconnected(self, call_exc: Exception | None) -> None:
7155
"""Call when connection is lost."""
7256
super().callback_disconnected(call_exc)
7357
try:
74-
if self.handler_task:
75-
self.handler_task.cancel()
76-
if hasattr(self.server, "on_connection_lost"):
77-
self.server.on_connection_lost()
7858
if call_exc is None:
7959
Log.debug(
8060
"Handler for stream [{}] has been canceled", self.comm_params.comm_name
@@ -93,66 +73,46 @@ def callback_disconnected(self, call_exc: Exception | None) -> None:
9373
traceback.format_exc(),
9474
)
9575

96-
async def handle(self) -> None:
97-
"""Coroutine which represents a single master <=> slave conversation.
98-
99-
Once the client connection is established, the data chunks will be
100-
fed to this coroutine via the asyncio.Queue object which is fed by
101-
the ServerRequestHandler class's callback Future.
102-
103-
This callback future gets data from either asyncio.BaseProtocol.data_received
104-
or asyncio.DatagramProtocol.datagram_received.
76+
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
77+
"""Handle received data."""
78+
try:
79+
used_len = super().callback_data(data, addr)
80+
except ModbusIOException:
81+
response = ExceptionResponse(
82+
40,
83+
exception_code=ExceptionResponse.ILLEGAL_FUNCTION
84+
)
85+
self.server_send(response, 0)
86+
return(len(data))
87+
if self.last_pdu:
88+
if self.is_server:
89+
self.loop.call_soon(self.handle_later)
90+
else:
91+
self.response_future.set_result(True)
92+
return used_len
10593

106-
This function will execute without blocking in the while-loop and
107-
yield to the asyncio event loop when the frame is exhausted.
108-
As a result, multiple clients can be interleaved without any
109-
interference between them.
110-
"""
111-
while self.running:
112-
try:
113-
pdu, *addr, exc = await self.server_execute()
114-
if exc:
115-
pdu = ExceptionResponse(
116-
40,
117-
exception_code=ExceptionResponse.ILLEGAL_FUNCTION
118-
)
119-
self.server_send(pdu, 0)
120-
continue
121-
await self.server_async_execute(pdu, *addr)
122-
except asyncio.CancelledError:
123-
# catch and ignore cancellation errors
124-
if self.running:
125-
Log.debug(
126-
"Handler for stream [{}] has been canceled", self.comm_params.comm_name
127-
)
128-
self.running = False
129-
except Exception as exc: # pylint: disable=broad-except
130-
# force TCP socket termination as framer
131-
# should handle application layer errors
132-
Log.error(
133-
'Unknown exception "{}" on stream {} forcing disconnect',
134-
exc,
135-
self.comm_params.comm_name,
136-
)
137-
self.close()
138-
self.callback_disconnected(exc)
94+
def handle_later(self):
95+
"""Change sync (async not allowed in call_soon) to async."""
96+
asyncio.run_coroutine_threadsafe(self.handle_request(), self.loop)
13997

140-
async def server_async_execute(self, request, *addr):
98+
async def handle_request(self):
14199
"""Handle request."""
142100
broadcast = False
101+
if not self.last_pdu:
102+
return
143103
try:
144-
if self.server.broadcast_enable and not request.dev_id:
104+
if self.server.broadcast_enable and not self.last_pdu.dev_id:
145105
broadcast = True
146106
# if broadcasting then execute on all slave contexts,
147107
# note response will be ignored
148108
for dev_id in self.server.context.slaves():
149-
response = await request.update_datastore(self.server.context[dev_id])
109+
response = await self.last_pdu.update_datastore(self.server.context[dev_id])
150110
else:
151-
context = self.server.context[request.dev_id]
152-
response = await request.update_datastore(context)
111+
context = self.server.context[self.last_pdu.dev_id]
112+
response = await self.last_pdu.update_datastore(context)
153113

154114
except NoSuchSlaveException:
155-
Log.error("requested slave does not exist: {}", request.dev_id)
115+
Log.error("requested slave does not exist: {}", self.last_pdu.dev_id)
156116
if self.server.ignore_missing_slaves:
157117
return # the client will simply timeout waiting for a response
158118
response = ExceptionResponse(0x00, ExceptionResponse.GATEWAY_NO_RESPONSE)
@@ -165,9 +125,9 @@ async def server_async_execute(self, request, *addr):
165125
response = ExceptionResponse(0x00, ExceptionResponse.SLAVE_FAILURE)
166126
# no response when broadcasting
167127
if not broadcast:
168-
response.transaction_id = request.transaction_id
169-
response.dev_id = request.dev_id
170-
self.server_send(response, *addr)
128+
response.transaction_id = self.last_pdu.transaction_id
129+
response.dev_id = self.last_pdu.dev_id
130+
self.server_send(response, self.last_addr)
171131

172132
def server_send(self, pdu, addr):
173133
"""Send message."""

pymodbus/server/startstop.py

+45-26
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import asyncio
55
import os
6-
from contextlib import suppress
76

87
from pymodbus.datastore import ModbusServerContext
98
from pymodbus.pdu import ModbusPDU
@@ -27,13 +26,13 @@ async def StartAsyncTcpServer( # pylint: disable=invalid-name
2726
:parameter context: Datastore object
2827
:parameter custom_functions: optional list of custom PDU objects
2928
:parameter kwargs: for parameter explanation see ModbusTcpServer
29+
30+
.. tip::
31+
Only handles a single server !
32+
33+
Use ModbusTcpServer to allow multiple servers in one app.
3034
"""
31-
server = ModbusTcpServer(context, **kwargs)
32-
if custom_functions:
33-
for func in custom_functions:
34-
server.decoder.register(func)
35-
with suppress(asyncio.exceptions.CancelledError):
36-
await server.serve_forever()
35+
await ModbusTcpServer(context, custom_pdu=custom_functions, **kwargs).serve_forever()
3736

3837

3938
def StartTcpServer( # pylint: disable=invalid-name
@@ -46,8 +45,13 @@ def StartTcpServer( # pylint: disable=invalid-name
4645
:parameter context: Datastore object
4746
:parameter custom_functions: optional list of custom PDU objects
4847
:parameter kwargs: for parameter explanation see ModbusTcpServer
48+
49+
.. tip::
50+
Only handles a single server !
51+
52+
Use ModbusTcpServer to allow multiple servers in one app.
4953
"""
50-
return asyncio.run(StartAsyncTcpServer(context, custom_functions=custom_functions, **kwargs))
54+
asyncio.run(StartAsyncTcpServer(context, custom_functions=custom_functions, **kwargs))
5155

5256

5357
async def StartAsyncTlsServer( # pylint: disable=invalid-name
@@ -60,13 +64,13 @@ async def StartAsyncTlsServer( # pylint: disable=invalid-name
6064
:parameter context: Datastore object
6165
:parameter custom_functions: optional list of custom PDU objects
6266
:parameter kwargs: for parameter explanation see ModbusTlsServer
67+
68+
.. tip::
69+
Only handles a single server !
70+
71+
Use ModbusTlsServer to allow multiple servers in one app.
6372
"""
64-
server = ModbusTlsServer(context, **kwargs)
65-
if custom_functions:
66-
for func in custom_functions:
67-
server.decoder.register(func)
68-
with suppress(asyncio.exceptions.CancelledError):
69-
await server.serve_forever()
73+
await ModbusTlsServer(context, custom_pdu=custom_functions, **kwargs).serve_forever()
7074

7175

7276
def StartTlsServer( # pylint: disable=invalid-name
@@ -79,6 +83,11 @@ def StartTlsServer( # pylint: disable=invalid-name
7983
:parameter context: Datastore object
8084
:parameter custom_functions: optional list of custom PDU objects
8185
:parameter kwargs: for parameter explanation see ModbusTlsServer
86+
87+
.. tip::
88+
Only handles a single server !
89+
90+
Use ModbusTlsServer to allow multiple servers in one app.
8291
"""
8392
asyncio.run(StartAsyncTlsServer(context, custom_functions=custom_functions, **kwargs))
8493

@@ -93,13 +102,13 @@ async def StartAsyncUdpServer( # pylint: disable=invalid-name
93102
:parameter context: Datastore object
94103
:parameter custom_functions: optional list of custom PDU objects
95104
:parameter kwargs: for parameter explanation see ModbusUdpServer
105+
106+
.. tip::
107+
Only handles a single server !
108+
109+
Use ModbusUdpServer to allow multiple servers in one app.
96110
"""
97-
server = ModbusUdpServer(context, **kwargs)
98-
if custom_functions:
99-
for func in custom_functions:
100-
server.decoder.register(func)
101-
with suppress(asyncio.exceptions.CancelledError):
102-
await server.serve_forever()
111+
await ModbusUdpServer(context, custom_pdu=custom_functions, **kwargs).serve_forever()
103112

104113

105114
def StartUdpServer( # pylint: disable=invalid-name
@@ -112,6 +121,11 @@ def StartUdpServer( # pylint: disable=invalid-name
112121
:parameter context: Datastore object
113122
:parameter custom_functions: optional list of custom PDU objects
114123
:parameter kwargs: for parameter explanation see ModbusUdpServer
124+
125+
.. tip::
126+
Only handles a single server !
127+
128+
Use ModbusUdpServer to allow multiple servers in one app.
115129
"""
116130
asyncio.run(StartAsyncUdpServer(context, custom_functions=custom_functions, **kwargs))
117131

@@ -126,13 +140,13 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name
126140
:parameter context: Datastore object
127141
:parameter custom_functions: optional list of custom PDU objects
128142
:parameter kwargs: for parameter explanation see ModbusSerialServer
143+
144+
.. tip::
145+
Only handles a single server !
146+
147+
Use ModbusSerialServer to allow multiple servers in one app.
129148
"""
130-
server = ModbusSerialServer(context, **kwargs)
131-
if custom_functions:
132-
for func in custom_functions:
133-
server.decoder.register(func)
134-
with suppress(asyncio.exceptions.CancelledError):
135-
await server.serve_forever()
149+
await ModbusSerialServer(context, custom_pdu=custom_functions, **kwargs).serve_forever()
136150

137151

138152
def StartSerialServer( # pylint: disable=invalid-name
@@ -145,6 +159,11 @@ def StartSerialServer( # pylint: disable=invalid-name
145159
:parameter context: Datastore object
146160
:parameter custom_functions: optional list of custom PDU objects
147161
:parameter kwargs: for parameter explanation see ModbusSerialServer
162+
163+
.. tip::
164+
Only handles a single server !
165+
166+
Use ModbusSerialServer to allow multiple servers in one app.
148167
"""
149168
asyncio.run(StartAsyncSerialServer(context, custom_functions=custom_functions, **kwargs))
150169

0 commit comments

Comments
 (0)