venv added, updated
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
84
myenv/lib/python3.12/site-packages/pymodbus/framer/ascii.py
Normal file
84
myenv/lib/python3.12/site-packages/pymodbus/framer/ascii.py
Normal 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
|
||||
43
myenv/lib/python3.12/site-packages/pymodbus/framer/base.py
Normal file
43
myenv/lib/python3.12/site-packages/pymodbus/framer/base.py
Normal 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)
|
||||
"""
|
||||
113
myenv/lib/python3.12/site-packages/pymodbus/framer/framer.py
Normal file
113
myenv/lib/python3.12/site-packages/pymodbus/framer/framer.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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?
|
||||
@@ -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?
|
||||
32
myenv/lib/python3.12/site-packages/pymodbus/framer/raw.py
Normal file
32
myenv/lib/python3.12/site-packages/pymodbus/framer/raw.py
Normal 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
|
||||
153
myenv/lib/python3.12/site-packages/pymodbus/framer/rtu.py
Normal file
153
myenv/lib/python3.12/site-packages/pymodbus/framer/rtu.py
Normal 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()
|
||||
44
myenv/lib/python3.12/site-packages/pymodbus/framer/socket.py
Normal file
44
myenv/lib/python3.12/site-packages/pymodbus/framer/socket.py
Normal 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
|
||||
20
myenv/lib/python3.12/site-packages/pymodbus/framer/tls.py
Normal file
20
myenv/lib/python3.12/site-packages/pymodbus/framer/tls.py
Normal 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
|
||||
Reference in New Issue
Block a user