diff --git a/docs/advancedusage.rst b/docs/advancedusage.rst index c0c6b53..32a719c 100644 --- a/docs/advancedusage.rst +++ b/docs/advancedusage.rst @@ -226,6 +226,22 @@ Add this to :func:`._extract_payload` function, after the argument validity test pass +Handling instrument address mismatch +-------------------------------------------------------------------------- +Some commands or instrument access patterns may result in responses whose reported slave +address differs from the address used to send the command. Examples include sending a +command to change the device's address or using a "universal address" to communicate +with a single connected device. Note that you should never use a "universal address" when +more than one device is connected to the bus. + +To ignore the slave address returned by a command, set the "ignoreslaveaddress" parameter +to True: + + instr = minimalmodbus.Instrument('/dev/ttyUSB0', 1) + # change device's address to 23 using register 0x17 + instr.write_register(0x17, 23, ignoreslaveaddress=True) + + Install or uninstalling a distribution -------------------------------------------------------------------------- diff --git a/minimalmodbus.py b/minimalmodbus.py index 8d22676..59fbada 100644 --- a/minimalmodbus.py +++ b/minimalmodbus.py @@ -297,7 +297,12 @@ def _print_debug(self, text: str) -> None: # Methods for talking to the slave # # ################################# # - def read_bit(self, registeraddress: int, functioncode: int = 2) -> int: + def read_bit( + self, + registeraddress: int, + functioncode: int = 2, + ignoreslaveaddress: bool = False, + ) -> int: """Read one bit from the slave (instrument). This is for a bit that has its individual address in the instrument. @@ -305,6 +310,7 @@ def read_bit(self, registeraddress: int, functioncode: int = 2) -> int: Args: * registeraddress: The slave register address. * functioncode Modbus function code. Can be 1 or 2. + * ignoreslaveaddress: Bypass the slave address check. Returns: The bit value 0 or 1. @@ -320,11 +326,16 @@ def read_bit(self, registeraddress: int, functioncode: int = 2) -> int: registeraddress, number_of_bits=1, payloadformat=_Payloadformat.BIT, + ignoreslaveaddress=ignoreslaveaddress, ) ) def write_bit( - self, registeraddress: int, value: int, functioncode: int = 5 + self, + registeraddress: int, + value: int, + functioncode: int = 5, + ignoreslaveaddress: bool = False, ) -> None: """Write one bit to the slave (instrument). @@ -334,6 +345,7 @@ def write_bit( * registeraddress: The slave register address. * value: 0 or 1, or True or False * functioncode: Modbus function code. Can be 5 or 15. + * ignoreslaveaddress: Bypass the slave address check. Raises: TypeError, ValueError, ModbusException, @@ -347,10 +359,15 @@ def write_bit( value, number_of_bits=1, payloadformat=_Payloadformat.BIT, + ignoreslaveaddress=ignoreslaveaddress, ) def read_bits( - self, registeraddress: int, number_of_bits: int, functioncode: int = 2 + self, + registeraddress: int, + number_of_bits: int, + functioncode: int = 2, + ignoreslaveaddress: bool = False, ) -> List[int]: """Read multiple bits from the slave (instrument). @@ -360,6 +377,7 @@ def read_bits( * registeraddress: The slave register start address. * number_of_bits: Number of bits to read * functioncode: Modbus function code. Can be 1 or 2. + * ignoreslaveaddress: Bypass the slave address check. Returns: A list of bit values 0 or 1. The first value in the list is for @@ -381,12 +399,18 @@ def read_bits( registeraddress, number_of_bits=number_of_bits, payloadformat=_Payloadformat.BITS, + ignoreslaveaddress=ignoreslaveaddress, ) # Make sure that we really return a list of integers assert isinstance(returnvalue, list) return [int(x) for x in returnvalue] - def write_bits(self, registeraddress: int, values: List[int]) -> None: + def write_bits( + self, + registeraddress: int, + values: List[int], + ignoreslaveaddress: bool = False, + ) -> None: """Write multiple bits to the slave (instrument). This is for bits that have individual addresses in the instrument. @@ -397,6 +421,7 @@ def write_bits(self, registeraddress: int, values: List[int]) -> None: * registeraddress: The slave register start address. * values: List of 0 or 1, or True or False. The first value in the list is for the bit at the given address. + * ignoreslaveaddress: Bypass the slave address check. Raises: TypeError, ValueError, ModbusException, @@ -420,6 +445,7 @@ def write_bits(self, registeraddress: int, values: List[int]) -> None: values, number_of_bits=len(values), payloadformat=_Payloadformat.BITS, + ignoreslaveaddress=ignoreslaveaddress, ) def read_register( @@ -428,6 +454,7 @@ def read_register( number_of_decimals: int = 0, functioncode: int = 3, signed: bool = False, + ignoreslaveaddress: bool = False, ) -> Union[int, float]: """Read an integer from one 16-bit register in the slave, possibly scaling it. @@ -439,6 +466,7 @@ def read_register( * number_of_decimals: The number of decimals for content conversion. * functioncode: Modbus function code. Can be 3 or 4. * signed: Whether the data should be interpreted as unsigned or signed. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_decimals was named numberOfDecimals before MinimalModbus 1.0 @@ -488,6 +516,7 @@ def read_register( number_of_registers=1, signed=signed, payloadformat=_Payloadformat.REGISTER, + ignoreslaveaddress=ignoreslaveaddress, ) if int(returnvalue) == returnvalue: return int(returnvalue) @@ -500,6 +529,7 @@ def write_register( number_of_decimals: int = 0, functioncode: int = 16, signed: bool = False, + ignoreslaveaddress: bool = False, ) -> None: """Write an integer to one 16-bit register in the slave, possibly scaling it. @@ -513,6 +543,7 @@ def write_register( * number_of_decimals: The number of decimals for content conversion. * functioncode: Modbus function code. Can be 6 or 16. * signed: Whether the data should be interpreted as unsigned or signed. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_decimals was named numberOfDecimals before MinimalModbus 1.0 @@ -557,6 +588,7 @@ def write_register( number_of_registers=1, signed=signed, payloadformat=_Payloadformat.REGISTER, + ignoreslaveaddress=ignoreslaveaddress, ) def read_long( @@ -566,6 +598,7 @@ def read_long( signed: bool = False, byteorder: int = BYTEORDER_BIG, number_of_registers: int = 2, + ignoreslaveaddress: bool = False, ) -> int: """Read a long integer (32 or 64 bits) from the slave. @@ -581,6 +614,7 @@ def read_long( :data:`minimalmodbus.BYTEORDER_BIG`. * number_of_registers: The number of registers allocated for the long. Can be 2 or 4. (New in version 2.1) + * ignoreslaveaddress: Bypass the slave address check. ======================= ============== =============== ===================== @@ -615,6 +649,7 @@ def read_long( signed=signed, byteorder=byteorder, payloadformat=_Payloadformat.LONG, + ignoreslaveaddress=ignoreslaveaddress, ) ) @@ -625,6 +660,7 @@ def write_long( signed: bool = False, byteorder: int = BYTEORDER_BIG, number_of_registers: int = 2, + ignoreslaveaddress: bool = False, ) -> None: """Write a long integer (32 or 64 bits) to the slave. @@ -645,6 +681,7 @@ def write_long( :data:`minimalmodbus.BYTEORDER_BIG`. * number_of_registers: The number of registers allocated for the long. Can be 2 or 4. (New in version 2.1) + * ignoreslaveaddress: Bypass the slave address check. Raises: TypeError, ValueError, ModbusException, @@ -684,6 +721,7 @@ def write_long( signed=signed, byteorder=byteorder, payloadformat=_Payloadformat.LONG, + ignoreslaveaddress=ignoreslaveaddress, ) def read_float( @@ -692,6 +730,7 @@ def read_float( functioncode: int = 3, number_of_registers: int = 2, byteorder: int = BYTEORDER_BIG, + ignoreslaveaddress: bool = False, ) -> float: r"""Read a floating point number from the slave. @@ -713,6 +752,7 @@ def read_float( * byteorder: How multi-register data should be interpreted. Use the BYTEORDER_xxx constants. Defaults to :data:`minimalmodbus.BYTEORDER_BIG`. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_registers was named numberOfRegisters before MinimalModbus 1.0 @@ -745,6 +785,7 @@ def read_float( number_of_registers=number_of_registers, byteorder=byteorder, payloadformat=_Payloadformat.FLOAT, + ignoreslaveaddress=ignoreslaveaddress, ) ) @@ -754,6 +795,7 @@ def write_float( value: Union[int, float], number_of_registers: int = 2, byteorder: int = BYTEORDER_BIG, + ignoreslaveaddress: bool = False, ) -> None: """Write a floating point number to the slave. @@ -772,6 +814,7 @@ def write_float( * byteorder: How multi-register data should be interpreted. Use the BYTEORDER_xxx constants. Defaults to :data:`minimalmodbus.BYTEORDER_BIG`. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_registers was named numberOfRegisters before MinimalModbus 1.0 @@ -794,10 +837,15 @@ def write_float( number_of_registers=number_of_registers, byteorder=byteorder, payloadformat=_Payloadformat.FLOAT, + ignoreslaveaddress=ignoreslaveaddress, ) def read_string( - self, registeraddress: int, number_of_registers: int = 16, functioncode: int = 3 + self, + registeraddress: int, + number_of_registers: int = 16, + functioncode: int = 3, + ignoreslaveaddress: bool = False, ) -> str: """Read an ASCII string from the slave. @@ -811,6 +859,7 @@ def read_string( * registeraddress: The slave register start address. * number_of_registers: The number of registers allocated for the string. * functioncode: Modbus function code. Can be 3 or 4. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_registers was named numberOfRegisters before MinimalModbus 1.0 @@ -835,11 +884,16 @@ def read_string( registeraddress, number_of_registers=number_of_registers, payloadformat=_Payloadformat.STRING, + ignoreslaveaddress=ignoreslaveaddress, ) ) def write_string( - self, registeraddress: int, textstring: str, number_of_registers: int = 16 + self, + registeraddress: int, + textstring: str, + number_of_registers: int = 16, + ignoreslaveaddress: bool = False, ) -> None: """Write an ASCII string to the slave. @@ -855,6 +909,7 @@ def write_string( * registeraddress: The slave register start address. * textstring: The string to store in the slave, must be ASCII. * number_of_registers: The number of registers allocated for the string. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_registers was named numberOfRegisters before MinimalModbus 1.0 @@ -888,10 +943,15 @@ def write_string( textstring, number_of_registers=number_of_registers, payloadformat=_Payloadformat.STRING, + ignoreslaveaddress=ignoreslaveaddress, ) def read_registers( - self, registeraddress: int, number_of_registers: int, functioncode: int = 3 + self, + registeraddress: int, + number_of_registers: int, + functioncode: int = 3, + ignoreslaveaddress: bool = False, ) -> List[int]: """Read integers from 16-bit registers in the slave. @@ -902,6 +962,7 @@ def read_registers( * registeraddress: The slave register start address. * number_of_registers: The number of registers to read, max 125 registers. * functioncode: Modbus function code. Can be 3 or 4. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_registers was named numberOfRegisters before MinimalModbus 1.0 @@ -929,12 +990,18 @@ def read_registers( registeraddress, number_of_registers=number_of_registers, payloadformat=_Payloadformat.REGISTERS, + ignoreslaveaddress=ignoreslaveaddress, ) # Make sure that we really return a list of integers assert isinstance(returnvalue, list) return [int(x) for x in returnvalue] - def write_registers(self, registeraddress: int, values: List[int]) -> None: + def write_registers( + self, + registeraddress: int, + values: List[int], + ignoreslaveaddress: bool = False, + ) -> None: """Write integers to 16-bit registers in the slave. The slave register can hold integer values in the range 0 to @@ -950,6 +1017,7 @@ def write_registers(self, registeraddress: int, values: List[int]) -> None: * values: The values to store in the slave registers, max 123 values. The first value in the list is for the register at the given address. + * ignoreslaveaddress: Bypass the slave address check. .. note:: The parameter number_of_registers was named numberOfRegisters before MinimalModbus 1.0 @@ -979,6 +1047,7 @@ def write_registers(self, registeraddress: int, values: List[int]) -> None: values, number_of_registers=len(values), payloadformat=_Payloadformat.REGISTERS, + ignoreslaveaddress=ignoreslaveaddress, ) # ############### # @@ -996,6 +1065,7 @@ def _generic_command( signed: bool = False, byteorder: int = BYTEORDER_BIG, payloadformat: _Payloadformat = _Payloadformat.REGISTER, + ignoreslaveaddress: bool = False, ) -> Any: """Perform generic command for reading and writing registers and bits. @@ -1013,6 +1083,7 @@ def _generic_command( Only for a single register or for payloadformat='long'. * byteorder: How multi-register data should be interpreted. * payloadformat: An _Payloadformat enum + * ignoreslaveaddress: Bypass the slave address check If a value of 77.0 is stored internally in the slave register as 770, then use ``number_of_decimals=1`` which will divide the received data @@ -1273,7 +1344,9 @@ def _generic_command( ) # Communicate with instrument - payload_from_slave = self._perform_command(functioncode, payload_to_slave) + payload_from_slave = self._perform_command( + functioncode, payload_to_slave, ignoreslaveaddress + ) # There is no response for broadcasts if self.address == _SLAVEADDRESS_BROADCAST: @@ -1297,7 +1370,12 @@ def _generic_command( # Communication implementation details # # #################################### # - def _perform_command(self, functioncode: int, payload_to_slave: bytes) -> bytes: + def _perform_command( + self, + functioncode: int, + payload_to_slave: bytes, + ignoreslaveaddress: bool = False, + ) -> bytes: """Perform the command having the *functioncode*. Args: @@ -1305,6 +1383,7 @@ def _perform_command(self, functioncode: int, payload_to_slave: bytes) -> bytes: Can for example be 'Write register' = 16. * payload_to_slave: Data to be transmitted to the slave (will be embedded in slaveaddress, CRC etc) + * ignoreslaveaddress: Bypass the slave address check Returns: The extracted data payload from the slave. It has been @@ -1357,7 +1436,7 @@ def _perform_command(self, functioncode: int, payload_to_slave: bytes) -> bytes: # Extract payload payload_from_slave = _extract_payload( - response_bytes, self.address, self.mode, functioncode + response_bytes, self.address, self.mode, functioncode, ignoreslaveaddress ) return payload_from_slave @@ -1787,7 +1866,11 @@ def _embed_payload( def _extract_payload( - response: bytes, slaveaddress: int, mode: str, functioncode: int + response: bytes, + slaveaddress: int, + mode: str, + functioncode: int, + ignoreslaveaddress: bool = False, ) -> bytes: """Extract the payload data part from the slave's response. @@ -1797,6 +1880,7 @@ def _extract_payload( * slaveaddress: The adress of the slave. Used here for error checking only. * mode: The modbus protocol mode (MODE_RTU or MODE_ASCII) * functioncode: Used here for error checking only. + * ignoreslaveaddress: Bypass slave address check. Returns: The payload part of the *response*. Conversion from Modbus ASCII @@ -1903,16 +1987,17 @@ def _extract_payload( ) raise InvalidResponseError(text) - # Check slave address - responseaddress = response[_BYTEPOSITION_FOR_SLAVEADDRESS] + if not ignoreslaveaddress: + # Check slave address + responseaddress = response[_BYTEPOSITION_FOR_SLAVEADDRESS] - if responseaddress != slaveaddress: - raise InvalidResponseError( - "Wrong return slave " - + "address: {} instead of {}. The response is: {!r}".format( - responseaddress, slaveaddress, response + if responseaddress != slaveaddress: + raise InvalidResponseError( + "Wrong return slave " + + "address: {} instead of {}. The response is: {!r}".format( + responseaddress, slaveaddress, response + ) ) - ) # Check if slave indicates error _check_response_slaveerrorcode(response) diff --git a/tests/test_minimalmodbus.py b/tests/test_minimalmodbus.py index 5d35c7f..9de0556 100644 --- a/tests/test_minimalmodbus.py +++ b/tests/test_minimalmodbus.py @@ -1503,6 +1503,20 @@ def testKnownValues(self) -> None: ) self.assertEqual(result, known_result) + def testIgnoredSlaveAddress(self) -> None: + for value in [3, 95, 128, 247, 255]: + for ( + slaveaddress, + functioncode, + mode, + known_result, + inputbytes, + ) in self.known_values: + result = minimalmodbus._extract_payload( + inputbytes, value, mode, functioncode, True + ) # Wrong slave address. Check explicitly disabled. + self.assertEqual(result, known_result) + def testWrongInputValue(self) -> None: self.assertRaises( InvalidResponseError, @@ -1622,6 +1636,16 @@ def testWrongInputValue(self) -> None: "rtu", 2, ) # Wrong slave address + for value in [3, 95, 128]: + self.assertRaises( + InvalidResponseError, + minimalmodbus._extract_payload, + b"\x02\x02123X\xc2", + value, + "rtu", + 2, + False, + ) # Wrong slave address. Check explicitly enabled. for value in [128, 256, -1]: self.assertRaises( ValueError,