venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 deletions

View File

@@ -0,0 +1,27 @@
"""Framer."""
__all__ = [
"Framer",
"FRAMER_NAME_TO_CLASS",
"ModbusFramer",
"ModbusAsciiFramer",
"ModbusRtuFramer",
"ModbusSocketFramer",
"ModbusTlsFramer",
"Framer",
"FramerType",
]
from pymodbus.framer.framer import Framer, FramerType
from pymodbus.framer.old_framer_ascii import ModbusAsciiFramer
from pymodbus.framer.old_framer_base import ModbusFramer
from pymodbus.framer.old_framer_rtu import ModbusRtuFramer
from pymodbus.framer.old_framer_socket import ModbusSocketFramer
from pymodbus.framer.old_framer_tls import ModbusTlsFramer
FRAMER_NAME_TO_CLASS = {
FramerType.ASCII: ModbusAsciiFramer,
FramerType.RTU: ModbusRtuFramer,
FramerType.SOCKET: ModbusSocketFramer,
FramerType.TLS: ModbusTlsFramer,
}

View File

@@ -0,0 +1,84 @@
"""ModbusMessage layer.
is extending ModbusProtocol to handle receiving and sending of messsagees.
ModbusMessage provides a unified interface to send/receive Modbus requests/responses.
"""
from __future__ import annotations
from binascii import a2b_hex, b2a_hex
from pymodbus.framer.base import FramerBase
from pymodbus.logging import Log
class FramerAscii(FramerBase):
r"""Modbus ASCII Frame Controller.
[ Start ][ Dev id ][ Function ][ Data ][ LRC ][ End ]
1c 2c 2c N*2c 1c 2c
* data can be 1 - 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.
"""
START = b':'
END = b'\r\n'
MIN_SIZE = 10
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU."""
buf_len = len(data)
used_len = 0
while True:
if buf_len - used_len < self.MIN_SIZE:
return used_len, 0, 0, self.EMPTY
buffer = data[used_len:]
if buffer[0:1] != self.START:
if (i := buffer.find(self.START)) == -1:
Log.debug("No frame start in data: {}, wait for data", data, ":hex")
return buf_len, 0, 0, self.EMPTY
used_len += i
continue
if (end := buffer.find(self.END)) == -1:
Log.debug("Incomplete frame: {} wait for more data", data, ":hex")
return used_len, 0, 0, self.EMPTY
dev_id = int(buffer[1:3], 16)
lrc = int(buffer[end - 2: end], 16)
msg = a2b_hex(buffer[1 : end - 2])
used_len += end + 2
if not self.check_LRC(msg, lrc):
Log.debug("LRC wrong in frame: {} skipping", data, ":hex")
continue
return used_len, dev_id, dev_id, msg[1:]
def encode(self, data: bytes, device_id: int, _tid: int) -> bytes:
"""Encode ADU."""
dev_id = device_id.to_bytes(1,'big')
checksum = self.compute_LRC(dev_id + data)
packet = (
self.START +
f"{device_id:02x}".encode() +
b2a_hex(data) +
f"{checksum:02x}".encode() +
self.END
).upper()
return packet
@classmethod
def compute_LRC(cls, data: bytes) -> int:
"""Use to compute the longitudinal redundancy check against a string."""
lrc = sum(int(a) for a in data) & 0xFF
lrc = (lrc ^ 0xFF) + 1
return lrc & 0xFF
@classmethod
def check_LRC(cls, data: bytes, check: int) -> bool:
"""Check if the passed in data matches the LRC."""
return cls.compute_LRC(data) == check

View File

@@ -0,0 +1,43 @@
"""Framer implementations.
The implementation is responsible for encoding/decoding requests/responses.
According to the selected type of modbus frame a prefix/suffix is added/removed
"""
from __future__ import annotations
from abc import abstractmethod
class FramerBase:
"""Intern base."""
EMPTY = b''
def __init__(self) -> None:
"""Initialize a ADU instance."""
def set_dev_ids(self, _dev_ids: list[int]):
"""Set/update allowed device ids."""
def set_fc_calc(self, _fc: int, _msg_size: int, _count_pos: int):
"""Set/Update function code information."""
@abstractmethod
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU.
returns:
used_len (int) or 0 to read more
transaction_id (int) or 0
device_id (int) or 0
modbus request/response (bytes)
"""
@abstractmethod
def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes:
"""Encode ADU.
returns:
modbus ADU (bytes)
"""

View File

@@ -0,0 +1,113 @@
"""Framing layer.
The framer layer is responsible for isolating/generating the request/request from
the frame (prefix - postfix)
According to the selected type of modbus frame a prefix/suffix is added/removed
This layer is also responsible for discarding invalid frames and frames for other slaves.
"""
from __future__ import annotations
from abc import abstractmethod
from enum import Enum
from pymodbus.framer.ascii import FramerAscii
from pymodbus.framer.raw import FramerRaw
from pymodbus.framer.rtu import FramerRTU
from pymodbus.framer.socket import FramerSocket
from pymodbus.framer.tls import FramerTLS
from pymodbus.transport.transport import CommParams, ModbusProtocol
class FramerType(str, Enum):
"""Type of Modbus frame."""
RAW = "raw" # only used for testing
ASCII = "ascii"
RTU = "rtu"
SOCKET = "socket"
TLS = "tls"
class Framer(ModbusProtocol):
"""Framer layer extending transport layer.
extends the ModbusProtocol to handle receiving and sending of complete modbus PDU.
When receiving:
- Secures full valid Modbus PDU is received (across multiple callbacks)
- Validates and removes Modbus prefix/suffix (CRC for serial, MBAP for others)
- Callback with pure request/response
- Skips invalid messagees
- Hunt for valid message (RTU type)
When sending:
- Add prefix/suffix to request/response (CRC for serial, MBAP for others)
- Call transport to send
The class is designed to take care of differences between the modbus message types,
and provide a neutral interface with pure requests/responses to/from the upper layers.
"""
def __init__(self,
framer_type: FramerType,
params: CommParams,
is_server: bool,
device_ids: list[int],
):
"""Initialize a framer instance.
:param framer_type: Modbus message type
:param params: parameter dataclass
:param is_server: true if object act as a server (listen/connect)
:param device_ids: list of device id to accept, 0 in list means broadcast.
"""
super().__init__(params, is_server)
self.device_ids = device_ids
self.broadcast: bool = (0 in device_ids)
self.handle = {
FramerType.RAW: FramerRaw(),
FramerType.ASCII: FramerAscii(),
FramerType.RTU: FramerRTU(),
FramerType.SOCKET: FramerSocket(),
FramerType.TLS: FramerTLS(),
}[framer_type]
def validate_device_id(self, dev_id: int) -> bool:
"""Check if device id is expected."""
return self.broadcast or (dev_id in self.device_ids)
def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
"""Handle received data."""
tot_len = 0
buf_len = len(data)
while True:
used_len, tid, device_id, msg = self.handle.decode(data[tot_len:])
tot_len += used_len
if msg:
if self.broadcast or device_id in self.device_ids:
self.callback_request_response(msg, device_id, tid)
if tot_len == buf_len:
return tot_len
else:
return tot_len
@abstractmethod
def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None:
"""Handle received modbus request/response."""
def build_send(self, data: bytes, device_id: int, tid: int, addr: tuple | None = None) -> None:
"""Send request/response.
:param data: non-empty bytes object with data to send.
:param device_id: device identifier (slave/unit)
:param tid: transaction id (0 if not used).
:param addr: optional addr, only used for UDP server.
"""
send_data = self.handle.encode(data, device_id, tid)
self.send(send_data, addr)

View File

@@ -0,0 +1,71 @@
"""Ascii_framer."""
from pymodbus.exceptions import ModbusIOException
from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
from pymodbus.logging import Log
from .ascii import FramerAscii
ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
# --------------------------------------------------------------------------- #
# Modbus ASCII olf framer
# --------------------------------------------------------------------------- #
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"
self.message_handler = FramerAscii()
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 frameProcessIncomingPacket(self, single, callback, slave, tid=None):
"""Process new packet pattern."""
while len(self._buffer):
used_len, tid, dev_id, data = self.message_handler.decode(self._buffer)
if not data:
if not used_len:
return
self._buffer = self._buffer[used_len :]
continue
self._header["uid"] = dev_id
if not self._validate_slave_id(slave, single):
Log.error("Not a valid slave id - {}, ignoring!!", dev_id)
self.resetFrame()
return
if (result := self.decoder.decode(data)) is None:
raise ModbusIOException("Unable to decode response")
self.populateResult(result)
self._buffer = self._buffer[used_len :]
self._header = {"uid": 0x00}
callback(result) # defer this

View File

@@ -0,0 +1,168 @@
"""Framer start."""
# pylint: disable=missing-type-doc
from __future__ import annotations
import time
from typing import TYPE_CHECKING, Any
from pymodbus.factory import ClientDecoder, ServerDecoder
from pymodbus.framer.base import FramerBase
from pymodbus.logging import Log
from pymodbus.pdu import ModbusRequest, ModbusResponse
if TYPE_CHECKING:
from pymodbus.client.base import ModbusBaseSyncClient
# 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: ClientDecoder | ServerDecoder,
client: ModbusBaseSyncClient,
) -> 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]
self._reset_header()
self._buffer = b""
self.message_handler: FramerBase
def _reset_header(self) -> None:
self._header = {
"lrc": "0000",
"len": 0,
"uid": 0x00,
"tid": 0,
"pid": 0,
"crc": b"\x00\x00",
}
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: bytes):
"""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: int) -> bytes:
"""Receive packet from the bus.
With specified len
:param size: Number of bytes to read
:return:
"""
packet = self.client.recv(size)
self.client.last_frame_end = round(time.time(), 6)
return packet
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: bytes, callback, slave, single=False, tid=None):
"""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 single: multiple slave ?
:param tid: transaction id
:raises ModbusIOException:
"""
Log.debug("Processing: {}", data, ":hex")
self._buffer += data
if self._buffer == b'':
return
if not isinstance(slave, (list, tuple)):
slave = [slave]
self.frameProcessIncomingPacket(single, callback, slave, tid=tid)
def frameProcessIncomingPacket(
self, _single, _callback, _slave, tid=None
) -> None:
"""Process new packet pattern."""
def buildPacket(self, message: ModbusRequest | ModbusResponse) -> bytes:
"""Create a ready to send modbus packet.
:param message: The populated request/response to send
"""
data = message.function_code.to_bytes(1,'big') + message.encode()
packet = self.message_handler.encode(data, message.slave_id, message.transaction_id)
return packet

View File

@@ -0,0 +1,227 @@
"""RTU framer."""
# pylint: disable=missing-type-doc
import struct
import time
from pymodbus.exceptions import ModbusIOException
from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer
from pymodbus.framer.rtu import FramerRTU
from pymodbus.logging import Log
from pymodbus.utilities import ModbusTransactionState
RTU_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER
# --------------------------------------------------------------------------- #
# Modbus RTU old Framer
# --------------------------------------------------------------------------- #
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.function_codes = decoder.lookup.keys() if decoder else {}
self.message_handler = FramerRTU()
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 frameProcessIncomingPacket(self, _single, callback, slave, tid=None): # noqa: C901
"""Process new packet pattern."""
def is_frame_ready(self):
"""Check if we should continue decode logic."""
size = self._header.get("len", 0)
if not size and len(self._buffer) > self._hsize:
try:
self._header["uid"] = int(self._buffer[0])
self._header["tid"] = int(self._buffer[0])
self._header["tid"] = 0 # fix for now
func_code = int(self._buffer[1])
pdu_class = self.decoder.lookupPduClass(func_code)
size = pdu_class.calculateRtuFrameSize(self._buffer)
self._header["len"] = size
if len(self._buffer) < size:
raise IndexError
self._header["crc"] = self._buffer[size - 2 : size]
except IndexError:
return False
return len(self._buffer) >= size if size > 0 else False
def get_frame_start(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
def check_frame(self):
"""Check if the next frame is available."""
try:
self._header["uid"] = int(self._buffer[0])
self._header["tid"] = int(self._buffer[0])
self._header["tid"] = 0 # fix for now
func_code = int(self._buffer[1])
pdu_class = self.decoder.lookupPduClass(func_code)
size = pdu_class.calculateRtuFrameSize(self._buffer)
self._header["len"] = size
if len(self._buffer) < size:
raise IndexError
self._header["crc"] = self._buffer[size - 2 : size]
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 FramerRTU.check_CRC(data, crc_val)
except (IndexError, KeyError, struct.error):
return False
broadcast = not slave[0]
skip_cur_frame = False
while get_frame_start(self, slave, broadcast, skip_cur_frame):
self._header = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"}
if not is_frame_ready(self):
Log.debug("Frame - not ready")
break
if not check_frame(self):
Log.debug("Frame check failed, ignoring!!")
x = self._buffer
self.resetFrame()
self._buffer: bytes = x
skip_cur_frame = True
continue
start = self._hsize
end = self._header["len"] - 2
buffer = self._buffer[start:end]
if end > 0:
Log.debug("Getting Frame - {}", buffer, ":hex")
data = buffer
else:
data = b""
if (result := self.decoder.decode(data)) is None:
raise ModbusIOException("Unable to decode request")
result.slave_id = self._header["uid"]
result.transaction_id = 0
self._buffer = self._buffer[self._header["len"] :]
Log.debug("Frame advanced, resetting header!!")
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
"""
packet = super().buildPacket(message)
# Ensure that transaction is actually the slave id for serial comms
message.transaction_id = 0
return packet
def sendPacket(self, message: bytes) -> int:
"""Send packets on the bus with 3.5char delay between frames.
:param message: Message to be sent over the bus
:return:
"""
super().resetFrame()
start = time.time()
if hasattr(self.client,"ctx"):
timeout = start + self.client.ctx.comm_params.timeout_connect
else:
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

View File

@@ -0,0 +1,96 @@
"""Socket framer."""
import struct
from pymodbus.exceptions import (
ModbusIOException,
)
from pymodbus.framer.old_framer_base import SOCKET_FRAME_HEADER, ModbusFramer
from pymodbus.framer.socket import FramerSocket
from pymodbus.logging import Log
# --------------------------------------------------------------------------- #
# Modbus TCP old framer
# --------------------------------------------------------------------------- #
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
self.message_handler = FramerSocket()
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):
"""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 self._buffer == b'':
return
used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer)
if not data:
return
self._header["uid"] = dev_id
self._header["tid"] = use_tid
self._header["pid"] = 0
if not self._validate_slave_id(slave, single):
Log.debug("Not a valid slave id - {}, ignoring!!", dev_id)
self.resetFrame()
return
if (result := self.decoder.decode(data)) is None:
self.resetFrame()
raise ModbusIOException("Unable to decode request")
self.populateResult(result)
self._buffer: bytes = self._buffer[used_len:]
self._reset_header()
if tid and tid != result.transaction_id:
self.resetFrame()
else:
callback(result) # defer or push to a thread?

View File

@@ -0,0 +1,69 @@
"""TLS framer."""
import struct
from time import sleep
from pymodbus.exceptions import (
ModbusIOException,
)
from pymodbus.framer.old_framer_base import TLS_FRAME_HEADER, ModbusFramer
from pymodbus.framer.tls import FramerTLS
# --------------------------------------------------------------------------- #
# Modbus TLS old framer
# --------------------------------------------------------------------------- #
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
self.message_handler = FramerTLS()
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 recvPacket(self, size):
"""Receive packet from the bus."""
sleep(0.5)
return super().recvPacket(size)
def frameProcessIncomingPacket(self, _single, callback, _slave, tid=None):
"""Process new packet pattern."""
# no slave id for Modbus Security Application Protocol
while True:
used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer)
if not data:
return
self._header["uid"] = dev_id
self._header["tid"] = use_tid
self._header["pid"] = 0
if (result := self.decoder.decode(data)) is None:
self.resetFrame()
raise ModbusIOException("Unable to decode request")
self.populateResult(result)
self._buffer: bytes = self._buffer[used_len:]
self._reset_header()
callback(result) # defer or push to a thread?

View File

@@ -0,0 +1,32 @@
"""Modbus Raw (passthrough) implementation."""
from __future__ import annotations
from pymodbus.framer.base import FramerBase
from pymodbus.logging import Log
class FramerRaw(FramerBase):
r"""Modbus RAW Frame Controller.
[ Device id ][Transaction id ][ Data ]
1b 2b Nb
* data can be 0 - X bytes
This framer is used for non modbus communication and testing purposes.
"""
MIN_SIZE = 3
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU."""
if len(data) < self.MIN_SIZE:
Log.debug("Short frame: {} wait for more data", data, ":hex")
return 0, 0, 0, self.EMPTY
dev_id = int(data[0])
tid = int(data[1])
return len(data), dev_id, tid, data[2:]
def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes:
"""Encode ADU."""
return dev_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu

View File

@@ -0,0 +1,153 @@
"""Modbus RTU frame implementation."""
from __future__ import annotations
from collections import namedtuple
from pymodbus.framer.base import FramerBase
from pymodbus.logging import Log
class FramerRTU(FramerBase):
"""Modbus RTU frame type.
[ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ]
3.5 chars 1b 1b Nb 2b
* Note: due to the USB converter and the OS drivers, timing cannot be quaranteed
neither when receiving nor when sending.
Decoding is a complicated process because the RTU frame does not have a fixed prefix
only suffix, therefore it is necessary to decode the content (PDU) to get length etc.
There are some protocol restrictions that help with the detection.
For client:
- a request causes 1 response !
- Multiple requests are NOT allowed (master-slave protocol)
- the server will not retransmit responses
this means decoding is always exactly 1 frame (response)
For server (Single device)
- only 1 request allowed (master-slave) protocol
- the client (master) may retransmit but in larger time intervals
this means decoding is always exactly 1 frame (request)
For server (Multidrop line --> devices in parallel)
- only 1 request allowed (master-slave) protocol
- other devices will send responses
- the client (master) may retransmit but in larger time intervals
this means decoding is always exactly 1 frame request, however some requests
will be for unknown slaves, which must be ignored together with the
response from the unknown slave.
Recovery from bad cabling and unstable USB etc is important,
the following scenarios is possible:
- garble data before frame
- garble data in frame
- garble data after frame
- data in frame garbled (wrong CRC)
decoding assumes the frame is sound, and if not enters a hunting mode.
The 3.5 byte transmission time at the slowest speed 1.200Bps is 31ms.
Device drivers will typically flush buffer after 10ms of silence.
If no data is received for 50ms the transmission / frame can be considered
complete.
"""
MIN_SIZE = 5
FC_LEN = namedtuple("FC_LEN", "req_len req_bytepos resp_len resp_bytepos")
def __init__(self) -> None:
"""Initialize a ADU instance."""
super().__init__()
self.fc_len: dict[int, FramerRTU.FC_LEN] = {}
@classmethod
def generate_crc16_table(cls) -> list[int]:
"""Generate a crc16 lookup table.
.. note:: This will only be generated once
"""
result = []
for byte in range(256):
crc = 0x0000
for _ in range(8):
if (byte ^ crc) & 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
byte >>= 1
result.append(crc)
return result
crc16_table: list[int] = [0]
def setup_fc_len(self, _fc: int,
_req_len: int, _req_byte_pos: int,
_resp_len: int, _resp_byte_pos: int
):
"""Define request/response lengths pr function code."""
return
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU."""
if (buf_len := len(data)) < self.MIN_SIZE:
Log.debug("Short frame: {} wait for more data", data, ":hex")
return 0, 0, 0, b''
i = -1
try:
while True:
i += 1
if i > buf_len - self.MIN_SIZE + 1:
break
dev_id = int(data[i])
fc_len = 5
msg_len = fc_len -2 if fc_len > 0 else int(data[i-fc_len])-fc_len+1
if msg_len + i + 2 > buf_len:
break
crc_val = (int(data[i+msg_len]) << 8) + int(data[i+msg_len+1])
if not self.check_CRC(data[i:i+msg_len], crc_val):
Log.debug("Skipping frame CRC with len {} at index {}!", msg_len, i)
raise KeyError
return i+msg_len+2, dev_id, dev_id, data[i+1:i+msg_len]
except KeyError:
i = buf_len
return i, 0, 0, b''
def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes:
"""Encode ADU."""
packet = device_id.to_bytes(1,'big') + pdu
return packet + FramerRTU.compute_CRC(packet).to_bytes(2,'big')
@classmethod
def check_CRC(cls, data: bytes, check: int) -> bool:
"""Check if the data matches the passed in CRC.
:param data: The data to create a crc16 of
:param check: The CRC to validate
:returns: True if matched, False otherwise
"""
return cls.compute_CRC(data) == check
@classmethod
def compute_CRC(cls, data: bytes) -> int:
"""Compute a crc16 on the passed in bytes.
The difference between modbus's crc16 and a normal crc16
is that modbus starts the crc value out at 0xffff.
:param data: The data to create a crc16 of
:returns: The calculated CRC
"""
crc = 0xFFFF
for data_byte in data:
idx = cls.crc16_table[(crc ^ int(data_byte)) & 0xFF]
crc = ((crc >> 8) & 0xFF) ^ idx
swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF)
return swapped
FramerRTU.crc16_table = FramerRTU.generate_crc16_table()

View File

@@ -0,0 +1,44 @@
"""Modbus Socket frame implementation."""
from __future__ import annotations
from pymodbus.framer.base import FramerBase
from pymodbus.logging import Log
class FramerSocket(FramerBase):
"""Modbus Socket frame type.
[ MBAP Header ] [ Function Code] [ Data ]
[ tid ][ pid ][ length ][ uid ]
2b 2b 2b 1b 1b Nb
* length = uid + function code + data
"""
MIN_SIZE = 8
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU."""
if (used_len := len(data)) < self.MIN_SIZE:
Log.debug("Very short frame (NO MBAP): {} wait for more data", data, ":hex")
return 0, 0, 0, self.EMPTY
msg_tid = int.from_bytes(data[0:2], 'big')
msg_len = int.from_bytes(data[4:6], 'big') + 6
msg_dev = int(data[6])
if used_len < msg_len:
Log.debug("Short frame: {} wait for more data", data, ":hex")
return 0, 0, 0, self.EMPTY
if msg_len == 8 and used_len == 9:
msg_len = 9
return msg_len, msg_tid, msg_dev, data[7:msg_len]
def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes:
"""Encode ADU."""
packet = (
tid.to_bytes(2, 'big') +
b'\x00\x00' +
(len(pdu) + 1).to_bytes(2, 'big') +
device_id.to_bytes(1, 'big') +
pdu
)
return packet

View File

@@ -0,0 +1,20 @@
"""Modbus TLS frame implementation."""
from __future__ import annotations
from pymodbus.framer.base import FramerBase
class FramerTLS(FramerBase):
"""Modbus TLS frame type.
[ Function Code] [ Data ]
1b Nb
"""
def decode(self, data: bytes) -> tuple[int, int, int, bytes]:
"""Decode ADU."""
return len(data), 0, 0, data
def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes:
"""Encode ADU."""
return pdu