initial
This commit is contained in:
17
env/lib/python3.11/site-packages/pymodbus/framer/__init__.py
vendored
Normal file
17
env/lib/python3.11/site-packages/pymodbus/framer/__init__.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Framer"""
|
||||
|
||||
__all__ = [
|
||||
"ModbusFramer",
|
||||
"ModbusAsciiFramer",
|
||||
"ModbusBinaryFramer",
|
||||
"ModbusRtuFramer",
|
||||
"ModbusSocketFramer",
|
||||
"ModbusTlsFramer",
|
||||
]
|
||||
|
||||
from pymodbus.framer.ascii_framer import ModbusAsciiFramer
|
||||
from pymodbus.framer.base import ModbusFramer
|
||||
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
|
||||
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/ascii_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/ascii_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/base.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/base.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/binary_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/binary_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/rtu_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/rtu_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/socket_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/socket_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/tls_framer.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/pymodbus/framer/__pycache__/tls_framer.cpython-311.pyc
vendored
Normal file
Binary file not shown.
152
env/lib/python3.11/site-packages/pymodbus/framer/ascii_framer.py
vendored
Normal file
152
env/lib/python3.11/site-packages/pymodbus/framer/ascii_framer.py
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Ascii_framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import checkLRC, computeLRC
|
||||
|
||||
|
||||
ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus ASCII Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusAsciiFramer(ModbusFramer):
|
||||
r"""Modbus ASCII Frame Controller.
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ LRC ][ End ]
|
||||
1c 2c 2c Nc 2c 2c
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is "\\r\\n" (Carriage return line feed), however the line feed
|
||||
character can be changed via a special command
|
||||
* start is ":"
|
||||
|
||||
This framer is used for serial transmission. Unlike the RTU protocol,
|
||||
the data in this framer is transferred in plain text ascii.
|
||||
"""
|
||||
|
||||
method = "ascii"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x02
|
||||
self._start = b":"
|
||||
self._end = b"\r\n"
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > 1:
|
||||
uid = int(data[1:3], 16)
|
||||
fcode = int(data[3:5], 16)
|
||||
return {"slave": uid, "fcode": fcode}
|
||||
return {}
|
||||
|
||||
def checkFrame(self):
|
||||
"""Check and decode the next frame.
|
||||
|
||||
:returns: True if we successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
start = 0
|
||||
|
||||
if (end := self._buffer.find(self._end)) != -1:
|
||||
self._header["len"] = end
|
||||
self._header["uid"] = int(self._buffer[1:3], 16)
|
||||
self._header["lrc"] = int(self._buffer[end - 2 : end], 16)
|
||||
data = a2b_hex(self._buffer[start + 1 : end - 2])
|
||||
return checkLRC(data, self._header["lrc"])
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header["len"] + 2 :]
|
||||
self._header = {"lrc": "0000", "len": 0, "uid": 0x00}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def getFrame(self):
|
||||
"""Get the next frame from the buffer.
|
||||
|
||||
:returns: The frame data or ""
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header["len"] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return a2b_hex(buffer)
|
||||
return b""
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
while self.isFrameReady():
|
||||
if not self.checkFrame():
|
||||
break
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.error("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
continue
|
||||
|
||||
frame = self.getFrame()
|
||||
if (result := self.decoder.decode(frame)) is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer this
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
Built off of a modbus request/response
|
||||
|
||||
:param message: The request/response to send
|
||||
:return: The encoded packet
|
||||
"""
|
||||
encoded = message.encode()
|
||||
buffer = struct.pack(
|
||||
ASCII_FRAME_HEADER, message.slave_id, message.function_code
|
||||
)
|
||||
checksum = computeLRC(encoded + buffer)
|
||||
|
||||
packet = bytearray()
|
||||
packet.extend(self._start)
|
||||
packet.extend(f"{message.slave_id:02x}{message.function_code:02x}".encode())
|
||||
packet.extend(b2a_hex(encoded))
|
||||
packet.extend(f"{checksum:02x}".encode())
|
||||
packet.extend(self._end)
|
||||
return bytes(packet).upper()
|
||||
|
||||
|
||||
# __END__
|
||||
142
env/lib/python3.11/site-packages/pymodbus/framer/base.py
vendored
Normal file
142
env/lib/python3.11/site-packages/pymodbus/framer/base.py
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Framer start."""
|
||||
# pylint: disable=missing-type-doc
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from pymodbus.factory import ClientDecoder, ServerDecoder
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# Unit ID, Function Code
|
||||
BYTE_ORDER = ">"
|
||||
FRAME_HEADER = "BB"
|
||||
|
||||
# Transaction Id, Protocol ID, Length, Unit ID, Function Code
|
||||
SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER
|
||||
|
||||
# Function Code
|
||||
TLS_FRAME_HEADER = BYTE_ORDER + "B"
|
||||
|
||||
|
||||
class ModbusFramer:
|
||||
"""Base Framer class."""
|
||||
|
||||
name = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decoder: Union[ClientDecoder, ServerDecoder],
|
||||
client=None,
|
||||
) -> None:
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
self.decoder = decoder
|
||||
self.client = client
|
||||
self._header: Dict[str, Any] = {
|
||||
"lrc": "0000",
|
||||
"len": 0,
|
||||
"uid": 0x00,
|
||||
"tid": 0,
|
||||
"pid": 0,
|
||||
"crc": b"\x00\x00",
|
||||
}
|
||||
self._buffer = b""
|
||||
|
||||
def _validate_slave_id(self, slaves: list, single: bool) -> bool:
|
||||
"""Validate if the received data is valid for the client.
|
||||
|
||||
:param slaves: list of slave id for which the transaction is valid
|
||||
:param single: Set to true to treat this as a single context
|
||||
:return:
|
||||
"""
|
||||
if single:
|
||||
return True
|
||||
if 0 in slaves or 0xFF in slaves:
|
||||
# Handle Modbus TCP slave identifier (0x00 0r 0xFF)
|
||||
# in asynchronous requests
|
||||
return True
|
||||
return self._header["uid"] in slaves
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""Send packets on the bus.
|
||||
|
||||
With 3.5char delay between frames
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
return self.client.send(message)
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""Receive packet from the bus.
|
||||
|
||||
With specified len
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
return self.client.recv(size)
|
||||
|
||||
def resetFrame(self):
|
||||
"""Reset the entire message frame.
|
||||
|
||||
This allows us to skip ovver errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
Log.debug(
|
||||
"Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex"
|
||||
)
|
||||
self._buffer = b""
|
||||
self._header = {
|
||||
"lrc": "0000",
|
||||
"crc": b"\x00\x00",
|
||||
"len": 0,
|
||||
"uid": 0x00,
|
||||
"pid": 0,
|
||||
"tid": 0,
|
||||
}
|
||||
|
||||
def populateResult(self, result):
|
||||
"""Populate the modbus result header.
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.slave_id = self._header.get("uid", 0)
|
||||
result.transaction_id = self._header.get("tid", 0)
|
||||
result.protocol_id = self._header.get("pid", 0)
|
||||
|
||||
def processIncomingPacket(self, data, callback, slave, **kwargs):
|
||||
"""Process new packet pattern.
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
|
||||
:param data: The new packet data
|
||||
:param callback: The function to send results to
|
||||
:param slave: Process if slave id matches, ignore otherwise (could be a
|
||||
list of slave ids (server) or single slave id(client/server))
|
||||
:param kwargs:
|
||||
:raises ModbusIOException:
|
||||
"""
|
||||
Log.debug("Processing: {}", data, ":hex")
|
||||
self._buffer += data
|
||||
if not isinstance(slave, (list, tuple)):
|
||||
slave = [slave]
|
||||
single = kwargs.pop("single", False)
|
||||
self.frameProcessIncomingPacket(single, callback, slave, **kwargs)
|
||||
|
||||
def frameProcessIncomingPacket(
|
||||
self, _single, _callback, _slave, _tid=None, **kwargs
|
||||
):
|
||||
"""Process new packet pattern."""
|
||||
171
env/lib/python3.11/site-packages/pymodbus/framer/binary_framer.py
vendored
Normal file
171
env/lib/python3.11/site-packages/pymodbus/framer/binary_framer.py
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Binary framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import ModbusIOException
|
||||
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import checkCRC, computeCRC
|
||||
|
||||
|
||||
BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus Binary Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusBinaryFramer(ModbusFramer):
|
||||
"""Modbus Binary Frame Controller.
|
||||
|
||||
[ Start ][Address ][ Function ][ Data ][ CRC ][ End ]
|
||||
1b 1b 1b Nb 2b 1b
|
||||
|
||||
* data can be 0 - 2x252 chars
|
||||
* end is "}"
|
||||
* start is "{"
|
||||
|
||||
The idea here is that we implement the RTU protocol, however,
|
||||
instead of using timing for message delimiting, we use start
|
||||
and end of message characters (in this case { and }). Basically,
|
||||
this is a binary framer.
|
||||
|
||||
The only case we have to watch out for is when a message contains
|
||||
the { or } characters. If we encounter these characters, we
|
||||
simply duplicate them. Hopefully we will not encounter those
|
||||
characters that often and will save a little bit of bandwitch
|
||||
without a real-time system.
|
||||
|
||||
Protocol defined by jamod.sourceforge.net.
|
||||
"""
|
||||
|
||||
method = "binary"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
# self._header.update({"crc": 0x0000})
|
||||
self._hsize = 0x01
|
||||
self._start = b"\x7b" # {
|
||||
self._end = b"\x7d" # }
|
||||
self._repeat = [b"}"[0], b"{"[0]] # python3 hack
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
uid = struct.unpack(">B", data[1:2])[0]
|
||||
fcode = struct.unpack(">B", data[2:3])[0]
|
||||
return {"slave": uid, "fcode": fcode}
|
||||
return {}
|
||||
|
||||
def checkFrame(self) -> bool:
|
||||
"""Check and decode the next frame.
|
||||
|
||||
:returns: True if we are successful, False otherwise
|
||||
"""
|
||||
start = self._buffer.find(self._start)
|
||||
if start == -1:
|
||||
return False
|
||||
if start > 0: # go ahead and skip old bad data
|
||||
self._buffer = self._buffer[start:]
|
||||
|
||||
if (end := self._buffer.find(self._end)) != -1:
|
||||
self._header["len"] = end
|
||||
self._header["uid"] = struct.unpack(">B", self._buffer[1:2])[0]
|
||||
self._header["crc"] = struct.unpack(">H", self._buffer[end - 2 : end])[0]
|
||||
data = self._buffer[start + 1 : end - 2]
|
||||
return checkCRC(data, self._header["crc"])
|
||||
return False
|
||||
|
||||
def advanceFrame(self) -> None:
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header["len"] + 2 :]
|
||||
self._header = {"crc": 0x0000, "len": 0, "uid": 0x00}
|
||||
|
||||
def isFrameReady(self) -> bool:
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > 1
|
||||
|
||||
def getFrame(self):
|
||||
"""Get the next frame from the buffer.
|
||||
|
||||
:returns: The frame data or ""
|
||||
"""
|
||||
start = self._hsize + 1
|
||||
end = self._header["len"] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
return buffer
|
||||
return b""
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
while self.isFrameReady():
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
break
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
break
|
||||
if (result := self.decoder.decode(self.getFrame())) is None:
|
||||
raise ModbusIOException("Unable to decode response")
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The request/response to send
|
||||
:returns: The encoded packet
|
||||
"""
|
||||
data = self._preflight(message.encode())
|
||||
packet = (
|
||||
struct.pack(BINARY_FRAME_HEADER, message.slave_id, message.function_code)
|
||||
+ data
|
||||
)
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
packet = self._start + packet + self._end
|
||||
return packet
|
||||
|
||||
def _preflight(self, data):
|
||||
"""Do preflight buffer test.
|
||||
|
||||
This basically scans the buffer for start and end
|
||||
tags and if found, escapes them.
|
||||
|
||||
:param data: The message to escape
|
||||
:returns: the escaped packet
|
||||
"""
|
||||
array = bytearray()
|
||||
for item in data:
|
||||
if item in self._repeat:
|
||||
array.append(item)
|
||||
array.append(item)
|
||||
return bytes(array)
|
||||
|
||||
|
||||
# __END__
|
||||
323
env/lib/python3.11/site-packages/pymodbus/framer/rtu_framer.py
vendored
Normal file
323
env/lib/python3.11/site-packages/pymodbus/framer/rtu_framer.py
vendored
Normal file
@@ -0,0 +1,323 @@
|
||||
"""RTU framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
import time
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
)
|
||||
from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
from pymodbus.utilities import ModbusTransactionState, checkCRC, computeCRC
|
||||
|
||||
|
||||
RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus RTU Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
class ModbusRtuFramer(ModbusFramer):
|
||||
"""Modbus RTU Frame controller.
|
||||
|
||||
[ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ]
|
||||
3.5 chars 1b 1b Nb 2b 3.5 chars
|
||||
|
||||
Wait refers to the amount of time required to transmit at least x many
|
||||
characters. In this case it is 3.5 characters. Also, if we receive a
|
||||
wait of 1.5 characters at any point, we must trigger an error message.
|
||||
Also, it appears as though this message is little endian. The logic is
|
||||
simplified as the following::
|
||||
|
||||
block-on-read:
|
||||
read until 3.5 delay
|
||||
check for errors
|
||||
decode
|
||||
|
||||
The following table is a listing of the baud wait times for the specified
|
||||
baud rates::
|
||||
|
||||
------------------------------------------------------------------
|
||||
Baud 1.5c (18 bits) 3.5c (38 bits)
|
||||
------------------------------------------------------------------
|
||||
1200 13333.3 us 31666.7 us
|
||||
4800 3333.3 us 7916.7 us
|
||||
9600 1666.7 us 3958.3 us
|
||||
19200 833.3 us 1979.2 us
|
||||
38400 416.7 us 989.6 us
|
||||
------------------------------------------------------------------
|
||||
1 Byte = start + 8 bits + parity + stop = 11 bits
|
||||
(1/Baud)(bits) = delay seconds
|
||||
"""
|
||||
|
||||
method = "rtu"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x01
|
||||
self._end = b"\x0d\x0a"
|
||||
self._min_frame_size = 4
|
||||
self.function_codes = decoder.lookup.keys() if decoder else {}
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
uid = int(data[0])
|
||||
fcode = int(data[1])
|
||||
return {"slave": uid, "fcode": fcode}
|
||||
return {}
|
||||
|
||||
def checkFrame(self):
|
||||
"""Check if the next frame is available.
|
||||
|
||||
Return True if we were successful.
|
||||
|
||||
1. Populate header
|
||||
2. Discard frame if UID does not match
|
||||
"""
|
||||
try:
|
||||
self.populateHeader()
|
||||
frame_size = self._header["len"]
|
||||
data = self._buffer[: frame_size - 2]
|
||||
crc = self._header["crc"]
|
||||
crc_val = (int(crc[0]) << 8) + int(crc[1])
|
||||
return checkCRC(data, crc_val)
|
||||
except (IndexError, KeyError, struct.error):
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = self._buffer[self._header["len"] :]
|
||||
Log.debug("Frame advanced, resetting header!!")
|
||||
self._header = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"}
|
||||
|
||||
def resetFrame(self):
|
||||
"""Reset the entire message frame.
|
||||
|
||||
This allows us to skip over errors that may be in the stream.
|
||||
It is hard to know if we are simply out of sync or if there is
|
||||
an error in the stream as we have no way to check the start or
|
||||
end of the message (python just doesn't have the resolution to
|
||||
check for millisecond delays).
|
||||
"""
|
||||
x = self._buffer
|
||||
super().resetFrame()
|
||||
self._buffer = x
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
size = self._header.get("len", 0)
|
||||
if not size and len(self._buffer) > self._hsize:
|
||||
try:
|
||||
# Frame is ready only if populateHeader() successfully
|
||||
# populates crc field which finishes RTU frame otherwise,
|
||||
# if buffer is not yet long enough, populateHeader() raises IndexError
|
||||
size = self.populateHeader()
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
return len(self._buffer) >= size if size > 0 else False
|
||||
|
||||
def populateHeader(self, data=None):
|
||||
"""Try to set the headers `uid`, `len` and `crc`.
|
||||
|
||||
This method examines `self._buffer` and writes meta
|
||||
information into `self._header`.
|
||||
|
||||
Beware that this method will raise an IndexError if
|
||||
`self._buffer` is not yet long enough.
|
||||
"""
|
||||
data = data if data is not None else self._buffer
|
||||
self._header["uid"] = int(data[0])
|
||||
self._header["tid"] = int(data[0])
|
||||
size = self.get_expected_response_length(data)
|
||||
self._header["len"] = size
|
||||
|
||||
if len(data) < size:
|
||||
# crc yet not available
|
||||
raise IndexError
|
||||
self._header["crc"] = data[size - 2 : size]
|
||||
return size
|
||||
|
||||
def getFrame(self):
|
||||
"""Get the next frame from the buffer.
|
||||
|
||||
:returns: The frame data or ""
|
||||
"""
|
||||
start = self._hsize
|
||||
end = self._header["len"] - 2
|
||||
buffer = self._buffer[start:end]
|
||||
if end > 0:
|
||||
Log.debug("Getting Frame - {}", buffer, ":hex")
|
||||
return buffer
|
||||
return b""
|
||||
|
||||
def populateResult(self, result):
|
||||
"""Populate the modbus result header.
|
||||
|
||||
The serial packets do not have any header information
|
||||
that is copied.
|
||||
|
||||
:param result: The response packet
|
||||
"""
|
||||
result.slave_id = self._header["uid"]
|
||||
result.transaction_id = self._header["tid"]
|
||||
|
||||
def getFrameStart(self, slaves, broadcast, skip_cur_frame):
|
||||
"""Scan buffer for a relevant frame start."""
|
||||
start = 1 if skip_cur_frame else 0
|
||||
if (buf_len := len(self._buffer)) < 4:
|
||||
return False
|
||||
for i in range(start, buf_len - 3): # <slave id><function code><crc 2 bytes>
|
||||
if not broadcast and self._buffer[i] not in slaves:
|
||||
continue
|
||||
if (
|
||||
self._buffer[i + 1] not in self.function_codes
|
||||
and (self._buffer[i + 1] - 0x80) not in self.function_codes
|
||||
):
|
||||
continue
|
||||
if i:
|
||||
self._buffer = self._buffer[i:] # remove preceding trash.
|
||||
return True
|
||||
if buf_len > 3:
|
||||
self._buffer = self._buffer[-3:]
|
||||
return False
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
broadcast = not slave[0]
|
||||
skip_cur_frame = False
|
||||
while self.getFrameStart(slave, broadcast, skip_cur_frame):
|
||||
if not self.isFrameReady():
|
||||
Log.debug("Frame - not ready")
|
||||
break
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
skip_cur_frame = True
|
||||
continue
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
skip_cur_frame = True
|
||||
continue
|
||||
self._process(callback)
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = (
|
||||
struct.pack(RTU_FRAME_HEADER, message.slave_id, message.function_code)
|
||||
+ data
|
||||
)
|
||||
packet += struct.pack(">H", computeCRC(packet))
|
||||
# Ensure that transaction is actually the slave id for serial comms
|
||||
message.transaction_id = message.slave_id
|
||||
return packet
|
||||
|
||||
def sendPacket(self, message):
|
||||
"""Send packets on the bus with 3.5char delay between frames.
|
||||
|
||||
:param message: Message to be sent over the bus
|
||||
:return:
|
||||
"""
|
||||
start = time.time()
|
||||
timeout = start + self.client.comm_params.timeout_connect
|
||||
while self.client.state != ModbusTransactionState.IDLE:
|
||||
if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE:
|
||||
timestamp = round(time.time(), 6)
|
||||
Log.debug(
|
||||
"Changing state to IDLE - Last Frame End - {} Current Time stamp - {}",
|
||||
self.client.last_frame_end,
|
||||
timestamp,
|
||||
)
|
||||
if self.client.last_frame_end:
|
||||
idle_time = self.client.idle_time()
|
||||
if round(timestamp - idle_time, 6) <= self.client.silent_interval:
|
||||
Log.debug(
|
||||
"Waiting for 3.5 char before next send - {} ms",
|
||||
self.client.silent_interval * 1000,
|
||||
)
|
||||
time.sleep(self.client.silent_interval)
|
||||
else:
|
||||
# Recovering from last error ??
|
||||
time.sleep(self.client.silent_interval)
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
elif self.client.state == ModbusTransactionState.RETRYING:
|
||||
# Simple lets settle down!!!
|
||||
# To check for higher baudrates
|
||||
time.sleep(self.client.comm_params.timeout_connect)
|
||||
break
|
||||
elif time.time() > timeout:
|
||||
Log.debug(
|
||||
"Spent more time than the read time out, "
|
||||
"resetting the transaction to IDLE"
|
||||
)
|
||||
self.client.state = ModbusTransactionState.IDLE
|
||||
else:
|
||||
Log.debug("Sleeping")
|
||||
time.sleep(self.client.silent_interval)
|
||||
size = self.client.send(message)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return size
|
||||
|
||||
def recvPacket(self, size):
|
||||
"""Receive packet from the bus with specified len.
|
||||
|
||||
:param size: Number of bytes to read
|
||||
:return:
|
||||
"""
|
||||
result = self.client.recv(size)
|
||||
self.client.last_frame_end = round(time.time(), 6)
|
||||
return result
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""Process incoming packets irrespective error condition."""
|
||||
data = self._buffer if error else self.getFrame()
|
||||
if (result := self.decoder.decode(data)) is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
if error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def get_expected_response_length(self, data):
|
||||
"""Get the expected response length.
|
||||
|
||||
:param data: Message data read so far
|
||||
:raises IndexError: If not enough data to read byte count
|
||||
:return: Total frame size
|
||||
"""
|
||||
func_code = int(data[1])
|
||||
pdu_class = self.decoder.lookupPduClass(func_code)
|
||||
return pdu_class.calculateRtuFrameSize(data)
|
||||
|
||||
|
||||
# __END__
|
||||
182
env/lib/python3.11/site-packages/pymodbus/framer/socket_framer.py
vendored
Normal file
182
env/lib/python3.11/site-packages/pymodbus/framer/socket_framer.py
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Socket framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
)
|
||||
from pymodbus.framer.base import SOCKET_FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TCP Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusSocketFramer(ModbusFramer):
|
||||
"""Modbus Socket Frame controller.
|
||||
|
||||
Before each modbus TCP message is an MBAP header which is used as a
|
||||
message frame. It allows us to easily separate messages as follows::
|
||||
|
||||
[ MBAP Header ] [ Function Code] [ Data ] \
|
||||
[ tid ][ pid ][ length ][ uid ]
|
||||
2b 2b 2b 1b 1b Nb
|
||||
|
||||
while len(message) > 0:
|
||||
tid, pid, length`, uid = struct.unpack(">HHHB", message)
|
||||
request = message[0:7 + length - 1`]
|
||||
message = [7 + length - 1:]
|
||||
|
||||
* length = uid + function code + data
|
||||
* The -1 is to account for the uid byte
|
||||
"""
|
||||
|
||||
method = "socket"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x07
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""Check and decode the next frame.
|
||||
|
||||
Return true if we were successful.
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
(
|
||||
self._header["tid"],
|
||||
self._header["pid"],
|
||||
self._header["len"],
|
||||
self._header["uid"],
|
||||
) = struct.unpack(">HHHB", self._buffer[0 : self._hsize])
|
||||
|
||||
# someone sent us an error? ignore it
|
||||
if self._header["len"] < 2:
|
||||
self.advanceFrame()
|
||||
# we have at least a complete message, continue
|
||||
elif len(self._buffer) - self._hsize + 1 >= self._header["len"]:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
length = self._hsize + self._header["len"] - 1
|
||||
self._buffer = self._buffer[length:]
|
||||
self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def getFrame(self):
|
||||
"""Return the next frame from the buffered data.
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
length = self._hsize + self._header["len"] - 1
|
||||
return self._buffer[self._hsize : length]
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
tid, pid, length, uid, fcode = struct.unpack(
|
||||
SOCKET_FRAME_HEADER, data[0 : self._hsize + 1]
|
||||
)
|
||||
return {
|
||||
"tid": tid,
|
||||
"pid": pid,
|
||||
"length": length,
|
||||
"slave": uid,
|
||||
"fcode": fcode,
|
||||
}
|
||||
return {}
|
||||
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs):
|
||||
"""Process new packet pattern.
|
||||
|
||||
This takes in a new request packet, adds it to the current
|
||||
packet stream, and performs framing on it. That is, checks
|
||||
for complete messages, and once found, will process all that
|
||||
exist. This handles the case when we read N + 1 or 1 // N
|
||||
messages at a time instead of 1.
|
||||
|
||||
The processed and decoded messages are pushed to the callback
|
||||
function to process and send.
|
||||
"""
|
||||
while True:
|
||||
if not self.isFrameReady():
|
||||
if len(self._buffer):
|
||||
# Possible error ???
|
||||
if self._header["len"] < 2:
|
||||
self._process(callback, tid, error=True)
|
||||
break
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
continue
|
||||
if not self._validate_slave_id(slave, single):
|
||||
header_txt = self._header["uid"]
|
||||
Log.debug("Not a valid slave id - {}, ignoring!!", header_txt)
|
||||
self.resetFrame()
|
||||
continue
|
||||
self._process(callback, tid)
|
||||
|
||||
def _process(self, callback, tid, error=False):
|
||||
"""Process incoming packets irrespective error condition."""
|
||||
data = self._buffer if error else self.getFrame()
|
||||
if (result := self.decoder.decode(data)) is None:
|
||||
self.resetFrame()
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
if error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
if tid and tid != result.transaction_id:
|
||||
self.resetFrame()
|
||||
else:
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(
|
||||
SOCKET_FRAME_HEADER,
|
||||
message.transaction_id,
|
||||
message.protocol_id,
|
||||
len(data) + 2,
|
||||
message.slave_id,
|
||||
message.function_code,
|
||||
)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
|
||||
# __END__
|
||||
127
env/lib/python3.11/site-packages/pymodbus/framer/tls_framer.py
vendored
Normal file
127
env/lib/python3.11/site-packages/pymodbus/framer/tls_framer.py
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
"""TLS framer."""
|
||||
# pylint: disable=missing-type-doc
|
||||
import struct
|
||||
|
||||
from pymodbus.exceptions import (
|
||||
InvalidMessageReceivedException,
|
||||
ModbusIOException,
|
||||
)
|
||||
from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer
|
||||
from pymodbus.logging import Log
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Modbus TLS Message
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
class ModbusTlsFramer(ModbusFramer):
|
||||
"""Modbus TLS Frame controller
|
||||
|
||||
No prefix MBAP header before decrypted PDU is used as a message frame for
|
||||
Modbus Security Application Protocol. It allows us to easily separate
|
||||
decrypted messages which is PDU as follows:
|
||||
|
||||
[ Function Code] [ Data ]
|
||||
1b Nb
|
||||
"""
|
||||
|
||||
method = "tls"
|
||||
|
||||
def __init__(self, decoder, client=None):
|
||||
"""Initialize a new instance of the framer.
|
||||
|
||||
:param decoder: The decoder factory implementation to use
|
||||
"""
|
||||
super().__init__(decoder, client)
|
||||
self._hsize = 0x0
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Private Helper Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def checkFrame(self):
|
||||
"""Check and decode the next frame.
|
||||
|
||||
Return true if we were successful.
|
||||
"""
|
||||
if self.isFrameReady():
|
||||
# we have at least a complete message, continue
|
||||
if len(self._buffer) - self._hsize >= 1:
|
||||
return True
|
||||
# we don't have enough of a message yet, wait
|
||||
return False
|
||||
|
||||
def advanceFrame(self):
|
||||
"""Skip over the current framed message.
|
||||
|
||||
This allows us to skip over the current message after we have processed
|
||||
it or determined that it contains an error. It also has to reset the
|
||||
current frame header handle
|
||||
"""
|
||||
self._buffer = b""
|
||||
self._header = {}
|
||||
|
||||
def isFrameReady(self):
|
||||
"""Check if we should continue decode logic.
|
||||
|
||||
This is meant to be used in a while loop in the decoding phase to let
|
||||
the decoder factory know that there is still data in the buffer.
|
||||
|
||||
:returns: True if ready, False otherwise
|
||||
"""
|
||||
return len(self._buffer) > self._hsize
|
||||
|
||||
def getFrame(self):
|
||||
"""Return the next frame from the buffered data.
|
||||
|
||||
:returns: The next full frame buffer
|
||||
"""
|
||||
return self._buffer[self._hsize :]
|
||||
|
||||
# ----------------------------------------------------------------------- #
|
||||
# Public Member Functions
|
||||
# ----------------------------------------------------------------------- #
|
||||
def decode_data(self, data):
|
||||
"""Decode data."""
|
||||
if len(data) > self._hsize:
|
||||
(fcode,) = struct.unpack(TLS_FRAME_HEADER, data[0 : self._hsize + 1])
|
||||
return {"fcode": fcode}
|
||||
return {}
|
||||
|
||||
def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs):
|
||||
"""Process new packet pattern."""
|
||||
# no slave id for Modbus Security Application Protocol
|
||||
if not self.isFrameReady():
|
||||
return
|
||||
if not self.checkFrame():
|
||||
Log.debug("Frame check failed, ignoring!!")
|
||||
self.resetFrame()
|
||||
return
|
||||
if not self._validate_slave_id(slave, single):
|
||||
Log.debug("Not in valid slave id - {}, ignoring!!", slave)
|
||||
self.resetFrame()
|
||||
self._process(callback)
|
||||
|
||||
def _process(self, callback, error=False):
|
||||
"""Process incoming packets irrespective error condition."""
|
||||
data = self._buffer if error else self.getFrame()
|
||||
if (result := self.decoder.decode(data)) is None:
|
||||
raise ModbusIOException("Unable to decode request")
|
||||
if error and result.function_code < 0x80:
|
||||
raise InvalidMessageReceivedException(result)
|
||||
self.populateResult(result)
|
||||
self.advanceFrame()
|
||||
callback(result) # defer or push to a thread?
|
||||
|
||||
def buildPacket(self, message):
|
||||
"""Create a ready to send modbus packet.
|
||||
|
||||
:param message: The populated request/response to send
|
||||
"""
|
||||
data = message.encode()
|
||||
packet = struct.pack(TLS_FRAME_HEADER, message.function_code)
|
||||
packet += data
|
||||
return packet
|
||||
|
||||
|
||||
# __END__
|
||||
Reference in New Issue
Block a user