Skip to content

Commit 60aca50

Browse files
committed
#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.
1 parent 249ad8f commit 60aca50

File tree

4 files changed

+112
-69
lines changed

4 files changed

+112
-69
lines changed

pymodbus/client/sync.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self, framer, **kwargs):
4545
self.transaction = FifoTransactionManager(self, **kwargs)
4646
self._debug = False
4747
self._debugfd = None
48+
self.broadcast_enable = kwargs.get('broadcast_enable', Defaults.broadcast_enable)
4849

4950
# ----------------------------------------------------------------------- #
5051
# Client interface

pymodbus/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ class Defaults(Singleton):
8888
should be returned or simply ignored. This is useful for the case of a
8989
serial server emulater where a request to a non-existant slave on a bus
9090
will never respond. The client in this case will simply timeout.
91+
92+
.. attribute:: broadcast_enable
93+
94+
When False unit_id 0 will be treated as any other unit_id. When True and
95+
the unit_id is 0 the server will execute all requests on all server
96+
contexts and not respond and the client will skip trying to receive a
97+
response. Default value False does not conform to Modbus spec but maintains
98+
legacy behavior for existing pymodbus users.
99+
91100
'''
92101
Port = 502
93102
Retries = 3
@@ -104,6 +113,7 @@ class Defaults(Singleton):
104113
ZeroMode = False
105114
IgnoreMissingSlaves = False
106115
ReadSize = 1024
116+
broadcast_enable = False
107117

108118
class ModbusStatus(Singleton):
109119
'''

pymodbus/server/sync.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,15 @@ def execute(self, request):
5959
:param request: The decoded request message
6060
"""
6161
try:
62-
context = self.server.context[request.unit_id]
63-
response = request.execute(context)
62+
if self.server.broadcast_enable and request.unit_id == 0:
63+
broadcast = True
64+
# if broadcasting then execute on all slave contexts, note response will be ignored
65+
for unit_id in self.server.context.slaves():
66+
response = request.execute(self.server.context[unit_id])
67+
else:
68+
broadcast = False
69+
context = self.server.context[request.unit_id]
70+
response = request.execute(context)
6471
except NoSuchSlaveException as ex:
6572
_logger.debug("requested slave does "
6673
"not exist: %s" % request.unit_id )
@@ -71,9 +78,11 @@ def execute(self, request):
7178
_logger.debug("Datastore unable to fulfill request: "
7279
"%s; %s", ex, traceback.format_exc())
7380
response = request.doException(merror.SlaveFailure)
74-
response.transaction_id = request.transaction_id
75-
response.unit_id = request.unit_id
76-
self.send(response)
81+
# no response when broadcasting
82+
if not broadcast:
83+
response.transaction_id = request.transaction_id
84+
response.unit_id = request.unit_id
85+
self.send(response)
7786

7887
# ----------------------------------------------------------------------- #
7988
# Base class implementations
@@ -105,6 +114,12 @@ def handle(self):
105114
data = self.request.recv(1024)
106115
if data:
107116
units = self.server.context.slaves()
117+
if not isinstance(units, (list, tuple)):
118+
units = [units]
119+
# if broadcast is enabled make sure to process requests to address 0
120+
if self.server.broadcast_enable:
121+
if 0 not in units:
122+
units.append(0)
108123
single = self.server.context.single
109124
self.framer.processIncomingPacket(data, self.execute,
110125
units, single=single)
@@ -288,8 +303,10 @@ def __init__(self, context, framer=None, identity=None,
288303
ModbusConnectedRequestHandler
289304
:param allow_reuse_address: Whether the server will allow the
290305
reuse of an address.
291-
:param ignore_missing_slaves: True to not send errors on a request
292-
to a missing slave
306+
:param ignore_missing_slaves: True to not send errors on a request
307+
to a missing slave
308+
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
309+
False to treat 0 as any other unit_id
293310
"""
294311
self.threads = []
295312
self.allow_reuse_address = allow_reuse_address
@@ -301,6 +318,8 @@ def __init__(self, context, framer=None, identity=None,
301318
self.handler = handler or ModbusConnectedRequestHandler
302319
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
303320
Defaults.IgnoreMissingSlaves)
321+
self.broadcast_enable = kwargs.get('broadcast_enable',
322+
Defaults.broadcast_enable)
304323

305324
if isinstance(identity, ModbusDeviceIdentification):
306325
self.control.Identity.update(identity)
@@ -358,7 +377,9 @@ def __init__(self, context, framer=None, identity=None, address=None,
358377
:param handler: A handler for each client session; default is
359378
ModbusDisonnectedRequestHandler
360379
:param ignore_missing_slaves: True to not send errors on a request
361-
to a missing slave
380+
to a missing slave
381+
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
382+
False to treat 0 as any other unit_id
362383
"""
363384
self.threads = []
364385
self.decoder = ServerDecoder()
@@ -369,6 +390,8 @@ def __init__(self, context, framer=None, identity=None, address=None,
369390
self.handler = handler or ModbusDisconnectedRequestHandler
370391
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
371392
Defaults.IgnoreMissingSlaves)
393+
self.broadcast_enable = kwargs.get('broadcast_enable',
394+
Defaults.broadcast_enable)
372395

373396
if isinstance(identity, ModbusDeviceIdentification):
374397
self.control.Identity.update(identity)
@@ -423,7 +446,9 @@ def __init__(self, context, framer=None, identity=None, **kwargs):
423446
:param baudrate: The baud rate to use for the serial device
424447
:param timeout: The timeout to use for the serial device
425448
:param ignore_missing_slaves: True to not send errors on a request
426-
to a missing slave
449+
to a missing slave
450+
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
451+
False to treat 0 as any other unit_id
427452
"""
428453
self.threads = []
429454
self.decoder = ServerDecoder()
@@ -442,6 +467,8 @@ def __init__(self, context, framer=None, identity=None, **kwargs):
442467
self.timeout = kwargs.get('timeout', Defaults.Timeout)
443468
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
444469
Defaults.IgnoreMissingSlaves)
470+
self.broadcast_enable = kwargs.get('broadcast_enable',
471+
Defaults.broadcast_enable)
445472
self.socket = None
446473
if self._connect():
447474
self.is_running = True

pymodbus/transaction.py

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -118,53 +118,53 @@ def execute(self, request):
118118
_logger.debug("Clearing current Frame : - {}".format(_buffer))
119119
self.client.framer.resetFrame()
120120

121-
expected_response_length = None
122-
if not isinstance(self.client.framer, ModbusSocketFramer):
123-
if hasattr(request, "get_response_pdu_size"):
124-
response_pdu_size = request.get_response_pdu_size()
125-
if isinstance(self.client.framer, ModbusAsciiFramer):
126-
response_pdu_size = response_pdu_size * 2
127-
if response_pdu_size:
128-
expected_response_length = self._calculate_response_length(response_pdu_size)
129-
if request.unit_id == 0:
130-
full = True
131-
expected_response_length = 0
132-
elif request.unit_id in self._no_response_devices:
133-
full = True
121+
if request.unit_id == 0 and self.client.broadcast_enable:
122+
response, last_exception = self._transact(request, None)
123+
response = b'Broadcast write sent - no response expected'
134124
else:
135-
full = False
136-
c_str = str(self.client)
137-
if "modbusudpclient" in c_str.lower().strip():
138-
full = True
139-
if not expected_response_length:
140-
expected_response_length = Defaults.ReadSize
141-
response, last_exception = self._transact(request,
142-
expected_response_length,
143-
full=full
144-
)
145-
if not response and (
146-
request.unit_id not in self._no_response_devices):
147-
self._no_response_devices.append(request.unit_id)
148-
elif request.unit_id in self._no_response_devices and response:
149-
self._no_response_devices.remove(request.unit_id)
150-
if not response and self.retry_on_empty and retries:
151-
while retries > 0:
152-
if hasattr(self.client, "state"):
153-
_logger.debug("RESETTING Transaction state to "
154-
"'IDLE' for retry")
155-
self.client.state = ModbusTransactionState.IDLE
156-
_logger.debug("Retry on empty - {}".format(retries))
157-
response, last_exception = self._transact(
158-
request,
159-
expected_response_length
160-
)
161-
if not response:
162-
retries -= 1
163-
continue
164-
# Remove entry
125+
expected_response_length = None
126+
if not isinstance(self.client.framer, ModbusSocketFramer):
127+
if hasattr(request, "get_response_pdu_size"):
128+
response_pdu_size = request.get_response_pdu_size()
129+
if isinstance(self.client.framer, ModbusAsciiFramer):
130+
response_pdu_size = response_pdu_size * 2
131+
if response_pdu_size:
132+
expected_response_length = self._calculate_response_length(response_pdu_size)
133+
if request.unit_id in self._no_response_devices:
134+
full = True
135+
else:
136+
full = False
137+
c_str = str(self.client)
138+
if "modbusudpclient" in c_str.lower().strip():
139+
full = True
140+
if not expected_response_length:
141+
expected_response_length = Defaults.ReadSize
142+
response, last_exception = self._transact(request,
143+
expected_response_length,
144+
full=full
145+
)
146+
if not response and (
147+
request.unit_id not in self._no_response_devices):
148+
self._no_response_devices.append(request.unit_id)
149+
elif request.unit_id in self._no_response_devices and response:
165150
self._no_response_devices.remove(request.unit_id)
166-
break
167-
if expected_response_length > 0:
151+
if not response and self.retry_on_empty and retries:
152+
while retries > 0:
153+
if hasattr(self.client, "state"):
154+
_logger.debug("RESETTING Transaction state to "
155+
"'IDLE' for retry")
156+
self.client.state = ModbusTransactionState.IDLE
157+
_logger.debug("Retry on empty - {}".format(retries))
158+
response, last_exception = self._transact(
159+
request,
160+
expected_response_length
161+
)
162+
if not response:
163+
retries -= 1
164+
continue
165+
# Remove entry
166+
self._no_response_devices.remove(request.unit_id)
167+
break
168168
addTransaction = partial(self.addTransaction,
169169
tid=request.transaction_id)
170170
self.client.framer.processIncomingPacket(response,
@@ -180,14 +180,12 @@ def execute(self, request):
180180
"/Unable to decode response")
181181
response = ModbusIOException(last_exception,
182182
request.function_code)
183-
else:
184-
_logger.debug("No response expected when sending to broadcast address 0")
185-
if hasattr(self.client, "state"):
186-
_logger.debug("Changing transaction state from "
187-
"'PROCESSING REPLY' to "
188-
"'TRANSACTION_COMPLETE'")
189-
self.client.state = (
190-
ModbusTransactionState.TRANSACTION_COMPLETE)
183+
if hasattr(self.client, "state"):
184+
_logger.debug("Changing transaction state from "
185+
"'PROCESSING REPLY' to "
186+
"'TRANSACTION_COMPLETE'")
187+
self.client.state = (
188+
ModbusTransactionState.TRANSACTION_COMPLETE)
191189
return response
192190
except ModbusIOException as ex:
193191
# Handle decode errors in processIncomingPacket method
@@ -211,13 +209,20 @@ def _transact(self, packet, response_length, full=False):
211209
if _logger.isEnabledFor(logging.DEBUG):
212210
_logger.debug("SEND: " + hexlify_packets(packet))
213211
size = self._send(packet)
214-
if size:
215-
_logger.debug("Changing transaction state from 'SENDING' "
216-
"to 'WAITING FOR REPLY'")
217-
self.client.state = ModbusTransactionState.WAITING_FOR_REPLY
218-
result = self._recv(response_length, full)
219-
if _logger.isEnabledFor(logging.DEBUG):
220-
_logger.debug("RECV: " + hexlify_packets(result))
212+
if response_length is not None:
213+
if size:
214+
_logger.debug("Changing transaction state from 'SENDING' "
215+
"to 'WAITING FOR REPLY'")
216+
self.client.state = ModbusTransactionState.WAITING_FOR_REPLY
217+
result = self._recv(response_length, full)
218+
if _logger.isEnabledFor(logging.DEBUG):
219+
_logger.debug("RECV: " + hexlify_packets(result))
220+
else:
221+
if size:
222+
_logger.debug("Changing transaction state from 'SENDING' "
223+
"to 'TRANSACTION_COMPLETE'")
224+
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
225+
result = b''
221226
except (socket.error, ModbusIOException,
222227
InvalidMessageReceivedException) as msg:
223228
self.client.close()

0 commit comments

Comments
 (0)