Initial commit for tac2100_compteur_mbus2mqtt
This commit is contained in:
+17
@@ -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
|
||||
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
Vendored
Executable
BIN
Binary file not shown.
+152
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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