diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 0ed9f6c01..c8e34b616 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -45,6 +45,7 @@ def __init__(self, framer, **kwargs): self.transaction = FifoTransactionManager(self, **kwargs) self._debug = False self._debugfd = None + self.broadcast_enable = kwargs.get('broadcast_enable', Defaults.broadcast_enable) # ----------------------------------------------------------------------- # # Client interface diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 5763c77d7..c05f0b555 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -88,6 +88,15 @@ class Defaults(Singleton): should be returned or simply ignored. This is useful for the case of a serial server emulater where a request to a non-existant slave on a bus will never respond. The client in this case will simply timeout. + + .. attribute:: broadcast_enable + + When False unit_id 0 will be treated as any other unit_id. When True and + the unit_id is 0 the server will execute all requests on all server + contexts and not respond and the client will skip trying to receive a + response. Default value False does not conform to Modbus spec but maintains + legacy behavior for existing pymodbus users. + ''' Port = 502 Retries = 3 @@ -104,6 +113,7 @@ class Defaults(Singleton): ZeroMode = False IgnoreMissingSlaves = False ReadSize = 1024 + broadcast_enable = False class ModbusStatus(Singleton): ''' diff --git a/pymodbus/repl/README.md b/pymodbus/repl/README.md index 2064f09d3..d5e970861 100644 --- a/pymodbus/repl/README.md +++ b/pymodbus/repl/README.md @@ -6,7 +6,7 @@ Depends on [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/stab Install dependencies ``` -$ pip install click prompt_toolkit --upgarde +$ pip install click prompt_toolkit --upgrade ``` Or diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 6fea466fa..d51d4cdc4 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -58,9 +58,16 @@ def execute(self, request): :param request: The decoded request message """ + broadcast = False try: - context = self.server.context[request.unit_id] - response = request.execute(context) + if self.server.broadcast_enable and request.unit_id == 0: + broadcast = True + # if broadcasting then execute on all slave contexts, note response will be ignored + for unit_id in self.server.context.slaves(): + response = request.execute(self.server.context[unit_id]) + else: + context = self.server.context[request.unit_id] + response = request.execute(context) except NoSuchSlaveException as ex: _logger.debug("requested slave does " "not exist: %s" % request.unit_id ) @@ -71,9 +78,11 @@ def execute(self, request): _logger.debug("Datastore unable to fulfill request: " "%s; %s", ex, traceback.format_exc()) response = request.doException(merror.SlaveFailure) - response.transaction_id = request.transaction_id - response.unit_id = request.unit_id - self.send(response) + # no response when broadcasting + if not broadcast: + response.transaction_id = request.transaction_id + response.unit_id = request.unit_id + self.send(response) # ----------------------------------------------------------------------- # # Base class implementations @@ -107,6 +116,12 @@ def handle(self): data = self.request.recv(1024) if data: units = self.server.context.slaves() + if not isinstance(units, (list, tuple)): + units = [units] + # if broadcast is enabled make sure to process requests to address 0 + if self.server.broadcast_enable: + if 0 not in units: + units.append(0) single = self.server.context.single self.framer.processIncomingPacket(data, self.execute, units, single=single) @@ -291,8 +306,10 @@ def __init__(self, context, framer=None, identity=None, ModbusConnectedRequestHandler :param allow_reuse_address: Whether the server will allow the reuse of an address. - :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + :param ignore_missing_slaves: True to not send errors on a request + to a missing slave + :param broadcast_enable: True to treat unit_id 0 as broadcast address, + False to treat 0 as any other unit_id """ self.threads = [] self.allow_reuse_address = allow_reuse_address @@ -304,6 +321,8 @@ def __init__(self, context, framer=None, identity=None, self.handler = handler or ModbusConnectedRequestHandler self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) + self.broadcast_enable = kwargs.get('broadcast_enable', + Defaults.broadcast_enable) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -361,7 +380,9 @@ def __init__(self, context, framer=None, identity=None, address=None, :param handler: A handler for each client session; default is ModbusDisonnectedRequestHandler :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + to a missing slave + :param broadcast_enable: True to treat unit_id 0 as broadcast address, + False to treat 0 as any other unit_id """ self.threads = [] self.decoder = ServerDecoder() @@ -372,6 +393,8 @@ def __init__(self, context, framer=None, identity=None, address=None, self.handler = handler or ModbusDisconnectedRequestHandler self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) + self.broadcast_enable = kwargs.get('broadcast_enable', + Defaults.broadcast_enable) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -426,7 +449,9 @@ def __init__(self, context, framer=None, identity=None, **kwargs): :param baudrate: The baud rate to use for the serial device :param timeout: The timeout to use for the serial device :param ignore_missing_slaves: True to not send errors on a request - to a missing slave + to a missing slave + :param broadcast_enable: True to treat unit_id 0 as broadcast address, + False to treat 0 as any other unit_id """ self.threads = [] self.decoder = ServerDecoder() @@ -445,6 +470,8 @@ def __init__(self, context, framer=None, identity=None, **kwargs): self.timeout = kwargs.get('timeout', Defaults.Timeout) self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) + self.broadcast_enable = kwargs.get('broadcast_enable', + Defaults.broadcast_enable) self.socket = None if self._connect(): self.is_running = True diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index d869f8413..17be1507b 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -118,70 +118,74 @@ def execute(self, request): _logger.debug("Clearing current Frame : - {}".format(_buffer)) self.client.framer.resetFrame() - expected_response_length = None - if not isinstance(self.client.framer, ModbusSocketFramer): - if hasattr(request, "get_response_pdu_size"): - response_pdu_size = request.get_response_pdu_size() - if isinstance(self.client.framer, ModbusAsciiFramer): - response_pdu_size = response_pdu_size * 2 - if response_pdu_size: - expected_response_length = self._calculate_response_length(response_pdu_size) - if request.unit_id in self._no_response_devices: - full = True + if request.unit_id == 0 and self.client.broadcast_enable: + response, last_exception = self._transact(request, None) + response = b'Broadcast write sent - no response expected' else: - full = False - c_str = str(self.client) - if "modbusudpclient" in c_str.lower().strip(): - full = True - if not expected_response_length: - expected_response_length = Defaults.ReadSize - response, last_exception = self._transact(request, - expected_response_length, - full=full - ) - if not response and ( - request.unit_id not in self._no_response_devices): - self._no_response_devices.append(request.unit_id) - elif request.unit_id in self._no_response_devices and response: - self._no_response_devices.remove(request.unit_id) - if not response and self.retry_on_empty and retries: - while retries > 0: - if hasattr(self.client, "state"): - _logger.debug("RESETTING Transaction state to " - "'IDLE' for retry") - self.client.state = ModbusTransactionState.IDLE - _logger.debug("Retry on empty - {}".format(retries)) - response, last_exception = self._transact( - request, - expected_response_length - ) - if not response: - retries -= 1 - continue - # Remove entry - self._no_response_devices.remove(request.unit_id) - break - addTransaction = partial(self.addTransaction, - tid=request.transaction_id) - self.client.framer.processIncomingPacket(response, - addTransaction, - request.unit_id) - response = self.getTransaction(request.transaction_id) - if not response: - if len(self.transactions): - response = self.getTransaction(tid=0) + expected_response_length = None + if not isinstance(self.client.framer, ModbusSocketFramer): + if hasattr(request, "get_response_pdu_size"): + response_pdu_size = request.get_response_pdu_size() + if isinstance(self.client.framer, ModbusAsciiFramer): + response_pdu_size = response_pdu_size * 2 + if response_pdu_size: + expected_response_length = self._calculate_response_length(response_pdu_size) + if request.unit_id in self._no_response_devices: + full = True else: - last_exception = last_exception or ( - "No Response received from the remote unit" - "/Unable to decode response") - response = ModbusIOException(last_exception, - request.function_code) - if hasattr(self.client, "state"): - _logger.debug("Changing transaction state from " - "'PROCESSING REPLY' to " - "'TRANSACTION_COMPLETE'") - self.client.state = ( - ModbusTransactionState.TRANSACTION_COMPLETE) + full = False + c_str = str(self.client) + if "modbusudpclient" in c_str.lower().strip(): + full = True + if not expected_response_length: + expected_response_length = Defaults.ReadSize + response, last_exception = self._transact(request, + expected_response_length, + full=full + ) + if not response and ( + request.unit_id not in self._no_response_devices): + self._no_response_devices.append(request.unit_id) + elif request.unit_id in self._no_response_devices and response: + self._no_response_devices.remove(request.unit_id) + if not response and self.retry_on_empty and retries: + while retries > 0: + if hasattr(self.client, "state"): + _logger.debug("RESETTING Transaction state to " + "'IDLE' for retry") + self.client.state = ModbusTransactionState.IDLE + _logger.debug("Retry on empty - {}".format(retries)) + response, last_exception = self._transact( + request, + expected_response_length + ) + if not response: + retries -= 1 + continue + # Remove entry + self._no_response_devices.remove(request.unit_id) + break + addTransaction = partial(self.addTransaction, + tid=request.transaction_id) + self.client.framer.processIncomingPacket(response, + addTransaction, + request.unit_id) + response = self.getTransaction(request.transaction_id) + if not response: + if len(self.transactions): + response = self.getTransaction(tid=0) + else: + last_exception = last_exception or ( + "No Response received from the remote unit" + "/Unable to decode response") + response = ModbusIOException(last_exception, + request.function_code) + if hasattr(self.client, "state"): + _logger.debug("Changing transaction state from " + "'PROCESSING REPLY' to " + "'TRANSACTION_COMPLETE'") + self.client.state = ( + ModbusTransactionState.TRANSACTION_COMPLETE) return response except ModbusIOException as ex: # Handle decode errors in processIncomingPacket method @@ -205,13 +209,20 @@ def _transact(self, packet, response_length, full=False): if _logger.isEnabledFor(logging.DEBUG): _logger.debug("SEND: " + hexlify_packets(packet)) size = self._send(packet) - if size: - _logger.debug("Changing transaction state from 'SENDING' " - "to 'WAITING FOR REPLY'") - self.client.state = ModbusTransactionState.WAITING_FOR_REPLY - result = self._recv(response_length, full) - if _logger.isEnabledFor(logging.DEBUG): - _logger.debug("RECV: " + hexlify_packets(result)) + if response_length is not None: + if size: + _logger.debug("Changing transaction state from 'SENDING' " + "to 'WAITING FOR REPLY'") + self.client.state = ModbusTransactionState.WAITING_FOR_REPLY + result = self._recv(response_length, full) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug("RECV: " + hexlify_packets(result)) + else: + if size: + _logger.debug("Changing transaction state from 'SENDING' " + "to 'TRANSACTION_COMPLETE'") + self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE + result = b'' except (socket.error, ModbusIOException, InvalidMessageReceivedException) as msg: self.client.close()