initial
This commit is contained in:
572
env/lib/python3.11/site-packages/pymodbus/transaction.py
vendored
Normal file
572
env/lib/python3.11/site-packages/pymodbus/transaction.py
vendored
Normal file
@@ -0,0 +1,572 @@
|
||||
"""Collection of transaction based abstractions."""
|
||||
|
||||
__all__ = [
|
||||
"FifoTransactionManager",
|
||||
"DictTransactionManager",
|
||||
"ModbusSocketFramer",
|
||||
"ModbusTlsFramer",
|
||||
"ModbusRtuFramer",
|
||||
"ModbusAsciiFramer",
|
||||
"ModbusBinaryFramer",
|
||||
]
|
||||
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
from threading import RLock
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
NotImplementedException,
|
||||
)
|
||||
from pymodbus.framer.ascii_framer import ModbusAsciiFramer
|
||||
from pymodbus.framer.binary_framer import ModbusBinaryFramer
|
||||
from pymodbus.framer.rtu_framer import ModbusRtuFramer
|
||||
from pymodbus.framer.socket_framer import ModbusSocketFramer
|
||||
from pymodbus.framer.tls_framer import ModbusTlsFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import ModbusTransactionState, hexlify_packets
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# The Global Transaction Manager
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusTransactionManager:
|
||||
"""Implement a transaction for a manager.
|
||||
|
||||
The transaction protocol can be represented by the following pseudo code::
|
||||
|
||||
count = 0
|
||||
do
|
||||
result = send(message)
|
||||
if (timeout or result == bad)
|
||||
count++
|
||||
else break
|
||||
while (count < 3)
|
||||
|
||||
This module helps to abstract this away from the framer and protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
"""Initialize an instance of the ModbusTransactionManager.
|
||||
|
||||
:param client: The client socket wrapper
|
||||
:param retry_on_empty: Should the client retry on empty
|
||||
:param retries: The number of retries to allow
|
||||
"""
|
||||
self.tid = 0
|
||||
self.client = client
|
||||
self.backoff = kwargs.get("backoff", 0.3)
|
||||
self.retry_on_empty = kwargs.get("retry_on_empty", False)
|
||||
self.retry_on_invalid = kwargs.get("retry_on_invalid", False)
|
||||
self.retries = kwargs.get("retries", 3)
|
||||
self._transaction_lock = RLock()
|
||||
self._no_response_devices = []
|
||||
if client:
|
||||
self._set_adu_size()
|
||||
|
||||
def _set_adu_size(self):
|
||||
"""Set adu size."""
|
||||
# base ADU size of modbus frame in bytes
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
self.base_adu_size = 7 # tid(2), pid(2), length(2), uid(1)
|
||||
elif isinstance(self.client.framer, ModbusRtuFramer):
|
||||
self.base_adu_size = 3 # address(1), CRC(2)
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
self.base_adu_size = 7 # start(1)+ Address(2), LRC(2) + end(2)
|
||||
elif isinstance(self.client.framer, ModbusBinaryFramer):
|
||||
self.base_adu_size = 5 # start(1) + Address(1), CRC(2) + end(1)
|
||||
elif isinstance(self.client.framer, ModbusTlsFramer):
|
||||
self.base_adu_size = 0 # no header and footer
|
||||
else:
|
||||
self.base_adu_size = -1
|
||||
|
||||
def _calculate_response_length(self, expected_pdu_size):
|
||||
"""Calculate response length."""
|
||||
if self.base_adu_size == -1:
|
||||
return None
|
||||
return self.base_adu_size + expected_pdu_size
|
||||
|
||||
def _calculate_exception_length(self):
|
||||
"""Return the length of the Modbus Exception Response according to the type of Framer."""
|
||||
if isinstance(self.client.framer, (ModbusSocketFramer, ModbusTlsFramer)):
|
||||
return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1)
|
||||
if isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
return self.base_adu_size + 4 # Fcode(2), ExceptionCode(2)
|
||||
if isinstance(self.client.framer, (ModbusRtuFramer, ModbusBinaryFramer)):
|
||||
return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1)
|
||||
return None
|
||||
|
||||
def _validate_response(self, request, response, exp_resp_len):
|
||||
"""Validate Incoming response against request.
|
||||
|
||||
:param request: Request sent
|
||||
:param response: Response received
|
||||
:param exp_resp_len: Expected response length
|
||||
:return: New transactions state
|
||||
"""
|
||||
if not response:
|
||||
return False
|
||||
|
||||
mbap = self.client.framer.decode_data(response)
|
||||
if (
|
||||
mbap.get("slave") != request.slave_id
|
||||
or mbap.get("fcode") & 0x7F != request.function_code
|
||||
):
|
||||
return False
|
||||
|
||||
if "length" in mbap and exp_resp_len:
|
||||
return mbap.get("length") == exp_resp_len
|
||||
return True
|
||||
|
||||
def execute(self, request): # noqa: C901
|
||||
"""Start the producer to send the next request to consumer.write(Frame(request))."""
|
||||
with self._transaction_lock:
|
||||
try:
|
||||
Log.debug(
|
||||
"Current transaction state - {}",
|
||||
ModbusTransactionState.to_string(self.client.state),
|
||||
)
|
||||
retries = self.retries
|
||||
request.transaction_id = self.getNextTID()
|
||||
Log.debug("Running transaction {}", request.transaction_id)
|
||||
if _buffer := hexlify_packets(
|
||||
self.client.framer._buffer # pylint: disable=protected-access
|
||||
):
|
||||
Log.debug("Clearing current Frame: - {}", _buffer)
|
||||
self.client.framer.resetFrame()
|
||||
if broadcast := (
|
||||
self.client.params.broadcast_enable and not request.slave_id
|
||||
):
|
||||
self._transact(request, None, broadcast=True)
|
||||
response = b"Broadcast write sent - no response expected"
|
||||
else:
|
||||
expected_response_length = None
|
||||
if not isinstance(self.client.framer, ModbusSocketFramer):
|
||||
if hasattr(request, "get_response_pdu_size"):
|
||||
response_pdu_size = request.get_response_pdu_size()
|
||||
if isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
response_pdu_size *= 2
|
||||
if response_pdu_size:
|
||||
expected_response_length = (
|
||||
self._calculate_response_length(response_pdu_size)
|
||||
)
|
||||
if ( # pylint: disable=simplifiable-if-statement
|
||||
request.slave_id in self._no_response_devices
|
||||
):
|
||||
full = True
|
||||
else:
|
||||
full = False
|
||||
c_str = str(self.client)
|
||||
if "modbusudpclient" in c_str.lower().strip():
|
||||
full = True
|
||||
if not expected_response_length:
|
||||
expected_response_length = 1024
|
||||
response, last_exception = self._transact(
|
||||
request,
|
||||
expected_response_length,
|
||||
full=full,
|
||||
broadcast=broadcast,
|
||||
)
|
||||
while retries > 0:
|
||||
valid_response = self._validate_response(
|
||||
request, response, expected_response_length
|
||||
)
|
||||
if valid_response:
|
||||
if (
|
||||
request.slave_id in self._no_response_devices
|
||||
and response
|
||||
):
|
||||
self._no_response_devices.remove(request.slave_id)
|
||||
Log.debug("Got response!!!")
|
||||
break
|
||||
if not response:
|
||||
if request.slave_id not in self._no_response_devices:
|
||||
self._no_response_devices.append(request.slave_id)
|
||||
if self.retry_on_empty:
|
||||
response, last_exception = self._retry_transaction(
|
||||
retries,
|
||||
"empty",
|
||||
request,
|
||||
expected_response_length,
|
||||
full=full,
|
||||
)
|
||||
retries -= 1
|
||||
else:
|
||||
# No response received and retries not enabled
|
||||
break
|
||||
elif self.retry_on_invalid:
|
||||
response, last_exception = self._retry_transaction(
|
||||
retries,
|
||||
"invalid",
|
||||
request,
|
||||
expected_response_length,
|
||||
full=full,
|
||||
)
|
||||
retries -= 1
|
||||
else:
|
||||
break
|
||||
# full = False
|
||||
addTransaction = partial( # pylint: disable=invalid-name
|
||||
self.addTransaction,
|
||||
tid=request.transaction_id,
|
||||
)
|
||||
self.client.framer.processIncomingPacket(
|
||||
response,
|
||||
addTransaction,
|
||||
request.slave_id,
|
||||
tid=request.transaction_id,
|
||||
)
|
||||
if not (response := self.getTransaction(request.transaction_id)):
|
||||
if len(self.transactions):
|
||||
response = self.getTransaction(tid=0)
|
||||
else:
|
||||
last_exception = last_exception or (
|
||||
"No Response received from the remote slave"
|
||||
"/Unable to decode response"
|
||||
)
|
||||
response = ModbusIOException(
|
||||
last_exception, request.function_code
|
||||
)
|
||||
self.client.close()
|
||||
if hasattr(self.client, "state"):
|
||||
Log.debug(
|
||||
"Changing transaction state from "
|
||||
'"PROCESSING REPLY" to '
|
||||
'"TRANSACTION_COMPLETE"'
|
||||
)
|
||||
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
|
||||
|
||||
return response
|
||||
except ModbusIOException as exc:
|
||||
# Handle decode errors in processIncomingPacket method
|
||||
Log.error("Modbus IO exception {}", exc)
|
||||
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
|
||||
self.client.close()
|
||||
return exc
|
||||
|
||||
def _retry_transaction(self, 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"')
|
||||
self.client.state = ModbusTransactionState.RETRYING
|
||||
if self.backoff:
|
||||
delay = 2 ** (self.retries - retries) * self.backoff
|
||||
time.sleep(delay)
|
||||
Log.debug("Sleeping {}", delay)
|
||||
self.client.connect()
|
||||
if hasattr(self.client, "_in_waiting"):
|
||||
if (
|
||||
in_waiting := self.client._in_waiting() # pylint: disable=protected-access
|
||||
):
|
||||
if response_length == in_waiting:
|
||||
result = self._recv(response_length, full)
|
||||
return result, None
|
||||
return self._transact(packet, response_length, full=full)
|
||||
|
||||
def _transact(self, packet, 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
|
||||
"""
|
||||
last_exception = None
|
||||
try:
|
||||
self.client.connect()
|
||||
packet = self.client.framer.buildPacket(packet)
|
||||
Log.debug("SEND: {}", packet, ":hex")
|
||||
size = self._send(packet)
|
||||
if (
|
||||
isinstance(size, bytes)
|
||||
and self.client.state == ModbusTransactionState.RETRYING
|
||||
):
|
||||
Log.debug(
|
||||
"Changing transaction state from "
|
||||
'"RETRYING" to "PROCESSING REPLY"'
|
||||
)
|
||||
self.client.state = ModbusTransactionState.PROCESSING_REPLY
|
||||
return size, None
|
||||
if self.client.comm_params.handle_local_echo is True:
|
||||
if self._recv(size, full) != packet:
|
||||
return b"", "Wrong local echo"
|
||||
if broadcast:
|
||||
if size:
|
||||
Log.debug(
|
||||
'Changing transaction state from "SENDING" '
|
||||
'to "TRANSACTION_COMPLETE"'
|
||||
)
|
||||
self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE
|
||||
return b"", None
|
||||
if size:
|
||||
Log.debug(
|
||||
'Changing transaction state from "SENDING" '
|
||||
'to "WAITING FOR REPLY"'
|
||||
)
|
||||
self.client.state = ModbusTransactionState.WAITING_FOR_REPLY
|
||||
result = self._recv(response_length, full)
|
||||
# result2 = self._recv(response_length, full)
|
||||
Log.debug("RECV: {}", result, ":hex")
|
||||
except (OSError, ModbusIOException, InvalidMessageReceivedException) as msg:
|
||||
self.client.close()
|
||||
Log.debug("Transaction failed. ({}) ", msg)
|
||||
last_exception = msg
|
||||
result = b""
|
||||
return result, last_exception
|
||||
|
||||
def _send(self, packet, _retrying=False):
|
||||
"""Send."""
|
||||
return self.client.framer.sendPacket(packet)
|
||||
|
||||
def _recv(self, expected_response_length, full): # noqa: C901
|
||||
"""Receive."""
|
||||
total = None
|
||||
if not full:
|
||||
exception_length = self._calculate_exception_length()
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
min_size = 8
|
||||
elif isinstance(self.client.framer, ModbusRtuFramer):
|
||||
min_size = 4
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
min_size = 5
|
||||
elif isinstance(self.client.framer, ModbusBinaryFramer):
|
||||
min_size = 3
|
||||
else:
|
||||
min_size = expected_response_length
|
||||
|
||||
read_min = self.client.framer.recvPacket(min_size)
|
||||
if len(read_min) != min_size:
|
||||
msg_start = "Incomplete message" if read_min else "No response"
|
||||
raise InvalidMessageReceivedException(
|
||||
f"{msg_start} received, expected at least {min_size} bytes "
|
||||
f"({len(read_min)} received)"
|
||||
)
|
||||
if read_min:
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
func_code = int(read_min[-1])
|
||||
elif isinstance(self.client.framer, ModbusRtuFramer):
|
||||
func_code = int(read_min[1])
|
||||
elif isinstance(self.client.framer, ModbusAsciiFramer):
|
||||
func_code = int(read_min[3:5], 16)
|
||||
elif isinstance(self.client.framer, ModbusBinaryFramer):
|
||||
func_code = int(read_min[-1])
|
||||
else:
|
||||
func_code = -1
|
||||
|
||||
if func_code < 0x80: # Not an error
|
||||
if isinstance(self.client.framer, ModbusSocketFramer):
|
||||
# Omit UID, which is included in header size
|
||||
h_size = (
|
||||
self.client.framer._hsize # pylint: disable=protected-access
|
||||
)
|
||||
length = struct.unpack(">H", read_min[4:6])[0] - 1
|
||||
expected_response_length = h_size + length
|
||||
elif expected_response_length is None and isinstance(
|
||||
self.client.framer, ModbusRtuFramer
|
||||
):
|
||||
with suppress(
|
||||
IndexError # response length indeterminate with available bytes
|
||||
):
|
||||
expected_response_length = (
|
||||
self.client.framer.get_expected_response_length(
|
||||
read_min
|
||||
)
|
||||
)
|
||||
if expected_response_length is not None:
|
||||
expected_response_length -= min_size
|
||||
total = expected_response_length + min_size
|
||||
else:
|
||||
expected_response_length = exception_length - min_size
|
||||
total = expected_response_length + min_size
|
||||
else:
|
||||
total = expected_response_length
|
||||
else:
|
||||
read_min = b""
|
||||
total = expected_response_length
|
||||
result = self.client.framer.recvPacket(expected_response_length)
|
||||
result = read_min + result
|
||||
actual = len(result)
|
||||
if total is not None and actual != total:
|
||||
msg_start = "Incomplete message" if actual else "No response"
|
||||
Log.debug(
|
||||
"{} received, Expected {} bytes Received {} bytes !!!!",
|
||||
msg_start,
|
||||
total,
|
||||
actual,
|
||||
)
|
||||
elif not actual:
|
||||
# If actual == 0 and total is not None then the above
|
||||
# should be triggered, so total must be None here
|
||||
Log.debug("No response received to unbounded read !!!!")
|
||||
if self.client.state != ModbusTransactionState.PROCESSING_REPLY:
|
||||
Log.debug(
|
||||
"Changing transaction state from "
|
||||
'"WAITING FOR REPLY" to "PROCESSING REPLY"'
|
||||
)
|
||||
self.client.state = ModbusTransactionState.PROCESSING_REPLY
|
||||
return result
|
||||
|
||||
def addTransaction(self, request, tid=None):
|
||||
"""Add a transaction to the handler.
|
||||
|
||||
This holds the request in case it needs to be resent.
|
||||
After being sent, the request is removed.
|
||||
|
||||
:param request: The request to hold on to
|
||||
:param tid: The overloaded transaction id to use
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException("addTransaction")
|
||||
|
||||
def getTransaction(self, tid):
|
||||
"""Return a transaction matching the referenced tid.
|
||||
|
||||
If the transaction does not exist, None is returned
|
||||
|
||||
:param tid: The transaction to retrieve
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException("getTransaction")
|
||||
|
||||
def delTransaction(self, tid):
|
||||
"""Remove a transaction matching the referenced tid.
|
||||
|
||||
:param tid: The transaction to remove
|
||||
:raises NotImplementedException:
|
||||
"""
|
||||
raise NotImplementedException("delTransaction")
|
||||
|
||||
def getNextTID(self):
|
||||
"""Retrieve the next unique transaction identifier.
|
||||
|
||||
This handles incrementing the identifier after
|
||||
retrieval
|
||||
|
||||
:returns: The next unique transaction identifier
|
||||
"""
|
||||
self.tid = (self.tid + 1) & 0xFFFF
|
||||
return self.tid
|
||||
|
||||
def reset(self):
|
||||
"""Reset the transaction identifier."""
|
||||
self.tid = 0
|
||||
self.transactions = type( # pylint: disable=attribute-defined-outside-init
|
||||
self.transactions
|
||||
)()
|
||||
|
||||
|
||||
class DictTransactionManager(ModbusTransactionManager):
|
||||
"""Implements a transaction for a manager.
|
||||
|
||||
Where the results are keyed based on the supplied transaction id.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
"""Initialize an instance of the ModbusTransactionManager.
|
||||
|
||||
:param client: The client socket wrapper
|
||||
"""
|
||||
self.transactions = {}
|
||||
super().__init__(client, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the current managed transactions.
|
||||
|
||||
:returns: An iterator of the managed transactions
|
||||
"""
|
||||
return iter(self.transactions.keys())
|
||||
|
||||
def addTransaction(self, request, tid=None):
|
||||
"""Add a transaction to the handler.
|
||||
|
||||
This holds the requests in case it needs to be resent.
|
||||
After being sent, the request is removed.
|
||||
|
||||
:param request: The request to hold on to
|
||||
:param tid: The overloaded transaction id to use
|
||||
"""
|
||||
tid = tid if tid is not None else request.transaction_id
|
||||
Log.debug("Adding transaction {}", tid)
|
||||
self.transactions[tid] = request
|
||||
|
||||
def getTransaction(self, tid):
|
||||
"""Return a transaction matching the referenced tid.
|
||||
|
||||
If the transaction does not exist, None is returned
|
||||
|
||||
:param tid: The transaction to retrieve
|
||||
|
||||
"""
|
||||
Log.debug("Getting transaction {}", tid)
|
||||
if not tid:
|
||||
if self.transactions:
|
||||
return self.transactions.popitem()[1]
|
||||
return None
|
||||
return self.transactions.pop(tid, None)
|
||||
|
||||
def delTransaction(self, tid):
|
||||
"""Remove a transaction matching the referenced tid.
|
||||
|
||||
:param tid: The transaction to remove
|
||||
"""
|
||||
Log.debug("deleting transaction {}", tid)
|
||||
self.transactions.pop(tid, None)
|
||||
|
||||
|
||||
class FifoTransactionManager(ModbusTransactionManager):
|
||||
"""Implements a transaction.
|
||||
|
||||
For a manager where the results are returned in a FIFO manner.
|
||||
"""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
"""Initialize an instance of the ModbusTransactionManager.
|
||||
|
||||
:param client: The client socket wrapper
|
||||
"""
|
||||
super().__init__(client, **kwargs)
|
||||
self.transactions = []
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the current managed transactions.
|
||||
|
||||
:returns: An iterator of the managed transactions
|
||||
"""
|
||||
return iter(self.transactions)
|
||||
|
||||
def addTransaction(self, request, tid=None):
|
||||
"""Add a transaction to the handler.
|
||||
|
||||
This holds the requests in case it needs to be resent.
|
||||
After being sent, the request is removed.
|
||||
|
||||
:param request: The request to hold on to
|
||||
:param tid: The overloaded transaction id to use
|
||||
"""
|
||||
tid = tid if tid is not None else request.transaction_id
|
||||
Log.debug("Adding transaction {}", tid)
|
||||
self.transactions.append(request)
|
||||
|
||||
def getTransaction(self, tid):
|
||||
"""Return a transaction matching the referenced tid.
|
||||
|
||||
If the transaction does not exist, None is returned
|
||||
|
||||
:param tid: The transaction to retrieve
|
||||
"""
|
||||
return self.transactions.pop(0) if self.transactions else None
|
||||
|
||||
def delTransaction(self, tid):
|
||||
"""Remove a transaction matching the referenced tid.
|
||||
|
||||
:param tid: The transaction to remove
|
||||
"""
|
||||
Log.debug("Deleting transaction {}", tid)
|
||||
if self.transactions:
|
||||
self.transactions.pop(0)
|
||||
Reference in New Issue
Block a user