From c719e0769a35d0e0c6b194aea524c5767d2ac4af Mon Sep 17 00:00:00 2001 From: bapowell Date: Mon, 11 May 2026 17:09:47 -0500 Subject: [PATCH 1/3] For ReadHoldingRegs and ReadInputRegs, let device (ModbusInterface) return a tuple, (registerSize, valueBuff), to indicate a non-standard register size (i.e. not 2 bytes); various small fixes/tweaks. --- libmodbuspy/clientport.py | 56 +++++++++-------- libmodbuspy/mbglobal.py | 109 ++++++++++++++++++++++++++++------ libmodbuspy/serialport.py | 20 ++++--- libmodbuspy/serverresource.py | 53 ++++++++++------- libmodbuspy/tcpserver.py | 8 ++- 5 files changed, 174 insertions(+), 72 deletions(-) diff --git a/libmodbuspy/clientport.py b/libmodbuspy/clientport.py index 9a9b081..f58894a 100644 --- a/libmodbuspy/clientport.py +++ b/libmodbuspy/clientport.py @@ -478,7 +478,7 @@ def _readDiscreteInputs(self, client:ModbusObject, unit: int, offset: int, count else: return None - def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, count: int) -> bytes: + def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, count: int) -> Union[bytes, None]: """Read holding registers from Modbus device. Args: @@ -488,7 +488,7 @@ def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, cou count: Number of holding registers to read. Returns: - Bytes containing the holding register values. + Bytes containing the holding register values, or None. """ status = self.getRequestStatus(client) @@ -514,21 +514,25 @@ def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, cou self._raiseError(StatusCode.Status_BadNotCorrectResponse, "No data was received") fcBytes = buff[0] # count of bytes received if fcBytes != len(buff) - 1: - self._raiseError(StatusCode.Status_BadNotCorrectResponse, "Incorrect received data size") - fcRegs = fcBytes // 2 - if fcRegs != self._count: - self._raiseError(StatusCode.Status_BadNotCorrectResponse, "'ByteCount' is not match received one") - # Extract holding register values from response + self._raiseError(StatusCode.Status_BadNotCorrectResponse, "'ByteCount' does not match the size of the received register data buffer") + regSize, remainder = divmod(fcBytes, self._count) # calculate the register size in bytes + if remainder != 0: + self._raiseError(StatusCode.Status_BadNotCorrectResponse, f"Received byte count ({fcBytes}) is not a multiple of the requested register count ({self._count})") values = bytearray(fcBytes) - for i in range(fcRegs): - values[i*2 ] = buff[2+i*2] - values[i*2+1] = buff[1+i*2] + if regSize == 2: + # Extract holding register values from response + for i in range(self._count): + values[i*2 ] = buff[i*2 + 2] + values[i*2 + 1] = buff[i*2 + 1] + else: + # Perform a byte-for-byte copy for non-standard register sizes (e.g. 4 bytes per register). + values[0: fcBytes] = buff[1: fcBytes + 1] self._setStatus(StatusCode.Status_Good) return bytes(values) else: return None - def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count: int) -> bytes: + def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count: int) -> Union[bytes, None]: """Read input registers from Modbus device. Args: @@ -538,7 +542,7 @@ def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count count: Number of input registers to read. Returns: - Bytes containing the input register values. + Bytes containing the input register values, or None. """ status = self.getRequestStatus(client) @@ -563,16 +567,20 @@ def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count if len(buff) == 0: self._raiseError(StatusCode.Status_BadNotCorrectResponse, "No data was received") fcBytes = buff[0] # count of bytes received - if fcBytes != len(buff) - 1: - self._raiseError(StatusCode.Status_BadNotCorrectResponse, "Incorrect received data size") - fcRegs = fcBytes // 2 - if fcRegs != self._count: - self._raiseError(StatusCode.Status_BadNotCorrectResponse, "'ByteCount' is not match received one") - # Extract input register values from response + if fcBytes != (len(buff) - 1): + self._raiseError(StatusCode.Status_BadNotCorrectResponse, "'ByteCount' does not match the size of the received register data buffer") + regSize, remainder = divmod(fcBytes, self._count) # calculate the register size in bytes + if remainder != 0: + self._raiseError(StatusCode.Status_BadNotCorrectResponse, f"Received byte count ({fcBytes}) is not a multiple of the requested register count ({self._count})") values = bytearray(fcBytes) - for i in range(fcRegs): - values[i*2 ] = buff[2+i*2] - values[i*2+1] = buff[1+i*2] + if regSize == 2: + # Extract input register values from response + for i in range(self._count): + values[i*2 ] = buff[i*2 + 2] + values[i*2 + 1] = buff[i*2 + 1] + else: + # Perform a byte-for-byte copy for non-standard register sizes (e.g. 4 bytes per register). + values[0: fcBytes] = buff[1: fcBytes + 1] self._setStatus(StatusCode.Status_Good) return bytes(values) else: @@ -1019,7 +1027,7 @@ def _readWriteMultipleRegisters(self, client:ModbusObject, unit: int, readOffset if buff is None: return None if self._isBroadcast(): - return StatusCode.Status_Good + return bytes() if len(buff) == 0: self._raiseError(StatusCode.Status_BadNotCorrectResponse, "No data was received") fcBytes = buff[0] # count of bytes received @@ -1083,7 +1091,7 @@ def _readFIFOQueue(self, client:ModbusObject, unit: int, fifoadr: int) -> bytes: else: return None - def _request(self, unit: int, func: int, buff: bytes) -> StatusCode: + def _request(self, unit: int, func: int, buff: bytes) -> Union[bytes, None]: """The function builds the packet that the write() function puts into the buffer. Args: @@ -1092,7 +1100,7 @@ def _request(self, unit: int, func: int, buff: bytes) -> StatusCode: buff: Buffer containing the data to write. Returns: - Status code of the operation. + Response payload as bytes, or None. """ fRepeatAgain = True while fRepeatAgain: diff --git a/libmodbuspy/mbglobal.py b/libmodbuspy/mbglobal.py index 6a4507e..0859231 100644 --- a/libmodbuspy/mbglobal.py +++ b/libmodbuspy/mbglobal.py @@ -105,10 +105,10 @@ def mb_unitmap_set_bit(unitmap: bytearray, unit: int, value: bool) -> None: # 8 = count bits in byte (byte size in bits) MB_BYTE_SZ_BITES = 8 -# 16 = count bits in 16 bit register (register size in bits) +# 16 = count bits in 16 bit register (register size in bits) MB_REGE_SZ_BITES = 16 -# 2 = count bytes in 16 bit register (register size in bytes) +# 2 = count bytes in 16 bit register (register size in bytes) MB_REGE_SZ_BYTES = 2 # 255 - count_of_bytes in function readHoldingRegisters, readCoils etc @@ -241,12 +241,72 @@ class Parity(IntEnum): SpaceParity = 3 # Space parity. The parity bit is sent in the space signal condition. It does not provide error detection information. MarkParity = 4 # Mark parity. The parity bit is always set to the mark signal condition (logical 1). It does not provide error detection information. + @classmethod + def from_char(cls, char: str) -> Union[Parity, None]: + """Finds an enum member by its character value.""" + match char.upper(): + case "N": + return cls.NoParity + case "E": + return cls.EvenParity + case "O": + return cls.OddParity + case "S": + return cls.SpaceParity + case "M": + return cls.MarkParity + case _: + return None + + @classmethod + def to_char(cls, parity: Union[Parity, None]) -> str: + """Converts an enum member to its character representation.""" + match parity: + case cls.NoParity: + return "N" + case cls.EvenParity: + return "E" + case cls.OddParity: + return "O" + case cls.SpaceParity: + return "S" + case cls.MarkParity: + return "M" + case _: + return None + class StopBits(IntEnum): """Defines Stop Bits for serial port.""" OneStop = 0 # 1 stop bit. OneAndHalfStop = 1 # 1.5 stop bit. TwoStop = 2 # 2 stop bits. + @classmethod + def from_float(cls, value: float) -> Union[StopBits, None]: + """Finds an enum member by its float value.""" + match value: + case 1.0: + return cls.OneStop + case 1.5: + return cls.OneAndHalfStop + case 2.0: + return cls.TwoStop + case _: + return None + + @classmethod + def to_float(cls, stop_bits: Union[StopBits, None]) -> float: + """Converts an enum member to its float representation.""" + match stop_bits: + case cls.OneStop: + return 1.0 + case cls.OneAndHalfStop: + return 1.5 + case cls.TwoStop: + return 2.0 + case _: + return None + class FlowControl(IntEnum): """FlowControl for serial port.""" NoFlowControl = 0 # No flow control. @@ -278,13 +338,13 @@ def lrc(byte_arr: Union[bytes, bytearray]) -> int: def readMemBits(bitoffset: int, bitcount: int, memBuff: bytearray) -> bytearray: """Function for copy (read) values from memory input `mem_buff` and return it as output buffer for discretes (bits). - + Args: offset: Memory offset to read from `memBuff` in bit size. count: Count of bits to read from memory `memBuff`. memBuff: Memory buffer which holds data. memBitCount: Size of memory buffer `memBuff` in bits. - + Returns: bytearray with read bits packed into bytes. """ @@ -299,7 +359,7 @@ def readMemBits(bitoffset: int, bitcount: int, memBuff: bytearray) -> bytearray: c = len(byarray)-1 for i in range(c): b1 = byarray[i] - b2 = byarray[i+1] + b2 = byarray[i+1] b = ((b2 << (8-shift)) | (b1 >> shift)) & 0xFF byarray[i] = b if rem: @@ -314,14 +374,14 @@ def readMemBits(bitoffset: int, bitcount: int, memBuff: bytearray) -> bytearray: def writeMemBits(bitoffset: int, bitcount: int, value: Union[bytes, bytearray], memBuff: bytearray): """Function for copy (write) values from input buffer `values` to memory `mem_buff` for discretes (bits). - + Args: bitoffset: Memory offset to write to `memBuff` in bit size. bitcount: Count of bits to write into memory `memBuff`. value: Input buffer that holds data to write. memBuff: Memory buffer. memBitCount: Size of memory buffer `memBuff` in bits. - + Returns: None """ @@ -369,7 +429,7 @@ def bytesToAscii(bytes_buff: Union[bytes, bytearray]) -> bytes: Every byte of bytes_buff are repr as two bytes in output, where most signified tetrabits represented as leading byte in hex digit in ASCII encoding (upper) and less signified tetrabits represented as tailing byte in hex digit in ASCII encoding (upper). - + Returns: bytes array that is twice the size of input """ return bytes_buff.hex().upper().encode('ascii') @@ -379,7 +439,7 @@ def asciiToBytes(ascii_buff: Union[bytes, bytearray]) -> bytes: Every byte of output are repr as two bytes in `ascii_buff`, where most signified tetrabits represented as leading byte in hex digit in ASCII encoding (upper) and less signified tetrabits represented as tailing byte in hex digit in ASCII encoding (upper). - + Returns: bytes array that is half the size of input """ if isinstance(ascii_buff, (bytes, bytearray)): @@ -460,7 +520,7 @@ def toflowControl(s: str) -> FlowControl: def timer() -> int: """Get timer value in milliseconds.""" - return int(time.time() * 1000) + return int(time.perf_counter() * 1000) def currentTimestamp() -> int: """Get current timestamp in UNIX format in milliseconds.""" @@ -491,7 +551,7 @@ class Address: ## @brief Python set that contains supported Modbus Address types - MemoryTypeSet = { MemoryType.Memory_0x, + MemoryTypeSet = { MemoryType.Memory_0x, MemoryType.Memory_1x, MemoryType.Memory_3x, MemoryType.Memory_4x } @@ -694,7 +754,7 @@ def __lt__(self, other): @details Return self.toint() < other.toint() """ return self.toint() < other.toint() - + def __le__(self, other): """ @details Return self.toint() <= other.toint() @@ -712,7 +772,7 @@ def __ne__(self, other): @details Return self.toint() != other.toint() """ return self.toint() != other.toint() - + def __gt__(self, other): """ @details Return self.toint() > other.toint() @@ -749,14 +809,14 @@ def __iadd__(self, other: int): """ self.setoffset(self._offset + other) return self - + def __isub__(self, other: int): """ @details Decrease the offset by the given integer. """ self.setoffset(self._offset - other) return self - + def __repr__(self): """ @details Return the string representation of the object. @@ -776,13 +836,28 @@ def __init__(self, meth, *args, **kwargs): self._meth = meth self._args = args self._kwargs = kwargs + # print(f"AwaitableMethod created for method '{meth.__name__}' with args {args} and kwargs {kwargs}") def __await__(self): + """Returns self, an instance of AwaitableMethod, which itself is an iterator (has a __next__ method). + Thus, when an AwaitableMethod instance is awaited, the asyncio event loop will call __next__() repeatedly + until it either yields to the loop, returns a value, or raises StopIteration. + """ + # print(f"AwaitableMethod({self._meth.__name__}): __await__ called") return self - + def __next__(self): + """Makes this class an iterator, i.e. implements the iterator protocol. + """ + # print(f"AwaitableMethod({self._meth.__name__}).__next__ called") res = self._meth(*self._args, **self._kwargs) if res is None: + # print(f"AwaitableMethod({self._meth.__name__}).__next__ : method returned None") + # print(f"AwaitableMethod({self._meth.__name__}).__next__ : sleeping for 1 sec") + # time.sleep(1) + # print(f"AwaitableMethod({self._meth.__name__}).__next__ : yielding to event loop") return None + + # print(f"AwaitableMethod({self._meth.__name__}).__next__ : method returned {res}, raising StopIteration") raise StopIteration(res) - + diff --git a/libmodbuspy/serialport.py b/libmodbuspy/serialport.py index de77f61..160af12 100644 --- a/libmodbuspy/serialport.py +++ b/libmodbuspy/serialport.py @@ -6,6 +6,8 @@ """ import os +from typing import Union + import serial from .port import ModbusPort @@ -43,7 +45,7 @@ class Defaults: stopBits = StopBits.OneStop # Default value for the serial port's stop bits flowControl = FlowControl.NoFlowControl # Default value for the serial port's flow control timeoutFirstByte = 3000 # Default value for the serial port's timeout waiting first byte of packet - timeoutInterByte = 50 # Default value for the serial port's timeout waiting next byte of packet + timeoutInterByte = 5 # Default value for the serial port's timeout waiting next byte of packet @staticmethod def toSerialParity(parity:Parity) -> str: @@ -165,43 +167,43 @@ def DataBits(self, value: int) -> None: """Property. Set the number of data bits.""" return self.setDataBits(value) - def parity(self) -> Parity: + def parity(self) -> Union[Parity, None]: """Get the parity setting.""" return self._parity - def setParity(self, value: Parity): + def setParity(self, value: Union[Parity, None]): """Set the parity setting.""" if self._parity != value: self._parity = value self._changed = True @property - def Parity(self) -> Parity: + def Parity(self) -> Union[Parity, None]: """Property. Get the parity setting.""" return self.parity() @Parity.setter - def Parity(self, value: Parity) -> None: + def Parity(self, value: Union[Parity, None]) -> None: """Property. Set the parity setting.""" return self.setParity(value) - def stopBits(self) -> StopBits: + def stopBits(self) -> Union[StopBits, None]: """Get the number of stop bits.""" return self._stopBits - def setStopBits(self, value: StopBits): + def setStopBits(self, value: Union[StopBits, None]): """Set the number of stop bits.""" if self._stopBits != value: self._stopBits = value self._changed = True @property - def StopBits(self) -> StopBits: + def StopBits(self) -> Union[StopBits, None]: """Property. Get the number of stop bits.""" return self.stopBits() @StopBits.setter - def StopBits(self, value: StopBits) -> None: + def StopBits(self, value: Union[StopBits, None]) -> None: """Property. Set the number of stop bits.""" return self.setStopBits(value) diff --git a/libmodbuspy/serverresource.py b/libmodbuspy/serverresource.py index ae3fe77..1a2bc7d 100644 --- a/libmodbuspy/serverresource.py +++ b/libmodbuspy/serverresource.py @@ -20,7 +20,7 @@ class ModbusServerResource(ModbusServerPort): """Implements direct control for ModbusPort derived classes (TCP or serial) for server side. - ModbusServerResource derived from ModbusServerPort and makes ModbusPort object behaves + ModbusServerResource derived from ModbusServerPort and makes ModbusPort object behaves like server port. Pointer to ModbusPort object is passed to ModbusServerResource constructor. Also ModbusServerResource have ModbusInterface object as second parameter of constructor which @@ -29,10 +29,10 @@ class ModbusServerResource(ModbusServerPort): def __init__(self, port: ModbusPort, device: ModbusInterface): """Constructor of the class. - + Args: port: Pointer to the ModbusPort which is managed by the current class object. - device: Pointer to the ModbusInterface implementation to which all requests + device: Pointer to the ModbusInterface implementation to which all requests for Modbus functions are forwarded. """ super().__init__(device) @@ -53,10 +53,11 @@ def __init__(self, port: ModbusPort, device: ModbusInterface): self._outByteCount = 0 self._valueBuff = bytearray() self._value = 0 + self._registerSize = 2 # bytes per register; standard Modbus is 2, but some devices may use 4 (e.g. for floating point registers) def port(self) -> ModbusPort: """Returns pointer to inner port which was previously passed in constructor. - + Returns: The ModbusPort instance managed by this resource. """ @@ -66,7 +67,7 @@ def port(self) -> ModbusPort: def type(self) -> ProtocolType: """Returns type of Modbus protocol. Same as port().type(). - + Returns: The protocol type (TCP, RTU, or ASC). """ @@ -80,7 +81,7 @@ def setTimeout(self, timeout: int) -> None: def open(self) -> StatusCode: """Opens the underlying port for server operations. - + Returns: StatusCode indicating the result of the operation. """ @@ -98,7 +99,7 @@ def close(self) -> StatusCode: def isOpen(self) -> bool: """Checks if the underlying port is open. - + Returns: True if the port is open, False otherwise. """ @@ -197,7 +198,7 @@ def process(self) -> StatusCode: try: r = self._processDevice() if r is None: - return None + return None except ModbusException as e: toRead = (e.code == StatusCode.Status_BadGatewayPathUnavailable) r = e.code @@ -373,7 +374,7 @@ def _processInputData(self, buff: bytes) -> StatusCode: def _processDevice(self) -> StatusCode: """Transfer input request Modbus function to inner device and returns status of the operation. - + Returns: StatusCode indicating the result of device processing. """ @@ -414,14 +415,20 @@ def _processDevice(self) -> StatusCode: res = self._device.readFIFOQueue(self._unit, self._offset) else: self._raiseError(exceptions.IllegalFunctionError, "Unsupported function") + if res is None: return None - self._valueBuff = res + + if isinstance(res, tuple): + self._registerSize, self._valueBuff = res + else: + self._registerSize, self._valueBuff = (2, res) + return StatusCode.Status_Good def _processOutputData(self) -> bytearray: """Process output data buff with size and returns status of the operation. - + Returns: The output data buffer to send. """ @@ -433,11 +440,16 @@ def _processOutputData(self) -> bytearray: elif self._func in (MBF_READ_HOLDING_REGISTERS, MBF_READ_INPUT_REGISTERS, MBF_READ_WRITE_MULTIPLE_REGISTERS): - buff = bytearray(self._count * 2 + 1) - buff[0] = (self._count * 2) & 0xFF - for i in range(self._count): - buff[2 + i * 2] = self._valueBuff[i * 2 ] - buff[1 + i * 2] = self._valueBuff[i * 2 + 1] + buffsize = self._count * self._registerSize + buff = bytearray(buffsize + 1) + buff[0] = buffsize & 0xFF + if self._registerSize == 2: + for i in range(self._count): + buff[i*2 + 2] = self._valueBuff[i*2 ] + buff[i*2 + 1] = self._valueBuff[i*2 + 1] + else: + # Perform a byte-for-byte copy for non-standard register sizes (e.g. 4 bytes per register). + buff[1: buffsize + 1] = self._valueBuff[0: buffsize] elif self._func == MBF_WRITE_SINGLE_COIL: buff = bytearray(4) buff[0] = (self._offset >> 8) & 0xFF # address of coil (Hi-byte) @@ -509,12 +521,12 @@ def _processOutputData(self) -> bytearray: def _isBroadcast(self): """Returns True if the current request is a broadcast request, False otherwise. - + Returns: True if the current request is a broadcast, False otherwise. """ return self._unit == 0 and self.isBroadcastEnabled() - + def _setError(self, e, text: Optional[str] = None): self._isErrorPort = False super()._setError(e, text) @@ -534,7 +546,7 @@ def _raisePortError(self, e, text: Optional[str] = None): class ModbusAsyncServerResource(ModbusServerResource): """Asynchronous version of ModbusServerResource. - + All methods that can be blocking in ModbusServerResource are overridden here to return `AwaitableMethod` objects. """ @@ -542,6 +554,7 @@ def __init__(self, port: ModbusPort, device: ModbusInterface): if port.isBlocking(): port.setBlocking(False) super().__init__(port, device) + self._process = super().process def process(self): - return AwaitableMethod(super().process) \ No newline at end of file + return AwaitableMethod(self._process) diff --git a/libmodbuspy/tcpserver.py b/libmodbuspy/tcpserver.py index abe4ed6..0955231 100644 --- a/libmodbuspy/tcpserver.py +++ b/libmodbuspy/tcpserver.py @@ -460,5 +460,9 @@ class ModbusAsyncTcpServer(ModbusTcpServer): All methods that can be blocking in ModbusServerResource are overridden here to return `AwaitableMethod` objects. """ - def process(self): - return AwaitableMethod(super().process) \ No newline at end of file + def __init__(self, device: ModbusInterface): + super().__init__(device) + self._process = super().process + + def process(self) -> AwaitableMethod: + return AwaitableMethod(self._process) From e2e86e761a28d5d5fec6b10ead37ecd6d72047c4 Mon Sep 17 00:00:00 2001 From: bapowell Date: Mon, 11 May 2026 20:46:09 -0500 Subject: [PATCH 2/3] In ModbusClientPort, for some of the F (fmt) methods, call the extended method in the same class directly --- libmodbuspy/clientport.py | 286 +++++++++++++++++++------------------- 1 file changed, 144 insertions(+), 142 deletions(-) diff --git a/libmodbuspy/clientport.py b/libmodbuspy/clientport.py index f58894a..6f5d155 100644 --- a/libmodbuspy/clientport.py +++ b/libmodbuspy/clientport.py @@ -18,7 +18,7 @@ class ModbusClientPort(ModbusObject, ModbusInterface): """Base class for Modbus client ports. - + Signals: * `signalOpened(source:str)` - Emitted when the port is successfully opened. * `signalClosed(source:str)` - Emitted when the port is closed. @@ -26,7 +26,7 @@ class ModbusClientPort(ModbusObject, ModbusInterface): * `signalTx(source:str, data:bytes)` - Emitted when data is transmitted. * `signalRx(source:str, data:bytes)` - Emitted when data is received. """ - + class State(IntEnum): STATE_UNKNOWN = 0 STATE_BEGIN_OPEN = 1 @@ -80,26 +80,26 @@ def __init__(self, port: ModbusPort): def type(self) -> ProtocolType: """Returns the Modbus protocol type. - + Returns: The protocol type (TCP, RTU, or ASC). """ return self._port.type() - + def port(self) -> ModbusPort: """Returns the Modbus port instance.""" return self._port - + def setPort(self, port: ModbusPort): """Sets the Modbus port instance.""" self._port = port def open(self) -> StatusCode: """Opens the Modbus client port. - + Usually this method is called internally by the ModbusClient object. So, the user does not need to call it directly. - + Returns: * `StatusCode` indicating the result of the operation. * `None` when operation is not finished yet (only for nonblocking mode). @@ -111,7 +111,7 @@ def close(self) -> StatusCode: For network socket it shuts down connection (TCP) and closes the socket. For serial port it closes the port. - + Returns: * `StatusCode` indicating the result of the operation. * `None` when operation is not finished yet (only for nonblocking mode). @@ -120,23 +120,23 @@ def close(self) -> StatusCode: def isOpen(self) -> bool: """Checks if the Modbus client port is open. - + Returns: True if the port is open, False otherwise. """ return self._port.isOpen() - + def tries(self) -> int: """Returns the number of connection tries.""" return self._settings_tries - + def setTries(self, tries: int): """Sets the number of tries a Modbus request is repeated if it fails.""" self._settings_tries = tries def repeatCount(self) -> int: """Same as tries(). Used for backward compatibility. - + Returns: The number of connection tries. """ @@ -144,7 +144,7 @@ def repeatCount(self) -> int: def setRepeatCount(self, v: int) -> None: """Same as setTries(). Used for backward compatibility. - + Args: v: The number of tries to set. """ @@ -152,9 +152,9 @@ def setRepeatCount(self, v: int) -> None: def isBroadcastEnabled(self) -> bool: """Returns True if broadcast mode for '0' unit address is enabled, False otherwise. - + Broadcast mode for '0' unit address is required by Modbus protocol so it is enabled by default. - + Returns: True if broadcast mode is enabled, False otherwise. """ @@ -162,7 +162,7 @@ def isBroadcastEnabled(self) -> bool: def setBroadcastEnabled(self, enable: bool) -> None: """Enables broadcast mode for '0' unit address. It is enabled by default. - + Args: enable: True to enable broadcast mode, False to disable. """ @@ -170,7 +170,7 @@ def setBroadcastEnabled(self, enable: bool) -> None: def readCoils(self, unit: int, offset: int, count: int) -> bytes: return self._readCoils(self, unit, offset, count) - + def readDiscreteInputs(self, unit: int, offset: int, count: int) -> bytes: return self._readDiscreteInputs(self, unit, offset, count) @@ -191,22 +191,22 @@ def readExceptionStatus(self, unit: int) -> bytes: def diagnostics(self, unit: int, subfunc: int, indata: Optional[bytes] = None) -> bytes: return self._diagnostics(self, unit, subfunc, indata) - + def getCommEventCounter(self, unit: int) -> bytes: return self._getCommEventCounter(self, unit) def getCommEventLog(self, unit: int) -> bytes: return self._getCommEventLog(self, unit) - + def writeMultipleCoils(self, unit: int, offset: int, values: bytes, count: int = -1) -> StatusCode: return self._writeMultipleCoils(self, unit, offset, values, count) - + def writeMultipleRegisters(self, unit: int, offset: int, values: bytes) -> StatusCode: return self._writeMultipleRegisters(self, unit, offset, values) - + def reportServerID(self, unit: int) -> bytes: return self._reportServerID(self, unit) - + def maskWriteRegister(self, unit: int, offset: int, andMask: int, orMask: int) -> StatusCode: return self._maskWriteRegister(self, unit, offset, andMask, orMask) @@ -235,20 +235,20 @@ def readInputRegistersF(self, unit: int, offset: int, count: int, fmt: str=MB_FM def writeMultipleCoilsF(self, unit: int, offset: int, values: Tuple, count: int = -1, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: return self._writeMultipleCoilsF(self, unit, offset, values, count, fmt=fmt) - + def writeMultipleRegistersF(self, unit: int, offset: int, values: Tuple, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: return self._writeMultipleRegistersF(self, unit, offset, values, fmt=fmt) - + def readWriteMultipleRegistersF(self, unit: int, readOffset: int, readCount: int, writeOffset: int, writeValues: Tuple, fmt: str=MB_FMT_UINT16_LE) -> Tuple: return self._readWriteMultipleRegistersF(self, unit, readOffset, readCount, writeOffset, writeValues, fmt=fmt) - + # Status methods - + def lastStatus(self) -> StatusCode: """Returns the status of the last operation performed. - + Returns: StatusCode of the last operation. """ @@ -256,7 +256,7 @@ def lastStatus(self) -> StatusCode: def lastStatusTimestamp(self) -> int: """Returns the timestamp of the last operation performed. - + Returns: Timestamp of the last operation in milliseconds. """ @@ -264,7 +264,7 @@ def lastStatusTimestamp(self) -> int: def lastErrorStatus(self) -> StatusCode: """Returns the status of the last error of the performed operation. - + Returns: StatusCode of the last error. """ @@ -272,7 +272,7 @@ def lastErrorStatus(self) -> StatusCode: def lastErrorText(self) -> str: """Returns the text of the last error of the performed operation. - + Returns: Text description of the last error. """ @@ -280,7 +280,7 @@ def lastErrorText(self) -> str: def lastTries(self) -> int: """Returns statistics of the count of tries already processed. - + Returns: Number of tries that were processed for the last operation. """ @@ -288,7 +288,7 @@ def lastTries(self) -> int: def lastRepeatCount(self) -> int: """Same as lastTries(). - + Returns: Number of tries that were processed for the last operation. """ @@ -296,7 +296,7 @@ def lastRepeatCount(self) -> int: def currentClient(self) -> ModbusObject: """Returns a pointer to the client object whose request is currently being processed by the current port. - + Returns: The ModbusObject client currently being processed, or None if no client is active. """ @@ -304,17 +304,17 @@ def currentClient(self) -> ModbusObject: def getRequestStatus(self, client: ModbusObject) -> 'RequestStatus': """Returns status the current request for client. - - The client usually calls this function to determine whether its request is + + The client usually calls this function to determine whether its request is pending/finished/blocked. - + Args: client: The client object to check status for. - + Returns: RequestStatus indicating: - Enable: client has just became current and can make request to the port - - Process: current client is already processing + - Process: current client is already processing - Disable: other client owns the port """ if self._currentClient is None: @@ -330,10 +330,10 @@ def getName(self) -> str: if self._currentClient is None: return self.objectName() return self._currentClient.objectName() - + def cancelRequest(self, client: ModbusObject) -> None: """Cancels the previous request specified by the client. - + Args: client: The client object whose request should be cancelled. """ @@ -342,25 +342,27 @@ def cancelRequest(self, client: ModbusObject) -> None: # formatting methods (extended) def _readCoilsF(self, client: ModbusObject, unit: int, offset: int, count: int, fmt: str=MB_FMT_UINT16_LE) -> Tuple: - buff = self._readCoils(client, unit, offset, count) + # Call the method in this class directly, rather than calling a child/overridden method, e.g. in ModbusAsyncClientPort. + # buff = self._readCoils(client, unit, offset, count) + buff = ModbusClientPort._readCoils(self, client, unit, offset, count) if buff is None: return None return unpack(fmt, buff) def _readDiscreteInputsF(self, client: ModbusObject, unit: int, offset: int, count: int, fmt: str=MB_FMT_UINT16_LE) -> Tuple: - buff = self._readDiscreteInputs(client, unit, offset, count) + buff = ModbusClientPort._readDiscreteInputs(self, client, unit, offset, count) if buff is None: return None return unpack(fmt, buff) def _readHoldingRegistersF(self, client: ModbusObject, unit: int, offset: int, count: int, fmt: str=MB_FMT_UINT16_LE) -> Tuple: - buff = self._readHoldingRegisters(client, unit, offset, count) + buff = ModbusClientPort._readHoldingRegisters(self, client, unit, offset, count) if buff is None: return None return unpack(fmt, buff) def _readInputRegistersF(self, client: ModbusObject, unit: int, offset: int, count: int, fmt: str=MB_FMT_UINT16_LE) -> Tuple: - buff = self._readInputRegisters(client, unit, offset, count) + buff = ModbusClientPort._readInputRegisters(self, client, unit, offset, count) if buff is None: return None return unpack(fmt, buff) @@ -370,13 +372,13 @@ def _writeMultipleCoilsF(self, client: ModbusObject, unit: int, offset: int, val return self._writeMultipleCoils(client, unit, offset, pack(fmt, values), count) elif self._currentClient == client: return self._writeMultipleCoils(client, unit, offset, bytes(), count) - + def _writeMultipleRegistersF(self, client: ModbusObject, unit: int, offset: int, values: Tuple, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: if self._currentClient is None: - return self._writeMultipleCoils(client, unit, offset, pack(fmt, values)) + return self._writeMultipleRegisters(client, unit, offset, pack(fmt, values)) elif self._currentClient == client: - return self._writeMultipleCoils(client, unit, offset, bytes()) - + return self._writeMultipleRegisters(client, unit, offset, bytes()) + def _readWriteMultipleRegistersF(self, client: ModbusObject, unit: int, readOffset: int, readCount: int, writeOffset: int, writeValues: Tuple, fmt: str=MB_FMT_UINT16_LE) -> Tuple: if self._currentClient is None: @@ -388,22 +390,22 @@ def _readWriteMultipleRegistersF(self, client: ModbusObject, unit: int, readOffs if buff is None: return None return unpack(fmt, buff) - + # extended methods def _readCoils(self, client: ModbusObject, unit: int, offset: int, count: int) -> bytes: """Read coils from Modbus device. - + Args: client: The client object making the request. unit: Modbus unit/slave address. offset: Starting address of coils to read. count: Number of coils to read. - + Returns: Bytes containing the coil values. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: if count > MB_MAX_DISCRETS: self.cancelRequest(client) @@ -415,7 +417,7 @@ def _readCoils(self, client: ModbusObject, unit: int, offset: int, count: int) - self._buff[2] = (count >> 8) & 0xFF # Quantity of coils - MS BYTE self._buff[3] = count & 0xFF # Quantity of coils - LS BYTE self._count = count - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_READ_COILS, self._buff) if buff is None: @@ -434,21 +436,21 @@ def _readCoils(self, client: ModbusObject, unit: int, offset: int, count: int) - return bytes(buff[1:fcBytes]) else: return None - + def _readDiscreteInputs(self, client:ModbusObject, unit: int, offset: int, count: int) -> bytes: """Read discrete inputs from Modbus device. - + Args: client: The client object making the request. unit: Modbus unit/slave address. offset: Starting address of discrete input to read. count: Number of discrete input to read. - + Returns: Bytes containing the discrete input values. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: if count > MB_MAX_DISCRETS: self.cancelRequest(client) @@ -460,7 +462,7 @@ def _readDiscreteInputs(self, client:ModbusObject, unit: int, offset: int, count self._buff[2] = (count >> 8) & 0xFF # Quantity of discrete inputs - MS BYTE self._buff[3] = count & 0xFF # Quantity of discrete inputs - LS BYTE self._count = count - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_READ_DISCRETE_INPUTS, self._buff) if buff is None: @@ -477,10 +479,10 @@ def _readDiscreteInputs(self, client:ModbusObject, unit: int, offset: int, count return bytes(buff[1:fcBytes]) else: return None - + def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, count: int) -> Union[bytes, None]: """Read holding registers from Modbus device. - + Args: client: The client object making the request. unit: Modbus unit/slave address. @@ -490,8 +492,8 @@ def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, cou Returns: Bytes containing the holding register values, or None. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: if count > MB_MAX_REGISTERS: self.cancelRequest(client) @@ -503,7 +505,7 @@ def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, cou self._buff[2] = (count >> 8) & 0xFF # Quantity of holding registers - MS BYTE self._buff[3] = count & 0xFF # Quantity of holding registers - LS BYTE self._count = count - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_READ_HOLDING_REGISTERS, self._buff) if buff is None: @@ -531,10 +533,10 @@ def _readHoldingRegisters(self, client:ModbusObject, unit: int, offset: int, cou return bytes(values) else: return None - + def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count: int) -> Union[bytes, None]: """Read input registers from Modbus device. - + Args: client: The client object making the request. unit: Modbus unit/slave address. @@ -544,8 +546,8 @@ def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count Returns: Bytes containing the input register values, or None. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: if count > MB_MAX_REGISTERS: self.cancelRequest(client) @@ -557,7 +559,7 @@ def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count self._buff[2] = (count >> 8) & 0xFF # Quantity of input registers - MS BYTE self._buff[3] = count & 0xFF # Quantity of input registers - LS BYTE self._count = count - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_READ_INPUT_REGISTERS, self._buff) if buff is None: @@ -585,7 +587,7 @@ def _readInputRegisters(self, client:ModbusObject, unit: int, offset: int, count return bytes(values) else: return None - + def _writeSingleCoil(self, client:ModbusObject, unit: int, offset: int, value: bool) -> StatusCode: """Write a single coil to Modbus device. @@ -598,8 +600,8 @@ def _writeSingleCoil(self, client:ModbusObject, unit: int, offset: int, value: b Returns: StatusCode of the operation. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray(4) @@ -608,7 +610,7 @@ def _writeSingleCoil(self, client:ModbusObject, unit: int, offset: int, value: b self._buff[2] = 0xFF if value else 0 # Value - 0xFF if true, 0x00 if false self._buff[3] = 0 # Value - must always be NULL self._offset = offset - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_WRITE_SINGLE_COIL, self._buff) if buff is None: @@ -624,7 +626,7 @@ def _writeSingleCoil(self, client:ModbusObject, unit: int, offset: int, value: b return StatusCode.Status_Good else: return None - + def _writeSingleRegister(self, client:ModbusObject, unit: int, offset: int, value: int) -> StatusCode: """Write a single register to Modbus device. @@ -637,8 +639,8 @@ def _writeSingleRegister(self, client:ModbusObject, unit: int, offset: int, valu Returns: StatusCode of the operation. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray(4) @@ -648,7 +650,7 @@ def _writeSingleRegister(self, client:ModbusObject, unit: int, offset: int, valu self._buff[3] = value & 0xFF # Value - LS BYTE self._offset = offset self._value = value - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_WRITE_SINGLE_REGISTER, self._buff) if buff is None: @@ -667,7 +669,7 @@ def _writeSingleRegister(self, client:ModbusObject, unit: int, offset: int, valu return StatusCode.Status_Good else: return None - + def _readExceptionStatus(self, client:ModbusObject, unit: int) -> bytes: """Read exception status from Modbus device. @@ -679,12 +681,12 @@ def _readExceptionStatus(self, client:ModbusObject, unit: int) -> bytes: Returns: `bytes` array with single byte that containing the exception status. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray() - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_READ_EXCEPTION_STATUS, self._buff) if buff is None: @@ -697,7 +699,7 @@ def _readExceptionStatus(self, client:ModbusObject, unit: int) -> bytes: return bytes(buff) else: return None - + def _diagnostics(self, client:ModbusObject, unit: int, subfunc: int, indata: Optional[bytes] = None) -> bytes: """Perform diagnostics on Modbus device. @@ -709,8 +711,8 @@ def _diagnostics(self, client:ModbusObject, unit: int, subfunc: int, indata: Opt Returns: Bytes containing the response data. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray(2) @@ -719,7 +721,7 @@ def _diagnostics(self, client:ModbusObject, unit: int, subfunc: int, indata: Opt if indata is not None: self._buff[2:] = indata self._subfunc = subfunc - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_DIAGNOSTICS, self._buff) if buff is None: @@ -735,7 +737,7 @@ def _diagnostics(self, client:ModbusObject, unit: int, subfunc: int, indata: Opt return bytes(buff[2:]) else: return None - + def _getCommEventCounter(self, client:ModbusObject, unit: int) -> bytes: """ Get communication event counter from Modbus device. @@ -747,12 +749,12 @@ def _getCommEventCounter(self, client:ModbusObject, unit: int) -> bytes: Bytes containing the communication status and event counter values, where first two bytes are status and next two bytes are event count. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray() - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_GET_COMM_EVENT_COUNTER, self._buff) if buff is None: @@ -770,7 +772,7 @@ def _getCommEventCounter(self, client:ModbusObject, unit: int) -> bytes: return bytes(values) else: return None - + def _getCommEventLog(self, client:ModbusObject, unit: int) -> bytes: """Get communication event log from Modbus device. @@ -779,16 +781,16 @@ def _getCommEventLog(self, client:ModbusObject, unit: int) -> bytes: unit: Modbus unit/slave address. Returns: - Bytes containing the communication event log data, + Bytes containing the communication event log data, where first two bytes are status, next two bytes are event count, next two bytes are message count, and the rest is event log data. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray() - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_GET_COMM_EVENT_LOG, self._buff) if buff is None: @@ -812,7 +814,7 @@ def _getCommEventLog(self, client:ModbusObject, unit: int) -> bytes: return bytes(values) else: return None - + def _writeMultipleCoils(self, client:ModbusObject, unit: int, offset: int, values: bytes, count: int = -1) -> StatusCode: """Write multiple coils to Modbus device. @@ -826,7 +828,7 @@ def _writeMultipleCoils(self, client:ModbusObject, unit: int, offset: int, value Returns: StatusCode of the operation. """ - status = self.getRequestStatus(client) + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: if count < 0: count = len(values) * 8 @@ -844,7 +846,7 @@ def _writeMultipleCoils(self, client:ModbusObject, unit: int, offset: int, value self._buff[5:] = values[0:byteCount] # Coil values self._offset = offset self._count = count - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_WRITE_MULTIPLE_COILS, self._buff) if buff is None: @@ -863,7 +865,7 @@ def _writeMultipleCoils(self, client:ModbusObject, unit: int, offset: int, value return StatusCode.Status_Good else: return None - + def _writeMultipleRegisters(self, client:ModbusObject, unit: int, offset: int, values: bytes) -> StatusCode: """Write multiple registers to Modbus device. @@ -877,7 +879,7 @@ def _writeMultipleRegisters(self, client:ModbusObject, unit: int, offset: int, v Returns: StatusCode of the operation. """ - status = self.getRequestStatus(client) + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: count = len(values) // 2 if count > MB_MAX_REGISTERS: @@ -896,7 +898,7 @@ def _writeMultipleRegisters(self, client:ModbusObject, unit: int, offset: int, v self._buff[6+i*2] = values[i*2 ] # Register value - MS BYTE self._offset = offset self._count = count - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_WRITE_MULTIPLE_REGISTERS, self._buff) if buff is None: @@ -915,7 +917,7 @@ def _writeMultipleRegisters(self, client:ModbusObject, unit: int, offset: int, v return StatusCode.Status_Good else: return None - + def _reportServerID(self, client:ModbusObject, unit: int) -> bytes: """Report server ID from Modbus device. @@ -926,12 +928,12 @@ def _reportServerID(self, client:ModbusObject, unit: int) -> bytes: Returns: Bytes containing the server ID data. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray() - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_REPORT_SERVER_ID, self._buff) if buff is None: @@ -947,7 +949,7 @@ def _reportServerID(self, client:ModbusObject, unit: int) -> bytes: return bytes(buff[1:]) else: return None - + def _maskWriteRegister(self, client:ModbusObject, unit: int, offset: int, andMask: int, orMask: int) -> StatusCode: """Mask write register on Modbus device. @@ -961,7 +963,7 @@ def _maskWriteRegister(self, client:ModbusObject, unit: int, offset: int, andMas Returns: StatusCode of the operation. """ - status = self.getRequestStatus(client) + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: self._buff = bytearray(6) self._buff[0] = (offset >> 8) & 0xFF # Start register offset - MS BYTE @@ -973,7 +975,7 @@ def _maskWriteRegister(self, client:ModbusObject, unit: int, offset: int, andMas self._offset = offset self._andMask = andMask self._orMask = orMask - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_MASK_WRITE_REGISTER, self._buff) if buff is None: @@ -999,7 +1001,7 @@ def _maskWriteRegister(self, client:ModbusObject, unit: int, offset: int, andMas def _readWriteMultipleRegisters(self, client:ModbusObject, unit: int, readOffset: int, readCount: int, writeOffset: int, writeValues: bytes) -> bytes: """Read/Write multiple registers on Modbus device.""" - status = self.getRequestStatus(client) + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: writeCount = len(writeValues) // 2 if readCount > MB_MAX_REGISTERS or writeCount > MB_MAX_REGISTERS: @@ -1045,7 +1047,7 @@ def _readWriteMultipleRegisters(self, client:ModbusObject, unit: int, readOffset return bytes(values) else: return None - + def _readFIFOQueue(self, client:ModbusObject, unit: int, fifoadr: int) -> bytes: """Read FIFO queue from Modbus device. @@ -1057,14 +1059,14 @@ def _readFIFOQueue(self, client:ModbusObject, unit: int, fifoadr: int) -> bytes: Returns: Bytes containing the FIFO queue values. """ - - status = self.getRequestStatus(client) + + status = self.getRequestStatus(client) if status == ModbusClientPort.RequestStatus.Enable: # Prepare request buffer self._buff = bytearray(2) self._buff[0] = (fifoadr >> 8) & 0xFF # Start register offset - MS BYTE self._buff[1] = fifoadr & 0xFF # Start register offset - LS BYTE - status = ModbusClientPort.RequestStatus.Process + status = ModbusClientPort.RequestStatus.Process if status == ModbusClientPort.RequestStatus.Process: buff = self._request(unit, MBF_READ_FIFO_QUEUE, self._buff) if buff is None: @@ -1093,12 +1095,12 @@ def _readFIFOQueue(self, client:ModbusObject, unit: int, fifoadr: int) -> bytes: def _request(self, unit: int, func: int, buff: bytes) -> Union[bytes, None]: """The function builds the packet that the write() function puts into the buffer. - + Args: unit: Modbus unit/slave address. func: Modbus function code. buff: Buffer containing the data to write. - + Returns: Response payload as bytes, or None. """ @@ -1285,16 +1287,16 @@ def _freeWriteBuffer(self): def _setStatus(self, status: StatusCode): """Sets the status parameters of the last operation performed. - + Args: status: StatusCode of the last operation. """ self._lastStatus = status - self._lastStatusTimestamp = currentTimestamp() + self._lastStatusTimestamp = currentTimestamp() def _setError(self, exc, text: str = ""): """Sets the error parameters of the last operation performed. - + Args: exc: Type of the ModbusException to raise. text: Text description of the error (optional). @@ -1304,7 +1306,7 @@ def _setError(self, exc, text: str = ""): def _raiseError(self, exc, text: str = ""): """Sets the error parameters of the last operation performed and raises the exception. - + Args: exc: Type of the ModbusException to raise. text: Text description of the error (optional). @@ -1314,7 +1316,7 @@ def _raiseError(self, exc, text: str = ""): def _setPortError(self, exc, text: str = ""): """Sets the error parameters of the last operation performed. - + Args: status: StatusCode of the last error. text: Text description of the error (optional). @@ -1324,7 +1326,7 @@ def _setPortError(self, exc, text: str = ""): def _raisePortError(self, exc, text: str = ""): """Sets the error parameters of the last operation performed and raises the exception. - + Args: exc: Type of the ModbusException to raise. text: Text description of the error (optional). @@ -1334,7 +1336,7 @@ def _raisePortError(self, exc, text: str = ""): def _setErrorBase(self, exc, text: str = ""): """Sets the error parameters of the last operation performed. - + Args: exc: Type of the ModbusException to raise. text: Text description of the error (optional). @@ -1353,7 +1355,7 @@ def _setErrorBase(self, exc, text: str = ""): def _raiseErrorBase(self, exc, text: str = ""): """Sets the error parameters of the last operation performed and raises the exception. - + Args: exc: Type of the ModbusException to raise. text: Text description of the error (optional). @@ -1384,13 +1386,13 @@ def __init__(self, port: ModbusPort): def open(self) -> StatusCode: return AwaitableMethod(super().open) - + def close(self) -> StatusCode: return AwaitableMethod(super().close) - + def readCoils(self, unit: int, offset: int, count: int) -> bytes: return AwaitableMethod(super().readCoils, unit, offset, count) - + def readDiscreteInputs(self, unit: int, offset: int, count: int) -> bytes: return AwaitableMethod(super().readDiscreteInputs, unit, offset, count) @@ -1411,22 +1413,22 @@ def readExceptionStatus(self, unit: int) -> bytes: def diagnostics(self, unit: int, subfunc: int, indata: Optional[bytes] = None) -> bytes: return AwaitableMethod(super().diagnostics, unit, subfunc, indata) - + def getCommEventCounter(self, unit: int) -> bytes: return AwaitableMethod(super().getCommEventCounter, unit) def getCommEventLog(self, unit: int) -> bytes: return AwaitableMethod(super().getCommEventLog, unit) - + def writeMultipleCoils(self, unit: int, offset: int, values: bytes, count: int = -1) -> StatusCode: return AwaitableMethod(super().writeMultipleCoils, unit, offset, values, count) - + def writeMultipleRegisters(self, unit: int, offset: int, values: bytes) -> StatusCode: return AwaitableMethod(super().writeMultipleRegisters, unit, offset, values) - + def reportServerID(self, unit: int) -> bytes: return AwaitableMethod(super().reportServerID, unit) - + def maskWriteRegister(self, unit: int, offset: int, andMask: int, orMask: int) -> StatusCode: return AwaitableMethod(super().maskWriteRegister, unit, offset, andMask, orMask) @@ -1455,10 +1457,10 @@ def readInputRegistersF(self, unit: int, offset: int, count: int, fmt: str=MB_FM def writeMultipleCoilsF(self, unit: int, offset: int, values: Tuple, count: int = -1, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: return AwaitableMethod(super().writeMultipleCoilsF, unit, offset, values, count, fmt=fmt) - + def writeMultipleRegistersF(self, unit: int, offset: int, values: Tuple, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: return AwaitableMethod(super().writeMultipleRegistersF, unit, offset, values, fmt=fmt) - + def readWriteMultipleRegistersF(self, unit: int, readOffset: int, readCount: int, writeOffset: int, writeValues: Tuple, @@ -1471,7 +1473,7 @@ def readWriteMultipleRegistersF(self, unit: int, # low-level methods def _readCoils(self, client:ModbusObject, unit: int, offset: int, count: int) -> bytes: return AwaitableMethod(super()._readCoils, client, unit, offset, count) - + def _readDiscreteInputs(self, client:ModbusObject, unit: int, offset: int, count: int) -> bytes: return AwaitableMethod(super()._readDiscreteInputs, client, unit, offset, count) @@ -1492,22 +1494,22 @@ def _readExceptionStatus(self, client:ModbusObject, unit: int) -> bytes: def _diagnostics(self, client:ModbusObject, unit: int, subfunc: int, indata: Optional[bytes] = None) -> bytes: return AwaitableMethod(super()._diagnostics, client, unit, subfunc, indata) - + def _getCommEventCounter(self, client:ModbusObject, unit: int) -> bytes: return AwaitableMethod(super()._getCommEventCounter, client, unit) def _getCommEventLog(self, client:ModbusObject, unit: int) -> bytes: return AwaitableMethod(super()._getCommEventLog, client, unit) - + def _writeMultipleCoils(self, client:ModbusObject, unit: int, offset: int, values: bytes, count: int = -1) -> StatusCode: return AwaitableMethod(super()._writeMultipleCoils, client, unit, offset, values, count) - + def _writeMultipleRegisters(self, client:ModbusObject, unit: int, offset: int, values: bytes) -> StatusCode: return AwaitableMethod(super()._writeMultipleRegisters, client, unit, offset, values) - + def _reportServerID(self, client:ModbusObject, unit: int) -> bytes: return AwaitableMethod(super()._reportServerID, client, unit) - + def _maskWriteRegister(self, client:ModbusObject, unit: int, offset: int, andMask: int, orMask: int) -> StatusCode: return AwaitableMethod(super()._maskWriteRegister, client, unit, offset, andMask, orMask) @@ -1527,7 +1529,7 @@ def _readCoilsF(self, client:ModbusObject, unit: int, offset: int, count: int, f def _readDiscreteInputsF(self, client:ModbusObject, unit: int, offset: int, count: int, fmt: str=MB_FMT_UINT16_LE) -> Tuple: return AwaitableMethod(super()._readDiscreteInputsF, client, unit, offset, count, fmt=fmt) - + def _readHoldingRegistersF(self, client:ModbusObject, unit: int, offset: int, count: int, fmt: str=MB_FMT_UINT16_LE) -> Tuple: return AwaitableMethod(super()._readHoldingRegistersF, client, unit, offset, count, fmt=fmt) @@ -1536,10 +1538,10 @@ def _readInputRegistersF(self, client:ModbusObject, unit: int, offset: int, coun def _writeMultipleCoilsF(self, client:ModbusObject, unit: int, offset: int, values: Tuple, count: int = -1, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: return AwaitableMethod(super()._writeMultipleCoilsF, client, unit, offset, values, count, fmt=fmt) - + def _writeMultipleRegistersF(self, client:ModbusObject, unit: int, offset: int, values: Tuple, fmt: str=MB_FMT_UINT16_LE) -> StatusCode: return AwaitableMethod(super()._writeMultipleRegistersF, client, unit, offset, values, fmt=fmt) - + def _readWriteMultipleRegistersF(self, client:ModbusObject, unit: int, readOffset: int, readCount: int, writeOffset: int, writeValues: Tuple, From b7ccc4499eaf0bcd0645691206307baf27074e6b Mon Sep 17 00:00:00 2001 From: bapowell Date: Wed, 27 May 2026 11:14:59 -0500 Subject: [PATCH 3/3] Added preTxDelay and postTxDelay options to ModbusSerialPort --- libmodbuspy/clientport.py | 2 +- libmodbuspy/serialport.py | 112 ++++++++++++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/libmodbuspy/clientport.py b/libmodbuspy/clientport.py index 6f5d155..a6afe14 100644 --- a/libmodbuspy/clientport.py +++ b/libmodbuspy/clientport.py @@ -1151,7 +1151,7 @@ def _request(self, unit: int, func: int, buff: bytes) -> Union[bytes, None]: return bytes() def _process(self) -> StatusCode: - """The function processes the packet that the read() function puts into the buffer. + """Cycle through the state machine to send the request and then read the response. """ fRepeatAgain = True while fRepeatAgain: diff --git a/libmodbuspy/serialport.py b/libmodbuspy/serialport.py index 160af12..745a4b0 100644 --- a/libmodbuspy/serialport.py +++ b/libmodbuspy/serialport.py @@ -6,6 +6,7 @@ """ import os +from time import sleep from typing import Union import serial @@ -35,6 +36,8 @@ class Strings: timeoutFirstByte = "timeoutFirstByte" # String key of setting 'Serial port timeout waiting first byte of packet' timeoutInterByte = "timeoutInterByte" # String key of setting 'Serial port timeout waiting next byte of packet' timeout = "timeout" # String key of setting 'Serial port timeout waiting first byte of packet' + preTxDelay = "preTxDelay" # String key of setting 'Serial port delay before write' + postTxDelay = "postTxDelay" # String key of setting 'Serial port delay after write' class Defaults: """Default serial port settings.""" @@ -45,7 +48,9 @@ class Defaults: stopBits = StopBits.OneStop # Default value for the serial port's stop bits flowControl = FlowControl.NoFlowControl # Default value for the serial port's flow control timeoutFirstByte = 3000 # Default value for the serial port's timeout waiting first byte of packet - timeoutInterByte = 5 # Default value for the serial port's timeout waiting next byte of packet + timeoutInterByte = 5 # Default value for the serial port's timeout waiting next byte of packet + preTxDelay = 0 # Default value for the serial port's delay before write (ms) + postTxDelay = 0 # Default value for the serial port's delay after write (ms) @staticmethod def toSerialParity(parity:Parity) -> str: @@ -87,6 +92,8 @@ def __init__(self, blocking: bool = True): self._flowControl = d.flowControl self._timeout = d.timeoutFirstByte self._timeoutInterByte = d.timeoutInterByte + self._preTxDelay = d.preTxDelay + self._postTxDelay = d.postTxDelay # Serial object self._serial = serial.Serial() # Other internal variables @@ -265,6 +272,46 @@ def TimeoutInterByte(self, value: int) -> None: """Property. Set the timeout for the inter-byte delay.""" return self.setTimeoutInterByte(value) + def preTxDelay(self) -> int: + """Get the delay before transmitting data.""" + return self._preTxDelay + + def setPreTxDelay(self, value: int): + """Set the delay before transmitting data.""" + if self._preTxDelay != value: + self._preTxDelay = value + self._changed = True + + @property + def PreTxDelay(self) -> int: + """Property. Get the delay before transmitting data.""" + return self.preTxDelay() + + @PreTxDelay.setter + def PreTxDelay(self, value: int) -> None: + """Property. Set the delay before transmitting data.""" + return self.setPreTxDelay(value) + + def postTxDelay(self) -> int: + """Get the delay after transmitting data.""" + return self._postTxDelay + + def setPostTxDelay(self, value: int): + """Set the delay after transmitting data.""" + if self._postTxDelay != value: + self._postTxDelay = value + self._changed = True + + @property + def PostTxDelay(self) -> int: + """Property. Get the delay after transmitting data.""" + return self.postTxDelay() + + @PostTxDelay.setter + def PostTxDelay(self, value: int) -> None: + """Property. Set the delay after transmitting data.""" + return self.setPostTxDelay(value) + def settings(self) -> dict: s = ModbusSerialPort.Strings return { @@ -276,7 +323,9 @@ def settings(self) -> dict: s.flowControl : self._flowControl , #s.timeoutFirstByte : self._timeoutFirstByte , s.timeoutInterByte : self._timeoutInterByte , - s.timeout : self._timeout + s.timeout : self._timeout , + s.preTxDelay : self._preTxDelay , + s.postTxDelay : self._postTxDelay , } def setSettings(self, settings: dict): @@ -308,12 +357,18 @@ def setSettings(self, settings: dict): v = settings.get(s.timeout, None) if v is not None: self.setTimeout(v) + v = settings.get(s.preTxDelay, None) + if v is not None: + self.setPreTxDelay(v) + v = settings.get(s.postTxDelay, None) + if v is not None: + self.setPostTxDelay(v) def isOpen(self) -> bool: """Check if the serial port is open.""" return self._serial.is_open - def open(self) -> StatusCode: + def open(self) -> StatusCode | None: fRepeatAgain = True while fRepeatAgain: fRepeatAgain = False @@ -376,17 +431,23 @@ def close(self) -> StatusCode: self._state = ModbusPort.State.STATE_CLOSED return StatusCode.Status_Good - def write(self) -> StatusCode: + def write(self) -> StatusCode | None: return self._writeMethod() - def read(self) -> StatusCode: + def read(self) -> StatusCode | None: return self._readMethod() def _blockingWrite(self) -> StatusCode: self._state = ModbusPort.State.STATE_OPENED try: self._serial.reset_input_buffer() + if self._preTxDelay > 0: + # print(f"Delaying {self._preTxDelay} ms before transmitting data...") + sleep(self._preTxDelay / 1000.0) # ms to seconds self._serial.write(self._buff) + if self._postTxDelay > 0: + # print(f"Delaying {self._postTxDelay} ms after transmitting data...") + sleep(self._postTxDelay / 1000.0) # ms to seconds except serial.SerialException as e: self._raiseError(StatusCode.Status_BadSerialWrite, f"Error while writing '{self._portName}' serial port. Error: {str(e)}") return StatusCode.Status_Good @@ -404,26 +465,45 @@ def _blockingRead(self) -> StatusCode: self._raiseError(StatusCode.Status_BadSerialRead, f"Error while reading '{self._portName}' serial port. Error: {str(e)}") return StatusCode.Status_Good - def _nonBlockingWrite(self) -> StatusCode: + def _nonBlockingWrite(self) -> StatusCode | None: fRepeatAgain = True while fRepeatAgain: fRepeatAgain = False - if self._state in (ModbusPort.State.STATE_OPENED, - ModbusPort.State.STATE_PREPARE_TO_WRITE): + if self._state == ModbusPort.State.STATE_OPENED: self._timestampRefresh() - self._state = ModbusPort.State.STATE_WAIT_FOR_WRITE + self._state = ModbusPort.State.STATE_PREPARE_TO_WRITE + # if self._preTxDelay > 0: + # print(f"Delaying (async) {self._preTxDelay} ms before transmitting data...") fRepeatAgain = True continue - elif self._state in (ModbusPort.State.STATE_WAIT_FOR_WRITE, - ModbusPort.State.STATE_WAIT_FOR_WRITE_ALL): - # Note: clean read buffer from garbage before write + elif self._state == ModbusPort.State.STATE_PREPARE_TO_WRITE: + if (self._preTxDelay <= 0) or ((timer() - self._timestamp) >= self._preTxDelay): + self._state = ModbusPort.State.STATE_WAIT_FOR_WRITE + # Otherwise, continue cycling here until preTxDelay has elapsed. + fRepeatAgain = True + continue + elif self._state == ModbusPort.State.STATE_WAIT_FOR_WRITE: try: - self._serial.reset_input_buffer() + self._serial.reset_input_buffer() # clean read buffer from garbage before write + # print("Writing data (async)...") self._serial.write(self._buff) - self._state = ModbusPort.State.STATE_OPENED - return StatusCode.Status_Good + self._state = ModbusPort.State.STATE_WAIT_FOR_WRITE_ALL + if self._postTxDelay > 0: + self._timestampRefresh() + # print(f"Delaying (async) {self._postTxDelay} ms after transmitting data...") + fRepeatAgain = True + continue except serial.SerialException as e: self._raiseError(exceptions.SerialWriteError, f"Error while writing '{self._portName}' serial port. Error: {str(e)}") + elif self._state == ModbusPort.State.STATE_WAIT_FOR_WRITE_ALL: + if (self._postTxDelay <= 0) or ((timer() - self._timestamp) >= self._postTxDelay): + # print("Done with nonBlockingWrite...") + self._state = ModbusPort.State.STATE_OPENED + return StatusCode.Status_Good + else: + # Continue cycling here until postTxDelay has elapsed. + fRepeatAgain = True + continue else: if self.isOpen(): self._state = ModbusPort.State.STATE_OPENED @@ -432,7 +512,7 @@ def _nonBlockingWrite(self) -> StatusCode: self._raiseError(exceptions.SerialWriteError, "Internal error") return None - def _nonBlockingRead(self) -> StatusCode: + def _nonBlockingRead(self) -> StatusCode | None: fRepeatAgain = True while fRepeatAgain: fRepeatAgain = False