Skip to content

Commit 1a24c1d

Browse files
committed
Merge branch 'pull/372' into pymodbus-2.2.0
2 parents 5030514 + e07e01e commit 1a24c1d

File tree

5 files changed

+128
-79
lines changed

5 files changed

+128
-79
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/repl/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Depends on [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/stab
66

77
Install dependencies
88
```
9-
$ pip install click prompt_toolkit --upgarde
9+
$ pip install click prompt_toolkit --upgrade
1010
```
1111

1212
Or

pymodbus/server/sync.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,16 @@ def execute(self, request):
5858
5959
:param request: The decoded request message
6060
"""
61+
broadcast = False
6162
try:
62-
context = self.server.context[request.unit_id]
63-
response = request.execute(context)
63+
if self.server.broadcast_enable and request.unit_id == 0:
64+
broadcast = True
65+
# if broadcasting then execute on all slave contexts, note response will be ignored
66+
for unit_id in self.server.context.slaves():
67+
response = request.execute(self.server.context[unit_id])
68+
else:
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
@@ -107,6 +116,12 @@ def handle(self):
107116
data = self.request.recv(1024)
108117
if data:
109118
units = self.server.context.slaves()
119+
if not isinstance(units, (list, tuple)):
120+
units = [units]
121+
# if broadcast is enabled make sure to process requests to address 0
122+
if self.server.broadcast_enable:
123+
if 0 not in units:
124+
units.append(0)
110125
single = self.server.context.single
111126
self.framer.processIncomingPacket(data, self.execute,
112127
units, single=single)
@@ -291,8 +306,10 @@ def __init__(self, context, framer=None, identity=None,
291306
ModbusConnectedRequestHandler
292307
:param allow_reuse_address: Whether the server will allow the
293308
reuse of an address.
294-
:param ignore_missing_slaves: True to not send errors on a request
295-
to a missing slave
309+
:param ignore_missing_slaves: True to not send errors on a request
310+
to a missing slave
311+
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
312+
False to treat 0 as any other unit_id
296313
"""
297314
self.threads = []
298315
self.allow_reuse_address = allow_reuse_address
@@ -304,6 +321,8 @@ def __init__(self, context, framer=None, identity=None,
304321
self.handler = handler or ModbusConnectedRequestHandler
305322
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
306323
Defaults.IgnoreMissingSlaves)
324+
self.broadcast_enable = kwargs.get('broadcast_enable',
325+
Defaults.broadcast_enable)
307326

308327
if isinstance(identity, ModbusDeviceIdentification):
309328
self.control.Identity.update(identity)
@@ -361,7 +380,9 @@ def __init__(self, context, framer=None, identity=None, address=None,
361380
:param handler: A handler for each client session; default is
362381
ModbusDisonnectedRequestHandler
363382
:param ignore_missing_slaves: True to not send errors on a request
364-
to a missing slave
383+
to a missing slave
384+
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
385+
False to treat 0 as any other unit_id
365386
"""
366387
self.threads = []
367388
self.decoder = ServerDecoder()
@@ -372,6 +393,8 @@ def __init__(self, context, framer=None, identity=None, address=None,
372393
self.handler = handler or ModbusDisconnectedRequestHandler
373394
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
374395
Defaults.IgnoreMissingSlaves)
396+
self.broadcast_enable = kwargs.get('broadcast_enable',
397+
Defaults.broadcast_enable)
375398

376399
if isinstance(identity, ModbusDeviceIdentification):
377400
self.control.Identity.update(identity)
@@ -426,7 +449,9 @@ def __init__(self, context, framer=None, identity=None, **kwargs):
426449
:param baudrate: The baud rate to use for the serial device
427450
:param timeout: The timeout to use for the serial device
428451
:param ignore_missing_slaves: True to not send errors on a request
429-
to a missing slave
452+
to a missing slave
453+
:param broadcast_enable: True to treat unit_id 0 as broadcast address,
454+
False to treat 0 as any other unit_id
430455
"""
431456
self.threads = []
432457
self.decoder = ServerDecoder()
@@ -445,6 +470,8 @@ def __init__(self, context, framer=None, identity=None, **kwargs):
445470
self.timeout = kwargs.get('timeout', Defaults.Timeout)
446471
self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves',
447472
Defaults.IgnoreMissingSlaves)
473+
self.broadcast_enable = kwargs.get('broadcast_enable',
474+
Defaults.broadcast_enable)
448475
self.socket = None
449476
if self._connect():
450477
self.is_running = True

pymodbus/transaction.py

Lines changed: 80 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -118,70 +118,74 @@ 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 in self._no_response_devices:
130-
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'
131124
else:
132-
full = False
133-
c_str = str(self.client)
134-
if "modbusudpclient" in c_str.lower().strip():
135-
full = True
136-
if not expected_response_length:
137-
expected_response_length = Defaults.ReadSize
138-
response, last_exception = self._transact(request,
139-
expected_response_length,
140-
full=full
141-
)
142-
if not response and (
143-
request.unit_id not in self._no_response_devices):
144-
self._no_response_devices.append(request.unit_id)
145-
elif request.unit_id in self._no_response_devices and response:
146-
self._no_response_devices.remove(request.unit_id)
147-
if not response and self.retry_on_empty and retries:
148-
while retries > 0:
149-
if hasattr(self.client, "state"):
150-
_logger.debug("RESETTING Transaction state to "
151-
"'IDLE' for retry")
152-
self.client.state = ModbusTransactionState.IDLE
153-
_logger.debug("Retry on empty - {}".format(retries))
154-
response, last_exception = self._transact(
155-
request,
156-
expected_response_length
157-
)
158-
if not response:
159-
retries -= 1
160-
continue
161-
# Remove entry
162-
self._no_response_devices.remove(request.unit_id)
163-
break
164-
addTransaction = partial(self.addTransaction,
165-
tid=request.transaction_id)
166-
self.client.framer.processIncomingPacket(response,
167-
addTransaction,
168-
request.unit_id)
169-
response = self.getTransaction(request.transaction_id)
170-
if not response:
171-
if len(self.transactions):
172-
response = self.getTransaction(tid=0)
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
173135
else:
174-
last_exception = last_exception or (
175-
"No Response received from the remote unit"
176-
"/Unable to decode response")
177-
response = ModbusIOException(last_exception,
178-
request.function_code)
179-
if hasattr(self.client, "state"):
180-
_logger.debug("Changing transaction state from "
181-
"'PROCESSING REPLY' to "
182-
"'TRANSACTION_COMPLETE'")
183-
self.client.state = (
184-
ModbusTransactionState.TRANSACTION_COMPLETE)
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:
150+
self._no_response_devices.remove(request.unit_id)
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
168+
addTransaction = partial(self.addTransaction,
169+
tid=request.transaction_id)
170+
self.client.framer.processIncomingPacket(response,
171+
addTransaction,
172+
request.unit_id)
173+
response = self.getTransaction(request.transaction_id)
174+
if not response:
175+
if len(self.transactions):
176+
response = self.getTransaction(tid=0)
177+
else:
178+
last_exception = last_exception or (
179+
"No Response received from the remote unit"
180+
"/Unable to decode response")
181+
response = ModbusIOException(last_exception,
182+
request.function_code)
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)
185189
return response
186190
except ModbusIOException as ex:
187191
# Handle decode errors in processIncomingPacket method
@@ -205,13 +209,20 @@ def _transact(self, packet, response_length, full=False):
205209
if _logger.isEnabledFor(logging.DEBUG):
206210
_logger.debug("SEND: " + hexlify_packets(packet))
207211
size = self._send(packet)
208-
if size:
209-
_logger.debug("Changing transaction state from 'SENDING' "
210-
"to 'WAITING FOR REPLY'")
211-
self.client.state = ModbusTransactionState.WAITING_FOR_REPLY
212-
result = self._recv(response_length, full)
213-
if _logger.isEnabledFor(logging.DEBUG):
214-
_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''
215226
except (socket.error, ModbusIOException,
216227
InvalidMessageReceivedException) as msg:
217228
self.client.close()

0 commit comments

Comments
 (0)