diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index d53e359e9..9dc9aebe2 100644 Binary files a/doc/source/_static/examples.tgz and b/doc/source/_static/examples.tgz differ diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index 79c23ddec..95b1c198c 100644 Binary files a/doc/source/_static/examples.zip and b/doc/source/_static/examples.zip differ diff --git a/doc/source/client.rst b/doc/source/client.rst index 4343def75..99d115c1a 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -199,14 +199,12 @@ The logical devices represented by the device is addressed with the :mod:`slave= With **Serial**, the comm port is defined when creating the object. The physical devices are addressed with the :mod:`slave=` parameter. -:mod:`slave=0` is used as broadcast in order to address all devices. -However experience shows that modern devices do not allow broadcast, mostly because it is -inheriently dangerous. With :mod:`slave=0` the application can get upto 254 responses on a single request, -and this is not handled with the normal API calls! +:mod:`slave=0` is defined as broadcast in the modbus standard, but pymodbus treats is a normal device. -The simple request calls (mixin) do NOT support broadcast, if an application wants to use broadcast -it must call :mod:`client.execute` and deal with the responses. +If an application is expecting multiple responses to a broadcast request, it must call :mod:`client.execute` and deal with the responses. +If no response is expected to a request, the :mod:`no_response_expected=True` argument can be used +in the normal API calls, this will cause the call to return imidiatble with :mod:`None` Client response handling @@ -235,6 +233,7 @@ And in case of read retrieve the data depending on type of request - :mod:`rr.bits` is set for coils / input_register requests - :mod:`rr.registers` is set for other requests +Remark if using :mod:`no_response_expected=True` rr will always be None. Client interface classes ------------------------ diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index ab64e7109..c28f5cf01 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -128,12 +128,12 @@ async def main(host="localhost", port=5020): client.register(CustomModbusPDU) slave=1 request = CustomRequest(32, slave=slave) - result = await client.execute(request) + result = await client.execute(False, request) print(result) # inherited request request = Read16CoilsRequest(32, slave) - result = await client.execute(request) + result = await client.execute(False, request) print(result) diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index ee3b18252..884d8f017 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -83,20 +83,16 @@ def close(self) -> None: """Close connection.""" self.ctx.close() - def execute(self, request: ModbusPDU): + def execute(self, no_response_expected: bool, request: ModbusPDU): """Execute request and get response (call **sync/async**). - :param request: The request to process - :returns: The result of the request execution - :raises ConnectionException: Check exception text. - :meta private: """ if not self.ctx.transport: raise ConnectionException(f"Not connected[{self!s}]") - return self.async_execute(request) + return self.async_execute(no_response_expected, request) - async def async_execute(self, request) -> ModbusPDU: + async def async_execute(self, no_response_expected: bool, request) -> ModbusPDU | None: """Execute requests asynchronously. :meta private: @@ -109,6 +105,9 @@ async def async_execute(self, request) -> ModbusPDU: async with self._lock: req = self.build_response(request) self.ctx.send(packet) + if no_response_expected: + resp = None + break try: resp = await asyncio.wait_for( req, timeout=self.ctx.comm_params.timeout_connect @@ -225,9 +224,10 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusPDU) -> ModbusPDU: + def execute(self, no_response_expected: bool, request: ModbusPDU) -> ModbusPDU: """Execute request and get response (call **sync/async**). + :param no_response_expected: The client will not expect a response to the request :param request: The request to process :returns: The result of the request execution :raises ConnectionException: Check exception text. @@ -236,7 +236,7 @@ def execute(self, request: ModbusPDU) -> ModbusPDU: """ if not self.connect(): raise ConnectionException(f"Failed to connect[{self!s}]") - return self.transaction.execute(request) + return self.transaction.execute(no_response_expected, request) # ----------------------------------------------------------------------- # # Internal methods diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 784153543..b31e1ef87 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -49,7 +49,7 @@ class ModbusClientMixin(Generic[T]): # pylint: disable=too-many-public-methods def __init__(self): """Initialize.""" - def execute(self, _request: ModbusPDU) -> T: + def execute(self, _no_response_expected: bool, _request: ModbusPDU,) -> T: """Execute request (code ???). :raises ModbusException: @@ -61,305 +61,331 @@ def execute(self, _request: ModbusPDU) -> T: """ raise NotImplementedError("execute of ModbusClientMixin needs to be overridden") - def read_coils(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_coils(self, address: int, count: int = 1, slave: int = 1, no_response_expected: bool = False) -> T: """Read coils (code 0x01). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_bit_read.ReadCoilsRequest(address=address, count=count, slave=slave)) - def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_discrete_inputs(self, + address: int, + count: int = 1, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_bit_read.ReadDiscreteInputsRequest(address=address, count=count, slave=slave, )) - def read_holding_registers(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_holding_registers(self, + address: int, + count: int = 1, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read holding registers (code 0x03). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_reg_read.ReadHoldingRegistersRequest(address=address, count=count, slave=slave)) - def read_input_registers(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_input_registers(self, + address: int, + count: int = 1, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read input registers (code 0x04). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) - def write_coil(self, address: int, value: bool, slave: int = 1) -> T: + def write_coil(self, address: int, value: bool, slave: int = 1, no_response_expected: bool = False) -> T: """Write single coil (code 0x05). :param address: Address to write to :param value: Boolean to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) + return self.execute(no_response_expected, pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) - def write_register(self, address: int, value: bytes | int, slave: int = 1) -> T: + def write_register(self, address: int, value: bytes | int, slave: int = 1, no_response_expected: bool = False) -> T: """Write register (code 0x06). :param address: Address to write to :param value: Value to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) + return self.execute(no_response_expected, pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) - def read_exception_status(self, slave: int = 1) -> T: + def read_exception_status(self, slave: int = 1, no_response_expected: bool = False) -> T: """Read Exception Status (code 0x07). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.ReadExceptionStatusRequest(slave=slave)) - - def diag_query_data( - self, msg: bytes, slave: int = 1) -> T: + def diag_query_data(self, msg: bytes, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) - def diag_restart_communication( - self, toggle: bool, slave: int = 1) -> T: + def diag_restart_communication(self, toggle: bool, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave)) - def diag_read_diagnostic_register(self, slave: int = 1) -> T: + def diag_read_diagnostic_register(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave)) - def diag_change_ascii_input_delimeter(self, slave: int = 1) -> T: + def diag_change_ascii_input_delimeter(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave)) - def diag_force_listen_only(self, slave: int = 1) -> T: + def diag_force_listen_only(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose force listen only (code 0x08 sub 0x04). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ForceListenOnlyModeRequest(slave=slave)) - def diag_clear_counters(self, slave: int = 1) -> T: + def diag_clear_counters(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ClearCountersRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ClearCountersRequest(slave=slave)) - def diag_read_bus_message_count(self, slave: int = 1) -> T: + def diag_read_bus_message_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnBusMessageCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnBusMessageCountRequest(slave=slave)) - def diag_read_bus_comm_error_count(self, slave: int = 1) -> T: + def diag_read_bus_comm_error_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave)) - def diag_read_bus_exception_error_count(self, slave: int = 1) -> T: + def diag_read_bus_exception_error_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave)) - def diag_read_slave_message_count(self, slave: int = 1) -> T: + def diag_read_slave_message_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave Message Count (code 0x08 sub 0x0E). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnSlaveMessageCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveMessageCountRequest(slave=slave)) - def diag_read_slave_no_response_count(self, slave: int = 1) -> T: + def diag_read_slave_no_response_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave)) - def diag_read_slave_nak_count(self, slave: int = 1) -> T: + def diag_read_slave_nak_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave NAK Count (code 0x08 sub 0x10). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) - def diag_read_slave_busy_count(self, slave: int = 1) -> T: + def diag_read_slave_busy_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave Busy Count (code 0x08 sub 0x11). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) - def diag_read_bus_char_overrun_count(self, slave: int = 1) -> T: + def diag_read_bus_char_overrun_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave)) - def diag_read_iop_overrun_count(self, slave: int = 1) -> T: + def diag_read_iop_overrun_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnIopOverrunCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnIopOverrunCountRequest(slave=slave)) - def diag_clear_overrun_counter(self, slave: int = 1) -> T: + def diag_clear_overrun_counter(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ClearOverrunCountRequest(slave=slave)) - def diag_getclear_modbus_response(self, slave: int = 1) -> T: + def diag_getclear_modbus_response(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.GetClearModbusPlusRequest(slave=slave)) - def diag_get_comm_event_counter(self, slave: int = 1) -> T: + def diag_get_comm_event_counter(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose get event counter (code 0x0B). + :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.GetCommEventCounterRequest(slave=slave)) - def diag_get_comm_event_log(self, slave: int = 1) -> T: + def diag_get_comm_event_log(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose get event counter (code 0x0C). + :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventLogRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.GetCommEventLogRequest(slave=slave)) def write_coils( self, address: int, values: list[bool] | bool, slave: int = 1, + no_response_expected: bool = False ) -> T: """Write coils (code 0x0F). :param address: Start address to write to :param values: List of booleans to write, or a single boolean to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave) - ) + return self.execute(no_response_expected, pdu_bit_write.WriteMultipleCoilsRequest(address, values=values, slave=slave)) def write_registers( - self, address: int, values: list[bytes | int], slave: int = 1, skip_encode: bool = False) -> T: + self, + address: int, + values: list[bytes | int], + slave: int = 1, + skip_encode: bool = False, + no_response_expected: bool = False + ) -> T: """Write registers (code 0x10). :param address: Start address to write to :param values: List of values to write :param slave: (optional) Modbus slave ID :param skip_encode: (optional) do not encode values + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode) - ) + return self.execute(no_response_expected, pdu_req_write.WriteMultipleRegistersRequest(address, values,slave=slave,skip_encode=skip_encode)) - def report_slave_id(self, slave: int = 1) -> T: + def report_slave_id(self, slave: int = 1, no_response_expected: bool = False) -> T: """Report slave ID (code 0x11). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.ReportSlaveIdRequest(slave=slave)) - def read_file_record(self, records: list[tuple], slave: int = 1) -> T: + def read_file_record(self, records: list[tuple], slave: int = 1, no_response_expected: bool = False) -> T: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) :param slave: device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) + return self.execute(no_response_expected, pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) - def write_file_record(self, records: list[tuple], slave: int = 1) -> T: + def write_file_record(self, records: list[tuple], slave: int = 1, no_response_expected: bool = False) -> T: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) :param slave: (optional) Device id + :param no_response_expected: (optional) The client will not expect a response to the request + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_file_msg.WriteFileRecordRequest(records, slave=slave)) + return self.execute(no_response_expected, pdu_file_msg.WriteFileRecordRequest(records,slave=slave)) def mask_write_register( self, @@ -367,6 +393,7 @@ def mask_write_register( and_mask: int = 0xFFFF, or_mask: int = 0x0000, slave: int = 1, + no_response_expected: bool = False ) -> T: """Mask write register (code 0x16). @@ -374,11 +401,10 @@ def mask_write_register( :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address :param slave: (optional) device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave) - ) + return self.execute(no_response_expected, pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave)) def readwrite_registers( self, @@ -388,6 +414,7 @@ def readwrite_registers( address: int | None = None, values: list[int] | int = 0, slave: int = 1, + no_response_expected: bool = False ) -> T: """Read/Write registers (code 0x17). @@ -397,44 +424,39 @@ def readwrite_registers( :param address: (optional) use as read/write address :param values: List of values to write, or a single value to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ if address: read_address = address write_address = address - return self.execute( - pdu_reg_read.ReadWriteMultipleRegistersRequest( - read_address=read_address, - read_count=read_count, - write_address=write_address, - write_registers=values, - slave=slave, - ) - ) + return self.execute(no_response_expected, pdu_reg_read.ReadWriteMultipleRegistersRequest( read_address=read_address, read_count=read_count, write_address=write_address, write_registers=values,slave=slave)) - def read_fifo_queue(self, address: int = 0x0000, slave: int = 1) -> T: + def read_fifo_queue(self, address: int = 0x0000, slave: int = 1, no_response_expected: bool = False) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from :param slave: (optional) device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, slave=slave)) + return self.execute(no_response_expected, pdu_file_msg.ReadFifoQueueRequest(address, slave=slave)) # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED - def read_device_information( - self, read_code: int | None = None, object_id: int = 0x00, slave: int = 1) -> T: + def read_device_information(self, read_code: int | None = None, + object_id: int = 0x00, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read FIFO queue (code 0x2B sub 0x0E). :param read_code: The device information read code :param object_id: The object to read from :param slave: (optional) Device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave) - ) + return self.execute(no_response_expected, pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave)) # ------------------ # Converter methods diff --git a/pymodbus/device.py b/pymodbus/device.py index b35533947..136ac0c82 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -323,25 +323,22 @@ class ModbusCountersHandler: 0x0D 3 Return Slave Exception Error Count Quantity of MODBUS exception error detected by the remote device - since its last restart, clear counters operation, or power-up. It - comprises also the error detected in broadcast messages even if an - exception message is not returned in this case. + since its last restart, clear counters operation, or power-up. Exception errors are described and listed in "MODBUS Application Protocol Specification" document. 0xOE 4 Return Slave Message Count - Quantity of messages addressed to the remote device, including - broadcast messages, that the remote device has processed since its - last restart, clear counters operation, or power-up. + Quantity of messages addressed to the remote device that the remote + device has processed since its last restart, clear counters operation, + or power-up. 0x0F 5 Return Slave No Response Count Quantity of messages received by the remote device for which it returned no response (neither a normal response nor an exception response), since its last restart, clear counters operation, or - power-up. Then, this counter counts the number of broadcast - messages it has received. + power-up. 0x10 6 Return Slave NAK Count diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index 3ef016106..176bc17f2 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -534,7 +534,7 @@ class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest): """Return slave message count. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ @@ -553,7 +553,7 @@ class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse): """Return slave message count. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ @@ -567,7 +567,7 @@ class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest): """Return slave no response. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ @@ -586,7 +586,7 @@ class ReturnSlaveNoResponseCountResponse(DiagnosticStatusSimpleResponse): """Return slave no response. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index f03a5ce26..e951a79c7 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -177,7 +177,7 @@ def _validate_response(self, response): return False return True - def execute(self, request: ModbusPDU): # noqa: C901 + def execute(self, no_response_expected: bool, request: ModbusPDU): # noqa: C901 """Start the producer to send the next request to consumer.write(Frame(request)).""" with self._transaction_lock: try: @@ -193,7 +193,6 @@ def execute(self, request: ModbusPDU): # noqa: C901 ): Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.databuffer = b'' - broadcast = not request.slave_id expected_response_length = None if not isinstance(self.client.framer, FramerSocket): response_pdu_size = request.get_response_pdu_size() @@ -214,11 +213,13 @@ def execute(self, request: ModbusPDU): # noqa: C901 if not expected_response_length: expected_response_length = 1024 response, last_exception = self._transact( + no_response_expected, request, expected_response_length, full=full, - broadcast=broadcast, ) + if no_response_expected: + return None while retries > 0: if self._validate_response(response): if ( @@ -265,7 +266,7 @@ def execute(self, request: ModbusPDU): # noqa: C901 self.client.close() return exc - def _retry_transaction(self, retries, reason, packet, response_length, full=False): + def _retry_transaction(self, no_response_expected, retries, reason, packet, response_length, full=False): """Retry transaction.""" Log.debug("Retry on {} response - {}", reason, retries) Log.debug('Changing transaction state from "WAITING_FOR_REPLY" to "RETRYING"') @@ -278,18 +279,10 @@ def _retry_transaction(self, retries, reason, packet, response_length, full=Fals if response_length == in_waiting: result = self._recv(response_length, full) return result, None - return self._transact(packet, response_length, full=full) + return self._transact(no_response_expected, packet, response_length, full=full) - def _transact(self, request: ModbusPDU, response_length, full=False, broadcast=False): - """Do a Write and Read transaction. - - :param packet: packet to be sent - :param response_length: Expected response length - :param full: the target device was notorious for its no response. Dont - waste time this time by partial querying - :param broadcast: - :return: response - """ + def _transact(self, no_response_expected: bool, request: ModbusPDU, response_length, full=False): + """Do a Write and Read transaction.""" last_exception = None try: self.client.connect() @@ -309,7 +302,7 @@ def _transact(self, request: ModbusPDU, response_length, full=False, broadcast=F if self.client.comm_params.handle_local_echo is True: if self._recv(size, full) != packet: return b"", "Wrong local echo" - if broadcast: + if no_response_expected: if size: Log.debug( 'Changing transaction state from "SENDING" ' diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 9871b2a85..74b4feff2 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -104,7 +104,7 @@ def test_client_mixin(arglist, method, arg, pdu_request): """Test mixin responses.""" pdu_to_call = None - def fake_execute(_self, request): + def fake_execute(_self, _no_response_expected, request): """Set PDU request.""" nonlocal pdu_to_call pdu_to_call = request @@ -241,7 +241,7 @@ async def test_client_instanciate( client.connect = lambda: False client.transport = None with pytest.raises(ConnectionException): - client.execute(ModbusPDU(0, 0, False)) + client.execute(False, ModbusPDU(0, 0, False)) async def test_client_modbusbaseclient(): """Test modbus base client class.""" @@ -418,7 +418,7 @@ async def test_client_protocol_execute(): transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) - response = await base.async_execute(request) + response = await base.async_execute(False, request) assert not response.isError() assert isinstance(response, pdu_bit_read.ReadCoilsResponse) @@ -432,13 +432,26 @@ async def test_client_execute_broadcast(): host="127.0.0.1", ), ) - request = pdu_bit_read.ReadCoilsRequest(1, 1) + request = pdu_bit_read.ReadCoilsRequest(1, 1, slave=0) transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) + assert await base.async_execute(False, request) + - # with pytest.raises(ModbusIOException): - # assert not await base.async_execute(request) - assert await base.async_execute(request) +async def test_client_execute_broadcast_no(): + """Test the client protocol execute method.""" + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams( + host="127.0.0.1", + ), + ) + request = pdu_bit_read.ReadCoilsRequest(1, 1, slave=0) + transport = MockTransport(base, request) + base.ctx.connection_made(transport=transport) + assert not await base.async_execute(True, request) async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" @@ -455,7 +468,7 @@ async def test_client_protocol_retry(): transport = MockTransport(base, request, retries=2) base.ctx.connection_made(transport=transport) - response = await base.async_execute(request) + response = await base.async_execute(False, request) assert transport.retries == 0 assert not response.isError() assert isinstance(response, pdu_bit_read.ReadCoilsResponse) @@ -478,7 +491,7 @@ async def test_client_protocol_timeout(): transport = MockTransport(base, request, retries=4) base.ctx.connection_made(transport=transport) - pdu = await base.async_execute(request) + pdu = await base.async_execute(False, request) assert isinstance(pdu, ExceptionResponse) assert transport.retries == 1 @@ -685,6 +698,6 @@ async def test_client_mixin_execute(): """Test dummy execute for both sync and async.""" client = ModbusClientMixin() with pytest.raises(NotImplementedError): - client.execute(ModbusPDU(0, 0, False)) + client.execute(False, ModbusPDU(0, 0, False)) with pytest.raises(NotImplementedError): - await client.execute(ModbusPDU(0, 0, False)) + await client.execute(False, ModbusPDU(0, 0, False)) diff --git a/test/sub_current/test_transaction.py b/test/sub_current/test_transaction.py index 25fdac103..4b9ff7e58 100755 --- a/test/sub_current/test_transaction.py +++ b/test/sub_current/test_transaction.py @@ -113,7 +113,7 @@ def test_execute(self, mock_get_transaction, mock_recv): assert trans.retries == 3 mock_get_transaction.return_value = b"response" - response = trans.execute(request) + response = trans.execute(False, request) assert response == b"response" # No response mock_recv.reset_mock( @@ -121,14 +121,14 @@ def test_execute(self, mock_get_transaction, mock_recv): ) trans.transactions = {} mock_get_transaction.return_value = None - response = trans.execute(request) + response = trans.execute(False, request) assert isinstance(response, ModbusIOException) # No response with retries mock_recv.reset_mock( side_effect=iter([b"", b"abcdef"]) ) - response = trans.execute(request) + response = trans.execute(False, request) assert isinstance(response, ModbusIOException) # wrong handle_local_echo @@ -136,14 +136,14 @@ def test_execute(self, mock_get_transaction, mock_recv): side_effect=iter([b"abcdef", b"deadbe", b"123456"]) ) client.comm_params.handle_local_echo = True - assert trans.execute(request).message == "[Input/Output] Wrong local echo" + assert trans.execute(False, request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False # retry on invalid response mock_recv.reset_mock( side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) ) - response = trans.execute(request) + response = trans.execute(False, request) assert isinstance(response, ModbusIOException) # Unable to decode response @@ -153,7 +153,7 @@ def test_execute(self, mock_get_transaction, mock_recv): client.framer.processIncomingFrame.side_effect = mock.MagicMock( side_effect=ModbusIOException() ) - assert isinstance(trans.execute(request), ModbusIOException) + assert isinstance(trans.execute(False, request), ModbusIOException) def test_transaction_manager_tid(self): """Test the transaction manager TID.""" @@ -165,9 +165,7 @@ def test_transaction_manager_tid(self): def test_get_transaction_manager_transaction(self): """Test the getting a transaction from the transaction manager.""" self._manager.reset() - handle = ModbusPDU( - 0, self._manager.getNextTID(), False - ) + handle = ModbusPDU(0, self._manager.getNextTID(), False) self._manager.addTransaction(handle) result = self._manager.getTransaction(handle.transaction_id) assert handle is result @@ -175,9 +173,7 @@ def test_get_transaction_manager_transaction(self): def test_delete_transaction_manager_transaction(self): """Test deleting a transaction from the dict transaction manager.""" self._manager.reset() - handle = ModbusPDU( - 0, self._manager.getNextTID(), False - ) + handle = ModbusPDU(0, self._manager.getNextTID(), False) self._manager.addTransaction(handle) self._manager.delTransaction(handle.transaction_id) assert not self._manager.getTransaction(handle.transaction_id)